@hasna/economy 0.2.12 → 0.2.14
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/index.js +224 -14
- package/dist/index.js +19 -11
- package/dist/ingest/claude.d.ts +5 -0
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/mcp/index.js +118 -15
- package/dist/server/index.js +20 -11
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -609,9 +609,15 @@ function collectJsonlFiles(projectDir) {
|
|
|
609
609
|
return files;
|
|
610
610
|
}
|
|
611
611
|
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
612
|
-
|
|
612
|
+
return ingestJsonlProjects(db, CLAUDE_PROJECTS_DIR, "claude", verbose);
|
|
613
|
+
}
|
|
614
|
+
async function ingestTakumi(db, verbose = false) {
|
|
615
|
+
return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
|
|
616
|
+
}
|
|
617
|
+
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
618
|
+
if (!existsSync3(projectsDir)) {
|
|
613
619
|
if (verbose)
|
|
614
|
-
console.log(
|
|
620
|
+
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
615
621
|
return { files: 0, requests: 0, sessions: 0 };
|
|
616
622
|
}
|
|
617
623
|
const machineId = getMachineId();
|
|
@@ -619,20 +625,20 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
619
625
|
let totalRequests = 0;
|
|
620
626
|
const touchedSessions = new Set;
|
|
621
627
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
622
|
-
const projectDirs = readdirSync2(
|
|
628
|
+
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
623
629
|
for (const projectDirEntry of projectDirs) {
|
|
624
|
-
const projectDirPath = join4(
|
|
630
|
+
const projectDirPath = join4(projectsDir, projectDirEntry.name);
|
|
625
631
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
626
632
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
627
633
|
for (const filePath of jsonlFiles) {
|
|
628
|
-
const stateKey = filePath.replace(
|
|
634
|
+
const stateKey = filePath.replace(projectsDir, "");
|
|
629
635
|
let fileMtime = "0";
|
|
630
636
|
try {
|
|
631
637
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
632
638
|
} catch {
|
|
633
639
|
continue;
|
|
634
640
|
}
|
|
635
|
-
const processed = getIngestState(db,
|
|
641
|
+
const processed = getIngestState(db, agentName, stateKey);
|
|
636
642
|
if (processed === fileMtime)
|
|
637
643
|
continue;
|
|
638
644
|
let lines;
|
|
@@ -673,10 +679,10 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
673
679
|
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
674
680
|
continue;
|
|
675
681
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
676
|
-
const reqId =
|
|
682
|
+
const reqId = `${agentName}-${sessionId}-${timestamp}`;
|
|
677
683
|
upsertRequest(db, {
|
|
678
684
|
id: reqId,
|
|
679
|
-
agent:
|
|
685
|
+
agent: agentName,
|
|
680
686
|
session_id: sessionId,
|
|
681
687
|
model,
|
|
682
688
|
input_tokens: inputTokens,
|
|
@@ -696,7 +702,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
696
702
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
697
703
|
const session = {
|
|
698
704
|
id: sessionId,
|
|
699
|
-
agent:
|
|
705
|
+
agent: agentName,
|
|
700
706
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
701
707
|
project_name: detectedProject ? detectedProject.name : "",
|
|
702
708
|
started_at: timestamp,
|
|
@@ -712,7 +718,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
712
718
|
}
|
|
713
719
|
totalRequests++;
|
|
714
720
|
}
|
|
715
|
-
setIngestState(db,
|
|
721
|
+
setIngestState(db, agentName, stateKey, fileMtime);
|
|
716
722
|
totalFiles++;
|
|
717
723
|
}
|
|
718
724
|
}
|
|
@@ -721,11 +727,12 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
721
727
|
}
|
|
722
728
|
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
723
729
|
}
|
|
724
|
-
var
|
|
730
|
+
var CLAUDE_PROJECTS_DIR, TAKUMI_PROJECTS_DIR;
|
|
725
731
|
var init_claude = __esm(() => {
|
|
726
732
|
init_database();
|
|
727
733
|
init_pricing();
|
|
728
|
-
|
|
734
|
+
CLAUDE_PROJECTS_DIR = join4(homedir2(), ".claude", "projects");
|
|
735
|
+
TAKUMI_PROJECTS_DIR = join4(homedir2(), ".takumi", "projects");
|
|
729
736
|
});
|
|
730
737
|
|
|
731
738
|
// src/ingest/codex.ts
|
|
@@ -1236,6 +1243,8 @@ function createHandler(db) {
|
|
|
1236
1243
|
const results = {};
|
|
1237
1244
|
if (sources === "all" || sources === "claude")
|
|
1238
1245
|
results["claude"] = await ingestClaude(db);
|
|
1246
|
+
if (sources === "all" || sources === "takumi")
|
|
1247
|
+
results["takumi"] = await ingestTakumi(db);
|
|
1239
1248
|
if (sources === "all" || sources === "codex")
|
|
1240
1249
|
results["codex"] = await ingestCodex(db);
|
|
1241
1250
|
if (sources === "all" || sources === "gemini")
|
|
@@ -1450,6 +1459,102 @@ function menubarStop() {
|
|
|
1450
1459
|
var APP_PATH = "/Applications/Economy Bar.app", REPO = "hasna/open-economy";
|
|
1451
1460
|
var init_menubar = () => {};
|
|
1452
1461
|
|
|
1462
|
+
// src/db/pg-migrations.ts
|
|
1463
|
+
var exports_pg_migrations = {};
|
|
1464
|
+
__export(exports_pg_migrations, {
|
|
1465
|
+
PG_MIGRATIONS: () => PG_MIGRATIONS
|
|
1466
|
+
});
|
|
1467
|
+
var PG_MIGRATIONS;
|
|
1468
|
+
var init_pg_migrations = __esm(() => {
|
|
1469
|
+
PG_MIGRATIONS = [
|
|
1470
|
+
`CREATE TABLE IF NOT EXISTS requests (
|
|
1471
|
+
id TEXT PRIMARY KEY,
|
|
1472
|
+
agent TEXT NOT NULL,
|
|
1473
|
+
session_id TEXT NOT NULL,
|
|
1474
|
+
model TEXT NOT NULL,
|
|
1475
|
+
input_tokens INTEGER DEFAULT 0,
|
|
1476
|
+
output_tokens INTEGER DEFAULT 0,
|
|
1477
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
1478
|
+
cache_create_tokens INTEGER DEFAULT 0,
|
|
1479
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1480
|
+
duration_ms INTEGER DEFAULT 0,
|
|
1481
|
+
timestamp TEXT NOT NULL,
|
|
1482
|
+
source_request_id TEXT,
|
|
1483
|
+
machine_id TEXT DEFAULT ''
|
|
1484
|
+
)`,
|
|
1485
|
+
`CREATE TABLE IF NOT EXISTS sessions (
|
|
1486
|
+
id TEXT PRIMARY KEY,
|
|
1487
|
+
agent TEXT NOT NULL,
|
|
1488
|
+
project_path TEXT DEFAULT '',
|
|
1489
|
+
project_name TEXT DEFAULT '',
|
|
1490
|
+
started_at TEXT NOT NULL,
|
|
1491
|
+
ended_at TEXT,
|
|
1492
|
+
total_cost_usd REAL DEFAULT 0,
|
|
1493
|
+
total_tokens INTEGER DEFAULT 0,
|
|
1494
|
+
request_count INTEGER DEFAULT 0,
|
|
1495
|
+
machine_id TEXT DEFAULT ''
|
|
1496
|
+
)`,
|
|
1497
|
+
`CREATE TABLE IF NOT EXISTS projects (
|
|
1498
|
+
id TEXT PRIMARY KEY,
|
|
1499
|
+
path TEXT UNIQUE NOT NULL,
|
|
1500
|
+
name TEXT NOT NULL,
|
|
1501
|
+
description TEXT,
|
|
1502
|
+
tags TEXT DEFAULT '[]',
|
|
1503
|
+
created_at TEXT NOT NULL
|
|
1504
|
+
)`,
|
|
1505
|
+
`CREATE TABLE IF NOT EXISTS budgets (
|
|
1506
|
+
id TEXT PRIMARY KEY,
|
|
1507
|
+
project_path TEXT,
|
|
1508
|
+
agent TEXT,
|
|
1509
|
+
period TEXT NOT NULL,
|
|
1510
|
+
limit_usd REAL NOT NULL,
|
|
1511
|
+
alert_at_percent INTEGER DEFAULT 80,
|
|
1512
|
+
created_at TEXT NOT NULL,
|
|
1513
|
+
updated_at TEXT NOT NULL
|
|
1514
|
+
)`,
|
|
1515
|
+
`CREATE TABLE IF NOT EXISTS goals (
|
|
1516
|
+
id TEXT PRIMARY KEY,
|
|
1517
|
+
period TEXT NOT NULL,
|
|
1518
|
+
project_path TEXT,
|
|
1519
|
+
agent TEXT,
|
|
1520
|
+
limit_usd REAL NOT NULL,
|
|
1521
|
+
created_at TEXT NOT NULL,
|
|
1522
|
+
updated_at TEXT NOT NULL
|
|
1523
|
+
)`,
|
|
1524
|
+
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
1525
|
+
source TEXT NOT NULL,
|
|
1526
|
+
key TEXT NOT NULL,
|
|
1527
|
+
value TEXT NOT NULL,
|
|
1528
|
+
PRIMARY KEY (source, key)
|
|
1529
|
+
)`,
|
|
1530
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
1531
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
1532
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
1533
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
1534
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
1535
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
1536
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
1537
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
1538
|
+
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
1539
|
+
model TEXT PRIMARY KEY,
|
|
1540
|
+
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
1541
|
+
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
1542
|
+
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
1543
|
+
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
1544
|
+
updated_at TEXT NOT NULL
|
|
1545
|
+
)`,
|
|
1546
|
+
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1547
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1548
|
+
message TEXT NOT NULL,
|
|
1549
|
+
email TEXT,
|
|
1550
|
+
category TEXT DEFAULT 'general',
|
|
1551
|
+
version TEXT,
|
|
1552
|
+
machine_id TEXT,
|
|
1553
|
+
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1554
|
+
)`
|
|
1555
|
+
];
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1453
1558
|
// src/cli/index.ts
|
|
1454
1559
|
import { Command } from "commander";
|
|
1455
1560
|
import chalk4 from "chalk";
|
|
@@ -1836,6 +1941,7 @@ async function autoSync() {
|
|
|
1836
1941
|
const db = openDatabase();
|
|
1837
1942
|
ensurePricingSeeded(db);
|
|
1838
1943
|
await ingestClaude(db);
|
|
1944
|
+
await ingestTakumi(db);
|
|
1839
1945
|
await ingestCodex(db);
|
|
1840
1946
|
await ingestGemini(db);
|
|
1841
1947
|
}
|
|
@@ -1948,7 +2054,7 @@ program.action(async () => {
|
|
|
1948
2054
|
}
|
|
1949
2055
|
console.log();
|
|
1950
2056
|
});
|
|
1951
|
-
program.command("sync").description("Ingest cost data from Claude Code, Codex, and Gemini").option("--claude", "Only ingest Claude Code telemetry").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").action(async (opts) => {
|
|
2057
|
+
program.command("sync").description("Ingest cost data from Claude Code, Codex, and Gemini").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").action(async (opts) => {
|
|
1952
2058
|
const db = openDatabase();
|
|
1953
2059
|
ensurePricingSeeded(db);
|
|
1954
2060
|
if (opts.force) {
|
|
@@ -1956,8 +2062,9 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
|
|
|
1956
2062
|
if (opts.verbose)
|
|
1957
2063
|
console.log(chalk4.dim("Cleared ingest cache"));
|
|
1958
2064
|
}
|
|
1959
|
-
const anySpecific = opts.claude || opts.codex || opts.gemini;
|
|
2065
|
+
const anySpecific = opts.claude || opts.takumi || opts.codex || opts.gemini;
|
|
1960
2066
|
const doClaude = opts.claude || !anySpecific;
|
|
2067
|
+
const doTakumi = opts.takumi || !anySpecific;
|
|
1961
2068
|
const doCodex = opts.codex || !anySpecific;
|
|
1962
2069
|
const doGemini = opts.gemini || !anySpecific;
|
|
1963
2070
|
if (doClaude) {
|
|
@@ -1965,6 +2072,11 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
|
|
|
1965
2072
|
const r = await ingestClaude(db, opts.verbose);
|
|
1966
2073
|
console.log(chalk4.green(`\u2713 ${r.files} files, ${r.requests} requests, ${r.sessions} sessions`));
|
|
1967
2074
|
}
|
|
2075
|
+
if (doTakumi) {
|
|
2076
|
+
process.stdout.write(chalk4.cyan("\u2192 Ingesting Takumi sessions... "));
|
|
2077
|
+
const r = await ingestTakumi(db, opts.verbose);
|
|
2078
|
+
console.log(chalk4.green(`\u2713 ${r.files} files, ${r.requests} requests, ${r.sessions} sessions`));
|
|
2079
|
+
}
|
|
1968
2080
|
if (doCodex) {
|
|
1969
2081
|
process.stdout.write(chalk4.cyan("\u2192 Ingesting Codex sessions... "));
|
|
1970
2082
|
const r = await ingestCodex(db, opts.verbose);
|
|
@@ -2746,5 +2858,103 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
|
|
|
2746
2858
|
process.exit(1);
|
|
2747
2859
|
}
|
|
2748
2860
|
});
|
|
2861
|
+
var CLOUD_RDS_HOST = "hasnaxyz-prod-opensource.c4limg0qgqvk.us-east-1.rds.amazonaws.com";
|
|
2862
|
+
var CLOUD_RDS_USER = "hasna_admin";
|
|
2863
|
+
var CLOUD_RDS_DB = "economy";
|
|
2864
|
+
var CLOUD_TABLES = ["requests", "sessions", "projects", "budgets", "goals", "model_pricing"];
|
|
2865
|
+
async function getCloudPassword() {
|
|
2866
|
+
if (process.env["ECONOMY_PG_PASSWORD"])
|
|
2867
|
+
return process.env["ECONOMY_PG_PASSWORD"];
|
|
2868
|
+
const { execSync: exec } = await import("child_process");
|
|
2869
|
+
const secretJson = exec(`aws --profile hasna-xyz-hq secretsmanager get-secret-value --secret-id 'rds!db-7a451ce6-83a9-40fa-b24a-81e5d5943511' --query SecretString --output text`, { timeout: 1e4, encoding: "utf-8" });
|
|
2870
|
+
return JSON.parse(secretJson).password;
|
|
2871
|
+
}
|
|
2872
|
+
async function getCloudPg() {
|
|
2873
|
+
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
2874
|
+
const pw = encodeURIComponent(await getCloudPassword());
|
|
2875
|
+
return new PgAdapterAsync(`postgresql://${CLOUD_RDS_USER}:${pw}@${CLOUD_RDS_HOST}:5432/${CLOUD_RDS_DB}?sslmode=require`);
|
|
2876
|
+
}
|
|
2877
|
+
var cloudCmd = program.command("cloud").description("Cross-machine sync via cloud PostgreSQL");
|
|
2878
|
+
cloudCmd.command("push").description("Push local economy data to cloud PostgreSQL").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
|
|
2879
|
+
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
2880
|
+
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
2881
|
+
const cloud = await getCloudPg();
|
|
2882
|
+
const local = new SqliteAdapter(getDbPath());
|
|
2883
|
+
process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
|
|
2884
|
+
for (const sql of PG_MIGRATIONS2) {
|
|
2885
|
+
await cloud.run(sql);
|
|
2886
|
+
}
|
|
2887
|
+
console.log(chalk4.green("\u2713"));
|
|
2888
|
+
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : CLOUD_TABLES;
|
|
2889
|
+
process.stdout.write(chalk4.cyan(`\u2192 Pushing ${tableList.join(", ")}... `));
|
|
2890
|
+
const results = await syncPush(local, cloud, { tables: tableList });
|
|
2891
|
+
const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2892
|
+
console.log(chalk4.green(`\u2713 ${totalRows} rows across ${tableList.length} tables`));
|
|
2893
|
+
local.close();
|
|
2894
|
+
await cloud.close();
|
|
2895
|
+
console.log(chalk4.bold.green(`
|
|
2896
|
+
\u2713 Push complete from ${getMachineId()}`));
|
|
2897
|
+
});
|
|
2898
|
+
cloudCmd.command("pull").description("Pull cloud PostgreSQL data to local").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
|
|
2899
|
+
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
2900
|
+
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
2901
|
+
const cloud = await getCloudPg();
|
|
2902
|
+
const local = new SqliteAdapter(getDbPath());
|
|
2903
|
+
process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
|
|
2904
|
+
for (const sql of PG_MIGRATIONS2) {
|
|
2905
|
+
await cloud.run(sql);
|
|
2906
|
+
}
|
|
2907
|
+
console.log(chalk4.green("\u2713"));
|
|
2908
|
+
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : CLOUD_TABLES;
|
|
2909
|
+
process.stdout.write(chalk4.cyan(`\u2192 Pulling ${tableList.join(", ")}... `));
|
|
2910
|
+
const results = await syncPull(cloud, local, { tables: tableList });
|
|
2911
|
+
const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2912
|
+
console.log(chalk4.green(`\u2713 ${totalRows} rows across ${tableList.length} tables`));
|
|
2913
|
+
local.close();
|
|
2914
|
+
await cloud.close();
|
|
2915
|
+
console.log(chalk4.bold.green(`
|
|
2916
|
+
\u2713 Pull complete to ${getMachineId()}`));
|
|
2917
|
+
});
|
|
2918
|
+
cloudCmd.command("sync").description("Full sync: ingest local \u2192 push to cloud \u2192 pull from cloud").action(async () => {
|
|
2919
|
+
console.log(chalk4.bold.cyan(` Cloud Sync \u2014 ${getMachineId()}
|
|
2920
|
+
`));
|
|
2921
|
+
process.stdout.write(chalk4.cyan("\u2192 Ingesting local data... "));
|
|
2922
|
+
await autoSync();
|
|
2923
|
+
console.log(chalk4.green("\u2713"));
|
|
2924
|
+
const { syncPush, syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
2925
|
+
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
2926
|
+
const cloud = await getCloudPg();
|
|
2927
|
+
const local = new SqliteAdapter(getDbPath());
|
|
2928
|
+
for (const sql of PG_MIGRATIONS2) {
|
|
2929
|
+
await cloud.run(sql);
|
|
2930
|
+
}
|
|
2931
|
+
process.stdout.write(chalk4.cyan("\u2192 Pushing local \u2192 cloud... "));
|
|
2932
|
+
const pushResults = await syncPush(local, cloud, { tables: CLOUD_TABLES });
|
|
2933
|
+
console.log(chalk4.green(`\u2713 ${pushResults.reduce((s, r) => s + r.rowsWritten, 0)} rows`));
|
|
2934
|
+
process.stdout.write(chalk4.cyan("\u2192 Pulling cloud \u2192 local... "));
|
|
2935
|
+
const pullResults = await syncPull(cloud, local, { tables: CLOUD_TABLES });
|
|
2936
|
+
console.log(chalk4.green(`\u2713 ${pullResults.reduce((s, r) => s + r.rowsWritten, 0)} rows`));
|
|
2937
|
+
local.close();
|
|
2938
|
+
await cloud.close();
|
|
2939
|
+
console.log(chalk4.bold.green(`
|
|
2940
|
+
\u2713 Cloud sync complete`));
|
|
2941
|
+
});
|
|
2942
|
+
cloudCmd.command("status").description("Check cloud connection status").action(async () => {
|
|
2943
|
+
console.log();
|
|
2944
|
+
console.log(` Machine: ${chalk4.white(getMachineId())}`);
|
|
2945
|
+
console.log(` RDS Host: ${chalk4.white(CLOUD_RDS_HOST)}`);
|
|
2946
|
+
console.log(` Database: ${chalk4.white(CLOUD_RDS_DB)}`);
|
|
2947
|
+
try {
|
|
2948
|
+
const cloud = await getCloudPg();
|
|
2949
|
+
await cloud.get("SELECT 1 as ok");
|
|
2950
|
+
const tables = await cloud.all("SELECT tablename FROM pg_tables WHERE schemaname = 'public'");
|
|
2951
|
+
console.log(` PostgreSQL: ${chalk4.green("connected")}`);
|
|
2952
|
+
console.log(` Tables: ${chalk4.white(tables.map((t) => t.tablename).join(", ") || "(none)")}`);
|
|
2953
|
+
await cloud.close();
|
|
2954
|
+
} catch (err2) {
|
|
2955
|
+
console.log(` PostgreSQL: ${chalk4.red(`failed \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`)}`);
|
|
2956
|
+
}
|
|
2957
|
+
console.log();
|
|
2958
|
+
});
|
|
2749
2959
|
registerBrainsCommand(program);
|
|
2750
2960
|
program.parse();
|
package/dist/index.js
CHANGED
|
@@ -809,7 +809,8 @@ import { join as join3, basename } from "path";
|
|
|
809
809
|
function autoDetectProject(cwd, projects) {
|
|
810
810
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
811
811
|
}
|
|
812
|
-
var
|
|
812
|
+
var CLAUDE_PROJECTS_DIR = join3(homedir2(), ".claude", "projects");
|
|
813
|
+
var TAKUMI_PROJECTS_DIR = join3(homedir2(), ".takumi", "projects");
|
|
813
814
|
function dirNameToPath(dirName) {
|
|
814
815
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
815
816
|
}
|
|
@@ -829,9 +830,15 @@ function collectJsonlFiles(projectDir) {
|
|
|
829
830
|
return files;
|
|
830
831
|
}
|
|
831
832
|
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
832
|
-
|
|
833
|
+
return ingestJsonlProjects(db, CLAUDE_PROJECTS_DIR, "claude", verbose);
|
|
834
|
+
}
|
|
835
|
+
async function ingestTakumi(db, verbose = false) {
|
|
836
|
+
return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
|
|
837
|
+
}
|
|
838
|
+
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
839
|
+
if (!existsSync3(projectsDir)) {
|
|
833
840
|
if (verbose)
|
|
834
|
-
console.log(
|
|
841
|
+
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
835
842
|
return { files: 0, requests: 0, sessions: 0 };
|
|
836
843
|
}
|
|
837
844
|
const machineId = getMachineId();
|
|
@@ -839,20 +846,20 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
839
846
|
let totalRequests = 0;
|
|
840
847
|
const touchedSessions = new Set;
|
|
841
848
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
842
|
-
const projectDirs = readdirSync2(
|
|
849
|
+
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
843
850
|
for (const projectDirEntry of projectDirs) {
|
|
844
|
-
const projectDirPath = join3(
|
|
851
|
+
const projectDirPath = join3(projectsDir, projectDirEntry.name);
|
|
845
852
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
846
853
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
847
854
|
for (const filePath of jsonlFiles) {
|
|
848
|
-
const stateKey = filePath.replace(
|
|
855
|
+
const stateKey = filePath.replace(projectsDir, "");
|
|
849
856
|
let fileMtime = "0";
|
|
850
857
|
try {
|
|
851
858
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
852
859
|
} catch {
|
|
853
860
|
continue;
|
|
854
861
|
}
|
|
855
|
-
const processed = getIngestState(db,
|
|
862
|
+
const processed = getIngestState(db, agentName, stateKey);
|
|
856
863
|
if (processed === fileMtime)
|
|
857
864
|
continue;
|
|
858
865
|
let lines;
|
|
@@ -893,10 +900,10 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
893
900
|
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
894
901
|
continue;
|
|
895
902
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
896
|
-
const reqId =
|
|
903
|
+
const reqId = `${agentName}-${sessionId}-${timestamp}`;
|
|
897
904
|
upsertRequest(db, {
|
|
898
905
|
id: reqId,
|
|
899
|
-
agent:
|
|
906
|
+
agent: agentName,
|
|
900
907
|
session_id: sessionId,
|
|
901
908
|
model,
|
|
902
909
|
input_tokens: inputTokens,
|
|
@@ -916,7 +923,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
916
923
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
917
924
|
const session = {
|
|
918
925
|
id: sessionId,
|
|
919
|
-
agent:
|
|
926
|
+
agent: agentName,
|
|
920
927
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
921
928
|
project_name: detectedProject ? detectedProject.name : "",
|
|
922
929
|
started_at: timestamp,
|
|
@@ -932,7 +939,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
932
939
|
}
|
|
933
940
|
totalRequests++;
|
|
934
941
|
}
|
|
935
|
-
setIngestState(db,
|
|
942
|
+
setIngestState(db, agentName, stateKey, fileMtime);
|
|
936
943
|
totalFiles++;
|
|
937
944
|
}
|
|
938
945
|
}
|
|
@@ -1030,6 +1037,7 @@ export {
|
|
|
1030
1037
|
listMachines,
|
|
1031
1038
|
listGoals,
|
|
1032
1039
|
listBudgets,
|
|
1040
|
+
ingestTakumi,
|
|
1033
1041
|
ingestCodex,
|
|
1034
1042
|
ingestClaude,
|
|
1035
1043
|
getProject,
|
package/dist/ingest/claude.d.ts
CHANGED
|
@@ -4,4 +4,9 @@ export declare function ingestClaude(db: Database, verbose?: boolean, _telemetry
|
|
|
4
4
|
requests: number;
|
|
5
5
|
sessions: number;
|
|
6
6
|
}>;
|
|
7
|
+
export declare function ingestTakumi(db: Database, verbose?: boolean): Promise<{
|
|
8
|
+
files: number;
|
|
9
|
+
requests: number;
|
|
10
|
+
sessions: number;
|
|
11
|
+
}>;
|
|
7
12
|
//# sourceMappingURL=claude.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/ingest/claude.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/ingest/claude.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AA4D7D,wBAAsB,YAAY,CAChC,EAAE,EAAE,QAAQ,EACZ,OAAO,UAAQ,EACf,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEhE;AAED,wBAAsB,YAAY,CAChC,EAAE,EAAE,QAAQ,EACZ,OAAO,UAAQ,GACd,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEhE"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -553,6 +553,95 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
553
553
|
import { registerCloudTools } from "@hasna/cloud";
|
|
554
554
|
import { z } from "zod";
|
|
555
555
|
|
|
556
|
+
// src/db/pg-migrations.ts
|
|
557
|
+
var PG_MIGRATIONS = [
|
|
558
|
+
`CREATE TABLE IF NOT EXISTS requests (
|
|
559
|
+
id TEXT PRIMARY KEY,
|
|
560
|
+
agent TEXT NOT NULL,
|
|
561
|
+
session_id TEXT NOT NULL,
|
|
562
|
+
model TEXT NOT NULL,
|
|
563
|
+
input_tokens INTEGER DEFAULT 0,
|
|
564
|
+
output_tokens INTEGER DEFAULT 0,
|
|
565
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
566
|
+
cache_create_tokens INTEGER DEFAULT 0,
|
|
567
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
568
|
+
duration_ms INTEGER DEFAULT 0,
|
|
569
|
+
timestamp TEXT NOT NULL,
|
|
570
|
+
source_request_id TEXT,
|
|
571
|
+
machine_id TEXT DEFAULT ''
|
|
572
|
+
)`,
|
|
573
|
+
`CREATE TABLE IF NOT EXISTS sessions (
|
|
574
|
+
id TEXT PRIMARY KEY,
|
|
575
|
+
agent TEXT NOT NULL,
|
|
576
|
+
project_path TEXT DEFAULT '',
|
|
577
|
+
project_name TEXT DEFAULT '',
|
|
578
|
+
started_at TEXT NOT NULL,
|
|
579
|
+
ended_at TEXT,
|
|
580
|
+
total_cost_usd REAL DEFAULT 0,
|
|
581
|
+
total_tokens INTEGER DEFAULT 0,
|
|
582
|
+
request_count INTEGER DEFAULT 0,
|
|
583
|
+
machine_id TEXT DEFAULT ''
|
|
584
|
+
)`,
|
|
585
|
+
`CREATE TABLE IF NOT EXISTS projects (
|
|
586
|
+
id TEXT PRIMARY KEY,
|
|
587
|
+
path TEXT UNIQUE NOT NULL,
|
|
588
|
+
name TEXT NOT NULL,
|
|
589
|
+
description TEXT,
|
|
590
|
+
tags TEXT DEFAULT '[]',
|
|
591
|
+
created_at TEXT NOT NULL
|
|
592
|
+
)`,
|
|
593
|
+
`CREATE TABLE IF NOT EXISTS budgets (
|
|
594
|
+
id TEXT PRIMARY KEY,
|
|
595
|
+
project_path TEXT,
|
|
596
|
+
agent TEXT,
|
|
597
|
+
period TEXT NOT NULL,
|
|
598
|
+
limit_usd REAL NOT NULL,
|
|
599
|
+
alert_at_percent INTEGER DEFAULT 80,
|
|
600
|
+
created_at TEXT NOT NULL,
|
|
601
|
+
updated_at TEXT NOT NULL
|
|
602
|
+
)`,
|
|
603
|
+
`CREATE TABLE IF NOT EXISTS goals (
|
|
604
|
+
id TEXT PRIMARY KEY,
|
|
605
|
+
period TEXT NOT NULL,
|
|
606
|
+
project_path TEXT,
|
|
607
|
+
agent TEXT,
|
|
608
|
+
limit_usd REAL NOT NULL,
|
|
609
|
+
created_at TEXT NOT NULL,
|
|
610
|
+
updated_at TEXT NOT NULL
|
|
611
|
+
)`,
|
|
612
|
+
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
613
|
+
source TEXT NOT NULL,
|
|
614
|
+
key TEXT NOT NULL,
|
|
615
|
+
value TEXT NOT NULL,
|
|
616
|
+
PRIMARY KEY (source, key)
|
|
617
|
+
)`,
|
|
618
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
619
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
620
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
621
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
622
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
623
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
624
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
625
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
626
|
+
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
627
|
+
model TEXT PRIMARY KEY,
|
|
628
|
+
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
629
|
+
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
630
|
+
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
631
|
+
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
632
|
+
updated_at TEXT NOT NULL
|
|
633
|
+
)`,
|
|
634
|
+
`CREATE TABLE IF NOT EXISTS feedback (
|
|
635
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
636
|
+
message TEXT NOT NULL,
|
|
637
|
+
email TEXT,
|
|
638
|
+
category TEXT DEFAULT 'general',
|
|
639
|
+
version TEXT,
|
|
640
|
+
machine_id TEXT,
|
|
641
|
+
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
642
|
+
)`
|
|
643
|
+
];
|
|
644
|
+
|
|
556
645
|
// src/ingest/claude.ts
|
|
557
646
|
init_database();
|
|
558
647
|
init_pricing();
|
|
@@ -562,7 +651,8 @@ import { join as join2, basename } from "path";
|
|
|
562
651
|
function autoDetectProject(cwd, projects) {
|
|
563
652
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
564
653
|
}
|
|
565
|
-
var
|
|
654
|
+
var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
655
|
+
var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
|
|
566
656
|
function dirNameToPath(dirName) {
|
|
567
657
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
568
658
|
}
|
|
@@ -582,9 +672,15 @@ function collectJsonlFiles(projectDir) {
|
|
|
582
672
|
return files;
|
|
583
673
|
}
|
|
584
674
|
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
585
|
-
|
|
675
|
+
return ingestJsonlProjects(db, CLAUDE_PROJECTS_DIR, "claude", verbose);
|
|
676
|
+
}
|
|
677
|
+
async function ingestTakumi(db, verbose = false) {
|
|
678
|
+
return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
|
|
679
|
+
}
|
|
680
|
+
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
681
|
+
if (!existsSync2(projectsDir)) {
|
|
586
682
|
if (verbose)
|
|
587
|
-
console.log(
|
|
683
|
+
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
588
684
|
return { files: 0, requests: 0, sessions: 0 };
|
|
589
685
|
}
|
|
590
686
|
const machineId = getMachineId();
|
|
@@ -592,20 +688,20 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
592
688
|
let totalRequests = 0;
|
|
593
689
|
const touchedSessions = new Set;
|
|
594
690
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
595
|
-
const projectDirs = readdirSync2(
|
|
691
|
+
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
596
692
|
for (const projectDirEntry of projectDirs) {
|
|
597
|
-
const projectDirPath = join2(
|
|
693
|
+
const projectDirPath = join2(projectsDir, projectDirEntry.name);
|
|
598
694
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
599
695
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
600
696
|
for (const filePath of jsonlFiles) {
|
|
601
|
-
const stateKey = filePath.replace(
|
|
697
|
+
const stateKey = filePath.replace(projectsDir, "");
|
|
602
698
|
let fileMtime = "0";
|
|
603
699
|
try {
|
|
604
700
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
605
701
|
} catch {
|
|
606
702
|
continue;
|
|
607
703
|
}
|
|
608
|
-
const processed = getIngestState(db,
|
|
704
|
+
const processed = getIngestState(db, agentName, stateKey);
|
|
609
705
|
if (processed === fileMtime)
|
|
610
706
|
continue;
|
|
611
707
|
let lines;
|
|
@@ -646,10 +742,10 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
646
742
|
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
647
743
|
continue;
|
|
648
744
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
649
|
-
const reqId =
|
|
745
|
+
const reqId = `${agentName}-${sessionId}-${timestamp}`;
|
|
650
746
|
upsertRequest(db, {
|
|
651
747
|
id: reqId,
|
|
652
|
-
agent:
|
|
748
|
+
agent: agentName,
|
|
653
749
|
session_id: sessionId,
|
|
654
750
|
model,
|
|
655
751
|
input_tokens: inputTokens,
|
|
@@ -669,7 +765,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
669
765
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
670
766
|
const session = {
|
|
671
767
|
id: sessionId,
|
|
672
|
-
agent:
|
|
768
|
+
agent: agentName,
|
|
673
769
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
674
770
|
project_name: detectedProject ? detectedProject.name : "",
|
|
675
771
|
started_at: timestamp,
|
|
@@ -685,7 +781,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
685
781
|
}
|
|
686
782
|
totalRequests++;
|
|
687
783
|
}
|
|
688
|
-
setIngestState(db,
|
|
784
|
+
setIngestState(db, agentName, stateKey, fileMtime);
|
|
689
785
|
totalFiles++;
|
|
690
786
|
}
|
|
691
787
|
}
|
|
@@ -955,7 +1051,7 @@ server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, to
|
|
|
955
1051
|
`));
|
|
956
1052
|
});
|
|
957
1053
|
server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
|
|
958
|
-
agent: z.enum(["claude", "codex", "gemini"]).optional(),
|
|
1054
|
+
agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional(),
|
|
959
1055
|
project: z.string().optional(),
|
|
960
1056
|
machine: z.string().optional(),
|
|
961
1057
|
limit: z.number().int().positive().max(100).optional()
|
|
@@ -974,7 +1070,7 @@ server.tool("get_sessions", "List sessions. Returns compact table. Params: agent
|
|
|
974
1070
|
});
|
|
975
1071
|
server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
|
|
976
1072
|
n: z.number().int().positive().max(100).optional(),
|
|
977
|
-
agent: z.enum(["claude", "codex", "gemini"]).optional()
|
|
1073
|
+
agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional()
|
|
978
1074
|
}, async ({ n, agent }) => {
|
|
979
1075
|
const sessions = queryTopSessions(db, n ?? 10, agent);
|
|
980
1076
|
const lines = ["rank id agent cost tokens project"];
|
|
@@ -1055,13 +1151,17 @@ server.tool("get_session_detail", "Per-request breakdown of a single session. Pa
|
|
|
1055
1151
|
return text(lines.join(`
|
|
1056
1152
|
`));
|
|
1057
1153
|
});
|
|
1058
|
-
server.tool("sync", "Ingest new cost data. sources: all|claude|codex|gemini", { sources: z.enum(["all", "claude", "codex", "gemini"]).optional() }, async ({ sources }) => {
|
|
1154
|
+
server.tool("sync", "Ingest new cost data. sources: all|claude|takumi|codex|gemini", { sources: z.enum(["all", "claude", "takumi", "codex", "gemini"]).optional() }, async ({ sources }) => {
|
|
1059
1155
|
const selected = sources ?? "all";
|
|
1060
1156
|
const parts = [];
|
|
1061
1157
|
if (selected === "all" || selected === "claude") {
|
|
1062
1158
|
const result = await ingestClaude(db);
|
|
1063
1159
|
parts.push(`claude: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
|
|
1064
1160
|
}
|
|
1161
|
+
if (selected === "all" || selected === "takumi") {
|
|
1162
|
+
const result = await ingestTakumi(db);
|
|
1163
|
+
parts.push(`takumi: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
|
|
1164
|
+
}
|
|
1065
1165
|
if (selected === "all" || selected === "codex") {
|
|
1066
1166
|
const result = await ingestCodex(db);
|
|
1067
1167
|
parts.push(`codex: ${result["sessions"]} sessions`);
|
|
@@ -1161,5 +1261,8 @@ server.tool("send_feedback", "Send feedback about this service.", {
|
|
|
1161
1261
|
}
|
|
1162
1262
|
});
|
|
1163
1263
|
var transport = new StdioServerTransport;
|
|
1164
|
-
registerCloudTools(server, "economy"
|
|
1264
|
+
registerCloudTools(server, "economy", {
|
|
1265
|
+
dbPath: getDbPath(),
|
|
1266
|
+
migrations: PG_MIGRATIONS
|
|
1267
|
+
});
|
|
1165
1268
|
await server.connect(transport);
|
package/dist/server/index.js
CHANGED
|
@@ -586,7 +586,8 @@ import { join as join2, basename } from "path";
|
|
|
586
586
|
function autoDetectProject(cwd, projects) {
|
|
587
587
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
588
588
|
}
|
|
589
|
-
var
|
|
589
|
+
var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
590
|
+
var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
|
|
590
591
|
function dirNameToPath(dirName) {
|
|
591
592
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
592
593
|
}
|
|
@@ -606,9 +607,15 @@ function collectJsonlFiles(projectDir) {
|
|
|
606
607
|
return files;
|
|
607
608
|
}
|
|
608
609
|
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
609
|
-
|
|
610
|
+
return ingestJsonlProjects(db, CLAUDE_PROJECTS_DIR, "claude", verbose);
|
|
611
|
+
}
|
|
612
|
+
async function ingestTakumi(db, verbose = false) {
|
|
613
|
+
return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
|
|
614
|
+
}
|
|
615
|
+
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
616
|
+
if (!existsSync2(projectsDir)) {
|
|
610
617
|
if (verbose)
|
|
611
|
-
console.log(
|
|
618
|
+
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
612
619
|
return { files: 0, requests: 0, sessions: 0 };
|
|
613
620
|
}
|
|
614
621
|
const machineId = getMachineId();
|
|
@@ -616,20 +623,20 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
616
623
|
let totalRequests = 0;
|
|
617
624
|
const touchedSessions = new Set;
|
|
618
625
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
619
|
-
const projectDirs = readdirSync2(
|
|
626
|
+
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
620
627
|
for (const projectDirEntry of projectDirs) {
|
|
621
|
-
const projectDirPath = join2(
|
|
628
|
+
const projectDirPath = join2(projectsDir, projectDirEntry.name);
|
|
622
629
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
623
630
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
624
631
|
for (const filePath of jsonlFiles) {
|
|
625
|
-
const stateKey = filePath.replace(
|
|
632
|
+
const stateKey = filePath.replace(projectsDir, "");
|
|
626
633
|
let fileMtime = "0";
|
|
627
634
|
try {
|
|
628
635
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
629
636
|
} catch {
|
|
630
637
|
continue;
|
|
631
638
|
}
|
|
632
|
-
const processed = getIngestState(db,
|
|
639
|
+
const processed = getIngestState(db, agentName, stateKey);
|
|
633
640
|
if (processed === fileMtime)
|
|
634
641
|
continue;
|
|
635
642
|
let lines;
|
|
@@ -670,10 +677,10 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
670
677
|
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
671
678
|
continue;
|
|
672
679
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
673
|
-
const reqId =
|
|
680
|
+
const reqId = `${agentName}-${sessionId}-${timestamp}`;
|
|
674
681
|
upsertRequest(db, {
|
|
675
682
|
id: reqId,
|
|
676
|
-
agent:
|
|
683
|
+
agent: agentName,
|
|
677
684
|
session_id: sessionId,
|
|
678
685
|
model,
|
|
679
686
|
input_tokens: inputTokens,
|
|
@@ -693,7 +700,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
693
700
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
694
701
|
const session = {
|
|
695
702
|
id: sessionId,
|
|
696
|
-
agent:
|
|
703
|
+
agent: agentName,
|
|
697
704
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
698
705
|
project_name: detectedProject ? detectedProject.name : "",
|
|
699
706
|
started_at: timestamp,
|
|
@@ -709,7 +716,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
709
716
|
}
|
|
710
717
|
totalRequests++;
|
|
711
718
|
}
|
|
712
|
-
setIngestState(db,
|
|
719
|
+
setIngestState(db, agentName, stateKey, fileMtime);
|
|
713
720
|
totalFiles++;
|
|
714
721
|
}
|
|
715
722
|
}
|
|
@@ -1018,6 +1025,8 @@ function createHandler(db) {
|
|
|
1018
1025
|
const results = {};
|
|
1019
1026
|
if (sources === "all" || sources === "claude")
|
|
1020
1027
|
results["claude"] = await ingestClaude(db);
|
|
1028
|
+
if (sources === "all" || sources === "takumi")
|
|
1029
|
+
results["takumi"] = await ingestTakumi(db);
|
|
1021
1030
|
if (sources === "all" || sources === "codex")
|
|
1022
1031
|
results["codex"] = await ingestCodex(db);
|
|
1023
1032
|
if (sources === "all" || sources === "gemini")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AA4D7D,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AA4D7D,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAiM/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,GAAG,IAAI,CAuC7C"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED