@phnx-labs/agents-cli 1.19.2 → 1.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +72 -12
  3. package/dist/browser.js +0 -0
  4. package/dist/commands/browser.js +88 -16
  5. package/dist/commands/cli.d.ts +14 -0
  6. package/dist/commands/cli.js +244 -0
  7. package/dist/commands/cloud.js +1 -1
  8. package/dist/commands/commands.js +27 -10
  9. package/dist/commands/computer.js +18 -1
  10. package/dist/commands/doctor.d.ts +1 -1
  11. package/dist/commands/doctor.js +2 -2
  12. package/dist/commands/exec.js +38 -18
  13. package/dist/commands/factory.d.ts +3 -14
  14. package/dist/commands/factory.js +3 -3
  15. package/dist/commands/feedback.d.ts +7 -0
  16. package/dist/commands/feedback.js +89 -0
  17. package/dist/commands/helper.d.ts +12 -0
  18. package/dist/commands/helper.js +87 -0
  19. package/dist/commands/hooks.js +89 -10
  20. package/dist/commands/mcp.js +166 -10
  21. package/dist/commands/packages.js +196 -27
  22. package/dist/commands/permissions.js +21 -6
  23. package/dist/commands/plugins.js +11 -4
  24. package/dist/commands/profiles.d.ts +8 -0
  25. package/dist/commands/profiles.js +118 -5
  26. package/dist/commands/prune.js +39 -160
  27. package/dist/commands/pull.js +58 -5
  28. package/dist/commands/routines.js +107 -14
  29. package/dist/commands/rules.js +8 -4
  30. package/dist/commands/secrets-migrate.d.ts +24 -0
  31. package/dist/commands/secrets-migrate.js +198 -0
  32. package/dist/commands/secrets-sync.d.ts +11 -0
  33. package/dist/commands/secrets-sync.js +155 -0
  34. package/dist/commands/secrets.js +79 -46
  35. package/dist/commands/sessions.d.ts +28 -0
  36. package/dist/commands/sessions.js +98 -33
  37. package/dist/commands/setup.d.ts +1 -0
  38. package/dist/commands/setup.js +37 -28
  39. package/dist/commands/skills.js +25 -8
  40. package/dist/commands/subagents.js +69 -49
  41. package/dist/commands/teams.js +61 -10
  42. package/dist/commands/utils.d.ts +33 -0
  43. package/dist/commands/utils.js +139 -0
  44. package/dist/commands/versions.d.ts +4 -3
  45. package/dist/commands/versions.js +134 -130
  46. package/dist/commands/view.d.ts +6 -0
  47. package/dist/commands/view.js +175 -19
  48. package/dist/commands/workflows.js +29 -6
  49. package/dist/computer.js +0 -0
  50. package/dist/index.js +38 -6
  51. package/dist/lib/acp/client.js +6 -1
  52. package/dist/lib/acp/harnesses.js +8 -0
  53. package/dist/lib/agents.d.ts +4 -0
  54. package/dist/lib/agents.js +125 -34
  55. package/dist/lib/auto-pull-worker.js +18 -1
  56. package/dist/lib/browser/cdp.d.ts +8 -1
  57. package/dist/lib/browser/cdp.js +40 -3
  58. package/dist/lib/browser/chrome.d.ts +13 -0
  59. package/dist/lib/browser/chrome.js +46 -3
  60. package/dist/lib/browser/domain-skills.d.ts +51 -0
  61. package/dist/lib/browser/domain-skills.js +157 -0
  62. package/dist/lib/browser/drivers/local.js +45 -4
  63. package/dist/lib/browser/drivers/ssh.js +2 -2
  64. package/dist/lib/browser/ipc.d.ts +8 -1
  65. package/dist/lib/browser/ipc.js +37 -28
  66. package/dist/lib/browser/profiles.d.ts +16 -3
  67. package/dist/lib/browser/profiles.js +44 -4
  68. package/dist/lib/browser/service.d.ts +3 -0
  69. package/dist/lib/browser/service.js +40 -5
  70. package/dist/lib/browser/types.d.ts +11 -4
  71. package/dist/lib/cli-resources.d.ts +137 -0
  72. package/dist/lib/cli-resources.js +477 -0
  73. package/dist/lib/cloud/factory.d.ts +1 -1
  74. package/dist/lib/cloud/factory.js +1 -1
  75. package/dist/lib/cloud/rush.js +5 -5
  76. package/dist/lib/command-skills.js +0 -2
  77. package/dist/lib/computer-rpc.d.ts +3 -0
  78. package/dist/lib/computer-rpc.js +53 -0
  79. package/dist/lib/daemon.js +20 -0
  80. package/dist/lib/events.d.ts +16 -2
  81. package/dist/lib/events.js +33 -2
  82. package/dist/lib/exec.d.ts +42 -13
  83. package/dist/lib/exec.js +127 -33
  84. package/dist/lib/help.js +11 -5
  85. package/dist/lib/hooks/cache.d.ts +38 -0
  86. package/dist/lib/hooks/cache.js +242 -0
  87. package/dist/lib/hooks/profile.d.ts +33 -0
  88. package/dist/lib/hooks/profile.js +129 -0
  89. package/dist/lib/hooks.d.ts +0 -10
  90. package/dist/lib/hooks.js +246 -11
  91. package/dist/lib/mcp.d.ts +15 -0
  92. package/dist/lib/mcp.js +46 -0
  93. package/dist/lib/migrate.js +1 -1
  94. package/dist/lib/overdue.d.ts +26 -0
  95. package/dist/lib/overdue.js +101 -0
  96. package/dist/lib/permissions.d.ts +13 -0
  97. package/dist/lib/permissions.js +55 -1
  98. package/dist/lib/plugin-marketplace.js +1 -1
  99. package/dist/lib/plugins.js +15 -1
  100. package/dist/lib/profiles-presets.d.ts +26 -0
  101. package/dist/lib/profiles-presets.js +216 -0
  102. package/dist/lib/profiles.d.ts +34 -0
  103. package/dist/lib/profiles.js +112 -1
  104. package/dist/lib/resources/mcp.js +37 -0
  105. package/dist/lib/resources.d.ts +1 -1
  106. package/dist/lib/rotate.js +10 -4
  107. package/dist/lib/routines-format.d.ts +47 -0
  108. package/dist/lib/routines-format.js +194 -0
  109. package/dist/lib/routines.d.ts +8 -2
  110. package/dist/lib/routines.js +34 -14
  111. package/dist/lib/runner.js +83 -15
  112. package/dist/lib/scheduler.js +8 -1
  113. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  114. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  115. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  116. package/dist/lib/secrets/bundles.d.ts +34 -17
  117. package/dist/lib/secrets/bundles.js +210 -36
  118. package/dist/lib/secrets/index.d.ts +49 -30
  119. package/dist/lib/secrets/index.js +126 -115
  120. package/dist/lib/secrets/install-helper.d.ts +45 -0
  121. package/dist/lib/secrets/install-helper.js +165 -0
  122. package/dist/lib/secrets/linux.js +4 -4
  123. package/dist/lib/secrets/sync.d.ts +56 -0
  124. package/dist/lib/secrets/sync.js +180 -0
  125. package/dist/lib/session/active.d.ts +8 -0
  126. package/dist/lib/session/active.js +3 -2
  127. package/dist/lib/session/db.d.ts +0 -4
  128. package/dist/lib/session/db.js +0 -26
  129. package/dist/lib/session/parse.d.ts +1 -0
  130. package/dist/lib/session/parse.js +44 -0
  131. package/dist/lib/session/render.js +4 -4
  132. package/dist/lib/session/types.d.ts +2 -2
  133. package/dist/lib/session/types.js +1 -1
  134. package/dist/lib/shims.d.ts +5 -2
  135. package/dist/lib/shims.js +70 -38
  136. package/dist/lib/state.d.ts +14 -2
  137. package/dist/lib/state.js +51 -20
  138. package/dist/lib/teams/agents.d.ts +5 -4
  139. package/dist/lib/teams/agents.js +48 -22
  140. package/dist/lib/teams/api.d.ts +2 -1
  141. package/dist/lib/teams/api.js +4 -3
  142. package/dist/lib/teams/parsers.d.ts +1 -1
  143. package/dist/lib/teams/parsers.js +153 -3
  144. package/dist/lib/teams/summarizer.js +18 -2
  145. package/dist/lib/teams/worktree.js +14 -3
  146. package/dist/lib/types.d.ts +63 -4
  147. package/dist/lib/types.js +8 -3
  148. package/dist/lib/usage.d.ts +27 -2
  149. package/dist/lib/usage.js +100 -17
  150. package/dist/lib/versions.d.ts +45 -3
  151. package/dist/lib/versions.js +455 -60
  152. package/package.json +15 -14
  153. package/scripts/install-helper.js +97 -0
  154. package/scripts/postinstall.js +16 -0
  155. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  156. package/npm-shrinkwrap.json +0 -3162
