huddle-cli 0.1.0 → 0.1.3

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 (73) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/QUICKSTART.md +135 -0
  3. package/README.md +26 -4
  4. package/dist/adapters/amp.js +1 -1
  5. package/dist/adapters/antigravity.js +2 -0
  6. package/dist/adapters/claude.js +19 -3
  7. package/dist/adapters/codex.js +9 -1
  8. package/dist/adapters/cursor.js +12 -5
  9. package/dist/adapters/custom.js +4 -2
  10. package/dist/adapters/droid.js +1 -0
  11. package/dist/adapters/goose.js +2 -0
  12. package/dist/adapters/index.js +44 -18
  13. package/dist/adapters/kiro.js +1 -1
  14. package/dist/adapters/opencode.js +1 -1
  15. package/dist/app/live.js +142 -0
  16. package/dist/app/page.js +701 -0
  17. package/dist/app/server.js +262 -0
  18. package/dist/args.js +18 -0
  19. package/dist/attach/connect.js +7 -4
  20. package/dist/attach/guard.js +21 -2
  21. package/dist/attach/live.js +5 -1
  22. package/dist/attach/plug.js +21 -3
  23. package/dist/attach/screen.js +150 -0
  24. package/dist/attach/session.js +7 -2
  25. package/dist/cli.js +134 -42
  26. package/dist/mcp/context-tools.js +79 -0
  27. package/dist/mcp/server.js +57 -11
  28. package/dist/orchestrator/ask.js +13 -10
  29. package/dist/orchestrator/brain.js +16 -4
  30. package/dist/orchestrator/budget.js +283 -0
  31. package/dist/orchestrator/busy.js +106 -0
  32. package/dist/orchestrator/council.js +434 -87
  33. package/dist/orchestrator/goal.js +196 -0
  34. package/dist/orchestrator/handoff.js +2 -2
  35. package/dist/orchestrator/launcher.js +31 -13
  36. package/dist/orchestrator/learn.js +101 -0
  37. package/dist/orchestrator/planner.js +54 -9
  38. package/dist/orchestrator/qa.js +71 -15
  39. package/dist/orchestrator/radar.js +719 -0
  40. package/dist/orchestrator/scheduler.js +575 -50
  41. package/dist/orchestrator/takeover.js +155 -0
  42. package/dist/platform.js +377 -0
  43. package/dist/room/context.js +1285 -0
  44. package/dist/room/contract.js +132 -0
  45. package/dist/room/db.js +79 -16
  46. package/dist/room/followup.js +233 -0
  47. package/dist/room/identity.js +2 -2
  48. package/dist/room/lessons.js +275 -0
  49. package/dist/room/live.js +75 -20
  50. package/dist/room/mail.js +225 -0
  51. package/dist/room/progress.js +40 -1
  52. package/dist/room/server.js +220 -39
  53. package/dist/room/tunnel.js +6 -5
  54. package/dist/room/user.js +54 -0
  55. package/dist/sandbox/sync.js +4 -4
  56. package/dist/sandbox/worktree.js +145 -14
  57. package/dist/ui/app.js +963 -159
  58. package/dist/ui/brief.js +494 -0
  59. package/dist/ui/catalog.js +62 -24
  60. package/dist/ui/dashboard.js +269 -55
  61. package/dist/ui/doctor.js +15 -11
  62. package/dist/ui/export.js +1194 -0
  63. package/dist/ui/lessons.js +81 -0
  64. package/dist/ui/narrator.js +56 -0
  65. package/dist/ui/paste.js +515 -0
  66. package/dist/ui/remote.js +11 -5
  67. package/dist/ui/setup.js +152 -0
  68. package/dist/ui/story.js +716 -94
  69. package/dist/ui/talk.js +176 -0
  70. package/dist/ui/term.js +182 -16
  71. package/dist/ui/transcript.js +434 -0
  72. package/dist/ui/watch.js +149 -48
  73. package/package.json +6 -3
