@hasna/economy 0.1.0 → 0.1.1

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/index.js CHANGED
@@ -73,19 +73,19 @@ var DEFAULT_PRICING;
73
73
  var init_pricing = __esm(() => {
74
74
  init_database();
75
75
  DEFAULT_PRICING = {
76
- "claude-opus-4-6": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
77
- "claude-opus-4-5": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
76
+ "claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
77
+ "claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
78
78
  "claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
79
79
  "claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
80
- "claude-haiku-4-5": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1 },
80
+ "claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
81
81
  "claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
82
- "claude-3-5-haiku": { inputPer1M: 0.8, outputPer1M: 4, cacheReadPer1M: 0.08, cacheWritePer1M: 1 },
82
+ "claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
83
83
  "claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
84
84
  "claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
85
85
  "claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
86
- "gpt-5.3-codex": { inputPer1M: 30, outputPer1M: 120, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
87
- "gpt-5.2-codex": { inputPer1M: 30, outputPer1M: 120, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
88
- "gpt-5-codex": { inputPer1M: 30, outputPer1M: 120, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
86
+ "gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
87
+ "gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
88
+ "gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
89
89
  "gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
90
90
  "gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
91
91
  o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
@@ -284,8 +284,22 @@ function querySummary(db, period) {
284
284
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
285
285
  FROM requests WHERE ${rWhere}
286
286
  `).get();
287
- const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
288
- return { total_usd: r.total_usd, requests: r.requests, tokens: r.tokens, sessions: s.sessions, period };
287
+ const codexTotals = db.prepare(`
288
+ SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
289
+ COALESCE(SUM(total_tokens), 0) as tokens,
290
+ COUNT(*) as sessions
291
+ FROM sessions
292
+ WHERE ${sWhere}
293
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
294
+ `).get();
295
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
296
+ return {
297
+ total_usd: r.total_usd + codexTotals.cost_usd,
298
+ requests: r.requests,
299
+ tokens: r.tokens + codexTotals.tokens,
300
+ sessions: sessionCount.sessions,
301
+ period
302
+ };
289
303
  }
290
304
  function queryModelBreakdown(db) {
291
305
  return db.prepare(`
@@ -300,14 +314,14 @@ function queryModelBreakdown(db) {
300
314
  }
301
315
  function queryProjectBreakdown(db) {
302
316
  return db.prepare(`
303
- SELECT s.project_path, s.project_name,
304
- COUNT(DISTINCT s.id) as sessions,
305
- COALESCE(SUM(r.input_tokens + r.output_tokens + r.cache_read_tokens + r.cache_create_tokens), 0) as total_tokens,
306
- COUNT(r.id) as requests,
307
- COALESCE(SUM(s.total_cost_usd), 0) as cost_usd,
308
- MAX(s.started_at) as last_active
309
- FROM sessions s LEFT JOIN requests r ON r.session_id = s.id
310
- GROUP BY s.project_path ORDER BY cost_usd DESC
317
+ SELECT project_path, project_name,
318
+ COUNT(*) as sessions,
319
+ COALESCE(SUM(total_tokens), 0) as total_tokens,
320
+ COALESCE(SUM(request_count), 0) as requests,
321
+ COALESCE(SUM(total_cost_usd), 0) as cost_usd,
322
+ MAX(started_at) as last_active
323
+ FROM sessions
324
+ GROUP BY project_path ORDER BY cost_usd DESC
311
325
  `).all();
312
326
  }
313
327
  function queryDailyBreakdown(db, days = 30) {
@@ -426,117 +440,139 @@ init_pricing();
426
440
 
427
441
  // src/ingest/claude.ts
428
442
  init_database();
429
- import { readdirSync, readFileSync, existsSync as existsSync2 } from "fs";
443
+ init_pricing();
444
+ import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
430
445
  import { homedir as homedir2 } from "os";
431
446
  import { join as join2, basename } from "path";
432
- import { randomUUID } from "crypto";
433
- var TELEMETRY_DIR = join2(homedir2(), ".claude", "telemetry");
434
447
  var PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
435
- function resolveProjectPath(sessionId) {
436
- if (!existsSync2(PROJECTS_DIR))
437
- return { projectPath: "", projectName: "unknown" };
438
- try {
439
- const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
440
- for (const dir of projectDirs) {
441
- const sessionFile = join2(PROJECTS_DIR, dir.name, `${sessionId}.jsonl`);
442
- if (existsSync2(sessionFile)) {
443
- const projectPath = dir.name.replace(/^-/, "/").replace(/-/g, "/");
444
- return { projectPath, projectName: basename(projectPath) };
448
+ function dirNameToPath(dirName) {
449
+ return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
450
+ }
451
+ function collectJsonlFiles(projectDir) {
452
+ const files = [];
453
+ function walk(dir) {
454
+ try {
455
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
456
+ if (entry.isDirectory())
457
+ walk(join2(dir, entry.name));
458
+ else if (entry.name.endsWith(".jsonl"))
459
+ files.push(join2(dir, entry.name));
445
460
  }
446
- }
447
- } catch {}
448
- return { projectPath: "", projectName: "unknown" };
461
+ } catch {}
462
+ }
463
+ walk(projectDir);
464
+ return files;
449
465
  }
450
- async function ingestClaude(db, verbose = false, telemetryDir = TELEMETRY_DIR) {
451
- if (!existsSync2(telemetryDir)) {
466
+ async function ingestClaude(db, verbose = false, _telemetryDir) {
467
+ if (!existsSync2(PROJECTS_DIR)) {
452
468
  if (verbose)
453
- console.log("Claude telemetry dir not found:", telemetryDir);
469
+ console.log("Claude projects dir not found:", PROJECTS_DIR);
454
470
  return { files: 0, requests: 0, sessions: 0 };
455
471
  }
456
- const files = readdirSync(telemetryDir).filter((f) => f.endsWith(".json"));
472
+ let totalFiles = 0;
457
473
  let totalRequests = 0;
458
- let processedFiles = 0;
459
474
  const touchedSessions = new Set;
460
- for (const filename of files) {
461
- const stateKey = filename;
462
- const processed = getIngestState(db, "claude", stateKey);
463
- if (processed === "done")
464
- continue;
465
- const filePath = join2(telemetryDir, filename);
466
- let events;
467
- try {
468
- const raw = readFileSync(filePath, "utf-8");
469
- events = JSON.parse(raw);
470
- if (!Array.isArray(events))
471
- events = [events];
472
- } catch {
473
- if (verbose)
474
- console.log("Skip unreadable:", filename);
475
- continue;
476
- }
477
- for (const event of events) {
478
- const ed = event.event_data;
479
- if (!ed || ed.event_name !== "tengu_api_success")
475
+ const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
476
+ for (const projectDirEntry of projectDirs) {
477
+ const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
478
+ const projectPath = dirNameToPath(projectDirEntry.name);
479
+ const projectName = basename(projectPath);
480
+ const jsonlFiles = collectJsonlFiles(projectDirPath);
481
+ for (const filePath of jsonlFiles) {
482
+ const stateKey = filePath.replace(PROJECTS_DIR, "");
483
+ let fileMtime = "0";
484
+ try {
485
+ fileMtime = statSync(filePath).mtimeMs.toString();
486
+ } catch {
480
487
  continue;
481
- const meta = ed.additional_metadata;
482
- if (!meta)
488
+ }
489
+ const processed = getIngestState(db, "claude", stateKey);
490
+ if (processed === fileMtime)
483
491
  continue;
484
- const sessionId = ed.session_id ?? randomUUID();
485
- const timestamp = ed.client_timestamp ?? new Date().toISOString();
486
- const model = meta.model ?? ed.model ?? "unknown";
487
- const costUsd = meta.costUSD ?? 0;
488
- const inputTokens = meta.inputTokens ?? (meta.uncachedInputTokens ?? 0);
489
- const outputTokens = meta.outputTokens ?? 0;
490
- const cacheReadTokens = meta.cachedInputTokens ?? 0;
491
- const requestId = meta.requestId ?? randomUUID();
492
- upsertRequest(db, {
493
- id: `claude-${requestId}`,
494
- agent: "claude",
495
- session_id: sessionId,
496
- model,
497
- input_tokens: inputTokens,
498
- output_tokens: outputTokens,
499
- cache_read_tokens: cacheReadTokens,
500
- cache_create_tokens: 0,
501
- cost_usd: costUsd,
502
- duration_ms: meta.durationMs ?? 0,
503
- timestamp,
504
- source_request_id: requestId
505
- });
506
- if (!touchedSessions.has(sessionId)) {
507
- const { projectPath, projectName } = resolveProjectPath(sessionId);
508
- const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
509
- if (!existing) {
510
- const session = {
511
- id: sessionId,
512
- agent: "claude",
513
- project_path: projectPath,
514
- project_name: projectName,
515
- started_at: timestamp,
516
- ended_at: null,
517
- total_cost_usd: 0,
518
- total_tokens: 0,
519
- request_count: 0
520
- };
521
- upsertSession(db, session);
492
+ let lines;
493
+ try {
494
+ lines = readFileSync(filePath, "utf-8").split(`
495
+ `).filter((l) => l.trim());
496
+ } catch {
497
+ continue;
498
+ }
499
+ const fileBasename = basename(filePath, ".jsonl");
500
+ const isUuid = /^[0-9a-f-]{36}$/.test(fileBasename);
501
+ let sessionId = isUuid ? fileBasename : fileBasename.replace(/^agent-/, "");
502
+ let sessionCwd = projectPath;
503
+ for (const line of lines) {
504
+ let entry;
505
+ try {
506
+ entry = JSON.parse(line);
507
+ } catch {
508
+ continue;
509
+ }
510
+ if (entry.sessionId)
511
+ sessionId = entry.sessionId;
512
+ if (entry.cwd)
513
+ sessionCwd = entry.cwd;
514
+ if (entry.message?.role !== "assistant")
515
+ continue;
516
+ const usage = entry.message.usage;
517
+ if (!usage)
518
+ continue;
519
+ const model = entry.message.model;
520
+ if (!model)
521
+ continue;
522
+ const inputTokens = usage.input_tokens ?? 0;
523
+ const outputTokens = usage.output_tokens ?? 0;
524
+ const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
525
+ const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
526
+ const timestamp = entry.timestamp ?? new Date().toISOString();
527
+ if (inputTokens + outputTokens + cacheWriteTokens === 0)
528
+ continue;
529
+ const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
530
+ const reqId = `claude-${sessionId}-${timestamp}`;
531
+ upsertRequest(db, {
532
+ id: reqId,
533
+ agent: "claude",
534
+ session_id: sessionId,
535
+ model,
536
+ input_tokens: inputTokens,
537
+ output_tokens: outputTokens,
538
+ cache_read_tokens: cacheReadTokens,
539
+ cache_create_tokens: cacheWriteTokens,
540
+ cost_usd: costUsd,
541
+ duration_ms: 0,
542
+ timestamp,
543
+ source_request_id: reqId
544
+ });
545
+ if (!touchedSessions.has(sessionId)) {
546
+ const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
547
+ if (!existing) {
548
+ const session = {
549
+ id: sessionId,
550
+ agent: "claude",
551
+ project_path: sessionCwd || projectPath,
552
+ project_name: basename(sessionCwd || projectPath),
553
+ started_at: timestamp,
554
+ ended_at: null,
555
+ total_cost_usd: 0,
556
+ total_tokens: 0,
557
+ request_count: 0
558
+ };
559
+ upsertSession(db, session);
560
+ }
561
+ touchedSessions.add(sessionId);
522
562
  }
523
- touchedSessions.add(sessionId);
563
+ totalRequests++;
524
564
  }
525
- totalRequests++;
565
+ setIngestState(db, "claude", stateKey, fileMtime);
566
+ totalFiles++;
526
567
  }
527
- setIngestState(db, "claude", stateKey, "done");
528
- processedFiles++;
529
- if (verbose)
530
- console.log(`Processed ${filename}: found ${events.length} events`);
531
568
  }
532
569
  for (const sessionId of touchedSessions) {
533
570
  rollupSession(db, sessionId);
534
571
  }
535
- return { files: processedFiles, requests: totalRequests, sessions: touchedSessions.size };
572
+ return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
536
573
  }
537
574
  // src/ingest/codex.ts
538
575
  init_database();
539
- init_pricing();
540
576
  import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
541
577
  import { homedir as homedir3 } from "os";
542
578
  import { join as join3, basename as basename2 } from "path";
@@ -571,9 +607,7 @@ async function ingestCodex(db, verbose = false) {
571
607
  const processed = getIngestState(db, "codex", stateKey);
572
608
  if (processed === "done")
573
609
  continue;
574
- const inputTokens = Math.floor(thread.tokens_used * 0.6);
575
- const outputTokens = thread.tokens_used - inputTokens;
576
- const costUsd = computeCost(model, inputTokens, outputTokens);
610
+ const costUsd = 0;
577
611
  const projectPath = thread.cwd ?? "";
578
612
  const projectName = projectPath ? basename2(projectPath) : "unknown";
579
613
  const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
@@ -0,0 +1,7 @@
1
+ import { Database } from 'bun:sqlite';
2
+ export declare function ingestClaude(db: Database, verbose?: boolean, _telemetryDir?: string): Promise<{
3
+ files: number;
4
+ requests: number;
5
+ sessions: number;
6
+ }>;
7
+ //# sourceMappingURL=claude.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/ingest/claude.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAwDrC,wBAAsB,YAAY,CAChC,EAAE,EAAE,QAAQ,EACZ,OAAO,UAAQ,EACf,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAsHhE"}
@@ -0,0 +1,7 @@
1
+ import { Database } from 'bun:sqlite';
2
+ declare function readCodexModel(): string;
3
+ export declare function ingestCodex(db: Database, verbose?: boolean): Promise<{
4
+ sessions: number;
5
+ }>;
6
+ export { readCodexModel };
7
+ //# sourceMappingURL=codex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/ingest/codex.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAmBrC,iBAAS,cAAc,IAAI,MAAM,CAShC;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA0D9F;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}
@@ -0,0 +1,10 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type { ModelPricing } from '../types/index.js';
3
+ export declare const DEFAULT_PRICING: Record<string, ModelPricing>;
4
+ export declare function normalizeModelName(raw: string): string;
5
+ export declare function ensurePricingSeeded(db: Database): void;
6
+ export declare function getPricingFromDb(db: Database, model: string): ModelPricing | null;
7
+ export declare function getPricing(model: string): ModelPricing | null;
8
+ export declare function computeCost(model: string, inputTokens: number, outputTokens: number, cacheReadTokens?: number, cacheWriteTokens?: number): number;
9
+ export declare function computeCostFromDb(db: Database, model: string, inputTokens: number, outputTokens: number, cacheReadTokens?: number, cacheWriteTokens?: number): number;
10
+ //# sourceMappingURL=pricing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pricing.d.ts","sourceRoot":"","sources":["../../src/lib/pricing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAKrD,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAwBxD,CAAA;AAGD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKtD;AAGD,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,CAEtD;AAGD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAuBjF;AAGD,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAO7D;AAED,wBAAgB,WAAW,CACzB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,eAAe,SAAI,EACnB,gBAAgB,SAAI,GACnB,MAAM,CASR;AAED,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,QAAQ,EACZ,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,eAAe,SAAI,EACnB,gBAAgB,SAAI,GACnB,MAAM,CASR"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bun
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mcp/index.ts"],"names":[],"mappings":""}