@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
@@ -90,25 +90,47 @@ export function planKeychainReap(snapshots, now, prevCandidates) {
90
90
  }
91
91
  return { kill, nextCandidates };
92
92
  }
93
+ /**
94
+ * Parse macOS `ps -o etime=` elapsed time into whole seconds.
95
+ *
96
+ * BSD `etime` renders as `[[dd-]hh:]mm:ss` (e.g. `05:03`, `01:02:03`,
97
+ * `14-04:10:52`). This is the portable keyword: `etimes` (raw seconds) is a
98
+ * GNU/Linux procps extension that macOS `ps` rejects with a non-zero exit, so
99
+ * the reaper — which only ever runs on darwin — must read `etime`.
100
+ * Returns null for an unparseable value so the caller drops the row.
101
+ */
102
+ export function parseEtimeToSeconds(raw) {
103
+ const m = raw.match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);
104
+ if (!m)
105
+ return null;
106
+ const days = m[1] ? parseInt(m[1], 10) : 0;
107
+ const hours = m[2] ? parseInt(m[2], 10) : 0;
108
+ const mins = parseInt(m[3], 10);
109
+ const secs = parseInt(m[4], 10);
110
+ if ([days, hours, mins, secs].some(isNaN))
111
+ return null;
112
+ return ((days * 24 + hours) * 60 + mins) * 60 + secs;
113
+ }
93
114
  /**
94
115
  * Parse one `ps` output line.
95
116
  *
96
- * Expected format from `ps -ax -o pid=,ppid=,etimes=,command=`:
97
- * "<pid> <ppid> <etimes> <command...>"
98
- * The command field is the remainder of the line and may contain spaces.
117
+ * Expected format from `ps -ax -o pid=,ppid=,etime=,command=`:
118
+ * "<pid> <ppid> <etime> <command...>"
119
+ * where `<etime>` is BSD elapsed time (`[[dd-]hh:]mm:ss`). The command field is
120
+ * the remainder of the line and may contain spaces.
99
121
  */
100
122
  function parsePsLine(line) {
101
123
  const trimmed = line.trim();
102
124
  if (!trimmed)
103
125
  return null;
104
- const m = trimmed.match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/);
126
+ const m = trimmed.match(/^(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/);
105
127
  if (!m)
106
128
  return null;
107
129
  const pid = parseInt(m[1], 10);
108
130
  const ppid = parseInt(m[2], 10);
109
- const elapsedSec = parseInt(m[3], 10);
131
+ const elapsedSec = parseEtimeToSeconds(m[3]);
110
132
  const command = m[4];
111
- if (isNaN(pid) || isNaN(ppid) || isNaN(elapsedSec))
133
+ if (isNaN(pid) || isNaN(ppid) || elapsedSec == null)
112
134
  return null;
113
135
  return { pid, ppid, elapsedSec, command };
114
136
  }
@@ -142,7 +164,7 @@ export function reapOrphanedKeychainProcesses() {
142
164
  }
143
165
  let out;
