@phnx-labs/agents-cli 1.20.32 → 1.20.34

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 (110) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +28 -2
  3. package/dist/commands/commands.js +3 -3
  4. package/dist/commands/computer-actions.js +1 -0
  5. package/dist/commands/computer.d.ts +23 -0
  6. package/dist/commands/computer.js +45 -3
  7. package/dist/commands/cost.js +2 -2
  8. package/dist/commands/doctor.d.ts +10 -0
  9. package/dist/commands/doctor.js +51 -2
  10. package/dist/commands/exec.js +56 -1
  11. package/dist/commands/hooks.js +3 -3
  12. package/dist/commands/import.js +1 -1
  13. package/dist/commands/inspect.js +13 -17
  14. package/dist/commands/mcp.js +3 -3
  15. package/dist/commands/permissions.js +3 -3
  16. package/dist/commands/rules.js +3 -3
  17. package/dist/commands/secrets-migrate.js +23 -11
  18. package/dist/commands/secrets.d.ts +20 -0
  19. package/dist/commands/secrets.js +53 -1
  20. package/dist/commands/sessions.js +18 -1
  21. package/dist/commands/skills.js +3 -3
  22. package/dist/commands/status.d.ts +12 -0
  23. package/dist/commands/status.js +81 -0
  24. package/dist/commands/sync.js +2 -2
  25. package/dist/commands/teams.js +77 -18
  26. package/dist/commands/usage.js +2 -2
  27. package/dist/commands/utils.d.ts +8 -0
  28. package/dist/commands/utils.js +20 -0
  29. package/dist/commands/versions.js +4 -3
  30. package/dist/commands/view.d.ts +39 -0
  31. package/dist/commands/view.js +217 -84
  32. package/dist/commands/workflows.js +3 -3
  33. package/dist/index.js +16 -2
  34. package/dist/lib/acp/harnesses.d.ts +1 -1
  35. package/dist/lib/acp/harnesses.js +2 -2
  36. package/dist/lib/agent-spec/index.d.ts +18 -0
  37. package/dist/lib/agent-spec/index.js +35 -0
  38. package/dist/lib/agent-spec/primitives.d.ts +28 -0
  39. package/dist/lib/agent-spec/primitives.js +57 -0
  40. package/dist/lib/agent-spec/provider.d.ts +2 -0
  41. package/dist/lib/agent-spec/provider.js +9 -0
  42. package/dist/lib/agent-spec/resolve.d.ts +33 -0
  43. package/dist/lib/agent-spec/resolve.js +174 -0
  44. package/dist/lib/agent-spec/types.d.ts +57 -0
  45. package/dist/lib/agent-spec/types.js +18 -0
  46. package/dist/lib/agents.d.ts +12 -0
  47. package/dist/lib/agents.js +115 -32
  48. package/dist/lib/browser/chrome.js +20 -0
  49. package/dist/lib/browser/drivers/ssh.d.ts +19 -0
  50. package/dist/lib/browser/drivers/ssh.js +18 -3
  51. package/dist/lib/crabbox/cli.d.ts +98 -0
  52. package/dist/lib/crabbox/cli.js +218 -0
  53. package/dist/lib/crabbox/lease.d.ts +41 -0
  54. package/dist/lib/crabbox/lease.js +73 -0
  55. package/dist/lib/crabbox/runtimes.d.ts +57 -0
  56. package/dist/lib/crabbox/runtimes.js +109 -0
  57. package/dist/lib/doctor-diff.js +29 -2
  58. package/dist/lib/drift-sync.d.ts +43 -0
  59. package/dist/lib/drift-sync.js +179 -0
  60. package/dist/lib/exec.d.ts +15 -0
  61. package/dist/lib/exec.js +21 -11
  62. package/dist/lib/hosts/dispatch.d.ts +27 -10
  63. package/dist/lib/hosts/dispatch.js +55 -19
  64. package/dist/lib/hosts/option.d.ts +14 -0
  65. package/dist/lib/hosts/option.js +19 -0
  66. package/dist/lib/hosts/passthrough.d.ts +30 -0
  67. package/dist/lib/hosts/passthrough.js +141 -0
  68. package/dist/lib/hosts/remote-cmd.d.ts +36 -0
  69. package/dist/lib/hosts/remote-cmd.js +56 -0
  70. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  71. package/dist/lib/platform/winpath.d.ts +31 -2
  72. package/dist/lib/platform/winpath.js +133 -24
  73. package/dist/lib/pwsh.d.ts +11 -0
  74. package/dist/lib/pwsh.js +13 -0
  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/agent.d.ts +42 -1
  78. package/dist/lib/secrets/agent.js +89 -11
  79. package/dist/lib/secrets/bundles.js +61 -21
  80. package/dist/lib/secrets/filestore.js +31 -1
  81. package/dist/lib/secrets/index.d.ts +44 -1
  82. package/dist/lib/secrets/index.js +108 -10
  83. package/dist/lib/secrets/linux.d.ts +14 -0
  84. package/dist/lib/secrets/linux.js +21 -0
  85. package/dist/lib/secrets/windows.d.ts +74 -0
  86. package/dist/lib/secrets/windows.js +440 -0
  87. package/dist/lib/session/active.d.ts +8 -0
  88. package/dist/lib/session/active.js +18 -1
  89. package/dist/lib/session/provenance.d.ts +56 -0
  90. package/dist/lib/session/provenance.js +157 -0
  91. package/dist/lib/shims.d.ts +20 -0
  92. package/dist/lib/shims.js +53 -20
  93. package/dist/lib/ssh-exec.d.ts +22 -0
  94. package/dist/lib/ssh-exec.js +59 -2
  95. package/dist/lib/ssh-tunnel.d.ts +0 -5
  96. package/dist/lib/ssh-tunnel.js +65 -8
  97. package/dist/lib/startup/command-registry.d.ts +1 -0
  98. package/dist/lib/startup/command-registry.js +2 -0
  99. package/dist/lib/sync-status.d.ts +102 -0
  100. package/dist/lib/sync-status.js +135 -0
  101. package/dist/lib/teams/agents.d.ts +24 -0
  102. package/dist/lib/teams/agents.js +30 -1
  103. package/dist/lib/types.d.ts +20 -1
  104. package/dist/lib/usage.d.ts +30 -0
  105. package/dist/lib/usage.js +159 -2
  106. package/dist/lib/versions.d.ts +2 -4
  107. package/dist/lib/versions.js +7 -20
  108. package/package.json +2 -1
  109. package/dist/lib/agent-spec.d.ts +0 -36
  110. package/dist/lib/agent-spec.js +0 -157
