@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.
- package/CHANGELOG.md +209 -0
- package/README.md +8 -2
- package/dist/commands/doctor.js +15 -7
- package/dist/commands/exec.js +20 -6
- package/dist/commands/focus.d.ts +76 -4
- package/dist/commands/focus.js +219 -40
- package/dist/commands/fork.d.ts +20 -2
- package/dist/commands/fork.js +91 -64
- package/dist/commands/go.d.ts +25 -0
- package/dist/commands/go.js +63 -2
- package/dist/commands/harness-wizard.d.ts +206 -0
- package/dist/commands/harness-wizard.js +403 -0
- package/dist/commands/harness.d.ts +12 -0
- package/dist/commands/harness.js +97 -101
- package/dist/commands/resume.js +10 -3
- package/dist/commands/secrets.js +25 -30
- package/dist/commands/sessions-resume.d.ts +21 -3
- package/dist/commands/sessions-resume.js +50 -11
- package/dist/commands/sessions.d.ts +81 -5
- package/dist/commands/sessions.js +325 -66
- package/dist/commands/watchdog.js +13 -2
- package/dist/lib/agents.d.ts +1 -1
- package/dist/lib/agents.js +132 -0
- package/dist/lib/codex-policy.d.ts +17 -0
- package/dist/lib/codex-policy.js +48 -0
- package/dist/lib/crabbox/lease.d.ts +25 -0
- package/dist/lib/crabbox/lease.js +62 -0
- package/dist/lib/daemon.js +70 -0
- package/dist/lib/exec.d.ts +4 -0
- package/dist/lib/exec.js +88 -54
- package/dist/lib/feed-broadcast.d.ts +1 -20
- package/dist/lib/feed-broadcast.js +31 -1
- package/dist/lib/hooks.js +12 -2
- package/dist/lib/mcp.js +44 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
- package/dist/lib/models.d.ts +0 -5
- package/dist/lib/models.js +48 -0
- package/dist/lib/plugin-marketplace.js +9 -0
- package/dist/lib/pricing/prices.json +119 -92
- package/dist/lib/pricing/table.js +13 -0
- package/dist/lib/remote-agents-json.d.ts +29 -1
- package/dist/lib/remote-agents-json.js +47 -10
- package/dist/lib/resources/mcp.js +2 -0
- package/dist/lib/resources/permissions.js +3 -0
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/runner.js +28 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +118 -49
- package/dist/lib/secrets/reaper.d.ts +87 -0
- package/dist/lib/secrets/reaper.js +184 -0
- package/dist/lib/secrets/remote.d.ts +29 -0
- package/dist/lib/secrets/remote.js +37 -1
- package/dist/lib/session/active.d.ts +36 -1
- package/dist/lib/session/active.js +60 -19
- package/dist/lib/session/actor-sidecar.d.ts +14 -0
- package/dist/lib/session/actor-sidecar.js +67 -4
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +32 -1
- package/dist/lib/session/discover.js +168 -0
- package/dist/lib/session/parse.d.ts +10 -0
- package/dist/lib/session/parse.js +98 -0
- package/dist/lib/session/remote-list.d.ts +10 -1
- package/dist/lib/session/remote-list.js +2 -8
- package/dist/lib/session/remote.d.ts +57 -3
- package/dist/lib/session/remote.js +90 -26
- package/dist/lib/session/resume-command.d.ts +6 -0
- package/dist/lib/session/resume-command.js +8 -0
- package/dist/lib/session/session-cache.d.ts +173 -0
- package/dist/lib/session/session-cache.js +399 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/width.d.ts +1 -1
- package/dist/lib/session/width.js +12 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +40 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/usage.d.ts +13 -0
- package/dist/lib/usage.js +215 -0
- package/dist/lib/versions.js +13 -2
- package/package.json +1 -1
- package/dist/bin/agents +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- 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
|
-
/**
|
|
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
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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 =
|
|
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' ?
|
|
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 =
|
|
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' ?
|
|
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
|
-
:
|
|
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 };
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -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;
|