@hasna/economy 0.2.11 → 0.2.13
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/dist/cli/index.js +367 -32
- package/dist/db/database.d.ts +10 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/index.js +81 -26
- package/dist/ingest/claude.d.ts +5 -0
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/mcp/index.js +207 -39
- package/dist/server/index.js +90 -28
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -109,8 +109,17 @@ var init_pricing = __esm(() => {
|
|
|
109
109
|
// src/db/database.ts
|
|
110
110
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
111
111
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
112
|
+
import { hostname } from "os";
|
|
112
113
|
import { homedir } from "os";
|
|
113
114
|
import { join } from "path";
|
|
115
|
+
function getMachineId() {
|
|
116
|
+
if (process.env["ECONOMY_MACHINE_ID"])
|
|
117
|
+
return process.env["ECONOMY_MACHINE_ID"];
|
|
118
|
+
const h = hostname().toLowerCase();
|
|
119
|
+
if (h.startsWith("spark") || h.startsWith("apple"))
|
|
120
|
+
return h.split(".")[0];
|
|
121
|
+
return h.split(".")[0];
|
|
122
|
+
}
|
|
114
123
|
function getDataDir() {
|
|
115
124
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
116
125
|
const newDir = join(home, ".hasna", "economy");
|
|
@@ -143,6 +152,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
143
152
|
}
|
|
144
153
|
const db = new Database(path);
|
|
145
154
|
db.exec("PRAGMA journal_mode = WAL");
|
|
155
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
146
156
|
db.exec("PRAGMA foreign_keys = ON");
|
|
147
157
|
initSchema(db);
|
|
148
158
|
if (!skipSeed) {
|
|
@@ -164,7 +174,8 @@ function initSchema(db) {
|
|
|
164
174
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
165
175
|
duration_ms INTEGER DEFAULT 0,
|
|
166
176
|
timestamp TEXT NOT NULL,
|
|
167
|
-
source_request_id TEXT
|
|
177
|
+
source_request_id TEXT,
|
|
178
|
+
machine_id TEXT DEFAULT ''
|
|
168
179
|
);
|
|
169
180
|
|
|
170
181
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -176,7 +187,8 @@ function initSchema(db) {
|
|
|
176
187
|
ended_at TEXT,
|
|
177
188
|
total_cost_usd REAL DEFAULT 0,
|
|
178
189
|
total_tokens INTEGER DEFAULT 0,
|
|
179
|
-
request_count INTEGER DEFAULT 0
|
|
190
|
+
request_count INTEGER DEFAULT 0,
|
|
191
|
+
machine_id TEXT DEFAULT ''
|
|
180
192
|
);
|
|
181
193
|
|
|
182
194
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -242,6 +254,15 @@ function initSchema(db) {
|
|
|
242
254
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
243
255
|
);
|
|
244
256
|
`);
|
|
257
|
+
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
258
|
+
if (!cols.some((c) => c.name === "machine_id")) {
|
|
259
|
+
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
260
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
261
|
+
}
|
|
262
|
+
db.exec(`
|
|
263
|
+
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
264
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
265
|
+
`);
|
|
245
266
|
}
|
|
246
267
|
function periodWhere(period) {
|
|
247
268
|
switch (period) {
|
|
@@ -280,17 +301,17 @@ function upsertRequest(db, req) {
|
|
|
280
301
|
INSERT OR REPLACE INTO requests
|
|
281
302
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
282
303
|
cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
|
|
283
|
-
timestamp, source_request_id)
|
|
284
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
285
|
-
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id);
|
|
304
|
+
timestamp, source_request_id, machine_id)
|
|
305
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
306
|
+
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cost_usd, req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "");
|
|
286
307
|
}
|
|
287
308
|
function upsertSession(db, session) {
|
|
288
309
|
db.prepare(`
|
|
289
310
|
INSERT OR REPLACE INTO sessions
|
|
290
311
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
291
|
-
total_cost_usd, total_tokens, request_count)
|
|
292
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
293
|
-
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count);
|
|
312
|
+
total_cost_usd, total_tokens, request_count, machine_id)
|
|
313
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
314
|
+
`).run(session.id, session.agent, session.project_path, session.project_name, session.started_at, session.ended_at ?? null, session.total_cost_usd, session.total_tokens, session.request_count, session.machine_id ?? "");
|
|
294
315
|
}
|
|
295
316
|
function rollupSession(db, sessionId) {
|
|
296
317
|
db.prepare(`
|
|
@@ -320,6 +341,10 @@ function querySessions(db, filter = {}) {
|
|
|
320
341
|
conditions.push("started_at >= ?");
|
|
321
342
|
params.push(filter.since);
|
|
322
343
|
}
|
|
344
|
+
if (filter.machine) {
|
|
345
|
+
conditions.push("machine_id = ?");
|
|
346
|
+
params.push(filter.machine);
|
|
347
|
+
}
|
|
323
348
|
if (filter.search) {
|
|
324
349
|
const q = `%${filter.search}%`;
|
|
325
350
|
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
@@ -338,24 +363,25 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
338
363
|
}
|
|
339
364
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
340
365
|
}
|
|
341
|
-
function querySummary(db, period) {
|
|
366
|
+
function querySummary(db, period, machine) {
|
|
342
367
|
const rWhere = periodWhere(period);
|
|
343
368
|
const sWhere = sessionPeriodWhere(period);
|
|
369
|
+
const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
344
370
|
const r = db.prepare(`
|
|
345
371
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
346
372
|
COUNT(*) as requests,
|
|
347
373
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
348
|
-
FROM requests WHERE ${rWhere}
|
|
374
|
+
FROM requests WHERE ${rWhere}${machineClause}
|
|
349
375
|
`).get();
|
|
350
376
|
const codexTotals = db.prepare(`
|
|
351
377
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
352
378
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
353
379
|
COUNT(*) as sessions
|
|
354
380
|
FROM sessions
|
|
355
|
-
WHERE ${sWhere}
|
|
381
|
+
WHERE ${sWhere}${machineClause}
|
|
356
382
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
357
383
|
`).get();
|
|
358
|
-
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
384
|
+
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
|
|
359
385
|
return {
|
|
360
386
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
361
387
|
requests: r.requests,
|
|
@@ -509,6 +535,20 @@ function setIngestState(db, source, key, value) {
|
|
|
509
535
|
function queryRequestsSince(db, since) {
|
|
510
536
|
return db.prepare(`SELECT * FROM requests WHERE timestamp > ? ORDER BY timestamp ASC`).all(since);
|
|
511
537
|
}
|
|
538
|
+
function listMachines(db) {
|
|
539
|
+
return db.prepare(`
|
|
540
|
+
SELECT
|
|
541
|
+
s.machine_id,
|
|
542
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
543
|
+
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
544
|
+
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
545
|
+
MAX(s.started_at) as last_active
|
|
546
|
+
FROM sessions s
|
|
547
|
+
WHERE s.machine_id != ''
|
|
548
|
+
GROUP BY s.machine_id
|
|
549
|
+
ORDER BY total_cost_usd DESC
|
|
550
|
+
`).all();
|
|
551
|
+
}
|
|
512
552
|
function upsertModelPricing(db, p) {
|
|
513
553
|
db.prepare(`
|
|
514
554
|
INSERT OR REPLACE INTO model_pricing
|
|
@@ -569,29 +609,36 @@ function collectJsonlFiles(projectDir) {
|
|
|
569
609
|
return files;
|
|
570
610
|
}
|
|
571
611
|
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
572
|
-
|
|
612
|
+
return ingestJsonlProjects(db, CLAUDE_PROJECTS_DIR, "claude", verbose);
|
|
613
|
+
}
|
|
614
|
+
async function ingestTakumi(db, verbose = false) {
|
|
615
|
+
return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
|
|
616
|
+
}
|
|
617
|
+
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
618
|
+
if (!existsSync3(projectsDir)) {
|
|
573
619
|
if (verbose)
|
|
574
|
-
console.log(
|
|
620
|
+
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
575
621
|
return { files: 0, requests: 0, sessions: 0 };
|
|
576
622
|
}
|
|
623
|
+
const machineId = getMachineId();
|
|
577
624
|
let totalFiles = 0;
|
|
578
625
|
let totalRequests = 0;
|
|
579
626
|
const touchedSessions = new Set;
|
|
580
627
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
581
|
-
const projectDirs = readdirSync2(
|
|
628
|
+
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
582
629
|
for (const projectDirEntry of projectDirs) {
|
|
583
|
-
const projectDirPath = join4(
|
|
630
|
+
const projectDirPath = join4(projectsDir, projectDirEntry.name);
|
|
584
631
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
585
632
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
586
633
|
for (const filePath of jsonlFiles) {
|
|
587
|
-
const stateKey = filePath.replace(
|
|
634
|
+
const stateKey = filePath.replace(projectsDir, "");
|
|
588
635
|
let fileMtime = "0";
|
|
589
636
|
try {
|
|
590
637
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
591
638
|
} catch {
|
|
592
639
|
continue;
|
|
593
640
|
}
|
|
594
|
-
const processed = getIngestState(db,
|
|
641
|
+
const processed = getIngestState(db, agentName, stateKey);
|
|
595
642
|
if (processed === fileMtime)
|
|
596
643
|
continue;
|
|
597
644
|
let lines;
|
|
@@ -632,10 +679,10 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
632
679
|
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
633
680
|
continue;
|
|
634
681
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
635
|
-
const reqId =
|
|
682
|
+
const reqId = `${agentName}-${sessionId}-${timestamp}`;
|
|
636
683
|
upsertRequest(db, {
|
|
637
684
|
id: reqId,
|
|
638
|
-
agent:
|
|
685
|
+
agent: agentName,
|
|
639
686
|
session_id: sessionId,
|
|
640
687
|
model,
|
|
641
688
|
input_tokens: inputTokens,
|
|
@@ -645,7 +692,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
645
692
|
cost_usd: costUsd,
|
|
646
693
|
duration_ms: 0,
|
|
647
694
|
timestamp,
|
|
648
|
-
source_request_id: reqId
|
|
695
|
+
source_request_id: reqId,
|
|
696
|
+
machine_id: machineId
|
|
649
697
|
});
|
|
650
698
|
if (!touchedSessions.has(sessionId)) {
|
|
651
699
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
@@ -654,14 +702,15 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
654
702
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
655
703
|
const session = {
|
|
656
704
|
id: sessionId,
|
|
657
|
-
agent:
|
|
705
|
+
agent: agentName,
|
|
658
706
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
659
707
|
project_name: detectedProject ? detectedProject.name : "",
|
|
660
708
|
started_at: timestamp,
|
|
661
709
|
ended_at: null,
|
|
662
710
|
total_cost_usd: 0,
|
|
663
711
|
total_tokens: 0,
|
|
664
|
-
request_count: 0
|
|
712
|
+
request_count: 0,
|
|
713
|
+
machine_id: machineId
|
|
665
714
|
};
|
|
666
715
|
upsertSession(db, session);
|
|
667
716
|
}
|
|
@@ -669,7 +718,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
669
718
|
}
|
|
670
719
|
totalRequests++;
|
|
671
720
|
}
|
|
672
|
-
setIngestState(db,
|
|
721
|
+
setIngestState(db, agentName, stateKey, fileMtime);
|
|
673
722
|
totalFiles++;
|
|
674
723
|
}
|
|
675
724
|
}
|
|
@@ -678,11 +727,12 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
678
727
|
}
|
|
679
728
|
return { files: totalFiles, requests: totalRequests, sessions: touchedSessions.size };
|
|
680
729
|
}
|
|
681
|
-
var
|
|
730
|
+
var CLAUDE_PROJECTS_DIR, TAKUMI_PROJECTS_DIR;
|
|
682
731
|
var init_claude = __esm(() => {
|
|
683
732
|
init_database();
|
|
684
733
|
init_pricing();
|
|
685
|
-
|
|
734
|
+
CLAUDE_PROJECTS_DIR = join4(homedir2(), ".claude", "projects");
|
|
735
|
+
TAKUMI_PROJECTS_DIR = join4(homedir2(), ".takumi", "projects");
|
|
686
736
|
});
|
|
687
737
|
|
|
688
738
|
// src/ingest/codex.ts
|
|
@@ -696,6 +746,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
696
746
|
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
697
747
|
return { sessions: 0 };
|
|
698
748
|
}
|
|
749
|
+
const machineId = getMachineId();
|
|
699
750
|
let codexDb = null;
|
|
700
751
|
let ingested = 0;
|
|
701
752
|
try {
|
|
@@ -720,7 +771,8 @@ async function ingestCodex(db, verbose = false) {
|
|
|
720
771
|
ended_at: endedAt,
|
|
721
772
|
total_cost_usd: costUsd,
|
|
722
773
|
total_tokens: thread.tokens_used,
|
|
723
|
-
request_count: 1
|
|
774
|
+
request_count: 1,
|
|
775
|
+
machine_id: machineId
|
|
724
776
|
});
|
|
725
777
|
setIngestState(db, "codex", stateKey, "done");
|
|
726
778
|
ingested++;
|
|
@@ -749,6 +801,7 @@ async function ingestGemini(db, verbose) {
|
|
|
749
801
|
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
750
802
|
return { sessions: 0 };
|
|
751
803
|
}
|
|
804
|
+
const machineId = getMachineId();
|
|
752
805
|
let totalSessions = 0;
|
|
753
806
|
const touchedSessions = new Set;
|
|
754
807
|
let projectHashDirs = [];
|
|
@@ -799,7 +852,8 @@ async function ingestGemini(db, verbose) {
|
|
|
799
852
|
ended_at: chatData.lastUpdated ?? null,
|
|
800
853
|
total_cost_usd: 0,
|
|
801
854
|
total_tokens: 0,
|
|
802
|
-
request_count: 0
|
|
855
|
+
request_count: 0,
|
|
856
|
+
machine_id: machineId
|
|
803
857
|
};
|
|
804
858
|
upsertSession(db, session);
|
|
805
859
|
touchedSessions.add(sessionId);
|
|
@@ -1072,7 +1126,11 @@ function createHandler(db) {
|
|
|
1072
1126
|
return ok({ status: "ok", ts: new Date().toISOString() });
|
|
1073
1127
|
if (path === "/api/summary" && method === "GET") {
|
|
1074
1128
|
const period = url.searchParams.get("period") ?? "today";
|
|
1075
|
-
|
|
1129
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
1130
|
+
return ok(querySummary(db, period, machine));
|
|
1131
|
+
}
|
|
1132
|
+
if (path === "/api/machines" && method === "GET") {
|
|
1133
|
+
return ok(listMachines(db), { current_machine: getMachineId() });
|
|
1076
1134
|
}
|
|
1077
1135
|
if (path === "/api/daily" && method === "GET") {
|
|
1078
1136
|
const days = Number(url.searchParams.get("days") ?? 30);
|
|
@@ -1082,6 +1140,7 @@ function createHandler(db) {
|
|
|
1082
1140
|
const agent = url.searchParams.get("agent");
|
|
1083
1141
|
const project = url.searchParams.get("project") ?? undefined;
|
|
1084
1142
|
const search = url.searchParams.get("search") ?? undefined;
|
|
1143
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
1085
1144
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
1086
1145
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
1087
1146
|
const since = url.searchParams.get("since") ?? undefined;
|
|
@@ -1091,6 +1150,7 @@ function createHandler(db) {
|
|
|
1091
1150
|
agent: agent ?? undefined,
|
|
1092
1151
|
project,
|
|
1093
1152
|
search,
|
|
1153
|
+
machine,
|
|
1094
1154
|
limit,
|
|
1095
1155
|
offset,
|
|
1096
1156
|
since
|
|
@@ -1183,6 +1243,8 @@ function createHandler(db) {
|
|
|
1183
1243
|
const results = {};
|
|
1184
1244
|
if (sources === "all" || sources === "claude")
|
|
1185
1245
|
results["claude"] = await ingestClaude(db);
|
|
1246
|
+
if (sources === "all" || sources === "takumi")
|
|
1247
|
+
results["takumi"] = await ingestTakumi(db);
|
|
1186
1248
|
if (sources === "all" || sources === "codex")
|
|
1187
1249
|
results["codex"] = await ingestCodex(db);
|
|
1188
1250
|
if (sources === "all" || sources === "gemini")
|
|
@@ -1397,6 +1459,102 @@ function menubarStop() {
|
|
|
1397
1459
|
var APP_PATH = "/Applications/Economy Bar.app", REPO = "hasna/open-economy";
|
|
1398
1460
|
var init_menubar = () => {};
|
|
1399
1461
|
|
|
1462
|
+
// src/db/pg-migrations.ts
|
|
1463
|
+
var exports_pg_migrations = {};
|
|
1464
|
+
__export(exports_pg_migrations, {
|
|
1465
|
+
PG_MIGRATIONS: () => PG_MIGRATIONS
|
|
1466
|
+
});
|
|
1467
|
+
var PG_MIGRATIONS;
|
|
1468
|
+
var init_pg_migrations = __esm(() => {
|
|
1469
|
+
PG_MIGRATIONS = [
|
|
1470
|
+
`CREATE TABLE IF NOT EXISTS requests (
|
|
1471
|
+
id TEXT PRIMARY KEY,
|
|
1472
|
+
agent TEXT NOT NULL,
|
|
1473
|
+
session_id TEXT NOT NULL,
|
|
1474
|
+
model TEXT NOT NULL,
|
|
1475
|
+
input_tokens INTEGER DEFAULT 0,
|
|
1476
|
+
output_tokens INTEGER DEFAULT 0,
|
|
1477
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
1478
|
+
cache_create_tokens INTEGER DEFAULT 0,
|
|
1479
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
1480
|
+
duration_ms INTEGER DEFAULT 0,
|
|
1481
|
+
timestamp TEXT NOT NULL,
|
|
1482
|
+
source_request_id TEXT,
|
|
1483
|
+
machine_id TEXT DEFAULT ''
|
|
1484
|
+
)`,
|
|
1485
|
+
`CREATE TABLE IF NOT EXISTS sessions (
|
|
1486
|
+
id TEXT PRIMARY KEY,
|
|
1487
|
+
agent TEXT NOT NULL,
|
|
1488
|
+
project_path TEXT DEFAULT '',
|
|
1489
|
+
project_name TEXT DEFAULT '',
|
|
1490
|
+
started_at TEXT NOT NULL,
|
|
1491
|
+
ended_at TEXT,
|
|
1492
|
+
total_cost_usd REAL DEFAULT 0,
|
|
1493
|
+
total_tokens INTEGER DEFAULT 0,
|
|
1494
|
+
request_count INTEGER DEFAULT 0,
|
|
1495
|
+
machine_id TEXT DEFAULT ''
|
|
1496
|
+
)`,
|
|
1497
|
+
`CREATE TABLE IF NOT EXISTS projects (
|
|
1498
|
+
id TEXT PRIMARY KEY,
|
|
1499
|
+
path TEXT UNIQUE NOT NULL,
|
|
1500
|
+
name TEXT NOT NULL,
|
|
1501
|
+
description TEXT,
|
|
1502
|
+
tags TEXT DEFAULT '[]',
|
|
1503
|
+
created_at TEXT NOT NULL
|
|
1504
|
+
)`,
|
|
1505
|
+
`CREATE TABLE IF NOT EXISTS budgets (
|
|
1506
|
+
id TEXT PRIMARY KEY,
|
|
1507
|
+
project_path TEXT,
|
|
1508
|
+
agent TEXT,
|
|
1509
|
+
period TEXT NOT NULL,
|
|
1510
|
+
limit_usd REAL NOT NULL,
|
|
1511
|
+
alert_at_percent INTEGER DEFAULT 80,
|
|
1512
|
+
created_at TEXT NOT NULL,
|
|
1513
|
+
updated_at TEXT NOT NULL
|
|
1514
|
+
)`,
|
|
1515
|
+
`CREATE TABLE IF NOT EXISTS goals (
|
|
1516
|
+
id TEXT PRIMARY KEY,
|
|
1517
|
+
period TEXT NOT NULL,
|
|
1518
|
+
project_path TEXT,
|
|
1519
|
+
agent TEXT,
|
|
1520
|
+
limit_usd REAL NOT NULL,
|
|
1521
|
+
created_at TEXT NOT NULL,
|
|
1522
|
+
updated_at TEXT NOT NULL
|
|
1523
|
+
)`,
|
|
1524
|
+
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
1525
|
+
source TEXT NOT NULL,
|
|
1526
|
+
key TEXT NOT NULL,
|
|
1527
|
+
value TEXT NOT NULL,
|
|
1528
|
+
PRIMARY KEY (source, key)
|
|
1529
|
+
)`,
|
|
1530
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
1531
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
1532
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
1533
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
1534
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
1535
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
1536
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
1537
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
1538
|
+
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
1539
|
+
model TEXT PRIMARY KEY,
|
|
1540
|
+
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
1541
|
+
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
1542
|
+
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
1543
|
+
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
1544
|
+
updated_at TEXT NOT NULL
|
|
1545
|
+
)`,
|
|
1546
|
+
`CREATE TABLE IF NOT EXISTS feedback (
|
|
1547
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
1548
|
+
message TEXT NOT NULL,
|
|
1549
|
+
email TEXT,
|
|
1550
|
+
category TEXT DEFAULT 'general',
|
|
1551
|
+
version TEXT,
|
|
1552
|
+
machine_id TEXT,
|
|
1553
|
+
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
1554
|
+
)`
|
|
1555
|
+
];
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1400
1558
|
// src/cli/index.ts
|
|
1401
1559
|
import { Command } from "commander";
|
|
1402
1560
|
import chalk4 from "chalk";
|
|
@@ -1783,6 +1941,7 @@ async function autoSync() {
|
|
|
1783
1941
|
const db = openDatabase();
|
|
1784
1942
|
ensurePricingSeeded(db);
|
|
1785
1943
|
await ingestClaude(db);
|
|
1944
|
+
await ingestTakumi(db);
|
|
1786
1945
|
await ingestCodex(db);
|
|
1787
1946
|
await ingestGemini(db);
|
|
1788
1947
|
}
|
|
@@ -1895,7 +2054,7 @@ program.action(async () => {
|
|
|
1895
2054
|
}
|
|
1896
2055
|
console.log();
|
|
1897
2056
|
});
|
|
1898
|
-
program.command("sync").description("Ingest cost data from Claude Code, Codex, and Gemini").option("--claude", "Only ingest Claude Code telemetry").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").action(async (opts) => {
|
|
2057
|
+
program.command("sync").description("Ingest cost data from Claude Code, Codex, and Gemini").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").action(async (opts) => {
|
|
1899
2058
|
const db = openDatabase();
|
|
1900
2059
|
ensurePricingSeeded(db);
|
|
1901
2060
|
if (opts.force) {
|
|
@@ -1903,8 +2062,9 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
|
|
|
1903
2062
|
if (opts.verbose)
|
|
1904
2063
|
console.log(chalk4.dim("Cleared ingest cache"));
|
|
1905
2064
|
}
|
|
1906
|
-
const anySpecific = opts.claude || opts.codex || opts.gemini;
|
|
2065
|
+
const anySpecific = opts.claude || opts.takumi || opts.codex || opts.gemini;
|
|
1907
2066
|
const doClaude = opts.claude || !anySpecific;
|
|
2067
|
+
const doTakumi = opts.takumi || !anySpecific;
|
|
1908
2068
|
const doCodex = opts.codex || !anySpecific;
|
|
1909
2069
|
const doGemini = opts.gemini || !anySpecific;
|
|
1910
2070
|
if (doClaude) {
|
|
@@ -1912,6 +2072,11 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
|
|
|
1912
2072
|
const r = await ingestClaude(db, opts.verbose);
|
|
1913
2073
|
console.log(chalk4.green(`\u2713 ${r.files} files, ${r.requests} requests, ${r.sessions} sessions`));
|
|
1914
2074
|
}
|
|
2075
|
+
if (doTakumi) {
|
|
2076
|
+
process.stdout.write(chalk4.cyan("\u2192 Ingesting Takumi sessions... "));
|
|
2077
|
+
const r = await ingestTakumi(db, opts.verbose);
|
|
2078
|
+
console.log(chalk4.green(`\u2713 ${r.files} files, ${r.requests} requests, ${r.sessions} sessions`));
|
|
2079
|
+
}
|
|
1915
2080
|
if (doCodex) {
|
|
1916
2081
|
process.stdout.write(chalk4.cyan("\u2192 Ingesting Codex sessions... "));
|
|
1917
2082
|
const r = await ingestCodex(db, opts.verbose);
|
|
@@ -1922,6 +2087,12 @@ program.command("sync").description("Ingest cost data from Claude Code, Codex, a
|
|
|
1922
2087
|
const r = await ingestGemini(db, opts.verbose);
|
|
1923
2088
|
console.log(chalk4.green(`\u2713 ${r.sessions} sessions`));
|
|
1924
2089
|
}
|
|
2090
|
+
if (opts.backfillMachine) {
|
|
2091
|
+
const machine = getMachineId();
|
|
2092
|
+
const reqCount = db.prepare(`UPDATE requests SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
|
|
2093
|
+
const sessCount = db.prepare(`UPDATE sessions SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
|
|
2094
|
+
console.log(chalk4.cyan(`\u2192 Backfilled machine_id='${machine}': ${reqCount.changes} requests, ${sessCount.changes} sessions`));
|
|
2095
|
+
}
|
|
1925
2096
|
try {
|
|
1926
2097
|
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
1927
2098
|
await checkAndFireWebhooks2(db);
|
|
@@ -1941,13 +2112,14 @@ program.command("month").description("Cost summary for this month").action(async
|
|
|
1941
2112
|
await autoSync();
|
|
1942
2113
|
printSummary("This Month", "month");
|
|
1943
2114
|
});
|
|
1944
|
-
program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", "Filter by agent (claude|codex)").option("--project <path>", "Filter by project path").option("--limit <n>", "Number of sessions", "20").option("--format <fmt>", "Output format: table|compact|csv|json", "table").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--search <query>", "Search by project name, session id prefix, or agent").action(async (opts) => {
|
|
2115
|
+
program.command("sessions").description("List coding sessions with costs").option("--agent <agent>", "Filter by agent (claude|codex)").option("--project <path>", "Filter by project path").option("--machine <id>", "Filter by machine hostname (e.g. spark01, apple01)").option("--limit <n>", "Number of sessions", "20").option("--format <fmt>", "Output format: table|compact|csv|json", "table").option("--since <date>", "Filter sessions since date or relative (e.g. 2026-03-01, 7d, 30d)").option("--search <query>", "Search by project name, session id prefix, or agent").action(async (opts) => {
|
|
1945
2116
|
await autoSync();
|
|
1946
2117
|
const db = openDatabase();
|
|
1947
2118
|
const sinceDate = opts.since ? parseSinceDate(opts.since) : undefined;
|
|
1948
2119
|
let sessions = querySessions(db, {
|
|
1949
2120
|
agent: opts.agent,
|
|
1950
2121
|
project: opts.project,
|
|
2122
|
+
machine: opts.machine,
|
|
1951
2123
|
limit: Number(opts.limit ?? 20),
|
|
1952
2124
|
since: sinceDate,
|
|
1953
2125
|
search: opts.search
|
|
@@ -2396,6 +2568,29 @@ program.command("session <id>").description("Show detailed breakdown of a single
|
|
|
2396
2568
|
}
|
|
2397
2569
|
console.log();
|
|
2398
2570
|
});
|
|
2571
|
+
program.command("machines").description("List all machines that have synced data").action(async () => {
|
|
2572
|
+
await autoSync();
|
|
2573
|
+
const db = openDatabase();
|
|
2574
|
+
const machines = listMachines(db);
|
|
2575
|
+
const current = getMachineId();
|
|
2576
|
+
if (machines.length === 0) {
|
|
2577
|
+
console.log(chalk4.yellow(`No machine data yet. Current machine: ${current}`));
|
|
2578
|
+
return;
|
|
2579
|
+
}
|
|
2580
|
+
console.log();
|
|
2581
|
+
console.log(chalk4.bold.cyan(" Machines"));
|
|
2582
|
+
console.log();
|
|
2583
|
+
printTable(["Machine", "Sessions", "Requests", "Cost", "Last Active"], machines.map((m) => [
|
|
2584
|
+
m.machine_id === current ? chalk4.green(`${m.machine_id} (this)`) : chalk4.white(m.machine_id),
|
|
2585
|
+
fmtCount(m.sessions),
|
|
2586
|
+
fmtCount(m.requests),
|
|
2587
|
+
fmt2(m.total_cost_usd),
|
|
2588
|
+
chalk4.dim(m.last_active?.substring(0, 16) ?? "\u2014")
|
|
2589
|
+
]));
|
|
2590
|
+
console.log(`
|
|
2591
|
+
${chalk4.dim("Current machine:")} ${chalk4.bold(current)}`);
|
|
2592
|
+
console.log();
|
|
2593
|
+
});
|
|
2399
2594
|
program.command("export").description("Export data as CSV").option("--type <type>", "Data type: sessions or requests", "sessions").option("--period <period>", "Period: today|week|month|all", "month").option("--output <file>", "Output file path (default: stdout)").action(async (opts) => {
|
|
2400
2595
|
await autoSync();
|
|
2401
2596
|
const db = openDatabase();
|
|
@@ -2663,5 +2858,145 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
|
|
|
2663
2858
|
process.exit(1);
|
|
2664
2859
|
}
|
|
2665
2860
|
});
|
|
2861
|
+
var cloudCmd = program.command("cloud").description("Cross-machine sync via cloud PostgreSQL");
|
|
2862
|
+
cloudCmd.command("push").description("Push local economy data to cloud PostgreSQL").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
|
|
2863
|
+
const { syncPush, PgAdapterAsync, getCloudConfig, SqliteAdapter } = await import("@hasna/cloud");
|
|
2864
|
+
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
2865
|
+
const config = getCloudConfig();
|
|
2866
|
+
if (!config.rds?.host) {
|
|
2867
|
+
console.error(chalk4.red("Cloud not configured. Set RDS host in ~/.hasna/cloud.json"));
|
|
2868
|
+
process.exit(1);
|
|
2869
|
+
}
|
|
2870
|
+
const connStr = `postgresql://${config.rds.username}:${process.env[config.rds.password_env] ?? ""}@${config.rds.host}:${config.rds.port ?? 5432}/economy?sslmode=require`;
|
|
2871
|
+
const local = new SqliteAdapter(getDbPath());
|
|
2872
|
+
const cloud = new PgAdapterAsync(connStr);
|
|
2873
|
+
process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
|
|
2874
|
+
for (const sql of PG_MIGRATIONS2) {
|
|
2875
|
+
await cloud.run(sql);
|
|
2876
|
+
}
|
|
2877
|
+
console.log(chalk4.green("\u2713"));
|
|
2878
|
+
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : ["requests", "sessions", "projects", "budgets", "goals", "model_pricing"];
|
|
2879
|
+
process.stdout.write(chalk4.cyan(`\u2192 Pushing ${tableList.join(", ")}... `));
|
|
2880
|
+
const results = await syncPush(local, cloud, { tables: tableList });
|
|
2881
|
+
const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2882
|
+
console.log(chalk4.green(`\u2713 ${totalRows} rows across ${tableList.length} tables`));
|
|
2883
|
+
local.close();
|
|
2884
|
+
await cloud.close();
|
|
2885
|
+
console.log(chalk4.bold.green(`
|
|
2886
|
+
\u2713 Push complete from ${getMachineId()}`));
|
|
2887
|
+
});
|
|
2888
|
+
cloudCmd.command("pull").description("Pull cloud PostgreSQL data to local").option("--tables <tables>", "Comma-separated table names (default: all)").action(async (opts) => {
|
|
2889
|
+
const { syncPull, PgAdapterAsync, getCloudConfig, SqliteAdapter } = await import("@hasna/cloud");
|
|
2890
|
+
const { PG_MIGRATIONS: PG_MIGRATIONS2 } = await Promise.resolve().then(() => (init_pg_migrations(), exports_pg_migrations));
|
|
2891
|
+
const config = getCloudConfig();
|
|
2892
|
+
if (!config.rds?.host) {
|
|
2893
|
+
console.error(chalk4.red("Cloud not configured. Set RDS host in ~/.hasna/cloud.json"));
|
|
2894
|
+
process.exit(1);
|
|
2895
|
+
}
|
|
2896
|
+
const connStr = `postgresql://${config.rds.username}:${process.env[config.rds.password_env] ?? ""}@${config.rds.host}:${config.rds.port ?? 5432}/economy?sslmode=require`;
|
|
2897
|
+
const local = new SqliteAdapter(getDbPath());
|
|
2898
|
+
const cloud = new PgAdapterAsync(connStr);
|
|
2899
|
+
process.stdout.write(chalk4.cyan("\u2192 Running PG migrations... "));
|
|
2900
|
+
for (const sql of PG_MIGRATIONS2) {
|
|
2901
|
+
await cloud.run(sql);
|
|
2902
|
+
}
|
|
2903
|
+
console.log(chalk4.green("\u2713"));
|
|
2904
|
+
const tableList = opts.tables ? opts.tables.split(",").map((t) => t.trim()) : ["requests", "sessions", "projects", "budgets", "goals", "model_pricing"];
|
|
2905
|
+
process.stdout.write(chalk4.cyan(`\u2192 Pulling ${tableList.join(", ")}... `));
|
|
2906
|
+
const results = await syncPull(cloud, local, { tables: tableList });
|
|
2907
|
+
const totalRows = results.reduce((s, r) => s + r.rowsWritten, 0);
|
|
2908
|
+
console.log(chalk4.green(`\u2713 ${totalRows} rows across ${tableList.length} tables`));
|
|
2909
|
+
local.close();
|
|
2910
|
+
await cloud.close();
|
|
2911
|
+
console.log(chalk4.bold.green(`
|
|
2912
|
+
\u2713 Pull complete to ${getMachineId()}`));
|
|
2913
|
+
});
|
|
2914
|
+
cloudCmd.command("sync").description("Full sync: ingest local, then merge data from all reachable machines via SSH").option("--machines <list>", "Comma-separated machine hostnames (default: spark01,apple01,apple03)").action(async (opts) => {
|
|
2915
|
+
const thisMachine = getMachineId();
|
|
2916
|
+
console.log(chalk4.bold.cyan(` Cloud Sync \u2014 ${thisMachine}
|
|
2917
|
+
`));
|
|
2918
|
+
process.stdout.write(chalk4.cyan("\u2192 Ingesting local data... "));
|
|
2919
|
+
await autoSync();
|
|
2920
|
+
console.log(chalk4.green("\u2713"));
|
|
2921
|
+
const allMachines = (opts.machines ?? "spark01,apple01,apple03").split(",").map((m) => m.trim());
|
|
2922
|
+
const remoteMachines = allMachines.filter((m) => m !== thisMachine);
|
|
2923
|
+
const db = openDatabase();
|
|
2924
|
+
const { existsSync: existsSync8, mkdirSync: mkdirSync4, unlinkSync } = await import("fs");
|
|
2925
|
+
const { join: join9 } = await import("path");
|
|
2926
|
+
const { execSync: exec } = await import("child_process");
|
|
2927
|
+
const tmpDir = join9(process.env["TMPDIR"] ?? "/tmp", "economy-sync");
|
|
2928
|
+
mkdirSync4(tmpDir, { recursive: true });
|
|
2929
|
+
const isLinux = process.platform === "linux";
|
|
2930
|
+
const remoteDbPath = isLinux ? ".hasna/economy/economy.db" : ".hasna/economy/economy.db";
|
|
2931
|
+
for (const machine of remoteMachines) {
|
|
2932
|
+
const localCopy = join9(tmpDir, `${machine}.db`);
|
|
2933
|
+
process.stdout.write(chalk4.cyan(`\u2192 Fetching from ${machine}... `));
|
|
2934
|
+
try {
|
|
2935
|
+
exec(`scp -o ConnectTimeout=5 -o StrictHostKeyChecking=no ${machine}:~/${remoteDbPath} "${localCopy}" 2>/dev/null`, { timeout: 30000 });
|
|
2936
|
+
if (!existsSync8(localCopy)) {
|
|
2937
|
+
console.log(chalk4.yellow("skipped (no file)"));
|
|
2938
|
+
continue;
|
|
2939
|
+
}
|
|
2940
|
+
const { SqliteAdapter } = await import("@hasna/cloud");
|
|
2941
|
+
const remoteDb = new SqliteAdapter(localCopy);
|
|
2942
|
+
remoteDb.exec("PRAGMA busy_timeout = 5000");
|
|
2943
|
+
const sessions = remoteDb.prepare("SELECT * FROM sessions").all();
|
|
2944
|
+
let sCount = 0;
|
|
2945
|
+
for (const s of sessions) {
|
|
2946
|
+
const existing = db.prepare("SELECT id FROM sessions WHERE id = ?").get(s["id"]);
|
|
2947
|
+
if (!existing) {
|
|
2948
|
+
db.prepare(`INSERT OR IGNORE INTO sessions (id, agent, project_path, project_name, started_at, ended_at, total_cost_usd, total_tokens, request_count, machine_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(s["id"], s["agent"], s["project_path"], s["project_name"], s["started_at"], s["ended_at"], s["total_cost_usd"], s["total_tokens"], s["request_count"], s["machine_id"] || machine);
|
|
2949
|
+
sCount++;
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
let rCount = 0;
|
|
2953
|
+
try {
|
|
2954
|
+
const cols = remoteDb.prepare("PRAGMA table_info(requests)").all();
|
|
2955
|
+
const hasMachineCol = cols.some((c) => c.name === "machine_id");
|
|
2956
|
+
const requests = remoteDb.prepare("SELECT * FROM requests").all();
|
|
2957
|
+
for (const r of requests) {
|
|
2958
|
+
const existing = db.prepare("SELECT id FROM requests WHERE id = ?").get(r["id"]);
|
|
2959
|
+
if (!existing) {
|
|
2960
|
+
db.prepare(`INSERT OR IGNORE INTO requests (id, agent, session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cost_usd, duration_ms, timestamp, source_request_id, machine_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(r["id"], r["agent"], r["session_id"], r["model"], r["input_tokens"], r["output_tokens"], r["cache_read_tokens"], r["cache_create_tokens"], r["cost_usd"], r["duration_ms"], r["timestamp"], r["source_request_id"], hasMachineCol ? r["machine_id"] || machine : machine);
|
|
2961
|
+
rCount++;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
} catch {}
|
|
2965
|
+
remoteDb.close();
|
|
2966
|
+
try {
|
|
2967
|
+
unlinkSync(localCopy);
|
|
2968
|
+
} catch {}
|
|
2969
|
+
console.log(chalk4.green(`\u2713 ${sCount} sessions, ${rCount} requests`));
|
|
2970
|
+
} catch (e) {
|
|
2971
|
+
console.log(chalk4.yellow(`skipped (${e instanceof Error ? e.message.split(`
|
|
2972
|
+
`)[0] : "unreachable"})`));
|
|
2973
|
+
try {
|
|
2974
|
+
unlinkSync(localCopy);
|
|
2975
|
+
} catch {}
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
console.log(chalk4.bold.green(`
|
|
2979
|
+
\u2713 Cloud sync complete`));
|
|
2980
|
+
});
|
|
2981
|
+
cloudCmd.command("status").description("Check cloud connection status").action(async () => {
|
|
2982
|
+
const { PgAdapterAsync, getCloudConfig } = await import("@hasna/cloud");
|
|
2983
|
+
const config = getCloudConfig();
|
|
2984
|
+
console.log();
|
|
2985
|
+
console.log(` Mode: ${chalk4.white(config.mode)}`);
|
|
2986
|
+
console.log(` Machine: ${chalk4.white(getMachineId())}`);
|
|
2987
|
+
console.log(` RDS Host: ${chalk4.white(config.rds?.host || "(not configured)")}`);
|
|
2988
|
+
if (config.rds?.host && config.rds?.username) {
|
|
2989
|
+
try {
|
|
2990
|
+
const connStr = `postgresql://${config.rds.username}:${process.env[config.rds.password_env] ?? ""}@${config.rds.host}:${config.rds.port ?? 5432}/economy?sslmode=require`;
|
|
2991
|
+
const pg = new PgAdapterAsync(connStr);
|
|
2992
|
+
await pg.get("SELECT 1 as ok");
|
|
2993
|
+
console.log(` PostgreSQL: ${chalk4.green("connected")}`);
|
|
2994
|
+
await pg.close();
|
|
2995
|
+
} catch (err2) {
|
|
2996
|
+
console.log(` PostgreSQL: ${chalk4.red(`failed \u2014 ${err2 instanceof Error ? err2.message : String(err2)}`)}`);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
console.log();
|
|
3000
|
+
});
|
|
2666
3001
|
registerBrainsCommand(program);
|
|
2667
3002
|
program.parse();
|