@hasna/economy 0.2.29 → 0.2.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +8 -8
  2. package/dist/cli/commands/tui.d.ts +1 -1
  3. package/dist/cli/commands/tui.d.ts.map +1 -1
  4. package/dist/cli/index.js +850 -187
  5. package/dist/db/database.d.ts +4 -2
  6. package/dist/db/database.d.ts.map +1 -1
  7. package/dist/db/storage-adapter.d.ts +34 -0
  8. package/dist/db/storage-adapter.d.ts.map +1 -0
  9. package/dist/index.d.ts +4 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1057 -54
  12. package/dist/ingest/billing.d.ts +1 -1
  13. package/dist/ingest/billing.d.ts.map +1 -1
  14. package/dist/ingest/claude-quota.d.ts +1 -1
  15. package/dist/ingest/claude-quota.d.ts.map +1 -1
  16. package/dist/ingest/claude.d.ts +1 -1
  17. package/dist/ingest/claude.d.ts.map +1 -1
  18. package/dist/ingest/codex-quota.d.ts +1 -1
  19. package/dist/ingest/codex-quota.d.ts.map +1 -1
  20. package/dist/ingest/codex.d.ts +1 -1
  21. package/dist/ingest/codex.d.ts.map +1 -1
  22. package/dist/ingest/cursor.d.ts +1 -1
  23. package/dist/ingest/cursor.d.ts.map +1 -1
  24. package/dist/ingest/gemini.d.ts +1 -1
  25. package/dist/ingest/gemini.d.ts.map +1 -1
  26. package/dist/ingest/hermes.d.ts +1 -1
  27. package/dist/ingest/hermes.d.ts.map +1 -1
  28. package/dist/ingest/opencode.d.ts +1 -1
  29. package/dist/ingest/opencode.d.ts.map +1 -1
  30. package/dist/ingest/otel.d.ts +1 -1
  31. package/dist/ingest/otel.d.ts.map +1 -1
  32. package/dist/ingest/pi.d.ts +1 -1
  33. package/dist/ingest/pi.d.ts.map +1 -1
  34. package/dist/ingest/plugin.d.ts +1 -1
  35. package/dist/ingest/plugin.d.ts.map +1 -1
  36. package/dist/lib/billing-diff.d.ts +1 -1
  37. package/dist/lib/billing-diff.d.ts.map +1 -1
  38. package/dist/lib/cloud-sync.d.ts +9 -2
  39. package/dist/lib/cloud-sync.d.ts.map +1 -1
  40. package/dist/lib/open-projects.d.ts +3 -2
  41. package/dist/lib/open-projects.d.ts.map +1 -1
  42. package/dist/lib/peer-sync.d.ts +1 -1
  43. package/dist/lib/peer-sync.d.ts.map +1 -1
  44. package/dist/lib/pricing.d.ts +1 -1
  45. package/dist/lib/pricing.d.ts.map +1 -1
  46. package/dist/lib/remote-storage.d.ts +15 -0
  47. package/dist/lib/remote-storage.d.ts.map +1 -0
  48. package/dist/lib/savings.d.ts +1 -1
  49. package/dist/lib/savings.d.ts.map +1 -1
  50. package/dist/lib/spikes.d.ts +1 -1
  51. package/dist/lib/spikes.d.ts.map +1 -1
  52. package/dist/lib/storage-sync.d.ts +27 -0
  53. package/dist/lib/storage-sync.d.ts.map +1 -0
  54. package/dist/lib/sync-all.d.ts +1 -1
  55. package/dist/lib/sync-all.d.ts.map +1 -1
  56. package/dist/lib/webhooks.d.ts +1 -1
  57. package/dist/lib/webhooks.d.ts.map +1 -1
  58. package/dist/mcp/http.d.ts +1 -0
  59. package/dist/mcp/http.d.ts.map +1 -1
  60. package/dist/mcp/index.js +518 -40
  61. package/dist/mcp/server.d.ts.map +1 -1
  62. package/dist/otel/index.js +442 -15
  63. package/dist/server/index.js +510 -51
  64. package/dist/server/serve.d.ts +1 -1
  65. package/dist/server/serve.d.ts.map +1 -1
  66. 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 Database(path);
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 machineClause = machine ? " AND machine_id = ?" : "";
1363
- const params = machine ? [machine] : [];
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 DATE(timestamp) = DATE('now')${machineClause}
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 = new SqliteAdapter(getDbPath());
2127
- await runCloudMigrations(cloud);
2128
- const tables = opts?.tables ?? [...CLOUD_TABLES];
2129
- const results = await syncPush(local, cloud, { tables, conflictColumn: "updated_at" });
2130
- const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
2131
- touchMachineRegistry(local, "push");
2132
- local.close();
2133
- await cloud.close();
2134
- return { rows, machine: getMachineId() };
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 = new SqliteAdapter(getDbPath());
2140
- await runCloudMigrations(cloud);
2141
- const tables = opts?.tables ?? [...CLOUD_TABLES];
2142
- const results = await syncPull(cloud, local, { tables, conflictColumn: "updated_at" });
2143
- const rows = results.reduce((s, r) => s + r.rowsWritten, 0);
2144
- touchMachineRegistry(local, "pull");
2145
- local.close();
2146
- await cloud.close();
2147
- setLastCloudPull();
2148
- return { rows, machine: getMachineId() };
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
- db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES ('cloud', 'last_pull_at', ?)`).run(at);
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
- const row = db.prepare(`SELECT value FROM ingest_state WHERE source = 'cloud' AND key = 'last_pull_at'`).get();
2162
- return row?.value ?? null;
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
- const { registerSyncSchedule } = await import("@hasna/cloud");
2210
- await registerSyncSchedule(intervalMinutes);
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
- const { removeSyncSchedule } = await import("@hasna/cloud");
2214
- await removeSyncSchedule();
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 { getSyncScheduleStatus } = await import("@hasna/cloud");
2218
- return getSyncScheduleStatus();
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
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
- var CLOUD_TABLES;
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 homedir2 } from "os";
2408
- import { join as join4 } from "path";
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"] ?? homedir2();
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: join4(home, ".claude", "projects"),
2416
- claudeCredentials: join4(home, ".claude", ".credentials.json"),
2417
- takumiProjects: join4(home, ".takumi", "projects"),
2418
- codexDir: join4(home, ".codex"),
2419
- codexDb: join4(home, ".codex", "state_5.sqlite"),
2420
- codexAuth: join4(home, ".codex", "auth.json"),
2421
- codexConfig: join4(home, ".codex", "config.toml"),
2422
- geminiTmp: join4(home, ".gemini", "tmp"),
2423
- geminiHistory: join4(home, ".gemini", "history"),
2424
- opencodeMessages: join4(home, ".local", "share", "opencode", "storage", "message"),
2425
- piSessions: join4(home, ".pi", "agent", "sessions"),
2426
- hermesDir: join4(home, ".hermes"),
2427
- hermesDb: join4(home, ".hermes", "state.db")
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 readFileSync3, existsSync as existsSync4, statSync as statSync2 } from "fs";
2569
- import { homedir as homedir3 } from "os";
2570
- import { join as join6, basename } from "path";
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(join6(dir, entry.name));
3233
+ walk(join7(dir, entry.name));
2584
3234
  else if (entry.name.endsWith(".jsonl"))
2585
- files.push(join6(dir, entry.name));
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 (!existsSync4(projectsDir)) {
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 = join6(projectsDir, projectDirEntry.name);
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 = readFileSync3(filePath, "utf-8").split(`
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 = join6(homedir3(), ".claude", "projects");
2750
- TAKUMI_PROJECTS_DIR = join6(homedir3(), ".takumi", "projects");
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 existsSync5, readFileSync as readFileSync4 } from "fs";
2755
- import { homedir as homedir4 } from "os";
2756
- import { join as join7, basename as basename2 } from "path";
2757
- import { Database as BunDatabase } from "bun:sqlite";
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 (!existsSync5(configPath))
3416
+ if (!existsSync6(configPath))
2767
3417
  return "gpt-5-codex";