@@ -0,0 +1,35 @@
1
+ // Public API for the agent-spec engine. Commands import from here and get the
2
+ // production provider bound automatically; tests import the pure `./resolve.js`
3
+ // core directly and inject a fake provider.
4
+ //
5
+ // One vocabulary, one resolver, reused by every subcommand that accepts
6
+ // `<agent>[@<qualifier>]`. Built for the hot path: exact / @pinned / bare specs
7
+ // resolve with no directory enumeration; only @latest/@oldest/@all enumerate,
8
+ // via the mtime-cached provider.
9
+ import { defaultVersionProvider } from './provider.js';
10
+ import * as core from './resolve.js';
11
+ export * from './types.js';
12
+ export * from './primitives.js';
13
+ /** Shared `--help` epilog so every agent-spec command documents the same grammar. */
14
+ export const AGENT_SPEC_HELP = 'Agent spec: <agent>[@<qualifier>]. Qualifiers: ' +
15
+ '@latest (highest installed), @oldest (lowest installed), ' +
16
+ '@pinned / @default (your configured default — synonyms), ' +
17
+ '@all (every installed version), or an exact @x.y.z. ' +
18
+ 'Bare <agent> uses the resolved default (project pin → global default). ' +
19
+ 'Comma-separate to combine: claude@all,codex@latest.';
20
+ /** Resolve a spec (single or comma-list) into concrete installed targets. */
21
+ export function resolveAgentTargets(spec, opts = {}) {
22
+ return core.resolveAgentTargets(spec, defaultVersionProvider, opts);
23
+ }
24
+ /** Resolve a spec that must name exactly one installed version. */
25
+ export function resolveSingleAgentTarget(spec, opts = {}) {
26
+ return core.resolveSingleAgentTarget(spec, defaultVersionProvider, opts);
27
+ }
28
+ /** Resolve a read/list command's version filter (undefined → all, @default → the default, else concrete). */
29
+ export function resolveVersionFilter(agent, qualifier, opts = {}) {
30
+ return core.resolveVersionFilter(agent, qualifier, defaultVersionProvider, opts);
31
+ }
32
+ /** Concrete version filter for list/display commands (undefined → show all, @default → the default version). */
33
+ export function resolveListFilter(agent, qualifier, opts = {}) {
34
+ return core.resolveListFilter(agent, qualifier, defaultVersionProvider, opts);
35
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The only shape a version string may take before it reaches an exec/shim/path
3
+ * boundary. Accepts the literal `latest` or a 1–64 char run of `[A-Za-z0-9._+-]`
4
+ * with no `..` traversal. This is THE validation gate — every resolver funnels
5
+ * exact-version tokens through it.
6
+ */
7
+ export declare const VERSION_RE: RegExp;
8
+ /** Canonical qualifier set, in help/display order. `pinned` ≡ `default`. */
9
+ export declare const AGENT_QUALIFIERS: readonly ["latest", "oldest", "pinned", "default", "all"];
10
+ export type AgentQualifier = (typeof AGENT_QUALIFIERS)[number];
11
+ /**
12
+ * Record-filter-only qualifiers. `any` means "no version constraint" for
13
+ * historical-record queries (sessions/teams resume) — accepted by the filter
14
+ * path but intentionally kept out of the display vocabulary.
15
+ */
16
+ export declare const RECORD_ONLY_QUALIFIERS: readonly ["any"];
17
+ /**
18
+ * Ordering for version strings (ascending).
19
+ * 1. numeric segment comparison (semver-ish: `2.1.187` > `2.1.143`)
20
+ * 2. tie → trailing `-N` build suffix, numerically (`2026.2.19-2` > `2026.2.19`)
21
+ * 3. still tied → 0 (legacy behavior: `1.0` == `1.0.0`, non-numeric tails == 0)
22
+ *
23
+ * Deliberately NOT a full semver comparator: OpenClaw's `-N` is a rebuild marker
24
+ * (higher = newer); a semver comparator would invert it. The `-N` tiebreak is the
25
+ * only addition over the historical numeric-only compare, so suffix-free versions
26
+ * (claude/codex semver) are unaffected.
27
+ */
28
+ export declare function compareVersions(a: string, b: string): number;
@@ -0,0 +1,57 @@
1
+ // Pure version primitives for the agent-spec engine — zero dependencies, so the
2
+ // whole module (and versions.ts, which re-uses these) stays trivially testable.
3
+ /**
4
+ * The only shape a version string may take before it reaches an exec/shim/path
5
+ * boundary. Accepts the literal `latest` or a 1–64 char run of `[A-Za-z0-9._+-]`
6
+ * with no `..` traversal. This is THE validation gate — every resolver funnels
7
+ * exact-version tokens through it.
8
+ */
9
+ export const VERSION_RE = /^(?:latest|(?!.*\.\.)[A-Za-z0-9._+-]{1,64})$/;
10
+ /** Canonical qualifier set, in help/display order. `pinned` ≡ `default`. */
11
+ export const AGENT_QUALIFIERS = ['latest', 'oldest', 'pinned', 'default', 'all'];
12
+ /**
13
+ * Record-filter-only qualifiers. `any` means "no version constraint" for
14
+ * historical-record queries (sessions/teams resume) — accepted by the filter
15
+ * path but intentionally kept out of the display vocabulary.
16
+ */
17
+ export const RECORD_ONLY_QUALIFIERS = ['any'];
18
+ /** Split a version into numeric `.`-segments (non-numeric tail → 0), e.g. `2026.2.19-2` → [2026,2,19]. */
19
+ function numericParts(v) {
20
+ return v.split('.').map((n) => parseInt(n, 10) || 0);
21
+ }
22
+ /**
23
+ * Trailing `-<digits>` build suffix as a number (0 when absent). OpenClaw ships
24
+ * same-day rebuilds as `2026.2.19-2` where a higher `-N` is NEWER — the opposite
25
+ * of a semver pre-release. Used only to break exact numeric ties, so semver-style
26
+ * versions (which carry no `-N`) are unaffected.
27
+ */
28
+ function buildSuffix(v) {
29
+ const m = /-(\d+)$/.exec(v);
30
+ return m ? parseInt(m[1], 10) : 0;
31
+ }
32
+ /**
33
+ * Ordering for version strings (ascending).
34
+ * 1. numeric segment comparison (semver-ish: `2.1.187` > `2.1.143`)
35
+ * 2. tie → trailing `-N` build suffix, numerically (`2026.2.19-2` > `2026.2.19`)
36
+ * 3. still tied → 0 (legacy behavior: `1.0` == `1.0.0`, non-numeric tails == 0)
37
+ *
38
+ * Deliberately NOT a full semver comparator: OpenClaw's `-N` is a rebuild marker
39
+ * (higher = newer); a semver comparator would invert it. The `-N` tiebreak is the
40
+ * only addition over the historical numeric-only compare, so suffix-free versions
41
+ * (claude/codex semver) are unaffected.
42
+ */
43
+ export function compareVersions(a, b) {
44
+ const na = numericParts(a);
45
+ const nb = numericParts(b);
46
+ for (let i = 0; i < Math.max(na.length, nb.length); i++) {
47
+ const av = na[i] || 0;
48
+ const bv = nb[i] || 0;
49
+ if (av !== bv)
50
+ return av - bv;
51
+ }
52
+ const sa = buildSuffix(a);
53
+ const sb = buildSuffix(b);
54
+ if (sa !== sb)
55
+ return sa - sb;
56
+ return 0;
57
+ }
@@ -0,0 +1,2 @@
1
+ import type { VersionProvider } from './types.js';
2
+ export declare const defaultVersionProvider: VersionProvider;
@@ -0,0 +1,9 @@
1
+ // The production VersionProvider: the only fs/meta-coupled file in the engine.
2
+ // Everything else in this module is pure and testable without it.
3
+ import { listInstalledVersions, getGlobalDefault, getProjectVersion, isVersionInstalled, } from '../versions.js';
4
+ export const defaultVersionProvider = {
5
+ listInstalled: (agent) => listInstalledVersions(agent),
6
+ getProjectVersion: (agent, cwd) => getProjectVersion(agent, cwd),
7
+ getGlobalDefault: (agent) => getGlobalDefault(agent),
8
+ isInstalled: (agent, version) => isVersionInstalled(agent, version),
9
+ };
@@ -0,0 +1,33 @@
1
+ import type { AgentId } from '../types.js';
2
+ import { type AgentTarget, type VersionProvider, type VersionSource, type ResolveOptions, type VersionFilter } from './types.js';
3
+ /**
4
+ * Resolve an agent spec (single or comma-list) into concrete installed targets.
5
+ * `@latest`/`@oldest`/`@all` range over installed versions.
6
+ */
7
+ export declare function resolveAgentTargets(spec: string, provider: VersionProvider, opts?: ResolveOptions): AgentTarget[];
8
+ /**
9
+ * Single-target commands (`run`, `sync`, `inspect`): resolve a spec that must
10
+ * name exactly one installed version. Rejects `@all` / multi-target specs.
11
+ */
12
+ export declare function resolveSingleAgentTarget(spec: string, provider: VersionProvider, opts?: ResolveOptions): {
13
+ agent: AgentId;
14
+ version: string;
15
+ source: VersionSource;
16
+ };
17
+ /**
18
+ * Read/list commands: resolve a qualifier into a version filter.
19
+ * undefined / '' / @any → no filter (show all installed versions)
20
+ * @default / @pinned → the literal 'default' sentinel (show the configured default)
21
+ * @latest/@oldest/x.y.z → a concrete version (throws if not installed)
22
+ * Uniform `@default` handling fixes the prior rules-vs-view inconsistency.
23
+ */
24
+ export declare function resolveVersionFilter(agent: AgentId, qualifier: string | undefined | null, provider: VersionProvider, opts?: ResolveOptions): VersionFilter;
25
+ /**
26
+ * Concrete version filter for list/display commands whose downstream code
27
+ * filters by an exact version string (not the `'default'` sentinel `view` uses).
28
+ * undefined / '' / @any → undefined (no filter → show all installed)
29
+ * @default / @pinned → the configured default version, or undefined if none
30
+ * is set (falls back to show-all rather than erroring)
31
+ * @latest/@oldest/x.y.z → a concrete version (throws AgentSpecError if bad)
32
+ */
33
+ export declare function resolveListFilter(agent: AgentId, qualifier: string | undefined | null, provider: VersionProvider, opts?: ResolveOptions): string | undefined;
@@ -0,0 +1,174 @@
1
+ // Pure agent-spec resolution — the engine core. Takes a VersionProvider instead
2
+ // of touching the filesystem, so every branch is unit-testable with in-memory
3
+ // fixtures. Domain = installed versions (`add`/install use a separate npm path).
4
+ // Never calls process.exit; throws AgentSpecError on bad input.
5
+ import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError } from '../agents.js';
6
+ import { VERSION_RE } from './primitives.js';
7
+ import { AgentSpecError, } from './types.js';
8
+ /**
9
+ * Resolve an agent spec (single or comma-list) into concrete installed targets.
10
+ * `@latest`/`@oldest`/`@all` range over installed versions.
11
+ */
12
+ export function resolveAgentTargets(spec, provider, opts = {}) {
13
+ const cwd = opts.cwd ?? process.cwd();
14
+ const available = opts.availableAgents ?? ALL_AGENT_IDS;
15
+ const onAmbiguous = opts.onAmbiguous ?? 'error';
16
+ const rawEntries = spec.split(',').map((s) => s.trim()).filter(Boolean);
17
+ if (rawEntries.length === 0) {
18
+ throw new AgentSpecError('Empty agent spec.', 'empty');
19
+ }
20
+ // Expand the bare literal `all` (or `all@all`) into every available agent that
21
+ // has ≥1 installed version. Lenient: agents with nothing installed are skipped.
22
+ const entries = [];
23
+ for (const e of rawEntries) {
24
+ if (e === 'all' || e === 'all@all') {
25
+ for (const a of available) {
26
+ if (provider.listInstalled(a).length > 0)
27
+ entries.push(`${a}@all`);
28
+ }
29
+ }
30
+ else {
31
+ entries.push(e);
32
+ }
33
+ }
34
+ const out = [];
35
+ const seen = new Set();
36
+ const push = (agent, version, source) => {
37
+ const key = `${agent}@${version ?? ''}`;
38
+ if (!seen.has(key)) {
39
+ seen.add(key);
40
+ out.push({ agent, version, source });
41
+ }
42
+ };
43
+ for (const entry of entries) {
44
+ const at = entry.indexOf('@');
45
+ const agentToken = (at === -1 ? entry : entry.slice(0, at)).trim();
46
+ const qualifier = at === -1 ? null : entry.slice(at + 1).trim();
47
+ if (!agentToken)
48
+ continue;
49
+ if (at !== -1 && !qualifier) {
50
+ throw new AgentSpecError(`Missing version in '${entry}'. Use ${agentToken}@x.y.z, @latest, @oldest, @pinned, @default, or @all.`, 'missing-version');
51
+ }
52
+ const agent = resolveAgentName(agentToken);
53
+ if (!agent || !available.includes(agent)) {
54
+ throw new AgentSpecError(formatAgentError(agentToken, [...available]), 'unknown-agent');
55
+ }
56
+ const name = AGENTS[agent].name;
57
+ // ----- bare: project pin → global default → sole/ambiguous installed -----
58
+ if (qualifier === null) {
59
+ const proj = provider.getProjectVersion(agent, cwd);
60
+ if (proj) {
61
+ push(agent, proj, 'project-pin');
62
+ continue;
63
+ }
64
+ const glob = provider.getGlobalDefault(agent);
65
+ if (glob) {
66
+ push(agent, glob, 'global-default');
67
+ continue;
68
+ }
69
+ const installed = provider.listInstalled(agent);
70
+ if (installed.length === 0) {
71
+ push(agent, null, 'none');
72
+ continue;
73
+ }
74
+ if (installed.length === 1) {
75
+ push(agent, installed[0], 'sole-installed');
76
+ continue;
77
+ }
78
+ if (onAmbiguous === 'newest') {
79
+ push(agent, installed[installed.length - 1], 'newest-installed');
80
+ continue;
81
+ }
82
+ throw new AgentSpecError(`No default version set for ${name}. Specify one (${agent}@<version>) or set it: agents use ${agent}@<version>.`, 'no-default', agent, installed);
83
+ }
84
+ // ----- @pinned / @default: the configured global default -----
85
+ if (qualifier === 'pinned' || qualifier === 'default') {
86
+ const def = provider.getGlobalDefault(agent);
87
+ if (!def) {
88
+ throw new AgentSpecError(`No default version set for ${name}. Run: agents use ${agent}@<version>`, 'no-default', agent, provider.listInstalled(agent));
89
+ }
90
+ push(agent, def, 'global-default(@pinned)');
91
+ continue;
92
+ }
93
+ // ----- @all: every installed version -----
94
+ if (qualifier === 'all') {
95
+ const installed = provider.listInstalled(agent);
96
+ if (installed.length === 0) {
97
+ throw new AgentSpecError(`No managed versions are installed for ${name}. Run: agents add ${agent}@latest`, 'none-installed', agent);
98
+ }
99
+ for (const v of installed)
100
+ push(agent, v, 'explicit');
101
+ continue;
102
+ }
103
+ // ----- @latest / @oldest: ends of the installed range -----
104
+ if (qualifier === 'latest' || qualifier === 'oldest') {
105
+ const installed = provider.listInstalled(agent);
106
+ if (installed.length === 0) {
107
+ throw new AgentSpecError(`No managed versions are installed for ${name}. Run: agents add ${agent}@latest`, 'none-installed', agent);
108
+ }
109
+ const isOldest = qualifier === 'oldest';
110
+ push(agent, isOldest ? installed[0] : installed[installed.length - 1], isOldest ? 'alias-oldest' : 'alias-latest');
111
+ continue;
112
+ }
113
+ // ----- exact version: validate then existence-check (no enumeration) -----
114
+ if (!VERSION_RE.test(qualifier)) {
115
+ throw new AgentSpecError(`Invalid version '${qualifier}' for ${name}. Allowed: latest or [A-Za-z0-9._+-]{1,64}.`, 'invalid-version', agent);
116
+ }
117
+ if (!provider.isInstalled(agent, qualifier)) {
118
+ const installed = provider.listInstalled(agent);
119
+ const hint = installed.length ? ` Installed: ${installed.join(', ')}.` : '';
120
+ throw new AgentSpecError(`${name}@${qualifier} is not installed.${hint} Install it: agents add ${agent}@${qualifier}`, 'not-installed', agent, installed);
121
+ }
122
+ push(agent, qualifier, 'explicit');
123
+ }
124
+ return out;
125
+ }
126
+ /**
127
+ * Single-target commands (`run`, `sync`, `inspect`): resolve a spec that must
128
+ * name exactly one installed version. Rejects `@all` / multi-target specs.
129
+ */
130
+ export function resolveSingleAgentTarget(spec, provider, opts = {}) {
131
+ const targets = resolveAgentTargets(spec, provider, opts);
132
+ if (targets.length !== 1) {
133
+ throw new AgentSpecError(`'${spec}' resolves to ${targets.length} targets; this command needs exactly one.`, 'multi-not-allowed');
134
+ }
135
+ const t = targets[0];
136
+ if (t.version === null) {
137
+ throw new AgentSpecError(`No installed version for ${AGENTS[t.agent].name}. Run: agents add ${t.agent}@latest`, 'none-installed', t.agent);
138
+ }
139
+ return { agent: t.agent, version: t.version, source: t.source };
140
+ }
141
+ /**
142
+ * Read/list commands: resolve a qualifier into a version filter.
143
+ * undefined / '' / @any → no filter (show all installed versions)
144
+ * @default / @pinned → the literal 'default' sentinel (show the configured default)
145
+ * @latest/@oldest/x.y.z → a concrete version (throws if not installed)
146
+ * Uniform `@default` handling fixes the prior rules-vs-view inconsistency.
147
+ */
148
+ export function resolveVersionFilter(agent, qualifier, provider, opts = {}) {
149
+ const q = qualifier?.trim();
150
+ if (!q)
151
+ return { version: null, source: 'all-versions' };
152
+ if (q === 'default' || q === 'pinned')
153
+ return { version: 'default', source: 'default' };
154
+ if (q === 'any')
155
+ return { version: null, source: 'all-versions' };
156
+ const { version, source } = resolveSingleAgentTarget(`${agent}@${q}`, provider, { ...opts, availableAgents: [agent] });
157
+ return { version, source };
158
+ }
159
+ /**
160
+ * Concrete version filter for list/display commands whose downstream code
161
+ * filters by an exact version string (not the `'default'` sentinel `view` uses).
162
+ * undefined / '' / @any → undefined (no filter → show all installed)
163
+ * @default / @pinned → the configured default version, or undefined if none
164
+ * is set (falls back to show-all rather than erroring)
165
+ * @latest/@oldest/x.y.z → a concrete version (throws AgentSpecError if bad)
166
+ */
167
+ export function resolveListFilter(agent, qualifier, provider, opts = {}) {
168
+ const q = qualifier?.trim();
169
+ if (!q || q === 'any')
170
+ return undefined;
171
+ if (q === 'default' || q === 'pinned')
172
+ return provider.getGlobalDefault(agent) ?? undefined;
173
+ return resolveSingleAgentTarget(`${agent}@${q}`, provider, { ...opts, availableAgents: [agent] }).version;
174
+ }
@@ -0,0 +1,57 @@
1
+ import type { AgentId } from '../types.js';
2
+ /** How a resolved version was arrived at — for provenance, display, and error rendering. */
3
+ export type VersionSource = 'explicit' | 'project-pin' | 'global-default' | 'global-default(@pinned)' | 'sole-installed' | 'newest-installed' | 'alias-latest' | 'alias-oldest' | 'none';
4
+ export interface AgentTarget {
5
+ agent: AgentId;
6
+ /** Resolved exact version, or null when the agent has no installed versions yet. */
7
+ version: string | null;
8
+ source: VersionSource;
9
+ }
10
+ export type AgentSpecErrorCode = 'empty' | 'unknown-agent' | 'missing-version' | 'invalid-version' | 'not-installed' | 'no-default' | 'none-installed' | 'multi-not-allowed';
11
+ /**
12
+ * Thrown on any bad spec — never `process.exit`, so the engine is safe on the
13
+ * hot path and in library contexts. `code` + `installed` let callers render a
14
+ * consistent message (e.g. the "No default … Specify one:" version list) without
15
+ * string-matching.
16
+ */
17
+ export declare class AgentSpecError extends Error {
18
+ readonly code: AgentSpecErrorCode;
19
+ readonly agent?: AgentId | undefined;
20
+ readonly installed?: string[] | undefined;
21
+ constructor(message: string, code: AgentSpecErrorCode, agent?: AgentId | undefined, installed?: string[] | undefined);
22
+ }
23
+ /**
24
+ * The filesystem/meta seam. The pure resolver takes this instead of importing
25
+ * versions.ts, so it is fully unit-testable with in-memory fixtures — no $HOME,
26
+ * no subprocess. `provider.ts` supplies the production adapter.
27
+ */
28
+ export interface VersionProvider {
29
+ /** Installed versions, sorted ascending by `compareVersions`. */
30
+ listInstalled(agent: AgentId): string[];
31
+ /** Version pinned by a project-root agents.yaml, or null. */
32
+ getProjectVersion(agent: AgentId, cwd: string): string | null;
33
+ /** The configured global default version, or null. */
34
+ getGlobalDefault(agent: AgentId): string | null;
35
+ /** Whether an exact version is installed. */
36
+ isInstalled(agent: AgentId, version: string): boolean;
37
+ }
38
+ export interface ResolveOptions {
39
+ /** Project dir for a bare spec's project pin. Defaults to process.cwd(). */
40
+ cwd?: string;
41
+ /** Restrict which agents a spec may name (e.g. only mcp-capable). Defaults to all. */
42
+ availableAgents?: readonly AgentId[];
43
+ /**
44
+ * Bare spec, >1 installed, no pin/default:
45
+ * 'error' (default) → throw AgentSpecError{code:'no-default'} — safe for
46
+ * state-mutating commands (sync/use).
47
+ * 'newest' → pick the newest installed (source:'newest-installed');
48
+ * callers should note it. For execution verbs (run/exec).
49
+ */
50
+ onAmbiguous?: 'error' | 'newest';
51
+ }
52
+ /** A read/list command's version filter. `null` = show all; `'default'` = show the configured default. */
53
+ export type FilterVersion = string | null | 'default';
54
+ export interface VersionFilter {
55
+ version: FilterVersion;
56
+ source: VersionSource | 'all-versions' | 'default';
57
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Thrown on any bad spec — never `process.exit`, so the engine is safe on the
3
+ * hot path and in library contexts. `code` + `installed` let callers render a
4
+ * consistent message (e.g. the "No default … Specify one:" version list) without
5
+ * string-matching.
6
+ */
7
+ export class AgentSpecError extends Error {
8
+ code;
9
+ agent;
10
+ installed;
11
+ constructor(message, code, agent, installed) {
12
+ super(message);
13
+ this.code = code;
14
+ this.agent = agent;
15
+ this.installed = installed;
16
+ this.name = 'AgentSpecError';
17
+ }
18
+ }
@@ -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
  },
