@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/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
|
-
} catch {
|
|
483
|
-
if (verbose)
|
|
484
|
-
console.log("Skip unreadable:", filename);
|
|
485
|
-
continue;
|
|
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 {
|
|
490
482
|
continue;
|
|
491
|
-
|
|
492
|
-
|
|
483
|
+
}
|
|
484
|
+
const processed = getIngestState(db, "claude", stateKey);
|
|
485
|
+
if (processed === fileMtime)
|
|
493
486
|
continue;
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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);
|
|
487
|
+
let lines;
|
|
488
|
+
try {
|
|
489
|
+
lines = readFileSync(filePath, "utf-8").split(`
|
|
490
|
+
`).filter((l) => l.trim());
|
|
491
|
+
} catch {
|
|
492
|
+
continue;
|
|
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,11 +635,119 @@ 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
|
});
|
|
622
641
|
|
|
642
|
+
// src/lib/config.ts
|
|
643
|
+
var exports_config = {};
|
|
644
|
+
__export(exports_config, {
|
|
645
|
+
setConfigValue: () => setConfigValue,
|
|
646
|
+
saveConfig: () => saveConfig,
|
|
647
|
+
loadConfig: () => loadConfig,
|
|
648
|
+
getConfigValue: () => getConfigValue
|
|
649
|
+
});
|
|
650
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
651
|
+
import { homedir as homedir4 } from "os";
|
|
652
|
+
import { join as join4 } from "path";
|
|
653
|
+
function loadConfig() {
|
|
654
|
+
try {
|
|
655
|
+
if (existsSync4(CONFIG_PATH)) {
|
|
656
|
+
const raw = readFileSync3(CONFIG_PATH, "utf-8");
|
|
657
|
+
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
658
|
+
}
|
|
659
|
+
} catch {}
|
|
660
|
+
return { ...DEFAULTS };
|
|
661
|
+
}
|
|
662
|
+
function saveConfig(config) {
|
|
663
|
+
const dir = CONFIG_PATH.substring(0, CONFIG_PATH.lastIndexOf("/"));
|
|
664
|
+
if (!existsSync4(dir))
|
|
665
|
+
mkdirSync2(dir, { recursive: true });
|
|
666
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + `
|
|
667
|
+
`);
|
|
668
|
+
}
|
|
669
|
+
function getConfigValue(key) {
|
|
670
|
+
const config = loadConfig();
|
|
671
|
+
return config[key] ?? null;
|
|
672
|
+
}
|
|
673
|
+
function setConfigValue(key, value) {
|
|
674
|
+
const config = loadConfig();
|
|
675
|
+
let parsed = value;
|
|
676
|
+
if (value === "true")
|
|
677
|
+
parsed = true;
|
|
678
|
+
else if (value === "false")
|
|
679
|
+
parsed = false;
|
|
680
|
+
else if (value === "null")
|
|
681
|
+
parsed = null;
|
|
682
|
+
else if (!isNaN(Number(value)))
|
|
683
|
+
parsed = Number(value);
|
|
684
|
+
else if (value.startsWith("[")) {
|
|
685
|
+
try {
|
|
686
|
+
parsed = JSON.parse(value);
|
|
687
|
+
} catch {}
|
|
688
|
+
}
|
|
689
|
+
config[key] = parsed;
|
|
690
|
+
saveConfig(config);
|
|
691
|
+
}
|
|
692
|
+
var CONFIG_PATH, DEFAULTS;
|
|
693
|
+
var init_config = __esm(() => {
|
|
694
|
+
CONFIG_PATH = join4(homedir4(), ".economy", "config.json");
|
|
695
|
+
DEFAULTS = {
|
|
696
|
+
port: 3456,
|
|
697
|
+
"default-period": "today",
|
|
698
|
+
"auto-sync": true,
|
|
699
|
+
"sync-interval": 30,
|
|
700
|
+
"alert-thresholds": [5, 10, 25, 50, 100],
|
|
701
|
+
"webhook-url": null
|
|
702
|
+
};
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// src/lib/webhooks.ts
|
|
706
|
+
var exports_webhooks = {};
|
|
707
|
+
__export(exports_webhooks, {
|
|
708
|
+
checkAndFireWebhooks: () => checkAndFireWebhooks
|
|
709
|
+
});
|
|
710
|
+
async function checkAndFireWebhooks(db) {
|
|
711
|
+
const config = loadConfig();
|
|
712
|
+
const url = config["webhook-url"];
|
|
713
|
+
if (!url)
|
|
714
|
+
return;
|
|
715
|
+
const statuses = getBudgetStatuses(db);
|
|
716
|
+
for (const b of statuses) {
|
|
717
|
+
if (!b.is_over_alert)
|
|
718
|
+
continue;
|
|
719
|
+
const key = `webhook-budget-${b.id}-${b.period}`;
|
|
720
|
+
const lastFired = getIngestState(db, "webhook", key);
|
|
721
|
+
const pctBucket = Math.floor(b.percent_used / 10) * 10;
|
|
722
|
+
if (lastFired === String(pctBucket))
|
|
723
|
+
continue;
|
|
724
|
+
await fireWebhook(url, {
|
|
725
|
+
event: "budget_alert",
|
|
726
|
+
budget_id: b.id,
|
|
727
|
+
project: b.project_path ?? "global",
|
|
728
|
+
period: b.period,
|
|
729
|
+
spend: b.current_spend_usd,
|
|
730
|
+
limit: b.limit_usd,
|
|
731
|
+
percent: Math.round(b.percent_used * 10) / 10
|
|
732
|
+
});
|
|
733
|
+
setIngestState(db, "webhook", key, String(pctBucket));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async function fireWebhook(url, payload) {
|
|
737
|
+
try {
|
|
738
|
+
await fetch(url, {
|
|
739
|
+
method: "POST",
|
|
740
|
+
headers: { "Content-Type": "application/json" },
|
|
741
|
+
body: JSON.stringify(payload),
|
|
742
|
+
signal: AbortSignal.timeout(5000)
|
|
743
|
+
});
|
|
744
|
+
} catch {}
|
|
745
|
+
}
|
|
746
|
+
var init_webhooks = __esm(() => {
|
|
747
|
+
init_config();
|
|
748
|
+
init_database();
|
|
749
|
+
});
|
|
750
|
+
|
|
623
751
|
// src/cli/commands/watch.ts
|
|
624
752
|
var exports_watch = {};
|
|
625
753
|
__export(exports_watch, {
|
|
@@ -703,7 +831,7 @@ var init_watch = __esm(() => {
|
|
|
703
831
|
});
|
|
704
832
|
|
|
705
833
|
// src/server/serve.ts
|
|
706
|
-
import { randomUUID
|
|
834
|
+
import { randomUUID } from "crypto";
|
|
707
835
|
function json(data, status = 200) {
|
|
708
836
|
return new Response(JSON.stringify(data), {
|
|
709
837
|
status,
|
|
@@ -764,7 +892,7 @@ function createHandler(db) {
|
|
|
764
892
|
const body = await req.json();
|
|
765
893
|
const now = new Date().toISOString();
|
|
766
894
|
upsertBudget(db, {
|
|
767
|
-
id:
|
|
895
|
+
id: randomUUID(),
|
|
768
896
|
project_path: body["project_path"] ?? null,
|
|
769
897
|
agent: body["agent"] ?? null,
|
|
770
898
|
period: body["period"] ?? "monthly",
|
|
@@ -788,7 +916,7 @@ function createHandler(db) {
|
|
|
788
916
|
const { basename: basename3 } = await import("path");
|
|
789
917
|
const projPath = body["path"];
|
|
790
918
|
upsertProject(db, {
|
|
791
|
-
id:
|
|
919
|
+
id: randomUUID(),
|
|
792
920
|
path: projPath,
|
|
793
921
|
name: body["name"] ?? basename3(projPath),
|
|
794
922
|
description: body["description"] ?? null,
|
|
@@ -838,8 +966,32 @@ function createHandler(db) {
|
|
|
838
966
|
function startServer(port = 3456) {
|
|
839
967
|
const db = openDatabase();
|
|
840
968
|
ensurePricingSeeded(db);
|
|
841
|
-
const
|
|
842
|
-
|
|
969
|
+
const apiHandler = createHandler(db);
|
|
970
|
+
const dashboardDir = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
971
|
+
Bun.serve({
|
|
972
|
+
port,
|
|
973
|
+
async fetch(req) {
|
|
974
|
+
const url = new URL(req.url);
|
|
975
|
+
if (url.pathname.startsWith("/api") || url.pathname === "/health") {
|
|
976
|
+
return apiHandler(req);
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
980
|
+
if (existsSync5(dashboardDir)) {
|
|
981
|
+
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
982
|
+
const fullPath = dashboardDir + filePath;
|
|
983
|
+
if (existsSync5(fullPath)) {
|
|
984
|
+
return new Response(Bun.file(fullPath));
|
|
985
|
+
}
|
|
986
|
+
const indexPath = dashboardDir + "/index.html";
|
|
987
|
+
if (existsSync5(indexPath)) {
|
|
988
|
+
return new Response(Bun.file(indexPath));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
} catch {}
|
|
992
|
+
return apiHandler(req);
|
|
993
|
+
}
|
|
994
|
+
});
|
|
843
995
|
console.log(`economy-serve listening on http://localhost:${port}`);
|
|
844
996
|
}
|
|
845
997
|
var CORS;
|
|
@@ -871,19 +1023,45 @@ init_codex();
|
|
|
871
1023
|
init_pricing();
|
|
872
1024
|
import { Command } from "commander";
|
|
873
1025
|
import chalk2 from "chalk";
|
|
874
|
-
import { randomUUID as
|
|
1026
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
875
1027
|
import { execSync } from "child_process";
|
|
876
1028
|
var program = new Command;
|
|
877
|
-
program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version("0.1.
|
|
1029
|
+
program.name("economy").description("AI coding cost tracker \u2014 Claude Code, Codex, and Gemini").version("0.1.1");
|
|
1030
|
+
async function autoSync() {
|
|
1031
|
+
const db = openDatabase();
|
|
1032
|
+
ensurePricingSeeded(db);
|
|
1033
|
+
await ingestClaude(db);
|
|
1034
|
+
await ingestCodex(db);
|
|
1035
|
+
}
|
|
1036
|
+
function sparkline(values) {
|
|
1037
|
+
const chars = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
1038
|
+
if (values.length === 0)
|
|
1039
|
+
return "";
|
|
1040
|
+
const max = Math.max(...values);
|
|
1041
|
+
if (max === 0)
|
|
1042
|
+
return chars[0].repeat(values.length);
|
|
1043
|
+
return values.map((v) => chars[Math.min(Math.round(v / max * 7), 7)]).join("");
|
|
1044
|
+
}
|
|
878
1045
|
function fmt2(usd) {
|
|
879
|
-
|
|
1046
|
+
let formatted;
|
|
1047
|
+
if (usd >= 0.01) {
|
|
1048
|
+
formatted = "$" + usd.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
1049
|
+
} else {
|
|
1050
|
+
formatted = "$" + usd.toFixed(6);
|
|
1051
|
+
}
|
|
1052
|
+
return chalk2.green(formatted);
|
|
880
1053
|
}
|
|
881
1054
|
function fmtTokens(n) {
|
|
1055
|
+
if (n >= 1e9)
|
|
1056
|
+
return `${(n / 1e9).toFixed(1)}B`;
|
|
882
1057
|
if (n >= 1e6)
|
|
883
|
-
return `${(n / 1e6).toFixed(
|
|
1058
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
884
1059
|
if (n >= 1000)
|
|
885
1060
|
return `${(n / 1000).toFixed(1)}k`;
|
|
886
|
-
return
|
|
1061
|
+
return n.toLocaleString("en-US");
|
|
1062
|
+
}
|
|
1063
|
+
function fmtCount(n) {
|
|
1064
|
+
return n.toLocaleString("en-US");
|
|
887
1065
|
}
|
|
888
1066
|
function printTable(headers, rows) {
|
|
889
1067
|
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").replace(/\x1b\[[0-9;]*m/g, "").length)));
|
|
@@ -910,15 +1088,53 @@ function printSummary(label, period) {
|
|
|
910
1088
|
console.log();
|
|
911
1089
|
printTable(["Metric", "Value"], [
|
|
912
1090
|
["Total cost", fmt2(s.total_usd)],
|
|
913
|
-
["Sessions", chalk2.yellow(
|
|
914
|
-
["Requests", chalk2.yellow(
|
|
1091
|
+
["Sessions", chalk2.yellow(fmtCount(s.sessions))],
|
|
1092
|
+
["Requests", chalk2.yellow(fmtCount(s.requests))],
|
|
915
1093
|
["Tokens", chalk2.yellow(fmtTokens(s.tokens))]
|
|
916
1094
|
]);
|
|
917
1095
|
console.log();
|
|
918
1096
|
}
|
|
919
|
-
program.
|
|
1097
|
+
program.action(async () => {
|
|
1098
|
+
await autoSync();
|
|
1099
|
+
const db = openDatabase();
|
|
1100
|
+
const t = querySummary(db, "today");
|
|
1101
|
+
const w = querySummary(db, "week");
|
|
1102
|
+
const m = querySummary(db, "month");
|
|
1103
|
+
const projects = queryProjectBreakdown(db).slice(0, 3);
|
|
1104
|
+
const daily = queryDailyBreakdown(db, 14).reduce((acc, d) => {
|
|
1105
|
+
acc[d.date] = (acc[d.date] ?? 0) + d.cost_usd;
|
|
1106
|
+
return acc;
|
|
1107
|
+
}, {});
|
|
1108
|
+
const dailyValues = Object.values(daily);
|
|
1109
|
+
console.log();
|
|
1110
|
+
console.log(chalk2.bold.cyan(" Economy"));
|
|
1111
|
+
console.log();
|
|
1112
|
+
printTable(["Period", "Cost", "Sessions", "Requests", "Tokens"], [
|
|
1113
|
+
["Today", fmt2(t.total_usd), fmtCount(t.sessions), fmtCount(t.requests), fmtTokens(t.tokens)],
|
|
1114
|
+
["This Week", fmt2(w.total_usd), fmtCount(w.sessions), fmtCount(w.requests), fmtTokens(w.tokens)],
|
|
1115
|
+
["This Month", fmt2(m.total_usd), fmtCount(m.sessions), fmtCount(m.requests), fmtTokens(m.tokens)]
|
|
1116
|
+
]);
|
|
1117
|
+
if (dailyValues.length > 0) {
|
|
1118
|
+
console.log(`
|
|
1119
|
+
${chalk2.dim("14-day trend:")} ${sparkline(dailyValues)}`);
|
|
1120
|
+
}
|
|
1121
|
+
if (projects.length > 0) {
|
|
1122
|
+
console.log(`
|
|
1123
|
+
${chalk2.dim("Top projects:")}`);
|
|
1124
|
+
for (const p of projects) {
|
|
1125
|
+
console.log(` ${chalk2.white(p.project_name.padEnd(25))} ${fmt2(p.cost_usd)}`);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
console.log();
|
|
1129
|
+
});
|
|
1130
|
+
program.command("sync").description("Ingest cost data from Claude Code and Codex").option("--claude", "Only ingest Claude Code telemetry").option("--codex", "Only ingest Codex sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").action(async (opts) => {
|
|
920
1131
|
const db = openDatabase();
|
|
921
1132
|
ensurePricingSeeded(db);
|
|
1133
|
+
if (opts.force) {
|
|
1134
|
+
db.exec(`DELETE FROM ingest_state WHERE source = 'claude'`);
|
|
1135
|
+
if (opts.verbose)
|
|
1136
|
+
console.log(chalk2.dim("Cleared ingest cache"));
|
|
1137
|
+
}
|
|
922
1138
|
const doClaude = opts.claude || !opts.claude && !opts.codex;
|
|
923
1139
|
const doCodex = opts.codex || !opts.claude && !opts.codex;
|
|
924
1140
|
if (doClaude) {
|
|
@@ -931,13 +1147,27 @@ program.command("sync").description("Ingest cost data from Claude Code and Codex
|
|
|
931
1147
|
const r = await ingestCodex(db, opts.verbose);
|
|
932
1148
|
console.log(chalk2.green(`\u2713 ${r.sessions} sessions`));
|
|
933
1149
|
}
|
|
1150
|
+
try {
|
|
1151
|
+
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
1152
|
+
await checkAndFireWebhooks2(db);
|
|
1153
|
+
} catch {}
|
|
934
1154
|
console.log(chalk2.bold.green(`
|
|
935
1155
|
\u2713 Sync complete`));
|
|
936
1156
|
});
|
|
937
|
-
program.command("today").description("Cost summary for today").action(() =>
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1157
|
+
program.command("today").description("Cost summary for today").action(async () => {
|
|
1158
|
+
await autoSync();
|
|
1159
|
+
printSummary("Today", "today");
|
|
1160
|
+
});
|
|
1161
|
+
program.command("week").description("Cost summary for this week").action(async () => {
|
|
1162
|
+
await autoSync();
|
|
1163
|
+
printSummary("This Week", "week");
|
|
1164
|
+
});
|
|
1165
|
+
program.command("month").description("Cost summary for this month").action(async () => {
|
|
1166
|
+
await autoSync();
|
|
1167
|
+
printSummary("This Month", "month");
|
|
1168
|
+
});
|
|
1169
|
+
program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", "Filter by agent (claude|codex)").option("--project <path>", "Filter by project path").option("--limit <n>", "Number of sessions", "20").action(async (opts) => {
|
|
1170
|
+
await autoSync();
|
|
941
1171
|
const db = openDatabase();
|
|
942
1172
|
const sessions = querySessions(db, {
|
|
943
1173
|
agent: opts.agent,
|
|
@@ -955,7 +1185,7 @@ program.command("sessions").description("List coding sessions with costs").optio
|
|
|
955
1185
|
chalk2.white(s.project_name || chalk2.dim("unknown")),
|
|
956
1186
|
fmt2(s.total_cost_usd),
|
|
957
1187
|
chalk2.cyan(fmtTokens(s.total_tokens)),
|
|
958
|
-
|
|
1188
|
+
fmtCount(s.request_count),
|
|
959
1189
|
chalk2.dim(s.started_at.substring(0, 16))
|
|
960
1190
|
]));
|
|
961
1191
|
console.log();
|
|
@@ -1015,7 +1245,7 @@ budgetCmd.command("set").description("Set a budget").option("--project <path>",
|
|
|
1015
1245
|
const db = openDatabase();
|
|
1016
1246
|
const now = new Date().toISOString();
|
|
1017
1247
|
upsertBudget(db, {
|
|
1018
|
-
id:
|
|
1248
|
+
id: randomUUID2(),
|
|
1019
1249
|
project_path: opts.project ?? null,
|
|
1020
1250
|
agent: opts.agent ?? null,
|
|
1021
1251
|
period: opts.period ?? "monthly",
|
|
@@ -1059,7 +1289,7 @@ projectCmd.command("add <path>").description("Add a project").option("--name <na
|
|
|
1059
1289
|
const db = openDatabase();
|
|
1060
1290
|
const { basename: basename3 } = __require("path");
|
|
1061
1291
|
upsertProject(db, {
|
|
1062
|
-
id:
|
|
1292
|
+
id: randomUUID2(),
|
|
1063
1293
|
path,
|
|
1064
1294
|
name: opts.name ?? basename3(path),
|
|
1065
1295
|
description: null,
|
|
@@ -1100,6 +1330,79 @@ projectCmd.command("rename <path> <name>").description("Rename a project").actio
|
|
|
1100
1330
|
upsertProject(db, { ...existing, name });
|
|
1101
1331
|
console.log(chalk2.green(`\u2713 Renamed to: ${name}`));
|
|
1102
1332
|
});
|
|
1333
|
+
projectCmd.command("show <nameOrPath>").description("Detailed project breakdown with sparkline").action(async (nameOrPath) => {
|
|
1334
|
+
await autoSync();
|
|
1335
|
+
const db = openDatabase();
|
|
1336
|
+
const sessions = db.prepare(`SELECT * FROM sessions WHERE project_name LIKE ? OR project_path LIKE ? ORDER BY started_at DESC`).all(`%${nameOrPath}%`, `%${nameOrPath}%`);
|
|
1337
|
+
if (sessions.length === 0) {
|
|
1338
|
+
console.log(chalk2.yellow(`No sessions found for: ${nameOrPath}`));
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
const projectName = sessions[0]["project_name"] || nameOrPath;
|
|
1342
|
+
const projectPath = sessions[0]["project_path"] || "";
|
|
1343
|
+
const totalCost = sessions.reduce((s, r) => s + r["total_cost_usd"], 0);
|
|
1344
|
+
const totalTokens = sessions.reduce((s, r) => s + r["total_tokens"], 0);
|
|
1345
|
+
const daily = db.prepare(`
|
|
1346
|
+
SELECT DATE(r.timestamp) as d, SUM(r.cost_usd) as cost
|
|
1347
|
+
FROM requests r JOIN sessions s ON r.session_id = s.id
|
|
1348
|
+
WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
|
|
1349
|
+
AND r.timestamp >= DATE('now', '-14 days')
|
|
1350
|
+
GROUP BY d ORDER BY d ASC
|
|
1351
|
+
`).all(`%${nameOrPath}%`, `%${nameOrPath}%`);
|
|
1352
|
+
const dailyValues = daily.map((d) => d.cost);
|
|
1353
|
+
const models = db.prepare(`
|
|
1354
|
+
SELECT r.model, COUNT(*) as reqs, SUM(r.cost_usd) as cost
|
|
1355
|
+
FROM requests r JOIN sessions s ON r.session_id = s.id
|
|
1356
|
+
WHERE s.project_name LIKE ? OR s.project_path LIKE ?
|
|
1357
|
+
GROUP BY r.model ORDER BY cost DESC LIMIT 5
|
|
1358
|
+
`).all(`%${nameOrPath}%`, `%${nameOrPath}%`);
|
|
1359
|
+
console.log();
|
|
1360
|
+
console.log(chalk2.bold.cyan(` ${projectName}`));
|
|
1361
|
+
console.log(chalk2.dim(` ${projectPath}`));
|
|
1362
|
+
console.log();
|
|
1363
|
+
printTable(["Metric", "Value"], [
|
|
1364
|
+
["Total cost", fmt2(totalCost)],
|
|
1365
|
+
["Sessions", fmtCount(sessions.length)],
|
|
1366
|
+
["Total tokens", fmtTokens(totalTokens)]
|
|
1367
|
+
]);
|
|
1368
|
+
if (dailyValues.length > 0) {
|
|
1369
|
+
console.log(`
|
|
1370
|
+
${chalk2.dim("14-day trend:")} ${sparkline(dailyValues)}`);
|
|
1371
|
+
}
|
|
1372
|
+
if (models.length > 0) {
|
|
1373
|
+
console.log(`
|
|
1374
|
+
${chalk2.dim("Model breakdown:")}`);
|
|
1375
|
+
for (const m of models) {
|
|
1376
|
+
console.log(` ${chalk2.white(m.model.padEnd(30))} ${fmt2(m.cost)} (${fmtCount(m.reqs)} reqs)`);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
const topSessions = sessions.sort((a, b) => b["total_cost_usd"] - a["total_cost_usd"]).slice(0, 5);
|
|
1380
|
+
if (topSessions.length > 0) {
|
|
1381
|
+
console.log(`
|
|
1382
|
+
${chalk2.dim("Top sessions:")}`);
|
|
1383
|
+
for (const s of topSessions) {
|
|
1384
|
+
console.log(` ${chalk2.dim(s["id"].substring(0, 12))} ${fmt2(s["total_cost_usd"])} ${chalk2.dim(String(s["started_at"]).substring(0, 16))}`);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
console.log();
|
|
1388
|
+
});
|
|
1389
|
+
var configCmd = program.command("config").description("Manage economy configuration");
|
|
1390
|
+
configCmd.command("set <key> <value>").description("Set a config value").action(async (_key, _value) => {
|
|
1391
|
+
const { setConfigValue: setConfigValue2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
1392
|
+
setConfigValue2(_key, _value);
|
|
1393
|
+
console.log(chalk2.green(`\u2713 ${_key} = ${_value}`));
|
|
1394
|
+
});
|
|
1395
|
+
configCmd.command("get <key>").description("Get a config value").action(async (key) => {
|
|
1396
|
+
const { getConfigValue: getConfigValue2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
1397
|
+
console.log(getConfigValue2(key) ?? chalk2.dim("(not set)"));
|
|
1398
|
+
});
|
|
1399
|
+
configCmd.action(async () => {
|
|
1400
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), exports_config));
|
|
1401
|
+
const config = loadConfig2();
|
|
1402
|
+
console.log();
|
|
1403
|
+
printTable(["Key", "Value"], Object.entries(config).map(([k, v]) => [k, String(v)]));
|
|
1404
|
+
console.log();
|
|
1405
|
+
});
|
|
1103
1406
|
var pricingCmd = program.command("pricing").description("Manage model pricing rates");
|
|
1104
1407
|
pricingCmd.command("list").description("List all model prices").action(() => {
|
|
1105
1408
|
const db = openDatabase();
|
|
@@ -1142,13 +1445,48 @@ program.command("serve").description("Start the REST API server").option("-p, --
|
|
|
1142
1445
|
const { startServer: startServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
|
|
1143
1446
|
startServer2(port2);
|
|
1144
1447
|
});
|
|
1145
|
-
program.command("dashboard").description("Open the web dashboard
|
|
1146
|
-
const port2 = opts.port ??
|
|
1147
|
-
|
|
1448
|
+
program.command("dashboard").description("Open the web dashboard (auto-starts server if not running)").option("-p, --port <port>", "Server port", "3456").action(async (opts) => {
|
|
1449
|
+
const port2 = Number(opts.port ?? 3456);
|
|
1450
|
+
const url = `http://localhost:${port2}`;
|
|
1451
|
+
let serverRunning = false;
|
|
1148
1452
|
try {
|
|
1149
|
-
|
|
1453
|
+
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(500) });
|
|
1454
|
+
serverRunning = res.ok;
|
|
1455
|
+
} catch {}
|
|
1456
|
+
if (!serverRunning) {
|
|
1457
|
+
console.log(chalk2.cyan(`\u2192 Starting economy server on port ${port2}...`));
|
|
1458
|
+
const { spawn } = await import("child_process");
|
|
1459
|
+
const { resolve, dirname: dirname2 } = await import("path");
|
|
1460
|
+
const serveScript = resolve(dirname2(process.argv[1]), "..", "server", "index.js");
|
|
1461
|
+
const child = spawn(process.execPath, [serveScript], {
|
|
1462
|
+
detached: true,
|
|
1463
|
+
stdio: "ignore",
|
|
1464
|
+
env: { ...process.env, ECONOMY_PORT: String(port2) }
|
|
1465
|
+
});
|
|
1466
|
+
child.unref();
|
|
1467
|
+
let attempts = 0;
|
|
1468
|
+
while (attempts < 20) {
|
|
1469
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
1470
|
+
try {
|
|
1471
|
+
const res = await fetch(`${url}/health`, { signal: AbortSignal.timeout(300) });
|
|
1472
|
+
if (res.ok) {
|
|
1473
|
+
serverRunning = true;
|
|
1474
|
+
break;
|
|
1475
|
+
}
|
|
1476
|
+
} catch {}
|
|
1477
|
+
attempts++;
|
|
1478
|
+
}
|
|
1479
|
+
if (serverRunning) {
|
|
1480
|
+
console.log(chalk2.green(`\u2713 Server started`));
|
|
1481
|
+
} else {
|
|
1482
|
+
console.log(chalk2.yellow(`\u26A0 Server didn't respond \u2014 open ${url} manually after running \`economy serve\``));
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
console.log(chalk2.cyan(`Opening ${url}`));
|
|
1486
|
+
try {
|
|
1487
|
+
execSync(`open ${url}`);
|
|
1150
1488
|
} catch {
|
|
1151
|
-
console.log(chalk2.yellow(`
|
|
1489
|
+
console.log(chalk2.yellow(`Open your browser at ${url}`));
|
|
1152
1490
|
}
|
|
1153
1491
|
});
|
|
1154
1492
|
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) => {
|
|
@@ -1167,4 +1505,197 @@ Codex (~/.codex/config.toml):`));
|
|
|
1167
1505
|
}
|
|
1168
1506
|
console.log();
|
|
1169
1507
|
});
|
|
1508
|
+
program.command("session <id>").description("Show detailed breakdown of a single session").action(async (id) => {
|
|
1509
|
+
await autoSync();
|
|
1510
|
+
const db = openDatabase();
|
|
1511
|
+
const session = db.prepare(`SELECT * FROM sessions WHERE id = ? OR id LIKE ?`).get(id, `%${id}%`);
|
|
1512
|
+
if (!session) {
|
|
1513
|
+
console.log(chalk2.red(`Session not found: ${id}`));
|
|
1514
|
+
process.exit(1);
|
|
1515
|
+
}
|
|
1516
|
+
const requests = db.prepare(`SELECT * FROM requests WHERE session_id = ? ORDER BY timestamp ASC`).all(session["id"]);
|
|
1517
|
+
console.log();
|
|
1518
|
+
console.log(chalk2.bold.cyan(` Session: ${session["id"].substring(0, 16)}...`));
|
|
1519
|
+
console.log();
|
|
1520
|
+
printTable(["Field", "Value"], [
|
|
1521
|
+
["Agent", String(session["agent"])],
|
|
1522
|
+
["Project", String(session["project_name"] || session["project_path"] || "\u2014")],
|
|
1523
|
+
["Started", String(session["started_at"]).substring(0, 19)],
|
|
1524
|
+
["Ended", session["ended_at"] ? String(session["ended_at"]).substring(0, 19) : "\u2014"],
|
|
1525
|
+
["Total cost", fmt2(session["total_cost_usd"])],
|
|
1526
|
+
["Total tokens", fmtTokens(session["total_tokens"])],
|
|
1527
|
+
["Requests", fmtCount(session["request_count"])]
|
|
1528
|
+
]);
|
|
1529
|
+
if (requests.length > 0) {
|
|
1530
|
+
console.log(chalk2.dim(`
|
|
1531
|
+
Requests (${requests.length}):
|
|
1532
|
+
`));
|
|
1533
|
+
printTable(["Time", "Model", "Input", "Output", "Cache R", "Cache W", "Cost"], requests.slice(0, 50).map((r) => [
|
|
1534
|
+
chalk2.dim(String(r["timestamp"]).substring(11, 19)),
|
|
1535
|
+
chalk2.white(String(r["model"]).substring(0, 22)),
|
|
1536
|
+
fmtTokens(r["input_tokens"]),
|
|
1537
|
+
fmtTokens(r["output_tokens"]),
|
|
1538
|
+
fmtTokens(r["cache_read_tokens"]),
|
|
1539
|
+
fmtTokens(r["cache_create_tokens"]),
|
|
1540
|
+
fmt2(r["cost_usd"])
|
|
1541
|
+
]));
|
|
1542
|
+
if (requests.length > 50)
|
|
1543
|
+
console.log(chalk2.dim(` ... and ${requests.length - 50} more requests`));
|
|
1544
|
+
}
|
|
1545
|
+
console.log();
|
|
1546
|
+
});
|
|
1547
|
+
program.command("export").description("Export data as CSV").option("--type <type>", "Data type: sessions or requests", "sessions").option("--period <period>", "Period: today|week|month|all", "month").option("--output <file>", "Output file path (default: stdout)").action(async (opts) => {
|
|
1548
|
+
await autoSync();
|
|
1549
|
+
const db = openDatabase();
|
|
1550
|
+
let csv;
|
|
1551
|
+
if (opts.type === "requests") {
|
|
1552
|
+
const where = opts.period === "today" ? `DATE(timestamp) = DATE('now')` : opts.period === "week" ? `timestamp >= DATE('now', '-7 days')` : opts.period === "all" ? "1=1" : `timestamp >= DATE('now', '-30 days')`;
|
|
1553
|
+
const rows = db.prepare(`SELECT * FROM requests WHERE ${where} ORDER BY timestamp ASC`).all();
|
|
1554
|
+
csv = `id,agent,session_id,model,input_tokens,output_tokens,cache_read_tokens,cache_create_tokens,cost_usd,duration_ms,timestamp
|
|
1555
|
+
`;
|
|
1556
|
+
for (const r of rows) {
|
|
1557
|
+
csv += `${r["id"]},${r["agent"]},${r["session_id"]},${r["model"]},${r["input_tokens"]},${r["output_tokens"]},${r["cache_read_tokens"]},${r["cache_create_tokens"]},${r["cost_usd"]},${r["duration_ms"]},${r["timestamp"]}
|
|
1558
|
+
`;
|
|
1559
|
+
}
|
|
1560
|
+
} else {
|
|
1561
|
+
const where = opts.period === "today" ? `DATE(started_at) = DATE('now')` : opts.period === "week" ? `started_at >= DATE('now', '-7 days')` : opts.period === "all" ? "1=1" : `started_at >= DATE('now', '-30 days')`;
|
|
1562
|
+
const rows = db.prepare(`SELECT * FROM sessions WHERE ${where} ORDER BY started_at DESC`).all();
|
|
1563
|
+
csv = `id,agent,project_path,project_name,started_at,ended_at,total_cost_usd,total_tokens,request_count
|
|
1564
|
+
`;
|
|
1565
|
+
for (const r of rows) {
|
|
1566
|
+
csv += `${r["id"]},${r["agent"]},"${r["project_path"]}","${r["project_name"]}",${r["started_at"]},${r["ended_at"] ?? ""},${r["total_cost_usd"]},${r["total_tokens"]},${r["request_count"]}
|
|
1567
|
+
`;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (opts.output) {
|
|
1571
|
+
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
1572
|
+
writeFileSync2(opts.output, csv);
|
|
1573
|
+
console.log(chalk2.green(`\u2713 Exported to ${opts.output}`));
|
|
1574
|
+
} else {
|
|
1575
|
+
process.stdout.write(csv);
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
program.command("compare <period1> <period2>").description("Compare two periods (today/yesterday/week/lastweek/month/lastmonth)").action(async (p1, p2) => {
|
|
1579
|
+
await autoSync();
|
|
1580
|
+
const db = openDatabase();
|
|
1581
|
+
function dateRange(period) {
|
|
1582
|
+
const now = new Date;
|
|
1583
|
+
const today = now.toISOString().substring(0, 10);
|
|
1584
|
+
switch (period) {
|
|
1585
|
+
case "today":
|
|
1586
|
+
return [today, today];
|
|
1587
|
+
case "yesterday": {
|
|
1588
|
+
const d = new Date(now);
|
|
1589
|
+
d.setDate(d.getDate() - 1);
|
|
1590
|
+
const s = d.toISOString().substring(0, 10);
|
|
1591
|
+
return [s, s];
|
|
1592
|
+
}
|
|
1593
|
+
case "week": {
|
|
1594
|
+
const d = new Date(now);
|
|
1595
|
+
d.setDate(d.getDate() - 7);
|
|
1596
|
+
return [d.toISOString().substring(0, 10), today];
|
|
1597
|
+
}
|
|
1598
|
+
case "lastweek": {
|
|
1599
|
+
const d1 = new Date(now);
|
|
1600
|
+
d1.setDate(d1.getDate() - 14);
|
|
1601
|
+
const d2 = new Date(now);
|
|
1602
|
+
d2.setDate(d2.getDate() - 7);
|
|
1603
|
+
return [d1.toISOString().substring(0, 10), d2.toISOString().substring(0, 10)];
|
|
1604
|
+
}
|
|
1605
|
+
case "month": {
|
|
1606
|
+
const d = new Date(now);
|
|
1607
|
+
d.setDate(d.getDate() - 30);
|
|
1608
|
+
return [d.toISOString().substring(0, 10), today];
|
|
1609
|
+
}
|
|
1610
|
+
case "lastmonth": {
|
|
1611
|
+
const d1 = new Date(now);
|
|
1612
|
+
d1.setDate(d1.getDate() - 60);
|
|
1613
|
+
const d2 = new Date(now);
|
|
1614
|
+
d2.setDate(d2.getDate() - 30);
|
|
1615
|
+
return [d1.toISOString().substring(0, 10), d2.toISOString().substring(0, 10)];
|
|
1616
|
+
}
|
|
1617
|
+
default:
|
|
1618
|
+
return [today, today];
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
function queryRange(from, to) {
|
|
1622
|
+
const r = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost, COUNT(*) as requests, COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0) as tokens FROM requests WHERE DATE(timestamp) BETWEEN ? AND ?`).get(from, to);
|
|
1623
|
+
const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
1624
|
+
return { ...r, sessions: s.sessions };
|
|
1625
|
+
}
|
|
1626
|
+
const [f1, t1] = dateRange(p1);
|
|
1627
|
+
const [f2, t2] = dateRange(p2);
|
|
1628
|
+
const a = queryRange(f1, t1);
|
|
1629
|
+
const b = queryRange(f2, t2);
|
|
1630
|
+
function delta(v1, v2) {
|
|
1631
|
+
const d = v1 - v2;
|
|
1632
|
+
const pct = v2 > 0 ? (d / v2 * 100).toFixed(1) : "\u2014";
|
|
1633
|
+
const sign = d >= 0 ? "+" : "";
|
|
1634
|
+
const color = d > 0 ? chalk2.red : d < 0 ? chalk2.green : chalk2.dim;
|
|
1635
|
+
return color(`${sign}${pct}%`);
|
|
1636
|
+
}
|
|
1637
|
+
console.log();
|
|
1638
|
+
console.log(chalk2.bold.cyan(` ${p1} vs ${p2}`));
|
|
1639
|
+
console.log();
|
|
1640
|
+
printTable(["Metric", p1, p2, "Change"], [
|
|
1641
|
+
["Cost", fmt2(a.cost), fmt2(b.cost), delta(a.cost, b.cost)],
|
|
1642
|
+
["Sessions", fmtCount(a.sessions), fmtCount(b.sessions), delta(a.sessions, b.sessions)],
|
|
1643
|
+
["Requests", fmtCount(a.requests), fmtCount(b.requests), delta(a.requests, b.requests)],
|
|
1644
|
+
["Tokens", fmtTokens(a.tokens), fmtTokens(b.tokens), delta(a.tokens, b.tokens)]
|
|
1645
|
+
]);
|
|
1646
|
+
console.log();
|
|
1647
|
+
});
|
|
1648
|
+
program.command("forecast").description("Project end-of-month cost based on current burn rate").action(async () => {
|
|
1649
|
+
await autoSync();
|
|
1650
|
+
const db = openDatabase();
|
|
1651
|
+
const now = new Date;
|
|
1652
|
+
const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1653
|
+
const dayOfMonth = now.getDate();
|
|
1654
|
+
const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
|
|
1655
|
+
const today = now.toISOString().substring(0, 10);
|
|
1656
|
+
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(monthStart);
|
|
1657
|
+
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
1658
|
+
const projected = dailyAvg * daysInMonth;
|
|
1659
|
+
const sevenDaysAgo = new Date(now);
|
|
1660
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
1661
|
+
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
|
|
1662
|
+
const last7DailyAvg = last7.cost / 7;
|
|
1663
|
+
const last7Projected = last7DailyAvg * daysInMonth;
|
|
1664
|
+
const dailyCosts = db.prepare(`SELECT DATE(timestamp) as d, SUM(cost_usd) as cost FROM requests WHERE DATE(timestamp) >= ? GROUP BY d ORDER BY cost ASC`).all(monthStart);
|
|
1665
|
+
const cheapest = dailyCosts[0];
|
|
1666
|
+
const mostExpensive = dailyCosts[dailyCosts.length - 1];
|
|
1667
|
+
console.log();
|
|
1668
|
+
console.log(chalk2.bold.cyan(` Forecast (${dayOfMonth} of ${daysInMonth} days)`));
|
|
1669
|
+
console.log();
|
|
1670
|
+
printTable(["Metric", "Value"], [
|
|
1671
|
+
["Spent so far", fmt2(monthSoFar.cost)],
|
|
1672
|
+
["Daily average", fmt2(dailyAvg)],
|
|
1673
|
+
[chalk2.bold("Projected total"), chalk2.bold(fmt2(projected).replace(chalk2.green(""), ""))],
|
|
1674
|
+
["Last 7-day rate", `${fmt2(last7DailyAvg)}/day \u2192 ${fmt2(last7Projected)}`],
|
|
1675
|
+
["Cheapest day", cheapest ? `${fmt2(cheapest.cost)} (${cheapest.d})` : "\u2014"],
|
|
1676
|
+
["Most expensive", mostExpensive ? `${fmt2(mostExpensive.cost)} (${mostExpensive.d})` : "\u2014"]
|
|
1677
|
+
]);
|
|
1678
|
+
console.log();
|
|
1679
|
+
});
|
|
1680
|
+
program.command("efficiency").description("Show output/input token ratio per model").action(async () => {
|
|
1681
|
+
await autoSync();
|
|
1682
|
+
const db = openDatabase();
|
|
1683
|
+
const models = db.prepare(`
|
|
1684
|
+
SELECT model, SUM(input_tokens) as input, SUM(output_tokens) as output,
|
|
1685
|
+
SUM(cache_read_tokens) as cache_read, SUM(cache_create_tokens) as cache_write,
|
|
1686
|
+
COUNT(*) as requests, SUM(cost_usd) as cost
|
|
1687
|
+
FROM requests GROUP BY model ORDER BY cost DESC
|
|
1688
|
+
`).all();
|
|
1689
|
+
console.log();
|
|
1690
|
+
console.log(chalk2.bold.cyan(" Token Efficiency"));
|
|
1691
|
+
console.log();
|
|
1692
|
+
printTable(["Model", "Output/Input", "Cache Hit%", "Cost/1k Output", "Requests"], models.map((m) => {
|
|
1693
|
+
const ratio = m.input > 0 ? (m.output / m.input).toFixed(2) : "\u2014";
|
|
1694
|
+
const totalInput = m.input + m.cache_read + m.cache_write;
|
|
1695
|
+
const cacheHit = totalInput > 0 ? (m.cache_read / totalInput * 100).toFixed(1) + "%" : "\u2014";
|
|
1696
|
+
const costPer1kOutput = m.output > 0 ? fmt2(m.cost / m.output * 1000) : "\u2014";
|
|
1697
|
+
return [chalk2.white(m.model), ratio, cacheHit, costPer1kOutput, fmtCount(m.requests)];
|
|
1698
|
+
}));
|
|
1699
|
+
console.log();
|
|
1700
|
+
});
|
|
1170
1701
|
program.parse();
|