@phnx-labs/agents-cli 1.22.22 → 1.22.24

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 (138) hide show
  1. package/CHANGELOG.md +402 -0
  2. package/README.md +13 -6
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/attach.d.ts +2 -0
  5. package/dist/commands/attach.js +32 -7
  6. package/dist/commands/defaults.js +2 -0
  7. package/dist/commands/doctor.js +20 -7
  8. package/dist/commands/exec.js +100 -34
  9. package/dist/commands/feed.d.ts +18 -0
  10. package/dist/commands/feed.js +44 -1
  11. package/dist/commands/focus.d.ts +108 -5
  12. package/dist/commands/focus.js +455 -32
  13. package/dist/commands/fork.d.ts +20 -2
  14. package/dist/commands/fork.js +91 -64
  15. package/dist/commands/go.d.ts +39 -1
  16. package/dist/commands/go.js +112 -7
  17. package/dist/commands/harness-wizard.d.ts +206 -0
  18. package/dist/commands/harness-wizard.js +403 -0
  19. package/dist/commands/harness.d.ts +12 -0
  20. package/dist/commands/harness.js +97 -101
  21. package/dist/commands/insights.js +6 -2
  22. package/dist/commands/inspect.js +39 -5
  23. package/dist/commands/menubar.js +6 -1
  24. package/dist/commands/models.js +1 -0
  25. package/dist/commands/modes.d.ts +12 -0
  26. package/dist/commands/modes.js +147 -0
  27. package/dist/commands/resume.js +10 -3
  28. package/dist/commands/secrets-sync.js +11 -13
  29. package/dist/commands/secrets.d.ts +2 -0
  30. package/dist/commands/secrets.js +79 -47
  31. package/dist/commands/sessions-browser.d.ts +35 -0
  32. package/dist/commands/sessions-browser.js +140 -14
  33. package/dist/commands/sessions-resume.d.ts +23 -3
  34. package/dist/commands/sessions-resume.js +73 -19
  35. package/dist/commands/sessions.d.ts +91 -5
  36. package/dist/commands/sessions.js +359 -125
  37. package/dist/commands/setup-secrets.js +1 -1
  38. package/dist/commands/sync.js +246 -42
  39. package/dist/commands/view.js +2 -0
  40. package/dist/commands/watchdog.js +13 -2
  41. package/dist/index.js +2 -1
  42. package/dist/lib/agent-modes.d.ts +49 -0
  43. package/dist/lib/agent-modes.js +70 -0
  44. package/dist/lib/agents.d.ts +1 -1
  45. package/dist/lib/agents.js +132 -0
  46. package/dist/lib/codex-policy.d.ts +17 -0
  47. package/dist/lib/codex-policy.js +48 -0
  48. package/dist/lib/crabbox/lease.d.ts +25 -0
  49. package/dist/lib/crabbox/lease.js +62 -0
  50. package/dist/lib/daemon.js +70 -0
  51. package/dist/lib/doctor-diff.d.ts +3 -0
  52. package/dist/lib/doctor-diff.js +15 -13
  53. package/dist/lib/event-stream.d.ts +3 -1
  54. package/dist/lib/event-stream.js +14 -1
  55. package/dist/lib/exec.d.ts +16 -0
  56. package/dist/lib/exec.js +175 -62
  57. package/dist/lib/feed-broadcast.d.ts +1 -20
  58. package/dist/lib/feed-broadcast.js +31 -1
  59. package/dist/lib/hooks/cache.js +36 -3
  60. package/dist/lib/hooks.d.ts +19 -7
  61. package/dist/lib/hooks.js +100 -40
  62. package/dist/lib/hosts/session-index.d.ts +4 -0
  63. package/dist/lib/hosts/session-index.js +7 -0
  64. package/dist/lib/manifest.d.ts +12 -2
  65. package/dist/lib/manifest.js +60 -5
  66. package/dist/lib/mcp.js +44 -0
  67. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  68. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  69. package/dist/lib/menubar/install-menubar.d.ts +52 -2
  70. package/dist/lib/menubar/install-menubar.js +128 -6
  71. package/dist/lib/models.d.ts +0 -5
  72. package/dist/lib/models.js +48 -0
  73. package/dist/lib/plugin-marketplace.js +9 -0
  74. package/dist/lib/pricing/prices.json +119 -92
  75. package/dist/lib/pricing/table.js +13 -0
  76. package/dist/lib/refresh.d.ts +5 -0
  77. package/dist/lib/refresh.js +37 -33
  78. package/dist/lib/remote-agents-json.d.ts +29 -1
  79. package/dist/lib/remote-agents-json.js +47 -10
  80. package/dist/lib/resource-inventory.d.ts +79 -0
  81. package/dist/lib/resource-inventory.js +122 -0
  82. package/dist/lib/resources/mcp.js +2 -0
  83. package/dist/lib/resources/permissions.js +3 -0
  84. package/dist/lib/resources/types.d.ts +2 -1
  85. package/dist/lib/resources.js +8 -5
  86. package/dist/lib/run-defaults.d.ts +2 -0
  87. package/dist/lib/run-defaults.js +23 -2
  88. package/dist/lib/runner.js +50 -36
  89. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  90. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  91. package/dist/lib/secrets/bundles.js +157 -65
  92. package/dist/lib/secrets/filestore.d.ts +5 -3
  93. package/dist/lib/secrets/filestore.js +12 -8
  94. package/dist/lib/secrets/index.d.ts +15 -1
  95. package/dist/lib/secrets/index.js +148 -53
  96. package/dist/lib/secrets/reaper.d.ts +97 -0
  97. package/dist/lib/secrets/reaper.js +219 -0
  98. package/dist/lib/secrets/remote.d.ts +29 -0
  99. package/dist/lib/secrets/remote.js +37 -1
  100. package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
  101. package/dist/lib/secrets/sync-passphrase.js +78 -0
  102. package/dist/lib/session/active.d.ts +36 -1
  103. package/dist/lib/session/active.js +60 -19
  104. package/dist/lib/session/actor-sidecar.d.ts +14 -0
  105. package/dist/lib/session/actor-sidecar.js +67 -4
  106. package/dist/lib/session/db.d.ts +1 -1
  107. package/dist/lib/session/db.js +32 -1
  108. package/dist/lib/session/discover.js +168 -0
  109. package/dist/lib/session/parse.d.ts +10 -0
  110. package/dist/lib/session/parse.js +98 -0
  111. package/dist/lib/session/recovery.d.ts +37 -0
  112. package/dist/lib/session/recovery.js +95 -0
  113. package/dist/lib/session/remote-list.d.ts +10 -1
  114. package/dist/lib/session/remote-list.js +2 -8
  115. package/dist/lib/session/remote.d.ts +57 -3
  116. package/dist/lib/session/remote.js +90 -26
  117. package/dist/lib/session/resume-command.d.ts +6 -0
  118. package/dist/lib/session/resume-command.js +8 -0
  119. package/dist/lib/session/session-cache.d.ts +173 -0
  120. package/dist/lib/session/session-cache.js +399 -0
  121. package/dist/lib/session/types.d.ts +1 -1
  122. package/dist/lib/session/types.js +1 -1
  123. package/dist/lib/session/width.d.ts +1 -1
  124. package/dist/lib/session/width.js +12 -2
  125. package/dist/lib/shims.d.ts +2 -2
  126. package/dist/lib/shims.js +62 -8
  127. package/dist/lib/staleness/writers/hooks.js +1 -1
  128. package/dist/lib/startup/command-registry.d.ts +1 -0
  129. package/dist/lib/startup/command-registry.js +2 -0
  130. package/dist/lib/sync-umbrella.d.ts +5 -0
  131. package/dist/lib/sync-umbrella.js +5 -4
  132. package/dist/lib/tmux/session.d.ts +4 -2
  133. package/dist/lib/tmux/session.js +5 -5
  134. package/dist/lib/types.d.ts +3 -1
  135. package/dist/lib/usage.d.ts +13 -0
  136. package/dist/lib/usage.js +215 -0
  137. package/dist/lib/versions.js +13 -2
  138. package/package.json +1 -1
