@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
@@ -23,7 +23,7 @@ import chalk from 'chalk';
23
23
  import * as crypto from 'crypto';
24
24
  import * as fs from 'fs';
25
25
  import * as path from 'path';
26
- import { deleteKeychainToken, getKeychainToken, hasKeychainToken, listKeychainItems, setKeychainToken, } from '../lib/secrets/index.js';
26
+ import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, setKeychainToken, } from '../lib/secrets/index.js';
27
27
  import { getBackupsDir } from '../lib/state.js';
28
28
  import { encryptBlob, MIN_PASSPHRASE_LEN } from '../lib/secrets/sync.js';
29
29
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
@@ -104,6 +104,7 @@ export function registerSecretsMigrateAclCommand(secrets) {
104
104
  .description('Refresh existing keychain ACLs to use the signed Agents CLI helper. Dry-run by default.')
105
105
  .option('--commit', 'Perform writes (default is dry-run reporting only)')
106
106
  .option('--prefix <p>', `Restrict to items beginning with PREFIX (default ${ITEM_PREFIX})`, ITEM_PREFIX)
107
+ .option('--all', 'Rewrite EVERY matching item, not just legacy stragglers (slower; a Touch ID prompt per item)')
107
108
  .option('--passphrase-env <var>', 'Read the backup passphrase from this env var instead of prompting')
108
109
  .action(async (opts) => {
109
110
  try {
@@ -114,15 +115,24 @@ export function registerSecretsMigrateAclCommand(secrets) {
114
115
  if (!prefix.startsWith(ITEM_PREFIX)) {
115
116
  throw new Error(`--prefix must start with '${ITEM_PREFIX}' to avoid touching unrelated Keychain items (got '${prefix}').`);
116
117
  }
117
- const items = listKeychainItems(prefix).map((item) => {
118
+ // Default: migrate ONLY legacy stragglers (items still in the file-based
119
+ // keychain) — modern DP items need no rewrite and touching them is a
120
+ // Touch ID prompt per item for nothing. `--all` forces the old full
121
+ // rewrite. Either way this is one command over every matching item — no
122
+ // one-by-one invocation.
123
+ const names = opts.all ? listKeychainItems(prefix) : listLegacyKeychainItems(prefix);
124
+ const items = names.map((item) => {
118
125
  const localExists = hasKeychainToken(item);
119
126
  return { item, sync: !localExists };
120
127
  });
121
128
  if (items.length === 0) {
122
- console.log(chalk.gray(`No keychain items with prefix '${prefix}'.`));
129
+ console.log(opts.all
130
+ ? chalk.gray(`No keychain items with prefix '${prefix}'.`)
131
+ : chalk.green(`Nothing to migrate — all items under '${prefix}' already use the modern ACL.`));
123
132
  return;
124
133
  }
125
- console.log(chalk.bold(`Found ${items.length} item(s) under '${prefix}'.`));
134
+ const label = opts.all ? 'item' : 'legacy item';
135
+ console.log(chalk.bold(`Found ${items.length} ${label}(s) under '${prefix}' to migrate.`));
126
136
  if (!opts.commit) {
127
137
  for (const { item, sync } of items) {
128
138
  console.log(` ${chalk.cyan(item)} ${chalk.gray(sync ? '(synced)' : '(local)')}`);
@@ -131,16 +141,18 @@ export function registerSecretsMigrateAclCommand(secrets) {
131
141
  console.log(chalk.gray('Dry-run — pass --commit to perform the migration.'));
132
142
  return;
133
143
  }
134
- // Commit phase. Snapshot every value first, encrypt, then mutate.
144
+ // Commit phase. Snapshot every value first (one batched read behind a
145
+ // single helper process, so DP items share one auth context), encrypt,
146
+ // then mutate.
147
+ const fetched = getKeychainTokens(items.map((i) => i.item));
135
148
  const records = [];
136
149
  for (const { item, sync } of items) {
137
- try {
138
- const value = getKeychainToken(item);
139
- records.push({ item, sync, value });
140
- }
141
- catch (err) {
142
- console.error(chalk.red(`Skipping '${item}': read failed (${err.message}).`));
150
+ const value = fetched.get(item);
151
+ if (value === undefined) {
152
+ console.error(chalk.red(`Skipping '${item}': read failed or item absent.`));
153
+ continue;
143
154
  }
155
+ records.push({ item, sync, value });
144
156
  }
145
157
  if (records.length === 0) {
146
158
  console.error(chalk.red('No items could be read. Aborting before any writes.'));
@@ -18,3 +18,23 @@ export { SSH_TARGET_RE, assertValidSshTarget };
18
18
  export declare function bundleEnvToDotenv(env: Record<string, string>): string;
19
19
  /** Register the `agents secrets` command tree. */
20
20
  export declare function registerSecretsCommands(program: Command): void;
21
+ /**
22
+ * Quote one argument for a Windows `cmd.exe` command line, as built by Node's
23
+ * `spawn(..., { shell: true })` on win32 (`agents secrets exec`). cmd.exe does
24
+ * NO quoting of its own, so an unquoted arg with a space is split into several
25
+ * args, and a cmd metacharacter (`&|<>()^`) would be interpreted by the shell.
26
+ * We wrap any arg with whitespace, a quote, or a metacharacter in double quotes
27
+ * and escape embedded quotes / trailing backslashes per the CommandLineToArgvW
28
+ * rules, so the *child's* argv parse reconstructs the original argument.
29
+ *
30
+ * CAVEAT: cmd.exe expands `%VAR%` (always) and `!VAR!` (under delayed expansion)
31
+ * BEFORE argv parsing, and double-quoting does NOT suppress `%`/`!` (the
32
+ * "BatBadBut" / CVE-2024-1874 class). We deliberately do not escape `%`/`!`:
33
+ * `agents secrets exec` runs a caller-supplied command against a bundle the
34
+ * caller owns, so caller-controlled `%`/`!` is not a privilege boundary. If that
35
+ * ever changes (exec'ing an untrusted command line), route through a shell that
36
+ * disables expansion rather than relying on this quoter. An empty arg becomes
37
+ * `""`. Exported for tests. No-ops on non-Windows (the caller only invokes it
38
+ * under `process.platform === 'win32'`).
39
+ */
40
+ export declare function quoteWin32ExecArg(arg: string): string;
@@ -1325,8 +1325,18 @@ Examples:
1325
1325
  secretEnv = readAndResolveBundleEnv(bundleName, { caller: `command ${cmd}` }).env;
1326
1326
  }
1327
1327
  const { spawn } = await import('child_process');
1328
- const proc = spawn(cmd, args, {
1328
+ // On Windows, spawn without a shell ENOENTs for `.cmd`/`.bat` launchers
1329
+ // (npm, yarn, most JS CLIs) and shell built-ins, so we set shell:true.
1330
+ // With shell:true Node hands cmd.exe a single command line with NO quoting
1331
+ // of its own, so args containing spaces or cmd metacharacters must be
1332
+ // quoted here (quoteWin32ExecArg) or they'd be split. See that helper for
1333
+ // the cmd.exe %VAR%/!VAR! expansion caveat.
1334
+ const useShell = process.platform === 'win32';
1335
+ const spawnCmd = useShell ? quoteWin32ExecArg(cmd) : cmd;
1336
+ const spawnArgs = useShell ? args.map(quoteWin32ExecArg) : args;
1337
+ const proc = spawn(spawnCmd, spawnArgs, {
1329
1338
  stdio: 'inherit',
1339
+ shell: useShell,
1330
1340
  env: { ...process.env, ...secretEnv },
1331
1341
  });
1332
1342
  proc.on('close', (code) => process.exit(code ?? 0));
@@ -1619,6 +1629,48 @@ function humanRemaining(expiresAt) {
1619
1629
  const days = Math.round(hours / 24);
1620
1630
  return `locks in ${days} day${days === 1 ? '' : 's'}`;
1621
1631
  }
1632
+ /**
1633
+ * Quote one argument for a Windows `cmd.exe` command line, as built by Node's
1634
+ * `spawn(..., { shell: true })` on win32 (`agents secrets exec`). cmd.exe does
1635
+ * NO quoting of its own, so an unquoted arg with a space is split into several
1636
+ * args, and a cmd metacharacter (`&|<>()^`) would be interpreted by the shell.
1637
+ * We wrap any arg with whitespace, a quote, or a metacharacter in double quotes
1638
+ * and escape embedded quotes / trailing backslashes per the CommandLineToArgvW
1639
+ * rules, so the *child's* argv parse reconstructs the original argument.
1640
+ *
1641
+ * CAVEAT: cmd.exe expands `%VAR%` (always) and `!VAR!` (under delayed expansion)
1642
+ * BEFORE argv parsing, and double-quoting does NOT suppress `%`/`!` (the
1643
+ * "BatBadBut" / CVE-2024-1874 class). We deliberately do not escape `%`/`!`:
1644
+ * `agents secrets exec` runs a caller-supplied command against a bundle the
1645
+ * caller owns, so caller-controlled `%`/`!` is not a privilege boundary. If that
1646
+ * ever changes (exec'ing an untrusted command line), route through a shell that
1647
+ * disables expansion rather than relying on this quoter. An empty arg becomes
1648
+ * `""`. Exported for tests. No-ops on non-Windows (the caller only invokes it
1649
+ * under `process.platform === 'win32'`).
1650
+ */
1651
+ export function quoteWin32ExecArg(arg) {
1652
+ if (arg.length > 0 && !/[\s"&|<>()^]/.test(arg))
1653
+ return arg;
1654
+ let result = '"';
1655
+ let backslashes = 0;
1656
+ for (const ch of arg) {
1657
+ if (ch === '\\') {
1658
+ backslashes += 1;
1659
+ continue;
1660
+ }
1661
+ if (ch === '"') {
1662
+ // Double the run of backslashes, then escape this quote.
1663
+ result += '\\'.repeat(backslashes * 2 + 1) + '"';
1664
+ backslashes = 0;
1665
+ continue;
1666
+ }
1667
+ result += '\\'.repeat(backslashes) + ch;
1668
+ backslashes = 0;
1669
+ }
1670
+ // Trailing backslashes precede the closing quote → must be doubled.
1671
+ result += '\\'.repeat(backslashes * 2) + '"';
1672
+ return result;
1673
+ }
1622
1674
  /**
1623
1675
  * Copy text to the system clipboard, cross-platform.
1624
1676
  * macOS: `pbcopy`. Windows: `clip`. Linux: tries `wl-copy` (Wayland), then
@@ -215,6 +215,23 @@ function signalBadges(s) {
215
215
  parts.push(chalk.magenta(`wt:${s.worktree.slug}`));
216
216
  return parts.join(' ');
217
217
  }
218
+ /**
219
+ * Compact provenance badge: how to reach the session, not what it's doing.
220
+ * `ssh` flags a remote host; the tmux pane id is the send-keys target the feed
221
+ * would type back into. Local, non-tmux sessions add nothing (the common case).
222
+ */
223
+ function provenanceBadge(p) {
224
+ if (!p)
225
+ return '';
226
+ const parts = [];
227
+ if (p.transport === 'ssh')
228
+ parts.push(chalk.red('ssh'));
229
+ if (p.mux?.kind === 'tmux' && p.mux.pane)
230
+ parts.push(chalk.green(`tmux ${p.mux.pane}`));
231
+ else if (p.mux?.kind === 'screen')
232
+ parts.push(chalk.green('screen'));
233
+ return parts.join(' ');
234
+ }
218
235
  /**
219
236
  * Render a single agent-session row inside an already-printed group header.
220
237
  * Indent is the leading whitespace (2 spaces for flat groups, 4 inside a
@@ -228,7 +245,7 @@ function printActiveRow(s, indent) {
228
245
  const hostCol = chalk.gray(padToWidth(truncateToWidth(s.host ?? '-', 8), 9));
229
246
  const statusCol = statusColor(s.status)(padToWidth(truncateToWidth(activityLabel(s), 8), 9));
230
247
  const fork = s.pidCount && s.pidCount > 1 ? chalk.dim(`×${s.pidCount} `) : '';
231
- const badges = (fork ? fork : '') + signalBadges(s);
248
+ const badges = (fork ? fork : '') + [signalBadges(s), provenanceBadge(s.provenance)].filter(Boolean).join(' ');
232
249
  const desc = buildSessionDescription(s) || '-';
233
250
  // Fill the remaining width with the preview so nothing wraps under tmux/SSH.
234
251
  const fixed = stringWidth(indent) + 9 + 9 + 9 + 9 + (badges ? stringWidth(badges) + 1 : 0);
@@ -8,9 +8,9 @@ import { AGENTS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/
8
8
  import { capableAgents } from '../lib/capabilities.js';
9
9
  import { cloneRepo } from '../lib/git.js';
10
10
  import { discoverSkillsFromRepo, installSkillCentrally, listInstalledSkills, listInstalledSkillsWithScope, getSkillInfo, getSkillRules, getSkillsDir, countSkillFiles, tryParseSkillMetadata, diffVersionSkills, iterSkillsCapableVersions, removeSkillFromVersion, } from '../lib/skills.js';
11
- import { getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, } from '../lib/versions.js';
11
+ import { getGlobalDefault, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, } from '../lib/versions.js';
12
12
  import { recordVersionResources } from '../lib/state.js';
13
- import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, resolveAgentTargetsAutoInstalling, promptRemovalTargets, } from './utils.js';
13
+ import { isPromptCancelled, isInteractiveTerminal, parseCommaSeparatedList, printWithPager, requireInteractiveSelection, resolveAgentTargetsAutoInstalling, promptRemovalTargets, resolveListFilterOrExit, } from './utils.js';
14
14
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
15
15
  /** Register the `agents skills` command tree (list, add, remove, sync, prune, view). */
16
16
  export function registerSkillsCommands(program) {
@@ -59,7 +59,7 @@ When to use:
59
59
  process.exit(1);
60
60
  }
61
61
  filterAgent = resolved;
62
- filterVersion = resolveVersionAlias(resolved, parts[1]);
62
+ filterVersion = resolveListFilterOrExit(resolved, parts[1]);
63
63
  }
64
64
  const rows = await buildSkillRows({ filterAgent, filterVersion });
65
65
  spinner.stop();
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `agents status` — the unified sync-status surface.
3
+ *
4
+ * One command that answers "is my fleet in sync?" the same way every other
5
+ * surface does, because it reads the same engine (computeSyncStatus). Human mode
6
+ * renders the summary and, when a TTY finds drift, offers the interactive
7
+ * "sync now?" flow (promptDriftSync). `--json` emits the stable UnifiedSyncStatus
8
+ * contract the menu-bar and Agency consume. `--yes` reconciles everything with no
9
+ * prompts (the "kick it" path, safe in scripts).
10
+ */
11
+ import { Command } from 'commander';
12
+ export declare function registerStatusCommand(program: Command): void;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `agents status` — the unified sync-status surface.
3
+ *
4
+ * One command that answers "is my fleet in sync?" the same way every other
5
+ * surface does, because it reads the same engine (computeSyncStatus). Human mode
6
+ * renders the summary and, when a TTY finds drift, offers the interactive
7
+ * "sync now?" flow (promptDriftSync). `--json` emits the stable UnifiedSyncStatus
8
+ * contract the menu-bar and Agency consume. `--yes` reconciles everything with no
9
+ * prompts (the "kick it" path, safe in scripts).
10
+ */
11
+ import chalk from 'chalk';
12
+ import { AGENTS } from '../lib/agents.js';
13
+ import { setHelpSections } from '../lib/help.js';
14
+ import { computeSyncStatus } from '../lib/sync-status.js';
15
+ import { promptDriftSync } from '../lib/drift-sync.js';
16
+ const agentName = (id) => AGENTS[id]?.name ?? id;
17
+ function versionSummary(v) {
18
+ if (!v.everSynced)
19
+ return chalk.gray('never synced');
20
+ if (v.needsSync) {
21
+ const bits = [];
22
+ if (v.counts.drifted)
23
+ bits.push(`${v.counts.drifted} drifted`);
24
+ if (v.counts.missing)
25
+ bits.push(`${v.counts.missing} missing`);
26
+ return chalk.yellow(bits.join(' · '));
27
+ }
28
+ return chalk.green('in sync');
29
+ }
30
+ export function registerStatusCommand(program) {
31
+ const cmd = program
32
+ .command('status')
33
+ .description('Unified sync status across the fleet — what is drifted, missing, or behind, with an option to sync it.')
34
+ .option('--json', 'Output the machine-readable UnifiedSyncStatus contract')
35
+ .option('--yes', 'Reconcile everything detected (pull .system if behind + sync drifted/missing resources) without prompting')
36
+ .option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())');
37
+ setHelpSections(cmd, {
38
+ examples: `
39
+ # Show what's out of sync; offer to fix it (interactive)
40
+ agents status
41
+
42
+ # Machine-readable status for the menu-bar / Agency
43
+ agents status --json
44
+
45
+ # Reconcile everything with no prompts (CI / scripts / the "kick it" path)
46
+ agents status --yes
47
+ `,
48
+ });
49
+ cmd.action(async (opts) => {
50
+ const cwd = opts.cwd ?? process.cwd();
51
+ if (opts.json) {
52
+ const status = await computeSyncStatus({ cwd });
53
+ console.log(JSON.stringify(status, null, 2));
54
+ return;
55
+ }
56
+ const status = await computeSyncStatus({ cwd });
57
+ // Human summary header (always) — the per-version readout.
58
+ console.log(chalk.bold('Fleet sync status'));
59
+ if (status.system.unknown) {
60
+ console.log(` ${'.system repo'.padEnd(28)} ${chalk.gray('freshness unknown (no upstream)')}`);
61
+ }
62
+ else if (status.system.behind > 0) {
63
+ console.log(` ${'.system repo'.padEnd(28)} ${chalk.yellow(`${status.system.behind} behind`)}`);
64
+ }
65
+ else {
66
+ console.log(` ${'.system repo'.padEnd(28)} ${chalk.green('up to date')}`);
67
+ }
68
+ if (status.agents.length === 0) {
69
+ console.log(chalk.gray(' (no installed agent versions)'));
70
+ }
71
+ for (const v of status.agents) {
72
+ const label = `${agentName(v.agent)}@${v.version}${v.isDefault ? chalk.gray(' (default)') : ''}`;
73
+ console.log(` ${label.padEnd(28)} ${versionSummary(v)}`);
74
+ }
75
+ if (status.totals.orphan > 0) {
76
+ console.log(chalk.gray(` (${status.totals.orphan} orphan${status.totals.orphan === 1 ? '' : 's'} — run \`agents prune cleanup\`)`));
77
+ }
78
+ // Hand off to the shared interactive/apply flow (summary already printed above).
79
+ await promptDriftSync({ cwd, yes: opts.yes, status, quiet: true });
80
+ });
81
+ }
@@ -35,10 +35,10 @@ import { compileRulesForProject } from '../lib/rules/compile.js';
35
35
  import { runLaunchSync } from '../lib/project-launch.js';
36
36
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
37
37
  import { runUmbrellaSync } from '../lib/sync-umbrella.js';
38
+ import { addHostOption } from '../lib/hosts/option.js';
38
39
  /** Register the `agents sync` command. */
39
40
  export function registerSyncCommand(program) {
40
- program
41
- .command('sync [agentSpec] [repo]')
41
+ addHostOption(program.command('sync [agentSpec] [repo]'))
42
42
  .summary('Make this machine current, or sync resources into one agent')
43
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", a selector: @latest / @oldest / @pinned (= @default), or @all for every installed version.\n\nAppend a [repo] (or pass --repo) to scope the sync to a single DotAgent repo — system / user / project / <alias>. e.g. "agents sync claude@all system" reconciles only the system repo\'s resources into every installed Claude.\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).')
44
44
  .option('--agent <agent>', 'Agent identifier (legacy form; prefer the positional spec)')
@@ -1,7 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import * as fs from 'fs/promises';
3
+ import { addHostOption } from '../lib/hosts/option.js';
3
4
  import * as path from 'path';
4
- import { AgentManager, checkAllClis, getAgentsDir, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
5
+ import { AgentManager, checkAllClis, checkCliSignedIn, getAgentsDir, resolveSignInAdvisory, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
5
6
  import { resolveProvider } from '../lib/cloud/registry.js';
6
7
  import { runSupervisor } from '../lib/teams/supervisor.js';
7
8
  import { handleSpawn, handleStatus, handleStop, handleTasks, toTaskStatusSummary, } from '../lib/teams/api.js';
@@ -9,6 +10,7 @@ import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } f
9
10
  import { setHelpSections } from '../lib/help.js';
10
11
  import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
11
12
  import { isVersionInstalled, resolveVersionAlias, resolveVersionAliasLoose } from '../lib/versions.js';
13
+ import { AGENTS, warnAgentDeprecated } from '../lib/agents.js';
12
14
  import { discoverSessions, parseTimeFilter, resolveSessionById } from '../lib/session/discover.js';
13
15
  import { buildPreview as buildSessionPreview } from './sessions-picker.js';
14
16
  import { parseExecEnv } from '../lib/exec.js';
@@ -132,7 +134,7 @@ function parseTeammate(spec) {
132
134
  }
133
135
  }
134
136
  die(`Unknown teammate '${spec}'. Available agents: ${VALID_AGENTS.join(', ')}.\n` +
135
- ` Use 'claude', 'claude@2.1.112', or the name of a profile from 'agents view'.`);
137
+ ` Use 'claude', 'kimi@latest', 'kimi@0.19.2' (a version from 'agents view'), or a profile name.`);
136
138
  }
137
139
  function shortId(id) {
138
140
  return id.slice(0, 8);
@@ -182,6 +184,31 @@ export function wireCloudDispatcher(mgr) {
182
184
  return { cloudSessionId: cloudTask.id };
183
185
  });
184
186
  }
187
+ /**
188
+ * Advisory: warn once per agent type of any still-pending (staged `--after`)
189
+ * teammate whose CLI may not be signed in. Warn-only — never blocks `start`.
190
+ * Local teammates only; cloud teammates authenticate through their provider.
191
+ */
192
+ async function warnUnsignedTeammates(mgr, team) {
193
+ let pending;
194
+ try {
195
+ pending = (await mgr.listByTask(team)).filter((a) => a.status === 'pending' && !a.cloudProvider);
196
+ }
197
+ catch {
198
+ return; // team not loadable yet — nothing to warn about
199
+ }
200
+ const seen = new Set();
201
+ for (const a of pending) {
202
+ const agent = a.agentType;
203
+ if (seen.has(agent) || !AGENT_NAMES[agent])
204
+ continue;
205
+ seen.add(agent);
206
+ if (!(await checkCliSignedIn(agent))) {
207
+ console.error(chalk.yellow(`⚠ ${AGENT_NAMES[agent]} may not be signed in (detection is unreliable). Launching anyway.`) +
208
+ chalk.gray(`\n If it fails to start, run \`${AGENTS[agent].cliCommand}\` to log in, or pass --force to silence this.`));
209
+ }
210
+ }
211
+ }
185
212
  /** Single-wave start used by `teams start` without --watch. */
186
213
  async function runOneWave(mgr, team, json) {
187
214
  const launched = await mgr.startReady(team);
@@ -673,8 +700,7 @@ export function registerTeamsCommands(program) {
673
700
  `,
674
701
  });
675
702
  // list
676
- teams
677
- .command('list [query]')
703
+ addHostOption(teams.command('list [query]'))
678
704
  .alias('ls')
679
705
  .description('List your teams, most recent activity first')
680
706
  .option('-a, --agent <agent>', 'Filter: only teams with this agent (e.g. claude or claude@2.1.112)')
@@ -785,8 +811,7 @@ export function registerTeamsCommands(program) {
785
811
  printTeamTable(rows);
786
812
  });
787
813
  // create
788
- teams
789
- .command('create <team>')
814
+ addHostOption(teams.command('create <team>'))
790
815
  .aliases(['c', 'new'])
791
816
  .description('Start a new team. No teammates yet; add them with `teams add`.')
792
817
  .option('-d, --description <text>', 'One-line summary of what this team is working on')
@@ -825,8 +850,7 @@ export function registerTeamsCommands(program) {
825
850
  }
826
851
  });
827
852
  // add
828
- teams
829
- .command('add <team> <teammate> <task>')
853
+ addHostOption(teams.command('add <team> <teammate> <task>'))
830
854
  .alias('a')
831
855
  .description("Add a teammate to work on a task. Runs in background; returns immediately. Use 'status' to check in.")
832
856
  .option('-n, --name <name>', 'Friendly name for this teammate (e.g. alice). Required if using --after. Unique within team.')
@@ -841,6 +865,7 @@ export function registerTeamsCommands(program) {
841
865
  .option('--cloud <provider>', `Dispatch to cloud backend instead of local CLI: ${VALID_CLOUD_PROVIDERS.join('|')}`)
842
866
  .option('--repo <owner/repo>', 'GitHub repository (required for --cloud rush)')
843
867
  .option('--branch <name>', 'Target git branch for cloud dispatch')
868
+ .option('--force', "Skip the advisory 'may not be signed in' warning (detection is unreliable)")
844
869
  .option('--json', 'Output machine-readable JSON')
845
870
  .action(async (team, teammate, task, opts) => {
846
871
  if (!VALID_MODES.includes(opts.mode)) {
@@ -867,10 +892,18 @@ export function registerTeamsCommands(program) {
867
892
  }
868
893
  }
869
894
  const { agent, version, profileName } = parseTeammate(teammate);
895
+ warnAgentDeprecated(agent);
870
896
  if (version && !isVersionInstalled(agent, version)) {
871
897
  die(`${AGENT_NAMES[agent]} ${version} isn't installed.\n` +
872
898
  ` Install it: agents add ${agent}@${version}\n` +
873
- ` Or see what's installed: agents view ${agent}`);
899
+ ` Or see what's installed (incl. @latest): agents view ${agent}`);
900
+ }
901
+ // Advisory sign-in check: warn but NEVER block. Detection is unreliable
902
+ // for opaque-cred agents, so a false negative must not stop a team. Cloud
903
+ // dispatch authenticates through the provider, not the local CLI — skip it.
904
+ if (!opts.force && !cloudProviderId && !(await checkCliSignedIn(agent))) {
905
+ console.error(chalk.yellow(`⚠ ${AGENT_NAMES[agent]} may not be signed in (detection is unreliable). Adding anyway.`) +
906
+ chalk.gray(`\n If it fails to start, run \`${AGENTS[agent].cliCommand}\` to log in, or pass --force to silence this.`));
874
907
  }
875
908
  if (opts.name !== undefined) {
876
909
  if (!opts.name || !/^[A-Za-z0-9_-]+$/.test(opts.name)) {
@@ -1041,8 +1074,7 @@ export function registerTeamsCommands(program) {
1041
1074
  }
1042
1075
  });
1043
1076
  // status
1044
- teams
1045
- .command('status [team]')
1077
+ addHostOption(teams.command('status [team]'))
1046
1078
  .aliases(['s', 'st', 'check'])
1047
1079
  .description("Check in on a team: status, files touched, recent commands, last messages. Pass --verbose for the full per-teammate dump; --since for delta polling.")
1048
1080
  .option('-f, --filter <state>', 'Show only teammates in this state: running, completed, failed, stopped, or all (default: all)', 'all')
@@ -1136,13 +1168,13 @@ export function registerTeamsCommands(program) {
1136
1168
  console.log(chalk.gray(`${running.length} teammate${running.length === 1 ? '' : 's'} running. See 'agents sessions --active' for the full cross-context view.`));
1137
1169
  });
1138
1170
  // start — fire any staged teammates whose --after deps have all completed
1139
- teams
1140
- .command('start [team]')
1171
+ addHostOption(teams.command('start [team]'))
1141
1172
  .description('Launch any pending teammates whose --after dependencies are satisfied. Use --watch to keep draining the DAG as teammates finish and as new tasks are added mid-flight.')
1142
1173
  .option('--json', 'Output machine-readable JSON')
1143
1174
  .option('--watch', 'Keep running: poll every --interval seconds, fire new waves, exit when the DAG drains.')
1144
1175
  .option('--interval <seconds>', 'Seconds between waves in --watch mode (default 8)', '8')
1145
1176
  .option('--max-waves <n>', 'Safety cap on waves in --watch mode (default 1000)', '1000')
1177
+ .option('--force', "Skip the advisory 'may not be signed in' warning for staged teammates (detection is unreliable)")
1146
1178
  .action(async (team, opts) => {
1147
1179
  const mgr = mkManager();
1148
1180
  wireCloudDispatcher(mgr);
@@ -1152,6 +1184,8 @@ export function registerTeamsCommands(program) {
1152
1184
  return;
1153
1185
  team = picked;
1154
1186
  }
1187
+ if (!opts.force && !isJsonMode(opts))
1188
+ await warnUnsignedTeammates(mgr, team);
1155
1189
  if (!opts.watch) {
1156
1190
  await runOneWave(mgr, team, Boolean(opts.json));
1157
1191
  return;
@@ -1190,8 +1224,7 @@ export function registerTeamsCommands(program) {
1190
1224
  }
1191
1225
  });
1192
1226
  // stop
1193
- teams
1194
- .command('stop [team] [teammate]')
1227
+ addHostOption(teams.command('stop [team] [teammate]'))
1195
1228
  .description('Stop a running teammate. Can be restarted later. Cleans up worktree if no uncommitted changes.')
1196
1229
  .option('--json', 'Output machine-readable JSON')
1197
1230
  .action(async (team, ref, opts) => {
@@ -1451,19 +1484,45 @@ export function registerTeamsCommands(program) {
1451
1484
  teams
1452
1485
  .command('doctor')
1453
1486
  .alias('dr')
1454
- .description('Check which agents are installed and available to join a team. Verifies CLI paths.')
1487
+ .description('Check which agents are installed and available to join a team. Verifies CLI paths and shows an advisory sign-in hint.')
1455
1488
  .option('--json', 'Output machine-readable JSON')
1456
1489
  .action(async (opts) => {
1457
1490
  const info = checkAllClis();
1491
+ // Advisory enrichment only. Sign-in detection is UNRELIABLE, so it never
1492
+ // changes the authoritative installed/ready column — it annotates. And an
1493
+ // agent that is actually running in a team is treated as signed in
1494
+ // regardless of the probe, so doctor never reports a working agent as
1495
+ // logged out ("don't show wrong stuff").
1496
+ const running = new Set();
1497
+ try {
1498
+ for (const a of await mkManager().listRunning())
1499
+ running.add(a.agentType);
1500
+ }
1501
+ catch { /* no teams yet — leave running empty */ }
1502
+ const auth = {};
1503
+ await Promise.all(Object.entries(info).map(async ([name, entry]) => {
1504
+ const isRunning = running.has(name);
1505
+ const probe = entry.installed && !isRunning ? await checkCliSignedIn(name) : false;
1506
+ auth[name] = resolveSignInAdvisory(entry.installed, isRunning, probe);
1507
+ }));
1458
1508
  if (isJsonMode(opts)) {
1459
- console.log(JSON.stringify(info, null, 2));
1509
+ const merged = {};
1510
+ for (const [name, entry] of Object.entries(info))
1511
+ merged[name] = { ...entry, ...auth[name] };
1512
+ console.log(JSON.stringify(merged, null, 2));
1460
1513
  return;
1461
1514
  }
1462
1515
  console.log(chalk.bold('Who can join a team:'));
1463
1516
  for (const [name, entry] of Object.entries(info)) {
1464
1517
  const pretty = AGENT_NAMES[name] || name;
1465
1518
  if (entry.installed) {
1466
- console.log(` ${chalk.green('ready')} ${pretty.padEnd(10)} ${chalk.gray(entry.path || '')}`);
1519
+ const { signedIn, running: isRunning } = auth[name];
1520
+ const hint = isRunning
1521
+ ? chalk.gray('in use')
1522
+ : signedIn
1523
+ ? chalk.gray('signed in')
1524
+ : chalk.gray('sign-in unverified');
1525
+ console.log(` ${chalk.green('ready')} ${pretty.padEnd(10)} ${chalk.gray(entry.path || '')} ${hint}`);
1467
1526
  }
1468
1527
  else {
1469
1528
  console.log(` ${chalk.red('no ')} ${pretty.padEnd(10)} ${chalk.gray(entry.error || 'not installed')}`);
@@ -1,3 +1,4 @@
1
+ import { addHostOption } from '../lib/hosts/option.js';
1
2
  import chalk from 'chalk';
2
3
  import { ALL_AGENT_IDS, AGENTS, getAccountInfo, agentLabel, resolveAgentName, formatAgentError, } from '../lib/agents.js';
3
4
  import { listInstalledVersions, getGlobalDefault, getVersionHomePath } from '../lib/versions.js';
@@ -5,8 +6,7 @@ import { formatUsageSection, getUsageInfoForIdentity } from '../lib/usage.js';
5
6
  /** Agents whose CLI surfaces usage data we can read today. */
6
7
  const USAGE_SUPPORTED = new Set(['claude', 'codex']);
7
8
  export function registerUsageCommand(program) {
8
- program
9
- .command('usage [agent]')
9
+ addHostOption(program.command('usage [agent]'))
10
10
  .description('Show rate-limit / quota usage per agent')
11
11
  .addHelpText('after', `
12
12
  Examples:
@@ -6,6 +6,14 @@
6
6
  */
7
7
  import type { AgentId } from '../lib/types.js';
8
8
  import { type InstalledAgentTargetResult, type VersionSelectionResult } from '../lib/versions.js';
9
+ /**
10
+ * Resolve a read/list command's `@version` filter through the agent-spec engine,
11
+ * exiting cleanly on a bad spec. Drop-in for the old
12
+ * `resolveVersionAlias(agent, parts[1])`:
13
+ * undefined → show all · @default/@pinned → the default version ·
14
+ * @latest/@oldest/@x.y.z → concrete.
15
+ */
16
+ export declare function resolveListFilterOrExit(agent: AgentId, qualifier: string | undefined | null): string | undefined;
9
17
  /**
10
18
  * Check if an error is from user cancelling a prompt (Ctrl+C)
11
19
  */
@@ -11,6 +11,26 @@ import ora from 'ora';
11
11
  import { confirm } from '@inquirer/prompts';
12
12
  import { agentLabel, resolveAgentName } from '../lib/agents.js';
13
13
  import { installVersion, listInstalledVersions, resolveAgentVersionTargets, resolveInstalledAgentTargets, } from '../lib/versions.js';
14
+ import { resolveListFilter, AgentSpecError } from '../lib/agent-spec/index.js';
15
+ /**
16
+ * Resolve a read/list command's `@version` filter through the agent-spec engine,
17
+ * exiting cleanly on a bad spec. Drop-in for the old
18
+ * `resolveVersionAlias(agent, parts[1])`:
19
+ * undefined → show all · @default/@pinned → the default version ·
20
+ * @latest/@oldest/@x.y.z → concrete.
21
+ */
22
+ export function resolveListFilterOrExit(agent, qualifier) {
23
+ try {
24
+ return resolveListFilter(agent, qualifier);
25
+ }
26
+ catch (e) {
27
+ if (e instanceof AgentSpecError) {
28
+ console.error(chalk.red(e.message));
29
+ process.exit(1);
30
+ }
31
+ throw e;
32
+ }
33
+ }
14
34
  /**
15
35
  * Check if an error is from user cancelling a prompt (Ctrl+C)
16
36
  */
@@ -1,9 +1,10 @@
1
+ import { addHostOption } from '../lib/hosts/option.js';
1
2
  import chalk from 'chalk';
2
3
  import ora from 'ora';
3
4
  import * as fs from 'fs';
4
5
  import * as path from 'path';
5
6
  import { select, confirm, checkbox } from '@inquirer/prompts';
6
- import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, } from '../lib/agents.js';
7
+ import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, } from '../lib/agents.js';
7
8
  import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
8
9
  import { viewAction } from './view.js';
9
10
  import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
@@ -272,6 +273,7 @@ export function registerVersionsCommands(program) {
272
273
  }
273
274
  const { agent, version } = parsed;
274
275
  const agentConfig = AGENTS[agent];
276
+ warnAgentDeprecated(agent);
275
277
  if (!agentConfig.npmPackage && !agentConfig.installScript) {
276
278
  console.log(chalk.yellow(`${agentLabel(agentConfig.id)} has no npm package. Install manually.`));
277
279
  continue;
@@ -762,8 +764,7 @@ export function registerVersionsCommands(program) {
762
764
  }
763
765
  });
764
766
  // Deprecated: use `agents view` instead
765
- program
766
- .command('list [agent]')
767
+ addHostOption(program.command('list [agent]'))
767
768
  .description('List installed agent CLI versions')
768
769
  .action(async (agentArg) => {
769
770
  console.log(chalk.red('Deprecated: "agents list" is now "agents view"\n'));