devrage 0.0.3 → 0.0.5

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.
Files changed (40) hide show
  1. package/dist/cli.js +2428 -200
  2. package/dist/cli.js.map +4 -4
  3. package/dist/lib/adapters/amp.d.ts.map +1 -1
  4. package/dist/lib/adapters/amp.js +171 -7
  5. package/dist/lib/adapters/amp.js.map +1 -1
  6. package/dist/lib/adapters/claude.d.ts.map +1 -1
  7. package/dist/lib/adapters/claude.js +147 -49
  8. package/dist/lib/adapters/claude.js.map +1 -1
  9. package/dist/lib/adapters/cline.d.ts.map +1 -1
  10. package/dist/lib/adapters/cline.js +17 -12
  11. package/dist/lib/adapters/cline.js.map +1 -1
  12. package/dist/lib/adapters/codex.d.ts.map +1 -1
  13. package/dist/lib/adapters/codex.js +127 -14
  14. package/dist/lib/adapters/codex.js.map +1 -1
  15. package/dist/lib/adapters/cursor.d.ts +3 -0
  16. package/dist/lib/adapters/cursor.d.ts.map +1 -0
  17. package/dist/lib/adapters/cursor.js +524 -0
  18. package/dist/lib/adapters/cursor.js.map +1 -0
  19. package/dist/lib/adapters/index.d.ts +61 -0
  20. package/dist/lib/adapters/index.d.ts.map +1 -1
  21. package/dist/lib/adapters/index.js +4 -0
  22. package/dist/lib/adapters/index.js.map +1 -1
  23. package/dist/lib/adapters/opencode.d.ts.map +1 -1
  24. package/dist/lib/adapters/opencode.js +96 -16
  25. package/dist/lib/adapters/opencode.js.map +1 -1
  26. package/dist/lib/adapters/pi.d.ts +3 -0
  27. package/dist/lib/adapters/pi.d.ts.map +1 -0
  28. package/dist/lib/adapters/pi.js +209 -0
  29. package/dist/lib/adapters/pi.js.map +1 -0
  30. package/dist/lib/adapters/zed.d.ts.map +1 -1
  31. package/dist/lib/adapters/zed.js +18 -11
  32. package/dist/lib/adapters/zed.js.map +1 -1
  33. package/dist/lib/detector/index.d.ts.map +1 -1
  34. package/dist/lib/detector/index.js +12 -6
  35. package/dist/lib/detector/index.js.map +1 -1
  36. package/dist/lib/index.d.ts +1 -1
  37. package/dist/lib/index.d.ts.map +1 -1
  38. package/dist/lib/index.js +1 -1
  39. package/dist/lib/index.js.map +1 -1
  40. package/package.json +3 -1
package/dist/cli.js CHANGED
@@ -1,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/commands/scan.ts
4
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
5
+ import { dirname as dirname2, join as join10 } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+
3
8
  // src/adapters/amp.ts
4
9
  import { readdir, readFile } from "node:fs/promises";
5
10
  import { homedir } from "node:os";
6
11
  import { join } from "node:path";
7
12
  function getAmpThreadsDir() {
8
- return join(
9
- process.env["XDG_DATA_HOME"] ?? join(homedir(), ".local", "share"),
10
- "amp",
11
- "threads"
12
- );
13
+ return join(process.env["XDG_DATA_HOME"] ?? join(homedir(), ".local", "share"), "amp", "threads");
13
14
  }
