@phnx-labs/agents-cli 1.20.26 → 1.20.28

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 (95) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/commands/doctor.d.ts +5 -2
  3. package/dist/commands/doctor.js +182 -30
  4. package/dist/commands/exec.d.ts +1 -1
  5. package/dist/commands/exec.js +177 -6
  6. package/dist/commands/hosts.d.ts +11 -0
  7. package/dist/commands/hosts.js +229 -0
  8. package/dist/commands/repo.d.ts +29 -0
  9. package/dist/commands/repo.js +174 -38
  10. package/dist/commands/secrets.d.ts +2 -7
  11. package/dist/commands/secrets.js +15 -23
  12. package/dist/commands/sessions.d.ts +2 -0
  13. package/dist/commands/sessions.js +27 -25
  14. package/dist/commands/sync.d.ts +2 -0
  15. package/dist/commands/sync.js +22 -5
  16. package/dist/commands/view.js +27 -11
  17. package/dist/index.js +4 -13
  18. package/dist/lib/agent-spec.d.ts +36 -0
  19. package/dist/lib/agent-spec.js +157 -0
  20. package/dist/lib/agents.d.ts +1 -0
  21. package/dist/lib/agents.js +45 -4
  22. package/dist/lib/browser/drivers/ssh.d.ts +47 -2
  23. package/dist/lib/browser/drivers/ssh.js +113 -24
  24. package/dist/lib/browser/profiles.js +28 -1
  25. package/dist/lib/browser/runtime-state.js +28 -8
  26. package/dist/lib/browser/types.d.ts +10 -1
  27. package/dist/lib/cli-resources.js +10 -1
  28. package/dist/lib/daemon.js +32 -0
  29. package/dist/lib/doctor-diff.d.ts +19 -0
  30. package/dist/lib/doctor-diff.js +107 -15
  31. package/dist/lib/exec.d.ts +27 -0
  32. package/dist/lib/exec.js +62 -19
  33. package/dist/lib/heal.d.ts +107 -0
  34. package/dist/lib/heal.js +279 -0
  35. package/dist/lib/hooks.d.ts +17 -0
  36. package/dist/lib/hooks.js +127 -3
  37. package/dist/lib/hosts/dispatch.d.ts +26 -0
  38. package/dist/lib/hosts/dispatch.js +71 -0
  39. package/dist/lib/hosts/progress.d.ts +21 -0
  40. package/dist/lib/hosts/progress.js +49 -0
  41. package/dist/lib/hosts/providers/local.d.ts +17 -0
  42. package/dist/lib/hosts/providers/local.js +81 -0
  43. package/dist/lib/hosts/ready.d.ts +37 -0
  44. package/dist/lib/hosts/ready.js +88 -0
  45. package/dist/lib/hosts/registry.d.ts +22 -0
  46. package/dist/lib/hosts/registry.js +65 -0
  47. package/dist/lib/hosts/ssh-config.d.ts +37 -0
  48. package/dist/lib/hosts/ssh-config.js +157 -0
  49. package/dist/lib/hosts/tasks.d.ts +32 -0
  50. package/dist/lib/hosts/tasks.js +58 -0
  51. package/dist/lib/hosts/types.d.ts +51 -0
  52. package/dist/lib/hosts/types.js +21 -0
  53. package/dist/lib/loop.d.ts +9 -0
  54. package/dist/lib/loop.js +13 -1
  55. package/dist/lib/mcp.js +12 -3
  56. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  57. package/dist/lib/migrate.js +9 -5
  58. package/dist/lib/platform/exec.d.ts +10 -0
  59. package/dist/lib/platform/exec.js +17 -0
  60. package/dist/lib/platform/index.d.ts +1 -0
  61. package/dist/lib/platform/index.js +1 -0
  62. package/dist/lib/platform/links.d.ts +15 -0
  63. package/dist/lib/platform/links.js +42 -0
  64. package/dist/lib/platform/paths.d.ts +18 -0
  65. package/dist/lib/platform/paths.js +22 -0
  66. package/dist/lib/platform/posixpath.d.ts +28 -0
  67. package/dist/lib/platform/posixpath.js +153 -0
  68. package/dist/lib/plugin-marketplace.d.ts +18 -0
  69. package/dist/lib/plugin-marketplace.js +67 -1
  70. package/dist/lib/plugins.d.ts +33 -1
  71. package/dist/lib/plugins.js +56 -11
  72. package/dist/lib/project-launch.js +6 -3
  73. package/dist/lib/sandbox.js +5 -2
  74. package/dist/lib/self-update.js +7 -2
  75. package/dist/lib/session/db.d.ts +23 -0
  76. package/dist/lib/session/db.js +76 -1
  77. package/dist/lib/session/discover.d.ts +26 -0
  78. package/dist/lib/session/discover.js +75 -4
  79. package/dist/lib/session/relative-time.d.ts +7 -0
  80. package/dist/lib/session/relative-time.js +28 -0
  81. package/dist/lib/session/remote.d.ts +61 -0
  82. package/dist/lib/session/remote.js +221 -0
  83. package/dist/lib/ssh-exec.d.ts +45 -0
  84. package/dist/lib/ssh-exec.js +61 -0
  85. package/dist/lib/staleness/detectors/commands.js +7 -6
  86. package/dist/lib/staleness/writers/commands.js +7 -12
  87. package/dist/lib/startup/command-registry.d.ts +1 -0
  88. package/dist/lib/startup/command-registry.js +2 -0
  89. package/dist/lib/startup/dev-build.d.ts +22 -0
  90. package/dist/lib/startup/dev-build.js +41 -0
  91. package/dist/lib/types.d.ts +28 -0
  92. package/dist/lib/versions.d.ts +9 -3
  93. package/dist/lib/versions.js +43 -7
  94. package/package.json +3 -3
  95. package/scripts/postinstall.js +62 -0
