@hasna/economy 0.2.30 → 0.2.31

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 (64) hide show
  1. package/README.md +6 -6
  2. package/dist/cli/commands/tui.d.ts +1 -1
  3. package/dist/cli/commands/tui.d.ts.map +1 -1
  4. package/dist/cli/index.js +850 -187
  5. package/dist/db/database.d.ts +4 -2
  6. package/dist/db/database.d.ts.map +1 -1
  7. package/dist/db/storage-adapter.d.ts +34 -0
  8. package/dist/db/storage-adapter.d.ts.map +1 -0
  9. package/dist/index.d.ts +4 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1057 -54
  12. package/dist/ingest/billing.d.ts +1 -1
  13. package/dist/ingest/billing.d.ts.map +1 -1
  14. package/dist/ingest/claude-quota.d.ts +1 -1
  15. package/dist/ingest/claude-quota.d.ts.map +1 -1
  16. package/dist/ingest/claude.d.ts +1 -1
  17. package/dist/ingest/claude.d.ts.map +1 -1
  18. package/dist/ingest/codex-quota.d.ts +1 -1
  19. package/dist/ingest/codex-quota.d.ts.map +1 -1
  20. package/dist/ingest/codex.d.ts +1 -1
  21. package/dist/ingest/codex.d.ts.map +1 -1
  22. package/dist/ingest/cursor.d.ts +1 -1
  23. package/dist/ingest/cursor.d.ts.map +1 -1
  24. package/dist/ingest/gemini.d.ts +1 -1
  25. package/dist/ingest/gemini.d.ts.map +1 -1
  26. package/dist/ingest/hermes.d.ts +1 -1
  27. package/dist/ingest/hermes.d.ts.map +1 -1
  28. package/dist/ingest/opencode.d.ts +1 -1
  29. package/dist/ingest/opencode.d.ts.map +1 -1
  30. package/dist/ingest/otel.d.ts +1 -1
  31. package/dist/ingest/otel.d.ts.map +1 -1
  32. package/dist/ingest/pi.d.ts +1 -1
  33. package/dist/ingest/pi.d.ts.map +1 -1
  34. package/dist/ingest/plugin.d.ts +1 -1
  35. package/dist/ingest/plugin.d.ts.map +1 -1
  36. package/dist/lib/billing-diff.d.ts +1 -1
  37. package/dist/lib/billing-diff.d.ts.map +1 -1
  38. package/dist/lib/cloud-sync.d.ts +9 -2
  39. package/dist/lib/cloud-sync.d.ts.map +1 -1
  40. package/dist/lib/open-projects.d.ts +3 -2
  41. package/dist/lib/open-projects.d.ts.map +1 -1
  42. package/dist/lib/peer-sync.d.ts +1 -1
  43. package/dist/lib/peer-sync.d.ts.map +1 -1
  44. package/dist/lib/pricing.d.ts +1 -1
  45. package/dist/lib/pricing.d.ts.map +1 -1
  46. package/dist/lib/remote-storage.d.ts +15 -0
  47. package/dist/lib/remote-storage.d.ts.map +1 -0
  48. package/dist/lib/savings.d.ts +1 -1
  49. package/dist/lib/savings.d.ts.map +1 -1
  50. package/dist/lib/spikes.d.ts +1 -1
  51. package/dist/lib/spikes.d.ts.map +1 -1
  52. package/dist/lib/storage-sync.d.ts +27 -0
  53. package/dist/lib/storage-sync.d.ts.map +1 -0
  54. package/dist/lib/sync-all.d.ts +1 -1
  55. package/dist/lib/sync-all.d.ts.map +1 -1
  56. package/dist/lib/webhooks.d.ts +1 -1
  57. package/dist/lib/webhooks.d.ts.map +1 -1
  58. package/dist/mcp/index.js +514 -38
  59. package/dist/mcp/server.d.ts.map +1 -1
  60. package/dist/otel/index.js +442 -15
  61. package/dist/server/index.js +510 -51
  62. package/dist/server/serve.d.ts +1 -1
  63. package/dist/server/serve.d.ts.map +1 -1
  64. package/package.json +4 -4
