@phnx-labs/agents-cli 1.22.23 → 1.22.25

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 (143) hide show
  1. package/CHANGELOG.md +359 -0
  2. package/README.md +19 -8
  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/cloud.js +9 -5
  7. package/dist/commands/defaults.js +2 -0
  8. package/dist/commands/doctor.d.ts +24 -0
  9. package/dist/commands/doctor.js +105 -9
  10. package/dist/commands/exec.js +99 -44
  11. package/dist/commands/feed.d.ts +23 -0
  12. package/dist/commands/feed.js +65 -2
  13. package/dist/commands/focus.d.ts +34 -3
  14. package/dist/commands/focus.js +301 -57
  15. package/dist/commands/go.d.ts +14 -1
  16. package/dist/commands/go.js +49 -5
  17. package/dist/commands/insights.js +6 -2
  18. package/dist/commands/inspect.js +39 -5
  19. package/dist/commands/menubar.js +14 -1
  20. package/dist/commands/models.js +1 -0
  21. package/dist/commands/modes.d.ts +12 -0
  22. package/dist/commands/modes.js +147 -0
  23. package/dist/commands/routines.d.ts +3 -0
  24. package/dist/commands/routines.js +70 -47
  25. package/dist/commands/run-cloud.js +1 -1
  26. package/dist/commands/secrets-sync.js +11 -13
  27. package/dist/commands/secrets.d.ts +2 -0
  28. package/dist/commands/secrets.js +54 -17
  29. package/dist/commands/sessions-browser.d.ts +36 -1
  30. package/dist/commands/sessions-browser.js +165 -19
  31. package/dist/commands/sessions-resume.d.ts +2 -0
  32. package/dist/commands/sessions-resume.js +28 -12
  33. package/dist/commands/sessions.d.ts +23 -1
  34. package/dist/commands/sessions.js +58 -61
  35. package/dist/commands/setup-secrets.js +1 -1
  36. package/dist/commands/setup-watchdog.js +5 -10
  37. package/dist/commands/setup.js +1 -1
  38. package/dist/commands/sync.js +246 -42
  39. package/dist/commands/teams.js +3 -2
  40. package/dist/commands/view.js +2 -0
  41. package/dist/commands/watchdog.d.ts +3 -4
  42. package/dist/commands/watchdog.js +26 -66
  43. package/dist/index.js +38 -2
  44. package/dist/lib/agent-modes.d.ts +49 -0
  45. package/dist/lib/agent-modes.js +70 -0
  46. package/dist/lib/agents.js +126 -21
  47. package/dist/lib/cloud/cursor.d.ts +79 -0
  48. package/dist/lib/cloud/cursor.js +228 -0
  49. package/dist/lib/cloud/registry.js +2 -0
  50. package/dist/lib/cloud/types.d.ts +7 -2
  51. package/dist/lib/cloud/types.js +14 -0
  52. package/dist/lib/crabbox/cli.d.ts +2 -2
  53. package/dist/lib/crabbox/config.d.ts +7 -8
  54. package/dist/lib/crabbox/config.js +14 -14
  55. package/dist/lib/crabbox/lease.d.ts +11 -4
  56. package/dist/lib/crabbox/lease.js +40 -8
  57. package/dist/lib/crabbox/setup-copy.d.ts +5 -0
  58. package/dist/lib/crabbox/setup-copy.js +17 -1
  59. package/dist/lib/daemon.js +27 -1
  60. package/dist/lib/device-config.js +7 -0
  61. package/dist/lib/devices/doctor-findings.d.ts +7 -1
  62. package/dist/lib/devices/doctor-findings.js +40 -1
  63. package/dist/lib/doctor-diff.d.ts +3 -0
  64. package/dist/lib/doctor-diff.js +15 -13
  65. package/dist/lib/event-stream.d.ts +3 -1
  66. package/dist/lib/event-stream.js +14 -1
  67. package/dist/lib/events.d.ts +9 -0
  68. package/dist/lib/events.js +58 -0
  69. package/dist/lib/exec.d.ts +15 -3
  70. package/dist/lib/exec.js +112 -19
  71. package/dist/lib/feed-outcome.d.ts +3 -0
  72. package/dist/lib/feed-outcome.js +18 -10
  73. package/dist/lib/feed.d.ts +4 -0
  74. package/dist/lib/hooks/cache.js +36 -3
  75. package/dist/lib/hooks.d.ts +19 -7
  76. package/dist/lib/hooks.js +88 -38
  77. package/dist/lib/hosts/passthrough.d.ts +21 -0
  78. package/dist/lib/hosts/passthrough.js +39 -12
  79. package/dist/lib/hosts/session-index.d.ts +4 -0
  80. package/dist/lib/hosts/session-index.js +7 -0
  81. package/dist/lib/manifest.d.ts +12 -2
  82. package/dist/lib/manifest.js +60 -5
  83. package/dist/lib/mcp.js +5 -1
  84. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  85. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
  86. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  87. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  88. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
  89. package/dist/lib/menubar/install-menubar.d.ts +52 -2
  90. package/dist/lib/menubar/install-menubar.js +128 -6
  91. package/dist/lib/menubar/snapshot.d.ts +15 -0
  92. package/dist/lib/menubar/snapshot.js +40 -0
  93. package/dist/lib/plugins.js +13 -1
  94. package/dist/lib/refresh.d.ts +5 -0
  95. package/dist/lib/refresh.js +37 -33
  96. package/dist/lib/resource-inventory.d.ts +79 -0
  97. package/dist/lib/resource-inventory.js +122 -0
  98. package/dist/lib/resources/mcp.js +3 -0
  99. package/dist/lib/resources.js +8 -5
  100. package/dist/lib/routine-process-cleanup.d.ts +9 -0
  101. package/dist/lib/routine-process-cleanup.js +73 -0
  102. package/dist/lib/run-defaults.d.ts +2 -0
  103. package/dist/lib/run-defaults.js +23 -2
  104. package/dist/lib/runner.js +27 -22
  105. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  106. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  107. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  108. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  109. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  110. package/dist/lib/secrets/bundles.js +158 -85
  111. package/dist/lib/secrets/filestore.d.ts +7 -3
  112. package/dist/lib/secrets/filestore.js +25 -8
  113. package/dist/lib/secrets/index.js +34 -8
  114. package/dist/lib/secrets/rc-hygiene.d.ts +14 -0
  115. package/dist/lib/secrets/rc-hygiene.js +14 -1
  116. package/dist/lib/secrets/reaper.d.ts +10 -0
  117. package/dist/lib/secrets/reaper.js +50 -15
  118. package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
  119. package/dist/lib/secrets/sync-passphrase.js +78 -0
  120. package/dist/lib/session/active.d.ts +4 -0
  121. package/dist/lib/session/db.js +8 -2
  122. package/dist/lib/session/recovery.d.ts +37 -0
  123. package/dist/lib/session/recovery.js +95 -0
  124. package/dist/lib/session/remote-list.d.ts +2 -0
  125. package/dist/lib/session/remote-list.js +1 -0
  126. package/dist/lib/session/session-cache.d.ts +4 -4
  127. package/dist/lib/session/session-cache.js +4 -4
  128. package/dist/lib/shims.d.ts +2 -2
  129. package/dist/lib/shims.js +45 -6
  130. package/dist/lib/signin-badge.js +2 -0
  131. package/dist/lib/startup/command-registry.d.ts +16 -0
  132. package/dist/lib/startup/command-registry.js +44 -0
  133. package/dist/lib/sync-umbrella.d.ts +5 -0
  134. package/dist/lib/sync-umbrella.js +5 -4
  135. package/dist/lib/teams/agents.js +1 -1
  136. package/dist/lib/teams/parsers.d.ts +1 -1
  137. package/dist/lib/tmux/session.d.ts +4 -2
  138. package/dist/lib/tmux/session.js +5 -5
  139. package/dist/lib/types.d.ts +3 -1
  140. package/dist/lib/versions.js +16 -1
  141. package/dist/lib/watchdog/service.d.ts +17 -0
  142. package/dist/lib/watchdog/service.js +39 -0
  143. package/package.json +1 -1