@@ -375,24 +386,6 @@ export const AGENTS = {
375
386
  supportsHooks: false,
376
387
  capabilities: { hooks: false, mcp: true, allowlist: false, skills: false, commands: false, plugins: false, subagents: false, rules: { file: 'AGENTS.md' }, workflows: false, modes: ['edit'] },
377
388
  },
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
389
  // Google Antigravity CLI (`agy`) — official replacement for Gemini CLI as of IO 2026.
397
390
  // configDir nests inside `~/.gemini/` since agy shares the parent dir with the Gemini
398
391
  // CLI but isolates its own state in the `antigravity-cli/` subdir. Per-version HOME
@@ -802,6 +795,52 @@ function resolveAccountCredentialPath(base, ...segments) {
802
795
  }
803
796
  return null;
804
797
  }
798
+ /**
799
+ * Factory Droid stores its OAuth credential encrypted at ~/.factory/auth.v2.file
800
+ * (AES-256-GCM, format `ivB64:tagB64:ctB64`) with the 32-byte key base64-stored
801
+ * in ~/.factory/auth.v2.key. On the keyfile-v2 source there is no OS-keychain /
802
+ * device binding — the key is on disk — so we can decrypt locally with no
803
+ * network call. The decrypted JSON credential's `access_token` is a WorkOS JWT
804
+ * carrying an `email` claim (plus org_id / role). We decode the claim WITHOUT
805
+ * verifying `exp`: the email is stable identity for display, not an
806
+ * authorization decision, so an expired token still yields the right address.
807
+ * Every failure (missing key file — e.g. a keyring-v2/legacy login with no
808
+ * on-disk key, a bad GCM tag, malformed JSON, or no email claim) returns null so
809
+ * the caller falls back to the file-presence signed-in signal. Never throws.
810
+ */
811
+ function decryptDroidCredential(base) {
812
+ const filePath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.file');
813
+ const keyPath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.key');
814
+ if (!filePath || !keyPath)
815
+ return null;
816
+ try {
817
+ const blob = fs.readFileSync(filePath, 'utf-8').trim();
818
+ const key = Buffer.from(fs.readFileSync(keyPath, 'utf-8').trim(), 'base64');
819
+ if (key.length !== 32)
820
+ return null;
821
+ const [ivB64, tagB64, ctB64] = blob.split(':');
822
+ if (!ivB64 || !tagB64 || !ctB64)
823
+ return null;
824
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(ivB64, 'base64'));
825
+ decipher.setAuthTag(Buffer.from(tagB64, 'base64'));
826
+ const plaintext = Buffer.concat([
827
+ decipher.update(Buffer.from(ctB64, 'base64')),
828
+ decipher.final(),
829
+ ]).toString('utf-8');
830
+ const cred = JSON.parse(plaintext);
831
+ const claims = typeof cred?.access_token === 'string' ? decodeJwtPayload(cred.access_token) : null;
832
+ if (!claims)
833
+ return null;
834
+ return {
835
+ email: typeof claims.email === 'string' ? claims.email : null,
836
+ orgId: normalizeIdentityPart(claims.org_id ?? cred.active_organization_id),
837
+ role: typeof claims.role === 'string' ? claims.role : null,
838
+ };
839
+ }
840
+ catch {
841
+ return null;
842
+ }
843
+ }
805
844
  let cachedAgyKeychainSignedIn;
