@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.
- 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 +850 -187
- package/dist/db/database.d.ts +4 -2
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/storage-adapter.d.ts +34 -0
- package/dist/db/storage-adapter.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1057 -54
- 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 +9 -2
- package/dist/lib/cloud-sync.d.ts.map +1 -1
- package/dist/lib/open-projects.d.ts +3 -2
- 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/remote-storage.d.ts +15 -0
- package/dist/lib/remote-storage.d.ts.map +1 -0
- 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/storage-sync.d.ts +27 -0
- package/dist/lib/storage-sync.d.ts.map +1 -0
- 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 +514 -38
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/otel/index.js +442 -15
- package/dist/server/index.js +510 -51
- package/dist/server/serve.d.ts +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/cli/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, {
|
|
@@ -561,9 +615,9 @@ __export(exports_database, {
|
|
|
561
615
|
deleteGoal: () => deleteGoal,
|
|
562
616
|
deleteBudget: () => deleteBudget,
|
|
563
617
|
dedupeRequests: () => dedupeRequests,
|
|
564
|
-
clearBillingRange: () => clearBillingRange
|
|
618
|
+
clearBillingRange: () => clearBillingRange,
|
|
619
|
+
SqliteAdapter: () => SqliteAdapter
|
|
565
620
|
});
|
|
566
|
-
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
567
621
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
568
622
|
import { hostname } from "os";
|
|
569
623
|
import { homedir } from "os";
|
|
@@ -606,7 +660,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
606
660
|
if (dir && !existsSync(dir))
|
|
607
661
|
mkdirSync(dir, { recursive: true });
|
|
608
662
|
}
|
|
609
|
-
const db = new
|
|
663
|
+
const db = new SqliteAdapter(path);
|
|
610
664
|
db.exec("PRAGMA journal_mode = WAL");
|
|
611
665
|
db.exec("PRAGMA busy_timeout = 5000");
|
|
612
666
|
db.exec("PRAGMA foreign_keys = ON");
|
|
@@ -1358,13 +1412,17 @@ function queryDailyBreakdown(db, days = 30, machine) {
|
|
|
1358
1412
|
ORDER BY date ASC
|
|
1359
1413
|
`).all(...params);
|
|
1360
1414
|
}
|
|
1361
|
-
function queryHourlyBreakdown(db, machine) {
|
|
1362
|
-
const
|
|
1363
|
-
const params =
|
|
1415
|
+
function queryHourlyBreakdown(db, machine, hours) {
|
|
1416
|
+
const clauses = hours == null ? [`DATE(timestamp) = DATE('now')`] : [`DATETIME(timestamp) >= DATETIME('now', ?)`];
|
|
1417
|
+
const params = hours == null ? [] : [`-${hours} hours`];
|
|
1418
|
+
if (machine) {
|
|
1419
|
+
clauses.push("machine_id = ?");
|
|
1420
|
+
params.push(machine);
|
|
1421
|
+
}
|
|
1364
1422
|
return db.prepare(`
|
|
1365
1423
|
SELECT STRFTIME('%H', timestamp) as hour, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
1366
1424
|
FROM requests
|
|
1367
|
-
WHERE
|
|
1425
|
+
WHERE ${clauses.join(" AND ")}
|
|
1368
1426
|
GROUP BY STRFTIME('%H', timestamp), agent
|
|
1369
1427
|
ORDER BY hour ASC
|
|
1370
1428
|
`).all(...params);
|
|
@@ -1638,7 +1696,10 @@ function dedupeRequests(db) {
|
|
|
1638
1696
|
}
|
|
1639
1697
|
return removed;
|
|
1640
1698
|
}
|
|
1641
|
-
var init_database = () => {
|
|
1699
|
+
var init_database = __esm(() => {
|
|
1700
|
+
init_storage_adapter();
|
|
1701
|
+
init_storage_adapter();
|
|
1702
|
+
});
|
|
1642
1703
|
|
|
1643
1704
|
// src/lib/savings.ts
|
|
1644
1705
|
function periodWhere2(period, column) {
|
|
@@ -1904,6 +1965,370 @@ var init_package_metadata = __esm(() => {
|
|
|
1904
1965
|
packageMetadata = getPackageMetadata();
|
|
1905
1966
|
});
|
|
1906
1967
|
|
|
1968
|
+
// src/lib/remote-storage.ts
|
|
1969
|
+
import pg from "pg";
|
|
1970
|
+
function translatePlaceholders(sql) {
|
|
1971
|
+
let index = 0;
|
|
1972
|
+
return sql.replace(/\?/g, () => `$${++index}`);
|
|
1973
|
+
}
|
|
1974
|
+
function normalizeParams(params) {
|
|
1975
|
+
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
1976
|
+
return flat.map((value) => value === undefined ? null : value);
|
|
1977
|
+
}
|
|
1978
|
+
function sslConfigFor(connectionString) {
|
|
1979
|
+
return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
class PgAdapterAsync {
|
|
1983
|
+
pool;
|
|
1984
|
+
constructor(source) {
|
|
1985
|
+
this.pool = typeof source === "string" ? new pg.Pool({ connectionString: source, ssl: sslConfigFor(source) }) : source;
|
|
1986
|
+
}
|
|
1987
|
+
async run(sql, ...params) {
|
|
1988
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
1989
|
+
return { changes: result.rowCount ?? 0, lastInsertRowid: 0 };
|
|
1990
|
+
}
|
|
1991
|
+
async get(sql, ...params) {
|
|
1992
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
1993
|
+
return result.rows[0] ?? null;
|
|
1994
|
+
}
|
|
1995
|
+
async all(sql, ...params) {
|
|
1996
|
+
const result = await this.pool.query(translatePlaceholders(sql), normalizeParams(params));
|
|
1997
|
+
return result.rows;
|
|
1998
|
+
}
|
|
1999
|
+
async exec(sql) {
|
|
2000
|
+
await this.pool.query(translatePlaceholders(sql));
|
|
2001
|
+
}
|
|
2002
|
+
async close() {
|
|
2003
|
+
await this.pool.end();
|
|
2004
|
+
}
|
|
2005
|
+
async transaction(fn) {
|
|
2006
|
+
const client = await this.pool.connect();
|
|
2007
|
+
try {
|
|
2008
|
+
await client.query("BEGIN");
|
|
2009
|
+
const result = await fn(client);
|
|
2010
|
+
await client.query("COMMIT");
|
|
2011
|
+
return result;
|
|
2012
|
+
} catch (error) {
|
|
2013
|
+
await client.query("ROLLBACK");
|
|
2014
|
+
throw error;
|
|
2015
|
+
} finally {
|
|
2016
|
+
client.release();
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
get raw() {
|
|
2020
|
+
return this.pool;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
var init_remote_storage = () => {};
|
|
2024
|
+
|
|
2025
|
+
// src/lib/storage-sync.ts
|
|
2026
|
+
async function syncPush(local, remote, options) {
|
|
2027
|
+
const tables = await getTableOrder(remote, options.tables);
|
|
2028
|
+
return syncTransfer(local, remote, { ...options, tables }, "push");
|
|
2029
|
+
}
|
|
2030
|
+
async function syncPull(remote, local, options) {
|
|
2031
|
+
const tables = await getTableOrder(remote, options.tables);
|
|
2032
|
+
return syncTransfer(remote, local, { ...options, tables }, "pull");
|
|
2033
|
+
}
|
|
2034
|
+
function quoteIdent(identifier) {
|
|
2035
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
2036
|
+
}
|
|
2037
|
+
async function getTableOrder(remote, tables) {
|
|
2038
|
+
if (tables.length <= 1)
|
|
2039
|
+
return tables;
|
|
2040
|
+
try {
|
|
2041
|
+
const rows = await remote.all(`
|
|
2042
|
+
SELECT DISTINCT
|
|
2043
|
+
tc.table_name AS source_table,
|
|
2044
|
+
ccu.table_name AS referenced_table
|
|
2045
|
+
FROM information_schema.table_constraints tc
|
|
2046
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
2047
|
+
ON tc.constraint_name = ccu.constraint_name
|
|
2048
|
+
AND tc.table_schema = ccu.table_schema
|
|
2049
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
2050
|
+
AND tc.table_schema = 'public'
|
|
2051
|
+
`);
|
|
2052
|
+
if (rows.length > 0)
|
|
2053
|
+
return topoSort(tables, rows);
|
|
2054
|
+
} catch {}
|
|
2055
|
+
return tables;
|
|
2056
|
+
}
|
|
2057
|
+
function topoSort(tables, foreignKeys) {
|
|
2058
|
+
const allowed = new Set(tables);
|
|
2059
|
+
const deps = new Map;
|
|
2060
|
+
for (const table of tables)
|
|
2061
|
+
deps.set(table, new Set);
|
|
2062
|
+
for (const fk of foreignKeys) {
|
|
2063
|
+
if (allowed.has(fk.source_table) && allowed.has(fk.referenced_table)) {
|
|
2064
|
+
deps.get(fk.source_table)?.add(fk.referenced_table);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
const sorted = [];
|
|
2068
|
+
const visited = new Set;
|
|
2069
|
+
const visiting = new Set;
|
|
2070
|
+
function visit(table) {
|
|
2071
|
+
if (visited.has(table))
|
|
2072
|
+
return;
|
|
2073
|
+
if (visiting.has(table)) {
|
|
2074
|
+
visited.add(table);
|
|
2075
|
+
sorted.push(table);
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
visiting.add(table);
|
|
2079
|
+
for (const dep of deps.get(table) ?? [])
|
|
2080
|
+
visit(dep);
|
|
2081
|
+
visiting.delete(table);
|
|
2082
|
+
visited.add(table);
|
|
2083
|
+
sorted.push(table);
|
|
2084
|
+
}
|
|
2085
|
+
for (const table of tables)
|
|
2086
|
+
visit(table);
|
|
2087
|
+
return sorted;
|
|
2088
|
+
}
|
|
2089
|
+
async function resolvePrimaryKeys(source, target, table, option) {
|
|
2090
|
+
if (option)
|
|
2091
|
+
return Array.isArray(option) ? option : [option];
|
|
2092
|
+
const sourceKeys = await detectPrimaryKeys(source, table);
|
|
2093
|
+
if (sourceKeys.length > 0)
|
|
2094
|
+
return sourceKeys;
|
|
2095
|
+
return detectPrimaryKeys(target, table);
|
|
2096
|
+
}
|
|
2097
|
+
async function detectPrimaryKeys(adapter, table) {
|
|
2098
|
+
if (isAsyncAdapter(adapter)) {
|
|
2099
|
+
try {
|
|
2100
|
+
const rows = await adapter.all(`
|
|
2101
|
+
SELECT kcu.column_name, kcu.ordinal_position
|
|
2102
|
+
FROM information_schema.table_constraints tc
|
|
2103
|
+
JOIN information_schema.key_column_usage kcu
|
|
2104
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
2105
|
+
AND tc.table_schema = kcu.table_schema
|
|
2106
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
2107
|
+
AND tc.table_schema = 'public'
|
|
2108
|
+
AND tc.table_name = ?
|
|
2109
|
+
ORDER BY kcu.ordinal_position
|
|
2110
|
+
`, table);
|
|
2111
|
+
return rows.map((row) => row.column_name);
|
|
2112
|
+
} catch {
|
|
2113
|
+
return [];
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
try {
|
|
2117
|
+
const rows = adapter.all(`PRAGMA table_info(${quoteIdent(table)})`);
|
|
2118
|
+
return rows.filter((row) => row.pk > 0).sort((a, b) => a.pk - b.pk).map((row) => row.name);
|
|
2119
|
+
} catch {
|
|
2120
|
+
return [];
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
async function ensureTablesExist(source, target, tables) {
|
|
2124
|
+
if (!isAsyncAdapter(source) || isAsyncAdapter(target))
|
|
2125
|
+
return;
|
|
2126
|
+
for (const table of tables)
|
|
2127
|
+
await ensureTableInSqliteFromPg(target, source, table);
|
|
2128
|
+
}
|
|
2129
|
+
async function ensureTableInSqliteFromPg(target, source, table) {
|
|
2130
|
+
const existing = target.all(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, table);
|
|
2131
|
+
if (existing.length > 0)
|
|
2132
|
+
return;
|
|
2133
|
+
const columns = await source.all(`
|
|
2134
|
+
SELECT column_name, data_type, is_nullable
|
|
2135
|
+
FROM information_schema.columns
|
|
2136
|
+
WHERE table_schema = 'public' AND table_name = ?
|
|
2137
|
+
ORDER BY ordinal_position
|
|
2138
|
+
`, table);
|
|
2139
|
+
if (columns.length === 0)
|
|
2140
|
+
return;
|
|
2141
|
+
const primaryKeys = new Set(await detectPrimaryKeys(source, table));
|
|
2142
|
+
const definitions = columns.filter((column) => !["tsvector", "tsquery"].includes(column.data_type.toLowerCase())).map((column) => {
|
|
2143
|
+
const type = pgTypeToSqlite(column.data_type);
|
|
2144
|
+
const notNull = column.is_nullable === "NO" && !primaryKeys.has(column.column_name) ? " NOT NULL" : "";
|
|
2145
|
+
return `${quoteIdent(column.column_name)} ${type}${notNull}`;
|
|
2146
|
+
});
|
|
2147
|
+
if (primaryKeys.size > 0) {
|
|
2148
|
+
definitions.push(`PRIMARY KEY (${[...primaryKeys].map(quoteIdent).join(", ")})`);
|
|
2149
|
+
}
|
|
2150
|
+
target.exec(`CREATE TABLE IF NOT EXISTS ${quoteIdent(table)} (${definitions.join(", ")})`);
|
|
2151
|
+
}
|
|
2152
|
+
function pgTypeToSqlite(pgType) {
|
|
2153
|
+
const type = pgType.toLowerCase();
|
|
2154
|
+
if (type.includes("int") || ["bigint", "smallint", "serial", "bigserial"].includes(type))
|
|
2155
|
+
return "INTEGER";
|
|
2156
|
+
if (type.includes("bool"))
|
|
2157
|
+
return "INTEGER";
|
|
2158
|
+
if (type.includes("float") || type.includes("double") || ["real", "numeric", "decimal"].includes(type))
|
|
2159
|
+
return "REAL";
|
|
2160
|
+
if (type === "bytea")
|
|
2161
|
+
return "BLOB";
|
|
2162
|
+
return "TEXT";
|
|
2163
|
+
}
|
|
2164
|
+
async function filterColumnsForTarget(target, table, columns) {
|
|
2165
|
+
if (columns.includes("machine_id") && table !== "machines")
|
|
2166
|
+
await ensureMachineIdColumnInTarget(target, table);
|
|
2167
|
+
try {
|
|
2168
|
+
if (isAsyncAdapter(target)) {
|
|
2169
|
+
const rows2 = await target.all(`
|
|
2170
|
+
SELECT column_name
|
|
2171
|
+
FROM information_schema.columns
|
|
2172
|
+
WHERE table_schema = 'public' AND table_name = ?
|
|
2173
|
+
`, table);
|
|
2174
|
+
if (rows2.length === 0)
|
|
2175
|
+
return columns;
|
|
2176
|
+
const targetColumns2 = new Set(rows2.map((row) => row.column_name));
|
|
2177
|
+
return columns.filter((column) => targetColumns2.has(column));
|
|
2178
|
+
}
|
|
2179
|
+
const rows = target.all(`PRAGMA table_info(${quoteIdent(table)})`);
|
|
2180
|
+
if (rows.length === 0)
|
|
2181
|
+
return columns;
|
|
2182
|
+
const targetColumns = new Set(rows.map((row) => row.name));
|
|
2183
|
+
return columns.filter((column) => targetColumns.has(column));
|
|
2184
|
+
} catch {
|
|
2185
|
+
return columns;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
async function ensureMachineIdColumnInTarget(target, table) {
|
|
2189
|
+
if (isAsyncAdapter(target)) {
|
|
2190
|
+
const rows2 = await target.all(`
|
|
2191
|
+
SELECT column_name
|
|
2192
|
+
FROM information_schema.columns
|
|
2193
|
+
WHERE table_schema = 'public' AND table_name = ? AND column_name = 'machine_id'
|
|
2194
|
+
`, table);
|
|
2195
|
+
if (rows2.length === 0)
|
|
2196
|
+
await target.exec(`ALTER TABLE ${quoteIdent(table)} ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
const rows = target.all(`PRAGMA table_info(${quoteIdent(table)})`);
|
|
2200
|
+
if (!rows.some((row) => row.name === "machine_id")) {
|
|
2201
|
+
target.exec(`ALTER TABLE ${quoteIdent(table)} ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
async function syncTransfer(source, target, options, _direction) {
|
|
2205
|
+
const { tables, onProgress, batchSize = 100, conflictColumn = "updated_at", primaryKey } = options;
|
|
2206
|
+
const results = [];
|
|
2207
|
+
const sqliteTarget = isAsyncAdapter(target) ? null : target;
|
|
2208
|
+
await ensureTablesExist(source, target, tables);
|
|
2209
|
+
if (sqliteTarget) {
|
|
2210
|
+
try {
|
|
2211
|
+
sqliteTarget.exec("PRAGMA foreign_keys = OFF");
|
|
2212
|
+
} catch {}
|
|
2213
|
+
}
|
|
2214
|
+
try {
|
|
2215
|
+
for (let i = 0;i < tables.length; i++) {
|
|
2216
|
+
const table = tables[i];
|
|
2217
|
+
const result = { table, rowsRead: 0, rowsWritten: 0, rowsSkipped: 0, errors: [] };
|
|
2218
|
+
try {
|
|
2219
|
+
onProgress?.({ table, phase: "reading", rowsRead: 0, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
|
|
2220
|
+
const rows = await readAll(source, `SELECT * FROM ${quoteIdent(table)}`);
|
|
2221
|
+
result.rowsRead = rows.length;
|
|
2222
|
+
if (rows.length === 0) {
|
|
2223
|
+
onProgress?.({ table, phase: "done", rowsRead: 0, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
|
|
2224
|
+
results.push(result);
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
const sourceColumns = Object.keys(rows[0]);
|
|
2228
|
+
const columns = await filterColumnsForTarget(target, table, sourceColumns);
|
|
2229
|
+
const primaryKeys = await resolvePrimaryKeys(source, target, table, primaryKey);
|
|
2230
|
+
if (primaryKeys.length === 0) {
|
|
2231
|
+
result.errors.push(`Table "${table}" has no primary key; inserted without conflict handling`);
|
|
2232
|
+
for (const batch of batches(rows, batchSize)) {
|
|
2233
|
+
await insertBatch(target, table, columns, batch);
|
|
2234
|
+
result.rowsWritten += batch.length;
|
|
2235
|
+
}
|
|
2236
|
+
results.push(result);
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
const missingKeys = primaryKeys.filter((key) => !columns.includes(key));
|
|
2240
|
+
if (missingKeys.length > 0) {
|
|
2241
|
+
result.errors.push(`Table "${table}" missing primary key column(s): ${missingKeys.join(", ")}`);
|
|
2242
|
+
results.push(result);
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
onProgress?.({ table, phase: "writing", rowsRead: result.rowsRead, rowsWritten: 0, totalTables: tables.length, currentTableIndex: i });
|
|
2246
|
+
const updateColumns = columns.filter((column) => !primaryKeys.includes(column));
|
|
2247
|
+
const newestWinsColumn = columns.includes(conflictColumn) ? conflictColumn : undefined;
|
|
2248
|
+
for (const batch of batches(rows, batchSize)) {
|
|
2249
|
+
await upsertBatch(target, table, columns, updateColumns, primaryKeys, batch, newestWinsColumn);
|
|
2250
|
+
result.rowsWritten += batch.length;
|
|
2251
|
+
onProgress?.({ table, phase: "writing", rowsRead: result.rowsRead, rowsWritten: result.rowsWritten, totalTables: tables.length, currentTableIndex: i });
|
|
2252
|
+
}
|
|
2253
|
+
onProgress?.({ table, phase: "done", rowsRead: result.rowsRead, rowsWritten: result.rowsWritten, totalTables: tables.length, currentTableIndex: i });
|
|
2254
|
+
} catch (error) {
|
|
2255
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
2256
|
+
}
|
|
2257
|
+
results.push(result);
|
|
2258
|
+
}
|
|
2259
|
+
} finally {
|
|
2260
|
+
if (sqliteTarget) {
|
|
2261
|
+
try {
|
|
2262
|
+
sqliteTarget.exec("PRAGMA foreign_keys = ON");
|
|
2263
|
+
} catch {}
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return results;
|
|
2267
|
+
}
|
|
2268
|
+
function batches(rows, size) {
|
|
2269
|
+
const result = [];
|
|
2270
|
+
for (let offset = 0;offset < rows.length; offset += size)
|
|
2271
|
+
result.push(rows.slice(offset, offset + size));
|
|
2272
|
+
return result;
|
|
2273
|
+
}
|
|
2274
|
+
async function upsertBatch(target, table, columns, updateColumns, primaryKeys, batch, conflictColumn) {
|
|
2275
|
+
if (batch.length === 0 || columns.length === 0)
|
|
2276
|
+
return;
|
|
2277
|
+
const fallbackKey = primaryKeys[0] ?? columns[0] ?? "id";
|
|
2278
|
+
const columnList = columns.map(quoteIdent).join(", ");
|
|
2279
|
+
const keyList = primaryKeys.map(quoteIdent).join(", ");
|
|
2280
|
+
const setClause = updateColumns.length > 0 ? updateColumns.map((column) => `${quoteIdent(column)} = EXCLUDED.${quoteIdent(column)}`).join(", ") : `${quoteIdent(fallbackKey)} = EXCLUDED.${quoteIdent(fallbackKey)}`;
|
|
2281
|
+
const whereClause = conflictColumn && updateColumns.includes(conflictColumn) ? ` WHERE ${quoteIdent(table)}.${quoteIdent(conflictColumn)} IS NULL OR EXCLUDED.${quoteIdent(conflictColumn)} >= ${quoteIdent(table)}.${quoteIdent(conflictColumn)}` : "";
|
|
2282
|
+
if (isAsyncAdapter(target)) {
|
|
2283
|
+
const placeholders2 = batch.map((_, rowIndex) => `(${columns.map((__, columnIndex) => `$${rowIndex * columns.length + columnIndex + 1}`).join(", ")})`).join(", ");
|
|
2284
|
+
const params2 = batch.flatMap((row) => columns.map((column) => row[column] ?? null));
|
|
2285
|
+
await target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders2}
|
|
2286
|
+
ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}${whereClause}`, ...params2);
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
const placeholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
2290
|
+
const params = batch.flatMap((row) => columns.map((column) => coerceForSqlite(row[column])));
|
|
2291
|
+
target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders}
|
|
2292
|
+
ON CONFLICT (${keyList}) DO UPDATE SET ${setClause}${whereClause}`, ...params);
|
|
2293
|
+
}
|
|
2294
|
+
async function insertBatch(target, table, columns, batch) {
|
|
2295
|
+
if (batch.length === 0 || columns.length === 0)
|
|
2296
|
+
return;
|
|
2297
|
+
const columnList = columns.map(quoteIdent).join(", ");
|
|
2298
|
+
if (isAsyncAdapter(target)) {
|
|
2299
|
+
const placeholders2 = batch.map((_, rowIndex) => `(${columns.map((__, columnIndex) => `$${rowIndex * columns.length + columnIndex + 1}`).join(", ")})`).join(", ");
|
|
2300
|
+
const params2 = batch.flatMap((row) => columns.map((column) => row[column] ?? null));
|
|
2301
|
+
await target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders2}`, ...params2);
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
const placeholders = batch.map(() => `(${columns.map(() => "?").join(", ")})`).join(", ");
|
|
2305
|
+
const params = batch.flatMap((row) => columns.map((column) => coerceForSqlite(row[column])));
|
|
2306
|
+
target.run(`INSERT INTO ${quoteIdent(table)} (${columnList}) VALUES ${placeholders}`, ...params);
|
|
2307
|
+
}
|
|
2308
|
+
function coerceForSqlite(value) {
|
|
2309
|
+
if (value === null || value === undefined)
|
|
2310
|
+
return null;
|
|
2311
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
|
|
2312
|
+
return value;
|
|
2313
|
+
if (value instanceof Date)
|
|
2314
|
+
return value.toISOString();
|
|
2315
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array)
|
|
2316
|
+
return value;
|
|
2317
|
+
if (typeof value === "object")
|
|
2318
|
+
return JSON.stringify(value);
|
|
2319
|
+
return String(value);
|
|
2320
|
+
}
|
|
2321
|
+
function isAsyncAdapter(adapter) {
|
|
2322
|
+
return adapter instanceof PgAdapterAsync;
|
|
2323
|
+
}
|
|
2324
|
+
async function readAll(adapter, sql) {
|
|
2325
|
+
const rows = adapter.all(sql);
|
|
2326
|
+
return rows instanceof Promise ? await rows : rows;
|
|
2327
|
+
}
|
|
2328
|
+
var init_storage_sync = __esm(() => {
|
|
2329
|
+
init_remote_storage();
|
|
2330
|
+
});
|
|
2331
|
+
|
|
1907
2332
|
// src/db/pg-migrations.ts
|
|
1908
2333
|
var exports_pg_migrations = {};
|
|
1909
2334
|
__export(exports_pg_migrations, {
|
|
@@ -2093,6 +2518,9 @@ var init_pg_migrations = __esm(() => {
|
|
|
2093
2518
|
});
|
|
2094
2519
|
|
|
2095
2520
|
// src/lib/cloud-sync.ts
|
|
2521
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
2522
|
+
import { homedir as homedir2, platform } from "os";
|
|
2523
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
2096
2524
|
function getCloudDatabaseUrl() {
|
|
2097
2525
|
return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
|
|
2098
2526
|
}
|
|
@@ -2111,7 +2539,6 @@ async function getCloudPg() {
|
|
|
2111
2539
|
if (!url) {
|
|
2112
2540
|
throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
|
|
2113
2541
|
}
|
|
2114
|
-
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
2115
2542
|
return new PgAdapterAsync(url);
|
|
2116
2543
|
}
|
|
2117
2544
|
async function runCloudMigrations(cloud) {
|
|
@@ -2121,31 +2548,35 @@ async function runCloudMigrations(cloud) {
|
|
|
2121
2548
|
}
|
|
2122
2549
|
}
|
|
2123
2550
|
async function cloudPush(opts) {
|
|
2124
|
-
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
2125
2551
|
const cloud = await getCloudPg();
|
|
2126
|
-
const local =
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2552
|
+
const local = openDatabase(getDbPath(), true);
|
|
2553
|
+
try {
|
|
2554
|
+
await runCloudMigrations(cloud);
|
|
2555
|
+
touchMachineRegistry(local, "push");
|
|
2556
|
+
const tables = resolveCloudTables(opts?.tables);
|
|
2557
|
+
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
2558
|
+
const rows = results.reduce((sum, result) => sum + result.rowsWritten, 0);
|
|
2559
|
+
return { rows, machine: getMachineId() };
|
|
2560
|
+
} finally {
|
|
2561
|
+
local.close();
|
|
2562
|
+
await cloud.close();
|
|
2563
|
+
}
|
|
2135
2564
|
}
|
|
2136
2565
|
async function cloudPull(opts) {
|
|
2137
|
-
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
2138
2566
|
const cloud = await getCloudPg();
|
|
2139
|
-
const local =
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2567
|
+
const local = openDatabase(getDbPath(), true);
|
|
2568
|
+
try {
|
|
2569
|
+
await runCloudMigrations(cloud);
|
|
2570
|
+
const tables = resolveCloudTables(opts?.tables);
|
|
2571
|
+
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
2572
|
+
const rows = results.reduce((sum, result) => sum + result.rowsWritten, 0);
|
|
2573
|
+
touchMachineRegistry(local, "pull");
|
|
2574
|
+
setLastCloudPull();
|
|
2575
|
+
return { rows, machine: getMachineId() };
|
|
2576
|
+
} finally {
|
|
2577
|
+
local.close();
|
|
2578
|
+
await cloud.close();
|
|
2579
|
+
}
|
|
2149
2580
|
}
|
|
2150
2581
|
async function cloudSyncFull() {
|
|
2151
2582
|
const push = await cloudPush();
|
|
@@ -2153,13 +2584,21 @@ async function cloudSyncFull() {
|
|
|
2153
2584
|
return { push: push.rows, pull: pull.rows, machine: getMachineId() };
|
|
2154
2585
|
}
|
|
2155
2586
|
function setLastCloudPull(at = new Date().toISOString()) {
|
|
2156
|
-
const db = openDatabase();
|
|
2157
|
-
|
|
2587
|
+
const db = openDatabase(undefined, true);
|
|
2588
|
+
try {
|
|
2589
|
+
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
|
|
2590
|
+
} finally {
|
|
2591
|
+
db.close();
|
|
2592
|
+
}
|
|
2158
2593
|
}
|
|
2159
2594
|
function getLastCloudPull() {
|
|
2160
|
-
const db = openDatabase();
|
|
2161
|
-
|
|
2162
|
-
|
|
2595
|
+
const db = openDatabase(undefined, true);
|
|
2596
|
+
try {
|
|
2597
|
+
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
|
|
2598
|
+
return row?.value ?? null;
|
|
2599
|
+
} finally {
|
|
2600
|
+
db.close();
|
|
2601
|
+
}
|
|
2163
2602
|
}
|
|
2164
2603
|
function shouldPullFromCloud() {
|
|
2165
2604
|
if (!getCloudDatabaseUrl())
|
|
@@ -2205,22 +2644,231 @@ function touchMachineRegistry(db, direction) {
|
|
|
2205
2644
|
updated_at = excluded.updated_at
|
|
2206
2645
|
`).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
|
|
2207
2646
|
}
|
|
2647
|
+
function resolveCloudTables(tables) {
|
|
2648
|
+
if (!tables || tables.length === 0)
|
|
2649
|
+
return [...CLOUD_TABLES];
|
|
2650
|
+
const allowed = new Set(CLOUD_TABLES);
|
|
2651
|
+
const requested = tables.map((table) => table.trim()).filter(Boolean);
|
|
2652
|
+
const invalid = requested.filter((table) => !allowed.has(table));
|
|
2653
|
+
if (invalid.length > 0) {
|
|
2654
|
+
throw new Error(`Unknown economy sync table(s): ${invalid.join(", ")}`);
|
|
2655
|
+
}
|
|
2656
|
+
return requested;
|
|
2657
|
+
}
|
|
2208
2658
|
async function registerCloudSchedule(intervalMinutes) {
|
|
2209
|
-
|
|
2210
|
-
|
|
2659
|
+
if (!Number.isFinite(intervalMinutes) || intervalMinutes <= 0) {
|
|
2660
|
+
throw new Error("Cloud sync interval must be greater than 0 minutes");
|
|
2661
|
+
}
|
|
2662
|
+
mkdirSync3(SCHEDULE_CONFIG_DIR, { recursive: true });
|
|
2663
|
+
if (platform() === "darwin") {
|
|
2664
|
+
await registerLaunchd(intervalMinutes);
|
|
2665
|
+
} else if (platform() === "linux") {
|
|
2666
|
+
await registerSystemd(intervalMinutes);
|
|
2667
|
+
} else {
|
|
2668
|
+
throw new Error(`Automatic economy cloud sync is not supported on ${platform()}`);
|
|
2669
|
+
}
|
|
2670
|
+
writeFileSync2(SCHEDULE_CONFIG_PATH, JSON.stringify({ intervalMinutes, updatedAt: new Date().toISOString() }, null, 2));
|
|
2211
2671
|
}
|
|
2212
2672
|
async function removeCloudSchedule() {
|
|
2213
|
-
|
|
2214
|
-
|
|
2673
|
+
if (platform() === "darwin")
|
|
2674
|
+
await removeLaunchd();
|
|
2675
|
+
if (platform() === "linux")
|
|
2676
|
+
await removeSystemd();
|
|
2677
|
+
try {
|
|
2678
|
+
unlinkSync(SCHEDULE_CONFIG_PATH);
|
|
2679
|
+
} catch {}
|
|
2215
2680
|
}
|
|
2216
2681
|
async function getCloudScheduleStatus() {
|
|
2217
|
-
const
|
|
2218
|
-
|
|
2682
|
+
const mechanism = platform() === "darwin" ? "launchd" : platform() === "linux" ? "systemd" : "none";
|
|
2683
|
+
const interval = readScheduleInterval();
|
|
2684
|
+
const registered = mechanism === "launchd" ? existsSync3(getLaunchdPlistPath()) : mechanism === "systemd" ? existsSync3(join4(getSystemdDir(), `${SCHEDULE_SERVICE_NAME}.timer`)) : false;
|
|
2685
|
+
return {
|
|
2686
|
+
registered,
|
|
2687
|
+
schedule_minutes: interval,
|
|
2688
|
+
cron_expression: interval > 0 ? minutesToCron(interval) : null,
|
|
2689
|
+
mechanism
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
function readScheduleInterval() {
|
|
2693
|
+
try {
|
|
2694
|
+
const parsed = JSON.parse(readFileSync3(SCHEDULE_CONFIG_PATH, "utf8"));
|
|
2695
|
+
return typeof parsed.intervalMinutes === "number" && parsed.intervalMinutes > 0 ? parsed.intervalMinutes : 0;
|
|
2696
|
+
} catch {
|
|
2697
|
+
return 0;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
function minutesToCron(minutes) {
|
|
2701
|
+
if (minutes < 60)
|
|
2702
|
+
return `*/${minutes} * * * *`;
|
|
2703
|
+
const hours = Math.floor(minutes / 60);
|
|
2704
|
+
const remainder = minutes % 60;
|
|
2705
|
+
return remainder === 0 && hours <= 24 ? `0 */${hours} * * *` : `*/${minutes} * * * *`;
|
|
2706
|
+
}
|
|
2707
|
+
function getModuleDir() {
|
|
2708
|
+
return typeof import.meta.dir === "string" ? import.meta.dir : dirname2(new URL(import.meta.url).pathname);
|
|
2709
|
+
}
|
|
2710
|
+
function getBunPath() {
|
|
2711
|
+
const candidates = [
|
|
2712
|
+
join4(homedir2(), ".bun", "bin", "bun"),
|
|
2713
|
+
"/opt/homebrew/bin/bun",
|
|
2714
|
+
"/usr/local/bin/bun",
|
|
2715
|
+
"/usr/bin/bun"
|
|
2716
|
+
];
|
|
2717
|
+
return candidates.find((candidate) => existsSync3(candidate)) ?? "bun";
|
|
2718
|
+
}
|
|
2719
|
+
function getEconomySyncCommand() {
|
|
2720
|
+
const dir = getModuleDir();
|
|
2721
|
+
const candidates = [
|
|
2722
|
+
join4(dir, "..", "cli", "index.js"),
|
|
2723
|
+
join4(dir, "..", "cli", "index.ts")
|
|
2724
|
+
];
|
|
2725
|
+
const cliPath = candidates.find((candidate) => existsSync3(candidate));
|
|
2726
|
+
return cliPath ? [getBunPath(), "run", cliPath, "cloud", "sync"] : ["economy", "cloud", "sync"];
|
|
2727
|
+
}
|
|
2728
|
+
function scheduleEnvironment() {
|
|
2729
|
+
const keys = [
|
|
2730
|
+
"ECONOMY_CLOUD_DATABASE_URL",
|
|
2731
|
+
"HASNA_ECONOMY_CLOUD_DATABASE_URL",
|
|
2732
|
+
"HASNA_ECONOMY_DB_PATH",
|
|
2733
|
+
"ECONOMY_DB",
|
|
2734
|
+
"ECONOMY_MACHINE_ID",
|
|
2735
|
+
"ECONOMY_CLOUD_AUTO"
|
|
2736
|
+
];
|
|
2737
|
+
const env = {
|
|
2738
|
+
HOME: homedir2(),
|
|
2739
|
+
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin"
|
|
2740
|
+
};
|
|
2741
|
+
for (const key of keys) {
|
|
2742
|
+
const value = process.env[key];
|
|
2743
|
+
if (value)
|
|
2744
|
+
env[key] = value;
|
|
2745
|
+
}
|
|
2746
|
+
return env;
|
|
2747
|
+
}
|
|
2748
|
+
function xmlEscape(value) {
|
|
2749
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2750
|
+
}
|
|
2751
|
+
function getLaunchdPlistPath() {
|
|
2752
|
+
return join4(homedir2(), "Library", "LaunchAgents", "com.hasna.economy-cloud-sync.plist");
|
|
2753
|
+
}
|
|
2754
|
+
function createLaunchdPlist(intervalMinutes) {
|
|
2755
|
+
const args = getEconomySyncCommand();
|
|
2756
|
+
const env = scheduleEnvironment();
|
|
2757
|
+
const stdout = join4(SCHEDULE_CONFIG_DIR, "cloud-sync.log");
|
|
2758
|
+
const stderr = join4(SCHEDULE_CONFIG_DIR, "cloud-sync-error.log");
|
|
2759
|
+
const programArgs = args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join(`
|
|
2760
|
+
`);
|
|
2761
|
+
const environment = Object.entries(env).map(([key, value]) => ` <key>${xmlEscape(key)}</key>
|
|
2762
|
+
<string>${xmlEscape(value)}</string>`).join(`
|
|
2763
|
+
`);
|
|
2764
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2765
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2766
|
+
<plist version="1.0">
|
|
2767
|
+
<dict>
|
|
2768
|
+
<key>Label</key>
|
|
2769
|
+
<string>com.hasna.economy-cloud-sync</string>
|
|
2770
|
+
<key>ProgramArguments</key>
|
|
2771
|
+
<array>
|
|
2772
|
+
${programArgs}
|
|
2773
|
+
</array>
|
|
2774
|
+
<key>StartInterval</key>
|
|
2775
|
+
<integer>${Math.round(intervalMinutes * 60)}</integer>
|
|
2776
|
+
<key>RunAtLoad</key>
|
|
2777
|
+
<true/>
|
|
2778
|
+
<key>StandardOutPath</key>
|
|
2779
|
+
<string>${xmlEscape(stdout)}</string>
|
|
2780
|
+
<key>StandardErrorPath</key>
|
|
2781
|
+
<string>${xmlEscape(stderr)}</string>
|
|
2782
|
+
<key>EnvironmentVariables</key>
|
|
2783
|
+
<dict>
|
|
2784
|
+
${environment}
|
|
2785
|
+
</dict>
|
|
2786
|
+
</dict>
|
|
2787
|
+
</plist>`;
|
|
2788
|
+
}
|
|
2789
|
+
async function registerLaunchd(intervalMinutes) {
|
|
2790
|
+
const plistPath = getLaunchdPlistPath();
|
|
2791
|
+
mkdirSync3(dirname2(plistPath), { recursive: true });
|
|
2792
|
+
try {
|
|
2793
|
+
await Bun.spawn(["launchctl", "unload", plistPath]).exited;
|
|
2794
|
+
} catch {}
|
|
2795
|
+
writeFileSync2(plistPath, createLaunchdPlist(intervalMinutes));
|
|
2796
|
+
await Bun.spawn(["launchctl", "load", plistPath]).exited;
|
|
2797
|
+
}
|
|
2798
|
+
async function removeLaunchd() {
|
|
2799
|
+
const plistPath = getLaunchdPlistPath();
|
|
2800
|
+
try {
|
|
2801
|
+
await Bun.spawn(["launchctl", "unload", plistPath]).exited;
|
|
2802
|
+
} catch {}
|
|
2803
|
+
try {
|
|
2804
|
+
unlinkSync(plistPath);
|
|
2805
|
+
} catch {}
|
|
2219
2806
|
}
|
|
2220
|
-
|
|
2807
|
+
function shellArg(value) {
|
|
2808
|
+
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2809
|
+
}
|
|
2810
|
+
function getSystemdDir() {
|
|
2811
|
+
return join4(homedir2(), ".config", "systemd", "user");
|
|
2812
|
+
}
|
|
2813
|
+
function createSystemdService() {
|
|
2814
|
+
const command = getEconomySyncCommand().map(shellArg).join(" ");
|
|
2815
|
+
const environment = Object.entries(scheduleEnvironment()).map(([key, value]) => `Environment=${key}=${shellArg(value)}`).join(`
|
|
2816
|
+
`);
|
|
2817
|
+
return `[Unit]
|
|
2818
|
+
Description=Hasna Economy Cloud Sync
|
|
2819
|
+
After=network.target
|
|
2820
|
+
|
|
2821
|
+
[Service]
|
|
2822
|
+
Type=oneshot
|
|
2823
|
+
ExecStart=${command}
|
|
2824
|
+
${environment}
|
|
2825
|
+
|
|
2826
|
+
[Install]
|
|
2827
|
+
WantedBy=default.target
|
|
2828
|
+
`;
|
|
2829
|
+
}
|
|
2830
|
+
function createSystemdTimer(intervalMinutes) {
|
|
2831
|
+
return `[Unit]
|
|
2832
|
+
Description=Hasna Economy Cloud Sync Timer
|
|
2833
|
+
|
|
2834
|
+
[Timer]
|
|
2835
|
+
OnBootSec=${intervalMinutes}min
|
|
2836
|
+
OnUnitActiveSec=${intervalMinutes}min
|
|
2837
|
+
Persistent=true
|
|
2838
|
+
|
|
2839
|
+
[Install]
|
|
2840
|
+
WantedBy=timers.target
|
|
2841
|
+
`;
|
|
2842
|
+
}
|
|
2843
|
+
async function registerSystemd(intervalMinutes) {
|
|
2844
|
+
const dir = getSystemdDir();
|
|
2845
|
+
mkdirSync3(dir, { recursive: true });
|
|
2846
|
+
writeFileSync2(join4(dir, `${SCHEDULE_SERVICE_NAME}.service`), createSystemdService());
|
|
2847
|
+
writeFileSync2(join4(dir, `${SCHEDULE_SERVICE_NAME}.timer`), createSystemdTimer(intervalMinutes));
|
|
2848
|
+
await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
|
|
2849
|
+
await Bun.spawn(["systemctl", "--user", "enable", "--now", `${SCHEDULE_SERVICE_NAME}.timer`]).exited;
|
|
2850
|
+
}
|
|
2851
|
+
async function removeSystemd() {
|
|
2852
|
+
try {
|
|
2853
|
+
await Bun.spawn(["systemctl", "--user", "disable", "--now", `${SCHEDULE_SERVICE_NAME}.timer`]).exited;
|
|
2854
|
+
} catch {}
|
|
2855
|
+
const dir = getSystemdDir();
|
|
2856
|
+
try {
|
|
2857
|
+
unlinkSync(join4(dir, `${SCHEDULE_SERVICE_NAME}.service`));
|
|
2858
|
+
} catch {}
|
|
2859
|
+
try {
|
|
2860
|
+
unlinkSync(join4(dir, `${SCHEDULE_SERVICE_NAME}.timer`));
|
|
2861
|
+
} catch {}
|
|
2862
|
+
try {
|
|
2863
|
+
await Bun.spawn(["systemctl", "--user", "daemon-reload"]).exited;
|
|
2864
|
+
} catch {}
|
|
2865
|
+
}
|
|
2866
|
+
var CLOUD_TABLES, SCHEDULE_SERVICE_NAME = "hasna-economy-cloud-sync", SCHEDULE_CONFIG_DIR, SCHEDULE_CONFIG_PATH;
|
|
2221
2867
|
var init_cloud_sync = __esm(() => {
|
|
2222
2868
|
init_database();
|
|
2223
2869
|
init_package_metadata();
|
|
2870
|
+
init_remote_storage();
|
|
2871
|
+
init_storage_sync();
|
|
2224
2872
|
CLOUD_TABLES = [
|
|
2225
2873
|
"requests",
|
|
2226
2874
|
"sessions",
|
|
@@ -2235,6 +2883,8 @@ var init_cloud_sync = __esm(() => {
|
|
|
2235
2883
|
"machines",
|
|
2236
2884
|
"ingest_state"
|
|
2237
2885
|
];
|
|
2886
|
+
SCHEDULE_CONFIG_DIR = join4(homedir2(), ".hasna", "economy");
|
|
2887
|
+
SCHEDULE_CONFIG_PATH = join4(SCHEDULE_CONFIG_DIR, "cloud-sync-schedule.json");
|
|
2238
2888
|
});
|
|
2239
2889
|
|
|
2240
2890
|
// src/lib/serve-auth.ts
|
|
@@ -2404,27 +3054,27 @@ var init_agents = __esm(() => {
|
|
|
2404
3054
|
});
|
|
2405
3055
|
|
|
2406
3056
|
// src/lib/paths.ts
|
|
2407
|
-
import { homedir as
|
|
2408
|
-
import { join as
|
|
3057
|
+
import { homedir as homedir3 } from "os";
|
|
3058
|
+
import { join as join5 } from "path";
|
|
2409
3059
|
function getHomeDir() {
|
|
2410
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ??
|
|
3060
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir3();
|
|
2411
3061
|
}
|
|
2412
3062
|
function agentPaths() {
|
|
2413
3063
|
const home = getHomeDir();
|
|
2414
3064
|
return {
|
|
2415
|
-
claudeProjects:
|
|
2416
|
-
claudeCredentials:
|
|
2417
|
-
takumiProjects:
|
|
2418
|
-
codexDir:
|
|
2419
|
-
codexDb:
|
|
2420
|
-
codexAuth:
|
|
2421
|
-
codexConfig:
|
|
2422
|
-
geminiTmp:
|
|
2423
|
-
geminiHistory:
|
|
2424
|
-
opencodeMessages:
|
|
2425
|
-
piSessions:
|
|
2426
|
-
hermesDir:
|
|
2427
|
-
hermesDb:
|
|
3065
|
+
claudeProjects: join5(home, ".claude", "projects"),
|
|
3066
|
+
claudeCredentials: join5(home, ".claude", ".credentials.json"),
|
|
3067
|
+
takumiProjects: join5(home, ".takumi", "projects"),
|
|
3068
|
+
codexDir: join5(home, ".codex"),
|
|
3069
|
+
codexDb: join5(home, ".codex", "state_5.sqlite"),
|
|
3070
|
+
codexAuth: join5(home, ".codex", "auth.json"),
|
|
3071
|
+
codexConfig: join5(home, ".codex", "config.toml"),
|
|
3072
|
+
geminiTmp: join5(home, ".gemini", "tmp"),
|
|
3073
|
+
geminiHistory: join5(home, ".gemini", "history"),
|
|
3074
|
+
opencodeMessages: join5(home, ".local", "share", "opencode", "storage", "message"),
|
|
3075
|
+
piSessions: join5(home, ".pi", "agent", "sessions"),
|
|
3076
|
+
hermesDir: join5(home, ".hermes"),
|
|
3077
|
+
hermesDb: join5(home, ".hermes", "state.db")
|
|
2428
3078
|
};
|
|
2429
3079
|
}
|
|
2430
3080
|
var init_paths = () => {};
|
|
@@ -2565,9 +3215,9 @@ var init_accounts = __esm(() => {
|
|
|
2565
3215
|
});
|
|
2566
3216
|
|
|
2567
3217
|
// src/ingest/claude.ts
|
|
2568
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
2569
|
-
import { homedir as
|
|
2570
|
-
import { join as
|
|
3218
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
3219
|
+
import { homedir as homedir4 } from "os";
|
|
3220
|
+
import { join as join7, basename } from "path";
|
|
2571
3221
|
function autoDetectProject(cwd, projects) {
|
|
2572
3222
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
2573
3223
|
}
|
|
@@ -2580,9 +3230,9 @@ function collectJsonlFiles(projectDir) {
|
|
|
2580
3230
|
try {
|
|
2581
3231
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
2582
3232
|
if (entry.isDirectory())
|
|
2583
|
-
walk(
|
|
3233
|
+
walk(join7(dir, entry.name));
|
|
2584
3234
|
else if (entry.name.endsWith(".jsonl"))
|
|
2585
|
-
files.push(
|
|
3235
|
+
files.push(join7(dir, entry.name));
|
|
2586
3236
|
}
|
|
2587
3237
|
} catch {}
|
|
2588
3238
|
}
|
|
@@ -2596,7 +3246,7 @@ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_D
|
|
|
2596
3246
|
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
2597
3247
|
}
|
|
2598
3248
|
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
2599
|
-
if (!
|
|
3249
|
+
if (!existsSync5(projectsDir)) {
|
|
2600
3250
|
if (verbose)
|
|
2601
3251
|
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
2602
3252
|
return { files: 0, requests: 0, sessions: 0 };
|
|
@@ -2609,7 +3259,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
2609
3259
|
const account = await resolveAccountForAgent(agentName);
|
|
2610
3260
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
2611
3261
|
for (const projectDirEntry of projectDirs) {
|
|
2612
|
-
const projectDirPath =
|
|
3262
|
+
const projectDirPath = join7(projectsDir, projectDirEntry.name);
|
|
2613
3263
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
2614
3264
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
2615
3265
|
for (const filePath of jsonlFiles) {
|
|
@@ -2625,7 +3275,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
2625
3275
|
continue;
|
|
2626
3276
|
let lines;
|
|
2627
3277
|
try {
|
|
2628
|
-
lines =
|
|
3278
|
+
lines = readFileSync4(filePath, "utf-8").split(`
|
|
2629
3279
|
`).filter((l) => l.trim());
|
|
2630
3280
|
} catch {
|
|
2631
3281
|
continue;
|
|
@@ -2746,15 +3396,15 @@ var init_claude = __esm(() => {
|
|
|
2746
3396
|
init_database();
|
|
2747
3397
|
init_pricing();
|
|
2748
3398
|
init_accounts();
|
|
2749
|
-
CLAUDE_PROJECTS_DIR =
|
|
2750
|
-
TAKUMI_PROJECTS_DIR =
|
|
3399
|
+
CLAUDE_PROJECTS_DIR = join7(homedir4(), ".claude", "projects");
|
|
3400
|
+
TAKUMI_PROJECTS_DIR = join7(homedir4(), ".takumi", "projects");
|
|
2751
3401
|
});
|
|
2752
3402
|
|
|
2753
3403
|
// src/ingest/codex.ts
|
|
2754
|
-
import { existsSync as
|
|
2755
|
-
import { homedir as
|
|
2756
|
-
import { join as
|
|
2757
|
-
import { Database as
|
|
3404
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
3405
|
+
import { homedir as homedir5 } from "os";
|
|
3406
|
+
import { join as join8, basename as basename2 } from "path";
|
|
3407
|
+
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
2758
3408
|
function codexDbPath() {
|
|
2759
3409
|
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
2760
3410
|
}
|
|
@@ -2763,10 +3413,10 @@ function codexConfigPath() {
|
|
|
2763
3413
|
}
|
|
2764
3414
|
function readCodexModel() {
|
|
2765
3415
|
const configPath = codexConfigPath();
|
|
2766
|
-
if (!
|
|
3416
|
+
if (!existsSync6(configPath))
|
|
2767
3417
|
return "gpt-5-codex";
|
|
2768
3418
|
try {
|
|
2769
|
-
const content =
|
|
3419
|
+
const content = readFileSync5(configPath, "utf-8");
|
|
2770
3420
|
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
2771
3421
|
return match?.[1] ?? "gpt-5-codex";
|
|
2772
3422
|
} catch {
|
|
@@ -2789,7 +3439,7 @@ function openCodexDb(dbPath, verbose) {
|
|
|
2789
3439
|
for (const readonly of [true, false]) {
|
|
2790
3440
|
let codexDb = null;
|
|
2791
3441
|
try {
|
|
2792
|
-
codexDb = readonly ? new
|
|
3442
|
+
codexDb = readonly ? new BunDatabase2(dbPath, { readonly: true }) : new BunDatabase2(dbPath);
|
|
2793
3443
|
codexDb.prepare("PRAGMA schema_version").get();
|
|
2794
3444
|
return codexDb;
|
|
2795
3445
|
} catch (error) {
|
|
@@ -2804,12 +3454,12 @@ function openCodexDb(dbPath, verbose) {
|
|
|
2804
3454
|
return null;
|
|
2805
3455
|
}
|
|
2806
3456
|
function readTokenEvents(rolloutPath) {
|
|
2807
|
-
if (!rolloutPath || !
|
|
3457
|
+
if (!rolloutPath || !existsSync6(rolloutPath))
|
|
2808
3458
|
return [];
|
|
2809
3459
|
const fallbackUsages = new Map;
|
|
2810
3460
|
let fallbackTimestamp;
|
|
2811
3461
|
let aggregate = null;
|
|
2812
|
-
for (const line of
|
|
3462
|
+
for (const line of readFileSync5(rolloutPath, "utf-8").split(`
|
|
2813
3463
|
`)) {
|
|
2814
3464
|
if (!line.trim())
|
|
2815
3465
|
continue;
|
|
@@ -2881,7 +3531,7 @@ function fallbackEvents(totalTokens) {
|
|
|
2881
3531
|
}
|
|
2882
3532
|
async function ingestCodex(db, verbose = false) {
|
|
2883
3533
|
const dbPath = codexDbPath();
|
|
2884
|
-
if (!
|
|
3534
|
+
if (!existsSync6(dbPath)) {
|
|
2885
3535
|
if (verbose)
|
|
2886
3536
|
console.log("Codex DB not found:", dbPath);
|
|
2887
3537
|
return { sessions: 0, requests: 0 };
|
|
@@ -2966,14 +3616,14 @@ var init_codex = __esm(() => {
|
|
|
2966
3616
|
init_database();
|
|
2967
3617
|
init_pricing();
|
|
2968
3618
|
init_accounts();
|
|
2969
|
-
DEFAULT_CODEX_DB_PATH =
|
|
2970
|
-
DEFAULT_CODEX_CONFIG_PATH =
|
|
3619
|
+
DEFAULT_CODEX_DB_PATH = join8(homedir5(), ".codex", "state_5.sqlite");
|
|
3620
|
+
DEFAULT_CODEX_CONFIG_PATH = join8(homedir5(), ".codex", "config.toml");
|
|
2971
3621
|
});
|
|
2972
3622
|
|
|
2973
3623
|
// src/ingest/gemini.ts
|
|
2974
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
2975
|
-
import { homedir as
|
|
2976
|
-
import { join as
|
|
3624
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync6, existsSync as existsSync7, statSync as statSync3 } from "fs";
|
|
3625
|
+
import { homedir as homedir6 } from "os";
|
|
3626
|
+
import { join as join9, basename as basename3 } from "path";
|
|
2977
3627
|
function geminiTmpDir() {
|
|
2978
3628
|
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
2979
3629
|
}
|
|
@@ -2990,12 +3640,12 @@ function numberField(...values) {
|
|
|
2990
3640
|
function listProjectDirs(...roots) {
|
|
2991
3641
|
const dirs = new Set;
|
|
2992
3642
|
for (const root of roots) {
|
|
2993
|
-
if (!
|
|
3643
|
+
if (!existsSync7(root))
|
|
2994
3644
|
continue;
|
|
2995
3645
|
try {
|
|
2996
3646
|
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
2997
3647
|
if (entry.isDirectory())
|
|
2998
|
-
dirs.add(
|
|
3648
|
+
dirs.add(join9(root, entry.name));
|
|
2999
3649
|
}
|
|
3000
3650
|
} catch {}
|
|
3001
3651
|
}
|
|
@@ -3006,17 +3656,17 @@ function projectRoot(projectDir, chatData) {
|
|
|
3006
3656
|
return chatData.projectPath;
|
|
3007
3657
|
if (chatData.project_path)
|
|
3008
3658
|
return chatData.project_path;
|
|
3009
|
-
const rootFile =
|
|
3659
|
+
const rootFile = join9(projectDir, ".project_root");
|
|
3010
3660
|
try {
|
|
3011
|
-
if (
|
|
3012
|
-
return
|
|
3661
|
+
if (existsSync7(rootFile))
|
|
3662
|
+
return readFileSync6(rootFile, "utf-8").trim();
|
|
3013
3663
|
} catch {}
|
|
3014
3664
|
return "";
|
|
3015
3665
|
}
|
|
3016
3666
|
async function ingestGemini(db, verbose) {
|
|
3017
3667
|
const tmpDir = geminiTmpDir();
|
|
3018
3668
|
const historyDir = geminiHistoryDir();
|
|
3019
|
-
if (!
|
|
3669
|
+
if (!existsSync7(tmpDir) && !existsSync7(historyDir)) {
|
|
3020
3670
|
if (verbose)
|
|
3021
3671
|
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
3022
3672
|
return { sessions: 0, requests: 0 };
|
|
@@ -3028,17 +3678,17 @@ async function ingestGemini(db, verbose) {
|
|
|
3028
3678
|
const account = await resolveAccountForAgent("gemini");
|
|
3029
3679
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
3030
3680
|
for (const projectDir of projectDirs) {
|
|
3031
|
-
const chatsDir =
|
|
3032
|
-
if (!
|
|
3681
|
+
const chatsDir = join9(projectDir, "chats");
|
|
3682
|
+
if (!existsSync7(chatsDir))
|
|
3033
3683
|
continue;
|
|
3034
3684
|
let chatFiles = [];
|
|
3035
3685
|
try {
|
|
3036
|
-
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) =>
|
|
3686
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join9(chatsDir, f));
|
|
3037
3687
|
} catch {
|
|
3038
3688
|
continue;
|
|
3039
3689
|
}
|
|
3040
3690
|
for (const filePath of chatFiles) {
|
|
3041
|
-
const stateKey = filePath.replace(
|
|
3691
|
+
const stateKey = filePath.replace(homedir6(), "~");
|
|
3042
3692
|
let fileMtime = "0";
|
|
3043
3693
|
try {
|
|
3044
3694
|
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
@@ -3050,7 +3700,7 @@ async function ingestGemini(db, verbose) {
|
|
|
3050
3700
|
continue;
|
|
3051
3701
|
let chatData;
|
|
3052
3702
|
try {
|
|
3053
|
-
chatData = JSON.parse(
|
|
3703
|
+
chatData = JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
3054
3704
|
} catch {
|
|
3055
3705
|
continue;
|
|
3056
3706
|
}
|
|
@@ -3129,19 +3779,19 @@ var init_gemini = __esm(() => {
|
|
|
3129
3779
|
init_database();
|
|
3130
3780
|
init_pricing();
|
|
3131
3781
|
init_accounts();
|
|
3132
|
-
DEFAULT_GEMINI_TMP_DIR =
|
|
3133
|
-
DEFAULT_GEMINI_HISTORY_DIR =
|
|
3782
|
+
DEFAULT_GEMINI_TMP_DIR = join9(homedir6(), ".gemini", "tmp");
|
|
3783
|
+
DEFAULT_GEMINI_HISTORY_DIR = join9(homedir6(), ".gemini", "history");
|
|
3134
3784
|
});
|
|
3135
3785
|
|
|
3136
3786
|
// src/ingest/opencode.ts
|
|
3137
|
-
import { existsSync as
|
|
3138
|
-
import { homedir as
|
|
3139
|
-
import { join as
|
|
3787
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
3788
|
+
import { homedir as homedir7 } from "os";
|
|
3789
|
+
import { join as join10 } from "path";
|
|
3140
3790
|
function walkJsonFiles(dir, acc = []) {
|
|
3141
|
-
if (!
|
|
3791
|
+
if (!existsSync8(dir))
|
|
3142
3792
|
return acc;
|
|
3143
3793
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
3144
|
-
const p =
|
|
3794
|
+
const p = join10(dir, entry.name);
|
|
3145
3795
|
if (entry.isDirectory())
|
|
3146
3796
|
walkJsonFiles(p, acc);
|
|
3147
3797
|
else if (entry.name.endsWith(".json"))
|
|
@@ -3161,7 +3811,7 @@ function parseSessionIdFromPath(filePath) {
|
|
|
3161
3811
|
return null;
|
|
3162
3812
|
}
|
|
3163
3813
|
async function ingestOpenCode(db, verbose = false) {
|
|
3164
|
-
const messageDir =
|
|
3814
|
+
const messageDir = join10(OPENCODE_STORAGE, "message");
|
|
3165
3815
|
const files = walkJsonFiles(messageDir);
|
|
3166
3816
|
let requests = 0;
|
|
3167
3817
|
const touched = new Set;
|
|
@@ -3176,7 +3826,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
3176
3826
|
continue;
|
|
3177
3827
|
let parsed;
|
|
3178
3828
|
try {
|
|
3179
|
-
parsed = JSON.parse(
|
|
3829
|
+
parsed = JSON.parse(readFileSync7(file, "utf-8"));
|
|
3180
3830
|
} catch {
|
|
3181
3831
|
continue;
|
|
3182
3832
|
}
|
|
@@ -3244,7 +3894,7 @@ var init_opencode = __esm(() => {
|
|
|
3244
3894
|
init_database();
|
|
3245
3895
|
init_pricing();
|
|
3246
3896
|
init_accounts();
|
|
3247
|
-
OPENCODE_STORAGE =
|
|
3897
|
+
OPENCODE_STORAGE = join10(homedir7(), ".local", "share", "opencode", "storage");
|
|
3248
3898
|
});
|
|
3249
3899
|
|
|
3250
3900
|
// src/ingest/cursor.ts
|
|
@@ -3372,14 +4022,14 @@ var init_cursor = __esm(() => {
|
|
|
3372
4022
|
});
|
|
3373
4023
|
|
|
3374
4024
|
// src/ingest/pi.ts
|
|
3375
|
-
import { existsSync as
|
|
3376
|
-
import { homedir as
|
|
3377
|
-
import { join as
|
|
4025
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
4026
|
+
import { homedir as homedir8 } from "os";
|
|
4027
|
+
import { join as join11 } from "path";
|
|
3378
4028
|
function walkSessions(dir, acc = []) {
|
|
3379
|
-
if (!
|
|
4029
|
+
if (!existsSync9(dir))
|
|
3380
4030
|
return acc;
|
|
3381
4031
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
3382
|
-
const p =
|
|
4032
|
+
const p = join11(dir, entry.name);
|
|
3383
4033
|
if (entry.isDirectory())
|
|
3384
4034
|
walkSessions(p, acc);
|
|
3385
4035
|
else if (entry.name.endsWith(".json"))
|
|
@@ -3401,7 +4051,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
3401
4051
|
continue;
|
|
3402
4052
|
let data;
|
|
3403
4053
|
try {
|
|
3404
|
-
data = JSON.parse(
|
|
4054
|
+
data = JSON.parse(readFileSync8(file, "utf-8"));
|
|
3405
4055
|
} catch {
|
|
3406
4056
|
continue;
|
|
3407
4057
|
}
|
|
@@ -3466,13 +4116,13 @@ var PI_SESSION_DIR;
|
|
|
3466
4116
|
var init_pi = __esm(() => {
|
|
3467
4117
|
init_database();
|
|
3468
4118
|
init_accounts();
|
|
3469
|
-
PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ??
|
|
4119
|
+
PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join11(homedir8(), ".pi", "agent", "sessions");
|
|
3470
4120
|
});
|
|
3471
4121
|
|
|
3472
4122
|
// src/ingest/hermes.ts
|
|
3473
|
-
import { existsSync as
|
|
3474
|
-
import { homedir as
|
|
3475
|
-
import { join as
|
|
4123
|
+
import { existsSync as existsSync10, statSync as statSync6 } from "fs";
|
|
4124
|
+
import { homedir as homedir9 } from "os";
|
|
4125
|
+
import { join as join12 } from "path";
|
|
3476
4126
|
function mapCostBasis(billingMode) {
|
|
3477
4127
|
if (billingMode === "subscription")
|
|
3478
4128
|
return "subscription_included";
|
|
@@ -3481,7 +4131,7 @@ function mapCostBasis(billingMode) {
|
|
|
3481
4131
|
return defaultCostBasisForAgent("hermes");
|
|
3482
4132
|
}
|
|
3483
4133
|
async function ingestHermes(db, verbose = false) {
|
|
3484
|
-
if (!
|
|
4134
|
+
if (!existsSync10(HERMES_DB)) {
|
|
3485
4135
|
return { sessions: 0, requests: 0 };
|
|
3486
4136
|
}
|
|
3487
4137
|
const { Database: Sqlite } = await import("bun:sqlite");
|
|
@@ -3561,19 +4211,19 @@ var HERMES_DB;
|
|
|
3561
4211
|
var init_hermes = __esm(() => {
|
|
3562
4212
|
init_database();
|
|
3563
4213
|
init_accounts();
|
|
3564
|
-
HERMES_DB =
|
|
4214
|
+
HERMES_DB = join12(homedir9(), ".hermes", "state.db");
|
|
3565
4215
|
});
|
|
3566
4216
|
|
|
3567
4217
|
// src/ingest/claude-quota.ts
|
|
3568
|
-
import { existsSync as
|
|
4218
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
3569
4219
|
function readClaudeToken() {
|
|
3570
4220
|
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
3571
4221
|
if (fromEnv)
|
|
3572
4222
|
return { token: fromEnv };
|
|
3573
|
-
if (!
|
|
4223
|
+
if (!existsSync11(CREDENTIALS_PATH))
|
|
3574
4224
|
return null;
|
|
3575
4225
|
try {
|
|
3576
|
-
const creds = JSON.parse(
|
|
4226
|
+
const creds = JSON.parse(readFileSync9(CREDENTIALS_PATH, "utf-8"));
|
|
3577
4227
|
const oauth = creds.claudeAiOauth;
|
|
3578
4228
|
if (!oauth?.accessToken)
|
|
3579
4229
|
return null;
|
|
@@ -3714,16 +4364,16 @@ var init_claude_quota = __esm(() => {
|
|
|
3714
4364
|
});
|
|
3715
4365
|
|
|
3716
4366
|
// src/ingest/codex-quota.ts
|
|
3717
|
-
import { existsSync as
|
|
4367
|
+
import { existsSync as existsSync12, readFileSync as readFileSync10 } from "fs";
|
|
3718
4368
|
function readCodexAuth() {
|
|
3719
4369
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
3720
4370
|
if (fromEnv)
|
|
3721
4371
|
return { token: fromEnv, authMode: "chatgpt" };
|
|
3722
4372
|
const authPath = agentPaths().codexAuth;
|
|
3723
|
-
if (!
|
|
4373
|
+
if (!existsSync12(authPath))
|
|
3724
4374
|
return null;
|
|
3725
4375
|
try {
|
|
3726
|
-
const auth = JSON.parse(
|
|
4376
|
+
const auth = JSON.parse(readFileSync10(authPath, "utf-8"));
|
|
3727
4377
|
const token = auth.tokens?.access_token;
|
|
3728
4378
|
if (!token)
|
|
3729
4379
|
return null;
|
|
@@ -3907,15 +4557,15 @@ __export(exports_billing, {
|
|
|
3907
4557
|
syncGeminiBilling: () => syncGeminiBilling,
|
|
3908
4558
|
syncAnthropicBilling: () => syncAnthropicBilling
|
|
3909
4559
|
});
|
|
3910
|
-
import { readFileSync as
|
|
4560
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
3911
4561
|
function getAnthropicAdminKey() {
|
|
3912
|
-
return process.env["
|
|
4562
|
+
return process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
|
|
3913
4563
|
}
|
|
3914
4564
|
function getOpenAIAdminKey() {
|
|
3915
|
-
return process.env["
|
|
4565
|
+
return process.env["OPENAI_ADMIN_API_KEY"] ?? null;
|
|
3916
4566
|
}
|
|
3917
4567
|
function getGeminiBillingExportPath() {
|
|
3918
|
-
return process.env["HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["
|
|
4568
|
+
return process.env["HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["GEMINI_BILLING_EXPORT_PATH"] ?? null;
|
|
3919
4569
|
}
|
|
3920
4570
|
function toISODate(d) {
|
|
3921
4571
|
return d.toISOString().substring(0, 10);
|
|
@@ -3991,7 +4641,7 @@ function parseBillingRows(content) {
|
|
|
3991
4641
|
async function syncAnthropicBilling(db, opts = {}) {
|
|
3992
4642
|
const key = getAnthropicAdminKey();
|
|
3993
4643
|
if (!key)
|
|
3994
|
-
throw new Error("Missing Anthropic admin key (
|
|
4644
|
+
throw new Error("Missing Anthropic admin key (ANTHROPIC_ADMIN_API_KEY)");
|
|
3995
4645
|
const now = new Date;
|
|
3996
4646
|
const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
|
|
3997
4647
|
const days = opts.days ?? 31;
|
|
@@ -4040,7 +4690,7 @@ async function syncAnthropicBilling(db, opts = {}) {
|
|
|
4040
4690
|
async function syncOpenAIBilling(db, opts = {}) {
|
|
4041
4691
|
const key = getOpenAIAdminKey();
|
|
4042
4692
|
if (!key)
|
|
4043
|
-
throw new Error("Missing OpenAI admin key (
|
|
4693
|
+
throw new Error("Missing OpenAI admin key (OPENAI_ADMIN_API_KEY)");
|
|
4044
4694
|
const now = new Date;
|
|
4045
4695
|
const end = opts.toDate ? new Date(opts.toDate) : now;
|
|
4046
4696
|
const days = opts.days ?? 31;
|
|
@@ -4092,7 +4742,7 @@ async function syncGeminiBilling(db, opts = {}) {
|
|
|
4092
4742
|
return {
|
|
4093
4743
|
days: 0,
|
|
4094
4744
|
totalUsd: 0,
|
|
4095
|
-
skipped: "Missing Gemini billing export path (HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH
|
|
4745
|
+
skipped: "Missing Gemini billing export path (HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH or GEMINI_BILLING_EXPORT_PATH)"
|
|
4096
4746
|
};
|
|
4097
4747
|
}
|
|
4098
4748
|
const now = new Date;
|
|
@@ -4101,7 +4751,7 @@ async function syncGeminiBilling(db, opts = {}) {
|
|
|
4101
4751
|
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
4102
4752
|
const fromDateStr = toISODate(start);
|
|
4103
4753
|
const toDateStr = toISODate(end);
|
|
4104
|
-
const rows = parseBillingRows(
|
|
4754
|
+
const rows = parseBillingRows(readFileSync11(exportPath, "utf-8"));
|
|
4105
4755
|
clearBillingRange(db, "gemini", fromDateStr, toDateStr);
|
|
4106
4756
|
const updatedAt = new Date().toISOString();
|
|
4107
4757
|
let totalUsd = 0;
|
|
@@ -4133,22 +4783,26 @@ __export(exports_open_projects, {
|
|
|
4133
4783
|
syncOpenProjectsRegistry: () => syncOpenProjectsRegistry
|
|
4134
4784
|
});
|
|
4135
4785
|
async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
4136
|
-
let
|
|
4137
|
-
if (!
|
|
4786
|
+
let listOpenProjects = listActiveProjects;
|
|
4787
|
+
if (!listOpenProjects) {
|
|
4138
4788
|
const projectsApi = await import("@hasna/projects");
|
|
4139
|
-
|
|
4789
|
+
listOpenProjects = projectsApi.listProjects ?? projectsApi.listWorkspaces;
|
|
4790
|
+
}
|
|
4791
|
+
if (!listOpenProjects) {
|
|
4792
|
+
throw new Error("@hasna/projects does not expose listWorkspaces or listProjects");
|
|
4140
4793
|
}
|
|
4141
|
-
const projects =
|
|
4794
|
+
const projects = listOpenProjects({ status: "active", limit: 5000 });
|
|
4142
4795
|
let imported = 0;
|
|
4143
4796
|
let skipped = 0;
|
|
4144
4797
|
for (const project of projects) {
|
|
4145
|
-
|
|
4798
|
+
const path = project.path ?? project.primary_path ?? "";
|
|
4799
|
+
if (!path) {
|
|
4146
4800
|
skipped++;
|
|
4147
4801
|
continue;
|
|
4148
4802
|
}
|
|
4149
4803
|
upsertProject(db, {
|
|
4150
4804
|
id: project.id,
|
|
4151
|
-
path
|
|
4805
|
+
path,
|
|
4152
4806
|
name: project.name,
|
|
4153
4807
|
description: project.description,
|
|
4154
4808
|
tags: project.tags ?? [],
|
|
@@ -4170,16 +4824,16 @@ __export(exports_config, {
|
|
|
4170
4824
|
loadConfig: () => loadConfig2,
|
|
4171
4825
|
getConfigValue: () => getConfigValue
|
|
4172
4826
|
});
|
|
4173
|
-
import { existsSync as
|
|
4174
|
-
import { dirname as
|
|
4827
|
+
import { existsSync as existsSync14, readFileSync as readFileSync12, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "fs";
|
|
4828
|
+
import { dirname as dirname3, join as join13 } from "path";
|
|
4175
4829
|
function getConfigPath() {
|
|
4176
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
4830
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join13(getDataDir(), "config.json");
|
|
4177
4831
|
}
|
|
4178
4832
|
function loadConfig2() {
|
|
4179
4833
|
try {
|
|
4180
4834
|
const configPath = getConfigPath();
|
|
4181
|
-
if (
|
|
4182
|
-
const raw =
|
|
4835
|
+
if (existsSync14(configPath)) {
|
|
4836
|
+
const raw = readFileSync12(configPath, "utf-8");
|
|
4183
4837
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
4184
4838
|
}
|
|
4185
4839
|
} catch {}
|
|
@@ -4187,10 +4841,10 @@ function loadConfig2() {
|
|
|
4187
4841
|
}
|
|
4188
4842
|
function saveConfig2(config) {
|
|
4189
4843
|
const configPath = getConfigPath();
|
|
4190
|
-
const dir =
|
|
4191
|
-
if (!
|
|
4192
|
-
|
|
4193
|
-
|
|
4844
|
+
const dir = dirname3(configPath);
|
|
4845
|
+
if (!existsSync14(dir))
|
|
4846
|
+
mkdirSync4(dir, { recursive: true });
|
|
4847
|
+
writeFileSync3(configPath, JSON.stringify(config, null, 2) + `
|
|
4194
4848
|
`);
|
|
4195
4849
|
}
|
|
4196
4850
|
function getConfigValue(key) {
|
|
@@ -4332,7 +4986,7 @@ var init_webhooks = __esm(() => {
|
|
|
4332
4986
|
});
|
|
4333
4987
|
|
|
4334
4988
|
// src/lib/watch-paths.ts
|
|
4335
|
-
import { existsSync as
|
|
4989
|
+
import { existsSync as existsSync15 } from "fs";
|
|
4336
4990
|
function getWatchPaths() {
|
|
4337
4991
|
const p = agentPaths();
|
|
4338
4992
|
const candidates = [
|
|
@@ -4345,7 +4999,7 @@ function getWatchPaths() {
|
|
|
4345
4999
|
p.piSessions,
|
|
4346
5000
|
p.hermesDir
|
|
4347
5001
|
];
|
|
4348
|
-
return candidates.filter((path) =>
|
|
5002
|
+
return candidates.filter((path) => existsSync15(path));
|
|
4349
5003
|
}
|
|
4350
5004
|
var init_watch_paths = __esm(() => {
|
|
4351
5005
|
init_paths();
|
|
@@ -4515,7 +5169,7 @@ __export(exports_serve, {
|
|
|
4515
5169
|
createHandler: () => createHandler
|
|
4516
5170
|
});
|
|
4517
5171
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
4518
|
-
import { existsSync as
|
|
5172
|
+
import { existsSync as existsSync16 } from "fs";
|
|
4519
5173
|
import { resolve, sep } from "path";
|
|
4520
5174
|
function json(data, status = 200) {
|
|
4521
5175
|
return new Response(JSON.stringify(data), {
|
|
@@ -4580,13 +5234,13 @@ function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
|
4580
5234
|
if (url.pathname.startsWith("/api") || url.pathname === "/health") {
|
|
4581
5235
|
return apiHandler(req);
|
|
4582
5236
|
}
|
|
4583
|
-
if (
|
|
5237
|
+
if (existsSync16(dashboardDir)) {
|
|
4584
5238
|
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
4585
|
-
if (filePath &&
|
|
5239
|
+
if (filePath && existsSync16(filePath)) {
|
|
4586
5240
|
return new Response(Bun.file(filePath));
|
|
4587
5241
|
}
|
|
4588
5242
|
const indexPath = dashboardPath(dashboardDir, "/");
|
|
4589
|
-
if (indexPath &&
|
|
5243
|
+
if (indexPath && existsSync16(indexPath)) {
|
|
4590
5244
|
return new Response(Bun.file(indexPath));
|
|
4591
5245
|
}
|
|
4592
5246
|
}
|
|
@@ -4634,7 +5288,16 @@ function createHandler(db) {
|
|
|
4634
5288
|
}
|
|
4635
5289
|
if (path === "/api/hourly" && method === "GET") {
|
|
4636
5290
|
const machine = url.searchParams.get("machine") ?? undefined;
|
|
4637
|
-
|
|
5291
|
+
const rawHours = url.searchParams.get("hours");
|
|
5292
|
+
let hours;
|
|
5293
|
+
if (rawHours != null) {
|
|
5294
|
+
const parsedHours = Number(rawHours);
|
|
5295
|
+
if (!Number.isInteger(parsedHours) || parsedHours < 1 || parsedHours > 48) {
|
|
5296
|
+
return err("hours must be between 1 and 48");
|
|
5297
|
+
}
|
|
5298
|
+
hours = parsedHours;
|
|
5299
|
+
}
|
|
5300
|
+
return ok(queryHourlyBreakdown(db, machine, hours));
|
|
4638
5301
|
}
|
|
4639
5302
|
if (path === "/api/sessions" && method === "GET") {
|
|
4640
5303
|
const agent = url.searchParams.get("agent");
|
|
@@ -4992,14 +5655,14 @@ __export(exports_menubar, {
|
|
|
4992
5655
|
});
|
|
4993
5656
|
import chalk6 from "chalk";
|
|
4994
5657
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
4995
|
-
import { cpSync, existsSync as
|
|
5658
|
+
import { cpSync, existsSync as existsSync17, mkdirSync as mkdirSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
|
|
4996
5659
|
import { tmpdir, arch } from "os";
|
|
4997
|
-
import { join as
|
|
5660
|
+
import { join as join14 } from "path";
|
|
4998
5661
|
function getArch() {
|
|
4999
5662
|
return arch() === "arm64" ? "arm64" : "x86_64";
|
|
5000
5663
|
}
|
|
5001
5664
|
function isInstalled() {
|
|
5002
|
-
return
|
|
5665
|
+
return existsSync17(APP_PATH);
|
|
5003
5666
|
}
|
|
5004
5667
|
function isRunning() {
|
|
5005
5668
|
try {
|
|
@@ -5036,15 +5699,15 @@ async function menubarInstall(opts) {
|
|
|
5036
5699
|
console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
|
|
5037
5700
|
process.exit(1);
|
|
5038
5701
|
}
|
|
5039
|
-
const zipPath =
|
|
5040
|
-
const extractDir =
|
|
5702
|
+
const zipPath = join14(tmpdir(), `economy-bar-${cpuArch}.zip`);
|
|
5703
|
+
const extractDir = join14(tmpdir(), "economy-bar-extracted");
|
|
5041
5704
|
console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
|
|
5042
5705
|
try {
|
|
5043
5706
|
const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
|
|
5044
5707
|
if (!res.ok)
|
|
5045
5708
|
throw new Error(`Download failed: ${res.status}`);
|
|
5046
5709
|
const buffer = await res.arrayBuffer();
|
|
5047
|
-
|
|
5710
|
+
writeFileSync4(zipPath, Buffer.from(buffer));
|
|
5048
5711
|
console.log(chalk6.green(`\u2713 Downloaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`));
|
|
5049
5712
|
} catch (e) {
|
|
5050
5713
|
console.error(chalk6.red(`\u2717 Download failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
@@ -5053,11 +5716,11 @@ async function menubarInstall(opts) {
|
|
|
5053
5716
|
console.log(chalk6.cyan("\u2192 Installing to /Applications..."));
|
|
5054
5717
|
try {
|
|
5055
5718
|
rmSync(extractDir, { recursive: true, force: true });
|
|
5056
|
-
|
|
5719
|
+
mkdirSync5(extractDir, { recursive: true });
|
|
5057
5720
|
execFileSync2("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
|
|
5058
5721
|
if (isInstalled())
|
|
5059
5722
|
rmSync(APP_PATH, { recursive: true, force: true });
|
|
5060
|
-
cpSync(
|
|
5723
|
+
cpSync(join14(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
|
|
5061
5724
|
try {
|
|
5062
5725
|
execFileSync2("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
|
|
5063
5726
|
} catch {}
|
|
@@ -5855,7 +6518,7 @@ var ROADMAP_PHASES = [
|
|
|
5855
6518
|
{
|
|
5856
6519
|
id: "phase-9",
|
|
5857
6520
|
title: "Multi-machine auto sync (4-machine fleet)",
|
|
5858
|
-
summary: "Investigation: machine_id + listMachines + manual cloud push/pull exist;
|
|
6521
|
+
summary: "Investigation: machine_id + listMachines + manual cloud push/pull exist; the previous shared cloud package had incremental sync, conflict resolution, and launchd/systemd schedulers \u2014 but economy did not own them, requests/sessions lacked updated_at, ingest was local-only, RDS was hardcoded, and default commands never pulled before query.",
|
|
5859
6522
|
tasks: [
|
|
5860
6523
|
{
|
|
5861
6524
|
id: "9.1",
|
|
@@ -5870,7 +6533,7 @@ var ROADMAP_PHASES = [
|
|
|
5870
6533
|
},
|
|
5871
6534
|
{
|
|
5872
6535
|
id: "9.3",
|
|
5873
|
-
title: "Switch cloud sync to
|
|
6536
|
+
title: "Switch cloud sync to repo-native incremental push/pull with _sync_meta (replace full table scans)",
|
|
5874
6537
|
status: "done",
|
|
5875
6538
|
deps: ["9.2"]
|
|
5876
6539
|
},
|
|
@@ -5894,7 +6557,7 @@ var ROADMAP_PHASES = [
|
|
|
5894
6557
|
},
|
|
5895
6558
|
{
|
|
5896
6559
|
id: "9.7",
|
|
5897
|
-
title: "CLI economy cloud schedule install|status|remove \u2014
|
|
6560
|
+
title: "CLI economy cloud schedule install|status|remove \u2014 repo-native launchd/systemd scheduler every 5\u201315m",
|
|
5898
6561
|
status: "done",
|
|
5899
6562
|
deps: ["9.5"]
|
|
5900
6563
|
},
|
|
@@ -6305,8 +6968,8 @@ init_agents();
|
|
|
6305
6968
|
init_cloud_sync();
|
|
6306
6969
|
import chalk4 from "chalk";
|
|
6307
6970
|
import { randomUUID } from "crypto";
|
|
6308
|
-
import { existsSync as
|
|
6309
|
-
import { join as
|
|
6971
|
+
import { existsSync as existsSync4 } from "fs";
|
|
6972
|
+
import { join as join6 } from "path";
|
|
6310
6973
|
|
|
6311
6974
|
// src/cli/commands/completion.ts
|
|
6312
6975
|
var TOP_LEVEL = [
|
|
@@ -6556,13 +7219,13 @@ function registerExtendedCommands(program) {
|
|
|
6556
7219
|
const paths = [
|
|
6557
7220
|
["claude", agentPaths().claudeProjects],
|
|
6558
7221
|
["codex", agentPaths().codexDb],
|
|
6559
|
-
["gemini",
|
|
6560
|
-
["opencode",
|
|
7222
|
+
["gemini", join6(agentPaths().geminiTmp, "..")],
|
|
7223
|
+
["opencode", join6(agentPaths().opencodeMessages, "..", "..")],
|
|
6561
7224
|
["pi", agentPaths().piSessions],
|
|
6562
7225
|
["hermes", agentPaths().hermesDb]
|
|
6563
7226
|
];
|
|
6564
7227
|
for (const [agent, path] of paths) {
|
|
6565
|
-
checks.push({ ok:
|
|
7228
|
+
checks.push({ ok: existsSync4(path), msg: `${agent}: ${existsSync4(path) ? path : "not found"}` });
|
|
6566
7229
|
}
|
|
6567
7230
|
checks.push({ ok: Boolean(process.env["CURSOR_SESSION_TOKEN"]), msg: `cursor token: ${process.env["CURSOR_SESSION_TOKEN"] ? "set" : "missing CURSOR_SESSION_TOKEN"}` });
|
|
6568
7231
|
checks.push({ ok: Boolean(getCloudDatabaseUrl()), msg: `cloud: ${getCloudDatabaseUrl() ? "ECONOMY_CLOUD_DATABASE_URL set" : "not configured"}` });
|
|
@@ -6693,8 +7356,8 @@ init_cloud_sync();
|
|
|
6693
7356
|
// src/lib/peer-sync.ts
|
|
6694
7357
|
init_database();
|
|
6695
7358
|
init_package_metadata();
|
|
6696
|
-
import { Database as
|
|
6697
|
-
import { existsSync as
|
|
7359
|
+
import { Database as BunDatabase3 } from "bun:sqlite";
|
|
7360
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6698
7361
|
var GENERIC_PEER_TABLES = [
|
|
6699
7362
|
"usage_snapshots",
|
|
6700
7363
|
"subscriptions",
|
|
@@ -6705,7 +7368,7 @@ var GENERIC_PEER_TABLES = [
|
|
|
6705
7368
|
"model_pricing",
|
|
6706
7369
|
"machines"
|
|
6707
7370
|
];
|
|
6708
|
-
function
|
|
7371
|
+
function quoteIdent2(identifier) {
|
|
6709
7372
|
return `"${identifier.replace(/"/g, '""')}"`;
|
|
6710
7373
|
}
|
|
6711
7374
|
function tableExists(db, table) {
|
|
@@ -6715,7 +7378,7 @@ function tableExists(db, table) {
|
|
|
6715
7378
|
function tableColumns(db, table) {
|
|
6716
7379
|
if (!tableExists(db, table))
|
|
6717
7380
|
return [];
|
|
6718
|
-
return db.prepare(`PRAGMA table_info(${
|
|
7381
|
+
return db.prepare(`PRAGMA table_info(${quoteIdent2(table)})`).all();
|
|
6719
7382
|
}
|
|
6720
7383
|
function commonColumns(source, target, table) {
|
|
6721
7384
|
const sourceCols = new Set(tableColumns(source, table).map((c) => c.name));
|
|
@@ -6727,19 +7390,19 @@ function primaryKeyColumns(db, table) {
|
|
|
6727
7390
|
function selectRows(source, table, columns) {
|
|
6728
7391
|
if (columns.length === 0)
|
|
6729
7392
|
return [];
|
|
6730
|
-
const select = columns.map(
|
|
6731
|
-
return source.prepare(`SELECT ${select} FROM ${
|
|
7393
|
+
const select = columns.map(quoteIdent2).join(", ");
|
|
7394
|
+
return source.prepare(`SELECT ${select} FROM ${quoteIdent2(table)}`).all();
|
|
6732
7395
|
}
|
|
6733
7396
|
function rowByKey(target, table, keyColumns, row) {
|
|
6734
7397
|
if (keyColumns.length === 0)
|
|
6735
7398
|
return null;
|
|
6736
7399
|
if (keyColumns.some((c) => row[c] == null))
|
|
6737
7400
|
return null;
|
|
6738
|
-
const where = keyColumns.map((c) => `${
|
|
6739
|
-
return target.prepare(`SELECT * FROM ${
|
|
7401
|
+
const where = keyColumns.map((c) => `${quoteIdent2(c)} = ?`).join(" AND ");
|
|
7402
|
+
return target.prepare(`SELECT * FROM ${quoteIdent2(table)} WHERE ${where}`).get(...keyColumns.map((c) => row[c]));
|
|
6740
7403
|
}
|
|
6741
7404
|
function hasId(target, table, id) {
|
|
6742
|
-
return target.prepare(`SELECT id, machine_id FROM ${
|
|
7405
|
+
return target.prepare(`SELECT id, machine_id FROM ${quoteIdent2(table)} WHERE id = ?`).get(id);
|
|
6743
7406
|
}
|
|
6744
7407
|
function shouldReplace(source, existing) {
|
|
6745
7408
|
if (!existing)
|
|
@@ -6766,10 +7429,10 @@ function normalizeRow(row, columns, sourceMachine, now) {
|
|
|
6766
7429
|
return next;
|
|
6767
7430
|
}
|
|
6768
7431
|
function insertOrReplace(target, table, columns, row) {
|
|
6769
|
-
const colSql = columns.map(
|
|
7432
|
+
const colSql = columns.map(quoteIdent2).join(", ");
|
|
6770
7433
|
const placeholders = columns.map(() => "?").join(", ");
|
|
6771
7434
|
target.prepare(`
|
|
6772
|
-
INSERT OR REPLACE INTO ${
|
|
7435
|
+
INSERT OR REPLACE INTO ${quoteIdent2(table)} (${colSql})
|
|
6773
7436
|
VALUES (${placeholders})
|
|
6774
7437
|
`).run(...columns.map((c) => row[c] ?? null));
|
|
6775
7438
|
}
|
|
@@ -6883,7 +7546,7 @@ function detectSourceMachine(source, fallback) {
|
|
|
6883
7546
|
continue;
|
|
6884
7547
|
const rows = source.prepare(`
|
|
6885
7548
|
SELECT machine_id, COUNT(*) as cnt
|
|
6886
|
-
FROM ${
|
|
7549
|
+
FROM ${quoteIdent2(table)}
|
|
6887
7550
|
WHERE machine_id != '' AND machine_id IS NOT NULL
|
|
6888
7551
|
GROUP BY machine_id
|
|
6889
7552
|
`).all();
|
|
@@ -6920,13 +7583,13 @@ function ensureMachineRegistry(target, machine, now) {
|
|
|
6920
7583
|
}
|
|
6921
7584
|
function openSourceDatabase(path) {
|
|
6922
7585
|
try {
|
|
6923
|
-
return new
|
|
7586
|
+
return new BunDatabase3(path, { readonly: true });
|
|
6924
7587
|
} catch {
|
|
6925
|
-
return new
|
|
7588
|
+
return new BunDatabase3(path);
|
|
6926
7589
|
}
|
|
6927
7590
|
}
|
|
6928
7591
|
function mergePeerDatabase(target, sourcePath, opts = {}) {
|
|
6929
|
-
if (!
|
|
7592
|
+
if (!existsSync13(sourcePath))
|
|
6930
7593
|
throw new Error(`source database does not exist: ${sourcePath}`);
|
|
6931
7594
|
const source = openSourceDatabase(sourcePath);
|
|
6932
7595
|
const now = opts.now ?? new Date().toISOString();
|
|
@@ -7789,8 +8452,8 @@ program.command("dashboard").description("Open the web dashboard (auto-starts se
|
|
|
7789
8452
|
if (!serverRunning) {
|
|
7790
8453
|
console.log(chalk7.cyan(`\u2192 Starting economy server on port ${port}...`));
|
|
7791
8454
|
const { spawn } = await import("child_process");
|
|
7792
|
-
const { resolve: resolve2, dirname:
|
|
7793
|
-
const serveScript = resolve2(
|
|
8455
|
+
const { resolve: resolve2, dirname: dirname4 } = await import("path");
|
|
8456
|
+
const serveScript = resolve2(dirname4(process.argv[1]), "..", "server", "index.js");
|
|
7794
8457
|
const child = spawn(process.execPath, [serveScript], {
|
|
7795
8458
|
detached: true,
|
|
7796
8459
|
stdio: "ignore",
|
|
@@ -7944,8 +8607,8 @@ program.command("export").description("Export data as CSV").option("--type <type
|
|
|
7944
8607
|
}
|
|
7945
8608
|
}
|
|
7946
8609
|
if (opts.output) {
|
|
7947
|
-
const { writeFileSync:
|
|
7948
|
-
|
|
8610
|
+
const { writeFileSync: writeFileSync5 } = await import("fs");
|
|
8611
|
+
writeFileSync5(opts.output, csv);
|
|
7949
8612
|
console.log(chalk7.green(`\u2713 Exported to ${opts.output}`));
|
|
7950
8613
|
} else {
|
|
7951
8614
|
process.stdout.write(csv);
|