@phnx-labs/agents-cli 1.22.22 → 1.22.23

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 (91) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +8 -2
  3. package/dist/commands/doctor.js +15 -7
  4. package/dist/commands/exec.js +20 -6
  5. package/dist/commands/focus.d.ts +76 -4
  6. package/dist/commands/focus.js +219 -40
  7. package/dist/commands/fork.d.ts +20 -2
  8. package/dist/commands/fork.js +91 -64
  9. package/dist/commands/go.d.ts +25 -0
  10. package/dist/commands/go.js +63 -2
  11. package/dist/commands/harness-wizard.d.ts +206 -0
  12. package/dist/commands/harness-wizard.js +403 -0
  13. package/dist/commands/harness.d.ts +12 -0
  14. package/dist/commands/harness.js +97 -101
  15. package/dist/commands/resume.js +10 -3
  16. package/dist/commands/secrets.js +25 -30
  17. package/dist/commands/sessions-resume.d.ts +21 -3
  18. package/dist/commands/sessions-resume.js +50 -11
  19. package/dist/commands/sessions.d.ts +81 -5
  20. package/dist/commands/sessions.js +325 -66
  21. package/dist/commands/watchdog.js +13 -2
  22. package/dist/lib/agents.d.ts +1 -1
  23. package/dist/lib/agents.js +132 -0
  24. package/dist/lib/codex-policy.d.ts +17 -0
  25. package/dist/lib/codex-policy.js +48 -0
  26. package/dist/lib/crabbox/lease.d.ts +25 -0
  27. package/dist/lib/crabbox/lease.js +62 -0
  28. package/dist/lib/daemon.js +70 -0
  29. package/dist/lib/exec.d.ts +4 -0
  30. package/dist/lib/exec.js +88 -54
  31. package/dist/lib/feed-broadcast.d.ts +1 -20
  32. package/dist/lib/feed-broadcast.js +31 -1
  33. package/dist/lib/hooks.js +12 -2
  34. package/dist/lib/mcp.js +44 -0
  35. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
  36. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  37. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
  38. package/dist/lib/models.d.ts +0 -5
  39. package/dist/lib/models.js +48 -0
  40. package/dist/lib/plugin-marketplace.js +9 -0
  41. package/dist/lib/pricing/prices.json +119 -92
  42. package/dist/lib/pricing/table.js +13 -0
  43. package/dist/lib/remote-agents-json.d.ts +29 -1
  44. package/dist/lib/remote-agents-json.js +47 -10
  45. package/dist/lib/resources/mcp.js +2 -0
  46. package/dist/lib/resources/permissions.js +3 -0
  47. package/dist/lib/resources/types.d.ts +2 -1
  48. package/dist/lib/runner.js +28 -19
  49. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  50. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  51. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  52. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  53. package/dist/lib/secrets/index.d.ts +15 -1
  54. package/dist/lib/secrets/index.js +118 -49
  55. package/dist/lib/secrets/reaper.d.ts +87 -0
  56. package/dist/lib/secrets/reaper.js +184 -0
  57. package/dist/lib/secrets/remote.d.ts +29 -0
  58. package/dist/lib/secrets/remote.js +37 -1
  59. package/dist/lib/session/active.d.ts +36 -1
  60. package/dist/lib/session/active.js +60 -19
  61. package/dist/lib/session/actor-sidecar.d.ts +14 -0
  62. package/dist/lib/session/actor-sidecar.js +67 -4
  63. package/dist/lib/session/db.d.ts +1 -1
  64. package/dist/lib/session/db.js +32 -1
  65. package/dist/lib/session/discover.js +168 -0
  66. package/dist/lib/session/parse.d.ts +10 -0
  67. package/dist/lib/session/parse.js +98 -0
  68. package/dist/lib/session/remote-list.d.ts +10 -1
  69. package/dist/lib/session/remote-list.js +2 -8
  70. package/dist/lib/session/remote.d.ts +57 -3
  71. package/dist/lib/session/remote.js +90 -26
  72. package/dist/lib/session/resume-command.d.ts +6 -0
  73. package/dist/lib/session/resume-command.js +8 -0
  74. package/dist/lib/session/session-cache.d.ts +173 -0
  75. package/dist/lib/session/session-cache.js +399 -0
  76. package/dist/lib/session/types.d.ts +1 -1
  77. package/dist/lib/session/types.js +1 -1
  78. package/dist/lib/session/width.d.ts +1 -1
  79. package/dist/lib/session/width.js +12 -2
  80. package/dist/lib/shims.d.ts +2 -2
  81. package/dist/lib/shims.js +40 -5
  82. package/dist/lib/staleness/writers/hooks.js +1 -1
  83. package/dist/lib/types.d.ts +1 -1
  84. package/dist/lib/usage.d.ts +13 -0
  85. package/dist/lib/usage.js +215 -0
  86. package/dist/lib/versions.js +13 -2
  87. package/package.json +1 -1
  88. package/dist/bin/agents +0 -0
  89. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  90. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  91. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
