@hasna/economy 0.2.12 → 0.2.13

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 CHANGED
@@ -609,9 +609,15 @@ function collectJsonlFiles(projectDir) {
609
609
  return files;
610
610
  }
611
611
  async function ingestClaude(db, verbose = false, _telemetryDir) {
612
- if (!existsSync3(PROJECTS_DIR)) {
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("Claude projects dir not found:", PROJECTS_DIR);
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(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
628
+ const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
623
629
  for (const projectDirEntry of projectDirs) {
624
- const projectDirPath = join4(PROJECTS_DIR, projectDirEntry.name);
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(PROJECTS_DIR, "");
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, "claude", stateKey);
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 = `claude-${sessionId}-${timestamp}`;
682
+ const reqId = `${agentName}-${sessionId}-${timestamp}`;
677
683
  upsertRequest(db, {
678
684
  id: reqId,
679
- agent: "claude",
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: "claude",
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, "claude", stateKey, fileMtime);
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 PROJECTS_DIR;
730
+ var CLAUDE_PROJECTS_DIR, TAKUMI_PROJECTS_DIR;
725
731
  var init_claude = __esm(() => {
726
732
  init_database();
727
733
  init_pricing();
728
- PROJECTS_DIR = join4(homedir2(), ".claude", "projects");
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,145 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
2746
2858
  process.exit(1);
2747
2859
  }
2748
2860
  });
2861
+ var cloudCmd = program.command("cloud").description("Cross-machine sync via cloud PostgreSQL");
2862
+ cloudCmd.command("push").description("Push local economy data to cloud PostgreSQL").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
2863
+ const { syncPush, PgAdapterAsync, getCloudConfig, SqliteAdapter } = await import("@hasna/cloud");
2864
+ const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
2865
+ const config = getCloudConfig();
2866
+ if (!config.rds?.host) {
2867
+ console.error(chalk4.red("Cloud not configured. Set RDS host in ~/.hasna/cloud.json"));
2868
+ process.exit(1);
2869
+ }
2870
+ const connStr = `postgresql://${config.rds.username}:${process.env[config.rds.password_env] ?? ""}@${config.rds.host}:${config.rds.port ?? 5432}/economy?sslmode=require`;
2871
+ const local = new SqliteAdapter(getDbPath());
2872
+ const cloud = new PgAdapterAsync(connStr);
2873
+ process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
2874
+ for (const sql of PG_MIGRATIONS2) {
2875
+ await cloud.run(sql);
2876
+ }
2877
+ console.log(chalk4.green("\u2713"));
2878
+ const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : ["requests", "sessions", "projects", "budgets", "goals", "model_pricing"];
2879
+ process.stdout.write(chalk4.cyan(`\u2192 Pushing ${tableList.join(", ")}... `));
2880
+ const results = await syncPush(local, cloud, { tables: tableList });
2881
+ const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
2882
+ console.log(chalk4.green(`\u2713 ${totalRows} rows across ${tableList.length} tables`));
2883
+ local.close();
2884
+ await cloud.close();
2885
+ console.log(chalk4.bold.green(`
2886
+ \u2713 Push complete from ${getMachineId()}`));
2887
+ });
2888
+ cloudCmd.command("pull").description("Pull cloud PostgreSQL data to local").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
2889
+ const { syncPull, PgAdapterAsync, getCloudConfig, SqliteAdapter } = await import("@hasna/cloud");
2890
+ const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
2891
+ const config = getCloudConfig();
2892
+ if (!config.rds?.host) {
2893
+ console.error(chalk4.red("Cloud not configured. Set RDS host in ~/.hasna/cloud.json"));
2894
+ process.exit(1);
2895
+ }
2896
+ const connStr = `postgresql://${config.rds.username}:${process.env[config.rds.password_env] ?? ""}@${config.rds.host}:${config.rds.port ?? 5432}/economy?sslmode=require`;
2897
+ const local = new SqliteAdapter(getDbPath());
2898
+ const cloud = new PgAdapterAsync(connStr);
2899
+ process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
2900
+ for (const sql of PG_MIGRATIONS2) {
2901
+ await cloud.run(sql);
2902
+ }
2903
+ console.log(chalk4.green("\u2713"));
2904
+ const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : ["requests", "sessions", "projects", "budgets", "goals", "model_pricing"];
2905
+ process.stdout.write(chalk4.cyan(`\u2192 Pulling ${tableList.join(", ")}... `));
2906
+ const results = await syncPull(cloud, local, { tables: tableList });
2907
+ const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
2908
+ console.log(chalk4.green(`\u2713 ${totalRows} rows across ${tableList.length} tables`));
2909
+ local.close();
2910
+ await cloud.close();
2911
+ console.log(chalk4.bold.green(`
2912
+ \u2713 Pull complete to ${getMachineId()}`));
2913
+ });
2914
+ cloudCmd.command("sync").description("Full sync: ingest local, then merge data from all reachable machines via SSH").option("--machines <list>", "Comma-separated machine hostnames (default: spark01,apple01,apple03)").action(async (opts) => {
2915
+ const thisMachine = getMachineId();
2916
+ console.log(chalk4.bold.cyan(` Cloud Sync \u2014 ${thisMachine}
2917
+ `));
2918
+ process.stdout.write(chalk4.cyan("\u2192 Ingesting local data... "));
2919
+ await autoSync();
2920
+ console.log(chalk4.green("\u2713"));
2921
+ const allMachines = (opts.machines ?? "spark01,apple01,apple03").split(",").map((m) => m.trim());
2922
+ const remoteMachines = allMachines.filter((m) => m !== thisMachine);
2923
+ const db = openDatabase();
2924
+ const { existsSync: existsSync8, mkdirSync: mkdirSync4, unlinkSync } = await import("fs");
2925
+ const { join: join9 } = await import("path");
2926
+ const { execSync: exec } = await import("child_process");
2927
+ const tmpDir = join9(process.env["TMPDIR"] ?? "/tmp", "economy-sync");
2928
+ mkdirSync4(tmpDir, { recursive: true });
2929
+ const isLinux = process.platform === "linux";
2930
+ const remoteDbPath = isLinux ? ".hasna/economy/economy.db" : ".hasna/economy/economy.db";
2931
+ for (const machine of remoteMachines) {
2932
+ const localCopy = join9(tmpDir, `${machine}.db`);
2933
+ process.stdout.write(chalk4.cyan(`\u2192 Fetching from ${machine}... `));
2934
+ try {
2935
+ exec(`scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no ${machine}:~/${remoteDbPath} "${localCopy}" 2>/dev/null`, { timeout: 30000 });
2936
+ if (!existsSync8(localCopy)) {
2937
+ console.log(chalk4.yellow("skipped (no file)"));
2938
+ continue;
2939
+ }
2940
+ const { SqliteAdapter } = await import("@hasna/cloud");
2941
+ const remoteDb = new SqliteAdapter(localCopy);
2942
+ remoteDb.exec("PRAGMA busy_timeout = 5000");
2943
+ const sessions = remoteDb.prepare("SELECT * FROM sessions").all();
2944
+ let sCount = 0;
2945
+ for (const s of sessions) {
2946
+ const existing = db.prepare("SELECT id FROM sessions WHERE id = ?").get(s["id"]);
2947
+ if (!existing) {
2948
+ db.prepare(`INSERT OR IGNORE INTO sessions (id, agent, project_path, project_name, started_at, ended_at, total_cost_usd, total_tokens, request_count, machine_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(s["id"], s["agent"], s["project_path"], s["project_name"], s["started_at"], s["ended_at"], s["total_cost_usd"], s["total_tokens"], s["request_count"], s["machine_id"] || machine);
2949
+ sCount++;
2950
+ }
2951
+ }
2952
+ let rCount = 0;
2953
+ try {
2954
+ const cols = remoteDb.prepare("PRAGMA table_info(requests)").all();
2955
+ const hasMachineCol = cols.some((c) => c.name === "machine_id");
2956
+ const requests = remoteDb.prepare("SELECT * FROM requests").all();
2957
+ for (const r of requests) {
2958
+ const existing = db.prepare("SELECT id FROM requests WHERE id = ?").get(r["id"]);
2959
+ if (!existing) {
2960
+ db.prepare(`INSERT OR IGNORE INTO requests (id, agent, session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cost_usd, duration_ms, timestamp, source_request_id, machine_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(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"], r["source_request_id"], hasMachineCol ? r["machine_id"] || machine : machine);
2961
+ rCount++;
2962
+ }
2963
+ }
2964
+ } catch {}
2965
+ remoteDb.close();
2966
+ try {
2967
+ unlinkSync(localCopy);
2968
+ } catch {}
2969
+ console.log(chalk4.green(`\u2713 ${sCount} sessions, ${rCount} requests`));
2970
+ } catch (e) {
2971
+ console.log(chalk4.yellow(`skipped (${e instanceof Error ? e.message.split(`
2972
+ `)[0] : "unreachable"})`));
2973
+ try {
2974
+ unlinkSync(localCopy);
2975
+ } catch {}
2976
+ }
2977
+ }
2978
+ console.log(chalk4.bold.green(`
2979
+ \u2713 Cloud sync complete`));
2980
+ });
2981
+ cloudCmd.command("status").description("Check cloud connection status").action(async () => {
2982
+ const { PgAdapterAsync, getCloudConfig } = await import("@hasna/cloud");
2983
+ const config = getCloudConfig();
2984
+ console.log();
2985
+ console.log(` Mode: ${chalk4.white(config.mode)}`);
2986
+ console.log(` Machine: ${chalk4.white(getMachineId())}`);
2987
+ console.log(` RDS Host: ${chalk4.white(config.rds?.host || "(not configured)")}`);
2988
+ if (config.rds?.host && config.rds?.username) {
2989
+ try {
2990
+ const connStr = `postgresql://${config.rds.username}:${process.env[config.rds.password_env] ?? ""}@${config.rds.host}:${config.rds.port ?? 5432}/economy?sslmode=require`;
2991
+ const pg = new PgAdapterAsync(connStr);
2992
+ await pg.get("SELECT 1 as ok");
2993
+ console.log(` PostgreSQL: ${chalk4.green("connected")}`);
2994
+ await pg.close();
2995
+ } catch (err2) {
2996
+ console.log(` PostgreSQL: ${chalk4.red(`failed \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`)}`);
2997
+ }
2998
+ }
2999
+ console.log();
3000
+ });
2749
3001
  registerBrainsCommand(program);
2750
3002
  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 PROJECTS_DIR = join3(homedir2(), ".claude", "projects");
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
- if (!existsSync3(PROJECTS_DIR)) {
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("Claude projects dir not found:", PROJECTS_DIR);
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(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
849
+ const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
843
850
  for (const projectDirEntry of projectDirs) {
844
- const projectDirPath = join3(PROJECTS_DIR, projectDirEntry.name);
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(PROJECTS_DIR, "");
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, "claude", stateKey);
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 = `claude-${sessionId}-${timestamp}`;
903
+ const reqId = `${agentName}-${sessionId}-${timestamp}`;
897
904
  upsertRequest(db, {
898
905
  id: reqId,
899
- agent: "claude",
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: "claude",
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, "claude", stateKey, fileMtime);
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,
@@ -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;AA2D7D,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,CA8HhE"}
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 PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
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
- if (!existsSync2(PROJECTS_DIR)) {
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("Claude projects dir not found:", PROJECTS_DIR);
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(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
691
+ const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
596
692
  for (const projectDirEntry of projectDirs) {
597
- const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
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(PROJECTS_DIR, "");
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, "claude", stateKey);
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 = `claude-${sessionId}-${timestamp}`;
745
+ const reqId = `${agentName}-${sessionId}-${timestamp}`;
650
746
  upsertRequest(db, {
651
747
  id: reqId,
652
- agent: "claude",
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: "claude",
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, "claude", stateKey, fileMtime);
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);
@@ -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 PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
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
- if (!existsSync2(PROJECTS_DIR)) {
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("Claude projects dir not found:", PROJECTS_DIR);
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(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
626
+ const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
620
627
  for (const projectDirEntry of projectDirs) {
621
- const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
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(PROJECTS_DIR, "");
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, "claude", stateKey);
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 = `claude-${sessionId}-${timestamp}`;
680
+ const reqId = `${agentName}-${sessionId}-${timestamp}`;
674
681
  upsertRequest(db, {
675
682
  id: reqId,
676
- agent: "claude",
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: "claude",
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, "claude", stateKey, fileMtime);
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,CAgM/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,GAAG,IAAI,CAuC7C"}
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"}
@@ -1,4 +1,4 @@
1
- export type Agent = 'claude' | 'codex' | 'gemini';
1
+ export type Agent = 'claude' | 'codex' | 'gemini' | 'takumi';
2
2
  export type Period = 'today' | 'yesterday' | 'week' | 'month' | 'year' | 'all';
3
3
  export interface EconomyRequest {
4
4
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, and Gemini",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",