@phnx-labs/agents-cli 1.20.0 → 1.20.4

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 (111) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +4 -4
  3. package/dist/commands/cli.js +3 -3
  4. package/dist/commands/cloud.js +1 -1
  5. package/dist/commands/commands.js +24 -7
  6. package/dist/commands/exec.js +36 -16
  7. package/dist/commands/feedback.d.ts +7 -0
  8. package/dist/commands/feedback.js +89 -0
  9. package/dist/commands/helper.d.ts +12 -0
  10. package/dist/commands/helper.js +87 -0
  11. package/dist/commands/hooks.js +86 -7
  12. package/dist/commands/import.js +90 -37
  13. package/dist/commands/mcp.js +166 -10
  14. package/dist/commands/packages.js +196 -27
  15. package/dist/commands/permissions.js +21 -6
  16. package/dist/commands/profiles.d.ts +8 -0
  17. package/dist/commands/profiles.js +117 -4
  18. package/dist/commands/pull.js +4 -4
  19. package/dist/commands/routines.js +6 -6
  20. package/dist/commands/rules.js +8 -4
  21. package/dist/commands/secrets-migrate.d.ts +24 -0
  22. package/dist/commands/secrets-migrate.js +198 -0
  23. package/dist/commands/secrets-sync.d.ts +11 -0
  24. package/dist/commands/secrets-sync.js +155 -0
  25. package/dist/commands/secrets.js +74 -39
  26. package/dist/commands/skills.js +22 -5
  27. package/dist/commands/subagents.js +69 -49
  28. package/dist/commands/teams.js +48 -10
  29. package/dist/commands/utils.d.ts +33 -0
  30. package/dist/commands/utils.js +139 -0
  31. package/dist/commands/versions.js +4 -4
  32. package/dist/commands/view.d.ts +6 -0
  33. package/dist/commands/view.js +169 -8
  34. package/dist/commands/workflows.js +29 -6
  35. package/dist/index.js +4 -0
  36. package/dist/lib/acp/client.js +6 -1
  37. package/dist/lib/agents.d.ts +4 -0
  38. package/dist/lib/agents.js +41 -17
  39. package/dist/lib/auto-pull-worker.js +18 -1
  40. package/dist/lib/browser/chrome.js +4 -0
  41. package/dist/lib/browser/drivers/ssh.js +1 -1
  42. package/dist/lib/browser/profiles.d.ts +3 -3
  43. package/dist/lib/browser/profiles.js +3 -3
  44. package/dist/lib/browser/service.js +19 -0
  45. package/dist/lib/browser/types.d.ts +4 -4
  46. package/dist/lib/cli-resources.d.ts +36 -8
  47. package/dist/lib/cli-resources.js +268 -46
  48. package/dist/lib/cloud/factory.d.ts +1 -1
  49. package/dist/lib/cloud/factory.js +1 -1
  50. package/dist/lib/events.d.ts +16 -2
  51. package/dist/lib/events.js +33 -2
  52. package/dist/lib/exec.d.ts +39 -11
  53. package/dist/lib/exec.js +90 -31
  54. package/dist/lib/help.js +11 -5
  55. package/dist/lib/hooks/cache.d.ts +38 -0
  56. package/dist/lib/hooks/cache.js +242 -0
  57. package/dist/lib/hooks/profile.d.ts +33 -0
  58. package/dist/lib/hooks/profile.js +129 -0
  59. package/dist/lib/hooks.d.ts +0 -10
  60. package/dist/lib/hooks.js +68 -15
  61. package/dist/lib/import.d.ts +21 -0
  62. package/dist/lib/import.js +55 -2
  63. package/dist/lib/mcp.d.ts +15 -0
  64. package/dist/lib/mcp.js +40 -0
  65. package/dist/lib/permissions.d.ts +13 -0
  66. package/dist/lib/permissions.js +51 -1
  67. package/dist/lib/plugin-marketplace.d.ts +10 -0
  68. package/dist/lib/plugin-marketplace.js +47 -1
  69. package/dist/lib/plugins.js +15 -1
  70. package/dist/lib/profiles-presets.d.ts +26 -0
  71. package/dist/lib/profiles-presets.js +187 -8
  72. package/dist/lib/profiles.d.ts +34 -0
  73. package/dist/lib/profiles.js +112 -1
  74. package/dist/lib/pty-server.js +27 -3
  75. package/dist/lib/routines-format.d.ts +17 -5
  76. package/dist/lib/routines-format.js +37 -16
  77. package/dist/lib/routines.d.ts +1 -1
  78. package/dist/lib/routines.js +2 -2
  79. package/dist/lib/runner.js +64 -10
  80. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  81. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  82. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  83. package/dist/lib/secrets/bundles.d.ts +18 -22
  84. package/dist/lib/secrets/bundles.js +75 -99
  85. package/dist/lib/secrets/index.d.ts +51 -27
  86. package/dist/lib/secrets/index.js +147 -156
  87. package/dist/lib/secrets/install-helper.d.ts +45 -0
  88. package/dist/lib/secrets/install-helper.js +165 -0
  89. package/dist/lib/secrets/linux.js +4 -4
  90. package/dist/lib/secrets/sync.d.ts +56 -0
  91. package/dist/lib/secrets/sync.js +180 -0
  92. package/dist/lib/session/render.js +4 -4
  93. package/dist/lib/session/types.d.ts +1 -1
  94. package/dist/lib/shims.d.ts +4 -1
  95. package/dist/lib/shims.js +5 -35
  96. package/dist/lib/state.d.ts +14 -1
  97. package/dist/lib/state.js +49 -5
  98. package/dist/lib/teams/agents.d.ts +5 -4
  99. package/dist/lib/teams/agents.js +47 -21
  100. package/dist/lib/teams/api.d.ts +2 -1
  101. package/dist/lib/teams/api.js +4 -3
  102. package/dist/lib/types.d.ts +57 -1
  103. package/dist/lib/types.js +2 -0
  104. package/dist/lib/usage.d.ts +27 -2
  105. package/dist/lib/usage.js +100 -17
  106. package/dist/lib/versions.d.ts +35 -1
  107. package/dist/lib/versions.js +288 -64
  108. package/package.json +13 -12
  109. package/scripts/install-helper.js +97 -0
  110. package/scripts/postinstall.js +16 -0
  111. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
