@phnx-labs/agents-cli 1.19.2 → 1.20.0

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 (103) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +69 -9
  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/commands.js +3 -3
  8. package/dist/commands/computer.js +18 -1
  9. package/dist/commands/doctor.d.ts +1 -1
  10. package/dist/commands/doctor.js +2 -2
  11. package/dist/commands/exec.js +3 -3
  12. package/dist/commands/factory.d.ts +3 -14
  13. package/dist/commands/factory.js +3 -3
  14. package/dist/commands/hooks.js +3 -3
  15. package/dist/commands/plugins.js +11 -4
  16. package/dist/commands/profiles.js +1 -1
  17. package/dist/commands/prune.js +39 -160
  18. package/dist/commands/pull.js +56 -3
  19. package/dist/commands/routines.js +106 -13
  20. package/dist/commands/secrets.js +5 -7
  21. package/dist/commands/sessions.d.ts +28 -0
  22. package/dist/commands/sessions.js +98 -33
  23. package/dist/commands/setup.d.ts +1 -0
  24. package/dist/commands/setup.js +37 -28
  25. package/dist/commands/skills.js +3 -3
  26. package/dist/commands/teams.js +13 -0
  27. package/dist/commands/versions.d.ts +4 -3
  28. package/dist/commands/versions.js +131 -127
  29. package/dist/commands/view.js +12 -12
  30. package/dist/computer.js +0 -0
  31. package/dist/index.js +34 -6
  32. package/dist/lib/acp/harnesses.js +8 -0
  33. package/dist/lib/agents.js +110 -23
  34. package/dist/lib/browser/cdp.d.ts +8 -1
  35. package/dist/lib/browser/cdp.js +40 -3
  36. package/dist/lib/browser/chrome.d.ts +13 -0
  37. package/dist/lib/browser/chrome.js +42 -3
  38. package/dist/lib/browser/domain-skills.d.ts +51 -0
  39. package/dist/lib/browser/domain-skills.js +157 -0
  40. package/dist/lib/browser/drivers/local.js +45 -4
  41. package/dist/lib/browser/drivers/ssh.js +1 -1
  42. package/dist/lib/browser/ipc.d.ts +8 -1
  43. package/dist/lib/browser/ipc.js +37 -28
  44. package/dist/lib/browser/profiles.d.ts +13 -0
  45. package/dist/lib/browser/profiles.js +41 -1
  46. package/dist/lib/browser/service.d.ts +3 -0
  47. package/dist/lib/browser/service.js +21 -5
  48. package/dist/lib/browser/types.d.ts +7 -0
  49. package/dist/lib/cli-resources.d.ts +109 -0
  50. package/dist/lib/cli-resources.js +255 -0
  51. package/dist/lib/cloud/rush.js +5 -5
  52. package/dist/lib/command-skills.js +0 -2
  53. package/dist/lib/computer-rpc.d.ts +3 -0
  54. package/dist/lib/computer-rpc.js +53 -0
  55. package/dist/lib/daemon.js +20 -0
  56. package/dist/lib/exec.d.ts +3 -2
  57. package/dist/lib/exec.js +44 -9
  58. package/dist/lib/hooks.js +182 -0
  59. package/dist/lib/mcp.js +6 -0
  60. package/dist/lib/migrate.js +1 -1
  61. package/dist/lib/overdue.d.ts +26 -0
  62. package/dist/lib/overdue.js +101 -0
  63. package/dist/lib/permissions.js +5 -1
  64. package/dist/lib/plugin-marketplace.js +1 -1
  65. package/dist/lib/profiles-presets.js +37 -0
  66. package/dist/lib/resources/mcp.js +37 -0
  67. package/dist/lib/resources.d.ts +1 -1
  68. package/dist/lib/rotate.js +10 -4
  69. package/dist/lib/routines-format.d.ts +35 -0
  70. package/dist/lib/routines-format.js +173 -0
  71. package/dist/lib/routines.d.ts +7 -1
  72. package/dist/lib/routines.js +32 -12
  73. package/dist/lib/runner.js +19 -5
  74. package/dist/lib/scheduler.js +8 -1
  75. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  76. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  77. package/dist/lib/secrets/bundles.d.ts +22 -1
  78. package/dist/lib/secrets/bundles.js +234 -36
  79. package/dist/lib/secrets/index.d.ts +6 -11
  80. package/dist/lib/secrets/index.js +107 -87
  81. package/dist/lib/session/active.d.ts +8 -0
  82. package/dist/lib/session/active.js +3 -2
  83. package/dist/lib/session/db.d.ts +0 -4
  84. package/dist/lib/session/db.js +0 -26
  85. package/dist/lib/session/parse.d.ts +1 -0
  86. package/dist/lib/session/parse.js +44 -0
  87. package/dist/lib/session/types.d.ts +1 -1
  88. package/dist/lib/session/types.js +1 -1
  89. package/dist/lib/shims.d.ts +1 -1
  90. package/dist/lib/shims.js +66 -4
  91. package/dist/lib/state.d.ts +0 -1
  92. package/dist/lib/state.js +2 -15
  93. package/dist/lib/teams/agents.js +1 -1
  94. package/dist/lib/teams/parsers.d.ts +1 -1
  95. package/dist/lib/teams/parsers.js +153 -3
  96. package/dist/lib/teams/summarizer.js +18 -2
  97. package/dist/lib/teams/worktree.js +14 -3
  98. package/dist/lib/types.d.ts +6 -3
  99. package/dist/lib/types.js +6 -3
  100. package/dist/lib/versions.d.ts +10 -2
  101. package/dist/lib/versions.js +227 -35
  102. package/package.json +7 -7
  103. package/npm-shrinkwrap.json +0 -3162
