@dadado/agent-kit-cli 6.2.0 → 6.4.0

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 (45) hide show
  1. package/dashboard/dashboard-data.mjs +14 -2
  2. package/dashboard/dashboard.html +139 -4
  3. package/dashboard/lib/chat-sessions.d.mts +63 -0
  4. package/dashboard/lib/chat-sessions.mjs +430 -0
  5. package/dashboard/serve.mjs +5 -1
  6. package/dist/{add-Y2426TGU.js → add-HFDX3U3R.js} +2 -2
  7. package/dist/{chunk-TW4BF5OE.js → chunk-3OM7JODZ.js} +2 -2
  8. package/dist/{chunk-TGNFGMPV.js → chunk-6OTJEQEC.js} +90 -5
  9. package/dist/{chunk-MAPOQEA2.js → chunk-6RRG3WOE.js} +8 -1
  10. package/dist/{chunk-NSSXSZLL.js → chunk-7BEHUBEJ.js} +5 -1
  11. package/dist/{chunk-CWDXNBEU.js → chunk-ANPSYWQO.js} +2 -2
  12. package/dist/{chunk-BEKQ6PVD.js → chunk-AXLXEAVJ.js} +1 -1
  13. package/dist/{chunk-X45O2AIE.js → chunk-E465FAWM.js} +1 -1
  14. package/dist/{chunk-C2SJDYAM.js → chunk-H46W7NHC.js} +1 -1
  15. package/dist/{chunk-SPYIP76F.js → chunk-HHR56LJQ.js} +64 -28
  16. package/dist/{chunk-IR7FFPUU.js → chunk-IMIVTDMK.js} +43 -8
  17. package/dist/chunk-IRTS7WEA.js +374 -0
  18. package/dist/{chunk-MO63KZBP.js → chunk-KX3OUY4W.js} +8 -6
  19. package/dist/chunk-MWTXI6C5.js +223 -0
  20. package/dist/{chunk-JEI6HACW.js → chunk-NJVWTROP.js} +24 -377
  21. package/dist/{chunk-CER2APV2.js → chunk-Q4PCMRM5.js} +1 -1
  22. package/dist/{chunk-GUQYNUV4.js → chunk-R7DA4KXG.js} +1 -1
  23. package/dist/{chunk-VO73OL5V.js → chunk-SE7ZIRLX.js} +2 -2
  24. package/dist/{chunk-KVY425NM.js → chunk-TZ5XYIEF.js} +4 -2
  25. package/dist/{chunk-Z7ABX5QI.js → chunk-WOHZ4T5V.js} +4 -2
  26. package/dist/{chunk-YRFSTHR5.js → chunk-XEEB4YSA.js} +13 -219
  27. package/dist/{chunk-QTBTKEXK.js → chunk-ZPZVROS5.js} +1 -1
  28. package/dist/{contribute-TVST3CJO.js → contribute-PP7HMPW7.js} +3 -3
  29. package/dist/{countdown-QXJD6TDB.js → countdown-LAUZMI7U.js} +10 -8
  30. package/dist/{diff-MECE5RZB.js → diff-HYWC4G6Y.js} +4 -4
  31. package/dist/{doctor-VLZNXJNA.js → doctor-2X3HBRJJ.js} +8 -8
  32. package/dist/engine-info-4ZWEIHSX.js +89 -0
  33. package/dist/{ground-control-NMYYIVCV.js → ground-control-WKV2BMOW.js} +5 -4
  34. package/dist/{guard-EHXF76ID.js → guard-ZZRQW3DJ.js} +107 -8
  35. package/dist/{hook-XCZWX462.js → hook-UEYR4M3Z.js} +10 -4
  36. package/dist/index.js +26 -22
  37. package/dist/{init-HKMRU2V4.js → init-UF5VEIJX.js} +11 -11
  38. package/dist/{install-X2J6KWZ5.js → install-6ILVA22C.js} +11 -11
  39. package/dist/{run-R66G276Q.js → run-2EKD2BEC.js} +12 -9
  40. package/dist/{run-plan-24Z5K4P3.js → run-plan-G72XMC5Z.js} +11 -7
  41. package/dist/sessions-O7MYEMCT.js +343 -0
  42. package/dist/{setup-global-ADJFMOZX.js → setup-global-ZZEY5KWE.js} +5 -5
  43. package/dist/{status-UB6NL3Q3.js → status-4LK2QXVQ.js} +6 -6
  44. package/dist/{update-PFBFY7GT.js → update-FPR2NZND.js} +33 -43
  45. package/package.json +1 -1
