@hasna/economy 0.2.31 → 0.2.32
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/cli/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, {
|
|
@@ -615,9 +561,9 @@ __export(exports_database, {
|
|
|
615
561
|
deleteGoal: () => deleteGoal,
|
|
616
562
|
deleteBudget: () => deleteBudget,
|
|
617
563
|
dedupeRequests: () => dedupeRequests,
|
|
618
|
-
clearBillingRange: () => clearBillingRange
|
|
619
|
-
SqliteAdapter: () => SqliteAdapter
|
|
564
|
+
clearBillingRange: () => clearBillingRange
|
|
620
565
|
});
|
|
566
|
+
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
621
567
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
622
568
|
import { hostname } from "os";
|
|
623
569
|
import { homedir } from "os";
|
|
@@ -660,7 +606,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
660
606
|
if (dir && !existsSync(dir))
|
|
661
607
|
mkdirSync(dir, { recursive: true });
|
|
662
608
|
}
|
|
663
|
-
const db = new
|
|
609
|
+
const db = new Database(path);
|
|
664
610
|
db.exec("PRAGMA journal_mode = WAL");
|
|
665
611
|
db.exec("PRAGMA busy_timeout = 5000");
|
|
666
612
|
db.exec("PRAGMA foreign_keys = ON");
|
|
@@ -1696,10 +1642,7 @@ function dedupeRequests(db) {
|
|
|
1696
1642
|
}
|
|
1697
1643
|
return removed;
|
|
1698
1644
|
}
|
|
1699
|
-
var init_database =
|
|
1700
|
-
init_storage_adapter();
|
|
1701
|
-
init_storage_adapter();
|
|
1702
|
-
});
|
|
1645
|
+
var init_database = () => {};
|
|
1703
1646
|
|
|
1704
1647
|
// src/lib/savings.ts
|
|
1705
1648
|
function periodWhere2(period, column) {
|
|
@@ -1965,370 +1908,6 @@ var init_package_metadata = __esm(() => {
|
|
|
1965
1908
|
packageMetadata = getPackageMetadata();
|
|
1966
1909
|
});
|
|
1967
1910
|
|
|
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
|
-
|
|
2332
1911
|
// src/db/pg-migrations.ts
|
|
2333
1912
|
var exports_pg_migrations = {};
|
|
2334
1913
|
__export(exports_pg_migrations, {
|
|
@@ -2518,9 +2097,6 @@ var init_pg_migrations = __esm(() => {
|
|
|
2518
2097
|
});
|
|
2519
2098
|
|
|
2520
2099
|
// 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";
|
|
2524
2100
|
function getCloudDatabaseUrl() {
|
|
2525
2101
|
return process.env["ECONOMY_CLOUD_DATABASE_URL"] ?? process.env["HASNA_ECONOMY_CLOUD_DATABASE_URL"] ?? null;
|
|
2526
2102
|
}
|
|
@@ -2539,6 +2115,7 @@ async function getCloudPg() {
|
|
|
2539
2115
|
if (!url) {
|
|
2540
2116
|
throw new Error("Missing ECONOMY_CLOUD_DATABASE_URL (or HASNA_ECONOMY_CLOUD_DATABASE_URL)");
|
|
2541
2117
|
}
|
|
2118
|
+
const { PgAdapterAsync } = await import("@hasna/cloud");
|
|
2542
2119
|
return new PgAdapterAsync(url);
|
|
2543
2120
|
}
|
|
2544
2121
|
async function runCloudMigrations(cloud) {
|
|
@@ -2548,35 +2125,31 @@ async function runCloudMigrations(cloud) {
|
|
|
2548
2125
|
}
|
|
2549
2126
|
}
|
|
2550
2127
|
async function cloudPush(opts) {
|
|
2128
|
+
const { syncPush, SqliteAdapter } = await import("@hasna/cloud");
|
|
2551
2129
|
const cloud = await getCloudPg();
|
|
2552
|
-
const local =
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
local.close();
|
|
2562
|
-
await cloud.close();
|
|
2563
|
-
}
|
|
2130
|
+
const local = new SqliteAdapter(getDbPath());
|
|
2131
|
+
await runCloudMigrations(cloud);
|
|
2132
|
+
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
2133
|
+
const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
|
|
2134
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2135
|
+
touchMachineRegistry(local, "push");
|
|
2136
|
+
local.close();
|
|
2137
|
+
await cloud.close();
|
|
2138
|
+
return { rows, machine: getMachineId() };
|
|
2564
2139
|
}
|
|
2565
2140
|
async function cloudPull(opts) {
|
|
2141
|
+
const { syncPull, SqliteAdapter } = await import("@hasna/cloud");
|
|
2566
2142
|
const cloud = await getCloudPg();
|
|
2567
|
-
const local =
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
local.close();
|
|
2578
|
-
await cloud.close();
|
|
2579
|
-
}
|
|
2143
|
+
const local = new SqliteAdapter(getDbPath());
|
|
2144
|
+
await runCloudMigrations(cloud);
|
|
2145
|
+
const tables = opts?.tables ?? [...CLOUD_TABLES];
|
|
2146
|
+
const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
|
|
2147
|
+
const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2148
|
+
touchMachineRegistry(local, "pull");
|
|
2149
|
+
local.close();
|
|
2150
|
+
await cloud.close();
|
|
2151
|
+
setLastCloudPull();
|
|
2152
|
+
return { rows, machine: getMachineId() };
|
|
2580
2153
|
}
|
|
2581
2154
|
async function cloudSyncFull() {
|
|
2582
2155
|
const push = await cloudPush();
|
|
@@ -2584,21 +2157,13 @@ async function cloudSyncFull() {
|
|
|
2584
2157
|
return { push: push.rows, pull: pull.rows, machine: getMachineId() };
|
|
2585
2158
|
}
|
|
2586
2159
|
function setLastCloudPull(at = new Date().toISOString()) {
|
|
2587
|
-
const db = openDatabase(
|
|
2588
|
-
|
|
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
|
-
}
|
|
2160
|
+
const db = openDatabase();
|
|
2161
|
+
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
|
|
2593
2162
|
}
|
|
2594
2163
|
function getLastCloudPull() {
|
|
2595
|
-
const db = openDatabase(
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
return row?.value ?? null;
|
|
2599
|
-
} finally {
|
|
2600
|
-
db.close();
|
|
2601
|
-
}
|
|
2164
|
+
const db = openDatabase();
|
|
2165
|
+
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
|
|
2166
|
+
return row?.value ?? null;
|
|
2602
2167
|
}
|
|
2603
2168
|
function shouldPullFromCloud() {
|
|
2604
2169
|
if (!getCloudDatabaseUrl())
|
|
@@ -2644,231 +2209,22 @@ function touchMachineRegistry(db, direction) {
|
|
|
2644
2209
|
updated_at = excluded.updated_at
|
|
2645
2210
|
`).run(machine, machine, now, direction === "push" ? now : null, direction === "pull" ? now : null, packageMetadata.version, now, direction, direction);
|
|
2646
2211
|
}
|
|
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
|
-
}
|
|
2658
2212
|
async function registerCloudSchedule(intervalMinutes) {
|
|
2659
|
-
|
|
2660
|
-
|
|
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));
|
|
2213
|
+
const { registerSyncSchedule } = await import("@hasna/cloud");
|
|
2214
|
+
await registerSyncSchedule(intervalMinutes);
|
|
2671
2215
|
}
|
|
2672
2216
|
async function removeCloudSchedule() {
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
if (platform() === "linux")
|
|
2676
|
-
await removeSystemd();
|
|
2677
|
-
try {
|
|
2678
|
-
unlinkSync(SCHEDULE_CONFIG_PATH);
|
|
2679
|
-
} catch {}
|
|
2217
|
+
const { removeSyncSchedule } = await import("@hasna/cloud");
|
|
2218
|
+
await removeSyncSchedule();
|
|
2680
2219
|
}
|
|
2681
2220
|
async function getCloudScheduleStatus() {
|
|
2682
|
-
const
|
|
2683
|
-
|
|
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
|
-
};
|
|
2221
|
+
const { getSyncScheduleStatus } = await import("@hasna/cloud");
|
|
2222
|
+
return getSyncScheduleStatus();
|
|
2691
2223
|
}
|
|
2692
|
-
|
|
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 {}
|
|
2806
|
-
}
|
|
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;
|
|
2224
|
+
var CLOUD_TABLES;
|
|
2867
2225
|
var init_cloud_sync = __esm(() => {
|
|
2868
2226
|
init_database();
|
|
2869
2227
|
init_package_metadata();
|
|
2870
|
-
init_remote_storage();
|
|
2871
|
-
init_storage_sync();
|
|
2872
2228
|
CLOUD_TABLES = [
|
|
2873
2229
|
"requests",
|
|
2874
2230
|
"sessions",
|
|
@@ -2883,8 +2239,6 @@ var init_cloud_sync = __esm(() => {
|
|
|
2883
2239
|
"machines",
|
|
2884
2240
|
"ingest_state"
|
|
2885
2241
|
];
|
|
2886
|
-
SCHEDULE_CONFIG_DIR = join4(homedir2(), ".hasna", "economy");
|
|
2887
|
-
SCHEDULE_CONFIG_PATH = join4(SCHEDULE_CONFIG_DIR, "cloud-sync-schedule.json");
|
|
2888
2242
|
});
|
|
2889
2243
|
|
|
2890
2244
|
// src/lib/serve-auth.ts
|
|
@@ -3054,27 +2408,27 @@ var init_agents = __esm(() => {
|
|
|
3054
2408
|
});
|
|
3055
2409
|
|
|
3056
2410
|
// src/lib/paths.ts
|
|
3057
|
-
import { homedir as
|
|
3058
|
-
import { join as
|
|
2411
|
+
import { homedir as homedir2 } from "os";
|
|
2412
|
+
import { join as join4 } from "path";
|
|
3059
2413
|
function getHomeDir() {
|
|
3060
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ??
|
|
2414
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir2();
|
|
3061
2415
|
}
|
|
3062
2416
|
function agentPaths() {
|
|
3063
2417
|
const home = getHomeDir();
|
|
3064
2418
|
return {
|
|
3065
|
-
claudeProjects:
|
|
3066
|
-
claudeCredentials:
|
|
3067
|
-
takumiProjects:
|
|
3068
|
-
codexDir:
|
|
3069
|
-
codexDb:
|
|
3070
|
-
codexAuth:
|
|
3071
|
-
codexConfig:
|
|
3072
|
-
geminiTmp:
|
|
3073
|
-
geminiHistory:
|
|
3074
|
-
opencodeMessages:
|
|
3075
|
-
piSessions:
|
|
3076
|
-
hermesDir:
|
|
3077
|
-
hermesDb:
|
|
2419
|
+
claudeProjects: join4(home, ".claude", "projects"),
|
|
2420
|
+
claudeCredentials: join4(home, ".claude", ".credentials.json"),
|
|
2421
|
+
takumiProjects: join4(home, ".takumi", "projects"),
|
|
2422
|
+
codexDir: join4(home, ".codex"),
|
|
2423
|
+
codexDb: join4(home, ".codex", "state_5.sqlite"),
|
|
2424
|
+
codexAuth: join4(home, ".codex", "auth.json"),
|
|
2425
|
+
codexConfig: join4(home, ".codex", "config.toml"),
|
|
2426
|
+
geminiTmp: join4(home, ".gemini", "tmp"),
|
|
2427
|
+
geminiHistory: join4(home, ".gemini", "history"),
|
|
2428
|
+
opencodeMessages: join4(home, ".local", "share", "opencode", "storage", "message"),
|
|
2429
|
+
piSessions: join4(home, ".pi", "agent", "sessions"),
|
|
2430
|
+
hermesDir: join4(home, ".hermes"),
|
|
2431
|
+
hermesDb: join4(home, ".hermes", "state.db")
|
|
3078
2432
|
};
|
|
3079
2433
|
}
|
|
3080
2434
|
var init_paths = () => {};
|
|
@@ -3215,9 +2569,9 @@ var init_accounts = __esm(() => {
|
|
|
3215
2569
|
});
|
|
3216
2570
|
|
|
3217
2571
|
// src/ingest/claude.ts
|
|
3218
|
-
import { readdirSync as readdirSync2, readFileSync as
|
|
3219
|
-
import { homedir as
|
|
3220
|
-
import { join as
|
|
2572
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
2573
|
+
import { homedir as homedir3 } from "os";
|
|
2574
|
+
import { join as join6, basename } from "path";
|
|
3221
2575
|
function autoDetectProject(cwd, projects) {
|
|
3222
2576
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
3223
2577
|
}
|
|
@@ -3230,9 +2584,9 @@ function collectJsonlFiles(projectDir) {
|
|
|
3230
2584
|
try {
|
|
3231
2585
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
3232
2586
|
if (entry.isDirectory())
|
|
3233
|
-
walk(
|
|
2587
|
+
walk(join6(dir, entry.name));
|
|
3234
2588
|
else if (entry.name.endsWith(".jsonl"))
|
|
3235
|
-
files.push(
|
|
2589
|
+
files.push(join6(dir, entry.name));
|
|
3236
2590
|
}
|
|
3237
2591
|
} catch {}
|
|
3238
2592
|
}
|
|
@@ -3246,7 +2600,7 @@ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_D
|
|
|
3246
2600
|
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
3247
2601
|
}
|
|
3248
2602
|
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
3249
|
-
if (!
|
|
2603
|
+
if (!existsSync4(projectsDir)) {
|
|
3250
2604
|
if (verbose)
|
|
3251
2605
|
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
3252
2606
|
return { files: 0, requests: 0, sessions: 0 };
|
|
@@ -3259,7 +2613,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
3259
2613
|
const account = await resolveAccountForAgent(agentName);
|
|
3260
2614
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
3261
2615
|
for (const projectDirEntry of projectDirs) {
|
|
3262
|
-
const projectDirPath =
|
|
2616
|
+
const projectDirPath = join6(projectsDir, projectDirEntry.name);
|
|
3263
2617
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
3264
2618
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
3265
2619
|
for (const filePath of jsonlFiles) {
|
|
@@ -3275,7 +2629,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
3275
2629
|
continue;
|
|
3276
2630
|
let lines;
|
|
3277
2631
|
try {
|
|
3278
|
-
lines =
|
|
2632
|
+
lines = readFileSync3(filePath, "utf-8").split(`
|
|
3279
2633
|
`).filter((l) => l.trim());
|
|
3280
2634
|
} catch {
|
|
3281
2635
|
continue;
|
|
@@ -3396,15 +2750,15 @@ var init_claude = __esm(() => {
|
|
|
3396
2750
|
init_database();
|
|
3397
2751
|
init_pricing();
|
|
3398
2752
|
init_accounts();
|
|
3399
|
-
CLAUDE_PROJECTS_DIR =
|
|
3400
|
-
TAKUMI_PROJECTS_DIR =
|
|
2753
|
+
CLAUDE_PROJECTS_DIR = join6(homedir3(), ".claude", "projects");
|
|
2754
|
+
TAKUMI_PROJECTS_DIR = join6(homedir3(), ".takumi", "projects");
|
|
3401
2755
|
});
|
|
3402
2756
|
|
|
3403
2757
|
// src/ingest/codex.ts
|
|
3404
|
-
import { existsSync as
|
|
3405
|
-
import { homedir as
|
|
3406
|
-
import { join as
|
|
3407
|
-
import { Database as
|
|
2758
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
2759
|
+
import { homedir as homedir4 } from "os";
|
|
2760
|
+
import { join as join7, basename as basename2 } from "path";
|
|
2761
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
3408
2762
|
function codexDbPath() {
|
|
3409
2763
|
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
3410
2764
|
}
|
|
@@ -3413,10 +2767,10 @@ function codexConfigPath() {
|
|
|
3413
2767
|
}
|
|
3414
2768
|
function readCodexModel() {
|
|
3415
2769
|
const configPath = codexConfigPath();
|
|
3416
|
-
if (!
|
|
2770
|
+
if (!existsSync5(configPath))
|
|
3417
2771
|
return "gpt-5-codex";
|
|
3418
2772
|
try {
|
|
3419
|
-
const content =
|
|
2773
|
+
const content = readFileSync4(configPath, "utf-8");
|
|
3420
2774
|
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
3421
2775
|
return match?.[1] ?? "gpt-5-codex";
|
|
3422
2776
|
} catch {
|
|
@@ -3439,7 +2793,7 @@ function openCodexDb(dbPath, verbose) {
|
|
|
3439
2793
|
for (const readonly of [true, false]) {
|
|
3440
2794
|
let codexDb = null;
|
|
3441
2795
|
try {
|
|
3442
|
-
codexDb = readonly ? new
|
|
2796
|
+
codexDb = readonly ? new BunDatabase(dbPath, { readonly: true }) : new BunDatabase(dbPath);
|
|
3443
2797
|
codexDb.prepare("PRAGMA schema_version").get();
|
|
3444
2798
|
return codexDb;
|
|
3445
2799
|
} catch (error) {
|
|
@@ -3454,12 +2808,12 @@ function openCodexDb(dbPath, verbose) {
|
|
|
3454
2808
|
return null;
|
|
3455
2809
|
}
|
|
3456
2810
|
function readTokenEvents(rolloutPath) {
|
|
3457
|
-
if (!rolloutPath || !
|
|
2811
|
+
if (!rolloutPath || !existsSync5(rolloutPath))
|
|
3458
2812
|
return [];
|
|
3459
2813
|
const fallbackUsages = new Map;
|
|
3460
2814
|
let fallbackTimestamp;
|
|
3461
2815
|
let aggregate = null;
|
|
3462
|
-
for (const line of
|
|
2816
|
+
for (const line of readFileSync4(rolloutPath, "utf-8").split(`
|
|
3463
2817
|
`)) {
|
|
3464
2818
|
if (!line.trim())
|
|
3465
2819
|
continue;
|
|
@@ -3531,7 +2885,7 @@ function fallbackEvents(totalTokens) {
|
|
|
3531
2885
|
}
|
|
3532
2886
|
async function ingestCodex(db, verbose = false) {
|
|
3533
2887
|
const dbPath = codexDbPath();
|
|
3534
|
-
if (!
|
|
2888
|
+
if (!existsSync5(dbPath)) {
|
|
3535
2889
|
if (verbose)
|
|
3536
2890
|
console.log("Codex DB not found:", dbPath);
|
|
3537
2891
|
return { sessions: 0, requests: 0 };
|
|
@@ -3616,14 +2970,14 @@ var init_codex = __esm(() => {
|
|
|
3616
2970
|
init_database();
|
|
3617
2971
|
init_pricing();
|
|
3618
2972
|
init_accounts();
|
|
3619
|
-
DEFAULT_CODEX_DB_PATH =
|
|
3620
|
-
DEFAULT_CODEX_CONFIG_PATH =
|
|
2973
|
+
DEFAULT_CODEX_DB_PATH = join7(homedir4(), ".codex", "state_5.sqlite");
|
|
2974
|
+
DEFAULT_CODEX_CONFIG_PATH = join7(homedir4(), ".codex", "config.toml");
|
|
3621
2975
|
});
|
|
3622
2976
|
|
|
3623
2977
|
// src/ingest/gemini.ts
|
|
3624
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
3625
|
-
import { homedir as
|
|
3626
|
-
import { join as
|
|
2978
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync5, existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
2979
|
+
import { homedir as homedir5 } from "os";
|
|
2980
|
+
import { join as join8, basename as basename3 } from "path";
|
|
3627
2981
|
function geminiTmpDir() {
|
|
3628
2982
|
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
3629
2983
|
}
|
|
@@ -3640,12 +2994,12 @@ function numberField(...values) {
|
|
|
3640
2994
|
function listProjectDirs(...roots) {
|
|
3641
2995
|
const dirs = new Set;
|
|
3642
2996
|
for (const root of roots) {
|
|
3643
|
-
if (!
|
|
2997
|
+
if (!existsSync6(root))
|
|
3644
2998
|
continue;
|
|
3645
2999
|
try {
|
|
3646
3000
|
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
3647
3001
|
if (entry.isDirectory())
|
|
3648
|
-
dirs.add(
|
|
3002
|
+
dirs.add(join8(root, entry.name));
|
|
3649
3003
|
}
|
|
3650
3004
|
} catch {}
|
|
3651
3005
|
}
|
|
@@ -3656,17 +3010,17 @@ function projectRoot(projectDir, chatData) {
|
|
|
3656
3010
|
return chatData.projectPath;
|
|
3657
3011
|
if (chatData.project_path)
|
|
3658
3012
|
return chatData.project_path;
|
|
3659
|
-
const rootFile =
|
|
3013
|
+
const rootFile = join8(projectDir, ".project_root");
|
|
3660
3014
|
try {
|
|
3661
|
-
if (
|
|
3662
|
-
return
|
|
3015
|
+
if (existsSync6(rootFile))
|
|
3016
|
+
return readFileSync5(rootFile, "utf-8").trim();
|
|
3663
3017
|
} catch {}
|
|
3664
3018
|
return "";
|
|
3665
3019
|
}
|
|
3666
3020
|
async function ingestGemini(db, verbose) {
|
|
3667
3021
|
const tmpDir = geminiTmpDir();
|
|
3668
3022
|
const historyDir = geminiHistoryDir();
|
|
3669
|
-
if (!
|
|
3023
|
+
if (!existsSync6(tmpDir) && !existsSync6(historyDir)) {
|
|
3670
3024
|
if (verbose)
|
|
3671
3025
|
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
3672
3026
|
return { sessions: 0, requests: 0 };
|
|
@@ -3678,17 +3032,17 @@ async function ingestGemini(db, verbose) {
|
|
|
3678
3032
|
const account = await resolveAccountForAgent("gemini");
|
|
3679
3033
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
3680
3034
|
for (const projectDir of projectDirs) {
|
|
3681
|
-
const chatsDir =
|
|
3682
|
-
if (!
|
|
3035
|
+
const chatsDir = join8(projectDir, "chats");
|
|
3036
|
+
if (!existsSync6(chatsDir))
|
|
3683
3037
|
continue;
|
|
3684
3038
|
let chatFiles = [];
|
|
3685
3039
|
try {
|
|
3686
|
-
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) =>
|
|
3040
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join8(chatsDir, f));
|
|
3687
3041
|
} catch {
|
|
3688
3042
|
continue;
|
|
3689
3043
|
}
|
|
3690
3044
|
for (const filePath of chatFiles) {
|
|
3691
|
-
const stateKey = filePath.replace(
|
|
3045
|
+
const stateKey = filePath.replace(homedir5(), "~");
|
|
3692
3046
|
let fileMtime = "0";
|
|
3693
3047
|
try {
|
|
3694
3048
|
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
@@ -3700,7 +3054,7 @@ async function ingestGemini(db, verbose) {
|
|
|
3700
3054
|
continue;
|
|
3701
3055
|
let chatData;
|
|
3702
3056
|
try {
|
|
3703
|
-
chatData = JSON.parse(
|
|
3057
|
+
chatData = JSON.parse(readFileSync5(filePath, "utf-8"));
|
|
3704
3058
|
} catch {
|
|
3705
3059
|
continue;
|
|
3706
3060
|
}
|
|
@@ -3779,19 +3133,19 @@ var init_gemini = __esm(() => {
|
|
|
3779
3133
|
init_database();
|
|
3780
3134
|
init_pricing();
|
|
3781
3135
|
init_accounts();
|
|
3782
|
-
DEFAULT_GEMINI_TMP_DIR =
|
|
3783
|
-
DEFAULT_GEMINI_HISTORY_DIR =
|
|
3136
|
+
DEFAULT_GEMINI_TMP_DIR = join8(homedir5(), ".gemini", "tmp");
|
|
3137
|
+
DEFAULT_GEMINI_HISTORY_DIR = join8(homedir5(), ".gemini", "history");
|
|
3784
3138
|
});
|
|
3785
3139
|
|
|
3786
3140
|
// src/ingest/opencode.ts
|
|
3787
|
-
import { existsSync as
|
|
3788
|
-
import { homedir as
|
|
3789
|
-
import { join as
|
|
3141
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
3142
|
+
import { homedir as homedir6 } from "os";
|
|
3143
|
+
import { join as join9 } from "path";
|
|
3790
3144
|
function walkJsonFiles(dir, acc = []) {
|
|
3791
|
-
if (!
|
|
3145
|
+
if (!existsSync7(dir))
|
|
3792
3146
|
return acc;
|
|
3793
3147
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
3794
|
-
const p =
|
|
3148
|
+
const p = join9(dir, entry.name);
|
|
3795
3149
|
if (entry.isDirectory())
|
|
3796
3150
|
walkJsonFiles(p, acc);
|
|
3797
3151
|
else if (entry.name.endsWith(".json"))
|
|
@@ -3811,7 +3165,7 @@ function parseSessionIdFromPath(filePath) {
|
|
|
3811
3165
|
return null;
|
|
3812
3166
|
}
|
|
3813
3167
|
async function ingestOpenCode(db, verbose = false) {
|
|
3814
|
-
const messageDir =
|
|
3168
|
+
const messageDir = join9(OPENCODE_STORAGE, "message");
|
|
3815
3169
|
const files = walkJsonFiles(messageDir);
|
|
3816
3170
|
let requests = 0;
|
|
3817
3171
|
const touched = new Set;
|
|
@@ -3826,7 +3180,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
3826
3180
|
continue;
|
|
3827
3181
|
let parsed;
|
|
3828
3182
|
try {
|
|
3829
|
-
parsed = JSON.parse(
|
|
3183
|
+
parsed = JSON.parse(readFileSync6(file, "utf-8"));
|
|
3830
3184
|
} catch {
|
|
3831
3185
|
continue;
|
|
3832
3186
|
}
|
|
@@ -3894,7 +3248,7 @@ var init_opencode = __esm(() => {
|
|
|
3894
3248
|
init_database();
|
|
3895
3249
|
init_pricing();
|
|
3896
3250
|
init_accounts();
|
|
3897
|
-
OPENCODE_STORAGE =
|
|
3251
|
+
OPENCODE_STORAGE = join9(homedir6(), ".local", "share", "opencode", "storage");
|
|
3898
3252
|
});
|
|
3899
3253
|
|
|
3900
3254
|
// src/ingest/cursor.ts
|
|
@@ -4022,14 +3376,14 @@ var init_cursor = __esm(() => {
|
|
|
4022
3376
|
});
|
|
4023
3377
|
|
|
4024
3378
|
// src/ingest/pi.ts
|
|
4025
|
-
import { existsSync as
|
|
4026
|
-
import { homedir as
|
|
4027
|
-
import { join as
|
|
3379
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
3380
|
+
import { homedir as homedir7 } from "os";
|
|
3381
|
+
import { join as join10 } from "path";
|
|
4028
3382
|
function walkSessions(dir, acc = []) {
|
|
4029
|
-
if (!
|
|
3383
|
+
if (!existsSync8(dir))
|
|
4030
3384
|
return acc;
|
|
4031
3385
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
4032
|
-
const p =
|
|
3386
|
+
const p = join10(dir, entry.name);
|
|
4033
3387
|
if (entry.isDirectory())
|
|
4034
3388
|
walkSessions(p, acc);
|
|
4035
3389
|
else if (entry.name.endsWith(".json"))
|
|
@@ -4051,7 +3405,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
4051
3405
|
continue;
|
|
4052
3406
|
let data;
|
|
4053
3407
|
try {
|
|
4054
|
-
data = JSON.parse(
|
|
3408
|
+
data = JSON.parse(readFileSync7(file, "utf-8"));
|
|
4055
3409
|
} catch {
|
|
4056
3410
|
continue;
|
|
4057
3411
|
}
|
|
@@ -4116,13 +3470,13 @@ var PI_SESSION_DIR;
|
|
|
4116
3470
|
var init_pi = __esm(() => {
|
|
4117
3471
|
init_database();
|
|
4118
3472
|
init_accounts();
|
|
4119
|
-
PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ??
|
|
3473
|
+
PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join10(homedir7(), ".pi", "agent", "sessions");
|
|
4120
3474
|
});
|
|
4121
3475
|
|
|
4122
3476
|
// src/ingest/hermes.ts
|
|
4123
|
-
import { existsSync as
|
|
4124
|
-
import { homedir as
|
|
4125
|
-
import { join as
|
|
3477
|
+
import { existsSync as existsSync9, statSync as statSync6 } from "fs";
|
|
3478
|
+
import { homedir as homedir8 } from "os";
|
|
3479
|
+
import { join as join11 } from "path";
|
|
4126
3480
|
function mapCostBasis(billingMode) {
|
|
4127
3481
|
if (billingMode === "subscription")
|
|
4128
3482
|
return "subscription_included";
|
|
@@ -4131,7 +3485,7 @@ function mapCostBasis(billingMode) {
|
|
|
4131
3485
|
return defaultCostBasisForAgent("hermes");
|
|
4132
3486
|
}
|
|
4133
3487
|
async function ingestHermes(db, verbose = false) {
|
|
4134
|
-
if (!
|
|
3488
|
+
if (!existsSync9(HERMES_DB)) {
|
|
4135
3489
|
return { sessions: 0, requests: 0 };
|
|
4136
3490
|
}
|
|
4137
3491
|
const { Database: Sqlite } = await import("bun:sqlite");
|
|
@@ -4211,19 +3565,19 @@ var HERMES_DB;
|
|
|
4211
3565
|
var init_hermes = __esm(() => {
|
|
4212
3566
|
init_database();
|
|
4213
3567
|
init_accounts();
|
|
4214
|
-
HERMES_DB =
|
|
3568
|
+
HERMES_DB = join11(homedir8(), ".hermes", "state.db");
|
|
4215
3569
|
});
|
|
4216
3570
|
|
|
4217
3571
|
// src/ingest/claude-quota.ts
|
|
4218
|
-
import { existsSync as
|
|
3572
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
|
|
4219
3573
|
function readClaudeToken() {
|
|
4220
3574
|
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
4221
3575
|
if (fromEnv)
|
|
4222
3576
|
return { token: fromEnv };
|
|
4223
|
-
if (!
|
|
3577
|
+
if (!existsSync10(CREDENTIALS_PATH))
|
|
4224
3578
|
return null;
|
|
4225
3579
|
try {
|
|
4226
|
-
const creds = JSON.parse(
|
|
3580
|
+
const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
|
|
4227
3581
|
const oauth = creds.claudeAiOauth;
|
|
4228
3582
|
if (!oauth?.accessToken)
|
|
4229
3583
|
return null;
|
|
@@ -4364,16 +3718,16 @@ var init_claude_quota = __esm(() => {
|
|
|
4364
3718
|
});
|
|
4365
3719
|
|
|
4366
3720
|
// src/ingest/codex-quota.ts
|
|
4367
|
-
import { existsSync as
|
|
3721
|
+
import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
|
|
4368
3722
|
function readCodexAuth() {
|
|
4369
3723
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
4370
3724
|
if (fromEnv)
|
|
4371
3725
|
return { token: fromEnv, authMode: "chatgpt" };
|
|
4372
3726
|
const authPath = agentPaths().codexAuth;
|
|
4373
|
-
if (!
|
|
3727
|
+
if (!existsSync11(authPath))
|
|
4374
3728
|
return null;
|
|
4375
3729
|
try {
|
|
4376
|
-
const auth = JSON.parse(
|
|
3730
|
+
const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
|
|
4377
3731
|
const token = auth.tokens?.access_token;
|
|
4378
3732
|
if (!token)
|
|
4379
3733
|
return null;
|
|
@@ -4557,15 +3911,15 @@ __export(exports_billing, {
|
|
|
4557
3911
|
syncGeminiBilling: () => syncGeminiBilling,
|
|
4558
3912
|
syncAnthropicBilling: () => syncAnthropicBilling
|
|
4559
3913
|
});
|
|
4560
|
-
import { readFileSync as
|
|
3914
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
4561
3915
|
function getAnthropicAdminKey() {
|
|
4562
|
-
return process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
|
|
3916
|
+
return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
|
|
4563
3917
|
}
|
|
4564
3918
|
function getOpenAIAdminKey() {
|
|
4565
|
-
return process.env["OPENAI_ADMIN_API_KEY"] ?? null;
|
|
3919
|
+
return process.env["HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY"] ?? process.env["OPENAI_ADMIN_API_KEY"] ?? null;
|
|
4566
3920
|
}
|
|
4567
3921
|
function getGeminiBillingExportPath() {
|
|
4568
|
-
return process.env["HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["GEMINI_BILLING_EXPORT_PATH"] ?? null;
|
|
3922
|
+
return process.env["HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["HASNAXYZ_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["GEMINI_BILLING_EXPORT_PATH"] ?? null;
|
|
4569
3923
|
}
|
|
4570
3924
|
function toISODate(d) {
|
|
4571
3925
|
return d.toISOString().substring(0, 10);
|
|
@@ -4641,7 +3995,7 @@ function parseBillingRows(content) {
|
|
|
4641
3995
|
async function syncAnthropicBilling(db, opts = {}) {
|
|
4642
3996
|
const key = getAnthropicAdminKey();
|
|
4643
3997
|
if (!key)
|
|
4644
|
-
throw new Error("Missing Anthropic admin key (
|
|
3998
|
+
throw new Error("Missing Anthropic admin key (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY)");
|
|
4645
3999
|
const now = new Date;
|
|
4646
4000
|
const end = opts.toDate ? new Date(opts.toDate) : new Date(now.getTime() + 24 * 3600000);
|
|
4647
4001
|
const days = opts.days ?? 31;
|
|
@@ -4690,7 +4044,7 @@ async function syncAnthropicBilling(db, opts = {}) {
|
|
|
4690
4044
|
async function syncOpenAIBilling(db, opts = {}) {
|
|
4691
4045
|
const key = getOpenAIAdminKey();
|
|
4692
4046
|
if (!key)
|
|
4693
|
-
throw new Error("Missing OpenAI admin key (
|
|
4047
|
+
throw new Error("Missing OpenAI admin key (HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY)");
|
|
4694
4048
|
const now = new Date;
|
|
4695
4049
|
const end = opts.toDate ? new Date(opts.toDate) : now;
|
|
4696
4050
|
const days = opts.days ?? 31;
|
|
@@ -4742,7 +4096,7 @@ async function syncGeminiBilling(db, opts = {}) {
|
|
|
4742
4096
|
return {
|
|
4743
4097
|
days: 0,
|
|
4744
4098
|
totalUsd: 0,
|
|
4745
|
-
skipped: "Missing Gemini billing export path (HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH or GEMINI_BILLING_EXPORT_PATH)"
|
|
4099
|
+
skipped: "Missing Gemini billing export path (HASNA_ECONOMY_GEMINI_BILLING_EXPORT_PATH, HASNAXYZ_ECONOMY_GEMINI_BILLING_EXPORT_PATH, or GEMINI_BILLING_EXPORT_PATH)"
|
|
4746
4100
|
};
|
|
4747
4101
|
}
|
|
4748
4102
|
const now = new Date;
|
|
@@ -4751,7 +4105,7 @@ async function syncGeminiBilling(db, opts = {}) {
|
|
|
4751
4105
|
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
4752
4106
|
const fromDateStr = toISODate(start);
|
|
4753
4107
|
const toDateStr = toISODate(end);
|
|
4754
|
-
const rows = parseBillingRows(
|
|
4108
|
+
const rows = parseBillingRows(readFileSync10(exportPath, "utf-8"));
|
|
4755
4109
|
clearBillingRange(db, "gemini", fromDateStr, toDateStr);
|
|
4756
4110
|
const updatedAt = new Date().toISOString();
|
|
4757
4111
|
let totalUsd = 0;
|
|
@@ -4783,26 +4137,22 @@ __export(exports_open_projects, {
|
|
|
4783
4137
|
syncOpenProjectsRegistry: () => syncOpenProjectsRegistry
|
|
4784
4138
|
});
|
|
4785
4139
|
async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
4786
|
-
let
|
|
4787
|
-
if (!
|
|
4140
|
+
let listProjects2 = listActiveProjects;
|
|
4141
|
+
if (!listProjects2) {
|
|
4788
4142
|
const projectsApi = await import("@hasna/projects");
|
|
4789
|
-
|
|
4143
|
+
listProjects2 = projectsApi.listProjects;
|
|
4790
4144
|
}
|
|
4791
|
-
|
|
4792
|
-
throw new Error("@hasna/projects does not expose listWorkspaces or listProjects");
|
|
4793
|
-
}
|
|
4794
|
-
const projects = listOpenProjects({ status: "active", limit: 5000 });
|
|
4145
|
+
const projects = listProjects2({ status: "active", limit: 5000 });
|
|
4795
4146
|
let imported = 0;
|
|
4796
4147
|
let skipped = 0;
|
|
4797
4148
|
for (const project of projects) {
|
|
4798
|
-
|
|
4799
|
-
if (!path) {
|
|
4149
|
+
if (!project.path) {
|
|
4800
4150
|
skipped++;
|
|
4801
4151
|
continue;
|
|
4802
4152
|
}
|
|
4803
4153
|
upsertProject(db, {
|
|
4804
4154
|
id: project.id,
|
|
4805
|
-
path,
|
|
4155
|
+
path: project.path,
|
|
4806
4156
|
name: project.name,
|
|
4807
4157
|
description: project.description,
|
|
4808
4158
|
tags: project.tags ?? [],
|
|
@@ -4824,16 +4174,16 @@ __export(exports_config, {
|
|
|
4824
4174
|
loadConfig: () => loadConfig2,
|
|
4825
4175
|
getConfigValue: () => getConfigValue
|
|
4826
4176
|
});
|
|
4827
|
-
import { existsSync as
|
|
4828
|
-
import { dirname as
|
|
4177
|
+
import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
4178
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
4829
4179
|
function getConfigPath() {
|
|
4830
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
4180
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join12(getDataDir(), "config.json");
|
|
4831
4181
|
}
|
|
4832
4182
|
function loadConfig2() {
|
|
4833
4183
|
try {
|
|
4834
4184
|
const configPath = getConfigPath();
|
|
4835
|
-
if (
|
|
4836
|
-
const raw =
|
|
4185
|
+
if (existsSync13(configPath)) {
|
|
4186
|
+
const raw = readFileSync11(configPath, "utf-8");
|
|
4837
4187
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
4838
4188
|
}
|
|
4839
4189
|
} catch {}
|
|
@@ -4841,10 +4191,10 @@ function loadConfig2() {
|
|
|
4841
4191
|
}
|
|
4842
4192
|
function saveConfig2(config) {
|
|
4843
4193
|
const configPath = getConfigPath();
|
|
4844
|
-
const dir =
|
|
4845
|
-
if (!
|
|
4846
|
-
|
|
4847
|
-
|
|
4194
|
+
const dir = dirname2(configPath);
|
|
4195
|
+
if (!existsSync13(dir))
|
|
4196
|
+
mkdirSync3(dir, { recursive: true });
|
|
4197
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2) + `
|
|
4848
4198
|
`);
|
|
4849
4199
|
}
|
|
4850
4200
|
function getConfigValue(key) {
|
|
@@ -4986,7 +4336,7 @@ var init_webhooks = __esm(() => {
|
|
|
4986
4336
|
});
|
|
4987
4337
|
|
|
4988
4338
|
// src/lib/watch-paths.ts
|
|
4989
|
-
import { existsSync as
|
|
4339
|
+
import { existsSync as existsSync14 } from "fs";
|
|
4990
4340
|
function getWatchPaths() {
|
|
4991
4341
|
const p = agentPaths();
|
|
4992
4342
|
const candidates = [
|
|
@@ -4999,7 +4349,7 @@ function getWatchPaths() {
|
|
|
4999
4349
|
p.piSessions,
|
|
5000
4350
|
p.hermesDir
|
|
5001
4351
|
];
|
|
5002
|
-
return candidates.filter((path) =>
|
|
4352
|
+
return candidates.filter((path) => existsSync14(path));
|
|
5003
4353
|
}
|
|
5004
4354
|
var init_watch_paths = __esm(() => {
|
|
5005
4355
|
init_paths();
|
|
@@ -5169,7 +4519,7 @@ __export(exports_serve, {
|
|
|
5169
4519
|
createHandler: () => createHandler
|
|
5170
4520
|
});
|
|
5171
4521
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
5172
|
-
import { existsSync as
|
|
4522
|
+
import { existsSync as existsSync15 } from "fs";
|
|
5173
4523
|
import { resolve, sep } from "path";
|
|
5174
4524
|
function json(data, status = 200) {
|
|
5175
4525
|
return new Response(JSON.stringify(data), {
|
|
@@ -5234,13 +4584,13 @@ function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
|
5234
4584
|
if (url.pathname.startsWith("/api") || url.pathname === "/health") {
|
|
5235
4585
|
return apiHandler(req);
|
|
5236
4586
|
}
|
|
5237
|
-
if (
|
|
4587
|
+
if (existsSync15(dashboardDir)) {
|
|
5238
4588
|
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
5239
|
-
if (filePath &&
|
|
4589
|
+
if (filePath && existsSync15(filePath)) {
|
|
5240
4590
|
return new Response(Bun.file(filePath));
|
|
5241
4591
|
}
|
|
5242
4592
|
const indexPath = dashboardPath(dashboardDir, "/");
|
|
5243
|
-
if (indexPath &&
|
|
4593
|
+
if (indexPath && existsSync15(indexPath)) {
|
|
5244
4594
|
return new Response(Bun.file(indexPath));
|
|
5245
4595
|
}
|
|
5246
4596
|
}
|
|
@@ -5655,14 +5005,14 @@ __export(exports_menubar, {
|
|
|
5655
5005
|
});
|
|
5656
5006
|
import chalk6 from "chalk";
|
|
5657
5007
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
5658
|
-
import { cpSync, existsSync as
|
|
5008
|
+
import { cpSync, existsSync as existsSync16, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
5659
5009
|
import { tmpdir, arch } from "os";
|
|
5660
|
-
import { join as
|
|
5010
|
+
import { join as join13 } from "path";
|
|
5661
5011
|
function getArch() {
|
|
5662
5012
|
return arch() === "arm64" ? "arm64" : "x86_64";
|
|
5663
5013
|
}
|
|
5664
5014
|
function isInstalled() {
|
|
5665
|
-
return
|
|
5015
|
+
return existsSync16(APP_PATH);
|
|
5666
5016
|
}
|
|
5667
5017
|
function isRunning() {
|
|
5668
5018
|
try {
|
|
@@ -5699,15 +5049,15 @@ async function menubarInstall(opts) {
|
|
|
5699
5049
|
console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
|
|
5700
5050
|
process.exit(1);
|
|
5701
5051
|
}
|
|
5702
|
-
const zipPath =
|
|
5703
|
-
const extractDir =
|
|
5052
|
+
const zipPath = join13(tmpdir(), `economy-bar-${cpuArch}.zip`);
|
|
5053
|
+
const extractDir = join13(tmpdir(), "economy-bar-extracted");
|
|
5704
5054
|
console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
|
|
5705
5055
|
try {
|
|
5706
5056
|
const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
|
|
5707
5057
|
if (!res.ok)
|
|
5708
5058
|
throw new Error(`Download failed: ${res.status}`);
|
|
5709
5059
|
const buffer = await res.arrayBuffer();
|
|
5710
|
-
|
|
5060
|
+
writeFileSync3(zipPath, Buffer.from(buffer));
|
|
5711
5061
|
console.log(chalk6.green(`\u2713 Downloaded (${(buffer.byteLength / 1024 / 1024).toFixed(1)} MB)`));
|
|
5712
5062
|
} catch (e) {
|
|
5713
5063
|
console.error(chalk6.red(`\u2717 Download failed: ${e instanceof Error ? e.message : String(e)}`));
|
|
@@ -5716,11 +5066,11 @@ async function menubarInstall(opts) {
|
|
|
5716
5066
|
console.log(chalk6.cyan("\u2192 Installing to /Applications..."));
|
|
5717
5067
|
try {
|
|
5718
5068
|
rmSync(extractDir, { recursive: true, force: true });
|
|
5719
|
-
|
|
5069
|
+
mkdirSync4(extractDir, { recursive: true });
|
|
5720
5070
|
execFileSync2("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
|
|
5721
5071
|
if (isInstalled())
|
|
5722
5072
|
rmSync(APP_PATH, { recursive: true, force: true });
|
|
5723
|
-
cpSync(
|
|
5073
|
+
cpSync(join13(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
|
|
5724
5074
|
try {
|
|
5725
5075
|
execFileSync2("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
|
|
5726
5076
|
} catch {}
|
|
@@ -5780,6 +5130,7 @@ var init_menubar = () => {};
|
|
|
5780
5130
|
|
|
5781
5131
|
// src/cli/index.ts
|
|
5782
5132
|
import { Command } from "commander";
|
|
5133
|
+
import { registerEventsCommands } from "@hasna/events/commander";
|
|
5783
5134
|
import chalk7 from "chalk";
|
|
5784
5135
|
|
|
5785
5136
|
// src/cli/brains.ts
|
|
@@ -6518,7 +5869,7 @@ var ROADMAP_PHASES = [
|
|
|
6518
5869
|
{
|
|
6519
5870
|
id: "phase-9",
|
|
6520
5871
|
title: "Multi-machine auto sync (4-machine fleet)",
|
|
6521
|
-
summary: "Investigation: machine_id + listMachines + manual cloud push/pull exist;
|
|
5872
|
+
summary: "Investigation: machine_id + listMachines + manual cloud push/pull exist; @hasna/cloud has incremental sync, conflict resolution, and launchd/systemd schedulers \u2014 but economy does not use them, requests/sessions lack updated_at, ingest is local-only, RDS is hardcoded, and default commands never pull before query.",
|
|
6522
5873
|
tasks: [
|
|
6523
5874
|
{
|
|
6524
5875
|
id: "9.1",
|
|
@@ -6533,7 +5884,7 @@ var ROADMAP_PHASES = [
|
|
|
6533
5884
|
},
|
|
6534
5885
|
{
|
|
6535
5886
|
id: "9.3",
|
|
6536
|
-
title: "Switch cloud sync to
|
|
5887
|
+
title: "Switch cloud sync to @hasna/cloud incrementalSyncPush/Pull with _sync_meta (replace full table scans)",
|
|
6537
5888
|
status: "done",
|
|
6538
5889
|
deps: ["9.2"]
|
|
6539
5890
|
},
|
|
@@ -6557,7 +5908,7 @@ var ROADMAP_PHASES = [
|
|
|
6557
5908
|
},
|
|
6558
5909
|
{
|
|
6559
5910
|
id: "9.7",
|
|
6560
|
-
title: "CLI economy cloud schedule install|status|remove \u2014
|
|
5911
|
+
title: "CLI economy cloud schedule install|status|remove \u2014 wrap @hasna/cloud registerSyncSchedule (launchd/systemd every 5\u201315m)",
|
|
6561
5912
|
status: "done",
|
|
6562
5913
|
deps: ["9.5"]
|
|
6563
5914
|
},
|
|
@@ -6968,8 +6319,8 @@ init_agents();
|
|
|
6968
6319
|
init_cloud_sync();
|
|
6969
6320
|
import chalk4 from "chalk";
|
|
6970
6321
|
import { randomUUID } from "crypto";
|
|
6971
|
-
import { existsSync as
|
|
6972
|
-
import { join as
|
|
6322
|
+
import { existsSync as existsSync3 } from "fs";
|
|
6323
|
+
import { join as join5 } from "path";
|
|
6973
6324
|
|
|
6974
6325
|
// src/cli/commands/completion.ts
|
|
6975
6326
|
var TOP_LEVEL = [
|
|
@@ -7219,13 +6570,13 @@ function registerExtendedCommands(program) {
|
|
|
7219
6570
|
const paths = [
|
|
7220
6571
|
["claude", agentPaths().claudeProjects],
|
|
7221
6572
|
["codex", agentPaths().codexDb],
|
|
7222
|
-
["gemini",
|
|
7223
|
-
["opencode",
|
|
6573
|
+
["gemini", join5(agentPaths().geminiTmp, "..")],
|
|
6574
|
+
["opencode", join5(agentPaths().opencodeMessages, "..", "..")],
|
|
7224
6575
|
["pi", agentPaths().piSessions],
|
|
7225
6576
|
["hermes", agentPaths().hermesDb]
|
|
7226
6577
|
];
|
|
7227
6578
|
for (const [agent, path] of paths) {
|
|
7228
|
-
checks.push({ ok:
|
|
6579
|
+
checks.push({ ok: existsSync3(path), msg: `${agent}: ${existsSync3(path) ? path : "not found"}` });
|
|
7229
6580
|
}
|
|
7230
6581
|
checks.push({ ok: Boolean(process.env["CURSOR_SESSION_TOKEN"]), msg: `cursor token: ${process.env["CURSOR_SESSION_TOKEN"] ? "set" : "missing CURSOR_SESSION_TOKEN"}` });
|
|
7231
6582
|
checks.push({ ok: Boolean(getCloudDatabaseUrl()), msg: `cloud: ${getCloudDatabaseUrl() ? "ECONOMY_CLOUD_DATABASE_URL set" : "not configured"}` });
|
|
@@ -7356,8 +6707,8 @@ init_cloud_sync();
|
|
|
7356
6707
|
// src/lib/peer-sync.ts
|
|
7357
6708
|
init_database();
|
|
7358
6709
|
init_package_metadata();
|
|
7359
|
-
import { Database as
|
|
7360
|
-
import { existsSync as
|
|
6710
|
+
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
6711
|
+
import { existsSync as existsSync12 } from "fs";
|
|
7361
6712
|
var GENERIC_PEER_TABLES = [
|
|
7362
6713
|
"usage_snapshots",
|
|
7363
6714
|
"subscriptions",
|
|
@@ -7368,7 +6719,7 @@ var GENERIC_PEER_TABLES = [
|
|
|
7368
6719
|
"model_pricing",
|
|
7369
6720
|
"machines"
|
|
7370
6721
|
];
|
|
7371
|
-
function
|
|
6722
|
+
function quoteIdent(identifier) {
|
|
7372
6723
|
return `"${identifier.replace(/"/g, '""')}"`;
|
|
7373
6724
|
}
|
|
7374
6725
|
function tableExists(db, table) {
|
|
@@ -7378,7 +6729,7 @@ function tableExists(db, table) {
|
|
|
7378
6729
|
function tableColumns(db, table) {
|
|
7379
6730
|
if (!tableExists(db, table))
|
|
7380
6731
|
return [];
|
|
7381
|
-
return db.prepare(`PRAGMA table_info(${
|
|
6732
|
+
return db.prepare(`PRAGMA table_info(${quoteIdent(table)})`).all();
|
|
7382
6733
|
}
|
|
7383
6734
|
function commonColumns(source, target, table) {
|
|
7384
6735
|
const sourceCols = new Set(tableColumns(source, table).map((c) => c.name));
|
|
@@ -7390,19 +6741,19 @@ function primaryKeyColumns(db, table) {
|
|
|
7390
6741
|
function selectRows(source, table, columns) {
|
|
7391
6742
|
if (columns.length === 0)
|
|
7392
6743
|
return [];
|
|
7393
|
-
const select = columns.map(
|
|
7394
|
-
return source.prepare(`SELECT ${select} FROM ${
|
|
6744
|
+
const select = columns.map(quoteIdent).join(", ");
|
|
6745
|
+
return source.prepare(`SELECT ${select} FROM ${quoteIdent(table)}`).all();
|
|
7395
6746
|
}
|
|
7396
6747
|
function rowByKey(target, table, keyColumns, row) {
|
|
7397
6748
|
if (keyColumns.length === 0)
|
|
7398
6749
|
return null;
|
|
7399
6750
|
if (keyColumns.some((c) => row[c] == null))
|
|
7400
6751
|
return null;
|
|
7401
|
-
const where = keyColumns.map((c) => `${
|
|
7402
|
-
return target.prepare(`SELECT * FROM ${
|
|
6752
|
+
const where = keyColumns.map((c) => `${quoteIdent(c)} = ?`).join(" AND ");
|
|
6753
|
+
return target.prepare(`SELECT * FROM ${quoteIdent(table)} WHERE ${where}`).get(...keyColumns.map((c) => row[c]));
|
|
7403
6754
|
}
|
|
7404
6755
|
function hasId(target, table, id) {
|
|
7405
|
-
return target.prepare(`SELECT id, machine_id FROM ${
|
|
6756
|
+
return target.prepare(`SELECT id, machine_id FROM ${quoteIdent(table)} WHERE id = ?`).get(id);
|
|
7406
6757
|
}
|
|
7407
6758
|
function shouldReplace(source, existing) {
|
|
7408
6759
|
if (!existing)
|
|
@@ -7429,10 +6780,10 @@ function normalizeRow(row, columns, sourceMachine, now) {
|
|
|
7429
6780
|
return next;
|
|
7430
6781
|
}
|
|
7431
6782
|
function insertOrReplace(target, table, columns, row) {
|
|
7432
|
-
const colSql = columns.map(
|
|
6783
|
+
const colSql = columns.map(quoteIdent).join(", ");
|
|
7433
6784
|
const placeholders = columns.map(() => "?").join(", ");
|
|
7434
6785
|
target.prepare(`
|
|
7435
|
-
INSERT OR REPLACE INTO ${
|
|
6786
|
+
INSERT OR REPLACE INTO ${quoteIdent(table)} (${colSql})
|
|
7436
6787
|
VALUES (${placeholders})
|
|
7437
6788
|
`).run(...columns.map((c) => row[c] ?? null));
|
|
7438
6789
|
}
|
|
@@ -7546,7 +6897,7 @@ function detectSourceMachine(source, fallback) {
|
|
|
7546
6897
|
continue;
|
|
7547
6898
|
const rows = source.prepare(`
|
|
7548
6899
|
SELECT machine_id, COUNT(*) as cnt
|
|
7549
|
-
FROM ${
|
|
6900
|
+
FROM ${quoteIdent(table)}
|
|
7550
6901
|
WHERE machine_id != '' AND machine_id IS NOT NULL
|
|
7551
6902
|
GROUP BY machine_id
|
|
7552
6903
|
`).all();
|
|
@@ -7583,13 +6934,13 @@ function ensureMachineRegistry(target, machine, now) {
|
|
|
7583
6934
|
}
|
|
7584
6935
|
function openSourceDatabase(path) {
|
|
7585
6936
|
try {
|
|
7586
|
-
return new
|
|
6937
|
+
return new BunDatabase2(path, { readonly: true });
|
|
7587
6938
|
} catch {
|
|
7588
|
-
return new
|
|
6939
|
+
return new BunDatabase2(path);
|
|
7589
6940
|
}
|
|
7590
6941
|
}
|
|
7591
6942
|
function mergePeerDatabase(target, sourcePath, opts = {}) {
|
|
7592
|
-
if (!
|
|
6943
|
+
if (!existsSync12(sourcePath))
|
|
7593
6944
|
throw new Error(`source database does not exist: ${sourcePath}`);
|
|
7594
6945
|
const source = openSourceDatabase(sourcePath);
|
|
7595
6946
|
const now = opts.now ?? new Date().toISOString();
|
|
@@ -8452,8 +7803,8 @@ program.command("dashboard").description("Open the web dashboard (auto-starts se
|
|
|
8452
7803
|
if (!serverRunning) {
|
|
8453
7804
|
console.log(chalk7.cyan(`\u2192 Starting economy server on port ${port}...`));
|
|
8454
7805
|
const { spawn } = await import("child_process");
|
|
8455
|
-
const { resolve: resolve2, dirname:
|
|
8456
|
-
const serveScript = resolve2(
|
|
7806
|
+
const { resolve: resolve2, dirname: dirname3 } = await import("path");
|
|
7807
|
+
const serveScript = resolve2(dirname3(process.argv[1]), "..", "server", "index.js");
|
|
8457
7808
|
const child = spawn(process.execPath, [serveScript], {
|
|
8458
7809
|
detached: true,
|
|
8459
7810
|
stdio: "ignore",
|
|
@@ -8607,8 +7958,8 @@ program.command("export").description("Export data as CSV").option("--type <type
|
|
|
8607
7958
|
}
|
|
8608
7959
|
}
|
|
8609
7960
|
if (opts.output) {
|
|
8610
|
-
const { writeFileSync:
|
|
8611
|
-
|
|
7961
|
+
const { writeFileSync: writeFileSync4 } = await import("fs");
|
|
7962
|
+
writeFileSync4(opts.output, csv);
|
|
8612
7963
|
console.log(chalk7.green(`\u2713 Exported to ${opts.output}`));
|
|
8613
7964
|
} else {
|
|
8614
7965
|
process.stdout.write(csv);
|
|
@@ -8960,4 +8311,5 @@ registerBrainsCommand(program);
|
|
|
8960
8311
|
registerTodosCommand(program);
|
|
8961
8312
|
registerExtendedCommands(program);
|
|
8962
8313
|
registerFleetCommands(program);
|
|
8314
|
+
registerEventsCommands(program, { source: "economy" });
|
|
8963
8315
|
program.parse();
|