@hasna/economy 0.2.31 → 0.2.33
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/README.md +6 -6
- package/dist/cli/commands/tui.d.ts +1 -1
- package/dist/cli/commands/tui.d.ts.map +1 -1
- package/dist/cli/index.js +184 -832
- package/dist/db/database.d.ts +1 -3
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +50 -1049
- package/dist/ingest/billing.d.ts +1 -1
- package/dist/ingest/billing.d.ts.map +1 -1
- package/dist/ingest/claude-quota.d.ts +1 -1
- package/dist/ingest/claude-quota.d.ts.map +1 -1
- package/dist/ingest/claude.d.ts +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex-quota.d.ts +1 -1
- package/dist/ingest/codex-quota.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/cursor.d.ts +1 -1
- package/dist/ingest/cursor.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +1 -1
- package/dist/ingest/hermes.d.ts.map +1 -1
- package/dist/ingest/opencode.d.ts +1 -1
- package/dist/ingest/opencode.d.ts.map +1 -1
- package/dist/ingest/otel.d.ts +1 -1
- package/dist/ingest/otel.d.ts.map +1 -1
- package/dist/ingest/pi.d.ts +1 -1
- package/dist/ingest/pi.d.ts.map +1 -1
- package/dist/ingest/plugin.d.ts +1 -1
- package/dist/ingest/plugin.d.ts.map +1 -1
- package/dist/lib/billing-diff.d.ts +1 -1
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/cloud-sync.d.ts +2 -9
- package/dist/lib/cloud-sync.d.ts.map +1 -1
- package/dist/lib/open-projects.d.ts +2 -3
- package/dist/lib/open-projects.d.ts.map +1 -1
- package/dist/lib/peer-sync.d.ts +1 -1
- package/dist/lib/peer-sync.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +1 -1
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/savings.d.ts +1 -1
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/lib/spikes.d.ts +1 -1
- package/dist/lib/spikes.d.ts.map +1 -1
- package/dist/lib/sync-all.d.ts +1 -1
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/webhooks.d.ts +1 -1
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/index.js +34 -514
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/otel/index.js +15 -442
- package/dist/server/index.js +46 -492
- package/dist/server/serve.d.ts +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/package.json +6 -5
- package/dist/db/storage-adapter.d.ts +0 -34
- package/dist/db/storage-adapter.d.ts.map +0 -1
- package/dist/lib/remote-storage.d.ts +0 -15
- package/dist/lib/remote-storage.d.ts.map +0 -1
- package/dist/lib/storage-sync.d.ts +0 -27
- package/dist/lib/storage-sync.d.ts.map +0 -1
package/dist/mcp/index.js
CHANGED
|
@@ -17,60 +17,6 @@ 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
|
-
|
|
74
20
|
// src/lib/pricing.ts
|
|
75
21
|
var exports_pricing = {};
|
|
76
22
|
__export(exports_pricing, {
|
|
@@ -567,6 +513,7 @@ var init_pricing = __esm(() => {
|
|
|
567
513
|
});
|
|
568
514
|
|
|
569
515
|
// src/db/database.ts
|
|
516
|
+
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
570
517
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
571
518
|
import { hostname } from "os";
|
|
572
519
|
import { homedir } from "os";
|
|
@@ -609,7 +556,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
609
556
|
if (dir && !existsSync(dir))
|
|
610
557
|
mkdirSync(dir, { recursive: true });
|
|
611
558
|
}
|
|
612
|
-
const db = new
|
|
559
|
+
const db = new Database(path);
|
|
613
560
|
db.exec("PRAGMA journal_mode = WAL");
|
|
614
561
|
db.exec("PRAGMA busy_timeout = 5000");
|
|
615
562
|
db.exec("PRAGMA foreign_keys = ON");
|
|
@@ -1597,10 +1544,7 @@ function dedupeRequests(db) {
|
|
|
1597
1544
|
}
|
|
1598
1545
|
return removed;
|
|
1599
1546
|
}
|
|
1600
|
-
var init_database =
|
|
1601
|
-
init_storage_adapter();
|
|
1602
|
-
init_storage_adapter();
|
|
1603
|
-
});
|
|
1547
|
+
var init_database = () => {};
|
|
1604
1548
|
|
|
1605
1549
|
// src/db/pg-migrations.ts
|
|
1606
1550
|
var exports_pg_migrations = {};
|
|
@@ -1813,6 +1757,7 @@ var packageMetadata = getPackageMetadata();
|
|
|
1813
1757
|
init_database();
|
|
1814
1758
|
import { randomUUID } from "crypto";
|
|
1815
1759
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1760
|
+
import { registerCloudTools } from "@hasna/cloud";
|
|
1816
1761
|
import { z } from "zod";
|
|
1817
1762
|
|
|
1818
1763
|
// src/ingest/claude.ts
|
|
@@ -2291,7 +2236,7 @@ init_pricing();
|
|
|
2291
2236
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
2292
2237
|
import { homedir as homedir3 } from "os";
|
|
2293
2238
|
import { join as join3, basename as basename2 } from "path";
|
|
2294
|
-
import { Database as
|
|
2239
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
2295
2240
|
var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
2296
2241
|
var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
2297
2242
|
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
@@ -2329,7 +2274,7 @@ function openCodexDb(dbPath, verbose) {
|
|
|
2329
2274
|
for (const readonly of [true, false]) {
|
|
2330
2275
|
let codexDb = null;
|
|
2331
2276
|
try {
|
|
2332
|
-
codexDb = readonly ? new
|
|
2277
|
+
codexDb = readonly ? new BunDatabase(dbPath, { readonly: true }) : new BunDatabase(dbPath);
|
|
2333
2278
|
codexDb.prepare("PRAGMA schema_version").get();
|
|
2334
2279
|
return codexDb;
|
|
2335
2280
|
} catch (error) {
|
|
@@ -3395,370 +3340,6 @@ init_database();
|
|
|
3395
3340
|
|
|
3396
3341
|
// src/lib/cloud-sync.ts
|
|
3397
3342
|
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
|
|
3762
3343
|
var CLOUD_TABLES = [
|
|
3763
3344
|
"requests",
|
|
3764
3345
|
"sessions",
|
|
@@ -3791,6 +3372,7 @@ async function getCloudPg() {
|
|
|
3791
3372
|
if (!url) {
|
|
3792
3373
|
throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
|
|
3793
3374
|
}
|
|
3375
|
+
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
3794
3376
|
return new PgAdapterAsync(url);
|
|
3795
3377
|
}
|
|
3796
3378
|
async function runCloudMigrations(cloud) {
|
|
@@ -3800,57 +3382,40 @@ async function runCloudMigrations(cloud) {
|
|
|
3800
3382
|
}
|
|
3801
3383
|
}
|
|
3802
3384
|
async function cloudPush(opts) {
|
|
3385
|
+
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
3803
3386
|
const cloud = await getCloudPg();
|
|
3804
|
-
const local =
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
local.close();
|
|
3814
|
-
await cloud.close();
|
|
3815
|
-
}
|
|
3387
|
+
const local = new SqliteAdapter(getDbPath());
|
|
3388
|
+
await runCloudMigrations(cloud);
|
|
3389
|
+
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3390
|
+
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
3391
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3392
|
+
touchMachineRegistry(local, "push");
|
|
3393
|
+
local.close();
|
|
3394
|
+
await cloud.close();
|
|
3395
|
+
return { rows, machine: getMachineId() };
|
|
3816
3396
|
}
|
|
3817
3397
|
async function cloudPull(opts) {
|
|
3398
|
+
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
3818
3399
|
const cloud = await getCloudPg();
|
|
3819
|
-
const local =
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
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() };
|
|
3400
|
+
const local = new SqliteAdapter(getDbPath());
|
|
3401
|
+
await runCloudMigrations(cloud);
|
|
3402
|
+
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
3403
|
+
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
3404
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
3405
|
+
touchMachineRegistry(local, "pull");
|
|
3406
|
+
local.close();
|
|
3407
|
+
await cloud.close();
|
|
3408
|
+
setLastCloudPull();
|
|
3409
|
+
return { rows, machine: getMachineId() };
|
|
3837
3410
|
}
|
|
3838
3411
|
function setLastCloudPull(at = new Date().toISOString()) {
|
|
3839
|
-
const db = openDatabase(
|
|
3840
|
-
|
|
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
|
-
}
|
|
3412
|
+
const db = openDatabase();
|
|
3413
|
+
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
|
|
3845
3414
|
}
|
|
3846
3415
|
function getLastCloudPull() {
|
|
3847
|
-
const db = openDatabase(
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
return row?.value ?? null;
|
|
3851
|
-
} finally {
|
|
3852
|
-
db.close();
|
|
3853
|
-
}
|
|
3416
|
+
const db = openDatabase();
|
|
3417
|
+
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
|
|
3418
|
+
return row?.value ?? null;
|
|
3854
3419
|
}
|
|
3855
3420
|
function shouldPullFromCloud() {
|
|
3856
3421
|
if (!getCloudDatabaseUrl())
|
|
@@ -3896,19 +3461,6 @@ function touchMachineRegistry(db, direction) {
|
|
|
3896
3461
|
updated_at = excluded.updated_at
|
|
3897
3462
|
`).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
|
|
3898
3463
|
}
|
|
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");
|
|
3912
3464
|
|
|
3913
3465
|
// src/lib/sync-all.ts
|
|
3914
3466
|
async function syncAll(db, opts = {}) {
|
|
@@ -4020,10 +3572,6 @@ function buildServer() {
|
|
|
4020
3572
|
"set_subscription",
|
|
4021
3573
|
"remove_subscription",
|
|
4022
3574
|
"sync",
|
|
4023
|
-
"cloud_status",
|
|
4024
|
-
"cloud_push",
|
|
4025
|
-
"cloud_pull",
|
|
4026
|
-
"cloud_sync",
|
|
4027
3575
|
"search_tools",
|
|
4028
3576
|
"describe_tools",
|
|
4029
3577
|
"get_goals",
|
|
@@ -4060,10 +3608,6 @@ function buildServer() {
|
|
|
4060
3608
|
set_subscription: `provider, plan, monthly_fee_usd?, included_usage_usd?, agent?(${AGENTS.join("|")}) -> create/update subscription plan`,
|
|
4061
3609
|
remove_subscription: "id -> delete subscription plan",
|
|
4062
3610
|
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",
|
|
4067
3611
|
search_tools: "query substring -> tool name list",
|
|
4068
3612
|
describe_tools: "names[] -> one-line parameter hints",
|
|
4069
3613
|
get_goals: "no params -> goal progress summary",
|
|
@@ -4321,31 +3865,6 @@ function buildServer() {
|
|
|
4321
3865
|
const result = await syncAll(db, opts);
|
|
4322
3866
|
return text(JSON.stringify(result, null, 2));
|
|
4323
3867
|
});
|
|
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)));
|
|
4349
3868
|
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 }) => {
|
|
4350
3869
|
const p = period ?? "month";
|
|
4351
3870
|
const snaps = queryUsageSnapshots(db, { agent, ...usageSnapshotFilterForPeriod(p) });
|
|
@@ -4489,6 +4008,7 @@ current machine: ${getMachineId()}`);
|
|
|
4489
4008
|
return textError(String(error));
|
|
4490
4009
|
}
|
|
4491
4010
|
});
|
|
4011
|
+
registerCloudTools(server, MCP_NAME);
|
|
4492
4012
|
return server;
|
|
4493
4013
|
}
|
|
4494
4014
|
|
package/dist/mcp/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,CAmsBjC"}
|