@@ -383,17 +383,17 @@ Examples:
383
383
  .action(() => {
384
384
  console.error(chalk.red('"agents commands sync" is gone.'));
385
385
  console.error(chalk.gray('Sync runs automatically when you launch the agent.'));
386
- console.error(chalk.gray('To remove orphans, use: agents prune commands'));
386
+ console.error(chalk.gray('To remove orphans, use: agents prune cleanup commands'));
387
387
  process.exit(1);
388
388
  });
389
- // `commands prune` moved to the top-level `agents prune` command.
389
+ // `commands prune` moved to the top-level `agents prune cleanup` command.
390
390
  commandsCmd
391
391
  .command('prune', { hidden: true })
392
392
  .allowUnknownOption()
393
393
  .allowExcessArguments()
394
394
  .action(() => {
395
395
  console.error(chalk.red('"agents commands prune" moved.'));
396
- console.error(chalk.gray('Use: agents prune commands (or `agents prune` for everything)'));
396
+ console.error(chalk.gray('Use: agents prune cleanup commands (or `agents prune cleanup` for everything)'));
397
397
  process.exit(1);
398
398
  });
399
399
  commandsCmd
@@ -3,7 +3,7 @@ import * as fs from 'fs';
3
3
  import * as os from 'os';
4
4
  import * as path from 'path';
5
5
  import { registerCommandGroups } from '../lib/help.js';
6
- import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, describeTransport, loadComputerAllowList, writeComputerPolicy, } from '../lib/computer-rpc.js';
6
+ import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, resolvePeersPath, describeTransport, loadComputerAllowList, loadDefaultPeers, writeComputerPolicy, writeComputerPeers, } from '../lib/computer-rpc.js';
7
7
  // Help groups — mirror `agents browser` so the mental model carries over.
