@agentproto/runtime 0.6.0 → 0.7.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.
@@ -1,66 +1,892 @@
1
- import { promises } from 'fs';
1
+ import { createReadStream, promises } from 'fs';
2
2
  import { homedir } from 'os';
3
- import { resolve, join } from 'path';
3
+ import { join, resolve } from 'path';
4
+ import { createInterface } from 'readline';
4
5
 
5
6
  /**
6
7
  * @agentproto/runtime v0.1.0-alpha
7
8
  * Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
8
9
  */
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __esm = (fn, res) => function __init() {
13
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
14
+ };
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
18
+ };
9
19
 
10
- var RESUME_STRATEGIES = {
11
- "claude-code": {
12
- // Printed by claude on graceful exit when session persistence
13
- // is on (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`
14
- outputHint: /claude\s+--resume\s+([0-9a-f-]{8,})/i,
15
- storeAs: "claudeResumeId",
16
- fsProbe: probeMtimeLatestJsonl(".claude/projects"),
17
- spawnArgs: (id) => ["claude", "--resume", id]
20
+ // src/tool-presenter.ts
21
+ function isRecord(value) {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+ function truncate(value, max) {
25
+ const oneLine = value.replace(/\s+/g, " ").trim();
26
+ return oneLine.length > max ? `${oneLine.slice(0, max - 1)}\u2026` : oneLine;
27
+ }
28
+ function formatArgValue(value) {
29
+ if (typeof value === "string") return value;
30
+ if (Array.isArray(value)) return `${value.length} item${value.length === 1 ? "" : "s"}`;
31
+ if (isRecord(value)) return JSON.stringify(value);
32
+ return String(value);
33
+ }
34
+ function pickSalientArg(args) {
35
+ for (const key of SALIENT_ARG_KEYS) {
36
+ const value = args[key];
37
+ if (value !== void 0 && value !== null && value !== "") {
38
+ return formatArgValue(value);
39
+ }
18
40
  }
19
- // Stubs for other shipped adapters — fill in as we learn each
20
- // provider's resume mechanism. Today they fall back to ACP-level
21
- // resume (whatever the agent-cli runtime supports) or fresh spawn.
22
- //
23
- // hermes: { storeAs: "hermesResumeId", ... }
24
- // codex: { storeAs: "codexResumeId", ... }
25
- // openclaw: { storeAs: "openClawResumeId", ... }
26
- // opencode: { storeAs: "openCodeResumeId", ... }
27
- };
28
- function probeMtimeLatestJsonl(storeRel) {
29
- return async (cwd, prevStartedAt, expectedId) => {
30
- const encoded = cwd.replace(/\//g, "-");
31
- const dir = resolve(homedir(), storeRel, encoded);
32
- if (expectedId) {
41
+ return null;
42
+ }
43
+ function subagentSummary(args) {
44
+ const description = typeof args.description === "string" ? args.description : typeof args.prompt === "string" ? args.prompt : "subagent";
45
+ return `\u21B3 subagent: ${truncate(description, MAX_CALL_LENGTH)}`;
46
+ }
47
+ function formatToolCall(toolName, args) {
48
+ const name = toolName || "tool";
49
+ const argsRecord = isRecord(args) ? args : {};
50
+ const bespoke = BESPOKE_TOOLS[name.toLowerCase()];
51
+ if (bespoke) return bespoke(argsRecord);
52
+ const salient = pickSalientArg(argsRecord);
53
+ if (salient !== null) {
54
+ if (name.toLowerCase().includes(salient.toLowerCase())) {
55
+ return truncate(name, MAX_CALL_LENGTH);
56
+ }
57
+ return truncate(`${name} ${salient}`, MAX_CALL_LENGTH);
58
+ }
59
+ if (Object.keys(argsRecord).length === 0) return name;
60
+ return truncate(`${name} ${JSON.stringify(args)}`, MAX_CALL_LENGTH);
61
+ }
62
+ var SALIENT_ARG_KEYS, MAX_CALL_LENGTH, BESPOKE_TOOLS;
63
+ var init_tool_presenter = __esm({
64
+ "src/tool-presenter.ts"() {
65
+ SALIENT_ARG_KEYS = [
66
+ "file_path",
67
+ "path",
68
+ "filePath",
69
+ "file",
70
+ "command",
71
+ "pattern",
72
+ "query",
73
+ "q",
74
+ "url",
75
+ "todos",
76
+ "description",
77
+ "prompt"
78
+ ];
79
+ MAX_CALL_LENGTH = 120;
80
+ BESPOKE_TOOLS = {
81
+ schedulewakeup: (args) => {
82
+ const delay = args.delaySeconds ?? args.delay ?? "?";
83
+ const reason = typeof args.reason === "string" ? args.reason : "";
84
+ return reason ? `\u23F0 wake in ${delay}s \u2014 ${reason}` : `\u23F0 wake in ${delay}s`;
85
+ },
86
+ task: subagentSummary,
87
+ agent: subagentSummary,
88
+ todowrite: (args) => {
89
+ const todos = Array.isArray(args.todos) ? args.todos : [];
90
+ return `\u2611 todos (${todos.length})`;
91
+ },
92
+ exitplanmode: () => "\u{1F4CB} plan ready"
93
+ };
94
+ }
95
+ });
96
+ function sessionTranscriptDir(sessionId, baseDir) {
97
+ return join(join(homedir(), ".agentproto", "sessions"), sessionId);
98
+ }
99
+ function sessionEventsPath(sessionId, baseDir) {
100
+ return join(sessionTranscriptDir(sessionId), "events.jsonl");
101
+ }
102
+ var init_transcript_writer = __esm({
103
+ "src/transcript-writer.ts"() {
104
+ }
105
+ });
106
+
107
+ // src/transcript-export.ts
108
+ var transcript_export_exports = {};
109
+ __export(transcript_export_exports, {
110
+ crossValidateHermesExport: () => crossValidateHermesExport,
111
+ discoverHermesSessions: () => discoverHermesSessions,
112
+ exportAgentSession: () => exportAgentSession,
113
+ exportClaudeCodeSession: () => exportClaudeCodeSession,
114
+ exportHermesSession: () => exportHermesSession,
115
+ renderJson: () => renderJson,
116
+ renderMarkdown: () => renderMarkdown
117
+ });
118
+ function trunc(text, n) {
119
+ if (text.length <= n) return text;
120
+ return text.slice(0, n) + `
121
+ \u2026 [${text.length - n} chars truncated]`;
122
+ }
123
+ function renderMarkdown(session, opts = {}) {
124
+ const { meta, messages } = session;
125
+ const maxToolChars = opts.maxToolChars ?? 1200;
126
+ const out = [];
127
+ out.push(`# ${meta.title ?? "(untitled)"}`);
128
+ out.push("");
129
+ const sourceNote = meta.source ? ` \xB7 source \`${meta.source}\`` : "";
130
+ out.push(`> Session${sourceNote}`);
131
+ out.push("");
132
+ out.push("| | |");
133
+ out.push("|---|---|");
134
+ if (meta.model) out.push(`| Model | \`${meta.model}\` |`);
135
+ if (meta.startedAt || meta.endedAt) {
136
+ out.push(`| Start \u2192 end | ${meta.startedAt ?? "?"} \u2192 ${meta.endedAt ?? "?"} |`);
137
+ }
138
+ if (meta.messageCount !== void 0 || meta.toolCallCount !== void 0) {
139
+ const parts = [];
140
+ if (meta.messageCount !== void 0) parts.push(`${meta.messageCount} messages`);
141
+ if (meta.toolCallCount !== void 0) parts.push(`${meta.toolCallCount} tool calls`);
142
+ out.push(`| Count | ${parts.join(" \xB7 ")} |`);
143
+ }
144
+ if (meta.tokens && Object.keys(meta.tokens).length) {
145
+ const t = meta.tokens;
146
+ const parts = [];
147
+ if (t.input !== void 0) parts.push(`in ${t.input.toLocaleString("en-US")}`);
148
+ if (t.output !== void 0) parts.push(`out ${t.output.toLocaleString("en-US")}`);
149
+ if (t.cacheRead !== void 0 || t.cacheWrite !== void 0) {
150
+ parts.push(
151
+ `cache r/w ${(t.cacheRead ?? 0).toLocaleString("en-US")}/${(t.cacheWrite ?? 0).toLocaleString("en-US")}`
152
+ );
153
+ }
154
+ if (t.reasoning !== void 0) parts.push(`reason. ${t.reasoning.toLocaleString("en-US")}`);
155
+ if (parts.length) out.push(`| Tokens | ${parts.join(" \xB7 ")} |`);
156
+ }
157
+ if (meta.costUsd !== void 0) {
158
+ out.push(`| Cost | $${meta.costUsd.toFixed(4)} |`);
159
+ }
160
+ out.push("");
161
+ out.push("---");
162
+ out.push("");
163
+ for (const m of messages) {
164
+ const icon = ROLE_ICON[m.role] ?? m.role;
165
+ const nameSuffix = m.toolName ? ` \xB7 \`${m.toolName}\`` : "";
166
+ out.push(`### ${icon}${nameSuffix}`);
167
+ if (m.reasoning?.trim()) {
168
+ out.push("");
169
+ out.push("<details><summary>\u{1F4AD} reasoning</summary>");
170
+ out.push("");
171
+ out.push(trunc(m.reasoning, 4e3));
172
+ out.push("");
173
+ out.push("</details>");
174
+ }
175
+ if (m.text?.trim()) {
176
+ out.push("");
177
+ if (m.role === "tool") {
178
+ out.push("```");
179
+ out.push(trunc(m.text, maxToolChars));
180
+ out.push("```");
181
+ } else {
182
+ out.push(m.text.trim());
183
+ }
184
+ }
185
+ if (m.toolCalls?.length) {
186
+ out.push("");
187
+ for (const tc of m.toolCalls) {
188
+ let parsedArgs;
189
+ try {
190
+ parsedArgs = JSON.parse(tc.args);
191
+ } catch {
192
+ parsedArgs = tc.args;
193
+ }
194
+ out.push(`> \u{1F4DE} ${formatToolCall(tc.name, parsedArgs)}`);
195
+ }
196
+ }
197
+ out.push("");
198
+ }
199
+ return out.join("\n");
200
+ }
201
+ function renderJson(session) {
202
+ return JSON.stringify(session, null, 2);
203
+ }
204
+ async function exportClaudeCodeSession(adapterSessionId, cwd) {
205
+ if (!cwd) {
206
+ throw new Error(
207
+ "claude-code exporter: cwd is required to locate the JSONL file.\nPass cwd explicitly or use a session id that is in the registry."
208
+ );
209
+ }
210
+ const encoded = cwd.replace(/\//g, "-");
211
+ const filePath = join(homedir(), ".claude", "projects", encoded, `${adapterSessionId}.jsonl`);
212
+ let stream;
213
+ try {
214
+ stream = createReadStream(filePath, { encoding: "utf8" });
215
+ await new Promise((resolve2, reject) => {
216
+ stream.once("error", reject);
217
+ stream.once("open", resolve2);
218
+ });
219
+ } catch (err) {
220
+ const code = err.code;
221
+ if (code === "ENOENT") {
222
+ throw new Error(
223
+ `claude-code: JSONL file not found: ${filePath}
224
+ Verify cwd="${cwd}" and adapterSessionId="${adapterSessionId}".`
225
+ );
226
+ }
227
+ throw err;
228
+ }
229
+ const messages = [];
230
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
231
+ for await (const line of rl) {
232
+ const trimmed = line.trim();
233
+ if (!trimmed) continue;
234
+ let entry;
235
+ try {
236
+ entry = JSON.parse(trimmed);
237
+ } catch {
238
+ continue;
239
+ }
240
+ if (IGNORED_CLAUDE_TYPES.has(entry.type)) continue;
241
+ if (entry.type !== "user" && entry.type !== "assistant") continue;
242
+ const msg = entry.message;
243
+ if (!msg) continue;
244
+ const role = msg.role === "assistant" ? "assistant" : "user";
245
+ const content = msg.content;
246
+ if (typeof content === "string") {
247
+ if (content.trim()) messages.push({ role, text: content.trim() });
248
+ continue;
249
+ }
250
+ if (!Array.isArray(content)) continue;
251
+ let textAcc = "";
252
+ let reasoningAcc = "";
253
+ const toolCalls = [];
254
+ const toolResults = [];
255
+ for (const block of content) {
256
+ switch (block.type) {
257
+ case "text": {
258
+ textAcc += block.text;
259
+ break;
260
+ }
261
+ case "thinking": {
262
+ reasoningAcc += block.thinking;
263
+ break;
264
+ }
265
+ case "tool_use": {
266
+ const tb = block;
267
+ const args = typeof tb.input === "string" ? tb.input : JSON.stringify(tb.input ?? {});
268
+ toolCalls.push({ name: tb.name, args });
269
+ break;
270
+ }
271
+ case "tool_result": {
272
+ const tb = block;
273
+ let resultText = "";
274
+ if (typeof tb.content === "string") {
275
+ resultText = tb.content;
276
+ } else if (Array.isArray(tb.content)) {
277
+ for (const c of tb.content) {
278
+ if (c.type === "text") resultText += c.text;
279
+ }
280
+ }
281
+ toolResults.push({ text: resultText });
282
+ break;
283
+ }
284
+ }
285
+ }
286
+ if (textAcc.trim() || toolCalls.length || reasoningAcc) {
287
+ const m = { role };
288
+ if (textAcc.trim()) m.text = textAcc.trim();
289
+ if (reasoningAcc) m.reasoning = reasoningAcc;
290
+ if (toolCalls.length) m.toolCalls = toolCalls;
291
+ messages.push(m);
292
+ }
293
+ for (const tr of toolResults) {
294
+ messages.push({ role: "tool", text: tr.text });
295
+ }
296
+ }
297
+ return { meta: { source: "claude-code" }, messages };
298
+ }
299
+ async function openHermesDb(dbPath) {
300
+ let DatabaseSync;
301
+ try {
302
+ const sqlite = await import('sqlite');
303
+ DatabaseSync = sqlite.DatabaseSync;
304
+ } catch {
305
+ throw new Error("hermes: node:sqlite unavailable. Requires Node.js \u226522.5.0.");
306
+ }
307
+ try {
308
+ return new DatabaseSync(dbPath, { readOnly: true });
309
+ } catch (err) {
310
+ const msg = String(err);
311
+ if (err.code === "ENOENT" || msg.includes("unable to open database file")) {
312
+ throw new Error(
313
+ `hermes: state.db not found at ${dbPath}. Has hermes been run at least once?`
314
+ );
315
+ }
316
+ if (msg.includes("SQLITE_BUSY") || msg.includes("database is locked")) {
317
+ throw new Error(
318
+ `hermes: database is locked (SQLITE_BUSY). Hermes may be writing. Try again in a moment.`
319
+ );
320
+ }
321
+ throw err;
322
+ }
323
+ }
324
+ function withRetryOnBusy(fn) {
325
+ try {
326
+ return fn();
327
+ } catch (err) {
328
+ const msg = String(err);
329
+ if (msg.includes("SQLITE_BUSY") || msg.includes("database is locked")) {
330
+ try {
331
+ return fn();
332
+ } catch {
333
+ throw new Error(
334
+ `hermes: database is locked. Hermes may be actively writing. Try again.`
335
+ );
336
+ }
337
+ }
338
+ throw err;
339
+ }
340
+ }
341
+ async function exportHermesSession(adapterSessionId) {
342
+ const dbPath = join(homedir(), ".hermes", "state.db");
343
+ const db = await openHermesDb(dbPath);
344
+ const session = withRetryOnBusy(
345
+ () => db.prepare("SELECT * FROM sessions WHERE id = ?").get(adapterSessionId)
346
+ );
347
+ if (!session) {
348
+ db.close();
349
+ throw new Error(
350
+ `hermes: session "${adapterSessionId}" not found in ${dbPath}. ACP sessions are indexed by the agentproto UUID passed as resumeSessionId.`
351
+ );
352
+ }
353
+ const rows = withRetryOnBusy(
354
+ () => db.prepare(
355
+ "SELECT * FROM messages WHERE session_id = ? ORDER BY id ASC"
356
+ ).all(adapterSessionId)
357
+ );
358
+ db.close();
359
+ const cost = session.actual_cost_usd != null ? session.actual_cost_usd : session.estimated_cost_usd;
360
+ const startedAt = session.started_at ? new Date(session.started_at * 1e3).toISOString().replace("T", " ").slice(0, 19) : void 0;
361
+ const endedAt = session.ended_at ? new Date(session.ended_at * 1e3).toISOString().replace("T", " ").slice(0, 19) : void 0;
362
+ const meta = {
363
+ ...session.title ? { title: session.title } : {},
364
+ ...session.model ? { model: session.model } : {},
365
+ ...startedAt ? { startedAt } : {},
366
+ ...endedAt ? { endedAt } : {},
367
+ ...session.message_count !== void 0 ? { messageCount: session.message_count } : {},
368
+ ...session.tool_call_count !== void 0 ? { toolCallCount: session.tool_call_count } : {},
369
+ ...cost != null ? { costUsd: Number(cost) } : {},
370
+ ...session.source ? { source: session.source } : {}
371
+ };
372
+ const tokensInput = session.input_tokens;
373
+ const tokensOutput = session.output_tokens;
374
+ const tokensCacheRead = session.cache_read_tokens;
375
+ const tokensCacheWrite = session.cache_write_tokens;
376
+ const tokensReasoning = session.reasoning_tokens;
377
+ if (tokensInput !== void 0 || tokensOutput !== void 0 || tokensCacheRead !== void 0 || tokensCacheWrite !== void 0 || tokensReasoning !== void 0) {
378
+ meta.tokens = {
379
+ ...tokensInput !== void 0 ? { input: tokensInput } : {},
380
+ ...tokensOutput !== void 0 ? { output: tokensOutput } : {},
381
+ ...tokensCacheRead !== void 0 ? { cacheRead: tokensCacheRead } : {},
382
+ ...tokensCacheWrite !== void 0 ? { cacheWrite: tokensCacheWrite } : {},
383
+ ...tokensReasoning !== void 0 ? { reasoning: tokensReasoning } : {}
384
+ };
385
+ }
386
+ const messages = rows.map((row) => {
387
+ const validRoles = /* @__PURE__ */ new Set(["user", "assistant", "tool", "system"]);
388
+ const role = validRoles.has(row.role) ? row.role : "user";
389
+ const reasoning = row.reasoning ?? row.reasoning_content;
390
+ let toolCalls;
391
+ if (row.tool_calls) {
33
392
  try {
34
- await promises.stat(join(dir, `${expectedId}.jsonl`));
35
- return expectedId;
393
+ const tc = JSON.parse(row.tool_calls);
394
+ if (Array.isArray(tc) && tc.length) {
395
+ toolCalls = tc.map((c) => {
396
+ const entry = c;
397
+ const fn = entry.function != null ? entry.function : entry;
398
+ const name = String(fn.name ?? entry.name ?? "tool");
399
+ const args = typeof fn.arguments === "string" ? fn.arguments : JSON.stringify(fn.arguments ?? {});
400
+ return { name, args };
401
+ });
402
+ }
36
403
  } catch {
37
- return null;
38
404
  }
39
405
  }
40
- let entries;
406
+ const m = { role };
407
+ if (row.content?.trim()) m.text = row.content.trim();
408
+ if (reasoning && String(reasoning).trim()) m.reasoning = String(reasoning);
409
+ if (row.tool_name) m.toolName = row.tool_name;
410
+ if (toolCalls) m.toolCalls = toolCalls;
411
+ if (row.timestamp) m.ts = row.timestamp;
412
+ return m;
413
+ });
414
+ void crossValidateHermesExport(adapterSessionId, messages.length).catch(() => {
415
+ });
416
+ return { meta, messages };
417
+ }
418
+ function hermesRowToCandidate(row) {
419
+ const startedAtIso = row.started_at !== void 0 ? new Date(row.started_at * 1e3).toISOString() : void 0;
420
+ const lastActivitySec = row.ended_at ?? row.started_at;
421
+ const lastActivityIso = lastActivitySec !== void 0 ? new Date(lastActivitySec * 1e3).toISOString() : void 0;
422
+ return {
423
+ conversationId: row.id,
424
+ ...startedAtIso ? { startedAt: startedAtIso } : {},
425
+ ...lastActivityIso ? { lastActivityAt: lastActivityIso } : {},
426
+ ...row.message_count !== void 0 ? { messageCount: row.message_count } : {},
427
+ ...row.title ? { preview: row.title } : {},
428
+ ...row.source ? { lastWriter: row.source } : {}
429
+ };
430
+ }
431
+ async function discoverHermesSessions(cwd, since, expectedId) {
432
+ const dbPath = join(homedir(), ".hermes", "state.db");
433
+ let db;
434
+ try {
435
+ db = await openHermesDb(dbPath);
436
+ } catch (err) {
437
+ const msg = err instanceof Error ? err.message : String(err);
438
+ if (msg.includes("state.db not found")) return [];
439
+ throw err;
440
+ }
441
+ try {
442
+ if (expectedId) {
443
+ const row = withRetryOnBusy(
444
+ () => db.prepare("SELECT * FROM sessions WHERE id = ?").get(expectedId)
445
+ );
446
+ return row ? [hermesRowToCandidate(row)] : [];
447
+ }
448
+ const rows = withRetryOnBusy(
449
+ () => db.prepare("SELECT * FROM sessions WHERE cwd = ?").all(cwd)
450
+ );
451
+ const sinceMs = since ? Date.parse(since) : NaN;
452
+ return rows.filter((row) => {
453
+ if (!Number.isFinite(sinceMs)) return true;
454
+ const lastActivitySec = row.ended_at ?? row.started_at;
455
+ if (lastActivitySec === void 0) return true;
456
+ return lastActivitySec * 1e3 >= sinceMs - 1e3;
457
+ }).map(hermesRowToCandidate);
458
+ } finally {
459
+ db.close();
460
+ }
461
+ }
462
+ async function defaultHermesRunner(adapterSessionId) {
463
+ const { spawn } = await import('child_process');
464
+ return new Promise((resolve2, reject) => {
465
+ const chunks = [];
466
+ const errChunks = [];
467
+ const proc = spawn(
468
+ "hermes",
469
+ ["sessions", "export", "--session-id", adapterSessionId, "-"],
470
+ { stdio: ["ignore", "pipe", "pipe"] }
471
+ );
472
+ proc.stdout.on("data", (chunk) => chunks.push(chunk));
473
+ proc.stderr.on("data", (chunk) => errChunks.push(chunk));
474
+ proc.on("error", reject);
475
+ proc.on("close", (code) => {
476
+ if (code !== 0) {
477
+ reject(
478
+ new Error(
479
+ `hermes exited with code ${code}: ${Buffer.concat(errChunks).toString()}`
480
+ )
481
+ );
482
+ } else {
483
+ resolve2(Buffer.concat(chunks).toString("utf8"));
484
+ }
485
+ });
486
+ });
487
+ }
488
+ async function crossValidateHermesExport(adapterSessionId, sqliteCount, _runner = defaultHermesRunner) {
489
+ let stdout;
490
+ try {
491
+ stdout = await _runner(adapterSessionId);
492
+ } catch (err) {
493
+ const msg = String(err);
494
+ if (err.code === "ENOENT" || msg.includes("ENOENT") || msg.includes("not found")) {
495
+ return { sqliteCount, binaryCount: -1, matched: true, binaryUnavailable: true };
496
+ }
497
+ return null;
498
+ }
499
+ const binaryCount = stdout.split("\n").filter((l) => l.trim()).filter((l) => {
500
+ try {
501
+ const obj = JSON.parse(l);
502
+ return typeof obj.role === "string";
503
+ } catch {
504
+ return false;
505
+ }
506
+ }).length;
507
+ const matched = binaryCount === sqliteCount;
508
+ if (!matched) {
509
+ console.warn(
510
+ `[hermes export] cross-validation mismatch for session ${adapterSessionId}: SQLite=${sqliteCount} messages, binary=${binaryCount} messages. The state.db schema may have changed \u2014 check the SQLite reader in transcript-export.ts.`
511
+ );
512
+ }
513
+ return { sqliteCount, binaryCount, matched };
514
+ }
515
+ async function exportDaemonEventsSession(sessionId, desc) {
516
+ const filePath = sessionEventsPath(sessionId);
517
+ let stream;
518
+ try {
519
+ stream = createReadStream(filePath, { encoding: "utf8" });
520
+ await new Promise((resolve2, reject) => {
521
+ stream.once("error", reject);
522
+ stream.once("open", resolve2);
523
+ });
524
+ } catch (err) {
525
+ const code = err.code;
526
+ if (code === "ENOENT") {
527
+ throw new Error(
528
+ `daemon-events: no events.jsonl for session "${sessionId}" (${filePath}).
529
+ Either the session never drove an agent-cli turn, or it predates this feature.`
530
+ );
531
+ }
532
+ throw err;
533
+ }
534
+ const messages = [];
535
+ const toolNameById = /* @__PURE__ */ new Map();
536
+ let toolCallCount = 0;
537
+ let lastUsage;
538
+ let asmText = "";
539
+ let asmReasoning = "";
540
+ let asmToolCalls = [];
541
+ let asmTs;
542
+ const flushAssistant = () => {
543
+ if (!asmText.trim() && !asmReasoning.trim() && asmToolCalls.length === 0) return;
544
+ const m = { role: "assistant" };
545
+ if (asmText.trim()) m.text = asmText.trim();
546
+ if (asmReasoning.trim()) m.reasoning = asmReasoning.trim();
547
+ if (asmToolCalls.length) m.toolCalls = asmToolCalls;
548
+ if (asmTs !== void 0) m.ts = asmTs;
549
+ messages.push(m);
550
+ asmText = "";
551
+ asmReasoning = "";
552
+ asmToolCalls = [];
553
+ asmTs = void 0;
554
+ };
555
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
556
+ for await (const line of rl) {
557
+ const trimmed = line.trim();
558
+ if (!trimmed) continue;
559
+ let rec;
41
560
  try {
42
- entries = await promises.readdir(dir);
561
+ rec = JSON.parse(trimmed);
43
562
  } catch {
44
- return null;
563
+ continue;
564
+ }
565
+ const ts = Date.parse(rec.ts);
566
+ const tsOrUndefined = Number.isNaN(ts) ? void 0 : ts;
567
+ switch (rec.kind) {
568
+ case "user-prompt":
569
+ flushAssistant();
570
+ messages.push({ role: "user", text: rec.text ?? "", ...tsOrUndefined !== void 0 ? { ts: tsOrUndefined } : {} });
571
+ break;
572
+ case "text-delta":
573
+ if (asmTs === void 0) asmTs = tsOrUndefined;
574
+ asmText += rec.text ?? "";
575
+ break;
576
+ case "thought":
577
+ if (asmTs === void 0) asmTs = tsOrUndefined;
578
+ asmReasoning += rec.text ?? "";
579
+ break;
580
+ case "tool-call": {
581
+ if (asmTs === void 0) asmTs = tsOrUndefined;
582
+ const name = rec.toolName ?? "tool";
583
+ if (rec.toolCallId) toolNameById.set(rec.toolCallId, name);
584
+ const args = typeof rec.arguments === "string" ? rec.arguments : JSON.stringify(rec.arguments ?? {});
585
+ asmToolCalls.push({ name, args });
586
+ toolCallCount += 1;
587
+ break;
588
+ }
589
+ case "tool-result": {
590
+ flushAssistant();
591
+ const name = rec.toolCallId ? toolNameById.get(rec.toolCallId) : void 0;
592
+ const text = typeof rec.result === "string" ? rec.result : JSON.stringify(rec.result ?? "");
593
+ messages.push({
594
+ role: "tool",
595
+ text: rec.isError ? `[error] ${text}` : text,
596
+ ...name ? { toolName: name } : {},
597
+ ...tsOrUndefined !== void 0 ? { ts: tsOrUndefined } : {}
598
+ });
599
+ break;
600
+ }
601
+ case "agent-prompt":
602
+ flushAssistant();
603
+ messages.push({ role: "system", text: "[awaiting input] agent requested a decision" });
604
+ break;
605
+ case "plan": {
606
+ flushAssistant();
607
+ const entries = rec.entries ?? [];
608
+ const done = entries.filter((e) => e.status === "completed").length;
609
+ const list = entries.map((e) => e.content).join("; ");
610
+ messages.push({ role: "system", text: `[plan] ${done}/${entries.length} ${list}`.trim() });
611
+ break;
612
+ }
613
+ case "usage_update":
614
+ lastUsage = { size: rec.size, used: rec.used, cost: rec.cost };
615
+ break;
616
+ case "error":
617
+ flushAssistant();
618
+ messages.push({ role: "system", text: `[error] ${rec.error?.message ?? "unknown"}` });
619
+ break;
620
+ case "turn-end":
621
+ flushAssistant();
622
+ break;
45
623
  }
46
- const jsonl = entries.filter((e) => e.endsWith(".jsonl"));
47
- if (jsonl.length === 0) return null;
48
- const startedAtMs = Date.parse(prevStartedAt);
49
- const candidates = [];
50
- for (const f of jsonl) {
624
+ }
625
+ flushAssistant();
626
+ const meta = { source: "daemon-events" };
627
+ if (desc?.label) meta.title = desc.label;
628
+ if (desc?.model) meta.model = desc.model;
629
+ if (desc?.startedAt) meta.startedAt = desc.startedAt;
630
+ if (desc?.endedAt) meta.endedAt = desc.endedAt;
631
+ meta.messageCount = messages.length;
632
+ meta.toolCallCount = toolCallCount;
633
+ if (desc?.costUsd !== void 0) meta.costUsd = desc.costUsd;
634
+ else if (lastUsage?.cost) meta.costUsd = lastUsage.cost.amount;
635
+ if (desc?.tokensIn !== void 0 || desc?.tokensOut !== void 0) {
636
+ meta.tokens = {
637
+ ...desc?.tokensIn !== void 0 ? { input: desc.tokensIn } : {},
638
+ ...desc?.tokensOut !== void 0 ? { output: desc.tokensOut } : {}
639
+ };
640
+ }
641
+ return { meta, messages };
642
+ }
643
+ async function exportAgentSession(input) {
644
+ const { sessionId, registry, format = "markdown", maxToolChars = 1200 } = input;
645
+ const source = input.source ?? "auto";
646
+ let adapterSlug = input.adapter;
647
+ let cwd = input.cwd;
648
+ let adapterSessionId = sessionId;
649
+ const err = (msg) => ({
650
+ sessionId,
651
+ adapter: adapterSlug ?? "unknown",
652
+ format,
653
+ meta: {},
654
+ content: `Error: ${msg}`
655
+ });
656
+ const desc = registry.findByIdOrName(sessionId);
657
+ const daemonSessionId = desc?.id ?? sessionId;
658
+ if (desc) {
659
+ adapterSlug = adapterSlug ?? desc.adapterSlug;
660
+ cwd = cwd ?? desc.cwd;
661
+ if (desc.adapterSessionId) adapterSessionId = desc.adapterSessionId;
662
+ }
663
+ const tryNative = async () => {
664
+ if (!adapterSlug) {
665
+ throw new Error(
666
+ `session "${sessionId}" not found in registry and no adapter override supplied.
667
+ Pass adapter explicitly or use a known session id (sess_xxx or name).`
668
+ );
669
+ }
670
+ const exporter = EXPORT_STRATEGIES[adapterSlug];
671
+ if (!exporter) {
672
+ const supported = Object.keys(EXPORT_STRATEGIES).join(", ");
673
+ throw new Error(
674
+ `no exporter for adapter "${adapterSlug}". Supported: ${supported}.
675
+ Only sessions spawned via claude-code or hermes can be exported.`
676
+ );
677
+ }
678
+ return exporter.exportSession(adapterSessionId, cwd);
679
+ };
680
+ const tryDaemon = () => exportDaemonEventsSession(daemonSessionId, desc);
681
+ let session;
682
+ try {
683
+ if (source === "native") {
684
+ session = await tryNative();
685
+ } else if (source === "daemon") {
686
+ session = await tryDaemon();
687
+ } else {
51
688
  try {
52
- const st = await promises.stat(join(dir, f));
53
- if (!Number.isFinite(startedAtMs) || st.mtimeMs >= startedAtMs - 1e3) {
54
- candidates.push({ name: f, mtime: st.mtimeMs });
689
+ session = await tryNative();
690
+ } catch (nativeErr) {
691
+ try {
692
+ session = await tryDaemon();
693
+ } catch (daemonErr) {
694
+ const nativeMsg = nativeErr instanceof Error ? nativeErr.message : String(nativeErr);
695
+ const daemonMsg = daemonErr instanceof Error ? daemonErr.message : String(daemonErr);
696
+ return err(`${nativeMsg}
697
+ (daemon-events fallback also failed: ${daemonMsg})`);
55
698
  }
56
- } catch {
57
699
  }
58
700
  }
59
- if (candidates.length === 0) return null;
60
- candidates.sort((a, b) => b.mtime - a.mtime);
61
- return candidates[0].name.replace(/\.jsonl$/, "");
701
+ } catch (e) {
702
+ return err(e instanceof Error ? e.message : String(e));
703
+ }
704
+ const content = format === "json" ? renderJson(session) : renderMarkdown(session, { maxToolChars });
705
+ return {
706
+ sessionId,
707
+ adapter: adapterSlug ?? (session.meta.source === "daemon-events" ? "daemon" : "unknown"),
708
+ format,
709
+ meta: session.meta,
710
+ content
62
711
  };
63
712
  }
713
+ var ROLE_ICON, IGNORED_CLAUDE_TYPES, EXPORT_STRATEGIES;
714
+ var init_transcript_export = __esm({
715
+ "src/transcript-export.ts"() {
716
+ init_tool_presenter();
717
+ init_transcript_writer();
718
+ ROLE_ICON = {
719
+ user: "\u{1F9D1} User",
720
+ assistant: "\u{1F916} Assistant",
721
+ tool: "\u{1F527} Tool",
722
+ system: "\u2699\uFE0F System"
723
+ };
724
+ IGNORED_CLAUDE_TYPES = /* @__PURE__ */ new Set([
725
+ "queue-operation",
726
+ "attachment",
727
+ "file-history-snapshot",
728
+ "ai-title",
729
+ "last-prompt"
730
+ ]);
731
+ EXPORT_STRATEGIES = {
732
+ "claude-code": {
733
+ exportSession: exportClaudeCodeSession
734
+ },
735
+ hermes: {
736
+ exportSession: (id) => exportHermesSession(id)
737
+ }
738
+ };
739
+ }
740
+ });
741
+ function claudeCodeProjectDir(cwd) {
742
+ const encoded = cwd.replace(/\//g, "-");
743
+ return resolve(homedir(), ".claude", "projects", encoded);
744
+ }
745
+ function extractFirstText(content) {
746
+ if (typeof content === "string") {
747
+ const t = content.trim();
748
+ return t || void 0;
749
+ }
750
+ if (Array.isArray(content)) {
751
+ for (const block of content) {
752
+ if (block && typeof block === "object" && block.type === "text" && typeof block.text === "string") {
753
+ const t = block.text.trim();
754
+ if (t) return t;
755
+ }
756
+ }
757
+ }
758
+ return void 0;
759
+ }
760
+ async function scanClaudeJsonl(filePath) {
761
+ const stream = createReadStream(filePath, { encoding: "utf8" });
762
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
763
+ let startedAt;
764
+ let lastActivityAt;
765
+ let messageCount = 0;
766
+ let preview;
767
+ let lastWriter;
768
+ for await (const line of rl) {
769
+ const trimmed = line.trim();
770
+ if (!trimmed) continue;
771
+ let entry;
772
+ try {
773
+ entry = JSON.parse(trimmed);
774
+ } catch {
775
+ continue;
776
+ }
777
+ if (typeof entry.timestamp === "string") {
778
+ if (!startedAt) startedAt = entry.timestamp;
779
+ lastActivityAt = entry.timestamp;
780
+ }
781
+ if (typeof entry.entrypoint === "string") {
782
+ lastWriter = entry.entrypoint;
783
+ }
784
+ if (entry.type === "user" || entry.type === "assistant") {
785
+ messageCount += 1;
786
+ if (preview === void 0 && entry.type === "user") {
787
+ const text = extractFirstText(entry.message?.content);
788
+ if (text !== void 0) {
789
+ preview = text.length > 120 ? text.slice(0, 120) : text;
790
+ }
791
+ }
792
+ }
793
+ }
794
+ return { startedAt, lastActivityAt, messageCount, preview, lastWriter };
795
+ }
796
+ async function buildClaudeCandidate(filePath, conversationId) {
797
+ const scanned = await scanClaudeJsonl(filePath);
798
+ return { conversationId, ...scanned };
799
+ }
800
+ function claudeEntrypointFor(mode) {
801
+ return mode === "native" ? "cli" : "sdk-ts";
802
+ }
803
+ async function discoverClaudeCode(input) {
804
+ const { cwd, since, until, attachmentMode, expectedId } = input;
805
+ const dir = claudeCodeProjectDir(cwd);
806
+ if (expectedId) {
807
+ const filePath = join(dir, `${expectedId}.jsonl`);
808
+ try {
809
+ await promises.stat(filePath);
810
+ } catch {
811
+ return [];
812
+ }
813
+ return [await buildClaudeCandidate(filePath, expectedId)];
814
+ }
815
+ let entries;
816
+ try {
817
+ entries = await promises.readdir(dir);
818
+ } catch {
819
+ return [];
820
+ }
821
+ const jsonlFiles = entries.filter((e) => e.endsWith(".jsonl"));
822
+ if (jsonlFiles.length === 0) return [];
823
+ const sinceMs = since ? Date.parse(since) : NaN;
824
+ const untilMs = until ? Date.parse(until) : NaN;
825
+ const wantEntrypoint = attachmentMode ? claudeEntrypointFor(attachmentMode) : void 0;
826
+ const scored = [];
827
+ for (const f of jsonlFiles) {
828
+ const filePath = join(dir, f);
829
+ let mtimeMs;
830
+ try {
831
+ mtimeMs = (await promises.stat(filePath)).mtimeMs;
832
+ } catch {
833
+ continue;
834
+ }
835
+ if (Number.isFinite(sinceMs) && mtimeMs < sinceMs - 1e3) continue;
836
+ const conversationId = f.replace(/\.jsonl$/, "");
837
+ const candidate = await buildClaudeCandidate(filePath, conversationId);
838
+ if (Number.isFinite(untilMs) && candidate.startedAt !== void 0) {
839
+ const startedMs = Date.parse(candidate.startedAt);
840
+ if (Number.isFinite(startedMs) && startedMs > untilMs) continue;
841
+ }
842
+ if (wantEntrypoint !== void 0 && candidate.lastWriter !== void 0 && candidate.lastWriter !== wantEntrypoint) {
843
+ continue;
844
+ }
845
+ scored.push({ candidate, mtimeMs });
846
+ }
847
+ scored.sort((a, b) => b.mtimeMs - a.mtimeMs);
848
+ return scored.map((s) => s.candidate);
849
+ }
850
+ async function readClaudeCode(conversationId, cwd) {
851
+ const { exportClaudeCodeSession: exportClaudeCodeSession2 } = await Promise.resolve().then(() => (init_transcript_export(), transcript_export_exports));
852
+ return exportClaudeCodeSession2(conversationId, cwd);
853
+ }
854
+ var CONVERSATION_STORES = {
855
+ "claude-code": {
856
+ storeAs: "claudeResumeId",
857
+ // Printed by claude on graceful exit when session persistence is on
858
+ // (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`
859
+ outputHint: /claude\s+--resume\s+([0-9a-f-]{8,})/i,
860
+ attachArgv: (conversationId) => ["claude", "--resume", conversationId],
861
+ discover: discoverClaudeCode,
862
+ read: readClaudeCode
863
+ }};
864
+
865
+ // src/resume-strategies.ts
866
+ var claudeCodeStore = CONVERSATION_STORES["claude-code"];
867
+ var RESUME_STRATEGIES = {
868
+ "claude-code": {
869
+ outputHint: claudeCodeStore.outputHint,
870
+ storeAs: claudeCodeStore.storeAs,
871
+ fsProbe: async (cwd, prevStartedAt, expectedId) => {
872
+ const candidates = await claudeCodeStore.discover({
873
+ cwd,
874
+ since: prevStartedAt,
875
+ expectedId
876
+ });
877
+ return candidates[0]?.conversationId ?? null;
878
+ },
879
+ spawnArgs: claudeCodeStore.attachArgv
880
+ }
881
+ // Stubs for other shipped adapters — fill in as we learn each
882
+ // provider's resume mechanism. Today they fall back to ACP-level
883
+ // resume (whatever the agent-cli runtime supports) or fresh spawn.
884
+ //
885
+ // hermes: { storeAs: "hermesResumeId", ... }
886
+ // codex: { storeAs: "codexResumeId", ... }
887
+ // openclaw: { storeAs: "openClawResumeId", ... }
888
+ // opencode: { storeAs: "openCodeResumeId", ... }
889
+ };
64
890
  function hasResumeStrategy(adapterSlug) {
65
891
  if (!adapterSlug) return false;
66
892
  const s = RESUME_STRATEGIES[adapterSlug];