144
166
  try {
145
- out = execFileSync('ps', ['-ax', '-o', 'pid=,ppid=,etimes=,command='], {
167
+ out = execFileSync('ps', ['-ax', '-o', 'pid=,ppid=,etime=,command='], {
146
168
  encoding: 'utf-8',
147
169
  stdio: ['ignore', 'pipe', 'ignore'],
148
170
  });
@@ -150,7 +172,7 @@ export function reapOrphanedKeychainProcesses() {
150
172
  catch (err) {
151
173
  return { reaped: 0, details: [`ps failed: ${err.message}`], plan: { kill: [], nextCandidates: new Map() } };
152
174
  }
153
- const snapshots = [];
175
+ const rows = [];
154
176
  for (const line of out.split('\n')) {
155
177
  const parsed = parsePsLine(line);
156
178
  if (!parsed)
@@ -159,14 +181,27 @@ export function reapOrphanedKeychainProcesses() {
159
181
  // Exact path-match: the helper invocation's command line begins with the
160
182
  // absolute helper path, followed by a space and its arguments (or nothing).
161
183
  const isHelper = command === helperPath || command.startsWith(`${helperPath} `);
162
- snapshots.push({
163
- pid,
164
- ppid,
165
- elapsedSec,
166
- startTime: captureProcessStartTime(pid),
167
- isHelper,
168
- });
184
+ rows.push({ pid, ppid, elapsedSec, isHelper, startTime: null });
185
+ }
186
+ const rowByPid = new Map(rows.map((r) => [r.pid, r]));
187
+ for (const row of rows) {
188
+ if (!row.isHelper)
189
+ continue;
190
+ row.startTime = captureProcessStartTime(row.pid);
191
+ if (row.ppid !== 1 && row.elapsedSec > STUCK_GRACE_SEC) {
192
+ const parent = rowByPid.get(row.ppid);
193
+ if (parent && parent.startTime === null) {
194
+ parent.startTime = captureProcessStartTime(row.ppid);
195
+ }
196
+ }
169
197
  }
198
+ const snapshots = rows.map((r) => ({
199
+ pid: r.pid,
200
+ ppid: r.ppid,
201
+ elapsedSec: r.elapsedSec,
202
+ startTime: r.startTime,
203
+ isHelper: r.isHelper,
204
+ }));
170
205
  const plan = planKeychainReap(snapshots, Date.now(), stuckParentCandidates);
171
206
  stuckParentCandidates = plan.nextCandidates;
172
207
  for (const pid of plan.kill) {
@@ -0,0 +1,27 @@
1
+ /** The current variable for transport/sync passphrases. */
2
+ export declare const SYNC_PASSPHRASE_ENV = "AGENTS_SYNC_PASSPHRASE";
3
+ /** The file-store master key, honoured for sync only as a deprecated fallback. */
4
+ export declare const LEGACY_PASSPHRASE_ENV = "AGENTS_SECRETS_PASSPHRASE";
5
+ /** Reset the one-shot warning latches. Tests only — production never re-warns. */
6
+ export declare function resetSyncPassphraseWarnings(): void;
7
+ /** Where a resolved sync passphrase came from, so callers can report honestly. */
8
+ export type SyncPassphraseSource = 'sync-env' | 'legacy-env' | null;
9
+ export interface ResolvedSyncPassphrase {
10
+ value: string | null;
11
+ source: SyncPassphraseSource;
12
+ }
13
+ /**
14
+ * Read the transport passphrase from the environment, preferring the current
15
+ * variable and falling back to the deprecated one with a single warning.
16
+ * Returns `{ value: null }` when neither is set — the caller decides whether to
17
+ * prompt (TTY) or fail (headless). Never prompts, never throws.
18
+ */
19
+ export declare function resolveSyncPassphraseFromEnv(): ResolvedSyncPassphrase;
20
+ /**
21
+ * The one-shot reminder that an env-sourced passphrase is readable by other
22
+ * same-user processes. Separate from the deprecation notice above so a caller
23
+ * using the CURRENT variable still gets the readability warning exactly once.
24
+ */
25
+ export declare function warnEnvPassphraseReadableOnce(): void;
26
+ /** The message a headless caller shows when no passphrase is available. */
27
+ export declare function missingSyncPassphraseMessage(): string;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The passphrase that seals a bundle for TRANSPORT — `secrets push`/`pull` and
3
+ * the portable `export --to-file` / `import --from-file` envelope.
4
+ *
5
+ * This is deliberately a DIFFERENT secret from the file store's master key
6
+ * (`AGENTS_SECRETS_PASSPHRASE`, `filestore.ts:getPassphrase`). The two were one
7
+ * variable, and that overload is what put a master key into `~/.zshenv` on seven
8
+ * worker boxes (RUSH-1968): the file store stopped needing a passphrase once it
9
+ * auto-provisioned a machine-local key, but headless `push`/`pull` still hard-
10
+ * failed without one, so an operator exported the master key fleet-wide to make
11
+ * sync work — handing every same-user process the key to the whole store.
12
+ *
13
+ * Splitting them means a box that only needs headless sync sets
14
+ * `AGENTS_SYNC_PASSPHRASE` and never has the store's master key in its
15
+ * environment at all.
16
+ *
17
+ * Resolution order:
18
+ * 1. `AGENTS_SYNC_PASSPHRASE` — the current name.
19
+ * 2. `AGENTS_SECRETS_PASSPHRASE` — deprecated fallback, warns once.
20
+ * 3. (caller prompts, or fails)
21
+ *
22
+ * The fallback exists so already-scripted CI and release automation keep working
23
+ * across the upgrade; it is not a second supported spelling.
24
+ */
25
+ import chalk from 'chalk';
26
+ /** The current variable for transport/sync passphrases. */
27
+ export const SYNC_PASSPHRASE_ENV = 'AGENTS_SYNC_PASSPHRASE';
28
+ /** The file-store master key, honoured for sync only as a deprecated fallback. */
29
+ export const LEGACY_PASSPHRASE_ENV = 'AGENTS_SECRETS_PASSPHRASE';
30
+ /** Warn at most once per process: a `--all` push over many bundles must not
31
+ * flood stderr with the same notice. */
32
+ let deprecatedVarWarned = false;
33
+ /** Same, for the "this came from an env var at all" readability notice. */
34
+ let envPassphraseWarned = false;
35
+ /** Reset the one-shot warning latches. Tests only — production never re-warns. */
36
+ export function resetSyncPassphraseWarnings() {
37
+ deprecatedVarWarned = false;
38
+ envPassphraseWarned = false;
39
+ }
40
+ /**
41
+ * Read the transport passphrase from the environment, preferring the current
42
+ * variable and falling back to the deprecated one with a single warning.
43
+ * Returns `{ value: null }` when neither is set — the caller decides whether to
44
+ * prompt (TTY) or fail (headless). Never prompts, never throws.
45
+ */
46
+ export function resolveSyncPassphraseFromEnv() {
47
+ const current = process.env[SYNC_PASSPHRASE_ENV];
48
+ if (current)
49
+ return { value: current, source: 'sync-env' };
50
+ const legacy = process.env[LEGACY_PASSPHRASE_ENV];
51
+ if (legacy) {
52
+ if (!deprecatedVarWarned) {
53
+ deprecatedVarWarned = true;
54
+ process.stderr.write(chalk.yellow(`warn: ${LEGACY_PASSPHRASE_ENV} is deprecated for sync — it is the file store's master key, ` +
55
+ `not a transport passphrase. Set ${SYNC_PASSPHRASE_ENV} instead; the old name still works ` +
56
+ 'for now. Keeping the master key in the environment exposes the whole store to every ' +
57
+ 'same-user process (RUSH-1968).\n'));
58
+ }
59
+ return { value: legacy, source: 'legacy-env' };
60
+ }
61
+ return { value: null, source: null };
62
+ }
63
+ /**
64
+ * The one-shot reminder that an env-sourced passphrase is readable by other
65
+ * same-user processes. Separate from the deprecation notice above so a caller
66
+ * using the CURRENT variable still gets the readability warning exactly once.
67
+ */
68
+ export function warnEnvPassphraseReadableOnce() {
69
+ if (envPassphraseWarned)
70
+ return;
71
+ envPassphraseWarned = true;
72
+ process.stderr.write(chalk.yellow('warn: using a sync passphrase from the environment. Env vars are readable by other ' +
73
+ 'same-user processes (/proc, ps, crash dumps, CI logs) — rotate after CI use.\n'));
74
+ }
75
+ /** The message a headless caller shows when no passphrase is available. */
76
+ export function missingSyncPassphraseMessage() {
77
+ return `A sync passphrase is required. Run from a TTY, or set ${SYNC_PASSPHRASE_ENV}.`;
78
+ }
@@ -126,6 +126,10 @@ export interface ActiveSession {
126
126
  */
127
127
  lastActivityMs?: number;
128
128
  status: ActiveStatus;
129
+ /** Indexed launch origin, backfilled by the sessions command for JSON consumers. */
130
+ origin?: 'cli' | 'routine';
131
+ /** Routine definition name when origin is `routine`. */
132
+ routineName?: string;
129
133
  /**
130
134
  * Foreground/background presence for the detach/attach model:
131
135
  * `attached` — live interactive TUI you're watching;
@@ -12,7 +12,7 @@ import Database from '../sqlite.js';
12
12
  import { parseSession } from './parse.js';
13
13
  import { extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
14
14
  import { getSessionsDir, getSessionsDbPath } from '../state.js';
15
- import { query as queryEvents } from '../events.js';
15
+ import { query as queryEvents, queryToolUsageForSessions } from '../events.js';
16
16
  import { machineForSessionFile } from './origin-machine.js';
17
17
  import { loadSessionActorIndex, readSessionActorRecord } from './actor-sidecar.js';
18
18
  import { toolCallsFromEvents } from './tool-calls.js';
@@ -1648,6 +1648,12 @@ export function upsertSessionsBatch(entries) {
1648
1648
  }
1649
1649
  });
1650
1650
  const writtenEntries = [];
1651
+ // Pre-compute browser/computer usage for all sessions outside the write
1652
+ // transaction. detectToolUsage scans all event log files (O(files) I/O
1653
+ // per call) and holding the SQLite write lock during that scan is what
1654
+ // causes the "DB locked" errors (RUSH-2006). One pass for the whole batch
1655
+ // costs O(files) total instead of O(N × files) inside the lock.
1656
+ const toolUsageBySession = queryToolUsageForSessions(new Set(enrichedEntries.map(e => e.meta.id)));
1651
1657
  const txn = db.transaction((items) => {
1652
1658
  // Re-read the ledger now that we hold the write lock. Any file committed
1653
1659
  // by a concurrent process since our pre-scan is visible here.
@@ -1678,7 +1684,7 @@ export function upsertSessionsBatch(entries) {
1678
1684
  // back when the error escapes `fn`, so catching + skipping here leaves the txn valid
1679
1685
  // and committable. We deliberately do NOT stamp the ledger for a skipped row, so the
1680
1686
  // next scan re-tries it (self-healing once the underlying parser is fixed).
1681
- const toolUsage = detectToolUsage(meta.id);
1687
+ const toolUsage = toolUsageBySession.get(meta.id) ?? { usedBrowser: false, usedComputer: false };
1682
1688
  // claude/codex skip enrichCachedSessionMeta above (preserving their
1683
1689
  // resumable-parse optimization) — write their pre-computed
1684
1690
  // skillsUsed/slashCommandsUsed (folded incrementally by discover.ts's
@@ -0,0 +1,37 @@
1
+ import { type RotateCandidate } from '../rotate.js';
2
+ import type { AgentId } from '../types.js';
3
+ import type { SessionMeta } from './types.js';
4
+ export type SessionRecoveryTarget = {
5
+ mode: 'native';
6
+ agent: AgentId;
7
+ version: string;
8
+ reason: string;
9
+ } | {
10
+ mode: 'continue';
11
+ agent: AgentId;
12
+ version: string;
13
+ reason: string;
14
+ };
15
+ export declare class SessionRecoveryError extends Error {
16
+ constructor(message: string);
17
+ }
18
+ /** Canonical origin-device label for every recovery consumer. */
19
+ export declare function sessionOriginDevice(session: Pick<SessionMeta, 'machine'>, self?: string): string;
20
+ /** The peer that must execute recovery, or undefined when this is the origin. */
21
+ export declare function sessionRecoveryPeer(session: Pick<SessionMeta, 'machine'>, selfCheck?: (host: string) => boolean): string | undefined;
22
+ /** Whether an explicit placement names the session's origin device. */
23
+ export declare function sessionRecoveryDestinationMatches(session: Pick<SessionMeta, 'machine'>, requestedHost: string, self?: string): boolean;
24
+ /**
25
+ * Decide how a durable session resumes on the device that owns it.
26
+ *
27
+ * Native resume is legal only in the exact origin version's isolated home and
28
+ * only while that account is healthy. Every other successful path stays on the
29
+ * same harness and uses `/continue`, whose indexed transcript reader can reach
30
+ * retained version trash. No healthy same-harness account is a loud failure.
31
+ */
32
+ export declare function resolveSessionRecoveryFromCandidates(session: SessionMeta, candidates: RotateCandidate[], supportsNative?: (agent: AgentId, version?: string) => boolean): SessionRecoveryTarget;
33
+ export declare function resolveSessionRecovery(session: SessionMeta): Promise<SessionRecoveryTarget>;
34
+ /** Stable self-command used by focus, resume, and attach. The owning device runs
35
+ * the recovery resolver above; callers must not native-resume another version's
36
+ * isolated home themselves. */
37
+ export declare function sessionRecoveryRunArgs(session: Pick<SessionMeta, 'id'>): string[];
@@ -0,0 +1,95 @@
1
+ import { AGENTS } from '../agents.js';
2
+ import { isSelfHost } from '../devices/self-host.js';
3
+ import { nativeResume } from '../exec.js';
4
+ import { machineId, normalizeHost } from '../machine-id.js';
5
+ import { collectRunCandidates, formatNoHealthyAccountError, pickBalancedCandidate, readinessFromCandidate, } from '../rotate.js';
6
+ export class SessionRecoveryError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'SessionRecoveryError';
10
+ }
11
+ }
12
+ /** Canonical origin-device label for every recovery consumer. */
13
+ export function sessionOriginDevice(session, self = machineId()) {
14
+ return normalizeHost(session.machine ?? self);
15
+ }
16
+ /** The peer that must execute recovery, or undefined when this is the origin. */
17
+ export function sessionRecoveryPeer(session, selfCheck = isSelfHost) {
18
+ if (!session.machine || selfCheck(session.machine))
19
+ return undefined;
20
+ return normalizeHost(session.machine);
21
+ }
22
+ /** Whether an explicit placement names the session's origin device. */
23
+ export function sessionRecoveryDestinationMatches(session, requestedHost, self = machineId()) {
24
+ const requested = normalizeHost(requestedHost.split('@').pop() || requestedHost);
25
+ return requested === sessionOriginDevice(session, self);
26
+ }
27
+ function runnableSessionAgent(session) {
28
+ if (!(session.agent in AGENTS)) {
29
+ throw new SessionRecoveryError(`Session ${session.shortId} belongs to ${session.agent}, which is indexed but cannot be launched by agents run.`);
30
+ }
31
+ return session.agent;
32
+ }
33
+ function sourceReason(session, candidates) {
34
+ if (!session.version)
35
+ return 'the origin version was not recorded';
36
+ const source = candidates.find((candidate) => candidate.version === session.version);
37
+ if (!source)
38
+ return `origin ${session.agent}@${session.version} is not installed`;
39
+ const readiness = readinessFromCandidate(source);
40
+ return readiness.ready
41
+ ? `origin ${session.agent}@${session.version} has no native resume form`
42
+ : `origin ${session.agent}@${session.version} is ${readiness.reason}`;
43
+ }
44
+ /**
45
+ * Decide how a durable session resumes on the device that owns it.
46
+ *
47
+ * Native resume is legal only in the exact origin version's isolated home and
48
+ * only while that account is healthy. Every other successful path stays on the
49
+ * same harness and uses `/continue`, whose indexed transcript reader can reach
50
+ * retained version trash. No healthy same-harness account is a loud failure.
51
+ */
52
+ export function resolveSessionRecoveryFromCandidates(session, candidates, supportsNative = nativeResume) {
53
+ const agent = runnableSessionAgent(session);
54
+ const device = sessionOriginDevice(session);
55
+ const source = session.version
56
+ ? candidates.find((candidate) => candidate.version === session.version)
57
+ : undefined;
58
+ const sourceReady = source ? readinessFromCandidate(source).ready : false;
59
+ // An exact healthy origin is deterministic: preserve its isolated home. If
60
+ // native resume is unavailable for that harness, /continue still launches in
61
+ // that same healthy home. Only an unusable/missing origin enters balanced
62
+ // account selection.
63
+ const selection = sourceReady
64
+ ? { picked: source }
65
+ : pickBalancedCandidate(candidates);
66
+ if (!selection) {
67
+ const detail = formatNoHealthyAccountError(agent, 'balanced', candidates);
68
+ throw new SessionRecoveryError(`Cannot recover session ${session.shortId} on ${device}; origin ${agent}@${session.version ?? 'unknown'}. ${detail}`);
69
+ }
70
+ const version = selection.picked.version;
71
+ if (session.version === version && supportsNative(agent, version)) {
72
+ return {
73
+ mode: 'native',
74
+ agent,
75
+ version,
76
+ reason: `origin ${agent}@${version} is installed and healthy`,
77
+ };
78
+ }
79
+ return {
80
+ mode: 'continue',
81
+ agent,
82
+ version,
83
+ reason: `${sourceReason(session, candidates)}; continuing with healthy ${agent}@${version}`,
84
+ };
85
+ }
86
+ export async function resolveSessionRecovery(session) {
87
+ const agent = runnableSessionAgent(session);
88
+ return resolveSessionRecoveryFromCandidates(session, await collectRunCandidates(agent));
89
+ }
90
+ /** Stable self-command used by focus, resume, and attach. The owning device runs
91
+ * the recovery resolver above; callers must not native-resume another version's
92
+ * isolated home themselves. */
93
+ export function sessionRecoveryRunArgs(session) {
94
+ return ['run', 'auto', '--resume', session.id, '--interactive'];
95
+ }
@@ -81,6 +81,8 @@ export interface GatherRemoteListOptions {
81
81
  * know whether the match is unique or conflicting.
82
82
  */
83
83
  isDefinitive?: (session: SessionMeta, machine: string) => boolean;
84
+ /** Per-peer deadline for slower indexed browse queries. */
85
+ timeoutMs?: number;
84
86
  }
85
87
  export declare function gatherRemoteList(forwardedArgs: string[], hosts?: string[], opts?: GatherRemoteListOptions): Promise<RemoteListResult>;
86
88
  export interface RemoteToolSearchResult {
@@ -197,6 +197,7 @@ export async function gatherRemoteList(forwardedArgs, hosts, opts) {
197
197
  args: forwardedArgs,
198
198
  noFanoutEnv: NO_FANOUT_ENV,
199
199
  hosts,
200
+ timeoutMs: opts?.timeoutMs,
200
201
  earlyExit: opts?.isDefinitive ? { isDefinitive: opts.isDefinitive } : undefined,
201
202
  parse: (stdout, machine) => parseRemoteListPayload(stdout, machine, safeResolver),
202
203
  });
@@ -4,11 +4,11 @@ import type { ActiveSession } from './active.js';
4
4
  * Short on purpose: live status (running/idle/waiting) must not go stale.
5
5
  * The daemon warm tick uses the same cadence (see {@link SESSION_CACHE_WARM_INTERVAL_MS}).
6
6
  */
7
- export declare const DEFAULT_ACTIVE_CACHE_MAX_AGE_MS = 15000;
7
+ export declare const DEFAULT_ACTIVE_CACHE_MAX_AGE_MS: number;
8
8
  /** Daemon warm interval — keep in sync with the setInterval in `lib/daemon.ts`. */
9
- export declare const SESSION_CACHE_WARM_INTERVAL_MS = 15000;
10
- /** Kick off the first warm ~25s after daemon start (staggered off other ticks). */
11
- export declare const SESSION_CACHE_WARM_KICKOFF_MS = 25000;
9
+ export declare const SESSION_CACHE_WARM_INTERVAL_MS: number;
10
+ /** Kick off the first warm 30s after daemon start (staggered off other ticks). */
11
+ export declare const SESSION_CACHE_WARM_KICKOFF_MS = 30000;
12
12
  /** Snapshot scope: this host only, or a fleet-wide merge written by a reader. */
13
13
  export type ActiveCacheScope = 'local' | 'fleet';
14
14
  export interface ActiveSessionsSnapshot {
@@ -34,11 +34,11 @@ const IMMUTABLE_FILE = '.active-session-immutable.json';
34
34
  * Short on purpose: live status (running/idle/waiting) must not go stale.
35
35
  * The daemon warm tick uses the same cadence (see {@link SESSION_CACHE_WARM_INTERVAL_MS}).
36
36
  */
37
- export const DEFAULT_ACTIVE_CACHE_MAX_AGE_MS = 15_000;
37
+ export const DEFAULT_ACTIVE_CACHE_MAX_AGE_MS = 4 * 60_000;
38
38
  /** Daemon warm interval — keep in sync with the setInterval in `lib/daemon.ts`. */
39
- export const SESSION_CACHE_WARM_INTERVAL_MS = 15_000;
40
- /** Kick off the first warm ~25s after daemon start (staggered off other ticks). */
41
- export const SESSION_CACHE_WARM_KICKOFF_MS = 25_000;
39
+ export const SESSION_CACHE_WARM_INTERVAL_MS = 3 * 60_000;
40
+ /** Kick off the first warm 30s after daemon start (staggered off other ticks). */
41
+ export const SESSION_CACHE_WARM_KICKOFF_MS = 30_000;
42
42
  /** Keys stored in the immutable memo (transcript-stable). */
43
43
  export const IMMUTABLE_FIELD_KEYS = [
44
44
  'topic',
@@ -77,7 +77,7 @@ export interface ConflictInfo {
77
77
  * top-level entry add/remove — deep edits to plugin contents won't
78
78
  * trigger auto-resync, run `agents sync` for that.
79
79
  */
80
- export declare const SHIM_SCHEMA_VERSION = 28;
80
+ export declare const SHIM_SCHEMA_VERSION = 29;
81
81
  /**
82
82
  * Generate the full bash shim script for the given agent. The returned string
83
83
  * is written to ~/.agents/shims/{cliCommand} and made executable.
@@ -160,7 +160,7 @@ export declare function removeShim(agent: AgentId): boolean;
160
160
  * the versioned home (installer run with GROK_HOME set, or grok
161
161
  * self-update under the shim).
162
162
  */
163
- export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 14;
163
+ export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 15;
164
164
  /**
165
165
  * Agents whose config directory can be relocated by an environment variable
166
166
  * (the per-agent `managedEnv` block in `generateVersionedAliasScript` below).
package/dist/lib/shims.js CHANGED
@@ -223,7 +223,7 @@ async function promptConflictStrategy(conflictInfos) {
223
223
  // The old dispatcher checked only the global dir, so a pinned grok that
224
224
  // installed into the versioned home fell through to the "not installed"
225
225
  // error.
226
- export const SHIM_SCHEMA_VERSION = 28;
226
+ export const SHIM_SCHEMA_VERSION = 29;
227
227
  /** Internal marker string used to embed the schema version in shim scripts. */
228
228
  const SHIM_VERSION_MARKER = 'agents-shim-version:';
229
229
  function shellQuote(value) {
@@ -299,7 +299,18 @@ export OPENCODE_CONFIG_DIR="$VERSION_DIR/home/.config/opencode"
299
299
  # mcp.json, sessions, skills, hooks). Point it at the versioned home.
300
300
  export KIMI_CODE_HOME="$VERSION_DIR/home/${configDirName}"
301
301
  `
302
- : '';
302
+ : agent === 'muse'
303
+ ? `
304
+ # Muse Code has no MUSE_CONFIG_DIR. It resolves config via XDG:
305
+ # $XDG_CONFIG_HOME/muse (settings, skills, hooks, auth)
306
+ # $XDG_DATA_HOME/muse (sessions, plugins)
307
+ # Pin XDG into the version home so managed runs never walk the adopt-time
308
+ # ~/.config/muse -> version-home symlink — Muse refuses agent-definition
309
+ # sources that are SymlinkOrReparse (exit 1). Same idea as CLAUDE_CONFIG_DIR.
310
+ export XDG_CONFIG_HOME="$VERSION_DIR/home/.config"
311
+ export XDG_DATA_HOME="$VERSION_DIR/home/.local/share"
312
+ `
313
+ : '';
303
314
  const launchArgs = agent === 'codex' ? ` ${codexShimLaunchArgs()}` : '';
304
315
  return `#!/bin/bash
305
316
  # Auto-generated by agents-cli - do not edit
@@ -573,6 +584,18 @@ elif [ "$AGENT" = "muse" ]; then
573
584
  esac
574
585
  fi
575
586
  fi
587
+ elif [ "$AGENT" = "warp" ]; then
588
+ # Warp Agent CLI installs a global, self-updating oz binary (brew cask on
589
+ # macOS, the oz-stable apt|yum|pacman package on Linux) -- a platform/package
590
+ # specific location, not ~/.local/bin -- so resolve it from PATH with the same
591
+ # shims-dir re-exec guard as droid/muse.
592
+ BINARY=$(adopted_original_bin || echo "")
593
+ if [ -z "$BINARY" ]; then
594
+ BINARY=$(command -v oz 2>/dev/null || echo "")
595
+ case "$(readlink -f "$BINARY" 2>/dev/null)" in
596
+ "$AGENTS_USER_DIR/.cache/shims/"*) BINARY="" ;;
597
+ esac
598
+ fi
576
599
  else
577
600
  BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
578
601
  fi
@@ -871,7 +894,7 @@ export function removeShim(agent) {
871
894
  * the versioned home (installer run with GROK_HOME set, or grok
872
895
  * self-update under the shim).
873
896
  */
874
- export const VERSIONED_ALIAS_SCHEMA_VERSION = 14;
897
+ export const VERSIONED_ALIAS_SCHEMA_VERSION = 15;
875
898
  /** Internal marker string used to embed the schema version in versioned alias scripts. */
876
899
  const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
877
900
  // The version string is interpolated into a generated bash script and into
@@ -898,7 +921,7 @@ function assertSafeVersion(version) {
898
921
  * KEEP IN SYNC with the `managedEnv` switch in `generateVersionedAliasScript`.
899
922
  * The colocated test `shims.isolation-capability.test.ts` enforces this.
900
923
  */
901
- export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'grok', 'kimi', 'opencode'];
924
+ export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'grok', 'kimi', 'opencode', 'muse'];
902
925
  /**
903
926
  * Whether an agent supports a clean `--isolated` install — i.e. its config
904
927
  * location can be redirected by an env var so the isolated copy stays fully
@@ -956,7 +979,15 @@ export OPENCODE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/
956
979
  # mcp.json, sessions, skills, hooks). Point direct aliases at the versioned home.
957
980
  export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
958
981
  `
959
- : '';
982
+ : agent === 'muse'
983
+ ? `
984
+ # Muse Code: no dedicated config env var. Pin XDG so config/sessions live under
985
+ # the version home as real directories (not via the adopt symlink at
986
+ # ~/.config/muse, which Muse rejects with SymlinkOrReparse).
987
+ export XDG_CONFIG_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/.config"
988
+ export XDG_DATA_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/.local/share"
989
+ `
990
+ : '';
960
991
  const launchArgs = agent === 'codex' ? ` ${codexShimLaunchArgs()}` : '';
961
992
  // Resolve the binary the same way the main shim does (see generateShimScript).
962
993
  // Grok and Droid do NOT ship into node_modules/.bin — Grok downloads a native
@@ -1022,7 +1053,15 @@ else
1022
1053
  "$HOME/.agents/.cache/shims/"*) BINARY="" ;;
1023
1054
  esac
1024
1055
  fi`
1025
- : `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
1056
+ : agent === 'warp'
1057
+ ? `# Warp Agent CLI installs a global self-updating \`oz\` binary (brew cask on
1058
+ # macOS, oz-stable apt|yum|pacman on Linux) — a platform-specific location, not
1059
+ # ~/.local/bin — so resolve it from PATH, refusing anything under our shims dir.
1060
+ BINARY=$(command -v oz 2>/dev/null || echo "")
1061
+ case "$BINARY" in
1062
+ "$HOME/.agents/.cache/shims/"*) BINARY="" ;;
1063
+ esac`
1064
+ : `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
1026
1065
  return `#!/bin/bash
1027
1066
  # Auto-generated by agents-cli - do not edit
1028
1067
  # ${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}
@@ -24,6 +24,8 @@ export function loginHint(agentId) {
24
24
  return `${cli}, then /login`;
25
25
  case 'codex':
26
26
  case 'grok':
27
+ // Warp Agent CLI: `oz login` opens a browser sign-in (or set WARP_API_KEY).
28
+ case 'warp':
27
29
  return `${cli} login`;
28
30
  case 'opencode':
29
31
  return `${cli} auth login`;
@@ -51,6 +51,7 @@ export declare const loadFork: ModuleLoader;
51
51
  export declare const loadDefaults: ModuleLoader;
52
52
  export declare const loadSet: ModuleLoader;
53
53
  export declare const loadModels: ModuleLoader;
54
+ export declare const loadModes: ModuleLoader;
54
55
  export declare const loadPrune: ModuleLoader;
55
56
  export declare const loadTrash: ModuleLoader;
56
57
  export declare const loadRestore: ModuleLoader;
@@ -130,3 +131,18 @@ export declare const LAZY_COMMAND_NAMES: ReadonlySet<string>;
130
131
  * are handled directly in src/index.ts.
131
132
  */
132
133
  export declare const COMMAND_LOADERS: Record<string, ModuleLoader[]>;
134
+ /**
135
+ * Every top-level command name the CLI answers to — the loader table plus the
136
+ * inline aliases/tombstones above. This is the "does this command exist?"
137
+ * predicate for code that runs BEFORE commander parses, most importantly the
138
+ * `--host`/`--device` router (lib/hosts/passthrough.ts): without it a typo'd
139
+ * command carrying `--host` reported a flag-support error instead of
140
+ * `unknown command` (RUSH-2022).
141
+ *
142
+ * Commander sub-aliases (`sessions ls`, `teams rm`, …) are deliberately absent —
143
+ * this set is top-level only. `command-registry.test.ts` pins it against the real
144
+ * registered command tree so a new command can never drift out of it.
145
+ */
146
+ export declare const KNOWN_TOP_LEVEL_COMMANDS: ReadonlySet<string>;
147
+ /** Whether `name` is a top-level command this CLI registers. See {@link KNOWN_TOP_LEVEL_COMMANDS}. */
148
+ export declare function isKnownTopLevelCommand(name: string): boolean;