2768
3418
  try {
2769
- const content = readFileSync4(configPath, "utf-8");
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 BunDatabase(dbPath, { readonly: true }) : new BunDatabase(dbPath);
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 || !existsSync5(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 readFileSync4(rolloutPath, "utf-8").split(`
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 (!existsSync5(dbPath)) {
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 = join7(homedir4(), ".codex", "state_5.sqlite");
2970
- DEFAULT_CODEX_CONFIG_PATH = join7(homedir4(), ".codex", "config.toml");
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 readFileSync5, existsSync as existsSync6, statSync as statSync3 } from "fs";
2975
- import { homedir as homedir5 } from "os";
2976
- import { join as join8, basename as basename3 } from "path";
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 (!existsSync6(root))
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(join8(root, entry.name));
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 = join8(projectDir, ".project_root");
3659
+ const rootFile = join9(projectDir, ".project_root");
3010
3660
  try {
3011
- if (existsSync6(rootFile))
3012
- return readFileSync5(rootFile, "utf-8").trim();
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 (!existsSync6(tmpDir) && !existsSync6(historyDir)) {
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 = join8(projectDir, "chats");
3032
- if (!existsSync6(chatsDir))
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) => join8(chatsDir, 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(homedir5(), "~");
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(readFileSync5(filePath, "utf-8"));
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 = join8(homedir5(), ".gemini", "tmp");
3133
- DEFAULT_GEMINI_HISTORY_DIR = join8(homedir5(), ".gemini", "history");
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 existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
3138
- import { homedir as homedir6 } from "os";
3139
- import { join as join9 } from "path";
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 (!existsSync7(dir))
3791
+ if (!existsSync8(dir))
3142
3792
  return acc;
3143
3793
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
3144
- const p = join9(dir, entry.name);
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 = join9(OPENCODE_STORAGE, "message");
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(readFileSync6(file, "utf-8"));
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 = join9(homedir6(), ".local", "share", "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 existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
3376
- import { homedir as homedir7 } from "os";
3377
- import { join as join10 } from "path";
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 (!existsSync8(dir))
4029
+ if (!existsSync9(dir))
3380
4030
  return acc;
3381
4031
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
3382
- const p = join10(dir, entry.name);
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(readFileSync7(file, "utf-8"));
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"] ?? join10(homedir7(), ".pi", "agent", "sessions");
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 existsSync9, statSync as statSync6 } from "fs";
3474
- import { homedir as homedir8 } from "os";
3475
- import { join as join11 } from "path";
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 (!existsSync9(HERMES_DB)) {
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 = join11(homedir8(), ".hermes", "state.db");
4214
+ HERMES_DB = join12(homedir9(), ".hermes", "state.db");
3565
4215
  });
3566
4216
 
3567
4217
  // src/ingest/claude-quota.ts
3568
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
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 (!existsSync10(CREDENTIALS_PATH))
4223
+ if (!existsSync11(CREDENTIALS_PATH))
3574
4224
  return null;
3575
4225
  try {
3576
- const creds = JSON.parse(readFileSync8(CREDENTIALS_PATH, "utf-8"));
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 existsSync11, readFileSync as readFileSync9 } from "fs";
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 (!existsSync11(authPath))
4373
+ if (!existsSync12(authPath))
3724
4374
  return null;
3725
4375
  try {
3726
- const auth = JSON.parse(readFileSync9(authPath, "utf-8"));
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 readFileSync10 } from "fs";
4560
+ import { readFileSync as readFileSync11 } from "fs";
3911
4561
  function getAnthropicAdminKey() {
3912
- return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
4562
+ return process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
3913
4563
  }
3914
4564
  function getOpenAIAdminKey() {
3915
- return process.env["HASNAXYZ_OPENAI_LIVE_ADMIN_API_KEY"] ?? process.env["OPENAI_ADMIN_API_KEY"] ?? null;
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["HASNAXYZ_ECONOMY_GEMINI_BILLING_EXPORT_PATH"] ?? process.env["GEMINI_BILLING_EXPORT_PATH"] ?? null;
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 (HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_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 (HASNAXYZ_OPENAI_LIVE_ADMIN_API_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, HASNAXYZ_ECONOMY_GEMINI_BILLING_EXPORT_PATH, or 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(readFileSync10(exportPath, "utf-8"));
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 listProjects2 = listActiveProjects;
4137
- if (!listProjects2) {
4786
+ let listOpenProjects = listActiveProjects;
4787
+ if (!listOpenProjects) {
4138
4788
  const projectsApi = await import("@hasna/projects");
4139
- listProjects2 = projectsApi.listProjects;
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 = listProjects2({ status: "active", limit: 5000 });
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
- if (!project.path) {
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: project.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 existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
4174
- import { dirname as dirname2, join as join12 } from "path";
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"] ?? join12(getDataDir(), "config.json");
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 (existsSync13(configPath)) {
4182
- const raw = readFileSync11(configPath, "utf-8");
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 = dirname2(configPath);
4191
- if (!existsSync13(dir))
4192
- mkdirSync3(dir, { recursive: true });
4193
- writeFileSync2(configPath, JSON.stringify(config, null, 2) + `
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 existsSync14 } from "fs";
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) => existsSync14(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 existsSync15 } from "fs";
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 (existsSync15(dashboardDir)) {
5237
+ if (existsSync16(dashboardDir)) {
4584
5238
  const filePath = dashboardPath(dashboardDir, url.pathname);
4585
- if (filePath && existsSync15(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 && existsSync15(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
- return ok(queryHourlyBreakdown(db, machine));
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 existsSync16, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
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 join13 } from "path";
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 existsSync16(APP_PATH);
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 = join13(tmpdir(), `economy-bar-${cpuArch}.zip`);
5040
- const extractDir = join13(tmpdir(), "economy-bar-extracted");
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
- writeFileSync3(zipPath, Buffer.from(buffer));
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
- mkdirSync4(extractDir, { recursive: true });
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(join13(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
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; @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.",
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 @hasna/cloud incrementalSyncPush/Pull with _sync_meta (replace full table scans)",
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 wrap @hasna/cloud registerSyncSchedule (launchd/systemd every 5\u201315m)",
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 existsSync3 } from "fs";
6309
- import { join as join5 } from "path";
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", join5(agentPaths().geminiTmp, "..")],
6560
- ["opencode", join5(agentPaths().opencodeMessages, "..", "..")],
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: existsSync3(path), msg: `${agent}: ${existsSync3(path) ? path : "not found"}` });
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 BunDatabase2 } from "bun:sqlite";
6697
- import { existsSync as existsSync12 } from "fs";
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 quoteIdent(identifier) {
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(${quoteIdent(table)})`).all();
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(quoteIdent).join(", ");
6731
- return source.prepare(`SELECT ${select} FROM ${quoteIdent(table)}`).all();
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) => `${quoteIdent(c)} = ?`).join(" AND ");
6739
- return target.prepare(`SELECT * FROM ${quoteIdent(table)} WHERE ${where}`).get(...keyColumns.map((c) => row[c]));
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 ${quoteIdent(table)} WHERE id = ?`).get(id);
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(quoteIdent).join(", ");
7432
+ const colSql = columns.map(quoteIdent2).join(", ");
6770
7433
  const placeholders = columns.map(() => "?").join(", ");
6771
7434
  target.prepare(`
6772
- INSERT OR REPLACE INTO ${quoteIdent(table)} (${colSql})
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 ${quoteIdent(table)}
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 BunDatabase2(path, { readonly: true });
7586
+ return new BunDatabase3(path, { readonly: true });
6924
7587
  } catch {
6925
- return new BunDatabase2(path);
7588
+ return new BunDatabase3(path);
6926
7589
  }
6927
7590
  }
6928
7591
  function mergePeerDatabase(target, sourcePath, opts = {}) {
6929
- if (!existsSync12(sourcePath))
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: dirname3 } = await import("path");
7793
- const serveScript = resolve2(dirname3(process.argv[1]), "..", "server", "index.js");
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: writeFileSync4 } = await import("fs");
7948
- writeFileSync4(opts.output, csv);
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);