package/dist/mcp/index.js CHANGED
@@ -17,6 +17,60 @@ 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/storage-adapter.ts
21
+ import { Database as BunDatabase } from "bun:sqlite";
22
+
23
+ class SqliteAdapter {
24
+ db;
25
+ constructor(path) {
26
+ this.db = new BunDatabase(path, { create: true });
27
+ }
28
+ run(sql, ...params) {
29
+ const result = this.db.prepare(sql).run(...params);
30
+ return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
31
+ }
32
+ get(sql, ...params) {
33
+ return this.db.prepare(sql).get(...params);
34
+ }
35
+ all(sql, ...params) {
36
+ return this.db.prepare(sql).all(...params);
37
+ }
38
+ exec(sql) {
39
+ this.db.exec(sql);
40
+ }
41
+ query(sql) {
42
+ return this.db.query(sql);
43
+ }
44
+ prepare(sql) {
45
+ const statement = this.db.prepare(sql);
46
+ return {
47
+ run(...params) {
48
+ const result = statement.run(...params);
49
+ return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
50
+ },
51
+ get(...params) {
52
+ return statement.get(...params);
53
+ },
54
+ all(...params) {
55
+ return statement.all(...params);
56
+ },
57
+ finalize() {
58
+ statement.finalize();
59
+ }
60
+ };
61
+ }
62
+ close() {
63
+ this.db.close();
64
+ }
65
+ transaction(fn) {
66
+ return this.db.transaction(fn)();
67
+ }
68
+ get raw() {
69
+ return this.db;
70
+ }
71
+ }
72
+ var init_storage_adapter = () => {};
73
+
20
74
  // src/lib/pricing.ts
21
75
  var exports_pricing = {};