@@ -0,0 +1,434 @@
1
+ // An agent's own session, as a readable transcript: what it said, what it
2
+ // thought (when the CLI shows it), every tool call with its arguments in short,
3
+ // and what came back — trimmed. `huddle export --level full` puts one of these
4
+ // under each task of a run.
5
+ //
6
+ // The logs are whatever the agent CLI printed (.huddle/logs/<agent>-<task>.log,
7
+ // src/orchestrator/launcher.ts):
8
+ // claude --output-format stream-json assistant/user messages, tool_use ↔ tool_result
9
+ // codex exec --json item.started / item.completed (agent_message,
10
+ // command_execution, mcp_tool_call, file_change, …)
11
+ // cursor --output-format stream-json assistant text, thinking deltas, tool_call
12
+ // started/completed (shellToolCall, readToolCall, …)
13
+ // amp and qwen speak Claude's shape. Anything else — another CLI, a log cut
14
+ // short, a format that changed — is kept as plain text, never dropped.
15
+ import fs from "node:fs";
16
+ /** One tool result keeps this much; the rest is cut with a note saying how much. */
17
+ export const OUTPUT_MAX = { chars: 2_000, lines: 40 };
18
+ /** Arguments, summarized, keep this much. */
19
+ const INPUT_MAX = 600;
20
+ /** The launch prompt (long and the same for every task) keeps this much. */
21
+ const PROMPT_MAX = 3_000;
22
+ /** A raw (unparsed) log keeps this much. */
23
+ export const RAW_MAX = 200_000;
24
+ /** Logs bigger than this are read from the start only. */
25
+ const READ_MAX = 32 * 1024 * 1024;
26
+ /** Cut text to `chars` and `lines`, saying what was cut. */
27
+ export function cap(text, max = OUTPUT_MAX) {
28
+ let t = text.replace(/\s+$/, "");
29
+ const lines = t.split("\n");
30
+ let cut = "";
31
+ if (lines.length > max.lines) {
32
+ t = lines.slice(0, max.lines).join("\n");
33
+ cut = `${lines.length - max.lines} more line${lines.length - max.lines === 1 ? "" : "s"}`;
34
+ }
35
+ if (t.length > max.chars) {
36
+ const more = text.length - max.chars;
37
+ t = t.slice(0, max.chars);
38
+ cut = `${more.toLocaleString("en-US")} more characters`;
39
+ }
40
+ return cut ? `${t}\n… [cut: ${cut} — the whole output is in the agent's log]` : t;
41
+ }
42
+ const one = (s, n = INPUT_MAX) => {
43
+ const l = s.replace(/\s+/g, " ").trim();
44
+ return l.length <= n ? l : l.slice(0, n - 1) + "…";
45
+ };
46
+ const str = (v) => (v === undefined || v === null ? "" : typeof v === "string" ? v : JSON.stringify(v));
47
+ /**
48
+ * Absolute worktree paths → repo-relative ones. Every tool call names
49
+ * /…/repo/.huddle/worktrees/claude-aCN9HG/src/csv.js; a reader wants src/csv.js.
50
+ */
51
+ export function relPaths(s) {
52
+ return s.replace(CD_WT, "").replace(WT_SLASH, "").replace(WT, ".");
53
+ }
54
+ const WT_SRC = String.raw `[^\s"'\`]*?\.huddle[\\/]worktrees[\\/][^\\/\s"'\`]+`;
55
+ const CD_WT = new RegExp(String.raw `cd\s+(["']?)` + WT_SRC + String.raw `\1\s*(?:&&|;|\n)\s*`, "g");
56
+ const WT_SLASH = new RegExp(WT_SRC + String.raw `[\\/]`, "g");
57
+ const WT = new RegExp(WT_SRC, "g");
58
+ const tidy = (s) => relPaths(s);
59
+ /** "mcp__huddle__huddle_send" / "huddle-huddle_send" → "huddle_send". */
60
+ const toolName = (n) => n.replace(/^mcp__[^_]+(?:_[^_]+)*?__/, "").replace(/^huddle-/, "");
61
+ /** A tool's arguments in a line: the command, the path, the message — not the JSON. */
62
+ export function summarizeInput(tool, input) {
63
+ const a = (input && typeof input === "object" ? input : {});
64
+ const t = toolName(tool).toLowerCase();
65
+ if (typeof input === "string")
66
+ return tidy(one(input));
67
+ // A command keeps its lines (a heredoc is a file being written), capped like an output.
68
+ if (a.command !== undefined)
69
+ return tidy(cap(Array.isArray(a.command) ? a.command.join(" ") : str(a.command), { chars: 1_500, lines: 30 }));
70
+ const file = a.file_path ?? a.path ?? a.notebook_path ?? a.targetFile;
71
+ if (/^(write|create)/.test(t) && file)
72
+ return `${tidy(str(file))} (${str(a.content ?? a.contents ?? a.streamContent).split("\n").length} lines)`;
73
+ if (/edit/.test(t) && file)
74
+ return tidy(str(file));
75
+ if (file && Object.keys(a).length <= 3)
76
+ return tidy(`${str(file)}${a.offset || a.limit ? ` (from line ${a.offset ?? 1}${a.limit ? `, ${a.limit} lines` : ""})` : ""}`);
77
+ if (a.pattern !== undefined || a.globPattern !== undefined)
78
+ return tidy(one(`${str(a.pattern ?? a.globPattern)}${a.path || a.targetDirectory ? " in " + str(a.path ?? a.targetDirectory) : ""}`));
79
+ // Huddle's own tools: who it's to and what it says — the conversation, in the agent's words.
80
+ if (a.to !== undefined && (a.message !== undefined || a.body !== undefined))
81
+ return one(`to ${str(a.to)}: ${str(a.message ?? a.body)}`, 2_000);
82
+ if (a.question !== undefined)
83
+ return one(`${a.to ? "to " + str(a.to) + ": " : ""}${str(a.question)}`, 2_000);
84
+ const keys = Object.keys(a);
85
+ if (!keys.length)
86
+ return "";
87
+ return tidy(one(keys.map((k) => `${k}: ${typeof a[k] === "string" ? a[k] : JSON.stringify(a[k])}`).join(" · ")));
88
+ }
89
+ /** Text out of an MCP / Claude tool result: strings, [{type:"text", text}], {content:[…]}. */
90
+ function resultText(v) {
91
+ if (v === undefined || v === null)
92
+ return "";
93
+ if (typeof v === "string")
94
+ return v;
95
+ if (Array.isArray(v))
96
+ return v
97
+ .map((x) => (typeof x === "string" ? x : x?.type === "text" || typeof x?.text === "string" ? str(x.text) : typeof x?.text?.text === "string" ? x.text.text : x?.type === "tool_reference" ? `(loaded ${toolName(str(x.tool_name))})` : x?.type === "image" ? "(an image)" : JSON.stringify(x)))
98
+ .join("\n");
99
+ if (typeof v === "object") {
100
+ const o = v;
101
+ if (o.content !== undefined)
102
+ return resultText(o.content);
103
+ if (o.text !== undefined)
104
+ return resultText(o.text);
105
+ if (o.output !== undefined)
106
+ return resultText(o.output);
107
+ return JSON.stringify(o, null, 1);
108
+ }
109
+ return String(v);
110
+ }
111
+ function json(line) {
112
+ const l = line.trim();
113
+ if (!l.startsWith("{"))
114
+ return undefined;
115
+ try {
116
+ return JSON.parse(l);
117
+ }
118
+ catch {
119
+ return undefined;
120
+ }
121
+ }
122
+ /** Which CLI wrote this log, from its lines (the agent's id is only a hint: custom ids exist). */
123
+ export function sniffFormat(lines, hint = "") {
124
+ let claude = 0;
125
+ let codex = 0;
126
+ let cursor = 0;
127
+ for (const l of lines.slice(0, 400)) {
128
+ const e = json(l);
129
+ if (!e || typeof e !== "object")
130
+ continue;
131
+ if (/^(thread\.started|turn\.(started|completed|failed)|item\.(started|updated|completed))$/.test(str(e.type)))
132
+ codex++;
133
+ else if (e.type === "tool_call" || (e.type === "thinking" && e.subtype) || e.model_call_id || e.timestamp_ms)
134
+ cursor++;
135
+ else if ((e.type === "assistant" || e.type === "user") && e.message && Array.isArray(e.message.content) && (e.message.id || e.parent_tool_use_id !== undefined || e.uuid))
136
+ claude++;
137
+ else if (e.type === "system" && e.subtype === "init" && Array.isArray(e.tools))
138
+ claude++;
139
+ else if (e.type === "assistant" || e.type === "user" || e.type === "result")
140
+ claude += 0.5;
141
+ }
142
+ const best = Math.max(claude, codex, cursor);
143
+ if (!best)
144
+ return "raw";
145
+ const h = hint.split("#")[0];
146
+ if (h === "cursor" && cursor === best)
147
+ return "cursor";
148
+ if (h === "codex" && codex === best)
149
+ return "codex";
150
+ return codex === best ? "codex" : cursor === best ? "cursor" : "claude";
151
+ }
152
+ /** Consecutive non-JSON lines (stderr, a banner) as one note. */
153
+ function notes(lines, into) {
154
+ const t = lines.join("\n").trim();
155
+ if (t)
156
+ into.push({ kind: "note", text: cap(tidy(t), { chars: 1_000, lines: 15 }) });
157
+ lines.length = 0;
158
+ }
159
+ function parseClaude(lines) {
160
+ const turns = [];
161
+ const calls = new Map();
162
+ const loose = [];
163
+ for (const line of lines) {
164
+ const e = json(line);
165
+ if (!e) {
166
+ if (line.trim())
167
+ loose.push(line);
168
+ continue;
169
+ }
170
+ notes(loose, turns);
171
+ if (e.type === "assistant" && Array.isArray(e.message?.content)) {
172
+ for (const c of e.message.content) {
173
+ if (c?.type === "text" && str(c.text).trim())
174
+ turns.push({ kind: "text", text: str(c.text).trim() });
175
+ else if (c?.type === "thinking" && str(c.thinking).trim())
176
+ turns.push({ kind: "thinking", text: str(c.thinking).trim() });
177
+ else if (c?.type === "tool_use") {
178
+ const name = toolName(str(c.name));
179
+ const t = { kind: "tool", tool: name, text: "", input: summarizeInput(str(c.name), c.input) };
180
+ // What it wrote: a Write's content, an Edit's change — trimmed like an output.
181
+ const i = c.input ?? {};
182
+ if (/^(Write|create_file)$/i.test(name) && typeof i.content === "string")
183
+ t.output = cap(i.content);
184
+ else if (/Edit$/i.test(name) && (typeof i.old_string === "string" || typeof i.new_string === "string"))
185
+ t.output = cap(`${str(i.old_string).split("\n").map((l) => "- " + l).join("\n")}\n${str(i.new_string).split("\n").map((l) => "+ " + l).join("\n")}`);
186
+ else if (/MultiEdit$/i.test(name) && Array.isArray(i.edits))
187
+ t.output = cap(`${i.edits.length} edit(s)`);
188
+ t.text = `${name}${t.input ? " " + t.input : ""}`;
189
+ if (typeof i.description === "string" && i.description.trim())
190
+ t.about = one(i.description, 200);
191
+ turns.push(t);
192
+ if (c.id)
193
+ calls.set(str(c.id), t);
194
+ }
195
+ }
196
+ }
197
+ else if (e.type === "user" && e.message) {
198
+ const content = Array.isArray(e.message.content) ? e.message.content : [{ type: "text", text: str(e.message.content) }];
199
+ for (const c of content) {
200
+ if (c?.type === "tool_result") {
201
+ const t = calls.get(str(c.tool_use_id));
202
+ const out = tidy(resultText(c.content));
203
+ const wrote = t?.output;
204
+ if (t) {
205
+ t.error = c.is_error === true || undefined;
206
+ // A Write's result ("File created successfully…") says less than what it wrote.
207
+ if (!wrote || t.error)
208
+ t.output = cap(out);
209
+ }
210
+ else
211
+ turns.push({ kind: "tool", tool: "?", text: "(result of an earlier call)", output: cap(out), ...(c.is_error ? { error: true } : {}) });
212
+ }
213
+ else if (c?.type === "text" && str(c.text).trim())
214
+ turns.push({ kind: turns.length ? "note" : "prompt", text: cap(tidy(str(c.text)), { chars: PROMPT_MAX, lines: 80 }) });
215
+ }
216
+ }
217
+ else if (e.type === "result") {
218
+ const bits = [e.subtype && e.subtype !== "success" ? str(e.subtype) : "finished", e.duration_ms ? `${Math.round(Number(e.duration_ms) / 1000)}s` : "", e.num_turns ? `${e.num_turns} turns` : "", e.is_error ? "with an error" : ""];
219
+ turns.push({ kind: "end", text: bits.filter(Boolean).join(" · ") });
220
+ }
221
+ else if (e.type === "system" && e.subtype === "init") {
222
+ turns.push({ kind: "note", text: `session started${e.model ? " · " + str(e.model) : ""}` });
223
+ }
224
+ // hook_*, thinking_tokens, rate_limit_event, tool_progress: plumbing.
225
+ }
226
+ notes(loose, turns);
227
+ return turns;
228
+ }
229
+ function parseCodex(lines) {
230
+ const turns = [];
231
+ const loose = [];
232
+ for (const line of lines) {
233
+ const e = json(line);
234
+ if (!e) {
235
+ // "Reading additional input from stdin..." is codex reading its prompt: noise.
236
+ if (line.trim() && !/^Reading (additional )?(input|prompt) from stdin/i.test(line.trim()))
237
+ loose.push(line);
238
+ continue;
239
+ }
240
+ notes(loose, turns);
241
+ if (e.type === "item.completed" && e.item) {
242
+ const it = e.item;
243
+ switch (it.type) {
244
+ case "agent_message":
245
+ case "assistant_message":
246
+ if (str(it.text).trim())
247
+ turns.push({ kind: "text", text: str(it.text).trim() });
248
+ break;
249
+ case "reasoning":
250
+ if (str(it.text ?? it.summary).trim())
251
+ turns.push({ kind: "thinking", text: resultText(it.text ?? it.summary).trim() });
252
+ break;
253
+ case "command_execution": {
254
+ const input = tidy(cap(str(it.command).replace(/^\/bin\/(?:ba|z)?sh -lc /, "").replace(/^(["'])([\s\S]*)\1$/, "$2"), { chars: 1_500, lines: 30 }));
255
+ const code = it.exit_code;
256
+ turns.push({ kind: "tool", tool: "shell", text: `shell ${input}`, input, output: cap(tidy(str(it.aggregated_output))) + (typeof code === "number" && code !== 0 ? `\n(exit ${code})` : ""), ...(typeof code === "number" && code !== 0 ? { error: true } : {}) });
257
+ break;
258
+ }
259
+ case "mcp_tool_call": {
260
+ const name = toolName(str(it.tool));
261
+ const input = summarizeInput(name, it.arguments);
262
+ const err = it.error ? resultText(it.error.message ?? it.error) : "";
263
+ turns.push({ kind: "tool", tool: name, text: `${name}${input ? " " + input : ""}`, input, output: cap(tidy(err || resultText(it.result))), ...(err || it.status === "failed" ? { error: true } : {}) });
264
+ break;
265
+ }
266
+ case "file_change": {
267
+ const ch = Array.isArray(it.changes) ? it.changes : [];
268
+ const input = ch.map((c) => `${str(c.kind) || "edit"} ${tidy(str(c.path))}`).join(", ");
269
+ turns.push({ kind: "tool", tool: "edit", text: `edit ${input}`, input, ...(it.status === "failed" ? { error: true } : {}) });
270
+ break;
271
+ }
272
+ case "web_search":
273
+ turns.push({ kind: "tool", tool: "web_search", text: `web_search ${one(str(it.query))}`, input: one(str(it.query)) });
274
+ break;
275
+ case "todo_list":
276
+ turns.push({ kind: "note", text: `plan: ${(Array.isArray(it.items) ? it.items : []).map((x) => `${x.completed ? "✓" : "○"} ${str(x.text)}`).join(" · ")}` });
277
+ break;
278
+ case "error":
279
+ turns.push({ kind: "note", text: `error: ${str(it.message)}` });
280
+ break;
281
+ default:
282
+ turns.push({ kind: "note", text: `[${str(it.type)}] ${one(JSON.stringify(it), 400)}` });
283
+ }
284
+ }
285
+ else if (e.type === "turn.completed") {
286
+ const u = e.usage ?? {};
287
+ const tok = (Number(u.input_tokens) || 0) + (Number(u.output_tokens) || 0);
288
+ turns.push({ kind: "end", text: `turn finished${tok ? ` · ${tok.toLocaleString("en-US")} tokens` : ""}` });
289
+ }
290
+ else if (e.type === "turn.failed" || e.type === "error") {
291
+ turns.push({ kind: "note", text: `error: ${str(e.error?.message ?? e.message ?? e.error)}` });
292
+ }
293
+ }
294
+ notes(loose, turns);
295
+ return turns;
296
+ }
297
+ /** cursor-agent's tool_call: { <name>ToolCall: { args, result: { success | failure } } }. */
298
+ function cursorTool(tc) {
299
+ const key = Object.keys(tc).find((k) => /ToolCall$/.test(k)) ?? Object.keys(tc)[0] ?? "tool";
300
+ const body = tc[key] ?? {};
301
+ const args = body.args ?? {};
302
+ let name = key.replace(/ToolCall$/, "");
303
+ let input;
304
+ if (name === "mcp") {
305
+ name = toolName(str(args.toolName ?? args.name));
306
+ input = summarizeInput(name, args.args);
307
+ }
308
+ else if (name === "shell")
309
+ input = tidy(cap(str(args.command), { chars: 1_500, lines: 30 }));
310
+ else if (name === "edit" || name === "write")
311
+ input = tidy(str(args.path));
312
+ else
313
+ input = summarizeInput(name, args);
314
+ const r = body.result ?? {};
315
+ const ok = r.success;
316
+ const bad = r.failure ?? r.error ?? r.rejected;
317
+ const res = ok ?? bad;
318
+ let output = "";
319
+ if (res && typeof res === "object") {
320
+ if (name === "shell")
321
+ output = [str(res.stdout), str(res.stderr)].filter((x) => x.trim()).join("\n") + (res.exitCode ? `\n(exit ${res.exitCode})` : "");
322
+ else if (name === "edit" || name === "write")
323
+ output = str(res.diffString) || str(res.message);
324
+ else if (name === "glob")
325
+ output = (Array.isArray(res.files) ? res.files : []).join("\n");
326
+ else if (name === "read")
327
+ output = str(res.content);
328
+ else
329
+ output = resultText(res);
330
+ }
331
+ else if (res !== undefined)
332
+ output = str(res);
333
+ const error = !!bad || (name === "shell" && Number(res?.exitCode) > 0) || ok?.isError === true;
334
+ return { kind: "tool", tool: name, text: `${name}${input ? " " + input : ""}`, input, output: cap(tidy(output)), ...(error ? { error: true } : {}) };
335
+ }
336
+ function parseCursor(lines) {
337
+ const turns = [];
338
+ const loose = [];
339
+ let thinking = "";
340
+ const flush = () => {
341
+ if (thinking.trim())
342
+ turns.push({ kind: "thinking", text: thinking.trim() });
343
+ thinking = "";
344
+ };
345
+ for (const line of lines) {
346
+ const e = json(line);
347
+ if (!e) {
348
+ if (line.trim())
349
+ loose.push(line);
350
+ continue;
351
+ }
352
+ notes(loose, turns);
353
+ const ts = Number(e.timestamp_ms) || undefined;
354
+ if (e.type === "thinking") {
355
+ if (e.subtype === "delta")
356
+ thinking += str(e.text);
357
+ else
358
+ flush();
359
+ continue;
360
+ }
361
+ flush();
362
+ if (e.type === "assistant") {
363
+ const text = resultText(e.message?.content).trim();
364
+ if (text)
365
+ turns.push({ kind: "text", text, ...(ts ? { ts } : {}) });
366
+ }
367
+ else if (e.type === "user") {
368
+ const text = resultText(e.message?.content).trim();
369
+ if (text)
370
+ turns.push({ kind: turns.some((t) => t.kind === "prompt") ? "note" : "prompt", text: cap(tidy(text), { chars: PROMPT_MAX, lines: 80 }) });
371
+ }
372
+ else if (e.type === "tool_call" && e.subtype === "completed" && e.tool_call) {
373
+ turns.push({ ...cursorTool(e.tool_call), ...(ts ? { ts } : {}) });
374
+ }
375
+ else if (e.type === "result") {
376
+ turns.push({ kind: "end", text: [e.is_error ? "finished with an error" : "finished", e.duration_ms ? `${Math.round(Number(e.duration_ms) / 1000)}s` : ""].filter(Boolean).join(" · ") });
377
+ }
378
+ else if (e.type === "system" && e.subtype === "init") {
379
+ turns.push({ kind: "note", text: `session started${e.model ? " · " + str(e.model) : ""}` });
380
+ }
381
+ }
382
+ flush();
383
+ notes(loose, turns);
384
+ return turns;
385
+ }
386
+ /** A log's text → turns. Unknown formats are kept as one raw block (capped). */
387
+ export function parseLog(text, agentHint = "") {
388
+ const lines = text.split(/\r?\n/);
389
+ const format = sniffFormat(lines, agentHint);
390
+ try {
391
+ const turns = format === "claude" ? parseClaude(lines) : format === "codex" ? parseCodex(lines) : format === "cursor" ? parseCursor(lines) : [];
392
+ if (format !== "raw" && turns.length)
393
+ return { format, turns };
394
+ }
395
+ catch {
396
+ /* a shape we didn't expect: fall back to the raw text below */
397
+ }
398
+ const raw = text.trim();
399
+ return { format: "raw", turns: raw ? [{ kind: "note", text: cap(tidy(raw), { chars: RAW_MAX, lines: 4_000 }) }] : [] };
400
+ }
401
+ /** Read and parse one log file; null when it isn't there. */
402
+ export function readLog(file, agentHint = "") {
403
+ let st;
404
+ try {
405
+ st = fs.statSync(file);
406
+ }
407
+ catch {
408
+ return null;
409
+ }
410
+ if (!st.isFile())
411
+ return null;
412
+ let text;
413
+ let partial = false;
414
+ if (st.size > READ_MAX) {
415
+ const fd = fs.openSync(file, "r");
416
+ try {
417
+ const buf = Buffer.alloc(READ_MAX);
418
+ const n = fs.readSync(fd, buf, 0, READ_MAX, 0);
419
+ text = buf.subarray(0, n).toString("utf8");
420
+ text = text.slice(0, text.lastIndexOf("\n") + 1);
421
+ partial = true;
422
+ }
423
+ finally {
424
+ fs.closeSync(fd);
425
+ }
426
+ }
427
+ else
428
+ text = fs.readFileSync(file, "utf8");
429
+ const parsed = parseLog(text, agentHint);
430
+ if (partial)
431
+ parsed.turns.push({ kind: "note", text: `… the log is ${Math.round(st.size / 1024 / 1024)} MB; only its first ${READ_MAX / 1024 / 1024} MB are shown` });
432
+ return { ...parsed, bytes: st.size, ...(partial ? { partial } : {}) };
433
+ }
434
+ //# sourceMappingURL=transcript.js.map