@phnx-labs/agents-cli 1.20.50 → 1.20.52

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 (124) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/commands/browser-picker.js +1 -18
  3. package/dist/commands/browser.js +215 -7
  4. package/dist/commands/cloud.js +7 -25
  5. package/dist/commands/computer.d.ts +1 -0
  6. package/dist/commands/computer.js +129 -8
  7. package/dist/commands/events.d.ts +1 -1
  8. package/dist/commands/events.js +2 -3
  9. package/dist/commands/exec.js +66 -8
  10. package/dist/commands/factory.js +9 -4
  11. package/dist/commands/feed.d.ts +9 -0
  12. package/dist/commands/feed.js +69 -0
  13. package/dist/commands/inspect.js +1 -11
  14. package/dist/commands/logs.d.ts +5 -1
  15. package/dist/commands/logs.js +248 -3
  16. package/dist/commands/mcp.js +9 -6
  17. package/dist/commands/message.js +1 -4
  18. package/dist/commands/profiles.js +1 -18
  19. package/dist/commands/repo.js +33 -14
  20. package/dist/commands/resource-view.d.ts +1 -0
  21. package/dist/commands/resource-view.js +5 -17
  22. package/dist/commands/secrets.d.ts +23 -0
  23. package/dist/commands/secrets.js +174 -70
  24. package/dist/commands/sessions-picker.js +1 -18
  25. package/dist/commands/sessions.js +6 -8
  26. package/dist/commands/teams-picker.js +1 -32
  27. package/dist/commands/teams.js +5 -27
  28. package/dist/commands/tmux.js +1 -3
  29. package/dist/commands/view.js +1 -9
  30. package/dist/commands/worktree.js +1 -4
  31. package/dist/index.js +6 -2
  32. package/dist/lib/agents.d.ts +0 -4
  33. package/dist/lib/agents.js +20 -33
  34. package/dist/lib/auto-dispatch-linear.d.ts +18 -0
  35. package/dist/lib/auto-dispatch-linear.js +107 -0
  36. package/dist/lib/auto-dispatch-provider.d.ts +10 -0
  37. package/dist/lib/auto-dispatch-provider.js +25 -0
  38. package/dist/lib/auto-dispatch.d.ts +87 -0
  39. package/dist/lib/auto-dispatch.js +142 -0
  40. package/dist/lib/browser/cdp.js +11 -2
  41. package/dist/lib/browser/drivers/ssh.d.ts +28 -10
  42. package/dist/lib/browser/drivers/ssh.js +57 -18
  43. package/dist/lib/browser/login-detection.d.ts +94 -0
  44. package/dist/lib/browser/login-detection.js +274 -0
  45. package/dist/lib/browser/profiles.d.ts +17 -8
  46. package/dist/lib/browser/profiles.js +27 -8
  47. package/dist/lib/browser/refs.js +1 -5
  48. package/dist/lib/browser/secret-ref.d.ts +10 -0
  49. package/dist/lib/browser/secret-ref.js +14 -0
  50. package/dist/lib/browser/service.js +14 -12
  51. package/dist/lib/cli-resources.d.ts +0 -2
  52. package/dist/lib/cli-resources.js +30 -13
  53. package/dist/lib/cloud/rush.d.ts +15 -24
  54. package/dist/lib/cloud/rush.js +7 -32
  55. package/dist/lib/crabbox/cli.js +4 -1
  56. package/dist/lib/crabbox/lease.d.ts +6 -0
  57. package/dist/lib/crabbox/lease.js +40 -10
  58. package/dist/lib/crabbox/runtimes.d.ts +38 -1
  59. package/dist/lib/crabbox/runtimes.js +98 -5
  60. package/dist/lib/daemon.d.ts +12 -9
  61. package/dist/lib/daemon.js +73 -17
  62. package/dist/lib/events.d.ts +31 -5
  63. package/dist/lib/events.js +288 -101
  64. package/dist/lib/exec.js +32 -14
  65. package/dist/lib/feed.d.ts +56 -0
  66. package/dist/lib/feed.js +251 -0
  67. package/dist/lib/format.d.ts +38 -0
  68. package/dist/lib/format.js +108 -0
  69. package/dist/lib/git.d.ts +21 -0
  70. package/dist/lib/git.js +92 -0
  71. package/dist/lib/hooks/cache.d.ts +9 -2
  72. package/dist/lib/hooks/cache.js +220 -8
  73. package/dist/lib/hooks.js +24 -10
  74. package/dist/lib/hosts/passthrough.js +1 -0
  75. package/dist/lib/platform/exec.d.ts +4 -1
  76. package/dist/lib/platform/exec.js +8 -2
  77. package/dist/lib/resources.d.ts +0 -8
  78. package/dist/lib/resources.js +0 -10
  79. package/dist/lib/rotate.js +2 -0
  80. package/dist/lib/runner.js +10 -2
  81. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  82. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  83. package/dist/lib/secrets/agent.d.ts +21 -0
  84. package/dist/lib/secrets/agent.js +63 -1
  85. package/dist/lib/secrets/bundles.d.ts +33 -1
  86. package/dist/lib/secrets/bundles.js +38 -8
  87. package/dist/lib/secrets/icloud-import.d.ts +70 -0
  88. package/dist/lib/secrets/icloud-import.js +173 -0
  89. package/dist/lib/secrets/index.d.ts +36 -0
  90. package/dist/lib/secrets/index.js +99 -9
  91. package/dist/lib/secrets/remote.js +1 -1
  92. package/dist/lib/secrets/sync.js +1 -1
  93. package/dist/lib/session/active.d.ts +11 -1
  94. package/dist/lib/session/active.js +3 -0
  95. package/dist/lib/session/db.d.ts +1 -4
  96. package/dist/lib/session/db.js +20 -25
  97. package/dist/lib/session/discover.d.ts +2 -2
  98. package/dist/lib/session/discover.js +60 -48
  99. package/dist/lib/session/parse.js +35 -34
  100. package/dist/lib/session/render.d.ts +7 -3
  101. package/dist/lib/session/render.js +15 -9
  102. package/dist/lib/session/state.d.ts +55 -0
  103. package/dist/lib/session/state.js +100 -11
  104. package/dist/lib/session/types.d.ts +9 -0
  105. package/dist/lib/shims.d.ts +9 -3
  106. package/dist/lib/shims.js +44 -8
  107. package/dist/lib/ssh-tunnel.d.ts +33 -2
  108. package/dist/lib/ssh-tunnel.js +94 -7
  109. package/dist/lib/staleness/types.d.ts +0 -1
  110. package/dist/lib/startup/command-registry.d.ts +1 -0
  111. package/dist/lib/startup/command-registry.js +2 -0
  112. package/dist/lib/state.d.ts +2 -0
  113. package/dist/lib/state.js +25 -8
  114. package/dist/lib/teams/agents.js +6 -3
  115. package/dist/lib/types.d.ts +24 -1
  116. package/dist/lib/versions.d.ts +0 -26
  117. package/dist/lib/versions.js +2 -145
  118. package/dist/lib/warn-unpushed.d.ts +40 -0
  119. package/dist/lib/warn-unpushed.js +128 -0
  120. package/dist/lib/whats-new.d.ts +5 -3
  121. package/dist/lib/whats-new.js +25 -5
  122. package/package.json +3 -1
  123. package/dist/lib/resources/index.d.ts +0 -53
  124. package/dist/lib/resources/index.js +0 -76
