@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.
- package/dashboard/dashboard-data.mjs +14 -2
- package/dashboard/dashboard.html +139 -4
- package/dashboard/lib/chat-sessions.d.mts +63 -0
- package/dashboard/lib/chat-sessions.mjs +430 -0
- package/dashboard/serve.mjs +5 -1
- package/dist/{add-Y2426TGU.js → add-HFDX3U3R.js} +2 -2
- package/dist/{chunk-TW4BF5OE.js → chunk-3OM7JODZ.js} +2 -2
- package/dist/{chunk-TGNFGMPV.js → chunk-6OTJEQEC.js} +90 -5
- package/dist/{chunk-MAPOQEA2.js → chunk-6RRG3WOE.js} +8 -1
- package/dist/{chunk-NSSXSZLL.js → chunk-7BEHUBEJ.js} +5 -1
- package/dist/{chunk-CWDXNBEU.js → chunk-ANPSYWQO.js} +2 -2
- package/dist/{chunk-BEKQ6PVD.js → chunk-AXLXEAVJ.js} +1 -1
- package/dist/{chunk-X45O2AIE.js → chunk-E465FAWM.js} +1 -1
- package/dist/{chunk-C2SJDYAM.js → chunk-H46W7NHC.js} +1 -1
- package/dist/{chunk-SPYIP76F.js → chunk-HHR56LJQ.js} +64 -28
- package/dist/{chunk-IR7FFPUU.js → chunk-IMIVTDMK.js} +43 -8
- package/dist/chunk-IRTS7WEA.js +374 -0
- package/dist/{chunk-MO63KZBP.js → chunk-KX3OUY4W.js} +8 -6
- package/dist/chunk-MWTXI6C5.js +223 -0
- package/dist/{chunk-JEI6HACW.js → chunk-NJVWTROP.js} +24 -377
- package/dist/{chunk-CER2APV2.js → chunk-Q4PCMRM5.js} +1 -1
- package/dist/{chunk-GUQYNUV4.js → chunk-R7DA4KXG.js} +1 -1
- package/dist/{chunk-VO73OL5V.js → chunk-SE7ZIRLX.js} +2 -2
- package/dist/{chunk-KVY425NM.js → chunk-TZ5XYIEF.js} +4 -2
- package/dist/{chunk-Z7ABX5QI.js → chunk-WOHZ4T5V.js} +4 -2
- package/dist/{chunk-YRFSTHR5.js → chunk-XEEB4YSA.js} +13 -219
- package/dist/{chunk-QTBTKEXK.js → chunk-ZPZVROS5.js} +1 -1
- package/dist/{contribute-TVST3CJO.js → contribute-PP7HMPW7.js} +3 -3
- package/dist/{countdown-QXJD6TDB.js → countdown-LAUZMI7U.js} +10 -8
- package/dist/{diff-MECE5RZB.js → diff-HYWC4G6Y.js} +4 -4
- package/dist/{doctor-VLZNXJNA.js → doctor-2X3HBRJJ.js} +8 -8
- package/dist/engine-info-4ZWEIHSX.js +89 -0
- package/dist/{ground-control-NMYYIVCV.js → ground-control-WKV2BMOW.js} +5 -4
- package/dist/{guard-EHXF76ID.js → guard-ZZRQW3DJ.js} +107 -8
- package/dist/{hook-XCZWX462.js → hook-UEYR4M3Z.js} +10 -4
- package/dist/index.js +26 -22
- package/dist/{init-HKMRU2V4.js → init-UF5VEIJX.js} +11 -11
- package/dist/{install-X2J6KWZ5.js → install-6ILVA22C.js} +11 -11
- package/dist/{run-R66G276Q.js → run-2EKD2BEC.js} +12 -9
- package/dist/{run-plan-24Z5K4P3.js → run-plan-G72XMC5Z.js} +11 -7
- package/dist/sessions-O7MYEMCT.js +343 -0
- package/dist/{setup-global-ADJFMOZX.js → setup-global-ZZEY5KWE.js} +5 -5
- package/dist/{status-UB6NL3Q3.js → status-4LK2QXVQ.js} +6 -6
- package/dist/{update-PFBFY7GT.js → update-FPR2NZND.js} +33 -43
- 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
|
+
}
|
package/dashboard/serve.mjs
CHANGED
|
@@ -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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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 =
|
|
111
|
+
const legacyPath = path2.join(projectRoot, LEGACY_ONBOARD_PATH);
|
|
31
112
|
if (!await fileExists(legacyPath)) return "absent";
|
|
32
|
-
const namespacedPath =
|
|
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-
|
|
3
|
+
} from "./chunk-E465FAWM.js";
|
|
4
4
|
import {
|
|
5
5
|
isProtectedPath,
|
|
6
6
|
resolveContained,
|
|
7
7
|
resolveProtectedGlobs
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-7BEHUBEJ.js";
|
|
9
9
|
|
|
10
10
|
// src/lifecycle/diff.ts
|
|
11
11
|
import { readFile } from "fs/promises";
|