Files
WeKnora/cli/AGENTS.md
T
nullkey 5b07c9ab87 feat(cli): chunk subtree + MCP chunk_list tool + curation rationale
New subtree (chunk list / view / delete) exposes RAG retrieval
debugging primitives with SDK-grounded field set (23 Chunk fields).
Pagination follows v0.4 canon: --limit / --page-size (1..1000) /
--all-pages.

- chunk list --doc <id>: enumerate by ChunkIndex (separate from
  search chunks which is hybrid retrieval; Long help documents the
  distinction)
- chunk view <id>: scope-less render via /chunks/by-id route; full
  content verbatim
- chunk delete <id> --doc <id>: scope-flag + scope-id; L-13
  destructive; 404 NOT idempotent; resource.not_found /
  auth.forbidden / input.confirmation_required typed exit codes
  documented in Long help

MCP server gains chunk_list as 10th curated tool. Schema deliberately
exposes only doc_id + limit (no pagination workflow on MCP); response
includes truncated_at_limit flag when total > limit.

cli/AGENTS.md MCP curation rationale rewritten: curated read-only is
a deliberate product call because the server side does not yet
enforce per-token scope. When server scope ships, mutation tools can
land in the MCP surface.

Shared helper cli/internal/text/timeago_string.go (FuzzyAgoStr)
extracted from session list during the C2 quality-review pass.
2026-05-16 16:56:33 +08:00

8.7 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 noun
  • internal/cmdutil/ — Factory, JSONOptions, typed Error, exit-code mapping, destructive-write confirm, KB id-or-name resolve
  • internal/format/ — bare JSON emitter (WriteJSON / WriteJSONFiltered)
  • internal/iostreams/ — global IO singleton + TTY detection + SetForTest swap
  • internal/secrets/ — Store interface; KeyringStore primary, FileStore 0600 fallback, MemStore for tests
  • internal/prompt/ — TTYPrompter (huh-based, password no-echo) + AgentPrompter (non-TTY no-prompt sentinel)
  • internal/sse/ — Accumulator for chat / agent invoke SSE streams
  • internal/mcp/ — curated 10-tool stdio MCP server (wired by cmd/mcp/serve.go); see MCP tool surface for the curation rationale and inventory
  • 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.go and list_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):

  1. Options struct with flag-bound fields
  2. Service interface declaring only the SDK methods this command calls. *sdk.Client satisfies it implicitly via duck typing.
  3. NewCmd<Verb>(f *cmdutil.Factory) *cobra.Command constructor — flag registration + cmdutil.AddJSONFlags
  4. Separate run<Verb>(ctx, opts, jopts, svc, args...) with the business logic — the test injection point

Key rules:

  • Each command owns its own Service interface; do NOT share interfaces across cmd/* packages. Per-file dependency graph is the goal.
  • Lazy-init f.Client() / f.Secrets() / f.Prompter() inside RunE, not the constructor (else --help forces 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 own NewCmd.

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 error
  • cmdutil.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 code
  • cmdutil.NewFlagError(err) — flag / argument problem → exit 2
  • cmdutil.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.

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 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.