14
15
  function ampAdapter() {
15
16
  return {
@@ -27,17 +28,27 @@ function ampAdapter() {
27
28
  const filePath = join(threadsDir, file);
28
29
  const threadId = file.replace(".json", "");
29
30
  try {
30
- const raw = await readFile(filePath, "utf-8");
31
- const thread = JSON.parse(raw);
32
- if (!thread.messages || !Array.isArray(thread.messages)) continue;
31
+ const thread = await readAmpThread(filePath);
32
+ if (!thread) {
33
+ continue;
34
+ }
35
+ if (!thread.messages || !Array.isArray(thread.messages)) {
36
+ continue;
37
+ }
33
38
  for (const msg of thread.messages) {
34
- if (msg.role !== "user") continue;
39
+ if (msg.role !== "user") {
40
+ continue;
41
+ }
35
42
  const text = extractText(msg.content);
36
- if (!text) continue;
43
+ if (!text) {
44
+ continue;
45
+ }
37
46
  const timestamp = msg.timestamp ?? msg.createdAt ?? void 0;
38
47
  if (options?.since && timestamp) {
39
48
  const ts = new Date(timestamp);
40
- if (ts < options.since) continue;
49
+ if (ts < options.since) {
50
+ continue;
51
+ }
41
52
  }
42
53
  yield {
43
54
  text,
@@ -48,11 +59,47 @@ function ampAdapter() {
48
59
  } catch {
49
60
  }
50
61
  }
62
+ },
63
+ async *usage(options) {
64
+ const threadsDir = getAmpThreadsDir();
65
+ let files;
66
+ try {
67
+ files = await readdir(threadsDir);
68
+ } catch {
69
+ return;
70
+ }
71
+ for (const file of files.filter((f) => f.endsWith(".json"))) {
72
+ const filePath = join(threadsDir, file);
73
+ const threadId = file.replace(".json", "");
74
+ try {
75
+ const thread = await readAmpThread(filePath);
76
+ if (!thread?.usageLedger) {
77
+ continue;
78
+ }
79
+ for (const record of extractAmpUsageRecords(thread.usageLedger, threadId)) {
80
+ if (options?.since && record.timestamp) {
81
+ const ts = new Date(record.timestamp);
82
+ if (ts < options.since) {
83
+ continue;
84
+ }
85
+ }
86
+ yield record;
87
+ }
88
+ } catch {
89
+ }
90
+ }
51
91
  }
52
92
  };
53
93
  }
94
+ async function readAmpThread(filePath) {
95
+ const raw = await readFile(filePath, "utf-8");
96
+ const parsed = JSON.parse(raw);
97
+ return asRecord(parsed) ? parsed : null;
98
+ }
54
99
  function extractText(content) {
55
- if (typeof content === "string") return content;
100
+ if (typeof content === "string") {
101
+ return content;
102
+ }
56
103
  if (Array.isArray(content)) {
57
104
  const parts = content.filter(
58
105
  (p) => typeof p === "object" && p !== null && typeof p.text === "string"
@@ -61,6 +108,125 @@ function extractText(content) {
61
108
  }
62
109
  return null;
63
110
  }
111
+ function extractAmpUsageRecords(usageLedger, threadId) {
112
+ const records = [];
113
+ collectAmpUsage(usageLedger, threadId, records, {});
114
+ return records;
115
+ }
116
+ function collectAmpUsage(value, threadId, records, context, depth = 0) {
117
+ if (depth > 12) {
118
+ return;
119
+ }
120
+ if (Array.isArray(value)) {
121
+ for (const item of value) {
122
+ collectAmpUsage(item, threadId, records, context, depth + 1);
123
+ }
124
+ return;
125
+ }
126
+ const record = asRecord(value);
127
+ if (!record) {
128
+ return;
129
+ }
130
+ const nextContext = {
131
+ provider: stringField(record, ["provider", "providerID", "providerId"]) ?? context.provider,
132
+ model: stringField(record, ["model", "modelID", "modelId"]) ?? context.model,
133
+ timestamp: timestampField(record) ?? context.timestamp
134
+ };
135
+ const usageSource = firstRecordField(record, ["usage", "tokens", "tokenUsage"]) ?? record;
136
+ const rawInputTokens = tokenField(usageSource, ["inputTokens", "input_tokens", "promptTokens"]);
137
+ const outputTokens = tokenField(usageSource, [
138
+ "outputTokens",
139
+ "output_tokens",
140
+ "completionTokens"
141
+ ]);
142
+ const reasoningTokens = tokenField(usageSource, ["reasoningTokens", "reasoning_output_tokens"]);
143
+ const cachedInputSubset = tokenField(usageSource, ["cachedInputTokens", "cached_input_tokens"]);
144
+ const cacheReadTokens = tokenField(usageSource, [
145
+ "cacheReadTokens",
146
+ "cache_read_tokens",
147
+ "cacheReadInputTokens",
148
+ "cache_read_input_tokens",
149
+ "cachedInputTokens",
150
+ "cached_input_tokens"
151
+ ]);
152
+ const inputTokens = Math.max(rawInputTokens - cachedInputSubset, 0);
153
+ const cacheWriteTokens = tokenField(usageSource, [
154
+ "cacheWriteTokens",
155
+ "cache_write_tokens",
156
+ "cacheCreationInputTokens",
157
+ "cache_creation_input_tokens",
158
+ "cacheWriteInputTokens",
159
+ "cache_write_input_tokens"
160
+ ]);
161
+ const billedCost = tokenField(record, [
162
+ "cost",
163
+ "totalCost",
164
+ "total_cost",
165
+ "billedCost",
166
+ "billed_cost"
167
+ ]);
168
+ if (inputTokens + outputTokens + reasoningTokens + cacheReadTokens + cacheWriteTokens + billedCost > 0) {
169
+ records.push({
170
+ agent: "amp",
171
+ provider: nextContext.provider,
172
+ model: nextContext.model,
173
+ timestamp: nextContext.timestamp,
174
+ session: threadId,
175
+ billedCost,
176
+ inputTokens,
177
+ outputTokens,
178
+ reasoningTokens,
179
+ cacheReadTokens,
180
+ cacheWriteTokens
181
+ });
182
+ }
183
+ for (const child of Object.values(record)) {
184
+ if (child !== usageSource && typeof child === "object" && child !== null) {
185
+ collectAmpUsage(child, threadId, records, nextContext, depth + 1);
186
+ }
187
+ }
188
+ }
189
+ function firstRecordField(record, fields) {
190
+ for (const field of fields) {
191
+ const value = asRecord(record[field]);
192
+ if (value) {
193
+ return value;
194
+ }
195
+ }
196
+ return null;
197
+ }
198
+ function stringField(record, fields) {
199
+ for (const field of fields) {
200
+ const value = record[field];
201
+ if (typeof value === "string" && value.trim()) {
202
+ return value;
203
+ }
204
+ }
205
+ return void 0;
206
+ }
207
+ function timestampField(record) {
208
+ const value = stringField(record, ["timestamp", "createdAt", "time", "date"]);
209
+ if (value) {
210
+ const date = new Date(value);
211
+ return Number.isFinite(date.getTime()) ? date.toISOString() : void 0;
212
+ }
213
+ return void 0;
214
+ }
215
+ function tokenField(record, fields) {
216
+ for (const field of fields) {
217
+ const value = record[field];
218
+ if (typeof value === "number" && Number.isFinite(value)) {
219
+ return value;
220
+ }
221
+ }
222
+ return 0;
223
+ }
224
+ function asRecord(value) {
225
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
226
+ return null;
227
+ }
228
+ return value;
229
+ }
64
230
 
65
231
  // src/adapters/claude.ts
66
232
  import { createReadStream } from "node:fs";
@@ -73,63 +239,78 @@ function claudeAdapter() {
73
239
  return {
74
240
  name: "claude",
75
241
  async *messages(options) {
76
- const projectsDir = CLAUDE_DIR;
77
- let projectDirs;
78
- try {
79
- projectDirs = await readdir2(projectsDir);
80
- } catch {
81
- return;
242
+ for await (const file of discoverClaudeJsonlFiles()) {
243
+ yield* parseClaudeJsonl(file.filePath, { ...file, since: options?.since });
82
244
  }
83
- for (const projectDir of projectDirs) {
84
- const projectPath = join2(projectsDir, projectDir);
85
- const projectStat = await stat(projectPath);
86
- if (!projectStat.isDirectory()) continue;
87
- const entries = await readdir2(projectPath);
88
- const jsonlFiles = entries.filter((f) => f.endsWith(".jsonl"));
89
- for (const file of jsonlFiles) {
90
- const filePath = join2(projectPath, file);
91
- const session = file.replace(".jsonl", "");
92
- yield* parseClaudeJsonl(filePath, {
93
- session,
94
- project: projectDir,
95
- since: options?.since
96
- });
97
- }
98
- const subdirs = entries.filter((f) => !f.includes("."));
99
- for (const subdir of subdirs) {
100
- const subagentsDir = join2(projectPath, subdir, "subagents");
101
- try {
102
- const subFiles = await readdir2(subagentsDir);
103
- const subJsonl = subFiles.filter((f) => f.endsWith(".jsonl"));
104
- for (const file of subJsonl) {
105
- yield* parseClaudeJsonl(join2(subagentsDir, file), {
106
- session: `${subdir}/${file.replace(".jsonl", "")}`,
107
- project: projectDir,
108
- since: options?.since
109
- });
110
- }
111
- } catch {
112
- }
113
- }
245
+ },
246
+ async *usage(options) {
247
+ for await (const file of discoverClaudeJsonlFiles()) {
248
+ yield* parseClaudeUsageJsonl(file.filePath, { ...file, since: options?.since });
114
249
  }
115
250
  }
116
251
  };
117
252
  }
253
+ async function* discoverClaudeJsonlFiles() {
254
+ let projectDirs;
255
+ try {
256
+ projectDirs = await readdir2(CLAUDE_DIR);
257
+ } catch {
258
+ return;
259
+ }
260
+ for (const projectDir of projectDirs) {
261
+ const projectPath = join2(CLAUDE_DIR, projectDir);
262
+ const projectStat = await stat(projectPath);
263
+ if (!projectStat.isDirectory()) {
264
+ continue;
265
+ }
266
+ const entries = await readdir2(projectPath);
267
+ const jsonlFiles = entries.filter((f) => f.endsWith(".jsonl"));
268
+ for (const file of jsonlFiles) {
269
+ yield {
270
+ filePath: join2(projectPath, file),
271
+ session: file.replace(".jsonl", ""),
272
+ project: projectDir
273
+ };
274
+ }
275
+ const subdirs = entries.filter((f) => !f.includes("."));
276
+ for (const subdir of subdirs) {
277
+ const subagentsDir = join2(projectPath, subdir, "subagents");
278
+ try {
279
+ const subFiles = await readdir2(subagentsDir);
280
+ const subJsonl = subFiles.filter((f) => f.endsWith(".jsonl"));
281
+ for (const file of subJsonl) {
282
+ yield {
283
+ filePath: join2(subagentsDir, file),
284
+ session: `${subdir}/${file.replace(".jsonl", "")}`,
285
+ project: projectDir
286
+ };
287
+ }
288
+ } catch {
289
+ }
290
+ }
291
+ }
292
+ }
118
293
  async function* parseClaudeJsonl(filePath, context) {
119
294
  const rl = createInterface({
120
295
  input: createReadStream(filePath, { encoding: "utf-8" }),
121
296
  crlfDelay: Infinity
122
297
  });
123
298
  for await (const line of rl) {
124
- if (!line.trim()) continue;
299
+ if (!line.trim()) {
300
+ continue;
301
+ }
125
302
  try {
126
303
  const entry = JSON.parse(line);
127
304
  const text = extractUserText(entry);
128
- if (!text) continue;
305
+ if (!text) {
306
+ continue;
307
+ }
129
308
  const timestamp = extractTimestamp(entry);
130
309
  if (context.since && timestamp) {
131
310
  const ts = new Date(timestamp);
132
- if (ts < context.since) continue;
311
+ if (ts < context.since) {
312
+ continue;
313
+ }
133
314
  }
134
315
  yield {
135
316
  text,
@@ -144,12 +325,16 @@ async function* parseClaudeJsonl(filePath, context) {
144
325
  function extractUserText(entry) {
145
326
  if (entry["type"] === "user") {
146
327
  const message = entry["message"];
147
- if (!message) return null;
328
+ if (!message) {
329
+ return null;
330
+ }
148
331
  return contentToString(message["content"]);
149
332
  }
150
333
  if (entry["type"] === "human") {
151
334
  const message = entry["message"];
152
- if (!message) return null;
335
+ if (!message) {
336
+ return null;
337
+ }
153
338
  return contentToString(message["content"]);
154
339
  }
155
340
  if (entry["role"] === "user") {
@@ -158,7 +343,9 @@ function extractUserText(entry) {
158
343
  return null;
159
344
  }
160
345
  function contentToString(content) {
161
- if (typeof content === "string") return content;
346
+ if (typeof content === "string") {
347
+ return content;
348
+ }
162
349
  if (Array.isArray(content)) {
163
350
  const parts = content.filter(
164
351
  (p) => typeof p === "object" && p !== null && p.type === "text"
@@ -168,10 +355,90 @@ function contentToString(content) {
168
355
  return null;
169
356
  }
170
357
  function extractTimestamp(entry) {
171
- if (typeof entry["timestamp"] === "string") return entry["timestamp"];
172
- if (typeof entry["createdAt"] === "string") return entry["createdAt"];
358
+ if (typeof entry["timestamp"] === "string") {
359
+ return entry["timestamp"];
360
+ }
361
+ if (typeof entry["createdAt"] === "string") {
362
+ return entry["createdAt"];
363
+ }
173
364
  return null;
174
365
  }
366
+ async function* parseClaudeUsageJsonl(filePath, context) {
367
+ const rl = createInterface({
368
+ input: createReadStream(filePath, { encoding: "utf-8" }),
369
+ crlfDelay: Infinity
370
+ });
371
+ const seen = /* @__PURE__ */ new Set();
372
+ for await (const line of rl) {
373
+ if (!line.trim()) {
374
+ continue;
375
+ }
376
+ try {
377
+ const entry = JSON.parse(line);
378
+ const message = asRecord2(entry["message"]);
379
+ if (!message || entry["type"] !== "assistant" || message["role"] !== "assistant") {
380
+ continue;
381
+ }
382
+ const usage2 = asRecord2(message["usage"]);
383
+ if (!usage2) {
384
+ continue;
385
+ }
386
+ const model = stringValue(message["model"]);
387
+ const timestamp = extractTimestamp(entry) ?? void 0;
388
+ if (context.since && timestamp) {
389
+ const ts = new Date(timestamp);
390
+ if (ts < context.since) {
391
+ continue;
392
+ }
393
+ }
394
+ const inputTokens = numberValue(usage2["input_tokens"]);
395
+ const outputTokens = numberValue(usage2["output_tokens"]);
396
+ const cacheReadTokens = numberValue(usage2["cache_read_input_tokens"]);
397
+ const cacheWriteTokens = cacheCreationTokens(usage2);
398
+ if (inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens === 0) {
399
+ continue;
400
+ }
401
+ const dedupeKey = stringValue(entry["requestId"]) ?? stringValue(message["id"]) ?? `${context.session}:${timestamp ?? ""}:${model ?? "unknown"}:${inputTokens}:${outputTokens}`;
402
+ if (seen.has(dedupeKey)) {
403
+ continue;
404
+ }
405
+ seen.add(dedupeKey);
406
+ yield {
407
+ agent: "claude",
408
+ provider: "anthropic",
409
+ model,
410
+ timestamp,
411
+ session: context.session,
412
+ inputTokens,
413
+ outputTokens,
414
+ reasoningTokens: 0,
415
+ cacheReadTokens,
416
+ cacheWriteTokens
417
+ };
418
+ } catch {
419
+ }
420
+ }
421
+ }
422
+ function cacheCreationTokens(usage2) {
423
+ const explicit = numberValue(usage2["cache_creation_input_tokens"]);
424
+ if (explicit > 0) {
425
+ return explicit;
426
+ }
427
+ const cacheCreation = asRecord2(usage2["cache_creation"]);
428
+ return numberValue(cacheCreation?.["ephemeral_1h_input_tokens"]) + numberValue(cacheCreation?.["ephemeral_5m_input_tokens"]);
429
+ }
430
+ function numberValue(value) {
431
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
432
+ }
433
+ function stringValue(value) {
434
+ return typeof value === "string" && value.trim() ? value : void 0;
435
+ }
436
+ function asRecord2(value) {
437
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
438
+ return null;
439
+ }
440
+ return value;
441
+ }
175
442
 
176
443
  // src/adapters/cline.ts
177
444
  import { readdir as readdir3, readFile as readFile2, stat as stat2 } from "node:fs/promises";
@@ -185,11 +452,15 @@ function getClineTaskDirs() {
185
452
  for (const basePath of vscodePaths) {
186
453
  for (const extId of extensionIds) {
187
454
  const tasksDir = join3(basePath, extId, "tasks");
188
- if (existsSync(tasksDir)) dirs.push(tasksDir);
455
+ if (existsSync(tasksDir)) {
456
+ dirs.push(tasksDir);
457
+ }
189
458
  }
190
459
  }
191
460
  const clineStandalone = join3(homedir3(), ".cline", "data", "tasks");
192
- if (existsSync(clineStandalone)) dirs.push(clineStandalone);
461
+ if (existsSync(clineStandalone)) {
462
+ dirs.push(clineStandalone);
463
+ }
193
464
  return dirs;
194
465
  }
195
466
  function getVSCodeGlobalStoragePaths() {
@@ -232,20 +503,30 @@ function clineAdapter() {
232
503
  for (const taskId of taskIds) {
233
504
  const taskDir = join3(tasksDir, taskId);
234
505
  const taskStat = await stat2(taskDir).catch(() => null);
235
- if (!taskStat?.isDirectory()) continue;
506
+ if (!taskStat?.isDirectory()) {
507
+ continue;
508
+ }
236
509
  const historyFile = join3(taskDir, "api_conversation_history.json");
237
510
  try {
238
511
  const raw = await readFile2(historyFile, "utf-8");
239
512
  const messages = JSON.parse(raw);
240
- if (!Array.isArray(messages)) continue;
513
+ if (!Array.isArray(messages)) {
514
+ continue;
515
+ }
241
516
  for (const msg of messages) {
242
- if (msg.role !== "user") continue;
517
+ if (msg.role !== "user") {
518
+ continue;
519
+ }
243
520
  const text = extractText2(msg.content);
244
- if (!text) continue;
521
+ if (!text) {
522
+ continue;
523
+ }
245
524
  const timestamp = msg.ts ?? void 0;
246
525
  if (options?.since && timestamp) {
247
526
  const ts = new Date(timestamp);
248
- if (ts < options.since) continue;
527
+ if (ts < options.since) {
528
+ continue;
529
+ }
249
530
  }
250
531
  yield {
251
532
  text,
@@ -260,7 +541,9 @@ function clineAdapter() {
260
541
  };
261
542
  }
262
543
  function extractText2(content) {
263
- if (typeof content === "string") return content;
544
+ if (typeof content === "string") {
545
+ return content;
546
+ }
264
547
  if (Array.isArray(content)) {
265
548
  const parts = content.filter(
266
549
  (p) => typeof p === "object" && p !== null && p.type === "text" && typeof p.text === "string"
@@ -281,11 +564,21 @@ function codexAdapter() {
281
564
  return {
282
565
  name: "codex",
283
566
  async *messages(options) {
284
- yield* walkCodexSessions(CODEX_SESSIONS_DIR, options);
567
+ for await (const file of discoverCodexSessionFiles(CODEX_SESSIONS_DIR)) {
568
+ yield* parseCodexJsonl(file.filePath, { session: file.session, since: options?.since });
569
+ }
570
+ },
571
+ async *usage(options) {
572
+ for await (const file of discoverCodexSessionFiles(CODEX_SESSIONS_DIR)) {
573
+ yield* parseCodexUsageJsonl(file.filePath, {
574
+ session: file.session,
575
+ since: options?.since
576
+ });
577
+ }
285
578
  }
286
579
  };
287
580
  }
288
- async function* walkCodexSessions(dir, options) {
581
+ async function* discoverCodexSessionFiles(dir) {
289
582
  let entries;
290
583
  try {
291
584
  entries = await readdir4(dir);
@@ -296,10 +589,9 @@ async function* walkCodexSessions(dir, options) {
296
589
  const fullPath = join4(dir, entry);
297
590
  const entryStat = await stat3(fullPath);
298
591
  if (entryStat.isDirectory()) {
299
- yield* walkCodexSessions(fullPath, options);
592
+ yield* discoverCodexSessionFiles(fullPath);
300
593
  } else if (entry.endsWith(".jsonl")) {
301
- const session = entry.replace(".jsonl", "");
302
- yield* parseCodexJsonl(fullPath, { session, since: options?.since });
594
+ yield { filePath: fullPath, session: entry.replace(".jsonl", "") };
303
595
  }
304
596
  }
305
597
  }
@@ -309,19 +601,33 @@ async function* parseCodexJsonl(filePath, context) {
309
601
  crlfDelay: Infinity
310
602
  });
311
603
  for await (const line of rl) {
312
- if (!line.trim()) continue;
604
+ if (!line.trim()) {
605
+ continue;
606
+ }
313
607
  try {
314
608
  const entry = JSON.parse(line);
315
- if (entry.type !== "response_item") continue;
609
+ if (entry.type !== "response_item") {
610
+ continue;
611
+ }
316
612
  const payload = entry.payload;
317
- if (!payload || payload.role !== "user") continue;
613
+ if (!payload || payload.role !== "user") {
614
+ continue;
615
+ }
318
616
  const text = extractText3(payload.content);
319
- if (!text) continue;
320
- if (text.startsWith("<environment_context>")) continue;
321
- if (text.startsWith("<permissions instructions>")) continue;
617
+ if (!text) {
618
+ continue;
619
+ }
620
+ if (text.startsWith("<environment_context>")) {
621
+ continue;
622
+ }
623
+ if (text.startsWith("<permissions instructions>")) {
624
+ continue;
625
+ }
322
626
  if (context.since && entry.timestamp) {
323
627
  const ts = new Date(entry.timestamp);
324
- if (ts < context.since) continue;
628
+ if (ts < context.since) {
629
+ continue;
630
+ }
325
631
  }
326
632
  yield {
327
633
  text,
@@ -333,108 +639,979 @@ async function* parseCodexJsonl(filePath, context) {
333
639
  }
334
640
  }
335
641
  function extractText3(content) {
336
- if (!Array.isArray(content)) return null;
642
+ if (!Array.isArray(content)) {
643
+ return null;
644
+ }
337
645
  const parts = content.filter(
338
646
  (p) => typeof p === "object" && p !== null && p.type === "input_text" && typeof p.text === "string"
339
647
  ).map((p) => p.text);
340
648
  return parts.length > 0 ? parts.join(" ") : null;
341
649
  }
650
+ async function* parseCodexUsageJsonl(filePath, context) {
651
+ const rl = createInterface2({
652
+ input: createReadStream2(filePath, { encoding: "utf-8" }),
653
+ crlfDelay: Infinity
654
+ });
655
+ let model;
656
+ let previousTotal = null;
657
+ for await (const line of rl) {
658
+ if (!line.trim()) {
659
+ continue;
660
+ }
661
+ try {
662
+ const entry = JSON.parse(line);
663
+ const payload = asRecord3(entry["payload"]);
664
+ if (entry["type"] === "turn_context") {
665
+ model = stringValue2(payload?.["model"]) ?? model;
666
+ continue;
667
+ }
668
+ if (entry["type"] !== "event_msg" || payload?.["type"] !== "token_count") {
669
+ continue;
670
+ }
671
+ const info = asRecord3(payload["info"]);
672
+ const total = parseCodexTokenUsage(info?.["total_token_usage"]);
673
+ if (!total) {
674
+ continue;
675
+ }
676
+ const delta = previousTotal ? subtractCodexUsage(total, previousTotal) : total;
677
+ previousTotal = total;
678
+ if (!hasPositiveUsage(delta)) {
679
+ continue;
680
+ }
681
+ const timestamp = stringValue2(entry["timestamp"]);
682
+ if (context.since && timestamp) {
683
+ const ts = new Date(timestamp);
684
+ if (ts < context.since) {
685
+ continue;
686
+ }
687
+ }
688
+ const reasoningTokens = Math.min(delta.reasoningOutputTokens, delta.outputTokens);
689
+ yield {
690
+ agent: "codex",
691
+ provider: "openai",
692
+ model,
693
+ timestamp,
694
+ session: context.session,
695
+ inputTokens: Math.max(delta.inputTokens - delta.cachedInputTokens, 0),
696
+ outputTokens: Math.max(delta.outputTokens - reasoningTokens, 0),
697
+ reasoningTokens,
698
+ cacheReadTokens: delta.cachedInputTokens,
699
+ cacheWriteTokens: 0
700
+ };
701
+ } catch {
702
+ }
703
+ }
704
+ }
705
+ function parseCodexTokenUsage(value) {
706
+ const usage2 = asRecord3(value);
707
+ if (!usage2) {
708
+ return null;
709
+ }
710
+ const parsed = {
711
+ inputTokens: numberValue2(usage2["input_tokens"]),
712
+ cachedInputTokens: numberValue2(usage2["cached_input_tokens"]),
713
+ outputTokens: numberValue2(usage2["output_tokens"]),
714
+ reasoningOutputTokens: numberValue2(usage2["reasoning_output_tokens"]),
715
+ totalTokens: numberValue2(usage2["total_tokens"])
716
+ };
717
+ return hasPositiveUsage(parsed) ? parsed : null;
718
+ }
719
+ function subtractCodexUsage(current, previous) {
720
+ return {
721
+ inputTokens: Math.max(current.inputTokens - previous.inputTokens, 0),
722
+ cachedInputTokens: Math.max(current.cachedInputTokens - previous.cachedInputTokens, 0),
723
+ outputTokens: Math.max(current.outputTokens - previous.outputTokens, 0),
724
+ reasoningOutputTokens: Math.max(
725
+ current.reasoningOutputTokens - previous.reasoningOutputTokens,
726
+ 0
727
+ ),
728
+ totalTokens: Math.max(current.totalTokens - previous.totalTokens, 0)
729
+ };
730
+ }
731
+ function hasPositiveUsage(usage2) {
732
+ return usage2.inputTokens + usage2.cachedInputTokens + usage2.outputTokens + usage2.totalTokens > 0;
733
+ }
734
+ function numberValue2(value) {
735
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
736
+ }
737
+ function stringValue2(value) {
738
+ return typeof value === "string" && value.trim() ? value : void 0;
739
+ }
740
+ function asRecord3(value) {
741
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
742
+ return null;
743
+ }
744
+ return value;
745
+ }
342
746
 
343
- // src/adapters/opencode.ts
747
+ // src/adapters/cursor.ts
344
748
  import { existsSync as existsSync2 } from "node:fs";
749
+ import { readdir as readdir5 } from "node:fs/promises";
345
750
  import { homedir as homedir5 } from "node:os";
346
751
  import { join as join5 } from "node:path";
347
- function getOpencodeDatabasePath() {
348
- const xdgPath = join5(
349
- process.env["XDG_DATA_HOME"] ?? join5(homedir5(), ".local", "share"),
350
- "opencode",
351
- "opencode.db"
352
- );
353
- if (existsSync2(xdgPath)) return xdgPath;
354
- if (process.platform === "darwin") {
355
- const macPath = join5(
356
- homedir5(),
357
- "Library",
358
- "Application Support",
359
- "opencode",
360
- "opencode.db"
361
- );
362
- if (existsSync2(macPath)) return macPath;
363
- }
364
- return null;
365
- }
366
- function opencodeAdapter() {
752
+ var CANDIDATE_KEY_PREFIXES = [
753
+ "bubbleId:",
754
+ "composerData:",
755
+ "composer.composerData",
756
+ "aiService.prompts",
757
+ "aiService.generations",
758
+ "workbench.panel.composerChatViewPane."
759
+ ];
760
+ var STATE_TABLES = ["ItemTable", "cursorDiskKV"];
761
+ function cursorAdapter() {
367
762
  return {
368
- name: "opencode",
763
+ name: "cursor",
369
764
  async *messages(options) {
370
- const dbPath = getOpencodeDatabasePath();
371
- if (!dbPath) return;
372
- let db;
373
- try {
374
- const BetterSqlite3 = await import("better-sqlite3");
375
- const Ctor = BetterSqlite3.default ?? BetterSqlite3;
376
- db = new Ctor(dbPath, { readonly: true });
377
- } catch {
378
- console.warn(
379
- "devrage: better-sqlite3 not available, skipping OpenCode sessions"
380
- );
381
- return;
765
+ const stores = await discoverCursorStateStores();
766
+ for (const store of stores) {
767
+ yield* parseCursorStore(store, options);
382
768
  }
383
- try {
384
- yield* queryUserMessages(db, options);
385
- } finally {
386
- db.close();
769
+ },
770
+ async *usage(options) {
771
+ const stores = await discoverCursorStateStores();
772
+ for (const store of stores) {
773
+ yield* parseCursorUsageStore(store, options);
387
774
  }
388
775
  }
389
776
  };
390
777
  }
391
- function* queryUserMessages(db, options) {
392
- let query = `
393
- SELECT
394
- m.session_id,
395
- m.time_created,
396
- json_extract(p.data, '$.text') as text
397
- FROM message m
398
- JOIN part p ON p.message_id = m.id
399
- WHERE json_extract(m.data, '$.role') = 'user'
400
- AND json_extract(p.data, '$.type') = 'text'
401
- `;
402
- if (options?.since) {
403
- const sinceMs = options.since.getTime();
404
- query += ` AND m.time_created >= ${sinceMs}`;
405
- }
406
- query += ` ORDER BY m.time_created ASC`;
407
- const rows = db.prepare(query).all();
408
- for (const row of rows) {
409
- if (!row.text || !row.text.trim()) continue;
410
- yield {
411
- text: row.text,
412
- timestamp: new Date(row.time_created).toISOString(),
413
- session: row.session_id
414
- };
778
+ async function discoverCursorStateStores() {
779
+ const stores = [];
780
+ const seen = /* @__PURE__ */ new Set();
781
+ for (const userDir of getCursorUserDirs()) {
782
+ if (!existsSync2(userDir)) {
783
+ continue;
784
+ }
785
+ const globalState = join5(userDir, "globalStorage", "state.vscdb");
786
+ if (existsSync2(globalState) && !seen.has(globalState)) {
787
+ seen.add(globalState);
788
+ stores.push({ path: globalState, scope: "global" });
789
+ }
790
+ const workspaceRoot = join5(userDir, "workspaceStorage");
791
+ let workspaceIds = [];
792
+ try {
793
+ workspaceIds = await readdir5(workspaceRoot);
794
+ } catch {
795
+ continue;
796
+ }
797
+ for (const workspaceId of workspaceIds) {
798
+ const statePath = join5(workspaceRoot, workspaceId, "state.vscdb");
799
+ if (existsSync2(statePath) && !seen.has(statePath)) {
800
+ seen.add(statePath);
801
+ stores.push({ path: statePath, scope: "workspace", project: workspaceId });
802
+ }
803
+ }
415
804
  }
805
+ return stores;
416
806
  }
417
-
418
- // src/adapters/zed.ts
419
- import { readdir as readdir5, readFile as readFile3 } from "node:fs/promises";
420
- import { existsSync as existsSync3 } from "node:fs";
421
- import { homedir as homedir6 } from "node:os";
422
- import { join as join6 } from "node:path";
423
- function getZedPaths() {
424
- if (process.platform === "darwin") {
425
- const base2 = join6(homedir6(), "Library", "Application Support", "Zed");
426
- return {
427
- conversations: join6(base2, "conversations"),
428
- db: join6(base2, "db")
429
- };
430
- }
431
- const base = join6(
807
+ function getCursorUserDirs() {
808
+ return uniqueStrings([
809
+ join5(homedir5(), "Library", "Application Support", "Cursor", "User"),
810
+ join5(process.env["XDG_CONFIG_HOME"] ?? join5(homedir5(), ".config"), "Cursor", "User"),
811
+ join5(process.env["APPDATA"] ?? join5(homedir5(), "AppData", "Roaming"), "Cursor", "User")
812
+ ]);
813
+ }
814
+ async function* parseCursorStore(store, options) {
815
+ const db = await openCursorDb(store.path);
816
+ if (!db) {
817
+ return;
818
+ }
819
+ try {
820
+ const rows = readStateRows(db);
821
+ const seen = /* @__PURE__ */ new Set();
822
+ for (const row of rows) {
823
+ if (!isCandidateKey(row.key)) {
824
+ continue;
825
+ }
826
+ const parsed = parseJsonValue(row.value);
827
+ if (parsed === void 0) {
828
+ continue;
829
+ }
830
+ for (const message of extractCursorMessages(parsed, row.key)) {
831
+ const text = message.text.trim();
832
+ if (!isLikelyMessageText(text)) {
833
+ continue;
834
+ }
835
+ if (options?.since && message.timestamp) {
836
+ const timestamp = new Date(message.timestamp);
837
+ if (Number.isFinite(timestamp.getTime()) && timestamp < options.since) {
838
+ continue;
839
+ }
840
+ }
841
+ const session = message.session ?? `${store.scope}:${row.key}`;
842
+ const dedupeKey = `${session}\0${message.timestamp ?? ""}\0${text}`;
843
+ if (seen.has(dedupeKey)) {
844
+ continue;
845
+ }
846
+ seen.add(dedupeKey);
847
+ yield {
848
+ text,
849
+ timestamp: message.timestamp,
850
+ session,
851
+ project: store.project
852
+ };
853
+ }
854
+ }
855
+ } finally {
856
+ db.close();
857
+ }
858
+ }
859
+ async function* parseCursorUsageStore(store, options) {
860
+ const db = await openCursorDb(store.path);
861
+ if (!db) {
862
+ return;
863
+ }
864
+ try {
865
+ const rows = readStateRows(db);
866
+ const composerModels = collectComposerModels(rows);
867
+ const seen = /* @__PURE__ */ new Set();
868
+ for (const row of rows) {
869
+ if (!row.key.startsWith("bubbleId:")) {
870
+ continue;
871
+ }
872
+ const parsed = parseJsonValue(row.value);
873
+ const usage2 = extractCursorBubbleUsage(parsed, row.key, composerModels);
874
+ if (!usage2) {
875
+ continue;
876
+ }
877
+ if (options?.since && usage2.timestamp) {
878
+ const timestamp = new Date(usage2.timestamp);
879
+ if (Number.isFinite(timestamp.getTime()) && timestamp < options.since) {
880
+ continue;
881
+ }
882
+ }
883
+ const dedupeKey = `${usage2.session ?? ""}\0${usage2.timestamp ?? ""}\0${usage2.model ?? ""}\0${usage2.inputTokens}\0${usage2.outputTokens}`;
884
+ if (seen.has(dedupeKey)) {
885
+ continue;
886
+ }
887
+ seen.add(dedupeKey);
888
+ yield usage2;
889
+ }
890
+ } finally {
891
+ db.close();
892
+ }
893
+ }
894
+ async function openCursorDb(dbPath) {
895
+ try {
896
+ const BetterSqlite3 = await import("better-sqlite3");
897
+ const Ctor = BetterSqlite3.default ?? BetterSqlite3;
898
+ return new Ctor(
899
+ dbPath,
900
+ { readonly: true }
901
+ );
902
+ } catch {
903
+ return null;
904
+ }
905
+ }
906
+ function readStateRows(db) {
907
+ const rows = [];
908
+ try {
909
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all();
910
+ const availableTables = new Set(tables.map((table) => table.name));
911
+ for (const table of STATE_TABLES) {
912
+ if (!availableTables.has(table)) {
913
+ continue;
914
+ }
915
+ const columns = db.prepare(`PRAGMA table_info("${table}")`).all();
916
+ const columnNames = new Set(columns.map((column) => column.name));
917
+ if (!columnNames.has("key") || !columnNames.has("value")) {
918
+ continue;
919
+ }
920
+ const tableRows = db.prepare(`SELECT key, value FROM "${table}"`).all();
921
+ rows.push(...tableRows);
922
+ }
923
+ } catch {
924
+ return rows;
925
+ }
926
+ return rows;
927
+ }
928
+ function isCandidateKey(key) {
929
+ return CANDIDATE_KEY_PREFIXES.some((prefix) => key === prefix || key.startsWith(prefix));
930
+ }
931
+ function parseJsonValue(value) {
932
+ const raw = decodeStateValue(value);
933
+ if (!raw) {
934
+ return void 0;
935
+ }
936
+ try {
937
+ const parsed = JSON.parse(raw);
938
+ if (typeof parsed !== "string") {
939
+ return parsed;
940
+ }
941
+ const trimmed = parsed.trim();
942
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
943
+ return parsed;
944
+ }
945
+ try {
946
+ return JSON.parse(trimmed);
947
+ } catch {
948
+ return parsed;
949
+ }
950
+ } catch {
951
+ return void 0;
952
+ }
953
+ }
954
+ function decodeStateValue(value) {
955
+ if (typeof value === "string") {
956
+ return value;
957
+ }
958
+ if (Buffer.isBuffer(value)) {
959
+ return value.toString("utf-8");
960
+ }
961
+ return null;
962
+ }
963
+ function extractCursorMessages(root, rowKey) {
964
+ if (rowKey.startsWith("bubbleId:")) {
965
+ const message = extractCursorBubbleMessage(root, rowKey);
966
+ return message ? [message] : [];
967
+ }
968
+ const messages = [];
969
+ collectRoleMessages(root, messages);
970
+ if (rowKey.startsWith("aiService.prompts") || rowKey.startsWith("aiService.generations")) {
971
+ collectPromptMessages(root, messages);
972
+ }
973
+ return uniqueMessages(messages);
974
+ }
975
+ function extractCursorBubbleMessage(root, rowKey) {
976
+ const record = asRecord4(root);
977
+ if (!record || numberValue3(record["type"]) !== 1) {
978
+ return null;
979
+ }
980
+ const text = firstTextField(record, ["text", "richText"]);
981
+ if (!text) {
982
+ return null;
983
+ }
984
+ return {
985
+ text,
986
+ timestamp: extractTimestamp2(record),
987
+ session: cursorBubbleSession(rowKey) ?? extractSession(record)
988
+ };
989
+ }
990
+ function collectComposerModels(rows) {
991
+ const models = /* @__PURE__ */ new Map();
992
+ for (const row of rows) {
993
+ if (!row.key.startsWith("composerData:")) {
994
+ continue;
995
+ }
996
+ const parsed = parseJsonValue(row.value);
997
+ const record = asRecord4(parsed);
998
+ if (!record) {
999
+ continue;
1000
+ }
1001
+ const composerId = stringValue3(record["composerId"]) ?? row.key.slice("composerData:".length);
1002
+ const model = extractCursorModel(record);
1003
+ if (composerId && model) {
1004
+ models.set(composerId, model);
1005
+ }
1006
+ }
1007
+ return models;
1008
+ }
1009
+ function extractCursorBubbleUsage(root, rowKey, composerModels) {
1010
+ const record = asRecord4(root);
1011
+ if (!record || numberValue3(record["type"]) !== 2) {
1012
+ return null;
1013
+ }
1014
+ const tokenCount = asRecord4(record["tokenCount"]);
1015
+ if (!tokenCount) {
1016
+ return null;
1017
+ }
1018
+ const inputTokens = numberValue3(tokenCount["inputTokens"] ?? tokenCount["input"]);
1019
+ const outputTokens = numberValue3(tokenCount["outputTokens"] ?? tokenCount["output"]);
1020
+ const cacheReadTokens = numberValue3(tokenCount["cacheReadTokens"] ?? tokenCount["cacheRead"]);
1021
+ const cacheWriteTokens = numberValue3(tokenCount["cacheWriteTokens"] ?? tokenCount["cacheWrite"]);
1022
+ if (inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens === 0) {
1023
+ return null;
1024
+ }
1025
+ const composerId = cursorBubbleSession(rowKey);
1026
+ const model = extractCursorModel(record) ?? (composerId ? composerModels.get(composerId) : void 0);
1027
+ return {
1028
+ agent: "cursor",
1029
+ model,
1030
+ timestamp: extractTimestamp2(record),
1031
+ session: composerId ?? extractSession(record),
1032
+ inputTokens,
1033
+ outputTokens,
1034
+ reasoningTokens: numberValue3(tokenCount["reasoningTokens"] ?? tokenCount["reasoning"]),
1035
+ cacheReadTokens,
1036
+ cacheWriteTokens
1037
+ };
1038
+ }
1039
+ function extractCursorModel(record) {
1040
+ const direct = firstStringField(record, ["model", "modelName", "modelId"]);
1041
+ if (direct) {
1042
+ return direct;
1043
+ }
1044
+ for (const field of ["modelInfo", "modelConfig"]) {
1045
+ const modelRecord = asRecord4(record[field]);
1046
+ if (!modelRecord) {
1047
+ continue;
1048
+ }
1049
+ const nested = firstStringField(modelRecord, ["modelName", "modelId", "id", "name"]);
1050
+ if (nested && nested !== "default") {
1051
+ return nested;
1052
+ }
1053
+ const selected = modelRecord["selectedModels"];
1054
+ if (Array.isArray(selected)) {
1055
+ for (const item of selected) {
1056
+ const itemRecord = asRecord4(item);
1057
+ const selectedModel = itemRecord ? firstStringField(itemRecord, ["modelId", "modelName", "id", "name"]) : void 0;
1058
+ if (selectedModel && selectedModel !== "default") {
1059
+ return selectedModel;
1060
+ }
1061
+ }
1062
+ }
1063
+ if (nested) {
1064
+ return nested;
1065
+ }
1066
+ }
1067
+ return void 0;
1068
+ }
1069
+ function cursorBubbleSession(rowKey) {
1070
+ const [, composerId] = rowKey.split(":");
1071
+ return composerId?.trim() || void 0;
1072
+ }
1073
+ function collectRoleMessages(value, messages, inheritedSession, depth = 0) {
1074
+ if (depth > 12) {
1075
+ return;
1076
+ }
1077
+ if (Array.isArray(value)) {
1078
+ for (const item of value) {
1079
+ collectRoleMessages(item, messages, inheritedSession, depth + 1);
1080
+ }
1081
+ return;
1082
+ }
1083
+ const record = asRecord4(value);
1084
+ if (!record) {
1085
+ return;
1086
+ }
1087
+ const session = extractSession(record) ?? inheritedSession;
1088
+ if (isUserAuthored(record)) {
1089
+ const text = extractMessageText(record);
1090
+ if (text) {
1091
+ messages.push({ text, timestamp: extractTimestamp2(record), session });
1092
+ }
1093
+ }
1094
+ for (const child of Object.values(record)) {
1095
+ if (typeof child === "object" && child !== null) {
1096
+ collectRoleMessages(child, messages, session, depth + 1);
1097
+ }
1098
+ }
1099
+ }
1100
+ function collectPromptMessages(value, messages, inheritedSession, depth = 0) {
1101
+ if (depth > 12) {
1102
+ return;
1103
+ }
1104
+ if (typeof value === "string") {
1105
+ messages.push({ text: value, session: inheritedSession });
1106
+ return;
1107
+ }
1108
+ if (Array.isArray(value)) {
1109
+ for (const item of value) {
1110
+ collectPromptMessages(item, messages, inheritedSession, depth + 1);
1111
+ }
1112
+ return;
1113
+ }
1114
+ const record = asRecord4(value);
1115
+ if (!record) {
1116
+ return;
1117
+ }
1118
+ const session = extractSession(record) ?? inheritedSession;
1119
+ const prompt = firstTextField(record, [
1120
+ "prompt",
1121
+ "userPrompt",
1122
+ "originalPrompt",
1123
+ "currentPrompt",
1124
+ "query",
1125
+ "input"
1126
+ ]);
1127
+ if (prompt && !isAssistantAuthored(record)) {
1128
+ messages.push({ text: prompt, timestamp: extractTimestamp2(record), session });
1129
+ }
1130
+ for (const child of Object.values(record)) {
1131
+ if (typeof child === "object" && child !== null) {
1132
+ collectPromptMessages(child, messages, session, depth + 1);
1133
+ }
1134
+ }
1135
+ }
1136
+ function isUserAuthored(record) {
1137
+ return ["role", "speaker", "sender", "author", "source", "from", "type", "kind"].some(
1138
+ (field) => actorIsUser(record[field])
1139
+ );
1140
+ }
1141
+ function isAssistantAuthored(record) {
1142
+ return ["role", "speaker", "sender", "author", "source", "from", "type", "kind"].some(
1143
+ (field) => actorIsAssistant(record[field])
1144
+ );
1145
+ }
1146
+ function actorIsUser(value) {
1147
+ const actor = actorString(value);
1148
+ return actor === "user" || actor === "human" || actor === "usermessage";
1149
+ }
1150
+ function actorIsAssistant(value) {
1151
+ const actor = actorString(value);
1152
+ return actor === "assistant" || actor === "ai" || actor === "assistantmessage";
1153
+ }
1154
+ function actorString(value) {
1155
+ if (typeof value === "string") {
1156
+ return value.toLowerCase().replace(/[^a-z]/g, "");
1157
+ }
1158
+ const record = asRecord4(value);
1159
+ if (!record) {
1160
+ return null;
1161
+ }
1162
+ for (const field of ["role", "type", "name"]) {
1163
+ if (typeof record[field] === "string") {
1164
+ return record[field].toLowerCase().replace(/[^a-z]/g, "");
1165
+ }
1166
+ }
1167
+ return null;
1168
+ }
1169
+ function extractMessageText(record) {
1170
+ return firstTextField(record, ["text", "content", "message", "prompt", "query", "input"]);
1171
+ }
1172
+ function firstTextField(record, fields) {
1173
+ for (const field of fields) {
1174
+ const text = contentToText(record[field]);
1175
+ if (text) {
1176
+ return text;
1177
+ }
1178
+ }
1179
+ return null;
1180
+ }
1181
+ function firstStringField(record, fields) {
1182
+ for (const field of fields) {
1183
+ const value = stringValue3(record[field]);
1184
+ if (value) {
1185
+ return value;
1186
+ }
1187
+ }
1188
+ return void 0;
1189
+ }
1190
+ function contentToText(value) {
1191
+ if (typeof value === "string") {
1192
+ return value;
1193
+ }
1194
+ if (Array.isArray(value)) {
1195
+ const parts = value.map(contentToText).filter((part) => Boolean(part));
1196
+ return parts.length > 0 ? parts.join(" ") : null;
1197
+ }
1198
+ const record = asRecord4(value);
1199
+ if (!record) {
1200
+ return null;
1201
+ }
1202
+ return firstTextField(record, ["text", "content", "message", "value"]);
1203
+ }
1204
+ function extractTimestamp2(record) {
1205
+ for (const field of ["timestamp", "createdAt", "updatedAt", "time", "created", "date", "ts"]) {
1206
+ const timestamp = normalizeTimestamp(record[field]);
1207
+ if (timestamp) {
1208
+ return timestamp;
1209
+ }
1210
+ }
1211
+ return void 0;
1212
+ }
1213
+ function normalizeTimestamp(value) {
1214
+ if (typeof value === "string") {
1215
+ const date = new Date(value);
1216
+ return Number.isFinite(date.getTime()) ? date.toISOString() : void 0;
1217
+ }
1218
+ if (typeof value === "number" && Number.isFinite(value)) {
1219
+ const milliseconds = value > 1e12 ? value : value * 1e3;
1220
+ const date = new Date(milliseconds);
1221
+ return Number.isFinite(date.getTime()) ? date.toISOString() : void 0;
1222
+ }
1223
+ return void 0;
1224
+ }
1225
+ function extractSession(record) {
1226
+ for (const field of ["conversationId", "composerId", "sessionId", "chatId", "threadId", "id"]) {
1227
+ const value = record[field];
1228
+ if (typeof value === "string" && value.trim()) {
1229
+ return value;
1230
+ }
1231
+ }
1232
+ return void 0;
1233
+ }
1234
+ function isLikelyMessageText(text) {
1235
+ return text.length > 0 && !text.startsWith("<environment_context>");
1236
+ }
1237
+ function uniqueMessages(messages) {
1238
+ const seen = /* @__PURE__ */ new Set();
1239
+ const unique = [];
1240
+ for (const message of messages) {
1241
+ const key = `${message.session ?? ""}\0${message.timestamp ?? ""}\0${message.text}`;
1242
+ if (seen.has(key)) {
1243
+ continue;
1244
+ }
1245
+ seen.add(key);
1246
+ unique.push(message);
1247
+ }
1248
+ return unique;
1249
+ }
1250
+ function uniqueStrings(values) {
1251
+ return Array.from(new Set(values));
1252
+ }
1253
+ function numberValue3(value) {
1254
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1255
+ }
1256
+ function stringValue3(value) {
1257
+ return typeof value === "string" && value.trim() ? value : void 0;
1258
+ }
1259
+ function asRecord4(value) {
1260
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1261
+ return null;
1262
+ }
1263
+ return value;
1264
+ }
1265
+
1266
+ // src/adapters/opencode.ts
1267
+ import { existsSync as existsSync3 } from "node:fs";
1268
+ import { homedir as homedir6 } from "node:os";
1269
+ import { join as join6 } from "node:path";
1270
+ function getOpencodeDatabasePath() {
1271
+ const xdgPath = join6(
432
1272
  process.env["XDG_DATA_HOME"] ?? join6(homedir6(), ".local", "share"),
433
- "zed"
1273
+ "opencode",
1274
+ "opencode.db"
434
1275
  );
1276
+ if (existsSync3(xdgPath)) {
1277
+ return xdgPath;
1278
+ }
1279
+ if (process.platform === "darwin") {
1280
+ const macPath = join6(homedir6(), "Library", "Application Support", "opencode", "opencode.db");
1281
+ if (existsSync3(macPath)) {
1282
+ return macPath;
1283
+ }
1284
+ }
1285
+ return null;
1286
+ }
1287
+ function opencodeAdapter() {
1288
+ return {
1289
+ name: "opencode",
1290
+ async *messages(options) {
1291
+ const db = await openOpencodeDb();
1292
+ if (!db) {
1293
+ return;
1294
+ }
1295
+ try {
1296
+ yield* queryUserMessages(db, options);
1297
+ } finally {
1298
+ db.close();
1299
+ }
1300
+ },
1301
+ async *usage(options) {
1302
+ const db = await openOpencodeDb();
1303
+ if (!db) {
1304
+ return;
1305
+ }
1306
+ try {
1307
+ yield* queryUsageRecords(db, options);
1308
+ } finally {
1309
+ db.close();
1310
+ }
1311
+ }
1312
+ };
1313
+ }
1314
+ async function openOpencodeDb() {
1315
+ const dbPath = getOpencodeDatabasePath();
1316
+ if (!dbPath) {
1317
+ return null;
1318
+ }
1319
+ try {
1320
+ const BetterSqlite3 = await import("better-sqlite3");
1321
+ const Ctor = BetterSqlite3.default ?? BetterSqlite3;
1322
+ return new Ctor(
1323
+ dbPath,
1324
+ { readonly: true }
1325
+ );
1326
+ } catch {
1327
+ console.warn("devrage: better-sqlite3 not available, skipping OpenCode sessions");
1328
+ return null;
1329
+ }
1330
+ }
1331
+ function* queryUserMessages(db, options) {
1332
+ let query = `
1333
+ SELECT
1334
+ m.session_id,
1335
+ m.time_created,
1336
+ json_extract(p.data, '$.text') as text
1337
+ FROM message m
1338
+ JOIN part p ON p.message_id = m.id
1339
+ WHERE json_extract(m.data, '$.role') = 'user'
1340
+ AND json_extract(p.data, '$.type') = 'text'
1341
+ `;
1342
+ const params = [];
1343
+ if (options?.since) {
1344
+ query += ` AND m.time_created >= ?`;
1345
+ params.push(options.since.getTime());
1346
+ }
1347
+ query += ` ORDER BY m.time_created ASC`;
1348
+ const rows = db.prepare(query).all(...params);
1349
+ for (const row of rows) {
1350
+ if (!row.text || !row.text.trim()) {
1351
+ continue;
1352
+ }
1353
+ yield {
1354
+ text: row.text,
1355
+ timestamp: new Date(row.time_created).toISOString(),
1356
+ session: row.session_id
1357
+ };
1358
+ }
1359
+ }
1360
+ function* queryUsageRecords(db, options) {
1361
+ let where = `WHERE json_type(data, '$.tokens') = 'object'`;
1362
+ const params = [];
1363
+ if (options?.since) {
1364
+ where += ` AND time_created >= ?`;
1365
+ params.push(options.since.getTime());
1366
+ }
1367
+ const rows = db.prepare(`
1368
+ SELECT
1369
+ session_id,
1370
+ time_created,
1371
+ COALESCE(json_extract(data, '$.providerID'), json_extract(data, '$.model.providerID')) AS provider,
1372
+ COALESCE(json_extract(data, '$.modelID'), json_extract(data, '$.model.modelID')) AS model,
1373
+ json_extract(data, '$.cost') AS billed_cost,
1374
+ json_extract(data, '$.tokens.input') AS input_tokens,
1375
+ json_extract(data, '$.tokens.output') AS output_tokens,
1376
+ json_extract(data, '$.tokens.reasoning') AS reasoning_tokens,
1377
+ json_extract(data, '$.tokens.cache.read') AS cache_read_tokens,
1378
+ json_extract(data, '$.tokens.cache.write') AS cache_write_tokens
1379
+ FROM message
1380
+ ${where}
1381
+ ORDER BY time_created ASC
1382
+ `).all(...params);
1383
+ for (const row of rows) {
1384
+ const inputTokens = numberValue4(row.input_tokens);
1385
+ const outputTokens = numberValue4(row.output_tokens);
1386
+ const reasoningTokens = numberValue4(row.reasoning_tokens);
1387
+ const cacheReadTokens = numberValue4(row.cache_read_tokens);
1388
+ const cacheWriteTokens = numberValue4(row.cache_write_tokens);
1389
+ const billedCost = numberValue4(row.billed_cost);
1390
+ if (inputTokens + outputTokens + reasoningTokens + cacheReadTokens + cacheWriteTokens === 0 && billedCost === 0) {
1391
+ continue;
1392
+ }
1393
+ yield {
1394
+ agent: "opencode",
1395
+ provider: stringValue4(row.provider),
1396
+ model: stringValue4(row.model),
1397
+ timestamp: row.time_created ? new Date(row.time_created).toISOString() : void 0,
1398
+ session: stringValue4(row.session_id),
1399
+ billedCost,
1400
+ inputTokens,
1401
+ outputTokens,
1402
+ reasoningTokens,
1403
+ cacheReadTokens,
1404
+ cacheWriteTokens
1405
+ };
1406
+ }
1407
+ }
1408
+ function numberValue4(value) {
1409
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1410
+ }
1411
+ function stringValue4(value) {
1412
+ return typeof value === "string" && value.trim() ? value : void 0;
1413
+ }
1414
+
1415
+ // src/adapters/pi.ts
1416
+ import { createReadStream as createReadStream3 } from "node:fs";
1417
+ import { readdir as readdir6, stat as stat4 } from "node:fs/promises";
1418
+ import { createInterface as createInterface3 } from "node:readline";
1419
+ import { homedir as homedir7 } from "node:os";
1420
+ import { join as join7 } from "node:path";
1421
+ var PI_SESSIONS_DIR = join7(homedir7(), ".pi", "agent", "sessions");
1422
+ function piAdapter() {
1423
+ return {
1424
+ name: "pi",
1425
+ async *messages(options) {
1426
+ yield* walkPiSessions(PI_SESSIONS_DIR, options);
1427
+ },
1428
+ async *usage(options) {
1429
+ yield* walkPiUsageSessions(PI_SESSIONS_DIR, options);
1430
+ }
1431
+ };
1432
+ }
1433
+ async function* walkPiSessions(dir, options, project) {
1434
+ let entries;
1435
+ try {
1436
+ entries = await readdir6(dir);
1437
+ } catch {
1438
+ return;
1439
+ }
1440
+ for (const entry of entries) {
1441
+ const fullPath = join7(dir, entry);
1442
+ const entryStat = await stat4(fullPath).catch(() => null);
1443
+ if (!entryStat) {
1444
+ continue;
1445
+ }
1446
+ if (entryStat.isDirectory()) {
1447
+ yield* walkPiSessions(fullPath, options, project ?? entry);
1448
+ } else if (entry.endsWith(".jsonl")) {
1449
+ const session = entry.replace(".jsonl", "");
1450
+ yield* parsePiJsonl(fullPath, { session, project, since: options?.since });
1451
+ }
1452
+ }
1453
+ }
1454
+ async function* walkPiUsageSessions(dir, options, project) {
1455
+ let entries;
1456
+ try {
1457
+ entries = await readdir6(dir);
1458
+ } catch {
1459
+ return;
1460
+ }
1461
+ for (const entry of entries) {
1462
+ const fullPath = join7(dir, entry);
1463
+ const entryStat = await stat4(fullPath).catch(() => null);
1464
+ if (!entryStat) {
1465
+ continue;
1466
+ }
1467
+ if (entryStat.isDirectory()) {
1468
+ yield* walkPiUsageSessions(fullPath, options, project ?? entry);
1469
+ } else if (entry.endsWith(".jsonl")) {
1470
+ const session = entry.replace(".jsonl", "");
1471
+ yield* parsePiUsageJsonl(fullPath, { session, project, since: options?.since });
1472
+ }
1473
+ }
1474
+ }
1475
+ async function* parsePiJsonl(filePath, context) {
1476
+ const rl = createInterface3({
1477
+ input: createReadStream3(filePath, { encoding: "utf-8" }),
1478
+ crlfDelay: Infinity
1479
+ });
1480
+ let project = context.project;
1481
+ for await (const line of rl) {
1482
+ if (!line.trim()) {
1483
+ continue;
1484
+ }
1485
+ try {
1486
+ const entry = JSON.parse(line);
1487
+ if (entry.type === "session") {
1488
+ project = entry.cwd ?? project;
1489
+ continue;
1490
+ }
1491
+ if (entry.type !== "message") {
1492
+ continue;
1493
+ }
1494
+ const message = entry.message;
1495
+ if (!message || message.role !== "user") {
1496
+ continue;
1497
+ }
1498
+ const text = contentToString2(message.content);
1499
+ if (!text) {
1500
+ continue;
1501
+ }
1502
+ const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : typeof message.timestamp === "number" ? new Date(message.timestamp).toISOString() : void 0;
1503
+ if (context.since && timestamp) {
1504
+ const ts = new Date(timestamp);
1505
+ if (ts < context.since) {
1506
+ continue;
1507
+ }
1508
+ }
1509
+ yield {
1510
+ text,
1511
+ timestamp,
1512
+ session: context.session,
1513
+ project
1514
+ };
1515
+ } catch {
1516
+ }
1517
+ }
1518
+ }
1519
+ async function* parsePiUsageJsonl(filePath, context) {
1520
+ const rl = createInterface3({
1521
+ input: createReadStream3(filePath, { encoding: "utf-8" }),
1522
+ crlfDelay: Infinity
1523
+ });
1524
+ for await (const line of rl) {
1525
+ if (!line.trim()) {
1526
+ continue;
1527
+ }
1528
+ try {
1529
+ const entry = JSON.parse(line);
1530
+ if (entry.type !== "message") {
1531
+ continue;
1532
+ }
1533
+ const message = entry.message;
1534
+ if (!message || message.role !== "assistant") {
1535
+ continue;
1536
+ }
1537
+ const usage2 = asRecord5(message.usage);
1538
+ if (!usage2) {
1539
+ continue;
1540
+ }
1541
+ const inputTokens = numberValue5(usage2["input"]);
1542
+ const outputTokens = numberValue5(usage2["output"]);
1543
+ const cacheReadTokens = numberValue5(usage2["cacheRead"]);
1544
+ const cacheWriteTokens = numberValue5(usage2["cacheWrite"]);
1545
+ if (inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens === 0) {
1546
+ continue;
1547
+ }
1548
+ const timestamp = typeof entry.timestamp === "string" ? entry.timestamp : typeof message.timestamp === "number" ? new Date(message.timestamp).toISOString() : void 0;
1549
+ if (context.since && timestamp) {
1550
+ const ts = new Date(timestamp);
1551
+ if (ts < context.since) {
1552
+ continue;
1553
+ }
1554
+ }
1555
+ const responseModel = stringValue5(message.responseModel);
1556
+ const model = responseModel ?? stringValue5(message.model);
1557
+ yield {
1558
+ agent: "pi",
1559
+ provider: responseModel?.includes("/") ? void 0 : stringValue5(message.provider),
1560
+ model,
1561
+ timestamp,
1562
+ session: context.session,
1563
+ inputTokens,
1564
+ outputTokens,
1565
+ reasoningTokens: 0,
1566
+ cacheReadTokens,
1567
+ cacheWriteTokens
1568
+ };
1569
+ } catch {
1570
+ }
1571
+ }
1572
+ }
1573
+ function contentToString2(content) {
1574
+ if (typeof content === "string") {
1575
+ return content;
1576
+ }
1577
+ if (Array.isArray(content)) {
1578
+ const parts = content.filter(
1579
+ (p) => typeof p === "object" && p !== null && p.type === "text" && typeof p.text === "string"
1580
+ ).map((p) => p.text);
1581
+ return parts.length > 0 ? parts.join(" ") : null;
1582
+ }
1583
+ return null;
1584
+ }
1585
+ function numberValue5(value) {
1586
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1587
+ }
1588
+ function stringValue5(value) {
1589
+ return typeof value === "string" && value.trim() ? value : void 0;
1590
+ }
1591
+ function asRecord5(value) {
1592
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1593
+ return null;
1594
+ }
1595
+ return value;
1596
+ }
1597
+
1598
+ // src/adapters/zed.ts
1599
+ import { readdir as readdir7, readFile as readFile3 } from "node:fs/promises";
1600
+ import { existsSync as existsSync4 } from "node:fs";
1601
+ import { homedir as homedir8 } from "node:os";
1602
+ import { join as join8 } from "node:path";
1603
+ function getZedPaths() {
1604
+ if (process.platform === "darwin") {
1605
+ const base2 = join8(homedir8(), "Library", "Application Support", "Zed");
1606
+ return {
1607
+ conversations: join8(base2, "conversations"),
1608
+ db: join8(base2, "db")
1609
+ };
1610
+ }
1611
+ const base = join8(process.env["XDG_DATA_HOME"] ?? join8(homedir8(), ".local", "share"), "zed");
435
1612
  return {
436
- conversations: join6(base, "conversations"),
437
- db: join6(base, "db")
1613
+ conversations: join8(base, "conversations"),
1614
+ db: join8(base, "db")
438
1615
  };
439
1616
  }
440
1617
  function zedAdapter() {
@@ -448,25 +1625,33 @@ function zedAdapter() {
448
1625
  };
449
1626
  }
450
1627
  async function* parseTextThreads(dir, _options) {
451
- if (!existsSync3(dir)) return;
1628
+ if (!existsSync4(dir)) {
1629
+ return;
1630
+ }
452
1631
  let files;
453
1632
  try {
454
- files = await readdir5(dir);
1633
+ files = await readdir7(dir);
455
1634
  } catch {
456
1635
  return;
457
1636
  }
458
1637
  const jsonFiles = files.filter((f) => f.endsWith(".json"));
459
1638
  for (const file of jsonFiles) {
460
- const filePath = join6(dir, file);
1639
+ const filePath = join8(dir, file);
461
1640
  const session = file.replace(".json", "");
462
1641
  try {
463
1642
  const raw = await readFile3(filePath, "utf-8");
464
1643
  const conversation = JSON.parse(raw);
465
- if (!conversation.messages || !Array.isArray(conversation.messages)) continue;
1644
+ if (!conversation.messages || !Array.isArray(conversation.messages)) {
1645
+ continue;
1646
+ }
466
1647
  for (const msg of conversation.messages) {
467
- if (msg.role !== "user") continue;
1648
+ if (msg.role !== "user") {
1649
+ continue;
1650
+ }
468
1651
  const text = typeof msg.content === "string" ? msg.content : null;
469
- if (!text) continue;
1652
+ if (!text) {
1653
+ continue;
1654
+ }
470
1655
  yield {
471
1656
  text,
472
1657
  session
@@ -477,15 +1662,19 @@ async function* parseTextThreads(dir, _options) {
477
1662
  }
478
1663
  }
479
1664
  async function* parseAgentThreads(dbDir, _options) {
480
- if (!existsSync3(dbDir)) return;
1665
+ if (!existsSync4(dbDir)) {
1666
+ return;
1667
+ }
481
1668
  let dbFiles;
482
1669
  try {
483
- const entries = await readdir5(dbDir);
1670
+ const entries = await readdir7(dbDir);
484
1671
  dbFiles = entries.filter((f) => f.endsWith(".db"));
485
1672
  } catch {
486
1673
  return;
487
1674
  }
488
- if (dbFiles.length === 0) return;
1675
+ if (dbFiles.length === 0) {
1676
+ return;
1677
+ }
489
1678
  let Database;
490
1679
  try {
491
1680
  const mod = await import("better-sqlite3");
@@ -494,13 +1683,12 @@ async function* parseAgentThreads(dbDir, _options) {
494
1683
  return;
495
1684
  }
496
1685
  for (const dbFile of dbFiles) {
497
- const dbPath = join6(dbDir, dbFile);
1686
+ const dbPath = join8(dbDir, dbFile);
498
1687
  let db;
499
1688
  try {
500
- db = new Database(
501
- dbPath,
502
- { readonly: true }
503
- );
1689
+ db = new Database(dbPath, {
1690
+ readonly: true
1691
+ });
504
1692
  } catch {
505
1693
  continue;
506
1694
  }
@@ -525,7 +1713,9 @@ async function* parseAgentThreads(dbDir, _options) {
525
1713
  let query = `SELECT "${contentCol}" as text FROM "${msgTable}" WHERE role = 'user'`;
526
1714
  const rows = db.prepare(query).all();
527
1715
  for (const row of rows) {
528
- if (!row.text?.trim()) continue;
1716
+ if (!row.text?.trim()) {
1717
+ continue;
1718
+ }
529
1719
  yield { text: row.text };
530
1720
  }
531
1721
  } catch {
@@ -539,17 +1729,17 @@ async function* parseAgentThreads(dbDir, _options) {
539
1729
  var ADAPTERS = {
540
1730
  claude: claudeAdapter,
541
1731
  codex: codexAdapter,
1732
+ cursor: cursorAdapter,
542
1733
  opencode: opencodeAdapter,
543
1734
  amp: ampAdapter,
544
1735
  cline: clineAdapter,
1736
+ pi: piAdapter,
545
1737
  zed: zedAdapter
546
1738
  };
547
1739
  function createAdapter(name) {
548
1740
  const factory = ADAPTERS[name];
549
1741
  if (!factory) {
550
- throw new Error(
551
- `unknown adapter: ${name} (available: ${Object.keys(ADAPTERS).join(", ")})`
552
- );
1742
+ throw new Error(`unknown adapter: ${name} (available: ${Object.keys(ADAPTERS).join(", ")})`);
553
1743
  }
554
1744
  return factory();
555
1745
  }
@@ -693,10 +1883,14 @@ function runPattern(_originalText, searchText, matches, seen) {
693
1883
  DEFAULT_PATTERN.lastIndex = 0;
694
1884
  let match;
695
1885
  while ((match = DEFAULT_PATTERN.exec(searchText)) !== null) {
696
- if (seen.has(match.index)) continue;
1886
+ if (seen.has(match.index)) {
1887
+ continue;
1888
+ }
697
1889
  const word = match[0].toLowerCase();
698
1890
  const entry = WORD_MAP.get(word);
699
- if (!entry) continue;
1891
+ if (!entry) {
1892
+ continue;
1893
+ }
700
1894
  seen.add(match.index);
701
1895
  matches.push({
702
1896
  word,
@@ -707,6 +1901,450 @@ function runPattern(_originalText, searchText, matches, seen) {
707
1901
  }
708
1902
  }
709
1903
 
1904
+ // src/pricing/index.ts
1905
+ import { mkdir, readFile as readFile4, writeFile } from "node:fs/promises";
1906
+ import { homedir as homedir9 } from "node:os";
1907
+ import { dirname, join as join9 } from "node:path";
1908
+ var MODELS_DEV_URL = "https://models.dev/api.json";
1909
+ var CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
1910
+ var FETCH_TIMEOUT_MS = 2e3;
1911
+ var FALLBACK_COSTS = {
1912
+ openai: {
1913
+ "gpt-5.5": {
1914
+ input: 5,
1915
+ output: 30,
1916
+ cache_read: 0.5,
1917
+ context_over_200k: { input: 10, output: 45, cache_read: 1 }
1918
+ },
1919
+ "gpt-5.5-pro": { input: 30, output: 180 },
1920
+ "gpt-5.4": { input: 2.5, output: 15, cache_read: 0.25 },
1921
+ "gpt-5.4-mini": { input: 0.75, output: 4.5, cache_read: 0.075 },
1922
+ "gpt-5.4-nano": { input: 0.2, output: 1.25, cache_read: 0.02 },
1923
+ "gpt-5.4-pro": { input: 30, output: 180 },
1924
+ "gpt-5.3-codex": { input: 1.75, output: 14, cache_read: 0.175 }
1925
+ },
1926
+ anthropic: {
1927
+ "claude-opus-4-7": { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }
1928
+ }
1929
+ };
1930
+ var PROVIDER_ALIASES = {
1931
+ anthropic: "anthropic",
1932
+ claude: "anthropic",
1933
+ openai: "openai"
1934
+ };
1935
+ async function loadPricingCatalog(options = {}) {
1936
+ const cachePath = getPricingCachePath();
1937
+ const cache = await readPricingCache(cachePath);
1938
+ const ttlMs = options.cacheTtlMs ?? CACHE_TTL_MS;
1939
+ if (!options.refresh && cache && isFresh(cache.fetchedAt, ttlMs)) {
1940
+ return {
1941
+ source: "catalog",
1942
+ fetchedAt: cache.fetchedAt,
1943
+ cachePath,
1944
+ catalog: cache.catalog
1945
+ };
1946
+ }
1947
+ try {
1948
+ const catalog = await fetchModelsDevCatalog(options.fetchTimeoutMs ?? FETCH_TIMEOUT_MS);
1949
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
1950
+ await writePricingCache(cachePath, {
1951
+ source: "models.dev",
1952
+ fetchedAt,
1953
+ schemaVersion: 1,
1954
+ catalog
1955
+ });
1956
+ return { source: "catalog", fetchedAt, cachePath, catalog };
1957
+ } catch {
1958
+ if (cache) {
1959
+ return {
1960
+ source: "stale-catalog",
1961
+ fetchedAt: cache.fetchedAt,
1962
+ cachePath,
1963
+ catalog: cache.catalog
1964
+ };
1965
+ }
1966
+ return { source: "fallback", cachePath };
1967
+ }
1968
+ }
1969
+ async function summarizeUsage(records, pricing) {
1970
+ const total = createCostAccumulator();
1971
+ const byDay = /* @__PURE__ */ new Map();
1972
+ for await (const record of records) {
1973
+ const priced = priceUsageRecord(record, pricing);
1974
+ const billedCost = record.billedCost ?? 0;
1975
+ const isUnpriced = priced.source === "stored" || priced.source === "unknown";
1976
+ addUsageToAccumulator(total, record, priced, billedCost, isUnpriced);
1977
+ const day = timestampDay(record.timestamp);
1978
+ if (day) {
1979
+ let dayAccumulator = byDay.get(day);
1980
+ if (!dayAccumulator) {
1981
+ dayAccumulator = createCostAccumulator();
1982
+ byDay.set(day, dayAccumulator);
1983
+ }
1984
+ addUsageToAccumulator(dayAccumulator, record, priced, billedCost, isUnpriced);
1985
+ }
1986
+ }
1987
+ return {
1988
+ requests: total.requests,
1989
+ estimatedCost: total.estimatedCost,
1990
+ billedCost: total.billedCost,
1991
+ unpricedRequests: total.unpricedRequests,
1992
+ inputTokens: total.inputTokens,
1993
+ outputTokens: total.outputTokens,
1994
+ reasoningTokens: total.reasoningTokens,
1995
+ cacheReadTokens: total.cacheReadTokens,
1996
+ cacheWriteTokens: total.cacheWriteTokens,
1997
+ models: sortedModels(total.byModel),
1998
+ days: Array.from(byDay.entries()).sort(([left], [right]) => left.localeCompare(right)).map(([day, bucket]) => costDaySummary(day, bucket)),
1999
+ pricing: {
2000
+ source: pricing.source,
2001
+ fetchedAt: pricing.fetchedAt
2002
+ }
2003
+ };
2004
+ }
2005
+ function getPricingCachePath() {
2006
+ if (process.env["XDG_CACHE_HOME"]) {
2007
+ return join9(process.env["XDG_CACHE_HOME"], "devrage", "models.dev.json");
2008
+ }
2009
+ if (process.platform === "darwin") {
2010
+ return join9(homedir9(), "Library", "Caches", "devrage", "models.dev.json");
2011
+ }
2012
+ if (process.platform === "win32") {
2013
+ const localAppData = process.env["LOCALAPPDATA"] ?? join9(homedir9(), "AppData", "Local");
2014
+ return join9(localAppData, "devrage", "models.dev.json");
2015
+ }
2016
+ return join9(homedir9(), ".cache", "devrage", "models.dev.json");
2017
+ }
2018
+ function createCostAccumulator() {
2019
+ return {
2020
+ requests: 0,
2021
+ estimatedCost: 0,
2022
+ billedCost: 0,
2023
+ unpricedRequests: 0,
2024
+ inputTokens: 0,
2025
+ outputTokens: 0,
2026
+ reasoningTokens: 0,
2027
+ cacheReadTokens: 0,
2028
+ cacheWriteTokens: 0,
2029
+ byModel: /* @__PURE__ */ new Map()
2030
+ };
2031
+ }
2032
+ function addUsageToAccumulator(bucket, record, priced, billedCost, isUnpriced) {
2033
+ const key = `${priced.provider ?? ""}:${priced.model}`;
2034
+ let model = bucket.byModel.get(key);
2035
+ if (!model) {
2036
+ model = {
2037
+ model: priced.model,
2038
+ provider: priced.provider,
2039
+ requests: 0,
2040
+ estimatedCost: 0,
2041
+ billedCost: 0,
2042
+ pricingSource: priced.source,
2043
+ unpricedRequests: 0,
2044
+ inputTokens: 0,
2045
+ outputTokens: 0,
2046
+ reasoningTokens: 0,
2047
+ cacheReadTokens: 0,
2048
+ cacheWriteTokens: 0
2049
+ };
2050
+ bucket.byModel.set(key, model);
2051
+ }
2052
+ model.requests += 1;
2053
+ model.estimatedCost += priced.estimatedCost;
2054
+ model.billedCost += billedCost;
2055
+ model.pricingSource = mergePricingSource(model.pricingSource, priced.source);
2056
+ model.inputTokens += record.inputTokens;
2057
+ model.outputTokens += record.outputTokens;
2058
+ model.reasoningTokens += record.reasoningTokens;
2059
+ model.cacheReadTokens += record.cacheReadTokens;
2060
+ model.cacheWriteTokens += record.cacheWriteTokens;
2061
+ bucket.requests += 1;
2062
+ bucket.estimatedCost += priced.estimatedCost;
2063
+ bucket.billedCost += billedCost;
2064
+ bucket.inputTokens += record.inputTokens;
2065
+ bucket.outputTokens += record.outputTokens;
2066
+ bucket.reasoningTokens += record.reasoningTokens;
2067
+ bucket.cacheReadTokens += record.cacheReadTokens;
2068
+ bucket.cacheWriteTokens += record.cacheWriteTokens;
2069
+ if (isUnpriced) {
2070
+ model.unpricedRequests += 1;
2071
+ bucket.unpricedRequests += 1;
2072
+ }
2073
+ }
2074
+ function costDaySummary(day, bucket) {
2075
+ return {
2076
+ day,
2077
+ requests: bucket.requests,
2078
+ estimatedCost: bucket.estimatedCost,
2079
+ billedCost: bucket.billedCost,
2080
+ unpricedRequests: bucket.unpricedRequests,
2081
+ inputTokens: bucket.inputTokens,
2082
+ outputTokens: bucket.outputTokens,
2083
+ reasoningTokens: bucket.reasoningTokens,
2084
+ cacheReadTokens: bucket.cacheReadTokens,
2085
+ cacheWriteTokens: bucket.cacheWriteTokens,
2086
+ models: sortedModels(bucket.byModel)
2087
+ };
2088
+ }
2089
+ function sortedModels(byModel) {
2090
+ return Array.from(byModel.values()).sort(
2091
+ (a, b) => b.estimatedCost - a.estimatedCost || b.requests - a.requests
2092
+ );
2093
+ }
2094
+ function timestampDay(timestamp) {
2095
+ if (!timestamp) {
2096
+ return null;
2097
+ }
2098
+ const time = new Date(timestamp).getTime();
2099
+ if (!Number.isFinite(time)) {
2100
+ return null;
2101
+ }
2102
+ return new Date(time).toISOString().slice(0, 10);
2103
+ }
2104
+ async function fetchModelsDevCatalog(timeoutMs) {
2105
+ const controller = new AbortController();
2106
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
2107
+ try {
2108
+ const response = await fetch(MODELS_DEV_URL, { signal: controller.signal });
2109
+ if (!response.ok) {
2110
+ throw new Error(`models.dev returned ${response.status}`);
2111
+ }
2112
+ const catalog = await response.json();
2113
+ if (!isModelsDevCatalog(catalog)) {
2114
+ throw new Error("models.dev response did not match expected shape");
2115
+ }
2116
+ return catalog;
2117
+ } finally {
2118
+ clearTimeout(timeout);
2119
+ }
2120
+ }
2121
+ async function readPricingCache(cachePath) {
2122
+ try {
2123
+ const raw = await readFile4(cachePath, "utf-8");
2124
+ const parsed = JSON.parse(raw);
2125
+ const cache = asRecord6(parsed);
2126
+ if (cache?.["source"] !== "models.dev" || cache["schemaVersion"] !== 1 || typeof cache["fetchedAt"] !== "string" || !isModelsDevCatalog(cache["catalog"])) {
2127
+ return null;
2128
+ }
2129
+ return {
2130
+ source: "models.dev",
2131
+ fetchedAt: cache["fetchedAt"],
2132
+ schemaVersion: 1,
2133
+ catalog: cache["catalog"]
2134
+ };
2135
+ } catch {
2136
+ return null;
2137
+ }
2138
+ }
2139
+ async function writePricingCache(cachePath, cache) {
2140
+ try {
2141
+ await mkdir(dirname(cachePath), { recursive: true });
2142
+ await writeFile(cachePath, `${JSON.stringify(cache)}
2143
+ `, "utf-8");
2144
+ } catch {
2145
+ }
2146
+ }
2147
+ function priceUsageRecord(record, catalog) {
2148
+ const resolved = resolveRates(record, catalog);
2149
+ if (!resolved) {
2150
+ return {
2151
+ provider: normalizeProvider(record.provider),
2152
+ model: normalizeModel(record.model) ?? "unknown",
2153
+ estimatedCost: 0,
2154
+ source: (record.billedCost ?? 0) > 0 ? "stored" : "unknown"
2155
+ };
2156
+ }
2157
+ const rates = selectContextRates(resolved.rates, record);
2158
+ const inputRate = rates.input ?? 0;
2159
+ const outputRate = rates.output ?? 0;
2160
+ const cacheReadRate = rates.cache_read ?? inputRate;
2161
+ const cacheWriteRate = rates.cache_write ?? inputRate;
2162
+ const outputTokens = record.outputTokens + record.reasoningTokens;
2163
+ return {
2164
+ provider: resolved.provider,
2165
+ model: resolved.model,
2166
+ estimatedCost: (record.inputTokens * inputRate + record.cacheReadTokens * cacheReadRate + record.cacheWriteTokens * cacheWriteRate + outputTokens * outputRate) / 1e6,
2167
+ source: resolved.source
2168
+ };
2169
+ }
2170
+ function resolveRates(record, pricing) {
2171
+ const candidates = modelCandidates(record.provider, record.model);
2172
+ for (const candidate of candidates) {
2173
+ if (!candidate.provider || !pricing.catalog) {
2174
+ continue;
2175
+ }
2176
+ const rates = getCatalogRates(pricing.catalog, candidate.provider, candidate.model);
2177
+ if (rates) {
2178
+ return {
2179
+ provider: candidate.provider,
2180
+ model: candidate.model,
2181
+ rates,
2182
+ source: pricing.source === "stale-catalog" ? "stale-catalog" : "catalog"
2183
+ };
2184
+ }
2185
+ }
2186
+ for (const candidate of candidates) {
2187
+ if (!candidate.provider) {
2188
+ continue;
2189
+ }
2190
+ const providerRates = FALLBACK_COSTS[candidate.provider];
2191
+ const rates = providerRates?.[candidate.model];
2192
+ if (rates) {
2193
+ return { provider: candidate.provider, model: candidate.model, rates, source: "fallback" };
2194
+ }
2195
+ }
2196
+ return null;
2197
+ }
2198
+ function modelCandidates(providerInput, modelInput) {
2199
+ const candidates = [];
2200
+ let provider = normalizeProvider(providerInput);
2201
+ let model = normalizeModel(modelInput);
2202
+ if (!model) {
2203
+ return candidates;
2204
+ }
2205
+ const prefixed = splitProviderModel(model);
2206
+ if (prefixed) {
2207
+ provider = provider ?? prefixed.provider;
2208
+ model = prefixed.model;
2209
+ }
2210
+ addCandidate(candidates, provider, model);
2211
+ addCandidate(candidates, provider, MODEL_ALIASES[model]);
2212
+ const inferred = provider ?? inferProvider(model);
2213
+ addCandidate(candidates, inferred, model);
2214
+ addCandidate(candidates, inferred, MODEL_ALIASES[model]);
2215
+ for (const fallbackProvider of Object.keys(FALLBACK_COSTS)) {
2216
+ addCandidate(candidates, fallbackProvider, model);
2217
+ addCandidate(candidates, fallbackProvider, MODEL_ALIASES[model]);
2218
+ }
2219
+ return candidates;
2220
+ }
2221
+ var MODEL_ALIASES = {
2222
+ "gpt-5.5-chat-latest": "gpt-5.5"
2223
+ };
2224
+ function addCandidate(candidates, provider, model) {
2225
+ if (!model) {
2226
+ return;
2227
+ }
2228
+ if (candidates.some((candidate) => candidate.provider === provider && candidate.model === model)) {
2229
+ return;
2230
+ }
2231
+ candidates.push({ provider, model });
2232
+ }
2233
+ function splitProviderModel(model) {
2234
+ const slash = model.indexOf("/");
2235
+ if (slash <= 0 || slash === model.length - 1) {
2236
+ return null;
2237
+ }
2238
+ const provider = normalizeProvider(model.slice(0, slash));
2239
+ const bareModel = normalizeModel(model.slice(slash + 1));
2240
+ if (!provider || !bareModel) {
2241
+ return null;
2242
+ }
2243
+ return { provider, model: bareModel };
2244
+ }
2245
+ function normalizeProvider(provider) {
2246
+ if (!provider) {
2247
+ return void 0;
2248
+ }
2249
+ const normalized = provider.trim().toLowerCase();
2250
+ return PROVIDER_ALIASES[normalized] ?? normalized;
2251
+ }
2252
+ function normalizeModel(model) {
2253
+ const normalized = model?.trim().toLowerCase();
2254
+ return normalized || void 0;
2255
+ }
2256
+ function inferProvider(model) {
2257
+ if (model.startsWith("gpt-") || /^o\d/.test(model)) {
2258
+ return "openai";
2259
+ }
2260
+ if (model.startsWith("claude-")) {
2261
+ return "anthropic";
2262
+ }
2263
+ return void 0;
2264
+ }
2265
+ function getCatalogRates(catalog, provider, model) {
2266
+ const root = asRecord6(catalog);
2267
+ const providerEntry = asRecord6(root?.[provider]);
2268
+ const models = asRecord6(providerEntry?.["models"]);
2269
+ const modelEntry = asRecord6(models?.[model]);
2270
+ return toRateTable(modelEntry?.["cost"]);
2271
+ }
2272
+ function selectContextRates(rates, record) {
2273
+ const contextTokens = record.inputTokens + record.cacheReadTokens + record.cacheWriteTokens;
2274
+ let selected = rates;
2275
+ let selectedSize = 0;
2276
+ for (const tier of rates.tiers ?? []) {
2277
+ const tierRecord = asRecord6(tier);
2278
+ const tierInfo = asRecord6(tierRecord?.["tier"]);
2279
+ const size = typeof tierInfo?.["size"] === "number" ? tierInfo["size"] : 0;
2280
+ if (tierInfo?.["type"] !== "context" || contextTokens < size || size < selectedSize) {
2281
+ continue;
2282
+ }
2283
+ const tierRates = toRateTable(tierRecord);
2284
+ if (tierRates) {
2285
+ selected = { ...rates, ...tierRates };
2286
+ selectedSize = size;
2287
+ }
2288
+ }
2289
+ if (selected === rates && rates.context_over_200k && contextTokens > 2e5) {
2290
+ selected = { ...rates, ...rates.context_over_200k };
2291
+ }
2292
+ return selected;
2293
+ }
2294
+ function toRateTable(value) {
2295
+ const record = asRecord6(value);
2296
+ if (!record) {
2297
+ return null;
2298
+ }
2299
+ const rates = {};
2300
+ const input = numberValue6(record["input"]);
2301
+ const output = numberValue6(record["output"]);
2302
+ const cacheRead = numberValue6(record["cache_read"]);
2303
+ const cacheWrite = numberValue6(record["cache_write"]);
2304
+ const contextOver200k = toRateTable(record["context_over_200k"]);
2305
+ if (input !== void 0) {
2306
+ rates.input = input;
2307
+ }
2308
+ if (output !== void 0) {
2309
+ rates.output = output;
2310
+ }
2311
+ if (cacheRead !== void 0) {
2312
+ rates.cache_read = cacheRead;
2313
+ }
2314
+ if (cacheWrite !== void 0) {
2315
+ rates.cache_write = cacheWrite;
2316
+ }
2317
+ if (Array.isArray(record["tiers"])) {
2318
+ rates.tiers = record["tiers"];
2319
+ }
2320
+ if (contextOver200k) {
2321
+ rates.context_over_200k = contextOver200k;
2322
+ }
2323
+ return rates.input !== void 0 || rates.output !== void 0 ? rates : null;
2324
+ }
2325
+ function numberValue6(value) {
2326
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2327
+ }
2328
+ function isModelsDevCatalog(value) {
2329
+ const catalog = asRecord6(value);
2330
+ const openai = asRecord6(catalog?.["openai"]);
2331
+ const anthropic = asRecord6(catalog?.["anthropic"]);
2332
+ return Boolean(asRecord6(openai?.["models"]) || asRecord6(anthropic?.["models"]));
2333
+ }
2334
+ function isFresh(fetchedAt, ttlMs) {
2335
+ const fetchedTime = new Date(fetchedAt).getTime();
2336
+ return Number.isFinite(fetchedTime) && Date.now() - fetchedTime <= ttlMs;
2337
+ }
2338
+ function mergePricingSource(left, right) {
2339
+ return left === right ? left : "mixed";
2340
+ }
2341
+ function asRecord6(value) {
2342
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2343
+ return null;
2344
+ }
2345
+ return value;
2346
+ }
2347
+
710
2348
  // src/commands/scan.ts
711
2349
  var c = {
712
2350
  reset: "\x1B[0m",
@@ -721,6 +2359,7 @@ var c = {
721
2359
  white: "\x1B[37m",
722
2360
  gray: "\x1B[90m"
723
2361
  };
2362
+ var MAX_TERMINAL_MODELS = 10;
724
2363
  var SPINNER_MESSAGES = [
725
2364
  "Tallying the damage",
726
2365
  "Reviewing your outbursts",
@@ -733,20 +2372,19 @@ var SPINNER_MESSAGES = [
733
2372
  "Auditing your language",
734
2373
  "Tabulating regrets"
735
2374
  ];
736
- function createSpinner() {
2375
+ var DAY_MS = 24 * 60 * 60 * 1e3;
2376
+ function createSpinner(messages = SPINNER_MESSAGES) {
737
2377
  let messageIdx = 0;
738
2378
  let dotCount = 0;
739
2379
  let timer = null;
740
2380
  return {
741
2381
  start() {
742
- messageIdx = Math.floor(Math.random() * SPINNER_MESSAGES.length);
2382
+ messageIdx = Math.floor(Math.random() * messages.length);
743
2383
  timer = setInterval(() => {
744
2384
  dotCount = (dotCount + 1) % 4;
745
- const msg = SPINNER_MESSAGES[messageIdx % SPINNER_MESSAGES.length];
2385
+ const msg = messages[messageIdx % messages.length];
746
2386
  const dots = ".".repeat(dotCount || 1);
747
- process.stdout.write(
748
- `\r ${c.dim}${msg}${dots}${c.reset} `
749
- );
2387
+ process.stdout.write(`\r ${c.dim}${msg}${dots}${c.reset} `);
750
2388
  }, 300);
751
2389
  },
752
2390
  update() {
@@ -770,24 +2408,106 @@ function parseArgs(args) {
770
2408
  } else if (arg === "--since" || arg === "-s") {
771
2409
  const val = args[++i];
772
2410
  if (val) {
773
- options.since = new Date(val);
774
- if (isNaN(options.since.getTime())) {
775
- console.error(`invalid date: ${val}`);
776
- process.exit(1);
777
- }
2411
+ setAbsoluteSince(options, val);
778
2412
  }
2413
+ } else if (arg === "--day" || arg === "--days") {
2414
+ const parsed = readOptionalDaysArg(args, i);
2415
+ setRelativeRange(options, parsed.days);
2416
+ if (parsed.consumed) {
2417
+ i++;
2418
+ }
2419
+ } else if (arg === "--week") {
2420
+ setRelativeRange(options, 7);
2421
+ } else if (arg === "--month") {
2422
+ setRelativeRange(options, 30);
779
2423
  } else if (arg === "--help" || arg === "-h") {
780
2424
  console.log(`devrage scan \u2014 scan sessions for profanity
781
2425
 
782
2426
  Options:
783
- --agent, -a <name> Scan only a specific agent (claude, codex, opencode, amp, cline, zed)
2427
+ --agent, -a <name> Scan only a specific agent (claude, codex, cursor, opencode, amp, cline, pi, zed)
784
2428
  --since, -s <date> Only scan messages after this date (ISO 8601)
2429
+ --day, --days [n] Only scan the last n days (default: 1)
2430
+ --week Only scan the last 7 days
2431
+ --month Only scan the last 30 days
2432
+ --help, -h Show this help`);
2433
+ process.exit(0);
2434
+ }
2435
+ }
2436
+ return options;
2437
+ }
2438
+ function parseCostArgs(args) {
2439
+ const options = {};
2440
+ for (let i = 0; i < args.length; i++) {
2441
+ const arg = args[i];
2442
+ if (arg === "--agent" || arg === "-a") {
2443
+ options.agent = args[++i];
2444
+ } else if (arg === "--refresh-prices") {
2445
+ options.refreshPrices = true;
2446
+ } else if (arg === "--since" || arg === "-s") {
2447
+ const val = args[++i];
2448
+ if (val) {
2449
+ setAbsoluteSince(options, val);
2450
+ }
2451
+ } else if (arg === "--day" || arg === "--days") {
2452
+ const parsed = readOptionalDaysArg(args, i);
2453
+ setRelativeRange(options, parsed.days);
2454
+ if (parsed.consumed) {
2455
+ i++;
2456
+ }
2457
+ } else if (arg === "--week") {
2458
+ setRelativeRange(options, 7);
2459
+ } else if (arg === "--month") {
2460
+ setRelativeRange(options, 30);
2461
+ } else if (arg === "--help" || arg === "-h") {
2462
+ console.log(`devrage cost \u2014 show API-equivalent coding agent cost
2463
+
2464
+ Usage:
2465
+ devrage cost [options]
2466
+
2467
+ Options:
2468
+ --agent, -a <name> Show only a specific agent (claude, codex, cursor, opencode, amp, pi)
2469
+ --refresh-prices Refresh models.dev pricing before estimating cost
2470
+ --since, -s <date> Only include usage after this date (ISO 8601)
2471
+ --day, --days [n] Only include the last n days (default: 1)
2472
+ --week Only include the last 7 days
2473
+ --month Only include the last 30 days
785
2474
  --help, -h Show this help`);
786
2475
  process.exit(0);
787
2476
  }
788
2477
  }
789
2478
  return options;
790
2479
  }
2480
+ function setAbsoluteSince(options, value) {
2481
+ options.since = parseDateArg(value);
2482
+ options.rangeLabel = void 0;
2483
+ }
2484
+ function setRelativeRange(options, days) {
2485
+ options.since = new Date(Date.now() - days * DAY_MS);
2486
+ options.rangeLabel = `last ${days} ${days === 1 ? "day" : "days"}`;
2487
+ }
2488
+ function readOptionalDaysArg(args, index) {
2489
+ const value = args[index + 1];
2490
+ if (!value || value.startsWith("-") && !/^-\d+$/.test(value)) {
2491
+ return { days: 1, consumed: false };
2492
+ }
2493
+ return { days: parseDaysArg(value), consumed: true };
2494
+ }
2495
+ function parseDaysArg(value) {
2496
+ const days = Number(value);
2497
+ if (!Number.isInteger(days) || days < 1) {
2498
+ console.error(`invalid days: ${value ?? ""}`);
2499
+ process.exit(1);
2500
+ }
2501
+ return days;
2502
+ }
2503
+ function parseDateArg(value) {
2504
+ const date = new Date(value);
2505
+ if (isNaN(date.getTime())) {
2506
+ console.error(`invalid date: ${value}`);
2507
+ process.exit(1);
2508
+ }
2509
+ return date;
2510
+ }
791
2511
  async function scan(args) {
792
2512
  const options = parseArgs(args);
793
2513
  const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
@@ -821,27 +2541,24 @@ async function scan(args) {
821
2541
  }
822
2542
  }
823
2543
  spinner.stop();
824
- console.log("");
825
- console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}report${c.reset}`);
826
- console.log(` ${c.dim}${"\u2500".repeat(30)}${c.reset}`);
827
- console.log("");
828
- console.log(` ${c.dim}messages scanned${c.reset} ${c.bold}${totalMessages}${c.reset}`);
829
- console.log(` ${c.dim}total swears${c.reset} ${c.bold}${c.red}${totalSwears}${c.reset}`);
830
2544
  const activeAgents = Object.entries(perAgent);
2545
+ console.log("");
2546
+ printReportHeader(options);
2547
+ printBasicOverview(totalMessages, totalSwears);
831
2548
  if (activeAgents.length > 1) {
832
2549
  console.log("");
833
- console.log(` ${c.bold}by agent${c.reset}`);
2550
+ console.log(` ${sectionTitle("agent language")}`);
834
2551
  for (const [name, stats] of activeAgents) {
835
2552
  const rate = (stats.swears / stats.messages * 100).toFixed(1);
836
2553
  console.log(
837
- ` ${c.cyan}${name.padEnd(10)}${c.reset} ${c.bold}${String(stats.swears).padStart(4)}${c.reset} ${c.dim}in ${stats.messages} messages (${rate}%)${c.reset}`
2554
+ ` ${colorText(name.padEnd(10), agentColor(name))} ${c.bold}${String(stats.swears).padStart(4)}${c.reset} ${c.dim}in ${stats.messages} messages (${rate}%)${c.reset}`
838
2555
  );
839
2556
  }
840
2557
  }
841
2558
  if (totalSwears > 0) {
842
2559
  const sorted = Object.entries(groupTally).sort(([, a], [, b]) => b - a);
843
2560
  console.log("");
844
- console.log(` ${c.bold}top words${c.reset}`);
2561
+ console.log(` ${sectionTitle("top words")}`);
845
2562
  for (const [group, count] of sorted.slice(0, 10)) {
846
2563
  const variants = variantTally[group] ?? {};
847
2564
  const variantList = Object.entries(variants).sort(([, a], [, b]) => b - a).filter(([v]) => v !== group).slice(0, 15).map(([v, cnt]) => `${c.dim}${v}${c.reset} ${cnt}`).join(`${c.dim},${c.reset} `);
@@ -857,9 +2574,518 @@ async function scan(args) {
857
2574
  console.log("");
858
2575
  }
859
2576
  }
2577
+ async function cost(args) {
2578
+ const options = parseCostArgs(args);
2579
+ const adapters = options.agent ? [createAdapter(options.agent)] : allAdapters();
2580
+ const costByAgent = {};
2581
+ const pricing = await loadPricingCatalog({ refresh: options.refreshPrices });
2582
+ for (const adapter of adapters) {
2583
+ if (!adapter.usage) {
2584
+ continue;
2585
+ }
2586
+ const summary = await summarizeUsage(adapter.usage({ since: options.since }), pricing);
2587
+ if (summary.requests > 0) {
2588
+ costByAgent[adapter.name] = summary;
2589
+ }
2590
+ }
2591
+ const totals = getCostTotals(costByAgent);
2592
+ console.log("");
2593
+ if (totals.entries.length === 0) {
2594
+ printCostCommandUnavailable(options);
2595
+ return;
2596
+ }
2597
+ const reportUrl = await writeCostHtmlReport(totals, options);
2598
+ printCostCommand(totals, options, reportUrl);
2599
+ }
2600
+ function printCostCommand(totals, options, reportUrl) {
2601
+ const modelTotals = aggregateModelCosts(totals.entries);
2602
+ printCompactHeader(options);
2603
+ printCompactTotal(totals);
2604
+ printCompactAgents(totals.entries);
2605
+ printCompactModels(modelTotals, totals.totalCost);
2606
+ console.log("");
2607
+ console.log(` ${c.dim}Report:${c.reset} ${reportUrl}`);
2608
+ console.log("");
2609
+ }
2610
+ function printCompactHeader(options) {
2611
+ const filters = [
2612
+ options.agent,
2613
+ options.rangeLabel ?? (options.since ? `since ${formatDate(options.since)}` : null)
2614
+ ].filter(Boolean);
2615
+ const suffix = filters.length > 0 ? ` ${c.dim}${filters.join(" \xB7 ")}${c.reset}` : "";
2616
+ console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}cost${c.reset}${suffix}`);
2617
+ console.log("");
2618
+ }
2619
+ function compactMeta(totals) {
2620
+ const parts = [formatRequests(totals.pricedRequests)];
2621
+ if (totals.unpricedRequests > 0) {
2622
+ parts.push(`${formatNumber(totals.unpricedRequests)} unpriced`);
2623
+ }
2624
+ return `${c.dim}${parts.join(" \xB7 ")}${c.reset}`;
2625
+ }
2626
+ function printCompactTotal(totals) {
2627
+ console.log(` ${c.bold}total${c.reset}`);
2628
+ console.log(
2629
+ ` ${c.bold}${c.green}${formatCurrency(totals.totalCost)}${c.reset} ${compactMeta(totals)}`
2630
+ );
2631
+ }
2632
+ function printCompactModels(models, totalCost) {
2633
+ if (models.length === 0) {
2634
+ return;
2635
+ }
2636
+ const visibleModels = models.slice(0, MAX_TERMINAL_MODELS);
2637
+ const maxCost = visibleModels[0]?.estimatedCost ?? 0;
2638
+ console.log("");
2639
+ console.log(` ${c.bold}models${c.reset}`);
2640
+ for (const model of visibleModels) {
2641
+ const share = totalCost > 0 ? model.estimatedCost / totalCost : 0;
2642
+ const color = modelColor(model);
2643
+ console.log(
2644
+ ` ${colorText(clip(model.model, 27).padEnd(27), color)} ${formatCurrency(model.estimatedCost).padStart(9)} ${c.dim}${formatPercent(share).padStart(6)}${c.reset} ${renderBar(model.estimatedCost, maxCost, 16, color)}`
2645
+ );
2646
+ }
2647
+ }
2648
+ function printCompactAgents(entries) {
2649
+ console.log("");
2650
+ console.log(` ${c.bold}agents${c.reset}`);
2651
+ for (const [name, stats] of entries.sort(
2652
+ ([, left], [, right]) => right.estimatedCost - left.estimatedCost
2653
+ )) {
2654
+ const color = agentColor(name);
2655
+ console.log(
2656
+ ` ${colorText(name.padEnd(10), color)} ${colorText(formatCurrency(stats.estimatedCost).padStart(9), color)} ${c.dim}${formatRequests(stats.requests).padStart(12)}${c.reset}`
2657
+ );
2658
+ }
2659
+ }
2660
+ function printCostCommandUnavailable(options) {
2661
+ printCompactHeader(options);
2662
+ console.log(` ${c.gray}no local usage found${c.reset}`);
2663
+ console.log("");
2664
+ }
2665
+ async function writeCostHtmlReport(totals, options) {
2666
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
2667
+ const reportPath = join10(
2668
+ dirname2(getPricingCachePath()),
2669
+ `cost-report-${safeTimestamp(generatedAt)}.html`
2670
+ );
2671
+ await mkdir2(dirname2(reportPath), { recursive: true });
2672
+ await writeFile2(
2673
+ reportPath,
2674
+ renderCostHtmlReport(costReportData(totals, options, generatedAt)),
2675
+ "utf-8"
2676
+ );
2677
+ return pathToFileURL(reportPath).href;
2678
+ }
2679
+ function safeTimestamp(value) {
2680
+ return value.replace(/[:.]/g, "-");
2681
+ }
2682
+ function costReportData(totals, options, generatedAt) {
2683
+ return {
2684
+ generatedAt,
2685
+ scope: options.rangeLabel ?? (options.since ? `since ${formatDate(options.since)}` : "all local history"),
2686
+ totalCost: totals.totalCost,
2687
+ pricedRequests: totals.pricedRequests,
2688
+ unpricedRequests: totals.unpricedRequests,
2689
+ agents: totals.entries.map(([name, summary]) => ({
2690
+ name,
2691
+ estimatedCost: summary.estimatedCost,
2692
+ requests: summary.requests - summary.unpricedRequests,
2693
+ models: summary.models.map(costReportModel),
2694
+ days: summary.days.map((day) => ({
2695
+ day: day.day,
2696
+ estimatedCost: day.estimatedCost,
2697
+ requests: day.requests - day.unpricedRequests,
2698
+ models: day.models.map(costReportModel)
2699
+ }))
2700
+ })).sort((left, right) => right.estimatedCost - left.estimatedCost)
2701
+ };
2702
+ }
2703
+ function costReportModel(model) {
2704
+ return {
2705
+ model: model.model,
2706
+ provider: model.provider,
2707
+ estimatedCost: model.estimatedCost,
2708
+ requests: model.requests - model.unpricedRequests,
2709
+ inputTokens: model.inputTokens,
2710
+ outputTokens: model.outputTokens,
2711
+ reasoningTokens: model.reasoningTokens,
2712
+ cacheReadTokens: model.cacheReadTokens,
2713
+ cacheWriteTokens: model.cacheWriteTokens
2714
+ };
2715
+ }
2716
+ function renderCostHtmlReport(data) {
2717
+ return `<!doctype html>
2718
+ <html lang="en">
2719
+ <head>
2720
+ <meta charset="utf-8" />
2721
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
2722
+ <title>devrage cost report</title>
2723
+ <style>
2724
+ :root {
2725
+ color-scheme: dark;
2726
+ --bg: #0f1117;
2727
+ --panel: #151923;
2728
+ --panel-2: #10141c;
2729
+ --border: #283040;
2730
+ --text: #edf1f7;
2731
+ --muted: #99a3b5;
2732
+ --faint: #677184;
2733
+ --green: #55c98f;
2734
+ --purple: #b18cff;
2735
+ --blue: #75a7ff;
2736
+ --yellow: #e5b75f;
2737
+ --cyan: #62c7df;
2738
+ }
2739
+ * { box-sizing: border-box; }
2740
+ body {
2741
+ margin: 0;
2742
+ background: var(--bg);
2743
+ color: var(--text);
2744
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2745
+ line-height: 1.45;
2746
+ }
2747
+ main { width: min(1180px, calc(100vw - 32px)); margin: 0 auto; padding: 28px 0 48px; }
2748
+ header { display: flex; justify-content: space-between; gap: 20px; align-items: end; margin-bottom: 22px; }
2749
+ h1 { margin: 0; font-size: 22px; letter-spacing: -0.02em; }
2750
+ .scope { color: var(--muted); font-size: 13px; margin-top: 4px; }
2751
+ .generated { color: var(--faint); font-size: 12px; text-align: right; }
2752
+ .summary { display: grid; grid-template-columns: 1.4fr repeat(3, 1fr); gap: 12px; margin-bottom: 18px; }
2753
+ .card, .panel { border: 1px solid var(--border); background: var(--panel); }
2754
+ .card { padding: 16px; min-height: 92px; }
2755
+ .label { color: var(--muted); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; }
2756
+ .value { margin-top: 8px; font-size: 26px; font-weight: 800; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }
2757
+ .primary .value { color: var(--green); font-size: 42px; }
2758
+ .controls { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 18px 0; }
2759
+ label { display: grid; gap: 6px; color: var(--muted); font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; }
2760
+ select { width: 100%; border: 1px solid var(--border); background: var(--panel-2); color: var(--text); padding: 10px 12px; font: inherit; border-radius: 0; }
2761
+ .grid { display: grid; grid-template-columns: 1fr; gap: 14px; align-items: start; }
2762
+ .panel { min-width: 0; }
2763
+ .panel h2 { margin: 0; padding: 13px 14px; border-bottom: 1px solid var(--border); font-size: 13px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted); }
2764
+ .panel-body { padding: 14px; }
2765
+ table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
2766
+ th, td { padding: 9px 8px; border-bottom: 1px solid #202838; text-align: left; white-space: nowrap; }
2767
+ th { color: var(--faint); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; }
2768
+ td:not(:first-child), th:not(:first-child) { text-align: right; }
2769
+ tr:last-child td { border-bottom: 0; }
2770
+ .name { color: var(--text); font-weight: 650; }
2771
+ .muted { color: var(--muted); }
2772
+ .chart-wrap { min-width: 0; }
2773
+ .chart { width: 100%; min-width: 0; height: 190px; display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: clamp(1px, 0.55vw, 8px); align-items: end; overflow: hidden; }
2774
+ .bar-column { display: flex; align-items: end; min-width: 0; height: 190px; overflow: hidden; font-variant-numeric: tabular-nums; }
2775
+ .chart-empty { align-self: center; }
2776
+ .axis { position: relative; height: 24px; margin-top: 8px; border-top: 1px solid #202838; color: var(--muted); font-size: 10px; font-variant-numeric: tabular-nums; overflow: hidden; }
2777
+ .axis-tick { position: absolute; top: 6px; transform: translateX(-50%); white-space: nowrap; }
2778
+ .axis-tick.edge-start { transform: translateX(0); }
2779
+ .axis-tick.edge-end { transform: translateX(-100%); }
2780
+ .column-track { width: 100%; min-width: 0; height: 190px; display: flex; align-items: end; background: #202838; overflow: hidden; }
2781
+ .column-fill { width: 100%; min-height: 2px; background: var(--cyan); }
2782
+ .legend { display: flex; flex-wrap: wrap; gap: 10px 14px; color: var(--muted); font-size: 12px; margin-top: 12px; }
2783
+ .dot { display: inline-block; width: 9px; height: 9px; margin-right: 5px; background: var(--cyan); }
2784
+ .tooltip { position: fixed; z-index: 20; display: none; pointer-events: none; border: 1px solid var(--border); background: #0b0e14; color: var(--text); padding: 7px 9px; font-size: 12px; font-variant-numeric: tabular-nums; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.35); }
2785
+ .tooltip .sub { color: var(--muted); margin-top: 2px; }
2786
+ .green { color: var(--green); } .purple { color: var(--purple); } .blue { color: var(--blue); } .yellow { color: var(--yellow); } .cyan { color: var(--cyan); }
2787
+ .bg-green { background: var(--green); } .bg-purple { background: var(--purple); } .bg-blue { background: var(--blue); } .bg-yellow { background: var(--yellow); } .bg-cyan { background: var(--cyan); }
2788
+ @media (max-width: 900px) { header, .grid { display: block; } .summary, .controls { grid-template-columns: 1fr; } .panel { margin-top: 14px; } .generated { text-align: left; margin-top: 8px; } }
2789
+ </style>
2790
+ </head>
2791
+ <body>
2792
+ <main>
2793
+ <header>
2794
+ <div>
2795
+ <h1>devrage cost report</h1>
2796
+ <div class="scope" id="scope"></div>
2797
+ </div>
2798
+ <div class="generated" id="generated"></div>
2799
+ </header>
2800
+ <section class="summary">
2801
+ <div class="card primary"><div class="label">total</div><div class="value" id="totalCost"></div></div>
2802
+ <div class="card"><div class="label">requests</div><div class="value" id="requestCount"></div></div>
2803
+ <div class="card"><div class="label">models</div><div class="value" id="modelCount"></div></div>
2804
+ <div class="card"><div class="label">agents</div><div class="value" id="agentCount"></div></div>
2805
+ </section>
2806
+ <section class="controls">
2807
+ <label>Agent<select id="agentFilter"></select></label>
2808
+ <label>Model<select id="modelFilter"></select></label>
2809
+ <label>Range<select id="rangeFilter"><option value="all">All included data</option><option value="7">Last 7 days</option><option value="30">Last 30 days</option><option value="90">Last 90 days</option></select></label>
2810
+ </section>
2811
+ <section class="grid">
2812
+ <div class="panel"><h2>Agents</h2><div class="panel-body"><table><thead><tr><th>Agent</th><th>Cost</th><th>Reqs</th></tr></thead><tbody id="agentRows"></tbody></table></div></div>
2813
+ <div class="panel"><h2>Models</h2><div class="panel-body"><table><thead><tr><th>Model</th><th>Cost</th><th>Share</th><th>Reqs</th><th>Input</th><th>Output</th><th>Cache</th></tr></thead><tbody id="modelRows"></tbody></table><div class="legend"><span><i class="dot bg-purple"></i>Claude/Anthropic</span><span><i class="dot bg-green"></i>OpenAI</span><span><i class="dot bg-blue"></i>Google</span><span><i class="dot bg-yellow"></i>Kimi/GLM</span></div></div></div>
2814
+ <div class="panel"><h2>Daily</h2><div class="panel-body"><div class="chart-wrap"><div class="chart" id="dailyChart"></div><div class="axis" id="dailyAxis"></div></div></div></div>
2815
+ </section>
2816
+ <div class="tooltip" id="tooltip"></div>
2817
+ </main>
2818
+ <script>
2819
+ const DATA = ${jsonForScript(data)};
2820
+ const $ = (id) => document.getElementById(id);
2821
+ const money = (value) => '$' + (Math.floor(Math.max(0, value) * 100 + 1e-9) / 100).toFixed(2);
2822
+ const number = (value) => Math.round(value).toLocaleString('en-US');
2823
+ const pct = (value) => (value * 100).toFixed(1) + '%';
2824
+ const esc = (value) => String(value).replace(/[&<>"']/g, (ch) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]));
2825
+ const modelClass = (name, provider) => {
2826
+ const model = String(name || '').toLowerCase();
2827
+ const p = String(provider || '').toLowerCase();
2828
+ if (p === 'anthropic' || model.startsWith('claude-')) return 'purple';
2829
+ if (p === 'openai' || model.startsWith('gpt-') || /^o\\d/.test(model)) return 'green';
2830
+ if (p === 'google' || model.startsWith('gemini-')) return 'blue';
2831
+ if (model.startsWith('kimi-') || model.startsWith('glm-')) return 'yellow';
2832
+ return 'cyan';
2833
+ };
2834
+ const shortDate = (day) => new Date(day + 'T00:00:00.000Z').toLocaleDateString('en-US', {month:'short', day:'numeric', timeZone:'UTC'});
2835
+ function dailyTicks(days) {
2836
+ const count = days.length;
2837
+ if (count === 0) return [];
2838
+ const maxTicks = count <= 7 ? count : count <= 31 ? 6 : count <= 90 ? 7 : 9;
2839
+ if (maxTicks <= 1) return [{index: 0, day: days[0].day}];
2840
+ const ticks = [];
2841
+ const seen = new Set();
2842
+ for (let tick = 0; tick < maxTicks; tick++) {
2843
+ const index = Math.round(((count - 1) * tick) / (maxTicks - 1));
2844
+ if (seen.has(index)) continue;
2845
+ seen.add(index);
2846
+ ticks.push({index, day: days[index].day});
2847
+ }
2848
+ return ticks;
2849
+ }
2850
+ function addModel(map, incoming) {
2851
+ const key = incoming.model;
2852
+ const row = map.get(key) || {model: incoming.model, provider: incoming.provider, estimatedCost: 0, requests: 0, inputTokens: 0, outputTokens: 0, reasoningTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0};
2853
+ row.estimatedCost += incoming.estimatedCost; row.requests += incoming.requests;
2854
+ row.inputTokens += incoming.inputTokens; row.outputTokens += incoming.outputTokens; row.reasoningTokens += incoming.reasoningTokens;
2855
+ row.cacheReadTokens += incoming.cacheReadTokens; row.cacheWriteTokens += incoming.cacheWriteTokens;
2856
+ map.set(key, row);
2857
+ }
2858
+ function filteredAgents() {
2859
+ const selected = $('agentFilter').value;
2860
+ return DATA.agents.filter((agent) => selected === 'all' || agent.name === selected);
2861
+ }
2862
+ function dayAllowed(day) {
2863
+ const range = $('rangeFilter').value;
2864
+ if (range === 'all') return true;
2865
+ const cutoff = new Date(DATA.generatedAt).getTime() - Number(range) * 24 * 60 * 60 * 1000;
2866
+ return new Date(day + 'T23:59:59.999Z').getTime() >= cutoff;
2867
+ }
2868
+ function selectedModelRows(models) {
2869
+ const selected = $('modelFilter').value;
2870
+ return models.filter((model) => selected === 'all' || model.model === selected);
2871
+ }
2872
+ function compute() {
2873
+ const agents = filteredAgents();
2874
+ const modelMap = new Map();
2875
+ const dayMap = new Map();
2876
+ let totalCost = 0, requests = 0;
2877
+ for (const agent of agents) {
2878
+ const models = selectedModelRows(agent.models);
2879
+ for (const model of models) { addModel(modelMap, model); totalCost += model.estimatedCost; requests += model.requests; }
2880
+ for (const day of agent.days) {
2881
+ if (!dayAllowed(day.day)) continue;
2882
+ const dayModels = selectedModelRows(day.models);
2883
+ const cost = dayModels.reduce((sum, model) => sum + model.estimatedCost, 0);
2884
+ const reqs = dayModels.reduce((sum, model) => sum + model.requests, 0);
2885
+ if (cost <= 0 && reqs <= 0) continue;
2886
+ const row = dayMap.get(day.day) || {day: day.day, estimatedCost: 0, requests: 0};
2887
+ row.estimatedCost += cost; row.requests += reqs; dayMap.set(day.day, row);
2888
+ }
2889
+ }
2890
+ return {agents, models: Array.from(modelMap.values()).sort((a,b) => b.estimatedCost - a.estimatedCost), days: Array.from(dayMap.values()).sort((a,b) => a.day.localeCompare(b.day)), totalCost, requests};
2891
+ }
2892
+ function render() {
2893
+ const view = compute();
2894
+ $('totalCost').textContent = money(view.totalCost);
2895
+ $('requestCount').textContent = number(view.requests);
2896
+ $('modelCount').textContent = number(view.models.length);
2897
+ $('agentCount').textContent = number(view.agents.length);
2898
+ const agentRows = view.agents.map((agent) => '<tr><td class="name">' + esc(agent.name) + '</td><td>' + money(agent.estimatedCost) + '</td><td>' + number(agent.requests) + '</td></tr>').join('');
2899
+ $('agentRows').innerHTML = agentRows || '<tr><td colspan="3" class="muted">No data</td></tr>';
2900
+ const maxModel = Math.max(1, ...view.models.map((model) => model.estimatedCost));
2901
+ $('modelRows').innerHTML = view.models.map((model) => {
2902
+ const klass = modelClass(model.model, model.provider);
2903
+ const cache = model.cacheReadTokens + model.cacheWriteTokens;
2904
+ return '<tr><td class="name ' + klass + '">' + esc(model.model) + '</td><td>' + money(model.estimatedCost) + '</td><td>' + pct(view.totalCost > 0 ? model.estimatedCost / view.totalCost : 0) + '</td><td>' + number(model.requests) + '</td><td>' + number(model.inputTokens) + '</td><td>' + number(model.outputTokens + model.reasoningTokens) + '</td><td>' + number(cache) + '</td></tr>';
2905
+ }).join('') || '<tr><td colspan="7" class="muted">No data</td></tr>';
2906
+ const maxDay = Math.max(1, ...view.days.map((day) => day.estimatedCost));
2907
+ $('dailyChart').innerHTML = view.days.length ? view.days.map((day) => {
2908
+ const tooltip = esc(shortDate(day.day) + '|' + money(day.estimatedCost) + '|' + number(day.requests) + ' reqs');
2909
+ return '<div class="bar-column" data-tooltip="' + tooltip + '"><div class="column-track"><div class="column-fill" style="height:' + Math.max(1, (day.estimatedCost / maxDay) * 100) + '%"></div></div></div>';
2910
+ }).join('') : '<div class="muted chart-empty">No data</div>';
2911
+ $('dailyAxis').innerHTML = dailyTicks(view.days).map((tick) => {
2912
+ const left = view.days.length === 1 ? 50 : ((tick.index + 0.5) / view.days.length) * 100;
2913
+ const edge = view.days.length === 1 ? '' : tick.index === 0 ? ' edge-start' : tick.index === view.days.length - 1 ? ' edge-end' : '';
2914
+ return '<span class="axis-tick' + edge + '" style="left:' + left.toFixed(4) + '%">' + esc(shortDate(tick.day)) + '</span>';
2915
+ }).join('');
2916
+ }
2917
+ function showTooltip(event) {
2918
+ const target = event.target.closest('[data-tooltip]');
2919
+ const tooltip = $('tooltip');
2920
+ if (!target) { tooltip.style.display = 'none'; return; }
2921
+ const [date, amount, requests] = target.dataset.tooltip.split('|');
2922
+ tooltip.innerHTML = '<div>' + esc(date) + '</div><div class="sub">' + esc(amount) + ' \xB7 ' + esc(requests) + '</div>';
2923
+ tooltip.style.display = 'block';
2924
+ moveTooltip(event);
2925
+ }
2926
+ function moveTooltip(event) {
2927
+ const tooltip = $('tooltip');
2928
+ if (tooltip.style.display !== 'block') return;
2929
+ const offset = 12;
2930
+ const nextLeft = Math.min(window.innerWidth - tooltip.offsetWidth - 8, event.clientX + offset);
2931
+ const nextTop = Math.min(window.innerHeight - tooltip.offsetHeight - 8, event.clientY + offset);
2932
+ tooltip.style.left = Math.max(8, nextLeft) + 'px';
2933
+ tooltip.style.top = Math.max(8, nextTop) + 'px';
2934
+ }
2935
+ function init() {
2936
+ $('scope').textContent = DATA.scope;
2937
+ $('generated').textContent = 'Generated ' + new Date(DATA.generatedAt).toLocaleString();
2938
+ $('agentFilter').innerHTML = '<option value="all">All agents</option>' + DATA.agents.map((agent) => '<option value="' + esc(agent.name) + '">' + esc(agent.name) + '</option>').join('');
2939
+ const models = Array.from(new Set(DATA.agents.flatMap((agent) => agent.models.map((model) => model.model)))).sort();
2940
+ $('modelFilter').innerHTML = '<option value="all">All models</option>' + models.map((model) => '<option value="' + esc(model) + '">' + esc(model) + '</option>').join('');
2941
+ ['agentFilter', 'modelFilter', 'rangeFilter'].forEach((id) => $(id).addEventListener('change', render));
2942
+ $('dailyChart').addEventListener('mouseover', showTooltip);
2943
+ $('dailyChart').addEventListener('mousemove', moveTooltip);
2944
+ $('dailyChart').addEventListener('mouseleave', () => { $('tooltip').style.display = 'none'; });
2945
+ render();
2946
+ }
2947
+ init();
2948
+ </script>
2949
+ </body>
2950
+ </html>`;
2951
+ }
2952
+ function jsonForScript(value) {
2953
+ return JSON.stringify(value).replace(/</g, "\\u003c");
2954
+ }
2955
+ function printReportHeader(options) {
2956
+ const scope = options.rangeLabel ?? (options.since ? `since ${formatDate(options.since)}` : "all local history");
2957
+ const agent = options.agent ? ` \xB7 ${options.agent}` : "";
2958
+ console.log(` ${c.bold}${c.red}devrage${c.reset} ${c.dim}report${c.reset}`);
2959
+ console.log(` ${c.dim}${scope}${agent}${c.reset}`);
2960
+ console.log(` ${c.dim}${"\u2500".repeat(54)}${c.reset}`);
2961
+ }
2962
+ function printBasicOverview(totalMessages, totalSwears) {
2963
+ console.log(
2964
+ ` ${c.dim}messages scanned${c.reset} ${c.bold}${formatNumber(totalMessages)}${c.reset}`
2965
+ );
2966
+ console.log(
2967
+ ` ${c.dim}total swears${c.reset} ${c.bold}${c.red}${formatNumber(totalSwears)}${c.reset}`
2968
+ );
2969
+ }
2970
+ function getCostTotals(costByAgent) {
2971
+ const entries = Object.entries(costByAgent);
2972
+ const totalCost = entries.reduce((sum, [, stats]) => sum + stats.estimatedCost, 0);
2973
+ const totalRequests = entries.reduce((sum, [, stats]) => sum + stats.requests, 0);
2974
+ const unpricedRequests = entries.reduce((sum, [, stats]) => sum + stats.unpricedRequests, 0);
2975
+ return {
2976
+ entries,
2977
+ totalCost,
2978
+ totalRequests,
2979
+ pricedRequests: totalRequests - unpricedRequests,
2980
+ unpricedRequests
2981
+ };
2982
+ }
2983
+ function aggregateModelCosts(entries) {
2984
+ const models = /* @__PURE__ */ new Map();
2985
+ for (const [, stats] of entries) {
2986
+ for (const model of stats.models) {
2987
+ mergeModelSummary(models, model);
2988
+ }
2989
+ }
2990
+ return sortedCostModels(models);
2991
+ }
2992
+ function mergeModelSummary(models, incoming) {
2993
+ const key = incoming.model;
2994
+ let model = models.get(key);
2995
+ if (!model) {
2996
+ models.set(key, { ...incoming });
2997
+ return;
2998
+ }
2999
+ model.requests += incoming.requests;
3000
+ model.estimatedCost += incoming.estimatedCost;
3001
+ model.billedCost += incoming.billedCost;
3002
+ model.pricingSource = mergeDisplayPricingSource(model.pricingSource, incoming.pricingSource);
3003
+ model.unpricedRequests += incoming.unpricedRequests;
3004
+ model.inputTokens += incoming.inputTokens;
3005
+ model.outputTokens += incoming.outputTokens;
3006
+ model.reasoningTokens += incoming.reasoningTokens;
3007
+ model.cacheReadTokens += incoming.cacheReadTokens;
3008
+ model.cacheWriteTokens += incoming.cacheWriteTokens;
3009
+ }
3010
+ function sortedCostModels(models) {
3011
+ return Array.from(models.values()).sort(
3012
+ (left, right) => right.estimatedCost - left.estimatedCost || right.requests - left.requests
3013
+ );
3014
+ }
3015
+ function mergeDisplayPricingSource(left, right) {
3016
+ return left === right ? left : "mixed";
3017
+ }
3018
+ function sectionTitle(label) {
3019
+ const width = 54;
3020
+ const lineLength = Math.max(4, width - label.length - 1);
3021
+ return `${c.bold}${label}${c.reset} ${c.dim}${"\u2500".repeat(lineLength)}${c.reset}`;
3022
+ }
3023
+ function colorText(value, color) {
3024
+ return `${color}${value}${c.reset}`;
3025
+ }
3026
+ function agentColor(agent) {
3027
+ switch (agent) {
3028
+ case "claude":
3029
+ return c.magenta;
3030
+ case "codex":
3031
+ return c.green;
3032
+ case "opencode":
3033
+ return c.cyan;
3034
+ case "amp":
3035
+ return c.yellow;
3036
+ case "pi":
3037
+ return c.blue;
3038
+ case "cursor":
3039
+ return c.blue;
3040
+ default:
3041
+ return c.white;
3042
+ }
3043
+ }
3044
+ function modelColor(model) {
3045
+ const provider = model.provider?.toLowerCase();
3046
+ const modelName = model.model.toLowerCase();
3047
+ if (provider === "anthropic" || modelName.startsWith("claude-")) {
3048
+ return c.magenta;
3049
+ }
3050
+ if (provider === "openai" || modelName.startsWith("gpt-") || /^o\d/.test(modelName)) {
3051
+ return c.green;
3052
+ }
3053
+ if (provider === "google" || modelName.startsWith("gemini-")) {
3054
+ return c.blue;
3055
+ }
3056
+ if (modelName.startsWith("kimi-") || modelName.startsWith("glm-")) {
3057
+ return c.yellow;
3058
+ }
3059
+ return c.cyan;
3060
+ }
3061
+ function renderBar(value, max, width, color = c.cyan) {
3062
+ const filled = max > 0 && value > 0 ? Math.max(1, Math.round(value / max * width)) : 0;
3063
+ const empty = width - filled;
3064
+ return `${color}${"\u2501".repeat(filled)}${c.gray}${"\u2500".repeat(empty)}${c.reset}`;
3065
+ }
3066
+ function clip(value, maxLength) {
3067
+ return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}\u2026`;
3068
+ }
3069
+ function formatCurrency(value) {
3070
+ const cents = Math.floor(Math.max(0, value) * 100 + 1e-9);
3071
+ return `$${(cents / 100).toFixed(2)}`;
3072
+ }
3073
+ function formatNumber(value) {
3074
+ return value.toLocaleString("en-US");
3075
+ }
3076
+ function formatRequests(value) {
3077
+ return `${formatNumber(value)} ${value === 1 ? "req" : "reqs"}`;
3078
+ }
3079
+ function formatPercent(value) {
3080
+ return `${(value * 100).toFixed(1)}%`;
3081
+ }
3082
+ function formatDate(value) {
3083
+ return value.toISOString().slice(0, 10);
3084
+ }
860
3085
 
861
3086
  // src/cli.ts
862
3087
  var COMMANDS = {
3088
+ cost,
863
3089
  scan
864
3090
  };
865
3091
  function usage() {
@@ -869,6 +3095,7 @@ Usage:
869
3095
  devrage <command> [options]
870
3096
 
871
3097
  Commands:
3098
+ cost Show API-equivalent coding agent cost
872
3099
  scan Scan sessions for profanity
873
3100
 
874
3101
  Options:
@@ -876,6 +3103,7 @@ Options:
876
3103
  --version Show version
877
3104
 
878
3105
  Examples:
3106
+ devrage cost
879
3107
  devrage scan
880
3108
  devrage scan --agent claude
881
3109
  devrage scan --since 2025-01-01`);
@@ -888,7 +3116,7 @@ async function main() {
888
3116
  process.exit(0);
889
3117
  }
890
3118
  if (command === "--version") {
891
- console.log("0.0.3");
3119
+ console.log("0.0.5");
892
3120
  process.exit(0);
893
3121
  }
894
3122
  const handler = command ? COMMANDS[command] : void 0;