@@ -7,12 +7,109 @@
7
7
  * but unlike skills/commands/hooks, CLI resources are NOT copied into per-agent
8
8
  * version homes — they install binaries onto the host PATH. The relationship is
9
9
  * "Brewfile-style": declare once in ~/.agents/cli/, install on any new machine.
10
+ *
11
+ * Security: every field that becomes a child-process argument is validated
12
+ * against a strict allowlist and dispatched via spawnSync with an argv array.
13
+ * Nothing here ever runs through a shell — manifests can come from project repos
14
+ * or pulled extras, so anything that would let a manifest author smuggle in
15
+ * `;`, `$(...)`, backticks, redirects, or pipe operators is a remote-code-
16
+ * execution sink.
10
17
  */
11
18
  import * as fs from 'fs';
12
- import { execSync, spawnSync } from 'child_process';
19
+ import * as os from 'os';
20
+ import * as path from 'path';
21
+ import { spawnSync } from 'child_process';
13
22
  import * as yaml from 'yaml';
14
23
  import { listResources, resolveResource } from './resources.js';
24
+ // ─── Validation primitives ───────────────────────────────────────────────────
25
+ /** Token allowed inside `check:` strings — letters, digits, underscore, dot, slash, dash. */
26
+ const SAFE_CHECK_TOKEN = /^[a-zA-Z0-9_./-]+$/;
27
+ /** npm package name with optional scope and optional version/tag. */
28
+ const NPM_PACKAGE = /^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*(@[a-zA-Z0-9._-]+)?$/;
29
+ /** Homebrew formula name (and optional tap prefix). */
30
+ const BREW_FORMULA = /^([a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*\/)?[a-z0-9][a-z0-9_.+-]*$/;
31
+ /** Path segment inside a tarball — no leading slash, no `..`, no shell metas. */
32
+ const SAFE_PATH_SEGMENT = /^[a-zA-Z0-9_./-]+$/;
33
+ function assertSafeCheckToken(tok) {
34
+ if (!SAFE_CHECK_TOKEN.test(tok)) {
35
+ throw new Error(`check contains unsafe token: ${JSON.stringify(tok)}`);
36
+ }
37
+ }
38
+ function assertNpmPackage(name) {
39
+ if (!NPM_PACKAGE.test(name)) {
40
+ throw new Error(`npm package name is not allowlisted: ${JSON.stringify(name)}`);
41
+ }
42
+ }
43
+ function assertBrewFormula(name) {
44
+ if (!BREW_FORMULA.test(name)) {
45
+ throw new Error(`brew formula name is not allowlisted: ${JSON.stringify(name)}`);
46
+ }
47
+ }
48
+ function assertHttpsUrl(url) {
49
+ let parsed;
50
+ try {
51
+ parsed = new URL(url);
52
+ }
53
+ catch {
54
+ throw new Error(`url is not parseable: ${JSON.stringify(url)}`);
55
+ }
56
+ if (parsed.protocol !== 'https:') {
57
+ throw new Error(`url must use https:// (got ${parsed.protocol}): ${JSON.stringify(url)}`);
58
+ }
59
+ }
60
+ function assertSafePathSegment(seg) {
61
+ if (!SAFE_PATH_SEGMENT.test(seg) || seg.startsWith('/') || seg.split('/').includes('..')) {
62
+ throw new Error(`extract path is not allowlisted: ${JSON.stringify(seg)}`);
63
+ }
64
+ }
15
65
  // ─── Parsing ─────────────────────────────────────────────────────────────────
66
+ /**
67
+ * Parse a `check:` field into a CheckSpec. Accepts either a structured object
68
+ * (`{ kind: 'which'|'version', cmd, args? }`) or a legacy whitespace-separated
69
+ * string. String form is split on whitespace and each token is validated against
70
+ * SAFE_CHECK_TOKEN — manifests cannot smuggle in shell metacharacters.
71
+ */
72
+ export function parseCheckSpec(raw, defaultName) {
73
+ if (raw == null) {
74
+ assertSafeCheckToken(defaultName);
75
+ return { kind: 'version', cmd: defaultName, args: ['--version'] };
76
+ }
77
+ if (typeof raw === 'string') {
78
+ const tokens = raw.trim().split(/\s+/).filter((t) => t.length > 0);
79
+ if (tokens.length === 0) {
80
+ assertSafeCheckToken(defaultName);
81
+ return { kind: 'version', cmd: defaultName, args: ['--version'] };
82
+ }
83
+ for (const tok of tokens)
84
+ assertSafeCheckToken(tok);
85
+ const [cmd, ...args] = tokens;
86
+ return args.length === 0 ? { kind: 'which', cmd } : { kind: 'version', cmd, args };
87
+ }
88
+ if (typeof raw === 'object') {
89
+ const r = raw;
90
+ const kind = r.kind;
91
+ if (kind !== 'which' && kind !== 'version') {
92
+ throw new Error(`check.kind must be "which" or "version" (got ${JSON.stringify(kind)})`);
93
+ }
94
+ if (typeof r.cmd !== 'string' || !r.cmd.trim()) {
95
+ throw new Error('check.cmd must be a non-empty string');
96
+ }
97
+ const cmd = r.cmd.trim();
98
+ assertSafeCheckToken(cmd);
99
+ if (kind === 'which')
100
+ return { kind: 'which', cmd };
101
+ const args = Array.isArray(r.args) ? r.args : [];
102
+ const safeArgs = [];
103
+ for (const a of args) {
104
+ if (typeof a !== 'string')
105
+ throw new Error('check.args entries must be strings');
106
+ assertSafeCheckToken(a);
107
+ safeArgs.push(a);
108
+ }
109
+ return { kind: 'version', cmd, args: safeArgs };
110
+ }
111
+ throw new Error('check must be a string or an object with { kind, cmd, args? }');
112
+ }
16
113
  /**
17
114
  * Parse a single CLI manifest from its YAML contents.
18
115
  * Returns a manifest on success; throws on schema violations so callers can
@@ -24,11 +121,10 @@ export function parseCliManifest(contents, opts) {
24
121
  throw new Error('manifest must be a YAML object');
25
122
  }
26
123
  const name = typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : opts.name;
124
+ assertSafeCheckToken(name);
27
125
  const description = typeof raw.description === 'string' ? raw.description : undefined;
28
126
  const homepage = typeof raw.homepage === 'string' ? raw.homepage : undefined;
29
- const check = typeof raw.check === 'string' && raw.check.trim()
30
- ? raw.check.trim()
31
- : `${name} --version`;
127
+ const check = parseCheckSpec(raw.check, name);
32
128
  const postInstall = typeof raw.post_install === 'string' ? raw.post_install : undefined;
33
129
  if (!Array.isArray(raw.install) || raw.install.length === 0) {
34
130
  throw new Error('install must be a non-empty list of methods');
@@ -44,11 +140,29 @@ export function parseCliManifest(contents, opts) {
44
140
  }
45
141
  const key = keys[0];
46
142
  const value = e[key];
47
- if (key === 'npm' || key === 'brew' || key === 'script') {
143
+ if (key === 'npm') {
144
+ if (typeof value !== 'string' || !value.trim()) {
145
+ throw new Error(`install[${i}].npm must be a non-empty string`);
146
+ }
147
+ const v = value.trim();
148
+ assertNpmPackage(v);
149
+ return { npm: v };
150
+ }
151
+ if (key === 'brew') {
152
+ if (typeof value !== 'string' || !value.trim()) {
153
+ throw new Error(`install[${i}].brew must be a non-empty string`);
154
+ }
155
+ const v = value.trim();
156
+ assertBrewFormula(v);
157
+ return { brew: v };
158
+ }
159
+ if (key === 'script') {
48
160
  if (typeof value !== 'string' || !value.trim()) {
49
- throw new Error(`install[${i}].${key} must be a non-empty string`);
161
+ throw new Error(`install[${i}].script must be a non-empty string`);
50
162
  }
51
- return { [key]: value.trim() };
163
+ const v = value.trim();
164
+ assertHttpsUrl(v);
165
+ return { script: v };
52
166
  }
53
167
  if (key === 'binary') {
54
168
  if (!value || typeof value !== 'object') {
@@ -63,10 +177,14 @@ export function parseCliManifest(contents, opts) {
63
177
  if (typeof s.url !== 'string' || !s.url.trim()) {
64
178
  throw new Error(`install[${i}].binary.${platform}.url must be a non-empty string`);
65
179
  }
66
- binary[platform] = {
67
- url: s.url.trim(),
68
- extract: typeof s.extract === 'string' ? s.extract : undefined,
69
- };
180
+ const url = s.url.trim();
181
+ assertHttpsUrl(url);
182
+ let extract;
183
+ if (typeof s.extract === 'string' && s.extract.length > 0) {
184
+ assertSafePathSegment(s.extract);
185
+ extract = s.extract;
186
+ }
187
+ binary[platform] = { url, extract };
70
188
  }
71
189
  return { binary };
72
190
  }
@@ -125,25 +243,34 @@ export function resolveCliManifest(name, cwd) {
125
243
  }
126
244
  // ─── Host detection ──────────────────────────────────────────────────────────
127
245
  /**
128
- * Return true if a command resolves on the current PATH. Uses `which` on
129
- * POSIX hosts; results are cached for the lifetime of the process.
246
+ * Return true if a command resolves on the current PATH. Uses POSIX `command -v`
247
+ * via spawn argv (no shell); results are cached for the lifetime of the process.
130
248
  */
131
249
  const cmdExistsCache = new Map();
132
250
  export function hasCommand(cmd) {
133
251
  if (cmdExistsCache.has(cmd))
134
252
  return cmdExistsCache.get(cmd);
135
- const result = spawnSync('command', ['-v', cmd], { shell: true, stdio: 'ignore' });
253
+ // `command` is a shell builtin on most POSIX shells; invoking `sh -c 'command -v X'`
254
+ // with X as an *argument* (not interpolated) is the safe path. `cmd` may be passed
255
+ // by callers that haven't validated it, so we route via argv to neutralize metas.
256
+ const result = spawnSync('sh', ['-c', 'command -v "$1" >/dev/null 2>&1', '_', cmd], {
257
+ stdio: 'ignore',
258
+ });
136
259
  const ok = result.status === 0;
137
260
  cmdExistsCache.set(cmd, ok);
138
261
  return ok;
139
262
  }
140
- /** Run the manifest's `check` command. Returns true when it exits 0. */
263
+ /**
264
+ * Run the manifest's check. Dispatches on CheckSpec.kind — never invokes a
265
+ * shell, never interpolates strings into a command line.
266
+ */
141
267
  export function isCliInstalled(manifest) {
142
- const result = spawnSync(manifest.check, {
143
- shell: true,
144
- stdio: 'ignore',
145
- timeout: 10_000,
146
- });
268
+ const c = manifest.check;
269
+ if (c.kind === 'which') {
270
+ cmdExistsCache.delete(c.cmd);
271
+ return hasCommand(c.cmd);
272
+ }
273
+ const result = spawnSync(c.cmd, c.args, { stdio: 'ignore', timeout: 10_000 });
147
274
  return result.status === 0;
148
275
  }
149
276
  // ─── Method selection ────────────────────────────────────────────────────────
@@ -167,6 +294,10 @@ export function selectInstallMethod(manifest) {
167
294
  }
168
295
  return null;
169
296
  }
