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.
This commit is contained in:
nullkey
2026-05-11 02:44:34 +08:00
committed by lyingbug
parent 9d2e740753
commit da9faa9e07
38 changed files with 634 additions and 104 deletions
+31
View File
@@ -30,6 +30,8 @@ type Options struct {
Data string
DataFile string
JSONOut bool
DryRun bool
Yes bool
}
// Service is the narrow SDK surface this command depends on. The production
@@ -58,6 +60,20 @@ Examples:
weknora api DELETE /api/v1/knowledge-bases/kb_xxx`,
Args: cobra.ExactArgs(2),
RunE: func(c *cobra.Command, args []string) error {
opts.DryRun = cmdutil.IsDryRun(c)
opts.Yes, _ = c.Flags().GetBool("yes")
method := strings.ToUpper(args[0])
// Escape-hatch DELETE through `weknora api` is just as destructive
// as `weknora kb delete` — exit-10 protocol must apply (AGENTS.md).
// Dry-run is read-only preview, so it skips confirmation.
if !opts.DryRun && method == "DELETE" {
if err := cmdutil.ConfirmDestructive(f.Prompter(), opts.Yes, opts.JSONOut, "endpoint", args[1]); err != nil {
return err
}
}
if opts.DryRun {
return runAPI(c.Context(), opts, nil, args[0], args[1])
}
cli, err := f.Client()
if err != nil {
return err
@@ -100,6 +116,21 @@ func runAPI(ctx context.Context, opts *Options, svc Service, methodArg, path str
body = json.RawMessage(contents)
}
// --dry-run only meaningful for write methods; GET/HEAD have no side
// effect to preview, so we proceed normally even with --dry-run.
if opts.DryRun && method != "GET" && method != "HEAD" {
level := format.RiskWrite
if method == "DELETE" {
level = format.RiskHighRiskWrite
}
preview := map[string]any{"method": method, "path": path}
if body != nil {
preview["body"] = body
}
return cmdutil.EmitDryRun(opts.JSONOut, preview, nil,
&format.Risk{Level: level, Action: fmt.Sprintf("%s %s", method, path)})
}
resp, err := svc.Raw(ctx, method, path, body)
if err != nil {
// Transport / DNS failure (Raw never returns a typed HTTP error of its
+70
View File
@@ -11,8 +11,11 @@ import (
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/Tencent/WeKnora/cli/internal/cmdutil"
"github.com/Tencent/WeKnora/cli/internal/iostreams"
"github.com/Tencent/WeKnora/cli/internal/prompt"
sdk "github.com/Tencent/WeKnora/client"
)
@@ -179,6 +182,73 @@ func TestAPI_PathWithoutSlash(t *testing.T) {
}
}
// withRootHarness wraps `weknora api ...` under a synthetic root cmd that
// registers the global `-y/--yes` persistent flag (mirrors addGlobalFlags in
// cmd/root.go). Required because api's NewCmd doesn't register --yes itself
// — it inherits from root in production.
func withRootHarness(api *cobra.Command, args ...string) *cobra.Command {
root := &cobra.Command{Use: "weknora"}
root.PersistentFlags().BoolP("yes", "y", false, "")
root.PersistentFlags().Bool("dry-run", false, "")
root.AddCommand(api)
root.SetArgs(append([]string{"api"}, args...))
root.SetContext(context.Background())
root.SilenceErrors = true
root.SilenceUsage = true
return root
}
// TestAPI_DELETE_RequiresConfirmation pins the exit-10 protocol on the
// escape-hatch DELETE path: agent invokes `weknora api DELETE /...` without
// -y/--yes, must get input.confirmation_required + exit 10. Confirmation is
// enforced in NewCmd.RunE (not runAPI), so the test drives the cobra cmd.
func TestAPI_DELETE_RequiresConfirmation(t *testing.T) {
iostreams.SetForTest(t) // non-TTY
f := &cmdutil.Factory{
Client: func() (*sdk.Client, error) { return nil, nil },
Prompter: func() prompt.Prompter { return prompt.AgentPrompter{} },
}
root := withRootHarness(NewCmd(f), "DELETE", "/api/v1/knowledge-bases/kb_xxx")
err := root.Execute()
if err == nil {
t.Fatal("expected confirmation_required error for DELETE without -y")
}
var ce *cmdutil.Error
if !asTypedError(err, &ce) || ce.Code != cmdutil.CodeInputConfirmationRequired {
t.Errorf("want input.confirmation_required, got %v", err)
}
if got := cmdutil.ExitCode(err); got != 10 {
t.Errorf("exit code = %d, want 10", got)
}
}
// TestAPI_DELETE_WithYes_Proceeds: -y/--yes opt-in skips confirmation and
// dispatches to the SDK. Server returns 200 to verify the happy-path lands
// on the response body emit.
func TestAPI_DELETE_WithYes_Proceeds(t *testing.T) {
iostreams.SetForTest(t)
called := false
cli, stop := newTestClient(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodDelete {
t.Errorf("expected DELETE, got %s", r.Method)
}
called = true
w.WriteHeader(http.StatusOK)
})
defer stop()
f := &cmdutil.Factory{
Client: func() (*sdk.Client, error) { return cli, nil },
Prompter: func() prompt.Prompter { return prompt.AgentPrompter{} },
}
root := withRootHarness(NewCmd(f), "DELETE", "/api/v1/knowledge-bases/kb_xxx", "-y")
if err := root.Execute(); err != nil {
t.Fatalf("execute: %v", err)
}
if !called {
t.Error("DELETE handler not called — confirmation may have blocked")
}
}
// asTypedError is a tiny wrapper around errors.As that keeps the call sites
// concise. Returns true on success, populating dst.
func asTypedError(err error, dst **cmdutil.Error) bool {
+5 -1
View File
@@ -22,7 +22,11 @@ func NewCmdUse(f *cmdutil.Factory) *cobra.Command {
The active context is what every subsequent command uses for auth + host. The
global --context flag (e.g. weknora --context staging kb list) overrides for
one command without writing to disk.`,
one command without writing to disk.
AI agents: Do NOT switch the active context unless the user explicitly asked
you to. Context selection is a user preference; one-shot overrides should use
the global --context flag instead, which writes nothing to disk.`,
Example: ` weknora context use staging # persist switch
weknora --context staging kb list # one-shot override (no disk write)
weknora context use --help # this help`,
+18 -2
View File
@@ -18,6 +18,7 @@ import (
type DeleteOptions struct {
Yes bool
JSONOut bool
DryRun bool
}
// DeleteService is the narrow SDK surface this command depends on.
@@ -41,13 +42,21 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
Short: "Delete a document from a knowledge base",
Long: `Permanently deletes one document. Prompts for confirmation by default
when stdout is a TTY and --json is not set; pass -y/--yes (global flag) to skip
the prompt (required in agent / CI / piped contexts).`,
the prompt (required in agent / CI / piped contexts).
AI agents: This is a high-risk write. Without -y/--yes the CLI exits 10 and
returns an envelope describing the missing confirmation. NEVER auto-pass -y
without the user's explicit go-ahead.`,
Example: ` weknora doc delete doc_abc # interactive confirm
weknora doc delete doc_abc -y # no prompt
weknora doc delete doc_abc -y --json # envelope output`,
Args: cobra.ExactArgs(1),
RunE: func(c *cobra.Command, args []string) error {
opts.Yes, _ = c.Flags().GetBool("yes")
opts.DryRun = cmdutil.IsDryRun(c)
if opts.DryRun {
return runDelete(c.Context(), opts, nil, f.Prompter(), args[0])
}
cli, err := f.Client()
if err != nil {
return err
@@ -61,6 +70,12 @@ the prompt (required in agent / CI / piped contexts).`,
}
func runDelete(ctx context.Context, opts *DeleteOptions, svc DeleteService, p prompt.Prompter, id string) error {
if opts.DryRun {
return cmdutil.EmitDryRun(opts.JSONOut,
deleteResult{ID: id, Deleted: false}, nil,
&format.Risk{Level: format.RiskHighRiskWrite, Action: fmt.Sprintf("delete document %s", id)})
}
if err := cmdutil.ConfirmDestructive(p, opts.Yes, opts.JSONOut, "document", id); err != nil {
return err
}
@@ -70,7 +85,8 @@ func runDelete(ctx context.Context, opts *DeleteOptions, svc DeleteService, p pr
}
if opts.JSONOut {
return format.WriteEnvelope(iostreams.IO.Out, format.Success(deleteResult{ID: id, Deleted: true}, nil))
risk := &format.Risk{Level: format.RiskHighRiskWrite, Action: fmt.Sprintf("deleted document %s", id)}
return format.WriteEnvelope(iostreams.IO.Out, format.SuccessWithRisk(deleteResult{ID: id, Deleted: true}, nil, risk))
}
fmt.Fprintf(iostreams.IO.Out, "✓ Deleted document %s\n", id)
return nil
+11 -8
View File
@@ -128,15 +128,18 @@ func TestDelete_AgentPrompterErrors(t *testing.T) {
assert.Equal(t, cmdutil.CodeInputMissingFlag, typed.Code)
}
// TestDelete_NoForce_NonTTY_Proceeds: when stdout isn't a TTY (typical agent
// pipe / CI), the confirm guard is skipped. This documents the existing
// contract — destructive ops in a pipe rely on the caller having chosen to
// pipe (intent expressed by the redirection) and on agents passing --force
// explicitly. Mirrors `weknora kb delete`.
func TestDelete_NoForce_NonTTY_Proceeds(t *testing.T) {
// TestDelete_NoYes_NonTTY_RequiresConfirmation: when stdout isn't a TTY
// (typical agent pipe / CI), the lark-cli skill protocol requires explicit
// -y/--yes. The CLI exits 10 with input.confirmation_required, never
// silently proceeds. See cli/AGENTS.md "Exit codes".
func TestDelete_NoYes_NonTTY_RequiresConfirmation(t *testing.T) {
_, _ = iostreams.SetForTest(t)
svc := &fakeDeleteSvc{}
err := runDelete(context.Background(), &DeleteOptions{Yes: false}, svc, errPrompter{}, "doc_abc")
require.NoError(t, err, "non-TTY non-json invocation should proceed without prompting")
assert.Equal(t, 1, svc.calls)
require.Error(t, err)
var typed *cmdutil.Error
require.ErrorAs(t, err, &typed)
assert.Equal(t, cmdutil.CodeInputConfirmationRequired, typed.Code)
assert.Equal(t, 0, svc.calls, "non-TTY without -y must not call DeleteKnowledge")
assert.Equal(t, 10, cmdutil.ExitCode(err))
}
+14 -1
View File
@@ -23,6 +23,7 @@ const uploadChannel = "api"
type UploadOptions struct {
Name string
JSONOut bool
DryRun bool
}
// UploadService is the narrow SDK surface this command depends on.
@@ -62,10 +63,14 @@ planned for v0.3.`,
if err := validateUploadPath(path); err != nil {
return err
}
opts.DryRun = cmdutil.IsDryRun(c)
kbID, err := f.ResolveKB(c)
if err != nil {
return err
}
if opts.DryRun {
return runUpload(c.Context(), opts, nil, kbID, path)
}
cli, err := f.Client()
if err != nil {
return err
@@ -104,13 +109,21 @@ func validateUploadPath(path string) error {
}
func runUpload(ctx context.Context, opts *UploadOptions, svc UploadService, kbID, path string) error {
if opts.DryRun {
return cmdutil.EmitDryRun(opts.JSONOut,
map[string]string{"file": path, "kb_id": kbID, "name": opts.Name},
&format.Meta{KBID: kbID},
&format.Risk{Level: format.RiskWrite, Action: fmt.Sprintf("upload %s to kb %s", path, kbID)})
}
k, err := svc.CreateKnowledgeFromFile(ctx, kbID, path, nil /*metadata*/, nil /*enableMultimodel*/, opts.Name, uploadChannel)
if err != nil {
return cmdutil.Wrapf(cmdutil.ClassifyHTTPError(err), err, "upload %s", path)
}
if opts.JSONOut {
return format.WriteEnvelope(iostreams.IO.Out, format.Success(k, &format.Meta{KBID: kbID}))
risk := &format.Risk{Level: format.RiskWrite, Action: fmt.Sprintf("uploaded %s", path)}
return format.WriteEnvelope(iostreams.IO.Out, format.SuccessWithRisk(k, &format.Meta{KBID: kbID}, risk))
}
displayed := opts.Name
if displayed == "" {
+5 -1
View File
@@ -52,7 +52,11 @@ directory (or any subdirectory) automatically resolve --kb-id from the link
unless overridden by the --kb-id / --kb flags or WEKNORA_KB_ID env var.
Mirrors the npm init / cargo init / git init UX pattern: one-time setup that
removes the need to re-pass --kb-id on every command.`,
removes the need to re-pass --kb-id on every command.
AI agents: ` + "`init`" + ` writes to the user's working directory. Only run
it when the user explicitly asked to link this directory — don't run it as a
side effect of unrelated automation.`,
Example: ` weknora init --kb-id kb_abc # explicit id
weknora init --kb engineering --yes # name → id, no prompt
weknora init # interactive (TTY)
+12 -3
View File
@@ -20,6 +20,7 @@ type CreateOptions struct {
Description string
EmbeddingModel string
JSONOut bool
DryRun bool
}
// CreateService is the narrow SDK surface this command depends on.
@@ -36,6 +37,10 @@ func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
Short: "Create a new knowledge base",
Args: cobra.NoArgs,
RunE: func(c *cobra.Command, _ []string) error {
opts.DryRun = cmdutil.IsDryRun(c)
if opts.DryRun {
return runCreate(c.Context(), opts, nil) // service unused on dry-run
}
cli, err := f.Client()
if err != nil {
return err
@@ -66,15 +71,19 @@ func runCreate(ctx context.Context, opts *CreateOptions, svc CreateService) erro
req.EmbeddingModelID = opts.EmbeddingModel
}
if opts.DryRun {
return cmdutil.EmitDryRun(opts.JSONOut, req, nil,
&format.Risk{Level: format.RiskWrite, Action: fmt.Sprintf("create knowledge base %q", opts.Name)})
}
created, err := svc.CreateKnowledgeBase(ctx, req)
if err != nil {
return cmdutil.Wrapf(cmdutil.ClassifyHTTPError(err), err, "create knowledge base")
}
if opts.JSONOut {
return format.WriteEnvelope(iostreams.IO.Out, format.Success(created, &format.Meta{
KBID: created.ID,
}))
risk := &format.Risk{Level: format.RiskWrite, Action: fmt.Sprintf("created knowledge base %s", created.ID)}
return format.WriteEnvelope(iostreams.IO.Out, format.SuccessWithRisk(created, &format.Meta{KBID: created.ID}, risk))
}
fmt.Fprintf(iostreams.IO.Out, "✓ Created knowledge base %q (id: %s)\n", created.Name, created.ID)
return nil
+19 -4
View File
@@ -18,6 +18,7 @@ import (
type DeleteOptions struct {
Yes bool
JSONOut bool
DryRun bool
}
// DeleteService is the narrow SDK surface this command depends on.
@@ -44,13 +45,22 @@ func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
Long: `Permanently deletes a knowledge base and all its contents.
Prompts for confirmation by default when stdout is a TTY and --json is not set.
Pass -y/--yes (global flag) to skip the prompt (required in agent / CI / piped contexts).`,
Pass -y/--yes (global flag) to skip the prompt (required in agent / CI / piped contexts).
AI agents: This is a high-risk write. Without -y/--yes the CLI exits 10 and
returns an envelope describing the missing confirmation. NEVER auto-pass -y
without the user's explicit go-ahead — the exit-10 protocol exists exactly to
guard against unintended deletes.`,
Example: ` weknora kb delete kb_abc # interactive confirm
weknora kb delete kb_abc -y # no prompt
weknora kb delete kb_abc -y --json # envelope output`,
Args: cobra.ExactArgs(1),
RunE: func(c *cobra.Command, args []string) error {
opts.Yes, _ = c.Flags().GetBool("yes")
opts.DryRun = cmdutil.IsDryRun(c)
if opts.DryRun {
return runDelete(c.Context(), opts, nil, f.Prompter(), args[0])
}
cli, err := f.Client()
if err != nil {
return err
@@ -64,6 +74,12 @@ Pass -y/--yes (global flag) to skip the prompt (required in agent / CI / piped c
}
func runDelete(ctx context.Context, opts *DeleteOptions, svc DeleteService, p prompt.Prompter, id string) error {
if opts.DryRun {
return cmdutil.EmitDryRun(opts.JSONOut,
deleteResult{ID: id, Deleted: false}, &format.Meta{KBID: id},
&format.Risk{Level: format.RiskHighRiskWrite, Action: fmt.Sprintf("delete knowledge base %s", id)})
}
if err := cmdutil.ConfirmDestructive(p, opts.Yes, opts.JSONOut, "knowledge base", id); err != nil {
return err
}
@@ -73,9 +89,8 @@ func runDelete(ctx context.Context, opts *DeleteOptions, svc DeleteService, p pr
}
if opts.JSONOut {
return format.WriteEnvelope(iostreams.IO.Out, format.Success(deleteResult{ID: id, Deleted: true}, &format.Meta{
KBID: id,
}))
risk := &format.Risk{Level: format.RiskHighRiskWrite, Action: fmt.Sprintf("deleted knowledge base %s", id)}
return format.WriteEnvelope(iostreams.IO.Out, format.SuccessWithRisk(deleteResult{ID: id, Deleted: true}, &format.Meta{KBID: id}, risk))
}
fmt.Fprintf(iostreams.IO.Out, "✓ Deleted knowledge base %s\n", id)
return nil
+35 -11
View File
@@ -67,17 +67,22 @@ func TestDelete_NotFound(t *testing.T) {
assert.Equal(t, cmdutil.CodeResourceNotFound, typed.Code)
}
func TestDelete_NonTTY_NoPrompt_NoForce(t *testing.T) {
// SetForTest uses bytes.Buffer for Out — IsStdoutTTY() = false. Confirm
// path must be skipped entirely, so DeleteKnowledgeBase should run.
out, _ := iostreams.SetForTest(t)
func TestDelete_NonTTY_NoYes_RequiresConfirmation(t *testing.T) {
// SetForTest uses bytes.Buffer for Out — IsStdoutTTY() = false. Without
// -y/--yes, exit-10 protocol fires (lark-cli skill protocol; AGENTS.md):
// the CLI must NOT silently proceed in scripted contexts.
iostreams.SetForTest(t)
svc := &fakeDeleteSvc{}
p := &confirmPrompter{}
require.NoError(t, runDelete(context.Background(), &DeleteOptions{}, svc, p, "kb_nontty"))
err := runDelete(context.Background(), &DeleteOptions{}, svc, p, "kb_nontty")
assert.True(t, svc.called, "non-TTY must skip prompt and proceed")
require.Error(t, err)
var typed *cmdutil.Error
require.ErrorAs(t, err, &typed)
assert.Equal(t, cmdutil.CodeInputConfirmationRequired, typed.Code)
assert.False(t, svc.called, "non-TTY without -y must not call DeleteKnowledgeBase")
assert.False(t, p.asked, "non-TTY ⇒ Confirm is never invoked")
assert.Contains(t, out.String(), "kb_nontty")
assert.Equal(t, 10, cmdutil.ExitCode(err), "exit code 10 per lark-cli skill protocol")
}
func TestDelete_JSONOutput(t *testing.T) {
@@ -137,15 +142,34 @@ func TestDelete_ConfirmPrompterError(t *testing.T) {
assert.False(t, svc.called)
}
func TestDelete_JSONOut_SkipsPrompt(t *testing.T) {
// Even on a TTY, --json indicates a scripted caller; don't prompt.
out, _ := iostreams.SetForTestWithTTY(t)
func TestDelete_JSONOut_NoYes_RequiresConfirmation(t *testing.T) {
// Even on a TTY, --json indicates a scripted caller; cannot prompt.
// Exit-10 protocol must fire when -y is absent.
iostreams.SetForTestWithTTY(t)
svc := &fakeDeleteSvc{}
p := &confirmPrompter{}
opts := &DeleteOptions{JSONOut: true}
err := runDelete(context.Background(), opts, svc, p, "kb_jtty")
require.Error(t, err)
var typed *cmdutil.Error
require.ErrorAs(t, err, &typed)
assert.Equal(t, cmdutil.CodeInputConfirmationRequired, typed.Code)
assert.False(t, p.asked, "--json must skip the prompt even on TTY")
assert.False(t, svc.called, "--json without -y must not call DeleteKnowledgeBase")
assert.Equal(t, 10, cmdutil.ExitCode(err))
}
func TestDelete_JSONOut_WithYes_Proceeds(t *testing.T) {
// --json + -y is the agent happy-path: scripted caller with explicit
// approval. Must call SDK and emit envelope.
out, _ := iostreams.SetForTestWithTTY(t)
svc := &fakeDeleteSvc{}
p := &confirmPrompter{}
opts := &DeleteOptions{Yes: true, JSONOut: true}
require.NoError(t, runDelete(context.Background(), opts, svc, p, "kb_jtty"))
assert.False(t, p.asked, "--json must skip the prompt even on TTY")
assert.False(t, p.asked, "-y must skip the prompt")
assert.True(t, svc.called)
assert.Contains(t, out.String(), `"deleted":true`)
}
+5 -2
View File
@@ -1,8 +1,10 @@
// Package cmd holds the cobra command tree. main.go calls Execute().
//
// v0.0 shipped: version / auth / search.
// v0.1 adds: whoami / doctor / kb (list + get) / context (use).
// v0.2 adds: init / link / kb (create + delete) / doc (list + upload + delete) / api / chat.
// v0.1 adds: whoami / doctor / kb (list + view) / context (use).
// v0.2 adds: init / link / kb (create + delete) / doc (list + upload + delete)
// / api / chat. The kb view command is the primary; "get"
// is preserved as a cobra alias for v0.0/v0.1 callers.
package cmd
import (
@@ -197,6 +199,7 @@ func addGlobalFlags(cmd *cobra.Command) {
pf := cmd.PersistentFlags()
pf.BoolP("yes", "y", false, "Skip confirmation prompts on destructive operations")
pf.String("context", "", "Override the active context for this invocation (no disk write)")
pf.Bool("dry-run", false, "Preview the operation without executing (write commands only; read commands ignore)")
}
// agentAwareHelpFunc wraps cobra's default help to append the AI agent guidance