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.
- package/README.md +36 -2
- package/dist/chunk-L62TVAWB.js +752 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +716 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +22 -0
- package/package.json +59 -4
|
@@ -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
|
+
};
|