@katyella/legio 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +555 -0
- package/agents/builder.md +141 -0
- package/agents/coordinator.md +351 -0
- package/agents/cto.md +196 -0
- package/agents/gateway.md +276 -0
- package/agents/lead.md +281 -0
- package/agents/merger.md +156 -0
- package/agents/monitor.md +212 -0
- package/agents/reviewer.md +142 -0
- package/agents/scout.md +131 -0
- package/agents/supervisor.md +416 -0
- package/bin/legio.mjs +38 -0
- package/package.json +77 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +102 -0
- package/src/agents/hooks-deployer.test.ts +1820 -0
- package/src/agents/hooks-deployer.ts +574 -0
- package/src/agents/identity.test.ts +614 -0
- package/src/agents/identity.ts +385 -0
- package/src/agents/lifecycle.test.ts +202 -0
- package/src/agents/lifecycle.ts +184 -0
- package/src/agents/manifest.test.ts +558 -0
- package/src/agents/manifest.ts +297 -0
- package/src/agents/overlay.test.ts +592 -0
- package/src/agents/overlay.ts +316 -0
- package/src/beads/client.test.ts +210 -0
- package/src/beads/client.ts +227 -0
- package/src/beads/molecules.test.ts +320 -0
- package/src/beads/molecules.ts +209 -0
- package/src/commands/agents.test.ts +325 -0
- package/src/commands/agents.ts +286 -0
- package/src/commands/clean.test.ts +730 -0
- package/src/commands/clean.ts +653 -0
- package/src/commands/completions.test.ts +346 -0
- package/src/commands/completions.ts +950 -0
- package/src/commands/coordinator.test.ts +1524 -0
- package/src/commands/coordinator.ts +880 -0
- package/src/commands/costs.test.ts +1015 -0
- package/src/commands/costs.ts +473 -0
- package/src/commands/dashboard.test.ts +94 -0
- package/src/commands/dashboard.ts +607 -0
- package/src/commands/doctor.test.ts +295 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/down.test.ts +308 -0
- package/src/commands/down.ts +124 -0
- package/src/commands/errors.test.ts +648 -0
- package/src/commands/errors.ts +255 -0
- package/src/commands/feed.test.ts +579 -0
- package/src/commands/feed.ts +368 -0
- package/src/commands/gateway.test.ts +698 -0
- package/src/commands/gateway.ts +419 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +539 -0
- package/src/commands/hooks.test.ts +292 -0
- package/src/commands/hooks.ts +210 -0
- package/src/commands/init.test.ts +211 -0
- package/src/commands/init.ts +622 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +455 -0
- package/src/commands/log.test.ts +1556 -0
- package/src/commands/log.ts +752 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +544 -0
- package/src/commands/mail.test.ts +1726 -0
- package/src/commands/mail.ts +926 -0
- package/src/commands/merge.test.ts +676 -0
- package/src/commands/merge.ts +374 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +150 -0
- package/src/commands/monitor.test.ts +151 -0
- package/src/commands/monitor.ts +394 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +373 -0
- package/src/commands/prime.test.ts +467 -0
- package/src/commands/prime.ts +386 -0
- package/src/commands/replay.test.ts +742 -0
- package/src/commands/replay.ts +367 -0
- package/src/commands/run.test.ts +443 -0
- package/src/commands/run.ts +365 -0
- package/src/commands/server.test.ts +626 -0
- package/src/commands/server.ts +298 -0
- package/src/commands/sling.test.ts +810 -0
- package/src/commands/sling.ts +700 -0
- package/src/commands/spec.test.ts +206 -0
- package/src/commands/spec.ts +171 -0
- package/src/commands/status.test.ts +276 -0
- package/src/commands/status.ts +339 -0
- package/src/commands/stop.test.ts +357 -0
- package/src/commands/stop.ts +119 -0
- package/src/commands/supervisor.test.ts +186 -0
- package/src/commands/supervisor.ts +544 -0
- package/src/commands/trace.test.ts +746 -0
- package/src/commands/trace.ts +332 -0
- package/src/commands/up.test.ts +597 -0
- package/src/commands/up.ts +275 -0
- package/src/commands/watch.test.ts +152 -0
- package/src/commands/watch.ts +238 -0
- package/src/commands/worktree.test.ts +648 -0
- package/src/commands/worktree.ts +266 -0
- package/src/config.test.ts +496 -0
- package/src/config.ts +616 -0
- package/src/doctor/agents.test.ts +448 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +184 -0
- package/src/doctor/config-check.ts +185 -0
- package/src/doctor/consistency.test.ts +645 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +284 -0
- package/src/doctor/databases.ts +211 -0
- package/src/doctor/dependencies.test.ts +150 -0
- package/src/doctor/dependencies.ts +179 -0
- package/src/doctor/logs.test.ts +244 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +210 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +285 -0
- package/src/doctor/structure.ts +195 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +130 -0
- package/src/doctor/version.ts +131 -0
- package/src/e2e/chat-flow.test.ts +346 -0
- package/src/e2e/init-sling-lifecycle.test.ts +288 -0
- package/src/errors.test.ts +21 -0
- package/src/errors.ts +246 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +344 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/global-setup.ts +14 -0
- package/src/index.ts +339 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +118 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +812 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +258 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +873 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +815 -0
- package/src/mail/store.ts +402 -0
- package/src/merge/queue.test.ts +449 -0
- package/src/merge/queue.ts +262 -0
- package/src/merge/resolver.test.ts +1453 -0
- package/src/merge/resolver.ts +759 -0
- package/src/metrics/store.test.ts +1167 -0
- package/src/metrics/store.ts +511 -0
- package/src/metrics/summary.test.ts +397 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +643 -0
- package/src/metrics/transcript.ts +351 -0
- package/src/mulch/client.test.ts +547 -0
- package/src/mulch/client.ts +416 -0
- package/src/server/audit-store.test.ts +384 -0
- package/src/server/audit-store.ts +257 -0
- package/src/server/headless.test.ts +180 -0
- package/src/server/headless.ts +151 -0
- package/src/server/index.test.ts +241 -0
- package/src/server/index.ts +317 -0
- package/src/server/public/app.js +187 -0
- package/src/server/public/apple-touch-icon.png +0 -0
- package/src/server/public/components/agent-badge.js +37 -0
- package/src/server/public/components/data-table.js +114 -0
- package/src/server/public/components/gateway-chat.js +256 -0
- package/src/server/public/components/issue-card.js +96 -0
- package/src/server/public/components/layout.js +88 -0
- package/src/server/public/components/message-bubble.js +120 -0
- package/src/server/public/components/stat-card.js +26 -0
- package/src/server/public/components/terminal-panel.js +140 -0
- package/src/server/public/favicon-16.png +0 -0
- package/src/server/public/favicon-32.png +0 -0
- package/src/server/public/favicon.ico +0 -0
- package/src/server/public/favicon.png +0 -0
- package/src/server/public/index.html +64 -0
- package/src/server/public/lib/api.js +35 -0
- package/src/server/public/lib/markdown.js +8 -0
- package/src/server/public/lib/preact-setup.js +8 -0
- package/src/server/public/lib/state.js +99 -0
- package/src/server/public/lib/utils.js +309 -0
- package/src/server/public/lib/ws.js +79 -0
- package/src/server/public/views/chat.js +983 -0
- package/src/server/public/views/costs.js +692 -0
- package/src/server/public/views/dashboard.js +781 -0
- package/src/server/public/views/gateway-chat.js +622 -0
- package/src/server/public/views/inspect.js +399 -0
- package/src/server/public/views/issues.js +470 -0
- package/src/server/public/views/setup.js +94 -0
- package/src/server/public/views/task-detail.js +422 -0
- package/src/server/routes.test.ts +3816 -0
- package/src/server/routes.ts +1964 -0
- package/src/server/websocket.test.ts +288 -0
- package/src/server/websocket.ts +196 -0
- package/src/sessions/compat.test.ts +109 -0
- package/src/sessions/compat.ts +17 -0
- package/src/sessions/store.test.ts +969 -0
- package/src/sessions/store.ts +480 -0
- package/src/test-helpers.test.ts +97 -0
- package/src/test-helpers.ts +143 -0
- package/src/types.ts +708 -0
- package/src/watchdog/daemon.test.ts +1233 -0
- package/src/watchdog/daemon.ts +533 -0
- package/src/watchdog/health.test.ts +371 -0
- package/src/watchdog/health.ts +248 -0
- package/src/watchdog/triage.test.ts +162 -0
- package/src/watchdog/triage.ts +193 -0
- package/src/worktree/manager.test.ts +444 -0
- package/src/worktree/manager.ts +224 -0
- package/src/worktree/tmux.test.ts +1238 -0
- package/src/worktree/tmux.ts +644 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +132 -0
- package/templates/overlay.md.tmpl +79 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parser for Claude Code transcript JSONL files.
|
|
5
|
+
*
|
|
6
|
+
* Extracts token usage data from assistant-type entries in transcript files
|
|
7
|
+
* at ~/.claude/projects/{project-slug}/{session-id}.jsonl.
|
|
8
|
+
*
|
|
9
|
+
* Each assistant entry contains per-turn usage:
|
|
10
|
+
* {
|
|
11
|
+
* "type": "assistant",
|
|
12
|
+
* "message": {
|
|
13
|
+
* "model": "claude-opus-4-6",
|
|
14
|
+
* "usage": {
|
|
15
|
+
* "input_tokens": 3,
|
|
16
|
+
* "output_tokens": 9,
|
|
17
|
+
* "cache_read_input_tokens": 19401,
|
|
18
|
+
* "cache_creation_input_tokens": 9918
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
interface TranscriptUsage {
|
|
25
|
+
inputTokens: number;
|
|
26
|
+
outputTokens: number;
|
|
27
|
+
cacheReadTokens: number;
|
|
28
|
+
cacheCreationTokens: number;
|
|
29
|
+
modelUsed: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A single extracted text message from a transcript entry. */
|
|
33
|
+
interface TranscriptMessage {
|
|
34
|
+
role: "user" | "assistant";
|
|
35
|
+
text: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Pricing per million tokens (USD). */
|
|
39
|
+
interface ModelPricing {
|
|
40
|
+
inputPerMTok: number;
|
|
41
|
+
outputPerMTok: number;
|
|
42
|
+
cacheReadPerMTok: number;
|
|
43
|
+
cacheCreationPerMTok: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Hardcoded pricing for known Claude models. */
|
|
47
|
+
const MODEL_PRICING: Record<string, ModelPricing> = {
|
|
48
|
+
// Opus 4 / 4.1 — legacy pricing
|
|
49
|
+
"opus-legacy": {
|
|
50
|
+
inputPerMTok: 15,
|
|
51
|
+
outputPerMTok: 75,
|
|
52
|
+
cacheReadPerMTok: 1.5, // 10% of input
|
|
53
|
+
cacheCreationPerMTok: 3.75, // 25% of input
|
|
54
|
+
},
|
|
55
|
+
// Opus 4.5+ (claude-opus-4-5, claude-opus-4-6, ...) — reduced pricing
|
|
56
|
+
"opus-new": {
|
|
57
|
+
inputPerMTok: 5,
|
|
58
|
+
outputPerMTok: 25,
|
|
59
|
+
cacheReadPerMTok: 0.5, // 10% of input
|
|
60
|
+
cacheCreationPerMTok: 1.25, // 25% of input
|
|
61
|
+
},
|
|
62
|
+
sonnet: {
|
|
63
|
+
inputPerMTok: 3,
|
|
64
|
+
outputPerMTok: 15,
|
|
65
|
+
cacheReadPerMTok: 0.3, // 10% of input
|
|
66
|
+
cacheCreationPerMTok: 0.75, // 25% of input
|
|
67
|
+
},
|
|
68
|
+
// Haiku 3.x / 4.0-4.4 — legacy pricing
|
|
69
|
+
"haiku-legacy": {
|
|
70
|
+
inputPerMTok: 0.8,
|
|
71
|
+
outputPerMTok: 4,
|
|
72
|
+
cacheReadPerMTok: 0.08, // 10% of input
|
|
73
|
+
cacheCreationPerMTok: 0.2, // 25% of input
|
|
74
|
+
},
|
|
75
|
+
// Haiku 4.5+ (claude-haiku-4-5-20251001, ...) — updated pricing
|
|
76
|
+
"haiku-new": {
|
|
77
|
+
inputPerMTok: 1,
|
|
78
|
+
outputPerMTok: 5,
|
|
79
|
+
cacheReadPerMTok: 0.1, // 10% of input
|
|
80
|
+
cacheCreationPerMTok: 0.25, // 25% of input
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Determine the pricing tier for a given model string.
|
|
86
|
+
* Parses the major.minor version suffix (e.g. "opus-4-6") to select the right tier:
|
|
87
|
+
* - Opus 4.5+ -> opus-new ($5/$25), Opus < 4.5 -> opus-legacy ($15/$75)
|
|
88
|
+
* - Haiku 4.5+ -> haiku-new ($1/$5), Haiku < 4.5 -> haiku-legacy ($0.80/$4)
|
|
89
|
+
* - Sonnet -> sonnet ($3/$15)
|
|
90
|
+
* Returns null if unrecognized.
|
|
91
|
+
*/
|
|
92
|
+
function getPricingForModel(model: string): ModelPricing | null {
|
|
93
|
+
const lower = model.toLowerCase();
|
|
94
|
+
|
|
95
|
+
if (lower.includes("opus")) {
|
|
96
|
+
// Detect major.minor version from pattern like "opus-4-6" or "opus-4-5"
|
|
97
|
+
const m = lower.match(/opus-(\d+)-(\d+)/);
|
|
98
|
+
if (m && m[1] !== undefined && m[2] !== undefined) {
|
|
99
|
+
const major = parseInt(m[1], 10);
|
|
100
|
+
const minor = parseInt(m[2], 10);
|
|
101
|
+
if (major > 4 || (major === 4 && minor >= 5)) {
|
|
102
|
+
return MODEL_PRICING["opus-new"] ?? null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return MODEL_PRICING["opus-legacy"] ?? null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (lower.includes("sonnet")) return MODEL_PRICING.sonnet ?? null;
|
|
109
|
+
|
|
110
|
+
if (lower.includes("haiku")) {
|
|
111
|
+
// Detect major.minor version from pattern like "haiku-4-5" or "haiku-3-5"
|
|
112
|
+
const m = lower.match(/haiku-(\d+)-(\d+)/);
|
|
113
|
+
if (m && m[1] !== undefined && m[2] !== undefined) {
|
|
114
|
+
const major = parseInt(m[1], 10);
|
|
115
|
+
const minor = parseInt(m[2], 10);
|
|
116
|
+
if (major > 4 || (major === 4 && minor >= 5)) {
|
|
117
|
+
return MODEL_PRICING["haiku-new"] ?? null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return MODEL_PRICING["haiku-legacy"] ?? null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Calculate the estimated cost in USD for a given usage and model.
|
|
128
|
+
* Returns null if the model is unrecognized.
|
|
129
|
+
*/
|
|
130
|
+
export function estimateCost(usage: TranscriptUsage): number | null {
|
|
131
|
+
if (usage.modelUsed === null) return null;
|
|
132
|
+
|
|
133
|
+
const pricing = getPricingForModel(usage.modelUsed);
|
|
134
|
+
if (pricing === null) return null;
|
|
135
|
+
|
|
136
|
+
const inputCost = (usage.inputTokens / 1_000_000) * pricing.inputPerMTok;
|
|
137
|
+
const outputCost = (usage.outputTokens / 1_000_000) * pricing.outputPerMTok;
|
|
138
|
+
const cacheReadCost = (usage.cacheReadTokens / 1_000_000) * pricing.cacheReadPerMTok;
|
|
139
|
+
const cacheCreationCost = (usage.cacheCreationTokens / 1_000_000) * pricing.cacheCreationPerMTok;
|
|
140
|
+
|
|
141
|
+
return inputCost + outputCost + cacheReadCost + cacheCreationCost;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Narrow an unknown value to determine if it looks like a transcript assistant entry.
|
|
146
|
+
* Returns the usage fields if valid, or null otherwise.
|
|
147
|
+
*/
|
|
148
|
+
function extractUsageFromEntry(entry: unknown): {
|
|
149
|
+
inputTokens: number;
|
|
150
|
+
outputTokens: number;
|
|
151
|
+
cacheReadTokens: number;
|
|
152
|
+
cacheCreationTokens: number;
|
|
153
|
+
model: string | undefined;
|
|
154
|
+
} | null {
|
|
155
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
156
|
+
|
|
157
|
+
const obj = entry as Record<string, unknown>;
|
|
158
|
+
if (obj.type !== "assistant") return null;
|
|
159
|
+
|
|
160
|
+
const message = obj.message;
|
|
161
|
+
if (typeof message !== "object" || message === null) return null;
|
|
162
|
+
|
|
163
|
+
const msg = message as Record<string, unknown>;
|
|
164
|
+
const usage = msg.usage;
|
|
165
|
+
if (typeof usage !== "object" || usage === null) return null;
|
|
166
|
+
|
|
167
|
+
const u = usage as Record<string, unknown>;
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
inputTokens: typeof u.input_tokens === "number" ? u.input_tokens : 0,
|
|
171
|
+
outputTokens: typeof u.output_tokens === "number" ? u.output_tokens : 0,
|
|
172
|
+
cacheReadTokens: typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : 0,
|
|
173
|
+
cacheCreationTokens:
|
|
174
|
+
typeof u.cache_creation_input_tokens === "number" ? u.cache_creation_input_tokens : 0,
|
|
175
|
+
model: typeof msg.model === "string" ? msg.model : undefined,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Extract the first text block content from an assistant transcript entry.
|
|
181
|
+
* Returns the text string, or null if the entry is not an assistant turn
|
|
182
|
+
* or contains no text content block.
|
|
183
|
+
*/
|
|
184
|
+
export function extractAssistantText(entry: unknown): string | null {
|
|
185
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
186
|
+
|
|
187
|
+
const obj = entry as Record<string, unknown>;
|
|
188
|
+
if (obj.type !== "assistant") return null;
|
|
189
|
+
|
|
190
|
+
const message = obj.message;
|
|
191
|
+
if (typeof message !== "object" || message === null) return null;
|
|
192
|
+
|
|
193
|
+
const msg = message as Record<string, unknown>;
|
|
194
|
+
const content = msg.content;
|
|
195
|
+
|
|
196
|
+
if (Array.isArray(content)) {
|
|
197
|
+
for (const block of content) {
|
|
198
|
+
if (typeof block === "object" && block !== null) {
|
|
199
|
+
const b = block as Record<string, unknown>;
|
|
200
|
+
if (b.type === "text" && typeof b.text === "string" && b.text.length > 0) {
|
|
201
|
+
return b.text;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Extract text content from a human transcript entry.
|
|
212
|
+
* Returns the first non-empty text block content, or null.
|
|
213
|
+
*/
|
|
214
|
+
function extractHumanText(entry: unknown): string | null {
|
|
215
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
216
|
+
|
|
217
|
+
const obj = entry as Record<string, unknown>;
|
|
218
|
+
if (obj.type !== "human") return null;
|
|
219
|
+
|
|
220
|
+
const message = obj.message;
|
|
221
|
+
if (typeof message !== "object" || message === null) return null;
|
|
222
|
+
|
|
223
|
+
const msg = message as Record<string, unknown>;
|
|
224
|
+
const content = msg.content;
|
|
225
|
+
|
|
226
|
+
// Content may be a string or an array of blocks
|
|
227
|
+
if (typeof content === "string" && content.length > 0) {
|
|
228
|
+
return content;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (Array.isArray(content)) {
|
|
232
|
+
for (const block of content) {
|
|
233
|
+
if (typeof block === "object" && block !== null) {
|
|
234
|
+
const b = block as Record<string, unknown>;
|
|
235
|
+
if (b.type === "text" && typeof b.text === "string" && b.text.length > 0) {
|
|
236
|
+
return b.text;
|
|
237
|
+
}
|
|
238
|
+
} else if (typeof block === "string" && block.length > 0) {
|
|
239
|
+
return block;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Parse a Claude Code transcript JSONL file and extract text messages.
|
|
249
|
+
*
|
|
250
|
+
* Reads from a given line offset (for incremental parsing across calls).
|
|
251
|
+
* Returns extracted messages with role ("user" | "assistant") and the
|
|
252
|
+
* next line index to use for the subsequent call.
|
|
253
|
+
*
|
|
254
|
+
* @param transcriptPath - Absolute path to the transcript JSONL file
|
|
255
|
+
* @param fromLine - 0-based line index to start reading from (default 0)
|
|
256
|
+
* @returns Messages found and the next line index
|
|
257
|
+
*/
|
|
258
|
+
export async function parseTranscriptTexts(
|
|
259
|
+
transcriptPath: string,
|
|
260
|
+
fromLine = 0,
|
|
261
|
+
): Promise<{ messages: TranscriptMessage[]; nextLine: number }> {
|
|
262
|
+
const fileText = await readFile(transcriptPath, "utf-8");
|
|
263
|
+
const lines = fileText.split("\n");
|
|
264
|
+
|
|
265
|
+
// When a JSONL file ends with \n, split() produces a trailing empty element.
|
|
266
|
+
// Exclude it from the watermark so that appended lines at that position are
|
|
267
|
+
// correctly processed on the next incremental call.
|
|
268
|
+
const lastLine = lines[lines.length - 1] ?? "";
|
|
269
|
+
const nextLine = lastLine.trim().length === 0 ? lines.length - 1 : lines.length;
|
|
270
|
+
|
|
271
|
+
const messages: TranscriptMessage[] = [];
|
|
272
|
+
|
|
273
|
+
for (let i = fromLine; i < lines.length; i++) {
|
|
274
|
+
const trimmed = lines[i]?.trim() ?? "";
|
|
275
|
+
if (trimmed.length === 0) continue;
|
|
276
|
+
|
|
277
|
+
let parsed: unknown;
|
|
278
|
+
try {
|
|
279
|
+
parsed = JSON.parse(trimmed);
|
|
280
|
+
} catch {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (typeof parsed !== "object" || parsed === null) continue;
|
|
285
|
+
const obj = parsed as Record<string, unknown>;
|
|
286
|
+
|
|
287
|
+
if (obj.type === "assistant") {
|
|
288
|
+
const text = extractAssistantText(parsed);
|
|
289
|
+
if (text !== null) {
|
|
290
|
+
messages.push({ role: "assistant", text });
|
|
291
|
+
}
|
|
292
|
+
} else if (obj.type === "human") {
|
|
293
|
+
const text = extractHumanText(parsed);
|
|
294
|
+
if (text !== null) {
|
|
295
|
+
messages.push({ role: "user", text });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { messages, nextLine };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Parse a Claude Code transcript JSONL file and aggregate token usage.
|
|
305
|
+
*
|
|
306
|
+
* Reads the file line by line, extracting usage data from each assistant
|
|
307
|
+
* entry. Returns aggregated totals and the model from the first assistant turn.
|
|
308
|
+
*
|
|
309
|
+
* @param transcriptPath - Absolute path to the transcript JSONL file
|
|
310
|
+
* @returns Aggregated usage data across all assistant turns
|
|
311
|
+
*/
|
|
312
|
+
export async function parseTranscriptUsage(transcriptPath: string): Promise<TranscriptUsage> {
|
|
313
|
+
const text = await readFile(transcriptPath, "utf-8");
|
|
314
|
+
const lines = text.split("\n");
|
|
315
|
+
|
|
316
|
+
const result: TranscriptUsage = {
|
|
317
|
+
inputTokens: 0,
|
|
318
|
+
outputTokens: 0,
|
|
319
|
+
cacheReadTokens: 0,
|
|
320
|
+
cacheCreationTokens: 0,
|
|
321
|
+
modelUsed: null,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
for (const line of lines) {
|
|
325
|
+
const trimmed = line.trim();
|
|
326
|
+
if (trimmed.length === 0) continue;
|
|
327
|
+
|
|
328
|
+
let parsed: unknown;
|
|
329
|
+
try {
|
|
330
|
+
parsed = JSON.parse(trimmed);
|
|
331
|
+
} catch {
|
|
332
|
+
// Skip malformed lines
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const usage = extractUsageFromEntry(parsed);
|
|
337
|
+
if (usage === null) continue;
|
|
338
|
+
|
|
339
|
+
result.inputTokens += usage.inputTokens;
|
|
340
|
+
result.outputTokens += usage.outputTokens;
|
|
341
|
+
result.cacheReadTokens += usage.cacheReadTokens;
|
|
342
|
+
result.cacheCreationTokens += usage.cacheCreationTokens;
|
|
343
|
+
|
|
344
|
+
// Capture model from first assistant turn
|
|
345
|
+
if (result.modelUsed === null && usage.model !== undefined) {
|
|
346
|
+
result.modelUsed = usage.model;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return result;
|
|
351
|
+
}
|