@hasna/economy 0.2.26 → 0.2.28

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.
@@ -0,0 +1,21 @@
1
+ import type { SqliteAdapter as Database } from '@hasna/cloud';
2
+ export interface PeerTableMergeStats {
3
+ table: string;
4
+ inserted: number;
5
+ updated: number;
6
+ skipped: number;
7
+ collisions: number;
8
+ }
9
+ export interface PeerMergeResult {
10
+ source_path: string;
11
+ source_machine: string;
12
+ rows_written: number;
13
+ collisions: number;
14
+ deduped: number;
15
+ tables: PeerTableMergeStats[];
16
+ }
17
+ export declare function mergePeerDatabase(target: Database, sourcePath: string, opts?: {
18
+ sourceMachine?: string;
19
+ now?: string;
20
+ }): PeerMergeResult;
21
+ //# sourceMappingURL=peer-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer-sync.d.ts","sourceRoot":"","sources":["../../src/lib/peer-sync.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,cAAc,CAAA;AAc7D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,mBAAmB,EAAE,CAAA;CAC9B;AAwQD,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,QAAQ,EAChB,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAO,GAClD,eAAe,CA0CjB"}
package/dist/mcp/index.js CHANGED
@@ -566,6 +566,26 @@ function openDatabase(dbPath, skipSeed = false) {
566
566
  }
567
567
  return db;
568
568
  }
569
+ function quoteSqlIdent(identifier) {
570
+ return `"${identifier.replace(/"/g, '""')}"`;
571
+ }
572
+ function hasColumn(db, table, column) {
573
+ const columns = db.prepare(`PRAGMA table_info(${quoteSqlIdent(table)})`).all();
574
+ return columns.some((c) => c.name === column);
575
+ }
576
+ function addColumnIfMissing(db, table, column, definition) {
577
+ if (hasColumn(db, table, column))
578
+ return false;
579
+ try {
580
+ db.exec(`ALTER TABLE ${quoteSqlIdent(table)} ADD COLUMN ${quoteSqlIdent(column)} ${definition}`);
581
+ return true;
582
+ } catch (error) {
583
+ const message = error instanceof Error ? error.message : String(error);
584
+ if (/duplicate column name/i.test(message))
585
+ return true;
586
+ throw error;
587
+ }
588
+ }
569
589
  function initSchema(db) {
570
590
  db.exec(`
571
591
  CREATE TABLE IF NOT EXISTS requests (
@@ -736,59 +756,31 @@ function initSchema(db) {
736
756
  CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
737
757
  CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
738
758
  `);
739
- const cols = db.prepare(`PRAGMA table_info(requests)`).all();
740
- if (!cols.some((c) => c.name === "machine_id")) {
741
- db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
742
- db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
743
- }
744
- if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
745
- db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
759
+ addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
760
+ addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
761
+ if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
746
762
  db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
747
763
  }
748
- if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
749
- db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
750
- }
751
- if (!cols.some((c) => c.name === "cost_basis")) {
752
- db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
753
- }
754
- if (!cols.some((c) => c.name === "attribution_tag")) {
755
- db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
756
- }
757
- if (!cols.some((c) => c.name === "updated_at")) {
758
- db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
764
+ addColumnIfMissing(db, "requests", "cache_create_1h_tokens", "INTEGER DEFAULT 0");
765
+ addColumnIfMissing(db, "requests", "cost_basis", `TEXT DEFAULT 'estimated'`);
766
+ addColumnIfMissing(db, "requests", "attribution_tag", `TEXT DEFAULT ''`);
767
+ if (addColumnIfMissing(db, "requests", "updated_at", `TEXT DEFAULT ''`)) {
759
768
  db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
760
769
  }
761
- if (!cols.some((c) => c.name === "synced_at")) {
762
- db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
763
- }
770
+ addColumnIfMissing(db, "requests", "synced_at", `TEXT DEFAULT ''`);
764
771
  for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
765
- if (!cols.some((c) => c.name === column)) {
766
- db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
767
- }
772
+ addColumnIfMissing(db, "requests", column, `TEXT DEFAULT ''`);
768
773
  }
