@phnx-labs/agents-cli 1.20.51 → 1.20.53

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 (142) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +12 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/browser.js +215 -7
  5. package/dist/commands/cloud.d.ts +3 -0
  6. package/dist/commands/cloud.js +8 -1
  7. package/dist/commands/events.d.ts +1 -1
  8. package/dist/commands/events.js +2 -3
  9. package/dist/commands/exec.js +82 -3
  10. package/dist/commands/factory.js +8 -0
  11. package/dist/commands/feed.d.ts +38 -0
  12. package/dist/commands/feed.js +274 -0
  13. package/dist/commands/logs.d.ts +5 -1
  14. package/dist/commands/logs.js +248 -3
  15. package/dist/commands/mcp.js +7 -0
  16. package/dist/commands/memory.d.ts +9 -0
  17. package/dist/commands/memory.js +164 -0
  18. package/dist/commands/message.d.ts +11 -6
  19. package/dist/commands/message.js +140 -5
  20. package/dist/commands/routines.js +12 -0
  21. package/dist/commands/secrets-migrate.d.ts +2 -1
  22. package/dist/commands/secrets-migrate.js +88 -13
  23. package/dist/commands/secrets.d.ts +22 -0
  24. package/dist/commands/secrets.js +176 -42
  25. package/dist/commands/sessions.js +10 -1
  26. package/dist/commands/teams.js +4 -0
  27. package/dist/commands/worktree.js +4 -2
  28. package/dist/index.js +21 -22
  29. package/dist/lib/agents.js +249 -17
  30. package/dist/lib/answer-router.d.ts +75 -0
  31. package/dist/lib/answer-router.js +149 -0
  32. package/dist/lib/ask-classifier.d.ts +71 -0
  33. package/dist/lib/ask-classifier.js +197 -0
  34. package/dist/lib/browser/login-detection.d.ts +94 -0
  35. package/dist/lib/browser/login-detection.js +274 -0
  36. package/dist/lib/browser/profiles.d.ts +17 -8
  37. package/dist/lib/browser/profiles.js +27 -8
  38. package/dist/lib/browser/secret-ref.d.ts +10 -0
  39. package/dist/lib/browser/secret-ref.js +14 -0
  40. package/dist/lib/browser/service.js +14 -12
  41. package/dist/lib/cloud/antigravity.d.ts +0 -2
  42. package/dist/lib/cloud/antigravity.js +2 -17
  43. package/dist/lib/cloud/codex.js +3 -18
  44. package/dist/lib/cloud/rush.d.ts +15 -0
  45. package/dist/lib/cloud/rush.js +10 -16
  46. package/dist/lib/cloud/stream.js +2 -0
  47. package/dist/lib/cloud/types.d.ts +21 -0
  48. package/dist/lib/cloud/types.js +81 -0
  49. package/dist/lib/crabbox/cli.d.ts +1 -1
  50. package/dist/lib/crabbox/cli.js +12 -2
  51. package/dist/lib/crabbox/lease.d.ts +19 -0
  52. package/dist/lib/crabbox/lease.js +22 -11
  53. package/dist/lib/crabbox/progress.d.ts +62 -0
  54. package/dist/lib/crabbox/progress.js +129 -0
  55. package/dist/lib/crabbox/runtimes.d.ts +38 -1
  56. package/dist/lib/crabbox/runtimes.js +98 -5
  57. package/dist/lib/daemon.d.ts +12 -9
  58. package/dist/lib/daemon.js +32 -17
  59. package/dist/lib/events.d.ts +31 -5
  60. package/dist/lib/events.js +291 -101
  61. package/dist/lib/exec.js +20 -1
  62. package/dist/lib/feed-outcome.d.ts +101 -0
  63. package/dist/lib/feed-outcome.js +244 -0
  64. package/dist/lib/feed-policy.d.ts +30 -0
  65. package/dist/lib/feed-policy.js +133 -0
  66. package/dist/lib/feed.d.ts +180 -0
  67. package/dist/lib/feed.js +627 -0
  68. package/dist/lib/git.d.ts +17 -1
  69. package/dist/lib/git.js +20 -1
  70. package/dist/lib/hooks.js +529 -14
  71. package/dist/lib/hosts/passthrough.d.ts +3 -3
  72. package/dist/lib/hosts/passthrough.js +3 -3
  73. package/dist/lib/mailbox-gc.d.ts +22 -0
  74. package/dist/lib/mailbox-gc.js +161 -0
  75. package/dist/lib/mailbox.d.ts +26 -2
  76. package/dist/lib/mailbox.js +80 -5
  77. package/dist/lib/mcp.js +82 -0
  78. package/dist/lib/memory.d.ts +55 -0
  79. package/dist/lib/memory.js +274 -0
  80. package/dist/lib/notify.d.ts +16 -0
  81. package/dist/lib/notify.js +61 -0
  82. package/dist/lib/operator.d.ts +26 -0
  83. package/dist/lib/operator.js +107 -0
  84. package/dist/lib/plugins.d.ts +35 -0
  85. package/dist/lib/plugins.js +217 -0
  86. package/dist/lib/remote-agents-json.d.ts +14 -0
  87. package/dist/lib/remote-agents-json.js +94 -0
  88. package/dist/lib/resources/mcp.js +44 -0
  89. package/dist/lib/resources/memory.d.ts +15 -0
  90. package/dist/lib/resources/memory.js +46 -0
  91. package/dist/lib/resources/types.d.ts +2 -2
  92. package/dist/lib/rotate.js +2 -0
  93. package/dist/lib/runner.d.ts +43 -0
  94. package/dist/lib/runner.js +323 -74
  95. package/dist/lib/sandbox.js +6 -0
  96. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  97. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  98. package/dist/lib/secrets/agent.d.ts +21 -0
  99. package/dist/lib/secrets/agent.js +63 -1
  100. package/dist/lib/secrets/bundles.d.ts +33 -1
  101. package/dist/lib/secrets/bundles.js +76 -21
  102. package/dist/lib/secrets/icloud-import.d.ts +79 -0
  103. package/dist/lib/secrets/icloud-import.js +203 -0
  104. package/dist/lib/secrets/index.d.ts +142 -2
  105. package/dist/lib/secrets/index.js +695 -30
  106. package/dist/lib/secrets/remote.js +1 -1
  107. package/dist/lib/secrets/sync.js +1 -1
  108. package/dist/lib/session/active.d.ts +18 -0
  109. package/dist/lib/session/active.js +47 -17
  110. package/dist/lib/session/db.d.ts +9 -1
  111. package/dist/lib/session/db.js +18 -3
  112. package/dist/lib/session/discover.d.ts +13 -0
  113. package/dist/lib/session/discover.js +32 -2
  114. package/dist/lib/session/parse.d.ts +8 -0
  115. package/dist/lib/session/parse.js +42 -21
  116. package/dist/lib/session/remote-active.js +8 -89
  117. package/dist/lib/session/state.d.ts +11 -0
  118. package/dist/lib/session/state.js +50 -1
  119. package/dist/lib/session/tail.d.ts +23 -4
  120. package/dist/lib/session/tail.js +34 -16
  121. package/dist/lib/session/throughput.d.ts +30 -0
  122. package/dist/lib/session/throughput.js +86 -0
  123. package/dist/lib/shim-heal.d.ts +12 -3
  124. package/dist/lib/shim-heal.js +12 -6
  125. package/dist/lib/staleness/detectors/subagents.js +57 -3
  126. package/dist/lib/staleness/writers/hooks.js +7 -3
  127. package/dist/lib/staleness/writers/subagents.js +37 -6
  128. package/dist/lib/startup/command-registry.d.ts +2 -0
  129. package/dist/lib/startup/command-registry.js +4 -0
  130. package/dist/lib/state.d.ts +2 -0
  131. package/dist/lib/state.js +25 -8
  132. package/dist/lib/subagents.d.ts +52 -0
  133. package/dist/lib/subagents.js +315 -12
  134. package/dist/lib/teams/agents.js +6 -3
  135. package/dist/lib/teams/worktree.d.ts +8 -0
  136. package/dist/lib/teams/worktree.js +8 -0
  137. package/dist/lib/types.d.ts +20 -2
  138. package/dist/lib/versions.js +38 -48
  139. package/dist/lib/whats-new.d.ts +5 -3
  140. package/dist/lib/whats-new.js +25 -5
  141. package/package.json +4 -3
  142. package/scripts/postinstall.js +61 -1
