@hasna/economy 0.3.7 → 0.3.8

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +0 -4
  2. package/README.md +28 -61
  3. package/dashboard/README.md +17 -64
  4. package/dist/cli/index.js +221 -114
  5. package/dist/db/cloud.d.ts +3 -4
  6. package/dist/db/cloud.d.ts.map +1 -1
  7. package/dist/db/database.d.ts +2 -2
  8. package/dist/db/database.d.ts.map +1 -1
  9. package/dist/db/dialect.d.ts +13 -0
  10. package/dist/db/dialect.d.ts.map +1 -0
  11. package/dist/db/pg-migrate.d.ts +30 -0
  12. package/dist/db/pg-migrate.d.ts.map +1 -0
  13. package/dist/db/sqlite-adapter.d.ts +36 -0
  14. package/dist/db/sqlite-adapter.d.ts.map +1 -0
  15. package/dist/db/sync-pg.d.ts +1 -1
  16. package/dist/db/sync-pg.d.ts.map +1 -1
  17. package/dist/index.js +129 -56
  18. package/dist/ingest/billing.d.ts +1 -1
  19. package/dist/ingest/billing.d.ts.map +1 -1
  20. package/dist/ingest/claude-quota.d.ts +1 -1
  21. package/dist/ingest/claude-quota.d.ts.map +1 -1
  22. package/dist/ingest/claude.d.ts +1 -1
  23. package/dist/ingest/claude.d.ts.map +1 -1
  24. package/dist/ingest/codex-quota.d.ts +1 -1
  25. package/dist/ingest/codex-quota.d.ts.map +1 -1
  26. package/dist/ingest/codex.d.ts +1 -1
  27. package/dist/ingest/codex.d.ts.map +1 -1
  28. package/dist/ingest/cursor.d.ts +1 -1
  29. package/dist/ingest/cursor.d.ts.map +1 -1
  30. package/dist/ingest/gemini.d.ts +1 -1
  31. package/dist/ingest/gemini.d.ts.map +1 -1
  32. package/dist/ingest/hermes.d.ts +1 -1
  33. package/dist/ingest/hermes.d.ts.map +1 -1
  34. package/dist/ingest/loops.d.ts +1 -1
  35. package/dist/ingest/loops.d.ts.map +1 -1
  36. package/dist/ingest/opencode.d.ts +1 -1
  37. package/dist/ingest/opencode.d.ts.map +1 -1
  38. package/dist/ingest/otel.d.ts +1 -1
  39. package/dist/ingest/otel.d.ts.map +1 -1
  40. package/dist/ingest/pi.d.ts +1 -1
  41. package/dist/ingest/pi.d.ts.map +1 -1
  42. package/dist/ingest/plugin.d.ts +1 -1
  43. package/dist/ingest/plugin.d.ts.map +1 -1
  44. package/dist/lib/accounts.d.ts.map +1 -1
  45. package/dist/lib/analytics.d.ts +1 -1
  46. package/dist/lib/analytics.d.ts.map +1 -1
  47. package/dist/lib/billing-diff.d.ts +1 -1
  48. package/dist/lib/billing-diff.d.ts.map +1 -1
  49. package/dist/lib/brief.d.ts +1 -1
  50. package/dist/lib/brief.d.ts.map +1 -1
  51. package/dist/lib/open-projects.d.ts +10 -2
  52. package/dist/lib/open-projects.d.ts.map +1 -1
  53. package/dist/lib/pricing.d.ts +1 -1
  54. package/dist/lib/pricing.d.ts.map +1 -1
  55. package/dist/lib/savings.d.ts +1 -1
  56. package/dist/lib/savings.d.ts.map +1 -1
  57. package/dist/lib/spikes.d.ts +1 -1
  58. package/dist/lib/spikes.d.ts.map +1 -1
  59. package/dist/lib/sync-all.d.ts +1 -1
  60. package/dist/lib/sync-all.d.ts.map +1 -1
  61. package/dist/lib/webhooks.d.ts +1 -1
  62. package/dist/lib/webhooks.d.ts.map +1 -1
  63. package/dist/mcp/index.js +144 -94
  64. package/dist/mcp/server.d.ts.map +1 -1
  65. package/dist/openapi.d.ts.map +1 -1
  66. package/dist/otel/index.js +69 -4
  67. package/dist/server/index.js +248 -88
  68. package/dist/server/serve.d.ts +1 -1
  69. package/dist/server/serve.d.ts.map +1 -1
  70. package/docs/README.md +12 -0
  71. package/docs/cli.md +88 -0
  72. package/docs/configuration.md +86 -0
  73. package/docs/ingestion.md +47 -0
  74. package/docs/mcp.md +65 -0
  75. package/docs/otel.md +53 -0
  76. package/docs/rest-api.md +89 -0
  77. package/package.json +4 -5
