@phnx-labs/agents-cli 1.20.33 → 1.20.35

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 (159) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +32 -2
  3. package/dist/commands/computer-actions.d.ts +15 -0
  4. package/dist/commands/computer-actions.js +120 -22
  5. package/dist/commands/computer.d.ts +23 -0
  6. package/dist/commands/computer.js +45 -3
  7. package/dist/commands/cost.js +17 -12
  8. package/dist/commands/doctor.d.ts +11 -0
  9. package/dist/commands/doctor.js +108 -14
  10. package/dist/commands/events.d.ts +16 -0
  11. package/dist/commands/events.js +173 -0
  12. package/dist/commands/exec.js +6 -1
  13. package/dist/commands/hosts.js +28 -24
  14. package/dist/commands/import.js +1 -1
  15. package/dist/commands/inspect.d.ts +2 -0
  16. package/dist/commands/inspect.js +54 -7
  17. package/dist/commands/logs.d.ts +17 -0
  18. package/dist/commands/logs.js +139 -0
  19. package/dist/commands/models.d.ts +3 -0
  20. package/dist/commands/models.js +27 -8
  21. package/dist/commands/permissions.js +9 -2
  22. package/dist/commands/repo.d.ts +34 -0
  23. package/dist/commands/repo.js +243 -65
  24. package/dist/commands/resource-view.d.ts +20 -0
  25. package/dist/commands/resource-view.js +90 -28
  26. package/dist/commands/rules.js +1 -1
  27. package/dist/commands/secrets-migrate.js +23 -11
  28. package/dist/commands/secrets.d.ts +20 -0
  29. package/dist/commands/secrets.js +93 -17
  30. package/dist/commands/sessions-resume.d.ts +2 -0
  31. package/dist/commands/sessions-resume.js +209 -0
  32. package/dist/commands/sessions-tail.d.ts +10 -0
  33. package/dist/commands/sessions-tail.js +11 -0
  34. package/dist/commands/sessions.d.ts +73 -1
  35. package/dist/commands/sessions.js +273 -53
  36. package/dist/commands/status.d.ts +12 -0
  37. package/dist/commands/status.js +81 -0
  38. package/dist/commands/teams.js +70 -6
  39. package/dist/commands/versions.js +2 -1
  40. package/dist/commands/view.d.ts +42 -0
  41. package/dist/commands/view.js +213 -83
  42. package/dist/commands/wallet.d.ts +6 -0
  43. package/dist/commands/wallet.js +22 -5
  44. package/dist/index.js +61 -32
  45. package/dist/lib/acp/harnesses.d.ts +1 -1
  46. package/dist/lib/acp/harnesses.js +2 -2
  47. package/dist/lib/agents.d.ts +12 -0
  48. package/dist/lib/agents.js +145 -41
  49. package/dist/lib/browser/cdp.js +5 -1
  50. package/dist/lib/browser/chrome.js +20 -0
  51. package/dist/lib/browser/drivers/ssh.d.ts +27 -0
  52. package/dist/lib/browser/drivers/ssh.js +94 -19
  53. package/dist/lib/browser/ipc.d.ts +3 -0
  54. package/dist/lib/browser/ipc.js +13 -9
  55. package/dist/lib/browser/service.js +71 -15
  56. package/dist/lib/daemon.d.ts +35 -0
  57. package/dist/lib/daemon.js +33 -5
  58. package/dist/lib/devices/connect.d.ts +3 -2
  59. package/dist/lib/devices/connect.js +5 -3
  60. package/dist/lib/doctor-diff.js +29 -2
  61. package/dist/lib/drift-sync.d.ts +43 -0
  62. package/dist/lib/drift-sync.js +179 -0
  63. package/dist/lib/events.d.ts +9 -2
  64. package/dist/lib/events.js +70 -11
  65. package/dist/lib/exec.d.ts +15 -0
  66. package/dist/lib/exec.js +43 -11
  67. package/dist/lib/hooks.js +8 -2
  68. package/dist/lib/hosts/dispatch.js +6 -7
  69. package/dist/lib/hosts/logs.d.ts +16 -0
  70. package/dist/lib/hosts/logs.js +45 -0
  71. package/dist/lib/hosts/progress.d.ts +66 -0
  72. package/dist/lib/hosts/progress.js +125 -17
  73. package/dist/lib/hosts/ready.d.ts +23 -2
  74. package/dist/lib/hosts/ready.js +35 -13
  75. package/dist/lib/hosts/reconcile.d.ts +53 -0
  76. package/dist/lib/hosts/reconcile.js +81 -0
  77. package/dist/lib/hosts/tasks.d.ts +8 -0
  78. package/dist/lib/hosts/tasks.js +14 -0
  79. package/dist/lib/permissions.d.ts +4 -0
  80. package/dist/lib/permissions.js +35 -0
  81. package/dist/lib/picker.d.ts +24 -0
  82. package/dist/lib/picker.js +124 -0
  83. package/dist/lib/platform/winpath.d.ts +31 -2
  84. package/dist/lib/platform/winpath.js +133 -24
  85. package/dist/lib/plugin-marketplace.d.ts +30 -0
  86. package/dist/lib/plugin-marketplace.js +215 -2
  87. package/dist/lib/plugins.d.ts +5 -0
  88. package/dist/lib/plugins.js +45 -4
  89. package/dist/lib/pwsh.d.ts +11 -0
  90. package/dist/lib/pwsh.js +13 -0
  91. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  92. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  93. package/dist/lib/secrets/agent.d.ts +42 -1
  94. package/dist/lib/secrets/agent.js +89 -11
  95. package/dist/lib/secrets/bundles.js +40 -9
  96. package/dist/lib/secrets/filestore.js +31 -1
  97. package/dist/lib/secrets/index.d.ts +33 -1
  98. package/dist/lib/secrets/index.js +90 -9
  99. package/dist/lib/secrets/windows.d.ts +74 -0
  100. package/dist/lib/secrets/windows.js +440 -0
  101. package/dist/lib/session/active.d.ts +7 -0
  102. package/dist/lib/session/active.js +11 -3
  103. package/dist/lib/session/discover.js +94 -0
  104. package/dist/lib/session/parse.d.ts +6 -0
  105. package/dist/lib/session/parse.js +283 -2
  106. package/dist/lib/session/pid-registry.d.ts +21 -0
  107. package/dist/lib/session/pid-registry.js +80 -0
  108. package/dist/lib/session/remote-active.d.ts +26 -0
  109. package/dist/lib/session/remote-active.js +141 -0
  110. package/dist/lib/session/remote.js +2 -6
  111. package/dist/lib/session/render.js +1 -1
  112. package/dist/lib/session/sync/config.d.ts +8 -0
  113. package/dist/lib/session/sync/config.js +11 -2
  114. package/dist/lib/session/types.d.ts +1 -1
  115. package/dist/lib/session/types.js +1 -1
  116. package/dist/lib/shims.d.ts +30 -2
  117. package/dist/lib/shims.js +107 -50
  118. package/dist/lib/ssh-exec.d.ts +26 -3
  119. package/dist/lib/ssh-exec.js +45 -3
  120. package/dist/lib/ssh-tunnel.d.ts +24 -5
  121. package/dist/lib/ssh-tunnel.js +60 -62
  122. package/dist/lib/staleness/writers/hooks.js +1 -1
  123. package/dist/lib/startup/command-registry.d.ts +3 -0
  124. package/dist/lib/startup/command-registry.js +6 -0
  125. package/dist/lib/sync-status.d.ts +102 -0
  126. package/dist/lib/sync-status.js +135 -0
  127. package/dist/lib/teams/agents.d.ts +24 -0
  128. package/dist/lib/teams/agents.js +30 -1
  129. package/dist/lib/teams/registry.js +25 -9
  130. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  131. package/dist/lib/terminal/backends/ghostty.js +69 -0
  132. package/dist/lib/terminal/backends/index.d.ts +18 -0
  133. package/dist/lib/terminal/backends/index.js +29 -0
  134. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  135. package/dist/lib/terminal/backends/iterm.js +62 -0
  136. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  137. package/dist/lib/terminal/backends/tmux.js +23 -0
  138. package/dist/lib/terminal/engine.d.ts +39 -0
  139. package/dist/lib/terminal/engine.js +54 -0
  140. package/dist/lib/terminal/index.d.ts +14 -0
  141. package/dist/lib/terminal/index.js +5 -0
  142. package/dist/lib/terminal/policy.d.ts +11 -0
  143. package/dist/lib/terminal/policy.js +11 -0
  144. package/dist/lib/terminal/quote.d.ts +11 -0
  145. package/dist/lib/terminal/quote.js +13 -0
  146. package/dist/lib/terminal/shell.d.ts +6 -0
  147. package/dist/lib/terminal/shell.js +23 -0
  148. package/dist/lib/terminal/transport.d.ts +15 -0
  149. package/dist/lib/terminal/transport.js +40 -0
  150. package/dist/lib/terminal/types.d.ts +59 -0
  151. package/dist/lib/terminal/types.js +13 -0
  152. package/dist/lib/types.d.ts +27 -1
  153. package/dist/lib/usage.d.ts +30 -0
  154. package/dist/lib/usage.js +159 -2
  155. package/dist/lib/versions.d.ts +7 -3
  156. package/dist/lib/versions.js +10 -5
  157. package/dist/lib/whats-new.d.ts +9 -0
  158. package/dist/lib/whats-new.js +35 -0
  159. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -51,9 +51,11 @@ if (IS_DEV_BUILD) {
51
51
  // module on each invocation (which loaded the whole ~50-module tree before the
52
52
  // first byte of output), the registry maps a command name to a thunk that
53
53
  // imports only what that command needs. See src/lib/startup/command-registry.ts.
54
- import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadBrowser, loadComputer, loadHosts, loadSsh, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
54
+ import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadSsh, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
55
55
  import { applyGlobalHelpConventions } from './lib/help.js';
56
+ import { renderWhatsNew } from './lib/whats-new.js';
56
57
  import { IS_WINDOWS } from './lib/platform/index.js';
58
+ import { emit, redactArgs } from './lib/events.js';
57
59
  // Transparent shim delegate: the generated Windows `.cmd` shims invoke
58
60
  // `agents __shim <agent>[@version] <raw args>`. Intercept here, before commander
59
61
  // parses anything, so the agent's own flags (`--help`, `--version`, etc.) pass
@@ -75,6 +77,56 @@ program
75
77
  .version(VERSION)
76
78
  .helpOption('-h, --help', 'Show help')
77
79
  .addHelpCommand(false);
80
+ // ─── Audit backbone ────────────────────────────────────────────────────────────
81
+ // One choke point logs every `agents <module> <cmd>` invocation to the structured
82
+ // event log — so team create/disband, agent run, secrets access, and everything
83
+ // else is captured generically (with SSH/remote-user attribution added in emit()),
84
+ // no per-command wiring. `agents events` reads it back. Attached to the root
85
+ // program, so it's inherited by every subcommand regardless of lazy registration.
86
+ /** Command path from the acting command up to (but excluding) the `agents` root. */
87
+ function auditCommandPath(cmd) {
88
+ const parts = [];
89
+ let c = cmd;
90
+ while (c && c.name() && c.name() !== 'agents') {
91
+ parts.unshift(c.name());
92
+ c = c.parent;
93
+ }
94
+ return parts;
95
+ }
96
+ const auditStarts = new WeakMap();
97
+ program.hook('preAction', (_thisCommand, actionCommand) => {
98
+ try {
99
+ const parts = auditCommandPath(actionCommand);
100
+ if (parts.length === 0)
101
+ return;
102
+ auditStarts.set(actionCommand, Date.now());
103
+ emit('command.start', {
104
+ module: parts[0],
105
+ command: parts.join(' '),
106
+ args: redactArgs(actionCommand.args),
107
+ cwd: process.cwd(),
108
+ });
109
+ }
110
+ catch {
111
+ // Audit logging must never break command dispatch.
112
+ }
113
+ });
114
+ program.hook('postAction', (_thisCommand, actionCommand) => {
115
+ try {
116
+ const parts = auditCommandPath(actionCommand);
117
+ if (parts.length === 0)
118
+ return;
119
+ const started = auditStarts.get(actionCommand);
120
+ emit('command.end', {
121
+ module: parts[0],
122
+ command: parts.join(' '),
123
+ ...(started !== undefined ? { durationMs: Date.now() - started } : {}),
124
+ });
125
+ }
126
+ catch {
127
+ // Best-effort completion record; the start line is the durable audit fact.
128
+ }
129
+ });
78
130
  // Custom help for the main program only
79
131
  const originalHelpInformation = program.helpInformation.bind(program);
80
132
  program.helpInformation = function () {
@@ -121,6 +173,7 @@ Run and dispatch:
121
173
  teams Coordinate multiple agents on shared work
122
174
  routines Run agents on a cron schedule (scheduler auto-starts)
123
175
  sessions Browse, search, and replay past runs (live-search in TTY; grouped by workspace)
176
+ logs [id] Show a run's log — host-dispatch task or session; -f to follow
124
177
  browser Automate a browser — navigate, click, screenshot, console, network
125
178
  pty Drive interactive terminal programs (REPLs, TUIs) via a persistent PTY session
126
179
 
@@ -175,41 +228,13 @@ async function showWhatsNew(fromVersion, toVersion) {
175
228
  const response = await fetch(`https://unpkg.com/@phnx-labs/agents-cli@${toVersion}/CHANGELOG.md`);
176
229
  if (!response.ok)
177
230
  return;
178
- const changelog = await response.text();
179
- const lines = changelog.split('\n');
180
- const relevantChanges = [];
181
- let inRelevantSection = false;
182
- let currentVersion = '';
183
- for (const line of lines) {
184
- const versionMatch = line.match(/^## (\d+\.\d+\.\d+)/);
185
- if (versionMatch) {
186
- currentVersion = versionMatch[1];
187
- // Only the range the user actually moved through: (fromVersion, toVersion].
188
- // Bounding the top end matters when upgrading to a specific older
189
- // version, and guards against a changelog that lists unreleased entries.
190
- const inRange = compareVersions(currentVersion, fromVersion) > 0 &&
191
- compareVersions(currentVersion, toVersion) <= 0;
192
- inRelevantSection = inRange;
193
- if (inRelevantSection) {
194
- relevantChanges.push('');
195
- relevantChanges.push(chalk.bold(`v${currentVersion}`));
196
- }
197
- continue;
198
- }
199
- if (inRelevantSection && line.trim()) {
200
- if (line.startsWith('**') && line.endsWith('**')) {
201
- relevantChanges.push(chalk.cyan(line.replace(/\*\*/g, '')));
202
- }
203
- else if (line.startsWith('- ')) {
204
- relevantChanges.push(chalk.gray(` ${line}`));
205
- }
206
- }
207
- }
231
+ const relevantChanges = renderWhatsNew(await response.text(), fromVersion, toVersion);
208
232
  if (relevantChanges.length > 0) {
209
233
  console.log(chalk.bold("\nWhat's new:\n"));
210
234
  for (const line of relevantChanges) {
211
235
  console.log(line);
212
236
  }
237
+ console.log(chalk.gray('\nFull notes: https://github.com/phnx-labs/agents-cli/blob/main/CHANGELOG.md'));
213
238
  console.log();
214
239
  }
215
240
  }
@@ -764,6 +789,7 @@ async function registerAllEagerCommands() {
764
789
  await reg(loadTrash);
765
790
  await reg(loadRestore);
766
791
  await reg(loadDoctor);
792
+ await reg(loadStatus);
767
793
  registerExecAliasCommand(program);
768
794
  await reg(loadProfiles);
769
795
  await reg(loadSecrets);
@@ -784,6 +810,8 @@ async function registerAllEagerCommands() {
784
810
  await reg(loadBrowser);
785
811
  await reg(loadComputer);
786
812
  await reg(loadHosts);
813
+ await reg(loadLogs);
814
+ await reg(loadEvents);
787
815
  await reg(loadSsh);
788
816
  registerJobsCronAliasCommand(program, 'jobs');
789
817
  registerJobsCronAliasCommand(program, 'cron');
@@ -894,7 +922,8 @@ if (!helpOrVersionRequested) {
894
922
  // fire-and-forget the next background sync. System repo gets a real fast-forward
895
923
  // pull (read-only locally, safe). User repo and extras get fetch-only + a
896
924
  // status marker that we'll print on the *next* invocation.
897
- const { spawnDetachedSync } = await import('./lib/auto-pull.js');
925
+ const { spawnDetachedSync, printPendingUpdateNotices } = await import('./lib/auto-pull.js');
926
+ printPendingUpdateNotices();
898
927
  spawnDetachedSync();
899
928
  }
900
929
  // First-run experience: no args + no config yet + TTY -> launch interactive setup.
@@ -2,7 +2,7 @@
2
2
  * Per-harness spawn configuration for Agent Client Protocol (ACP) mode.
3
3
  *
4
4
  * Each entry describes how to launch a coding agent CLI as an ACP server
5
- * (stdio JSON-RPC). Unsupported harnesses (amp, roo, goose-stdio) are omitted;
5
+ * (stdio JSON-RPC). Unsupported harnesses (amp, goose-stdio) are omitted;
6
6
  * callers should fall back to the legacy direct-exec path for those.
7
7
  *
8
8
  * Sources: https://agentclientprotocol.com/get-started/agents + vendor docs.
@@ -2,7 +2,7 @@
2
2
  * Per-harness spawn configuration for Agent Client Protocol (ACP) mode.
3
3
  *
4
4
  * Each entry describes how to launch a coding agent CLI as an ACP server
5
- * (stdio JSON-RPC). Unsupported harnesses (amp, roo, goose-stdio) are omitted;
5
+ * (stdio JSON-RPC). Unsupported harnesses (amp, goose-stdio) are omitted;
6
6
  * callers should fall back to the legacy direct-exec path for those.
7
7
  *
8
8
  * Sources: https://agentclientprotocol.com/get-started/agents + vendor docs.
@@ -61,7 +61,7 @@ export const ACP_HARNESSES = {
61
61
  // goose: ACP over HTTP via `goosed`, not a clean stdio subcommand.
62
62
  // copilot, kiro: excluded for now (not installed in the reference environment,
63
63
  // no local verification possible).
64
- // amp, roo: not on the ACP agents list.
64
+ // amp: not on the ACP agents list.
65
65
  };
66
66
  /** Returns the ACP spawn spec for an agent, or undefined if the harness does not speak ACP. */
67
67
  export function getAcpSpec(agent) {
@@ -219,6 +219,18 @@ export declare const AGENT_NAME_ALIASES: Record<string, AgentId>;
219
219
  export declare function resolveAgentName(input: string): AgentId | null;
220
220
  /** Check whether the input string matches any known agent name or alias. */
221
221
  export declare function isAgentName(input: string): boolean;
222
+ /**
223
+ * Build the deprecation notice lines for an agent, or null if it isn't
224
+ * deprecated. Split from the printer so tests can assert the content without
225
+ * capturing stdout. Lines are plain (uncolored) text.
226
+ */
227
+ export declare function deprecationNotice(agent: AgentId): string[] | null;
228
+ /**
229
+ * Print a deprecation warning (yellow) if the agent's registry entry carries a
230
+ * `deprecated` marker; no-op otherwise. Call from any user entry point that
231
+ * acts on a chosen agent — install (`agents add`) and `agents teams add`.
232
+ */
233
+ export declare function warnAgentDeprecated(agent: AgentId): void;
222
234
  /** Format an error message for an unrecognized agent name, listing valid options. */
223
235
  export declare function formatAgentError(agentName: string, validAgents?: AgentId[]): string;
224
236
  export {};
@@ -2,7 +2,7 @@
2
2
  * Core agent configuration and detection module.
3
3
  *
4
4
  * Defines the canonical registry of all supported AI coding agents (Claude, Codex,
5
- * Gemini, Cursor, OpenCode, OpenClaw, Copilot, Amp, Kiro, Goose, Roo, Grok) with their
5
+ * Gemini, Cursor, OpenCode, OpenClaw, Copilot, Amp, Kiro, Goose, Grok) with their
6
6
  * CLI commands, config paths, capability flags, and MCP integration points.
7
7
  *
8
8
  * Provides functions for detecting installed CLIs, resolving version-managed binaries,
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { execFile } from 'child_process';
12
12
  import { promisify } from 'util';
13
+ import * as crypto from 'crypto';
13
14
  import * as fs from 'fs';
14
15
  import * as path from 'path';
15
16
  import * as os from 'os';
@@ -249,6 +250,16 @@ export const AGENTS = {
249
250
  variableSyntax: '{{args}}',
250
251
  supportsHooks: true,
251
252
  nativeAgentsSkillsDir: true,
253
+ // Google retired the Gemini CLI (announced at Google I/O 2026, May 19); the `gemini`
254
+ // command stopped serving free/Pro/Ultra requests on June 18, 2026. Antigravity CLI
255
+ // (`agy`) is the official successor. See warnAgentDeprecated() for the surfaced warning.
256
+ deprecated: {
257
+ by: 'Google',
258
+ date: 'June 18, 2026',
259
+ reason: 'The Gemini CLI was retired for free, Pro, and Ultra tiers and no longer serves requests (announced at Google I/O 2026 on May 19).',
260
+ replacement: 'antigravity',
261
+ url: 'https://developers.googleblog.com/an-important-update-transitioning-gemini-cli-to-antigravity-cli/',
262
+ },
252
263
  // gemini hooks: shipped in v0.26.0 (Jan 2026); older binaries silently ignore the `hooks` key.
253
264
  capabilities: { hooks: { since: '0.26.0' }, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'GEMINI.md' }, workflows: false, modes: ['plan', 'edit', 'skip'], rulesImports: true },
254
265
  },
@@ -316,11 +327,16 @@ export const AGENTS = {
316
327
  commandsSubdir: 'commands',
317
328
  skillsDir: path.join(HOME, '.copilot', 'skills'),
318
329
  hooksDir: 'hooks',
330
+ // Copilot reads a plugin's manifest from the plugin ROOT (plugin.json),
331
+ // not `.claude-plugin/plugin.json`. Mirror it there. Verified against the
332
+ // GitHub Copilot CLI (1.0.56): `copilot plugin install` produces an
333
+ // installed plugin dir whose manifest sits at the root.
334
+ pluginManifestDir: '.',
319
335
  instructionsFile: 'AGENTS.md',
320
336
  format: 'markdown',
321
337
  variableSyntax: '$ARGUMENTS',
322
338
  supportsHooks: false,
323
- capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'auto', 'skip'] },
339
+ capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: true, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit', 'auto', 'skip'] },
324
340
  },
325
341
  amp: {
326
342
  id: 'amp',
@@ -375,24 +391,6 @@ export const AGENTS = {
375
391
  supportsHooks: false,
376
392
  capabilities: { hooks: false, mcp: true, allowlist: false, skills: false, commands: false, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit'] },
377
393
  },
378
- roo: {
379
- id: 'roo',
380
- name: 'Roo Code',
381
- color: 'cyanBright',
382
- cliCommand: 'roo',
383
- npmPackage: '',
384
- installScript: 'curl -fsSL https://raw.githubusercontent.com/RooCodeInc/Roo-Code/main/apps/cli/install.sh | sh',
385
- configDir: path.join(HOME, '.roo'),
386
- commandsDir: path.join(HOME, '.roo', 'commands'),
387
- commandsSubdir: 'commands',
388
- skillsDir: path.join(HOME, '.roo', 'skills'),
389
- hooksDir: 'hooks',
390
- instructionsFile: 'AGENTS.md',
391
- format: 'markdown',
392
- variableSyntax: '$ARGUMENTS',
393
- supportsHooks: false,
394
- capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['plan', 'edit'] },
395
- },
396
394
  // Google Antigravity CLI (`agy`) — official replacement for Gemini CLI as of IO 2026.
397
395
  // configDir nests inside `~/.gemini/` since agy shares the parent dir with the Gemini
398
396
  // CLI but isolates its own state in the `antigravity-cli/` subdir. Per-version HOME
@@ -498,11 +496,26 @@ export const AGENTS = {
498
496
  // Install: `curl -fsSL https://app.factory.ai/cli | sh` (no npm package).
499
497
  // Binary is NOT in node_modules/.bin — the shim resolves the fixed install
500
498
  // path ~/.local/bin/droid directly (see the droid branch in shims.ts).
501
- // Config: `~/.factory/` (settings.json, mcp.json, droids/, commands/).
502
- // Memory: native AGENTS.md. Subagents = custom droids (top-level .md files
503
- // in ~/.factory/droids/). Config isolation rides the ~/.factory symlink
504
- // switch (no FACTORY_HOME env var exists). Headless: `droid exec "<prompt>"`
505
- // with --auto low|medium|high, -o stream-json, -m <model>, -r <effort>.
499
+ // Config: `~/.factory/` (settings.json, mcp.json, droids/, commands/, hooks/,
500
+ // plugins/). Memory: native AGENTS.md. Subagents = custom droids (top-level
501
+ // .md files in ~/.factory/droids/). Config isolation rides the ~/.factory
502
+ // symlink switch (no FACTORY_HOME env var exists). Headless:
503
+ // `droid exec "<prompt>"` with --auto low|medium|high, -o stream-json,
504
+ // -m <model>, -r <effort>.
505
+ //
506
+ // Hooks: Claude-shaped. settings.json carries a top-level `hooks` object keyed
507
+ // by event (PreToolUse, PostToolUse, UserPromptSubmit, SessionStart,
508
+ // SessionEnd, Stop, SubagentStop, Notification, PreCompact), each an array of
509
+ // `{ matcher?, hooks: [{ type: "command", command, timeout? }] }` matcher
510
+ // groups — verified against the droid binary's zod schema. So the Claude
511
+ // registrar is reused verbatim, just targeting `.factory/settings.json`.
512
+ //
513
+ // Plugins: native `droid plugin` command group + ~/.factory/plugins/ with the
514
+ // same marketplace layout Claude uses (known_marketplaces.json +
515
+ // marketplaces/<name>/plugins/<plugin>/). Droid's plugin manifest dir is
516
+ // `.factory-plugin/` (it also reads `.claude-plugin/` for compatibility) — set
517
+ // pluginManifestDir so syncPluginToVersion mirrors the manifest into it, the
518
+ // same pattern codex uses with `.codex-plugin`.
506
519
  droid: {
507
520
  id: 'droid',
508
521
  name: 'Droid',
@@ -516,20 +529,21 @@ export const AGENTS = {
516
529
  commandsSubdir: 'commands',
517
530
  skillsDir: '', // no skills concept
518
531
  hooksDir: 'hooks',
532
+ pluginManifestDir: '.factory-plugin',
519
533
  instructionsFile: 'AGENTS.md',
520
534
  format: 'markdown',
521
535
  variableSyntax: '$ARGUMENTS',
522
- supportsHooks: false,
536
+ supportsHooks: true,
523
537
  // Factory Droid Computers (cloud VMs) reached via `droid computer ssh` +
524
538
  // remote headless `droid exec`.
525
539
  cloudProvider: 'factory',
526
540
  capabilities: {
527
- hooks: false,
541
+ hooks: true,
528
542
  mcp: true,
529
543
  allowlist: false,
530
544
  skills: false,
531
545
  commands: true,
532
- plugins: false,
546
+ plugins: true,
533
547
  subagents: true,
534
548
  rules: { file: 'AGENTS.md' },
535
549
  workflows: false,
@@ -802,6 +816,52 @@ function resolveAccountCredentialPath(base, ...segments) {
802
816
  }
803
817
  return null;
804
818
  }
819
+ /**
820
+ * Factory Droid stores its OAuth credential encrypted at ~/.factory/auth.v2.file
821
+ * (AES-256-GCM, format `ivB64:tagB64:ctB64`) with the 32-byte key base64-stored
822
+ * in ~/.factory/auth.v2.key. On the keyfile-v2 source there is no OS-keychain /
823
+ * device binding — the key is on disk — so we can decrypt locally with no
824
+ * network call. The decrypted JSON credential's `access_token` is a WorkOS JWT
825
+ * carrying an `email` claim (plus org_id / role). We decode the claim WITHOUT
826
+ * verifying `exp`: the email is stable identity for display, not an
827
+ * authorization decision, so an expired token still yields the right address.
828
+ * Every failure (missing key file — e.g. a keyring-v2/legacy login with no
829
+ * on-disk key, a bad GCM tag, malformed JSON, or no email claim) returns null so
830
+ * the caller falls back to the file-presence signed-in signal. Never throws.
831
+ */
832
+ function decryptDroidCredential(base) {
833
+ const filePath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.file');
834
+ const keyPath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.key');
835
+ if (!filePath || !keyPath)
836
+ return null;
837
+ try {
838
+ const blob = fs.readFileSync(filePath, 'utf-8').trim();
839
+ const key = Buffer.from(fs.readFileSync(keyPath, 'utf-8').trim(), 'base64');
840
+ if (key.length !== 32)
841
+ return null;
842
+ const [ivB64, tagB64, ctB64] = blob.split(':');
843
+ if (!ivB64 || !tagB64 || !ctB64)
844
+ return null;
845
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(ivB64, 'base64'));
846
+ decipher.setAuthTag(Buffer.from(tagB64, 'base64'));
847
+ const plaintext = Buffer.concat([
848
+ decipher.update(Buffer.from(ctB64, 'base64')),
849
+ decipher.final(),
850
+ ]).toString('utf-8');
851
+ const cred = JSON.parse(plaintext);
852
+ const claims = typeof cred?.access_token === 'string' ? decodeJwtPayload(cred.access_token) : null;
853
+ if (!claims)
854
+ return null;
855
+ return {
856
+ email: typeof claims.email === 'string' ? claims.email : null,
857
+ orgId: normalizeIdentityPart(claims.org_id ?? cred.active_organization_id),
858
+ role: typeof claims.role === 'string' ? claims.role : null,
859
+ };
860
+ }
861
+ catch {
862
+ return null;
863
+ }
864
+ }
805
865
  let cachedAgyKeychainSignedIn;
806
866
  /**
807
867
  * Antigravity (`agy`, a Codeium/Windsurf-based CLI) stores its OAuth token in
@@ -1034,12 +1094,29 @@ export async function getAccountInfo(agentId, home) {
1034
1094
  return { ...empty, signedIn: true, accountId: userId, accountKey, lastActive };
1035
1095
  }
1036
1096
  case 'droid': {
1037
- // Factory Droid stores auth at ~/.factory/auth.v2.file (+ auth.v2.key,
1038
- // an encrypted blob). No email/JWT is readable locally, so presence of
1039
- // the auth file is the only signed-in signal we can derive without a
1040
- // network call same pattern as antigravity/kimi. `.factory` is the
1041
- // config dir on every platform (macOS/Linux ~/.factory, Windows
1042
- // %USERPROFILE%\.factory), so path.join keeps this cross-platform.
1097
+ // Factory Droid stores auth at ~/.factory/auth.v2.file (AES-256-GCM,
1098
+ // decrypted with the on-disk ~/.factory/auth.v2.key). We decrypt locally
1099
+ // no network and surface the email/org/role from the WorkOS
1100
+ // access-token JWT, same as claude/codex/grok. If the credential can't be
1101
+ // decrypted (a keyring-v2/legacy login with no on-disk key, or a decrypt
1102
+ // failure) we fall back to the file-presence signed-in signal so the row
1103
+ // still reads as logged in — the conservative floor antigravity/kimi use.
1104
+ // `.factory` is the config dir on every platform (macOS/Linux
1105
+ // ~/.factory, Windows %USERPROFILE%\.factory).
1106
+ const decoded = decryptDroidCredential(base);
1107
+ if (decoded?.email) {
1108
+ const organizationId = decoded.orgId;
1109
+ const accountKey = buildIdentityKey(agentId, [['org', organizationId]]);
1110
+ return {
1111
+ ...empty,
1112
+ email: decoded.email,
1113
+ organizationId,
1114
+ accountId: organizationId,
1115
+ accountKey,
1116
+ signedIn: true,
1117
+ lastActive,
1118
+ };
1119
+ }
1043
1120
  const authPath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.file');
1044
1121
  if (!authPath)
1045
1122
  return { ...empty, lastActive };
@@ -1574,8 +1651,6 @@ export function getMcpConfigPathForHome(agentId, home) {
1574
1651
  return path.join(home, '.kiro', 'settings', 'mcp.json');
1575
1652
  case 'goose':
1576
1653
  return path.join(home, '.config', 'goose', 'config.yaml');
1577
- case 'roo':
1578
- return path.join(home, '.roo', 'mcp.json');
1579
1654
  case 'antigravity':
1580
1655
  return path.join(home, '.gemini', 'antigravity-cli', 'mcp_config.json');
1581
1656
  case 'grok':
@@ -1612,8 +1687,6 @@ function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
1612
1687
  return path.join(cwd, '.kiro', 'settings', 'mcp.json');
1613
1688
  case 'goose':
1614
1689
  return path.join(cwd, '.goose', 'config.yaml');
1615
- case 'roo':
1616
- return path.join(cwd, '.roo', 'mcp.json');
1617
1690
  case 'antigravity':
1618
1691
  return path.join(cwd, '.gemini', 'antigravity-cli', 'mcp_config.json');
1619
1692
  case 'grok':
@@ -1746,9 +1819,6 @@ export const AGENT_NAME_ALIASES = {
1746
1819
  'kiro-cli': 'kiro',
1747
1820
  goose: 'goose',
1748
1821
  'block-goose': 'goose',
1749
- roo: 'roo',
1750
- 'roo-code': 'roo',
1751
- roocode: 'roo',
1752
1822
  antigravity: 'antigravity',
1753
1823
  'google-antigravity': 'antigravity',
1754
1824
  agy: 'antigravity',
@@ -1789,6 +1859,40 @@ export function resolveAgentName(input) {
1789
1859
  export function isAgentName(input) {
1790
1860
  return resolveAgentName(input) !== null;
1791
1861
  }
1862
+ /**
1863
+ * Build the deprecation notice lines for an agent, or null if it isn't
1864
+ * deprecated. Split from the printer so tests can assert the content without
1865
+ * capturing stdout. Lines are plain (uncolored) text.
1866
+ */
1867
+ export function deprecationNotice(agent) {
1868
+ const dep = AGENTS[agent].deprecated;
1869
+ if (!dep)
1870
+ return null;
1871
+ const name = AGENTS[agent].name;
1872
+ const lines = [
1873
+ `Warning: ${name} was deprecated by ${dep.by} (${dep.date}).`,
1874
+ ` ${dep.reason}`,
1875
+ ];
1876
+ if (dep.replacement) {
1877
+ const rep = AGENTS[dep.replacement];
1878
+ lines.push(` Consider using ${rep.name} instead: agents add ${rep.id}`);
1879
+ }
1880
+ if (dep.url)
1881
+ lines.push(` ${dep.url}`);
1882
+ return lines;
1883
+ }
1884
+ /**
1885
+ * Print a deprecation warning (yellow) if the agent's registry entry carries a
1886
+ * `deprecated` marker; no-op otherwise. Call from any user entry point that
1887
+ * acts on a chosen agent — install (`agents add`) and `agents teams add`.
1888
+ */
1889
+ export function warnAgentDeprecated(agent) {
1890
+ const lines = deprecationNotice(agent);
1891
+ if (!lines)
1892
+ return;
1893
+ for (const line of lines)
1894
+ console.log(chalk.yellow(line));
1895
+ }
1792
1896
  /** Format an error message for an unrecognized agent name, listing valid options. */
1793
1897
  export function formatAgentError(agentName, validAgents = ALL_AGENT_IDS) {
1794
1898
  return `Unknown agent '${agentName}'. Valid agents: ${validAgents.join(', ')}`;
@@ -219,7 +219,11 @@ export function verifyBrowserIdentity(reported, expected, port, host = 'localhos
219
219
  // to a Comet instance doesn't trip a false "identity mismatch".
220
220
  comet: ['comet', 'chrome'],
221
221
  brave: ['brave', 'brave-browser', 'chrome'],
222
- edge: ['edge', 'microsoft-edge', 'msedge', 'chrome'],
222
+ // Windows/Chromium Edge reports its /json/version "Browser" field as
223
+ // "Edg/<version>" (the `Edg` token, not `Edge`), which normalizeBrowserName
224
+ // reduces to "edg". Accept it so attaching to a real Edge doesn't trip a
225
+ // false identity mismatch (the profile enum only allows "edge").
226
+ edge: ['edge', 'edg', 'microsoft-edge', 'msedge', 'chrome'],
223
227
  };
224
228
  const accepted = matches[expected] || [expected];
225
229
  if (accepted.includes(reported))
@@ -51,6 +51,7 @@ const BROWSER_PATHS = {
51
51
  edge: [
52
52
  `${WIN_PROGRAMFILES}\\Microsoft\\Edge\\Application\\msedge.exe`,
53
53
  `${WIN_PROGRAMFILES_X86}\\Microsoft\\Edge\\Application\\msedge.exe`,
54
+ `${WIN_LOCALAPPDATA}\\Microsoft\\Edge\\Application\\msedge.exe`,
54
55
  ],
55
56
  custom: [],
56
57
  },
@@ -282,6 +283,25 @@ export async function attachToChrome(port) {
282
283
  return wsUrl;
283
284
  }
284
285
  export function killChrome(pid) {
286
+ if (process.platform === 'win32') {
287
+ // On Windows `process.kill(pid, 'SIGINT')` maps to TerminateProcess — a
288
+ // hard kill that skips Chromium's on-exit profile flush (leaving a dirty
289
+ // "Chrome didn't shut down correctly" state). Ask taskkill for a graceful
290
+ // stop first (no /F → posts WM_CLOSE), and only force-kill the tree if that
291
+ // fails or the process is still around.
292
+ try {
293
+ execFileSync('taskkill', ['/pid', String(pid)], { stdio: 'ignore' });
294
+ }
295
+ catch {
296
+ try {
297
+ execFileSync('taskkill', ['/pid', String(pid), '/t', '/f'], { stdio: 'ignore' });
298
+ }
299
+ catch {
300
+ // Process already dead
301
+ }
302
+ }
303
+ return;
304
+ }
285
305
  try {
286
306
  process.kill(pid, 'SIGINT');
287
307
  }
@@ -53,4 +53,31 @@ export declare function buildLaunchCmd(remoteOs: RemoteOs, browserType: string,
53
53
  * (Get-NetTCPConnection → Stop-Process).
54
54
  */
55
55
  export declare function buildKillCmd(remoteOs: RemoteOs, port: number): string;
56
+ export declare function ensureRemoteBrowser(user: string, host: string, browserType: string, port: number, remoteOs: RemoteOs, customBinary?: string): Promise<void>;
56
57
  export declare function restartRemoteBrowser(user: string, host: string, browserType: string, port: number, remoteOs: RemoteOs, customBinary?: string): Promise<void>;
58
+ /**
59
+ * Kill the remote browser holding the CDP port. Invoked on stop/cleanup so the
60
+ * WMI-spawned (Windows) / detached (posix) browser process is not orphaned when
61
+ * the ssh tunnel is torn down — killing only the tunnel left it running on
62
+ * win-mini after every `browser stop`. Best-effort: never rejects.
63
+ */
64
+ export declare function killRemoteBrowser(user: string, host: string, remoteOs: RemoteOs, port: number): Promise<void>;
65
+ /**
66
+ * Identify whether a pid listening on our target local port is an SSH
67
+ * tunnel WE would have spawned for `host:remotePort`. Used so that two
68
+ * agents-browser invocations of the same SSH profile share a tunnel
69
+ * rather than failing the second one with "port in use".
70
+ *
71
+ * Best-effort match against the ssh -L command line via `ps`. If we
72
+ * can't read the cmd or the args don't look like ours, treat as not-ours.
73
+ *
74
+ * Windows has no `ps`, so the POSIX branch below always throws there and every
75
+ * reuse check returned false — the second invocation to the same host:port then
76
+ * failed "port in use". On win32 we instead reuse the netstat -ano + tasklist
77
+ * occupant lookup (getPortOccupant): the tunnel binds `remotePort` locally
78
+ * (localPort === remotePort in the caller), so the pid holding that port is our
79
+ * ssh.exe. tasklist only exposes the image name, not the full command line, so
80
+ * we can't match host/remotePort as tightly as the POSIX branch — an ssh
81
+ * process on our exact local port is our tunnel in practice.
82
+ */
83
+ export declare function isOwnTunnel(pid: number, host: string, remotePort: number): boolean;