@@ -1,6 +1,7 @@
1
1
  import type { Command } from 'commander';
2
2
  import type { SessionMeta } from '../lib/session/types.js';
3
3
  import { type ActiveSession } from '../lib/session/active.js';
4
+ import { type PickedSession } from './sessions-picker.js';
4
5
  /** Grouped + sorted view of active sessions for the --active renderer. */
5
6
  export interface ActiveSessionsLayout {
6
7
  workspaces: Array<{
@@ -28,6 +29,7 @@ export interface ActiveSessionsLayout {
28
29
  * - sessions within a window/flat bucket: input order preserved
29
30
  */
30
31
  export declare function groupActiveSessions(sessions: ActiveSession[]): ActiveSessionsLayout;
32
+ export declare function pickSessionInteractive(sessions: SessionMeta[], message?: string, initialSearch?: string, hiddenCount?: number): Promise<PickedSession | null>;
31
33
  /**
32
34
  * Build the shell command that resumes a picked session.
33
35
  *
@@ -20,6 +20,8 @@ import { getActiveSessions } from '../lib/session/active.js';
20
20
  import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex } from '../lib/session/discover.js';
21
21
  import { filterTeamSessions } from '../lib/session/team-filter.js';
22
22
  import { parseSession } from '../lib/session/parse.js';
23
+ import { runRemoteSessions } from '../lib/session/remote.js';
24
+ import { formatRelativeTime } from '../lib/session/relative-time.js';
23
25
  import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList } from '../lib/session/render.js';
24
26
  import { renderMarkdown } from '../lib/markdown.js';
25
27
  import { colorAgent, resolveAgentName } from '../lib/agents.js';
@@ -313,6 +315,16 @@ async function renderActiveSessions(asJson) {
313
315
  }
314
316
  /** Main action handler for `agents sessions`. Routes to picker, table, or single-session render. */
315
317
  async function sessionsAction(query, options) {
318
+ if (options.host && options.host.length > 0) {
319
+ try {
320
+ runRemoteSessions(options.host);
321
+ }
322
+ catch (err) {
323
+ console.error(chalk.red(err.message));
324
+ process.exit(1);
325
+ }
326
+ return;
327
+ }
316
328
  if (options.active) {
317
329
  await renderActiveSessions(options.json === true);
318
330
  return;
@@ -645,7 +657,7 @@ function formatPickerLabel(s, query) {
645
657
  renderTopicCell(label, topic, query, 48, 50) +
646
658
  chalk.gray(when));
647
659
  }
648
- async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0) {
660
+ export async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0) {
649
661
  if (hiddenCount > 0) {
650
662
  console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
651
663
  }
@@ -1116,7 +1128,8 @@ export function registerSessionsCommands(program) {
1116
1128
  .option('--artifacts', 'List all files written or edited during a session')
1117
1129
  .option('--artifact <name>', 'Read a specific artifact by filename or path (outputs to stdout)')
1118
1130
  .option('--active', 'Show only sessions running right now across terminals, teams, cloud, and headless agents')
1119
- .option('--cloud', 'Source sessions from Rush Cloud (captured runs) instead of local disk');
1131
+ .option('--cloud', 'Source sessions from Rush Cloud (captured runs) instead of local disk')
1132
+ .option('-H, --host <target...>', 'Run this query on remote machine(s) over SSH (host alias or user@host; repeatable)');
1120
1133
  setHelpSections(sessionsCmd, {
1121
1134
  examples: `
1122
1135
  # Search prior sessions in this project by topic, file path, or command
@@ -1136,8 +1149,15 @@ export function registerSessionsCommands(program) {
1136
1149
 
1137
1150
  # Export for analysis
1138
1151
  agents sessions --since 30d --limit 200 --json > sessions.json
1152
+
1153
+ # Search another machine's sessions live over SSH (no sync needed)
1154
+ agents sessions "auth bug" --last 3 --host yosemite-s1
1155
+
1156
+ # Fan the same query out across several machines
1157
+ agents sessions --all "deploy script" --host box-a --host box-b
1139
1158
  `,
1140
1159
  notes: `
1160
+ - --host runs the query on the remote's own index over SSH (host alias or user@host); repeat or pass several to fan out. SSH access is the only auth.
1141
1161
  - --include and --exclude are mutually exclusive.
1142
1162
  - --first and --last are mutually exclusive.
1143
1163
  - A filter flag (--include/--exclude/--first/--last) without --markdown/--json defaults to --markdown output.
@@ -1313,7 +1333,11 @@ function findClaudeResumeTimestamp(filePath, targetTimestampMs) {
1313
1333
  }
1314
1334
  }
1315
1335
  function isWithinProject(sessionCwd, projectRoot) {
1316
- return sessionCwd === projectRoot || sessionCwd.startsWith(projectRoot + path.sep);
1336
+ // Compare separator- and case-normalized (Windows folds `\`→`/` and lowercases)
1337
+ // so a backslash session cwd matches a forward-slash project root and vice versa.
1338
+ const cwd = toComparablePath(sessionCwd);
1339
+ const root = toComparablePath(projectRoot);
1340
+ return cwd === root || cwd.startsWith(root + '/');
1317
1341
  }
1318
1342
  function sessionDistance(session, historyEntry) {
1319
1343
  if (!historyEntry.timestampMs)
@@ -1341,25 +1365,3 @@ function padRight(s, width) {
1341
1365
  function truncate(s, max) {
1342
1366
  return s.length > max ? s.slice(0, max - 1) + '.' : s;
1343
1367
  }
1344
- function formatRelativeTime(isoTimestamp) {
1345
- const now = Date.now();
1346
- const then = new Date(isoTimestamp).getTime();
1347
- if (isNaN(then))
1348
- return isoTimestamp;
1349
- const diffMs = now - then;
1350
- const diffMin = Math.floor(diffMs / 60_000);
1351
- const diffHrs = Math.floor(diffMs / 3_600_000);
1352
- const diffDays = Math.floor(diffMs / 86_400_000);
1353
- if (diffMin < 1)
1354
- return 'just now';
1355
- if (diffMin < 60)
1356
- return `${diffMin} min ago`;
1357
- if (diffHrs < 24)
1358
- return `${diffHrs} hour${diffHrs === 1 ? '' : 's'} ago`;
1359
- if (diffDays < 7)
1360
- return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
1361
- // Older: show date
1362
- const d = new Date(then);
1363
- const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
1364
- return `${months[d.getMonth()]} ${d.getDate()}`;
1365
- }
@@ -9,6 +9,8 @@
9
9
  * agents sync claude # one agent: uses default/sole installed version
10
10
  * agents sync claude@2.1.142 # one agent: explicit version
11
11
  * agents sync claude@latest # one agent: newest installed
12
+ * agents sync claude@oldest # one agent: oldest installed
13
+ * agents sync claude@pinned (= claude@default) # one agent: the pinned default version
12
14
  * agents sync --agent claude --agent-version 2.1.142 # legacy form, still supported
13
15
  *
14
16
  * The umbrella stages live in lib/sync-umbrella.ts; this file dispatches to them
@@ -9,6 +9,8 @@
9
9
  * agents sync claude # one agent: uses default/sole installed version
10
10
  * agents sync claude@2.1.142 # one agent: explicit version
11
11
  * agents sync claude@latest # one agent: newest installed
12
+ * agents sync claude@oldest # one agent: oldest installed
13
+ * agents sync claude@pinned (= claude@default) # one agent: the pinned default version
12
14
  * agents sync --agent claude --agent-version 2.1.142 # legacy form, still supported
13
15
  *
14
16
  * The umbrella stages live in lib/sync-umbrella.ts; this file dispatches to them
@@ -28,7 +30,7 @@
28
30
  import * as path from 'path';
29
31
  import chalk from 'chalk';
30
32
  import { agentLabel, resolveAgentName } from '../lib/agents.js';
31
- import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, listInstalledVersions, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, } from '../lib/versions.js';
33
+ import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, resolveVersionAlias, listInstalledVersions, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, } from '../lib/versions.js';
32
34
  import { compileRulesForProject } from '../lib/rules/compile.js';
33
35
  import { runLaunchSync } from '../lib/project-launch.js';
34
36
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
@@ -38,7 +40,7 @@ export function registerSyncCommand(program) {
38
40
  program
39
41
  .command('sync [agentSpec]')
40
42
  .summary('Make this machine current, or sync resources into one agent')
41
- .description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude" or "claude@2.1.142".\n\nWith NO agent, runs the umbrella verb: fetch remote state (config repos + secrets + sessions) then reconcile it into every installed agent. Scope it with --repos / --secrets / --sessions, --cloud (fetch only), or --local (reconcile only).')
43
+ .description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude", "claude@2.1.142", or a selector: @latest / @oldest / @pinned (= @default).\n\nWith NO agent, runs the umbrella verb: fetch remote state (config repos + secrets + sessions) then reconcile it into every installed agent. Scope it with --repos / --secrets / --sessions, --cloud (fetch only), or --local (reconcile only).')
42
44
  .option('--agent <agent>', 'Agent identifier (legacy form; prefer the positional spec)')
43
45
  .option('--agent-version <version>', 'Version to sync into (legacy form; prefer "agent@version")')
44
46
  .option('--project-dir <path>', 'Path to project-level .agents/ directory containing project-scoped resources')
@@ -116,17 +118,23 @@ async function runSync(agentSpec, opts) {
116
118
  // ---------- 1. Resolve agent + version ----------
117
119
  let agentId;
118
120
  let version;
121
+ // A positional @selector typed by the user (latest/oldest/pinned/default/
122
+ // explicit). parseAgentSpec defaults a missing version to 'latest', so a bare
123
+ // `agents sync claude` and `agents sync claude@latest` are indistinguishable
124
+ // after parsing — we only treat the version as a selector when an '@' was
125
+ // actually typed, keeping bare `claude` on the default-version path.
126
+ let selector;
119
127
  if (agentSpec) {
120
128
  const parsed = parseAgentSpec(agentSpec);
121
129
  if (!parsed) {
122
130
  errLog(chalk.red(`Invalid agent spec '${agentSpec}'.`));
123
- errLog(chalk.gray('Examples: claude, claude@2.1.142, codex@latest'));
131
+ errLog(chalk.gray('Examples: claude, claude@2.1.142, claude@latest, claude@oldest, claude@pinned'));
124
132
  process.exitCode = 1;
125
133
  return;
126
134
  }
127
135
  agentId = parsed.agent;
128
- if (parsed.version !== 'latest' && parsed.version !== 'oldest')
129
- version = parsed.version;
136
+ if (agentSpec.includes('@'))
137
+ selector = parsed.version;
130
138
  }
131
139
  if (opts.agent) {
132
140
  const resolved = resolveAgentName(opts.agent);
@@ -138,6 +146,8 @@ async function runSync(agentSpec, opts) {
138
146
  agentId = resolved;
139
147
  }
140
148
  if (opts.agentVersion) {
149
+ // Legacy flag and the launch-shim hot path (`--agent-version <concrete>`):
150
+ // pass through verbatim. Selector aliases are a positional-spec feature.
141
151
  version = opts.agentVersion;
142
152
  }
143
153
  if (!agentId) {
@@ -147,6 +157,13 @@ async function runSync(agentSpec, opts) {
147
157
  return;
148
158
  }
149
159
  // ---------- 2. Resolve version (project pin → global default → sole installed) ----------
160
+ // A positional @selector wins over the default-resolution below.
161
+ // @latest / @oldest → newest / oldest installed (process.exit if none)
162
+ // @pinned / @default → undefined → fall through to the default path
163
+ // @x.y.z → that version (process.exit if not installed)
164
+ if (selector !== undefined && !version) {
165
+ version = resolveVersionAlias(agentId, selector);
166
+ }
150
167
  if (!version) {
151
168
  version = resolveVersion(agentId, process.cwd()) || undefined;
152
169
  if (!version) {
@@ -21,6 +21,9 @@ import { listProfiles, profileSummary } from '../lib/profiles.js';
21
21
  import { loadManifest, isStale } from '../lib/staleness/index.js';
22
22
  import { confirm } from '@inquirer/prompts';
23
23
  import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
24
+ // Shown in the email column for agents that are signed in but expose no email
25
+ // address locally (Antigravity, Kimi store an opaque OAuth/JWT credential).
26
+ const SIGNED_IN_LABEL = 'signed in';
24
27
  /**
25
28
  * Group profile summaries by their host harness, optionally filtered to a
26
29
  * single agent. Profile YAMLs that fail validation are silently skipped by
@@ -350,6 +353,8 @@ async function showInstalledVersions(filterAgentId) {
350
353
  const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
351
354
  if (info?.email)
352
355
  maxEmail = Math.max(maxEmail, info.email.length);
356
+ else if (info?.signedIn)
357
+ maxEmail = Math.max(maxEmail, SIGNED_IN_LABEL.length);
353
358
  if (info?.plan)
354
359
  maxPlanWidth = Math.max(maxPlanWidth, info.plan.length);
355
360
  }
@@ -406,6 +411,7 @@ async function showInstalledVersions(filterAgentId) {
406
411
  // Build columns, trimming trailing whitespace when columns are empty
407
412
  const parts = [` ${label}`];
408
413
  const hasEmail = !!vInfo?.email;
414
+ const signedIn = !!vInfo?.signedIn;
409
415
  const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth);
410
416
  const hasUsage = usageStr.length > 0;
411
417
  // Only show lastActive for versions with an actual logged-in account.
@@ -418,14 +424,17 @@ async function showInstalledVersions(filterAgentId) {
418
424
  runDefaultBits.push(`mode:${runDefaults.mode}`);
419
425
  if (runDefaults.model)
420
426
  runDefaultBits.push(`model:${runDefaults.model}`);
421
- if (!hasEmail && !hasUsage) {
427
+ if (!hasEmail && !hasUsage && !signedIn) {
422
428
  // Installed but never signed in
423
429
  parts.push(chalk.gray('(not signed in — run ' + agent.cliCommand + ' to log in)'));
424
430
  }
425
431
  else {
426
- if (hasEmail || hasUsage || hasActive) {
427
- const emailCol = (vInfo?.email || '').padEnd(maxEmail);
428
- parts.push(hasEmail ? chalk.cyan(emailCol) : ' '.repeat(maxEmail));
432
+ if (hasEmail || hasUsage || hasActive || signedIn) {
433
+ // Signed-in agents without a local email (Antigravity, Kimi) show a
434
+ // "signed in" placeholder so they read as logged in, not blank.
435
+ const display = vInfo?.email || (signedIn ? SIGNED_IN_LABEL : '');
436
+ const emailCol = display.padEnd(maxEmail);
437
+ parts.push(display ? chalk.cyan(emailCol) : ' '.repeat(maxEmail));
429
438
  }
430
439
  if (hasUsage || hasActive) {
431
440
  const usagePad = ' '.repeat(Math.max(0, maxUsageWidth - visibleWidth(usageStr)));
@@ -503,8 +512,10 @@ async function showInstalledVersions(filterAgentId) {
503
512
  const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
504
513
  const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null);
505
514
  const gActiveStr = gInfo ? formatLastActive(gInfo.lastActive) : '';
506
- if (gInfo?.email || gUsageStr || gActiveStr)
507
- parts.push(gInfo?.email ? chalk.cyan(gInfo.email) : '');
515
+ if (gInfo?.email || gUsageStr || gActiveStr || gInfo?.signedIn) {
516
+ const gDisplay = gInfo?.email || (gInfo?.signedIn ? SIGNED_IN_LABEL : '');
517
+ parts.push(gDisplay ? chalk.cyan(gDisplay) : '');
518
+ }
508
519
  if (gUsageStr || gActiveStr)
509
520
  parts.push(gUsageStr);
510
521
  const gStatusStr = formatUsageStatusBadge(gInfo?.usageStatus);
@@ -814,7 +825,11 @@ async function showAgentResources(agentId, requestedVersion, filter) {
814
825
  cliVersion: version,
815
826
  info: accountInfo,
816
827
  });
817
- const emailStr = accountInfo.email ? chalk.cyan(` ${accountInfo.email}`) : '';
828
+ const emailStr = accountInfo.email
829
+ ? chalk.cyan(` ${accountInfo.email}`)
830
+ : accountInfo.signedIn
831
+ ? chalk.cyan(` ${SIGNED_IN_LABEL}`)
832
+ : '';
818
833
  const status = chalk.green(version);
819
834
  const usageStr = formatUsageSummary(accountInfo.plan, null);
820
835
  const usagePart = usageStr ? ` ${usageStr}` : '';
@@ -1006,7 +1021,7 @@ async function collectAgentsJson(filterAgentId) {
1006
1021
  const entry = {
1007
1022
  version,
1008
1023
  isDefault: version === globalDefault,
1009
- signedIn: !!info.email,
1024
+ signedIn: info.signedIn,
1010
1025
  email: info.email,
1011
1026
  plan: info.plan,
1012
1027
  usageStatus: info.usageStatus,
@@ -1273,9 +1288,10 @@ export async function viewAction(agentArg, options) {
1273
1288
  console.log(chalk.red(formatAgentError(agentName)));
1274
1289
  process.exit(1);
1275
1290
  }
1276
- // Keep 'default' as-is since showAgentResources handles it; resolveVersionAlias
1277
- // returns undefined for 'default' which would skip the detailed view.
1278
- const requestedVersion = parts[1] === 'default'
1291
+ // Keep 'default'/'pinned' as-is since showAgentResources handles 'default';
1292
+ // resolveVersionAlias returns undefined for both (they're synonyms), which
1293
+ // would otherwise skip the detailed view.
1294
+ const requestedVersion = (parts[1] === 'default' || parts[1] === 'pinned')
1279
1295
  ? 'default'
1280
1296
  : (resolveVersionAlias(agentId, parts[1]) ?? null);
1281
1297
  if (prune) {
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import * as fs from 'fs';
11
11
  import * as os from 'os';
12
12
  import * as path from 'path';
13
13
  import { fileURLToPath } from 'url';
14
+ import { detectDevBuild } from './lib/startup/dev-build.js';
14
15
  // `ora`, `@inquirer/prompts`, `./commands/utils.js`, and the agents/versions/shims
15
16
  // modules are imported dynamically at their use sites: they are needed only on
16
17
  // interactive / update / shim-repair paths, never for fast commands like
@@ -37,18 +38,7 @@ import { NPM_PACKAGE_NAME, deriveGlobalPrefix, detectPackageManager, installPack
37
38
  // must not scribble on the user's real ~/.agents/), and skip the update prompt
38
39
  // (the "0.0.0-dev -> 1.x.y" message is misleading). Each individual env var
39
40
  // can still be set explicitly to override (set to '0' to re-enable).
40
- const IS_DEV_BUILD = (() => {
41
- if (VERSION.startsWith('0.0.0-dev'))
42
- return true;
43
- try {
44
- const cliPath = process.argv[1] || '';
45
- const repoRoot = path.dirname(path.dirname(cliPath));
46
- return fs.existsSync(path.join(repoRoot, '.git'));
47
- }
48
- catch {
49
- return false;
50
- }
51
- })();
41
+ const IS_DEV_BUILD = detectDevBuild(process.argv[1] || '', VERSION);
52
42
  if (IS_DEV_BUILD) {
53
43
  if (process.env.AGENTS_NO_AUTOPULL === undefined)
54
44
  process.env.AGENTS_NO_AUTOPULL = '1';
@@ -61,7 +51,7 @@ if (IS_DEV_BUILD) {
61
51
  // module on each invocation (which loaded the whole ~50-module tree before the
62
52
  // first byte of output), the registry maps a command name to a thunk that
63
53
  // imports only what that command needs. See src/lib/startup/command-registry.ts.
64
- 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, 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, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadBudget, loadAlias, loadPty, loadTmux, loadBrowser, loadComputer, loadHosts, loadPull, loadPush, loadRepo, loadSetup, } from './lib/startup/command-registry.js';
65
55
  import { applyGlobalHelpConventions } from './lib/help.js';
66
56
  import { IS_WINDOWS } from './lib/platform/index.js';
67
57
  // Transparent shim delegate: the generated Windows `.cmd` shims invoke
@@ -793,6 +783,7 @@ async function registerAllEagerCommands() {
793
783
  await reg(loadTmux);
794
784
  await reg(loadBrowser);
795
785
  await reg(loadComputer);
786
+ await reg(loadHosts);
796
787
  registerJobsCronAliasCommand(program, 'jobs');
797
788
  registerJobsCronAliasCommand(program, 'cron');
798
789
  registerUpgradeCommand(program);
@@ -0,0 +1,36 @@
1
+ import type { AgentId } from './types.js';
2
+ export interface AgentTarget {
3
+ agent: AgentId;
4
+ /** Resolved exact version, or null when the agent has no installed versions yet. */
5
+ version: string | null;
6
+ }
7
+ /** Canonical qualifier set, in help/display order. `pinned` ≡ `default`. */
8
+ export declare const AGENT_QUALIFIERS: readonly ["latest", "oldest", "pinned", "default", "all"];
9
+ export type AgentQualifier = (typeof AGENT_QUALIFIERS)[number];
10
+ /** Shared `--help` epilog so every agent-spec command documents the same grammar. */
11
+ export declare const AGENT_SPEC_HELP: string;
12
+ export declare class AgentSpecError extends Error {
13
+ constructor(message: string);
14
+ }
15
+ export interface ResolveAgentTargetsOptions {
16
+ /** Project dir for resolving a bare spec's project pin. Defaults to process.cwd(). */
17
+ cwd?: string;
18
+ /** Restrict the agents a spec may name (e.g. only mcp-capable). Defaults to all. */
19
+ availableAgents?: readonly AgentId[];
20
+ }
21
+ /**
22
+ * Resolve an agent spec (single or comma-list) into concrete installed targets.
23
+ * Domain = installed: `@latest`/`@oldest`/`@all` range over installed versions
24
+ * (`add`/`install` use a separate available-version path). Throws AgentSpecError
25
+ * on bad input — never calls process.exit, so it is safe on the hot path and in
26
+ * library contexts.
27
+ */
28
+ export declare function resolveAgentTargets(spec: string, opts?: ResolveAgentTargetsOptions): AgentTarget[];
29
+ /**
30
+ * Convenience for single-target commands (`use`, `run`): resolve a spec that
31
+ * must name exactly one installed version. Rejects `@all` / multi-target specs.
32
+ */
33
+ export declare function resolveSingleAgentTarget(spec: string, opts?: ResolveAgentTargetsOptions): {
34
+ agent: AgentId;
35
+ version: string;
36
+ };
@@ -0,0 +1,157 @@
1
+ // Centralized agent-spec resolution — one vocabulary, one resolver, reused by
2
+ // every subcommand that accepts `<agent>[@<qualifier>]`.
3
+ //
4
+ // The qualifier vocabulary used to be split across three functions in
5
+ // versions.ts (parseAgentSpec, resolveVersionAlias, resolveInstalledAgentTargets)
6
+ // with diverging support — `@latest`/`@oldest` in one, `@all`/`@default` in
7
+ // another, `@pinned` nowhere. This module is the single source of truth.
8
+ //
9
+ // Built for the hot path (`--launch`, ~100ms budget): the common specs resolve
10
+ // with NO directory enumeration —
11
+ // exact `claude@2.1.181` → one isVersionInstalled() (existsSync)
12
+ // `claude@pinned|@default` → memoized getGlobalDefault() + existsSync
13
+ // bare `claude` → resolveVersion() (memoized meta), no readdir
14
+ // Only the relative qualifiers `@latest`/`@oldest`/`@all` enumerate, and even
15
+ // then via the mtime-cached listInstalledVersions().
16
+ import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError } from './agents.js';
17
+ import { listInstalledVersions, getGlobalDefault, isVersionInstalled, resolveVersion, } from './versions.js';
18
+ /** Canonical qualifier set, in help/display order. `pinned` ≡ `default`. */
19
+ export const AGENT_QUALIFIERS = ['latest', 'oldest', 'pinned', 'default', 'all'];
20
+ /** Shared `--help` epilog so every agent-spec command documents the same grammar. */
21
+ export const AGENT_SPEC_HELP = 'Agent spec: <agent>[@<qualifier>]. Qualifiers: ' +
22
+ '@latest (highest installed), @oldest (lowest installed), ' +
23
+ '@pinned / @default (your configured default — synonyms), ' +
24
+ '@all (every installed version), or an exact @x.y.z. ' +
25
+ 'Bare <agent> uses the resolved default (project pin → global default). ' +
26
+ 'Comma-separate to combine: claude@all,codex@latest.';
27
+ export class AgentSpecError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = 'AgentSpecError';
31
+ }
32
+ }
33
+ /**
34
+ * Resolve an agent spec (single or comma-list) into concrete installed targets.
35
+ * Domain = installed: `@latest`/`@oldest`/`@all` range over installed versions
36
+ * (`add`/`install` use a separate available-version path). Throws AgentSpecError
37
+ * on bad input — never calls process.exit, so it is safe on the hot path and in
38
+ * library contexts.
39
+ */
40
+ export function resolveAgentTargets(spec, opts = {}) {
41
+ const cwd = opts.cwd ?? process.cwd();
42
+ const available = opts.availableAgents ?? ALL_AGENT_IDS;
43
+ const rawEntries = spec
44
+ .split(',')
45
+ .map((s) => s.trim())
46
+ .filter(Boolean);
47
+ if (rawEntries.length === 0) {
48
+ throw new AgentSpecError('Empty agent spec.');
49
+ }
50
+ // Expand the bare literal `all` (or `all@all`) into every available agent that
51
+ // has at least one installed version. Lenient: agents with nothing installed
52
+ // are skipped rather than erroring.
53
+ const entries = [];
54
+ for (const e of rawEntries) {
55
+ if (e === 'all' || e === 'all@all') {
56
+ for (const a of available) {
57
+ if (listInstalledVersions(a).length > 0)
58
+ entries.push(`${a}@all`);
59
+ }
60
+ }
61
+ else {
62
+ entries.push(e);
63
+ }
64
+ }
65
+ const out = [];
66
+ const seen = new Set();
67
+ const push = (agent, version) => {
68
+ const key = `${agent}@${version ?? ''}`;
69
+ if (!seen.has(key)) {
70
+ seen.add(key);
71
+ out.push({ agent, version });
72
+ }
73
+ };
74
+ for (const entry of entries) {
75
+ const at = entry.indexOf('@');
76
+ const agentToken = (at === -1 ? entry : entry.slice(0, at)).trim();
77
+ const qualifier = at === -1 ? null : entry.slice(at + 1).trim();
78
+ if (!agentToken)
79
+ continue;
80
+ if (at !== -1 && !qualifier) {
81
+ throw new AgentSpecError(`Missing version in '${entry}'. Use ${agentToken}@x.y.z, @latest, @oldest, @pinned, @default, or @all.`);
82
+ }
83
+ const agent = resolveAgentName(agentToken);
84
+ if (!agent || !available.includes(agent)) {
85
+ throw new AgentSpecError(formatAgentError(agentToken, [...available]));
86
+ }
87
+ const name = AGENTS[agent].name;
88
+ // ----- bare: resolved default, NO enumeration in the common case -----
89
+ if (qualifier === null) {
90
+ const resolved = resolveVersion(agent, cwd); // project pin → global default (meta-only)
91
+ if (resolved) {
92
+ push(agent, resolved);
93
+ }
94
+ else {
95
+ const installed = listInstalledVersions(agent);
96
+ if (installed.length === 0)
97
+ push(agent, null);
98
+ else if (installed.length === 1)
99
+ push(agent, installed[0]);
100
+ else
101
+ throw new AgentSpecError(`No default version set for ${name}. Specify one (${agent}@<version>) or set it: agents use ${agent}@<version>.`);
102
+ }
103
+ continue;
104
+ }
105
+ // ----- @pinned / @default: synonyms, meta-only fast path -----
106
+ if (qualifier === 'pinned' || qualifier === 'default') {
107
+ const def = getGlobalDefault(agent);
108
+ if (!def) {
109
+ throw new AgentSpecError(`No default version set for ${name}. Run: agents use ${agent}@<version>`);
110
+ }
111
+ push(agent, def);
112
+ continue;
113
+ }
114
+ // ----- @all: every installed version -----
115
+ if (qualifier === 'all') {
116
+ const installed = listInstalledVersions(agent);
117
+ if (installed.length === 0) {
118
+ throw new AgentSpecError(`No managed versions are installed for ${name}. Run: agents add ${agent}@latest`);
119
+ }
120
+ for (const v of installed)
121
+ push(agent, v);
122
+ continue;
123
+ }
124
+ // ----- @latest / @oldest: enumerate (mtime-cached), pick an end -----
125
+ if (qualifier === 'latest' || qualifier === 'oldest') {
126
+ const installed = listInstalledVersions(agent); // already sorted ascending
127
+ if (installed.length === 0) {
128
+ throw new AgentSpecError(`No managed versions are installed for ${name}. Run: agents add ${agent}@latest`);
129
+ }
130
+ push(agent, qualifier === 'oldest' ? installed[0] : installed[installed.length - 1]);
131
+ continue;
132
+ }
133
+ // ----- exact version: one existsSync, NO enumeration -----
134
+ if (!isVersionInstalled(agent, qualifier)) {
135
+ const installed = listInstalledVersions(agent);
136
+ const hint = installed.length ? ` Installed: ${installed.join(', ')}.` : '';
137
+ throw new AgentSpecError(`${name}@${qualifier} is not installed.${hint} Install it: agents add ${agent}@${qualifier}`);
138
+ }
139
+ push(agent, qualifier);
140
+ }
141
+ return out;
142
+ }
143
+ /**
144
+ * Convenience for single-target commands (`use`, `run`): resolve a spec that
145
+ * must name exactly one installed version. Rejects `@all` / multi-target specs.
146
+ */
147
+ export function resolveSingleAgentTarget(spec, opts = {}) {
148
+ const targets = resolveAgentTargets(spec, opts);
149
+ if (targets.length !== 1) {
150
+ throw new AgentSpecError(`'${spec}' resolves to ${targets.length} targets; this command needs exactly one.`);
151
+ }
152
+ const t = targets[0];
153
+ if (t.version === null) {
154
+ throw new AgentSpecError(`No installed version for ${AGENTS[t.agent].name}. Run: agents add ${t.agent}@latest`);
155
+ }
156
+ return { agent: t.agent, version: t.version };
157
+ }
@@ -107,6 +107,7 @@ export interface AccountInfo {
107
107
  currency: string;
108
108
  } | null;
109
109
  lastActive: Date | null;
110
+ signedIn: boolean;
110
111
  }
111
112
  /** Return the email address associated with the agent's auth config, or null. */
112
113
  export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;