@@ -4,7 +4,9 @@ import * as os from 'os';
4
4
  import * as path from 'path';
5
5
  import { registerCommandGroups } from '../lib/help.js';
6
6
  import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, resolvePeersPath, resolveTcpEndpoint, loadComputerAllowList, loadDefaultPeers, writeComputerPolicy, writeComputerPeers, } from '../lib/computer-rpc.js';
7
- import { setupRemoteHelper, startRemoteTunnel, stopRemoteHelper, hydrateRemoteEnvFromState, } from '../lib/ssh-tunnel.js';
7
+ import { setupRemoteHelper, startRemoteTunnel, stopRemoteHelper, hydrateRemoteEnvFromState, readRemoteState, resolveRemoteDevice, REMOTE_TASK_NAME, WIN_HELPER_EXE, } from '../lib/ssh-tunnel.js';
8
+ import { sshExec } from '../lib/ssh-exec.js';
9
+ import { encodePowershell } from '../lib/hosts/remote-cmd.js';
8
10
  import { registerActionCommands, withClient, unwrap, pickTarget } from './computer-actions.js';
9
11
  import { runComputerLoop } from '../lib/computer/loop.js';
10
12
  import { makeVerbDispatcher } from '../lib/computer/dispatch.js';
@@ -18,9 +20,10 @@ const COMPUTER_HELP_GROUPS = [
18
20
  { title: 'Interact', names: ['launch', 'raise', 'click', 'right-click', 'type', 'type-text', 'key', 'drag', 'scroll', 'ax-action', 'focus', 'wait'] },
19
21
  ];
20
22
  // Subcommands that manage the `--host` remote path themselves (provisioning /
