Commit Graph

8 Commits

Author SHA1 Message Date
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 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 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 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 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 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