@phnx-labs/agents-cli 1.22.22 → 1.22.24
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 +402 -0
- package/README.md +13 -6
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.js +20 -7
- package/dist/commands/exec.js +100 -34
- package/dist/commands/feed.d.ts +18 -0
- package/dist/commands/feed.js +44 -1
- package/dist/commands/focus.d.ts +108 -5
- package/dist/commands/focus.js +455 -32
- package/dist/commands/fork.d.ts +20 -2
- package/dist/commands/fork.js +91 -64
- package/dist/commands/go.d.ts +39 -1
- package/dist/commands/go.js +112 -7
- 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/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +6 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/resume.js +10 -3
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +79 -47
- package/dist/commands/sessions-browser.d.ts +35 -0
- package/dist/commands/sessions-browser.js +140 -14
- package/dist/commands/sessions-resume.d.ts +23 -3
- package/dist/commands/sessions-resume.js +73 -19
- package/dist/commands/sessions.d.ts +91 -5
- package/dist/commands/sessions.js +359 -125
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/view.js +2 -0
- package/dist/commands/watchdog.js +13 -2
- package/dist/index.js +2 -1
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- 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/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/exec.d.ts +16 -0
- package/dist/lib/exec.js +175 -62
- package/dist/lib/feed-broadcast.d.ts +1 -20
- package/dist/lib/feed-broadcast.js +31 -1
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +100 -40
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/mcp.js +44 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- 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/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/remote-agents-json.d.ts +29 -1
- package/dist/lib/remote-agents-json.js +47 -10
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- 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/resources.js +8 -5
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +50 -36
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.js +157 -65
- package/dist/lib/secrets/filestore.d.ts +5 -3
- package/dist/lib/secrets/filestore.js +12 -8
- package/dist/lib/secrets/index.d.ts +15 -1
- package/dist/lib/secrets/index.js +148 -53
- package/dist/lib/secrets/reaper.d.ts +97 -0
- package/dist/lib/secrets/reaper.js +219 -0
- package/dist/lib/secrets/remote.d.ts +29 -0
- package/dist/lib/secrets/remote.js +37 -1
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- 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/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -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 +62 -8
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +3 -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
|
@@ -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 = 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 =
|
|
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
|
@@ -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 = 29;
|
|
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
|
}
|
|
@@ -291,8 +299,19 @@ export OPENCODE_CONFIG_DIR="$VERSION_DIR/home/.config/opencode"
|
|
|
291
299
|
# mcp.json, sessions, skills, hooks). Point it at the versioned home.
|
|
292
300
|
export KIMI_CODE_HOME="$VERSION_DIR/home/${configDirName}"
|
|
293
301
|
`
|
|
294
|
-
: ''
|
|
295
|
-
|
|
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
|
+
: '';
|
|
314
|
+
const launchArgs = agent === 'codex' ? ` ${codexShimLaunchArgs()}` : '';
|
|
296
315
|
return `#!/bin/bash
|
|
297
316
|
# Auto-generated by agents-cli - do not edit
|
|
298
317
|
# Shim for ${agentConfig.name}
|
|
@@ -549,6 +568,22 @@ elif [ "$AGENT" = "droid" ]; then
|
|
|
549
568
|
esac
|
|
550
569
|
fi
|
|
551
570
|
fi
|
|
571
|
+
elif [ "$AGENT" = "muse" ]; then
|
|
572
|
+
# Muse Code installs a self-updating launcher at ~/.local/bin/muse (curl
|
|
573
|
+
# installer from dev.meta.ai). No npm package. Same shims-dir re-exec guard
|
|
574
|
+
# as droid.
|
|
575
|
+
BINARY=$(adopted_original_bin || echo "")
|
|
576
|
+
if [ -z "$BINARY" ]; then
|
|
577
|
+
MUSE_BINARY="$HOME/.local/bin/muse"
|
|
578
|
+
if [ -x "$MUSE_BINARY" ] && [ "$(readlink -f "$MUSE_BINARY" 2>/dev/null)" != "$(readlink -f "$AGENTS_USER_DIR/.cache/shims/$CLI_COMMAND" 2>/dev/null)" ]; then
|
|
579
|
+
BINARY="$MUSE_BINARY"
|
|
580
|
+
else
|
|
581
|
+
BINARY=$(command -v muse 2>/dev/null || echo "")
|
|
582
|
+
case "$(readlink -f "$BINARY" 2>/dev/null)" in
|
|
583
|
+
"$AGENTS_USER_DIR/.cache/shims/"*) BINARY="" ;;
|
|
584
|
+
esac
|
|
585
|
+
fi
|
|
586
|
+
fi
|
|
552
587
|
else
|
|
553
588
|
BINARY="$VERSION_DIR/node_modules/.bin/$CLI_COMMAND"
|
|
554
589
|
fi
|
|
@@ -847,7 +882,7 @@ export function removeShim(agent) {
|
|
|
847
882
|
* the versioned home (installer run with GROK_HOME set, or grok
|
|
848
883
|
* self-update under the shim).
|
|
849
884
|
*/
|
|
850
|
-
export const VERSIONED_ALIAS_SCHEMA_VERSION =
|
|
885
|
+
export const VERSIONED_ALIAS_SCHEMA_VERSION = 15;
|
|
851
886
|
/** Internal marker string used to embed the schema version in versioned alias scripts. */
|
|
852
887
|
const VERSIONED_ALIAS_VERSION_MARKER = 'agents-versioned-alias-version:';
|
|
853
888
|
// The version string is interpolated into a generated bash script and into
|
|
@@ -874,7 +909,7 @@ function assertSafeVersion(version) {
|
|
|
874
909
|
* KEEP IN SYNC with the `managedEnv` switch in `generateVersionedAliasScript`.
|
|
875
910
|
* The colocated test `shims.isolation-capability.test.ts` enforces this.
|
|
876
911
|
*/
|
|
877
|
-
export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'grok', 'kimi', 'opencode'];
|
|
912
|
+
export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'grok', 'kimi', 'opencode', 'muse'];
|
|
878
913
|
/**
|
|
879
914
|
* Whether an agent supports a clean `--isolated` install — i.e. its config
|
|
880
915
|
* location can be redirected by an env var so the isolated copy stays fully
|
|
@@ -932,8 +967,16 @@ export OPENCODE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/
|
|
|
932
967
|
# mcp.json, sessions, skills, hooks). Point direct aliases at the versioned home.
|
|
933
968
|
export KIMI_CODE_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
|
|
934
969
|
`
|
|
935
|
-
: ''
|
|
936
|
-
|
|
970
|
+
: agent === 'muse'
|
|
971
|
+
? `
|
|
972
|
+
# Muse Code: no dedicated config env var. Pin XDG so config/sessions live under
|
|
973
|
+
# the version home as real directories (not via the adopt symlink at
|
|
974
|
+
# ~/.config/muse, which Muse rejects with SymlinkOrReparse).
|
|
975
|
+
export XDG_CONFIG_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/.config"
|
|
976
|
+
export XDG_DATA_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/.local/share"
|
|
977
|
+
`
|
|
978
|
+
: '';
|
|
979
|
+
const launchArgs = agent === 'codex' ? ` ${codexShimLaunchArgs()}` : '';
|
|
937
980
|
// Resolve the binary the same way the main shim does (see generateShimScript).
|
|
938
981
|
// Grok and Droid do NOT ship into node_modules/.bin — Grok downloads a native
|
|
939
982
|
// binary to ~/.grok/downloads and Droid (Factory AI) installs a standalone
|
|
@@ -987,7 +1030,18 @@ else
|
|
|
987
1030
|
"$HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
988
1031
|
esac
|
|
989
1032
|
fi`
|
|
990
|
-
:
|
|
1033
|
+
: agent === 'muse'
|
|
1034
|
+
? `# Muse Code installs a self-updating launcher at ~/.local/bin/muse.
|
|
1035
|
+
MUSE_BINARY="$HOME/.local/bin/muse"
|
|
1036
|
+
if [ -x "$MUSE_BINARY" ]; then
|
|
1037
|
+
BINARY="$MUSE_BINARY"
|
|
1038
|
+
else
|
|
1039
|
+
BINARY=$(command -v muse 2>/dev/null || echo "")
|
|
1040
|
+
case "$BINARY" in
|
|
1041
|
+
"$HOME/.agents/.cache/shims/"*) BINARY="" ;;
|
|
1042
|
+
esac
|
|
1043
|
+
fi`
|
|
1044
|
+
: `BINARY="${versionDir}/node_modules/.bin/${agentConfig.cliCommand}"`;
|
|
991
1045
|
return `#!/bin/bash
|
|
992
1046
|
# Auto-generated by agents-cli - do not edit
|
|
993
1047
|
# ${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 };
|
|
@@ -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;
|
|
@@ -29,6 +29,7 @@ export const loadFork = async () => (await import('../../commands/fork.js')).reg
|
|
|
29
29
|
export const loadDefaults = async () => (await import('../../commands/defaults.js')).registerDefaultsCommands;
|
|
30
30
|
export const loadSet = async () => (await import('../../commands/set.js')).registerSetCommand;
|
|
31
31
|
export const loadModels = async () => (await import('../../commands/models.js')).registerModelsCommand;
|
|
32
|
+
export const loadModes = async () => (await import('../../commands/modes.js')).registerModesCommand;
|
|
32
33
|
export const loadPrune = async () => (await import('../../commands/prune.js')).registerPruneCommand;
|
|
33
34
|
export const loadTrash = async () => (await import('../../commands/trash.js')).registerTrashCommands;
|
|
34
35
|
export const loadRestore = async () => (await import('../../commands/trash.js')).registerRestoreCommand;
|
|
@@ -154,6 +155,7 @@ export const COMMAND_LOADERS = {
|
|
|
154
155
|
defaults: [loadDefaults],
|
|
155
156
|
set: [loadSet],
|
|
156
157
|
models: [loadModels],
|
|
158
|
+
modes: [loadModes],
|
|
157
159
|
trash: [loadTrash],
|
|
158
160
|
restore: [loadRestore],
|
|
159
161
|
doctor: [loadDoctor],
|
|
@@ -68,6 +68,11 @@ export interface RunUmbrellaArgs {
|
|
|
68
68
|
yes: boolean;
|
|
69
69
|
/** Secrets passphrase, if available (env var or prompt). Undefined => skip secrets. */
|
|
70
70
|
passphrase?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Suppress human progress from the reconcile stage (`refresh`). Required so
|
|
73
|
+
* `agents sync --json` / fleet fan-out leave stdout as a single JSON object.
|
|
74
|
+
*/
|
|
75
|
+
quiet?: boolean;
|
|
71
76
|
}
|
|
72
77
|
/**
|
|
73
78
|
* Execute the planned stages in order: repos -> secrets -> reconcile.
|