Commit Graph

63 Commits

Author SHA1 Message Date
nullkey
c29d36238b docs(cli): AGENTS + README + CHANGELOG for v0.8
AGENTS.md gains three sections for the v0.8 surfaces:
  - Stream recovery   — session continue-stream replay-from-0 semantics
                        and the dedupe contract agents must implement
  - Dry-run contract  — when --dry-run applies, the meta.{dry_run,plan}
                        envelope shape, exit-code semantics (no exit 10
                        on destructive + --dry-run), the GET-reject rule
                        for `weknora api`, and the validation-parity
                        guarantee with the live path
  - Risk metadata     — what the Risk: prefix in --help means and how
                        cobra.Annotations["risk.{level,action}"] are
                        populated

README.md gains user-facing Dry-run preview and Resuming streams
sections.

CHANGELOG.md adds the v0.8 entry covering the new --dry-run flag,
MCP Tool.Annotations, session continue-stream, and the Risk: line.
2026-05-28 19:29:50 +08:00
nullkey
1bae6b6b6c feat(cli): session continue-stream + NDJSON init MessageID
Adds `weknora session continue-stream <session-id> --message <id>` for
re-attaching to an in-progress or already-completed SSE event buffer.

Server semantics (replay-from-0 + tail):
  - Every connection replays the full stored event log from index 0,
    then tails any new events. NOT cursor-from-disconnect. Agents that
    already consumed events on the original stream MUST dedupe by
    message_id + event hash to avoid double-processing.
  - Buffer TTL: redis mode 1h hardcoded; memory mode = process lifetime.
    After expiry the CLI surfaces local.sse_stream_aborted.

Output is NDJSON: one CLI-injected init line carrying
{session_id, message_id, profile} at stream head, then raw SDK
StreamResponse events verbatim. The init line lets agents thread the
resume to the original message in their dedupe table before the first
SDK frame arrives — output.InitEvent gains an omitempty MessageID field
for this purpose; non-resume init events stay unchanged.

The command always emits NDJSON regardless of --format — there is no
human-text use case for raw event-log replay (operator scenarios are
incident response / debugging). --dry-run is excluded for the same
reason streaming commands always are: a buffered plan makes no sense
for an event stream.
2026-05-28 19:29:50 +08:00
nullkey
6d8c8650cd feat(cli): --dry-run + risk metadata + validation parity on 19 mutations
Two intertwined agent safety nets that share the same files:

1. --dry-run flag for offline preview of mutation commands
2. Risk: metadata + SetRisk helper for destructive command surfaces

Coverage (19 mutation commands with --dry-run):
  kb.create/edit/delete           agent.create/edit/delete
  doc.create/upload/fetch/delete  doc.delete_all (special variant)
  session.delete  chunk.delete    profile.add/remove
  auth.refresh/logout             link  unlink
  api.{post,put,patch,delete}     (api.get + --dry-run rejected, exit 2)

Envelope additions (omitempty in non-dry-run paths):
  meta.dry_run: bool        true when --dry-run was used
  meta.plan:    map         {action, args} per the per-command taxonomy

Risk: metadata
--------------
cmdutil.SetRisk(cmd, action) stamps cobra.Annotations with
risk.level=destructive + risk.action=<action> on the 9 destructive
commands. The SetAgentHelp wrapper prepends a "Risk: <action>
(destructive)" line in the default text help path so agents see a clear
warning before parsing Usage. WEKNORA_AGENT_HELP=1 JSON path stays
unchanged — structured agent-help already carries warnings[].

Validation parity with the live path
------------------------------------
Every pure-local validation (flag presence, mutual exclusion, enum
bounds, URL/regex format, ResolveKBLocal for KB resolution that does not
require an SDK call) runs BEFORE the dry-run gate. This matches the
industry-standard "preview shows what live would do" contract:
--dry-run accepts exactly the same invocations the live path accepts and
rejects exactly the same invocations the live path rejects, modulo the
side-effecting work itself.

The side-effecting work (SDK calls, file writes, keyring writes, server-
side name → id resolution) is what --dry-run actually gates. Each
mutation file pairs its RunE validation block with a regression test
under *_dry_run_test.go / dryrun_validation_test.go so future refactors
don't reintroduce the gap.

Helper surface
--------------
- HandleDryRun(cmd, dryRun, plan) extracts the early-return so the
  19 RunE call sites stay 3 lines each.
- EmitDryRun routes through FormatOptions.Emit, inheriting _notice /
  TTY indent / --jq filtering for free.
- ResolveKBLocal mirrors ResolveKB but never calls the SDK; dry-run
  paths use it so the plan reports the raw --kb value (UUID or name)
  without a name → id lookup.

Streaming commands (chat, session ask, session continue-stream) are
deliberately excluded: a buffered plan makes no sense for an event
stream.

Lock semantics in the dry-run path:
  - destructive + --dry-run: exit 0, no exit-10 confirmation prompt
  - --dry-run + -y: byte-identical envelope to --dry-run alone
  - --dry-run + --jq: filter applies to the preview envelope normally
2026-05-28 19:29:50 +08:00
nullkey
c11df51c79 feat(cli): MCP Tool.Annotations on 10 tools (spec 2025-06-18)
Bumps modelcontextprotocol/go-sdk to v1.6.1 and populates Tool.Annotations
on every registered MCP tool per the per-tool hint table:

  Read tools (8):   destructiveHint=false, readOnlyHint=true,
                    idempotentHint=true,   openWorldHint=false
  Invoke tools (2): destructiveHint=false, readOnlyHint=false,
                    idempotentHint=false,  openWorldHint=true

Invoke-class tools (chat, agent_invoke) carry openWorldHint=true because
the server may dispatch external skills (web_search etc.). Read tools are
sealed: idempotent + read-only + closed-world.