21
- // tunnel lifecycle). Every other `--host`-bearing subcommand is a plain verb
22
- // that just needs the TCP endpoint hydrated before it runs.
23
- const REMOTE_LIFECYCLE = new Set(['setup', 'start', 'stop']);
23
+ // tunnel lifecycle, or daemon-state reporting that must degrade gracefully
24
+ // when no tunnel is recorded). Every other `--host`-bearing subcommand is a
25
+ // plain verb that just needs the TCP endpoint hydrated before it runs.
26
+ const REMOTE_LIFECYCLE = new Set(['setup', 'start', 'stop', 'status', 'reload']);
24
27
  /**
25
28
  * Pure platform gate. The computer subsystem is macOS-only for LOCAL driving
26
29
  * (Accessibility / launchctl). It is NOT blocked off macOS when a remote daemon
@@ -111,8 +114,13 @@ export function registerComputerSubcommands(program) {
111
114
  function registerStatusCommand(program) {
112
115
  program
113
116
  .command('status')
114
- .description('Report install state, daemon state, and Accessibility trust')
115
- .action(async () => {
117
+ .description('Report install state, daemon state, and Accessibility trust — or a remote Windows daemon with --host')
118
+ .option('--host <device>', 'Report the remote Windows daemon (tunnel + liveness) instead of the local helper')
119
+ .action(async (opts) => {
120
+ if (opts.host) {
121
+ await reportRemoteStatus(opts.host);
122
+ return;
123
+ }
116
124
  const socketPath = resolveSocketPath();
117
125
  const installed = fs.existsSync(HELPER_APP_DEST);
118
126
  const socketUp = fs.existsSync(socketPath);
@@ -159,6 +167,108 @@ function registerStatusCommand(program) {
159
167
  }
160
168
  });
161
169
  }
170
+ // status --host: the local checks (app install, launchd socket, policy files)
171
+ // are macOS concepts — a remote Windows daemon is reported from what actually
172
+ // exists for it: the recorded tunnel and a live trust_status probe through it.
173
+ async function reportRemoteStatus(host) {
174
+ console.log(`host: ${host}`);
175
+ const state = readRemoteState(host);
176
+ if (!state) {
177
+ console.log('tunnel: none');
178
+ console.log('daemon: unknown (no tunnel to probe through)');
179
+ console.log('');
180
+ console.log(`Run: agents computer start --host ${host}`);
181
+ process.exit(1);
182
+ }
183
+ console.log(`tunnel: 127.0.0.1:${state.localPort} -> ${state.target} (127.0.0.1:${state.remotePort})`);
184
+ hydrateRemoteEnvFromState(host);
185
+ try {
186
+ const client = openComputerClient();
187
+ try {
188
+ const r = await client.call('trust_status');
189
+ if (r.error) {
190
+ console.error(`error: ${r.error.code}: ${r.error.message}`);
191
+ process.exit(1);
192
+ }
193
+ console.log('daemon: running');
194
+ console.log(`trust: ${r.result?.trusted ? 'granted' : 'denied'} (Windows UIAutomation needs no per-app grant)`);
195
+ if (typeof r.result?.pid === 'number')
196
+ console.log(`pid: ${r.result.pid}`);
197
+ if (typeof r.result?.path === 'string' && r.result.path)
198
+ console.log(`exe: ${r.result.path}`);
199
+ }
200
+ finally {
201
+ await client.close();
202
+ }
203
+ }
204
+ catch (err) {
205
+ console.log('daemon: unreachable');
206
+ console.log(` ${err.message}`);
207
+ console.log('');
208
+ console.log(`Run: agents computer start --host ${host}`);
209
+ process.exit(1);
210
+ }
211
+ }
212
+ // PowerShell to bounce the remote daemon: kill the running exe (tolerating
213
+ // "not running"), then start the LOGON scheduled task that owns its
214
+ // lifecycle. Pure so tests can assert the exact script (mirrors the
215
+ // ssh-tunnel script builders).
216
+ export function buildRestartTaskScript(taskName, exeName) {
217
+ const procName = exeName.replace(/\.exe$/i, '');
218
+ return [
219
+ `$ErrorActionPreference = 'Stop'`,
220
+ `Stop-Process -Name '${procName}' -Force -ErrorAction SilentlyContinue`,
221
+ `Start-ScheduledTask -TaskName '${taskName}'`,
222
+ `Write-Output 'restarted'`,
223
+ ].join('; ');
224
+ }
225
+ // reload --host: the Windows daemon has no policy file to re-read (it
226
+ // enforces no allow-list — see TrustStatus in native/computer-win/Rpc.cs), so
227
+ // reload means bounce the daemon via its scheduled task — the way to pick up
228
+ // a freshly pushed exe — then prove it answers through the recorded tunnel.
229
+ async function reloadRemoteHelper(host) {
230
+ const { target } = await resolveRemoteDevice(host);
231
+ const script = buildRestartTaskScript(REMOTE_TASK_NAME, WIN_HELPER_EXE);
232
+ const res = sshExec(target, `powershell -NoProfile -NonInteractive -EncodedCommand ${encodePowershell(script)}`, { timeoutMs: 60_000 });
233
+ if (res.code !== 0) {
234
+ const msg = (res.stderr || res.stdout || '').trim();
235
+ console.error(`restart failed on ${target}${res.timedOut ? ' (timed out)' : ''}${msg ? `: ${msg}` : ''}`);
236
+ process.exit(1);
237
+ }
238
+ console.log(`task: restarted "${REMOTE_TASK_NAME}" on ${target}`);
239
+ const state = readRemoteState(host);
240
+ if (!state) {
241
+ console.log(`(no tunnel recorded — run \`agents computer start --host ${host}\` to drive it)`);
242
+ return;
243
+ }
244
+ hydrateRemoteEnvFromState(host);
245
+ // The relaunched daemon needs a beat to rebind its port; poll through the
246
+ // tunnel until it answers.
247
+ const deadline = Date.now() + 15_000;
248
+ let lastErr = '';
249
+ while (Date.now() < deadline) {
250
+ try {
251
+ const client = openComputerClient();
252
+ try {
253
+ const r = await client.call('trust_status');
254
+ if (!r.error && r.result) {
255
+ console.log(`reloaded: daemon answering (pid ${r.result.pid ?? '?'})`);
256
+ return;
257
+ }
258
+ lastErr = r.error ? `${r.error.code}: ${r.error.message}` : 'empty result';
259
+ }
260
+ finally {
261
+ await client.close();
262
+ }
263
+ }
264
+ catch (err) {
265
+ lastErr = err.message;
266
+ }
267
+ await sleep(500);
268
+ }
269
+ console.error(`daemon did not answer within 15s after restart${lastErr ? ` (${lastErr})` : ''}`);
270
+ process.exit(1);
271
+ }
162
272
  // run — the embedded observe -> act -> verify agent loop. A reasoning model
163
273
  // (Claude API by default, or any Anthropic-shaped endpoint via --base-url for
164
274
  // Ollama / vLLM / LiteLLM) drives the EXISTING computer verbs as tools over the
@@ -560,8 +670,19 @@ function registerStartCommand(program) {
560
670
  function registerReloadCommand(program) {
561
671
  program
562
672
  .command('reload')
563
- .description('Reload the allow-list policy from ~/.agents/permissions/groups/ (SIGHUP the daemon)')
564
- .action(async () => {
673
+ .description('Reload the allow-list policy (SIGHUP the local daemon) — or restart a remote Windows daemon with --host')
674
+ .option('--host <device>', 'Restart the remote Windows daemon (its scheduled task) instead of SIGHUPing the local one')
675
+ .action(async (opts) => {
676
+ if (opts.host) {
677
+ try {
678
+ await reloadRemoteHelper(opts.host);
679
+ }
680
+ catch (err) {
681
+ console.error(`error: ${err.message}`);
682
+ process.exit(1);
683
+ }
684
+ return;
685
+ }
565
686
  const socketPath = resolveSocketPath();
566
687
  if (!fs.existsSync(socketPath)) {
567
688
  console.error(`daemon not running (no socket at ${socketPath})`);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `agents events` — read the structured audit/event log.
3
3
  *
4
- * The event log (`~/.agents/.cache/logs/events-YYYY-MM-DD.jsonl`) records every
4
+ * The event log (`~/.agents/events.jsonl`) records every
5
5
  * `agents <module> <cmd>` invocation plus richer typed events (secrets access,
6
6
  * version installs, ...), each stamped with who ran it and from where (OS user,
7
7
  * local vs SSH, remote client IP). This command reads it back — the audit trail
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `agents events` — read the structured audit/event log.
3
3
  *
4
- * The event log (`~/.agents/.cache/logs/events-YYYY-MM-DD.jsonl`) records every
4
+ * The event log (`~/.agents/events.jsonl`) records every
5
5
  * `agents <module> <cmd>` invocation plus richer typed events (secrets access,
6
6
  * version installs, ...), each stamped with who ran it and from where (OS user,
7
7
  * local vs SSH, remote client IP). This command reads it back — the audit trail
@@ -125,8 +125,7 @@ function collect(value, previous) {
125
125
  }
126
126
  /** Tail today's event file, printing new lines as they land. */