22
76
  __export(exports_pricing, {
@@ -513,7 +567,6 @@ var init_pricing = __esm(() => {
513
567
  });
514
568
 
515
569
  // src/db/database.ts
516
- import { SqliteAdapter as Database } from "@hasna/cloud";
517
570
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
518
571
  import { hostname } from "os";
519
572
  import { homedir } from "os";
@@ -556,7 +609,7 @@ function openDatabase(dbPath, skipSeed = false) {
556
609
  if (dir && !existsSync(dir))
557
610
  mkdirSync(dir, { recursive: true });
558
611
  }
559
- const db = new Database(path);
612
+ const db = new SqliteAdapter(path);
560
613
  db.exec("PRAGMA journal_mode = WAL");
561
614
  db.exec("PRAGMA busy_timeout = 5000");
562
615
  db.exec("PRAGMA foreign_keys = ON");
@@ -1544,7 +1597,10 @@ function dedupeRequests(db) {
1544
1597
  }
1545
1598
  return removed;
1546
1599
  }
1547
- var init_database = () => {};
1600
+ var init_database = __esm(() => {
1601
+ init_storage_adapter();
1602
+ init_storage_adapter();
1603
+ });
1548
1604
 
1549
1605
  // src/db/pg-migrations.ts
1550
1606
  var exports_pg_migrations = {};
@@ -1755,10 +1811,8 @@ var packageMetadata = getPackageMetadata();
1755
1811
 
1756
1812
  // src/mcp/server.ts
1757
1813
  init_database();
1758
- init_pg_migrations();
1759
1814
  import { randomUUID } from "crypto";
1760
1815
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1761
- import { registerCloudTools } from "@hasna/cloud";
1762
1816
  import { z } from "zod";
1763
1817
 
1764
1818
  // src/ingest/claude.ts
@@ -2237,7 +2291,7 @@ init_pricing();
2237
2291
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2238
2292
  import { homedir as homedir3 } from "os";
2239
2293
  import { join as join3, basename as basename2 } from "path";
2240
- import { Database as BunDatabase } from "bun:sqlite";
2294
+ import { Database as BunDatabase2 } from "bun:sqlite";
2241
2295
  var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
2242
2296
  var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
2243
2297
  var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
@@ -2275,7 +2329,7 @@ function openCodexDb(dbPath, verbose) {
2275
2329
  for (const readonly of [true, false]) {
2276
2330
  let codexDb = null;
2277
2331
  try {
2278
- codexDb = readonly ? new BunDatabase(dbPath, { readonly: true }) : new BunDatabase(dbPath);
2332
+ codexDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
2279
2333
  codexDb.prepare("PRAGMA schema_version").get();
2280
2334
  return codexDb;
2281
2335
  } catch (error) {
@@ -3341,6 +3395,370 @@ init_database();
3341
3395
 
3342
3396
  // src/lib/cloud-sync.ts
3343
3397
  init_database();
3398
+ import { homedir as homedir9, platform } from "os";
3399
+ import { dirname, join as join9 } from "path";
3400
+
3401
+ // src/lib/remote-storage.ts
3402
+ import pg from "pg";
3403
+ function translatePlaceholders(sql) {
3404
+ let index = 0;
3405
+ return sql.replace(/\?/g, () => `$${++index}`);
3406
+ }
3407
+ function normalizeParams(params) {
3408
+ const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
3409
+ return flat.map((value) => value === undefined ? null : value);
3410
+ }
3411
+ function sslConfigFor(connectionString) {
3412
+ return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
3413
+ }
3414
+
3415
+ class PgAdapterAsync {
3416
+ pool;
3417
+ constructor(source) {
3418
+ this.pool = typeof source === "string" ? new pg.Pool({ connectionString: source, ssl: sslConfigFor(source) }) : source;
3419
+ }
3420
+ async run(sql, ...params) {
3421
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
3422
+ return { changes: result.rowCount ?? 0, lastInsertRowid: 0 };
3423
+ }
3424
+ async get(sql, ...params) {
3425
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
3426
+ return result.rows[0] ?? null;
3427
+ }
3428
+ async all(sql, ...params) {
3429
+ const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
3430
+ return result.rows;
3431
+ }
3432
+ async exec(sql) {
3433
+ await this.pool.query(translatePlaceholders(sql));
3434
+ }
3435
+ async close() {
3436
+ await this.pool.end();
3437
+ }
3438
+ async transaction(fn) {
3439
+ const client = await this.pool.connect();
3440
+ try {
3441
+ await client.query("BEGIN");
3442
+ const result = await fn(client);
3443
+ await client.query("COMMIT");
3444
+ return result;
3445
+ } catch (error) {
3446
+ await client.query("ROLLBACK");
3447
+ throw error;
3448
+ } finally {
3449
+ client.release();
3450
+ }
3451
+ }
3452
+ get raw() {
3453
+ return this.pool;
3454
+ }
3455
+ }
3456
+
3457
+ // src/lib/storage-sync.ts
3458
+ async function syncPush(local, remote, options) {
3459
+ const tables = await getTableOrder(remote, options.tables);
3460
+ return syncTransfer(local, remote, { ...options, tables }, "push");
3461
+ }
3462
+ async function syncPull(remote, local, options) {
3463
+ const tables = await getTableOrder(remote, options.tables);
3464
+ return syncTransfer(remote, local, { ...options, tables }, "pull");
3465
+ }
3466
+ function quoteIdent(identifier) {
3467
+ return `"${identifier.replace(/"/g, '""')}"`;
3468
+ }
3469
+ async function getTableOrder(remote, tables) {
3470
+ if (tables.length <= 1)
3471
+ return tables;
3472
+ try {
3473
+ const rows = await remote.all(`
3474
+ SELECT DISTINCT
3475
+ tc.table_name AS source_table,
3476
+ ccu.table_name AS referenced_table
3477
+ FROM information_schema.table_constraints tc
3478
+ JOIN information_schema.constraint_column_usage ccu
3479
+ ON tc.constraint_name = ccu.constraint_name
3480
+ AND tc.table_schema = ccu.table_schema
3481
+ WHERE tc.constraint_type = 'FOREIGN KEY'
3482
+ AND tc.table_schema = 'public'
3483
+ `);
3484
+ if (rows.length > 0)
3485
+ return topoSort(tables, rows);
3486
+ } catch {}
3487
+ return tables;
3488
+ }
3489
+ function topoSort(tables, foreignKeys) {
3490
+ const allowed = new Set(tables);
3491
+ const deps = new Map;
3492
+ for (const table of tables)
3493
+ deps.set(table, new Set);
3494
+ for (const fk of foreignKeys) {
3495
+ if (allowed.has(fk.source_table) && allowed.has(fk.referenced_table)) {
3496
+ deps.get(fk.source_table)?.add(fk.referenced_table);
3497
+ }
3498
+ }
3499
+ const sorted = [];
3500
+ const visited = new Set;
3501
+ const visiting = new Set;
3502
+ function visit(table) {
3503
+ if (visited.has(table))
3504
+ return;
3505
+ if (visiting.has(table)) {
3506
+ visited.add(table);
3507
+ sorted.push(table);
3508
+ return;
3509
+ }
3510
+ visiting.add(table);
3511
+ for (const dep of deps.get(table) ?? [])
3512
+ visit(dep);
3513
+ visiting.delete(table);
3514
+ visited.add(table);
3515
+ sorted.push(table);
3516
+ }
3517
+ for (const table of tables)
3518
+ visit(table);
3519
+ return sorted;
3520
+ }
3521
+ async function resolvePrimaryKeys(source, target, table, option) {
3522
+ if (option)
3523
+ return Array.isArray(option) ? option : [option];
3524
+ const sourceKeys = await detectPrimaryKeys(source, table);
3525
+ if (sourceKeys.length > 0)
3526
+ return sourceKeys;
3527
+ return detectPrimaryKeys(target, table);
3528
+ }
3529
+ async function detectPrimaryKeys(adapter, table) {
3530
+ if (isAsyncAdapter(adapter)) {
3531
+ try {
3532
+ const rows = await adapter.all(`
3533
+ SELECT kcu.column_name, kcu.ordinal_position
3534
+ FROM information_schema.table_constraints tc
3535
+ JOIN information_schema.key_column_usage kcu
3536
+ ON tc.constraint_name = kcu.constraint_name
3537
+ AND tc.table_schema = kcu.table_schema
3538
+ WHERE tc.constraint_type = 'PRIMARY KEY'
3539
+ AND tc.table_schema = 'public'
3540
+ AND tc.table_name = ?
3541
+ ORDER BY kcu.ordinal_position
3542
+ `, table);
3543
+ return rows.map((row) => row.column_name);
3544
+ } catch {
3545
+ return [];
3546
+ }
3547
+ }
3548
+ try {
3549
+ const rows = adapter.all(`PRAGMA table_info(${quoteIdent(table)})`);
3550
+ return rows.filter((row) => row.pk > 0).sort((a, b) => a.pk - b.pk).map((row) => row.name);
3551
+ } catch {
3552
+ return [];
3553
+ }
3554
+ }
3555
+ async function ensureTablesExist(source, target, tables) {
3556
+ if (!isAsyncAdapter(source) || isAsyncAdapter(target))
3557
+ return;
3558
+ for (const table of tables)
3559
+ await ensureTableInSqliteFromPg(target, source, table);
3560
+ }
3561
+ async function ensureTableInSqliteFromPg(target, source, table) {
3562
+ const existing = target.all(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table);
3563
+ if (existing.length > 0)
3564
+ return;
3565
+ const columns = await source.all(`
3566
+ SELECT column_name, data_type, is_nullable
3567
+ FROM information_schema.columns
3568
+ WHERE table_schema = 'public' AND table_name = ?
3569
+ ORDER BY ordinal_position
3570
+ `, table);
3571
+ if (columns.length === 0)
3572
+ return;
3573
+ const primaryKeys = new Set(await detectPrimaryKeys(source, table));
3574
+ const definitions = columns.filter((column) => !["tsvector", "tsquery"].includes(column.data_type.toLowerCase())).map((column) => {
3575
+ const type = pgTypeToSqlite(column.data_type);
3576
+ const notNull = column.is_nullable === "NO" && !primaryKeys.has(column.column_name) ? " NOT NULL" : "";
3577
+ return `${quoteIdent(column.column_name)} ${type}${notNull}`;
3578
+ });
3579
+ if (primaryKeys.size > 0) {
3580
+ definitions.push(`PRIMARY KEY (${[...primaryKeys].map(quoteIdent).join(", ")})`);
3581
+ }
3582
+ target.exec(`CREATE TABLE IF NOT EXISTS ${quoteIdent(table)} (${definitions.join(", ")})`);
3583
+ }
3584
+ function pgTypeToSqlite(pgType) {
3585
+ const type = pgType.toLowerCase();
3586
+ if (type.includes("int") || ["bigint", "smallint", "serial", "bigserial"].includes(type))
3587
+ return "INTEGER";
3588
+ if (type.includes("bool"))
3589
+ return "INTEGER";
3590
+ if (type.includes("float") || type.includes("double") || ["real", "numeric", "decimal"].includes(type))
3591
+ return "REAL";
3592
+ if (type === "bytea")
3593
+ return "BLOB";
3594
+ return "TEXT";
3595
+ }
3596
+ async function filterColumnsForTarget(target, table, columns) {
3597
+ if (columns.includes("machine_id") && table !== "machines")
3598
+ await ensureMachineIdColumnInTarget(target, table);
3599
+ try {
3600
+ if (isAsyncAdapter(target)) {
3601
+ const rows2 = await target.all(`
3602
+ SELECT column_name
3603
+ FROM information_schema.columns
3604
+ WHERE table_schema = 'public' AND table_name = ?
3605
+ `, table);
3606
+ if (rows2.length === 0)
3607
+ return columns;
3608
+ const targetColumns2 = new Set(rows2.map((row) => row.column_name));
3609
+ return columns.filter((column) => targetColumns2.has(column));
3610
+ }
3611
+ const rows = target.all(`PRAGMA table_info(${quoteIdent(table)})`);
3612
+ if (rows.length === 0)
3613
+ return columns;
3614
+ const targetColumns = new Set(rows.map((row) => row.name));
3615
+ return columns.filter((column) => targetColumns.has(column));
3616
+ } catch {
3617
+ return columns;
3618
+ }
3619
+ }
3620
+ async function ensureMachineIdColumnInTarget(target, table) {
3621
+ if (isAsyncAdapter(target)) {
3622
+ const rows2 = await target.all(`
3623
+ SELECT column_name
3624
+ FROM information_schema.columns
3625
+ WHERE table_schema = 'public' AND table_name = ? AND column_name = 'machine_id'
3626
+ `, table);
3627
+ if (rows2.length === 0)
3628
+ await target.exec(`ALTER TABLE ${quoteIdent(table)} ADD COLUMN machine_id TEXT DEFAULT ''`);
3629
+ return;
3630
+ }
3631
+ const rows = target.all(`PRAGMA table_info(${quoteIdent(table)})`);
3632
+ if (!rows.some((row) => row.name === "machine_id")) {
3633
+ target.exec(`ALTER TABLE ${quoteIdent(table)} ADD COLUMN machine_id TEXT DEFAULT ''`);
3634
+ }
3635
+ }
3636
+ async function syncTransfer(source, target, options, _direction) {
3637
+ const { tables, onProgress, batchSize = 100, conflictColumn = "updated_at", primaryKey } = options;
3638
+ const results = [];
3639
+ const sqliteTarget = isAsyncAdapter(target) ? null : target;
3640
+ await ensureTablesExist(source, target, tables);
3641
+ if (sqliteTarget) {
3642
+ try {
3643
+ sqliteTarget.exec("PRAGMA foreign_keys = OFF");
3644
+ } catch {}
3645
+ }
3646
+ try {
3647
+ for (let i = 0;i < tables.length; i++) {
3648
+ const table = tables[i];
3649
+ const result = { table, rowsRead: 0, rowsWritten: 0, rowsSkipped: 0, errors: [] };
3650
+ try {
3651
+ onProgress?.({ table, phase: "reading", rowsRead: 0, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
3652
+ const rows = await readAll(source, `SELECT * FROM ${quoteIdent(table)}`);
3653
+ result.rowsRead = rows.length;
3654
+ if (rows.length === 0) {
3655
+ onProgress?.({ table, phase: "done", rowsRead: 0, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
3656
+ results.push(result);
3657
+ continue;
3658
+ }
3659
+ const sourceColumns = Object.keys(rows[0]);
3660
+ const columns = await filterColumnsForTarget(target, table, sourceColumns);
3661
+ const primaryKeys = await resolvePrimaryKeys(source, target, table, primaryKey);
3662
+ if (primaryKeys.length === 0) {
3663
+ result.errors.push(`Table "${table}" has no primary key; inserted without conflict handling`);
3664
+ for (const batch of batches(rows, batchSize)) {
3665
+ await insertBatch(target, table, columns, batch);
3666
+ result.rowsWritten += batch.length;
3667
+ }
3668
+ results.push(result);
3669
+ continue;
3670
+ }
3671
+ const missingKeys = primaryKeys.filter((key) => !columns.includes(key));
3672
+ if (missingKeys.length > 0) {
3673
+ result.errors.push(`Table "${table}" missing primary key column(s): ${missingKeys.join(", ")}`);
3674
+ results.push(result);
3675
+ continue;
3676
+ }
3677
+ onProgress?.({ table, phase: "writing", rowsRead: result.rowsRead, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
3678
+ const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
3679
+ const newestWinsColumn = columns.includes(conflictColumn) ? conflictColumn : undefined;
3680
+ for (const batch of batches(rows, batchSize)) {
3681
+ await upsertBatch(target, table, columns, updateColumns, primaryKeys, batch, newestWinsColumn);
3682
+ result.rowsWritten += batch.length;
3683
+ onProgress?.({ table, phase: "writing", rowsRead: result.rowsRead, rowsWritten: result.rowsWritten, totalTables: tables.length, currentTableIndex: i });
3684
+ }
3685
+ onProgress?.({ table, phase: "done", rowsRead: result.rowsRead, rowsWritten: result.rowsWritten, totalTables: tables.length, currentTableIndex: i });
3686
+ } catch (error) {
3687
+ result.errors.push(error instanceof Error ? error.message : String(error));
3688
+ }
3689
+ results.push(result);
3690
+ }
3691
+ } finally {
3692
+ if (sqliteTarget) {
3693
+ try {
3694
+ sqliteTarget.exec("PRAGMA foreign_keys = ON");
3695
+ } catch {}
3696
+ }
3697
+ }
3698
+ return results;
3699
+ }
3700
+ function batches(rows, size) {
3701
+ const result = [];
3702
+ for (let offset = 0;offset < rows.length; offset += size)
3703
+ result.push(rows.slice(offset, offset + size));
3704
+ return result;
3705
+ }
3706
+ async function upsertBatch(target, table, columns, updateColumns, primaryKeys, batch, conflictColumn) {
3707
+ if (batch.length === 0 || columns.length === 0)
3708
+ return;
3709
+ const fallbackKey = primaryKeys[0] ?? columns[0] ?? "id";
3710
+ const columnList = columns.map(quoteIdent).join(", ");
3711
+ const keyList = primaryKeys.map(quoteIdent).join(", ");
3712
+ const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
3713
+ const whereClause = conflictColumn && updateColumns.includes(conflictColumn) ? ` WHERE ${quoteIdent(table)}.${quoteIdent(conflictColumn)} IS NULL OR EXCLUDED.${quoteIdent(conflictColumn)} >= ${quoteIdent(table)}.${quoteIdent(conflictColumn)}` : "";
3714
+ if (isAsyncAdapter(target)) {
3715
+ const placeholders2 = batch.map((_, rowIndex) => `(${columns.map((__, columnIndex) => `$${rowIndex * columns.length + columnIndex + 1}`).join(", ")})`).join(", ");
3716
+ const params2 = batch.flatMap((row) => columns.map((column) => row[column] ?? null));
3717
+ await target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders2}
3718
+ ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}${whereClause}`, ...params2);
3719
+ return;
3720
+ }
3721
+ const placeholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
3722
+ const params = batch.flatMap((row) => columns.map((column) => coerceForSqlite(row[column])));
3723
+ target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders}
3724
+ ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}${whereClause}`, ...params);
3725
+ }
3726
+ async function insertBatch(target, table, columns, batch) {
3727
+ if (batch.length === 0 || columns.length === 0)
3728
+ return;
3729
+ const columnList = columns.map(quoteIdent).join(", ");
3730
+ if (isAsyncAdapter(target)) {
3731
+ const placeholders2 = batch.map((_, rowIndex) => `(${columns.map((__, columnIndex) => `$${rowIndex * columns.length + columnIndex + 1}`).join(", ")})`).join(", ");
3732
+ const params2 = batch.flatMap((row) => columns.map((column) => row[column] ?? null));
3733
+ await target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders2}`, ...params2);
3734
+ return;
3735
+ }
3736
+ const placeholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
3737
+ const params = batch.flatMap((row) => columns.map((column) => coerceForSqlite(row[column])));
3738
+ target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders}`, ...params);
3739
+ }
3740
+ function coerceForSqlite(value) {
3741
+ if (value === null || value === undefined)
3742
+ return null;
3743
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
3744
+ return value;
3745
+ if (value instanceof Date)
3746
+ return value.toISOString();
3747
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array)
3748
+ return value;
3749
+ if (typeof value === "object")
3750
+ return JSON.stringify(value);
3751
+ return String(value);
3752
+ }
3753
+ function isAsyncAdapter(adapter) {
3754
+ return adapter instanceof PgAdapterAsync;
3755
+ }
3756
+ async function readAll(adapter, sql) {
3757
+ const rows = adapter.all(sql);
3758
+ return rows instanceof Promise ? await rows : rows;
3759
+ }
3760
+
3761
+ // src/lib/cloud-sync.ts
3344
3762
  var CLOUD_TABLES = [
3345
3763
  "requests",
3346
3764
  "sessions",
@@ -3373,7 +3791,6 @@ async function getCloudPg() {
3373
3791
  if (!url) {
3374
3792
  throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
3375
3793
  }
3376
- const { PgAdapterAsync } = await import("@hasna/cloud");
3377
3794
  return new PgAdapterAsync(url);
3378
3795
  }
3379
3796
  async function runCloudMigrations(cloud) {
@@ -3383,40 +3800,57 @@ async function runCloudMigrations(cloud) {
3383
3800
  }
3384
3801
  }
3385
3802
  async function cloudPush(opts) {
3386
- const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
3387
3803
  const cloud = await getCloudPg();
3388
- const local = new SqliteAdapter(getDbPath());
3389
- await runCloudMigrations(cloud);
3390
- const tables = opts?.tables ?? [...CLOUD_TABLES];
3391
- const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
3392
- const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
3393
- touchMachineRegistry(local, "push");
3394
- local.close();
3395
- await cloud.close();
3396
- return { rows, machine: getMachineId() };
3804
+ const local = openDatabase(getDbPath(), true);
3805
+ try {
3806
+ await runCloudMigrations(cloud);
3807
+ touchMachineRegistry(local, "push");
3808
+ const tables = resolveCloudTables(opts?.tables);
3809
+ const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
3810
+ const rows = results.reduce((sum, result) => sum + result.rowsWritten, 0);
3811
+ return { rows, machine: getMachineId() };
3812
+ } finally {
3813
+ local.close();
3814
+ await cloud.close();
3815
+ }
3397
3816
  }
3398
3817
  async function cloudPull(opts) {
3399
- const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
3400
3818
  const cloud = await getCloudPg();
3401
- const local = new SqliteAdapter(getDbPath());
3402
- await runCloudMigrations(cloud);
3403
- const tables = opts?.tables ?? [...CLOUD_TABLES];
3404
- const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
3405
- const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
3406
- touchMachineRegistry(local, "pull");
3407
- local.close();
3408
- await cloud.close();
3409
- setLastCloudPull();
3410
- return { rows, machine: getMachineId() };
3819
+ const local = openDatabase(getDbPath(), true);
3820
+ try {
3821
+ await runCloudMigrations(cloud);
3822
+ const tables = resolveCloudTables(opts?.tables);
3823
+ const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
3824
+ const rows = results.reduce((sum, result) => sum + result.rowsWritten, 0);
3825
+ touchMachineRegistry(local, "pull");
3826
+ setLastCloudPull();
3827
+ return { rows, machine: getMachineId() };
3828
+ } finally {
3829
+ local.close();
3830
+ await cloud.close();
3831
+ }
3832
+ }
3833
+ async function cloudSyncFull() {
3834
+ const push = await cloudPush();
3835
+ const pull = await cloudPull();
3836
+ return { push: push.rows, pull: pull.rows, machine: getMachineId() };
3411
3837
  }
3412
3838
  function setLastCloudPull(at = new Date().toISOString()) {
3413
- const db = openDatabase();
3414
- db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
3839
+ const db = openDatabase(undefined, true);
3840
+ try {
3841
+ db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
3842
+ } finally {
3843
+ db.close();
3844
+ }
3415
3845
  }
3416
3846
  function getLastCloudPull() {
3417
- const db = openDatabase();
3418
- const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
3419
- return row?.value ?? null;
3847
+ const db = openDatabase(undefined, true);
3848
+ try {
3849
+ const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
3850
+ return row?.value ?? null;
3851
+ } finally {
3852
+ db.close();
3853
+ }
3420
3854
  }
3421
3855
  function shouldPullFromCloud() {
3422
3856
  if (!getCloudDatabaseUrl())
@@ -3462,6 +3896,19 @@ function touchMachineRegistry(db, direction) {
3462
3896
  updated_at = excluded.updated_at
3463
3897
  `).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
3464
3898
  }
3899
+ function resolveCloudTables(tables) {
3900
+ if (!tables || tables.length === 0)
3901
+ return [...CLOUD_TABLES];
3902
+ const allowed = new Set(CLOUD_TABLES);
3903
+ const requested = tables.map((table) => table.trim()).filter(Boolean);
3904
+ const invalid = requested.filter((table) => !allowed.has(table));
3905
+ if (invalid.length > 0) {
3906
+ throw new Error(`Unknown economy sync table(s): ${invalid.join(", ")}`);
3907
+ }
3908
+ return requested;
3909
+ }
3910
+ var SCHEDULE_CONFIG_DIR = join9(homedir9(), ".hasna", "economy");
3911
+ var SCHEDULE_CONFIG_PATH = join9(SCHEDULE_CONFIG_DIR, "cloud-sync-schedule.json");
3465
3912
 
3466
3913
  // src/lib/sync-all.ts
3467
3914
  async function syncAll(db, opts = {}) {
@@ -3573,6 +4020,10 @@ function buildServer() {
3573
4020
  "set_subscription",
3574
4021
  "remove_subscription",
3575
4022
  "sync",
4023
+ "cloud_status",
4024
+ "cloud_push",
4025
+ "cloud_pull",
4026
+ "cloud_sync",
3576
4027
  "search_tools",
3577
4028
  "describe_tools",
3578
4029
  "get_goals",
@@ -3609,6 +4060,10 @@ function buildServer() {
3609
4060
  set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}) -> create/update subscription plan`,
3610
4061
  remove_subscription: "id -> delete subscription plan",
3611
4062
  sync: `sources(all|${AGENTS.join("|")}) -> ingest latest cost data`,
4063
+ cloud_status: "no params -> check configured economy PostgreSQL remote and synced tables",
4064
+ cloud_push: "tables?[] -> push local economy SQLite rows to remote PostgreSQL",
4065
+ cloud_pull: "tables?[] -> pull remote PostgreSQL rows into local economy SQLite",
4066
+ cloud_sync: "no params -> push local rows, then pull remote rows",
3612
4067
  search_tools: "query substring -> tool name list",
3613
4068
  describe_tools: "names[] -> one-line parameter hints",
3614
4069
  get_goals: "no params -> goal progress summary",
@@ -3866,6 +4321,31 @@ function buildServer() {
3866
4321
  const result = await syncAll(db, opts);
3867
4322
  return text(JSON.stringify(result, null, 2));
3868
4323
  });
4324
+ server.tool("cloud_status", "Check configured economy PostgreSQL remote and synced tables.", {}, async () => {
4325
+ const url = getCloudDatabaseUrl();
4326
+ if (!url)
4327
+ return text("cloud: not configured");
4328
+ let cloud = null;
4329
+ try {
4330
+ cloud = await getCloudPg();
4331
+ await cloud.get("SELECT 1 as ok");
4332
+ const tables = await cloud.all("SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename");
4333
+ return text([
4334
+ "cloud: connected",
4335
+ `database: ${url.replace(/:[^:@/]+@/, ":***@")}`,
4336
+ `tables: ${tables.map((row) => row.tablename).join(", ") || "none"}`
4337
+ ].join(`
4338
+ `));
4339
+ } catch (error) {
4340
+ return textError(`cloud: ${error instanceof Error ? error.message : String(error)}`);
4341
+ } finally {
4342
+ if (cloud)
4343
+ await cloud.close().catch(() => {});
4344
+ }
4345
+ });
4346
+ server.tool("cloud_push", "Push local economy SQLite rows to remote PostgreSQL.", { tables: z.array(z.string()).optional() }, async ({ tables }) => text(JSON.stringify(await cloudPush({ tables }), null, 2)));
4347
+ server.tool("cloud_pull", "Pull remote PostgreSQL rows into local economy SQLite.", { tables: z.array(z.string()).optional() }, async ({ tables }) => text(JSON.stringify(await cloudPull({ tables }), null, 2)));
4348
+ server.tool("cloud_sync", "Push local rows, then pull remote rows.", {}, async () => text(JSON.stringify(await cloudSyncFull(), null, 2)));
3869
4349
  server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional() }, async ({ period, agent }) => {
3870
4350
  const p = period ?? "month";
3871
4351
  const snaps = queryUsageSnapshots(db, { agent, ...usageSnapshotFilterForPeriod(p) });
@@ -4009,10 +4489,6 @@ current machine: ${getMachineId()}`);
4009
4489
  return textError(String(error));
4010
4490
  }
4011
4491
  });
4012
- registerCloudTools(server, MCP_NAME, {
4013
- dbPath: getDbPath(),
4014
- migrations: PG_MIGRATIONS
4015
- });
4016
4492
  return server;
4017
4493
  }
4018
4494
 
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC,wBAAgB,WAAW,IAAI,GAAG,CAssBjC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAgBA,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC,wBAAgB,WAAW,IAAI,GAAG,CAyvBjC"}