tokenoscopy 0.0.0-stage → 0.3.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.
@@ -0,0 +1,752 @@
1
+ // src/auth-mode.ts
2
+ import { readFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ function keyWasRejected(key, cfg) {
6
+ const rejected = cfg?.customApiKeyResponses?.rejected ?? [];
7
+ return rejected.includes(key.slice(-20));
8
+ }
9
+ var SEAT_ORG = /^claude_(pro|max|team|enterprise)/i;
10
+ var SEAT_TIER = /claude_(pro|max|team|enterprise)/i;
11
+ function classifyAuth(env, cfg) {
12
+ if (env.CLAUDE_CODE_USE_BEDROCK === "1") {
13
+ return { mode: "api_key", evidence: "CLAUDE_CODE_USE_BEDROCK=1" };
14
+ }
15
+ if (env.CLAUDE_CODE_USE_VERTEX === "1") {
16
+ return { mode: "api_key", evidence: "CLAUDE_CODE_USE_VERTEX=1" };
17
+ }
18
+ if (env.ANTHROPIC_AUTH_TOKEN?.trim()) {
19
+ return { mode: "api_key", evidence: "ANTHROPIC_AUTH_TOKEN set" };
20
+ }
21
+ const key = env.ANTHROPIC_API_KEY?.trim();
22
+ if (key && !keyWasRejected(key, cfg)) {
23
+ return { mode: "api_key", evidence: "ANTHROPIC_API_KEY set" };
24
+ }
25
+ if (env.CLAUDE_CODE_OAUTH_TOKEN?.trim()) {
26
+ return { mode: "subscription", evidence: "CLAUDE_CODE_OAUTH_TOKEN set" };
27
+ }
28
+ const acct = cfg?.oauthAccount;
29
+ if (acct?.organizationType && SEAT_ORG.test(acct.organizationType)) {
30
+ return {
31
+ mode: "subscription",
32
+ evidence: `oauthAccount.organizationType=${acct.organizationType}`
33
+ };
34
+ }
35
+ if (acct?.billingType && /subscription/i.test(acct.billingType)) {
36
+ return {
37
+ mode: "subscription",
38
+ evidence: `oauthAccount.billingType=${acct.billingType}`
39
+ };
40
+ }
41
+ const tier = acct?.organizationRateLimitTier ?? acct?.userRateLimitTier;
42
+ if (tier && SEAT_TIER.test(tier)) {
43
+ return { mode: "subscription", evidence: `rate limit tier ${tier}` };
44
+ }
45
+ if (cfg?.hasAvailableSubscription === true) {
46
+ return { mode: "subscription", evidence: "hasAvailableSubscription=true" };
47
+ }
48
+ if (acct?.billingType) {
49
+ return {
50
+ mode: "api_key",
51
+ evidence: `oauthAccount.billingType=${acct.billingType}`
52
+ };
53
+ }
54
+ if (acct?.accountUuid) {
55
+ return {
56
+ mode: "unknown",
57
+ evidence: "signed in, but the account doesn't say how it bills"
58
+ };
59
+ }
60
+ return { mode: "unknown", evidence: "no API key and no Claude login found" };
61
+ }
62
+ function readClaudeJson() {
63
+ try {
64
+ return JSON.parse(
65
+ readFileSync(join(homedir(), ".claude.json"), "utf8")
66
+ );
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ function detectAuth() {
72
+ return classifyAuth(process.env, readClaudeJson());
73
+ }
74
+ function detectAuthMode() {
75
+ return detectAuth().mode;
76
+ }
77
+ function costIsRealMoney(mode) {
78
+ return mode === "api_key";
79
+ }
80
+
81
+ // ../shared/src/redact.ts
82
+ var REDACTED = "[REDACTED]";
83
+ var REDACTED_EMAIL = "[EMAIL]";
84
+ var PATTERNS = [
85
+ // Anthropic
86
+ { name: "anthropic_key", re: /\bsk-ant-[A-Za-z0-9_-]{10,}/g },
87
+ // OpenAI & friends (sk-..., sk-proj-...)
88
+ { name: "openai_key", re: /\bsk-[A-Za-z0-9_-]{20,}/g },
89
+ // GitHub tokens
90
+ { name: "github_token", re: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g },
91
+ { name: "github_pat", re: /\bgithub_pat_[A-Za-z0-9_]{20,}/g },
92
+ // Slack
93
+ { name: "slack_token", re: /\bxox[abprs]-[A-Za-z0-9-]{10,}/g },
94
+ {
95
+ name: "slack_webhook",
96
+ re: /https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/]+/g
97
+ },
98
+ // AWS
99
+ { name: "aws_access_key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
100
+ // Google API keys
101
+ { name: "google_api_key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
102
+ // Stripe
103
+ { name: "stripe_key", re: /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{10,}/g },
104
+ // Supabase/JWTs (three base64url segments starting with eyJ)
105
+ {
106
+ name: "jwt",
107
+ re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}\b/g
108
+ },
109
+ // Private key blocks
110
+ {
111
+ name: "private_key_block",
112
+ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g
113
+ },
114
+ // Authorization headers
115
+ {
116
+ name: "authorization_header",
117
+ re: /\b(Authorization\s*:\s*(?:Bearer|Basic|token)\s+)[^\s"'`,;]+/gi,
118
+ replacement: `$1${REDACTED}`
119
+ },
120
+ // Connection strings with credentials: scheme://user:pass@host
121
+ {
122
+ name: "connection_string_password",
123
+ re: /\b([a-z][a-z0-9+.-]*:\/\/[^\s:/@"']+:)[^\s@"']+(@)/gi,
124
+ replacement: `$1${REDACTED}$2`
125
+ },
126
+ // key=value / key: value assignments for sensitive names
127
+ {
128
+ name: "sensitive_assignment",
129
+ re: /\b((?:api[_-]?key|apikey|secret|token|passwd|password|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token)\s*[=:]\s*["']?)[^\s"',;&]{6,}/gi,
130
+ replacement: `$1${REDACTED}`
131
+ },
132
+ // Emails (PII) — applied last so credential shapes win first
133
+ {
134
+ name: "email",
135
+ re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
136
+ replacement: REDACTED_EMAIL
137
+ }
138
+ ];
139
+ function redactText(text) {
140
+ let out = text;
141
+ for (const p of PATTERNS) {
142
+ out = out.replace(p.re, p.replacement ?? REDACTED);
143
+ }
144
+ return out;
145
+ }
146
+ function redactValue(value) {
147
+ if (typeof value === "string") return redactText(value);
148
+ if (Array.isArray(value)) {
149
+ return value.map((v) => redactValue(v));
150
+ }
151
+ if (value !== null && typeof value === "object") {
152
+ const out = {};
153
+ for (const [k, v] of Object.entries(value)) {
154
+ out[k] = redactValue(v);
155
+ }
156
+ return out;
157
+ }
158
+ return value;
159
+ }
160
+
161
+ // ../shared/src/events.ts
162
+ import { z } from "zod";
163
+ var agentProductSchema = z.enum([
164
+ "claude_code",
165
+ "codex",
166
+ "cursor",
167
+ "gemini_cli",
168
+ "ai_sdk",
169
+ "other"
170
+ ]);
171
+ var eventSourceSchema = z.enum([
172
+ "cc_hook",
173
+ // Claude Code http hook POST
174
+ "cc_jsonl",
175
+ // parsed local transcript (flush/tail upload)
176
+ "cc_otel",
177
+ // OTLP logs intake (claude_code.api_request etc.)
178
+ "api"
179
+ // created by our own backend (guard decisions, system notes)
180
+ ]);
181
+ var usageSchema = z.object({
182
+ input: z.number().int().nonnegative(),
183
+ output: z.number().int().nonnegative(),
184
+ cache_read: z.number().int().nonnegative().default(0),
185
+ cache_write_5m: z.number().int().nonnegative().default(0),
186
+ cache_write_1h: z.number().int().nonnegative().default(0)
187
+ });
188
+ var eventKindSchema = z.enum([
189
+ "session_start",
190
+ "prompt",
191
+ // user prompt submitted
192
+ "assistant",
193
+ // assistant text/thinking turn
194
+ "api_request",
195
+ // one billed LLM request (spend atom; unique per request_id)
196
+ "tool_call",
197
+ // tool invocation with input
198
+ "tool_result",
199
+ // tool outcome (stdout/stderr/result payload)
200
+ "file_diff",
201
+ // Edit/Write derived diff
202
+ "subagent",
203
+ // subagent lifecycle
204
+ "guard_decision",
205
+ // tokenoscopy guard allow/ask/deny
206
+ "session_end",
207
+ "system"
208
+ // parser drift notes, api errors, misc
209
+ ]);
210
+ var sessionEventSchema = z.object({
211
+ /** Claude Code session UUID (or equivalent per product). */
212
+ session_id: z.string().min(8),
213
+ ts: z.string().datetime({ offset: true }),
214
+ kind: eventKindSchema,
215
+ source: eventSourceSchema,
216
+ agent_product: agentProductSchema.default("claude_code"),
217
+ // Join / dedup keys
218
+ uuid: z.string().optional(),
219
+ // upstream record uuid (JSONL `uuid`)
220
+ parent_uuid: z.string().nullish(),
221
+ prompt_id: z.string().optional(),
222
+ tool_use_id: z.string().optional(),
223
+ request_id: z.string().optional(),
224
+ // spend dedup key
225
+ // Normalized core
226
+ model: z.string().optional(),
227
+ usage: usageSchema.optional(),
228
+ /** List-price estimate in USD for this event (api_request events only). */
229
+ cost_usd: z.number().nonnegative().optional(),
230
+ tool_name: z.string().optional(),
231
+ is_sidechain: z.boolean().optional(),
232
+ /** Kind-specific display payload (REDACTED before upload). */
233
+ payload: z.record(z.string(), z.unknown()).default({})
234
+ });
235
+ var authModeSchema = z.enum(["subscription", "api_key", "unknown"]);
236
+ var costBasisSchema = z.enum(["estimated", "billed"]);
237
+ var sessionSummarySchema = z.object({
238
+ session_id: z.string().min(8),
239
+ agent_product: agentProductSchema.default("claude_code"),
240
+ agent_version: z.string().optional(),
241
+ actor: z.object({
242
+ email: z.string().optional(),
243
+ /** Display name — git user.name, else OS user. */
244
+ name: z.string().optional(),
245
+ machine_id: z.string().optional()
246
+ }).default({}),
247
+ cwd: z.string().optional(),
248
+ git_repo: z.string().optional(),
249
+ // basename or remote-derived slug, never a full local path
250
+ git_branch: z.string().optional(),
251
+ /** Branch drift from local refs at flush time (as of the last fetch). */
252
+ git_base: z.string().optional(),
253
+ git_behind: z.number().int().nonnegative().optional(),
254
+ git_ahead: z.number().int().nonnegative().optional(),
255
+ started_at: z.string().datetime({ offset: true }).optional(),
256
+ ended_at: z.string().datetime({ offset: true }).optional(),
257
+ auth_mode: authModeSchema.default("unknown"),
258
+ cost_basis: costBasisSchema.default("estimated"),
259
+ models: z.array(z.string()).default([]),
260
+ usage_totals: usageSchema,
261
+ estimated_cost_usd: z.number().nonnegative(),
262
+ api_request_count: z.number().int().nonnegative().default(0),
263
+ tool_call_count: z.number().int().nonnegative().default(0),
264
+ lines_added: z.number().int().nonnegative().default(0),
265
+ lines_removed: z.number().int().nonnegative().default(0),
266
+ /** Count of transcript records the parser did not recognize (drift signal). */
267
+ unknown_record_count: z.number().int().nonnegative().default(0)
268
+ });
269
+ var flushUploadSchema = z.object({
270
+ schema_version: z.literal(1),
271
+ summary: sessionSummarySchema,
272
+ events: z.array(sessionEventSchema)
273
+ });
274
+ var hookEventSchema = z.object({
275
+ hook_event_name: z.string(),
276
+ session_id: z.string().min(8),
277
+ transcript_path: z.string().optional(),
278
+ cwd: z.string().optional(),
279
+ permission_mode: z.string().optional(),
280
+ prompt_id: z.string().optional(),
281
+ tool_name: z.string().optional(),
282
+ tool_use_id: z.string().optional(),
283
+ tool_input: z.unknown().optional(),
284
+ tool_response: z.unknown().optional(),
285
+ prompt: z.string().optional(),
286
+ stop_reason: z.string().optional(),
287
+ reason: z.string().optional(),
288
+ agent_id: z.string().optional(),
289
+ agent_type: z.string().optional()
290
+ }).passthrough();
291
+ var guardCheckRequestSchema = z.object({
292
+ session_id: z.string().min(8),
293
+ tool_name: z.string(),
294
+ cwd: z.string().optional(),
295
+ git_repo: z.string().optional()
296
+ });
297
+ var guardDecisionSchema = z.enum(["allow", "ask", "deny"]);
298
+ var guardCheckResponseSchema = z.object({
299
+ decision: guardDecisionSchema,
300
+ reason: z.string().optional(),
301
+ budget: z.object({
302
+ scope: z.string(),
303
+ cap_usd: z.number(),
304
+ spent_usd: z.number()
305
+ }).optional()
306
+ });
307
+
308
+ // ../shared/src/pricing.ts
309
+ var CACHE_READ_MULT = 0.1;
310
+ var CACHE_WRITE_5M_MULT = 1.25;
311
+ var CACHE_WRITE_1H_MULT = 2;
312
+ var MODEL_PRICES = [
313
+ // Claude 5 family
314
+ { prefix: "claude-fable-5", input: 10, output: 50 },
315
+ { prefix: "claude-mythos-5", input: 10, output: 50 },
316
+ { prefix: "claude-opus-5", input: 5, output: 25 },
317
+ { prefix: "claude-sonnet-5", input: 3, output: 15 },
318
+ // Claude 4.x
319
+ { prefix: "claude-opus-4-8", input: 5, output: 25 },
320
+ { prefix: "claude-opus-4-7", input: 5, output: 25 },
321
+ { prefix: "claude-opus-4-6", input: 5, output: 25 },
322
+ { prefix: "claude-opus-4-5", input: 5, output: 25 },
323
+ { prefix: "claude-opus-4-1", input: 15, output: 75 },
324
+ { prefix: "claude-opus-4-2", input: 15, output: 75 },
325
+ // dated opus-4-0 ids: claude-opus-4-20250514
326
+ { prefix: "claude-opus-4-0", input: 15, output: 75 },
327
+ { prefix: "claude-sonnet-4", input: 3, output: 15 },
328
+ // covers 4-6, 4-5, 4-0, dated ids
329
+ { prefix: "claude-haiku-4-5", input: 1, output: 5 },
330
+ // Claude 3.x stragglers
331
+ { prefix: "claude-3-5-haiku", input: 0.8, output: 4 },
332
+ { prefix: "claude-3-7-sonnet", input: 3, output: 15 },
333
+ { prefix: "claude-3-haiku", input: 0.25, output: 1.25 }
334
+ ];
335
+ function normalizeModelId(model) {
336
+ let m = model.trim().toLowerCase();
337
+ const anthropicIdx = m.indexOf("anthropic.");
338
+ if (anthropicIdx >= 0) m = m.slice(anthropicIdx + "anthropic.".length);
339
+ m = m.replace(/-v\d+:\d+$/, "");
340
+ return m;
341
+ }
342
+ function findModelPrice(model) {
343
+ const normalized = normalizeModelId(model);
344
+ let best = null;
345
+ for (const row of MODEL_PRICES) {
346
+ if (normalized.startsWith(row.prefix)) {
347
+ if (!best || row.prefix.length > best.prefix.length) best = row;
348
+ }
349
+ }
350
+ return best;
351
+ }
352
+ function estimateCostUsd(model, usage) {
353
+ const price = findModelPrice(model);
354
+ if (!price) return null;
355
+ const perTok = 1 / 1e6;
356
+ const cost = usage.input * price.input * perTok + usage.output * price.output * perTok + usage.cache_read * price.input * CACHE_READ_MULT * perTok + usage.cache_write_5m * price.input * CACHE_WRITE_5M_MULT * perTok + usage.cache_write_1h * price.input * CACHE_WRITE_1H_MULT * perTok;
357
+ return Math.round(cost * 1e6) / 1e6;
358
+ }
359
+
360
+ // ../shared/src/diagnose.ts
361
+ var DIAG = {
362
+ /** A session counts as live if it produced an event this recently. */
363
+ LIVE_WINDOW_MS: 15 * 6e4,
364
+ RUNAWAY_MIN_COST: 10,
365
+ RUNAWAY_CTX_TOKENS: 4e5,
366
+ RUNAWAY_CYCLE: 40,
367
+ RUNAWAY_REPEAT: 10,
368
+ BLOAT_CTX_TOKENS: 2e5,
369
+ BLOAT_MIN_COST: 5,
370
+ CYCLE_MIN: 25,
371
+ REPEAT_MIN: 5
372
+ };
373
+
374
+ // ../shared/src/guard-contract.ts
375
+ var GUARD_TIMEOUT_MS = 1500;
376
+ var ALLOW_CACHE_MS = 15e3;
377
+
378
+ // ../shared/src/dedup.ts
379
+ function dedupKeyPrefix(kind) {
380
+ switch (kind) {
381
+ case "api_request":
382
+ return "api";
383
+ case "tool_call":
384
+ return "tc";
385
+ case "tool_result":
386
+ return "tr";
387
+ case "file_diff":
388
+ return "fd";
389
+ case "prompt":
390
+ return "pr";
391
+ case "assistant":
392
+ return "as";
393
+ default:
394
+ return kind;
395
+ }
396
+ }
397
+ function naturalDedupKey(e) {
398
+ const id = (() => {
399
+ switch (e.kind) {
400
+ case "api_request":
401
+ return e.request_id ?? e.uuid;
402
+ case "tool_call":
403
+ case "tool_result":
404
+ case "file_diff":
405
+ return e.tool_use_id ?? e.uuid;
406
+ case "prompt":
407
+ return e.prompt_id ?? e.uuid;
408
+ default:
409
+ return e.uuid;
410
+ }
411
+ })();
412
+ return id ? `${dedupKeyPrefix(e.kind)}:${id}` : null;
413
+ }
414
+ var MAX_PAYLOAD_TEXT = 2e4;
415
+ var MARKER = /^\n…\[truncated \d+ chars\]$/;
416
+ function truncatePayload(value) {
417
+ if (typeof value === "string") {
418
+ if (value.length <= MAX_PAYLOAD_TEXT) return value;
419
+ if (MARKER.test(value.slice(MAX_PAYLOAD_TEXT))) return value;
420
+ return `${value.slice(0, MAX_PAYLOAD_TEXT)}
421
+ \u2026[truncated ${value.length - MAX_PAYLOAD_TEXT} chars]`;
422
+ }
423
+ if (Array.isArray(value)) return value.map(truncatePayload);
424
+ if (value !== null && typeof value === "object") {
425
+ const out = {};
426
+ for (const [k, v] of Object.entries(value)) {
427
+ out[k] = truncatePayload(v);
428
+ }
429
+ return out;
430
+ }
431
+ return value;
432
+ }
433
+
434
+ // ../shared/src/work-map.ts
435
+ function trimSlashes(p) {
436
+ return p.replace(/\/+$/, "");
437
+ }
438
+ function relativeToRoot(abs, root) {
439
+ if (!abs || !root) return void 0;
440
+ if (!abs.startsWith("/") || !root.startsWith("/")) return void 0;
441
+ const r = trimSlashes(root);
442
+ if (r.length === 0) return void 0;
443
+ if (!abs.startsWith(`${r}/`)) return void 0;
444
+ const rel = abs.slice(r.length + 1).replace(/\/{2,}/g, "/");
445
+ if (rel.length === 0 || rel.split("/").includes("..")) return void 0;
446
+ return rel;
447
+ }
448
+ function toolFilePath(input) {
449
+ if (!input || typeof input !== "object") return void 0;
450
+ const i = input;
451
+ const p = i.file_path ?? i.notebook_path;
452
+ return typeof p === "string" && p.length > 0 ? p : void 0;
453
+ }
454
+ function withRelPath(event, root) {
455
+ if (!root) return event;
456
+ let abs;
457
+ if (event.kind === "tool_call") abs = toolFilePath(event.payload.input);
458
+ else if (event.kind === "file_diff") {
459
+ const p = event.payload.file_path;
460
+ abs = typeof p === "string" ? p : void 0;
461
+ }
462
+ const rel = relativeToRoot(abs, root);
463
+ if (!rel) return event;
464
+ return { ...event, payload: { ...event.payload, rel_path: rel } };
465
+ }
466
+
467
+ // src/transcript.ts
468
+ var SKIPPED_TYPES = /* @__PURE__ */ new Set([
469
+ "summary",
470
+ "attachment",
471
+ "file-history-snapshot",
472
+ "last-prompt",
473
+ "mode",
474
+ "permission-mode",
475
+ "queue-operation",
476
+ "progress"
477
+ ]);
478
+ var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "NotebookEdit", "MultiEdit"]);
479
+ function normalizeUsage(raw) {
480
+ const split = raw.cache_creation;
481
+ const write5m = split?.ephemeral_5m_input_tokens;
482
+ const write1h = split?.ephemeral_1h_input_tokens;
483
+ return {
484
+ input: raw.input_tokens ?? 0,
485
+ output: raw.output_tokens ?? 0,
486
+ cache_read: raw.cache_read_input_tokens ?? 0,
487
+ // No TTL split reported → attribute the whole write to the cheaper 5m class
488
+ // (under-estimate beats over-estimate for a number we label "estimated").
489
+ cache_write_5m: write5m ?? raw.cache_creation_input_tokens ?? 0,
490
+ cache_write_1h: write1h ?? 0
491
+ };
492
+ }
493
+ function addUsage(a, b) {
494
+ return {
495
+ input: a.input + b.input,
496
+ output: a.output + b.output,
497
+ cache_read: a.cache_read + b.cache_read,
498
+ cache_write_5m: a.cache_write_5m + b.cache_write_5m,
499
+ cache_write_1h: a.cache_write_1h + b.cache_write_1h
500
+ };
501
+ }
502
+ function countLines(s) {
503
+ if (typeof s !== "string" || s.length === 0) return 0;
504
+ return s.split("\n").length;
505
+ }
506
+ function repoLabelFromCwd(cwd) {
507
+ if (!cwd) return void 0;
508
+ const parts = cwd.split("/").filter(Boolean);
509
+ return parts[parts.length - 1];
510
+ }
511
+ function parseTranscript(jsonl) {
512
+ const warnings = [];
513
+ const events = [];
514
+ const seenRequestIds = /* @__PURE__ */ new Set();
515
+ const models = /* @__PURE__ */ new Set();
516
+ let sessionId = "";
517
+ let agentVersion;
518
+ let cwd;
519
+ let gitBranch;
520
+ let firstTs;
521
+ let lastTs;
522
+ let unknownCount = 0;
523
+ let unknownModelFlagged = false;
524
+ let usageTotals = {
525
+ input: 0,
526
+ output: 0,
527
+ cache_read: 0,
528
+ cache_write_5m: 0,
529
+ cache_write_1h: 0
530
+ };
531
+ let totalCost = 0;
532
+ let apiRequestCount = 0;
533
+ let toolCallCount = 0;
534
+ let linesAdded = 0;
535
+ let linesRemoved = 0;
536
+ const lines = jsonl.split("\n");
537
+ for (const line of lines) {
538
+ const trimmed = line.trim();
539
+ if (!trimmed) continue;
540
+ let record;
541
+ try {
542
+ record = JSON.parse(trimmed);
543
+ } catch {
544
+ unknownCount++;
545
+ warnings.push("unparseable_line");
546
+ continue;
547
+ }
548
+ sessionId ||= record.sessionId ?? "";
549
+ agentVersion ||= record.version;
550
+ cwd ||= record.cwd;
551
+ gitBranch ||= record.gitBranch;
552
+ if (record.timestamp) {
553
+ firstTs ??= record.timestamp;
554
+ lastTs = record.timestamp;
555
+ }
556
+ const type = record.type ?? "NO_TYPE";
557
+ if (SKIPPED_TYPES.has(type)) continue;
558
+ const base = {
559
+ session_id: record.sessionId ?? sessionId ?? "unknown-session",
560
+ ts: record.timestamp ?? lastTs ?? (/* @__PURE__ */ new Date(0)).toISOString(),
561
+ source: "cc_jsonl",
562
+ agent_product: "claude_code",
563
+ uuid: record.uuid,
564
+ parent_uuid: record.parentUuid,
565
+ prompt_id: record.promptId,
566
+ is_sidechain: record.isSidechain
567
+ };
568
+ if (type === "assistant") {
569
+ const msg = record.message ?? {};
570
+ const model = msg.model;
571
+ if (model) models.add(model);
572
+ const requestId = record.requestId ?? msg.id;
573
+ if (requestId && !seenRequestIds.has(requestId) && msg.usage) {
574
+ seenRequestIds.add(requestId);
575
+ const usage = normalizeUsage(msg.usage);
576
+ usageTotals = addUsage(usageTotals, usage);
577
+ apiRequestCount++;
578
+ let cost;
579
+ if (model) {
580
+ const estimated = estimateCostUsd(model, usage);
581
+ if (estimated === null) {
582
+ if (!unknownModelFlagged) {
583
+ warnings.push(`unknown_model_price:${model}`);
584
+ unknownModelFlagged = true;
585
+ }
586
+ } else {
587
+ cost = estimated;
588
+ totalCost += estimated;
589
+ }
590
+ }
591
+ events.push({
592
+ ...base,
593
+ kind: "api_request",
594
+ request_id: requestId,
595
+ model,
596
+ usage,
597
+ cost_usd: cost,
598
+ payload: { stop_reason: msg.stop_reason ?? null }
599
+ });
600
+ }
601
+ const content = Array.isArray(msg.content) ? msg.content : [];
602
+ for (const block of content) {
603
+ if (typeof block !== "object" || block === null) continue;
604
+ const b = block;
605
+ if (b.type === "text" || b.type === "thinking") {
606
+ const text = b.type === "text" ? b.text : b.thinking;
607
+ if (!text) continue;
608
+ events.push({
609
+ ...base,
610
+ kind: "assistant",
611
+ request_id: requestId,
612
+ model,
613
+ payload: { block_type: b.type, text }
614
+ });
615
+ } else if (b.type === "tool_use") {
616
+ toolCallCount++;
617
+ const toolName = typeof b.name === "string" ? b.name : "unknown";
618
+ const input = b.input;
619
+ events.push({
620
+ ...base,
621
+ kind: "tool_call",
622
+ request_id: requestId,
623
+ tool_use_id: typeof b.id === "string" ? b.id : void 0,
624
+ tool_name: toolName,
625
+ payload: { input: input ?? {} }
626
+ });
627
+ if (EDIT_TOOLS.has(toolName) && input) {
628
+ const filePath = input.file_path;
629
+ const oldStr = input.old_string;
630
+ const newStr = input.new_string ?? input.content;
631
+ const added = toolName === "Write" ? countLines(newStr) : countLines(newStr);
632
+ const removed = toolName === "Write" ? 0 : countLines(oldStr);
633
+ linesAdded += added;
634
+ linesRemoved += removed;
635
+ events.push({
636
+ ...base,
637
+ kind: "file_diff",
638
+ request_id: requestId,
639
+ tool_use_id: typeof b.id === "string" ? b.id : void 0,
640
+ tool_name: toolName,
641
+ payload: {
642
+ file_path: filePath,
643
+ old_string: oldStr,
644
+ new_string: newStr,
645
+ lines_added: added,
646
+ lines_removed: removed
647
+ }
648
+ });
649
+ }
650
+ }
651
+ }
652
+ continue;
653
+ }
654
+ if (type === "user") {
655
+ const msg = record.message ?? {};
656
+ const content = msg.content;
657
+ if (typeof content === "string") {
658
+ events.push({ ...base, kind: "prompt", payload: { text: content } });
659
+ continue;
660
+ }
661
+ if (Array.isArray(content)) {
662
+ for (const block of content) {
663
+ if (typeof block !== "object" || block === null) continue;
664
+ const b = block;
665
+ if (b.type === "text" && typeof b.text === "string") {
666
+ events.push({ ...base, kind: "prompt", payload: { text: b.text } });
667
+ } else if (b.type === "tool_result") {
668
+ const tur = record.toolUseResult;
669
+ const isError = b.is_error === true;
670
+ const payload = { is_error: isError };
671
+ if (typeof tur === "object" && tur !== null) {
672
+ if (typeof tur.stdout === "string") payload.stdout = tur.stdout;
673
+ if (typeof tur.stderr === "string") payload.stderr = tur.stderr;
674
+ if (typeof tur.interrupted === "boolean")
675
+ payload.interrupted = tur.interrupted;
676
+ if (payload.stdout === void 0 && payload.stderr === void 0) {
677
+ payload.result = tur;
678
+ }
679
+ } else if (typeof tur === "string") {
680
+ payload.result = tur;
681
+ } else if (typeof b.content === "string") {
682
+ payload.result = b.content;
683
+ }
684
+ events.push({
685
+ ...base,
686
+ kind: "tool_result",
687
+ tool_use_id: typeof b.tool_use_id === "string" ? b.tool_use_id : void 0,
688
+ payload
689
+ });
690
+ }
691
+ }
692
+ continue;
693
+ }
694
+ continue;
695
+ }
696
+ if (type === "system") {
697
+ events.push({
698
+ ...base,
699
+ kind: "system",
700
+ payload: {
701
+ subtype: record.subtype ?? "unknown",
702
+ // api_error records carry retry metadata worth keeping raw
703
+ detail: record.content ?? record.error ?? null
704
+ }
705
+ });
706
+ continue;
707
+ }
708
+ unknownCount++;
709
+ if (!warnings.includes(`unknown_record_type:${type}`)) {
710
+ warnings.push(`unknown_record_type:${type}`);
711
+ }
712
+ }
713
+ const summary = sessionSummarySchema.parse({
714
+ session_id: sessionId || "unknown-session",
715
+ agent_product: "claude_code",
716
+ agent_version: agentVersion,
717
+ actor: {},
718
+ cwd,
719
+ git_repo: repoLabelFromCwd(cwd),
720
+ git_branch: gitBranch,
721
+ started_at: firstTs,
722
+ ended_at: lastTs,
723
+ auth_mode: "unknown",
724
+ cost_basis: "estimated",
725
+ models: [...models],
726
+ usage_totals: usageTotals,
727
+ estimated_cost_usd: Math.round(totalCost * 1e6) / 1e6,
728
+ api_request_count: apiRequestCount,
729
+ tool_call_count: toolCallCount,
730
+ lines_added: linesAdded,
731
+ lines_removed: linesRemoved,
732
+ unknown_record_count: unknownCount
733
+ });
734
+ return { summary, events, warnings };
735
+ }
736
+
737
+ export {
738
+ classifyAuth,
739
+ readClaudeJson,
740
+ detectAuth,
741
+ detectAuthMode,
742
+ costIsRealMoney,
743
+ redactText,
744
+ redactValue,
745
+ GUARD_TIMEOUT_MS,
746
+ ALLOW_CACHE_MS,
747
+ naturalDedupKey,
748
+ truncatePayload,
749
+ withRelPath,
750
+ repoLabelFromCwd,
751
+ parseTranscript
752
+ };