806
845
  /**
807
846
  * Antigravity (`agy`, a Codeium/Windsurf-based CLI) stores its OAuth token in
@@ -1034,12 +1073,29 @@ export async function getAccountInfo(agentId, home) {
1034
1073
  return { ...empty, signedIn: true, accountId: userId, accountKey, lastActive };
1035
1074
  }
1036
1075
  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.
1076
+ // Factory Droid stores auth at ~/.factory/auth.v2.file (AES-256-GCM,
1077
+ // decrypted with the on-disk ~/.factory/auth.v2.key). We decrypt locally
1078
+ // no network and surface the email/org/role from the WorkOS
1079
+ // access-token JWT, same as claude/codex/grok. If the credential can't be
1080
+ // decrypted (a keyring-v2/legacy login with no on-disk key, or a decrypt
1081
+ // failure) we fall back to the file-presence signed-in signal so the row
1082
+ // still reads as logged in — the conservative floor antigravity/kimi use.
1083
+ // `.factory` is the config dir on every platform (macOS/Linux
1084
+ // ~/.factory, Windows %USERPROFILE%\.factory).
1085
+ const decoded = decryptDroidCredential(base);
1086
+ if (decoded?.email) {
1087
+ const organizationId = decoded.orgId;
1088
+ const accountKey = buildIdentityKey(agentId, [['org', organizationId]]);
1089
+ return {
1090
+ ...empty,
1091
+ email: decoded.email,
1092
+ organizationId,
1093
+ accountId: organizationId,
1094
+ accountKey,
1095
+ signedIn: true,
1096
+ lastActive,
1097
+ };
1098
+ }
1043
1099
  const authPath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.file');
1044
1100
  if (!authPath)
1045
1101
  return { ...empty, lastActive };
@@ -1574,8 +1630,6 @@ export function getMcpConfigPathForHome(agentId, home) {
1574
1630
  return path.join(home, '.kiro', 'settings', 'mcp.json');
1575
1631
  case 'goose':
1576
1632
  return path.join(home, '.config', 'goose', 'config.yaml');
1577
- case 'roo':
1578
- return path.join(home, '.roo', 'mcp.json');
1579
1633
  case 'antigravity':
1580
1634
  return path.join(home, '.gemini', 'antigravity-cli', 'mcp_config.json');
1581
1635
  case 'grok':
@@ -1612,8 +1666,6 @@ function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
1612
1666
  return path.join(cwd, '.kiro', 'settings', 'mcp.json');
1613
1667
  case 'goose':
1614
1668
  return path.join(cwd, '.goose', 'config.yaml');
1615
- case 'roo':
1616
- return path.join(cwd, '.roo', 'mcp.json');
1617
1669
  case 'antigravity':
1618
1670
  return path.join(cwd, '.gemini', 'antigravity-cli', 'mcp_config.json');
1619
1671
  case 'grok':
@@ -1746,9 +1798,6 @@ export const AGENT_NAME_ALIASES = {
1746
1798
  'kiro-cli': 'kiro',
1747
1799
  goose: 'goose',
1748
1800
  'block-goose': 'goose',
1749
- roo: 'roo',
1750
- 'roo-code': 'roo',
1751
- roocode: 'roo',
1752
1801
  antigravity: 'antigravity',
1753
1802
  'google-antigravity': 'antigravity',
1754
1803
  agy: 'antigravity',
@@ -1789,6 +1838,40 @@ export function resolveAgentName(input) {
1789
1838
  export function isAgentName(input) {
1790
1839
  return resolveAgentName(input) !== null;
1791
1840
  }
1841
+ /**
1842
+ * Build the deprecation notice lines for an agent, or null if it isn't
1843
+ * deprecated. Split from the printer so tests can assert the content without
1844
+ * capturing stdout. Lines are plain (uncolored) text.
1845
+ */
1846
+ export function deprecationNotice(agent) {
1847
+ const dep = AGENTS[agent].deprecated;
1848
+ if (!dep)
1849
+ return null;
1850
+ const name = AGENTS[agent].name;
1851
+ const lines = [
1852
+ `Warning: ${name} was deprecated by ${dep.by} (${dep.date}).`,
1853
+ ` ${dep.reason}`,
1854
+ ];
1855
+ if (dep.replacement) {
1856
+ const rep = AGENTS[dep.replacement];
1857
+ lines.push(` Consider using ${rep.name} instead: agents add ${rep.id}`);
1858
+ }
1859
+ if (dep.url)
1860
+ lines.push(` ${dep.url}`);
1861
+ return lines;
1862
+ }
1863
+ /**
1864
+ * Print a deprecation warning (yellow) if the agent's registry entry carries a
1865
+ * `deprecated` marker; no-op otherwise. Call from any user entry point that
1866
+ * acts on a chosen agent — install (`agents add`) and `agents teams add`.
1867
+ */
1868
+ export function warnAgentDeprecated(agent) {
1869
+ const lines = deprecationNotice(agent);
1870
+ if (!lines)
1871
+ return;
1872
+ for (const line of lines)
1873
+ console.log(chalk.yellow(line));
1874
+ }
1792
1875
  /** Format an error message for an unrecognized agent name, listing valid options. */
1793
1876
  export function formatAgentError(agentName, validAgents = ALL_AGENT_IDS) {
1794
1877
  return `Unknown agent '${agentName}'. Valid agents: ${validAgents.join(', ')}`;