@@ -10,27 +10,21 @@
10
10
  * agents watchdog --nudge one tick, actually injects (explicit opt-in)
11
11
  * agents watchdog --watch manual poll loop (dry unless --nudge)
12
12
  * agents watchdog --json machine-readable tick output (for the menu-bar)
13
- * agents watchdog on|off turn the always-on watchdog routine on/off
13
+ * agents watchdog on|off turn the device-local daemon pass on/off
14
14
  * agents watchdog policy <id> <p> per-session policy: off | keep | handsoff
15
15
  *
16
- * The always-on watchdog is a daemon-fired ROUTINE, not a private sentinel + a
17
- * hand-rolled loop. Its immutable definition comes from the system DotAgents
18
- * repo; on/off only changes this device's routine membership.
16
+ * The agents daemon is the sole automatic watchdog scheduler. The menu bar reads
17
+ * persisted state; it never runs a tick.
19
18
  */
20
19
  import chalk from 'chalk';
21
20
  import * as path from 'path';
22
21
  import { setHelpSections } from '../lib/help.js';
23
22
  import { parseDuration } from '../lib/hooks/cache.js';
24
23
  import { getRuntimeStateDir } from '../lib/state.js';
25
- import { readJob, setJobEnabled } from '../lib/routines.js';
26
- import { getActiveSessions } from '../lib/session/active.js';
27
- import { loadLocalActiveSessions } from '../lib/session/session-cache.js';
28
- import { mailboxIdForActiveSession } from '../lib/mailbox-target.js';
29
- import { gcMailbox } from '../lib/mailbox-gc.js';
30
- import { devicesWithRoutineEnabled, routineEnabledOnThisDevice } from '../lib/routine-activation.js';
31
- import { runWatchdogTick, writePolicySentinel, DEFAULT_THRESHOLDS, } from '../lib/watchdog/runner.js';
24
+ import { getConfigValue, setConfigValue } from '../lib/device-config.js';
25
+ import { writePolicySentinel, DEFAULT_THRESHOLDS, } from '../lib/watchdog/runner.js';
32
26
  import { isWatchdogRotateEnabled, listRotateStates, setWatchdogRotateEnabled } from '../lib/watchdog/rotate.js';
