@hasna/economy 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -74,19 +74,19 @@ var DEFAULT_PRICING;
74
74
  var init_pricing = __esm(() => {
75
75
  init_database();
76
76
  DEFAULT_PRICING = {
77
- "claude-opus-4-6": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
78
- "claude-opus-4-5": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
77
+ "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
78
+ "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
79
79
  "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
80
80
  "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
81
- "claude-haiku-4-5": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1 },
81
+ "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
82
82
  "claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
83
- "claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1 },
83
+ "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
84
84
  "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
85
85
  "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
86
86
  "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
87
- "gpt-5.3-codex": { inputPer1M: 30, outputPer1M: 120, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
88
- "gpt-5.2-codex": { inputPer1M: 30, outputPer1M: 120, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
89
- "gpt-5-codex": { inputPer1M: 30, outputPer1M: 120, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
87
+ "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
88
+ "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
89
+ "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
90
90
  "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
91
91
  "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
92
92
  o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
@@ -285,8 +285,22 @@ function querySummary(db, period) {
285
285
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
286
286
  FROM requests WHERE ${rWhere}
287
287
  `).get();
288
- const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
289
- return { total_usd: r.total_usd, requests: r.requests, tokens: r.tokens, sessions: s.sessions, period };
288
+ const codexTotals = db.prepare(`
289
+ SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
290
+ COALESCE(SUM(total_tokens), 0) as tokens,
291
+ COUNT(*) as sessions
292
+ FROM sessions
293
+ WHERE ${sWhere}
294
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
295
+ `).get();
296
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
297
+ return {
298
+ total_usd: r.total_usd + codexTotals.cost_usd,
299
+ requests: r.requests,
300
+ tokens: r.tokens + codexTotals.tokens,
301
+ sessions: sessionCount.sessions,
302
+ period
303
+ };
290
304
  }
291
305
  function queryModelBreakdown(db) {
292
306
  return db.prepare(`
@@ -301,14 +315,14 @@ function queryModelBreakdown(db) {
301
315
  }
302
316
  function queryProjectBreakdown(db) {
303
317
  return db.prepare(`
304
- SELECT s.project_path, s.project_name,
305
- COUNT(DISTINCT s.id) as sessions,
306
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
307
- COUNT(r.id) as requests,
308
- COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
309
- MAX(s.started_at) as last_active
310
- FROM sessions s LEFT JOIN requests r ON r.session_id = s.id
311
- GROUP BY s.project_path ORDER BY cost_usd DESC
318
+ SELECT project_path, project_name,
319
+ COUNT(*) as sessions,
320
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
321
+ COALESCE(SUM(request_count), 0) as requests,
322
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
323
+ MAX(started_at) as last_active
324
+ FROM sessions
325
+ GROUP BY project_path ORDER BY cost_usd DESC
312
326
  `).all();
313
327
  }
314
328
  function listBudgets(db) {
@@ -383,118 +397,140 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
383
397
 
384
398
  // src/ingest/claude.ts
385
399
  init_database();
386
- import { readdirSync, readFileSync, existsSync as existsSync2 } from "fs";
400
+ init_pricing();
401
+ import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
387
402
  import { homedir as homedir2 } from "os";
388
403
  import { join as join2, basename } from "path";
389
- import { randomUUID } from "crypto";
390
- var TELEMETRY_DIR = join2(homedir2(), ".claude", "telemetry");
391
404
  var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
392
- function resolveProjectPath(sessionId) {
393
- if (!existsSync2(PROJECTS_DIR))
394
- return { projectPath: "", projectName: "unknown" };
395
- try {
396
- const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
397
- for (const dir of projectDirs) {
398
- const sessionFile = join2(PROJECTS_DIR, dir.name, `${sessionId}.jsonl`);
399
- if (existsSync2(sessionFile)) {
400
- const projectPath = dir.name.replace(/^-/, "/").replace(/-/g, "/");
401
- return { projectPath, projectName: basename(projectPath) };
405
+ function dirNameToPath(dirName) {
406
+ return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
407
+ }
408
+ function collectJsonlFiles(projectDir) {
409
+ const files = [];
410
+ function walk(dir) {
411
+ try {
412
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
413
+ if (entry.isDirectory())
414
+ walk(join2(dir, entry.name));
415
+ else if (entry.name.endsWith(".jsonl"))
416
+ files.push(join2(dir, entry.name));
402
417
  }
403
- }
404
- } catch {}
405
- return { projectPath: "", projectName: "unknown" };
418
+ } catch {}
419
+ }
420
+ walk(projectDir);
421
+ return files;
406
422
  }
407
- async function ingestClaude(db, verbose = false, telemetryDir = TELEMETRY_DIR) {
408
- if (!existsSync2(telemetryDir)) {
423
+ async function ingestClaude(db, verbose = false, _telemetryDir) {
424
+ if (!existsSync2(PROJECTS_DIR)) {
409
425
  if (verbose)
410
- console.log("Claude telemetry dir not found:", telemetryDir);
426
+ console.log("Claude projects dir not found:", PROJECTS_DIR);
411
427
  return { files: 0, requests: 0, sessions: 0 };
412
428
  }
413
- const files = readdirSync(telemetryDir).filter((f) => f.endsWith(".json"));
429
+ let totalFiles = 0;
414
430
  let totalRequests = 0;
415
- let processedFiles = 0;
416
431
  const touchedSessions = new Set;
417
- for (const filename of files) {
418
- const stateKey = filename;
419
- const processed = getIngestState(db, "claude", stateKey);
420
- if (processed === "done")
421
- continue;
422
- const filePath = join2(telemetryDir, filename);
423
- let events;
424
- try {
425
- const raw = readFileSync(filePath, "utf-8");
426
- events = JSON.parse(raw);
427
- if (!Array.isArray(events))
428
- events = [events];
429
- } catch {
430
- if (verbose)
431
- console.log("Skip unreadable:", filename);
432
- continue;
433
- }
434
- for (const event of events) {
435
- const ed = event.event_data;
436
- if (!ed || ed.event_name !== "tengu_api_success")
432
+ const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
433
+ for (const projectDirEntry of projectDirs) {
434
+ const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
435
+ const projectPath = dirNameToPath(projectDirEntry.name);
436
+ const projectName = basename(projectPath);
437
+ const jsonlFiles = collectJsonlFiles(projectDirPath);
438
+ for (const filePath of jsonlFiles) {
439
+ const stateKey = filePath.replace(PROJECTS_DIR, "");
440
+ let fileMtime = "0";
441
+ try {
442
+ fileMtime = statSync(filePath).mtimeMs.toString();
443
+ } catch {
437
444
  continue;
438
- const meta = ed.additional_metadata;
439
- if (!meta)
445
+ }
446
+ const processed = getIngestState(db, "claude", stateKey);
447
+ if (processed === fileMtime)
440
448
  continue;
441
- const sessionId = ed.session_id ?? randomUUID();
442
- const timestamp = ed.client_timestamp ?? new Date().toISOString();
443
- const model = meta.model ?? ed.model ?? "unknown";
444
- const costUsd = meta.costUSD ?? 0;
445
- const inputTokens = meta.inputTokens ?? (meta.uncachedInputTokens ?? 0);
446
- const outputTokens = meta.outputTokens ?? 0;
447
- const cacheReadTokens = meta.cachedInputTokens ?? 0;
448
- const requestId = meta.requestId ?? randomUUID();
449
- upsertRequest(db, {
450
- id: `claude-${requestId}`,
451
- agent: "claude",
452
- session_id: sessionId,
453
- model,
454
- input_tokens: inputTokens,
455
- output_tokens: outputTokens,
456
- cache_read_tokens: cacheReadTokens,
457
- cache_create_tokens: 0,
458
- cost_usd: costUsd,
459
- duration_ms: meta.durationMs ?? 0,
460
- timestamp,
461
- source_request_id: requestId
462
- });
463
- if (!touchedSessions.has(sessionId)) {
464
- const { projectPath, projectName } = resolveProjectPath(sessionId);
465
- const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
466
- if (!existing) {
467
- const session = {
468
- id: sessionId,
469
- agent: "claude",
470
- project_path: projectPath,
471
- project_name: projectName,
472
- started_at: timestamp,
473
- ended_at: null,
474
- total_cost_usd: 0,
475
- total_tokens: 0,
476
- request_count: 0
477
- };
478
- upsertSession(db, session);
449
+ let lines;
450
+ try {
451
+ lines = readFileSync(filePath, "utf-8").split(`
452
+ `).filter((l) => l.trim());
453
+ } catch {
454
+ continue;
455
+ }
456
+ const fileBasename = basename(filePath, ".jsonl");
457
+ const isUuid = /^[0-9a-f-]{36}$/.test(fileBasename);
458
+ let sessionId = isUuid ? fileBasename : fileBasename.replace(/^agent-/, "");
459
+ let sessionCwd = projectPath;
460
+ for (const line of lines) {
461
+ let entry;
462
+ try {
463
+ entry = JSON.parse(line);
464
+ } catch {
465
+ continue;
479
466
  }
480
- touchedSessions.add(sessionId);
467
+ if (entry.sessionId)
468
+ sessionId = entry.sessionId;
469
+ if (entry.cwd)
470
+ sessionCwd = entry.cwd;
471
+ if (entry.message?.role !== "assistant")
472
+ continue;
473
+ const usage = entry.message.usage;
474
+ if (!usage)
475
+ continue;
476
+ const model = entry.message.model;
477
+ if (!model)
478
+ continue;
479
+ const inputTokens = usage.input_tokens ?? 0;
480
+ const outputTokens = usage.output_tokens ?? 0;
481
+ const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
482
+ const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
483
+ const timestamp = entry.timestamp ?? new Date().toISOString();
484
+ if (inputTokens + outputTokens + cacheWriteTokens === 0)
485
+ continue;
486
+ const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
487
+ const reqId = `claude-${sessionId}-${timestamp}`;
488
+ upsertRequest(db, {
489
+ id: reqId,
490
+ agent: "claude",
491
+ session_id: sessionId,
492
+ model,
493
+ input_tokens: inputTokens,
494
+ output_tokens: outputTokens,
495
+ cache_read_tokens: cacheReadTokens,
496
+ cache_create_tokens: cacheWriteTokens,
497
+ cost_usd: costUsd,
498
+ duration_ms: 0,
499
+ timestamp,
500
+ source_request_id: reqId
501
+ });
502
+ if (!touchedSessions.has(sessionId)) {
503
+ const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
504
+ if (!existing) {
505
+ const session = {
506
+ id: sessionId,
507
+ agent: "claude",
508
+ project_path: sessionCwd || projectPath,
509
+ project_name: basename(sessionCwd || projectPath),
510
+ started_at: timestamp,
511
+ ended_at: null,
512
+ total_cost_usd: 0,
513
+ total_tokens: 0,
514
+ request_count: 0
515
+ };
516
+ upsertSession(db, session);
517
+ }
518
+ touchedSessions.add(sessionId);
519
+ }
520
+ totalRequests++;
481
521
  }
482
- totalRequests++;
522
+ setIngestState(db, "claude", stateKey, fileMtime);
523
+ totalFiles++;
483
524
  }
484
- setIngestState(db, "claude", stateKey, "done");
485
- processedFiles++;
486
- if (verbose)
487
- console.log(`Processed ${filename}: found ${events.length} events`);
488
525
  }
489
526
  for (const sessionId of touchedSessions) {
490
527
  rollupSession(db, sessionId);
491
528
  }
492
- return { files: processedFiles, requests: totalRequests, sessions: touchedSessions.size };
529
+ return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
493
530
  }
494
531
 
495
532
  // src/ingest/codex.ts
496
533
  init_database();
497
- init_pricing();
498
534
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
499
535
  import { homedir as homedir3 } from "os";
500
536
  import { join as join3, basename as basename2 } from "path";
@@ -529,9 +565,7 @@ async function ingestCodex(db, verbose = false) {
529
565
  const processed = getIngestState(db, "codex", stateKey);
530
566
  if (processed === "done")
531
567
  continue;
532
- const inputTokens = Math.floor(thread.tokens_used * 0.6);
533
- const outputTokens = thread.tokens_used - inputTokens;
534
- const costUsd = computeCost(model, inputTokens, outputTokens);
568
+ const costUsd = 0;
535
569
  const projectPath = thread.cwd ?? "";
536
570
  const projectName = projectPath ? basename2(projectPath) : "unknown";
537
571
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
@@ -632,7 +666,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
632
666
  case "get_cost_summary": {
633
667
  const period = a["period"] ?? "today";
634
668
  const summary = querySummary(db, period);
635
- return { content: [{ type: "text", text: JSON.stringify(summary, null, 2) }] };
669
+ const fmtUsd = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
670
+ const fmtTok = (n) => n >= 1e9 ? `${(n / 1e9).toFixed(1)}B` : n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
671
+ const result = { ...summary, summary: `You've spent ${fmtUsd(summary.total_usd)} ${period === "all" ? "total" : period} across ${summary.sessions} sessions (${summary.requests.toLocaleString()} requests, ${fmtTok(summary.tokens)} tokens)` };
672
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
636
673
  }
637
674
  case "get_sessions": {
638
675
  const sessions = querySessions(db, {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":""}