d7b58355198eacda1394fbf8ea2d3bf17c6d3926
25 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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.
|
||
|
|
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).
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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).
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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`.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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).
|
||
|
|
3fb3583a92 |
feat(cli): add api passthrough, chat streaming, doctor warn status
Close the v0.2 RAG demo loop and ship the validation infrastructure:
- weknora api <method> <path> [--data X | --data-file F]
Raw passthrough wrapping client.Raw, gh-style. JSON envelope mode
surfaces status / headers / parsed body. Non-2xx routes through
cmdutil.ClassifyHTTPStatus (factored out of ClassifyHTTPError so
both SDK-error and direct-status paths stay aligned — reuse review).
- weknora chat <text> [--session-id S] [--no-stream]
KnowledgeQAStream consumer with two output modes:
- TTY default: token streaming + references footer
- --json / --no-stream / non-TTY: buffered single envelope
Auto-creates a session when --session-id is omitted; the id prints
to stderr at start AND on stream failure (^C scrolls past the
first announcement, so the recovery hint is re-surfaced when the
user is most likely to need it).
- cli/internal/sse/Accumulator
buffers Content / References / SessionID across SDK callbacks.
Idempotent post-Done so misbehaving servers don't corrupt state.
- doctor: ok → ok / warn / fail / skip
warn marks soft issues that don't block: server within compat range
but >=1 minor behind CLI; credential storage falling back to file
because keyring is unavailable. Envelope.ok stays true on warn,
flips false on fail (exit 1). doctor.error_network golden updated.
- cli/acceptance/e2e/ real-server RAG full loop
Build-tagged //go:build acceptance_e2e — kept out of the default
`go test ./...`. Exercises kb create → doc upload → poll ready →
search → chat → cleanup against a server pointed at by
WEKNORA_E2E_HOST / _TOKEN.
- .github/workflows/cli-e2e.yml
manual workflow_dispatch + label-gated PR trigger
("acceptance-e2e"). No-ops gracefully when the secrets aren't set
so cross-fork PRs can't accidentally fail the suite.
|
||
|
|
8a0674186e |
feat(cli): add kb create/delete and doc list/upload/delete commands
Add the resource-management surface to the v0.2 CLI: - weknora kb create --name X [--description Y] [--embedding-model Z] - weknora kb delete <id> [--force] - weknora doc list [--kb-id X | --kb NAME] [--page N] [--page-size M] - weknora doc upload <file> [--kb-id X | --kb NAME] [--name custom] - weknora doc delete <id> [--force] doc/* uses Factory.ResolveKB so the cwd's project link is honored when --kb-id is omitted. doc upload validates path existence with os.Stat (rejects directories; follows symlinks to mirror SDK os.Open behavior). doc list sorts by updated_at desc so newer items surface first. Both delete commands route through cmdutil.ConfirmDestructive — the "destructive op needs explicit user opt-in" pattern was about to be copy-pasted across the new subtree, so it was extracted with the delete commands as their first consumer. Saves the same dedup pass when v0.3 adds session/agent delete. (PR-12 later renames the flag from --force to the global -y/--yes for gh-style consistency.) iostreams.SetForTestWithTTY pairs with the existing SetForTest helper: the latter never reports stdout as a TTY (singleton replacement uses an in-memory buffer), so the confirm-yes / confirm-no test branches need a TTY-on variant. |
||
|
|
19afd5eed9 |
feat(cli): add project-link foundation with init and link commands
v0.2 grounds the CLI's resource commands (kb / doc / chat / query) in a
per-project link file (.weknora/project.yaml) so users don't have to pass
--kb-id on every invocation. Mirrors npm/cargo/git: walk up the cwd tree
to find the project root, override via flag or env when needed.
This commit ships the foundation layer:
- cli/internal/projectlink/ Discover (walk-up, depth=64) / Load / Save
- cmdutil.Factory.ResolveKB 5-level fallback chain:
--kb-id flag → --kb name (ListKnowledgeBases lookup) →
WEKNORA_KB_ID env → walk-up project link → CodeKBIDRequired
- cmdutil.ResolveKBNameToID shared name→id helper used by init / link
/ Factory.ResolveKB (was duplicated 3 ways
in early implementation; reuse review #2)
- cli/cmd/init/ interactive (huh prompt) or flag-driven
first-time setup; refuses to overwrite
without --force
- cli/cmd/link/ non-interactive update; --kb-id and --kb
are mutually exclusive and one is required
Also registers the v0.2 ErrorCode set (all codes for the eight new
commands) and AST-scan identToErrorCode mapping in one place — keeps the
acceptance/contract suite green across the v0.2 commit chain even before
later commits reference each code.
|
||
|
|
bb592a59a6 |
feat(cli): contract test suite + dependabot (PR-8)
cli/acceptance/contract/:
envelope_test.go — 16 envelope golden cases (9 commands × {success/error
variants}; 3 cases dropped with rationale: doctor.success
non-offline has unstable timing detail; auth_login.* needs
stdin/keyring scaffold deferred to v0.2; context_use.error
needs leaf-local --json deferred to follow-up)
errorcodes_test.go — single-direction AST scan of cli/cmd/ extracting first
arg of cmdutil.NewError / cmdutil.Wrapf calls;
ClassifyHTTPError dynamic-classify bridged via
cmdutil.ClassifyHTTPErrorOutputs() per spec §4.3.
testdata/envelopes/ — 16 JSON golden files
helpers_test.go (PR-6 scaffold) extended:
runCmd now wires cobra Out/Err sinks (version uses c.OutOrStdout) AND
replicates cmd.Execute()'s error-envelope path so error-case goldens are
populated. Without this, every error scenario's golden was 0 bytes.
cli/cmd/root.go: mapCobraError → MapCobraError, wantsJSONOutput → WantsJSONOutput
(exported so the contract test helper can replicate Execute()'s
envelope-printing path without calling Execute() itself).
root_test.go updated to use new exported names.
.github/dependabot.yml (新增):gomod /cli + github-actions weekly,gh-style
ignore semver-major to avoid noise. Open-source
dependency safety,independent of release cadence.
v0.1 不发布到任何分发平台 (release infra 推迟到发布窗口 milestone)。
|
||
|
|
cf84bf2a38 |
feat(cli): add whoami / doctor / kb / context commands (PR-7)
5 new leaf commands wired into the root tree:
whoami — simplified `auth status` (user_id + tenant_id only)
doctor — 4-item self-check (base_url / auth / server_version / cred_storage)
with --offline / --no-cache flags + skip cascade + summary.all_passed
防 agent 看到 envelope.ok=true 误判命令整体 success
kb list — list KBs (default updated_at desc; 0 KB → "(no knowledge bases)")
tabwriter 4-col (ID/NAME/DOCS/UPDATED), display-width truncation
kb get — show single KB details (KEY: VALUE, suppress empty fields)
context use — switch default context (writes config.current_context),
带 levenshtein distance ≤ 2 的 did-you-mean hint
Each command uses the v0.0 narrow Service interface pattern (testable via
fakes), agent.SetAgentHelp for AI-friendly hints, and ClassifyHTTPError
for stable error code mapping.
cmdutil/errors.go: 新增 CodeLocalContextNotFound for `context use`,加入 AllCodes() 注册集.
cli/cmd/root.go: NewRootCmd 改为 exported (acceptance/contract 测试需要),
注册 4 个新命令 + 1 parent group; root_test.go 跟随更新.
|
||
|
|
fc5d16e331 |
feat(cli): top-level search command (PR-5)
The fourth and final v0.0 command — chunk hybrid retrieval, the demo
headline operation per ADR-3 (only one search command in the tree).
Maps to client.HybridSearch / GET /knowledge-bases/{id}/hybrid-search;
no SDK or server changes.
Flags:
- --kb (required): target knowledge base
- --top-k (default 8): max results
- --vector-threshold / --keyword-threshold: similarity floors
- --no-vector / --no-keyword: disable individual channels (mutually
exclusive at the validation gate; checked before the SDK client is
built so flag misuse fails fast)
- --json: emit envelope JSON (otherwise pretty list)
Service interface narrowed to just HybridSearch; tests inject fakes via
Factory.Client closure override.
Pretty rendering is a minimal text indent today; lipgloss tables arrive
in PR-3 of v0.2 (format/ split into presenters/style/tableprinter).
|
||
|
|
811802485e |
feat(cli): auth login + auth status commands (PR-4)
First two leaf commands and the auth/ command group, registered under the root command tree. auth login: - Email + password (interactive huh prompter) OR --with-token (read API key from stdin) for headless / CI use - Validates --host (required, must be http/https URL) - Persists access + refresh tokens (password mode) or api_key (token mode) into the secrets store wired in PR-3 — uses Store.Ref to record the URI scheme without leaking concrete backend type - Writes the named context to ~/.config/weknora/config.yaml and sets it as current_context - Typed loginResult payload for --json output (no map[string]any) auth status: - Calls client.GetCurrentUser -> /auth/me, prints context / host / user / tenant. --json emits a typed statusResult envelope with _meta.context and tenant_id populated for agents. - Service interface narrowed to just GetCurrentUser; tests inject fakes by overriding Factory.Client (no runF gymnastics). cmd/root.go: register \`auth\` parent under the root tree. |
||
|
|
a39805aa89 |
feat(cli): scaffold weknora CLI module foundation (PR-1)
Empty cli/ Go module with Factory + Options skeleton that all v0.x commands plug into. Foundation packages (10 internal/): - cmdutil: Factory(3 closure: Config/Client/Prompter) + Options + Errors with namespaced ErrorCode + typed predicates (IsAuthError / IsNotFound / IsTransient / IsAuthExpired) + Exporter + AddJSONFlags + ExitCode/PrintError - iostreams: package singleton with TTY detection; ColorEnabled honors NO_COLOR / TERM=dumb on demand - agent: mode + detect + annotations (Stripe pkg/useragent pattern; CLAUDECODE / CURSOR_AGENT / CODEX_* / AIDER_* / CONTINUE_* / OPENCODE_* / GEMINICODER env detection) - build: ldflags-injected version/commit/date - safepaths: separator-aware path-traversal protection - config: yaml.v3 schema + atomic save 0600 (XDG-style) - format: success/failure envelope (code/hint/request_id/risk/retryable/ console_url) with typed RiskLevel - prompt: Prompter interface + AgentPrompter (rejects in agent mode) - secrets: file 0600 store with weknora:<context>:<key> namespace - testutil: XDGTempDir(t) test helper Smoke command: weknora version (-/--json). Build/test/vet matrix in .github/workflows/cli.yml across linux x macos x windows x Go 1.24. Factory.Client returns CodeLocalUnimplemented until PR-3 wires the SDK adapter; PR-1 commands (version) don't need it. |