33
- const WATCHDOG_ROUTINE_NAME = 'watchdog';
27
+ import { loadWatchdogSessions, runWatchdogPass } from '../lib/watchdog/service.js';
34
28
  /** Default state dir the runner and these subcommands share. */
35
29
  function stateDir() {
36
30
  return path.join(getRuntimeStateDir(), 'watchdog');
@@ -63,21 +57,6 @@ function humanMs(ms) {
63
57
  return `${Math.round(ms / 60_000)}m`;
64
58
  return `${Math.round(ms / 1000)}s`;
65
59
  }
66
- /**
67
- * Run the mailbox liveness sweep against the same session set the tick just used.
68
- * Idempotent and cheap: archiving a message twice is a no-op, and GC only scans
69
- * directories. Failures are swallowed so a mailbox-root problem cannot break the
70
- * watchdog tick.
71
- */
72
- async function runMailboxGc(sessions) {
73
- const activeBoxIds = new Set(sessions.map(mailboxIdForActiveSession).filter((id) => !!id));
74
- try {
75
- gcMailbox(activeBoxIds);
76
- }
77
- catch {
78
- // GC is best-effort housekeeping; the next tick will retry.
79
- }
80
- }
81
60
  function colorForOutcome(o) {
82
61
  if (o.injected)
83
62
  return chalk.green;
@@ -139,45 +118,35 @@ export function registerWatchdogCommand(program) {
139
118
  };
140
119
  // Injection gate: --nudge is the explicit opt-in to actually inject. Bare
141
120
  // `agents watchdog` (and `--watch` without `--nudge`) is dry. The always-on
142
- // path is the daemon routine, which runs `agents watchdog --nudge` so the
143
- // routine's enabled state IS the on/off switch, not a flag read here.
121
+ // path is the daemon-owned pass. Device config is its on/off switch; this
122
+ // explicit command still requires --nudge before it can inject.
144
123
  const computeWillInject = () => opts.nudge === true;
145
- const tickOnce = async (willInject, sessions) => runWatchdogTick({
124
+ const tickOnce = async (willInject, sessions) => runWatchdogPass({
146
125
  nudge: willInject,
147
126
  nudgeText: opts.text,
148
127
  smart: opts.smart === true,
149
128
  smartAgent: opts.smartAgent,
150
129
  thresholds,
151
130
  allowGhosttyFocus: opts.allowGhosttyFocus === true,
152
- stateDir: stateDir(),
153
131
  sessions,
154
132
  });
155
133
  // RUSH-2062: share the daemon-warmed local active-session snapshot with
156
134
  // 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
- };
165
135
  if (!opts.watch) {
166
136
  const willInject = computeWillInject();
167
137
  const sessions = await loadWatchdogSessions();
168
138
  const result = await tickOnce(willInject, sessions);
169
- await runMailboxGc(sessions);
170
139
  if (opts.json)
171
140
  console.log(JSON.stringify(result, null, 2));
172
141
  else
173
142
  printTick(result, willInject);
174
143
  return;
175
144
  }
176
- // Manual poll loop (for ad-hoc use; the always-on path is `enable`'s routine).
145
+ // Manual poll loop for ad-hoc use; the daemon owns the automatic cadence.
177
146
  const intervalMs = durationMsOr(opts.interval, 30_000);
178
147
  if (!computeWillInject() && !opts.json) {
179
148
  console.log(chalk.yellow(`watchdog --watch is DETECT-ONLY. Pass --nudge to inject, ` +
180
- `or run 'agents watchdog on' for the always-on daemon routine.`));
149
+ `or run 'agents watchdog on' for the daemon-owned pass.`));
181
150
  }
182
151
  // eslint-disable-next-line no-constant-condition
183
152
  while (true) {
@@ -185,7 +154,6 @@ export function registerWatchdogCommand(program) {
185
154
  const willInject = computeWillInject();
186
155
  const sessions = await loadWatchdogSessions();
187
156
  const result = await tickOnce(willInject, sessions);
188
- await runMailboxGc(sessions);
189
157
  if (opts.json)
190
158
  console.log(JSON.stringify(result));
191
159
  else
@@ -207,10 +175,10 @@ export function registerWatchdogCommand(program) {
207
175
  # Machine-readable for the menu-bar
208
176
  agents watchdog --json
209
177
 
210
- # Turn on the ALWAYS-ON watchdog (a daemon-fired routine)
178
+ # Turn on the daemon-owned watchdog pass (every three minutes)
211
179
  agents watchdog on
212
180
 
213
- # Show the routine, rotate config, and any in-flight in-place rotates
181
+ # Show device enablement, rotate config, and in-flight rotates
214
182
  agents watchdog status
215
183
 
216
184
  # Leave one session detected-but-untouched
@@ -253,37 +221,32 @@ export function registerWatchdogCommand(program) {
253
221
  'watchdog.rotate: off' to ~/.agents/agents.yaml; nudging stays on).
254
222
  State machine: ~/.agents/.cache/state/watchdog/rotate/<sessionId>.json.
255
223
 
256
- Always-on: 'agents watchdog on' enables the system-defined 'watchdog' routine
257
- on this device and reloads the daemon; 'off' disables it here. Inspect it with
258
- 'agents routines list'. Defaults
259
- OFF. Per-session policy: off (ignore), keep (default), handsoff (detect + flag).
224
+ Always-on: 'agents watchdog on' enables one daemon-owned pass every three
225
+ minutes on this device and reloads the daemon; 'off' disables it here.
226
+ Defaults OFF. Per-session policy: off (ignore), keep (default), handsoff
227
+ (detect + flag).
260
228
 
261
229
  State (tray-readable): ${path.join('~/.agents/.cache/state/watchdog', '{nudges,flags,last-tick}.json')}
262
230
  `,
263
231
  });
264
232
  // --- always-on enable/disable/status (backed by the daemon routine) --------
265
233
  const turnOn = async () => {
266
- const routine = readJob(WATCHDOG_ROUTINE_NAME);
267
- if (!routine)
268
- throw new Error("Built-in routine 'watchdog' is missing. Run: agents repo pull system");
269
- setJobEnabled(WATCHDOG_ROUTINE_NAME, true);
234
+ setConfigValue('watchdog.enabled', true);
270
235
  await reloadDaemonForRoutine(true);
271
- console.log(chalk.green(`watchdog: ON on this device (${routine.schedule ?? 'event-triggered'})`));
236
+ console.log(chalk.green('watchdog: ON on this device (every 3 minutes)'));
272
237
  };
273
238
  const turnOff = async () => {
274
- if (!readJob(WATCHDOG_ROUTINE_NAME))
275
- throw new Error("Built-in routine 'watchdog' is missing. Run: agents repo pull system");
276
- setJobEnabled(WATCHDOG_ROUTINE_NAME, false);
239
+ setConfigValue('watchdog.enabled', false);
277
240
  await reloadDaemonForRoutine(false);
278
241
  console.log(chalk.yellow('watchdog: OFF on this device'));
279
242
  };
280
243
  cmd.command('on')
281
- .description('Enable the built-in watchdog routine on this device.')
244
+ .description('Enable the daemon watchdog pass on this device.')
282
245
  .action(async () => {
283
246
  await turnOn();
284
247
  });
285
248
  cmd.command('off')
286
- .description('Disable the built-in watchdog routine on this device.')
249
+ .description('Disable the daemon watchdog pass on this device.')
287
250
  .action(async () => {
288
251
  await turnOff();
289
252
  });
@@ -304,7 +267,7 @@ export function registerWatchdogCommand(program) {
304
267
  chalk.dim(`(watchdog.rotate: ${s} in agents.yaml)`));
305
268
  });
306
269
  cmd.command('status')
307
- .description('Show whether the always-on watchdog routine is enabled and where state is written.')
270
+ .description('Show whether the daemon watchdog pass is enabled and where state is written.')
308
271
  .option('--json', 'Emit status as JSON (for the menu-bar / scripts)')
309
272
  .action((_opts, command) => {
310
273
  // The parent `watchdog` command also declares --json and greedily parses it
@@ -312,16 +275,14 @@ export function registerWatchdogCommand(program) {
312
275
  // parent, not this subcommand. optsWithGlobals() merges both levels, so we
313
276
  // read it correctly regardless of which command commander bound it to.
314
277
  const json = command.optsWithGlobals().json === true;
315
- const on = routineEnabledOnThisDevice(WATCHDOG_ROUTINE_NAME) === true;
316
- const enabledDevices = devicesWithRoutineEnabled(WATCHDOG_ROUTINE_NAME);
278
+ const on = getConfigValue('watchdog.enabled').value === true;
317
279
  const rotate = isWatchdogRotateEnabled() ? 'on' : 'off';
318
280
  const rotates = listRotateStates(stateDir());
319
281
  const inflight = rotates.filter((r) => r.phase !== 'done' && r.phase !== 'failed');
320
282
  if (json) {
321
283
  console.log(JSON.stringify({
322
284
  enabled: on,
323
- enabledDevices,
324
- routine: WATCHDOG_ROUTINE_NAME,
285
+ cadenceMs: 180_000,
325
286
  stateDir: stateDir(),
326
287
  rotate,
327
288
  rotates: rotates.map((r) => ({
@@ -335,8 +296,7 @@ export function registerWatchdogCommand(program) {
335
296
  }));
336
297
  return;
337
298
  }
338
- console.log(`always-on watchdog: ${on ? chalk.green('ON') : chalk.dim('off')} (routine '${WATCHDOG_ROUTINE_NAME}')`);
339
- console.log(`enabled devices: ${enabledDevices.length > 0 ? enabledDevices.join(', ') : chalk.dim('none')}`);
299
+ console.log(`always-on watchdog: ${on ? chalk.green('ON') : chalk.dim('off')} (every 3 minutes on this device)`);
340
300
  console.log(`rotate: ${rotate === 'on' ? chalk.green('on') : chalk.yellow('off')} (watchdog.rotate in agents.yaml) · ${inflight.length} in-flight`);
341
301
  for (const r of inflight) {
342
302
  console.log(` ${chalk.magenta(r.phase.padEnd(12))} ${chalk.bold(r.sessionId.slice(0, 8))} → ${r.newSessionId.slice(0, 8)}${r.error ? chalk.red(` ${r.error}`) : ''}`);
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);
@@ -1034,7 +1035,23 @@ program.on('command:*', (operands) => {
1034
1035
  if (minDist === 1 && closest) {
1035
1036
  const args = process.argv.slice(2);
1036
1037
  args[0] = closest;
1037
- program.parse(['node', 'agents', ...args]);
1038
+ // The typo'd name was unknown, so the top-level --host router (which ran
1039
+ // before commander parsing, against the ORIGINAL name) could not have
1040
+ // routed it - it correctly fell through to reach this handler at all
1041
+ // (that fallthrough is this ticket's own fix). But falling through to a
1042
+ // plain local re-parse means a routing flag on a corrected REAL
1043
+ // host-routable command (e.g. `docto --host box`, corrected to `doctor`)
1044
+ // silently ran LOCALLY instead of remotely, with no error - worse than
1045
+ // the loud "does not support --host" this ticket replaced. Re-run the
1046
+ // router with the CORRECTED name before falling through to local parse;
1047
+ // it already no-ops when no routing flag is present. RUSH-2022 review r2.
1048
+ void (async () => {
1049
+ const { maybeRunOnHost } = await import('./lib/hosts/passthrough.js');
1050
+ if (await maybeRunOnHost(closest, args)) {
1051
+ process.exit(process.exitCode ?? 0);
1052
+ }
1053
+ program.parse(['node', 'agents', ...args]);
1054
+ })();
1038
1055
  return;
1039
1056
  }
1040
1057
  console.error(`error: unknown command '${unknown}'`);
@@ -1073,6 +1090,9 @@ if (requestedCommand !== undefined && !helpOrVersionRequested && !requestedIsDis
1073
1090
  // Register only the command(s) this invocation actually uses. Lazy commands
1074
1091
  // (sessions/teams/cloud) are handled after applyGlobalHelpConventions below.
1075
1092
  const isLazyRequest = requestedCommand !== undefined && LAZY_COMMAND_NAMES.has(requestedCommand);
1093
+ // Set when the requested name maps to no command: the lazy tree is then also
1094
+ // registered below so the spellcheck can suggest `sessions`/`teams`/`cloud`.
1095
+ let requestedIsUnknown = false;
1076
1096
  if (requestedIsDisabled) {
1077
1097
  // The brand turned this command off: register the full tree so the "did you
1078
1098
  // mean" picker still works, then strip the disabled commands below so the
@@ -1086,6 +1106,7 @@ else if (requestedCommand !== undefined && !isLazyRequest) {
1086
1106
  // spellcheck and edit-distance-1 auto-correct (the command:* handler above)
1087
1107
  // see the same candidate set — and ordering — as main.
1088
1108
  await registerAllEagerCommands();
1109
+ requestedIsUnknown = true;
1089
1110
  }
1090
1111
  }
1091
1112
  // When requestedCommand is undefined (bare invocation, --version, --help, -h) no
@@ -1101,6 +1122,21 @@ if (isLazyRequest && !requestedIsDisabled) {
1101
1122
  for (const loader of COMMAND_LOADERS[requestedCommand])
1102
1123
  await reg(loader);
1103
1124
  }
1125
+ else if (requestedIsUnknown) {
1126
+ // Unknown command: the lazy names must be candidates too, or `agents session`
1127
+ // (a typo for the very much lazy `sessions`) gets no "did you mean" at all —
1128
+ // the miss the RUSH-2022 report walked into. Registration order still mirrors
1129
+ // main: lazy commands come after applyGlobalHelpConventions.
1130
+ const seen = new Set();
1131
+ for (const name of LAZY_COMMAND_NAMES) {
1132
+ for (const loader of COMMAND_LOADERS[name] ?? []) {
1133
+ if (seen.has(loader))
1134
+ continue;
1135
+ seen.add(loader);
1136
+ await reg(loader);
1137
+ }
1138
+ }
1139
+ }
1104
1140
  // White-label: remove any commands this brand disabled so they resolve as
1105
1141
  // unknown (the command:* handler then reports "unknown command"). Unbranded or
1106
1142
  // nothing-disabled → no-op. Done after all registration paths above.
@@ -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;
@@ -0,0 +1,70 @@
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 { ALL_MODES } from './types.js';
10
+ import { AGENTS } from './agents.js';
11
+ import { AGENT_COMMANDS, defaultModeFor } from './exec.js';
12
+ import { resolveRunDefaults } from './run-defaults.js';
13
+ /** Human description for each canonical permission mode. */
14
+ export const MODE_DESCRIPTIONS = {
15
+ plan: 'read-only investigation; no writes, no shell side-effects',
16
+ edit: 'may edit files; prompts for shell / risky operations',
17
+ auto: 'smart classifier auto-approves safe ops, prompts for risky',
18
+ skip: 'bypass every permission prompt (dangerously-skip-permissions)',
19
+ };
20
+ /**
21
+ * Build the permission-mode catalog for one harness.
22
+ * `version` only affects the configured run.defaults lookup (modes themselves
23
+ * are per-agent today, not version-gated).
24
+ */
25
+ export function getAgentModesCatalog(agent, version, cwd = process.cwd()) {
26
+ const supported = AGENTS[agent].capabilities.modes;
27
+ const defaultMode = defaultModeFor(agent);
28
+ const modeFlags = AGENT_COMMANDS[agent]?.modeFlags ?? {};
29
+ const headlessPlan = AGENTS[agent].capabilities.headlessPlan !== false;
30
+ const modes = supported.map((mode) => ({
31
+ mode,
32
+ flags: modeFlags[mode] ?? [],
33
+ description: MODE_DESCRIPTIONS[mode],
34
+ isDefault: mode === defaultMode,
35
+ }));
36
+ const unsupported = ALL_MODES.filter((m) => !supported.includes(m));
37
+ const runDefaults = resolveRunDefaults(agent, version, cwd);
38
+ const configuredMode = runDefaults.mode ?? null;
39
+ const configuredModeSource = runDefaults.sources.mode ?? null;
40
+ const notes = [];
41
+ notes.push(`'full' is accepted as a silent alias for 'skip'.`);
42
+ if (unsupported.includes('auto')) {
43
+ notes.push(`--mode auto degrades to edit on ${agent} (no native auto classifier).`);
44
+ }
45
+ if (unsupported.includes('plan')) {
46
+ notes.push(`--mode plan degrades to ${defaultMode} on ${agent} (no native read-only mode).`);
47
+ }
48
+ if (!headlessPlan && supported.includes('plan')) {
49
+ notes.push(`headless --mode plan is not supported on ${agent}; a prompt-based plan run auto-downgrades (see resolveHeadlessMode).`);
50
+ }
51
+ if (unsupported.includes('skip')) {
52
+ notes.push(`${agent} has no skip/full bypass mode.`);
53
+ }
54
+ return {
55
+ agent,
56
+ modes,
57
+ defaultMode,
58
+ configuredMode,
59
+ configuredModeSource,
60
+ headlessPlan,
61
+ unsupported: [...unsupported],
62
+ notes,
63
+ };
64
+ }
65
+ /** Format native flags for display, e.g. `--permission-mode plan` or `(harness default)`. */
66
+ export function formatModeFlags(flags) {
67
+ if (flags.length === 0)
68
+ return '(harness default)';
69
+ return flags.join(' ');
70
+ }
@@ -279,6 +279,7 @@ export const AGENTS = {
279
279
  color: 'cyan',
280
280
  cliCommand: 'cursor-agent',
281
281
  npmPackage: '',
282
+ cloudProvider: 'cursor',
282
283
  installScript: 'curl https://cursor.com/install -fsS | bash && mv ~/.local/bin/agent ~/.local/bin/cursor-agent && grep -q "/.local/bin" ~/.zshrc || echo \'export PATH="$HOME/.local/bin:$PATH"\' >> ~/.zshrc',
283
284
  configDir: path.join(HOME, '.cursor'),
284
285
  commandsDir: path.join(HOME, '.cursor', 'commands'),
@@ -306,7 +307,7 @@ export const AGENTS = {
306
307
  // See transformSubagentForCursor / https://cursor.com/docs/subagents.
307
308
  // interactiveRepl: false — cursor-agent exits immediately with no argv. It requires a
308
309
  // prompt to do anything useful; a bare invocation is not a REPL (RUSH-2185, EXEC-23a).
309
- capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '2026.1.22' }, rules: { file: '.cursorrules' }, workflows: false, memory: false, modes: ['edit', 'skip'], interactiveRepl: false }, // allowlist: ~/.cursor/cli-config.json
310
+ capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '2026.1.22' }, rules: { file: '.cursorrules' }, workflows: false, memory: false, modes: ['plan', 'edit', 'skip'], interactiveRepl: false }, // allowlist: ~/.cursor/cli-config.json
310
311
  },
311
312
  opencode: {
312
313
  id: 'opencode',
@@ -811,6 +812,75 @@ export const AGENTS = {
811
812
  interactiveRepl: true,
812
813
  },
813
814
  },
815
+ // Warp Agent CLI (`oz`) — the standalone coding-agent CLI on Warp's "Oz"
816
+ // platform (the same shared Warp binary invoked via the `oz` symlink). Native
817
+ // binary via `brew install --cask oz` (macOS) / `oz-stable` apt|yum|pacman
818
+ // package (Linux); self-updating, no npm package. Config lives under `~/.warp/`
819
+ // (`.mcp.json`, `skills/`, `tab_configs/`, `worktrees/`, `remote-server`).
820
+ // Headless: `oz agent run --prompt "<task>" [--model <id>]` (local) /
821
+ // `oz agent run-cloud` (cloud). Auth: `oz login` (browser OAuth) or a
822
+ // `WARP_API_KEY` env token for headless/CI (`oz api-key create`). Rules/context
823
+ // file is `AGENTS.md` (also reads WARP.md / CLAUDE.md). Autonomy is governed by
824
+ // the selected agent profile (`--profile`), not a per-run permission flag.
825
+ // Sessions/conversations are stored SERVER-SIDE (retrieved with auth via
826
+ // `oz run conversation get <id>`), so there is no local transcript for
827
+ // `agents sessions` to index — warp is intentionally absent from SESSION_AGENTS.
828
+ // Docs: https://docs.warp.dev/reference/cli
829
+ warp: {
830
+ id: 'warp',
831
+ name: 'Warp',
832
+ color: 'blueBright',
833
+ cliCommand: 'oz',
834
+ npmPackage: '',
835
+ installScript: 'brew install --cask oz',
836
+ configDir: path.join(HOME, '.warp'),
837
+ commandsDir: '',
838
+ commandsSubdir: '',
839
+ skillsDir: path.join(HOME, '.warp', 'skills'),
840
+ hooksDir: 'hooks',
841
+ instructionsFile: 'AGENTS.md',
842
+ format: 'markdown',
843
+ variableSyntax: '$ARGUMENTS',
844
+ // Oz has no event->shell-command hook registration surface (its documented
845
+ // capabilities are agent-profiles-permissions, codebase-context,
846
+ // full-terminal-use, mcp, planning, rules, slash-commands — no hooks).
847
+ supportsHooks: false,
848
+ capabilities: {
849
+ hooks: false,
850
+ // MCP: Oz reads the Claude `{ "mcpServers": {...} }` schema from
851
+ // `.warp/.mcp.json` (user `~/.warp/.mcp.json`, project `<root>/.warp/.mcp.json`)
852
+ // and accepts `--mcp <path|json>` at run time; `oz mcp list` reads them
853
+ // back. stdio + http with headers, same schema as Claude's .mcp.json.
854
+ mcp: true,
855
+ mcpHttp: true,
856
+ mcpHeaders: true,
857
+ // Autonomy is governed by the selected agent profile
858
+ // (agent-profiles-permissions), not a Claude-style tool-name allow/deny
859
+ // list agents-cli can write — so no allowlist writer (mirrors muse).
860
+ allowlist: false,
861
+ // Skills: `--skill <spec>` + `oz agent skills`; searched in
862
+ // `.agents/skills/`, `.warp/skills/`, `.claude/skills/`, `.codex/skills/`.
863
+ skills: true,
864
+ // Slash-commands are native/server-managed (no droppable markdown
865
+ // command-file directory for agents-cli to sync into).
866
+ commands: false,
867
+ // No Claude marketplace / plugin manifest support.
868
+ plugins: false,
869
+ // Cloud agents + agent profiles are server-side; no installable
870
+ // subagent-definition directory to sync into (keeps the table truthful).
871
+ subagents: false,
872
+ rules: { file: 'AGENTS.md' },
873
+ workflows: false,
874
+ memory: false,
875
+ // Oz has no per-run permission flag; a single autonomous mode maps to no
876
+ // flags (the `--profile` selection governs autonomy). Mirrors hermes.
877
+ modes: ['edit'],
878
+ rulesImports: false,
879
+ // `oz agent run` requires a prompt (--prompt/--saved-prompt/--task-id/
880
+ // --skill), so a bare invocation opens no REPL.
881
+ interactiveRepl: false,
882
+ },
883
+ },
814
884
  };
815
885
  /** All current and legacy agent IDs derived from the AGENTS registry. */
816
886
  export const ALL_AGENT_IDS = Object.keys(AGENTS);
@@ -1507,11 +1577,16 @@ function isValidOpenCodeCredential(value) {
1507
1577
  }
1508
1578
  /**
1509
1579
  * Whether a Muse Code `~/.config/muse/auth.json` document holds any usable
1510
- * access token. The launcher stores per-provider slots (e.g. `meta`) each with
1511
- * an `access_token` string; a top-level `access_token` is also accepted.
1512
- * Never returns the secret itself presence only.
1580
+ * access token. Live shape from `muse login` (device OAuth, Muse Code 0.1.0):
1581
+ * { schema_version: 1, providers: { meta: { access_token, api_key, user_email, … } } }
1582
+ * Also accept top-level or one-level-nested tokens for older/alternate writers.
1583
+ * Recurses into objects so `providers.meta.access_token` is found — a flat
1584
+ * one-level walk only saw `providers` and reported signed-out after a successful
1585
+ * login (balanced then excluded the account). Never returns the secret itself.
1513
1586
  */
1514
- function museAuthHasToken(value) {
1587
+ function museAuthHasToken(value, depth = 0) {
1588
+ if (depth > 4)
1589
+ return false;
1515
1590
  if (!value || typeof value !== 'object' || Array.isArray(value))
1516
1591
  return false;
1517
1592
  const root = value;
@@ -1520,16 +1595,31 @@ function museAuthHasToken(value) {
1520
1595
  if (typeof root.api_key === 'string' && root.api_key.length > 0)
1521
1596
  return true;
1522
1597
  for (const slot of Object.values(root)) {
1523
- if (!slot || typeof slot !== 'object' || Array.isArray(slot))
1524
- continue;
1525
- const entry = slot;
1526
- if (typeof entry.access_token === 'string' && entry.access_token.length > 0)
1527
- return true;
1528
- if (typeof entry.api_key === 'string' && entry.api_key.length > 0)
1598
+ if (museAuthHasToken(slot, depth + 1))
1529
1599
  return true;
1530
1600
  }
1531
1601
  return false;
1532
1602
  }
1603
+ /** Best-effort email from a Muse auth.json (providers.meta.user_email, etc.). */
1604
+ function museAuthEmail(value, depth = 0) {
1605
+ if (depth > 4)
1606
+ return null;
1607
+ if (!value || typeof value !== 'object' || Array.isArray(value))
1608
+ return null;
1609
+ const root = value;
1610
+ if (typeof root.user_email === 'string' && root.user_email.includes('@')) {
1611
+ return root.user_email;
1612
+ }
1613
+ if (typeof root.email === 'string' && root.email.includes('@')) {
1614
+ return root.email;
1615
+ }
1616
+ for (const slot of Object.values(root)) {
1617
+ const found = museAuthEmail(slot, depth + 1);
1618
+ if (found)
1619
+ return found;
1620
+ }
1621
+ return null;
1622
+ }
1533
1623
  /**
1534
1624
  * Whether a Claude version home's credential file is present but carries no
1535
1625
  * token — the "must have a real credential" floor (see
@@ -1924,10 +2014,8 @@ export async function getAccountInfo(agentId, home) {
1924
2014
  }
1925
2015
  case 'muse': {
1926
2016
  // Muse Code authenticates with META_API_KEY (env, highest priority) or
1927
- // a stored OAuth/API credential at ~/.config/muse/auth.json. The auth
1928
- // file is launcher-managed JSON with per-provider slots holding
1929
- // access_token; there is no email claim, so we report signed-in + a
1930
- // stable identity key (env-key vs provider slots) like kimi/opencode.
2017
+ // a stored OAuth/API credential at ~/.config/muse/auth.json. Live file
2018
+ // shape nests under providers.meta (access_token + optional user_email).
1931
2019
  if (process.env.META_API_KEY?.trim() || process.env.MODEL_API_KEY?.trim()) {
1932
2020
  const accountKey = buildIdentityKey(agentId, [['auth', 'env']]);
1933
2021
  return { ...empty, signedIn: true, accountId: 'env', accountKey, lastActive };
@@ -1938,13 +2026,18 @@ export async function getAccountInfo(agentId, home) {
1938
2026
  const data = JSON.parse(await fs.promises.readFile(authPath, 'utf-8'));
1939
2027
  if (!data || typeof data !== 'object')
1940
2028
  return { ...empty, lastActive };
1941
- // auth.json shape: { meta?: { access_token }, … } or similar slots.
1942
- // Any object with a nested access_token (or a top-level one) counts.
1943
- const hasToken = museAuthHasToken(data);
1944
- if (!hasToken)
2029
+ if (!museAuthHasToken(data))
1945
2030
  return { ...empty, lastActive };
1946
- const accountKey = buildIdentityKey(agentId, [['auth', 'file']]);
1947
- return { ...empty, signedIn: true, accountId: 'file', accountKey, lastActive };
2031
+ const email = museAuthEmail(data);
2032
+ const accountKey = buildIdentityKey(agentId, email ? [['email', email]] : [['auth', 'file']]);
2033
+ return {
2034
+ ...empty,
2035
+ signedIn: true,
2036
+ email,
2037
+ accountId: email ?? 'file',
2038
+ accountKey,
2039
+ lastActive,
2040
+ };
1948
2041
  }
1949
2042
  default:
1950
2043
  return { ...empty, lastActive };
@@ -2563,6 +2656,9 @@ export function getUserMcpConfigPath(agentId) {
2563
2656
  case 'muse':
2564
2657
  // Muse Code: MCP lives in ~/.config/muse/settings.json under mcp_servers.
2565
2658
  return path.join(agent.configDir, 'settings.json');
2659
+ case 'warp':
2660
+ // Oz reads user-scope MCP from ~/.warp/.mcp.json (Claude schema).
2661
+ return path.join(agent.configDir, '.mcp.json');
2566
2662
  default:
2567
2663
  // Gemini and others use settings.json
2568
2664
  return path.join(agent.configDir, 'settings.json');
@@ -2603,6 +2699,8 @@ export function getMcpConfigPathForHome(agentId, home) {
2603
2699
  return path.join(home, '.omp', 'agent', '.mcp.json');
2604
2700
  case 'muse':
2605
2701
  return path.join(home, '.config', 'muse', 'settings.json');
2702
+ case 'warp':
2703
+ return path.join(home, '.warp', '.mcp.json');
2606
2704
  default:
2607
2705
  return path.join(home, agentConfigDirName(agentId), 'settings.json');
2608
2706
  }
@@ -2649,6 +2747,9 @@ export function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
2649
2747
  // Muse project MCP rides the same settings schema under .muse/settings.json
2650
2748
  // when present; otherwise fall back to the user settings path.
2651
2749
  return path.join(cwd, '.muse', 'settings.json');
2750
+ case 'warp':
2751
+ // Oz reads project MCP from <root>/.warp/.mcp.json (Claude schema).
2752
+ return path.join(cwd, '.warp', '.mcp.json');
2652
2753
  default:
2653
2754
  return path.join(cwd, `.${agentId}`, 'settings.json');
2654
2755
  }
@@ -2796,6 +2897,10 @@ export const AGENT_NAME_ALIASES = {
2796
2897
  'muse-code': 'muse',
2797
2898
  'muse-spark': 'muse',
2798
2899
  'meta-muse': 'muse',
2900
+ warp: 'warp',
2901
+ oz: 'warp',
2902
+ 'warp-agent': 'warp',
2903
+ 'warp-cli': 'warp',
2799
2904
  };
2800
2905
  /**
2801
2906
  * Resolve a user-provided agent name (alias, shorthand, or canonical) to its AgentId.