@@ -0,0 +1,430 @@
1
+ /**
2
+ * Saved agent chat sessions for one workspace (the "Chats" list).
3
+ *
4
+ * Shared by `agent-kit sessions` (packages/cli) and Mission Control
5
+ * (dashboard-data.mjs). Colony reads the same contract natively, so the field
6
+ * names below are fixed:
7
+ * envelope: { vendor, status: "ok"|"unsupported"|"none", projectDir, sessions, total }
8
+ * session: { id, title, titleSource: "custom"|"ai"|"first-prompt"|"none", modified, sizeBytes }
9
+ * `status` is "none" when the workspace has no saved session (no folder, no HOME,
10
+ * unreadable folder, or an empty one), "unsupported" for a vendor this reader
11
+ * does not parse yet. `sessions` is newest first and capped; `total` counts all.
12
+ *
13
+ * Claude Code stores each session as ~/.claude/projects/<slug>/<uuid>.jsonl,
14
+ * one JSON record per line. Subagent transcripts live under <slug>/<uuid>/ and
15
+ * are not sessions. Read-only: this module never writes.
16
+ */
17
+ import {
18
+ closeSync,
19
+ existsSync,
20
+ fstatSync,
21
+ openSync,
22
+ readdirSync,
23
+ readSync,
24
+ realpathSync,
25
+ statSync,
26
+ } from "node:fs";
27
+ import { join } from "node:path";
28
+
29
+ export const CHAT_SESSIONS_CAP = 200;
30
+ /** First-prompt titles are cut to this many characters. */
31
+ export const CHAT_TITLE_MAX = 80;
32
+ /** Custom and generated titles are kept whole up to this safety bound. */
33
+ export const CHAT_NAMED_TITLE_MAX = 200;
34
+ /** Files above this size are read head + tail only (titles are appended, the first prompt is near the top). */
35
+ export const CHAT_FULL_SCAN_MAX_BYTES = 64 * 1024 * 1024;
36
+ export const CHAT_HEAD_BYTES = 2 * 1024 * 1024;
37
+ export const CHAT_TAIL_BYTES = 8 * 1024 * 1024;
38
+ /**
39
+ * Bytes one listing may read in full. Past it, the remaining (older) transcripts are read
40
+ * through a small head + tail window, so 200 large sessions cannot stall a snapshot.
41
+ */
42
+ export const CHAT_SCAN_BUDGET_BYTES = 256 * 1024 * 1024;
43
+ const BUDGET_HEAD_BYTES = 256 * 1024;
44
+ const BUDGET_TAIL_BYTES = 1024 * 1024;
45
+ export const CHAT_VENDORS = Object.freeze(["claude", "codex", "cursor"]);
46
+
47
+ const SESSION_FILE_RE = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
48
+ const SLUG_MAX = 200;
49
+ const READ_CHUNK = 1024 * 1024;
50
+ const NEWLINE = 0x0a;
51
+
52
+ const MARK_CUSTOM = Buffer.from('"type":"custom-title"');
53
+ const MARK_AI = Buffer.from('"type":"ai-title"');
54
+ const MARK_USER = Buffer.from('"type":"user"');
55
+
56
+ /** Prompt text that is harness plumbing, not something the operator typed. */
57
+ const NON_PROMPT_PREFIXES = [
58
+ "<command-name>",
59
+ "<command-message>",
60
+ "<command-args>",
61
+ "<local-command-",
62
+ "<bash-input>",
63
+ "<bash-stdout>",
64
+ "<bash-stderr>",
65
+ "<system-reminder>",
66
+ "<task-notification>",
67
+ "Caveat:",
68
+ "[Request interrupted",
69
+ ];
70
+
71
+ /** Java-style 32-bit string hash over UTF-16 code units (Claude Code's long-slug suffix). */
72
+ function slugHash(value) {
73
+ let h = 0;
74
+ for (let i = 0; i < value.length; i++) h = ((h << 5) - h + value.charCodeAt(i)) | 0;
75
+ return h;
76
+ }
77
+
78
+ /**
79
+ * Claude Code project folder name for a working directory: every character that is
80
+ * not an ASCII letter or digit becomes "-" (so "/" and "." do, and a ".cursor"
81
+ * segment becomes "--cursor"); names over 200 characters are cut and get a hash suffix.
82
+ * @param {string} cwd absolute working directory
83
+ * @returns {string}
84
+ */
85
+ export function claudeProjectSlug(cwd) {
86
+ const raw = String(cwd ?? "");
87
+ const slug = raw.replace(/[^a-zA-Z0-9]/g, "-");
88
+ if (slug.length <= SLUG_MAX) return slug;
89
+ return `${slug.slice(0, SLUG_MAX)}-${Math.abs(slugHash(raw)).toString(36)}`;
90
+ }
91
+
92
+ /**
93
+ * ~/.claude/projects/<slug> for `cwd`, or null without a usable HOME.
94
+ * @param {string} cwd
95
+ * @param {{ home?: string | null }} [opts]
96
+ */
97
+ export function claudeProjectDir(cwd, opts = {}) {
98
+ const home = opts.home === undefined ? process.env.HOME : opts.home;
99
+ if (typeof home !== "string" || !home.trim() || !home.startsWith("/")) return null;
100
+ return join(home, ".claude", "projects", claudeProjectSlug(cwd));
101
+ }
102
+
103
+ /**
104
+ * Display-safe single line: control characters (incl. ANSI escapes and bidi
105
+ * overrides) removed, whitespace collapsed, cut to `max` characters.
106
+ * @param {unknown} value
107
+ * @param {number} [max]
108
+ * @returns {string}
109
+ */
110
+ export function cleanChatTitle(value, max = CHAT_TITLE_MAX) {
111
+ const text = String(value ?? "")
112
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping control characters is the point
113
+ .replace(/[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, " ")
114
+ .replace(/\s+/g, " ")
115
+ .trim();
116
+ const chars = [...text];
117
+ if (chars.length <= max) return text;
118
+ return `${chars
119
+ .slice(0, max - 1)
120
+ .join("")
121
+ .trimEnd()}…`;
122
+ }
123
+
124
+ /** First operator-typed text of a user record, or "" when it is meta, a command or a tool result. */
125
+ function promptText(record) {
126
+ if (record?.type !== "user" || record.isMeta === true || record.isSidechain === true) {
127
+ return "";
128
+ }
129
+ const content = record.message?.content;
130
+ let text = "";
131
+ if (typeof content === "string") {
132
+ text = content;
133
+ } else if (Array.isArray(content)) {
134
+ if (content.some((part) => part && part.type === "tool_result")) return "";
135
+ text = content
136
+ .filter((part) => part && part.type === "text" && typeof part.text === "string")
137
+ .map((part) => part.text)
138
+ .join(" ");
139
+ }
140
+ const trimmed = text.trim();
141
+ if (!trimmed) return "";
142
+ if (NON_PROMPT_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) return "";
143
+ return trimmed;
144
+ }
145
+
146
+ function parseRecord(line) {
147
+ try {
148
+ const value = JSON.parse(line.toString("utf8"));
149
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Stream complete lines of byte range [start, end) to `onLine` (Buffer per line).
157
+ * The first line is dropped when `start` is mid-file (it is a partial record).
158
+ */
159
+ function forEachLine(fd, start, end, onLine) {
160
+ const buf = Buffer.allocUnsafe(READ_CHUNK);
161
+ /** @type {Buffer[]} */
162
+ let pending = [];
163
+ let skipFirst = start > 0;
164
+ let pos = start;
165
+ while (pos < end) {
166
+ const n = readSync(fd, buf, 0, Math.min(READ_CHUNK, end - pos), pos);
167
+ if (n <= 0) break;
168
+ pos += n;
169
+ let from = 0;
170
+ for (;;) {
171
+ const nl = buf.indexOf(NEWLINE, from);
172
+ if (nl === -1 || nl >= n) {
173
+ if (from < n) pending.push(Buffer.from(buf.subarray(from, n)));
174
+ break;
175
+ }
176
+ const piece = buf.subarray(from, nl);
177
+ const line = pending.length ? Buffer.concat([...pending, piece]) : piece;
178
+ pending = [];
179
+ from = nl + 1;
180
+ if (skipFirst) {
181
+ skipFirst = false;
182
+ continue;
183
+ }
184
+ onLine(line);
185
+ }
186
+ }
187
+ // A trailing record without a newline is still a record (only at true EOF).
188
+ if (pending.length && !skipFirst) onLine(Buffer.concat(pending));
189
+ }
190
+
191
+ /**
192
+ * Title of one session transcript. Precedence: last custom-title > last ai-title >
193
+ * first typed prompt (cut to CHAT_TITLE_MAX) > none. Lines are JSON-parsed only
194
+ * when their raw bytes carry a marker; malformed lines are skipped. A file above
195
+ * `fullScanMaxBytes` is read through its first `headBytes` and last `tailBytes` only.
196
+ * @param {string} filePath
197
+ * @param {{ fullScanMaxBytes?: number, headBytes?: number, tailBytes?: number }} [window]
198
+ * @returns {{ title: string | null, titleSource: "custom"|"ai"|"first-prompt"|"none" }}
199
+ */
200
+ export function readChatTitle(filePath, window = {}) {
201
+ const fullScanMax = window.fullScanMaxBytes ?? CHAT_FULL_SCAN_MAX_BYTES;
202
+ const headBytes = window.headBytes ?? CHAT_HEAD_BYTES;
203
+ const tailBytes = window.tailBytes ?? CHAT_TAIL_BYTES;
204
+ let custom = "";
205
+ let ai = "";
206
+ let prompt = "";
207
+ let fd;
208
+ try {
209
+ fd = openSync(filePath, "r");
210
+ } catch {
211
+ return { title: null, titleSource: "none" };
212
+ }
213
+ try {
214
+ const size = fstatSync(fd).size;
215
+ const visit = (line) => {
216
+ if (line.indexOf(MARK_CUSTOM) !== -1) {
217
+ const rec = parseRecord(line);
218
+ if (rec?.type === "custom-title" && typeof rec.customTitle === "string") {
219
+ const t = cleanChatTitle(rec.customTitle, CHAT_NAMED_TITLE_MAX);
220
+ if (t) custom = t;
221
+ }
222
+ return;
223
+ }
224
+ if (line.indexOf(MARK_AI) !== -1) {
225
+ const rec = parseRecord(line);
226
+ if (rec?.type === "ai-title" && typeof rec.aiTitle === "string") {
227
+ const t = cleanChatTitle(rec.aiTitle, CHAT_NAMED_TITLE_MAX);
228
+ if (t) ai = t;
229
+ }
230
+ return;
231
+ }
232
+ if (!prompt && line.indexOf(MARK_USER) !== -1) {
233
+ prompt = cleanChatTitle(promptText(parseRecord(line)));
234
+ }
235
+ };
236
+ if (size <= fullScanMax || size <= headBytes + tailBytes) {
237
+ forEachLine(fd, 0, size, visit);
238
+ } else {
239
+ forEachLine(fd, 0, headBytes, visit);
240
+ forEachLine(fd, size - tailBytes, size, visit);
241
+ }
242
+ } catch {
243
+ // Unreadable mid-way: keep whatever was found.
244
+ } finally {
245
+ closeSync(fd);
246
+ }
247
+ if (custom) return { title: custom, titleSource: "custom" };
248
+ if (ai) return { title: ai, titleSource: "ai" };
249
+ if (prompt) return { title: prompt, titleSource: "first-prompt" };
250
+ return { title: null, titleSource: "none" };
251
+ }
252
+
253
+ /** Module-level title cache keyed by path + mtime + size (one entry per transcript version). */
254
+ const defaultTitleCache = new Map();
255
+
256
+ /**
257
+ * @param {string} vendor
258
+ * @param {"ok"|"unsupported"|"none"} status
259
+ * @param {string | null} projectDir
260
+ */
261
+ function envelope(vendor, status, projectDir, sessions = [], total = 0) {
262
+ return { vendor, status, projectDir, sessions, total };
263
+ }
264
+
265
+ /**
266
+ * Project folders to try for `cwd`: the path as given, then its resolved real path
267
+ * (Claude Code keys sessions on the physical working directory).
268
+ * @param {string} cwd
269
+ * @param {string | null | undefined} home
270
+ * @returns {string[]}
271
+ */
272
+ function claudeProjectDirCandidates(cwd, home) {
273
+ const out = [];
274
+ const first = claudeProjectDir(cwd, { home });
275
+ if (!first) return out;
276
+ out.push(first);
277
+ try {
278
+ const real = realpathSync(cwd);
279
+ const second = claudeProjectDir(real, { home });
280
+ if (second && second !== first) out.push(second);
281
+ } catch {
282
+ // Workspace path does not exist (or is unreadable): the given path is all we have.
283
+ }
284
+ return out;
285
+ }
286
+
287
+ /**
288
+ * Claude Code sessions saved for `cwd`.
289
+ * @param {{ cwd: string, home?: string | null, cap?: number, scanBudgetBytes?: number, cache?: Map<string, {title: string|null, titleSource: string}> }} opts
290
+ */
291
+ export function listClaudeSessions(opts) {
292
+ const cap = Number.isInteger(opts?.cap) && opts.cap >= 0 ? opts.cap : CHAT_SESSIONS_CAP;
293
+ const cache = opts?.cache ?? defaultTitleCache;
294
+ const candidates = claudeProjectDirCandidates(String(opts?.cwd ?? ""), opts?.home);
295
+ if (candidates.length === 0) return envelope("claude", "none", null);
296
+ const projectDir = candidates.find((dir) => existsSync(dir)) ?? candidates[0];
297
+ let entries;
298
+ try {
299
+ entries = readdirSync(projectDir, { withFileTypes: true });
300
+ } catch {
301
+ return envelope("claude", "none", projectDir);
302
+ }
303
+ const found = [];
304
+ for (const entry of entries) {
305
+ // Regular files only: subagent folders, symlinks and FIFOs are never opened.
306
+ if (!entry.isFile()) continue;
307
+ const m = SESSION_FILE_RE.exec(entry.name);
308
+ if (!m) continue;
309
+ const path = join(projectDir, entry.name);
310
+ try {
311
+ const st = statSync(path);
312
+ found.push({ id: m[1], path, mtimeMs: st.mtimeMs, size: st.size });
313
+ } catch {
314
+ // Vanished between readdir and stat.
315
+ }
316
+ }
317
+ if (found.length === 0) return envelope("claude", "none", projectDir);
318
+ found.sort((a, b) => b.mtimeMs - a.mtimeMs || (a.id < b.id ? -1 : 1));
319
+ let budget = opts?.scanBudgetBytes ?? CHAT_SCAN_BUDGET_BYTES;
320
+ const sessions = found.slice(0, cap).map((f) => {
321
+ const key = `${f.path}\u0000${f.mtimeMs}\u0000${f.size}`;
322
+ let title = cache.get(key);
323
+ if (!title) {
324
+ const cost = f.size <= CHAT_FULL_SCAN_MAX_BYTES ? f.size : CHAT_HEAD_BYTES + CHAT_TAIL_BYTES;
325
+ if (cost <= budget) {
326
+ title = readChatTitle(f.path);
327
+ budget -= cost;
328
+ cache.set(key, title);
329
+ } else {
330
+ // Budget spent: a small window, not cached as a full read.
331
+ title = readChatTitle(f.path, {
332
+ fullScanMaxBytes: 0,
333
+ headBytes: BUDGET_HEAD_BYTES,
334
+ tailBytes: BUDGET_TAIL_BYTES,
335
+ });
336
+ }
337
+ }
338
+ return {
339
+ id: f.id,
340
+ title: title.title,
341
+ titleSource: title.titleSource,
342
+ modified: Math.floor(f.mtimeMs),
343
+ sizeBytes: f.size,
344
+ };
345
+ });
346
+ return envelope("claude", "ok", projectDir, sessions, found.length);
347
+ }
348
+
349
+ /**
350
+ * One envelope per vendor (Claude Code read; Codex and Cursor explicitly unsupported).
351
+ * @param {{ cwd: string, home?: string | null, cap?: number, cache?: Map<string, any> }} opts
352
+ */
353
+ export function listChatSessions(opts) {
354
+ return CHAT_VENDORS.map((vendor) =>
355
+ vendor === "claude" ? listClaudeSessions(opts) : envelope(vendor, "unsupported", null),
356
+ );
357
+ }
358
+
359
+ /**
360
+ * Snapshot / `--json` shape: `{ withheld, vendors }`. `withheld` is true when the
361
+ * list was left out on purpose (Mission Control broadcast mode: titles are private).
362
+ * @param {{ cwd: string, home?: string | null, cap?: number, cache?: Map<string, any>, withhold?: boolean }} opts
363
+ */
364
+ export function collectChats(opts) {
365
+ if (opts?.withhold) return { withheld: true, vendors: [] };
366
+ return { withheld: false, vendors: listChatSessions(opts) };
367
+ }
368
+
369
+ /**
370
+ * Drop chat data from a parsed snapshot before it leaves the machine (broadcast).
371
+ * Returns a shallow copy; other fields are untouched.
372
+ * @template {Record<string, unknown>} T
373
+ * @param {T} snapshot
374
+ * @returns {T}
375
+ */
376
+ export function withholdChats(snapshot) {
377
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return snapshot;
378
+ return { ...snapshot, chats: { withheld: true, vendors: [] } };
379
+ }
380
+
381
+ /**
382
+ * Same as `withholdChats` for the serialized payload serve.mjs caches and pushes.
383
+ * Unparseable payloads pass through (they carry no snapshot).
384
+ * @param {string} payload
385
+ * @returns {string}
386
+ */
387
+ export function withholdChatsFromPayload(payload) {
388
+ try {
389
+ const parsed = JSON.parse(payload);
390
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return payload;
391
+ return JSON.stringify(withholdChats(parsed));
392
+ } catch {
393
+ return payload;
394
+ }
395
+ }
396
+
397
+ /**
398
+ * POSIX single-quote a path for a pasted shell command.
399
+ * @param {string} value
400
+ */
401
+ export function shellQuote(value) {
402
+ const s = String(value ?? "");
403
+ if (/^[A-Za-z0-9_./-]+$/.test(s)) return s;
404
+ return `'${s.replace(/'/g, `'\\''`)}'`;
405
+ }
406
+
407
+ /**
408
+ * Command that reopens a session from any terminal.
409
+ * @param {string} workspace absolute workspace path (the session's cwd)
410
+ * @param {string} id session uuid
411
+ */
412
+ export function claudeResumeCommand(workspace, id) {
413
+ return `cd ${shellQuote(workspace)} && claude --resume ${shellQuote(id)}`;
414
+ }
415
+
416
+ /**
417
+ * "just now" / "5m ago" / "3h ago" / "2d ago" (same buckets as Mission Control).
418
+ * @param {number} modifiedMs
419
+ * @param {number} [nowMs]
420
+ */
421
+ export function formatChatAge(modifiedMs, nowMs = Date.now()) {
422
+ const secs = Math.floor(Math.max(0, nowMs - modifiedMs) / 1000);
423
+ if (secs < 5) return "just now";
424
+ if (secs < 60) return `${secs}s ago`;
425
+ const mins = Math.floor(secs / 60);
426
+ if (mins < 60) return `${mins}m ago`;
427
+ const hrs = Math.floor(mins / 60);
428
+ if (hrs < 24) return `${hrs}h ago`;
429
+ return `${Math.floor(hrs / 24)}d ago`;
430
+ }
@@ -11,6 +11,7 @@ import { createServer } from "node:http";
11
11
  import { dirname, extname, join } from "node:path";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { shareShellTokenRequired } from "./lib/broadcast-share.mjs";
14
+ import { withholdChatsFromPayload } from "./lib/chat-sessions.mjs";
14
15
  import {
15
16
  allowlistConfig,
16
17
  applyCorsHeaders,
@@ -260,6 +261,7 @@ function errorPayload(message) {
260
261
  memory: {},
261
262
  git: {},
262
263
  terminals: [],
264
+ chats: { withheld: authResolved.broadcast, vendors: [] },
263
265
  processes: [],
264
266
  health: { status: "error", checks: [] },
265
267
  missionControl: null,
@@ -308,7 +310,9 @@ function runDataScript() {
308
310
  if (baseline) previousInventoryJson = baseline;
309
311
  const pipelineCache = pipelineCacheJsonFromPayload(stdout);
310
312
  if (pipelineCache) previousPipelineJson = pipelineCache;
311
- resolve(stdout);
313
+ // Chat titles are private: a broadcast (LAN, token-gated) server never serves them, even
314
+ // if the child missed the bind env. Every route (/api/data, SSE connect, push) reads this.
315
+ resolve(authResolved.broadcast ? withholdChatsFromPayload(stdout) : stdout);
312
316
  },
313
317
  );
314
318
  });
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  REGISTRY_CLI_ARGS,
7
7
  resolveRegistryFromCli
8
- } from "./chunk-C2SJDYAM.js";
8
+ } from "./chunk-H46W7NHC.js";
9
9
  import {
10
10
  logger
11
11
  } from "./chunk-ZB43YMHW.js";
@@ -22,7 +22,7 @@ import {
22
22
  resolveProtectedGlobs,
23
23
  saveManifest,
24
24
  upsertIdList
25
- } from "./chunk-NSSXSZLL.js";
25
+ } from "./chunk-7BEHUBEJ.js";
26
26
  import {
27
27
  KIT_VERSION
28
28
  } from "./chunk-7HDQEK6T.js";
@@ -6,13 +6,13 @@ import {
6
6
  import {
7
7
  diffAgainstRegistry,
8
8
  summarizeDiff
9
- } from "./chunk-CWDXNBEU.js";
9
+ } from "./chunk-ANPSYWQO.js";
10
10
  import {
11
11
  DEFAULT_REGISTRY_URL,
12
12
  assertSafeRegistrySource,
13
13
  loadAgentKitManifest,
14
14
  resolveRegistryRoot
15
- } from "./chunk-NSSXSZLL.js";
15
+ } from "./chunk-7BEHUBEJ.js";
16
16
  import {
17
17
  KIT_PACKAGE_SPEC,
18
18
  KIT_VERSION
@@ -1,6 +1,11 @@
1
+ import {
2
+ compareSemver,
3
+ normalizeSemver,
4
+ readLocalKitVersion
5
+ } from "./chunk-3OM7JODZ.js";
1
6
  import {
2
7
  L0_ARTIFACTS
3
- } from "./chunk-X45O2AIE.js";
8
+ } from "./chunk-E465FAWM.js";
4
9
  import {
5
10
  copyRegistryFile,
6
11
  emptyStats,
@@ -12,24 +17,100 @@ import {
12
17
  recordOutcome,
13
18
  resolveProtectedGlobs,
14
19
  saveManagedHashLedger
15
- } from "./chunk-NSSXSZLL.js";
20
+ } from "./chunk-7BEHUBEJ.js";
21
+ import {
22
+ KIT_PACKAGE_SPEC,
23
+ KIT_VERSION
24
+ } from "./chunk-7HDQEK6T.js";
16
25
  import {
17
26
  fileExists
18
27
  } from "./chunk-5B4DVO6C.js";
19
28
 
29
+ // src/lifecycle/stale-cli.ts
30
+ import path from "path";
31
+ async function detectStaleCli(registryRoot, options) {
32
+ const registryVersionRaw = await readLocalKitVersion(registryRoot);
33
+ const registryVersion = registryVersionRaw ? normalizeSemver(registryVersionRaw) : null;
34
+ const cliVersion = normalizeSemver(options.cliVersion ?? KIT_VERSION);
35
+ if (!registryVersion) {
36
+ options.warn(
37
+ "Could not read a version from the registry checkout - skipping the CLI freshness check."
38
+ );
39
+ return null;
40
+ }
41
+ if (cliVersion && compareSemver(cliVersion, registryVersion) < 0) {
42
+ return { cliVersion, registryVersion };
43
+ }
44
+ return null;
45
+ }
46
+ var SHELL_SAFE = /^[A-Za-z0-9_./:@=+,-]+$/;
47
+ function shellQuote(value) {
48
+ return SHELL_SAFE.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
49
+ }
50
+ function formatRerunFlags(args, flags, currentDir = process.cwd()) {
51
+ const out = [];
52
+ for (const flag of flags) {
53
+ const value = args[flag];
54
+ if (value === true) {
55
+ out.push(flag === "yes" ? "-y" : `--${flag}`);
56
+ } else if (typeof value === "string" && value.length > 0) {
57
+ if (flag === "cwd" && path.resolve(value) === path.resolve(currentDir)) continue;
58
+ out.push(`--${flag}`, shellQuote(value));
59
+ }
60
+ }
61
+ return out.join(" ");
62
+ }
63
+ function formatStaleCliRefusal(input) {
64
+ const { command, stale, rerun } = input;
65
+ const { cliVersion, registryVersion } = stale;
66
+ const tail = rerun ? ` ${rerun}` : "";
67
+ return {
68
+ error: `This CLI is v${cliVersion}, older than the registry it would apply (v${registryVersion}).`,
69
+ recovery: [
70
+ "",
71
+ `\`agent-kit ${command}\` syncs registry content but stamps .cursor/agent-kit.json with the`,
72
+ `version of the CLI running it, so applying now would write v${cliVersion} again, not v${registryVersion}.`,
73
+ "",
74
+ "Upgrade the binary first, then re-run:",
75
+ ` npm i -g ${KIT_PACKAGE_SPEC}@${registryVersion}`,
76
+ ` agent-kit ${command}${tail}`,
77
+ "",
78
+ "Or run that version once without a global install:",
79
+ ` npx -y ${KIT_PACKAGE_SPEC}@${registryVersion} ${command}${tail}`,
80
+ "",
81
+ `If npm has no ${registryVersion} yet, the release is still mid-publish`,
82
+ "(content sync and npm publish run in parallel) - retry shortly.",
83
+ "",
84
+ "Factory/dev checkouts that intend to apply newer content with an older",
85
+ "binary can pass --allow-stale-cli.",
86
+ ""
87
+ ].join("\n")
88
+ };
89
+ }
90
+ var StaleCliError = class extends Error {
91
+ constructor(stale) {
92
+ super(
93
+ `This CLI is v${stale.cliVersion}, older than the registry it would apply (v${stale.registryVersion}).`
94
+ );
95
+ this.stale = stale;
96
+ this.name = "StaleCliError";
97
+ }
98
+ stale;
99
+ };
100
+
20
101
  // src/lifecycle/onboard-migration.ts
21
102
  import { createHash } from "crypto";
22
103
  import { readFile, unlink } from "fs/promises";
23
- import path from "path";
104
+ import path2 from "path";
24
105
  var LEGACY_ONBOARD_PATH = ".cursor/commands/onboard.md";
25
106
  var NAMESPACED_ONBOARD_PATH = ".cursor/commands/agent-kit-onboard.md";
26
107
  var MANAGED_LEGACY_HASHES = /* @__PURE__ */ new Set([
27
108
  "b274a68941813f19b185893cb7c5561dff027f53270890029992f208e24992fe"
28
109
  ]);
29
110
  async function migrateLegacyOnboardCommand(projectRoot, managedHashes = MANAGED_LEGACY_HASHES) {
30
- const legacyPath = path.join(projectRoot, LEGACY_ONBOARD_PATH);
111
+ const legacyPath = path2.join(projectRoot, LEGACY_ONBOARD_PATH);
31
112
  if (!await fileExists(legacyPath)) return "absent";
32
- const namespacedPath = path.join(projectRoot, NAMESPACED_ONBOARD_PATH);
113
+ const namespacedPath = path2.join(projectRoot, NAMESPACED_ONBOARD_PATH);
33
114
  if (!await fileExists(namespacedPath)) return "preserved-customized";
34
115
  const content = await readFile(legacyPath);
35
116
  const hash = createHash("sha256").update(content).digest("hex");
@@ -100,6 +181,10 @@ var NON_INTERACTIVE_ROOT_ARGS = {
100
181
  };
101
182
 
102
183
  export {
184
+ detectStaleCli,
185
+ formatRerunFlags,
186
+ formatStaleCliRefusal,
187
+ StaleCliError,
103
188
  installL0,
104
189
  syncFromManifest,
105
190
  NON_INTERACTIVE_ROOT_ARGS
@@ -36,8 +36,15 @@ var HEADLESS_DENY_RULES = [
36
36
  ...HEADLESS_ONLY_DENY_RULES
37
37
  ];
38
38
  var INTERACTIVE_DENY_RULES = [...SHARED_DENY_RULES];
39
+ var INTERACTIVE_ALLOW_RULES = [
40
+ "Bash(gh pr merge)",
41
+ "Bash(gh pr merge *)",
42
+ "Bash(glab mr merge)",
43
+ "Bash(glab mr merge *)"
44
+ ];
39
45
 
40
46
  export {
41
47
  HEADLESS_DENY_RULES,
42
- INTERACTIVE_DENY_RULES
48
+ INTERACTIVE_DENY_RULES,
49
+ INTERACTIVE_ALLOW_RULES
43
50
  };
@@ -693,7 +693,11 @@ var KNOWN_SHIPPED_OVERLAY_HASHES = /* @__PURE__ */ new Set([
693
693
  "aef5e06c10d1166e1a3efc3cc1f1deda28430334aa7d3fc1e3fe04de478d4bc3",
694
694
  "2b7978c318169d00c7274e27e5082bd6bfe47168c36407c66c828a4e56dab4c1",
695
695
  "d92297c4675bf9b91d7aaa39fdfafcf2568c9b8b867c80e8121e6225bcf9d5a3",
696
- "5656ec26963f4d43f9c440f4cae65ad7aa3a33a603de13d81400d3cb5acd1b40"
696
+ "5656ec26963f4d43f9c440f4cae65ad7aa3a33a603de13d81400d3cb5acd1b40",
697
+ "64cfb0dba5d6bcdcd919eaa73fe32e9e34689323d65c70c9ee8aa107bf228677",
698
+ "267537b445490788f4c146e0f5e9276b77a6cc4be77abca8f5696d758bbddaf9",
699
+ "ae3d264354f2f89509bf1d0565ca3906aef30b7f7bc8f62297764e7d5436329a",
700
+ "f95f94170f26beb4e03d4e24b22139e1aaa6bbb3d4b43d524ea9f12ed296beb4"
697
701
  ]);
698
702
 
699
703
  // src/lifecycle/paths.ts
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  managedPairs
3
- } from "./chunk-X45O2AIE.js";
3
+ } from "./chunk-E465FAWM.js";
4
4
  import {
5
5
  isProtectedPath,
6
6
  resolveContained,
7
7
  resolveProtectedGlobs
8
- } from "./chunk-NSSXSZLL.js";
8
+ } from "./chunk-7BEHUBEJ.js";
9
9
 
10
10
  // src/lifecycle/diff.ts
11
11
  import { readFile } from "fs/promises";
@@ -5,7 +5,7 @@ import {
5
5
  RESOLVE_AGENT_KIT_REL,
6
6
  SESSION_START_HOOK_MARKER,
7
7
  discoverInstalledCommands
8
- } from "./chunk-SPYIP76F.js";
8
+ } from "./chunk-HHR56LJQ.js";
9
9
  import {
10
10
  findOnPath
11
11
  } from "./chunk-WGLPZXNU.js";
@@ -4,7 +4,7 @@ import {
4
4
  loadRegistry,
5
5
  packMemberFileTargets,
6
6
  skillFileTargets
7
- } from "./chunk-NSSXSZLL.js";
7
+ } from "./chunk-7BEHUBEJ.js";
8
8
 
9
9
  // src/lifecycle/l0.ts
10
10
  var L0_ARTIFACTS = [