769
- const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
770
- if (!sessionCols.some((c) => c.name === "attribution_tag")) {
771
- db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
772
- }
773
- if (!sessionCols.some((c) => c.name === "updated_at")) {
774
- db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
774
+ addColumnIfMissing(db, "sessions", "attribution_tag", `TEXT DEFAULT ''`);
775
+ if (addColumnIfMissing(db, "sessions", "updated_at", `TEXT DEFAULT ''`)) {
775
776
  db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
776
777
  }
777
- if (!sessionCols.some((c) => c.name === "synced_at")) {
778
- db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
779
- }
778
+ addColumnIfMissing(db, "sessions", "synced_at", `TEXT DEFAULT ''`);
780
779
  for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
781
- if (!sessionCols.some((c) => c.name === column)) {
782
- db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
783
- }
784
- }
785
- const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
786
- if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
787
- db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
788
- }
789
- if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
790
- db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
780
+ addColumnIfMissing(db, "sessions", column, `TEXT DEFAULT ''`);
791
781
  }
782
+ addColumnIfMissing(db, "model_pricing", "cache_write_1h_per_1m", "REAL NOT NULL DEFAULT 0");
783
+ addColumnIfMissing(db, "model_pricing", "cache_storage_per_1m_hour", "REAL NOT NULL DEFAULT 0");
792
784
  db.exec(`
793
785
  CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
794
786
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
@@ -949,17 +941,22 @@ function querySummary(db, period, machine, allMachines = false) {
949
941
  const codexTotals = db.prepare(`
950
942
  SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
951
943
  COALESCE(SUM(total_tokens), 0) as tokens,
944
+ COALESCE(SUM(request_count), 0) as requests,
952
945
  COUNT(*) as sessions
953
946
  FROM sessions
954
947
  WHERE ${sWhere}${machineClause}
955
948
  AND id NOT IN (SELECT DISTINCT session_id FROM requests)
956
949
  `).get();
957
- const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
950
+ const requestSessionCount = db.prepare(`
951
+ SELECT COUNT(DISTINCT session_id) as sessions
952
+ FROM requests
953
+ WHERE ${rWhere}${machineClause}
954
+ `).get();
958
955
  return {
959
956
  total_usd: r.total_usd + codexTotals.cost_usd,
960
- requests: r.requests,
957
+ requests: r.requests + codexTotals.requests,
961
958
  tokens: r.tokens + codexTotals.tokens,
962
- sessions: sessionCount.sessions,
959
+ sessions: requestSessionCount.sessions + codexTotals.sessions,
963
960
  period
964
961
  };
965
962
  }
@@ -1038,9 +1035,7 @@ function queryAgentBreakdown(db, period = "all") {
1038
1035
  }
1039
1036
  return [...groups.values()].sort((a, b) => b.api_equivalent_usd - a.api_equivalent_usd);
1040
1037
  }
1041
- function labelForPath(projectPath, projectName) {
1042
- if (projectName && projectName.trim() !== "")
1043
- return projectName;
1038
+ function pathProjectLabel(projectPath) {
1044
1039
  if (!projectPath)
1045
1040
  return "";
1046
1041
  const segments = projectPath.split("/").filter(Boolean);
@@ -1049,12 +1044,45 @@ function labelForPath(projectPath, projectName) {
1049
1044
  if (projectPrefix.test(seg))
1050
1045
  return seg;
1051
1046
  }
1052
- const generic = new Set(["web", "app", "apps", "packages", "src", "lib", "server", "client", "api", "frontend", "backend"]);
1047
+ const generic = new Set([
1048
+ "web",
1049
+ "app",
1050
+ "apps",
1051
+ "packages",
1052
+ "src",
1053
+ "lib",
1054
+ "server",
1055
+ "client",
1056
+ "api",
1057
+ "frontend",
1058
+ "backend",
1059
+ "home",
1060
+ "users",
1061
+ "workspace",
1062
+ "workspaces",
1063
+ "hasna"
1064
+ ]);
1053
1065
  for (let i = segments.length - 1;i >= 0; i--) {
1054
1066
  if (!generic.has(segments[i].toLowerCase()))
1055
1067
  return segments[i];
1056
1068
  }
1057
- return segments[segments.length - 1] ?? projectPath;
1069
+ return null;
1070
+ }
1071
+ function isRepoLikeLabel(label) {
1072
+ return /^(open|skill|hook|service|connect|platform|agent|tool|iapp|project|scaffold|capp)-/.test(label) || label.includes("-");
1073
+ }
1074
+ function labelForPath(projectPath, projectName) {
1075
+ const pathLabel = pathProjectLabel(projectPath);
1076
+ if (pathLabel && (!projectName || projectName.trim() === "" || isRepoLikeLabel(pathLabel)))
1077
+ return pathLabel;
1078
+ if (projectName && projectName.trim() !== "")
1079
+ return projectName;
1080
+ if (pathLabel)
1081
+ return pathLabel;
1082
+ return projectPath;
1083
+ }
1084
+ function groupKeyForPath(projectPath, projectName) {
1085
+ return labelForPath(projectPath, projectName).trim().toLowerCase();
1058
1086
  }
1059
1087
  function queryProjectBreakdown(db, period = "all") {
1060
1088
  const requestWhere = requestPeriodWhere(period);
@@ -1069,14 +1097,15 @@ function queryProjectBreakdown(db, period = "all") {
1069
1097
  const label = labelForPath(s.project_path, s.project_name);
1070
1098
  if (!label)
1071
1099
  continue;
1072
- const g = groups.get(label) ?? { sessionIds: [], samplePath: s.project_path };
1100
+ const key = groupKeyForPath(s.project_path, s.project_name);
1101
+ const g = groups.get(key) ?? { label, sessionIds: [], samplePath: s.project_path };
1073
1102
  g.sessionIds.push(s.id);
1074
1103
  if (!g.samplePath)
1075
1104
  g.samplePath = s.project_path;
1076
- groups.set(label, g);
1105
+ groups.set(key, g);
1077
1106
  }
1078
1107
  const result = [];
1079
- for (const [label, g] of groups.entries()) {
1108
+ for (const g of groups.values()) {
1080
1109
  const placeholders = g.sessionIds.map(() => "?").join(",");
1081
1110
  const reqStats = placeholders.length ? db.prepare(`
1082
1111
  SELECT
@@ -1107,7 +1136,7 @@ function queryProjectBreakdown(db, period = "all") {
1107
1136
  const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
1108
1137
  result.push({
1109
1138
  project_path: g.samplePath,
1110
- project_name: label,
1139
+ project_name: g.label,
1111
1140
  sessions: totalSessions,
1112
1141
  requests: reqStats.requests + sessionOnlyStats.requests,
1113
1142
  total_tokens: reqStats.total_tokens + sessionOnlyStats.total_tokens,
@@ -1317,17 +1346,48 @@ function queryBillingSummary(db, period) {
1317
1346
  }
1318
1347
  return { total_usd: total, by_provider };
1319
1348
  }
1320
- function listMachines(db) {
1349
+ function listMachines(db, period = "all") {
1350
+ const rWhere = requestPeriodWhere(period);
1351
+ const sWhere = sessionPeriodWhere(period);
1321
1352
  return db.prepare(`
1353
+ WITH request_stats AS (
1354
+ SELECT
1355
+ machine_id,
1356
+ COUNT(DISTINCT session_id) as sessions,
1357
+ COUNT(*) as requests,
1358
+ COALESCE(SUM(cost_usd), 0) as total_cost_usd,
1359
+ MAX(timestamp) as last_active
1360
+ FROM requests
1361
+ WHERE machine_id != ''
1362
+ AND ${rWhere}
1363
+ GROUP BY machine_id
1364
+ ),
1365
+ session_only_stats AS (
1366
+ SELECT
1367
+ machine_id,
1368
+ COUNT(*) as sessions,
1369
+ COALESCE(SUM(request_count), 0) as requests,
1370
+ COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
1371
+ MAX(started_at) as last_active
1372
+ FROM sessions
1373
+ WHERE machine_id != ''
1374
+ AND ${sWhere}
1375
+ AND id NOT IN (SELECT DISTINCT session_id FROM requests)
1376
+ GROUP BY machine_id
1377
+ ),
1378
+ combined AS (
1379
+ SELECT * FROM request_stats
1380
+ UNION ALL
1381
+ SELECT * FROM session_only_stats
1382
+ )
1322
1383
  SELECT
1323
- s.machine_id,
1324
- COUNT(DISTINCT s.id) as sessions,
1325
- COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
1326
- COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
1327
- MAX(s.started_at) as last_active
1328
- FROM sessions s
1329
- WHERE s.machine_id != ''
1330
- GROUP BY s.machine_id
1384
+ machine_id,
1385
+ COALESCE(SUM(sessions), 0) as sessions,
1386
+ COALESCE(SUM(requests), 0) as requests,
1387
+ COALESCE(SUM(total_cost_usd), 0) as total_cost_usd,
1388
+ MAX(last_active) as last_active
1389
+ FROM combined
1390
+ GROUP BY machine_id
1331
1391
  ORDER BY total_cost_usd DESC
1332
1392
  `).all();
1333
1393
  }
@@ -1406,17 +1466,21 @@ function queryUsageSnapshots(db, opts = {}) {
1406
1466
  }
1407
1467
  function dedupeRequests(db) {
1408
1468
  const dupes = db.prepare(`
1409
- SELECT source_request_id, agent, MIN(id) as keep_id, COUNT(*) as cnt
1469
+ SELECT source_request_id, agent, COALESCE(machine_id, '') as machine_id, MIN(id) as keep_id, COUNT(*) as cnt
1410
1470
  FROM requests
1411
1471
  WHERE source_request_id != '' AND source_request_id IS NOT NULL
1412
- GROUP BY source_request_id, agent
1472
+ GROUP BY source_request_id, agent, COALESCE(machine_id, '')
1413
1473
  HAVING cnt > 1
1414
1474
  `).all();
1415
1475
  let removed = 0;
1416
1476
  for (const row of dupes) {
1417
1477
  const result = db.prepare(`
1418
- DELETE FROM requests WHERE source_request_id = ? AND agent = ? AND id != ?
1419
- `).run(row.source_request_id, row.agent, row.keep_id);
1478
+ DELETE FROM requests
1479
+ WHERE source_request_id = ?
1480
+ AND agent = ?
1481
+ AND COALESCE(machine_id, '') = ?
1482
+ AND id != ?
1483
+ `).run(row.source_request_id, row.agent, row.machine_id, row.keep_id);
1420
1484
  removed += result.changes;
1421
1485
  }
1422
1486
  return removed;
@@ -566,6 +566,26 @@ function openDatabase(dbPath, skipSeed = false) {
566
566
  }
567
567
  return db;
568
568
  }
569
+ function quoteSqlIdent(identifier) {
570
+ return `"${identifier.replace(/"/g, '""')}"`;
571
+ }
572
+ function hasColumn(db, table, column) {
573
+ const columns = db.prepare(`PRAGMA table_info(${quoteSqlIdent(table)})`).all();
574
+ return columns.some((c) => c.name === column);
575
+ }
576
+ function addColumnIfMissing(db, table, column, definition) {
577
+ if (hasColumn(db, table, column))
578
+ return false;
579
+ try {
580
+ db.exec(`ALTER TABLE ${quoteSqlIdent(table)} ADD COLUMN ${quoteSqlIdent(column)} ${definition}`);
581
+ return true;
582
+ } catch (error) {
583
+ const message = error instanceof Error ? error.message : String(error);
584
+ if (/duplicate column name/i.test(message))
585
+ return true;
586
+ throw error;
587
+ }
588
+ }
569
589
  function initSchema(db) {
570
590
  db.exec(`
571
591
  CREATE TABLE IF NOT EXISTS requests (
@@ -736,59 +756,31 @@ function initSchema(db) {
736
756
  CREATE INDEX IF NOT EXISTS idx_usage_agent_date ON usage_snapshots(agent, date);
737
757
  CREATE INDEX IF NOT EXISTS idx_savings_date ON savings_daily(date);
738
758
  `);
739
- const cols = db.prepare(`PRAGMA table_info(requests)`).all();
740
- if (!cols.some((c) => c.name === "machine_id")) {
741
- db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
742
- db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
743
- }
744
- if (!cols.some((c) => c.name === "cache_create_5m_tokens")) {
745
- db.exec(`ALTER TABLE requests ADD COLUMN cache_create_5m_tokens INTEGER DEFAULT 0`);
759
+ addColumnIfMissing(db, "requests", "machine_id", `TEXT DEFAULT ''`);
760
+ addColumnIfMissing(db, "sessions", "machine_id", `TEXT DEFAULT ''`);
761
+ if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
746
762
  db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
747
763
  }
748
- if (!cols.some((c) => c.name === "cache_create_1h_tokens")) {
749
- db.exec(`ALTER TABLE requests ADD COLUMN cache_create_1h_tokens INTEGER DEFAULT 0`);
750
- }
751
- if (!cols.some((c) => c.name === "cost_basis")) {
752
- db.exec(`ALTER TABLE requests ADD COLUMN cost_basis TEXT DEFAULT 'estimated'`);
753
- }
754
- if (!cols.some((c) => c.name === "attribution_tag")) {
755
- db.exec(`ALTER TABLE requests ADD COLUMN attribution_tag TEXT DEFAULT ''`);
756
- }
757
- if (!cols.some((c) => c.name === "updated_at")) {
758
- db.exec(`ALTER TABLE requests ADD COLUMN updated_at TEXT DEFAULT ''`);
764
+ addColumnIfMissing(db, "requests", "cache_create_1h_tokens", "INTEGER DEFAULT 0");
765
+ addColumnIfMissing(db, "requests", "cost_basis", `TEXT DEFAULT 'estimated'`);
766
+ addColumnIfMissing(db, "requests", "attribution_tag", `TEXT DEFAULT ''`);
767
+ if (addColumnIfMissing(db, "requests", "updated_at", `TEXT DEFAULT ''`)) {
759
768
  db.exec(`UPDATE requests SET updated_at = timestamp WHERE updated_at = '' OR updated_at IS NULL`);
760
769
  }
761
- if (!cols.some((c) => c.name === "synced_at")) {
762
- db.exec(`ALTER TABLE requests ADD COLUMN synced_at TEXT DEFAULT ''`);
763
- }
770
+ addColumnIfMissing(db, "requests", "synced_at", `TEXT DEFAULT ''`);
764
771
  for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
765
- if (!cols.some((c) => c.name === column)) {
766
- db.exec(`ALTER TABLE requests ADD COLUMN ${column} TEXT DEFAULT ''`);
767
- }
768
- }
769
- const sessionCols = db.prepare(`PRAGMA table_info(sessions)`).all();
770
- if (!sessionCols.some((c) => c.name === "attribution_tag")) {
771
- db.exec(`ALTER TABLE sessions ADD COLUMN attribution_tag TEXT DEFAULT ''`);
772
+ addColumnIfMissing(db, "requests", column, `TEXT DEFAULT ''`);
772
773
  }
773
- if (!sessionCols.some((c) => c.name === "updated_at")) {
774
- db.exec(`ALTER TABLE sessions ADD COLUMN updated_at TEXT DEFAULT ''`);
774
+ addColumnIfMissing(db, "sessions", "attribution_tag", `TEXT DEFAULT ''`);
775
+ if (addColumnIfMissing(db, "sessions", "updated_at", `TEXT DEFAULT ''`)) {
775
776
  db.exec(`UPDATE sessions SET updated_at = started_at WHERE updated_at = '' OR updated_at IS NULL`);
776
777
  }
777
- if (!sessionCols.some((c) => c.name === "synced_at")) {
778
- db.exec(`ALTER TABLE sessions ADD COLUMN synced_at TEXT DEFAULT ''`);
779
- }
778
+ addColumnIfMissing(db, "sessions", "synced_at", `TEXT DEFAULT ''`);
780
779
  for (const column of ["account_key", "account_tool", "account_name", "account_email", "account_source"]) {
781
- if (!sessionCols.some((c) => c.name === column)) {
782
- db.exec(`ALTER TABLE sessions ADD COLUMN ${column} TEXT DEFAULT ''`);
783
- }
784
- }
785
- const pricingCols = db.prepare(`PRAGMA table_info(model_pricing)`).all();
786
- if (!pricingCols.some((c) => c.name === "cache_write_1h_per_1m")) {
787
- db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_write_1h_per_1m REAL NOT NULL DEFAULT 0`);
788
- }
789
- if (!pricingCols.some((c) => c.name === "cache_storage_per_1m_hour")) {
790
- db.exec(`ALTER TABLE model_pricing ADD COLUMN cache_storage_per_1m_hour REAL NOT NULL DEFAULT 0`);
780
+ addColumnIfMissing(db, "sessions", column, `TEXT DEFAULT ''`);
791
781
  }
782
+ addColumnIfMissing(db, "model_pricing", "cache_write_1h_per_1m", "REAL NOT NULL DEFAULT 0");
783
+ addColumnIfMissing(db, "model_pricing", "cache_storage_per_1m_hour", "REAL NOT NULL DEFAULT 0");
792
784
  db.exec(`
793
785
  CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
794
786
  CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);