@phnx-labs/agents-cli 1.18.6 → 1.19.1

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 (104) hide show
  1. package/CHANGELOG.md +13 -2
  2. package/README.md +22 -20
  3. package/dist/commands/browser.js +25 -2
  4. package/dist/commands/cloud.js +3 -3
  5. package/dist/commands/computer.d.ts +6 -0
  6. package/dist/commands/computer.js +477 -0
  7. package/dist/commands/doctor.js +19 -17
  8. package/dist/commands/exec.js +37 -59
  9. package/dist/commands/factory.js +12 -5
  10. package/dist/commands/import.js +6 -1
  11. package/dist/commands/mcp.js +9 -4
  12. package/dist/commands/packages.d.ts +3 -0
  13. package/dist/commands/packages.js +20 -12
  14. package/dist/commands/permissions.d.ts +2 -0
  15. package/dist/commands/permissions.js +20 -1
  16. package/dist/commands/plugins.d.ts +2 -0
  17. package/dist/commands/plugins.js +23 -4
  18. package/dist/commands/profiles.js +1 -1
  19. package/dist/commands/pty.js +126 -112
  20. package/dist/commands/pull.js +29 -25
  21. package/dist/commands/repo.js +24 -26
  22. package/dist/commands/routines.js +29 -26
  23. package/dist/commands/secrets.js +66 -73
  24. package/dist/commands/sessions-tail.js +21 -22
  25. package/dist/commands/sessions.js +36 -68
  26. package/dist/commands/setup.js +20 -24
  27. package/dist/commands/teams.js +30 -39
  28. package/dist/commands/versions.js +60 -68
  29. package/dist/commands/worktree.d.ts +20 -0
  30. package/dist/commands/worktree.js +242 -0
  31. package/dist/computer.d.ts +2 -0
  32. package/dist/computer.js +7 -0
  33. package/dist/index.js +70 -26
  34. package/dist/lib/agents.d.ts +4 -1
  35. package/dist/lib/agents.js +23 -5
  36. package/dist/lib/browser/cdp.d.ts +15 -1
  37. package/dist/lib/browser/cdp.js +77 -8
  38. package/dist/lib/browser/chrome.js +17 -24
  39. package/dist/lib/browser/drivers/ssh.d.ts +1 -0
  40. package/dist/lib/browser/drivers/ssh.js +20 -8
  41. package/dist/lib/browser/ipc.js +38 -5
  42. package/dist/lib/browser/profiles.js +34 -2
  43. package/dist/lib/browser/runtime-state.d.ts +1 -2
  44. package/dist/lib/browser/runtime-state.js +11 -3
  45. package/dist/lib/browser/service.d.ts +5 -0
  46. package/dist/lib/browser/service.js +32 -4
  47. package/dist/lib/browser/types.d.ts +1 -1
  48. package/dist/lib/browser/upload.d.ts +2 -0
  49. package/dist/lib/browser/upload.js +34 -0
  50. package/dist/lib/cloud/rush.d.ts +2 -1
  51. package/dist/lib/cloud/rush.js +28 -9
  52. package/dist/lib/computer-rpc.d.ts +24 -0
  53. package/dist/lib/computer-rpc.js +263 -0
  54. package/dist/lib/daemon.js +7 -7
  55. package/dist/lib/exec.d.ts +2 -1
  56. package/dist/lib/exec.js +3 -2
  57. package/dist/lib/fs-atomic.d.ts +18 -0
  58. package/dist/lib/fs-atomic.js +76 -0
  59. package/dist/lib/git.js +2 -4
  60. package/dist/lib/help.d.ts +15 -0
  61. package/dist/lib/help.js +41 -0
  62. package/dist/lib/hooks/match.d.ts +1 -0
  63. package/dist/lib/hooks/match.js +57 -12
  64. package/dist/lib/hooks.d.ts +1 -0
  65. package/dist/lib/hooks.js +27 -10
  66. package/dist/lib/import.d.ts +1 -0
  67. package/dist/lib/import.js +7 -0
  68. package/dist/lib/manifest.js +27 -1
  69. package/dist/lib/mcp.d.ts +14 -0
  70. package/dist/lib/mcp.js +79 -14
  71. package/dist/lib/migrate.js +3 -3
  72. package/dist/lib/models.js +3 -1
  73. package/dist/lib/permissions.d.ts +5 -0
  74. package/dist/lib/permissions.js +35 -0
  75. package/dist/lib/plugin-marketplace.d.ts +3 -1
  76. package/dist/lib/plugin-marketplace.js +36 -1
  77. package/dist/lib/plugins.d.ts +19 -1
  78. package/dist/lib/plugins.js +99 -8
  79. package/dist/lib/redact.d.ts +4 -0
  80. package/dist/lib/redact.js +18 -0
  81. package/dist/lib/registry.d.ts +2 -0
  82. package/dist/lib/registry.js +15 -0
  83. package/dist/lib/sandbox.js +15 -5
  84. package/dist/lib/secrets/bundles.d.ts +7 -12
  85. package/dist/lib/secrets/bundles.js +45 -29
  86. package/dist/lib/secrets/index.js +4 -4
  87. package/dist/lib/session/cloud.d.ts +2 -0
  88. package/dist/lib/session/cloud.js +34 -6
  89. package/dist/lib/session/parse.js +7 -2
  90. package/dist/lib/session/render.d.ts +4 -1
  91. package/dist/lib/session/render.js +81 -35
  92. package/dist/lib/shims.d.ts +5 -2
  93. package/dist/lib/shims.js +29 -7
  94. package/dist/lib/state.d.ts +5 -5
  95. package/dist/lib/state.js +43 -13
  96. package/dist/lib/teams/agents.js +1 -1
  97. package/dist/lib/types.d.ts +4 -3
  98. package/dist/lib/types.js +0 -2
  99. package/dist/lib/versions.js +65 -40
  100. package/dist/lib/workflows.d.ts +7 -0
  101. package/dist/lib/workflows.js +42 -1
  102. package/npm-shrinkwrap.json +3162 -0
  103. package/package.json +32 -26
  104. package/scripts/postinstall.js +8 -2