@@ -15,6 +15,9 @@ export declare function resolveLogPath(): string;
15
15
  export declare function resolvePolicyPath(): string;
16
16
  export declare function loadComputerAllowList(): string[];
17
17
  export declare function writeComputerPolicy(allowedBundleIds: string[]): void;
18
+ export declare function resolvePeersPath(): string;
19
+ export declare function loadDefaultPeers(): string[];
20
+ export declare function writeComputerPeers(allowedExecPaths: string[]): void;
18
21
  export declare function resolveHelperExec(): string | null;
19
22
  export declare function resolveHelperApp(): string | null;
20
23
  export declare function openComputerClient(): ComputerClient;
@@ -110,6 +110,59 @@ export function writeComputerPolicy(allowedBundleIds) {
110
110
  const policy = { allow: allowedBundleIds };
111
111
  fs.writeFileSync(resolvePolicyPath(), JSON.stringify(policy, null, 2), { mode: 0o600 });
112
112
  }
113
+ // Peer-auth (F5): the helper reads a list of executable paths it will
114
+ // accept connections from. Anything else — `nc`, `/usr/bin/python3`, a
115
+ // random electron app — gets the socket closed before its first RPC.
116
+ // File mirrors computer-policy.json: JSON, mode 0600, missing/unparseable
117
+ // means deny-everything.
118
+ export function resolvePeersPath() {
119
+ return path.join(getHelpersDir(), 'computer-peers.json');
120
+ }
121
+ // Default peer set: this exact `agents` CLI binary plus Rush.app if it's
122
+ // installed. realpath() the symlink chain so we record the on-disk path
123
+ // the helper will see via proc_pidpath, not the shim path.
124
+ //
125
+ // Why path-based instead of codesign-team-id? The agents CLI is unsigned
126
+ // today (npm distribution), and even if we sign Rush.app the team-id
127
+ // check would need a separate roundtrip. Path is concrete and fast; the
128
+ // daemon already runs as the user so anyone who can swap a binary at
129
+ // these paths can do worse via other means.
130
+ export function loadDefaultPeers() {
131
+ const out = new Set();
132
+ const add = (p) => {
133
+ try {
134
+ out.add(fs.realpathSync(p));
135
+ }
136
+ catch {
137
+ out.add(p);
138
+ }
139
+ };
140
+ // The Node executable currently running the CLI. This is what
141
+ // proc_pidpath() will report when the CLI calls into the daemon.
142
+ if (process.execPath)
143
+ add(process.execPath);
144
+ // Rush.app — the consumer Electron client. Both the helper-binary and
145
+ // the main app binary are possible callers depending on how Rush wires
146
+ // the RPC client.
147
+ const rushCandidates = [
148
+ '/Applications/Rush.app/Contents/MacOS/Rush',
149
+ '/Applications/Rush.app/Contents/MacOS/Electron',
150
+ ];
151
+ for (const p of rushCandidates) {
152
+ if (fs.existsSync(p))
153
+ add(p);
154
+ }
155
+ return [...out].sort();
156
+ }
157
+ // Write the peer-auth allow list. Same mode 0600 + atomic-ish semantics
158
+ // as the policy file. The daemon picks it up at startup and on SIGHUP.
159
+ export function writeComputerPeers(allowedExecPaths) {
160
+ const dir = getHelpersDir();
161
+ if (!fs.existsSync(dir)) {
162
+ fs.mkdirSync(dir, { recursive: true });
163
+ }
164
+ fs.writeFileSync(resolvePeersPath(), JSON.stringify({ allow: allowedExecPaths }, null, 2), { mode: 0o600 });
165
+ }
113
166
  // Resolve the helper executable inside the dist .app bundle. Used by the