TestToolAnnotations_AllToolsHaveExpectedHints asserts the matrix so any
future drift surfaces in CI rather than at first client integration.
2026-05-28 19:29:50 +08:00
nullkey
b395db55de chore(cli): adapt ListKnowledgeChunks interface to variadic chunk_type filter
Upstream commit 5c0243cd ("feat: Support filtering chunks by multiple
chunk_type params") extended client.Client.ListKnowledgeChunks with a
trailing `chunkTypes ...string` variadic. The two narrow service
interfaces in cli/ — ListService in cli/cmd/chunk/list.go and
chunkListService in cli/internal/mcp/tools.go — plus the two fake
implementations in their *_test.go siblings, are updated to match so
*client.Client continues to satisfy them via duck typing.

No CLI flag surfaced yet; both call sites pass no filter and behavior
is unchanged. Exposing `--chunk-type` as a flag on `weknora chunk list`
and as an input field on the MCP `chunk_list` tool is a candidate for a
follow-up commit, per the broader SDK-required → CLI-required alignment
discussion.

Pre-flight: go build ./... clean; go test -count=1 ./... 28/28 packages
pass; go vet ./... clean; gofmt -l . empty.
2026-05-27 10:56:34 +08:00
nullkey
2ee9741fa1 refactor(cli): finish context→profile cascade + post-review hardening (BREAKING)
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.
2026-05-27 10:56:34 +08:00
nullkey
2ce348d020 feat(cli): --format json default + NDJSON event stream + context→profile cascade + help calibration + docs (BREAKING)
D1 — --format default flipped to json regardless of TTY:
- v0.6: smart default (text on TTY, json on pipe).
- v0.7: always json; TTY only affects indent (compact in pipe). Enum
  values unchanged (text | json | ndjson).
- Typed FormatMode enum replaces untyped string consts.
- --format / --jq promoted to persistent root flags so unknown-
  subcommand paths still reach the typed-envelope guard (per-command
  registration in v0.6 would have rejected --format on unknown
  commands as cobra-prose exit 2).
- WEKNORA_FORMAT env var added; precedence --format > env > default.
  Invalid env values silently ignored.

D2 — chat / session ask default to NDJSON event-stream:
- New cli/internal/output/ndjson_stream.go: InitEvent struct +
  EmitInit / EmitSDKEvent / WriteNDJSONLine helpers. EmitInit doc
  encodes the must-be-first-line invariant agents key on.
- chat / session ask: --format json AND --format ndjson both emit one
  JSON event per line (no envelope wrapping). CLI injects exactly one
  `init` event at stream head carrying session_id + optional kb_id /
  agent_id / profile. Subsequent events pass through verbatim from the
  SDK (passthrough discipline per spec §5.1).
- --format text keeps the SSE-style live renderer.

context → profile full cascade:
- Command group: cli/cmd/context/ → cli/cmd/profile/ (git mv;
  package contextcmd → profilecmd).
- Global flag --context → --profile. Factory.ContextOverride →
  ProfileOverride. WEKNORA_PROFILE env var honored
  (--profile flag > env > config.CurrentContext). When --profile or
  WEKNORA_PROFILE references a missing profile, the error is
  input.invalid_argument with hint "weknora profile list" — not the
  destructive local.config_corrupt path (which would have told users
  to delete their config file).
- Binding file .weknora/project.yaml field context: → profile:
  (no backwards-compat alias; re-run weknora link).
- profile use JSON fields current_context / previous_context →
  current_profile / previous_profile.
- weknora link JSON field context → profile.
- CodeLocalContextNotFound → CodeLocalProfileNotFound (typed code
  rename).
- Envelope top-level profile field populated via globalProfile (set
  by root PersistentPreRunE from Factory.ActiveProfile). chat /
  session ask NDJSON init event carries the same profile.
- Rationale: "context" collided with LLM context window / RAG context
  / Go context.Context; mainstream multi-credential CLIs (AWS /
  Stripe / OpenAI / Anthropic / lark) all use "profile".

H2/C1' help calibration:
- AgentHelp gains Warnings []string; single SetAgentHelp helper
  routes on WEKNORA_AGENT_HELP=1 (emits JSON blob including
  warnings) vs human help (appends "AI agents:" block from same
  source). Warnings surface as both a structured JSON field and
  visible help-text addendum without drift.
- 9 destructive commands carry warnings: kb / doc / agent / session /
  chunk delete; profile remove; kb / agent edit; auth logout.
- weknora doc wait dedups ids at entry; SIGINT mid-wait returns
  silently (root signal handler maps to exit 130) instead of being
  miscategorised as operation.timeout / operation.failed.

A4 — docs:
- cli/AGENTS.md gains four agent-facing sections: Wire contract for
  AI agents (stdout / stderr / NDJSON / _notice evolution / SDK
  contract boundary); Deliberate deviations + mainstream alignments;
  Pre-1.0 breaking policy; Exit-10 anti-patterns. ERROR_REFERENCE
  table extended.
- cli/README.md adds Agent quick start under Wire contract.
- cli/CHANGELOG.md v0.7 section: BREAKING entries with migration
  notes, Added (WEKNORA_FORMAT / WEKNORA_PROFILE / retry_command /
  retry_after_seconds / risk / _notice reserved infra / meta.count /
  meta.has_more / doc fetch / doc create / session ask / doc delete
  --all / NDJSON init), Changed (docs additions), Deprecated (none —
  pre-release one-shot breaking).

Spec: docs/superpowers/specs/2026-05-20-weknora-cli-v0.7-design.md §3 / §4 / §5 / §6 / §11
2026-05-27 10:56:34 +08:00
nullkey
a2e368b1e8 refactor(cli): command-surface rename — session ask / doc fetch / doc create / doc delete --all
Three command renames consolidate the v0.7 verb table:

- `weknora agent invoke` → `weknora session ask --agent <id>`.
  Server route POST /sessions/{session_id}/agent-qa is session-
  anchored, so the verb moves with it. `weknora agent` subtree keeps
  CRUD only (list / view / create / edit / delete / status / check).
  SDK call (AgentQAStreamWithRequest) preserved verbatim; only the
  command surface + flag layout move.

- `weknora doc upload` split into three commands:
  - `weknora doc upload <file>`  — local file (only).
  - `weknora doc fetch <url>`    — server-side remote fetch (was
    `upload --from-url`). URL-only flags (--title / --file-type /
    --tag-id) move with the verb.
  - `weknora doc create --text`  — direct text knowledge entry via
    server CreateManualKnowledge.

- `weknora kb empty <id>` → `weknora doc delete --all --kb=<id>`.
  Atomic server ClearKnowledgeBaseContents (no list-then-delete
  race). Same exit-10 -y/--yes guard as other destructive verbs;
  unified through the extended ConfirmDestructive helper.

Parent commands (agent, kb, doc, chunk, session, auth, profile,
search) lose their explicit Args:NoArgs + Run:cmd.Help so the
unknown-subcommand guard fires correctly — `weknora agent invoke
ag_x q` now emits the typed input.unknown_subcommand envelope with
detail.available[] instead of cobra's free-form exit-2 prose.

Spec: docs/superpowers/specs/2026-05-20-weknora-cli-v0.7-design.md §3.4 / §10.7
2026-05-27 10:56:34 +08:00
nullkey
ad150c7126 refactor(cli): envelope sweep — Emit shape, batch ops, MCP StructuredContent, api flag cleanup
Sweep every CLI output path through the v0.7 envelope contract.

FormatOptions.Emit signature changes to (w, data, meta *output.Meta)
so list commands surface meta.count / has_more in the wire envelope.
TTY indent decision plumbed via FormatOptions.TTY (set by
ResolveDefault). --format and --jq promoted to persistent root flags
so unknown-subcommand paths still reach the typed-envelope guard
(per-command registration would have rejected --format on unknown
commands as cobra-prose exit 2). WEKNORA_FORMAT env-var fallback
honored before TTY-resolved default. AddFormatFlag retained for the
per-command --jq field-hint Long-text addendum.

~55 production Emit call sites swept across cli/cmd/**: list paths
populate meta.count; mutation paths pass nil meta; envelope.profile
threaded via globalProfile. Existing JSON-shape tests migrated to
decode the envelope wrapper.

Multi-target batch operations get a unified shape via new helpers:
- output.BatchItem + output.WriteBatchEnvelope (§4.5 per-item ok
  pattern; data:[{id, ok, result?|error?}], meta:{count, successes*,
  failures*}; Successes/Failures *int so explicit zero survives
  omitempty in all-fail case).
- cmdutil.RunBatch + cmdutil.EmitBatch + cmdutil.DeletedAtNow +
  cmdutil.ClassifyContextErr collapse the three previously-duplicated
  multi-delete pipelines (doc / chunk / session) into one call site
  pattern. doc upload --recursive uses the same helpers with a per-
  file resultFn. Summary error sets Silent:true so the stderr
  envelope path doesn't duplicate the stdout batch detail (exit 1
  preserved via Code → ExitCode).

ConfirmDestructive / ConfirmDestructiveBatch extended with action +
retryCmd parameters. Every destructive command (kb delete, kb edit,
doc delete, doc delete --all, agent delete, agent edit, session
delete, chunk delete, auth logout, api -X DELETE) now attaches
error.risk.{level:"destructive", action:"<noun.verb>"} +
error.retry_command on the exit-10 envelope. kb edit / agent edit
also gain the destructive guard wiring (CHANGELOG listed them as
gated but they were silently executing without -y).

MCP tool handlers' 31 error paths return CallToolResult with
IsError + Content text fallback + StructuredContent (envelope.error
shape via cmdutil.ErrorToDetail). Handler Out type changed from
typed *sdk.X to any so the go-sdk auto-marshal doesn't overwrite
StructuredContent with a zero-struct on error returns. Success path
manually populates CallToolResult.StructuredContent via successResult
helper.

weknora api cleanup:
- -d/--data flag removed; body via --input <file> or --input -
  (stdin) only. Aligns with gh / curl convention.
- HTTP method whitelist removed; any non-empty method accepted.
- HTTP Retry-After header → cmdutil.Error.RetryAfterSeconds →
  envelope.error.retry_after_seconds.
- runAPIPaginated raw-passthrough fallback now wraps response in
  envelope so --paginate never emits bare JSON to stdout.

Test sweep: ~60 cmd/** tests migrated to decode envelope shape.
9 wire-contract goldens updated under cli/acceptance/testdata/wire/.

Spec: docs/superpowers/specs/2026-05-20-weknora-cli-v0.7-design.md §4 / §5
2026-05-27 10:56:34 +08:00
nullkey
733bb3aaa1 refactor(cli): symmetric envelope infrastructure (supersedes e623e820)
Re-introduce the agent-first symmetric envelope deleted in commit
e623e820 (2026-05-15). Under the v0.7 constraint that AI agents are
primary consumers, the bare-JSON shape can't carry protocol channels
(_notice / risk / meta / request_id / profile) that agents need.
Errors-on-stderr and typed exit codes from e623e820 are preserved;
the envelope wraps the success/error payloads on top.

- cli/internal/output/envelope.go (new): Envelope / ErrorEnvelope /
  Meta / ErrDetail / RiskDetail structs; WriteEnvelope +
  WriteErrorEnvelope writers; PendingNotice plumbing for the open-map
  _notice channel (reserved infra; producer wiring planned for v0.8).
- cli/internal/output/envelope_test.go (new): 7 tests covering
  success / error / TTY indent / Notice / Risk shapes.
- cmdutil.Error extended with RetryCommand (directly-executable
  argv distinct from prose Hint), RetryAfterSeconds (HTTP
  Retry-After), *RiskInfo (nested level+action; ErrInternalServer-
  vs-NotFound distinction), Detail (open structured payload),
  Silent (suppress stderr emit while preserving Code for ExitCode).
- ErrorToDetail single source of envelope.error construction; reused
  by stderr PrintError, MCP StructuredContent, and batch per-item
  error path.
- WithHint / WithRetryCommand / WithRetryAfter / WithDetail /
  WithRisk builders. IsCancelled helper. AsError unwrap helper.
- defaultHint covers ~21 codes; defaultRetryCommand symmetric
  counterpart for 6 keyway codes. DefaultHint / DefaultRetryCommand
  exported wrappers for cross-package callers.
- ClassifyHTTPError tightened: rescues HTTP 500 with structured
  server-side code 1003 (ErrNotFound) into resource.not_found.
  Server's generic 1007 (ErrInternalServer) bucket stays as
  server.error — including it would mis-route validation failures
  (e.g. SQLSTATE 22001) as not-found.
- PrintError dual-mode: text/human → prose with code:msg / hint /
  retry lines; json/ndjson → envelope on stderr. Mode pinned by
  root PersistentPreRunE via SetFormatMode. resolveFormatEarly()
  scans argv before cobra dispatch so cobra-side validators
  (unknown flag, arg-count) still surface as envelope when
  --format json is in effect. Silent typed errors short-circuit
  the stderr emit. FlagError mapped to input.invalid_argument for
  the envelope (exit code stays 2 via FlagError class).
- Unknown-subcommand guard installed recursively at the root:
  parents with subcommands but no Run/RunE get a typed RunE that
  emits input.unknown_subcommand envelope with
  detail.{unknown, command_path, available[]} and
  retry_command "<parent> --help". cobra.ArbitraryArgs bypasses
  legacyArgs validation so the guard receives unmatched argv.
- cmdutil.Error.Error() returns "<code>: <message>[: <cause>]" for
  chain debugging; ErrorToDetail strips the code prefix from
  envelope.message since the separate type field carries it
  (prevents the doubled-prefix "code: code: ..." that agents would
  see).

Spec: docs/superpowers/specs/2026-05-20-weknora-cli-v0.7-design.md §0 / §4
2026-05-27 10:56:34 +08:00
nullkey
7611d59d71 docs(cli): README / AGENTS.md / CHANGELOG + CI parity test
Wire-contract documentation and the CI check that keeps it honest.

* cli/README.md gains a verbatim --help block (top-level + subtrees),
  an Exit codes table covering 0/1/2/3/4/5/6/7/10/124/130, a "Status
  vs check" verb-pair subtable, and a "doc wait" paragraph spelling out
  the four exit codes (0 / 1 / 124 / 130). The api passthrough note
  trims storage provider out of the deep-config list now that
  kb create --storage-provider is a polished flag.
* cli/AGENTS.md becomes the contributor guide: build/test, CRUD flag
  conventions, the status/check verb pattern, long-poll wait commands,
  the SetAgentHelp pattern, and a full Error code reference with 35
  typed codes mapped to namespaces, exit codes, retryable / hint
  guidance. Reference section is bracketed by HTML markers so a CI
  parity test can keep it in sync with AllCodes().
* cli/internal/cmdutil/errors_doc_test.go enforces parity: every code
  in AllCodes() must appear in AGENTS.md inside the markers, and
  AGENTS.md must not reference codes that no longer exist. Fails CI
  if a new typed code is added without documentation.
* CHANGELOG.md gets the v0.6 entry: BREAKING (--json / --no-stream /
  WEKNORA_SDK_DEBUG / kb create --name), Added (--format / --jq /
  doc wait / --log-level / kb-and-agent status & check / multi-id
  delete / api --paginate / MCP schema extension / SetAgentHelp /
  signal-aware ctx / kb create --storage-provider / new operation.*
  namespace), Changed (multi-id partial-failure exit code, doc upload
  FlagError, --log-level FlagError, multi-id stdout cleanup, README /
  AGENTS.md changes), with a Migration from v0.5 section walking
  every BREAKING through its v0.6 replacement.
2026-05-18 11:10:19 +08:00
nullkey
34bb0b5096 feat(cli): doc delete multi-id + api --paginate + paginate fixes + batch deletes
Batch-write surface and pagination consistency:

* weknora doc delete <doc-id> [<doc-id>...] — positional multi-id, default
  keep-going on failure. Single -y confirms entire batch.
* weknora session delete <session-id> [<session-id>...] — same shape.
* weknora chunk delete <chunk-id> [<chunk-id>...] --doc <doc-id> — multi-id
  with shared --doc parent.
* Multi-id partial-failure rolls up as operation.failed (exit 1), not
  server.error (exit 7) — failures are operation outcomes, not transient
  transport issues, and the retry-with-backoff hint for server.* would
  mislead callers.
* weknora api <path> --paginate — auto-walks offset pagination and merges
  pages into a single {data, total} JSON response.
* Paginate truncation fix across 6 list/follower call sites.
* All doc / search / chunk / session / kb list commands migrated to
  FormatOptions API.

Multi-id RunE only emits the {ok, failed} envelope when the operation
actually ran — pre-flight failures (e.g. confirmation_required) leave
stdout empty per the wire contract.

doc upload's missing-positional-or-flag check is wrapped as FlagError so
the exit code (2) matches the convention used by other commands that
require a positional argument directly.
2026-05-18 11:10:19 +08:00
nullkey
0e081aec5c feat(cli): --log-level + kb/agent status & check + cross-cutting refactor
Operability surface and the bulk of the jopts→fopts migration:

* --log-level error|warn|info|debug + WEKNORA_LOG_LEVEL env, wired to
  the SDK via client.SetDebugLevel. Invalid --log-level returns FlagError
  (exit 2).
* kb status <kb-id> / kb check <kb-id> verb split (1 HTTP vs 1+N for
  failed_count aggregation).
* agent status <agent-id> / agent check <agent-id> verb split
  (probes kb_scope_all_reachable via 1+N HTTP).
* kb create <name> positional (matches agent create).
* Positional id help strings namespaced (<kb-id> / <agent-id>).
* All auth / context / link / doctor / kb / agent CRUD commands migrated
  to the FormatOptions API.
* root.go Execute(ctx) takes a context so signal-cancellation propagates
  via cmd.Context() into long-running commands.
* Pagination termination uses len(accum) >= total (not page*pageSize)
  so server-capped page sizes do not truncate aggregations.
2026-05-18 11:10:19 +08:00
nullkey
7eeb3bec5d feat(cli): doc wait command (multi-target wait-all)
weknora doc wait <doc-id> [<doc-id>...] blocks until every given document
reaches a terminal parse_status (completed / failed), --timeout expires,
or the user interrupts (SIGINT).

* --timeout DURATION (default 10m; exit 124 on timeout, matches GNU
  timeout(1) convention)
* --interval DURATION (default 2s; exponential backoff to 15s + jitter)
* Multi-id polled concurrently (max 5 parallel)
* Exit code priority 1 > 124 > 0 (failed > timeout > completed)

New typed errors:
* operation.timeout → exit 124
* operation.failed → exit 1
* operation.cancelled → exit 1 (main raises to 130 on signal)

server.session_create_failed gets a special case in ExitCode to map to
exit 1 (workflow failure, not transient retry).

doc view and doc download positional id namespaced to <doc-id>.
2026-05-18 11:10:19 +08:00
nullkey
567d7ac74e feat(cli): --format / NDJSON / chat & agent invoke / MCP / SetAgentHelp / signal-aware ctx / kb create --storage-provider
Adds the structured-output and agent-help surface plus root-level signal
handling so AI agents (and humans working through pipes) get a stable
wire contract.

* --format text|json|ndjson flag, registered per-command on outputs that
  need it; default text on TTY, json on pipe. --jq <expr> pairs with json
  / ndjson to filter or project. FormatOptions absorbs JQ; WantsJSON()
  helper for the JSON dispatch.
* WriteNDJSON helper in internal/format/ (per ndjson.org: one JSON value
  per line, arrays split element-per-line, empty slice → zero bytes).
* chat / agent invoke wire --format ndjson via SDK StreamResponse /
  AgentStreamResponse 1:1 passthrough. Both commands detect ctx.Cancelled
  in every stream + session-create path and emit a stable
  "operation.cancelled" code on Ctrl-C / SIGTERM.
* main.go wires signal.NotifyContext(SIGINT, SIGTERM) into the root
  context so long-running commands run their cancellation cleanup
  (re-emit auto-created session id, etc); the process exits 130 when
  the context was signal-cancelled, matching Unix convention.
* MCP chat / agent_invoke output schemas extended with thinking /
  tool_calls / assistant_message_id (server-side accumulated; MCP
  tools/call has no standard partial-response). doc_view and doc_download
  now use doc_id (not knowledge_id) so agents see a single id naming
  convention across all tools — matches the chunk_list / search_chunks
  schemas and the CLI's <doc-id> positional.
* SetAgentHelp(cmd, AgentHelp{...}) — opt-in machine-friendly --help
  payload activated by WEKNORA_AGENT_HELP=1. Applied to chat / kb list.
* kb create --storage-provider <local|minio|cos|tos|s3|oss|ks3> — sets
  the new KB's storage_provider_config.provider at creation time (server
  does not expose it on update). Required on self-hosted deployments
  where the server-side default doesn't pre-populate a provider —
  without it, subsequent doc upload returns a misleading "kb not found".
2026-05-18 11:10:19 +08:00
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
nullkey
f89d54362d feat(cli): doc/kb resource expansion — upload flags + list filters + view fields
Closes deep-tuning gaps in v0.4-shipped doc / kb / session commands.
Each command had multiple SDK fields the CLI silently hardcoded or
omitted; this commit threads them through.

doc upload:
- --enable-multimodel (tri-state via NoOptDefVal): toggle multimodal
  extraction (PDF/DOCX image-to-text) per upload. Empty explicit
  value (e.g. --enable-multimodel="" from uninterpolated shell var)
  rejected as input.invalid_argument rather than silently coerced.
- --metadata key=value (repeatable): attach arbitrary metadata
- --channel <name> (default api, override for browser / wechat ingests)
- URL mode gains --title / --file-type / --tag-id; URL-only flags
  rejected with input.invalid_argument when used without --from-url

doc list filter flags:
- --keyword (server-side LIKE — case-sensitive per PG)
- --file-type / --source / --tag-id
- --start-time / --end-time (RFC3339)

search docs switches from client-side substring to server-side Keyword
via ListKnowledgeWithFilter — smaller wire payload, but case-sensitivity
shifts (documented in CHANGELOG + help text + Long).

MCP doc_list schema gains the same 6 filter fields (parity with CLI).

session view --full + --limit: loads chat history via LoadMessages SDK
method.

kb view human KV expanded: type / pinned / temporary / processing
state + count / summary model / created timestamp. All omit-empty.

doc view human KV expanded: title (when distinct from filename) /
description / source / channel / summary_status / enable_status /
tag_id / storage (human bytes) / file_hash (12-char prefix).
2026-05-16 16:56:33 +08:00
nullkey
7bccd72ba3 feat(cli): search --all-pages canon catch-up + AGENTS.md SOP / CRUD canon
Brings search docs and search sessions to v0.4 pagination canon
(--limit / --page-size / --all-pages, matching session list / doc list).
Both default --all-pages=true to preserve prior silent walk-all
behavior; explicit knobs added for users who want one-page fetch.

cli/AGENTS.md gains two new sections:
- Command surface design SOP — a 5-step SDK-schema-first pre-design
  checklist for future contributors. Earlier spec drafts produced
  schema-error classes (missing/mismatched fields, missing pagination
  flags) when commands were designed from convention rather than from
  the SDK; the SOP makes the SDK the ground truth.
- CRUD command flag canon — Mode A (hard-required + flag error, no
  interactive prompts), the established pattern for non-auth CRUD.

Also fixes the agent invoke rationale source: the CLI-layer precedent
for invoke being a separate verb (not a chat mode) is documented
inline rather than referencing other vendor CLI behavior.
2026-05-16 16:56:33 +08:00
nullkey
26fa43e2cc fix(cli): post-audit fixes (MCP MatchCount + sessions + auth + view)
Four unrelated shipped-code drifts found during v0.5 audit cycles:

1. MCP search_chunks tool omitted MatchCount from SearchParams. Server
   fell back to its default cap; agents asking for limit:50 silently
   got fewer results. Adds MatchCount: limit to the struct literal.

2. search sessions printed UpdatedAt as raw RFC3339 while session list
   used a fuzzy "X hours ago" render — same SDK field, two human
   renderings. Switches to the shared text.FuzzyAgoStr helper for
   parity.

3. auth status --json omitted three operationally-meaningful AuthUser
   fields (username, is_active, can_access_all_tenants). Agents
   branching on can_access_all_tenants previously needed a second
   round-trip.

4. session view Long help claimed the SDK doesn't wrap session_messages;
   it does (LoadMessages / GetMessagesBefore / GetRecentMessages all
   exist in client/message.go). Rewrites the comment to be accurate.
2026-05-16 16:56:33 +08:00
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
nullkey
59132a56f6 feat(cli): agent CRUD + view full config rendering
Adds the three management verbs missing from v0.4's agent subtree
(create / edit / delete) and expands v0.4-shipped agent view to render
all 34 AgentConfig fields in human output (was 7).

Surface: hot-path flags (--model required + 7 optional) +
--config-file YAML/JSON tail + --generate-skeleton template emit.
Flag > file > server-default precedence for hybrid invocation.

- agent create <name> --model <id> [flags] + --from <agent-id> for
  copy-then-overlay (CopyAgent + UpdateAgent); preserves source
  config except for fields explicitly overridden
- agent edit <id> with --add-kb / --remove-kb idempotent pair,
  L-2 fetch-then-update, at-least-one-flag validation,
  --description "" clearing via Flags().Changed(). --config-file
  fully replaces the AgentConfig baseline (use surgical flags for
  partial edits; the Long help spells this out + a test pins the
  contract).
- agent delete <id> with ConfirmDestructive + exit-10 protocol;
  404 propagates resource.not_found (not idempotent)
- agent view: 10 grouped sections (Identity / LLM / KB attachment /
  Retrieval / Query rewrite / Tools / FAQ / Web search / Multi-turn /
  Fallback / Templates); --json field discovery includes all
  config.* keys

Shared helper cli/internal/cmdutil/agentconfig.go handles YAML/JSON
parsing, flag-overlay-file fusion, and skeleton emission.
2026-05-16 16:56:33 +08:00
nullkey
69fbbfc252 chore(cli): gofmt sweep
`gofmt -w cli/` — trailing newlines, minor whitespace alignment. Caught
during PR-readiness review (`gofmt -l .` had been non-clean since the
em-dash → ASCII bulk replace earlier in the v0.4 series).
2026-05-15 12:03:56 +08:00
nullkey
f2e8e3f56c refactor(cli): drop aiclient package; align AGENTS.md with mainstream
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
2026-05-15 12:03:56 +08:00
nullkey
a0dd989c81 refactor(cli): auth security audit — gh CLI parity hardening
Compared the auth subtree (login/logout/list/status/refresh/token)
against gh CLI's auth implementation. Three gaps closed:

1. `auth login --with-token` validates the API key against `/auth/me`
   before persisting (mirrors gh's pre-persist GetCurrentLogin probe).
   A typo'd / expired / wrong-host key fails fast with
   `auth.bad_credential` (exit 3) and nothing is written to the
   keyring. Side benefit: api-key contexts now carry the resolved
   `user` + `tenant_id` at rest, so `auth list` reflects who owns
   the key — previously these columns were blank for `--with-token`
   contexts because we never queried the server.

2. `auth login` prints a stderr advisory when the secrets store falls
   back to the 0600 plaintext file (keychain unavailable — typical on
   headless CI, WSL without DBus, agent containers). `weknora doctor`
   carried the same info in its credential_storage check, but users
   who go straight to `auth login` could miss it. gh has the same
   silent-fallback gap; we're stricter here.

3. AGENTS.md adds an "Auth security contract" section documenting:
   - Credential storage (keychain primary, 0600 file fallback)
   - `--with-token` reads stdin (not flag value), pre-validated
   - No env-var token bypass — by design, to avoid the
     `/proc/<pid>/environ` / `ps -E` leak surface that
     `GH_TOKEN`-style env vars expose
   - `auth status` / `auth list` never emit token values
   - `auth refresh --json` returns only `{context}` (never the
     new tokens)
   - `auth token` stdout has no trailing newline + TTY stderr hint
   - `auth logout` is local-only (no server-side revocation)

Verified against gh CLI behavior (cli.github.com manual + cli/cli
trunk source):

| dimension                       | gh             | weknora v0.4 |
|---------------------------------|----------------|--------------|
| pre-persist token validation    | ✓              | ✓ (new)      |
| OS keychain primary             | go-keyring     | go-keyring   |
| stderr warning on file fallback | ✗ silent       | ✓ (new)      |
| `auth status` default token     | masked prefix  | not shown    |
| `auth token` TTY warning        | ✗              | ✓            |
| env-var token bypass            | ✓ (GH_TOKEN)   | ✗ by design  |
| process-args / `ps` leak surface| ✗ stdin only   | ✗ stdin only |
2026-05-15 12:03:56 +08:00
nullkey
e623e8208f refactor(cli): delete envelope infrastructure, errors to stderr
Removes the entire envelope machinery now that every success path
emits bare JSON:

- cli/internal/format/envelope.go (Envelope, Success, Failure,
  SuccessWithRisk, WriteEnvelope, Meta, Notice, UpdateNotice,
  VersionSkewNotice, Risk, RiskLevel, ErrorBody) + tests.
- cli/internal/format/filter.go envelope-specific helpers
  (WriteEnvelopeFiltered, marshalEnvelope, applyFieldFilter,
  filterDataPayload, filterObjectData); the reusable
  filterArrayItems / filterObjectKeys / writeJQ stay for bare.go.
- cli/internal/cmdutil/exporter.go + tests (envelope-only).
- cli/internal/cmdutil/PrintErrorEnvelope + ToErrorBody +
  operationRiskOf + Error.OperationRisk field + OperationRisk struct.

Error path: all errors now go to stderr via cmdutil.PrintError in
`code: message\nhint: ...` form, regardless of --json. Stdout stays
empty (or holds the partial-success the command already wrote) so
downstream `--json | jq` pipelines never have to filter error shapes
out of the success stream. Typed exit codes (3 auth.* / 4
resource.not_found / 5 input.* / 6 server.rate_limited / 7 server.*
+ network.* / 10 input.confirmation_required) carry the failure
class for agents that branch on it.

Acceptance contract:
- envelope_test.go → wire_test.go (TestEnvelopeGolden → TestWireGolden).
- testdata/envelopes/ → testdata/wire/.
- Error-path cases assert the typed code substring on stderr.
- Orphan whoami.*.json goldens deleted.

AGENTS.md + README.md rewritten for the bare-data contract:
- Drop envelope schema section + dry-run rule.
- Document bare JSON on stdout + `code: msg\nhint: …` on stderr.
- ADR-3 reframed around bare data and why error separation matters
  for `--json | jq` pipelines.

WriteJSONFiltered short-circuits to WriteJSON when both filters are
empty (skip the marshal-buffer round-trip for the common case).

Final review pass:
- Fix wire-contract bug: `--json id,name` (space form) is broken by
  pflag's NoOptDefVal; AGENTS.md / README.md / SetAgentHelp + the
  field-discovery help text all switched to `--json=id,name`.
- Fix `weknora api --jq` silently ignored: api.go now routes through
  WriteJSONFiltered with jopts.JQ.
- AGENTS.md: drop the false claim that `auth logout` honors `-y`
  (logout is local-only with no ConfirmDestructive guard); list the
  actual destructive commands instead.
- Rewrite cli/acceptance/e2e/e2e_test.go for the bare-data wire shape
  (was still parsing `out["data"]` / `env["ok"]`).
- Add `JSONOptions.Emit(w, v)` helper; collapse ~33 repeated
  `format.WriteJSONFiltered(iostreams.IO.Out, X, jopts.Fields,
  jopts.JQ)` sites to `jopts.Emit(iostreams.IO.Out, X)` — drops the
  format import from 22 cmd/* files.
- Delete single-caller `cmdutil.MustRequireFlag`; inline as
  `_ = cmd.MarkFlagRequired(...)` everywhere.
- Add `_ = cmd.MarkFlagRequired("name")` to `kb create`; it was the
  only write command relying on runtime --name validation while
  `context add` already used the cobra-level mark.
- `context use`: register `--json` / `--jq` (was always emitting JSON
  unconditionally with no human path and no flag — diverged from
  every other write command); human mode now prints
  `✓ Switched context to X (was Y)`.
- Replace per-package `confirmPrompter` / `scriptedConfirm` /
  `errPrompter` test doubles with `testutil.ConfirmPrompter`.
- Rename `chatService` → `ChatService` (export to match siblings
  `ListService` / `ViewService`); rename `printUploadSuccess` →
  `renderUploadSuccess` (siblings use `render*`).
- `defaultHint(CodeResourceNotFound)`: drop the hardcoded
  "list available with `weknora kb list`" — misleading on agent /
  doc / session 404. Replaced with "verify the resource ID and try
  again".
- Strip stale `v0.2/v0.3` / "envelope" / "v0.0/v0.1 supports only"
  historical tags from production comments and a few test
  descriptions.
2026-05-15 12:03:56 +08:00
nullkey
cc8254f862 refactor(cli): drop --dry-run + introduce bare-JSON output path
Two intertwined mainstream-alignment moves bundled because they share
the migration target (every command's --json path):

1. Drop --dry-run entirely. Survey of comparable API-wrapper CLIs
   (gh, aws, stripe, lark): none expose --dry-run. The mainstream that
   does (kubectl/git/helm/ansible) operates on declarative manifests
   or local state where the preview is materially different from the
   executed action. WeKnora's CLI just echoed the same parameters
   that would have gone on the wire — the preview added no real
   signal over `--help` + reading the call site. Removes:
   - root --dry-run persistent flag + cmdutil/dryrun.go
   - DryRun fields + EmitDryRun calls in 12 write commands
   - format.Envelope.DryRun field
   - 8 corresponding *_test.go cases
   - --dry-run mention from README.md and CHANGELOG.md
   - "dry_run":false from 16 golden envelopes

2. Migrate every --json output to bare data:
   - New format.WriteJSON / WriteJSONFiltered helpers
     (cli/internal/format/bare.go) share filterArrayItems /
     filterObjectKeys / writeJQ with the (still-live for now) envelope
     filter helpers.
   - Read commands (kb/doc/session list+view, search chunks/docs/
     sessions/kb, auth list/status, agent list/view, context list,
     doctor) emit bare arrays / objects on stdout.
   - Write commands (kb create/edit/delete/pin/empty, doc upload/
     upload_recursive/delete, session delete, auth login/logout/
     refresh/token, link/unlink, context add/use/remove, agent
     invoke, chat, api, version) emit bare result objects. Risk
     classification dropped — the resource + exit code already
     convey the action.

Per-command shape changes:
   list / search       → []T   (was {ok, data:{items:[…]}})
   view                → T     (was {ok, data:T, _meta:…})
   create / edit       → T
   delete / pin / etc. → {id, …action result…}
   doctor              → {summary, checks}
   api                 → {status, headers, body}

_meta dropped on the read path:
   pagination (page/page_size/total/has_more) — agents iterate with
   --all-pages or accept --limit (gh CLI parity);
   kb_id / context echo — caller already knows what it asked for.

Acceptance contract goldens regenerated for the new bare shape.
Error envelope on stdout (PrintErrorEnvelope) stays live for now —
the envelope-infra deletion lands in the next commit.
2026-05-15 12:03:56 +08:00
nullkey
bdc589e1c0 refactor(cli): --limit/--all-pages, Go 1.26, internal/agent → aiclient
Cross-cutting cleanup that lands alongside the new feature surface:

- `--limit / -L` and `--all-pages` on every list command. Default
  --limit 30 (gh-parity); --all-pages drains every server page
  client-side, capped by --limit. Closes the audit finding that the
  old "1000 max per call" implicit cap was undiscoverable.
- `auth token` emits a TTY-only stderr advisory when stdout is a
  terminal (the credential just got displayed in scrollback) plus an
  api-key-mode rotation hint.
- Comment + doc discipline pass: drop external project name
  references from in-code comments (we reference them in design notes,
  not inline).
- Bump `go` directive to 1.26.0 and CI matrix to 1.26.x to align with
  the main module's go.mod.
- Rename `cli/internal/agent` → `cli/internal/aiclient` to
  disambiguate from the new `cli/cmd/agent` resource subtree. The
  package handles AI coding-agent env detection + per-command --help
  annotations; the new name reflects that more precisely.
2026-05-15 12:03:56 +08:00
nullkey
9bb83b47fd feat(cli): mcp serve curated stdio MCP server
`weknora mcp serve` — long-lived stdio MCP (Model Context Protocol)
transport that exposes a fixed, curated tool surface to MCP-aware
agents (Claude Desktop, Claude Code, custom MCP clients).

Curated tool set (readonly by default):
- whoami — active context + tenant
- search (hybrid retrieval against a KB)
- kb list / view
- doc list / view
- agent list / view / invoke
- session list / view

The list is intentionally narrow to the read + agent-invoke surface;
destructive verbs (`delete` / `empty` / `upload`) are gated behind
`--write`. Schema is built from each leaf cobra command's flags so
adding a new tool is a single registry entry plus a Service interface.

Includes the simplify post-review polish + a second simplify pass to
fold the resulting feedback (typed schemas, agent_help wording, unify
chat / agent invoke option names).
2026-05-15 12:03:56 +08:00
nullkey
493fc41e98 feat(cli): agent subtree (list/view/invoke)
Manages WeKnora's first-class Custom Agent resources — server-side
records (system prompt + model + allowed tools + KB scope) that the
user authored in the web UI.

Commands:
- `weknora agent list` — tenant-visible agents (built-in + custom),
  sorted updated_at desc; `--limit`/`-L` caps the slice client-side.
- `weknora agent view <id>` — full sdk.Agent including nested
  AgentConfig (mode / model / allowed_tools / KB scope). Human mode
  prints a compact KV layout + Config: block.
- `weknora agent invoke <agent-id> "<text>"` — streams the agent's
  configured workflow against a query over SSE. Auto-creates a fresh
  session unless `--session` is passed. Streaming defaults to TTY +
  no-stream/no-json; agent-friendly buffered single-object output
  with `--json` (or `--no-stream`).

Decoupled from the existing `chat` subtree: agents bring their own
system prompt / tool surface / KB selection, so the chat / agent split
matches the server-side resource boundary.
2026-05-15 12:03:56 +08:00
nullkey
3b67986863 feat(cli): per-resource filter flags on list commands
Adds the filter flags users were reaching for via `--jq` post-filter:

- `kb list --pinned` — client-side filter to KBs with `IsPinned`.
- `doc list --status <pending|processing|completed|failed>` —
  server-side query-param filter; `failed` surfaces ingestion errors
  immediately for triage.
- `session list --since <duration>` — client-side filter to sessions
  updated within the past duration. Accepts time.ParseDuration forms
  (24h, 1h30m, 30m) plus a `<N>d` suffix for whole days (7d, 0.5d).

Server-side filters are forwarded as query params (where the API
supports them) to avoid pulling the full list into memory; client-side
filters apply after the fetch so they compose with `--limit`.
2026-05-15 12:03:56 +08:00
nullkey
1b20b06f5e feat(cli): --json field-select, --jq, auth token, doc --from-url
Output ergonomics:
- `--json` accepts a comma-separated field list (gh-parity); selects
  named keys from the per-command payload. Bare `--json` keeps the
  full shape.
- `--jq <expr>` evaluates a gojq expression over the JSON; pairs with
  `--json field-list` so projection runs before jq.
- `--version` is a global cobra flag in addition to the `version`
  subcommand; both render the same line.
- Per-command `--help` now renders the available JSON field list under
  "JSON fields available via `--json id,name,...`" (field-discovery
  parity with gh / kubectl `-o jsonpath`).

New commands:
- `auth token` — print the active context's credential to stdout for
  shell command substitution (`WEKNORA_TOKEN=$(weknora auth token)`).
  Default: raw secret, no trailing newline. `--json` emits
  `{token, mode, context}`.
- `doc upload --from-url <URL>` — ingest a remote URL via the SDK
  `CreateKnowledgeFromURL`. `--name` forwarded as `FileName` so the
  server's known-extension heuristic upgrades crawl-mode to
  file-download-mode where appropriate.

Includes the simplify post-review polish pass (field-filter unit
tests, --json/--jq compose check, agent_help copy fixes).
2026-05-15 12:03:56 +08:00
nullkey
35c79281c8 feat(cli): doc view + unlink (fill v0.3 design-gap audit)
Final design-pass audit on the v0.3 surface flagged two real gaps.

(A) doc view <id> was missing. Every other resource subtree exposes
a view verb (kb view, session view) for inspecting a single record,
but doc — which has the richest metadata of the three (title, file
name, type, size, parse_status, embedding_model, processed_at,
error_message) — had no single-doc surface. Users wanting one
doc's metadata had to `doc list | grep`.

Implementation mirrors kb view: narrow ViewService(GetKnowledge)
interface, --json envelope path, human KEY: VALUE renderer. Optional
fields are omitted rather than rendered as "-" so the panel is
dense. Tested: human renderer, title fallback when FileName empty,
omit-empty contract, JSON envelope shape, 404 classification.

(B) link had no counterpart. Once .weknora/project.yaml is written,
the only way to clear it was `rm` by hand. Both vercel and netlify
ship `unlink` as a top-level verb; not having one was a
discoverability gap. Top-level rather than `link --clear` follows
the verb-noun convention of the rest of the surface — the verb
stands alone and the operation isn't parameterised.

unlink walks up from cwd via projectlink.Discover (the same
parent-chain logic Factory.ResolveKB uses on the read side), so a
user in a subdirectory of a linked project can unlink without
cd-ing up. Errors with input.invalid_argument when no link is
found anywhere in the chain. Idempotent under racy concurrent
removal: os.ErrNotExist on os.Remove falls through to a Success
envelope since the post-condition holds either way.

projectlink package gained Remove() alongside Save / Load /
Discover so unlink doesn't reimplement the idempotent-remove
pattern inline.

Top-level registration in cmd/root.go, alongside link.
cli/AGENTS.md verb canon line adds unlink to the locally-introduced
list. cli/CHANGELOG.md gains an Added entry for each.

5 unit tests for view + 4 for unlink (cwd / walk-up / no-link
error / JSON envelope). Full suite green.

Intentionally deferred:
- session edit (rename a session): sessions auto-name from the
  first prompt; polish rather than a gap.
- link --clear as an alternative to unlink: top-level unlink is
  the documented form; aliases would just multiply the surface.
2026-05-14 10:57:17 +08:00
nullkey
4a5449233d fix(cli): plug v0.3 final review findings (json + auth + path + bounds + kb)
Seven bugs surfaced via two audit rounds — parallel reviewer agents
plus a real-server end-to-end demo. Each fix arrives with a
regression test.

1. doc upload --recursive --json corrupted the envelope stream.
   Per-file FAIL/OK plain lines printed unconditionally to stdout,
   then a Success envelope, then on partial failure a typed error
   that the root handler turned into a SECOND Failure envelope —
   three outputs where one was expected. Fix: gate the plain lines
   behind !opts.JSONOut, and add cmdutil.Error.Silent so the JSON-
   path partial-failure preserves its typed exit code without
   triggering PrintErrorEnvelope's default Failure-envelope write.

2. auth refresh / AuthRetryTransport misclassified HTTP failures as
   network.error. RefreshAndPersist wrapped every refresher error
   with CodeNetworkError, but the SDK emits "HTTP error 401: ..."
   for a rejected refresh token — which should surface as
   auth.token_expired. Switched to WrapHTTP for proper status-
   derived classification. Affects both `auth refresh` and the
   transport's refresh closure.

3. doc download accepted ".." as a server-suggested filename. The
   rejection list covered "" / "." / filepath.Separator but not
   bare ".." — filepath.Base("..") is "..", which slipped through
   to os.Create and produced a confusing local.file_io wrap. Added
   to the rejection set.

4. search chunks / docs / kb / sessions had no lower bound on
   --limit. `-L 0` / `-L -1` was forwarded to the server with
   undefined behavior. Added a 1..1000 bound at the RunE boundary
   across all four (matching doc list / session list page-size
   bounds). Internal callers in tests can still pass Limit==0 for
   the "no client-side cap" runChunks path — the bound only applies
   at the user-input layer.

5. cli/AGENTS.md ADR-3 verb-canon summary listed only v0.2 verbs as
   "gh-canonical" and missed v0.3 additions (edit, pin, unpin,
   download — all gh-canonical) plus locally-introduced ones
   (empty, refresh, add, remove, link). Rewritten as an explicit
   gh-canonical / locally-introduced split.

6. kb pin returned 404. Server registers /knowledge-bases/{id}/pin
   as PUT (router.go:292); SDK was using POST. gin's router silently
   404s on method-mismatch (treats it as path-not-found, not 405),
   so the CLI classified the response as resource.not_found and
   masked the real failure mode. Switched the SDK to http.MethodPut.

   The asymmetry that hid this past round 1: kb unpin on a freshly-
   created KB hits the no-op branch in cmd/kb/pin.go that skips the
   SDK call entirely, so unpin "worked" without ever exercising the
   broken path. Only the real-server demo, where kb pin actually
   fires, surfaced it.

7. kb edit clobbered current Name when only --description was
   passed. EditOptions used *string to distinguish "unset" from
   "set to empty", but sdk.UpdateKnowledgeBaseRequest declares both
   fields as plain string (no omitempty), so the JSON body always
   carried `"name": ""`. Server requires Name → 400. Fix: runEdit
   does fetch-then-update — GetKnowledgeBase first, build the PUT
   body with current values, then overlay user-set fields. Same
   TOCTOU window as kb pin / unpin.

Audit-flagged items intentionally NOT changed:
- kb pin / unpin check-then-toggle TOCTOU: documented; the clean
  fix would be a server-side setter and belongs in a separate API
  change.
- AuthRetryTransport singleflight test gap for one concurrency
  scenario; v0.4 polish.
- cli/README.md:50 "once v0.2 ships" and CHANGELOG.md:8
  "10 top-level commands": v0.2-PR artifacts, not v0.3-introduced.
- kb edit / kb pin are v0.3-new commands, so neither bug needs a
  cli/CHANGELOG.md Fixed entry — the v0.3 release ships them
  working as the Added bullets advertise.
2026-05-14 10:57:17 +08:00
nullkey
13cce78332 fix(cli): drop link --context flag (shadowed global --context)
The `link` subcommand declared a local `--context` StringVar that
shadowed the root-level persistent `--context` flag at the cobra layer.
Two different semantics under one name:

  - root global `--context <name>`: "override the active context for
    THIS invocation only, no disk write" (single-shot connection
    override, applied via Factory.ContextOverride).
  - link local `--context <name>`: "the context name to record in
    .weknora/project.yaml" (persisted state, written to disk).

The shadow meant `weknora --context staging link` (intent: link runs
against staging) silently did NOT propagate the override into link's
runtime; instead link's local "" beat the global. `weknora link
--context staging` (intent: record staging in the file) did work, but
shared a name with the unrelated global behavior, which is a usability
trap.

Resolution: drop the local flag entirely. The active context at link
time is what gets recorded; users who want to bind under a different
context use the global `--context X link --kb my-kb` form, which now
propagates correctly (no local shadow). This matches the bind-command
patterns surveyed across mainstream CLIs:

  - lark-cli `config bind` — uses domain-specific flags (--source /
    --app-id / --identity); the global --profile is named distinctly.
  - gh `repo set-default` — uses a positional for the bind target;
    the global -R/--repo is the only flag-form path.
  - netlify `link` — uses --id/--name for the bind target; no
    --site global flag at all (env var only).
  - vercel `link` — reuses --project for both global and link, but
    only works because vercel ships a custom parser that merges
    flag/env/file precedence; cobra's persistent-flag shadowing is
    silent-override, not graceful merge.

The lark-cli / gh / netlify pattern of "bind command's target flag
must not share a name with the global override flag" is the cobra-
friendly choice; dropping the flag is the simplest form of that.

No behavior change for the common path (`weknora link --kb my-kb`
without --context still records the active context). The "record
under a specific non-current context" use case is now expressed via
the global flag, which is what it was designed for.

link_test.go untouched (no test referenced the dropped flag).
2026-05-14 10:57:17 +08:00
nullkey
c9b837dfce docs(cli): sync README + AGENTS.md, add cli/CHANGELOG.md, clear stale e2e refs
v0.3 feature commits didn't update the docs alongside; this commit
syncs them and introduces a CLI-local changelog so v0.3+ release
notes stop crowding the project root file.

cli/CHANGELOG.md (new):
- Subsystem-local pattern, mirroring mcp-server/CHANGELOG.md. CLI
  versions independently from server / frontend cadence; reduces
  merge-conflict surface on the shared root file.
- Scope: Added + SDK additions only. v0.3-internal dev churn
  (--top-k → --limit, kb clear-contents → kb empty, link --context
  introduce-then-drop, internal Go type-name leaks) never reached a
  shipped release so it doesn't belong in Changed / Fixed sections.
  mcp-server's v1.0.0 changelog is Added-only for the same reason.
- v0.0–v0.2 history stays in the project root CHANGELOG.md;
  cross-referenced from the top of cli/CHANGELOG.md.

Stale --help / quickstart examples fixed in cli/cmd/root.go,
cli/README.md, and cli/AGENTS.md — all three showed the dropped bare
`weknora search "<q>" --kb=...` form; updated to `search chunks ...`.

AGENTS.md updates:
- Verb canon table gained edit / empty / download / pin / unpin /
  add / remove.
- `auth` subtree description gained `refresh` and the transparent
  401-retry transport (replacing the now-inverted "deferred to v0.3"
  sentence).
- `search` and `session` subtree paragraphs added; top-level
  verb list gained `context` and `session`.

cli/README.md top-level command list gained `session`; `search`
short retitled to the parent description ("Search across chunks,
knowledge bases, documents, or sessions") since search is now a
pure dispatcher.

Pre-existing stale e2e refs swept up while syncing:
- cli/acceptance/doc.go listed e2e/ under "Future v0.2+:" — moved
  into the present-tense Sub-packages block.
- envelope_test.go preamble "Deferred to v0.2 e2e" rephrased to
  "Deferred to the e2e harness" so it isn't pinned to a past version.

Not changed (out of scope, flagged for future PRs):
- envelope_test.go "Implemented count: 16" vs the actual 14 named
  entries — could be a different counting rule; verify with PR-8
  author before editing.
- envelope_test.go context_use deferred-cases narrative is loose
  (context_use.success IS golden-pinned today) but rewriting needs
  careful re-derivation of which error scenarios are still deferred.
- cli/README.md:50 "once v0.2 ships" — v0.2-PR-original wording;
  not load-bearing once a release tag exists.

No project-root CHANGELOG.md change in this commit.
2026-05-14 10:57:17 +08:00
nullkey
5adcedf170 refactor(cli): v0.3 cross-cutting cleanup
Cross-cutting findings surfaced by the branch-completion review.

Perf bug:
- Factory.Client closure was not memoized. Factory.ResolveKB internally
  calls f.Client() to resolve --kb name → id, then the command's RunE
  calls f.Client() again. Two SDK clients, two keyring reads, two
  AuthRetryTransport allocations per name-resolved invocation, with
  *independent* token state (a refresh in one was invisible to the
  other). Switched to sync.Once like Secrets already does.

Silent bug bait:
- cmdutil.NormalizeHost docstring claimed CodeInputMissingFlag for the
  empty case; code returned CodeInputInvalidArgument. Aligned doc to
  code (present-but-empty is a bad value, not a missing flag).

Agent contract gaps:
- Five user-facing subcommands lacked SetAgentHelp: auth login /
  logout / list / status and chat. Added concise strings with error-
  code call-outs so agents can branch without parsing human strings.

Helper extraction (≥3 callers):
- text.KnowledgeDisplayName(fileName, title, id) — byte-identical
  formatter that was in both cmd/doc/list.go and cmd/search/docs.go.
  Takes fields directly so internal/text stays SDK-free.
- cmdutil.WrapHTTP(cause, fmt, args...) *Error — replaces the
  `Wrapf(ClassifyHTTPError(err), err, ...)` pattern across 24 SDK
  call sites. Sed-driven migration; off-pattern shapes in chat.go
  (used streamErr) and cmdutil/kb.go (in-package) hand-edited.
  Contract test gains a comment update: post-migration the dominant
  pattern is WrapHTTP which the AST scanner skips entirely (only
  NewError/Wrapf selectors inspected); ClassifyHTTPErrorOutputs()
  bridge still covers the dynamic codes those paths can yield.

UX consistency:
- cmd/doc/list.go --page-size help now reads "Items per page
  (1..1000)" matching cmd/session/list.go. The bounds validation
  already enforced 1..1000; the help text was the last drift.

Comment-discipline sweep:
- Deleted the WHAT-only "*Options captures `weknora ...` flag state"
  docstring across 23 files (context, kb, auth, doc, session, search,
  chat, doctor, link). Where the line carried a real WHY clause
  (kb/delete, doc/delete, session/delete, kb/edit), kept the WHY and
  dropped only the leading WHAT phrase.
- Stripped third-party project-name attribution from inline comments
  and one user-visible flag-help string across ~40 files in cli/cmd
  and cli/internal (plus 4 test-file comments). Removed phrases like
  "Mirrors `gh X`", "borrowed from lark-cli", "kubectl-style",
  "gcloud `--project`", "Stripe pattern", and the embedded GitHub
  URLs pointing at those projects. Behavioral descriptions and the
  WHY behind each comment are preserved; only the upstream-name
  attribution is gone. Inspiration / north-star references belong in
  cli/AGENTS.md (the design doc) and commit messages, not scattered
  through every file.

  Triggered by an audit round that surfaced several false / fragile
  parity claims (e.g. "Mirrors `gh repo edit`" — gh repo edit has no
  --name flag; "matches gcloud `--project` id-or-name" — gcloud's
  --project accepts ID only). Rather than fix them one by one, the
  whole category of in-comment external-project references was
  stripped uniformly.
2026-05-14 10:57:17 +08:00
nullkey
73a88b4f0a feat(cli): api --input + completion smoke
api (3-11):
- `--input <file>` reads the request body from disk; `--input -` reads
  from stdin. Matches gh CLI canonical naming verified against the gh
  manual ("The file to use as body for the HTTP request — use \"-\" to
  read from standard input"). `--data` / `--input` are mutually
  exclusive.
- Options.StdinReader (defaults to iostreams.IO.In) for test injection.

completion (3-13 smoke only — release-artifact ship deferred to release
milestone):
- Smoke test asserts cobra's auto-registered bash/zsh/fish/powershell
  scripts produce non-trivially-sized output with the per-shell
  signature (#compdef / complete -c weknora / etc.). Guards against
  cobra bumps silently breaking completion for one shell.

3-14 doctor --no-cache: already implemented (factory.go:297) with
TestDoctor_NoCache_BypassesCache covering it — verified, no change
needed.

Roadmap: 3-11, 3-13 (smoke), 3-14 (verified).
2026-05-14 10:57:17 +08:00
nullkey
d54a7a5834 feat(cli): search verb-noun subtree (chunks/kb/docs/sessions)
Roadmap 3-1. Verb-noun shape borrowed from gh search (gh search repos
/ code / commits / issues / prs verified against the gh manual).

Subcommands:
- `search chunks "<q>" --kb X` — hybrid retrieval (RAG search).
- `search kb "<q>"` — case-insensitive substring match across KB names
  and descriptions; sorted by name length (shortest hits first).
- `search docs "<q>" --kb X` — pages through ListKnowledge filtering by
  title / file_name; stops once --limit matches are found.
- `search sessions "<q>"` — pages through GetSessionsByTenant filtering
  by title / description.

kb / docs / sessions are client-side filters because the server has no
fuzzy search endpoint for any of them. ListKnowledgeBases returns the
full tenant catalog in one call; the doc/session walkers chunk at 200
per request and stop early on limit.

The parent `search` command is a pure dispatcher — there is no bare-
positional form (no `weknora search "<q>"`).

Cleanups surfaced by the post-commit reviewer round:
- UX consistency: search docs's displayDocName ordered Title →
  FileName → "-", while doc list's displayName uses FileName → Title
  → ID. Same Knowledge rendered differently across commands. Aligned
  search docs on doc list's existing FileName-first convention.
- cmdutil.ResolveKBFlag(ctx, lister, raw) — extracted the
  `IsKBID ? raw : ResolveKBNameToID` block duplicated across chunks
  and docs.
- text.ContainsFold(needle, fields...) — replaces inline
  `strings.Contains(strings.ToLower(field), needle)` patterns.

37 unit tests across chunks/kb/docs/sessions plus the parent
registration smoke-test.

Roadmap: 3-1.
2026-05-14 10:57:17 +08:00
nullkey
78f3994112 feat(cli): doc download + upload --recursive
Roadmap items 3-9 (download) and 3-10 (recursive upload).

SDK addition (additive, non-breaking):
- OpenKnowledgeFile(ctx, id) (filename, body io.ReadCloser, err) —
  the new primitive that returns the body as a stream plus the
  server-suggested Content-Disposition filename. The existing path-
  form DownloadKnowledgeFile is now a thin wrapper (also gained
  partial-file-on-error cleanup, a pre-existing bug exposed by the
  reshape).

doc download <id>:
Borrows shape from `gh release download` (positional id, output flag,
`-` sentinel for stdout). Flag names match gh canon verified against
the gh manual: `-O, --output <file>` for destination; `--clobber` for
overwrite control.

- Default: writes to cwd under the server-suggested filename. If the
  server didn't send one, errors with input.missing_flag.
- --output FILE / -O FILE: writes to FILE. Refuses overwrite without
  --clobber.
- --output -: stream to stdout (binary-safe).
- Partial writes on error are cleaned up.

doc upload --recursive <dir> --glob '*.pdf':
NOTE on upstream parity: `gh release upload` does NOT support
--recursive (verified — it takes individual file args only). `aws s3
cp --recursive` does, but uses `--include`/`--exclude` glob pattern
pairs rather than a single `--glob`. weknora's single positive `--glob`
is a deliberate simplification, not a direct mirror of either tool.

- Walks the tree, filters by base-name glob, uploads each match
  sequentially. Per-file line output: OK / FAIL with the underlying
  error. Exit 0 only on full success; on partial failure returns the
  first failure's typed code so callers can branch. Rejects --name
  with --recursive.
- --dry-run lists matches without uploading.
- --json emits {kb_id, uploaded[], failed[]} envelope at completion.

Bugs caught in the post-commit reviewer round:
- SECURITY: server-supplied filename was used in os.Rename without
  sanitization. A malicious / buggy server returning
  "../../etc/shadow" could escape cwd. Now filepath.Base'd; "." / "/"
  / "" rejected. Regression test added.
- Wasted-bytes path eliminated via the SDK reshape: the CLI now
  inspects filename and applies refuseIfExists BEFORE streaming.
  Two-phase temp+rename gone.
- refuseIfExists(path, clobber) helper extracted.
- --json honored in --recursive (uploadOutcome was JSON-tagged but
  the envelope was never emitted).

7 + 7 unit tests for download (+ path-traversal regression) and
recursive upload (+ JSON envelope regression).

Roadmap: 3-9, 3-10.
2026-05-14 10:57:17 +08:00
nullkey
2f8681b48e feat(cli): session subtree + kb edit / pin / empty
Roadmap items 3-5 (session) and 3-6/7/8 (kb manage).

cli/cmd/session/ (new package; sessioncmd to avoid shadowing stdlib):
- session list: paginated table (ID/TITLE/UPDATED). --page / --page-size
  with 1..1000 validation. _meta.has_more from page*size < total.
- session view <id>: prints metadata; non-empty fields only. Server
  timestamps arrive as strings; parsed best-effort as RFC3339.
- session delete <id>: high-risk-write; exit-10 confirmation in non-
  TTY/--json paths; --dry-run emits envelope.risk + dry_run:true.

cli/cmd/kb (extended):
- kb edit <id> [--name N] [--description D]: at least one flag required;
  *string options so unset fields stay unset in the PUT body. SDK
  UpdateKnowledgeBaseRequest has no embedding_model field, so the
  roadmap's --embedding-model dropped.
- kb pin <id> / kb unpin <id>: direct parity with gh issue pin /
  gh issue unpin (verified against gh manual). Idempotent: GetKnowledgeBase
  reads IsPinned, TogglePinKnowledgeBase fires only on state change.
  SDK KnowledgeBase struct gained the IsPinned field (server already
  returned it; SDK just hadn't modeled it — non-breaking additive).
- kb empty <id>: high-risk-write; exit-10 confirmation;
  --dry-run. Returns deleted_count from the async clear response.
  weknora-specific operation; no mainstream parallel.

Golden envelopes for kb_list and kb_view updated to include the new
is_pinned field — strict-additive change.

Cleanups surfaced by the post-commit reviewer round:
- ConfirmPrompter promoted to cli/internal/testutil/ (4-copy threshold
  reached: context/remove, kb/delete, kb/empty, session/delete).
  kb/delete_test.go's pre-existing local copy left untouched per the
  upstream-respect convention.
- kb pin/unpin idempotent no-op path no longer emits a write-class
  envelope. Added _meta.warnings "already {un}pinned — no server
  call made" and dropped the risk classification on the no-op branch.
- doc list --page-size was unbounded while session list enforces
  1..1000. Same validation added to doc list.

18 + 18 unit tests; e2e exit codes verified.

Roadmap: 3-5, 3-6, 3-7, 3-8.
2026-05-14 10:57:17 +08:00
nullkey
4c26bc9ecc feat(cli): auth refresh + transparent 401 retry transport
Two halves of v0.3 roadmap item 3-2.

(1) `weknora auth refresh` — explicit token renewal:
Reads the stored refresh_token, spends it via POST /api/v1/auth/refresh
(OAuth refresh-token grant), and persists both new tokens. API-key
contexts rejected with input.invalid_argument (no refresh semantic).

NOTE: gh CLI has `gh auth refresh` but with different semantics —
gh's variant is an OAuth scope expansion / re-prompt via the browser
(verified against the gh manual). The two share a name but solve
different problems; there's no direct gh parallel for refresh-token
grant because gh's PAT/OAuth-app model doesn't expose a short-lived
access_token + refresh_token pair to clients.

Error mapping:
- no current context → auth.unauthenticated
- --name unknown → local.context_not_found
- missing refresh in keyring → auth.token_expired (hint: re-login)
- server Success=false → auth.token_expired
- network → network.error
Envelope omits the token values (would leak into agent transcripts).

(2) AuthRetryTransport — transparent retry:
Wraps the SDK http.Client. On a 401 from a non-/auth/* endpoint:
- JWT context: read refresh token, hit /auth/refresh, persist new pair,
  replay original request with new bearer.
- API-key context: pass through (no refresh semantic).
- Non-replayable body (req.GetBody == nil): pass through.
- /auth/login or /auth/refresh: pass through (no recursion).
Concurrent 401s are singleflight-coalesced via sync.Mutex — 5 parallel
calls trigger exactly 1 refresh.

SDK additions (additive, non-breaking):
- WithTransport(rt http.RoundTripper) ClientOption.
- PathAuthLogin / PathAuthRefresh constants (cli/internal/cmdutil/authretry
  imports them so the CLI and SDK can't drift on path strings).

Refactor surfaced by the post-commit reviewer round:
- cmdutil.RefreshAndPersist(ctx, store, refresher, ctxName) — the
  load-refresh → call-SDK → persist-pair sequence was duplicated between
  the standalone `auth refresh` and the transport's refresh closure;
  collapsed to one canonical implementation.
- refreshFn signature takes context.Context so Ctrl+C during a
  transparent refresh cancels.
- AuthRetryTransport.CurrentToken() removed — never called.

8 + 8 + 8 unit tests cover happy path / refresh-fail / auth-endpoint
skip / api-key passthrough / singleflight under concurrency / non-
replayable-body fallback.

Roadmap: 3-2.
2026-05-14 10:57:17 +08:00
nullkey
41a98b5743 feat(cli): context CRUD
New v0.3 P0 entry 3-4: kubectl-style context-management subtree using
gh's `<noun> <verb>` surface convention consistent with the rest of
this CLI.

- context list: tabwriter rendering + --json envelope; reads config.yaml
  only.
- context add <name> --host <url> [--user]: validates http(s) URL, first
  context auto-becomes current, rejects duplicates with did-you-mean.
- context remove <name>: best-effort keyring cleanup like `auth logout`.
  Removing the current context triggers exit-10 confirmation (lark-cli
  skill protocol) — subsequent commands would lose their default
  --context.

(`context use` predates v0.3; the subtree was previously use-only.)

Bugs caught and fixed inline by the post-commit reviewer round:
- auth login was accepting `http://` (empty host portion) because the
  old validateHost only checked the scheme. New cmdutil.NormalizeHost
  (shared by both login and context add) requires u.Host != "".
- context add's validateName claimed `..` was rejected but only denied
  / \\ space. Switched to positive allowlist [A-Za-z0-9._-] plus
  explicit ./../path-separator rejection.

Helper consolidation:
- cli/internal/cmdutil/host.go: NormalizeHost (trim, scheme, host
  non-empty) — both auth login and context add share it.
- cli/internal/format/dash.go: DashIfEmpty — promoted from copies in
  cmd/auth/list.go and cmd/context/list.go.
- recordingStore test stub dropped in favor of secrets.NewMemStore;
  contextKeyList test helper replaced by the existing contextKeys.

14 unit tests; 13 e2e branches verified.

Roadmap: 3-4.
2026-05-14 10:57:17 +08:00
dependabot[bot]
3675c0f656 chore(deps): bump github.com/spf13/pflag in /cli in the cli-deps group
Bumps the cli-deps group in /cli with 1 update: [github.com/spf13/pflag](https://github.com/spf13/pflag).


Updates `github.com/spf13/pflag` from 1.0.9 to 1.0.10
- [Release notes](https://github.com/spf13/pflag/releases)
- [Commits](https://github.com/spf13/pflag/compare/v1.0.9...v1.0.10)

---
updated-dependencies:
- dependency-name: github.com/spf13/pflag
  dependency-version: 1.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cli-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 13:33:18 +08:00
nullkey
e236be1ced fix(cli): correct KB id detection, SSE terminal-frame, and CI test isolation
Three defects surfaced during end-to-end RAG verification — the first two
block real chat usage, the third makes Linux CI flaky:

1. KB id detection — `IsKBID` was checking
   `strings.HasPrefix(s, "kb_")`, but WeKnora generates KB ids as bare
   UUIDs (internal/types/knowledge_base.go: `uuid.New().String()` stored
   in a `varchar(36)` column). Real ids therefore fell through to the
   name-resolution path:

     $ weknora chat ... --kb a32a63ff-fb36-4874-bcaa-30f48570a694
     Error: knowledge base not found: a32a63ff-...

   Switched the discriminator to a UUID regex
   (`^[0-9a-fA-F]{8}-…-[0-9a-fA-F]{12}$`). KB names are arbitrary
   user-supplied strings, so the canonical 8-4-4-4-12 form is an
   unambiguous signal. Mirrors gcloud `--project`'s id-vs-name detection.

2. SSE terminal-frame — the accumulator's `Append` was gating
   finalization on `r.Done`, but the server's KnowledgeQAStream protocol
   emits a leading `agent_query` frame with `done=true` to deliver
   session + message metadata *before* the answer fragments arrive:

     event: message
     data: {"response_type":"agent_query","content":"","done":true,…}

     event: message
     data: {"response_type":"answer","content":"你好","done":false}
     …
     event: message
     data: {"response_type":"complete","content":"","done":true}

   The accumulator therefore flipped to `finished=true` on frame #1 and
   discarded every subsequent answer fragment — `weknora chat … --json`
   returned `answer: ""` even though the LLM reported completion_tokens
   > 0. Fixed: terminate only on `response_type == complete`.
   References still captured opportunistically (they may arrive on a
   dedicated `references` event before the terminator).

3. doctor credential_storage CI isolation — the check probes the real
   OS keyring via `secrets.NewBestEffortStore()`: present on macOS dev
   machines → StatusOK; absent on Linux CI runners without libsecret /
   Gnome-Keyring → StatusWarn ("falling back to file store"). That
   host-dependence was leaking into two test classes that assumed
   StatusOK:

     * cmd/doctor/doctor_test.go: TestDoctor_AllOK and
       TestDoctor_NoConfig_StillRunsCredentialStorage already had a
       withCredStoreFactory seam but didn't use it. Added the pin.

     * acceptance/contract/envelope_test.go: doctor.success_offline
       and doctor.error_network golden cases. The contract test runs
       through the cobra tree in-process and shares cmd/doctor's
       package-level credStoreFactory var — but couldn't reach it
       because the existing seam was unexported.

   Fix: export `doctor.SetCredStoreFactoryForTest(fn) (restore func())`
   for out-of-package tests; acceptance/contract/helpers_test.go adds
   a TestMain that pins the factory to a MemStore-returning closure
   for the whole suite (MemStore is neither *FileStore nor a real
   keyring, so doctor's type-switch hits StatusOK). Production stays
   at secrets.NewBestEffortStore — only the test hook is now reachable
   from across packages.

Test fixtures and goldens that used the old `kb_xxx` literals or
`Done: true` terminators were rewritten to use real UUIDs and
`ResponseType: ResponseTypeComplete` respectively. Per-command --help
text and Long descriptions / Examples now show a UUID rather than
`kb_…` so users see the correct shape from the start. New
TestAccumulator_IgnoresAgentQueryDone pins the SSE terminator bug so
it can't regress.

Tests: 24 cli packages green on macOS dev + Linux/macOS/Windows CI
matrix. Verified end-to-end against a live WeKnora server: `weknora
chat "..." --kb <UUID> --no-stream --json` returns the full LLM answer
in the envelope, live token streaming in TTY mode works, and the
credential_storage check renders deterministic envelopes across hosts.
2026-05-12 13:20:42 +08:00
nullkey
bdbd15bf75 docs(cli): add CLI README, top-level mention, CHANGELOG, ADR section
Discoverability gaps surfaced by the pre-PR review:

- New cli/README.md: install (build-from-source / pre-built once shipped)
  + 5-minute quickstart (auth login → kb list → link → doc upload →
  chat) + multi-context walkthrough + JSON envelope shape + agent /
  scripting integration overview + dev workflow. Points readers at
  cli/AGENTS.md for the full operational contract.

- Top-level README.md: new "⌨️ Command-Line Interface" section between
  Key Features and Getting Started, with a one-paragraph pitch + four
  representative commands and links to cli/README.md and cli/AGENTS.md.
  English README only this round; CN / JA / KO translations to follow
  in v0.3 to match the existing four-language pattern.

- CHANGELOG.md [Unreleased] gets a "weknora CLI v0.2" bullet listing
  the headline capabilities (10-command surface, project-link,
  envelope, agent affordance, multi-context auth, doctor) and pointing
  at cli/README.md.

- cli/AGENTS.md gains an "Architecture decisions" section documenting
  ADR-3 (gh as primary mainstream north star + the four documented
  deviations: link, chat/search, context use, doctor) and ADR-4
  (Factory closures + narrow Service interfaces). The in-source
  references (`(v0.2 ADR-3)`, `(per ADR-4)`) now point at committed
  prose rather than dangling.
2026-05-12 13:20:42 +08:00
nullkey
ca90ce422f feat(cli): add auth logout and auth list commands
gh / lark / gcloud / stripe all ship a logout command and a way to
enumerate stored credentials on day one. WeKnora's `auth` subtree had
only login + status, leaving no documented purge path for keyring
secrets — a real concern for `--with-token` (sk-…) and JWT flows that
write credentials to OS keychains.

auth logout [--name <ctx>] [--all] [--json]
  Clears keyring + file-fallback secrets (access / refresh / api_key
  slots) for the named context (default: current) or every context
  with --all. Removes the context entry from ~/.config/weknora/config.yaml
  and clears current_context if the removed entry was active.

  Mirrors `gh auth logout` and `lark auth logout`. As gh documents,
  this does NOT revoke server-side — for API keys users must rotate in
  the server UI, JWTs continue to be accepted until expiry.

auth list [--json]
  Renders a compact table (NAME / HOST / USER / MODE) with the active
  context marked `*`. Reads only config.yaml — no network, no keyring
  touch. Mode is inferred from which credential ref is set (api_key
  → "api-key", token → "password"; both → "password" wins).

  Mirrors gh's per-host enumeration (gh auth status iterates accounts)
  and lark `auth list`. For weknora the contexts file already had this
  data — the command is a thin renderer to match user muscle memory.

Deferred to a follow-up release:
  - auth refresh + transparent 401 retry in the SDK (we already persist
    refresh_token at login but never spend it; explicit gap)
  - login --web browser OAuth flow (requires a server-side endpoint)
  - auth token printer (cheap; defer with the rest)

Tests: 24 cli packages green. New: cmd/auth/logout_test.go (current
context, named, --all, no-contexts, unknown-name, no-current-no-flag,
mutex flags) + cmd/auth/list_test.go (human render, empty, JSON
envelope, inferMode edge cases). AGENTS.md command-surface note adds
the four-command auth subtree; screenshot section 4 adds `auth list`
alongside `auth status`.
2026-05-12 13:20:42 +08:00
nullkey
8bcbf5a154 refactor(cli): align command surface with mainstream conventions
Empirical mainstream-CLI surveys (gh / kubectl / aws / gcloud / stripe /
flyctl / terraform / vercel / netlify / lark) drove five alignment
fixes — each replaces a weknora-only design choice that mainstream CLIs
do not share. No backwards-compat shims; the CLI has no v0.1 users yet.

1. Single --kb flag (was --kb-id + --kb mutually exclusive)

   Survey: 0/7 mainstream CLIs use two parallel flags for "by id" vs
   "by name". Single flag (gh -R, gcloud --project) or positional
   (kubectl, stripe, terraform). Closest analog — gcloud --project —
   collapses identifier types onto one flag.

   Now: every command exposes one --kb flag; client-side prefix
   detection (cmdutil.IsKBID looks for "kb_") routes id-form values
   through directly and name-form values through ListKnowledgeBases.
   Mirrors gcloud --project's id-or-name auto-detection.

   Touched: search, chat, doc list / upload / delete, link.
   Factory.ResolveKB chain trimmed from 5 levels to 4.

2. link supersedes init

   Survey: only vercel and netlify ship both `init` AND `link` as
   siblings, and they keep them semantically distinct. weknora's pair
   wrote the same .weknora/project.yaml file with the same meaning,
   differentiated only by interactivity — that's a flag concern, not
   a command concern.

   Now: cmd/init/ deleted. cmd/link absorbs the interactive flow:
     - link --kb <id-or-name>  → non-interactive write
     - link on a TTY            → interactive prompt (lists KBs)
     - link non-TTY without --kb → CodeKBIDRequired
   Always overwrites silently (matches vercel link / netlify link /
   kubectl apply rather than git init's refuse-if-exists).

   Dead code purged: --force flag, CodeProjectAlreadyLinked error code.

3. whoami dropped

   Survey: 7/7 mainstream CLIs ship exactly one identity command —
   never both a status and a whoami. gh / gcloud / stripe pick status
   (config + live API); aws / kubectl / flyctl pick whoami (live API).

   weknora's auth status was already a superset of whoami (host +
   context + user + email + tenant_id + tenant_name vs user_id +
   tenant_id), so dropping whoami preserves all functionality and
   aligns with the gh / gcloud / stripe form.

4. kb get alias dropped

   `view` was already primary (gh repo view / gh pr view convention);
   `get` was kept as a cobra alias for v0.0/v0.1 callers. With no
   v0.0/v0.1 users to break, the alias is just noise on the command
   surface. Acceptance contract envelope cases renamed kb_get.* →
   kb_view.*; goldens renamed in lockstep.

5. api refactored to gh shape (-X/--method, default GET, auto-POST)

   gh CLI's signature is `gh api <endpoint> [--method M]` — single
   positional path, method as a flag, default GET, auto-promoted to
   POST when a body is supplied. weknora's previous `api <method>
   <path>` inverted this and forced the method to be passed even for
   GET — a needless deviation from our declared north star.

   Now: `api <path> [-X METHOD] [--data ...]`. Exit-10 protocol
   on the DELETE escape-hatch is preserved; -X DELETE still hits
   ConfirmDestructive when -y absent.

Plus: AGENTS.md gains an explicit note that `doctor` is a deliberate
divergence from gh / lark — borrowed from `flutter doctor` / `brew
doctor` because RAG deployments routinely break on misconfigured
embeddings / storage / credentials and a 4-status structured envelope
is the cleanest surface for it.

Tests: 24 cli packages green (was 26 in PR-14; init + whoami packages
removed). Acceptance contract envelope cases for whoami removed,
kb_get → kb_view renamed, search args / mock path updated for the
kb_<id> form. e2e harness flag args updated. Factory.ResolveKB tests
rewritten for the single-flag shape. api_test driver updated for the
positional-path / -X-method shape.
2026-05-12 13:20:42 +08:00
nullkey
f7d7c8054d chore(cli): remove unused v0.0 scaffolding
Foundation PR-1 reserved several internal packages and helpers as
scaffolding for follow-up PRs that ended up taking different routes.
Audit confirms zero production references; this commit removes them so
the cli/ tree reflects what's actually shipped.

Removed (148 LOC):

  cli/internal/safepaths/                 — `Validate` / `WithinRoot` /
                                            three sentinel errors. Reserved
                                            for `weknora doc upload`'s path
                                            scrubbing; that command landed
                                            in PR-10 using its own
                                            `validateUploadPath` (os.Stat +
                                            regular-file check) — sufficient
                                            for the actual threat model
                                            (local CLI invocations).

  cli/internal/cmdutil/json_flags.go      — `AddJSONFlags` helper +
                                            unused --jq / --template flag
                                            registration. Reserved for PR-3
                                            "lipgloss tables / jq evaluator"
                                            which never materialized; every
                                            command directly registers
                                            BoolVar(&JSONOut, "json", ...)
                                            since v0.0 ship time.

  cmdutil.NewTableExporter                — empty alias for jsonExporter,
                                            reserved for the same PR-3
                                            renderer. Removed; jsonExporter
                                            stays under NewJSONExporter.

  cmdutil.Options marker interface        — empty interface{} reserved as a
                                            convention; no command embeds
                                            or asserts against it.

Stale comments fixed:

  - cmd/root.go: package comment updated kb (list+get) → kb
    (list+view+create+delete) and noted the `get` cobra alias.
  - cmd/root.go: dropped --no-version-check forward-reference (no such flag).
  - cmd/root.go: removed "(PR-7)" attribution from NewRootCmd doc comment.
  - cmd/kb/kb.go: same package-comment update.
  - cmd/chat/chat.go: replaced "PR-7" mention in --help example with a
    generic placeholder so cobra-rendered help is review-clean.
  - cmd/search/search.go: removed "Lipgloss tables arrive in PR-3"
    forward-reference; the inline indent helper is the shipped form.
  - internal/agent/annotations.go: ShouldUseAgentMode → DetectAIAgent
    (removed in PR-12).

AGENTS.md "Known limitations" section added:
  Documents that chat / search / doc upload currently surface server-side
  precondition misses (LLM / vector store / storage engine not configured)
  as `network.error` with `context deadline exceeded`. A planned future
  release will introduce a `precondition.*` typed error namespace
  (server returns HTTP 412 before opening the SSE / streaming response).
  This documents the limitation honestly for reviewers and integrators
  rather than claiming a behavior we don't yet have.

Tests: 27 cli packages pass (safepaths_test was the 28th — gone with the
package). go vet clean.
2026-05-12 13:20:42 +08:00
nullkey
da9faa9e07 feat(cli): add agent-first affordance — envelope, exit-10, --dry-run
Borrows the lark-cli agent-affordance model
(https://github.com/larksuite/cli/blob/main/AGENTS.md +
skills/lark-shared/SKILL.md) so weknora is designed to be agent-friendly:
error messages, output format, and flag design follow conventions agents
can rely on.

cli/AGENTS.md (operational reference for LLM agents invoking weknora):
  Public document covering envelope schema, exit-code protocol
  (0/1/2/10/130), stdout/stderr separation, and behavioral rules.
  Sensitive commands (\`context use\`, \`kb delete\`, \`doc delete\`, \`init\`)
  gain "AI agents:" paragraphs in their cobra Long descriptions so
  guidance shows in --help.

format.Envelope schema additions:
  Risk    per-operation classification (read / write / high-risk-write +
          action description), populated by write commands on both success
          and failure paths.
  Notice  system advisories (CLI update available, server-CLI version
          skew); type defined, emit sites land in v0.3.
  DryRun  marker for envelopes returned from --dry-run preview paths.

  RiskLevel constants realigned to lark's taxonomy: read / write /
  high-risk-write (was: read / mutating / destructive — not yet wired by
  any command).

  cmdutil.Error gains OperationRisk; PrintErrorEnvelope auto-attaches it
  to envelope.Risk so destructive failure paths surface uniformly.

Exit-10 confirmation protocol:
  New ErrorCode \`input.confirmation_required\` mapped to exit code 10 in
  cmdutil.ExitCode. ConfirmDestructive now returns this code (with
  OperationRisk attached) when stdout is non-TTY or --json was set, with
  -y/--yes absent. Previous behavior — silent proceed in non-TTY — was
  unsafe: scripts and agents could delete resources with no explicit
  approval. Three test cases re-pinned around the new contract.

  This is a wire-contract change for any caller who relied on silent
  proceed; v0.0/v0.1 had no destructive commands, so the blast radius is
  contained to v0.2 itself.

--dry-run global flag:
  cmd write paths (kb create/delete, doc upload/delete, api POST/PUT/PATCH/
  DELETE) check cmdutil.IsDryRun(cmd) and skip the SDK call, emitting an
  envelope with dry_run=true plus a Risk classification. Read commands
  ignore --dry-run by design (no side effect to preview). Human-mode
  prints \`[dry-run] would <action>\` to stdout.

Command discovery: agents introspect via the existing \`--help\` surface
(consistent with gh / kubectl / aws / gcloud / terraform — none of them
ship a CLI-tree self-description command). An earlier draft added a
\`weknora schema\` reflection command; dropped after a mainstream survey
found it has no stable analog (lark-cli's schema describes Lark API
methods, not its own CLI tree).

Tests: 27 cli packages pass at this commit. Added two new tests covering
envelope.risk and envelope._notice serialization.
2026-05-12 13:20:42 +08:00
nullkey
9d2e740753 refactor(cli): align command surface with gh CLI conventions (ADR-3)
Audited the v0.0~v0.2 21-command surface against gh / kubectl / cargo /
npm / git / docker / flyctl / vercel / supabase / brew. WeKnora was
cherry-picking from multiple heritages, producing an inconsistent feel:
the kb subtree mixed gh verbs (create / delete / list) with a kubectl
verb (get); confirmation flag duplicated --force (docker/kubectl) with
global -y/--yes (gh/vercel/npm); the --agent flag stretched Stripe's
telemetry-tag pattern into a behavior-mode switch that no mainstream
CLI does.

ADR-3 picks gh as the primary north star. Documented deviations remain
for project-link (vercel/cargo), chat (openai-cli), context (kubectl-
light), and doctor (brew/flutter). The decision and its deviations are
documented self-contained in cli/AGENTS.md.

Surface changes:

  - kb get → kb view (gh repo view convention); "get" kept as cobra Alias
    for v0.0/v0.1 callers — see https://cli.github.com/manual/gh_repo_view.

  - kb delete --force / doc delete --force removed in favor of the global
    -y/--yes persistent flag (gh repo delete --yes convention). One
    mechanism skips destructive prompts; ConfirmDestructive's parameter
    renamed `force` → `yes` to match.

  - --agent omnibus mode-switch removed. Stripe's DetectAIAgent (the
    cited inspiration) only tags User-Agent for telemetry, never flips
    behavior; gh / kubectl / aws / docker / flyctl all decline this kind
    of flag. The 7-env auto-detect list is reduced to the two entries
    Stripe also recognizes (CLAUDECODE, CURSOR_AGENT) — the other five
    had no agent-documented source. ApplyAgentSugar / ShouldUseAgentMode
    and the dead --no-interactive / --no-progress globals are deleted
    entirely.

  - DetectAIAgent and SetAgentHelp annotations are kept: env detection
    now only triggers AGENT-targeted help text rendering (no behavior
    change), matching Stripe's narrower scope.

Tests: 27 cli packages green (acceptance/contract still pins kb get
golden; the alias keeps it valid).
2026-05-12 13:20:42 +08:00