@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/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 +233 -144
- 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 +143 -109
- 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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/watch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAEjD,UAAU,YAAY;IACpB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,KAAK,CAAA;CACd;AAuBD,wBAAsB,UAAU,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiElE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":""}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var __create = Object.create;
|
|
4
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
5
3
|
var __defProp = Object.defineProperty;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
-
for (let key of __getOwnPropNames(mod))
|
|
12
|
-
if (!__hasOwnProp.call(to, key))
|
|
13
|
-
__defProp(to, key, {
|
|
14
|
-
get: () => mod[key],
|
|
15
|
-
enumerable: true
|
|
16
|
-
});
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
4
|
var __export = (target, all) => {
|
|
20
5
|
for (var name in all)
|
|
21
6
|
__defProp(target, name, {
|
|
@@ -90,19 +75,19 @@ var DEFAULT_PRICING;
|
|
|
90
75
|
var init_pricing = __esm(() => {
|
|
91
76
|
init_database();
|
|
92
77
|
DEFAULT_PRICING = {
|
|
93
|
-
"claude-opus-4-6": { inputPer1M:
|
|
94
|
-
"claude-opus-4-5": { inputPer1M:
|
|
78
|
+
"claude-opus-4-6": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
79
|
+
"claude-opus-4-5": { inputPer1M: 5, outputPer1M: 25, cacheReadPer1M: 0.5, cacheWritePer1M: 6.25 },
|
|
95
80
|
"claude-sonnet-4-6": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
96
81
|
"claude-sonnet-4-5": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
97
|
-
"claude-haiku-4-5": { inputPer1M:
|
|
82
|
+
"claude-haiku-4-5": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
98
83
|
"claude-3-5-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
99
|
-
"claude-3-5-haiku": { inputPer1M:
|
|
84
|
+
"claude-3-5-haiku": { inputPer1M: 1, outputPer1M: 5, cacheReadPer1M: 0.1, cacheWritePer1M: 1.25 },
|
|
100
85
|
"claude-3-opus": { inputPer1M: 15, outputPer1M: 75, cacheReadPer1M: 1.5, cacheWritePer1M: 18.75 },
|
|
101
86
|
"claude-3-sonnet": { inputPer1M: 3, outputPer1M: 15, cacheReadPer1M: 0.3, cacheWritePer1M: 3.75 },
|
|
102
87
|
"claude-3-haiku": { inputPer1M: 0.25, outputPer1M: 1.25, cacheReadPer1M: 0.03, cacheWritePer1M: 0.3 },
|
|
103
|
-
"gpt-5.3-codex": { inputPer1M:
|
|
104
|
-
"gpt-5.2-codex": { inputPer1M:
|
|
105
|
-
"gpt-5-codex": { inputPer1M:
|
|
88
|
+
"gpt-5.3-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
89
|
+
"gpt-5.2-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
90
|
+
"gpt-5-codex": { inputPer1M: 1.75, outputPer1M: 14, cacheReadPer1M: 0.44, cacheWritePer1M: 0 },
|
|
106
91
|
"gpt-4o": { inputPer1M: 2.5, outputPer1M: 10, cacheReadPer1M: 1.25, cacheWritePer1M: 0 },
|
|
107
92
|
"gpt-4o-mini": { inputPer1M: 0.15, outputPer1M: 0.6, cacheReadPer1M: 0.075, cacheWritePer1M: 0 },
|
|
108
93
|
o1: { inputPer1M: 15, outputPer1M: 60, cacheReadPer1M: 7.5, cacheWritePer1M: 0 },
|
|
@@ -301,8 +286,22 @@ function querySummary(db, period) {
|
|
|
301
286
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
302
287
|
FROM requests WHERE ${rWhere}
|
|
303
288
|
`).get();
|
|
304
|
-
const
|
|
305
|
-
|
|
289
|
+
const codexTotals = db.prepare(`
|
|
290
|
+
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
291
|
+
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
292
|
+
COUNT(*) as sessions
|
|
293
|
+
FROM sessions
|
|
294
|
+
WHERE ${sWhere}
|
|
295
|
+
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
296
|
+
`).get();
|
|
297
|
+
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
298
|
+
return {
|
|
299
|
+
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
300
|
+
requests: r.requests,
|
|
301
|
+
tokens: r.tokens + codexTotals.tokens,
|
|
302
|
+
sessions: sessionCount.sessions,
|
|
303
|
+
period
|
|
304
|
+
};
|
|
306
305
|
}
|
|
307
306
|
function queryModelBreakdown(db) {
|
|
308
307
|
return db.prepare(`
|
|
@@ -317,14 +316,14 @@ function queryModelBreakdown(db) {
|
|
|
317
316
|
}
|
|
318
317
|
function queryProjectBreakdown(db) {
|
|
319
318
|
return db.prepare(`
|
|
320
|
-
SELECT
|
|
321
|
-
COUNT(
|
|
322
|
-
COALESCE(SUM(
|
|
323
|
-
|
|
324
|
-
COALESCE(SUM(
|
|
325
|
-
MAX(
|
|
326
|
-
FROM sessions
|
|
327
|
-
GROUP BY
|
|
319
|
+
SELECT project_path, project_name,
|
|
320
|
+
COUNT(*) as sessions,
|
|
321
|
+
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
322
|
+
COALESCE(SUM(request_count), 0) as requests,
|
|
323
|
+
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
324
|
+
MAX(started_at) as last_active
|
|
325
|
+
FROM sessions
|
|
326
|
+
GROUP BY project_path ORDER BY cost_usd DESC
|
|
328
327
|
`).all();
|
|
329
328
|
}
|
|
330
329
|
function queryDailyBreakdown(db, days = 30) {
|
|
@@ -438,116 +437,139 @@ function seedModelPricing(db, defaults) {
|
|
|
438
437
|
var init_database = () => {};
|
|
439
438
|
|
|
440
439
|
// src/ingest/claude.ts
|
|
441
|
-
import { readdirSync, readFileSync, existsSync as existsSync2 } from "fs";
|
|
440
|
+
import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
|
|
442
441
|
import { homedir as homedir2 } from "os";
|
|
443
442
|
import { join as join2, basename } from "path";
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
443
|
+
function dirNameToPath(dirName) {
|
|
444
|
+
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
445
|
+
}
|
|
446
|
+
function collectJsonlFiles(projectDir) {
|
|
447
|
+
const files = [];
|
|
448
|
+
function walk(dir) {
|
|
449
|
+
try {
|
|
450
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
451
|
+
if (entry.isDirectory())
|
|
452
|
+
walk(join2(dir, entry.name));
|
|
453
|
+
else if (entry.name.endsWith(".jsonl"))
|
|
454
|
+
files.push(join2(dir, entry.name));
|
|
455
455
|
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
456
|
+
} catch {}
|
|
457
|
+
}
|
|
458
|
+
walk(projectDir);
|
|
459
|
+
return files;
|
|
459
460
|
}
|
|
460
|
-
async function ingestClaude(db, verbose = false,
|
|
461
|
-
if (!existsSync2(
|
|
461
|
+
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
462
|
+
if (!existsSync2(PROJECTS_DIR)) {
|
|
462
463
|
if (verbose)
|
|
463
|
-
console.log("Claude
|
|
464
|
+
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
464
465
|
return { files: 0, requests: 0, sessions: 0 };
|
|
465
466
|
}
|
|
466
|
-
|
|
467
|
+
let totalFiles = 0;
|
|
467
468
|
let totalRequests = 0;
|
|
468
|
-
let processedFiles = 0;
|
|
469
469
|
const touchedSessions = new Set;
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
for (const event of events) {
|
|
488
|
-
const ed = event.event_data;
|
|
489
|
-
if (!ed || ed.event_name !== "tengu_api_success")
|
|
470
|
+
const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
471
|
+
for (const projectDirEntry of projectDirs) {
|
|
472
|
+
const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
|
|
473
|
+
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
474
|
+
const projectName = basename(projectPath);
|
|
475
|
+
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
476
|
+
for (const filePath of jsonlFiles) {
|
|
477
|
+
const stateKey = filePath.replace(PROJECTS_DIR, "");
|
|
478
|
+
let fileMtime = "0";
|
|
479
|
+
try {
|
|
480
|
+
fileMtime = statSync(filePath).mtimeMs.toString();
|
|
481
|
+
} catch {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const processed = getIngestState(db, "claude", stateKey);
|
|
485
|
+
if (processed === fileMtime)
|
|
490
486
|
continue;
|
|
491
|
-
|
|
492
|
-
|
|
487
|
+
let lines;
|
|
488
|
+
try {
|
|
489
|
+
lines = readFileSync(filePath, "utf-8").split(`
|
|
490
|
+
`).filter((l) => l.trim());
|
|
491
|
+
} catch {
|
|
493
492
|
continue;
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
session_id: sessionId,
|
|
506
|
-
model,
|
|
507
|
-
input_tokens: inputTokens,
|
|
508
|
-
output_tokens: outputTokens,
|
|
509
|
-
cache_read_tokens: cacheReadTokens,
|
|
510
|
-
cache_create_tokens: 0,
|
|
511
|
-
cost_usd: costUsd,
|
|
512
|
-
duration_ms: meta.durationMs ?? 0,
|
|
513
|
-
timestamp,
|
|
514
|
-
source_request_id: requestId
|
|
515
|
-
});
|
|
516
|
-
if (!touchedSessions.has(sessionId)) {
|
|
517
|
-
const { projectPath, projectName } = resolveProjectPath(sessionId);
|
|
518
|
-
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
519
|
-
if (!existing) {
|
|
520
|
-
const session = {
|
|
521
|
-
id: sessionId,
|
|
522
|
-
agent: "claude",
|
|
523
|
-
project_path: projectPath,
|
|
524
|
-
project_name: projectName,
|
|
525
|
-
started_at: timestamp,
|
|
526
|
-
ended_at: null,
|
|
527
|
-
total_cost_usd: 0,
|
|
528
|
-
total_tokens: 0,
|
|
529
|
-
request_count: 0
|
|
530
|
-
};
|
|
531
|
-
upsertSession(db, session);
|
|
493
|
+
}
|
|
494
|
+
const fileBasename = basename(filePath, ".jsonl");
|
|
495
|
+
const isUuid = /^[0-9a-f-]{36}$/.test(fileBasename);
|
|
496
|
+
let sessionId = isUuid ? fileBasename : fileBasename.replace(/^agent-/, "");
|
|
497
|
+
let sessionCwd = projectPath;
|
|
498
|
+
for (const line of lines) {
|
|
499
|
+
let entry;
|
|
500
|
+
try {
|
|
501
|
+
entry = JSON.parse(line);
|
|
502
|
+
} catch {
|
|
503
|
+
continue;
|
|
532
504
|
}
|
|
533
|
-
|
|
505
|
+
if (entry.sessionId)
|
|
506
|
+
sessionId = entry.sessionId;
|
|
507
|
+
if (entry.cwd)
|
|
508
|
+
sessionCwd = entry.cwd;
|
|
509
|
+
if (entry.message?.role !== "assistant")
|
|
510
|
+
continue;
|
|
511
|
+
const usage = entry.message.usage;
|
|
512
|
+
if (!usage)
|
|
513
|
+
continue;
|
|
514
|
+
const model = entry.message.model;
|
|
515
|
+
if (!model)
|
|
516
|
+
continue;
|
|
517
|
+
const inputTokens = usage.input_tokens ?? 0;
|
|
518
|
+
const outputTokens = usage.output_tokens ?? 0;
|
|
519
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
|
|
520
|
+
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
|
|
521
|
+
const timestamp = entry.timestamp ?? new Date().toISOString();
|
|
522
|
+
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
523
|
+
continue;
|
|
524
|
+
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
525
|
+
const reqId = `claude-${sessionId}-${timestamp}`;
|
|
526
|
+
upsertRequest(db, {
|
|
527
|
+
id: reqId,
|
|
528
|
+
agent: "claude",
|
|
529
|
+
session_id: sessionId,
|
|
530
|
+
model,
|
|
531
|
+
input_tokens: inputTokens,
|
|
532
|
+
output_tokens: outputTokens,
|
|
533
|
+
cache_read_tokens: cacheReadTokens,
|
|
534
|
+
cache_create_tokens: cacheWriteTokens,
|
|
535
|
+
cost_usd: costUsd,
|
|
536
|
+
duration_ms: 0,
|
|
537
|
+
timestamp,
|
|
538
|
+
source_request_id: reqId
|
|
539
|
+
});
|
|
540
|
+
if (!touchedSessions.has(sessionId)) {
|
|
541
|
+
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
542
|
+
if (!existing) {
|
|
543
|
+
const session = {
|
|
544
|
+
id: sessionId,
|
|
545
|
+
agent: "claude",
|
|
546
|
+
project_path: sessionCwd || projectPath,
|
|
547
|
+
project_name: basename(sessionCwd || projectPath),
|
|
548
|
+
started_at: timestamp,
|
|
549
|
+
ended_at: null,
|
|
550
|
+
total_cost_usd: 0,
|
|
551
|
+
total_tokens: 0,
|
|
552
|
+
request_count: 0
|
|
553
|
+
};
|
|
554
|
+
upsertSession(db, session);
|
|
555
|
+
}
|
|
556
|
+
touchedSessions.add(sessionId);
|
|
557
|
+
}
|
|
558
|
+
totalRequests++;
|
|
534
559
|
}
|
|
535
|
-
|
|
560
|
+
setIngestState(db, "claude", stateKey, fileMtime);
|
|
561
|
+
totalFiles++;
|
|
536
562
|
}
|
|
537
|
-
setIngestState(db, "claude", stateKey, "done");
|
|
538
|
-
processedFiles++;
|
|
539
|
-
if (verbose)
|
|
540
|
-
console.log(`Processed ${filename}: found ${events.length} events`);
|
|
541
563
|
}
|
|
542
564
|
for (const sessionId of touchedSessions) {
|
|
543
565
|
rollupSession(db, sessionId);
|
|
544
566
|
}
|
|
545
|
-
return { files:
|
|
567
|
+
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
546
568
|
}
|
|
547
|
-
var
|
|
569
|
+
var PROJECTS_DIR;
|
|
548
570
|
var init_claude = __esm(() => {
|
|
549
571
|
init_database();
|
|
550
|
-
|
|
572
|
+
init_pricing();
|
|
551
573
|
PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
552
574
|
});
|
|
553
575
|
|
|
@@ -584,9 +606,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
584
606
|
const processed = getIngestState(db, "codex", stateKey);
|
|
585
607
|
if (processed === "done")
|
|
586
608
|
continue;
|
|
587
|
-
const
|
|
588
|
-
const outputTokens = thread.tokens_used - inputTokens;
|
|
589
|
-
const costUsd = computeCost(model, inputTokens, outputTokens);
|
|
609
|
+
const costUsd = 0;
|
|
590
610
|
const projectPath = thread.cwd ?? "";
|
|
591
611
|
const projectName = projectPath ? basename2(projectPath) : "unknown";
|
|
592
612
|
const startedAt = thread.created_at ? new Date(thread.created_at * 1000).toISOString() : new Date().toISOString();
|
|
@@ -615,7 +635,6 @@ async function ingestCodex(db, verbose = false) {
|
|
|
615
635
|
var CODEX_DB_PATH, CODEX_CONFIG_PATH;
|
|
616
636
|
var init_codex = __esm(() => {
|
|
617
637
|
init_database();
|
|
618
|
-
init_pricing();
|
|
619
638
|
CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
620
639
|
CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
621
640
|
});
|
|
@@ -703,7 +722,7 @@ var init_watch = __esm(() => {
|
|
|
703
722
|
});
|
|
704
723
|
|
|
705
724
|
// src/server/serve.ts
|
|
706
|
-
import { randomUUID
|
|
725
|
+
import { randomUUID } from "crypto";
|
|
707
726
|
function json(data, status = 200) {
|
|
708
727
|
return new Response(JSON.stringify(data), {
|
|
709
728
|
status,
|
|
@@ -764,7 +783,7 @@ function createHandler(db) {
|
|
|
764
783
|
const body = await req.json();
|
|
765
784
|
const now = new Date().toISOString();
|
|
766
785
|
upsertBudget(db, {
|
|
767
|
-
id:
|
|
786
|
+
id: randomUUID(),
|
|
768
787
|
project_path: body["project_path"] ?? null,
|
|
769
788
|
agent: body["agent"] ?? null,
|
|
770
789
|
period: body["period"] ?? "monthly",
|
|
@@ -788,7 +807,7 @@ function createHandler(db) {
|
|
|
788
807
|
const { basename: basename3 } = await import("path");
|
|
789
808
|
const projPath = body["path"];
|
|
790
809
|
upsertProject(db, {
|
|
791
|
-
id:
|
|
810
|
+
id: randomUUID(),
|
|
792
811
|
path: projPath,
|
|
793
812
|
name: body["name"] ?? basename3(projPath),
|
|
794
813
|
description: body["description"] ?? null,
|
|
@@ -838,8 +857,32 @@ function createHandler(db) {
|
|
|
838
857
|
function startServer(port = 3456) {
|
|
839
858
|
const db = openDatabase();
|
|
840
859
|
ensurePricingSeeded(db);
|
|
841
|
-
const
|
|
842
|
-
|
|
860
|
+
const apiHandler = createHandler(db);
|
|
861
|
+
const dashboardDir = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
862
|
+
Bun.serve({
|
|
863
|
+
port,
|
|
864
|
+
async fetch(req) {
|
|
865
|
+
const url = new URL(req.url);
|
|
866
|
+
if (url.pathname.startsWith("/api") || url.pathname === "/health") {
|
|
867
|
+
return apiHandler(req);
|
|
868
|
+
}
|
|
869
|
+
try {
|
|
870
|
+
const { existsSync: existsSync4 } = await import("fs");
|
|
871
|
+
if (existsSync4(dashboardDir)) {
|
|
872
|
+
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
873
|
+
const fullPath = dashboardDir + filePath;
|
|
874
|
+
if (existsSync4(fullPath)) {
|
|
875
|
+
return new Response(Bun.file(fullPath));
|
|
876
|
+
}
|
|
877
|
+
const indexPath = dashboardDir + "/index.html";
|
|
878
|
+
if (existsSync4(indexPath)) {
|
|
879
|
+
return new Response(Bun.file(indexPath));
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
} catch {}
|
|
883
|
+
return apiHandler(req);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
843
886
|
console.log(`economy-serve listening on http://localhost:${port}`);
|
|
844
887
|
}
|
|
845
888
|
var CORS;
|
|
@@ -871,19 +914,30 @@ init_codex();
|
|
|
871
914
|
init_pricing();
|
|
872
915
|
import { Command } from "commander";
|
|
873
916
|
import chalk2 from "chalk";
|
|
874
|
-
import { randomUUID as
|
|
917
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
875
918
|
import { execSync } from "child_process";
|
|
876
919
|
var program = new Command;
|
|
877
920
|
program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version("0.1.0");
|
|
878
921
|
function fmt2(usd) {
|
|
879
|
-
|
|
922
|
+
let formatted;
|
|
923
|
+
if (usd >= 0.01) {
|
|
924
|
+
formatted = "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
925
|
+
} else {
|
|
926
|
+
formatted = "$" + usd.toFixed(6);
|
|
927
|
+
}
|
|
928
|
+
return chalk2.green(formatted);
|
|
880
929
|
}
|
|
881
930
|
function fmtTokens(n) {
|
|
931
|
+
if (n >= 1e9)
|
|
932
|
+
return `${(n / 1e9).toFixed(1)}B`;
|
|
882
933
|
if (n >= 1e6)
|
|
883
|
-
return `${(n / 1e6).toFixed(
|
|
934
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
884
935
|
if (n >= 1000)
|
|
885
936
|
return `${(n / 1000).toFixed(1)}k`;
|
|
886
|
-
return
|
|
937
|
+
return n.toLocaleString("en-US");
|
|
938
|
+
}
|
|
939
|
+
function fmtCount(n) {
|
|
940
|
+
return n.toLocaleString("en-US");
|
|
887
941
|
}
|
|
888
942
|
function printTable(headers, rows) {
|
|
889
943
|
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").replace(/\x1b\[[0-9;]*m/g, "").length)));
|
|
@@ -910,8 +964,8 @@ function printSummary(label, period) {
|
|
|
910
964
|
console.log();
|
|
911
965
|
printTable(["Metric", "Value"], [
|
|
912
966
|
["Total cost", fmt2(s.total_usd)],
|
|
913
|
-
["Sessions", chalk2.yellow(
|
|
914
|
-
["Requests", chalk2.yellow(
|
|
967
|
+
["Sessions", chalk2.yellow(fmtCount(s.sessions))],
|
|
968
|
+
["Requests", chalk2.yellow(fmtCount(s.requests))],
|
|
915
969
|
["Tokens", chalk2.yellow(fmtTokens(s.tokens))]
|
|
916
970
|
]);
|
|
917
971
|
console.log();
|
|
@@ -955,7 +1009,7 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
955
1009
|
chalk2.white(s.project_name || chalk2.dim("unknown")),
|
|
956
1010
|
fmt2(s.total_cost_usd),
|
|
957
1011
|
chalk2.cyan(fmtTokens(s.total_tokens)),
|
|
958
|
-
|
|
1012
|
+
fmtCount(s.request_count),
|
|
959
1013
|
chalk2.dim(s.started_at.substring(0, 16))
|
|
960
1014
|
]));
|
|
961
1015
|
console.log();
|
|
@@ -1015,7 +1069,7 @@ budgetCmd.command("set").description("Set a budget").option("--project <path>",
|
|
|
1015
1069
|
const db = openDatabase();
|
|
1016
1070
|
const now = new Date().toISOString();
|
|
1017
1071
|
upsertBudget(db, {
|
|
1018
|
-
id:
|
|
1072
|
+
id: randomUUID2(),
|
|
1019
1073
|
project_path: opts.project ?? null,
|
|
1020
1074
|
agent: opts.agent ?? null,
|
|
1021
1075
|
period: opts.period ?? "monthly",
|
|
@@ -1059,7 +1113,7 @@ projectCmd.command("add <path>").description("Add a project").option("--name <na
|
|
|
1059
1113
|
const db = openDatabase();
|
|
1060
1114
|
const { basename: basename3 } = __require("path");
|
|
1061
1115
|
upsertProject(db, {
|
|
1062
|
-
id:
|
|
1116
|
+
id: randomUUID2(),
|
|
1063
1117
|
path,
|
|
1064
1118
|
name: opts.name ?? basename3(path),
|
|
1065
1119
|
description: null,
|
|
@@ -1142,13 +1196,48 @@ program.command("serve").description("Start the REST API server").option("-p, --
|
|
|
1142
1196
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
1143
1197
|
startServer2(port2);
|
|
1144
1198
|
});
|
|
1145
|
-
program.command("dashboard").description("Open the web dashboard
|
|
1146
|
-
const port2 = opts.port ??
|
|
1147
|
-
|
|
1199
|
+
program.command("dashboard").description("Open the web dashboard (auto-starts server if not running)").option("-p, --port <port>", "Server port", "3456").action(async (opts) => {
|
|
1200
|
+
const port2 = Number(opts.port ?? 3456);
|
|
1201
|
+
const url = `http://localhost:${port2}`;
|
|
1202
|
+
let serverRunning = false;
|
|
1203
|
+
try {
|
|
1204
|
+
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(500) });
|
|
1205
|
+
serverRunning = res.ok;
|
|
1206
|
+
} catch {}
|
|
1207
|
+
if (!serverRunning) {
|
|
1208
|
+
console.log(chalk2.cyan(`\u2192 Starting economy server on port ${port2}...`));
|
|
1209
|
+
const { spawn } = await import("child_process");
|
|
1210
|
+
const { resolve, dirname: dirname2 } = await import("path");
|
|
1211
|
+
const serveScript = resolve(dirname2(process.argv[1]), "..", "server", "index.js");
|
|
1212
|
+
const child = spawn(process.execPath, [serveScript], {
|
|
1213
|
+
detached: true,
|
|
1214
|
+
stdio: "ignore",
|
|
1215
|
+
env: { ...process.env, ECONOMY_PORT: String(port2) }
|
|
1216
|
+
});
|
|
1217
|
+
child.unref();
|
|
1218
|
+
let attempts = 0;
|
|
1219
|
+
while (attempts < 20) {
|
|
1220
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
1221
|
+
try {
|
|
1222
|
+
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(300) });
|
|
1223
|
+
if (res.ok) {
|
|
1224
|
+
serverRunning = true;
|
|
1225
|
+
break;
|
|
1226
|
+
}
|
|
1227
|
+
} catch {}
|
|
1228
|
+
attempts++;
|
|
1229
|
+
}
|
|
1230
|
+
if (serverRunning) {
|
|
1231
|
+
console.log(chalk2.green(`\u2713 Server started`));
|
|
1232
|
+
} else {
|
|
1233
|
+
console.log(chalk2.yellow(`\u26A0 Server didn't respond \u2014 open ${url} manually after running \`economy serve\``));
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
console.log(chalk2.cyan(`Opening ${url}`));
|
|
1148
1237
|
try {
|
|
1149
|
-
execSync(`open
|
|
1238
|
+
execSync(`open ${url}`);
|
|
1150
1239
|
} catch {
|
|
1151
|
-
console.log(chalk2.yellow(`
|
|
1240
|
+
console.log(chalk2.yellow(`Open your browser at ${url}`));
|
|
1152
1241
|
}
|
|
1153
1242
|
});
|
|
1154
1243
|
program.command("mcp").description("Show MCP server install commands").option("--claude", "Install into Claude Code").option("--codex", "Install into Codex").option("--all", "Install into all agents").action(async (opts) => {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import type { EconomyRequest, EconomySession, EconomyProject, Budget, BudgetStatus, CostSummary, ModelBreakdown, ProjectBreakdown, Period, SessionFilter } from '../types/index.js';
|
|
3
|
+
export declare function getDbPath(): string;
|
|
4
|
+
export declare function openDatabase(dbPath?: string, skipSeed?: boolean): Database;
|
|
5
|
+
export declare function upsertRequest(db: Database, req: EconomyRequest): void;
|
|
6
|
+
export declare function upsertSession(db: Database, session: EconomySession): void;
|
|
7
|
+
export declare function rollupSession(db: Database, sessionId: string): void;
|
|
8
|
+
export declare function querySessions(db: Database, filter?: SessionFilter): EconomySession[];
|
|
9
|
+
export declare function queryTopSessions(db: Database, n?: number, agent?: string): EconomySession[];
|
|
10
|
+
export declare function querySummary(db: Database, period: Period): CostSummary;
|
|
11
|
+
export declare function queryModelBreakdown(db: Database): ModelBreakdown[];
|
|
12
|
+
export declare function queryProjectBreakdown(db: Database): ProjectBreakdown[];
|
|
13
|
+
export declare function queryDailyBreakdown(db: Database, days?: number): Array<{
|
|
14
|
+
date: string;
|
|
15
|
+
cost_usd: number;
|
|
16
|
+
agent: string;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function upsertProject(db: Database, project: EconomyProject): void;
|
|
19
|
+
export declare function getProject(db: Database, path: string): EconomyProject | null;
|
|
20
|
+
export declare function listProjects(db: Database): EconomyProject[];
|
|
21
|
+
export declare function deleteProject(db: Database, path: string): void;
|
|
22
|
+
export declare function upsertBudget(db: Database, budget: Budget): void;
|
|
23
|
+
export declare function listBudgets(db: Database): Budget[];
|
|
24
|
+
export declare function deleteBudget(db: Database, id: string): void;
|
|
25
|
+
export declare function getBudgetStatuses(db: Database): BudgetStatus[];
|
|
26
|
+
export declare function getIngestState(db: Database, source: string, key: string): string | null;
|
|
27
|
+
export declare function setIngestState(db: Database, source: string, key: string, value: string): void;
|
|
28
|
+
export declare function queryRequestsSince(db: Database, since: string): EconomyRequest[];
|
|
29
|
+
export interface DbModelPricing {
|
|
30
|
+
model: string;
|
|
31
|
+
input_per_1m: number;
|
|
32
|
+
output_per_1m: number;
|
|
33
|
+
cache_read_per_1m: number;
|
|
34
|
+
cache_write_per_1m: number;
|
|
35
|
+
updated_at: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function upsertModelPricing(db: Database, p: DbModelPricing): void;
|
|
38
|
+
export declare function getModelPricing(db: Database, model: string): DbModelPricing | null;
|
|
39
|
+
export declare function listModelPricing(db: Database): DbModelPricing[];
|
|
40
|
+
export declare function deleteModelPricing(db: Database, model: string): void;
|
|
41
|
+
export declare function seedModelPricing(db: Database, defaults: Record<string, {
|
|
42
|
+
inputPer1M: number;
|
|
43
|
+
outputPer1M: number;
|
|
44
|
+
cacheReadPer1M: number;
|
|
45
|
+
cacheWritePer1M: number;
|
|
46
|
+
}>): void;
|
|
47
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAIrC,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,cAAc,EACd,MAAM,EACN,YAAY,EACZ,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,MAAM,EACN,aAAa,EACd,MAAM,mBAAmB,CAAA;AAE1B,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,UAAQ,GAAG,QAAQ,CAexE;AAgGD,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI,CAarE;AAID,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAWzE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAYnE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAE,aAAkB,GAAG,cAAc,EAAE,CAYxF;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,SAAK,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,cAAc,EAAE,CAKvF;AAID,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CA+BtE;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAUlE;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,QAAQ,GAAG,gBAAgB,EAAE,CAWtE;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,SAAK,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAQrH;AAID,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAKzE;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAI5E;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAG3D;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE9D;AAID,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAU/D;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,EAAE,CAElD;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAE3D;AAED,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,YAAY,EAAE,CA2B9D;AAID,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGvF;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAE7F;AAID,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,EAAE,CAEhF;AAID,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB,EAAE,MAAM,CAAA;IACzB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,cAAc,GAAG,IAAI,CAMxE;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAElF;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,GAAG,cAAc,EAAE,CAE/D;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAEpE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,IAAI,CAc3K"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA"}
|