@@ -5,6 +5,87 @@
5
5
  * Factory) implement, plus the shared task and event types that flow through
6
6
  * the dispatch pipeline.
7
7
  */
8
+ /**
9
+ * Normalize a provider's raw wire status into the canonical `CloudTaskStatus`.
10
+ *
11
+ * Each cloud backend speaks its own status vocabulary and had its own copy of
12
+ * this mapping, which had drifted. This dispatches per provider while keeping
13
+ * provider-specific defaults explicit:
14
+ * - `rush` — switch over the Factory Floor's known strings; includes
15
+ * `allocating` and stopped/resumable `idle` states, has no
16
+ * `queued`, default `running`.
17
+ * - `codex` — substring match on the lowercased CLI status; default
18
+ * `running`.
19
+ * - `antigravity` — substring match on the (possibly `undefined`) Interactions
20
+ * API status; default `completed` (its synchronous response
21
+ * is terminal), and `undefined`-safe.
22
+ *
23
+ * Factory's `mapResultStatus` is structurally different (it maps a droid *exit*
24
+ * result, not a lifecycle string) and deliberately stays in `factory.ts`.
25
+ */
26
+ export function normalizeProviderStatus(provider, wireStatus) {
27
+ switch (provider) {
28
+ case 'rush':
29
+ return normalizeRushStatus(wireStatus ?? '');
30
+ case 'codex':
31
+ return normalizeCodexStatus(wireStatus ?? '');
32
+ case 'antigravity':
33
+ return normalizeAntigravityStatus(wireStatus);
34
+ }
35
+ }
36
+ /** Rush Factory Floor status → canonical enum. Default `running`; no `queued`. */
37
+ function normalizeRushStatus(s) {
38
+ switch (s) {
39
+ case 'allocating': return 'allocating';
40
+ case 'running': return 'running';
41
+ case 'idle':
42
+ case 'paused':
43
+ case 'needs_review': return 'idle';
44
+ case 'input_required': return 'input_required';
45
+ case 'completed': return 'completed';
46
+ case 'failed': return 'failed';
47
+ case 'cancelled': return 'cancelled';
48
+ default: return 'running';
49
+ }
50
+ }
51
+ /** Codex Cloud CLI status → canonical enum. Substring match; default `running`. */
52
+ function normalizeCodexStatus(s) {
53
+ const lower = s.toLowerCase();
54
+ if (lower.includes('queued') || lower.includes('pending'))
55
+ return 'queued';
56
+ if (lower.includes('running') || lower.includes('in_progress'))
57
+ return 'running';
58
+ if (lower.includes('idle') || lower.includes('paused') || lower.includes('needs_review'))
59
+ return 'idle';
60
+ if (lower.includes('completed') || lower.includes('succeeded') || lower.includes('success'))
61
+ return 'completed';
62
+ if (lower.includes('failed') || lower.includes('error'))
63
+ return 'failed';
64
+ if (lower.includes('cancelled') || lower.includes('canceled'))
65
+ return 'cancelled';
66
+ return 'running';
67
+ }
68
+ /**
69
+ * Antigravity Interactions API status → canonical enum. Substring match,
70
+ * `undefined`-safe; default `completed` because the synchronous response is
71
+ * already terminal.
72
+ */
73
+ function normalizeAntigravityStatus(s) {
74
+ const lower = (s ?? '').toLowerCase();
75
+ if (lower.includes('queue') || lower.includes('pending'))
76
+ return 'queued';
77
+ if (lower.includes('run') || lower.includes('progress'))
78
+ return 'running';
79
+ if (lower.includes('idle') || lower.includes('paused') || lower.includes('needs_review'))
80
+ return 'idle';
81
+ if (lower.includes('complete') || lower.includes('success'))
82
+ return 'completed';
83
+ if (lower.includes('fail') || lower.includes('error'))
84
+ return 'failed';
85
+ if (lower.includes('cancel'))
86
+ return 'cancelled';
87
+ return 'completed';
88
+ }
8
89
  /** Maximum images allowed per dispatch (matches Cursor Background Agents). */