@@ -12,6 +12,7 @@ import { createShim, createVersionedAlias, removeShim, shimExists, getShimsDir,
12
12
  import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
13
13
  import { tryAutoPull } from '../lib/git.js';
14
14
  import { getAgentsDir } from '../lib/state.js';
15
+ import { setHelpSections } from '../lib/help.js';
15
16
  /**
16
17
  * Helper to get actual installed version for an agent.
17
18
  * Returns the latest installed version, or throws if none installed.
@@ -79,34 +80,32 @@ function warnIfShimShadowed(agent) {
79
80
  }
80
81
  /** Register `agents add`, `agents remove`, `agents use`, and `agents list` (deprecated). */
81
82
  export function registerVersionsCommands(program) {
82
- program
83
+ const addCmd = program
83
84
  .command('add <specs...>')
84
85
  .description('Download and install agent CLI versions. Enables subsidized API usage through managed binaries.')
85
- .option('-p, --project', 'Lock this version to the current project directory only, stored in .agents/agents.yaml')
86
- .option('-y, --yes', 'Auto-accept defaults without prompting (useful for scripts and CI)')
87
- .addHelpText('after', `
88
- Examples:
89
- # Fresh install of the latest Claude CLI
90
- agents add claude@latest
91
-
92
- # Install specific version (use this when you need reproducibility)
93
- agents add claude@2.1.112
86
+ .option('-p, --project', 'Lock this version to the current project directory only, stored in project-root agents.yaml')
87
+ .option('-y, --yes', 'Auto-accept defaults without prompting (useful for scripts and CI)');
88
+ setHelpSections(addCmd, {
89
+ examples: `
90
+ # Install the latest version of an agent
91
+ agents add claude@latest
94
92
 
95
- # Install multiple agents at once
96
- agents add claude@latest codex@0.116.0
93
+ # Install a specific version (reproducibility)
94
+ agents add claude@2.1.112
97
95
 
98
- # Pin a version to this project only (won't affect global default)
99
- agents add claude@2.1.100 --project
96
+ # Install multiple agents at once
97
+ agents add claude@latest codex@0.116.0
100
98
 
101
- When to use:
102
- - First time setup: install the agent CLI you want to use
103
- - Upgrading: install a newer version (old versions remain available)
104
- - Multi-account: install different versions for different accounts (each version has its own auth)
105
- - Project-specific: lock a version for a repo with --project
106
-
107
- Note: The first version you install becomes the default automatically.
108
- `)
109
- .action(async (specs, options) => {
99
+ # Lock a version to this project only (won't affect global default)
100
+ agents add claude@2.1.100 --project
101
+ `,
102
+ notes: `
103
+ - The first version you install becomes the default automatically.
104
+ - 'add' does NOT change the default if a default already exists. Use 'agents use' to switch.
105
+ - Multi-account: each installed version has separate auth, so you can install the same agent twice for two accounts.
106
+ `,
107
+ });
108
+ addCmd.action(async (specs, options) => {
110
109
  const isProject = options.project;
111
110
  const skipPrompts = options.yes || !isInteractiveTerminal();
112
111
  for (const spec of specs) {
@@ -228,7 +227,7 @@ Note: The first version you install becomes the default automatically.
228
227
  await setDefaultVersion(agent, installedVersion);
229
228
  }
230
229
  else if (skipPrompts) {
231
- await setDefaultVersion(agent, installedVersion);
230
+ console.log(chalk.gray(` Default remains ${agentLabel(agentConfig.id)}@${currentDefault}. Run 'agents use ${agent}@${installedVersion}' to switch.`));
232
231
  }
233
232
  else {
234
233
  try {
@@ -297,31 +296,27 @@ Note: The first version you install becomes the default automatically.
297
296
  }
298
297
  }
299
298
  });
300
- program
299
+ const removeCmd = program
301
300
  .command('remove <specs...>')
302
- .description('Uninstall agent CLI versions from your system. Frees up disk space and removes auth tokens.')
303
- .option('-p, --project', 'Also clear the pinned version from .agents/agents.yaml in the current project')
304
- .addHelpText('after', `
305
- Examples:
306
- # Remove a specific version
307
- agents remove claude@2.0.50
308
-
309
- # Remove without specifying version (opens interactive picker)
310
- agents remove claude
301
+ .description('Uninstall agent CLI versions. Frees disk space and removes the version\'s auth token.')
302
+ .option('-p, --project', 'Also clear the pinned version from .agents/agents.yaml in the current project');
303
+ setHelpSections(removeCmd, {
304
+ examples: `
305
+ # Remove a specific version
306
+ agents remove claude@2.0.50
311
307
 
312
- # Remove and also clear project pin
313
- agents remove claude@2.0.50 --project
308
+ # Pick interactively if you omit the version
309
+ agents remove claude
314
310
 
315
- When to use:
316
- - Freeing disk space by removing old unused versions
317
- - Cleaning up after testing a version you no longer need
318
- - Removing a version tied to an account you've deactivated
319
-
320
- Notes:
321
- - Removing the default version will unset the default; run 'agents use' to pick a new one
322
- - You can always reinstall with 'agents add' later
323
- `)
324
- .action(async (specs, options) => {
311
+ # Remove and also clear the project pin
312
+ agents remove claude@2.0.50 --project
313
+ `,
314
+ notes: `
315
+ - Removing the default version unsets the default; run 'agents use' to pick a new one.
316
+ - Reinstall any time with 'agents add'.
317
+ `,
318
+ });
319
+ removeCmd.action(async (specs, options) => {
325
320
  const isProject = options.project;
326
321
  for (const spec of specs) {
327
322
  const parsed = parseAgentSpec(spec);
@@ -418,32 +413,29 @@ Notes:
418
413
  }
419
414
  }
420
415
  });
