Post-review polish on the v0.7 wire / surface contract. Bundles five
follow-ups that landed after the main BREAKING feat commit:
1. Complete context→profile cascade (internal API + YAML schema)
The prior commit renamed only the user-visible surface (commands /
flags / env / project link / envelope field). The internal Go API
and on-disk config schema were still half-renamed — an L-25
self-consistency violation flagged by post-merge review. Closed here:
Internal Go API:
- config.Context → config.Profile
- config.Config.CurrentContext → CurrentProfile
- config.Config.Contexts → Profiles
- LoginOptions.Context → LoginOptions.Profile
- clearContextSecrets() → clearProfileSecrets()
- saveContextRef() → saveProfileRef()
- secrets.Store: param name `context` → `profile` (interface +
FileStore + KeyringStore + MemStore)
- cmdutil.LoadSecret(store, context, key) → LoadSecret(store, profile, key)
- cmdutil.RefreshAndPersist's ctxName → profileName
- Local var `ctx := &config.Profile{...}` → `prof := &config.Profile{...}`
in auth/login.go to eliminate the visual collision with Go stdlib
context.Context that motivated the whole rename in the first place.
On-disk config.yaml schema:
- current_context: → current_profile:
- contexts: → profiles:
- Pre-1.0 break, no compat alias. Users on v0.6 dogfooded configs
must delete ~/.config/weknora/config.yaml or hand-rename the two
keys (CHANGELOG migration note added).
Tests / fixtures / golden files:
- factory_test.go YAML fixture + assertion updated.
- acceptance/e2e/e2e_test.go writeContextYAML → writeProfileYAML,
fixture YAML keys updated.
- acceptance/testdata/wire/doctor.error_network.json golden updated
("active context" → "active profile" in hint string).
User-visible prose sweep:
- cmd/mcp/serve.go --help Long: "active context (or --context)" →
"active profile (or --profile)" — most-visible miss.
- cmd/{kb/list, search/kb, session/list, api/api} Short/Long help.
- cmd/auth/login.go stdout: `(context=%s)` → `(profile=%s)`.
- cmd/auth/logout.go error: `"no current context"` → `"no current profile"`.
- cmd/doctor/doctor.go hint string (also the wire golden above).
- cmd/auth/refresh.go error: `"refresh token missing for context"` →
`"refresh token missing for profile"`.
- README.md: `## Multi-context` H2 → `## Multi-profile`; code-block
comment `# current context` → `# current profile`.
Code-comment / docstring sweep across cli/cmd/auth/ and
cli/internal/cmdutil/. Comments referencing Go stdlib context.Context,
the RAG / LLM "context window" concept, and historical CHANGELOG
entries for v0.4 / v0.5 were left alone.
CHANGELOG v0.7 BREAKING entry gains the on-disk-schema bullet under
the existing "context → profile" item.
2. Profile name validation (shell-injection guard)
`envelope.error.retry_command` is a single shell-string field. An
AI agent that exec()s it via `sh -c <retry_command>` was injectable
through a maliciously-named profile:
weknora auth logout --name 'x; rm -rf ~'
# would produce: retry_command = "weknora auth logout --name x; rm -rf ~ -y"
`cmd/profile/add.go` already enforced an alphanumeric + `-_.`
allowlist via `validateName`. The `auth login` and `auth logout`
paths bypassed it.
- Moved validation from `cmd/profile/add.go` to
`cli/internal/cmdutil/profilename.go` as exported
`ValidateProfileName` (cmdutil is the import-cycle-safe home;
internal/config can't depend on cmdutil).
- `auth login` runs the validator before any persist call.
- `auth logout` runs the validator on `opts.Name` before
constructing `retry_command`.
- Unit tests (`profilename_test.go`) cover the allowlist, empty
rejection, path-traversal, shell metacharacters (`;`, `&`, `|`,
`$()`, backticks, quotes, whitespace, glob, redirects), and the
user-facing hint text. The shell-metachar test exists as a
regression guard.
Wire shape (`retry_command` string → `retry_command_argv []string`)
remains a v0.8 additive change per ROADMAP — this fix removes the
practical exploit path without touching the wire contract.
3. AI-agent terminology disambiguation
"agent" has three referents in this codebase: (a) WeKnora's
server-side Custom Agent resource, (b) the removed `agent invoke`
verb, (c) external LLM/automation consumers. Per project memory
feedback_no_meta_disambiguation_in_docs, the fix is full-term
naming, not "X has N meanings" prose. Surgical changes at section
headers + ambiguous prose:
- AGENTS.md: "Agent decision shortcuts" → "AI agent decision
shortcuts"; "agent-callable surface" → "AI-agent-callable
surface".
- README.md: "Designed to be agent-first" → "AI-agent-first";
"Other agent ergonomics" → "Other AI-agent ergonomics"; "in
agent contexts" → "in AI-agent contexts"; "for CI / agents" →
"for CI / AI agents".
Anaphoric "agents" inside paragraphs that already established
"AI agents" was left alone — full substitution everywhere would
have been prose noise without clarity gain.
4. Wire-contract review follow-ups
Real findings from a second-pass review of the v0.7 envelope /
streaming / surface design. Per project memory
feedback_check_in_domain_anchor_first, candidate findings were
first verified against the in-domain peer CLI explicitly cited as
the envelope anchor; two earlier-flagged issues turned out to be
in-pattern and were withdrawn.
Surviving fixes:
- AGENTS.md success-envelope example rewritten. The prior example
showed `has_more: false` / `_notice: {}` as if they were always
present, but both fields are `omitempty` and never serialize
when zero / nil. Replaced with three realistic shapes (list /
single resource / mutation with no payload) and added a note
that optional fields are omitted when empty.
- cmd/chat/chat.go Args: MinimumNArgs(1) → ExactArgs(1).
v0.6 silently joined `weknora chat hello world` into
`"hello world"`. v0.7 now rejects multi-arg with exit 2,
matching `weknora session ask`. BREAKING; CHANGELOG entry
added under v0.7 BREAKING.
- internal/output/envelope.go extracts NewEnvelope(data, meta,
profile) constructor. The jq-filter path in
cmdutil.FormatOptions.Emit was manually rebuilding the
envelope literal alongside the canonical WriteEnvelope path —
drift risk when fields are added. Single construction point now.
- internal/cmdutil/factory.go adds AddKBFlag(cmd) helper.
Five files (chat, doc/list, doc/upload, doc/create, doc/fetch)
had verbatim-identical `cmd.Flags().String("kb", ...)`
declarations. Centralised so flag name + help text stay
in sync with Factory.ResolveKB. Docstring reordering + gofmt
fixup landed in the same edit to keep ResolveKB's own godoc
attached to its function.
5. OSS-readiness comment / doc sweep
Pre-publication scrub of code, comments, and shipped Markdown to
remove references that only make sense in the development repo:
- AGENTS.md "Deliberate deviations + mainstream alignments"
section: removed peer-project name-drops from the comparison
table; rewrote as five flagged design decisions with rationale
but no specific competitor named. The four rows that previously
contrasted against a named peer CLI now state WeKnora's choice
+ rationale directly. Section header renamed to "Design
decisions worth flagging" since it is no longer a
deviation/alignment matrix.
- CHANGELOG v0.7 BREAKING rationales: three references to a
named peer CLI removed; the context→profile rationale now
cites only mainstream multi-credential CLIs by category (AWS /
Stripe / OpenAI / Anthropic), and the `api -d/--data` removal
rationale cites only `gh api` / `curl`. `chat` BREAKING entry
rationale similarly simplified.
- 35 cross-references to design-spec section numbers (§4.1 /
§4.5 / §5.3 etc.) removed from Go doc comments and test
comments across 13 files. The referenced spec lives outside
the shipped tree; readers of the public repo cannot resolve
them. Each reference replaced with a self-contained semantic
description (e.g. "the batch envelope" / "AGENTS.md section
on the success path").
- Mixed-language strings translated to English:
- Four Go comments: internal/cmdutil/exit.go:213,215,
internal/cmdutil/errors.go:156,
internal/output/batch_test.go:90,
internal/output/envelope_test.go:27.
- One CHANGELOG section title:
`v0.7 — Agent-first wire contract + 命令面集中清理` →
`... + command-surface cleanup`.
- CJK test fixtures (internal/text/truncate_test.go CJK
truncation cases, cmd/session/list_test.go Chinese session
title, acceptance/e2e/e2e_test.go Chinese RAG corpus)
retained — they are intentional test inputs, not stray prose.
- Makefile help comment: `golangci-lint added in PR-9` →
`golangci-lint planned`. Internal PR numbering should not
surface in shipped Makefile prose.
Build green, 28/28 packages, +5 new ValidateProfileName tests.
go vet / gofmt / go mod verify / go mod tidy all clean.
Rationale for the cascade: pre-1.0 is the cheapest moment to close
L-25 self-consistency (L-26). The half-finished internal rename
would have perpetuated the very `context` vs `context.Context`
ambiguity that motivated v0.7's user-visible rename in the first
place.
28 KiB
AGENTS.md
This is the WeKnora CLI (weknora), a command-line client for the WeKnora RAG server. The module path is github.com/Tencent/WeKnora/cli.
The wire contract for AI agents consuming weknora output (JSON shape, exit codes, error format) is documented below and in README.md. Read this file if you're integrating with the CLI binary — build / test / architecture details follow the wire contract sections.
Wire contract for AI agents
This CLI's primary consumers include AI agents (Claude Code, Cursor, Gemini CLI, etc.). Output format is the agent-facing API. Every error message and every JSON field you write becomes part of an agent's decision-making input.
Stdout (success path)
All --format json (default) commands emit a symmetric envelope. Optional
fields are omitempty — they only appear when populated:
// list (kb list, doc list, ...) — data is an array, meta carries count
{
"ok": true,
"data": [ {"id": "kb_abc", "name": "prod"} ],
"meta": {"count": 1},
"profile": "prod"
}
// single resource (kb view, doc view, ...) — data is an object
{
"ok": true,
"data": {"id": "kb_abc", "name": "prod", "description": "..."},
"profile": "prod"
}
// mutation success with no payload (some delete / edit paths)
{"ok": true, "profile": "prod"}
data is omitted on mutation-only success (no payload). meta carries list
counters (count, has_more) and batch successes/failures, and is omitted
when empty. meta.next_cursor, meta.total_count, and meta.request_id are
reserved — not currently populated; planned for v0.8 when the SDK exposes
pagination cursors and response headers. _notice is reserved — open-map
infrastructure is in place for deprecation / version_skew / security notices;
the field is omitted until a producer is wired in v0.8. profile echoes the
resolved profile name and is omitted when no profile is configured.
Stderr (error path)
Errors emit an error envelope on stderr (--format json) or prose
code: message\nhint: ...\nretry: ... (--format text):
{
"ok": false,
"error": {
"type": "auth.unauthenticated",
"message": "fetch current user: HTTP error 401",
"hint": "run `weknora auth login`",
"retry_command": "weknora auth login --host https://kb.example.com",
"retry_after_seconds": 0,
"risk": {"level": "destructive", "action": "noun.verb"},
"detail": {}
},
"_notice": {}
}
type is the typed code (see Error code reference
below). hint is prose; retry_command is the suggested next argv (single
shell-escaped string). For non-destructive errors agents may execute it; on
exit-10 (input.confirmation_required) it is informational only — the human
must approve the destructive write explicitly. See "Exit-10 anti-patterns" for
details. Note: tokens in retry_command are built via fmt.Sprintf with
user-supplied IDs unquoted — callers that auto-execute must shell-quote each
token (emitting as a JSON array is planned for v0.8).
retry_after_seconds mirrors HTTP Retry-After. risk tags high-risk writes.
detail carries structured per-error context (e.g. unknown_subcommand's
available[] list).
NDJSON event stream (chat / session ask)
--format json and --format ndjson both produce one JSON event per line —
no envelope wrapping. The CLI injects exactly one event (init) at the head;
all subsequent events pass through verbatim from the SDK:
{"type":"init","session_id":"...","kb_id":"...","profile":"...","agent_id":"..."}
{"type":"thinking","content":"..."}
{"type":"answer","content":"Hello"}
{"type":"tool_call","name":"...","input":{}}
{"type":"complete","done":true}
For prose rendering, pass --format text.
_notice evolution policy
_notice is an open map. New keys are additive non-breaking; agents MUST
ignore unknown keys. v0.7 reserves three keys: deprecation / version_skew /
security. New keys follow snake_case convention. The _notice field is
currently always empty — producer wiring is planned for v0.8 when the SDK
exposes version metadata. The wire infrastructure is in place so adding a
producer in v0.8 will not change the envelope shape.
CLI vs server SDK contract boundary
CLI 1.0 contract covers:
- ✅ Envelope wire shape (success + error)
- ✅ CLI-injected events (
initonly) - ✅ NDJSON line shape (bare
{type:...,...}) - ✅ Passthrough discipline (CLI doesn't rename SDK events)
CLI 1.0 contract does NOT cover:
- ❌ Specific SDK event names (
answer/tool_call/complete/ ...) - ❌ SDK event field shapes (server's own version contract)
Analogy: kubectl 1.0 doesn't lock K8s API resource schemas; you handle K8s version separately. WeKnora CLI 1.0 same.
The one rule
Every error message you write will be parsed by an AI to decide its next action. Make errors structured, actionable, and specific.
Environment variables
| Variable | Purpose |
|---|---|
WEKNORA_PROFILE |
Active profile name. Equivalent to the global --profile <name> flag; overridden by --profile. Useful in CI scripts that cannot pass global flags. |
WEKNORA_FORMAT |
Default --format value (text | json | ndjson). Overridden by explicit --format. Invalid values ignored silently. |
WEKNORA_KB_ID |
Default KB ID for commands that accept --kb. Overridden by --kb. |
WEKNORA_LOG_LEVEL |
SDK debug log level (error | warn | info | debug). Overridden by --log-level. |
WEKNORA_AGENT_HELP |
Set to 1 to emit structured JSON agent-help (machine-readable) instead of human help text when --help is invoked. |
Note for agents — machine-readable version and help: The
--versionflag and--helpflag on the root command bypass--format jsonand always emit prose (cobra built-in paths). For machine-readable output use theversionsubcommand (weknora version --format json) orWEKNORA_AGENT_HELP=1 weknora <cmd> --help. Planned fix for v0.8.
Design decisions worth flagging
Five design decisions readers may want context on: where WeKnora picks an opinionated default, what the trade-off is, and what mainstream practice it is or isn't aligned with.
1. Channel split: success → stdout, error → stderr
| WeKnora | success envelope → stdout; error envelope → stderr |
| Rationale | weknora ... --format json | jq '.data[]' must not mix error objects into the data stream. Channel split lets pipeline consumers suppress errors with 2>/dev/null and still get clean JSON on stdout. (kubectl follows this convention.) |
2. weknora api DELETE triggers exit-10 confirmation
| WeKnora | DELETE triggers exit-10 (input.confirmation_required); user bypasses with -y/--yes |
| Rationale | DELETE is irreversible. Most raw-API CLI commands rely on restricted credentials for safety, but self-hosted deployments may not have restricted-credential infrastructure available. Defensive default because agents are common consumers. |
3. retry_command distinct from hint
| WeKnora | two separate fields: retry_command (suggested next argv, directly-executable for non-destructive errors; informational only on exit-10) + hint (prose) |
| Rationale | Agents don't regex-extract argv from prose — known fragility. Trade-off: one extra envelope field. On exit-10, the user must approve the destructive write; agents surface retry_command for human review, not auto-execution. |
4. NDJSON event stream has no envelope wrapping
| WeKnora | streaming commands (chat, session ask) emit bare {type:...} per line; no envelope |
| Rationale | This matches established practice across NDJSON-emitting CLIs and webhook protocols. A streaming envelope requires unwrap before dispatch — net burden with no benefit. |
5. No schema_version field in payload
| Mainstream | some APIs (Anthropic / OpenAI) embed a version field in payload |
| WeKnora | version identity via CLI binary semver + CHANGELOG ### BREAKING + skill tested_against + CI parity tests |
| Rationale | Mainstream CLIs don't embed version in payload. Agents have complete version awareness via weknora --version and skill version binding. |
Pre-1.0 breaking policy
CLI is in v0.x pre-release. Breaking changes ship together in concentrated
batches (v0.5 / v0.6 / v0.7) rather than scattered across patch releases.
After 1.0, any breaking change requires a 2-version deprecation period.
CHANGELOG.md ### BREAKING section is authoritative.
For agents: pin the CLI version in your skill's tested_against field; bump
tested_against only after manually validating against the new CLI.
Build, Test, and Lint
go build -o weknora . # build (from cli/)
go test -count=1 ./... # unit + contract tests
go test -run TestFoo ./internal/format/ # single test
go test ./acceptance/contract/ -args -update # refresh wire goldens
go test -tags acceptance_e2e ./acceptance/e2e/... # live-server e2e (gated by env)
go vet ./...
Both go test -count=1 ./... and go vet ./... must pass before committing.
Architecture
Entry point: cmd/main.go → cmd.Execute() → cmd.NewRootCmd(cmdutil.New()).
Key packages:
cmd/<name>/— cobra command implementations, one subdir per top-level commandinternal/cmdutil/—Factory,FormatOptions, typedError, exit-code mapping, destructive-write confirm, KB id-or-name resolveinternal/format/— bare JSON emitter (WriteJSON/WriteJSONFiltered)internal/iostreams/— global IO singleton + TTY detection +SetForTestswapinternal/secrets/—Storeinterface;KeyringStoreprimary,FileStore0600 fallback,MemStorefor testsinternal/prompt/—TTYPrompter(password no-echo) +AgentPrompter(non-TTY no-prompt sentinel)internal/sse/—Accumulatorfor chat / session ask SSE streamsinternal/mcp/— curated 10-tool stdio MCP server (wired bycmd/mcp/serve.go); see MCP tool surface for the curation rationale and inventoryclient/(parent module) — generated SDK
Command Structure
A command weknora foo bar lives in cmd/foo/bar.go with bar_test.go.
Canonical Examples
- Command + tests:
cmd/kb/list.goandlist_test.go - Destructive write + confirm protocol:
cmd/kb/delete.go - SSE streaming command:
cmd/chat/chat.go - Factory wiring:
internal/cmdutil/factory.go
The Options + Narrow Service Pattern
Every command follows this structure (see cmd/kb/list.go):
Optionsstruct with flag-bound fieldsServiceinterface declaring only the SDK methods this command calls.*sdk.Clientsatisfies it implicitly via duck typing.NewCmd<Verb>(f *cmdutil.Factory) *cobra.Commandconstructor — flag registration +cmdutil.AddFormatFlag- Separate
run<Verb>(ctx, opts, fopts, svc, args...)with the business logic — the test injection point
Key rules:
- Each command owns its own
Serviceinterface; do NOT share interfaces acrosscmd/*packages. Per-file dependency graph is the goal. - Lazy-init
f.Client()/f.Secrets()/f.Prompter()insideRunE, not the constructor (else--helpforces auth). - Required flags:
_ = cmd.MarkFlagRequired("name")— cobra returns the error only on registration-time typo. - New subtrees register in
cmd/root.go NewRootCmd. Verb subtrees register their leaves in the subtree's ownNewCmd.
Command Examples and Help Text
Use a Go raw string with weknora as the example prefix. Keep one-line Short ≤ 70 chars; Long may run multi-paragraph; Example always includes weknora so copy-paste works:
Example: ` weknora kb view <id>
weknora kb view kb_abc --format json
weknora kb view kb_abc --format json --jq '{id, name}'`,
JSON Output
Add --format / --jq via cmdutil.AddFormatFlag(cmd, fieldNames...). In RunE:
fopts, err := cmdutil.CheckFormatFlag(c)
if err != nil { return err }
fopts.ResolveDefault(iostreams.IO.IsStdoutTTY())
// ...
if fopts.WantsJSON() {
return fopts.Emit(iostreams.IO.Out, result)
}
Emit is the single source for the bare-JSON contract — it honors --format json|ndjson and --jq <expr> filtering. Never call format.WriteJSON* directly from a command. See cmd/kb/list.go.
Destructive Writes
Commands that delete / empty / overwrite call cmdutil.ConfirmDestructive(p, opts.Yes, fopts.WantsJSON(), what, id) before mutation. In non-TTY OR JSON-output mode without -y, it returns CodeInputConfirmationRequired → exit 10. See internal/cmdutil/confirm.go.
Testing
Narrow Service Fakes
Each command's runX(ctx, opts, fopts, svc, ...) takes its interface, not *sdk.Client. Tests inject plain-struct fakes:
type fakeBarSvc struct {
gotID string
resp *sdk.Bar
err error
}
func (f *fakeBarSvc) GetBar(_ context.Context, id string) (*sdk.Bar, error) {
f.gotID = id
return f.resp, f.err
}
No mocking library; the narrow-interface design makes fakes 5 lines each.
IOStreams in Tests
out, errBuf := iostreams.SetForTest(t) // bytes.Buffer sinks, non-TTY
ios, _ := iostreams.SetForTestWithTTY(t) // simulate terminal
Confirm Prompts
Use testutil.ConfirmPrompter{Answer: bool, Err: error} from internal/testutil/. Single source for the prompt double — do NOT re-define confirmPrompter per package.
Assertions
Use testify. Prefer require (not assert) for error checks so the test halts immediately, and assert for value comparisons:
require.NoError(t, err)
require.ErrorAs(t, err, &typed)
assert.Equal(t, "expected", actual)
Acceptance: Wire-Shape Goldens
acceptance/contract/wire_test.go drives the in-process cobra tree against httptest.Server fixtures and compares stdout to acceptance/testdata/wire/<case>.json. Error-path cases also assert stderr contains the typed code substring (e.g. auth.unauthenticated). Update goldens with go test ./acceptance/contract/ -args -update.
Table-Driven Tests
Use for flag validation, error classification, parser edge cases. See internal/cmdutil/exit_test.go and cmd/kb/list_test.go.
tests := []struct{ name string; ...}{
{name: "descriptive case", ...},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { /* arrange, act, assert */ })
}
Code Style
- Add godoc to every exported function, type, and constant. Explain why, not what — the name already says what.
- Don't comment to restate the code. Delete comments that narrate the next line.
- Don't reference task numbers, commit SHAs, or version tags in inline comments — they belong in CHANGELOG or git log.
- Never paste em-dashes (—) into Go source; use ASCII
-or rewrite. (Markdown docs may use em-dashes.) - Don't add a helper for a single caller — inline.
Error Handling
Typed error helpers in internal/cmdutil/errors.go:
cmdutil.NewError(code, msg)— fresh typed errorcmdutil.WrapHTTP(err, format, args...)— wrap an SDK error + classify from HTTP status (404 →resource.not_found, 401 →auth.unauthenticated, …). Use at every SDK call site.cmdutil.Wrapf(code, err, format, args...)— explicit wrap with a chosen codecmdutil.NewFlagError(err)— flag / argument problem → exit 2cmdutil.SilentError— exit 1 without printing (when output already emitted)cmd.MarkFlagsMutuallyExclusive("a", "b")— cobra-level mutex
Errors print to STDERR via cmdutil.PrintError(w, err) as code: msg\nhint: .... STDOUT stays bare JSON or empty on failure, so --json | jq pipelines never have to filter error shapes.
User-facing exit-code mapping lives in README.md "Exit codes". When adding a new ErrorCode constant, also append to AllCodes() so the acceptance contract picks it up.
Error code reference
Audience: AI agents and scripted callers parsing
weknorastderr. Code authors writing new error sites — see## Error Handlingabove.
When weknora exits non-zero, stderr carries a structured triplet:
<code>: <message>
hint: <actionable next step>
Agents parse the first colon to extract the typed code. The exit code class (see README.md "Exit codes") controls retry / surface decisions; the typed code disambiguates within a class.
| Code | Exit | Retryable | Default hint |
|---|---|---|---|
auth.unauthenticated |
3 | no (run auth login) |
run weknora auth login |
auth.token_expired |
3 | yes (after refresh) | your session expired; run weknora auth login to re-authenticate |
auth.bad_credential |
3 | no (re-login) | run weknora auth login |
auth.forbidden |
3 | no | active profile lacks permission for this resource |
auth.cross_tenant_blocked |
3 | no | verify tenant context with weknora auth status |
auth.tenant_mismatch |
3 | no | verify tenant context with weknora auth status |
input.invalid_argument |
5 | no | see weknora <command> --help for valid usage |
input.missing_flag |
5 | no | see weknora <command> --help for valid usage |
input.confirmation_required |
10 | NO automatic retry | high-risk write - re-run with -y/--yes after the user explicitly approves |
input.unknown_subcommand |
5 | no | invocation reached a command path with no matching subcommand. Detail includes available list; retry with <path> --help. |
resource.not_found |
4 | no | verify the resource ID and try again |
resource.already_exists |
1 | no | use a different name or fetch the existing resource |
resource.locked |
1 | maybe (transient lock) | (no canonical hint; check resource state) |
server.error |
7 | yes (with backoff for 5xx) | (no canonical hint) |
server.timeout |
7 | yes (with backoff) | request timed out; retry, or run weknora doctor to check connectivity |
server.rate_limited |
6 | yes (back off, then retry) | rate-limited; retry after a few seconds |
server.session_create_failed |
1 | yes (with backoff) | could not create a chat session; pass --session to reuse an existing session |
server.incompatible_version |
7 | no (upgrade required) | run weknora doctor to see version skew details |
network.error |
7 | yes (with backoff) | check base URL reachability with weknora doctor |
operation.timeout |
124 | yes (raise --timeout) |
wait timed out; raise --timeout or check the underlying job |
operation.failed |
1 | no (target reached terminal failure) | one or more targets reached a terminal failure (e.g. doc parse_status=failed) |
operation.cancelled |
1 (main overrides to 130) | no | command interrupted by SIGINT / SIGTERM. The typed code maps to exit 1, but main raises the exit to 130 when the root context was signal-cancelled so the user-visible exit follows Unix signal convention. |
local.config_corrupt |
1 | no (manual fix) | remove ~/.config/weknora/config.yaml and re-run weknora auth login |
local.profile_not_found |
1 | no | (no canonical hint; check weknora profile list) |
local.file_io |
1 | no | check file permissions under $XDG_CONFIG_HOME/weknora/ |
local.kb_id_required |
1 | no | run weknora link to bind this directory to a knowledge base, or pass --kb |
local.kb_not_found |
1 | no | list available with weknora kb list |
local.keychain_denied |
1 | no (system-level) | verify keyring access; falls back to file storage |
local.project_link_corrupt |
1 | no | remove .weknora/project.yaml and run weknora link again |
local.sse_stream_aborted |
1 | yes (rerun chat / session ask) | the streaming answer was cut off mid-flight; retry, or pass --format json to buffer the full response |
local.unimplemented |
1 | no | (planned in a future release) |
local.upload_file_not_found |
1 | no | verify the path is correct and readable |
local.user_aborted |
1 | no (user said no) | no action taken; pass -y/--yes to skip the confirmation prompt |
mcp.readonly_mode |
1 | no | MCP tool surface is read-only; mutations not exposed in this mode |
mcp.schema_unknown_command |
1 | no | (no canonical hint) |
mcp.tool_not_allowed |
1 | no | MCP tool not in the curated allowlist |
AI agent decision shortcuts
For common retry patterns, AI agents can hardcode:
network.*→ retry with exponential backoffauth.token_expired→ runweknora auth refresh, then retry onceserver.rate_limited→ back off (Retry-After if present) then retryoperation.timeout→ raise--timeoutand retry, or surface to userinput.confirmation_required→ NEVER auto-pass-ywithout explicit user authorization*.invalid_argument/*.missing_flag→ surface to user (don't retry)
Exit-10 anti-patterns
Exit code 10 (input.confirmation_required) marks a destructive write where the
CLI refused to proceed without explicit user approval. The retry envelope includes
retry_command showing the exact argv that would proceed. AI agents must NEVER
auto-retry this exit code — every exit 10 is a user-in-the-loop decision.
Don't do these:
-
Auto-add
-y/--yesand retry. The flag exists for the user, not the agent. Surface the exit-10 envelope to the user verbatim and wait for explicit go-ahead. -
Parse the retry_command and run it. The retry_command is informational -- showing what would execute. Running it without user input collapses two steps the user is supposed to see.
-
Wrap the call in a retry-with-backoff loop. Exit 10 is not transient. It's a "this needs human approval" signal, not a transient transport error.
-
Treat exit 10 as a generic error and fall back to a less-destructive verb. The user asked for the destructive verb. If they want something else they'll say so. Don't substitute.
-
Auto-add
-ybecause the previous exit-10 was approved. Each invocation stands on its own. The user's prior approval doesn't extend to similar calls. -
Skip the prompt by switching to
--format json. JSON mode still emitsinput.confirmation_required(just in envelope form). It's the same gate.
MCP Tool Surface
WeKnora's MCP server exposes a curated read-only tool surface. Many MCP servers in the wild ship write / mutation operations on by default and rely on credential-scope or sandbox restrictions for safety. WeKnora opts for curation instead: the server side doesn't yet enforce per-token scope, so an agent holding a user's token has full write access. Until server-side scope ships, the CLI keeps mutation tools out of the MCP surface as a belt-and-braces second line of defense. When server scope arrives this stance can loosen.
The curated 10 tools (cli/internal/mcp/tools.go):
| Tool | Purpose |
|---|---|
kb_list |
list knowledge bases |
kb_view |
fetch a knowledge base by id |
doc_list |
list documents in a kb (paginated, status-filterable) |
doc_view |
fetch a document by id |
doc_download |
download raw bytes (1 MiB cap, base64 for binary) |
chunk_list |
list chunks of a document for RAG retrieval debug |
search_chunks |
hybrid (vector + keyword) retrieval |
chat |
stream a RAG answer; auto-creates a session if absent |
agent_list |
list custom agents |
agent_invoke |
run a query through a custom agent |
Adding a tool is a deliberate API expansion — the AI-agent-callable surface is the reason this CLI ships an MCP server, not its CLI command list, so the registration list in registerTools is maintained by hand.
Command surface design SOP
Before specifying any CLI command, do this in order:
grep -A 50 "type Foo struct" client/foo.go— dump SDK request/response schemas.- List every field with type and source line.
- For each field, decide: hot-path flag / config-file only / hidden / never-expose.
- Cross-check pagination signatures: an SDK
(ctx, id, page, pageSize)shape demands--limit+--all-pages+--page-sizeon the CLI side. - ONLY THEN consult mainstream CLI conventions to choose flag names, positionals, mutex, and confirm semantics.
- Decide which fields are "top use case" (flag) / "advanced" (
--config-fileor escape hatch viaweknora api). Don't try to flag-cover every SDK field — mature CLIs that curate ship a tighter surface; CLIs that 1:1 mirror their API pay the UX cost.
Rationale: earlier drafts produced three categories of schema errors — fields that didn't exist on the underlying SDK, wrong field counts in user-facing docs, and missing pagination flags — that all stemmed from "design from convention, not from SDK." The fix is canonical: the SDK schema is the ground truth; convention decides names and shapes around that ground truth.
CRUD command flag conventions
CRUD commands follow the hard-required-flags pattern: every required input is a flag or positional, and a missing one yields an immediate input.invalid_argument exit. The contrast is TTY-prompts-fill, where missing input opens an interactive prompt; that pattern is reserved for auth login (the one command where a human must be at the terminal).
Required-input idioms in this codebase:
- Positional required:
cobra.ExactArgs(N)orcobra.MinimumNArgs(1) - Flag required:
cmd.MarkFlagRequired("flag") - Custom required (e.g.,
agent editneeds at-least-one-edit-flag): RunE-level validation that returnsinput.invalid_argument - Mutex:
cmd.MarkFlagsMutuallyExclusive("a", "b")
Reasons hard-required-flags is the v0.5+ default:
-
Admin / debug commands have no natural human-interactive prompt to lean on.
-
Agent-friendly: MCP callers do not stall waiting for stdin prompts.
-
Consistent with every existing non-auth WeKnora command.
-
Agent help blob: Commands MAY call
cmdutil.SetAgentHelp(cmd, cmdutil.AgentHelp{...})to expose a stable JSON used_for / required_flags / examples / output / warnings shape. Activated byWEKNORA_AGENT_HELP=1at--helptime. Warnings are always rendered in human help (stderr, not env-gated). Applied tochat,kb list,session ask, and all destructive commands. Extending to another command requires touching only that command'sNewCmd.
Status / check verb pair pattern
When a resource has both a cheap "is it alive?" probe and a deeper "verify its dependencies / aggregate state" probe, expose them as two verbs so the verb itself communicates cost:
status <id>— single HTTP, returns reachable + cheap fields.check <id>— 1 + N HTTP, adds derived state that needs follow-up calls (e.g., aggregatingfailed_countvia doc-list page-walk, probing every KB in an agent's scope).
Current pairs: kb status / kb check, agent status / agent check.
The deep verb's Long help text must enumerate the extra HTTP calls so
cost is predictable.
Long-poll wait commands
doc wait <doc-id> [<doc-id>...] is the model for any future
wait command:
- Always wait-all on multi-target (no fail-fast flag); compose in shell
(
wait id1 && wait id2) when fail-fast is needed. - Exponential backoff with jitter (initial
--interval, cap 15s). - Concurrency capped (5 in flight); large fan-out via
xargs -P. - Exit-code priority: failed (1) > timeout (124) > completed (0). The
failed bucket is
operation.failed, notserver.error— a target's own terminal failure is not a transient transport issue. - Validate
--format/--jqbefore polling so an invalid flag does not cost the caller a multi-minute poll.