Survey of 10 mainstream CLIs (gh, lark, stripe, vercel, supabase, aws, azure, gcloud, openai/codex, github-copilot-cli) showed env-gated per-command --help blurbs are a Stripe-only pattern; gh uses env detect for telemetry only, and lark relies on installed agent Skills + MCP. Our cmd/mcp/serve already covers the dominant 2025/26 path, so internal/aiclient/ (136 LOC + 38 callsites) is net maintenance burden without precedent. - Drop internal/aiclient/ entirely (annotations + detect + tests) - Remove 38 SetAgentHelp callsites + agentAwareHelpFunc / SetHelpFunc wiring in cmd/root.go - Migrate 4 command-level rules to standard Long help (visible to all, not env-gated): doc upload mode mutex, kb edit at-least-one, kb pin idempotent, search chunks channel mutex - Rewrite AGENTS.md as a developer guide (gh-style 6 H2 / 167 lines): audience preamble + Build / Architecture / Command Structure / Testing / Code Style / Error Handling. Drops sections absent in surveyed projects (Commit & PR Conventions, Who Uses This CLI) - Clean 14 internal doc refs (ADR-N, spec §X, v0.X) in source comments and docs that pointed at docs/superpowers/ — that directory is local-only / uncommitted, so refs are dead for outside readers - Drop forward-looking "once v0.2 ships" from README
7.2 KiB
AGENTS.md
WeKnora CLI (weknora) is a noun-verb wrapper around the WeKnora server API; module path github.com/Tencent/WeKnora/cli. This file is the developer guide for coding agents and human contributors editing the CLI. The user-facing wire contract (output shape, exit codes, error format) lives in README.md.
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/<noun>/— cobra command implementations, one subdir per nouninternal/cmdutil/—Factory,JSONOptions, 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(huh-based, password no-echo) +AgentPrompter(non-TTY no-prompt sentinel)internal/sse/—Accumulatorfor chat / agent invoke SSE streamsinternal/mcp/— curated stdio MCP server (wired bycmd/mcp/serve.go)client/(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.AddJSONFlags- Separate
run<Verb>(ctx, opts, jopts, 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 --json
weknora kb view kb_abc --json=id,name`,
JSON Output
Add --json / --jq via cmdutil.AddJSONFlags(cmd, fieldNames). In RunE:
if jopts.Enabled() {
return jopts.Emit(iostreams.IO.Out, result)
}
Emit is the single source for the bare-JSON contract — it honors --json=fields,... projection 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, jopts.Enabled(), what, id) before mutation. In non-TTY OR --json mode without -y, it returns CodeInputConfirmationRequired → exit 10. See internal/cmdutil/confirm.go.
Testing
Narrow Service Fakes
Each command's runX(ctx, opts, jopts, 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.