127
127
  async function followLog() {
128
- const today = new Date();
129
- const file = `${getLogsPath()}/events-${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}.jsonl`;
128
+ const file = getLogsPath();
130
129
  let offset = 0;
131
130
  try {
132
131
  offset = fs.statSync(file).size;
@@ -11,6 +11,7 @@ import { setHelpSections } from '../lib/help.js';
11
11
  import { parseLoopInterval } from '../lib/loop.js';
12
12
  import { AGENTS } from '../lib/agents.js';
13
13
  import { recordDispatchedRun } from '../lib/audit/log.js';
14
+ import { warnUnpushedWork, shouldWarnUnpushed } from '../lib/warn-unpushed.js';
14
15
  import * as fs from 'fs';
15
16
  import * as path from 'path';
16
17
  import * as os from 'os';
@@ -211,7 +212,7 @@ export function registerRunCommand(program) {
211
212
  .option('--secrets-keys <keys>', 'Inject only this comma-separated subset of keys from --secrets bundles (e.g. KEY1,KEY2). Missing keys are an error. Applies to all --secrets bundles on this run.')
212
213
  .option('--allow-expired', 'Inject secrets even if their expiry date has passed (overrides the pre-run expiry abort).')
213
214
  .option('--cwd <dir>', 'Working directory for the agent (defaults to current directory)')
214
- .option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude only, repeatable)', (val, prev) => [...prev, val], [])
215
+ .option('--add-dir <dir>', 'Grant access to an additional directory outside the project (Claude and Codex, repeatable)', (val, prev) => [...prev, val], [])
215
216
  .option('--json', 'Stream events as JSON lines (for parsing by other tools)')
216
217
  .option('--quiet', 'Suppress preamble (rotation banner, "Running:" line). Useful when piping JSON events to a parser.', false)
217
218
  .option('--headless', 'Force headless mode. Auto-enabled when a prompt is provided; pass explicitly to stay headless with no prompt (reads the prompt from stdin).', false)
@@ -245,6 +246,12 @@ export function registerRunCommand(program) {
245
246
  // `--on` and `--computer` are hidden aliases of `--host` — same behavior.
246
247
  runCmd.addOption(new Option('--on <name>', 'Alias of --host.').hideHelp());
247
248
  runCmd.addOption(new Option('--computer <name>', 'Alias of --host.').hideHelp());
249
+ // Required for the documented `agents run <agent> [prompt] -- <native flags>`
250
+ // passthrough: commander >=13 rejects excess operands by default, so any
251
+ // post-`--` token died with "too many arguments" before the action ran. The
252
+ // action re-derives the `--` boundary from rawArgs and still errors on excess
253
+ // operands that are NOT behind `--`.
254
+ runCmd.allowExcessArguments(true);
248
255
  setHelpSections(runCmd, {
249
256
  examples: `
250
257
  # Headless, read-only: investigate or summarize without writing files
@@ -293,10 +300,25 @@ export function registerRunCommand(program) {
293
300
  `,
294
301
  });
295
302
  runCmd.action(async (agentSpec, prompt, options, command) => {
296
- // Capture everything after -- as passthrough args forwarded verbatim to the underlying CLI.
297
- // Use command.args (all positional strings) and strip the declared positional args from the front.
298
- const declaredArgCount = prompt !== undefined ? 2 : 1;
299
- const passthroughArgs = command.args.slice(declaredArgCount);
303
+ // Capture everything after -- as passthrough args forwarded verbatim to the
304
+ // underlying CLI. Commander strips the literal `--` and folds what follows
305
+ // into the positional operands (so `agents run codex -- --yolo` would parse
306
+ // `--yolo` as the PROMPT) — recover the boundary from rawArgs instead of
307
+ // operand counts. Excess operands not behind `--` are still an error (an
308
+ // unquoted prompt must not silently become agent flags).
309
+ const rawArgs = process.argv;
310
+ const separatorIdx = rawArgs.indexOf('--');
311
+ const passthroughArgs = separatorIdx === -1 ? [] : rawArgs.slice(separatorIdx + 1);
312
+ const operandsBeforeSeparator = command.args.length - passthroughArgs.length;
313
+ if (operandsBeforeSeparator > 2) {
314
+ console.error(chalk.red(`Too many arguments for 'run'. Quote the prompt ("fix the bug"), and put agent-native flags after -- (agents run codex -- --yolo).`));
315
+ process.exit(1);
316
+ }
317
+ if (prompt !== undefined && operandsBeforeSeparator < 2) {
318
+ // The token commander assigned to [prompt] came from behind `--` — it is
319
+ // a native flag, not a prompt. Run interactively.
320
+ prompt = undefined;
321
+ }
300
322
  // --lease: invent a disposable cloud box for this run (via crabbox), run
301
323
  // the agent there, then tear it down. Unlike --host, nothing is registered.
302
324
  if (options.lease) {
@@ -305,7 +327,7 @@ export function registerRunCommand(program) {
305
327
  process.exit(1);
306
328
  }
307
329
  const backend = typeof options.lease === 'string' ? options.lease : undefined;
308
- const { detectSignedInRuntimes, pickRuntimes } = await import('../lib/crabbox/runtimes.js');
330
+ const { detectSignedInRuntimes, pickRuntimes, resolveClaudeCredentialsBlob } = await import('../lib/crabbox/runtimes.js');
309
331
  const { leaseAndRun } = await import('../lib/crabbox/lease.js');
310
332
  const { confirm } = await import('@inquirer/prompts');
311
333
  const detected = await detectSignedInRuntimes();
@@ -322,14 +344,28 @@ export function registerRunCommand(program) {
322
344
  return `${d?.label ?? id}${d?.email ? ` (${d.email})` : ''}`;
323
345
  })
324
346
  .join(', ');
347
+ // Claude ships its OAuth token (not just the .claude.json config) — name it
348
+ // explicitly so the consent covers the actual credential transferred.
349
+ const whatShips = runtimes.includes('claude') ? 'credentials + Claude OAuth token' : 'credentials';
325
350
  const ok = await confirm({
326
- message: `Copy credentials for ${names} to a disposable cloud box, run there, then destroy it?`,
351
+ message: `Copy ${whatShips} for ${names} to a disposable cloud box, run there, then destroy it?`,
327
352
  default: false,
328
353
  });
329
354
  if (!ok) {
330
355
  console.error(chalk.yellow('Aborted — no credentials pushed, no box leased.'));
331
356
  process.exit(1);
332
357
  }
358
+ // Read the Claude OAuth token from the local Keychain (silent) so it can be
359
+ // written to ~/.claude/.credentials.json on the box — otherwise Claude boots
360
+ // "Not logged in". Consent above already covered this transfer.
361
+ let claudeCredentialsJson = null;
362
+ if (runtimes.includes('claude')) {
363
+ const claudeEmail = detected.find((d) => d.id === 'claude')?.email ?? null;
364
+ claudeCredentialsJson = await resolveClaudeCredentialsBlob({ preferEmail: claudeEmail });
365
+ if (!claudeCredentialsJson) {
366
+ console.error(chalk.yellow('Warning: could not read the local Claude OAuth token — the box may come up "Not logged in".'));
367
+ }
368
+ }
333
369
  try {
334
370
  const { exitCode, box, toreDown } = await leaseAndRun({
335
371
  agent: agentSpec.split('@')[0],
@@ -339,10 +375,11 @@ export function registerRunCommand(program) {
339
375
  backend,
340
376
  runtimes,
341
377
  detected,
378
+ claudeCredentialsJson,
342
379
  secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE,
343
380
  keep: options.keepBox,
344
381
  });
345
- console.error(chalk.gray(toreDown ? `Box ${box.slug} destroyed.` : `Box ${box.slug} kept${box.ip ? ` (${box.ip})` : ''}. Stop it: crabbox stop --id ${box.slug}`));
382
+ console.error(chalk.gray(toreDown ? `Box ${box.slug} destroyed.` : `Box ${box.slug} kept${box.ip ? ` (${box.ip})` : ''}. Stop it: crabbox stop ${box.slug}`));
346
383
  process.exit(exitCode === null ? 1 : exitCode);
347
384
  }
348
385
  catch (err) {
@@ -568,6 +605,9 @@ export function registerRunCommand(program) {
568
605
  cwd: resumeExec.cwd ?? process.cwd(),
569
606
  exitCode: resumeExit,
570
607
  });
608
+ // A resumed loop is always headless; surface any commits it left unpushed.
609
+ if (shouldWarnUnpushed(resumeExec.mode ?? 'auto', false))
610
+ await warnUnpushedWork(resumeExec.cwd ?? process.cwd());
571
611
  process.exit(resumeExit);
572
612
  }
573
613
  const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle, assertRemoteBundleFlagsUnsupported }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, rotationFailoverChain, shouldArmRotationFailover, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias, ensureAgentRunnable }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents, pruneStaleWorkflowSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
@@ -1279,6 +1319,9 @@ export function registerRunCommand(program) {
1279
1319
  // Governance chokepoint (#347): the --acp path exits here, bypassing
1280
1320
  // the normal finalize below — record its one audit entry.
1281
1321
  recordDispatchedRun({ agent, version: defaultVersion ?? 'unknown', mode, cwd, exitCode });
1322
+ // ACP headless run always has a prompt; surface any unpushed commits.
1323
+ if (shouldWarnUnpushed(mode, false))
1324
+ await warnUnpushedWork(cwd);
1282
1325
  process.exit(exitCode);
1283
1326
  }
1284
1327
  catch (err) {
@@ -1435,6 +1478,9 @@ export function registerRunCommand(program) {
1435
1478
  // the normal finalize below — record its one audit entry.
1436
1479
  const loopExit = loopExitCode(result.stoppedBy);
1437
1480
  recordDispatchedRun({ agent, version: defaultVersion ?? 'unknown', mode, cwd, exitCode: loopExit });
1481
+ // A loop is always headless; surface any commits it left unpushed.
1482
+ if (shouldWarnUnpushed(mode, false))
1483
+ await warnUnpushedWork(cwd);
1438
1484
  process.exit(loopExit);
1439
1485
  }
1440
1486
  catch (err) {
@@ -1462,6 +1508,13 @@ export function registerRunCommand(program) {
1462
1508
  }
1463
1509
  cleanupWorkflowMcpConfig();
1464
1510
  cleanupWorkflowSubagents();
1511
+ // Surface committed-but-unpushed work a headless writable run left
1512
+ // behind, so it isn't silently stranded in a worktree. Advisory only,
1513
+ // never throws; skipped for interactive runs (the human sees the shell)
1514
+ // and read-only plan mode (can't commit).
1515
+ if (shouldWarnUnpushed(mode, resolveInteractive(execOptions))) {
1516
+ await warnUnpushedWork(cwd);
1517
+ }
1465
1518
  // Governance chokepoint (#347): every dispatched run finalizes here.
1466
1519
  // ONE tamper-evident audit record per run — non-fatal by contract.
1467
1520
  recordDispatchedRun({ agent: ranAgent, version: ranVersion ?? 'unknown', mode, cwd, exitCode });
@@ -1470,6 +1523,11 @@ export function registerRunCommand(program) {
1470
1523
  catch (err) {
1471
1524
  cleanupWorkflowMcpConfig();
1472
1525
  cleanupWorkflowSubagents();
1526
+ // An agent that committed then crashed is the case where stranded work
1527
+ // matters most — warn before exiting the error path too.
1528
+ if (shouldWarnUnpushed(mode, resolveInteractive(execOptions))) {
1529
+ await warnUnpushedWork(cwd);
1530
+ }
1473
1531
  console.error(chalk.red(`Failed to execute ${agent}: ${err.message}`));
1474
1532
  process.exit(1);
1475
1533
  }
@@ -1,13 +1,11 @@
1
1
  import chalk from 'chalk';
2
+ import { die } from '../lib/format.js';
2
3
  import * as fs from 'fs';
3
4
  import * as path from 'path';
4
5
  import { homedir } from 'os';
5
6
  import { betaEnableHint, isBetaEnabled } from '../lib/beta.js';
6
7
  import { insertTask } from '../lib/cloud/store.js';
7
- function die(msg, code = 1) {
8
- console.error(chalk.red(msg));
9
- process.exit(code);
10
- }
8
+ import { emit } from '../lib/events.js';
11
9
  function requireFactoryUrl() {
12
10
  const url = process.env.FACTORY_FLOOR_URL;
13
11
  if (!url) {
@@ -82,6 +80,13 @@ Examples:
82
80
  createdAt: now,
83
81
  updatedAt: now,
84
82
  });
83
+ emit('cloud.dispatch', {
84
+ module: 'factory',
85
+ taskId: result.cloud_execution_id,
86
+ ref,
87
+ linear_identifier: result.linear_identifier,
88
+ status: 'queued',
89
+ });
85
90
  console.log(chalk.green(`Submitted ${result.linear_identifier} (${result.label})`));
86
91
  console.log(` ticket ${result.ticket_id}`);
87
92
  console.log(` execution ${result.cloud_execution_id}`);
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `agents feed` -- list open blocks (decisions agents are waiting on).
3
+ *
4
+ * Aggregates block records from the local feed store and, with --host, from
5
+ * reachable remote hosts via SSH passthrough. Each block carries enough
6
+ * identity for `agents message` to route a reply back to the right agent.
7
+ */
8
+ import type { Command } from 'commander';
9
+ export declare function registerFeedCommand(program: Command): void;
@@ -0,0 +1,69 @@
1
+ import chalk from 'chalk';
2
+ import { ensureFeedPublishHook, listBlocks } from '../lib/feed.js';
3
+ import { machineId } from '../lib/machine-id.js';
4
+ import { relTime } from '../lib/format.js';
5
+ function renderBlock(b, localHost) {
6
+ const host = b.host !== localHost ? chalk.yellow(` [${b.host}]`) : '';
7
+ const runtime = chalk.gray(b.runtime);
8
+ const age = chalk.gray(relTime(b.ts));
9
+ console.log(`${chalk.cyan(b.mailboxId)}${host} ${runtime} ${age}`);
10
+ for (const question of b.questions) {
11
+ const header = question.header ? chalk.gray(`[${question.header}] `) : '';
12
+ console.log(` ${header}${question.text}`);
13
+ if (question.options?.length) {
14
+ for (let i = 0; i < question.options.length; i++) {
15
+ const o = question.options[i];
16
+ const desc = o.description ? chalk.gray(` -- ${o.description}`) : '';
17
+ console.log(` ${chalk.dim(`${i + 1}.`)} ${o.label}${desc}`);
18
+ }
19
+ }
20
+ }
21
+ if (b.ticket || b.pr) {
22
+ const meta = [b.ticket, b.pr].filter(Boolean).join(' ');
23
+ console.log(` ${chalk.gray(meta)}`);
24
+ }
25
+ console.log(` ${chalk.dim('reply:')} agents message ${b.mailboxId} "<answer>"`);
26
+ console.log();
27
+ }
28
+ export function registerFeedCommand(program) {
29
+ program
30
+ .command('feed')
31
+ .description('List open blocks -- decisions agents are waiting on')
32
+ .option('--json', 'Output as JSON')
33
+ .action(async (opts) => {
34
+ const setupWarnings = [];
35
+ const hookInstall = ensureFeedPublishHook();
36
+ if (hookInstall.error) {
37
+ setupWarnings.push(hookInstall.error);
38
+ }
39
+ else {
40
+ const [{ iterHooksCapableVersions, parseHookManifest, registerHooksToSettings }, { getVersionHomePath }] = await Promise.all([
41
+ import('../lib/hooks.js'),
42
+ import('../lib/versions.js'),
43
+ ]);
44
+ const manifest = parseHookManifest({ warn: false });
45
+ for (const { agent, version } of iterHooksCapableVersions({ agent: 'claude' })) {
46
+ const result = registerHooksToSettings(agent, getVersionHomePath(agent, version), manifest);
47
+ if (result.errors.length > 0) {
48
+ setupWarnings.push(`${agent}@${version}: ${result.errors.join('; ')}`);
49
+ }
50
+ }
51
+ }
52
+ const blocks = listBlocks();
53
+ for (const warning of setupWarnings) {
54
+ console.error(chalk.yellow(`Feed hook setup warning: ${warning}`));
55
+ }
56
+ if (opts.json) {
57
+ console.log(JSON.stringify(blocks, null, 2));
58
+ return;
59
+ }
60
+ if (blocks.length === 0) {
61
+ console.log(chalk.gray('No open blocks.'));
62
+ return;
63
+ }
64
+ const local = machineId();
65
+ console.log(chalk.bold(`${blocks.length} open block${blocks.length === 1 ? '' : 's'}:\n`));
66
+ for (const b of blocks)
67
+ renderBlock(b, local);
68
+ });
69
+ }
@@ -17,6 +17,7 @@ import * as fs from 'fs';
17
17
  import * as os from 'os';
18
18
  import * as path from 'path';
19
19
  import chalk from 'chalk';
20
+ import { truncate, termLink } from '../lib/format.js';
20
21
  import * as yaml from 'yaml';
21
22
  import { AGENTS, getCliState, resolveAgentName } from '../lib/agents.js';
22
23
  import { supports } from '../lib/capabilities.js';
@@ -1216,12 +1217,6 @@ function readFirstProseLine(p) {
1216
1217
  return '';
1217
1218
  }
1218
1219
  // ─── OSC-8 + path helpers ────────────────────────────────────────────────────
1219
- function termLink(text, filePath) {
1220
- if (!filePath || !process.stdout.isTTY)
1221
- return text;
1222
- const url = `file://${filePath}`;
1223
- return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
1224
- }
1225
1220
  function linkTarget(p) {
1226
1221
  if (!p)
1227
1222
  return '';
@@ -1271,8 +1266,3 @@ function safeCountSessions(agent) {
1271
1266
  return 0;
1272
1267
  }
1273
1268
  }
1274
- function truncate(s, n) {
1275
- if (s.length <= n)
1276
- return s;
1277
- return s.slice(0, n - 1) + '…';
1278
- }
@@ -1,10 +1,14 @@
1
1
  /**
2
- * `agents logs` — unified, discoverable run-log viewer.
2
+ * `agents logs` — unified, discoverable run-log viewer + audit trail.
3
3
  *
4
4
  * Resolves a run across two substrates and shows (or `-f` follows) its log:
5
5
  * - host-dispatch tasks (`agents run --host`) → combined-stdout log, offset-tailed
6
6
  * - sessions (the local index) → transcript, tailed via the sessions tailer
7
7
  *
8
+ * Subcommands:
9
+ * - `agents logs audit` — read the structured audit/event log
10
+ * - `agents logs stats` — show aggregate audit statistics
11
+ *
8
12
  * Concise by default: a bare `agents logs <id>` prints the same summary digest as
9
13
  * `agents sessions <id>` — cheap for an agent to glance at. The token-heavy full
10
14
  * transcript / raw stdout is opt-in behind `--full` (alias `-m/--markdown`).