114
167
  // stdio fallback and by install-helper to find the source bundle.
115
168
  export function resolveHelperExec() {
@@ -14,6 +14,7 @@ import { getDaemonDir as getDaemonDirRoot } from './state.js';
14
14
  import { listJobs as listAllJobs } from './routines.js';
15
15
  import { JobScheduler } from './scheduler.js';
16
16
  import { executeJobDetached, monitorRunningJobs } from './runner.js';
17
+ import { detectOverdueJobs, notifyOverdue } from './overdue.js';
17
18
  import { BrowserService } from './browser/service.js';
18
19
  import { BrowserIPCServer } from './browser/ipc.js';
19
20
  const PID_FILE = 'daemon.pid';
@@ -178,6 +179,25 @@ export async function runDaemon() {
178
179
  for (const job of scheduled) {
179
180
  log('INFO', ` ${job.name} -> next: ${job.nextRun?.toISOString() || 'unknown'}`);
180
181
  }
182
+ // Backlog detection: any enabled recurring job whose most-recent expected
183
+ // fire is older than its most-recent recorded run is overdue. Happens when
184
+ // the laptop was off or the daemon crashed through a scheduled fire.
185
+ // We log it and pop a native notification — the user can review with
186
+ // `agents routines list` and run them with `agents routines catchup`.
187
+ try {
188
+ const overdue = detectOverdueJobs();
189
+ if (overdue.length > 0) {
190
+ log('WARN', `${overdue.length} routine(s) overdue:`);
191
+ for (const job of overdue) {
192
+ const last = job.lastRanAt ? job.lastRanAt.toISOString() : 'never';
193
+ log('WARN', ` ${job.name} -- expected ${job.expectedAt.toISOString()}, last ran ${last}`);
194
+ }
195
+ notifyOverdue(overdue);
196
+ }
197
+ }
198
+ catch (err) {
199
+ log('ERROR', `Overdue detection failed: ${err.message}`);
200
+ }
181
201
  // Before the BrowserService comes up, reap browser + tunnel processes
182
202
  // spawned by previous daemons that are no longer alive. Without this,
183
203
  // a daemon hard-crash (SIGKILL, OOM) would leak every browser and SSH
@@ -32,7 +32,8 @@ export interface EventPayload {
32
32
  args?: string[];
33
33
  input?: string;
34
34
  output?: string;
35
- prompt?: string;
35
+ prompt_length?: number;
36
+ prompt_sha256?: string;
36
37
  durationMs?: number;
37
38
  startupMs?: number;
38
39
  exitCode?: number;
@@ -42,6 +43,19 @@ export interface EventPayload {
42
43
  [key: string]: unknown;
43
44
  }
44
45
  export type EventRecord = EventMeta & EventPayload;
46
+ /**
47
+ * Replace a prompt string with length + short SHA so we can correlate runs
48
+ * without persisting the raw text. Returns the fields to spread into a payload.
49
+ */
50
+ export declare function redactPrompt(prompt: string | null | undefined): {
51
+ prompt_length?: number;
52
+ prompt_sha256?: string;
53
+ };
54
+ /**
55
+ * Mask argv entries that look like tokens or secret paths. Preserves structure
56
+ * for debugging but drops the sensitive substring.
57
+ */
58
+ export declare function redactArgs(args: string[] | undefined): string[] | undefined;
45
59
  /**
46
60
  * Truncate a string to maxLength, adding ellipsis if truncated.
47
61
  * Returns undefined for null/undefined input.
@@ -124,7 +138,7 @@ export declare function emitError(err: Error | string, payload?: EventPayload):
124
138
  * Remove log files older than the retention period.
125
139
  * Called lazily on emit or explicitly via CLI.
126
140
  *
127
- * @param retentionDays - Number of days to keep (default 30)
141
+ * @param retentionDays - Number of days to keep (default 7, from DEFAULT_RETENTION_DAYS)
128
142
  * @returns Number of files removed
129
143
  */
130
144
  export declare function rotate(retentionDays?: number): number;
@@ -14,11 +14,12 @@
14
14
  import * as fs from 'fs';
15
15
  import * as path from 'path';
16
16
  import * as os from 'os';
17
+ import { createHash } from 'node:crypto';
17
18
  // ─── Constants ────────────────────────────────────────────────────────────────
18
19
  // Logs live under the cache bucket — they're regenerable telemetry.
19
20
  const LOGS_DIR = path.join(os.homedir(), '.agents', '.cache', 'logs');
20
21
  /** Default retention period in days. */
21
- const DEFAULT_RETENTION_DAYS = 30;
22
+ const DEFAULT_RETENTION_DAYS = 7;
22
23
  /** Default max length for truncated strings. */
23
24
  const DEFAULT_TRUNCATE_LENGTH = 500;
24
25
  /** Environment variable to disable event logging. */
@@ -68,6 +69,36 @@ function ensureLogsDir() {
68
69
  }
69
70
  }
70
71
  }
72
+ // ─── Redaction ────────────────────────────────────────────────────────────────
73
+ /**
74
+ * Replace a prompt string with length + short SHA so we can correlate runs
75
+ * without persisting the raw text. Returns the fields to spread into a payload.
76
+ */
77
+ export function redactPrompt(prompt) {
78
+ if (prompt == null)
79
+ return {};
80
+ return {
81
+ prompt_length: prompt.length,
82
+ prompt_sha256: createHash('sha256').update(prompt).digest('hex').slice(0, 16),
83
+ };
84
+ }
85
+ const TOKEN_LIKE = /(sk_(?:live|test)_|pk_(?:live|test)_|ghp_|gho_|ghu_|ghs_|xox[bpars]-|AKIA|ASIA|AIza|Bearer\s+|eyJ[A-Za-z0-9_-]+\.)/i;
86
+ const SECRET_PATH = /\/(secrets|credentials|\.env|user\.yaml)\b/i;
87
+ /**
88
+ * Mask argv entries that look like tokens or secret paths. Preserves structure
89
+ * for debugging but drops the sensitive substring.
90
+ */
91
+ export function redactArgs(args) {
92
+ if (!args)
93
+ return undefined;
94
+ return args.map(a => {
95
+ if (typeof a !== 'string')
96
+ return a;
97
+ if (TOKEN_LIKE.test(a) || SECRET_PATH.test(a))
98
+ return '[REDACTED]';
99
+ return a;
100
+ });
101
+ }
71
102
  // ─── Truncation ───────────────────────────────────────────────────────────────
72
103
  /**
73
104
  * Truncate a string to maxLength, adding ellipsis if truncated.
@@ -324,7 +355,7 @@ export function emitError(err, payload = {}) {
324
355
  * Remove log files older than the retention period.
325
356
  * Called lazily on emit or explicitly via CLI.
326
357
  *
327
- * @param retentionDays - Number of days to keep (default 30)
358
+ * @param retentionDays - Number of days to keep (default 7, from DEFAULT_RETENTION_DAYS)
328
359
  * @returns Number of files removed
329
360
  */
330
361
  export function rotate(retentionDays = DEFAULT_RETENTION_DAYS) {
@@ -1,6 +1,28 @@
1
- import type { AgentId } from './types.js';
2
- /** Agent execution modes controlling tool access and autonomy level. */
3
- export type ExecMode = 'plan' | 'edit' | 'full' | 'auto';
1
+ import type { AgentId, Mode } from './types.js';
2
+ /**
3
+ * Agent execution modes. Canonical name `skip` (dangerously skip permissions);
4
+ * `full` is accepted as a permanent silent alias via normalizeMode().
5
+ */
6
+ export type ExecMode = Mode;
7
+ /**
8
+ * Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
9
+ *
10
+ * Accepts the historical `full` spelling and rewrites it to `skip`. Throws on
11
+ * anything outside the four canonical values so bad input fails loud at the
12
+ * boundary rather than silently picking a wrong code path.
13
+ */
14
+ export declare function normalizeMode(input: string | null | undefined): Mode;
15
+ /**
16
+ * Resolve a requested mode against an agent's capability table.
17
+ *
18
+ * - `auto` on an agent without auto support silently degrades to `edit`
19
+ * (every agent supports edit-like behavior as its default).
20
+ * - `skip` on an agent without skip support throws with a clear message
21
+ * naming the agent's supported modes. No silent fallback — the user
22
+ * explicitly asked to bypass permissions; pretending we did is unsafe.
23
+ * - `plan` on an agent without plan support throws the same way.
24
+ */
25
+ export declare function resolveMode(agent: AgentId, requested: Mode): Mode;
4
26
  /** Reasoning effort levels passed to agents that support them. 'auto' defers to the agent's default. */
5
27
  export type ExecEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
6
28
  /** Options for spawning an agent process. Omitting `prompt` launches the CLI interactively. */
@@ -27,26 +49,33 @@ export interface ExecOptions {
27
49
  export declare function parseExecEnv(entries: string[]): Record<string, string> | undefined;
28
50
  /**
29
51
  * Build the process environment for an agent invocation.
30
- * Pins CLAUDE_CONFIG_DIR for Claude and CODEX_HOME for Codex; strips the
31
- * other agent's env var so it doesn't leak into unrelated invocations.
52
+ * Pins CLAUDE_CONFIG_DIR for Claude, CODEX_HOME for Codex, and COPILOT_HOME
53
+ * for GitHub Copilot; strips the other agents' env vars so they don't leak
54
+ * into unrelated invocations.
32
55
  */
33
56
  export declare function buildExecEnv(options: ExecOptions): NodeJS.ProcessEnv;
34
- /** Describes how to translate ExecOptions into CLI arguments for a specific agent. */
57
+ /**
58
+ * Describes how to translate ExecOptions into CLI arguments for a specific agent.
59
+ *
60
+ * `modeFlags` only declares modes this agent natively supports. Keys must agree
61
+ * with AGENTS[agent].capabilities.modes — resolveMode() routes a request to a
62
+ * supported mode (or throws), then buildExecCommand looks up the flags here.
63
+ */
35
64
  export interface AgentCommandTemplate {
36
65
  base: string[];
37
66
  promptFlag: 'positional' | string;
38
- modeFlags: {
39
- plan: string[];
40
- edit: string[];
41
- full: string[];
42
- auto?: string[];
43
- };
67
+ modeFlags: Partial<Record<Mode, string[]>>;
44
68
  jsonFlags?: string[];
45
69
  modelFlag?: string;
46
70
  printFlags?: string[];
47
71
  verboseFlag?: string;
48
72
  }
49
- /** CLI command templates for every supported agent. */
73
+ /**
74
+ * CLI command templates for every supported agent.
75
+ *
76
+ * Each agent's `modeFlags` keys MUST match the modes listed in
77
+ * AGENTS[agent].capabilities.modes. A test in exec.test.ts asserts this.
78
+ */
50
79
  export declare const AGENT_COMMANDS: Record<AgentId, AgentCommandTemplate>;
51
80
  /** Assemble the full CLI argument array for an agent invocation. */
52
81
  export declare function buildExecCommand(options: ExecOptions): string[];
package/dist/lib/exec.js CHANGED
@@ -7,10 +7,52 @@
7
7
  import { spawn } from 'child_process';
8
8
  import { randomUUID } from 'crypto';
9
9
  import * as path from 'path';
10
+ import { ALL_MODES } from './types.js';
11
+ import { AGENTS } from './agents.js';
10
12
  import { parseTimeout } from './routines.js';
11
13
  import { getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
12
14
  import { resolveModel, buildReasoningFlags } from './models.js';
13
- import { maybeRotate, createTimer, truncate } from './events.js';
15
+ import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
16
+ import { sanitizeProcessEnv } from './secrets/bundles.js';
17
+ /**
18
+ * Map a raw mode string (CLI flag, YAML field, env var) to the canonical Mode.
19
+ *
20
+ * Accepts the historical `full` spelling and rewrites it to `skip`. Throws on
21
+ * anything outside the four canonical values so bad input fails loud at the
22
+ * boundary rather than silently picking a wrong code path.
23
+ */
24
+ export function normalizeMode(input) {
25
+ if (!input) {
26
+ throw new Error(`Mode is required. Use one of: ${ALL_MODES.join(', ')}.`);
27
+ }
28
+ const v = input.trim().toLowerCase();
29
+ if (v === 'full')
30
+ return 'skip';
31
+ if (ALL_MODES.includes(v))
32
+ return v;
33
+ throw new Error(`Invalid mode '${input}'. Use one of: ${ALL_MODES.join(', ')} (or 'full' as a deprecated alias for 'skip').`);
34
+ }
35
+ /**
36
+ * Resolve a requested mode against an agent's capability table.
37
+ *
38
+ * - `auto` on an agent without auto support silently degrades to `edit`
39
+ * (every agent supports edit-like behavior as its default).
40
+ * - `skip` on an agent without skip support throws with a clear message
41
+ * naming the agent's supported modes. No silent fallback — the user
42
+ * explicitly asked to bypass permissions; pretending we did is unsafe.
43
+ * - `plan` on an agent without plan support throws the same way.
44
+ */
45
+ export function resolveMode(agent, requested) {
46
+ const supported = AGENTS[agent].capabilities.modes;
47
+ if (supported.includes(requested))
48
+ return requested;
49
+ if (requested === 'auto') {
50
+ // Fall back to edit — guaranteed to exist on every agent (every agent has
51
+ // at least 'edit' in its modes table, since that's the default behavior).
52
+ return 'edit';
53
+ }
54
+ throw new Error(`${agent} does not support '${requested}' mode. Supported modes: ${supported.join(', ')}.`);
55
+ }
14
56
  /** Pattern for valid environment variable names (C identifier rules). */
15
57
  const EXEC_ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
16
58
  /** Parse a single KEY=VALUE string into a tuple, validating the key name. */
@@ -35,11 +77,12 @@ export function parseExecEnv(entries) {
35
77
  }
36
78
  /**
37
79
  * Build the process environment for an agent invocation.
38
- * Pins CLAUDE_CONFIG_DIR for Claude and CODEX_HOME for Codex; strips the
39
- * other agent's env var so it doesn't leak into unrelated invocations.
80
+ * Pins CLAUDE_CONFIG_DIR for Claude, CODEX_HOME for Codex, and COPILOT_HOME
81
+ * for GitHub Copilot; strips the other agents' env vars so they don't leak
82
+ * into unrelated invocations.
40
83
  */
41
84
  export function buildExecEnv(options) {
42
- const result = { ...process.env };
85
+ const result = { ...sanitizeProcessEnv(process.env) };
43
86
  // Config-dir env vars are agent-specific. When the caller is running inside
44
87
  // an agent-managed shell, process.env already carries one; spreading into a
45
88
  // different agent's env would leak a config pointer the target CLI doesn't
@@ -56,6 +99,7 @@ export function buildExecEnv(options) {
56
99
  result.CLAUDE_CONFIG_DIR = path.join(getVersionHomePath('claude', version), '.claude');
57
100
  }
58
101
  delete result.CODEX_HOME;
102
+ delete result.COPILOT_HOME;
59
103
  }
60
104
  else if (options.agent === 'codex') {
61
105
  const cwd = options.cwd || process.cwd();
@@ -67,17 +111,39 @@ export function buildExecEnv(options) {
67
111
  result.CODEX_HOME = path.join(getVersionHomePath('codex', version), '.codex');
68
112
  }
69
113
  delete result.CLAUDE_CONFIG_DIR;
114
+ delete result.COPILOT_HOME;
115
+ }
116
+ else if (options.agent === 'copilot') {
117
+ // Copilot honors COPILOT_HOME (relocates ~/.copilot, including settings,
118
+ // mcp-config.json, sessions, logs). Pin it at the per-version home so
119
+ // version switches isolate MCP servers, auth, and session history.
120
+ const cwd = options.cwd || process.cwd();
121
+ const resolvedVersion = options.version ?? resolveVersion('copilot', cwd);
122
+ const version = options.version
123
+ ? resolvedVersion
124
+ : (resolvedVersion && isVersionInstalled('copilot', resolvedVersion) ? resolvedVersion : null);
125
+ if (version) {
126
+ result.COPILOT_HOME = path.join(getVersionHomePath('copilot', version), '.copilot');
127
+ }
128
+ delete result.CLAUDE_CONFIG_DIR;
129
+ delete result.CODEX_HOME;
70
130
  }
71
131
  else {
72
132
  delete result.CLAUDE_CONFIG_DIR;
73
133
  delete result.CODEX_HOME;
134
+ delete result.COPILOT_HOME;
74
135
  }
75
136
  return {
76
137
  ...result,
77
138
  ...options.env,
78
139
  };
79
140
  }
80
- /** CLI command templates for every supported agent. */
141
+ /**
142
+ * CLI command templates for every supported agent.
143
+ *
144
+ * Each agent's `modeFlags` keys MUST match the modes listed in
145
+ * AGENTS[agent].capabilities.modes. A test in exec.test.ts asserts this.
146
+ */
81
147
  export const AGENT_COMMANDS = {
82
148
  claude: {
83
149
  base: ['claude'],
@@ -85,8 +151,8 @@ export const AGENT_COMMANDS = {
85
151
  modeFlags: {
86
152
  plan: ['--permission-mode', 'plan'],
87
153
  edit: ['--permission-mode', 'acceptEdits'],
88
- full: ['--dangerously-skip-permissions'],
89
154
  auto: ['--permission-mode', 'auto'],
155
+ skip: ['--dangerously-skip-permissions'],
90
156
  },
91
157
  jsonFlags: ['--output-format', 'stream-json', '--verbose'],
92
158
  modelFlag: '--model',
@@ -97,9 +163,13 @@ export const AGENT_COMMANDS = {
97
163
  base: ['codex', 'exec'],
98
164
  promptFlag: 'positional',
99
165
  modeFlags: {
166
+ // NOTE: codex has no read-only mode in --sandbox; 'plan' here means
167
+ // "workspace-write but no auto-approval" — closer to plan-as-restraint.
168
+ // True read-only requires --sandbox read-only which we haven't wired.
100
169
  plan: ['--sandbox', 'workspace-write'],
101
170
  edit: ['--sandbox', 'workspace-write', '--full-auto'],
102
- full: ['--full-auto'],
171
+ // skip drops the sandbox entirely; --full-auto then approves anything.
172
+ skip: ['--full-auto'],
103
173
  },
104
174
  jsonFlags: ['--json'],
105
175
  modelFlag: '--model',
@@ -108,9 +178,9 @@ export const AGENT_COMMANDS = {
108
178
  base: ['gemini'],
109
179
  promptFlag: 'positional',
110
180
  modeFlags: {
111
- plan: [],
112
- edit: ['--yolo'],
113
- full: ['--yolo'],
181
+ plan: ['--approval-mode', 'plan'],
182
+ edit: ['--approval-mode', 'auto_edit'],
183
+ skip: ['--yolo'],
114
184
  },
115
185
  jsonFlags: ['--output-format', 'stream-json'],
116
186
  modelFlag: '--model',
@@ -119,9 +189,9 @@ export const AGENT_COMMANDS = {
119
189
  base: ['cursor-agent'],
120
190
  promptFlag: '-p',
121
191
  modeFlags: {
122
- plan: [],
123
- edit: ['-f'],
124
- full: ['-f'],
192
+ // cursor-agent has no read-only flag; we only expose edit + skip.
193
+ edit: [],
194
+ skip: ['-f'],
125
195
  },
126
196
  jsonFlags: ['--output-format', 'stream-json'],
127
197
  modelFlag: '--model',
@@ -132,7 +202,6 @@ export const AGENT_COMMANDS = {
132
202
  modeFlags: {
133
203
  plan: ['--agent', 'plan'],
134
204
  edit: ['--agent', 'build'],
135
- full: ['--agent', 'build'],
136
205
  },
137
206
  jsonFlags: ['--format', 'json'],
138
207
  modelFlag: '--model',
@@ -143,19 +212,32 @@ export const AGENT_COMMANDS = {
143
212
  modeFlags: {
144
213
  plan: ['--mode', 'plan'],
145
214
  edit: ['--mode', 'edit'],
146
- full: ['--mode', 'full'],
215
+ skip: ['--mode', 'full'],
147
216
  },
148
217
  jsonFlags: ['--output-format', 'stream-json'],
149
218
  modelFlag: '--model',
150
219
  },
220
+ // GitHub Copilot CLI (`@github/copilot`, GA 2026-02-25). Flags verified
221
+ // against `copilot --help` from v0.0.413+:
222
+ // -p, --prompt <text> non-interactive one-shot
223
+ // --mode <interactive|plan|autopilot>
224
+ // --autopilot start in autopilot (smart-classifier) mode
225
+ // --allow-all-tools required for non-interactive tool exec
226
+ // --allow-all (alias --yolo) tools + paths + URLs
227
+ // --output-format <text|json> json => JSONL, one object per line
228
+ // --model <model>
229
+ // Plan mode is read-only so it does not need an allow-tools grant; edit
230
+ // needs --allow-all-tools so headless runs don't stall on prompts.
151
231
  copilot: {
152
232
  base: ['copilot'],
153
- promptFlag: 'positional',
233
+ promptFlag: '-p',
154
234
  modeFlags: {
155
- plan: [],
156
- edit: [],
157
- full: [],
235
+ plan: ['--mode', 'plan'],
236
+ edit: ['--allow-all-tools'],
237
+ auto: ['--autopilot'],
238
+ skip: ['--allow-all'],
158
239
  },
240
+ jsonFlags: ['--output-format', 'json'],
159
241
  modelFlag: '--model',
160
242
  },
161
243
  amp: {
@@ -164,7 +246,6 @@ export const AGENT_COMMANDS = {
164
246
  modeFlags: {
165
247
  plan: ['--mode', 'plan'],
166
248
  edit: ['--mode', 'edit'],
167
- full: ['--mode', 'edit'],
168
249
  },
169
250
  modelFlag: '--model',
170
251
  },
@@ -172,9 +253,8 @@ export const AGENT_COMMANDS = {
172
253
  base: ['kiro-cli'],
173
254
  promptFlag: 'positional',
174
255
  modeFlags: {
175
- plan: [],
256
+ // kiro-cli has no permission flags — edit is the default behavior.
176
257
  edit: [],
177
- full: [],
178
258
  },
179
259
  modelFlag: '--model',
180
260
  },
@@ -182,9 +262,8 @@ export const AGENT_COMMANDS = {
182
262
  base: ['goose', 'run'],
183
263
  promptFlag: 'positional',
184
264
  modeFlags: {
185
- plan: [],
265
+ // goose has no permission flags — edit is the default behavior.
186
266
  edit: [],
187
- full: [],
188
267
  },
189
268
  },
190
269
  roo: {
@@ -193,27 +272,33 @@ export const AGENT_COMMANDS = {
193
272
  modeFlags: {
194
273
  plan: ['--mode', 'architect'],
195
274
  edit: ['--mode', 'code'],
196
- full: ['--mode', 'code'],
197
275
  },
198
276
  modelFlag: '--model',
199
277
  },
278
+ // TODO: --output-format json is documented but currently broken upstream
279
+ // ("flags provided but not defined: -output-format"). Track resolution at
280
+ // https://github.com/google-antigravity/antigravity-cli/issues/7 before
281
+ // adding `jsonFlags` here.
200
282
  antigravity: {
201
283
  base: ['agy'],
202
284
  promptFlag: 'positional',
203
285
  modeFlags: {
204
- plan: [],
286
+ // agy --help shows no plan/edit flags; default behavior is edit-like
287
+ // (prompts on tool use). Only skip has an explicit flag.
205
288
  edit: [],
206
- full: [],
289
+ skip: ['--dangerously-skip-permissions'],
207
290
  },
291
+ printFlags: ['--print'],
208
292
  modelFlag: '--model',
209
293
  },
210
294
  grok: {
211
295
  base: ['grok'],
212
296
  promptFlag: '-p',
213
297
  modeFlags: {
214
- plan: [],
298
+ // grok --help lists `--permission-mode plan`; the TUI defaults to ask.
299
+ plan: ['--permission-mode', 'plan'],
215
300
  edit: [],
216
- full: [],
301
+ skip: ['--always-approve'],
217
302
  },
218
303
  jsonFlags: ['--output-format', 'streaming-json'],
219
304
  modelFlag: '--model',
@@ -248,8 +333,17 @@ export function buildExecCommand(options) {
248
333
  }
249
334
  }
250
335
  }
251
- // Add mode flags. 'auto' is only defined for claude; other agents fall back to edit flags.
252
- const modeFlags = template.modeFlags[options.mode] ?? template.modeFlags.edit;
336
+ // Resolve the requested mode against the agent's capability table.
337
+ // - `auto` on an agent without auto support → silently degrades to `edit`
338
+ // - `skip`/`plan` on an unsupported agent → throws a clear error
339
+ // After resolveMode, the chosen mode is guaranteed to be in template.modeFlags.
340
+ const resolvedMode = resolveMode(options.agent, normalizeMode(options.mode));
341
+ const modeFlags = template.modeFlags[resolvedMode];
342
+ if (!modeFlags) {
343
+ // Defense in depth: would only fire if AGENTS.capabilities.modes and
344
+ // AGENT_COMMANDS.modeFlags drifted apart. Tests assert they agree.
345
+ throw new Error(`Internal error: ${options.agent} declares '${resolvedMode}' in capabilities.modes but has no entry in AGENT_COMMANDS.modeFlags.${resolvedMode}.`);
346
+ }
253
347
  cmd.push(...modeFlags);
254
348
  // Add print/headless flags only when a prompt is provided. Without a prompt
255
349
  // the caller wants an interactive REPL -- passing --print would immediately
@@ -335,9 +429,9 @@ async function spawnAgent(options) {
335
429
  model: options.model,
336
430
  interactive,
337
431
  sessionId: options.sessionId,
338
- prompt: truncate(options.prompt, 200),
432
+ ...redactPrompt(options.prompt),
339
433
  command: executable,
340
- args: args.slice(0, 10),
434
+ args: redactArgs(args.slice(0, 10)),
341
435
  });
342
436
  return new Promise((resolve, reject) => {
343
437
  // Interactive mode inherits all stdio so the CLI owns the TTY (TUI
package/dist/lib/help.js CHANGED
@@ -47,15 +47,21 @@ function formatHelpCommandsFirst(cmd, helper) {
47
47
  const helpWidth = helper.helpWidth || 80;
48
48
  const itemIndentWidth = 2;
49
49
  const itemSeparatorWidth = 2;
50
+ // commander v15 dropped `Help.wrap(str, width, indent)` in favor of
51
+ // `boxWrap(str, width)` plus a built-in `formatItem(term, termWidth,
52
+ // description, helper)` that handles the term-pad + continuation-indent
53
+ // math we used to do by hand. Delegate to it so callers get the same
54
+ // continuation-line alignment under the description column.
50
55
  function formatItem(term, description) {
51
56
  if (description) {
52
- const fullText = `${term.padEnd(termWidth + itemSeparatorWidth)}${description}`;
53
- return helper.wrap(fullText, helpWidth - itemIndentWidth, termWidth + itemSeparatorWidth);
57
+ return helper.formatItem(term, termWidth, description, helper);
54
58
  }
55
- return term;
59
+ return ' '.repeat(itemIndentWidth) + term;
56
60
  }
57
61
  function formatList(textArray) {
58
- return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth));
62
+ // formatItem already prefixes each item with its 2-space indent, so just
63
+ // join. Single-line items (no description) are indented above.
64
+ return textArray.join('\n');
59
65
  }
60
66
  // Drop arguments flagged as hidden (deprecation / compat slots) from both
61
67
  // the Usage line and the Arguments section. Commander v12's Argument lacks
@@ -81,7 +87,7 @@ function formatHelpCommandsFirst(cmd, helper) {
81
87
  let output = [`Usage: ${usageLine}`, ''];
82
88
  const commandDescription = helper.commandDescription(cmd);
83
89
  if (commandDescription.length > 0) {
84
- output = output.concat([helper.wrap(commandDescription, helpWidth, 0), '']);
90
+ output = output.concat([helper.boxWrap(commandDescription, helpWidth), '']);
85
91
  }
86
92
  const sections = helpSectionRegistry.get(cmd);
87
93
  if (sections?.examples) {
@@ -0,0 +1,38 @@
1
+ import type { HookCache, HookCacheConfig } from '../types.js';
2
+ /**
3
+ * Parse a `cache:` value from hooks.yaml into the canonical config form.
4
+ * Accepts the shorthand string ("5m", "30s-bg") or the full object form.
5
+ * Returns null if the value is missing or unparseable.
6
+ */
7
+ export declare function parseCacheConfig(raw: HookCache | undefined): HookCacheConfig | null;
8
+ /** Parse "30s" | "5m" | "1h" | plain seconds. Returns seconds, or null on failure. */
9
+ export declare function parseDuration(d: number | string | undefined): number | null;
10
+ /** Absolute path of the generated shim for a hook name. */
11
+ export declare function getHookShimPath(name: string): string;
12
+ /**
13
+ * Optional path overrides for tests that need to redirect cache + logs to a
14
+ * temp dir. Production callers omit `paths`; the shim uses real state.ts dirs.
15
+ * (state.ts captures HOME at module load, so mutating process.env.HOME in a
16
+ * test's beforeEach doesn't reach getHookCacheDir() — this is the explicit
17
+ * seam.)
18
+ */
19
+ export interface HookShimPaths {
20
+ shimsDir?: string;
21
+ cacheDir?: string;
22
+ logsDir?: string;
23
+ }
24
+ /**
25
+ * Generate (or refresh) the shim script for a hook. Idempotent — only writes
26
+ * when the content differs from what's on disk. Returns the absolute shim path.
27
+ */
28
+ export declare function generateHookShim(args: {
29
+ name: string;
30
+ scriptPath: string;
31
+ cache: HookCacheConfig;
32
+ paths?: HookShimPaths;
33
+ }): string;
34
+ /**
35
+ * Remove a hook's shim. Called by the registrar's garbage collection when a
36
+ * hook is renamed/deleted or has its `cache:` field removed.
37
+ */
38
+ export declare function removeHookShim(name: string, shimsDir?: string): void;