9
90
  export const MAX_IMAGES_PER_DISPATCH = 5;
10
91
  /**
@@ -62,7 +62,7 @@ export interface WarmupOptions extends CrabboxOptions {
62
62
  * created even if warmup's stdout format changes — the new lease id is the one
63
63
  * that wasn't present before.
64
64
  */
65
- export declare function crabboxWarmup(opts?: WarmupOptions): CrabboxBox;
65
+ export declare function crabboxWarmup(opts?: WarmupOptions): Promise<CrabboxBox>;
66
66
  /**
67
67
  * Poll until the box reports ready, or throw after timeoutMs.
68
68
  * `sleep` is injectable so tests don't wall-clock wait.
@@ -87,7 +87,7 @@ export function crabboxFind(slug, opts = {}) {
87
87
  * created even if warmup's stdout format changes — the new lease id is the one
88
88
  * that wasn't present before.
89
89
  */
90
- export function crabboxWarmup(opts = {}) {
90
+ export async function crabboxWarmup(opts = {}) {
91
91
  findCrabbox();
92
92
  const env = crabboxEnv(opts);
93
93
  const before = new Set(crabboxList(opts).map((b) => b.lease));
@@ -100,7 +100,17 @@ export function crabboxWarmup(opts = {}) {
100
100
  args.push('--provider', opts.provider);
101
101
  if (opts.code)
102
102
  args.push('--code');
103
- const r = spawnSync('crabbox', args, { encoding: 'utf-8', env, stdio: ['ignore', 'pipe', 'pipe'] });
103
+ // Async spawn (not spawnSync): provisioning takes 30-90s and a blocking call
104
+ // would freeze any caller's progress spinner. Output is captured, not streamed.
105
+ const r = await new Promise((resolve) => {
106
+ const proc = spawn('crabbox', args, { env, stdio: ['ignore', 'pipe', 'pipe'] });
107
+ let stdout = '';
108
+ let stderr = '';
109
+ proc.stdout.on('data', (c) => (stdout += c.toString('utf-8')));
110
+ proc.stderr.on('data', (c) => (stderr += c.toString('utf-8')));
111
+ proc.on('error', () => resolve({ status: null, stdout, stderr }));
112
+ proc.on('close', (code) => resolve({ status: code, stdout, stderr }));
113
+ });
104
114
  if (r.status !== 0) {
105
115
  const detail = (r.stderr || r.stdout || '').trim();
106
116
  throw new Error(`crabbox warmup failed: ${detail || 'unknown error'}. ` +
@@ -9,6 +9,17 @@
9
9
  import type { AgentId } from '../types.js';
10
10
  import { type CrabboxBox } from './cli.js';
11
11
  import { type DetectedRuntime } from './runtimes.js';
12
+ /** Phase signal for a lease run, so the command layer can drive a progress UI. */
13
+ export type LeasePhase = {
14
+ kind: 'warmup';
15
+ backend?: string;
16
+ } | {
17
+ kind: 'ready';
18
+ box: CrabboxBox;
19
+ elapsedMs: number;
20
+ } | {
21
+ kind: 'teardown';
22
+ };
12
23
  export interface LeaseRunOptions {
13
24
  agent: string;
14
25
  prompt: string;
@@ -24,8 +35,16 @@ export interface LeaseRunOptions {
24
35
  /** Secrets bundle providing crabbox's provider token. */
25
36
  secretsBundle?: string;
26
37
  onData?: (s: string) => void;
38
+ /** Progress phases (warmup → ready → teardown) for a command-layer spinner. */
39
+ onPhase?: (phase: LeasePhase) => void;
27
40
  /** Keep the box after the run instead of stopping it. */
28
41
  keep?: boolean;
42
+ /**
43
+ * Raw wrapped Claude OAuth payload (from `resolveClaudeCredentialsBlob`), written
44
+ * to `~/.claude/.credentials.json` on the box. The command layer resolves it
45
+ * (after consent) so this module stays free of Keychain I/O and unit-testable.
46
+ */
47
+ claudeCredentialsJson?: string | null;
29
48
  }
30
49
  export interface LeaseRunResult {
31
50
  box: CrabboxBox;
@@ -7,7 +7,8 @@
7
7
  * `--script-stdin` body so the token contents never touch argv.
8
8
  */
9
9
  import { crabboxWarmup, crabboxWaitReady, crabboxRunScript, crabboxStop } from './cli.js';
10
- import { buildCredentialScript } from './runtimes.js';
10
+ import { buildCredentialScript, CLAUDE_TOKEN_REMOTE } from './runtimes.js';
11
+ import { LEASE_AGENT_MARKER } from './progress.js';
11
12
  /** POSIX single-quote for safe embedding in the generated bootstrap script. */
12
13
  function q(s) {
13
14
  return "'" + s.replace(/'/g, "'\\''") + "'";
@@ -46,26 +47,31 @@ const ENSURE_AGENTS_CLI = [
46
47
  * the credential files. Best-effort install steps never abort the run.
47
48
  */
48
49
  export function buildBootstrapScript(opts) {
49
- const credScript = buildCredentialScript(opts.runtimes, opts.detected);
50
+ const credScript = buildCredentialScript(opts.runtimes, opts.detected, {
51
+ claudeCredentialsJson: opts.claudeCredentialsJson,
52
+ });
50
53
  const runParts = ['agents', 'run', q(opts.agent), q(opts.prompt), '--quiet'];
51
54
  if (opts.mode)
52
55
  runParts.push('--mode', q(opts.mode));
53
56
  if (opts.model)
54
57
  runParts.push('--model', q(opts.model));
55
58
  // Credential files to shred after the run (home-level paths written above).
56
- const shred = opts.runtimes
57
- .map((id) => {
58
- const cred = { claude: '.claude.json', codex: '.codex/auth.json', gemini: '.gemini/google_accounts.json', grok: '.grok/auth.json' }[id];
59
- return cred ? `rm -f "$HOME/${cred}" 2>/dev/null || true` : '';
60
- })
61
- .filter(Boolean)
62
- .join('\n');
59
+ // Runs regardless of --keep-box (it's in the box body, not teardown), so a kept
60
+ // box still loses the token after the run — minimizing the credential window.
61
+ const shredPaths = opts.runtimes.flatMap((id) => {
62
+ const paths = { claude: ['.claude.json', CLAUDE_TOKEN_REMOTE], codex: ['.codex/auth.json'], gemini: ['.gemini/google_accounts.json'], grok: ['.grok/auth.json'] }[id];
63
+ return paths ?? [];
64
+ });
65
+ const shred = shredPaths.map((p) => `rm -f "$HOME/${p}" 2>/dev/null || true`).join('\n');
63
66
  const installRuntimes = opts.runtimes.map((id) => `agents add ${q(id)} >/dev/null 2>&1 || true`).join('\n');
64
67
  return [
65
68
  'set -uo pipefail',
66
69
  ENSURE_AGENTS_CLI,
67
70
  installRuntimes,
68
71
  credScript,
72
+ // Marker on its own line: the command layer shows everything before this as
73
+ // setup progress and everything after (the agent's output) verbatim.
74
+ `echo ${q(LEASE_AGENT_MARKER)}`,
69
75
  `${runParts.join(' ')}`,
70
76
  'rc=$?',
71
77
  shred,
@@ -75,13 +81,16 @@ export function buildBootstrapScript(opts) {
75
81
  .join('\n');
76
82
  }
77
83
  export async function leaseAndRun(opts) {
78
- const box = crabboxWarmup({
84
+ const startedAt = Date.now();
85
+ opts.onPhase?.({ kind: 'warmup', backend: opts.backend });
86
+ const box = await crabboxWarmup({
79
87
  class: opts.boxClass,
80
88
  profile: opts.profile,
81
89
  provider: opts.backend,
82
90
  secretsBundle: opts.secretsBundle,
83
91
  });
84
92
  await crabboxWaitReady(box.slug, { secretsBundle: opts.secretsBundle });
93
+ opts.onPhase?.({ kind: 'ready', box, elapsedMs: Date.now() - startedAt });
85
94
  const script = buildBootstrapScript(opts);
86
95
  let exitCode = null;
87
96
  let toreDown = false;
@@ -94,8 +103,10 @@ export async function leaseAndRun(opts) {
94
103
  finally {
95
104
  // Always attempt teardown (bounds credential lifetime to the run) unless the
96
105
  // caller explicitly asked to keep the box.
97
- if (!opts.keep)
106
+ if (!opts.keep) {
107
+ opts.onPhase?.({ kind: 'teardown' });
98
108
  toreDown = crabboxStop(box.slug, { secretsBundle: opts.secretsBundle });
109
+ }
99
110
  }
100
111
  return { box, exitCode, toreDown };
101
112
  }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Progress routing for `agents run --lease`.
3
+ *
4
+ * The box-side bootstrap emits `LEASE_AGENT_MARKER` on its own line right before
5
+ * `agents run`. Everything the crabbox run streams BEFORE that marker is setup
6
+ * noise (sync bytes, node/agents install, `agents setup`) — shown as spinner
7
+ * text and captured for a failure dump. Everything AFTER is the agent's own
8
+ * output — printed through verbatim. The marker line itself is swallowed.
9
+ */
10
+ /** Sentinel echoed on the box right before `agents run`. Distinctive + collision-proof. */
11
+ export declare const LEASE_AGENT_MARKER = "___AGENTS_LEASE_AGENT_OUTPUT_b1f4c2___";
12
+ export interface Spinner {
13
+ /** Begin animating a phase (renders `frame text` on one line, TTY only). */
14
+ start(text: string): void;
15
+ /** Change the text shown; rendered on the next throttled tick (no write itself). */
16
+ update(text: string): void;
17
+ /** Finalize the current line with a symbol (e.g. ✔) and a newline. */
18
+ stopAndPersist(symbol: string, text: string): void;
19
+ /** Clear the current animated line without persisting anything. */
20
+ stop(): void;
21
+ /** True while a phase is active. */
22
+ readonly active: boolean;
23
+ }
24
+ /**
25
+ * A deliberately minimal, self-throttled spinner. Unlike `ora`, it does NOT hook
26
+ * the stream's `write` to re-render on external output — it writes exactly one
27
+ * short line per fixed tick and nowhere else, so it is structurally incapable of
28
+ * a re-render feedback loop (the failure mode that made `ora` blow up when a
29
+ * lease streamed output past a live spinner). On a non-TTY it prints each phase
30
+ * label once and stays silent on `update`, so piped/CI output never floods.
31
+ *
32
+ * Only ever run ONE phase at a time, and never stream other output to the same
33
+ * stream while a phase is active — stop it first.
34
+ */
35
+ export declare function createSpinner(opts?: {
36
+ stream?: {
37
+ write(s: string): unknown;
38
+ isTTY?: boolean;
39
+ };
40
+ enabled?: boolean;
41
+ intervalMs?: number;
42
+ }): Spinner;
43
+ export interface LeaseOutputRouter {
44
+ /** Feed a raw chunk of the crabbox run's combined stdout/stderr. */
45
+ push(chunk: string): void;
46
+ /** Flush any buffered partial line (call once the stream closes). */
47
+ end(): void;
48
+ /** True once the agent-output marker has been seen. */
49
+ sawAgent(): boolean;
50
+ /** The setup lines seen so far (for a failure dump). */
51
+ setupLines(): string[];
52
+ }
53
+ /**
54
+ * Split the crabbox run stream at `LEASE_AGENT_MARKER`. `onSetupLine` fires for
55
+ * each complete non-empty line before the marker; `onAgentChunk` fires with raw
56
+ * text after it (streamed promptly, not line-buffered, so agent output is live).
57
+ */
58
+ export declare function createLeaseOutputRouter(cb: {
59
+ onSetupLine: (line: string) => void;
60
+ onAgentChunk: (chunk: string) => void;
61
+ marker?: string;
62
+ }): LeaseOutputRouter;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Progress routing for `agents run --lease`.
3
+ *
4
+ * The box-side bootstrap emits `LEASE_AGENT_MARKER` on its own line right before
5
+ * `agents run`. Everything the crabbox run streams BEFORE that marker is setup
6
+ * noise (sync bytes, node/agents install, `agents setup`) — shown as spinner
7
+ * text and captured for a failure dump. Everything AFTER is the agent's own
8
+ * output — printed through verbatim. The marker line itself is swallowed.
9
+ */
10
+ /** Sentinel echoed on the box right before `agents run`. Distinctive + collision-proof. */
11
+ export const LEASE_AGENT_MARKER = '___AGENTS_LEASE_AGENT_OUTPUT_b1f4c2___';
12
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
13
+ /**
14
+ * A deliberately minimal, self-throttled spinner. Unlike `ora`, it does NOT hook
15
+ * the stream's `write` to re-render on external output — it writes exactly one
16
+ * short line per fixed tick and nowhere else, so it is structurally incapable of
17
+ * a re-render feedback loop (the failure mode that made `ora` blow up when a
18
+ * lease streamed output past a live spinner). On a non-TTY it prints each phase
19
+ * label once and stays silent on `update`, so piped/CI output never floods.
20
+ *
21
+ * Only ever run ONE phase at a time, and never stream other output to the same
22
+ * stream while a phase is active — stop it first.
23
+ */
24
+ export function createSpinner(opts = {}) {
25
+ const stream = opts.stream ?? process.stderr;
26
+ const enabled = opts.enabled ?? !!stream.isTTY;
27
+ const intervalMs = opts.intervalMs ?? 120;
28
+ const CLEAR = '\r\u001b[2K';
29
+ let text = '';
30
+ let frame = 0;
31
+ let timer;
32
+ let running = false;
33
+ const write = (s) => stream.write(s);
34
+ return {
35
+ start(t) {
36
+ text = t;
37
+ running = true;
38
+ if (!enabled) {
39
+ write(`${t}\n`);
40
+ return;
41
+ }
42
+ if (timer)
43
+ return;
44
+ frame = 0;
45
+ write(`${CLEAR}${SPINNER_FRAMES[0]} ${text}`);
46
+ timer = setInterval(() => {
47
+ frame = (frame + 1) % SPINNER_FRAMES.length;
48
+ write(`${CLEAR}${SPINNER_FRAMES[frame]} ${text}`);
49
+ }, intervalMs);
50
+ },
51
+ update(t) {
52
+ text = t; // next tick renders it; on a non-TTY we stay silent (no flood)
53
+ },
54
+ stopAndPersist(symbol, t) {
55
+ if (timer) {
56
+ clearInterval(timer);
57
+ timer = undefined;
58
+ }
59
+ running = false;
60
+ write(enabled ? `${CLEAR}${symbol} ${t}\n` : `${symbol} ${t}\n`);
61
+ },
62
+ stop() {
63
+ if (timer) {
64
+ clearInterval(timer);
65
+ timer = undefined;
66
+ if (enabled)
67
+ write(CLEAR);
68
+ }
69
+ running = false;
70
+ },
71
+ get active() {
72
+ return running;
73
+ },
74
+ };
75
+ }
76
+ /**
77
+ * Split the crabbox run stream at `LEASE_AGENT_MARKER`. `onSetupLine` fires for
78
+ * each complete non-empty line before the marker; `onAgentChunk` fires with raw
79
+ * text after it (streamed promptly, not line-buffered, so agent output is live).
80
+ */
81
+ export function createLeaseOutputRouter(cb) {
82
+ const marker = cb.marker ?? LEASE_AGENT_MARKER;
83
+ let seen = false;
84
+ let buf = '';
85
+ const setup = [];
86
+ const emitLine = (line) => {
87
+ const t = line.replace(/\r$/, '');
88
+ if (t.trim()) {
89
+ setup.push(t);
90
+ cb.onSetupLine(t);
91
+ }
92
+ };
93
+ return {
94
+ push(chunk) {
95
+ if (seen) {
96
+ cb.onAgentChunk(chunk);
97
+ return;
98
+ }
99
+ buf += chunk;
100
+ let idx;
101
+ while ((idx = buf.indexOf('\n')) !== -1) {
102
+ const line = buf.slice(0, idx);
103
+ buf = buf.slice(idx + 1);
104
+ if (line.includes(marker)) {
105
+ seen = true;
106
+ // Anything already buffered past the marker is agent output.
107
+ if (buf) {
108
+ cb.onAgentChunk(buf);
109
+ buf = '';
110
+ }
111
+ return;
112
+ }
113
+ emitLine(line);
114
+ }
115
+ // A trailing partial line stays in `buf` — it may be the marker forming.
116
+ },
117
+ end() {
118
+ if (!seen && buf)
119
+ emitLine(buf);
120
+ buf = '';
121
+ },
122
+ sawAgent() {
123
+ return seen;
124
+ },
125
+ setupLines() {
126
+ return setup;
127
+ },
128
+ };
129
+ }
@@ -47,11 +47,48 @@ export declare function pickRuntimes(detected: DetectedRuntime[], prompt?: (choi
47
47
  checked: boolean;
48
48
  disabled: boolean | string;
49
49
  }[]) => Promise<AgentId[]>): Promise<AgentId[]>;
50
+ /**
51
+ * Where Claude Code reads its OAuth token on the box. `.claude.json` (the file
52
+ * LEASE_RUNTIMES copies) is config/account-metadata ONLY — the actual token
53
+ * lives here, so without it the box boots "Not logged in".
54
+ */
55
+ export declare const CLAUDE_TOKEN_REMOTE = ".claude/.credentials.json";
56
+ /**
57
+ * The RAW wrapped Claude credential payload (`{"claudeAiOauth":{…}}`) to write to
58
+ * the box's `~/.claude/.credentials.json`, or null if no signed-in token is found.
59
+ *
60
+ * On macOS the token is in the login Keychain, read SILENTLY via
61
+ * `getKeychainToken` (the `/usr/bin/security … -w` path — Claude's item trusts it,
62
+ * no Touch ID). A default native install uses the bare `Claude Code-credentials`
63
+ * service; an agents-cli managed install (where `~/.claude` symlinks into a
64
+ * versioned home) uses a hash-suffixed service, so we try the bare service first,
65
+ * then enumerate installed version homes (preferring the account whose email
66
+ * matches `preferEmail`, so the token matches the `.claude.json` config we copy).
67
+ * Off macOS the local Claude CLI stores the token in `.credentials.json` already —
68
+ * reuse the rush.ts Linux branch verbatim.
69
+ *
70
+ * The reader/service/version helpers are injected so unit tests never touch the
71
+ * real Keychain.
72
+ */
73
+ export declare function resolveClaudeCredentialsBlob(opts?: {
74
+ preferEmail?: string | null;
75
+ readItem?: (service: string) => string;
76
+ service?: (home?: string) => string;
77
+ listVersions?: () => string[];
78
+ versionHome?: (version: string) => string;
79
+ accountEmail?: (home: string) => Promise<string | null>;
80
+ }): Promise<string | null>;
50
81
  /**
51
82
  * Build a bash snippet that writes each picked runtime's token file to the box's
52
83
  * home-level config path (0600), from the token contents read locally. Returns
53
84
  * `''` when no runtimes were selected. The snippet is meant to be embedded in
54
85
  * the `--script-stdin` body (never argv).
86
+ *
87
+ * `extras.claudeCredentialsJson` (the raw wrapped payload from
88
+ * `resolveClaudeCredentialsBlob`) is written to `~/.claude/.credentials.json` in
89
+ * ADDITION to claude's `.claude.json` config — without it the box is "Not logged in".
55
90
  */
56
- export declare function buildCredentialScript(picked: AgentId[], detected: DetectedRuntime[]): string;
91
+ export declare function buildCredentialScript(picked: AgentId[], detected: DetectedRuntime[], extras?: {
92
+ claudeCredentialsJson?: string | null;
93
+ }): string;
57
94
  export {};
@@ -16,6 +16,10 @@ import * as os from 'os';
16
16
  import * as path from 'path';
17
17
  import * as fs from 'fs';
18
18
  import { getAccountInfo } from '../agents.js';
19
+ import { getKeychainToken } from '../secrets/index.js';
20
+ import { getClaudeKeychainService } from '../usage.js';
21
+ import { listInstalledVersions, getVersionHomePath } from '../versions.js';
22
+ import { readClaudeCredentialsBlob } from '../cloud/rush.js';
19
23
  export const LEASE_RUNTIMES = [
20
24
  { id: 'claude', label: 'Claude Code', localCandidates: ['.claude/.claude.json', '.claude.json'], remote: '.claude.json' },
21
25
  { id: 'codex', label: 'Codex CLI', localCandidates: ['.codex/auth.json'], remote: '.codex/auth.json' },
@@ -79,15 +83,102 @@ export async function pickRuntimes(detected, prompt) {
79
83
  // token's contents effectively impossible, so the quoted heredoc can never be
80
84
  // closed early by the credential body.
81
85
  const CRED_EOF = 'AGENTS_LEASE_CRED_EOF_9f3c1a7b5e2d4068';
86
+ /**
87
+ * Where Claude Code reads its OAuth token on the box. `.claude.json` (the file
88
+ * LEASE_RUNTIMES copies) is config/account-metadata ONLY — the actual token
89
+ * lives here, so without it the box boots "Not logged in".
90
+ */
91
+ export const CLAUDE_TOKEN_REMOTE = '.claude/.credentials.json';
92
+ /** True when `s` parses to a Claude keychain payload with an OAuth access token. */
93
+ function isClaudeCredentialsBlob(s) {
94
+ try {
95
+ const p = JSON.parse(s);
96
+ return typeof p?.claudeAiOauth?.accessToken === 'string';
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ /**
103
+ * The RAW wrapped Claude credential payload (`{"claudeAiOauth":{…}}`) to write to
104
+ * the box's `~/.claude/.credentials.json`, or null if no signed-in token is found.
105
+ *
106
+ * On macOS the token is in the login Keychain, read SILENTLY via
107
+ * `getKeychainToken` (the `/usr/bin/security … -w` path — Claude's item trusts it,
108
+ * no Touch ID). A default native install uses the bare `Claude Code-credentials`
109
+ * service; an agents-cli managed install (where `~/.claude` symlinks into a
110
+ * versioned home) uses a hash-suffixed service, so we try the bare service first,
111
+ * then enumerate installed version homes (preferring the account whose email
112
+ * matches `preferEmail`, so the token matches the `.claude.json` config we copy).
113
+ * Off macOS the local Claude CLI stores the token in `.credentials.json` already —
114
+ * reuse the rush.ts Linux branch verbatim.
115
+ *
116
+ * The reader/service/version helpers are injected so unit tests never touch the
117
+ * real Keychain.
118
+ */
119
+ export async function resolveClaudeCredentialsBlob(opts) {
120
+ const readItem = opts?.readItem ?? getKeychainToken;
121
+ const service = opts?.service ?? getClaudeKeychainService;
122
+ const listVersions = opts?.listVersions ?? (() => listInstalledVersions('claude'));
123
+ const versionHome = opts?.versionHome ?? ((v) => getVersionHomePath('claude', v));
124
+ const accountEmail = opts?.accountEmail ?? (async (home) => (await getAccountInfo('claude', home)).email);
125
+ const tryRead = (svc) => {
126
+ try {
127
+ const raw = readItem(svc).trim();
128
+ return isClaudeCredentialsBlob(raw) ? raw : null;
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ };
134
+ if (process.platform === 'darwin') {
135
+ // 1) Bare service — the default native (non-managed) install.
136
+ const bare = tryRead(service(undefined));
137
+ if (bare)
138
+ return bare;
139
+ // 2) Managed installs — hash-suffixed service keyed to each version home.
140
+ // Prefer the version whose account email matches the copied config.
141
+ let homes;
142
+ try {
143
+ homes = listVersions().map(versionHome);
144
+ }
145
+ catch {
146
+ homes = [];
147
+ }
148
+ if (opts?.preferEmail) {
149
+ const scored = await Promise.all(homes.map(async (home) => ({ home, match: (await accountEmail(home).catch(() => null)) === opts.preferEmail })));
150
+ homes = [...scored.filter((s) => s.match), ...scored.filter((s) => !s.match)].map((s) => s.home);
151
+ }
152
+ for (const home of homes) {
153
+ const hit = tryRead(service(home));
154
+ if (hit)
155
+ return hit;
156
+ }
157
+ return null;
158
+ }
159
+ // Off darwin: the local Claude CLI already stores the wrapped blob on disk.
160
+ const home = process.env.AGENTS_REAL_HOME || os.homedir();
161
+ return readClaudeCredentialsBlob(home);
162
+ }
82
163
  /**
83
164
  * Build a bash snippet that writes each picked runtime's token file to the box's
84
165
  * home-level config path (0600), from the token contents read locally. Returns
85
166
  * `''` when no runtimes were selected. The snippet is meant to be embedded in
86
167
  * the `--script-stdin` body (never argv).
168
+ *
169
+ * `extras.claudeCredentialsJson` (the raw wrapped payload from
170
+ * `resolveClaudeCredentialsBlob`) is written to `~/.claude/.credentials.json` in
171
+ * ADDITION to claude's `.claude.json` config — without it the box is "Not logged in".
87
172
  */
88
- export function buildCredentialScript(picked, detected) {
173
+ export function buildCredentialScript(picked, detected, extras) {
89
174
  const byId = new Map(detected.map((d) => [d.id, d]));
90
175
  const parts = [];
176
+ const writeFile = (remote, contents) => {
177
+ const dir = path.posix.dirname(remote);
178
+ const mkdir = dir && dir !== '.' ? `mkdir -p "$HOME/${dir}"\n` : '';
179
+ return (`${mkdir}cat > "$HOME/${remote}" <<'${CRED_EOF}'\n${contents}${contents.endsWith('\n') ? '' : '\n'}${CRED_EOF}\n` +
180
+ `chmod 600 "$HOME/${remote}"`);
181
+ };
91
182
  for (const id of picked) {
92
183
  const d = byId.get(id);
93
184
  const cred = LEASE_RUNTIMES.find((c) => c.id === id);
@@ -100,10 +191,12 @@ export function buildCredentialScript(picked, detected) {
100
191
  catch {
101
192
  continue;
102
193
  }
103
- const dir = path.posix.dirname(cred.remote);
104
- const mkdir = dir && dir !== '.' ? `mkdir -p "$HOME/${dir}"\n` : '';
105
- parts.push(`${mkdir}cat > "$HOME/${cred.remote}" <<'${CRED_EOF}'\n${contents}${contents.endsWith('\n') ? '' : '\n'}${CRED_EOF}\n` +
106
- `chmod 600 "$HOME/${cred.remote}"`);
194
+ parts.push(writeFile(cred.remote, contents));
195
+ // For claude the file above is config/state only; the OAuth token is a
196
+ // second artifact without it the box comes up "Not logged in".
197
+ if (id === 'claude' && extras?.claudeCredentialsJson) {
198
+ parts.push(writeFile(CLAUDE_TOKEN_REMOTE, extras.claudeCredentialsJson));
199
+ }
107
200
  }
108
201
  return parts.join('\n');
109
202
  }
@@ -54,13 +54,15 @@ export declare function runDaemon(): Promise<void>;
54
54
  /**
55
55
  * Read the long-lived Claude OAuth token (from `claude setup-token`) that the
56
56
  * user stored under the `claude` secrets bundle. Resolves the bundle the same
57
- * way `agents run --secrets` does, so the token is found whether it was stored
58
- * keychain-backed or as a literal. Returns null when the bundle/key isn't
59
- * configured, the Keychain read is cancelled, or the platform has no keychain —
60
- * the daemon then behaves exactly as before (relying on the interactive OAuth
61
- * session). Never throws: a misconfigured token must not block daemon startup.
57
+ * way `agents run --secrets` does. Interactive starts may prompt Keychain;
58
+ * headless auto-starts are broker-only and return null unless the user already
59
+ * unlocked the bundle in the secrets agent. That keeps a background browser
60
+ * command from hanging on an unseen biometric prompt. Never throws: an absent
61
+ * token leaves the daemon on its existing interactive OAuth session.
62
62
  */
63
- export declare function readDaemonClaudeOAuthToken(): string | null;
63
+ export declare function readDaemonClaudeOAuthToken(opts?: {
64
+ allowPrompt?: boolean;
65
+ }): string | null;
64
66
  /**
65
67
  * Write a launchd plist or systemd unit with owner-only permissions atomically.
66
68
  *
@@ -72,9 +74,10 @@ export declare function readDaemonClaudeOAuthToken(): string | null;
72
74
  */
73
75
  export declare function writeOwnerOnlyServiceManifest(filePath: string, content: string): void;
74
76
  /** Generate a macOS launchd plist for auto-starting the daemon. */
75
- export declare function generateLaunchdPlist(): string;
77
+ export declare function generateLaunchdPlist(oauthToken?: string | null): string;
76
78
  /** Generate a Linux systemd user unit for auto-starting the daemon. */
77
- export declare function generateSystemdUnit(): string;
79
+ export declare function generateSystemdUnit(oauthToken?: string | null): string;
80
+ export declare function getAgentsBinPath(): string;
78
81
  /** Start the daemon via launchd, systemd, or as a detached process. */
79
82
  export declare function startDaemon(): {
80
83
  pid: number | null;
@@ -102,7 +105,7 @@ export declare function ensureDaemonStarted(): {
102
105
  * the daemon then passes it to every routine run it spawns. An already-set
103
106
  * value (e.g. inherited from launchd) is left untouched.
104
107
  */
105
- export declare function buildDetachedDaemonEnv(baseEnv?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
108
+ export declare function buildDetachedDaemonEnv(baseEnv?: NodeJS.ProcessEnv, oauthToken?: string | null): NodeJS.ProcessEnv;
106
109
  /**
107
110
  * Resolve how to launch the daemon: `node <entry> daemon _run`, matching the
108
111
  * exact form that works under a direct `daemon _run`.