@@ -0,0 +1,399 @@
1
+ /**
2
+ * Daemon-warmed cross-surface cache for live session status (RUSH-2062).
3
+ *
4
+ * Problem: menubar / Factory / watchdog / CLI each independently run a full
5
+ * `sessions --active` gather (~9s wall / ~170MB) with no sharing. Measured on
6
+ * zion: back-to-back runs were 9.22s then 8.16s — no speedup, full SSH fan-out
7
+ * every call.
8
+ *
9
+ * Pattern mirrors {@link ../devices/stats-cache.ts} / {@link ../fleet-status.ts}:
10
+ *
11
+ * - **Daemon warm:** each daemon publishes THIS host's local active sessions
12
+ * on a short tick (`publishLocalActiveSessions`). Publish-own only — no
13
+ * cross-host SSH from the daemon (avoids the N² fan-out of RUSH-2061).
14
+ * - **Readers** (`loadLocalActiveSessions`, fleet path in `gatherActiveSessions`)
15
+ * serve the warm snapshot when it is within {@link DEFAULT_ACTIVE_CACHE_MAX_AGE_MS};
16
+ * `forceRefresh` / age expiry re-gathers live.
17
+ * - **Immutable memo:** per-session fields that only change when the transcript
18
+ * does (topic, label, cwd, …) are keyed on `(sessionId, transcriptMtimeMs)`.
19
+ * Live status fields (`status`, `activity`, `preview`, `pidAlive`, …) are
20
+ * NEVER stored in that memo — they only ride the short-lived snapshot.
21
+ *
22
+ * Best-effort disk IO: a missing/corrupt cache never throws; the caller falls
23
+ * through to a live gather.
24
+ */
25
+ import * as fs from 'fs';
26
+ import * as path from 'path';
27
+ import { getCacheDir } from '../state.js';
28
+ /** Snapshot file under `getCacheDir()` (regenerable, gitignored). */
29
+ const SNAPSHOT_FILE = '.active-sessions.json';
30
+ /** Immutable-field memo file (keyed by sessionId + transcript mtime). */
31
+ const IMMUTABLE_FILE = '.active-session-immutable.json';
32
+ /**
33
+ * How long a snapshot may be served before a reader re-gathers.
34
+ * Short on purpose: live status (running/idle/waiting) must not go stale.
35
+ * The daemon warm tick uses the same cadence (see {@link SESSION_CACHE_WARM_INTERVAL_MS}).
36
+ */
37
+ export const DEFAULT_ACTIVE_CACHE_MAX_AGE_MS = 15_000;
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;
42
+ /** Keys stored in the immutable memo (transcript-stable). */
43
+ export const IMMUTABLE_FIELD_KEYS = [
44
+ 'topic',
45
+ 'label',
46
+ 'name',
47
+ 'cwd',
48
+ 'project',
49
+ 'attachments',
50
+ 'startedAtMs',
51
+ 'version',
52
+ 'pr',
53
+ 'worktree',
54
+ 'ticket',
55
+ 'createdTickets',
56
+ 'spawnedTeam',
57
+ 'sessionFile',
58
+ 'owner',
59
+ 'assignedTask',
60
+ 'kind',
61
+ 'context',
62
+ ];
63
+ /**
64
+ * Live / volatile fields that MUST NOT be served from the immutable memo.
65
+ * They either change without a transcript write (pid death, attach state) or
66
+ * are short-window signals (preview, tok/s). The short snapshot TTL is the
67
+ * only cache that may carry them — and only as a whole-row snapshot.
68
+ */
69
+ export const LIVE_STATUS_KEYS = [
70
+ 'status',
71
+ 'activity',
72
+ 'preview',
73
+ 'tokPerSec',
74
+ 'awaitingReason',
75
+ 'question',
76
+ 'todos',
77
+ 'tail',
78
+ 'lastActivityMs',
79
+ 'hostLink',
80
+ 'presence',
81
+ 'pidAlive',
82
+ 'tmuxClients',
83
+ 'windowHeartbeatMs',
84
+ 'provenance',
85
+ 'rateLimited',
86
+ 'plan',
87
+ ];
88
+ // ── path overrides (test seam) ─────────────────────────────────────────────
89
+ let snapshotPathOverride = null;
90
+ let immutablePathOverride = null;
91
+ /** Test seam: redirect the snapshot file. Returns the previous override. */
92
+ export function setActiveSessionsSnapshotPathForTest(p) {
93
+ const prev = snapshotPathOverride;
94
+ snapshotPathOverride = p;
95
+ return prev;
96
+ }
97
+ /** Test seam: redirect the immutable-memo file. Returns the previous override. */
98
+ export function setImmutableMemoPathForTest(p) {
99
+ const prev = immutablePathOverride;
100
+ immutablePathOverride = p;
101
+ return prev;
102
+ }
103
+ function snapshotPath() {
104
+ return snapshotPathOverride ?? path.join(getCacheDir(), SNAPSHOT_FILE);
105
+ }
106
+ function immutablePath() {
107
+ return immutablePathOverride ?? path.join(getCacheDir(), IMMUTABLE_FILE);
108
+ }
109
+ // ── snapshot read / write ──────────────────────────────────────────────────
110
+ /** Read one scope from the snapshot file (best-effort; missing/corrupt → null). */
111
+ export function readActiveSessionsCache(scope) {
112
+ try {
113
+ const parsed = JSON.parse(fs.readFileSync(snapshotPath(), 'utf-8'));
114
+ if (!parsed || parsed.version !== 1 || !parsed.entries)
115
+ return null;
116
+ const entry = parsed.entries[scope];
117
+ if (!entry || !Array.isArray(entry.sessions) || typeof entry.capturedAt !== 'number')
118
+ return null;
119
+ return entry;
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
125
+ /**
126
+ * Persist a snapshot for one scope (best-effort). Other scopes are preserved
127
+ * so a local warm never drops a fleet snapshot a reader just wrote.
128
+ */
129
+ export function writeActiveSessionsCache(scope, sessions, opts = {}) {
130
+ const snap = {
131
+ version: 1,
132
+ scope,
133
+ capturedAt: opts.capturedAt ?? Date.now(),
134
+ sessions,
135
+ ...(opts.remoteDeviceCount !== undefined ? { remoteDeviceCount: opts.remoteDeviceCount } : {}),
136
+ };
137
+ try {
138
+ const dir = path.dirname(snapshotPath());
139
+ if (!fs.existsSync(dir))
140
+ fs.mkdirSync(dir, { recursive: true });
141
+ let entries = {};
142
+ try {
143
+ const prev = JSON.parse(fs.readFileSync(snapshotPath(), 'utf-8'));
144
+ if (prev?.entries && typeof prev.entries === 'object')
145
+ entries = { ...prev.entries };
146
+ }
147
+ catch {
148
+ // empty
149
+ }
150
+ entries[scope] = snap;
151
+ const body = { version: 1, entries };
152
+ // Atomic replace so a concurrent reader never sees a partial write.
153
+ const tmp = `${snapshotPath()}.tmp`;
154
+ fs.writeFileSync(tmp, JSON.stringify(body));
155
+ fs.renameSync(tmp, snapshotPath());
156
+ }
157
+ catch {
158
+ // best-effort
159
+ }
160
+ return snap;
161
+ }
162
+ /**
163
+ * True when a snapshot is still within the freshness window. Pure — the
164
+ * staleness invariant for live status lives here.
165
+ */
166
+ export function isActiveSnapshotFresh(capturedAt, nowMs, maxAgeMs = DEFAULT_ACTIVE_CACHE_MAX_AGE_MS) {
167
+ if (!Number.isFinite(capturedAt) || !Number.isFinite(maxAgeMs))
168
+ return false;
169
+ if (maxAgeMs < 0)
170
+ return false;
171
+ return nowMs - capturedAt <= maxAgeMs;
172
+ }
173
+ // ── immutable memo ─────────────────────────────────────────────────────────
174
+ /** Pull only the transcript-stable fields from a live row. */
175
+ export function pickImmutableFields(s) {
176
+ const out = {};
177
+ for (const k of IMMUTABLE_FIELD_KEYS) {
178
+ const v = s[k];
179
+ if (v !== undefined)
180
+ out[k] = v;
181
+ }
182
+ return out;
183
+ }
184
+ /**
185
+ * Transcript mtime used as the memo key. Prefer {@link ActiveSession.lastActivityMs}
186
+ * (the transcript's last write); fall back to `startedAtMs` when no activity
187
+ * stamp exists. Returns null when neither is known — caller must not memoize.
188
+ */
189
+ export function transcriptMtimeMs(s) {
190
+ if (typeof s.lastActivityMs === 'number' && Number.isFinite(s.lastActivityMs))
191
+ return s.lastActivityMs;
192
+ if (typeof s.startedAtMs === 'number' && Number.isFinite(s.startedAtMs))
193
+ return s.startedAtMs;
194
+ return null;
195
+ }
196
+ function readImmutableFile() {
197
+ try {
198
+ const parsed = JSON.parse(fs.readFileSync(immutablePath(), 'utf-8'));
199
+ if (parsed && parsed.version === 1 && parsed.entries && typeof parsed.entries === 'object') {
200
+ return parsed;
201
+ }
202
+ }
203
+ catch {
204
+ // missing/corrupt
205
+ }
206
+ return { version: 1, entries: {} };
207
+ }
208
+ /**
209
+ * Read memoized immutable fields for `(sessionId, mtimeMs)`.
210
+ * Returns null when missing OR when the stored mtime does not match — a
211
+ * transcript rewrite must force re-derivation of topic/label/etc.
212
+ */
213
+ export function readImmutableMemo(sessionId, mtimeMs) {
214
+ if (!sessionId || !Number.isFinite(mtimeMs))
215
+ return null;
216
+ const file = readImmutableFile();
217
+ const entry = file.entries[sessionId];
218
+ if (!entry || entry.mtimeMs !== mtimeMs)
219
+ return null;
220
+ // Defence in depth: strip any live-status key that snuck into a bad write.
221
+ return stripLiveStatusKeys({ ...entry.fields });
222
+ }
223
+ /** Persist immutable fields for `(sessionId, mtimeMs)`. Live keys are stripped. */
224
+ export function writeImmutableMemo(sessionId, mtimeMs, fields, nowMs = Date.now()) {
225
+ if (!sessionId || !Number.isFinite(mtimeMs))
226
+ return;
227
+ try {
228
+ const dir = path.dirname(immutablePath());
229
+ if (!fs.existsSync(dir))
230
+ fs.mkdirSync(dir, { recursive: true });
231
+ const file = readImmutableFile();
232
+ file.entries[sessionId] = {
233
+ mtimeMs,
234
+ fields: stripLiveStatusKeys({ ...fields }),
235
+ writtenAt: nowMs,
236
+ };
237
+ const tmp = `${immutablePath()}.tmp`;
238
+ fs.writeFileSync(tmp, JSON.stringify(file));
239
+ fs.renameSync(tmp, immutablePath());
240
+ }
241
+ catch {
242
+ // best-effort
243
+ }
244
+ }
245
+ /** Drop every live-status key from a field bag (defence in depth). */
246
+ export function stripLiveStatusKeys(fields) {
247
+ const out = { ...fields };
248
+ for (const k of LIVE_STATUS_KEYS) {
249
+ if (k in out)
250
+ delete out[k];
251
+ }
252
+ return out;
253
+ }
254
+ /**
255
+ * True when `fields` contains no live-status key. The invariant the tests pin:
256
+ * the immutable memo never carries status/activity/preview/etc.
257
+ */
258
+ export function assertNoLiveStatusFields(fields) {
259
+ for (const k of LIVE_STATUS_KEYS) {
260
+ if (k in fields && fields[k] !== undefined)
261
+ return false;
262
+ }
263
+ return true;
264
+ }
265
+ /**
266
+ * Write immutable memos for every session that has an id + transcript mtime.
267
+ * Called after a live gather so the next gather (when mtime is unchanged) can
268
+ * refill identity fields without re-deriving them.
269
+ */
270
+ export function updateImmutableMemos(sessions, nowMs = Date.now()) {
271
+ for (const s of sessions) {
272
+ if (!s.sessionId)
273
+ continue;
274
+ const mtime = transcriptMtimeMs(s);
275
+ if (mtime === null)
276
+ continue;
277
+ writeImmutableMemo(s.sessionId, mtime, pickImmutableFields(s), nowMs);
278
+ }
279
+ }
280
+ /**
281
+ * Fill missing immutable fields on a live row from the memo when the transcript
282
+ * mtime matches. Never overwrites a field the live gather already set, and
283
+ * never copies live-status keys.
284
+ */
285
+ export function applyImmutableMemo(s) {
286
+ if (!s.sessionId)
287
+ return s;
288
+ const mtime = transcriptMtimeMs(s);
289
+ if (mtime === null)
290
+ return s;
291
+ const memo = readImmutableMemo(s.sessionId, mtime);
292
+ if (!memo)
293
+ return s;
294
+ for (const k of IMMUTABLE_FIELD_KEYS) {
295
+ if (s[k] === undefined && memo[k] !== undefined) {
296
+ s[k] = memo[k];
297
+ }
298
+ }
299
+ return s;
300
+ }
301
+ /**
302
+ * Cache-first load of THIS host's active sessions. Default path serves the
303
+ * daemon-warmed snapshot when fresh; `forceRefresh` or an expired snapshot
304
+ * re-gathers and rewrites the cache.
305
+ */
306
+ export async function loadLocalActiveSessions(opts = {}) {
307
+ const now = opts.nowMs ?? Date.now();
308
+ const maxAge = opts.maxAgeMs ?? DEFAULT_ACTIVE_CACHE_MAX_AGE_MS;
309
+ const readCache = opts.readCache ?? readActiveSessionsCache;
310
+ const writeCache = opts.writeCache ?? writeActiveSessionsCache;
311
+ if (!opts.forceRefresh) {
312
+ const cached = readCache('local');
313
+ if (cached && isActiveSnapshotFresh(cached.capturedAt, now, maxAge)) {
314
+ return {
315
+ sessions: cached.sessions,
316
+ servedFromCache: true,
317
+ capturedAt: cached.capturedAt,
318
+ };
319
+ }
320
+ }
321
+ const gather = opts.gather ??
322
+ (async () => {
323
+ const { getActiveSessions } = await import('./active.js');
324
+ return getActiveSessions({ localOnly: true });
325
+ });
326
+ const sessions = await gather();
327
+ for (const s of sessions)
328
+ applyImmutableMemo(s);
329
+ updateImmutableMemos(sessions, now);
330
+ const snap = writeCache('local', sessions, { capturedAt: now });
331
+ return { sessions, servedFromCache: false, capturedAt: snap.capturedAt };
332
+ }
333
+ /**
334
+ * Cache-first load of the fleet-wide active set. A prior gather (or any surface
335
+ * that just paid the SSH cost) leaves a fleet snapshot; subsequent menubar /
336
+ * Factory / CLI / watchdog calls within the freshness window share it.
337
+ */
338
+ export async function loadFleetActiveSessions(opts) {
339
+ const now = opts.nowMs ?? Date.now();
340
+ const maxAge = opts.maxAgeMs ?? DEFAULT_ACTIVE_CACHE_MAX_AGE_MS;
341
+ const readCache = opts.readCache ?? readActiveSessionsCache;
342
+ const writeCache = opts.writeCache ?? writeActiveSessionsCache;
343
+ if (!opts.forceRefresh) {
344
+ const cached = readCache('fleet');
345
+ if (cached && isActiveSnapshotFresh(cached.capturedAt, now, maxAge)) {
346
+ return {
347
+ sessions: cached.sessions,
348
+ remoteDeviceCount: cached.remoteDeviceCount ?? 0,
349
+ servedFromCache: true,
350
+ capturedAt: cached.capturedAt,
351
+ };
352
+ }
353
+ }
354
+ const live = await opts.gather();
355
+ for (const s of live.sessions)
356
+ applyImmutableMemo(s);
357
+ updateImmutableMemos(live.sessions, now);
358
+ const snap = writeCache('fleet', live.sessions, {
359
+ capturedAt: now,
360
+ remoteDeviceCount: live.remoteDeviceCount,
361
+ });
362
+ // Also refresh the local slice so a `--local` reader benefits from this gather.
363
+ // Identity is machineId() when available; fall back to "rows with no machine
364
+ // stamp" so a pure-local gather that never set machine still warms the cache.
365
+ let self;
366
+ try {
367
+ const { machineId } = await import('../machine-id.js');
368
+ self = machineId();
369
+ }
370
+ catch {
371
+ self = undefined;
372
+ }
373
+ // Always rewrite the local slice when we know self — including the empty
374
+ // case. If the last local session just died, leaving a still-fresh snapshot
375
+ // with ghost rows would make watchdog / `--local` report sessions that are
376
+ // gone (review on RUSH-2062 / PR #2116).
377
+ if (self !== undefined) {
378
+ const localOnly = live.sessions.filter((s) => !s.machine || s.machine === self);
379
+ writeCache('local', localOnly, { capturedAt: now });
380
+ }
381
+ return {
382
+ sessions: live.sessions,
383
+ remoteDeviceCount: live.remoteDeviceCount,
384
+ servedFromCache: false,
385
+ capturedAt: snap.capturedAt,
386
+ };
387
+ }
388
+ /**
389
+ * Daemon warm entry point: live-gather THIS host and write the local snapshot.
390
+ * Never SSHes. Returns the published row count for the daemon log line.
391
+ */
392
+ export async function publishLocalActiveSessions(opts = {}) {
393
+ const result = await loadLocalActiveSessions({
394
+ forceRefresh: true,
395
+ gather: opts.gather,
396
+ nowMs: opts.nowMs,
397
+ });
398
+ return { sessions: result.sessions, capturedAt: result.capturedAt };
399
+ }
@@ -7,7 +7,7 @@
7
7
  * speaks these types.
8
8
  */
9
9
  /** Agents that store session data on disk and can be discovered by `agents sessions`. */
10
- export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'antigravity' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi' | 'droid' | 'cursor';
10
+ export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'antigravity' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi' | 'droid' | 'cursor' | 'muse';
11
11
  /** Effective permissions mode used to launch a managed agent session. */
12
12
  export type SessionRunMode = 'plan' | 'edit' | 'auto' | 'skip';
13
13
  /** All agents with session discovery support, in display order. */
@@ -7,7 +7,7 @@
7
7
  * speaks these types.
8
8
  */
9
9
  /** All agents with session discovery support, in display order. */
10
- export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'antigravity', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi', 'droid', 'cursor'];
10
+ export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'antigravity', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi', 'droid', 'cursor', 'muse'];
11
11
  /**
12
12
  * True when `agent` stores session data `agents sessions` can discover (a member
13
13
  * of {@link SESSION_AGENTS}). The single predicate every session-index writer
@@ -8,7 +8,7 @@
8
8
  * cell measures and truncates through this module so alignment is computed once,
9
9
  * correctly, from the same source of truth.
10
10
  */
11
- /** Strip SGR colour escapes so width is measured on visible characters only. */
11
+ /** Strip SGR colour + OSC-8 hyperlink escapes so width is measured on visible characters only. */
12
12
  export declare function stripAnsi(s: string): string;
13
13
  /** Visible display width of a string, ANSI-aware and wide-char-aware. */
14
14
  export declare function stringWidth(s: string): number;
@@ -10,9 +10,19 @@
10
10
  */
11
11
  /** SGR colour sequences emitted by chalk (e.g. `\x1b[32m`). */
12
12
  const SGR_REGEX = /\x1b\[[0-9;]*m/g;
13
- /** Strip SGR colour escapes so width is measured on visible characters only. */
13
+ /**
14
+ * OSC-8 hyperlink sequences (`\x1b]8;;<url>\x1b\\` … `\x1b]8;;\x1b\\`, or with a
15
+ * BEL `\x07` terminator) emitted by `osc8()` in render.ts for clickable
16
+ * ticket/PR/path badges. These carry the URL, which is NOT visible — leaving it
17
+ * in over-counts width and, worse, lets a truncation land *inside* the escape and
18
+ * corrupt it. Stripping both the opener and closer leaves only the visible label
19
+ * between them, so width is measured on what the user sees and `truncateToWidth`
20
+ * can never cut a hyperlink in half.
21
+ */
22
+ const OSC8_REGEX = /\x1b\]8;[^;]*;.*?(?:\x1b\\|\x07)/g;
23
+ /** Strip SGR colour + OSC-8 hyperlink escapes so width is measured on visible characters only. */
14
24
  export function stripAnsi(s) {
15
- return s.replace(SGR_REGEX, '');
25
+ return s.replace(OSC8_REGEX, '').replace(SGR_REGEX, '');
16
26
  }
17
27
  /**
18
28
  * Display cells for one code point: 0 for zero-width combining/ZWJ/variation
@@ -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 = 27;
80
+ export declare const SHIM_SCHEMA_VERSION = 28;
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 = 13;
163
+ export declare const VERSIONED_ALIAS_SCHEMA_VERSION = 14;
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
@@ -18,6 +18,7 @@ import { getShimsDir, getVersionsDir, getBackupsDir, getHistoryDir, ensureAgents
18
18
  export { getShimsDir };
19
19
  import { AGENTS, agentConfigDirName, readAuthAccountIdentity } from './agents.js';
20
20
  import { codexHomeShimBash } from './codex-home.js';
21
+ import { codexPolicyArgs } from './codex-policy.js';
21
22
  /**
22
23
  * Files and directories to always skip during conflict detection and migration.
23
24
  * These are never user config that should be migrated.
@@ -222,12 +223,19 @@ async function promptConflictStrategy(conflictInfos) {
222
223
  // The old dispatcher checked only the global dir, so a pinned grok that
223
224
  // installed into the versioned home fell through to the "not installed"
224
225
  // error.
225
- export const SHIM_SCHEMA_VERSION = 27;
226
+ export const SHIM_SCHEMA_VERSION = 28;
226
227
  /** Internal marker string used to embed the schema version in shim scripts. */
227
228
  const SHIM_VERSION_MARKER = 'agents-shim-version:';
228
229
  function shellQuote(value) {
229
230
  return `'${value.replace(/'/g, `'\\''`)}'`;
230
231
  }
232
+ function codexShimLaunchArgs() {
233
+ return [
234
+ '-c',
235
+ 'check_for_update_on_startup=false',
236
+ ...codexPolicyArgs('edit'),
237
+ ].map(shellQuote).join(' ');
238
+ }
231
239
  function getAgentsBinForGeneratedShim() {
232
240
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'index.js');
233
241
  }
@@ -292,7 +300,7 @@ export OPENCODE_CONFIG_DIR="$VERSION_DIR/home/.config/opencode"
292
300
  export KIMI_CODE_HOME="$VERSION_DIR/home/${configDirName}"
293
301
  `
294
302
  : '';
295
- const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
303
+ const launchArgs = agent === 'codex' ? ` ${codexShimLaunchArgs()}` : '';
296
304
  return `#!/bin/bash
297
305
  # Auto-generated by agents-cli - do not edit
298
306
  # Shim for ${agentConfig.name}
@@ -549,6 +557,22 @@ elif [ "$AGENT" = "droid" ]; then
549
557
  esac
550
558
  fi
551
559
  fi
560
+ elif [ "$AGENT" = "muse" ]; then
561
+ # Muse Code installs a self-updating launcher at ~/.local/bin/muse (curl
562
+ # installer from dev.meta.ai). No npm package. Same shims-dir re-exec guard
563
+ # as droid.
564
+ BINARY=$(adopted_original_bin || echo "")
565
+ if [ -z "$BINARY" ]; then
566
+ MUSE_BINARY="$HOME/.local/bin/muse"
567
+ if [ -x "$MUSE_BINARY" ] && [ "$(readlink -f "$MUSE_BINARY" 2>/dev/null)" != "$(readlink -f "$AGENTS_USER_DIR/.cache/shims/$CLI_COMMAND" 2>/dev/null)" ]; then
568
+ BINARY="$MUSE_BINARY"
569
+ else
570
+ BINARY=$(command -v muse 2>/dev/null || echo "")
571
+ case "$(readlink -f "$BINARY" 2>/dev/null)" in
572
+ "$AGENTS_USER_DIR/.cache/shims/"*) BINARY="" ;;
573
+ esac
574
+ fi
575
+ fi
552
576
  else
553
577
  BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
554
578
  fi
@@ -847,7 +871,7 @@ export function removeShim(agent) {
847
871
  * the versioned home (installer run with GROK_HOME set, or grok
848
872
  * self-update under the shim).
849
873
  */
850
- export const VERSIONED_ALIAS_SCHEMA_VERSION = 13;
874
+ export const VERSIONED_ALIAS_SCHEMA_VERSION = 14;
851
875
  /** Internal marker string used to embed the schema version in versioned alias scripts. */
852
876
  const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
853
877
  // The version string is interpolated into a generated bash script and into
@@ -933,7 +957,7 @@ export OPENCODE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/
933
957
  export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
934
958
  `
935
959
  : '';
936
- const launchArgs = agent === 'codex' ? ' -c check_for_update_on_startup=false' : '';
960
+ const launchArgs = agent === 'codex' ? ` ${codexShimLaunchArgs()}` : '';
937
961
  // Resolve the binary the same way the main shim does (see generateShimScript).
938
962
  // Grok and Droid do NOT ship into node_modules/.bin — Grok downloads a native
939
963
  // binary to ~/.grok/downloads and Droid (Factory AI) installs a standalone
@@ -987,7 +1011,18 @@ else
987
1011
  "$HOME/.agents/.cache/shims/"*) BINARY="" ;;
988
1012
  esac
989
1013
  fi`
990
- : `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
1014
+ : agent === 'muse'
1015
+ ? `# Muse Code installs a self-updating launcher at ~/.local/bin/muse.
1016
+ MUSE_BINARY="$HOME/.local/bin/muse"
1017
+ if [ -x "$MUSE_BINARY" ]; then
1018
+ BINARY="$MUSE_BINARY"
1019
+ else
1020
+ BINARY=$(command -v muse 2>/dev/null || echo "")
1021
+ case "$BINARY" in
1022
+ "$HOME/.agents/.cache/shims/"*) BINARY="" ;;
1023
+ esac
1024
+ fi`
1025
+ : `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
991
1026
  return `#!/bin/bash
992
1027
  # Auto-generated by agents-cli - do not edit
993
1028
  # ${VERSIONED_ALIAS_VERSION_MARKER} ${VERSIONED_ALIAS_SCHEMA_VERSION}
@@ -90,7 +90,7 @@ function buildHooksWriter(agent) {
90
90
  // registerHooksForGrok — file copy alone only sees top-level available.hooks
91
91
  // names (RUSH-1353). Copilot/Kiro/Goose load managed *.json under their
92
92
  // hooks dirs the same way.
93
- if (agent === 'claude' || agent === 'codex' || agent === 'antigravity' || agent === 'kimi' || agent === 'droid' || agent === 'copilot' || agent === 'kiro' || agent === 'goose' || agent === 'cursor' || agent === 'grok' || agent === 'hermes') {
93
+ if (agent === 'claude' || agent === 'codex' || agent === 'antigravity' || agent === 'kimi' || agent === 'droid' || agent === 'copilot' || agent === 'kiro' || agent === 'goose' || agent === 'cursor' || agent === 'grok' || agent === 'hermes' || agent === 'muse') {
94
94
  registerHooksToSettings(agent, versionHome);
95
95
  }
96
96
  return { synced };
@@ -8,7 +8,7 @@
8
8
  import type { CloudProviderId } from './cloud/types.js';
9
9
  import type { FeedBroadcastConfig } from './feed-broadcast.js';
10
10
  /** Unique identifier for a current or legacy AI coding agent. */
11
- export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi';
11
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi' | 'muse';
12
12
  /** How `agents run <agent>` chooses an installed version when none is pinned. */
13
13
  export type RunStrategy = 'pinned' | 'available' | 'balanced';
14
14
  /**
@@ -141,6 +141,10 @@ declare const USAGE_SOURCES: {
141
141
  readonly fetch: typeof getAntigravityUsageInfo;
142
142
  readonly network: true;
143
143
  };
144
+ readonly muse: {
145
+ readonly fetch: typeof getMuseUsageInfo;
146
+ readonly network: true;
147
+ };
144
148
  };
145
149
  export declare const USAGE_SOURCE_AGENT_IDS: (keyof typeof USAGE_SOURCES)[];
146
150
  /** Fetch usage info for a given agent through the canonical source registry. */
@@ -510,6 +514,15 @@ export declare function renderBar(usedPercent: number, length: number, minimumVi
510
514
  export declare function getUsageColor(usedPercent: number): (text: string) => string;
511
515
  /** Parse the latest billing info from Grok's unified log. */
512
516
  declare function getGrokUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
517
+ /**
518
+ * Muse Code usage.
519
+ *
520
+ * Prefer live Meta Model API rate-limit headers when a key is available
521
+ * (META_API_KEY / MODEL_API_KEY / ~/.config/muse/auth.json). Fall back to
522
+ * aggregating `model_completed.usage` from local session.jsonl logs under
523
+ * ~/.local/share/muse/sessions for a last-7-days token window.
524
+ */
525
+ declare function getMuseUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
513
526
  /** Per-model bucket in Cursor's /api/usage response. */
514
527
  interface CursorUsageModel {
515
528
  numRequests?: number | null;