@@ -17,6 +17,62 @@ var __export = (target, all) => {
17
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
18
  var __require = import.meta.require;
19
19
 
20
+ // src/db/sqlite-adapter.ts
21
+ import { Database } from "bun:sqlite";
22
+
23
+ class SqliteAdapter {
24
+ db;
25
+ constructor(path) {
26
+ this.db = new Database(path, { create: true });
27
+ this.db.exec("PRAGMA journal_mode=WAL");
28
+ this.db.exec("PRAGMA foreign_keys=ON");
29
+ }
30
+ run(sql, ...params) {
31
+ const result = this.db.prepare(sql).run(...params);
32
+ return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
33
+ }
34
+ get(sql, ...params) {
35
+ return this.db.prepare(sql).get(...params);
36
+ }
37
+ all(sql, ...params) {
38
+ return this.db.prepare(sql).all(...params);
39
+ }
40
+ exec(sql) {
41
+ this.db.exec(sql);
42
+ }
43
+ query(sql) {
44
+ return this.db.query(sql);
45
+ }
46
+ prepare(sql) {
47
+ const stmt = this.db.prepare(sql);
48
+ return {
49
+ run(...params) {
50
+ const r = stmt.run(...params);
51
+ return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
52
+ },
53
+ get(...params) {
54
+ return stmt.get(...params);
55
+ },
56
+ all(...params) {
57
+ return stmt.all(...params);
58
+ },
59
+ finalize() {
60
+ stmt.finalize();
61
+ }
62
+ };
63
+ }
64
+ close() {
65
+ this.db.close();
66
+ }
67
+ transaction(fn) {
68
+ return this.db.transaction(fn)();
69
+ }
70
+ get raw() {
71
+ return this.db;
72
+ }
73
+ }
74
+ var init_sqlite_adapter = () => {};
75
+
20
76
  // src/lib/pricing.ts
21
77
  var exports_pricing = {};
22
78
  __export(exports_pricing, {
@@ -535,7 +591,6 @@ var init_pricing = __esm(() => {
535
591
  });
536
592
 
537
593
  // src/db/database.ts
538
- import { SqliteAdapter as Database } from "@hasna/cloud";
539
594
  import { execFileSync } from "child_process";
540
595
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
541
596
  import { hostname, platform } from "os";
@@ -628,7 +683,7 @@ function openDatabase(dbPath, skipSeed = false) {
628
683
  mkdirSync(dir, { recursive: true });
629
684
  }
630
685
  const db = withSqliteBusyRetry(() => {
631
- const opened = new Database(path);
686
+ const opened = new SqliteAdapter(path);
632
687
  try {
633
688
  opened.exec("PRAGMA busy_timeout = 10000");
634
689
  opened.exec("PRAGMA journal_mode = WAL");
@@ -1216,9 +1271,12 @@ function labelForPath(projectPath, projectName) {
1216
1271
  function groupKeyForPath(projectPath, projectName) {
1217
1272
  return labelForPath(projectPath, projectName).trim().toLowerCase();
1218
1273
  }
1219
- function queryProjectBreakdown(db, period = "all", machine) {
1220
- const requestWhere = requestPeriodWhere(period);
1221
- const sessionWhere = sessionPeriodWhere(period);
1274
+ function laterTimestamp(current, candidate) {
1275
+ if (!candidate)
1276
+ return current;
1277
+ return candidate > current ? candidate : current;
1278
+ }
1279
+ function queryProjectBreakdownScoped(db, requestWhere, sessionWhere, requestScopeParams = [], sessionScopeParams = [], machine) {
1222
1280
  const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
1223
1281
  const requestMachineClause = machine ? " AND machine_id = ?" : "";
1224
1282
  const sessionMachineParams = machine ? [machine, machine] : [];
@@ -1226,67 +1284,89 @@ function queryProjectBreakdown(db, period = "all", machine) {
1226
1284
  const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
1227
1285
  const sessionOnlyMachineParams = machine ? [machine] : [];
1228
1286
  const sessions = db.prepare(`
1229
- SELECT id, project_path, project_name, total_cost_usd, started_at
1287
+ SELECT id, project_path, project_name
1230
1288
  FROM sessions
1231
1289
  WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
1232
1290
  `).all(...sessionMachineParams);
1233
1291
  const groups = new Map;
1292
+ const groupKeyBySession = new Map;
1234
1293
  for (const s of sessions) {
1235
1294
  const label = labelForPath(s.project_path, s.project_name);
1236
1295
  if (!label)
1237
1296
  continue;
1238
1297
  const key = groupKeyForPath(s.project_path, s.project_name);
1239
- const g = groups.get(key) ?? { label, sessionIds: [], samplePath: s.project_path };
1240
- g.sessionIds.push(s.id);
1298
+ const g = groups.get(key) ?? { label, samplePath: s.project_path, sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: "" };
1241
1299
  if (!g.samplePath)
1242
1300
  g.samplePath = s.project_path;
1243
1301
  groups.set(key, g);
1302
+ groupKeyBySession.set(s.id, key);
1303
+ }
1304
+ const groupFor = (sessionId) => {
1305
+ const key = groupKeyBySession.get(sessionId);
1306
+ return key === undefined ? undefined : groups.get(key);
1307
+ };
1308
+ const requestRows = db.prepare(`
1309
+ SELECT session_id,
1310
+ COUNT(*) as requests,
1311
+ COALESCE(SUM(cost_usd), 0) as cost_usd,
1312
+ COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1313
+ MAX(timestamp) as last_active
1314
+ FROM requests
1315
+ WHERE ${requestWhere}${requestMachineClause}
1316
+ GROUP BY session_id
1317
+ `).all(...requestScopeParams, ...requestMachineParams);
1318
+ for (const row of requestRows) {
1319
+ const g = groupFor(row.session_id);
1320
+ if (!g)
1321
+ continue;
1322
+ g.sessions += 1;
1323
+ g.requests += row.requests;
1324
+ g.cost_usd += row.cost_usd;
1325
+ g.total_tokens += row.total_tokens;
1326
+ g.last_active = laterTimestamp(g.last_active, row.last_active);
1327
+ }
1328
+ const sessionsWithRequests = new Set(db.prepare(`SELECT DISTINCT session_id FROM requests`).all().map((row) => row.session_id));
1329
+ const sessionOnlyRows = db.prepare(`
1330
+ SELECT id,
1331
+ COALESCE(request_count, 0) as request_count,
1332
+ COALESCE(total_tokens, 0) as total_tokens,
1333
+ COALESCE(total_cost_usd, 0) as total_cost_usd,
1334
+ started_at
1335
+ FROM sessions
1336
+ WHERE ${sessionWhere}${sessionOnlyMachineClause}
1337
+ `).all(...sessionScopeParams, ...sessionOnlyMachineParams);
1338
+ for (const row of sessionOnlyRows) {
1339
+ if (sessionsWithRequests.has(row.id))
1340
+ continue;
1341
+ const g = groupFor(row.id);
1342
+ if (!g)
1343
+ continue;
1344
+ g.sessions += 1;
1345
+ g.requests += row.request_count;
1346
+ g.total_tokens += row.total_tokens;
1347
+ g.cost_usd += row.total_cost_usd;
1348
+ g.last_active = laterTimestamp(g.last_active, row.started_at);
1244
1349
  }
1245
1350
  const result = [];
1246
1351
  for (const g of groups.values()) {
1247
- const placeholders = g.sessionIds.map(() => "?").join(",");
1248
- const reqStats = placeholders.length ? db.prepare(`
1249
- SELECT
1250
- COUNT(DISTINCT session_id) as sessions,
1251
- COUNT(*) as requests,
1252
- COALESCE(SUM(cost_usd), 0) as cost_usd,
1253
- COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
1254
- MAX(timestamp) as last_active
1255
- FROM requests
1256
- WHERE session_id IN (${placeholders})
1257
- AND ${requestWhere}
1258
- ${requestMachineClause}
1259
- `).get(...g.sessionIds, ...requestMachineParams) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
1260
- const sessionOnlyStats = placeholders.length ? db.prepare(`
1261
- SELECT
1262
- COUNT(*) as sessions,
1263
- COALESCE(SUM(request_count), 0) as requests,
1264
- COALESCE(SUM(total_tokens), 0) as total_tokens,
1265
- COALESCE(SUM(total_cost_usd), 0) as cost_usd,
1266
- MAX(started_at) as last_active
1267
- FROM sessions
1268
- WHERE id IN (${placeholders})
1269
- AND ${sessionWhere}
1270
- ${sessionOnlyMachineClause}
1271
- AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1272
- `).get(...g.sessionIds, ...sessionOnlyMachineParams) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
1273
- const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
1274
- if (totalSessions === 0)
1352
+ if (g.sessions === 0)
1275
1353
  continue;
1276
- const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
1277
1354
  result.push({
1278
1355
  project_path: g.samplePath,
1279
1356
  project_name: g.label,
1280
- sessions: totalSessions,
1281
- requests: reqStats.requests + sessionOnlyStats.requests,
1282
- total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
1283
- cost_usd: reqStats.cost_usd + sessionOnlyStats.cost_usd,
1284
- last_active: lastActive
1357
+ sessions: g.sessions,
1358
+ requests: g.requests,
1359
+ total_tokens: g.total_tokens,
1360
+ cost_usd: g.cost_usd,
1361
+ last_active: g.last_active
1285
1362
  });
1286
1363
  }
1287
1364
  result.sort((a, b) => b.cost_usd - a.cost_usd);
1288
1365
  return result;
1289
1366
  }
1367
+ function queryProjectBreakdown(db, period = "all", machine) {
1368
+ return queryProjectBreakdownScoped(db, requestPeriodWhere(period), sessionPeriodWhere(period), [], [], machine);
1369
+ }
1290
1370
  function normalizeAccountEmail(email) {
1291
1371
  return (email ?? "").trim().toLowerCase();
1292
1372
  }
@@ -1560,6 +1640,9 @@ function queryCostCenterBreakdown(db, period = "all", filter = {}) {
1560
1640
  filtered.sort((a, b) => b.cost_usd - a.cost_usd);
1561
1641
  return filtered;
1562
1642
  }
1643
+ function queryProjectBreakdownSince(db, since, machine) {
1644
+ return queryProjectBreakdownScoped(db, "timestamp >= ?", "started_at >= ?", [since], [since], machine);
1645
+ }
1563
1646
  function queryDailyBreakdown(db, days = 30, machine) {
1564
1647
  const machineClause = machine ? " AND machine_id = ?" : "";
1565
1648
  const params = machine ? [`-${days}`, machine] : [`-${days}`];
@@ -2119,14 +2202,52 @@ var numOr = (v, d = 0) => {
2119
2202
  const num = Number(v);
2120
2203
  return Number.isFinite(num) ? num : d;
2121
2204
  }, strOr = (v, d = "") => v == null ? d : String(v);
2122
- var init_database = () => {};
2205
+ var init_database = __esm(() => {
2206
+ init_sqlite_adapter();
2207
+ });
2208
+
2209
+ // src/db/dialect.ts
2210
+ function translateSql(sql, dialect) {
2211
+ if (dialect === "sqlite")
2212
+ return sql;
2213
+ return sqliteToPostgres(sql);
2214
+ }
2215
+ function translateParams(params) {
2216
+ const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
2217
+ return flat.map((p) => p === undefined ? null : p);
2218
+ }
2219
+ function sqliteToPostgres(sql) {
2220
+ let out = sql;
2221
+ let paramIdx = 0;
2222
+ out = out.replace(/\?/g, () => `$${++paramIdx}`);
2223
+ out = out.replace(/datetime\s*\(\s*'now'\s*\)/gi, "NOW()");
2224
+ out = out.replace(/datetime\s*\(\s*'now'\s*,\s*'(-?\d+)\s+(minutes?|hours?|days?|seconds?)'\s*\)/gi, (_match, amount, unit) => {
2225
+ const n = parseInt(amount, 10);
2226
+ const absN = Math.abs(n);
2227
+ const normalizedUnit = unit.toLowerCase().replace(/s$/, "");
2228
+ const plural = absN === 1 ? normalizedUnit : `${normalizedUnit}s`;
2229
+ return n < 0 ? `NOW() - INTERVAL '${absN} ${plural}'` : `NOW() + INTERVAL '${absN} ${plural}'`;
2230
+ });
2231
+ out = out.replace(/lower\s*\(\s*hex\s*\(\s*randomblob\s*\(\s*\d+\s*\)\s*\)\s*\)/gi, "gen_random_uuid()::text");
2232
+ out = out.replace(/GROUP_CONCAT\s*\(/gi, "STRING_AGG(");
2233
+ out = out.replace(/json_extract\s*\(\s*(\w+)\s*,\s*'\$\.(\w+)'\s*\)/gi, (_match, col, key) => `${col}->>'${key}'`);
2234
+ out = out.replace(/\bAUTOINCREMENT\b/gi, "GENERATED ALWAYS AS IDENTITY");
2235
+ out = out.replace(/(?<![I])LIKE/gi, "ILIKE");
2236
+ out = out.replace(/\bIFNULL\s*\(/gi, "COALESCE(");
2237
+ out = out.replace(/INSERT\s+OR\s+REPLACE\s+INTO\s+"?(\w+)"?\s*\(([^)]+)\)\s*VALUES/gi, (_match, table, colList) => `INSERT INTO "${table}" (${colList}) VALUES`);
2238
+ out = out.replace(/INSERT\s+OR\s+REPLACE\s+INTO/gi, "INSERT INTO");
2239
+ if (/INSERT\s+OR\s+IGNORE\s+INTO/i.test(out)) {
2240
+ out = out.replace(/INSERT\s+OR\s+IGNORE\s+INTO/gi, "INSERT INTO");
2241
+ out = out.replace(/;?\s*$/, " ON CONFLICT DO NOTHING");
2242
+ }
2243
+ return out;
2244
+ }
2123
2245
 
2124
2246
  // src/db/sync-pg.ts
2125
2247
  import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
2126
2248
  import { fileURLToPath } from "url";
2127
2249
  import { dirname, join as join10 } from "path";
2128
2250
  import { existsSync as existsSync11 } from "fs";
2129
- import { translateSql, translateParams } from "@hasna/cloud";
2130
2251
  function translateSqliteDates(sql) {
2131
2252
  let s = sql;
2132
2253
  const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
@@ -2248,6 +2369,54 @@ class SyncPgAdapter {
2248
2369
  }
2249
2370
  var init_sync_pg = () => {};
2250
2371
 
2372
+ // src/db/pg-migrate.ts
2373
+ var exports_pg_migrate = {};
2374
+ __export(exports_pg_migrate, {
2375
+ resolvePgSsl: () => resolvePgSsl,
2376
+ applyPgMigrations: () => applyPgMigrations
2377
+ });
2378
+ import pg from "pg";
2379
+ function resolvePgSsl(dsn) {
2380
+ return dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
2381
+ }
2382
+ async function applyPgMigrations(connectionString, migrations, service = "unknown") {
2383
+ const pool = new pg.Pool({ connectionString, ssl: resolvePgSsl(connectionString) });
2384
+ const result = {
2385
+ service,
2386
+ applied: [],
2387
+ alreadyApplied: [],
2388
+ errors: [],
2389
+ totalMigrations: migrations.length
2390
+ };
2391
+ try {
2392
+ await pool.query(`CREATE TABLE IF NOT EXISTS _pg_migrations (
2393
+ id SERIAL PRIMARY KEY,
2394
+ version INT UNIQUE NOT NULL,
2395
+ applied_at TIMESTAMPTZ DEFAULT NOW()
2396
+ )`);
2397
+ const existing = await pool.query("SELECT version FROM _pg_migrations ORDER BY version");
2398
+ const appliedSet = new Set(existing.rows.map((row) => Number(row["version"])));
2399
+ for (let i = 0;i < migrations.length; i++) {
2400
+ if (appliedSet.has(i)) {
2401
+ result.alreadyApplied.push(i);
2402
+ continue;
2403
+ }
2404
+ try {
2405
+ await pool.query(migrations[i]);
2406
+ await pool.query("INSERT INTO _pg_migrations (version) VALUES ($1) ON CONFLICT DO NOTHING", [i]);
2407
+ result.applied.push(i);
2408
+ } catch (err) {
2409
+ result.errors.push(`Migration ${i}: ${err instanceof Error ? err.message : String(err)}`);
2410
+ break;
2411
+ }
2412
+ }
2413
+ } finally {
2414
+ await pool.end().catch(() => {});
2415
+ }
2416
+ return result;
2417
+ }
2418
+ var init_pg_migrate = () => {};
2419
+
2251
2420
  // src/db/pg-migrations.ts
2252
2421
  var PG_MIGRATIONS;
2253
2422
  var init_pg_migrations = __esm(() => {
@@ -2491,7 +2660,7 @@ __export(exports_cloud, {
2491
2660
  cloudMigrations: () => cloudMigrations,
2492
2661
  authClientFromPool: () => authClientFromPool
2493
2662
  });
2494
- import pg from "pg";
2663
+ import pg2 from "pg";
2495
2664
  function getCloudDatabaseUrl() {
2496
2665
  return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
2497
2666
  }
@@ -2515,8 +2684,7 @@ function openCloudDatabase(dsn = getCloudDatabaseUrl()) {
2515
2684
  function createCloudPool(dsn = getCloudDatabaseUrl()) {
2516
2685
  if (!dsn)
2517
2686
  throw new Error("economy cloud mode requires a Postgres DSN (HASNA_ECONOMY_DATABASE_URL)");
2518
- const ssl = dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
2519
- const pool = new pg.Pool({ connectionString: dsn, ssl, max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4 });
2687
+ const pool = new pg2.Pool({ connectionString: dsn, ssl: resolvePgSsl(dsn), max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4 });
2520
2688
  pool.on("error", (err) => {
2521
2689
  console.error(JSON.stringify({ evt: "pg_pool_error", message: err.message }));
2522
2690
  });
@@ -2542,6 +2710,7 @@ function cloudMigrations() {
2542
2710
  }
2543
2711
  var init_cloud = __esm(() => {
2544
2712
  init_sync_pg();
2713
+ init_pg_migrate();
2545
2714
  init_pg_migrations();
2546
2715
  });
2547
2716
 
@@ -2820,13 +2989,14 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
2820
2989
  let imported = 0;
2821
2990
  let skipped = 0;
2822
2991
  for (const project of projects) {
2823
- if (!project.path) {
2992
+ const path = resolveProjectPath(project);
2993
+ if (!path) {
2824
2994
  skipped++;
2825
2995
  continue;
2826
2996
  }
2827
2997
  upsertProject(db, {
2828
2998
  id: project.id,
2829
- path: project.path,
2999
+ path,
2830
3000
  name: project.name,
2831
3001
  description: project.description,
2832
3002
  tags: project.tags ?? [],
@@ -2836,6 +3006,7 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
2836
3006
  }
2837
3007
  return { imported, skipped };
2838
3008
  }
3009
+ var resolveProjectPath = (project) => project.primary_path || project.path || "";
2839
3010
  var init_open_projects = __esm(() => {
2840
3011
  init_database();
2841
3012
  });
@@ -3154,6 +3325,7 @@ function defaultCostBasisForAgent(agent) {
3154
3325
  }
3155
3326
 
3156
3327
  // src/lib/accounts.ts
3328
+ import { appliedProfileName, resolveStore } from "@hasna/accounts";
3157
3329
  var AGENT_ACCOUNT_TOOLS = {
3158
3330
  claude: ["claude"],
3159
3331
  takumi: ["takumi", "claude"],
@@ -3218,57 +3390,35 @@ function envOverride(agent, env) {
3218
3390
  account_source: "override"
3219
3391
  };
3220
3392
  }
3221
- function knownToolIds(api) {
3222
- try {
3223
- return new Set(api.listTools().map((tool) => tool.id));
3224
- } catch {
3225
- return new Set;
3226
- }
3227
- }
3228
- function profileForEnvDir(api, tool, env) {
3393
+ async function profileForEnvDir(store, tool, env) {
3229
3394
  const configuredDir = env[tool.envVar];
3230
3395
  if (!configuredDir)
3231
3396
  return null;
3232
3397
  const normalized = normalizeDir(configuredDir);
3233
- try {
3234
- return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
3235
- } catch {
3236
- return null;
3237
- }
3398
+ return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
3238
3399
  }
3239
3400
  async function resolveAccountForAgent(agent, env = process.env) {
3240
3401
  const override = envOverride(agent, env);
3241
3402
  if (override)
3242
3403
  return override;
3243
- let api;
3244
- try {
3245
- api = await import("@hasna/accounts");
3246
- } catch {
3247
- return null;
3248
- }
3249
- const toolIds = knownToolIds(api);
3404
+ const store = resolveStore(env);
3405
+ const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
3250
3406
  for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
3251
- if (!toolIds.has(toolId))
3252
- continue;
3253
- let tool;
3254
- try {
3255
- tool = api.getTool(toolId);
3256
- } catch {
3407
+ const tool = tools.get(toolId);
3408
+ if (!tool)
3257
3409
  continue;
3258
- }
3259
- const envProfile = profileForEnvDir(api, tool, env);
3410
+ const envProfile = await profileForEnvDir(store, tool, env);
3260
3411
  if (envProfile)
3261
3412
  return fromProfile(envProfile, "env");
3262
- try {
3263
- const applied = api.appliedProfile(toolId);
3413
+ const appliedName = appliedProfileName(toolId);
3414
+ if (appliedName) {
3415
+ const applied = await store.findProfile(appliedName, toolId);
3264
3416
  if (applied)
3265
3417
  return fromProfile(applied, "applied");
3266
- } catch {}
3267
- try {
3268
- const current = api.currentProfile(toolId);
3269
- if (current)
3270
- return fromProfile(current, "current");
3271
- } catch {}
3418
+ }
3419
+ const current = await store.currentProfile(toolId);
3420
+ if (current)
3421
+ return fromProfile(current, "current");
3272
3422
  }
3273
3423
  return null;
3274
3424
  }
@@ -6446,6 +6596,15 @@ var openApiSpec = {
6446
6596
  type: "string"
6447
6597
  }
6448
6598
  },
6599
+ {
6600
+ name: "since",
6601
+ in: "query",
6602
+ description: "ISO timestamp lower bound; takes precedence over period",
6603
+ schema: {
6604
+ type: "string",
6605
+ format: "date-time"
6606
+ }
6607
+ },
6449
6608
  {
6450
6609
  name: "machine",
6451
6610
  in: "query",
@@ -7538,9 +7697,10 @@ function createHandler(db, options = {}) {
7538
7697
  if (path === "/api/breakdown" && method === "GET") {
7539
7698
  const by = url.searchParams.get("by") ?? "model";
7540
7699
  const period = url.searchParams.get("period") ?? "all";
7700
+ const since = url.searchParams.get("since") ?? undefined;
7541
7701
  const machine = url.searchParams.get("machine") ?? undefined;
7542
7702
  if (by === "project")
7543
- return ok(queryProjectBreakdown(db, period, machine));
7703
+ return ok(since ? queryProjectBreakdownSince(db, since, machine) : queryProjectBreakdown(db, period, machine));
7544
7704
  if (by === "agent")
7545
7705
  return ok(queryAgentBreakdown(db, period, machine));
7546
7706
  if (by === "account")
@@ -7934,12 +8094,12 @@ function parsePort(raw, label) {
7934
8094
  }
7935
8095
  async function runMigrate() {
7936
8096
  const { getCloudDatabaseUrl: getCloudDatabaseUrl2, createCloudPool: createCloudPool2, authClientFromPool: authClientFromPool2, cloudMigrations: cloudMigrations2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
7937
- const { applyPgMigrations } = await import("@hasna/cloud");
8097
+ const { applyPgMigrations: applyPgMigrations2 } = await Promise.resolve().then(() => (init_pg_migrate(), exports_pg_migrate));
7938
8098
  const { ApiKeyStore: ApiKeyStore2 } = await import("@hasna/contracts/auth");
7939
8099
  const dsn = getCloudDatabaseUrl2();
7940
8100
  if (!dsn)
7941
8101
  throw new Error("migrate requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
7942
- const result = await applyPgMigrations(dsn, cloudMigrations2(), "economy");
8102
+ const result = await applyPgMigrations2(dsn, cloudMigrations2(), "economy");
7943
8103
  const pool = createCloudPool2(dsn);
7944
8104
  try {
7945
8105
  await new ApiKeyStore2(authClientFromPool2(pool)).ensureSchema();
@@ -1,4 +1,4 @@
1
- import type { SqliteAdapter as Database } from '@hasna/cloud';
1
+ import type { SqliteAdapter as Database } from '../db/sqlite-adapter.js';
2
2
  /** The serve OpenAPI document (source of the generated SDK), version-synced. */
3
3
  export declare function openApiDocument(): Record<string, unknown>;
4
4
  /**
@@ -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;AA8C7D,gFAAgF;AAChF,wBAAgB,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAGzD;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CACV,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GAC7F,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/E;AAuBD,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AA6ED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,MAAM,WAAW,cAAc;IAC7B;;wEAEoE;IACpE,aAAa,CAAC,EAAE,gBAAgB,CAAA;IAChC,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAChE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,IACxC,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA8e/D;AAMD,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAkDvG"}
1
+ {"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AA8CxE,gFAAgF;AAChF,wBAAgB,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAGzD;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CACV,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GAC7F,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/E;AAuBD,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AA6ED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,MAAM,WAAW,cAAc;IAC7B;;wEAEoE;IACpE,aAAa,CAAC,EAAE,gBAAgB,CAAA;IAChC,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAChE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,IACxC,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA+e/D;AAMD,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAkDvG"}
package/docs/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # Economy documentation
2
+
3
+ Economy can run as an on-machine SQLite application or as a client of a shared self-hosted HTTP service. These guides describe the current command and network surfaces:
4
+
5
+ - [CLI reference](cli.md) — the `economy` command and the four installed binaries.
6
+ - [Ingestion](ingestion.md) — supported sources, default paths, sync behavior, billing, and account attribution.
7
+ - [Configuration and deployment](configuration.md) — data paths, environment variables, local/server modes, and authentication.
8
+ - [REST API](rest-api.md) — canonical `/v1` routes, request conventions, probes, and legacy aliases.
9
+ - [MCP server](mcp.md) — stdio/HTTP setup and the available tools.
10
+ - [OTLP sidecar](otel.md) — OTLP metrics and the simplified `/ingest` payload.
11
+
12
+ For generated API details, start `economy-serve` and open [`/openapi.json`](http://127.0.0.1:3456/openapi.json). The checked-in source is [`openapi/economy.json`](../openapi/economy.json).
package/docs/cli.md ADDED
@@ -0,0 +1,88 @@
1
+ # CLI reference
2
+
3
+ Installing `@hasna/economy` provides four binaries:
4
+
5
+ | Binary | Purpose |
6
+ | --- | --- |
7
+ | `economy` | Ingest, query, and manage Economy data. |
8
+ | `economy-mcp` | Run the MCP server over stdio or Streamable HTTP. |
9
+ | `economy-serve` | Serve the REST API and built dashboard, or migrate a self-hosted database. |
10
+ | `economy-otel` | Ingest OTLP/HTTP metrics or simplified cost events into local SQLite. |
11
+
12
+ Use `<binary> --help` and `economy <command> --help` for the exact help emitted by the installed version.
13
+
14
+ ## Core commands
15
+
16
+ The supported coding-agent values are `claude`, `takumi`, `codex`, `gemini`, `opencode`, `cursor`, `pi`, and `hermes`.
17
+
18
+ | Command | Current behavior and principal options |
19
+ | --- | --- |
20
+ | `economy sync` | Ingest every local source, or select sources with `--claude`, `--takumi`, `--codex`, `--gemini`, `--opencode`, `--cursor`, `--pi`, `--hermes`, or `--loops`. Maintenance flags are `--force`, `--backfill-machine`, and `--recalculate`; `--verbose` prints source details. |
21
+ | `economy today`, `week`, `month` | Auto-sync in local mode and print the corresponding summary. Auto-sync is skipped in cloud-client mode. |
22
+ | `economy sessions` | List sessions. Filters: `--agent`, `--project`, `--account`, `--machine`, `--since`, and `--search`; `--limit` defaults to 20; `--format` is `table`, `compact`, `csv`, or `json`. |
23
+ | `economy session <id>` | Show one session and its requests. IDs may be prefixes. `--limit` defaults to 20 and `--verbose` shows up to 50 requests. |
24
+ | `economy top` | Rank expensive sessions with `-n`, `--agent`, and `--since`. |
25
+ | `economy breakdown` | Group by `model`, `agent`, `project`, `account`, `cost-center`, `loop`, `app`, or `repo`; accepts `--since`, `--limit`, `--verbose`, and `--json`. |
26
+ | `economy accounts [period]` | Account/profile totals for `today`, `week`, `month`, `year`, or `all`; accepts `--limit`, `--verbose`, and `--json`. |
27
+ | `economy machines` | Machines represented in stored data; accepts `--limit` and `--verbose`. |
28
+ | `economy fleet` | Shared summary plus per-machine rows; accepts `--period`, `--limit`, `--verbose`, and `--json`. |
29
+ | `economy brief` | Fleet brief with `--since <24h|7d|ISO-date>`, `--machine <id|all>`, or `--json`. |
30
+ | `economy usage [period]` | Quota/usage snapshots with `--agent`, `--limit`, `--verbose`, or `--json`. |
31
+ | `economy savings [period]` | Subscription-versus-API-equivalent savings with `--agent` or `--json`. |
32
+ | `economy watch` | Poll recent costs, or use `--daemon` to sync watched local paths. Also accepts `--interval`, `--agent`, and macOS `--notify`. In cloud mode it streams the API and does not ingest local files. |
33
+ | `economy status` | Print one-line spend, fleet, storage, top-agent, and available quota status. |
34
+ | `economy doctor` | Check source paths/token availability, storage mode, pricing gaps, deduplication, and billing drift. |
35
+ | `economy init` | Print first-run local and cloud-client setup hints. |
36
+
37
+ Human output for high-cardinality commands is intentionally capped. Use the command's `--json`, `--verbose`, or `--limit` option when available. JSON output is complete; `--verbose` has command-specific semantics, so consult `--help`.
38
+
39
+ ## Analysis and export
40
+
41
+ | Command | Behavior |
42
+ | --- | --- |
43
+ | `economy export` | Export `sessions` or `requests` as CSV. `--period` accepts `today`, `week`, `month`, or `all`; `--output` writes a file instead of stdout. |
44
+ | `economy compare <period1> <period2>` | Compare `today`, `yesterday`, `week`, `lastweek`, `month`, or `lastmonth`. |
45
+ | `economy forecast` | Project the current month's total from the observed burn rate. |
46
+ | `economy efficiency` | Show output/input ratio, cache-hit percentage, and cost per 1,000 output tokens by model. |
47
+ | `economy estimate --model <id>` | Estimate cost from `--input` and `--output` token counts using the active pricing table. |
48
+ | `economy tui` | Terminal dashboard; `--watch` refreshes at `--interval` seconds. |
49
+ | `economy waybar` | Emit a Waybar-compatible JSON status object. |
50
+ | `economy bar tui`, `bar waybar` | Aliases for the TUI and Waybar commands. |
51
+
52
+ ## Data management
53
+
54
+ | Command group | Subcommands |
55
+ | --- | --- |
56
+ | `economy budget` | `set` (`--limit` required; optional `--project`, `--agent`, `--cost-center`, `--period`, `--alert`), `list` (`--limit`, `--verbose`), `remove <id>`. |
57
+ | `economy goal` | `set` (`--limit` required; optional `--period`, `--project`, `--agent`), `list`, `status`, `remove <id>`. List/status accept `--limit` and `--verbose`. |
58
+ | `economy project` | `add <path> [--name]`, `list`, `show <nameOrPath>`, `rename <path> <name>`, `remove <path>`. |
59
+ | `economy pricing` | `list`, `set <model>`, `remove <model>`. Set accepts `--input`, `--output`, `--cache-read`, `--cache-write`, `--cache-write-1h`, and `--cache-storage`. |
60
+ | `economy subscriptions` | `set` (requires `--provider` and `--plan`; optional `--agent`, `--fee`, `--included`), `list`, `remove <id>`. |
61
+ | `economy billing` | `sync` (`--days`, provider selectors), `show --period`, and `diff --period`. Provider sync is local-only. |
62
+ | `economy config` | Show config, `get <key>`, `set <key> <value>`, or `webhook-test`. See [configuration](configuration.md). |
63
+ | `economy remove <type> <id>` | Top-level alias for removing a `budget`, `project`, `goal`, or `pricing` row. Alias: `rm`. |
64
+
65
+ ## Integrations and utilities
66
+
67
+ | Command | Behavior |
68
+ | --- | --- |
69
+ | `economy serve --port <port>` | Start the REST API in-process. Equivalent server controls are documented under [`economy-serve`](configuration.md#rest-server). |
70
+ | `economy dashboard --port <port>` | Start the server when necessary and open the dashboard URL. |
71
+ | `economy mcp` | Print Claude Code, Codex, and Gemini MCP configuration; select one or use `--all`. |
72
+ | `economy completion <shell>` | Print completion for `bash`, `zsh`, or `fish`. |
73
+ | `economy menubar` | `install [--force]`, `start`, `stop`, or `uninstall` the macOS Economy Bar app. |
74
+ | `economy brains` | `gather`, `train`, `model [set|clear]`, and `status`. Fine-tuning requires the optional `@hasna/brains` package; gathering does not. |
75
+ | `economy events`, `economy webhooks` | Event emit/list/replay and event-subscription commands supplied by `@hasna/events`; use the nested `--help` for options. |
76
+ | `economy todos` | Display the bundled Economy roadmap, filter tasks, or show a task ID. This is project planning data, not live service status. |
77
+
78
+ ## Other binary help
79
+
80
+ ```text
81
+ economy-mcp [--http] [--port <port>]
82
+ economy-serve [--port <port>]
83
+ economy-serve migrate
84
+ economy-serve version
85
+ economy-otel [--port <port>]
86
+ ```
87
+
88
+ `economy-mcp` and `economy-serve` support `--version`. See [MCP](mcp.md), [configuration](configuration.md), and [OTLP](otel.md) for environment controls and endpoints.