@@ -66,7 +66,7 @@ function printBackendNotes(backend) {
66
66
  console.log(chalk.gray('backend: keychain — macOS reads may ask for Touch ID or the device password.'));
67
67
  }
68
68
  else if (backend === 'file') {
69
- console.log(chalk.gray('backend: file — set AGENTS_SECRETS_PASSPHRASE for headless encrypted-file reads.'));
69
+ console.log(chalk.gray('backend: file — encrypted at rest; headless reads need no passphrase by default, via a machine-local key at ~/.agents/.secrets-key/passphrase.'));
70
70
  }
71
71
  else {
72
72
  console.log(chalk.gray('backend: vault — synced ~/.agents/vault.age storage; unlock it with agents login.'));
@@ -31,6 +31,7 @@
31
31
  */
32
32
  import * as path from 'path';
33
33
  import chalk from 'chalk';
34
+ import { resolveSyncPassphraseFromEnv } from '../lib/secrets/sync-passphrase.js';
34
35
  import { agentLabel, resolveAgentName, MANAGED_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
35
36
  import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, resolveVersionAlias, listInstalledVersions, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, buildRepoScopedSelection, mergeRepoScopedSelections, listRepoNames, getVersionHomePath, } from '../lib/versions.js';
36
37
  import { capableAgents } from '../lib/capabilities.js';
@@ -43,6 +44,10 @@ import { runUmbrellaSync } from '../lib/sync-umbrella.js';
43
44
  import { addHostOption } from '../lib/hosts/option.js';
44
45
  import { syncRepoGit } from '../lib/git.js';
45
46
  import { getSystemAgentsDir, getUserAgentsDir, getEnabledExtraRepos } from '../lib/state.js';
47
+ /** Emit one JSON object to stdout for `--json` callers / fleet fan-out. */
48
+ function emitJson(payload) {
49
+ console.log(JSON.stringify(payload));
50
+ }
46
51
  /** Register the `agents sync` command. */
47
52
  export function registerSyncCommand(program) {
48
53
  addHostOption(program.command('sync [agentSpec] [repo]'))
@@ -57,6 +62,7 @@ export function registerSyncCommand(program) {
57
62
  .option('-y, --yes', 'Skip the interactive preview and auto-sync all detected resources', false)
58
63
  .option('--force', 'Re-sync even if no changes are detected since the last sync', false)
59
64
  .option('--quiet', 'Suppress all output (exit code indicates success)', false)
65
+ .option('--json', 'Emit machine-readable JSON (also accepted so fleet fan-out via --host all can parse each peer)', false)
60
66
  // Umbrella verb (no agent given): make this machine current.
61
67
  .option('--repos', 'Umbrella: git-pull ~/.agents + enabled ~/.agents-* extras', false)
62
68
  .option('--secrets', 'Umbrella: pull encrypted secret bundles from the remote', false)
@@ -88,22 +94,47 @@ function resolveRepoGitTarget(repo) {
88
94
  * tree, else pull --rebase against origin, pushing local commits for user-owned
89
95
  * repos. Delegates the git work to `syncRepoGit`.
90
96
  */
91
- async function runRepoGitSync(repo, quiet, outLog, errLog) {
97
+ async function runRepoGitSync(repo, quiet, outLog, errLog, json = false) {
92
98
  const target = resolveRepoGitTarget(repo);
93
99
  if (!target) {
94
- errLog(chalk.red(`The '${repo}' repo isn't independently git-synced.`));
95
- errLog(chalk.gray('Syncable repos: system (pull-only), user, and enabled extra-repo aliases.'));
100
+ if (json) {
101
+ emitJson({
102
+ ok: false,
103
+ mode: 'repo-git',
104
+ repo,
105
+ error: `The '${repo}' repo isn't independently git-synced.`,
106
+ });
107
+ }
108
+ else {
109
+ errLog(chalk.red(`The '${repo}' repo isn't independently git-synced.`));
110
+ errLog(chalk.gray('Syncable repos: system (pull-only), user, and enabled extra-repo aliases.'));
111
+ }
96
112
  process.exitCode = 1;
97
113
  return;
98
114
  }
99
- if (!quiet)
115
+ if (!quiet && !json)
100
116
  outLog(chalk.bold(`Syncing ${repo} repo…`) + chalk.gray(` (${target.dir})`));
101
117
  const result = await syncRepoGit(target.dir, { push: target.push });
102
118
  if (!result.success) {
103
- errLog(chalk.red(`sync ${repo} failed: ${result.error}`));
119
+ if (json) {
120
+ emitJson({ ok: false, mode: 'repo-git', repo, error: result.error ?? 'sync failed' });
121
+ }
122
+ else {
123
+ errLog(chalk.red(`sync ${repo} failed: ${result.error}`));
124
+ }
104
125
  process.exitCode = 1;
105
126
  return;
106
127
  }
128
+ if (json) {
129
+ emitJson({
130
+ ok: true,
131
+ mode: 'repo-git',
132
+ repo,
133
+ commit: result.commit,
134
+ pushed: !!result.pushed,
135
+ });
136
+ return;
137
+ }
107
138
  if (!quiet) {
108
139
  const note = result.pushed ? ' · pushed' : ' · pull-only';
109
140
  outLog(chalk.green(`✓ ${repo} → ${result.commit}${note}`));
@@ -201,12 +232,14 @@ async function runInteractiveReconcile(opts, outLog, errLog) {
201
232
  * arrives with `agents login`) and runs the fetch+reconcile stages, then prints
202
233
  * a one-line summary. Stage failures are non-fatal and surfaced as warnings.
203
234
  */
204
- async function runUmbrella(opts, quiet, outLog, errLog) {
235
+ async function runUmbrella(opts, quiet, outLog, errLog, json = false) {
205
236
  // Interactive bare `agents sync` (a TTY, no --yes, no scope flag) drops into
206
237
  // the two-checklist picker: which repos to sync from, which agents to sync
207
- // into. Any explicit flag or --yes keeps the non-interactive umbrella below.
238
+ // into. Any explicit flag, --yes, or --json keeps the non-interactive path.
239
+ // --json is a machine consumer (and the fleet fan-out injects it), so never
240
+ // open a picker under it.
208
241
  const anyExplicitFlag = !!(opts.repos || opts.secrets || opts.cloud || opts.local);
209
- if (!quiet && !opts.yes && !anyExplicitFlag && isInteractiveTerminal()) {
242
+ if (!quiet && !json && !opts.yes && !anyExplicitFlag && isInteractiveTerminal()) {
210
243
  await runInteractiveReconcile(opts, outLog, errLog);
211
244
  return;
212
245
  }
@@ -216,17 +249,36 @@ async function runUmbrella(opts, quiet, outLog, errLog) {
216
249
  cloud: opts.cloud,
217
250
  local: opts.local,
218
251
  };
219
- const passphrase = process.env.AGENTS_SECRETS_PASSPHRASE || undefined;
220
- if (!quiet)
252
+ // Same chokepoint as `agents secrets push/pull` — prefers AGENTS_SYNC_PASSPHRASE,
253
+ // falls back to the deprecated master-key name with a single warning.
254
+ const passphrase = resolveSyncPassphraseFromEnv().value ?? undefined;
255
+ // Fleet fan-out only injects --json (not --yes). Treat --json as non-interactive
256
+ // so refresh({ skipPrompts }) never tries to prompt over SSH.
257
+ const yes = !!opts.yes || json;
258
+ if (!quiet && !json)
221
259
  outLog(chalk.bold('Syncing this machine…'));
222
260
  try {
223
261
  const result = await runUmbrellaSync({
224
262
  flags,
225
- yes: !!opts.yes,
263
+ yes,
226
264
  passphrase,
227
- log: (msg) => { if (!quiet)
265
+ // quiet under --json so refresh() cannot pollute the JSON stdout the fleet parses.
266
+ quiet: quiet || json,
267
+ log: (msg) => { if (!quiet && !json)
228
268
  outLog(chalk.gray(` ${msg}`)); },
229
269
  });
270
+ if (json) {
271
+ emitJson({
272
+ ok: true,
273
+ mode: 'umbrella',
274
+ plan: result.plan,
275
+ repos: result.repos,
276
+ secrets: result.secrets,
277
+ devices: result.devices,
278
+ reconciled: result.reconciled,
279
+ });
280
+ return;
281
+ }
230
282
  if (!quiet) {
231
283
  const parts = [];
232
284
  if (result.repos) {
@@ -245,16 +297,30 @@ async function runUmbrella(opts, quiet, outLog, errLog) {
245
297
  }
246
298
  }
247
299
  catch (err) {
248
- errLog(chalk.red(`sync failed: ${err.message}`));
300
+ if (json) {
301
+ emitJson({ ok: false, mode: 'umbrella', error: err.message });
302
+ }
303
+ else {
304
+ errLog(chalk.red(`sync failed: ${err.message}`));
305
+ }
249
306
  process.exitCode = 1;
250
307
  }
251
308
  }
252
309
  async function runSync(agentSpec, repoArg, opts) {
253
- const quiet = !!opts.quiet;
310
+ const json = !!opts.json;
311
+ // --json is a machine consumer: suppress human stdout/stderr chatter so the
312
+ // single JSON object on stdout stays parseable for fleet fan-out.
313
+ const quiet = !!opts.quiet || json;
254
314
  const errLog = (msg) => { if (!quiet)
255
315
  console.error(msg); };
256
316
  const outLog = (msg) => { if (!quiet)
257
317
  console.log(msg); };
318
+ // Failures under --json still need a structured line on stdout so fleet
319
+ // fan-out's safeJsonParse gets a real object (not "unknown option").
320
+ const failJson = (payload) => {
321
+ if (json)
322
+ emitJson({ ok: false, ...payload });
323
+ };
258
324
  // ---------- 1. Resolve agent + version ----------
259
325
  let agentId;
260
326
  let version;
@@ -271,14 +337,21 @@ async function runSync(agentSpec, repoArg, opts) {
271
337
  // below, and it precedes agent-spec parsing because repo names like
272
338
  // "system"/"user" would otherwise fail parseAgentSpec.
273
339
  if (agentSpec && !opts.agent && !repoArg && listRepoNames().includes(agentSpec)) {
274
- await runRepoGitSync(agentSpec, quiet, outLog, errLog);
340
+ await runRepoGitSync(agentSpec, quiet, outLog, errLog, json);
275
341
  return;
276
342
  }
277
343
  if (agentSpec) {
278
344
  const parsed = parseAgentSpec(agentSpec);
279
345
  if (!parsed) {
280
- errLog(chalk.red(`Invalid agent spec '${agentSpec}'.`));
281
- errLog(chalk.gray('Examples: claude, claude@2.1.142, claude@latest, claude@oldest, claude@pinned, claude@all'));
346
+ failJson({
347
+ mode: 'agent',
348
+ error: `Invalid agent spec '${agentSpec}'.`,
349
+ hint: 'Examples: claude, claude@2.1.142, claude@latest, claude@oldest, claude@pinned, claude@all',
350
+ });
351
+ if (!json) {
352
+ errLog(chalk.red(`Invalid agent spec '${agentSpec}'.`));
353
+ errLog(chalk.gray('Examples: claude, claude@2.1.142, claude@latest, claude@oldest, claude@pinned, claude@all'));
354
+ }
282
355
  process.exitCode = 1;
283
356
  return;
284
357
  }
@@ -292,8 +365,11 @@ async function runSync(agentSpec, repoArg, opts) {
292
365
  if (repoScope !== undefined) {
293
366
  const known = listRepoNames();
294
367
  if (!known.includes(repoScope)) {
295
- errLog(chalk.red(`Unknown repo '${repoScope}'.`));
296
- errLog(chalk.gray(`Known repos: ${known.join(', ')}`));
368
+ failJson({ mode: 'agent', error: `Unknown repo '${repoScope}'.`, known });
369
+ if (!json) {
370
+ errLog(chalk.red(`Unknown repo '${repoScope}'.`));
371
+ errLog(chalk.gray(`Known repos: ${known.join(', ')}`));
372
+ }
297
373
  process.exitCode = 1;
298
374
  return;
299
375
  }
@@ -301,14 +377,18 @@ async function runSync(agentSpec, repoArg, opts) {
301
377
  if (opts.agent) {
302
378
  const resolved = resolveAgentName(opts.agent);
303
379
  if (!resolved) {
304
- errLog(chalk.red(`Unknown agent '${opts.agent}'.`));
380
+ failJson({ mode: 'agent', error: `Unknown agent '${opts.agent}'.` });
381
+ if (!json)
382
+ errLog(chalk.red(`Unknown agent '${opts.agent}'.`));
305
383
  process.exitCode = 1;
306
384
  return;
307
385
  }
308
386
  agentId = resolved;
309
387
  }
310
388
  if (agentId && isAgentHardDeprecated(agentId)) {
311
- errLog(chalk.red(hardDeprecationError(agentId)));
389
+ failJson({ mode: 'agent', error: hardDeprecationError(agentId) });
390
+ if (!json)
391
+ errLog(chalk.red(hardDeprecationError(agentId)));
312
392
  process.exitCode = 1;
313
393
  return;
314
394
  }
@@ -320,7 +400,8 @@ async function runSync(agentSpec, repoArg, opts) {
320
400
  if (!agentId) {
321
401
  // No agent specified → the umbrella verb: make this machine current
322
402
  // (fetch repos + secrets + sessions, then reconcile all installed agents).
323
- await runUmbrella(opts, quiet, outLog, errLog);
403
+ // This is the path fleet fan-out (`--host all`) hits with injected --json.
404
+ await runUmbrella(opts, quiet, outLog, errLog, json);
324
405
  return;
325
406
  }
326
407
  const projectDir = opts.projectDir;
@@ -332,8 +413,11 @@ async function runSync(agentSpec, repoArg, opts) {
332
413
  if (selector === 'all') {
333
414
  const installed = listInstalledVersions(agentId);
334
415
  if (installed.length === 0) {
335
- errLog(chalk.red(`No ${agentLabel(agentId)} versions installed.`));
336
- errLog(chalk.gray(`Install one: agents add ${agentId}@latest`));
416
+ failJson({ mode: 'agent-all', agent: agentId, error: `No ${agentLabel(agentId)} versions installed.` });
417
+ if (!json) {
418
+ errLog(chalk.red(`No ${agentLabel(agentId)} versions installed.`));
419
+ errLog(chalk.gray(`Install one: agents add ${agentId}@latest`));
420
+ }
337
421
  process.exitCode = 1;
338
422
  return;
339
423
  }
@@ -341,17 +425,43 @@ async function runSync(agentSpec, repoArg, opts) {
341
425
  if (repoScope) {
342
426
  selection = buildRepoScopedSelection(repoScope, cwd);
343
427
  if (Object.keys(selection).length === 0) {
344
- outLog(chalk.gray(`Nothing from repo '${repoScope}' to sync.`));
428
+ if (json)
429
+ emitJson({ ok: true, mode: 'agent-all', agent: agentId, repo: repoScope, versions: [], note: 'nothing to sync' });
430
+ else
431
+ outLog(chalk.gray(`Nothing from repo '${repoScope}' to sync.`));
345
432
  return;
346
433
  }
347
434
  }
348
435
  const scopeLabel = repoScope ? chalk.gray(` (repo: ${repoScope})`) : '';
349
- outLog(chalk.cyan(`Syncing ${installed.length} ${agentLabel(agentId)} version(s)${scopeLabel}.`));
436
+ if (!json)
437
+ outLog(chalk.cyan(`Syncing ${installed.length} ${agentLabel(agentId)} version(s)${scopeLabel}.`));
438
+ const versions = [];
350
439
  for (const v of installed) {
351
440
  const result = syncResourcesToVersion(agentId, v, selection, { projectDir, cwd, force });
352
- if (!quiet)
441
+ versions.push({ version: v, result });
442
+ if (!quiet && !json)
353
443
  printSyncDetail(result, agentId, v, cwd);
354
444
  }
445
+ if (json) {
446
+ emitJson({
447
+ ok: true,
448
+ mode: 'agent-all',
449
+ agent: agentId,
450
+ repo: repoScope,
451
+ versions: versions.map(({ version: v, result }) => ({
452
+ version: v,
453
+ commands: !!result.commands,
454
+ skills: !!result.skills,
455
+ hooks: !!result.hooks,
456
+ memory: result.memory,
457
+ mcp: result.mcp,
458
+ permissions: !!result.permissions,
459
+ subagents: result.subagents,
460
+ plugins: result.plugins,
461
+ workflows: result.workflows,
462
+ })),
463
+ });
464
+ }
355
465
  return;
356
466
  }
357
467
  // ---------- 2. Resolve version (project pin → global default → sole installed) ----------
@@ -370,15 +480,26 @@ async function runSync(agentSpec, repoArg, opts) {
370
480
  version = installed[0];
371
481
  }
372
482
  else if (installed.length === 0) {
373
- errLog(chalk.red(`No ${agentLabel(agentId)} versions installed.`));
374
- errLog(chalk.gray(`Install one: agents add ${agentId}@latest`));
483
+ failJson({ mode: 'agent', agent: agentId, error: `No ${agentLabel(agentId)} versions installed.` });
484
+ if (!json) {
485
+ errLog(chalk.red(`No ${agentLabel(agentId)} versions installed.`));
486
+ errLog(chalk.gray(`Install one: agents add ${agentId}@latest`));
487
+ }
375
488
  process.exitCode = 1;
376
489
  return;
377
490
  }
378
491
  else {
379
- errLog(chalk.red(`No default ${agentLabel(agentId)} version pinned. Specify one:`));
380
- for (const v of installed) {
381
- errLog(chalk.gray(` agents sync ${agentId}@${v}`));
492
+ failJson({
493
+ mode: 'agent',
494
+ agent: agentId,
495
+ error: `No default ${agentLabel(agentId)} version pinned.`,
496
+ installed,
497
+ });
498
+ if (!json) {
499
+ errLog(chalk.red(`No default ${agentLabel(agentId)} version pinned. Specify one:`));
500
+ for (const v of installed) {
501
+ errLog(chalk.gray(` agents sync ${agentId}@${v}`));
502
+ }
382
503
  }
383
504
  process.exitCode = 1;
384
505
  return;
@@ -386,18 +507,27 @@ async function runSync(agentSpec, repoArg, opts) {
386
507
  }
387
508
  }
388
509
  if (!isVersionInstalled(agentId, version)) {
389
- errLog(chalk.red(`${agentLabel(agentId)}@${version} is not installed.`));
390
510
  const installed = listInstalledVersions(agentId);
391
- if (installed.length > 0) {
392
- errLog(chalk.gray(`Installed: ${installed.join(', ')}`));
511
+ failJson({
512
+ mode: 'agent',
513
+ agent: agentId,
514
+ version,
515
+ error: `${agentLabel(agentId)}@${version} is not installed.`,
516
+ installed,
517
+ });
518
+ if (!json) {
519
+ errLog(chalk.red(`${agentLabel(agentId)}@${version} is not installed.`));
520
+ if (installed.length > 0) {
521
+ errLog(chalk.gray(`Installed: ${installed.join(', ')}`));
522
+ }
523
+ errLog(chalk.gray(`Install it: agents add ${agentId}@${version}`));
393
524
  }
394
- errLog(chalk.gray(`Install it: agents add ${agentId}@${version}`));
395
525
  process.exitCode = 1;
396
526
  return;
397
527
  }
398
528
  // ---------- 3. --launch mode bypasses everything below ----------
399
529
  if (opts.launch) {
400
- runLaunchMode(agentId, version, cwd, quiet);
530
+ runLaunchMode(agentId, version, cwd, quiet, json);
401
531
  return;
402
532
  }
403
533
  // ---------- 3b. Repo-scoped single-version sync ----------
@@ -406,16 +536,33 @@ async function runSync(agentSpec, repoArg, opts) {
406
536
  if (repoScope) {
407
537
  const scoped = buildRepoScopedSelection(repoScope, cwd);
408
538
  if (Object.keys(scoped).length === 0) {
409
- outLog(chalk.gray(`Nothing from repo '${repoScope}' to sync into ${agentLabel(agentId)}@${version}.`));
539
+ if (json) {
540
+ emitJson({
541
+ ok: true,
542
+ mode: 'agent',
543
+ agent: agentId,
544
+ version,
545
+ repo: repoScope,
546
+ note: 'nothing to sync',
547
+ });
548
+ }
549
+ else {
550
+ outLog(chalk.gray(`Nothing from repo '${repoScope}' to sync into ${agentLabel(agentId)}@${version}.`));
551
+ }
410
552
  return;
411
553
  }
412
554
  const result = syncResourcesToVersion(agentId, version, scoped, { projectDir, cwd, force });
413
- if (!quiet)
555
+ if (json) {
556
+ emitJson(agentSyncJson(agentId, version, result, repoScope));
557
+ }
558
+ else if (!quiet) {
414
559
  printSyncDetail(result, agentId, version, cwd);
560
+ }
415
561
  return;
416
562
  }
417
563
  // ---------- 4. Decide selection (interactive preview vs auto) ----------
418
- const yes = !!opts.yes;
564
+ // --json forces non-interactive (machine consumer / fleet fan-out).
565
+ const yes = !!opts.yes || json;
419
566
  const interactive = !quiet && !yes && isInteractiveTerminal();
420
567
  let selection;
421
568
  if (interactive) {
@@ -468,6 +615,20 @@ async function runSync(agentSpec, repoArg, opts) {
468
615
  const projectRoot = path.dirname(projectDir);
469
616
  projectCompile = compileRulesForProject(projectRoot);
470
617
  }
618
+ if (json) {
619
+ emitJson({
620
+ ...agentSyncJson(agentId, version, result),
621
+ projectCompile: projectCompile
622
+ ? {
623
+ compiled: !!projectCompile.compiled,
624
+ agentsPath: projectCompile.agentsPath,
625
+ symlinks: projectCompile.symlinks,
626
+ skippedClobber: projectCompile.skippedClobber,
627
+ }
628
+ : null,
629
+ });
630
+ return;
631
+ }
471
632
  if (quiet)
472
633
  return;
473
634
  // ---------- 6. Detailed output ----------
@@ -482,6 +643,26 @@ async function runSync(agentSpec, repoArg, opts) {
482
643
  console.log(chalk.yellow(`Skipped (user-authored, not overwritten): ${projectCompile.skippedClobber.join(', ')}`));
483
644
  }
484
645
  }
646
+ /** Stable `--json` payload for a single agent@version resource sync. */
647
+ function agentSyncJson(agent, version, result, repo) {
648
+ return {
649
+ ok: true,
650
+ mode: 'agent',
651
+ agent,
652
+ version,
653
+ ...(repo !== undefined ? { repo } : {}),
654
+ commands: !!result.commands,
655
+ skills: !!result.skills,
656
+ hooks: !!result.hooks,
657
+ memory: result.memory,
658
+ mcp: result.mcp,
659
+ permissions: !!result.permissions,
660
+ subagents: result.subagents,
661
+ plugins: result.plugins,
662
+ workflows: result.workflows,
663
+ projectSkipped: result.projectSkipped,
664
+ };
665
+ }
485
666
  function anyResources(r) {
486
667
  return r.commands.length + r.skills.length + r.hooks.length + r.memory.length +
487
668
  r.mcp.length + r.permissions.length + r.subagents.length +
@@ -533,17 +714,40 @@ function printSyncDetail(result, agent, version, cwd) {
533
714
  if (kept)
534
715
  console.log(chalk.gray(kept));
535
716
  }
536
- function runLaunchMode(agent, version, cwd, quiet) {
717
+ function runLaunchMode(agent, version, cwd, quiet, json = false) {
537
718
  let result;
538
719
  try {
539
720
  result = runLaunchSync({ agent, version, cwd });
540
721
  }
541
722
  catch (err) {
542
- if (!quiet) {
723
+ if (json) {
724
+ emitJson({
725
+ ok: false,
726
+ mode: 'launch',
727
+ agent,
728
+ version,
729
+ error: err.message,
730
+ });
731
+ process.exitCode = 1;
732
+ }
733
+ else if (!quiet) {
543
734
  console.error(chalk.yellow(`agents: launch sync skipped (${err.message})`));
544
735
  }
545
736
  return;
546
737
  }
738
+ if (json) {
739
+ emitJson({
740
+ ok: true,
741
+ mode: 'launch',
742
+ agent,
743
+ version,
744
+ rulesCompiled: !!result.rulesCompiled,
745
+ workspaceLinks: result.workspaceLinks,
746
+ marketplaces: result.marketplaces,
747
+ workspaceSkipped: result.workspaceSkipped,
748
+ });
749
+ return;
750
+ }
547
751
  if (quiet)
548
752
  return;
549
753
  const bits = [];
@@ -578,6 +578,8 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
578
578
  const runDefaultBits = [];
579
579
  if (runDefaults.mode)
580
580
  runDefaultBits.push(`mode:${runDefaults.mode}`);
581
+ if (runDefaults.effort)
582
+ runDefaultBits.push('effort:' + runDefaults.effort);
581
583
  if (!hasEmail && !hasUsage && !signedIn) {
582
584
  // No per-version credential. That is NOT the same as unusable: Claude
583
585
  // Code authenticates from `CLAUDE_CODE_OAUTH_TOKEN` when the
@@ -24,6 +24,7 @@ import { parseDuration } from '../lib/hooks/cache.js';
24
24
  import { getRuntimeStateDir } from '../lib/state.js';
25
25
  import { readJob, setJobEnabled } from '../lib/routines.js';
26
26
  import { getActiveSessions } from '../lib/session/active.js';
27
+ import { loadLocalActiveSessions } from '../lib/session/session-cache.js';
27
28
  import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
28
29
  import { gcMailbox } from '../lib/mailbox-gc.js';
29
30
  import { devicesWithRoutineEnabled, routineEnabledOnThisDevice } from '../lib/routine-activation.js';
@@ -151,9 +152,19 @@ export function registerWatchdogCommand(program) {
151
152
  stateDir: stateDir(),
152
153
  sessions,
153
154
  });
155
+ // RUSH-2062: share the daemon-warmed local active-session snapshot with
156
+ // menubar/CLI/Factory instead of re-running a full gather every tick.
157
+ const loadWatchdogSessions = async () => {
158
+ const loaded = await loadLocalActiveSessions({
159
+ // Force a live gather only when the warm path is unavailable; the
160
+ // cache layer already re-gathers past DEFAULT_ACTIVE_CACHE_MAX_AGE_MS.
161
+ gather: () => getActiveSessions({ localOnly: true }),
162
+ });
163
+ return loaded.sessions;
164
+ };
154
165
  if (!opts.watch) {
155
166
  const willInject = computeWillInject();
156
- const sessions = await getActiveSessions();
167
+ const sessions = await loadWatchdogSessions();
157
168
  const result = await tickOnce(willInject, sessions);
158
169
  await runMailboxGc(sessions);
159
170
  if (opts.json)
@@ -172,7 +183,7 @@ export function registerWatchdogCommand(program) {
172
183
  while (true) {
173
184
  // Re-evaluated each tick: picks up enable/disable flips mid-run.
174
185
  const willInject = computeWillInject();
175
- const sessions = await getActiveSessions();
186
+ const sessions = await loadWatchdogSessions();
176
187
  const result = await tickOnce(willInject, sessions);
177
188
  await runMailboxGc(sessions);
178
189
  if (opts.json)
package/dist/index.js CHANGED
@@ -94,7 +94,7 @@ if (IS_DEV_BUILD) {
94
94
  // module on each invocation (which loaded the whole ~50-module tree before the
95
95
  // first byte of output), the registry maps a command name to a thunk that
96
96
  // imports only what that command needs. See src/lib/startup/command-registry.ts.
97
- import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadInsights, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadHumans, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
97
+ import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadModes, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadInsights, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadHumans, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
98
98
  import { applyGlobalHelpConventions } from './lib/help.js';
99
99
  import { renderWhatsNew } from './lib/whats-new.js';
100
100
  import { getCliLaunch } from './lib/cli-entry.js';
@@ -941,6 +941,7 @@ async function registerAllEagerCommands() {
941
941
  await reg(loadDefaults);
942
942
  await reg(loadSet);
943
943
  await reg(loadModels);
944
+ await reg(loadModes);
944
945
  await reg(loadPrune);
945
946
  await reg(loadTrash);
946
947
  await reg(loadRestore);
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Permission-mode catalog for a harness.
3
+ *
4
+ * Source of truth is AGENTS[id].capabilities.modes plus the native CLI flags
5
+ * in AGENT_COMMANDS.modeFlags. This is the modes analog of the model catalog
6
+ * (`agents models`) — what a human or orchestrating agent reads before
7
+ * `agents run <agent> --mode …` / `agents teams add … --mode …`.
8
+ */
9
+ import type { AgentId, Mode } from './types.js';
10
+ /** Human description for each canonical permission mode. */
11
+ export declare const MODE_DESCRIPTIONS: Record<Mode, string>;
12
+ export interface AgentModeEntry {
13
+ mode: Mode;
14
+ /** Native CLI flags agents-cli forwards for this mode (empty = harness default). */
15
+ flags: string[];
16
+ description: string;
17
+ /** First entry in capabilities.modes — the safest native mode. */
18
+ isDefault: boolean;
19
+ }
20
+ export interface AgentModesCatalog {
21
+ agent: AgentId;
22
+ /** Modes this harness natively supports, in declaration order. */
23
+ modes: AgentModeEntry[];
24
+ /** capabilities.modes[0] — safest native mode; run may still default elsewhere (e.g. CLI plan, teams edit). */
25
+ defaultMode: Mode;
26
+ /**
27
+ * Configured run.defaults mode for this agent@version, when set.
28
+ * Null means no pin — CLI default (plan for most harnesses) or modes[0] applies.
29
+ */
30
+ configuredMode: Mode | null;
31
+ configuredModeSource: string | null;
32
+ /**
33
+ * Whether plan works in a headless (`-p` / prompt) run. false means a headless
34
+ * --mode plan degrades (see resolveHeadlessMode). undefined = assumed true.
35
+ */
36
+ headlessPlan: boolean;
37
+ /** Canonical modes this harness does NOT list in capabilities.modes. */
38
+ unsupported: Mode[];
39
+ /** Notes an orchestrator should read before picking a mode. */
40
+ notes: string[];
41
+ }
42
+ /**
43
+ * Build the permission-mode catalog for one harness.
44
+ * `version` only affects the configured run.defaults lookup (modes themselves
45
+ * are per-agent today, not version-gated).
46
+ */
47
+ export declare function getAgentModesCatalog(agent: AgentId, version?: string | null, cwd?: string): AgentModesCatalog;
48
+ /** Format native flags for display, e.g. `--permission-mode plan` or `(harness default)`. */
49
+ export declare function formatModeFlags(flags: string[]): string;