297
+ /** Render a CheckSpec back to a human-readable command string (display only). */
298
+ export function describeCheck(check) {
299
+ return check.kind === 'which' ? check.cmd : `${check.cmd} ${check.args.join(' ')}`.trim();
300
+ }
170
301
  /** Short description of a method for display. */
171
302
  export function describeMethod(method) {
172
303
  if ('npm' in method)
@@ -179,6 +310,122 @@ export function describeMethod(method) {
179
310
  const spec = method.binary[key];
180
311
  return spec ? `download ${spec.url}` : 'binary download';
181
312
  }
313
+ /**
314
+ * Display-only rendering of how a method would be run, for `--dry-run` and
315
+ * status output. Not used by installCli — execution goes through runInstallMethod
316
+ * which dispatches to spawnSync with argv arrays.
317
+ */
318
+ export function buildInstallCommand(method) {
319
+ if ('npm' in method)
320
+ return `npm install -g ${method.npm}`;
321
+ if ('brew' in method)
322
+ return `brew install ${method.brew}`;
323
+ if ('script' in method) {
324
+ return hasCommand('curl')
325
+ ? `curl -fsSL ${method.script} | sh`
326
+ : `wget -qO- ${method.script} | sh`;
327
+ }
328
+ const key = `${process.platform}-${process.arch}`;
329
+ const spec = method.binary[key];
330
+ if (!spec)
331
+ return 'binary download';
332
+ return spec.extract
333
+ ? `curl -fsSL ${spec.url} -o /tmp/agents-cli-bin.tgz && tar -xzf /tmp/agents-cli-bin.tgz -C /usr/local/bin ${spec.extract}`
334
+ : `curl -fsSL ${spec.url} -o /usr/local/bin/agents-cli-downloaded`;
335
+ }
336
+ /**
337
+ * Execute an install method via spawnSync with argv arrays. Each branch
338
+ * re-validates the relevant field — defense in depth, since callers may
339
+ * construct InstallMethod values without going through parseCliManifest
340
+ * (tests, future programmatic use).
341
+ *
342
+ * For `script`, the download is staged to a temp file and then exec'd as
343
+ * `sh <file>` so we never need a shell pipe (`curl | sh`).
344
+ */
345
+ function runInstallMethod(method) {
346
+ if ('npm' in method) {
347
+ assertNpmPackage(method.npm);
348
+ const r = spawnSync('npm', ['install', '-g', method.npm], { stdio: 'inherit' });
349
+ if (r.status !== 0) {
350
+ throw new Error(`npm install -g ${method.npm} exited with status ${r.status ?? 'unknown'}`);
351
+ }
352
+ return;
353
+ }
354
+ if ('brew' in method) {
355
+ assertBrewFormula(method.brew);
356
+ const r = spawnSync('brew', ['install', method.brew], { stdio: 'inherit' });
357
+ if (r.status !== 0) {
358
+ throw new Error(`brew install ${method.brew} exited with status ${r.status ?? 'unknown'}`);
359
+ }
360
+ return;
361
+ }
362
+ if ('script' in method) {
363
+ assertHttpsUrl(method.script);
364
+ const tmp = path.join(os.tmpdir(), `agents-cli-install-${process.pid}-${Date.now()}.sh`);
365
+ try {
366
+ let dl;
367
+ if (hasCommand('curl')) {
368
+ dl = spawnSync('curl', ['-fsSL', method.script, '-o', tmp], { stdio: 'inherit' });
369
+ }
370
+ else if (hasCommand('wget')) {
371
+ dl = spawnSync('wget', ['-q', '-O', tmp, method.script], { stdio: 'inherit' });
372
+ }
373
+ else {
374
+ throw new Error('neither curl nor wget is available on PATH');
375
+ }
376
+ if (dl.status !== 0) {
377
+ throw new Error(`download of install script failed (status ${dl.status ?? 'unknown'})`);
378
+ }
379
+ const r = spawnSync('sh', [tmp], { stdio: 'inherit' });
380
+ if (r.status !== 0) {
381
+ throw new Error(`install script exited with status ${r.status ?? 'unknown'}`);
382
+ }
383
+ }
384
+ finally {
385
+ try {
386
+ fs.unlinkSync(tmp);
387
+ }
388
+ catch { /* best effort */ }
389
+ }
390
+ return;
391
+ }
392
+ if ('binary' in method) {
393
+ const key = `${process.platform}-${process.arch}`;
394
+ const spec = method.binary[key];
395
+ if (!spec)
396
+ throw new Error(`no binary declared for ${key}`);
397
+ assertHttpsUrl(spec.url);
398
+ if (spec.extract) {
399
+ assertSafePathSegment(spec.extract);
400
+ const tmp = path.join(os.tmpdir(), `agents-cli-bin-${process.pid}-${Date.now()}.tgz`);
401
+ try {
402
+ const dl = spawnSync('curl', ['-fsSL', spec.url, '-o', tmp], { stdio: 'inherit' });
403
+ if (dl.status !== 0) {
404
+ throw new Error(`binary download failed (status ${dl.status ?? 'unknown'})`);
405
+ }
406
+ const x = spawnSync('tar', ['-xzf', tmp, '-C', '/usr/local/bin', spec.extract], {
407
+ stdio: 'inherit',
408
+ });
409
+ if (x.status !== 0) {
410
+ throw new Error(`tar extract failed (status ${x.status ?? 'unknown'})`);
411
+ }
412
+ }
413
+ finally {
414
+ try {
415
+ fs.unlinkSync(tmp);
416
+ }
417
+ catch { /* best effort */ }
418
+ }
419
+ }
420
+ else {
421
+ const r = spawnSync('curl', ['-fsSL', spec.url, '-o', '/usr/local/bin/agents-cli-downloaded'], { stdio: 'inherit' });
422
+ if (r.status !== 0) {
423
+ throw new Error(`binary download failed (status ${r.status ?? 'unknown'})`);
424
+ }
425
+ }
426
+ return;
427
+ }
428
+ }
182
429
  /**
183
430
  * Install a single CLI by running its first compatible method. Streams the
184
431
  * underlying command's output to the parent terminal so users see brew/npm
@@ -197,9 +444,8 @@ export function installCli(manifest, opts = {}) {
197
444
  if (opts.dryRun) {
198
445
  return { manifest, method, installed: false, output: `[dry-run] would run: ${describeMethod(method)}` };
199
446
  }
200
- const cmd = buildInstallCommand(method);
201
447
  try {
202
- execSync(cmd, { stdio: 'inherit' });
448
+ runInstallMethod(method);
203
449
  }
204
450
  catch (err) {
205
451
  return {
@@ -216,30 +462,6 @@ export function installCli(manifest, opts = {}) {
216
462
  const installed = isCliInstalled(manifest);
217
463
  return { manifest, method, installed };
218
464
  }
219
- /**
220
- * Map a declarative method to a shell command. Centralized so tests and dry-run
221
- * surface the exact string that would execute.
222
- */
223
- export function buildInstallCommand(method) {
224
- if ('npm' in method)
225
- return `npm install -g ${method.npm}`;
226
- if ('brew' in method)
227
- return `brew install ${method.brew}`;
228
- if ('script' in method) {
229
- // Prefer curl when both are present; fall back to wget.
230
- return hasCommand('curl')
231
- ? `curl -fsSL ${method.script} | sh`
232
- : `wget -qO- ${method.script} | sh`;
233
- }
234
- const key = `${process.platform}-${process.arch}`;
235
- const spec = method.binary[key];
236
- // The downloader is intentionally minimal — binary install is mostly used
237
- // for pre-built tarballs whose extract path varies per project. We expect
238
- // the manifest author to document any post-download steps in post_install.
239
- return spec.extract
240
- ? `curl -fsSL ${spec.url} -o /tmp/agents-cli-bin.tgz && tar -xzf /tmp/agents-cli-bin.tgz -C /usr/local/bin ${spec.extract}`
241
- : `curl -fsSL ${spec.url} -o /usr/local/bin/agents-cli-downloaded`;
242
- }
243
465
  /** Convenience: list all manifests + their installed-on-host status. */
244
466
  export function listCliStatus(cwd) {
245
467
  const { manifests, errors } = listCliManifests(cwd);
@@ -8,7 +8,7 @@ import type { CloudProvider, CloudTask, CloudTaskStatus, CloudEvent, DispatchOpt
8
8
  /**
9
9
  * Factory/Droid cloud provider — stub for Phase 2.
10
10
  *
11
- * Integration path: `droid daemon` running on a remote machine (mac-mini, cloud VM, k8s pod).
11
+ * Integration path: `droid daemon` running on a remote machine (workstation, cloud VM, k8s pod).
12
12
  * Dispatch via HTTP to the daemon, stream output, cancel via HTTP DELETE.
13
13
  *
14
14
  * Not yet implemented because:
@@ -7,7 +7,7 @@
7
7
  /**
8
8
  * Factory/Droid cloud provider — stub for Phase 2.
9
9
  *
10
- * Integration path: `droid daemon` running on a remote machine (mac-mini, cloud VM, k8s pod).
10
+ * Integration path: `droid daemon` running on a remote machine (workstation, cloud VM, k8s pod).
11
11
  * Dispatch via HTTP to the daemon, stream output, cancel via HTTP DELETE.
12
12
  *
13
13
  * Not yet implemented because:
@@ -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. */
@@ -32,22 +54,28 @@ export declare function parseExecEnv(entries: string[]): Record<string, string>
32
54
  * into unrelated invocations.
33
55
  */
34
56
  export declare function buildExecEnv(options: ExecOptions): NodeJS.ProcessEnv;
35
- /** 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
+ */
36
64
  export interface AgentCommandTemplate {
37
65
  base: string[];
38
66
  promptFlag: 'positional' | string;
39
- modeFlags: {
40
- plan: string[];
41
- edit: string[];
42
- full: string[];
43
- auto?: string[];
44
- };
67
+ modeFlags: Partial<Record<Mode, string[]>>;
45
68
  jsonFlags?: string[];
46
69
  modelFlag?: string;
47
70
  printFlags?: string[];
48
71
  verboseFlag?: string;
49
72
  }
50
- /** 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
+ */
51
79
  export declare const AGENT_COMMANDS: Record<AgentId, AgentCommandTemplate>;
52
80
  /** Assemble the full CLI argument array for an agent invocation. */
53
81
  export declare function buildExecCommand(options: ExecOptions): string[];