@hasna/economy 0.2.11 → 0.2.12

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
@@ -109,8 +109,17 @@ var init_pricing = __esm(() => {
109
109
  // src/db/database.ts
110
110
  import { SqliteAdapter as Database } from "@hasna/cloud";
111
111
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
112
+ import { hostname } from "os";
112
113
  import { homedir } from "os";
113
114
  import { join } from "path";
115
+ function getMachineId() {
116
+ if (process.env["ECONOMY_MACHINE_ID"])
117
+ return process.env["ECONOMY_MACHINE_ID"];
118
+ const h = hostname().toLowerCase();
119
+ if (h.startsWith("spark") || h.startsWith("apple"))
120
+ return h.split(".")[0];
121
+ return h.split(".")[0];
122
+ }
114
123
  function getDataDir() {
115
124
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
116
125
  const newDir = join(home, ".hasna", "economy");
@@ -143,6 +152,7 @@ function openDatabase(dbPath, skipSeed = false) {
143
152
  }
144
153
  const db = new Database(path);
145
154
  db.exec("PRAGMA journal_mode = WAL");
155
+ db.exec("PRAGMA busy_timeout = 5000");
146
156
  db.exec("PRAGMA foreign_keys = ON");
147
157
  initSchema(db);
148
158
  if (!skipSeed) {
@@ -164,7 +174,8 @@ function initSchema(db) {
164
174
  cost_usd REAL NOT NULL DEFAULT 0,
165
175
  duration_ms INTEGER DEFAULT 0,
166
176
  timestamp TEXT NOT NULL,
167
- source_request_id TEXT
177
+ source_request_id TEXT,
178
+ machine_id TEXT DEFAULT ''
168
179
  );
169
180
 
170
181
  CREATE TABLE IF NOT EXISTS sessions (
@@ -176,7 +187,8 @@ function initSchema(db) {
176
187
  ended_at TEXT,
177
188
  total_cost_usd REAL DEFAULT 0,
178
189
  total_tokens INTEGER DEFAULT 0,
179
- request_count INTEGER DEFAULT 0
190
+ request_count INTEGER DEFAULT 0,
191
+ machine_id TEXT DEFAULT ''
180
192
  );
181
193
 
182
194
  CREATE TABLE IF NOT EXISTS projects (
@@ -242,6 +254,15 @@ function initSchema(db) {
242
254
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
243
255
  );
244
256
  `);
257
+ const cols = db.prepare(`PRAGMA table_info(requests)`).all();
258
+ if (!cols.some((c) => c.name === "machine_id")) {
259
+ db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
260
+ db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
261
+ }
262
+ db.exec(`
263
+ CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
264
+ CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
265
+ `);
245
266
  }
246
267
  function periodWhere(period) {
247
268
  switch (period) {
@@ -280,17 +301,17 @@ function upsertRequest(db, req) {
280
301
  INSERT OR REPLACE INTO requests
281
302
  (id, agent, session_id, model, input_tokens, output_tokens,
282
303
  cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
283
- timestamp, source_request_id)
284
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
285
- `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id);
304
+ timestamp, source_request_id, machine_id)
305
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
306
+ `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "");
286
307
  }
287
308
  function upsertSession(db, session) {
288
309
  db.prepare(`
289
310
  INSERT OR REPLACE INTO sessions
290
311
  (id, agent, project_path, project_name, started_at, ended_at,
291
- total_cost_usd, total_tokens, request_count)
292
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
293
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count);
312
+ total_cost_usd, total_tokens, request_count, machine_id)
313
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
314
+ `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "");
294
315
  }
295
316
  function rollupSession(db, sessionId) {
296
317
  db.prepare(`
@@ -320,6 +341,10 @@ function querySessions(db, filter = {}) {
320
341
  conditions.push("started_at >= ?");
321
342
  params.push(filter.since);
322
343
  }
344
+ if (filter.machine) {
345
+ conditions.push("machine_id = ?");
346
+ params.push(filter.machine);
347
+ }
323
348
  if (filter.search) {
324
349
  const q = `%${filter.search}%`;
325
350
  conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
@@ -338,24 +363,25 @@ function queryTopSessions(db, n = 10, agent) {
338
363
  }
339
364
  return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
340
365
  }
341
- function querySummary(db, period) {
366
+ function querySummary(db, period, machine) {
342
367
  const rWhere = periodWhere(period);
343
368
  const sWhere = sessionPeriodWhere(period);
369
+ const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
344
370
  const r = db.prepare(`
345
371
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
346
372
  COUNT(*) as requests,
347
373
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
348
- FROM requests WHERE ${rWhere}
374
+ FROM requests WHERE ${rWhere}${machineClause}
349
375
  `).get();
350
376
  const codexTotals = db.prepare(`
351
377
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
352
378
  COALESCE(SUM(total_tokens), 0) as tokens,
353
379
  COUNT(*) as sessions
354
380
  FROM sessions
355
- WHERE ${sWhere}
381
+ WHERE ${sWhere}${machineClause}
356
382
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
357
383
  `).get();
358
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
384
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
359
385
  return {
360
386
  total_usd: r.total_usd + codexTotals.cost_usd,
361
387
  requests: r.requests,
@@ -509,6 +535,20 @@ function setIngestState(db, source, key, value) {
509
535
  function queryRequestsSince(db, since) {
510
536
  return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
511
537
  }
538
+ function listMachines(db) {
539
+ return db.prepare(`
540
+ SELECT
541
+ s.machine_id,
542
+ COUNT(DISTINCT s.id) as sessions,
543
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
544
+ COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
545
+ MAX(s.started_at) as last_active
546
+ FROM sessions s
547
+ WHERE s.machine_id != ''
548
+ GROUP BY s.machine_id
549
+ ORDER BY total_cost_usd DESC
550
+ `).all();
551
+ }
512
552
  function upsertModelPricing(db, p) {
513
553
  db.prepare(`
514
554
  INSERT OR REPLACE INTO model_pricing
@@ -574,6 +614,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
574
614
  console.log("Claude projects dir not found:", PROJECTS_DIR);
575
615
  return { files: 0, requests: 0, sessions: 0 };
576
616
  }
617
+ const machineId = getMachineId();
577
618
  let totalFiles = 0;
578
619
  let totalRequests = 0;
579
620
  const touchedSessions = new Set;
@@ -645,7 +686,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
645
686
  cost_usd: costUsd,
646
687
  duration_ms: 0,
647
688
  timestamp,
648
- source_request_id: reqId
689
+ source_request_id: reqId,
690
+ machine_id: machineId
649
691
  });
650
692
  if (!touchedSessions.has(sessionId)) {
651
693
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
@@ -661,7 +703,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
661
703
  ended_at: null,
662
704
  total_cost_usd: 0,
663
705
  total_tokens: 0,
664
- request_count: 0
706
+ request_count: 0,
707
+ machine_id: machineId
665
708
  };
666
709
  upsertSession(db, session);
667
710
  }
@@ -696,6 +739,7 @@ async function ingestCodex(db, verbose = false) {
696
739
  console.log("Codex DB not found:", CODEX_DB_PATH);
697
740
  return { sessions: 0 };
698
741
  }
742
+ const machineId = getMachineId();
699
743
  let codexDb = null;
700
744
  let ingested = 0;
701
745
  try {
@@ -720,7 +764,8 @@ async function ingestCodex(db, verbose = false) {
720
764
  ended_at: endedAt,
721
765
  total_cost_usd: costUsd,
722
766
  total_tokens: thread.tokens_used,
723
- request_count: 1
767
+ request_count: 1,
768
+ machine_id: machineId
724
769
  });
725
770
  setIngestState(db, "codex", stateKey, "done");
726
771
  ingested++;
@@ -749,6 +794,7 @@ async function ingestGemini(db, verbose) {
749
794
  console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
750
795
  return { sessions: 0 };
751
796
  }
797
+ const machineId = getMachineId();
752
798
  let totalSessions = 0;
753
799
  const touchedSessions = new Set;
754
800
  let projectHashDirs = [];
@@ -799,7 +845,8 @@ async function ingestGemini(db, verbose) {
799
845
  ended_at: chatData.lastUpdated ?? null,
800
846
  total_cost_usd: 0,
801
847
  total_tokens: 0,
802
- request_count: 0
848
+ request_count: 0,
849
+ machine_id: machineId
803
850
  };
804
851
  upsertSession(db, session);
805
852
  touchedSessions.add(sessionId);
@@ -1072,7 +1119,11 @@ function createHandler(db) {
1072
1119
  return ok({ status: "ok", ts: new Date().toISOString() });
1073
1120
  if (path === "/api/summary" && method === "GET") {
1074
1121
  const period = url.searchParams.get("period") ?? "today";
1075
- return ok(querySummary(db, period));
1122
+ const machine = url.searchParams.get("machine") ?? undefined;
1123
+ return ok(querySummary(db, period, machine));
1124
+ }
1125
+ if (path === "/api/machines" && method === "GET") {
1126
+ return ok(listMachines(db), { current_machine: getMachineId() });
1076
1127
  }
1077
1128
  if (path === "/api/daily" && method === "GET") {
1078
1129
  const days = Number(url.searchParams.get("days") ?? 30);
@@ -1082,6 +1133,7 @@ function createHandler(db) {
1082
1133
  const agent = url.searchParams.get("agent");
1083
1134
  const project = url.searchParams.get("project") ?? undefined;
1084
1135
  const search = url.searchParams.get("search") ?? undefined;
1136
+ const machine = url.searchParams.get("machine") ?? undefined;
1085
1137
  const limit = Number(url.searchParams.get("limit") ?? 50);
1086
1138
  const offset = Number(url.searchParams.get("offset") ?? 0);
1087
1139
  const since = url.searchParams.get("since") ?? undefined;
@@ -1091,6 +1143,7 @@ function createHandler(db) {
1091
1143
  agent: agent ?? undefined,
1092
1144
  project,
1093
1145
  search,
1146
+ machine,
1094
1147
  limit,
1095
1148
  offset,
1096
1149
  since
@@ -1895,7 +1948,7 @@ program.action(async () => {
1895
1948
  }
1896
1949
  console.log();
1897
1950
  });
1898
- 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)").action(async (opts) => {
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) => {
1899
1952
  const db = openDatabase();
1900
1953
  ensurePricingSeeded(db);
1901
1954
  if (opts.force) {
@@ -1922,6 +1975,12 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
1922
1975
  const r = await ingestGemini(db, opts.verbose);
1923
1976
  console.log(chalk4.green(`\u2713 ${r.sessions} sessions`));
1924
1977
  }
1978
+ if (opts.backfillMachine) {
1979
+ const machine = getMachineId();
1980
+ const reqCount = db.prepare(`UPDATE requests SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
1981
+ const sessCount = db.prepare(`UPDATE sessions SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
1982
+ console.log(chalk4.cyan(`\u2192 Backfilled machine_id='${machine}': ${reqCount.changes} requests, ${sessCount.changes} sessions`));
1983
+ }
1925
1984
  try {
1926
1985
  const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
1927
1986
  await checkAndFireWebhooks2(db);
@@ -1941,13 +2000,14 @@ program.command("month").description("Cost summary for this month").action(async
1941
2000
  await autoSync();
1942
2001
  printSummary("This Month", "month");
1943
2002
  });
1944
- 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").option("--format <fmt>", "Output format: table|compact|csv|json", "table").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--search <query>", "Search by project name, session id prefix, or agent").action(async (opts) => {
2003
+ 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("--machine <id>", "Filter by machine hostname (e.g. spark01, apple01)").option("--limit <n>", "Number of sessions", "20").option("--format <fmt>", "Output format: table|compact|csv|json", "table").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--search <query>", "Search by project name, session id prefix, or agent").action(async (opts) => {
1945
2004
  await autoSync();
1946
2005
  const db = openDatabase();
1947
2006
  const sinceDate = opts.since ? parseSinceDate(opts.since) : undefined;
1948
2007
  let sessions = querySessions(db, {
1949
2008
  agent: opts.agent,
1950
2009
  project: opts.project,
2010
+ machine: opts.machine,
1951
2011
  limit: Number(opts.limit ?? 20),
1952
2012
  since: sinceDate,
1953
2013
  search: opts.search
@@ -2396,6 +2456,29 @@ program.command("session <id>").description("Show detailed breakdown of a single
2396
2456
  }
2397
2457
  console.log();
2398
2458
  });
2459
+ program.command("machines").description("List all machines that have synced data").action(async () => {
2460
+ await autoSync();
2461
+ const db = openDatabase();
2462
+ const machines = listMachines(db);
2463
+ const current = getMachineId();
2464
+ if (machines.length === 0) {
2465
+ console.log(chalk4.yellow(`No machine data yet. Current machine: ${current}`));
2466
+ return;
2467
+ }
2468
+ console.log();
2469
+ console.log(chalk4.bold.cyan(" Machines"));
2470
+ console.log();
2471
+ printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], machines.map((m) => [
2472
+ m.machine_id === current ? chalk4.green(`${m.machine_id} (this)`) : chalk4.white(m.machine_id),
2473
+ fmtCount(m.sessions),
2474
+ fmtCount(m.requests),
2475
+ fmt2(m.total_cost_usd),
2476
+ chalk4.dim(m.last_active?.substring(0, 16) ?? "\u2014")
2477
+ ]));
2478
+ console.log(`
2479
+ ${chalk4.dim("Current machine:")} ${chalk4.bold(current)}`);
2480
+ console.log();
2481
+ });
2399
2482
  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) => {
2400
2483
  await autoSync();
2401
2484
  const db = openDatabase();
@@ -1,5 +1,6 @@
1
1
  import { SqliteAdapter as Database } from '@hasna/cloud';
2
2
  import type { EconomyRequest, EconomySession, EconomyProject, Budget, BudgetStatus, CostSummary, ModelBreakdown, ProjectBreakdown, Period, SessionFilter } from '../types/index.js';
3
+ export declare function getMachineId(): string;
3
4
  export declare function getDataDir(): string;
4
5
  export declare function getDbPath(): string;
5
6
  export declare function openDatabase(dbPath?: string, skipSeed?: boolean): Database;
@@ -8,7 +9,7 @@ export declare function upsertSession(db: Database, session: EconomySession): vo
8
9
  export declare function rollupSession(db: Database, sessionId: string): void;
9
10
  export declare function querySessions(db: Database, filter?: SessionFilter): EconomySession[];
10
11
  export declare function queryTopSessions(db: Database, n?: number, agent?: string): EconomySession[];
11
- export declare function querySummary(db: Database, period: Period): CostSummary;
12
+ export declare function querySummary(db: Database, period: Period, machine?: string): CostSummary;
12
13
  export declare function queryModelBreakdown(db: Database): ModelBreakdown[];
13
14
  export declare function queryProjectBreakdown(db: Database): ProjectBreakdown[];
14
15
  export declare function queryDailyBreakdown(db: Database, days?: number): Array<{
@@ -47,6 +48,14 @@ export declare function getGoalStatuses(db: Database): GoalStatus[];
47
48
  export declare function getIngestState(db: Database, source: string, key: string): string | null;
48
49
  export declare function setIngestState(db: Database, source: string, key: string, value: string): void;
49
50
  export declare function queryRequestsSince(db: Database, since: string): EconomyRequest[];
51
+ export interface MachineInfo {
52
+ machine_id: string;
53
+ sessions: number;
54
+ requests: number;
55
+ total_cost_usd: number;
56
+ last_active: string;
57
+ }
58
+ export declare function listMachines(db: Database): MachineInfo[];
50
59
  export interface DbModelPricing {
51
60
  model: string;
52
61
  input_per_1m: number;
@@ -1 +1 @@
1
- {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"pg-migrations.d.ts","sourceRoot":"","sources":["../../src/db/pg-migrations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,aAAa,EAAE,MAAM,EAmGjC,CAAC"}
1
+ {"version":3,"file":"pg-migrations.d.ts","sourceRoot":"","sources":["../../src/db/pg-migrations.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,aAAa,EAAE,MAAM,EAuGjC,CAAC"}
package/dist/index.js CHANGED
@@ -107,8 +107,17 @@ var init_pricing = __esm(() => {
107
107
  // src/db/database.ts
108
108
  import { SqliteAdapter as Database } from "@hasna/cloud";
109
109
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
110
+ import { hostname } from "os";
110
111
  import { homedir } from "os";
111
112
  import { join } from "path";
113
+ function getMachineId() {
114
+ if (process.env["ECONOMY_MACHINE_ID"])
115
+ return process.env["ECONOMY_MACHINE_ID"];
116
+ const h = hostname().toLowerCase();
117
+ if (h.startsWith("spark") || h.startsWith("apple"))
118
+ return h.split(".")[0];
119
+ return h.split(".")[0];
120
+ }
112
121
  function getDataDir() {
113
122
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
114
123
  const newDir = join(home, ".hasna", "economy");
@@ -141,6 +150,7 @@ function openDatabase(dbPath, skipSeed = false) {
141
150
  }
142
151
  const db = new Database(path);
143
152
  db.exec("PRAGMA journal_mode = WAL");
153
+ db.exec("PRAGMA busy_timeout = 5000");
144
154
  db.exec("PRAGMA foreign_keys = ON");
145
155
  initSchema(db);
146
156
  if (!skipSeed) {
@@ -162,7 +172,8 @@ function initSchema(db) {
162
172
  cost_usd REAL NOT NULL DEFAULT 0,
163
173
  duration_ms INTEGER DEFAULT 0,
164
174
  timestamp TEXT NOT NULL,
165
- source_request_id TEXT
175
+ source_request_id TEXT,
176
+ machine_id TEXT DEFAULT ''
166
177
  );
167
178
 
168
179
  CREATE TABLE IF NOT EXISTS sessions (
@@ -174,7 +185,8 @@ function initSchema(db) {
174
185
  ended_at TEXT,
175
186
  total_cost_usd REAL DEFAULT 0,
176
187
  total_tokens INTEGER DEFAULT 0,
177
- request_count INTEGER DEFAULT 0
188
+ request_count INTEGER DEFAULT 0,
189
+ machine_id TEXT DEFAULT ''
178
190
  );
179
191
 
180
192
  CREATE TABLE IF NOT EXISTS projects (
@@ -240,6 +252,15 @@ function initSchema(db) {
240
252
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
241
253
  );
242
254
  `);
255
+ const cols = db.prepare(`PRAGMA table_info(requests)`).all();
256
+ if (!cols.some((c) => c.name === "machine_id")) {
257
+ db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
258
+ db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
259
+ }
260
+ db.exec(`
261
+ CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
262
+ CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
263
+ `);
243
264
  }
244
265
  function periodWhere(period) {
245
266
  switch (period) {
@@ -278,17 +299,17 @@ function upsertRequest(db, req) {
278
299
  INSERT OR REPLACE INTO requests
279
300
  (id, agent, session_id, model, input_tokens, output_tokens,
280
301
  cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
281
- timestamp, source_request_id)
282
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
283
- `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id);
302
+ timestamp, source_request_id, machine_id)
303
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
304
+ `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "");
284
305
  }
285
306
  function upsertSession(db, session) {
286
307
  db.prepare(`
287
308
  INSERT OR REPLACE INTO sessions
288
309
  (id, agent, project_path, project_name, started_at, ended_at,
289
- total_cost_usd, total_tokens, request_count)
290
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
291
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count);
310
+ total_cost_usd, total_tokens, request_count, machine_id)
311
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
312
+ `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "");
292
313
  }
293
314
  function rollupSession(db, sessionId) {
294
315
  db.prepare(`
@@ -318,6 +339,10 @@ function querySessions(db, filter = {}) {
318
339
  conditions.push("started_at >= ?");
319
340
  params.push(filter.since);
320
341
  }
342
+ if (filter.machine) {
343
+ conditions.push("machine_id = ?");
344
+ params.push(filter.machine);
345
+ }
321
346
  if (filter.search) {
322
347
  const q = `%${filter.search}%`;
323
348
  conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
@@ -336,24 +361,25 @@ function queryTopSessions(db, n = 10, agent) {
336
361
  }
337
362
  return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
338
363
  }
339
- function querySummary(db, period) {
364
+ function querySummary(db, period, machine) {
340
365
  const rWhere = periodWhere(period);
341
366
  const sWhere = sessionPeriodWhere(period);
367
+ const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
342
368
  const r = db.prepare(`
343
369
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
344
370
  COUNT(*) as requests,
345
371
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
346
- FROM requests WHERE ${rWhere}
372
+ FROM requests WHERE ${rWhere}${machineClause}
347
373
  `).get();
348
374
  const codexTotals = db.prepare(`
349
375
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
350
376
  COALESCE(SUM(total_tokens), 0) as tokens,
351
377
  COUNT(*) as sessions
352
378
  FROM sessions
353
- WHERE ${sWhere}
379
+ WHERE ${sWhere}${machineClause}
354
380
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
355
381
  `).get();
356
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
382
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
357
383
  return {
358
384
  total_usd: r.total_usd + codexTotals.cost_usd,
359
385
  requests: r.requests,
@@ -507,6 +533,20 @@ function setIngestState(db, source, key, value) {
507
533
  function queryRequestsSince(db, since) {
508
534
  return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
509
535
  }
536
+ function listMachines(db) {
537
+ return db.prepare(`
538
+ SELECT
539
+ s.machine_id,
540
+ COUNT(DISTINCT s.id) as sessions,
541
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
542
+ COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
543
+ MAX(s.started_at) as last_active
544
+ FROM sessions s
545
+ WHERE s.machine_id != ''
546
+ GROUP BY s.machine_id
547
+ ORDER BY total_cost_usd DESC
548
+ `).all();
549
+ }
510
550
  function upsertModelPricing(db, p) {
511
551
  db.prepare(`
512
552
  INSERT OR REPLACE INTO model_pricing
@@ -794,6 +834,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
794
834
  console.log("Claude projects dir not found:", PROJECTS_DIR);
795
835
  return { files: 0, requests: 0, sessions: 0 };
796
836
  }
837
+ const machineId = getMachineId();
797
838
  let totalFiles = 0;
798
839
  let totalRequests = 0;
799
840
  const touchedSessions = new Set;
@@ -865,7 +906,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
865
906
  cost_usd: costUsd,
866
907
  duration_ms: 0,
867
908
  timestamp,
868
- source_request_id: reqId
909
+ source_request_id: reqId,
910
+ machine_id: machineId
869
911
  });
870
912
  if (!touchedSessions.has(sessionId)) {
871
913
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
@@ -881,7 +923,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
881
923
  ended_at: null,
882
924
  total_cost_usd: 0,
883
925
  total_tokens: 0,
884
- request_count: 0
926
+ request_count: 0,
927
+ machine_id: machineId
885
928
  };
886
929
  upsertSession(db, session);
887
930
  }
@@ -923,6 +966,7 @@ async function ingestCodex(db, verbose = false) {
923
966
  console.log("Codex DB not found:", CODEX_DB_PATH);
924
967
  return { sessions: 0 };
925
968
  }
969
+ const machineId = getMachineId();
926
970
  let codexDb = null;
927
971
  let ingested = 0;
928
972
  try {
@@ -947,7 +991,8 @@ async function ingestCodex(db, verbose = false) {
947
991
  ended_at: endedAt,
948
992
  total_cost_usd: costUsd,
949
993
  total_tokens: thread.tokens_used,
950
- request_count: 1
994
+ request_count: 1,
995
+ machine_id: machineId
951
996
  });
952
997
  setIngestState(db, "codex", stateKey, "done");
953
998
  ingested++;
@@ -982,6 +1027,7 @@ export {
982
1027
  normalizeModelName,
983
1028
  listProjects,
984
1029
  listModelPricing,
1030
+ listMachines,
985
1031
  listGoals,
986
1032
  listBudgets,
987
1033
  ingestCodex,
@@ -990,6 +1036,7 @@ export {
990
1036
  getPricingFromDb,
991
1037
  getPricing,
992
1038
  getModelPricing,
1039
+ getMachineId,
993
1040
  getIngestState,
994
1041
  getGoalStatuses,
995
1042
  getDbPath,
@@ -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,CA2HhE"}
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 +1 @@
1
- {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/ingest/codex.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAkB7D,iBAAS,cAAc,IAAI,MAAM,CAShC;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAyD9F;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}
1
+ {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/ingest/codex.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAkB7D,iBAAS,cAAc,IAAI,MAAM,CAShC;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,UAAQ,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA2D9F;AAED,OAAO,EAAE,cAAc,EAAE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../src/ingest/gemini.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AA0B7D,wBAAsB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA2EjG"}
1
+ {"version":3,"file":"gemini.d.ts","sourceRoot":"","sources":["../../src/ingest/gemini.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AA0B7D,wBAAsB,YAAY,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA6EjG"}
package/dist/mcp/index.js CHANGED
@@ -108,8 +108,17 @@ var init_pricing = __esm(() => {
108
108
  // src/db/database.ts
109
109
  import { SqliteAdapter as Database } from "@hasna/cloud";
110
110
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
111
+ import { hostname } from "os";
111
112
  import { homedir } from "os";
112
113
  import { join } from "path";
114
+ function getMachineId() {
115
+ if (process.env["ECONOMY_MACHINE_ID"])
116
+ return process.env["ECONOMY_MACHINE_ID"];
117
+ const h = hostname().toLowerCase();
118
+ if (h.startsWith("spark") || h.startsWith("apple"))
119
+ return h.split(".")[0];
120
+ return h.split(".")[0];
121
+ }
113
122
  function getDataDir() {
114
123
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
115
124
  const newDir = join(home, ".hasna", "economy");
@@ -142,6 +151,7 @@ function openDatabase(dbPath, skipSeed = false) {
142
151
  }
143
152
  const db = new Database(path);
144
153
  db.exec("PRAGMA journal_mode = WAL");
154
+ db.exec("PRAGMA busy_timeout = 5000");
145
155
  db.exec("PRAGMA foreign_keys = ON");
146
156
  initSchema(db);
147
157
  if (!skipSeed) {
@@ -163,7 +173,8 @@ function initSchema(db) {
163
173
  cost_usd REAL NOT NULL DEFAULT 0,
164
174
  duration_ms INTEGER DEFAULT 0,
165
175
  timestamp TEXT NOT NULL,
166
- source_request_id TEXT
176
+ source_request_id TEXT,
177
+ machine_id TEXT DEFAULT ''
167
178
  );
168
179
 
169
180
  CREATE TABLE IF NOT EXISTS sessions (
@@ -175,7 +186,8 @@ function initSchema(db) {
175
186
  ended_at TEXT,
176
187
  total_cost_usd REAL DEFAULT 0,
177
188
  total_tokens INTEGER DEFAULT 0,
178
- request_count INTEGER DEFAULT 0
189
+ request_count INTEGER DEFAULT 0,
190
+ machine_id TEXT DEFAULT ''
179
191
  );
180
192
 
181
193
  CREATE TABLE IF NOT EXISTS projects (
@@ -241,6 +253,15 @@ function initSchema(db) {
241
253
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
242
254
  );
243
255
  `);
256
+ const cols = db.prepare(`PRAGMA table_info(requests)`).all();
257
+ if (!cols.some((c) => c.name === "machine_id")) {
258
+ db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
259
+ db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
260
+ }
261
+ db.exec(`
262
+ CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
263
+ CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
264
+ `);
244
265
  }
245
266
  function periodWhere(period) {
246
267
  switch (period) {
@@ -279,17 +300,17 @@ function upsertRequest(db, req) {
279
300
  INSERT OR REPLACE INTO requests
280
301
  (id, agent, session_id, model, input_tokens, output_tokens,
281
302
  cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
282
- timestamp, source_request_id)
283
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
284
- `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id);
303
+ timestamp, source_request_id, machine_id)
304
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
305
+ `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "");
285
306
  }
286
307
  function upsertSession(db, session) {
287
308
  db.prepare(`
288
309
  INSERT OR REPLACE INTO sessions
289
310
  (id, agent, project_path, project_name, started_at, ended_at,
290
- total_cost_usd, total_tokens, request_count)
291
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
292
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count);
311
+ total_cost_usd, total_tokens, request_count, machine_id)
312
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
313
+ `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "");
293
314
  }
294
315
  function rollupSession(db, sessionId) {
295
316
  db.prepare(`
@@ -319,6 +340,10 @@ function querySessions(db, filter = {}) {
319
340
  conditions.push("started_at >= ?");
320
341
  params.push(filter.since);
321
342
  }
343
+ if (filter.machine) {
344
+ conditions.push("machine_id = ?");
345
+ params.push(filter.machine);
346
+ }
322
347
  if (filter.search) {
323
348
  const q = `%${filter.search}%`;
324
349
  conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
@@ -337,24 +362,25 @@ function queryTopSessions(db, n = 10, agent) {
337
362
  }
338
363
  return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
339
364
  }
340
- function querySummary(db, period) {
365
+ function querySummary(db, period, machine) {
341
366
  const rWhere = periodWhere(period);
342
367
  const sWhere = sessionPeriodWhere(period);
368
+ const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
343
369
  const r = db.prepare(`
344
370
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
345
371
  COUNT(*) as requests,
346
372
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
347
- FROM requests WHERE ${rWhere}
373
+ FROM requests WHERE ${rWhere}${machineClause}
348
374
  `).get();
349
375
  const codexTotals = db.prepare(`
350
376
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
351
377
  COALESCE(SUM(total_tokens), 0) as tokens,
352
378
  COUNT(*) as sessions
353
379
  FROM sessions
354
- WHERE ${sWhere}
380
+ WHERE ${sWhere}${machineClause}
355
381
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
356
382
  `).get();
357
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
383
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
358
384
  return {
359
385
  total_usd: r.total_usd + codexTotals.cost_usd,
360
386
  requests: r.requests,
@@ -477,6 +503,20 @@ function getIngestState(db, source, key) {
477
503
  function setIngestState(db, source, key, value) {
478
504
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
479
505
  }
506
+ function listMachines(db) {
507
+ return db.prepare(`
508
+ SELECT
509
+ s.machine_id,
510
+ COUNT(DISTINCT s.id) as sessions,
511
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
512
+ COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
513
+ MAX(s.started_at) as last_active
514
+ FROM sessions s
515
+ WHERE s.machine_id != ''
516
+ GROUP BY s.machine_id
517
+ ORDER BY total_cost_usd DESC
518
+ `).all();
519
+ }
480
520
  function upsertModelPricing(db, p) {
481
521
  db.prepare(`
482
522
  INSERT OR REPLACE INTO model_pricing
@@ -547,6 +587,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
547
587
  console.log("Claude projects dir not found:", PROJECTS_DIR);
548
588
  return { files: 0, requests: 0, sessions: 0 };
549
589
  }
590
+ const machineId = getMachineId();
550
591
  let totalFiles = 0;
551
592
  let totalRequests = 0;
552
593
  const touchedSessions = new Set;
@@ -618,7 +659,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
618
659
  cost_usd: costUsd,
619
660
  duration_ms: 0,
620
661
  timestamp,
621
- source_request_id: reqId
662
+ source_request_id: reqId,
663
+ machine_id: machineId
622
664
  });
623
665
  if (!touchedSessions.has(sessionId)) {
624
666
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
@@ -634,7 +676,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
634
676
  ended_at: null,
635
677
  total_cost_usd: 0,
636
678
  total_tokens: 0,
637
- request_count: 0
679
+ request_count: 0,
680
+ machine_id: machineId
638
681
  };
639
682
  upsertSession(db, session);
640
683
  }
@@ -666,6 +709,7 @@ async function ingestCodex(db, verbose = false) {
666
709
  console.log("Codex DB not found:", CODEX_DB_PATH);
667
710
  return { sessions: 0 };
668
711
  }
712
+ const machineId = getMachineId();
669
713
  let codexDb = null;
670
714
  let ingested = 0;
671
715
  try {
@@ -690,7 +734,8 @@ async function ingestCodex(db, verbose = false) {
690
734
  ended_at: endedAt,
691
735
  total_cost_usd: costUsd,
692
736
  total_tokens: thread.tokens_used,
693
- request_count: 1
737
+ request_count: 1,
738
+ machine_id: machineId
694
739
  });
695
740
  setIngestState(db, "codex", stateKey, "done");
696
741
  ingested++;
@@ -715,6 +760,7 @@ async function ingestGemini(db, verbose) {
715
760
  console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
716
761
  return { sessions: 0 };
717
762
  }
763
+ const machineId = getMachineId();
718
764
  let totalSessions = 0;
719
765
  const touchedSessions = new Set;
720
766
  let projectHashDirs = [];
@@ -765,7 +811,8 @@ async function ingestGemini(db, verbose) {
765
811
  ended_at: chatData.lastUpdated ?? null,
766
812
  total_cost_usd: 0,
767
813
  total_tokens: 0,
768
- request_count: 0
814
+ request_count: 0,
815
+ machine_id: machineId
769
816
  };
770
817
  upsertSession(db, session);
771
818
  touchedSessions.add(sessionId);
@@ -838,6 +885,7 @@ var TOOL_NAMES = [
838
885
  "get_goals",
839
886
  "set_goal",
840
887
  "remove_goal",
888
+ "list_machines",
841
889
  "register_agent",
842
890
  "heartbeat",
843
891
  "set_focus",
@@ -845,9 +893,10 @@ var TOOL_NAMES = [
845
893
  "send_feedback"
846
894
  ];
847
895
  var TOOL_DESCRIPTIONS = {
848
- get_cost_summary: "period(today|week|month|year|all) -> {total_usd, sessions, requests, tokens, summary}",
849
- get_sessions: "agent(claude|codex|gemini), project(partial), limit(20) -> compact session table",
896
+ get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
897
+ get_sessions: "agent(claude|codex|gemini), project(partial), machine?(hostname), limit(20) -> compact session table",
850
898
  get_top_sessions: "n(10), agent(claude|codex|gemini) -> top sessions by cost",
899
+ list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
851
900
  get_model_breakdown: "no params -> model, requests, tokens, cost",
852
901
  get_project_breakdown: "no params -> project_name, sessions, cost",
853
902
  get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
@@ -891,27 +940,30 @@ server.tool("describe_tools", "Get param hints for specific tools by name.", { n
891
940
  `);
892
941
  return text(result);
893
942
  });
894
- server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
943
+ server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all. machine: filter by hostname.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), machine: z.string().optional() }, async ({ period, machine }) => {
895
944
  const resolved = period ?? "today";
896
- const s = querySummary(db, resolved);
945
+ const s = querySummary(db, resolved, machine);
946
+ const machineLabel = machine ? ` on ${machine}` : "";
897
947
  return text([
898
- `period: ${resolved}`,
948
+ `period: ${resolved}${machineLabel}`,
899
949
  `cost: ${fmtUsd(s.total_usd)}`,
900
950
  `sessions: ${s.sessions}`,
901
951
  `requests: ${s.requests.toLocaleString()}`,
902
952
  `tokens: ${fmtTok(s.tokens)}`,
903
- `summary: You've spent ${fmtUsd(s.total_usd)} ${resolved === "all" ? "total" : resolved} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
953
+ `summary: You've spent ${fmtUsd(s.total_usd)} ${resolved === "all" ? "total" : resolved}${machineLabel} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
904
954
  ].join(`
905
955
  `));
906
956
  });
907
- server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, limit(20)", {
957
+ server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
908
958
  agent: z.enum(["claude", "codex", "gemini"]).optional(),
909
959
  project: z.string().optional(),
960
+ machine: z.string().optional(),
910
961
  limit: z.number().int().positive().max(100).optional()
911
- }, async ({ agent, project, limit }) => {
962
+ }, async ({ agent, project, machine, limit }) => {
912
963
  const sessions = querySessions(db, {
913
964
  agent,
914
965
  project,
966
+ machine,
915
967
  limit: limit ?? 20
916
968
  });
917
969
  const lines = ["id agent cost tokens project"];
@@ -1057,6 +1109,19 @@ server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({
1057
1109
  deleteGoal(db, id);
1058
1110
  return text("Goal removed.");
1059
1111
  });
1112
+ server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
1113
+ const machines = listMachines(db);
1114
+ if (machines.length === 0)
1115
+ return text(`No machine data yet. Current machine: ${getMachineId()}`);
1116
+ const lines = ["machine sessions requests cost last_active"];
1117
+ for (const m of machines) {
1118
+ lines.push(`${m.machine_id.padEnd(17)}${String(m.sessions).padEnd(10)}${String(m.requests).padEnd(10)}${fmtUsd(m.total_cost_usd).padEnd(12)}${m.last_active?.substring(0, 16) ?? "\u2014"}`);
1119
+ }
1120
+ lines.push(`
1121
+ current machine: ${getMachineId()}`);
1122
+ return text(lines.join(`
1123
+ `));
1124
+ });
1060
1125
  server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
1061
1126
  const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
1062
1127
  if (existing) {
@@ -109,8 +109,17 @@ var init_pricing = __esm(() => {
109
109
  // src/db/database.ts
110
110
  import { SqliteAdapter as Database } from "@hasna/cloud";
111
111
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
112
+ import { hostname } from "os";
112
113
  import { homedir } from "os";
113
114
  import { join } from "path";
115
+ function getMachineId() {
116
+ if (process.env["ECONOMY_MACHINE_ID"])
117
+ return process.env["ECONOMY_MACHINE_ID"];
118
+ const h = hostname().toLowerCase();
119
+ if (h.startsWith("spark") || h.startsWith("apple"))
120
+ return h.split(".")[0];
121
+ return h.split(".")[0];
122
+ }
114
123
  function getDataDir() {
115
124
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
116
125
  const newDir = join(home, ".hasna", "economy");
@@ -143,6 +152,7 @@ function openDatabase(dbPath, skipSeed = false) {
143
152
  }
144
153
  const db = new Database(path);
145
154
  db.exec("PRAGMA journal_mode = WAL");
155
+ db.exec("PRAGMA busy_timeout = 5000");
146
156
  db.exec("PRAGMA foreign_keys = ON");
147
157
  initSchema(db);
148
158
  if (!skipSeed) {
@@ -164,7 +174,8 @@ function initSchema(db) {
164
174
  cost_usd REAL NOT NULL DEFAULT 0,
165
175
  duration_ms INTEGER DEFAULT 0,
166
176
  timestamp TEXT NOT NULL,
167
- source_request_id TEXT
177
+ source_request_id TEXT,
178
+ machine_id TEXT DEFAULT ''
168
179
  );
169
180
 
170
181
  CREATE TABLE IF NOT EXISTS sessions (
@@ -176,7 +187,8 @@ function initSchema(db) {
176
187
  ended_at TEXT,
177
188
  total_cost_usd REAL DEFAULT 0,
178
189
  total_tokens INTEGER DEFAULT 0,
179
- request_count INTEGER DEFAULT 0
190
+ request_count INTEGER DEFAULT 0,
191
+ machine_id TEXT DEFAULT ''
180
192
  );
181
193
 
182
194
  CREATE TABLE IF NOT EXISTS projects (
@@ -242,6 +254,15 @@ function initSchema(db) {
242
254
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
243
255
  );
244
256
  `);
257
+ const cols = db.prepare(`PRAGMA table_info(requests)`).all();
258
+ if (!cols.some((c) => c.name === "machine_id")) {
259
+ db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
260
+ db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
261
+ }
262
+ db.exec(`
263
+ CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
264
+ CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
265
+ `);
245
266
  }
246
267
  function periodWhere(period) {
247
268
  switch (period) {
@@ -280,17 +301,17 @@ function upsertRequest(db, req) {
280
301
  INSERT OR REPLACE INTO requests
281
302
  (id, agent, session_id, model, input_tokens, output_tokens,
282
303
  cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
283
- timestamp, source_request_id)
284
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
285
- `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id);
304
+ timestamp, source_request_id, machine_id)
305
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
306
+ `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "");
286
307
  }
287
308
  function upsertSession(db, session) {
288
309
  db.prepare(`
289
310
  INSERT OR REPLACE INTO sessions
290
311
  (id, agent, project_path, project_name, started_at, ended_at,
291
- total_cost_usd, total_tokens, request_count)
292
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
293
- `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count);
312
+ total_cost_usd, total_tokens, request_count, machine_id)
313
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
314
+ `).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "");
294
315
  }
295
316
  function rollupSession(db, sessionId) {
296
317
  db.prepare(`
@@ -320,6 +341,10 @@ function querySessions(db, filter = {}) {
320
341
  conditions.push("started_at >= ?");
321
342
  params.push(filter.since);
322
343
  }
344
+ if (filter.machine) {
345
+ conditions.push("machine_id = ?");
346
+ params.push(filter.machine);
347
+ }
323
348
  if (filter.search) {
324
349
  const q = `%${filter.search}%`;
325
350
  conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
@@ -338,24 +363,25 @@ function queryTopSessions(db, n = 10, agent) {
338
363
  }
339
364
  return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
340
365
  }
341
- function querySummary(db, period) {
366
+ function querySummary(db, period, machine) {
342
367
  const rWhere = periodWhere(period);
343
368
  const sWhere = sessionPeriodWhere(period);
369
+ const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
344
370
  const r = db.prepare(`
345
371
  SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
346
372
  COUNT(*) as requests,
347
373
  COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
348
- FROM requests WHERE ${rWhere}
374
+ FROM requests WHERE ${rWhere}${machineClause}
349
375
  `).get();
350
376
  const codexTotals = db.prepare(`
351
377
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
352
378
  COALESCE(SUM(total_tokens), 0) as tokens,
353
379
  COUNT(*) as sessions
354
380
  FROM sessions
355
- WHERE ${sWhere}
381
+ WHERE ${sWhere}${machineClause}
356
382
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
357
383
  `).get();
358
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
384
+ const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
359
385
  return {
360
386
  total_usd: r.total_usd + codexTotals.cost_usd,
361
387
  requests: r.requests,
@@ -500,6 +526,20 @@ function getIngestState(db, source, key) {
500
526
  function setIngestState(db, source, key, value) {
501
527
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
502
528
  }
529
+ function listMachines(db) {
530
+ return db.prepare(`
531
+ SELECT
532
+ s.machine_id,
533
+ COUNT(DISTINCT s.id) as sessions,
534
+ COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
535
+ COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
536
+ MAX(s.started_at) as last_active
537
+ FROM sessions s
538
+ WHERE s.machine_id != ''
539
+ GROUP BY s.machine_id
540
+ ORDER BY total_cost_usd DESC
541
+ `).all();
542
+ }
503
543
  function upsertModelPricing(db, p) {
504
544
  db.prepare(`
505
545
  INSERT OR REPLACE INTO model_pricing
@@ -571,6 +611,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
571
611
  console.log("Claude projects dir not found:", PROJECTS_DIR);
572
612
  return { files: 0, requests: 0, sessions: 0 };
573
613
  }
614
+ const machineId = getMachineId();
574
615
  let totalFiles = 0;
575
616
  let totalRequests = 0;
576
617
  const touchedSessions = new Set;
@@ -642,7 +683,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
642
683
  cost_usd: costUsd,
643
684
  duration_ms: 0,
644
685
  timestamp,
645
- source_request_id: reqId
686
+ source_request_id: reqId,
687
+ machine_id: machineId
646
688
  });
647
689
  if (!touchedSessions.has(sessionId)) {
648
690
  const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
@@ -658,7 +700,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
658
700
  ended_at: null,
659
701
  total_cost_usd: 0,
660
702
  total_tokens: 0,
661
- request_count: 0
703
+ request_count: 0,
704
+ machine_id: machineId
662
705
  };
663
706
  upsertSession(db, session);
664
707
  }
@@ -690,6 +733,7 @@ async function ingestCodex(db, verbose = false) {
690
733
  console.log("Codex DB not found:", CODEX_DB_PATH);
691
734
  return { sessions: 0 };
692
735
  }
736
+ const machineId = getMachineId();
693
737
  let codexDb = null;
694
738
  let ingested = 0;
695
739
  try {
@@ -714,7 +758,8 @@ async function ingestCodex(db, verbose = false) {
714
758
  ended_at: endedAt,
715
759
  total_cost_usd: costUsd,
716
760
  total_tokens: thread.tokens_used,
717
- request_count: 1
761
+ request_count: 1,
762
+ machine_id: machineId
718
763
  });
719
764
  setIngestState(db, "codex", stateKey, "done");
720
765
  ingested++;
@@ -739,6 +784,7 @@ async function ingestGemini(db, verbose) {
739
784
  console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
740
785
  return { sessions: 0 };
741
786
  }
787
+ const machineId = getMachineId();
742
788
  let totalSessions = 0;
743
789
  const touchedSessions = new Set;
744
790
  let projectHashDirs = [];
@@ -789,7 +835,8 @@ async function ingestGemini(db, verbose) {
789
835
  ended_at: chatData.lastUpdated ?? null,
790
836
  total_cost_usd: 0,
791
837
  total_tokens: 0,
792
- request_count: 0
838
+ request_count: 0,
839
+ machine_id: machineId
793
840
  };
794
841
  upsertSession(db, session);
795
842
  touchedSessions.add(sessionId);
@@ -854,7 +901,11 @@ function createHandler(db) {
854
901
  return ok({ status: "ok", ts: new Date().toISOString() });
855
902
  if (path === "/api/summary" && method === "GET") {
856
903
  const period = url.searchParams.get("period") ?? "today";
857
- return ok(querySummary(db, period));
904
+ const machine = url.searchParams.get("machine") ?? undefined;
905
+ return ok(querySummary(db, period, machine));
906
+ }
907
+ if (path === "/api/machines" && method === "GET") {
908
+ return ok(listMachines(db), { current_machine: getMachineId() });
858
909
  }
859
910
  if (path === "/api/daily" && method === "GET") {
860
911
  const days = Number(url.searchParams.get("days") ?? 30);
@@ -864,6 +915,7 @@ function createHandler(db) {
864
915
  const agent = url.searchParams.get("agent");
865
916
  const project = url.searchParams.get("project") ?? undefined;
866
917
  const search = url.searchParams.get("search") ?? undefined;
918
+ const machine = url.searchParams.get("machine") ?? undefined;
867
919
  const limit = Number(url.searchParams.get("limit") ?? 50);
868
920
  const offset = Number(url.searchParams.get("offset") ?? 0);
869
921
  const since = url.searchParams.get("since") ?? undefined;
@@ -873,6 +925,7 @@ function createHandler(db) {
873
925
  agent: agent ?? undefined,
874
926
  project,
875
927
  search,
928
+ machine,
876
929
  limit,
877
930
  offset,
878
931
  since
@@ -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;AA2D7D,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwL/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,CAgM/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,GAAG,IAAI,CAuC7C"}
@@ -13,6 +13,7 @@ export interface EconomyRequest {
13
13
  duration_ms: number;
14
14
  timestamp: string;
15
15
  source_request_id: string;
16
+ machine_id?: string;
16
17
  }
17
18
  export interface EconomySession {
18
19
  id: string;
@@ -24,6 +25,7 @@ export interface EconomySession {
24
25
  total_cost_usd: number;
25
26
  total_tokens: number;
26
27
  request_count: number;
28
+ machine_id?: string;
27
29
  }
28
30
  export interface EconomyProject {
29
31
  id: string;
@@ -97,5 +99,6 @@ export interface SessionFilter {
97
99
  offset?: number;
98
100
  since?: string;
99
101
  search?: string;
102
+ machine?: string;
100
103
  }
101
104
  //# sourceMappingURL=index.d.ts.map
@@ -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.11",
3
+ "version": "0.2.12",
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",