8
8
  const COMPUTER_HELP_GROUPS = [
9
9
  { title: 'Installation', names: ['install-helper'] },
@@ -46,6 +46,8 @@ function registerStatusCommand(program) {
46
46
  const previewParts = allowed.slice(0, 5);
47
47
  const previewSuffix = allowed.length > 5 ? ` (+${allowed.length - 5} more)` : '';
48
48
  console.log(`policy: ${allowed.length} app${allowed.length === 1 ? '' : 's'} allowed${allowed.length > 0 ? `: ${previewParts.join(', ')}${previewSuffix}` : ''}`);
49
+ const callers = loadDefaultPeers();
50
+ console.log(`peers: ${callers.length} caller${callers.length === 1 ? '' : 's'} (peer-auth on socket)`);
49
51
  if (!installed) {
50
52
  console.log('');
51
53
  console.log('Run: agents computer install-helper');
@@ -284,6 +286,15 @@ function registerStartCommand(program) {
284
286
  console.log(` add to ~/.agents/permissions/groups/<name>.yaml under allow:`);
285
287
  console.log(` - "Computer(com.apple.finder)"`);
286
288
  }
289
+ // Peer-auth allow list — which caller executables may connect to
290
+ // the socket. Default: this CLI's Node binary, plus Rush.app if
291
+ // installed. A `nc -U socket` from a malicious npm postinstall has
292
+ // a different exec path and gets refused at accept().
293
+ const callers = loadDefaultPeers();
294
+ writeComputerPeers(callers);
295
+ console.log(`peers: ${callers.length} caller${callers.length === 1 ? '' : 's'} allowed (${resolvePeersPath()})`);
296
+ for (const p of callers)
297
+ console.log(` ${p}`);
287
298
  // Bootout first to clear any prior registration. Best-effort.
288
299
  try {
289
300
  execFileSync('/bin/launchctl', ['bootout', domain, plistPath], { stdio: 'pipe' });
@@ -356,6 +367,12 @@ function registerReloadCommand(program) {
356
367
  const allowed = loadComputerAllowList();
357
368
  writeComputerPolicy(allowed);
358
369
  console.log(`policy: ${allowed.length} app${allowed.length === 1 ? '' : 's'} allowed (${resolvePolicyPath()})`);
370
+ // Rewrite peers list too — an upgrade of the npm-global CLI moves
371
+ // its node path; without this the reloaded daemon would reject the
372
+ // very binary that just signaled it.
373
+ const callers = loadDefaultPeers();
374
+ writeComputerPeers(callers);
375
+ console.log(`peers: ${callers.length} caller${callers.length === 1 ? '' : 's'} allowed (${resolvePeersPath()})`);
359
376
  // Resolve the daemon's pid via `launchctl list <label>`. The plist
360
377
  // output includes a "PID" key when the service is running.
361
378
  const uid = process.getuid?.();
@@ -15,7 +15,7 @@
15
15
  * unified diff body for each divergent file. Mirrors the resolution that
16
16
  * the shim drives at runtime: project > user > system > extras.
17
17
  *
18
- * Read-only: doctor never mutates state. Run `agents prune` to act on orphan
18
+ * Read-only: doctor never mutates state. Run `agents prune cleanup` to act on orphan
19
19
  * readouts, or just launch the agent to apply pending sync.
20
20
  */
21
21
  import type { Command } from 'commander';
@@ -114,7 +114,7 @@ function renderOverviewText(clis, syncRows, orphanRows) {
114
114
  const label = `${AGENT_NAMES[row.agent] || row.agent}@${row.version}`;
115
115
  console.log(` ${chalk.yellow('warn ')} ${label} ${chalk.gray(parts.join(', '))}`);
116
116
  }
117
- console.log(chalk.gray(' Run `agents prune` to remove.'));
117
+ console.log(chalk.gray(' Run `agents prune cleanup` to remove.'));
118
118
  }
119
119
  }
120
120
  function parseTargetArg(arg) {
@@ -310,7 +310,7 @@ function renderTargetText(report, options) {
310
310
  }
311
311
  else {
312
312
  console.log(` Verdict: ${verdictParts.join(', ')}.`);
313
- console.log(chalk.gray(` Run \`agents sync --agent ${report.agent} --agent-version ${report.version}\` to reconcile, or \`agents prune\` to drop extras.`));
313
+ console.log(chalk.gray(` Run \`agents sync --agent ${report.agent} --agent-version ${report.version}\` to reconcile, or \`agents prune cleanup\` to drop extras.`));
314
314
  }
315
315
  }
316
316
  // ─── command registration ────────────────────────────────────────────────────
@@ -9,7 +9,7 @@ import chalk from 'chalk';
9
9
  import { buildExecCommand, parseExecEnv, execAgent, runWithFallback, AGENT_COMMANDS, } from '../lib/exec.js';
10
10
  import { profileExists, resolveProfileForRun } from '../lib/profiles.js';
11
11
  import { setHelpSections } from '../lib/help.js';
12
- import { readBundle, resolveBundleEnv, describeBundle } from '../lib/secrets/bundles.js';
12
+ import { readAndResolveBundleEnv, describeBundle } from '../lib/secrets/bundles.js';
13
13
  import { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES, } from '../lib/rotate.js';
14
14
  import { getGlobalDefault, getVersionHomePath, resolveVersionAlias } from '../lib/versions.js';
15
15
  import { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion } from '../lib/plugins.js';
@@ -268,7 +268,7 @@ export function registerRunCommand(program) {
268
268
  let secretsEnv = {};
269
269
  for (const bundleName of options.secrets) {
270
270
  try {
271
- const bundle = readBundle(bundleName);
271
+ const { bundle, env: bundleEnv } = readAndResolveBundleEnv(bundleName, { caller: `agent ${agent}` });
272
272
  const entries = describeBundle(bundle);
273
273
  const counts = {};
274
274
  for (const e of entries) {
@@ -276,7 +276,7 @@ export function registerRunCommand(program) {
276
276
  }
277
277
  const breakdown = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(', ');
278
278
  console.log(chalk.gray(`[secrets] Resolved ${bundleName}: ${entries.length} keys (${breakdown})`));
279
- secretsEnv = { ...secretsEnv, ...resolveBundleEnv(bundle, { caller: `agent ${agent}` }) };
279
+ secretsEnv = { ...secretsEnv, ...bundleEnv };
280
280
  }
281
281
  catch (err) {
282
282
  console.error(chalk.red(err.message));
@@ -1,19 +1,8 @@
1
1
  /**
2
- * Software Factory CLI -- thin client over `prix/factory/service`.
2
+ * Software Factory CLI submits Linear issues to a remote orchestrator.
3
3
  *
4
- * One verb today:
5
- *
6
- * agents factory submit <linear-ref> POST /factory/submit
7
- *
8
- * Everything else (planner pod, worker dispatch, PR-merged/CI-failed
9
- * webhooks, retry caps, heartbeat reaper) lives server-side in
10
- * `agents/prix/factory/service/src/factory.ts`, driven by the
11
- * `factory-tick` k8s CronJob. The laptop is optional after submit.
12
- *
13
- * Future verbs (list / status / tail / cancel / message) are intentionally
14
- * deferred until the matching server endpoints land; they'll be thin
15
- * clients too. No supervisor, ledger, oracle, or `~/.agents/factory/`
16
- * registry on the laptop -- ever.
4
+ * Requires FACTORY_FLOOR_URL pointing at a Factory-compatible endpoint.
5
+ * Beta-gated; enable with `agents beta enable factory`.
17
6
  */
18
7
  import type { Command } from 'commander';
19
8
  export declare function registerFactoryCommands(program: Command): void;
@@ -51,8 +51,8 @@ export function registerFactoryCommands(program) {
51
51
  .description('Software Factory -- submit Linear tickets to the cloud orchestrator.')
52
52
  .addHelpText('after', `
53
53
  Examples:
54
- agents factory submit EXAMPLE-2451
55
- agents factory submit https://linear.app/example/issue/EXAMPLE-2451
54
+ agents factory submit PROJ-123
55
+ agents factory submit https://linear.app/example/issue/PROJ-123
56
56
  `);
57
57
  factory.hook('preAction', () => {
58
58
  if (enabled)
@@ -63,7 +63,7 @@ Examples:
63
63
  });
64
64
  factory
65
65
  .command('submit <linear-ref>')
66
- .description('Submit a Linear issue (EXAMPLE-123 or URL) to the Software Factory.')
66
+ .description('Submit a Linear issue (PROJ-123 or URL) to the Software Factory.')
67
67
  .option('--json', 'Output machine-readable JSON')
68
68
  .action(async (ref, opts) => {
69
69
  const result = await postFactorySubmit(ref);
@@ -508,17 +508,17 @@ Examples:
508
508
  .action(() => {
509
509
  console.error(chalk.red('"agents hooks sync" is gone.'));
510
510
  console.error(chalk.gray('Sync runs automatically when you launch the agent.'));
511
- console.error(chalk.gray('To remove orphans, use: agents prune hooks'));
511
+ console.error(chalk.gray('To remove orphans, use: agents prune cleanup hooks'));
512
512
  process.exit(1);
513
513
  });
514
- // `hooks prune` moved to the top-level `agents prune` command.
514
+ // `hooks prune` moved to the top-level `agents prune cleanup` command.
515
515
  hooksCmd
516
516
  .command('prune', { hidden: true })
517
517
  .allowUnknownOption()
518
518
  .allowExcessArguments()
519
519
  .action(() => {
520
520
  console.error(chalk.red('"agents hooks prune" moved.'));
521
- console.error(chalk.gray('Use: agents prune hooks (or `agents prune` for everything)'));
521
+ console.error(chalk.gray('Use: agents prune cleanup hooks (or `agents prune cleanup` for everything)'));
522
522
  process.exit(1);
523
523
  });
524
524
  hooksCmd
@@ -237,6 +237,7 @@ Examples:
237
237
  pluginsCmd
238
238
  .command('sync <name> [agent]')
239
239
  .description('Apply a plugin to the default version of an agent (or all supported agents if none specified)')
240
+ .option('--allow-exec-surfaces', 'Enable the plugin even when it ships hooks/, .mcp.json, bin/, scripts/, settings.json, or permissions/')
240
241
  .addHelpText('after', `
241
242
  Examples:
242
243
  # Sync a plugin to a specific agent (default version)
@@ -244,8 +245,11 @@ Examples:
244
245
 
245
246
  # Sync to all supported agents
246
247
  agents plugins sync rush-toolkit
248
+
249
+ # Re-affirm consent for a hooks-bearing plugin
250
+ agents plugins sync hivemind claude --allow-exec-surfaces
247
251
  `)
248
- .action(async (name, agentArg) => {
252
+ .action(async (name, agentArg, options) => {
249
253
  const plugin = getPlugin(name);
250
254
  if (!plugin) {
251
255
  console.log(chalk.red(`Plugin '${name}' not found`));
@@ -268,6 +272,7 @@ Examples:
268
272
  else {
269
273
  targetAgents = PLUGINS_CAPABLE_AGENTS.filter(a => pluginSupportsAgent(plugin, a));
270
274
  }
275
+ const allowExec = options.allowExecSurfaces === true;
271
276
  for (const agentId of targetAgents) {
272
277
  const versions = listInstalledVersions(agentId);
273
278
  if (versions.length === 0)
@@ -275,9 +280,11 @@ Examples:
275
280
  const defaultVer = getGlobalDefault(agentId);
276
281
  const targetVersions = defaultVer ? [defaultVer] : [versions[versions.length - 1]];
277
282
  for (const version of targetVersions) {
278
- const syncResult = syncResourcesToVersion(agentId, version, { plugins: [name] });
279
- if (syncResult.plugins.length > 0) {
280
- console.log(chalk.green(`Synced ${name} to ${agentLabel(agentId)}@${version}`));
283
+ const didSync = allowExec
284
+ ? syncPluginToVersion(plugin, agentId, getVersionHomePath(agentId, version), { allowExecSurfaces: true, version }).success
285
+ : syncResourcesToVersion(agentId, version, { plugins: [name] }).plugins.length > 0;
286
+ if (didSync) {
287
+ console.log(chalk.green(`Synced ${name} to ${agentLabel(agentId)}@${version}${allowExec ? ' (exec surfaces enabled)' : ''}`));
281
288
  }
282
289
  else {
283
290
  console.log(chalk.gray(`${name} already synced to ${agentLabel(agentId)}@${version}`));
@@ -116,7 +116,7 @@ Examples:
116
116
 
117
117
  # Add MiniMax for SWE-bench style fixes; reuses the same OpenRouter key
118
118
  agents profiles add minimax
119
- agents run minimax "investigate EXAMPLE-2317 and patch the off-by-one in pagination"
119
+ agents run minimax "investigate PROJ-456 and patch the off-by-one in pagination"
120
120
 
121
121
  # Add DeepSeek for cheap, fast non-reasoning work
122
122
  agents profiles add deepseek
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Top-level `agents prune` — destructive cleanup across the install.
2
+ * `agents prune cleanup` — destructive cleanup across the install.
3
3
  *
4
4
  * Cleanup targets:
5
5
  * - Resource orphans: command/skill/hook files inside a version home that no
@@ -7,8 +7,9 @@
7
7
  * into the version install).
8
8
  * - Version duplicates: older installed versions of an agent that share an
9
9
  * account with a newer installed version of the same agent.
10
- * - Trash: soft-deleted resources in ~/.agents/.trash/ older than N days.
11
- * - Sessions: session records in sessions.db older than N days.
10
+ * - Trash/session targets are retained as no-op compatibility shims: version
11
+ * homes and session history are durable and must not be hard-deleted by
12
+ * agents-cli.
12
13
  * - Runs: routine execution logs, keeping only the last N per job.
13
14
  *
14
15
  * Sync (additive: copy missing/changed files into version homes) is no longer
@@ -21,7 +22,6 @@
21
22
  * to widen orphan cleanup to every installed version.
22
23
  */
23
24
  import * as fs from 'fs';
24
- import * as path from 'path';
25
25
  import chalk from 'chalk';
26
26
  import { confirm } from '@inquirer/prompts';
27
27
  import { diffVersionCommands, iterCommandsCapableVersions, removeCommandFromVersion, } from '../lib/commands.js';
@@ -34,7 +34,6 @@ import { resolveAgentName, formatAgentError } from '../lib/agents.js';
34
34
  import { pruneDuplicates } from './view.js';
35
35
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
36
36
  import { getTrashDir } from '../lib/state.js';
37
- import { countSessionsOlderThan, deleteSessionsOlderThan } from '../lib/session/db.js';
38
37
  import { previewRunsPrune, pruneRuns, countAllRuns } from '../lib/routines.js';
39
38
  const RESOURCE_TYPES = ['commands', 'skills', 'hooks', 'plugins', 'subagents'];
40
39
  const STATE_TYPES = ['trash', 'sessions', 'runs'];
@@ -125,12 +124,6 @@ function parseTarget(arg) {
125
124
  console.log(chalk.gray(formatAgentError(arg)));
126
125
  process.exit(1);
127
126
  }
128
- function parseDays(value, defaultDays) {
129
- const match = value.match(/^(\d+)d?$/);
130
- if (match)
131
- return parseInt(match[1], 10);
132
- return defaultDays;
133
- }
134
127
  function formatBytes(bytes) {
135
128
  if (bytes < 1024)
136
129
  return `${bytes} B`;
@@ -140,137 +133,25 @@ function formatBytes(bytes) {
140
133
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
141
134
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
142
135
  }
143
- function getDirSize(dirPath) {
144
- if (!fs.existsSync(dirPath))
145
- return 0;
146
- let size = 0;
147
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
148
- for (const entry of entries) {
149
- const fullPath = path.join(dirPath, entry.name);
150
- if (entry.isDirectory()) {
151
- size += getDirSize(fullPath);
152
- }
153
- else {
154
- try {
155
- size += fs.statSync(fullPath).size;
156
- }
157
- catch { /* ignore */ }
158
- }
159
- }
160
- return size;
161
- }
162
136
  async function runTrashPrune(options) {
163
137
  const trashDir = getTrashDir();
164
138
  if (!fs.existsSync(trashDir)) {
165
139
  console.log(chalk.green('Trash is empty.'));
166
140
  return;
167
141
  }
168
- const days = parseDays(options.olderThan || '30d', 30);
169
- const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1000;
170
- const toPrune = [];
171
- function scanDir(dir) {
172
- if (!fs.existsSync(dir))
173
- return;
174
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
175
- const fullPath = path.join(dir, entry.name);
176
- try {
177
- const stat = fs.statSync(fullPath);
178
- if (stat.mtimeMs < cutoffMs) {
179
- toPrune.push({ path: fullPath, mtime: stat.mtimeMs, size: entry.isDirectory() ? getDirSize(fullPath) : stat.size });
180
- }
181
- else if (entry.isDirectory()) {
182
- scanDir(fullPath);
183
- }
184
- }
185
- catch { /* skip inaccessible */ }
186
- }
187
- }
188
- scanDir(trashDir);
189
- if (toPrune.length === 0) {
190
- console.log(chalk.green(`No trash entries older than ${days} days.`));
191
- return;
192
- }
193
- const totalSize = toPrune.reduce((sum, e) => sum + e.size, 0);
194
- console.log(chalk.bold(`Trash entries older than ${days} days\n`));
195
- for (const entry of toPrune.slice(0, 20)) {
196
- const age = Math.floor((Date.now() - entry.mtime) / (24 * 60 * 60 * 1000));
197
- console.log(` ${chalk.gray(`${age}d ago`)} ${path.relative(trashDir, entry.path)}`);
198
- }
199
- if (toPrune.length > 20) {
200
- console.log(chalk.gray(` ... and ${toPrune.length - 20} more`));
201
- }
202
- console.log();
203
- if (options.dryRun) {
204
- console.log(chalk.gray(`${toPrune.length} entries (${formatBytes(totalSize)}). Run without --dry-run to delete.`));
205
- return;
206
- }
207
- if (!options.yes) {
208
- if (!isInteractiveTerminal()) {
209
- console.log(chalk.yellow('Non-interactive shell: pass -y to confirm, or --dry-run to preview.'));
210
- process.exit(1);
211
- }
212
- let ok = false;
213
- try {
214
- ok = await confirm({ message: `Delete ${toPrune.length} entries (${formatBytes(totalSize)})?`, default: false });
215
- }
216
- catch (err) {
217
- if (isPromptCancelled(err)) {
218
- console.log(chalk.gray('Cancelled'));
219
- return;
220
- }
221
- throw err;
222
- }
223
- if (!ok) {
224
- console.log(chalk.gray('Cancelled'));
225
- return;
226
- }
227
- }
228
- let deleted = 0;
229
- for (const entry of toPrune) {
230
- try {
231
- fs.rmSync(entry.path, { recursive: true, force: true });
232
- deleted++;
233
- }
234
- catch { /* ignore */ }
142
+ if (options.olderThan || options.yes || options.dryRun) {
143
+ console.log(chalk.gray('Trash expiry flags are accepted for compatibility but do not delete data.'));
235
144
  }
236
- console.log(chalk.green(`Pruned ${deleted} trash entries (${formatBytes(totalSize)}).`));
145
+ console.log(chalk.yellow('Trash is durable. agents-cli does not hard-delete soft-deleted version data.'));
146
+ console.log(chalk.gray('Inspect recoverable versions with: agents trash list'));
147
+ console.log(chalk.gray(`Trash path: ${trashDir}`));
237
148
  }
238
149
  async function runSessionsPrune(options) {
239
- const days = parseDays(options.olderThan || '90d', 90);
240
- const cutoffMs = Date.now() - days * 24 * 60 * 60 * 1000;
241
- const count = countSessionsOlderThan(cutoffMs);
242
- if (count === 0) {
243
- console.log(chalk.green(`No sessions older than ${days} days.`));
244
- return;
150
+ if (options.olderThan || options.yes || options.dryRun) {
151
+ console.log(chalk.gray('Session prune flags are accepted for compatibility but do not delete data.'));
245
152
  }
246
- console.log(chalk.bold(`Sessions older than ${days} days: ${count}\n`));
247
- if (options.dryRun) {
248
- console.log(chalk.gray(`${count} session(s). Run without --dry-run to delete.`));
249
- return;
250
- }
251
- if (!options.yes) {
252
- if (!isInteractiveTerminal()) {
253
- console.log(chalk.yellow('Non-interactive shell: pass -y to confirm, or --dry-run to preview.'));
254
- process.exit(1);
255
- }
256
- let ok = false;
257
- try {
258
- ok = await confirm({ message: `Delete ${count} session records?`, default: false });
259
- }
260
- catch (err) {
261
- if (isPromptCancelled(err)) {
262
- console.log(chalk.gray('Cancelled'));
263
- return;
264
- }
265
- throw err;
266
- }
267
- if (!ok) {
268
- console.log(chalk.gray('Cancelled'));
269
- return;
270
- }
271
- }
272
- const deleted = deleteSessionsOlderThan(cutoffMs);
273
- console.log(chalk.green(`Pruned ${deleted} session records.`));
153
+ console.log(chalk.yellow('Session history is durable. agents-cli does not hard-delete session records.'));
154
+ console.log(chalk.gray('Browse sessions with: agents sessions'));
274
155
  }
275
156
  async function runRunsPrune(options) {
276
157
  const keep = options.keep ? parseInt(options.keep, 10) : 10;
@@ -380,13 +261,16 @@ async function runOrphanPrune(resourceTypes, options) {
380
261
  console.log(chalk.green(summary) + (failures > 0 ? chalk.red(`, ${failures} failed`) : '') + '.');
381
262
  }
382
263
  export function registerPruneCommand(program) {
383
- program
384
- .command('prune [target]')
385
- .description('Remove orphan resources, old versions, trash, sessions, or routine runs')
264
+ const pruneCmd = program.commands.find((cmd) => cmd.name() === 'prune') ?? program
265
+ .command('prune <specs...>')
266
+ .description('Uninstall agent CLI versions. Moves version data to trash for recovery.');
267
+ pruneCmd
268
+ .command('cleanup [target]')
269
+ .description('Remove orphan resources, old versions, or routine runs')
386
270
  .option('--all', 'For orphan cleanup: sweep every installed version (default: current default version per agent)')
387
271
  .option('--dry-run', 'Show what would be removed without deleting (default for state targets)')
388
272
  .option('-y, --yes', 'Skip confirmation prompt')
389
- .option('--older-than <days>', 'For trash/sessions: delete entries older than N days (default: 30d for trash, 90d for sessions)')
273
+ .option('--older-than <days>', 'Deprecated for trash/sessions; accepted but no data is deleted')
390
274
  .option('--keep <n>', 'For runs: keep the last N runs per job (default: 10)')
391
275
  .addHelpText('after', `
392
276
  Targets:
@@ -396,46 +280,40 @@ Targets:
396
280
  hooks Orphan hook scripts only
397
281
  versions Older duplicate version installs only
398
282
  <agent> Older duplicate versions for one agent (e.g. 'claude')
399
- trash Soft-deleted resources older than --older-than days (default 30)
400
- sessions Session records in sessions.db older than --older-than days (default 90)
283
+ trash No-op compatibility target; trash is durable
284
+ sessions No-op compatibility target; session history is durable
401
285
  runs Routine execution logs, keeping only --keep per job (default 10)
402
286
 
403
287
  Examples:
404
288
  # Full sweep: orphan resources + duplicate versions for current defaults
405
- agents prune
289
+ agents prune cleanup
406
290
 
407
291
  # Preview what a full sweep would remove
408
- agents prune --dry-run
292
+ agents prune cleanup --dry-run
409
293
 
410
294
  # Just orphan skills
411
- agents prune skills
295
+ agents prune cleanup skills
412
296
 
413
297
  # Just version dedup
414
- agents prune versions
298
+ agents prune cleanup versions
415
299
 
416
300
  # Deduplicate versions for one agent only
417
- agents prune claude
301
+ agents prune cleanup claude
418
302
 
419
303
  # Sweep every installed version's orphans, not only the defaults
420
- agents prune --all
421
-
422
- # Preview trash entries older than 30 days
423
- agents prune trash --dry-run
424
-
425
- # Delete trash entries older than 60 days
426
- agents prune trash --older-than 60 -y
304
+ agents prune cleanup --all
427
305
 
428
- # Preview session cleanup (90+ days old)
429
- agents prune sessions --dry-run
306
+ # Show the durable-trash notice
307
+ agents prune cleanup trash --dry-run
430
308
 
431
- # Delete sessions older than 180 days
432
- agents prune sessions --older-than 180 -y
309
+ # Show the durable-session notice
310
+ agents prune cleanup sessions --dry-run
433
311
 
434
312
  # Preview runs cleanup (keeping last 10)
435
- agents prune runs --dry-run
313
+ agents prune cleanup runs --dry-run
436
314
 
437
315
  # Keep only the last 5 runs per job
438
- agents prune runs --keep 5 -y
316
+ agents prune cleanup runs --keep 5 -y
439
317
 
440
318
  What's an orphan?
441
319
  A command, skill, or hook present inside a version home but missing from every
@@ -443,10 +321,11 @@ What's an orphan?
443
321
  repos). Usually leftovers from a resource that was deleted or moved but never
444
322
  reconciled into the version install.
445
323
 
446
- Soft-delete:
447
- Version directories are NEVER hard-deleted. \`prune\` moves them to
448
- ~/.agents/.trash/versions/<agent>/<version>/<timestamp>/. Use
449
- \`agents prune trash\` to expire old trash entries.
324
+ Durability:
325
+ Version directories are NEVER hard-deleted by agents-cli. Version prune and
326
+ cleanup move them to ~/.agents/.history/trash/versions/<agent>/<version>/<timestamp>/.
327
+ Session records are also durable; the sessions target remains only as a no-op
328
+ compatibility shim.
450
329
  `)
451
330
  .action(async (target, options) => {
452
331
  const parsed = parseTarget(target);
@@ -14,10 +14,11 @@ import { isGitRepo, pullRepo, isSystemRepoOrigin, } from '../lib/git.js';
14
14
  import * as fs from 'fs';
15
15
  import * as path from 'path';
16
16
  import { installVersion, listInstalledVersions, getGlobalDefault, setGlobalDefault, getVersionHomePath, syncResourcesToVersion, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, promptNewResourceSelection, promptResourceSelection, resolveConfiguredAgentTargets, } from '../lib/versions.js';
17
+ import { listCliStatus, installCli, describeMethod, selectInstallMethod, } from '../lib/cli-resources.js';
17
18
  import { ensureShimCurrent, isShimsInPath, addShimsToPath, getPathSetupInstructions, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
18
19
  import { parseHookManifest, registerHooksToSettings } from '../lib/hooks.js';
19
20
  import { setHelpSections } from '../lib/help.js';
20
- import { select } from '@inquirer/prompts';
21
+ import { select, confirm } from '@inquirer/prompts';
21
22
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
22
23
  /**
23
24
  * Old repo layout stored promptcuts under claude/promptcuts.yaml (agent-scoped).
@@ -244,10 +245,10 @@ export function registerPullCommand(program) {
244
245
  if (userSelection)
245
246
  selection = userSelection;
246
247
  }
247
- else if (hasNewResources(newResources, agentId)) {
248
+ else if (hasNewResources(newResources, agentId, defaultVer)) {
248
249
  // Has synced before, but NEW items available
249
250
  console.log(chalk.cyan(`\n${agentLabel(agentId)}@${defaultVer}:`));
250
- const userSelection = await promptNewResourceSelection(agentId, newResources);
251
+ const userSelection = await promptNewResourceSelection(agentId, newResources, defaultVer);
251
252
  if (userSelection)
252
253
  selection = userSelection;
253
254
  }
@@ -366,6 +367,58 @@ export function registerPullCommand(program) {
366
367
  console.log(chalk.green(`Set ${agentLabel(agent.id)}@${version} as default`));
367
368
  }
368
369
  }
370
+ // Report (and optionally install) any declared CLIs that are missing
371
+ // from the host. Skipped under -y so non-interactive pulls don't trigger
372
+ // package-manager prompts.
373
+ try {
374
+ const { statuses, errors } = listCliStatus(process.cwd());
375
+ for (const err of errors) {
376
+ console.log(chalk.yellow(` CLI manifest parse error: ${err.file}: ${err.reason}`));
377
+ }
378
+ const missing = statuses.filter((s) => !s.installed);
379
+ if (missing.length > 0) {
380
+ console.log(chalk.bold('\nDeclared CLIs missing from this host:'));
381
+ for (const s of missing) {
382
+ const method = selectInstallMethod(s.manifest);
383
+ const action = method ? describeMethod(method) : chalk.red('no compatible install method');
384
+ console.log(` ${chalk.cyan(s.manifest.name.padEnd(20))} ${chalk.gray(action)}`);
385
+ }
386
+ console.log('');
387
+ if (!skipPrompts) {
388
+ const proceed = await confirm({ message: `Install ${missing.length} missing CLI(s) now?`, default: true });
389
+ if (proceed) {
390
+ for (const s of missing) {
391
+ console.log(chalk.bold(`\n→ ${s.manifest.name}`));
392
+ const result = installCli(s.manifest);
393
+ if (result.error) {
394
+ console.log(chalk.red(` ${result.error}`));
395
+ continue;
396
+ }
397
+ if (result.installed) {
398
+ console.log(chalk.green(` installed`));
399
+ if (s.manifest.postInstall) {
400
+ console.log(chalk.gray(s.manifest.postInstall.trim().split('\n').map((l) => ' ' + l).join('\n')));
401
+ }
402
+ }
403
+ else {
404
+ console.log(chalk.yellow(` install ran but \`${s.manifest.check}\` still fails`));
405
+ }
406
+ }
407
+ }
408
+ else {
409
+ console.log(chalk.gray(`Skipped. Run 'agents cli install' later.`));
410
+ }
411
+ }
412
+ else {
413
+ console.log(chalk.gray(`Run 'agents cli install' to install them.`));
414
+ }
415
+ }
416
+ }
417
+ catch (err) {
418
+ if (!isPromptCancelled(err)) {
419
+ console.log(chalk.yellow(`CLI install skipped: ${err.message}`));
420
+ }
421
+ }
369
422
  console.log(chalk.green('\nPull complete'));
370
423
  }
371
424
  catch (err) {