421
- program
416
+ const useCmd = program
422
417
  .command('use <agent> [version]')
423
- .description('Switch the active version for an agent. This is how you set defaults globally or per-project.')
418
+ .description('Switch the active version for an agent. This is the only command that sets the default.')
424
419
  .option('-p, --project', 'Pin to this project directory only (stored in .agents/agents.yaml)')
425
- .option('-y, --yes', 'Auto-sync resources without prompting')
426
- .addHelpText('after', `
427
- Examples:
428
- # Set global default (interactive picker if version omitted)
429
- agents use claude
430
- agents use claude@2.1.112
420
+ .option('-y, --yes', 'Auto-sync resources without prompting');
421
+ setHelpSections(useCmd, {
422
+ examples: `
423
+ # Set global default (interactive picker if version omitted)
424
+ agents use claude
425
+ agents use claude@2.1.112
431
426
 
432
- # Lock this project to a specific version (overrides global default when in this directory)
433
- agents use claude@2.1.100 --project
427
+ # Pin this project to a version (overrides the global default in this directory)
428
+ agents use claude@2.1.100 --project
434
429
 
435
- # Switch accounts (each version can be logged into a different account)
436
- agents use claude@2.1.50
437
-
438
- When to use:
439
- - After installing: 'agents add' does NOT set the default; 'use' does
440
- - Switching accounts: each installed version has separate auth; 'use' switches which one is active
441
- - Project overrides: set a different version for a specific project with --project
442
- - Testing: quickly switch between versions without reinstalling
443
-
444
- Important: This is the ONLY command that sets the default version. If you want a version to be active, you must run 'agents use'.
445
- `)
446
- .action(async (agentArg, versionArg, options) => {
430
+ # Switch accounts each installed version has its own auth
431
+ agents use claude@2.1.50
432
+ `,
433
+ notes: `
434
+ - 'agents add' installs but does NOT set the default. Always follow with 'agents use'.
435
+ - --project pins to the current directory only via .agents/agents.yaml.
436
+ `,
437
+ });
438
+ useCmd.action(async (agentArg, versionArg, options) => {
447
439
  try {
448
440
  const skipPrompts = options.yes || !isInteractiveTerminal();
449
441
  // Auto-pull ~/.agents-system if it's a git repo with remote (silent on success)
@@ -0,0 +1,20 @@
1
+ /**
2
+ * agents worktree -- provision, release, and prune per-terminal git worktrees.
3
+ *
4
+ * Used by surfaces that want to spawn each agent terminal in an isolated
5
+ * working tree (Companion extension opt-in toggle). Mirrors the in-process
6
+ * worktree helpers in lib/teams/worktree.ts but exposes them as a CLI so
7
+ * other processes (IDE extensions, shell aliases, hooks) can call them.
8
+ *
9
+ * agents worktree provision <terminal-id> -> prints absolute worktree path
10
+ * agents worktree release <terminal-id> -> removes if clean + merged
11
+ * agents worktree prune -> removes every clean+merged one
12
+ *
13
+ * Worktrees live at <repo>/.history/worktrees/<terminal-id>, on a branch
14
+ * named agent/<terminal-id>. The branch starts at HEAD of the parent repo.
15
+ * .history/ mirrors the agents-cli runtime-state convention at ~/.agents/.history/
16
+ * but scoped to the repo. .agents/ is reserved for project resources
17
+ * (skills, hooks, commands) per the agents-cli DotAgents repo layout.
18
+ */
19
+ import type { Command } from 'commander';
20
+ export declare function registerWorktreeCommands(program: Command): void;
@@ -0,0 +1,242 @@
1
+ import chalk from 'chalk';
2
+ import { execFile } from 'child_process';
3
+ import { promisify } from 'util';
4
+ import * as fs from 'fs/promises';
5
+ import * as fsSync from 'fs';
6
+ import * as path from 'path';
7
+ import { setHelpSections } from '../lib/help.js';
8
+ const execFileAsync = promisify(execFile);
9
+ const WORKTREE_SUBDIR = path.join('.history', 'worktrees');
10
+ const BRANCH_PREFIX = 'agent/';
11
+ function die(msg, code = 1) {
12
+ console.error(chalk.red(msg));
13
+ process.exit(code);
14
+ }
15
+ function isValidTerminalId(id) {
16
+ // Allow letters, digits, dot, dash, underscore. Reject anything else so a
17
+ // hostile or buggy caller can't inject path traversal or shell metachars.
18
+ return /^[A-Za-z0-9._-]+$/.test(id) && id.length > 0 && id.length <= 128;
19
+ }
20
+ async function gitRoot(cwd) {
21
+ try {
22
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], { cwd });
23
+ return stdout.trim();
24
+ }
25
+ catch {
26
+ die(`Not inside a git repo: ${cwd}`);
27
+ }
28
+ }
29
+ function worktreePathFor(root, terminalId) {
30
+ return path.join(root, WORKTREE_SUBDIR, terminalId);
31
+ }
32
+ function branchNameFor(terminalId) {
33
+ return `${BRANCH_PREFIX}${terminalId}`;
34
+ }
35
+ async function inspect(root, terminalId) {
36
+ const wt = worktreePathFor(root, terminalId);
37
+ const branch = branchNameFor(terminalId);
38
+ const exists = fsSync.existsSync(wt);
39
+ if (!exists) {
40
+ return { exists: false, dirty: false, aheadOfUpstream: false, hasUpstream: false, branchMerged: false };
41
+ }
42
+ let dirty = false;
43
+ try {
44
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain'], { cwd: wt });
45
+ dirty = stdout.trim().length > 0;
46
+ }
47
+ catch {
48
+ dirty = true; // err on the side of caution
49
+ }
50
+ let hasUpstream = false;
51
+ let aheadOfUpstream = false;
52
+ try {
53
+ await execFileAsync('git', ['rev-parse', '--abbrev-ref', '@{u}'], { cwd: wt });
54
+ hasUpstream = true;
55
+ const { stdout } = await execFileAsync('git', ['rev-list', '--count', '@{u}..HEAD'], { cwd: wt });
56
+ aheadOfUpstream = parseInt(stdout.trim(), 10) > 0;
57
+ }
58
+ catch {
59
+ hasUpstream = false; // never pushed -- treat as "has commits we'd lose"
60
+ }
61
+ let branchMerged = false;
62
+ try {
63
+ const { stdout } = await execFileAsync('git', ['branch', '--merged', 'origin/main', '--list', branch], { cwd: root });
64
+ branchMerged = stdout.trim().length > 0;
65
+ }
66
+ catch {
67
+ branchMerged = false;
68
+ }
69
+ return { exists, dirty, aheadOfUpstream, hasUpstream, branchMerged };
70
+ }
71
+ async function provision(root, terminalId) {
72
+ const wt = worktreePathFor(root, terminalId);
73
+ const branch = branchNameFor(terminalId);
74
+ if (fsSync.existsSync(wt))
75
+ return wt;
76
+ await fs.mkdir(path.dirname(wt), { recursive: true });
77
+ // If the branch already exists (e.g. left over from a previous run), reuse
78
+ // it instead of failing. Surfaces typically reuse a terminal-id when
79
+ // restoring a session.
80
+ let branchExists = false;
81
+ try {
82
+ await execFileAsync('git', ['rev-parse', '--verify', `refs/heads/${branch}`], { cwd: root });
83
+ branchExists = true;
84
+ }
85
+ catch {
86
+ branchExists = false;
87
+ }
88
+ const args = branchExists
89
+ ? ['worktree', 'add', wt, branch]
90
+ : ['worktree', 'add', '-b', branch, wt, 'HEAD'];
91
+ await execFileAsync('git', args, { cwd: root });
92
+ return wt;
93
+ }
94
+ async function release(root, terminalId, force) {
95
+ const wt = worktreePathFor(root, terminalId);
96
+ const branch = branchNameFor(terminalId);
97
+ if (!fsSync.existsSync(wt)) {
98
+ // Already gone; treat as success but tell the caller.
99
+ return { removed: false, reason: 'worktree does not exist' };
100
+ }
101
+ if (!force) {
102
+ const report = await inspect(root, terminalId);
103
+ if (report.dirty)
104
+ return { removed: false, reason: 'worktree has uncommitted changes' };
105
+ if (!report.hasUpstream) {
106
+ // Local-only commits exist if HEAD differs from origin/main.
107
+ try {
108
+ const { stdout } = await execFileAsync('git', ['rev-list', '--count', 'origin/main..HEAD'], { cwd: wt });
109
+ if (parseInt(stdout.trim(), 10) > 0) {
110
+ return { removed: false, reason: 'branch has local commits not on origin/main' };
111
+ }
112
+ }
113
+ catch {
114
+ return { removed: false, reason: 'cannot verify branch state vs origin/main' };
115
+ }
116
+ }
117
+ if (report.aheadOfUpstream)
118
+ return { removed: false, reason: 'branch has unpushed commits' };
119
+ }
120
+ try {
121
+ await execFileAsync('git', ['worktree', 'remove', force ? '--force' : '', wt].filter(Boolean), { cwd: root });
122
+ }
123
+ catch (err) {
124
+ if (err.message?.includes('is not a working tree')) {
125
+ await execFileAsync('git', ['worktree', 'prune'], { cwd: root });
126
+ }
127
+ else {
128
+ throw err;
129
+ }
130
+ }
131
+ // Delete the branch if it's safe to do so. -d (lowercase) refuses to drop
132
+ // an unmerged branch by itself, which is exactly the safety net we want.
133
+ try {
134
+ await execFileAsync('git', ['branch', '-d', branch], { cwd: root });
135
+ }
136
+ catch {
137
+ // Branch may not exist or may not be merged. We already validated above;
138
+ // leaving the branch behind is preferable to silently dropping commits.
139
+ }
140
+ return { removed: true };
141
+ }
142
+ async function listAgentWorktrees(root) {
143
+ const dir = path.join(root, WORKTREE_SUBDIR);
144
+ if (!fsSync.existsSync(dir))
145
+ return [];
146
+ const entries = await fs.readdir(dir, { withFileTypes: true });
147
+ return entries.filter(e => e.isDirectory()).map(e => e.name);
148
+ }
149
+ export function registerWorktreeCommands(program) {
150
+ const wt = program
151
+ .command('worktree')
152
+ .description('Provision, release, and prune per-terminal git worktrees for agent isolation.');
153
+ setHelpSections(wt, {
154
+ examples: `
155
+ # Create (or reuse) an isolated worktree for an agent terminal — prints the path
156
+ agents worktree provision CC-1747509823-3
157
+
158
+ # Remove the worktree if it's clean and merged/pushed
159
+ agents worktree release CC-1747509823-3
160
+
161
+ # Preview what 'prune' would release across all agent worktrees
162
+ agents worktree prune --dry-run
163
+
164
+ # Release every agent worktree that's safe to remove
165
+ agents worktree prune
166
+ `,
167
+ notes: `
168
+ Worktrees live at <repo>/.history/worktrees/<terminal-id> on branch agent/<terminal-id>.
169
+ Use --force on 'release' to skip safety checks (DANGEROUS — discards unpushed work).
170
+ `,
171
+ });
172
+ wt.command('provision <terminal-id>')
173
+ .description('Create (or reuse) an isolated worktree for an agent terminal. Prints the absolute path.')
174
+ .option('--root <path>', 'Repo root (defaults to current working directory)')
175
+ .action(async (terminalId, opts) => {
176
+ if (!isValidTerminalId(terminalId)) {
177
+ die(`Invalid terminal-id: ${terminalId} (allowed: [A-Za-z0-9._-], <=128 chars)`);
178
+ }
179
+ const root = await gitRoot(opts.root ?? process.cwd());
180
+ const wtPath = await provision(root, terminalId);
181
+ console.log(wtPath);
182
+ });
183
+ wt.command('release <terminal-id>')
184
+ .description('Remove the worktree if clean and the branch is merged or has no unpushed commits.')
185
+ .option('--root <path>', 'Repo root (defaults to current working directory)')
186
+ .option('--force', 'Skip safety checks (DANGEROUS: discards unpushed work)')
187
+ .action(async (terminalId, opts) => {
188
+ if (!isValidTerminalId(terminalId)) {
189
+ die(`Invalid terminal-id: ${terminalId} (allowed: [A-Za-z0-9._-], <=128 chars)`);
190
+ }
191
+ const root = await gitRoot(opts.root ?? process.cwd());
192
+ const result = await release(root, terminalId, Boolean(opts.force));
193
+ if (result.removed) {
194
+ console.log(chalk.green(`removed ${worktreePathFor(root, terminalId)}`));
195
+ }
196
+ else {
197
+ console.log(chalk.yellow(`kept ${worktreePathFor(root, terminalId)} (${result.reason})`));
198
+ }
199
+ });
200
+ wt.command('prune')
201
+ .description('Try to release every agent worktree under .history/worktrees/. Skips dirty or unpushed ones.')
202
+ .option('--root <path>', 'Repo root (defaults to current working directory)')
203
+ .option('--dry-run', 'Report what would be removed without touching anything')
204
+ .action(async (opts) => {
205
+ const root = await gitRoot(opts.root ?? process.cwd());
206
+ const ids = await listAgentWorktrees(root);
207
+ if (ids.length === 0) {
208
+ console.log(chalk.gray('no agent worktrees to prune'));
209
+ return;
210
+ }
211
+ for (const id of ids) {
212
+ if (!isValidTerminalId(id)) {
213
+ console.log(chalk.yellow(`skip ${id} (name not in expected format)`));
214
+ continue;
215
+ }
216
+ if (opts.dryRun) {
217
+ const report = await inspect(root, id);
218
+ const blocker = report.dirty
219
+ ? 'dirty'
220
+ : report.aheadOfUpstream
221
+ ? 'unpushed'
222
+ : !report.hasUpstream
223
+ ? 'never pushed'
224
+ : null;
225
+ if (blocker) {
226
+ console.log(chalk.yellow(`keep ${id} (${blocker})`));
227
+ }
228
+ else {
229
+ console.log(chalk.green(`remove ${id}`));
230
+ }
231
+ continue;
232
+ }
233
+ const result = await release(root, id, false);
234
+ if (result.removed) {
235
+ console.log(chalk.green(`removed ${id}`));
236
+ }
237
+ else {
238
+ console.log(chalk.yellow(`kept ${id} (${result.reason})`));
239
+ }
240
+ }
241
+ });
242
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { registerComputerSubcommands } from './commands/computer.js';
4
+ const program = new Command();
5
+ program.name('computer').description('Drive macOS apps via Accessibility — list, screenshot, click, type');
6
+ registerComputerSubcommands(program);
7
+ program.parse();
package/dist/index.js CHANGED
@@ -23,6 +23,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
23
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
24
24
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
25
25
  const VERSION = packageJson.version;
26
+ const NPM_PACKAGE_NAME = '@phnx-labs/agents-cli';
26
27
  // Detect dev/working-tree builds and default the noisy startup steps off.
27
28
  // Three cases trip this:
28
29
  // 1. Dev install (scripts/install.sh) — package.json version stamped 0.0.0-dev.<sha>
@@ -78,11 +79,13 @@ import { registerDoctorCommand } from './commands/doctor.js';
78
79
  import { registerSubagentsCommands } from './commands/subagents.js';
79
80
  import { registerPluginsCommands } from './commands/plugins.js';
80
81
  import { registerWorkflowsCommands } from './commands/workflows.js';
82
+ import { registerWorktreeCommands } from './commands/worktree.js';
81
83
  import { registerSyncCommand } from './commands/sync.js';
82
84
  import { registerRefreshRulesCommand } from './commands/refresh-rules.js';
83
85
  import { registerDriveCommands } from './commands/drive.js';
84
86
  import { registerPtyCommands } from './commands/pty.js';
85
87
  import { registerBrowserCommand } from './commands/browser.js';
88
+ import { registerComputerCommand } from './commands/computer.js';
86
89
  import { registerProfilesCommands } from './commands/profiles.js';
87
90
  import { registerSecretsCommands } from './commands/secrets.js';
88
91
  import { registerFactoryCommands } from './commands/factory.js';
@@ -268,10 +271,36 @@ function saveUpdateCheck(latestVersion) {
268
271
  /* best-effort cache update */
269
272
  }
270
273
  }
274
+ /** Fetch the exact latest npm version plus its registry integrity hash. */
275
+ async function fetchLatestNpmPackageMetadata(timeoutMs = 5000) {
276
+ const response = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`, {
277
+ signal: AbortSignal.timeout(timeoutMs),
278
+ });
279
+ if (!response.ok) {
280
+ throw new Error('Could not reach npm registry');
281
+ }
282
+ const data = await response.json();
283
+ if (typeof data.version !== 'string' || typeof data.dist?.integrity !== 'string') {
284
+ throw new Error('npm registry response did not include version and integrity');
285
+ }
286
+ return { version: data.version, integrity: data.dist.integrity };
287
+ }
288
+ function printResolvedPackage(metadata) {
289
+ console.log(chalk.gray(`Resolved: ${NPM_PACKAGE_NAME}@${metadata.version}`));
290
+ console.log(chalk.gray(`Integrity: ${metadata.integrity}`));
291
+ }
292
+ async function installResolvedPackage(metadata) {
293
+ const { execFile } = await import('child_process');
294
+ const { promisify } = await import('util');
295
+ const execFileAsync = promisify(execFile);
296
+ const installArgs = ['install', '-g', '@phnx-labs/agents-cli', '--ignore-scripts'];
297
+ installArgs[2] = `${NPM_PACKAGE_NAME}@${metadata.version}`;
298
+ await execFileAsync('npm', installArgs);
299
+ }
271
300
  /** Present an interactive upgrade prompt (TTY) or a one-line hint (non-TTY). */
272
301
  async function promptUpgrade(latestVersion) {
273
302
  if (!isInteractiveTerminal()) {
274
- console.error(chalk.yellow(`Update available: ${VERSION} -> ${latestVersion}. Run: npm install -g @phnx-labs/agents-cli@latest`));
303
+ console.error(chalk.yellow(`Update available: ${VERSION} -> ${latestVersion}. Run: agents upgrade --yes`));
275
304
  return;
276
305
  }
277
306
  const answer = await select({
@@ -299,14 +328,24 @@ async function promptUpgrade(latestVersion) {
299
328
  return;
300
329
  }
301
330
  if (answer === 'now') {
302
- const { execFile, spawnSync } = await import('child_process');
303
- const { promisify } = await import('util');
304
- const execFileAsync = promisify(execFile);
305
- const spinner = ora('Upgrading...').start();
331
+ const { spawnSync } = await import('child_process');
332
+ let spinner = ora('Resolving package metadata...').start();
306
333
  try {
307
- await execFileAsync('npm', ['install', '-g', '@phnx-labs/agents-cli@latest']);
308
- spinner.succeed(`Upgraded to ${latestVersion}`);
309
- await showWhatsNew(VERSION, latestVersion);
334
+ const metadata = await fetchLatestNpmPackageMetadata();
335
+ spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${metadata.version}`);
336
+ printResolvedPackage(metadata);
337
+ const approved = await confirm({
338
+ message: `Install ${NPM_PACKAGE_NAME}@${metadata.version}?`,
339
+ default: false,
340
+ });
341
+ if (!approved) {
342
+ console.log(chalk.gray('Upgrade cancelled'));
343
+ return;
344
+ }
345
+ spinner = ora('Upgrading...').start();
346
+ await installResolvedPackage(metadata);
347
+ spinner.succeed(`Upgraded to ${metadata.version}`);
348
+ await showWhatsNew(VERSION, metadata.version);
310
349
  console.log();
311
350
  // Re-exec with new version and exit
312
351
  const result = spawnSync('agents', process.argv.slice(2), {
@@ -317,7 +356,7 @@ async function promptUpgrade(latestVersion) {
317
356
  }
318
357
  catch {
319
358
  spinner.fail('Upgrade failed');
320
- console.log(chalk.gray('Run manually: npm install -g @phnx-labs/agents-cli@latest'));
359
+ console.log(chalk.gray('Run manually: agents upgrade --yes'));
321
360
  }
322
361
  console.log();
323
362
  }
@@ -493,6 +532,7 @@ registerMcpCommands(program);
493
532
  registerSubagentsCommands(program);
494
533
  registerPluginsCommands(program);
495
534
  registerWorkflowsCommands(program);
535
+ registerWorktreeCommands(program);
496
536
  registerVersionsCommands(program);
497
537
  registerImportCommand(program);
498
538
  registerPackagesCommands(program);
@@ -525,6 +565,7 @@ registerUsageCommand(program);
525
565
  registerAliasCommand(program);
526
566
  registerPtyCommands(program);
527
567
  registerBrowserCommand(program);
568
+ registerComputerCommand(program);
528
569
  // Deprecated 'jobs' and 'cron' aliases for 'routines'
529
570
  for (const alias of ['jobs', 'cron']) {
530
571
  program
@@ -541,18 +582,12 @@ for (const alias of ['jobs', 'cron']) {
541
582
  program
542
583
  .command('upgrade')
543
584
  .description('Upgrade agents-cli to the latest version')
544
- .action(async () => {
545
- const spinner = ora('Checking for updates...').start();
585
+ .option('-y, --yes', 'Install without an interactive confirmation prompt')
586
+ .action(async (options) => {
587
+ let spinner = ora('Checking for updates...').start();
546
588
  try {
547
- const response = await fetch('https://registry.npmjs.org/@phnx-labs/agents-cli/latest', {
548
- signal: AbortSignal.timeout(5000),
549
- });
550
- if (!response.ok) {
551
- spinner.fail('Could not reach npm registry');
552
- process.exit(1);
553
- }
554
- const data = (await response.json());
555
- const latestVersion = data.version;
589
+ const metadata = await fetchLatestNpmPackageMetadata();
590
+ const latestVersion = metadata.version;
556
591
  if (latestVersion === VERSION) {
557
592
  spinner.succeed(`Already on latest version (${VERSION})`);
558
593
  return;
@@ -561,17 +596,26 @@ program
561
596
  spinner.succeed(`Already ahead of latest (${VERSION} >= ${latestVersion})`);
562
597
  return;
563
598
  }
564
- spinner.text = `Upgrading ${VERSION} -> ${latestVersion}...`;
565
- const { execFile } = await import('child_process');
566
- const { promisify } = await import('util');
567
- const execFileAsync = promisify(execFile);
568
- await execFileAsync('npm', ['install', '-g', '@phnx-labs/agents-cli@latest']);
599
+ spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${latestVersion}`);
600
+ printResolvedPackage(metadata);
601
+ if (isInteractiveTerminal() && !options.yes) {
602
+ const approved = await confirm({
603
+ message: `Install ${NPM_PACKAGE_NAME}@${latestVersion}?`,
604
+ default: false,
605
+ });
606
+ if (!approved) {
607
+ console.log(chalk.gray('Upgrade cancelled'));
608
+ return;
609
+ }
610
+ }
611
+ spinner = ora(`Upgrading ${VERSION} -> ${latestVersion}...`).start();
612
+ await installResolvedPackage(metadata);
569
613
  spinner.succeed(`Upgraded to ${latestVersion}`);
570
614
  await showWhatsNew(VERSION, latestVersion);
571
615
  }
572
616
  catch (err) {
573
617
  spinner.fail('Upgrade failed');
574
- console.log(chalk.gray('Run manually: npm install -g @phnx-labs/agents-cli@latest'));
618
+ console.log(chalk.gray('Run manually: agents upgrade --yes'));
575
619
  }
576
620
  });
577
621
  registerPullCommand(program);
@@ -102,6 +102,7 @@ export declare function isMcpRegistered(agentId: AgentId, mcpName: string): Prom
102
102
  export declare function registerMcp(agentId: AgentId, name: string, command: string, scope?: 'user' | 'project', transport?: string, options?: {
103
103
  home?: string;
104
104
  binary?: string;
105
+ headers?: Record<string, string>;
105
106
  }): Promise<{
106
107
  success: boolean;
107
108
  error?: string;
@@ -128,7 +129,9 @@ export interface McpTargetOperationResult {
128
129
  export declare function registerMcpToTargets(targets: {
129
130
  directAgents: AgentId[];
130
131
  versionSelections: Map<AgentId, string[]>;
131
- }, name: string, command: string, scope?: 'user' | 'project', transport?: string): Promise<McpTargetOperationResult[]>;
132
+ }, name: string, command: string, scope?: 'user' | 'project', transport?: string, options?: {
133
+ headers?: Record<string, string>;
134
+ }): Promise<McpTargetOperationResult[]>;
132
135
  /**
133
136
  * Unregister an MCP server from multiple agent targets, including both direct
134
137
  * agents and specific version-managed installs.