Files
Enterprise-Knowledge-Base/cli/AGENTS.md
T
nullkey c87e35b34b chore(cli): polish + docs sync + pre-PR audit fixes
Code-reuse polish (post-implementation review pass):
- Extract text.OneLine(maxWidth, s) helper combining preview-row
  normalization (newline/CR/tab → space) with text.Truncate's
  UTF-8-safe truncation. Replaces agent/view.go truncate1Line (ASCII
  '...' + byte-slice CJK-unsafe) and chunk/list.go singleLine.
- Lift cmdutil.OpenInput(path) for the '-' = stdin / else os.Open
  pattern shared across agent create/edit and the api command.
  Replaces agent/create.go's private openInput.
- Strip inline doc-spec parentheticals from source comments — those
  belong in commit messages and project docs, not in source where
  they rot.

Pre-PR audit fixes:
- doc upload: reject `--metadata` paired with `--from-url` as
  input.invalid_argument up-front (the URL-ingest request type has
  no metadata field server-side, so the pair would otherwise silently
  drop). Long help and CHANGELOG updated to call out the asymmetry.
- doc upload (file path): map sdk.ErrDuplicateFile sentinel to
  resource.already_exists. The sentinel arrives with no "HTTP error <n>:"
  prefix because the SDK short-circuits on file-hash before reading the
  HTTP status, so the previous WrapHTTP fall-through misclassified it
  as network.error with a misleading "check base URL reachability" hint.
  The --from-url branch already handled ErrDuplicateURL this way; this
  closes the asymmetry. Caught by e2e re-upload of an already-ingested
  file; regression test added.
- README exit-10 enumeration adds `agent delete` and `chunk delete`
  (these were missing alongside the v0.5 destructive verbs they were
  meant to gate).

Docs sync:
- cli/README.md: command tree now includes the chunk subtree; adds
  agent / chunk lines to the 5-minute quickstart; adds a "Contributing
  / Reporting issues" section pointing at the repo's SECURITY.md and
  AGENTS.md; drops third-party CLI parallels from the surface
  description.
- cli/AGENTS.md: "Command surface design SOP" gains the
  flag-vs-escape-hatch step. "CRUD command flag canon" renamed to the
  hard-required-flags pattern with the contrast (TTY-prompts-fill)
  defined inline rather than via opaque shorthand.
- cli/CHANGELOG.md: search docs case-sensitivity shift promoted to its
  own #### Breaking changes subsection. MCP doc_list filter count
  corrected from 5 to 6. Drops the bogus go.mod yaml.v3 entry (yaml.v3
  was already a dependency on main; v0.5 added zero go.mod lines).
  Replaces internal-Go identifiers (fuzzyTime, NoOptDefVal) with
  user-language and drops the § section-symbol jargon.
2026-05-16 16:56:33 +08:00

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

Command surface design SOP

Before specifying any CLI command, do this in order:

  1. grep -A 50 "type Foo struct" client/foo.go — dump SDK request/response schemas.
  2. List every field with type and source line.
  3. For each field, decide: hot-path flag / config-file only / hidden / never-expose.
  4. Cross-check pagination signatures: an SDK (ctx, id, page, pageSize) shape demands --limit + --all-pages + --page-size on the CLI side.
  5. ONLY THEN consult mainstream CLI conventions to choose flag names, positionals, mutex, and confirm semantics.
  6. Decide which fields are "top use case" (flag) / "advanced" (--config-file or escape hatch via weknora 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 canon

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) or cobra.MinimumNArgs(1)
  • Flag required: cmd.MarkFlagRequired("flag")
  • Custom required (e.g., agent edit needs at-least-one-edit-flag): RunE-level validation that returns input.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.