@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/cli/commands/watch.d.ts +8 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +681 -150
- package/dist/db/database.d.ts +47 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +143 -109
- package/dist/ingest/claude.d.ts +7 -0
- package/dist/ingest/claude.d.ts.map +1 -0
- package/dist/ingest/codex.d.ts +7 -0
- package/dist/ingest/codex.d.ts.map +1 -0
- package/dist/lib/pricing.d.ts +10 -0
- package/dist/lib/pricing.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +147 -110
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +172 -129
- package/dist/server/serve.d.ts +4 -0
- package/dist/server/serve.d.ts.map +1 -0
- package/dist/types/index.d.ts +100 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +17 -3
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:
|
|
78
|
-
"claude-opus-4-5": { inputPer1M:
|
|
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:
|
|
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:
|
|
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:
|
|
88
|
-
"gpt-5.2-codex": { inputPer1M:
|
|
89
|
-
"gpt-5-codex": { inputPer1M:
|
|
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
|
|
289
|
-
|
|
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
|
|
305
|
-
COUNT(
|
|
306
|
-
COALESCE(SUM(
|
|
307
|
-
|
|
308
|
-
COALESCE(SUM(
|
|
309
|
-
MAX(
|
|
310
|
-
FROM sessions
|
|
311
|
-
GROUP BY
|
|
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
|
-
|
|
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
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
}
|
|
405
|
-
|
|
418
|
+
} catch {}
|
|
419
|
+
}
|
|
420
|
+
walk(projectDir);
|
|
421
|
+
return files;
|
|
406
422
|
}
|
|
407
|
-
async function ingestClaude(db, verbose = false,
|
|
408
|
-
if (!existsSync2(
|
|
423
|
+
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
424
|
+
if (!existsSync2(PROJECTS_DIR)) {
|
|
409
425
|
if (verbose)
|
|
410
|
-
console.log("Claude
|
|
426
|
+
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
411
427
|
return { files: 0, requests: 0, sessions: 0 };
|
|
412
428
|
}
|
|
413
|
-
|
|
429
|
+
let totalFiles = 0;
|
|
414
430
|
let totalRequests = 0;
|
|
415
|
-
let processedFiles = 0;
|
|
416
431
|
const touchedSessions = new Set;
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
439
|
-
|
|
445
|
+
}
|
|
446
|
+
const processed = getIngestState(db, "claude", stateKey);
|
|
447
|
+
if (processed === fileMtime)
|
|
440
448
|
continue;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":""}
|