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.
- package/README.md +68 -62
- package/dist/agents/index.js +2 -0
- package/dist/agents/observability/adapters/archbyte.d.ts +3 -0
- package/dist/agents/observability/adapters/archbyte.js +104 -0
- package/dist/agents/observability/adapters/claude-transcripts.d.ts +31 -0
- package/dist/agents/observability/adapters/claude-transcripts.js +692 -0
- package/dist/agents/observability/reader.d.ts +7 -0
- package/dist/agents/observability/reader.js +86 -0
- package/dist/agents/observability/types.d.ts +125 -0
- package/dist/agents/observability/types.js +3 -0
- package/dist/agents/observability/writer.d.ts +9 -0
- package/dist/agents/observability/writer.js +100 -0
- package/dist/agents/pipeline/index.d.ts +1 -1
- package/dist/agents/pipeline/index.js +85 -3
- package/dist/agents/pipeline/types.d.ts +2 -0
- package/dist/agents/prompt-data.js +9 -0
- package/dist/cli/analyze.js +81 -1
- package/dist/cli/config.js +21 -1
- package/dist/server/src/index.js +301 -13
- package/package.json +1 -1
- package/ui/dist/assets/index-ClEznf0G.js +85 -0
- package/ui/dist/assets/{index-BQouokNH.css → index-FuWCQecR.css} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-CWGPRsWP.js +0 -72
|
@@ -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[]>;
|