archbyte 0.6.1 → 0.7.1

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.
@@ -0,0 +1,692 @@
1
+ // Claude Transcripts Adapter — reads native Claude Code JSONL session transcripts
2
+ // from ~/.claude/projects/{encoded-project-path}/
3
+ // Converts full conversation transcripts into AgentSession(s) for the Sessions panel.
4
+ import { readFile, readdir, stat } from "fs/promises";
5
+ import { existsSync } from "fs";
6
+ import { homedir } from "os";
7
+ import path from "path";
8
+ // --- Cost model (USD per million tokens) ---
9
+ const COST_INPUT = 15; // $15/MTok
10
+ const COST_OUTPUT = 75; // $75/MTok
11
+ const COST_CACHE_READ = 1.5; // $1.50/MTok
12
+ const COST_CACHE_CREATE = 18.75; // $18.75/MTok
13
+ const MAX_TRUNCATE = 500; // Max chars for tool input/result truncation
14
+ // --- Per-file mtime cache for scanTranscriptIndex ---
15
+ const _indexCache = new Map();
16
+ /** Invalidate cached index entries. Call with a path to invalidate one file, or no args to clear all. */
17
+ export function invalidateIndexCache(filePath) {
18
+ if (filePath) {
19
+ _indexCache.delete(filePath);
20
+ }
21
+ else {
22
+ _indexCache.clear();
23
+ }
24
+ }
25
+ // --- Discovery helpers ---
26
+ export function getClaudeProjectsDir() {
27
+ return path.join(homedir(), ".claude", "projects");
28
+ }
29
+ export function encodeProjectPath(cwd) {
30
+ return "-" + cwd.replace(/^\//, "").replace(/\//g, "-");
31
+ }
32
+ export function getProjectTranscriptDir(cwd) {
33
+ const dir = path.join(getClaudeProjectsDir(), encodeProjectPath(cwd));
34
+ return existsSync(dir) ? dir : null;
35
+ }
36
+ async function findSessionFiles(projectDir) {
37
+ const results = [];
38
+ try {
39
+ const entries = await readdir(projectDir, { withFileTypes: true });
40
+ for (const entry of entries) {
41
+ if (entry.isFile() && entry.name.endsWith(".jsonl")) {
42
+ const sessionId = entry.name.replace(/\.jsonl$/, "");
43
+ const jsonlPath = path.join(projectDir, entry.name);
44
+ const subDir = path.join(projectDir, sessionId, "subagents");
45
+ results.push({
46
+ sessionId,
47
+ jsonlPath,
48
+ subagentDir: existsSync(subDir) ? subDir : undefined,
49
+ });
50
+ }
51
+ }
52
+ }
53
+ catch {
54
+ // Permission error or unreadable
55
+ }
56
+ return results;
57
+ }
58
+ const WRITE_TOOLS = new Set(["Edit", "Write", "NotebookEdit"]);
59
+ const READ_TOOLS = new Set(["Read", "Glob", "Grep"]);
60
+ const PIPELINE_PROMPT_PATTERNS = [
61
+ "Analyze this project and identify ALL architecturally significant components",
62
+ "## Components (use these exact IDs)",
63
+ "## Components\n",
64
+ "## Components\r",
65
+ "Event-driven patterns detected:",
66
+ "Detected language:",
67
+ "Import Graph (",
68
+ "Components (affected",
69
+ "Components (use these",
70
+ ];
71
+ function classifySession(firstPrompt, toolNames, eventCount) {
72
+ // Pipeline agent calls: short sessions matching known pipeline prompts
73
+ const hasWriteTools = [...toolNames].some(t => WRITE_TOOLS.has(t));
74
+ if (eventCount <= 8 && !hasWriteTools) {
75
+ for (const prefix of PIPELINE_PROMPT_PATTERNS) {
76
+ if (firstPrompt.startsWith(prefix))
77
+ return "pipeline";
78
+ }
79
+ if (firstPrompt.startsWith("Project: ") && firstPrompt.includes("Detected language:"))
80
+ return "pipeline";
81
+ if (firstPrompt.startsWith("## Components") && toolNames.size <= 1)
82
+ return "pipeline";
83
+ }
84
+ // Implementation: used write/edit tools
85
+ if ([...toolNames].some(t => WRITE_TOOLS.has(t)))
86
+ return "implementation";
87
+ // Exploration: used read/search tools but no writes
88
+ if ([...toolNames].some(t => READ_TOOLS.has(t)))
89
+ return "exploration";
90
+ // Conversation: everything else (Q&A, discussions)
91
+ return "conversation";
92
+ }
93
+ function extractLabel(firstPrompt, category) {
94
+ if (category === "pipeline")
95
+ return "pipeline agent";
96
+ let text = firstPrompt;
97
+ // Handle command invocations: <command-name>/foo</command-name><command-message>bar</command-message>
98
+ if (text.startsWith("<command-name>") || text.startsWith("<command-message>")) {
99
+ const match = text.match(/<command-message>([^<]+)/);
100
+ if (match)
101
+ text = `/${match[1].trim()}`;
102
+ }
103
+ // Strip local-command-caveat blocks (tag + content)
104
+ text = text.replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>\s*/g, "");
105
+ // Strip command XML wrappers but keep content
106
+ text = text.replace(/<\/?(?:command-name|command-args|system-reminder)[^>]*>/g, "");
107
+ // Strip any remaining XML/HTML tags
108
+ text = text.replace(/<[^>]+>/g, "").trim();
109
+ // Strip common prefixes
110
+ if (text.startsWith("Implement the following plan:")) {
111
+ text = text.slice("Implement the following plan:".length);
112
+ }
113
+ // Extract first meaningful line (skip blank lines, markdown headers)
114
+ const lines = text.trim().split("\n");
115
+ text = "";
116
+ for (const line of lines) {
117
+ const cleaned = line.replace(/^#+\s*/, "").trim();
118
+ if (cleaned && cleaned.length > 3) {
119
+ text = cleaned;
120
+ break;
121
+ }
122
+ }
123
+ // Truncate
124
+ if (text.length > 80)
125
+ text = text.slice(0, 77) + "...";
126
+ return text || "session";
127
+ }
128
+ function resolveTopLevelDir(filePath) {
129
+ const segments = filePath.split("/").filter(Boolean);
130
+ const rootIdx = segments.lastIndexOf("archbyte");
131
+ if (rootIdx < 0)
132
+ return null; // skip paths outside project tree
133
+ const startIdx = rootIdx + 1;
134
+ if (startIdx < segments.length - 1) {
135
+ return segments[startIdx];
136
+ }
137
+ else if (startIdx < segments.length) {
138
+ // Single file at project root
139
+ const ext = segments[segments.length - 1].split(".").pop();
140
+ if (ext && ["ts", "tsx", "js", "json", "css", "md"].includes(ext)) {
141
+ return "root";
142
+ }
143
+ }
144
+ return null;
145
+ }
146
+ export async function scanTranscriptIndex(projectDir) {
147
+ const files = await findSessionFiles(projectDir);
148
+ const entries = [];
149
+ for (const file of files) {
150
+ try {
151
+ // Check mtime cache — skip re-parsing if file hasn't changed
152
+ const fileStat = await stat(file.jsonlPath);
153
+ const cached = _indexCache.get(file.jsonlPath);
154
+ if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) {
155
+ if (cached.entry)
156
+ entries.push(cached.entry);
157
+ continue;
158
+ }
159
+ const entry = await scanSingleIndex(file);
160
+ _indexCache.set(file.jsonlPath, { mtimeMs: fileStat.mtimeMs, size: fileStat.size, entry });
161
+ if (entry)
162
+ entries.push(entry);
163
+ }
164
+ catch {
165
+ // Skip unreadable files
166
+ }
167
+ }
168
+ return entries;
169
+ }
170
+ async function scanSingleIndex(file) {
171
+ const raw = await readFile(file.jsonlPath, "utf-8");
172
+ const lines = raw.split("\n").filter(Boolean);
173
+ if (lines.length < 2)
174
+ return null;
175
+ let startedAt = "";
176
+ let completedAt = "";
177
+ let model = "";
178
+ let version = "";
179
+ let gitBranch = "";
180
+ let firstUserPrompt = "";
181
+ const toolNames = new Set();
182
+ const touchedFiles = new Set();
183
+ const fileToolOps = [];
184
+ let eventCount = 0;
185
+ let totalInputTokens = 0;
186
+ let totalOutputTokens = 0;
187
+ let totalCacheReadTokens = 0;
188
+ let totalCacheCreateTokens = 0;
189
+ const seenRequestIds = new Set();
190
+ for (const line of lines) {
191
+ let parsed;
192
+ try {
193
+ parsed = JSON.parse(line);
194
+ }
195
+ catch {
196
+ continue;
197
+ }
198
+ const type = parsed.type;
199
+ if (type === "file-history-snapshot" || type === "queue-operation" || type === "progress")
200
+ continue;
201
+ eventCount++;
202
+ const ts = parsed.timestamp || "";
203
+ if (ts && !startedAt)
204
+ startedAt = ts;
205
+ if (ts)
206
+ completedAt = ts;
207
+ if (!version && parsed.version)
208
+ version = parsed.version;
209
+ if (!gitBranch && parsed.gitBranch)
210
+ gitBranch = parsed.gitBranch;
211
+ if (type === "user") {
212
+ const msg = parsed.message;
213
+ const content = msg?.content;
214
+ if (!firstUserPrompt) {
215
+ if (typeof content === "string") {
216
+ firstUserPrompt = content;
217
+ }
218
+ else if (Array.isArray(content)) {
219
+ const textBlock = content.find((b) => b.type === "text");
220
+ if (textBlock)
221
+ firstUserPrompt = textBlock.text || "";
222
+ }
223
+ }
224
+ }
225
+ else if (type === "assistant") {
226
+ const msg = parsed.message;
227
+ if (msg?.model && !model)
228
+ model = msg.model;
229
+ // Aggregate token usage (deduplicate by requestId)
230
+ const reqId = parsed.requestId || "";
231
+ const usage = msg?.usage;
232
+ if (usage && reqId) {
233
+ if (!seenRequestIds.has(reqId)) {
234
+ seenRequestIds.add(reqId);
235
+ totalInputTokens += usage.input_tokens || 0;
236
+ totalOutputTokens += usage.output_tokens || 0;
237
+ totalCacheReadTokens += usage.cache_read_input_tokens || 0;
238
+ totalCacheCreateTokens += usage.cache_creation_input_tokens || 0;
239
+ }
240
+ }
241
+ const content = msg?.content;
242
+ if (Array.isArray(content)) {
243
+ for (const block of content) {
244
+ const b = block;
245
+ if (b.type === "tool_use" && b.name) {
246
+ const toolName = b.name;
247
+ toolNames.add(toolName);
248
+ // Extract file paths from tool inputs
249
+ const input = b.input;
250
+ if (input) {
251
+ const fp = (input.file_path || input.path || input.notebook_path);
252
+ if (fp) {
253
+ touchedFiles.add(fp);
254
+ if (WRITE_TOOLS.has(toolName) || READ_TOOLS.has(toolName)) {
255
+ fileToolOps.push({ filePath: fp, toolName });
256
+ }
257
+ }
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ if (!startedAt || eventCount === 0)
265
+ return null;
266
+ const category = classifySession(firstUserPrompt, toolNames, eventCount);
267
+ // Filter pipeline noise from the index entirely
268
+ if (category === "pipeline")
269
+ return null;
270
+ // Count subagent files
271
+ let subagentCount = 0;
272
+ if (file.subagentDir) {
273
+ try {
274
+ const subFiles = await readdir(file.subagentDir);
275
+ subagentCount = subFiles.filter(f => f.endsWith(".jsonl")).length;
276
+ }
277
+ catch { /* skip */ }
278
+ }
279
+ const label = extractLabel(firstUserPrompt, category);
280
+ // Map file paths to top-level project directories
281
+ const dirs = new Set();
282
+ for (const fp of touchedFiles) {
283
+ const dir = resolveTopLevelDir(fp);
284
+ if (dir)
285
+ dirs.add(dir);
286
+ }
287
+ // Build per-dir read/write metrics
288
+ const dirMetrics = {};
289
+ for (const op of fileToolOps) {
290
+ const dir = resolveTopLevelDir(op.filePath);
291
+ if (!dir)
292
+ continue;
293
+ if (!dirMetrics[dir])
294
+ dirMetrics[dir] = { reads: 0, writes: 0 };
295
+ if (WRITE_TOOLS.has(op.toolName)) {
296
+ dirMetrics[dir].writes++;
297
+ }
298
+ else {
299
+ dirMetrics[dir].reads++;
300
+ }
301
+ }
302
+ // Compute estimated cost from token usage
303
+ const costInput = (totalInputTokens / 1_000_000) * COST_INPUT;
304
+ const costOutput = (totalOutputTokens / 1_000_000) * COST_OUTPUT;
305
+ const costCacheRead = (totalCacheReadTokens / 1_000_000) * COST_CACHE_READ;
306
+ const costCacheCreate = (totalCacheCreateTokens / 1_000_000) * COST_CACHE_CREATE;
307
+ const estimatedCost = costInput + costOutput + costCacheRead + costCacheCreate;
308
+ return {
309
+ sessionId: file.sessionId,
310
+ startedAt,
311
+ completedAt: completedAt || undefined,
312
+ status: "success",
313
+ runCount: 1 + subagentCount,
314
+ phases: subagentCount > 0 ? ["orchestrator", "subagent"] : ["orchestrator"],
315
+ source: "claude-transcript",
316
+ model,
317
+ version,
318
+ gitBranch,
319
+ subagentCount,
320
+ category,
321
+ label,
322
+ touchedDirs: dirs.size > 0 ? [...dirs] : undefined,
323
+ eventCount,
324
+ dirMetrics: Object.keys(dirMetrics).length > 0 ? dirMetrics : undefined,
325
+ estimatedCost: estimatedCost > 0 ? estimatedCost : undefined,
326
+ };
327
+ }
328
+ function truncate(s, max = MAX_TRUNCATE) {
329
+ const str = typeof s === "string" ? s : JSON.stringify(s ?? "");
330
+ return str.length > max ? str.slice(0, max) + "..." : str;
331
+ }
332
+ function classifyAgentType(firstPrompt) {
333
+ const lower = firstPrompt.toLowerCase();
334
+ if (lower.includes("explore") || lower.includes("search") || lower.includes("find"))
335
+ return "explorer";
336
+ if (lower.includes("plan") || lower.includes("design") || lower.includes("architect"))
337
+ return "planner";
338
+ if (lower.includes("test") || lower.includes("verify") || lower.includes("check"))
339
+ return "tester";
340
+ if (lower.includes("git") || lower.includes("worktree") || lower.includes("branch"))
341
+ return "git";
342
+ if (lower.includes("docker") || lower.includes("container"))
343
+ return "docker";
344
+ return "general";
345
+ }
346
+ function parseTranscriptFile(raw, agentId, agentSlug) {
347
+ const events = [];
348
+ const usage = new Map();
349
+ const models = new Set();
350
+ let firstTimestamp = "";
351
+ let lastTimestamp = "";
352
+ let firstUserPrompt = "";
353
+ const payloadFiles = [];
354
+ const pendingToolUses = new Map();
355
+ const lines = raw.split("\n");
356
+ for (const line of lines) {
357
+ if (!line)
358
+ continue;
359
+ let entry;
360
+ try {
361
+ entry = JSON.parse(line);
362
+ }
363
+ catch {
364
+ continue;
365
+ }
366
+ const { type } = entry;
367
+ if (type === "file-history-snapshot" || type === "queue-operation" || type === "progress")
368
+ continue;
369
+ const ts = entry.timestamp || "";
370
+ if (ts && !firstTimestamp)
371
+ firstTimestamp = ts;
372
+ if (ts)
373
+ lastTimestamp = ts;
374
+ if (type === "user") {
375
+ const content = entry.message?.content;
376
+ if (typeof content === "string") {
377
+ if (!firstUserPrompt)
378
+ firstUserPrompt = content;
379
+ events.push({ timestamp: ts, type: "user", agentId, agentSlug, text: truncate(content, 2000) });
380
+ }
381
+ else if (Array.isArray(content)) {
382
+ for (const block of content) {
383
+ if (block.type === "text") {
384
+ if (!firstUserPrompt)
385
+ firstUserPrompt = block.text || "";
386
+ events.push({ timestamp: ts, type: "user", agentId, agentSlug, text: truncate(block.text, 2000) });
387
+ }
388
+ else if (block.type === "tool_result") {
389
+ const resultText = typeof block.content === "string"
390
+ ? block.content
391
+ : Array.isArray(block.content)
392
+ ? block.content.map((c) => c.text || "").join("")
393
+ : "";
394
+ const pending = pendingToolUses.get(block.tool_use_id);
395
+ events.push({
396
+ timestamp: ts,
397
+ type: "tool_result",
398
+ agentId,
399
+ agentSlug,
400
+ toolUseId: block.tool_use_id,
401
+ toolName: pending?.name,
402
+ toolResult: truncate(resultText),
403
+ toolSuccess: !block.is_error,
404
+ });
405
+ pendingToolUses.delete(block.tool_use_id);
406
+ }
407
+ }
408
+ }
409
+ }
410
+ else if (type === "assistant") {
411
+ const msg = entry.message;
412
+ if (!msg)
413
+ continue;
414
+ const model = msg.model || "";
415
+ if (model)
416
+ models.add(model);
417
+ // Aggregate usage per requestId (streaming produces duplicates)
418
+ const reqId = entry.requestId || `anon-${events.length}`;
419
+ if (msg.usage) {
420
+ const u = msg.usage;
421
+ // Only keep the latest usage per requestId (last one has final counts)
422
+ usage.set(reqId, {
423
+ inputTokens: u.input_tokens || 0,
424
+ outputTokens: u.output_tokens || 0,
425
+ cacheReadTokens: u.cache_read_input_tokens || 0,
426
+ cacheCreationTokens: u.cache_creation_input_tokens || 0,
427
+ });
428
+ }
429
+ const content = msg.content;
430
+ if (Array.isArray(content)) {
431
+ for (const block of content) {
432
+ if (block.type === "text") {
433
+ events.push({
434
+ timestamp: ts,
435
+ type: "assistant",
436
+ agentId,
437
+ agentSlug,
438
+ text: truncate(block.text, 2000),
439
+ model,
440
+ });
441
+ }
442
+ else if (block.type === "thinking") {
443
+ events.push({
444
+ timestamp: ts,
445
+ type: "thinking",
446
+ agentId,
447
+ agentSlug,
448
+ text: truncate(block.thinking || block.text, 1000),
449
+ model,
450
+ });
451
+ }
452
+ else if (block.type === "tool_use") {
453
+ const toolName = block.name || "unknown";
454
+ pendingToolUses.set(block.id, { name: toolName, timestamp: ts });
455
+ // Extract payload files from Read tool calls
456
+ if (toolName === "Read" && block.input) {
457
+ const filePath = typeof block.input === "object" ? block.input.file_path : undefined;
458
+ if (filePath) {
459
+ payloadFiles.push({ path: filePath, timestamp: ts, agentId });
460
+ }
461
+ }
462
+ events.push({
463
+ timestamp: ts,
464
+ type: "tool_use",
465
+ agentId,
466
+ agentSlug,
467
+ toolName,
468
+ toolUseId: block.id,
469
+ toolInput: truncate(block.input),
470
+ model,
471
+ });
472
+ }
473
+ }
474
+ }
475
+ }
476
+ else if (type === "system") {
477
+ events.push({
478
+ timestamp: ts,
479
+ type: "system",
480
+ agentId,
481
+ agentSlug,
482
+ text: entry.subtype || "system",
483
+ });
484
+ }
485
+ }
486
+ return { events, usage, models, firstTimestamp, lastTimestamp, firstUserPrompt, payloadFiles };
487
+ }
488
+ function computeCloudStats(usageMaps, allModels) {
489
+ let totalInput = 0;
490
+ let totalOutput = 0;
491
+ let totalCacheRead = 0;
492
+ let totalCacheCreate = 0;
493
+ let apiCalls = 0;
494
+ for (const usageMap of usageMaps) {
495
+ for (const u of usageMap.values()) {
496
+ totalInput += u.inputTokens;
497
+ totalOutput += u.outputTokens;
498
+ totalCacheRead += u.cacheReadTokens;
499
+ totalCacheCreate += u.cacheCreationTokens;
500
+ apiCalls++;
501
+ }
502
+ }
503
+ const costInput = (totalInput / 1_000_000) * COST_INPUT;
504
+ const costOutput = (totalOutput / 1_000_000) * COST_OUTPUT;
505
+ const costCacheRead = (totalCacheRead / 1_000_000) * COST_CACHE_READ;
506
+ const costCacheCreate = (totalCacheCreate / 1_000_000) * COST_CACHE_CREATE;
507
+ return {
508
+ totalInputTokens: totalInput,
509
+ totalOutputTokens: totalOutput,
510
+ totalCacheReadTokens: totalCacheRead,
511
+ totalCacheCreationTokens: totalCacheCreate,
512
+ estimatedCost: {
513
+ input: costInput,
514
+ output: costOutput,
515
+ cacheRead: costCacheRead,
516
+ cacheCreation: costCacheCreate,
517
+ total: costInput + costOutput + costCacheRead + costCacheCreate,
518
+ },
519
+ models: [...allModels],
520
+ apiCalls,
521
+ };
522
+ }
523
+ export async function importClaudeTranscript(projectDir, sessionId) {
524
+ const jsonlPath = path.join(projectDir, `${sessionId}.jsonl`);
525
+ if (!existsSync(jsonlPath))
526
+ return null;
527
+ let raw;
528
+ try {
529
+ raw = await readFile(jsonlPath, "utf-8");
530
+ }
531
+ catch {
532
+ return null;
533
+ }
534
+ const lines = raw.split("\n").filter(Boolean);
535
+ if (lines.length < 2)
536
+ return null;
537
+ // Parse main transcript
538
+ const main = parseTranscriptFile(raw);
539
+ // Filter warmup — if first user prompt is literally "warmup" and < 5 entries, skip
540
+ if (main.firstUserPrompt.trim().toLowerCase() === "warmup" && main.events.length < 5) {
541
+ return null;
542
+ }
543
+ const runs = [];
544
+ const allUsageMaps = [main.usage];
545
+ const allModels = new Set(main.models);
546
+ let allEvents = [...main.events];
547
+ let allPayload = [...main.payloadFiles];
548
+ // Orchestrator run
549
+ const mainTokens = { input: 0, output: 0 };
550
+ for (const u of main.usage.values()) {
551
+ mainTokens.input += u.inputTokens;
552
+ mainTokens.output += u.outputTokens;
553
+ }
554
+ const mainElapsed = main.firstTimestamp && main.lastTimestamp
555
+ ? (new Date(main.lastTimestamp).getTime() - new Date(main.firstTimestamp).getTime()) / 1000
556
+ : 0;
557
+ runs.push({
558
+ runId: `${sessionId}-orchestrator`,
559
+ sessionId,
560
+ phase: "orchestrator",
561
+ passId: "orchestrator",
562
+ agent: "Claude",
563
+ model: [...main.models][0] || "unknown",
564
+ iteration: 1,
565
+ startedAt: main.firstTimestamp,
566
+ completedAt: main.lastTimestamp || undefined,
567
+ elapsedSeconds: Math.max(0, mainElapsed),
568
+ status: "success",
569
+ tokens: mainTokens,
570
+ agentType: "orchestrator",
571
+ transcript: main.events,
572
+ });
573
+ // Parse subagents
574
+ const subagentDir = path.join(projectDir, sessionId, "subagents");
575
+ if (existsSync(subagentDir)) {
576
+ try {
577
+ const subFiles = await readdir(subagentDir);
578
+ for (const subFile of subFiles) {
579
+ if (!subFile.endsWith(".jsonl"))
580
+ continue;
581
+ const agentIdMatch = subFile.match(/^agent-(.+)\.jsonl$/);
582
+ if (!agentIdMatch)
583
+ continue;
584
+ const agentId = agentIdMatch[1];
585
+ try {
586
+ const subRaw = await readFile(path.join(subagentDir, subFile), "utf-8");
587
+ // Extract agentId and slug from first line
588
+ let slug = "";
589
+ try {
590
+ const firstLine = JSON.parse(subRaw.split("\n")[0]);
591
+ slug = firstLine.slug || "";
592
+ }
593
+ catch { /* skip */ }
594
+ const sub = parseTranscriptFile(subRaw, agentId, slug);
595
+ // Filter warmup agents
596
+ if (sub.firstUserPrompt.trim().toLowerCase() === "warmup" || sub.events.length < 3)
597
+ continue;
598
+ const agentType = classifyAgentType(sub.firstUserPrompt);
599
+ const subTokens = { input: 0, output: 0 };
600
+ for (const u of sub.usage.values()) {
601
+ subTokens.input += u.inputTokens;
602
+ subTokens.output += u.outputTokens;
603
+ }
604
+ const subElapsed = sub.firstTimestamp && sub.lastTimestamp
605
+ ? (new Date(sub.lastTimestamp).getTime() - new Date(sub.firstTimestamp).getTime()) / 1000
606
+ : 0;
607
+ for (const m of sub.models)
608
+ allModels.add(m);
609
+ allUsageMaps.push(sub.usage);
610
+ allEvents.push(...sub.events);
611
+ allPayload.push(...sub.payloadFiles);
612
+ runs.push({
613
+ runId: `${sessionId}-agent-${agentId}`,
614
+ sessionId,
615
+ phase: "subagent",
616
+ passId: `agent-${agentId}`,
617
+ agent: slug ? `${slug} (${agentType})` : `agent-${agentId} (${agentType})`,
618
+ model: [...sub.models][0] || "unknown",
619
+ iteration: 1,
620
+ startedAt: sub.firstTimestamp,
621
+ completedAt: sub.lastTimestamp || undefined,
622
+ elapsedSeconds: Math.max(0, subElapsed),
623
+ status: "success",
624
+ tokens: subTokens,
625
+ agentType,
626
+ transcript: sub.events,
627
+ });
628
+ }
629
+ catch {
630
+ // Skip unreadable subagent files
631
+ }
632
+ }
633
+ }
634
+ catch {
635
+ // Skip if subagent dir unreadable
636
+ }
637
+ }
638
+ // Sort all events by timestamp
639
+ allEvents.sort((a, b) => (a.timestamp || "").localeCompare(b.timestamp || ""));
640
+ // Compute cloud stats
641
+ const cloudStats = computeCloudStats(allUsageMaps, allModels);
642
+ // Compute totals
643
+ const totalTokens = { input: 0, output: 0 };
644
+ for (const usageMap of allUsageMaps) {
645
+ for (const u of usageMap.values()) {
646
+ totalTokens.input += u.inputTokens;
647
+ totalTokens.output += u.outputTokens;
648
+ }
649
+ }
650
+ const totalElapsed = runs.reduce((sum, r) => sum + (r.elapsedSeconds ?? 0), 0);
651
+ // Extract metadata from first few lines
652
+ let version = "";
653
+ let gitBranch = "";
654
+ try {
655
+ for (let i = 0; i < Math.min(5, lines.length); i++) {
656
+ const parsed = JSON.parse(lines[i]);
657
+ if (!version && parsed.version)
658
+ version = parsed.version;
659
+ if (!gitBranch && parsed.gitBranch)
660
+ gitBranch = parsed.gitBranch;
661
+ if (version && gitBranch)
662
+ break;
663
+ }
664
+ }
665
+ catch { /* skip */ }
666
+ return {
667
+ sessionId,
668
+ projectName: gitBranch ? `branch: ${gitBranch}` : undefined,
669
+ startedAt: main.firstTimestamp,
670
+ completedAt: main.lastTimestamp || undefined,
671
+ status: "success",
672
+ source: "claude-transcript",
673
+ projectMeta: {
674
+ model: [...allModels].join(", "),
675
+ mode: version ? `Claude Code ${version}` : undefined,
676
+ },
677
+ summary: {
678
+ totalRuns: runs.length,
679
+ successfulRuns: runs.length,
680
+ failedRuns: 0,
681
+ skippedRuns: 0,
682
+ totalElapsedSeconds: totalElapsed,
683
+ totalTokens,
684
+ phases: [...new Set(runs.map(r => r.phase))],
685
+ models: [...allModels],
686
+ },
687
+ runs,
688
+ transcript: allEvents,
689
+ cloudStats,
690
+ payload: allPayload,
691
+ };
692
+ }
@@ -0,0 +1,7 @@
1
+ import type { AgentSession, SessionIndex } from "./types.js";
2
+ /** Load session index — reads index.json, or scans session dirs if missing */
3
+ export declare function loadSessionIndex(projectRoot: string): Promise<SessionIndex>;
4
+ /** Load a full session by ID */
5
+ export declare function loadSession(projectRoot: string, sessionId: string): Promise<AgentSession | null>;
6
+ /** Read recent activity log entries (newest first) */
7
+ export declare function loadRecentActivity(projectRoot: string, limit?: number): Promise<string[]>;