@hasna/economy 0.3.7 → 0.3.8
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/CHANGELOG.md +0 -4
- package/README.md +28 -61
- package/dashboard/README.md +17 -64
- package/dist/cli/index.js +221 -114
- package/dist/db/cloud.d.ts +3 -4
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +2 -2
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/dialect.d.ts +13 -0
- package/dist/db/dialect.d.ts.map +1 -0
- package/dist/db/pg-migrate.d.ts +30 -0
- package/dist/db/pg-migrate.d.ts.map +1 -0
- package/dist/db/sqlite-adapter.d.ts +36 -0
- package/dist/db/sqlite-adapter.d.ts.map +1 -0
- package/dist/db/sync-pg.d.ts +1 -1
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/index.js +129 -56
- package/dist/ingest/billing.d.ts +1 -1
- package/dist/ingest/billing.d.ts.map +1 -1
- package/dist/ingest/claude-quota.d.ts +1 -1
- package/dist/ingest/claude-quota.d.ts.map +1 -1
- package/dist/ingest/claude.d.ts +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex-quota.d.ts +1 -1
- package/dist/ingest/codex-quota.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/cursor.d.ts +1 -1
- package/dist/ingest/cursor.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +1 -1
- package/dist/ingest/hermes.d.ts.map +1 -1
- package/dist/ingest/loops.d.ts +1 -1
- package/dist/ingest/loops.d.ts.map +1 -1
- package/dist/ingest/opencode.d.ts +1 -1
- package/dist/ingest/opencode.d.ts.map +1 -1
- package/dist/ingest/otel.d.ts +1 -1
- package/dist/ingest/otel.d.ts.map +1 -1
- package/dist/ingest/pi.d.ts +1 -1
- package/dist/ingest/pi.d.ts.map +1 -1
- package/dist/ingest/plugin.d.ts +1 -1
- package/dist/ingest/plugin.d.ts.map +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/analytics.d.ts +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/billing-diff.d.ts +1 -1
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/brief.d.ts +1 -1
- package/dist/lib/brief.d.ts.map +1 -1
- package/dist/lib/open-projects.d.ts +10 -2
- package/dist/lib/open-projects.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +1 -1
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/savings.d.ts +1 -1
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/lib/spikes.d.ts +1 -1
- package/dist/lib/spikes.d.ts.map +1 -1
- package/dist/lib/sync-all.d.ts +1 -1
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/webhooks.d.ts +1 -1
- package/dist/lib/webhooks.d.ts.map +1 -1
- package/dist/mcp/index.js +144 -94
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/otel/index.js +69 -4
- package/dist/server/index.js +248 -88
- package/dist/server/serve.d.ts +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/docs/README.md +12 -0
- package/docs/cli.md +88 -0
- package/docs/configuration.md +86 -0
- package/docs/ingestion.md +47 -0
- package/docs/mcp.md +65 -0
- package/docs/otel.md +53 -0
- package/docs/rest-api.md +89 -0
- package/package.json +4 -5
package/dist/server/index.js
CHANGED
|
@@ -17,6 +17,62 @@ 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/sqlite-adapter.ts
|
|
21
|
+
import { Database } from "bun:sqlite";
|
|
22
|
+
|
|
23
|
+
class SqliteAdapter {
|
|
24
|
+
db;
|
|
25
|
+
constructor(path) {
|
|
26
|
+
this.db = new Database(path, { create: true });
|
|
27
|
+
this.db.exec("PRAGMA journal_mode=WAL");
|
|
28
|
+
this.db.exec("PRAGMA foreign_keys=ON");
|
|
29
|
+
}
|
|
30
|
+
run(sql, ...params) {
|
|
31
|
+
const result = this.db.prepare(sql).run(...params);
|
|
32
|
+
return { changes: result.changes, lastInsertRowid: result.lastInsertRowid };
|
|
33
|
+
}
|
|
34
|
+
get(sql, ...params) {
|
|
35
|
+
return this.db.prepare(sql).get(...params);
|
|
36
|
+
}
|
|
37
|
+
all(sql, ...params) {
|
|
38
|
+
return this.db.prepare(sql).all(...params);
|
|
39
|
+
}
|
|
40
|
+
exec(sql) {
|
|
41
|
+
this.db.exec(sql);
|
|
42
|
+
}
|
|
43
|
+
query(sql) {
|
|
44
|
+
return this.db.query(sql);
|
|
45
|
+
}
|
|
46
|
+
prepare(sql) {
|
|
47
|
+
const stmt = this.db.prepare(sql);
|
|
48
|
+
return {
|
|
49
|
+
run(...params) {
|
|
50
|
+
const r = stmt.run(...params);
|
|
51
|
+
return { changes: r.changes, lastInsertRowid: r.lastInsertRowid };
|
|
52
|
+
},
|
|
53
|
+
get(...params) {
|
|
54
|
+
return stmt.get(...params);
|
|
55
|
+
},
|
|
56
|
+
all(...params) {
|
|
57
|
+
return stmt.all(...params);
|
|
58
|
+
},
|
|
59
|
+
finalize() {
|
|
60
|
+
stmt.finalize();
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
close() {
|
|
65
|
+
this.db.close();
|
|
66
|
+
}
|
|
67
|
+
transaction(fn) {
|
|
68
|
+
return this.db.transaction(fn)();
|
|
69
|
+
}
|
|
70
|
+
get raw() {
|
|
71
|
+
return this.db;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
var init_sqlite_adapter = () => {};
|
|
75
|
+
|
|
20
76
|
// src/lib/pricing.ts
|
|
21
77
|
var exports_pricing = {};
|
|
22
78
|
__export(exports_pricing, {
|
|
@@ -535,7 +591,6 @@ var init_pricing = __esm(() => {
|
|
|
535
591
|
});
|
|
536
592
|
|
|
537
593
|
// src/db/database.ts
|
|
538
|
-
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
539
594
|
import { execFileSync } from "child_process";
|
|
540
595
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
541
596
|
import { hostname, platform } from "os";
|
|
@@ -628,7 +683,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
628
683
|
mkdirSync(dir, { recursive: true });
|
|
629
684
|
}
|
|
630
685
|
const db = withSqliteBusyRetry(() => {
|
|
631
|
-
const opened = new
|
|
686
|
+
const opened = new SqliteAdapter(path);
|
|
632
687
|
try {
|
|
633
688
|
opened.exec("PRAGMA busy_timeout = 10000");
|
|
634
689
|
opened.exec("PRAGMA journal_mode = WAL");
|
|
@@ -1216,9 +1271,12 @@ function labelForPath(projectPath, projectName) {
|
|
|
1216
1271
|
function groupKeyForPath(projectPath, projectName) {
|
|
1217
1272
|
return labelForPath(projectPath, projectName).trim().toLowerCase();
|
|
1218
1273
|
}
|
|
1219
|
-
function
|
|
1220
|
-
|
|
1221
|
-
|
|
1274
|
+
function laterTimestamp(current, candidate) {
|
|
1275
|
+
if (!candidate)
|
|
1276
|
+
return current;
|
|
1277
|
+
return candidate > current ? candidate : current;
|
|
1278
|
+
}
|
|
1279
|
+
function queryProjectBreakdownScoped(db, requestWhere, sessionWhere, requestScopeParams = [], sessionScopeParams = [], machine) {
|
|
1222
1280
|
const sessionMachineClause = machine ? " AND (machine_id = ? OR id IN (SELECT DISTINCT session_id FROM requests WHERE machine_id = ?))" : "";
|
|
1223
1281
|
const requestMachineClause = machine ? " AND machine_id = ?" : "";
|
|
1224
1282
|
const sessionMachineParams = machine ? [machine, machine] : [];
|
|
@@ -1226,67 +1284,89 @@ function queryProjectBreakdown(db, period = "all", machine) {
|
|
|
1226
1284
|
const sessionOnlyMachineClause = machine ? " AND machine_id = ?" : "";
|
|
1227
1285
|
const sessionOnlyMachineParams = machine ? [machine] : [];
|
|
1228
1286
|
const sessions = db.prepare(`
|
|
1229
|
-
SELECT id, project_path, project_name
|
|
1287
|
+
SELECT id, project_path, project_name
|
|
1230
1288
|
FROM sessions
|
|
1231
1289
|
WHERE (project_path != '' OR project_name != '')${sessionMachineClause}
|
|
1232
1290
|
`).all(...sessionMachineParams);
|
|
1233
1291
|
const groups = new Map;
|
|
1292
|
+
const groupKeyBySession = new Map;
|
|
1234
1293
|
for (const s of sessions) {
|
|
1235
1294
|
const label = labelForPath(s.project_path, s.project_name);
|
|
1236
1295
|
if (!label)
|
|
1237
1296
|
continue;
|
|
1238
1297
|
const key = groupKeyForPath(s.project_path, s.project_name);
|
|
1239
|
-
const g = groups.get(key) ?? { label,
|
|
1240
|
-
g.sessionIds.push(s.id);
|
|
1298
|
+
const g = groups.get(key) ?? { label, samplePath: s.project_path, sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: "" };
|
|
1241
1299
|
if (!g.samplePath)
|
|
1242
1300
|
g.samplePath = s.project_path;
|
|
1243
1301
|
groups.set(key, g);
|
|
1302
|
+
groupKeyBySession.set(s.id, key);
|
|
1303
|
+
}
|
|
1304
|
+
const groupFor = (sessionId) => {
|
|
1305
|
+
const key = groupKeyBySession.get(sessionId);
|
|
1306
|
+
return key === undefined ? undefined : groups.get(key);
|
|
1307
|
+
};
|
|
1308
|
+
const requestRows = db.prepare(`
|
|
1309
|
+
SELECT session_id,
|
|
1310
|
+
COUNT(*) as requests,
|
|
1311
|
+
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1312
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1313
|
+
MAX(timestamp) as last_active
|
|
1314
|
+
FROM requests
|
|
1315
|
+
WHERE ${requestWhere}${requestMachineClause}
|
|
1316
|
+
GROUP BY session_id
|
|
1317
|
+
`).all(...requestScopeParams, ...requestMachineParams);
|
|
1318
|
+
for (const row of requestRows) {
|
|
1319
|
+
const g = groupFor(row.session_id);
|
|
1320
|
+
if (!g)
|
|
1321
|
+
continue;
|
|
1322
|
+
g.sessions += 1;
|
|
1323
|
+
g.requests += row.requests;
|
|
1324
|
+
g.cost_usd += row.cost_usd;
|
|
1325
|
+
g.total_tokens += row.total_tokens;
|
|
1326
|
+
g.last_active = laterTimestamp(g.last_active, row.last_active);
|
|
1327
|
+
}
|
|
1328
|
+
const sessionsWithRequests = new Set(db.prepare(`SELECT DISTINCT session_id FROM requests`).all().map((row) => row.session_id));
|
|
1329
|
+
const sessionOnlyRows = db.prepare(`
|
|
1330
|
+
SELECT id,
|
|
1331
|
+
COALESCE(request_count, 0) as request_count,
|
|
1332
|
+
COALESCE(total_tokens, 0) as total_tokens,
|
|
1333
|
+
COALESCE(total_cost_usd, 0) as total_cost_usd,
|
|
1334
|
+
started_at
|
|
1335
|
+
FROM sessions
|
|
1336
|
+
WHERE ${sessionWhere}${sessionOnlyMachineClause}
|
|
1337
|
+
`).all(...sessionScopeParams, ...sessionOnlyMachineParams);
|
|
1338
|
+
for (const row of sessionOnlyRows) {
|
|
1339
|
+
if (sessionsWithRequests.has(row.id))
|
|
1340
|
+
continue;
|
|
1341
|
+
const g = groupFor(row.id);
|
|
1342
|
+
if (!g)
|
|
1343
|
+
continue;
|
|
1344
|
+
g.sessions += 1;
|
|
1345
|
+
g.requests += row.request_count;
|
|
1346
|
+
g.total_tokens += row.total_tokens;
|
|
1347
|
+
g.cost_usd += row.total_cost_usd;
|
|
1348
|
+
g.last_active = laterTimestamp(g.last_active, row.started_at);
|
|
1244
1349
|
}
|
|
1245
1350
|
const result = [];
|
|
1246
1351
|
for (const g of groups.values()) {
|
|
1247
|
-
|
|
1248
|
-
const reqStats = placeholders.length ? db.prepare(`
|
|
1249
|
-
SELECT
|
|
1250
|
-
COUNT(DISTINCT session_id) as sessions,
|
|
1251
|
-
COUNT(*) as requests,
|
|
1252
|
-
COALESCE(SUM(cost_usd), 0) as cost_usd,
|
|
1253
|
-
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as total_tokens,
|
|
1254
|
-
MAX(timestamp) as last_active
|
|
1255
|
-
FROM requests
|
|
1256
|
-
WHERE session_id IN (${placeholders})
|
|
1257
|
-
AND ${requestWhere}
|
|
1258
|
-
${requestMachineClause}
|
|
1259
|
-
`).get(...g.sessionIds, ...requestMachineParams) : { sessions: 0, requests: 0, cost_usd: 0, total_tokens: 0, last_active: null };
|
|
1260
|
-
const sessionOnlyStats = placeholders.length ? db.prepare(`
|
|
1261
|
-
SELECT
|
|
1262
|
-
COUNT(*) as sessions,
|
|
1263
|
-
COALESCE(SUM(request_count), 0) as requests,
|
|
1264
|
-
COALESCE(SUM(total_tokens), 0) as total_tokens,
|
|
1265
|
-
COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
1266
|
-
MAX(started_at) as last_active
|
|
1267
|
-
FROM sessions
|
|
1268
|
-
WHERE id IN (${placeholders})
|
|
1269
|
-
AND ${sessionWhere}
|
|
1270
|
-
${sessionOnlyMachineClause}
|
|
1271
|
-
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
1272
|
-
`).get(...g.sessionIds, ...sessionOnlyMachineParams) : { sessions: 0, requests: 0, total_tokens: 0, cost_usd: 0, last_active: null };
|
|
1273
|
-
const totalSessions = reqStats.sessions + sessionOnlyStats.sessions;
|
|
1274
|
-
if (totalSessions === 0)
|
|
1352
|
+
if (g.sessions === 0)
|
|
1275
1353
|
continue;
|
|
1276
|
-
const lastActive = [reqStats.last_active, sessionOnlyStats.last_active].filter(Boolean).sort().at(-1) ?? "";
|
|
1277
1354
|
result.push({
|
|
1278
1355
|
project_path: g.samplePath,
|
|
1279
1356
|
project_name: g.label,
|
|
1280
|
-
sessions:
|
|
1281
|
-
requests:
|
|
1282
|
-
total_tokens:
|
|
1283
|
-
cost_usd:
|
|
1284
|
-
last_active:
|
|
1357
|
+
sessions: g.sessions,
|
|
1358
|
+
requests: g.requests,
|
|
1359
|
+
total_tokens: g.total_tokens,
|
|
1360
|
+
cost_usd: g.cost_usd,
|
|
1361
|
+
last_active: g.last_active
|
|
1285
1362
|
});
|
|
1286
1363
|
}
|
|
1287
1364
|
result.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1288
1365
|
return result;
|
|
1289
1366
|
}
|
|
1367
|
+
function queryProjectBreakdown(db, period = "all", machine) {
|
|
1368
|
+
return queryProjectBreakdownScoped(db, requestPeriodWhere(period), sessionPeriodWhere(period), [], [], machine);
|
|
1369
|
+
}
|
|
1290
1370
|
function normalizeAccountEmail(email) {
|
|
1291
1371
|
return (email ?? "").trim().toLowerCase();
|
|
1292
1372
|
}
|
|
@@ -1560,6 +1640,9 @@ function queryCostCenterBreakdown(db, period = "all", filter = {}) {
|
|
|
1560
1640
|
filtered.sort((a, b) => b.cost_usd - a.cost_usd);
|
|
1561
1641
|
return filtered;
|
|
1562
1642
|
}
|
|
1643
|
+
function queryProjectBreakdownSince(db, since, machine) {
|
|
1644
|
+
return queryProjectBreakdownScoped(db, "timestamp >= ?", "started_at >= ?", [since], [since], machine);
|
|
1645
|
+
}
|
|
1563
1646
|
function queryDailyBreakdown(db, days = 30, machine) {
|
|
1564
1647
|
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
1565
1648
|
const params = machine ? [`-${days}`, machine] : [`-${days}`];
|
|
@@ -2119,14 +2202,52 @@ var numOr = (v, d = 0) => {
|
|
|
2119
2202
|
const num = Number(v);
|
|
2120
2203
|
return Number.isFinite(num) ? num : d;
|
|
2121
2204
|
}, strOr = (v, d = "") => v == null ? d : String(v);
|
|
2122
|
-
var init_database = () => {
|
|
2205
|
+
var init_database = __esm(() => {
|
|
2206
|
+
init_sqlite_adapter();
|
|
2207
|
+
});
|
|
2208
|
+
|
|
2209
|
+
// src/db/dialect.ts
|
|
2210
|
+
function translateSql(sql, dialect) {
|
|
2211
|
+
if (dialect === "sqlite")
|
|
2212
|
+
return sql;
|
|
2213
|
+
return sqliteToPostgres(sql);
|
|
2214
|
+
}
|
|
2215
|
+
function translateParams(params) {
|
|
2216
|
+
const flat = params.length === 1 && Array.isArray(params[0]) ? params[0] : params;
|
|
2217
|
+
return flat.map((p) => p === undefined ? null : p);
|
|
2218
|
+
}
|
|
2219
|
+
function sqliteToPostgres(sql) {
|
|
2220
|
+
let out = sql;
|
|
2221
|
+
let paramIdx = 0;
|
|
2222
|
+
out = out.replace(/\?/g, () => `$${++paramIdx}`);
|
|
2223
|
+
out = out.replace(/datetime\s*\(\s*'now'\s*\)/gi, "NOW()");
|
|
2224
|
+
out = out.replace(/datetime\s*\(\s*'now'\s*,\s*'(-?\d+)\s+(minutes?|hours?|days?|seconds?)'\s*\)/gi, (_match, amount, unit) => {
|
|
2225
|
+
const n = parseInt(amount, 10);
|
|
2226
|
+
const absN = Math.abs(n);
|
|
2227
|
+
const normalizedUnit = unit.toLowerCase().replace(/s$/, "");
|
|
2228
|
+
const plural = absN === 1 ? normalizedUnit : `${normalizedUnit}s`;
|
|
2229
|
+
return n < 0 ? `NOW() - INTERVAL '${absN} ${plural}'` : `NOW() + INTERVAL '${absN} ${plural}'`;
|
|
2230
|
+
});
|
|
2231
|
+
out = out.replace(/lower\s*\(\s*hex\s*\(\s*randomblob\s*\(\s*\d+\s*\)\s*\)\s*\)/gi, "gen_random_uuid()::text");
|
|
2232
|
+
out = out.replace(/GROUP_CONCAT\s*\(/gi, "STRING_AGG(");
|
|
2233
|
+
out = out.replace(/json_extract\s*\(\s*(\w+)\s*,\s*'\$\.(\w+)'\s*\)/gi, (_match, col, key) => `${col}->>'${key}'`);
|
|
2234
|
+
out = out.replace(/\bAUTOINCREMENT\b/gi, "GENERATED ALWAYS AS IDENTITY");
|
|
2235
|
+
out = out.replace(/(?<![I])LIKE/gi, "ILIKE");
|
|
2236
|
+
out = out.replace(/\bIFNULL\s*\(/gi, "COALESCE(");
|
|
2237
|
+
out = out.replace(/INSERT\s+OR\s+REPLACE\s+INTO\s+"?(\w+)"?\s*\(([^)]+)\)\s*VALUES/gi, (_match, table, colList) => `INSERT INTO "${table}" (${colList}) VALUES`);
|
|
2238
|
+
out = out.replace(/INSERT\s+OR\s+REPLACE\s+INTO/gi, "INSERT INTO");
|
|
2239
|
+
if (/INSERT\s+OR\s+IGNORE\s+INTO/i.test(out)) {
|
|
2240
|
+
out = out.replace(/INSERT\s+OR\s+IGNORE\s+INTO/gi, "INSERT INTO");
|
|
2241
|
+
out = out.replace(/;?\s*$/, " ON CONFLICT DO NOTHING");
|
|
2242
|
+
}
|
|
2243
|
+
return out;
|
|
2244
|
+
}
|
|
2123
2245
|
|
|
2124
2246
|
// src/db/sync-pg.ts
|
|
2125
2247
|
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
2126
2248
|
import { fileURLToPath } from "url";
|
|
2127
2249
|
import { dirname, join as join10 } from "path";
|
|
2128
2250
|
import { existsSync as existsSync11 } from "fs";
|
|
2129
|
-
import { translateSql, translateParams } from "@hasna/cloud";
|
|
2130
2251
|
function translateSqliteDates(sql) {
|
|
2131
2252
|
let s = sql;
|
|
2132
2253
|
const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
|
|
@@ -2248,6 +2369,54 @@ class SyncPgAdapter {
|
|
|
2248
2369
|
}
|
|
2249
2370
|
var init_sync_pg = () => {};
|
|
2250
2371
|
|
|
2372
|
+
// src/db/pg-migrate.ts
|
|
2373
|
+
var exports_pg_migrate = {};
|
|
2374
|
+
__export(exports_pg_migrate, {
|
|
2375
|
+
resolvePgSsl: () => resolvePgSsl,
|
|
2376
|
+
applyPgMigrations: () => applyPgMigrations
|
|
2377
|
+
});
|
|
2378
|
+
import pg from "pg";
|
|
2379
|
+
function resolvePgSsl(dsn) {
|
|
2380
|
+
return dsn.includes("sslmode=require") || dsn.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
|
|
2381
|
+
}
|
|
2382
|
+
async function applyPgMigrations(connectionString, migrations, service = "unknown") {
|
|
2383
|
+
const pool = new pg.Pool({ connectionString, ssl: resolvePgSsl(connectionString) });
|
|
2384
|
+
const result = {
|
|
2385
|
+
service,
|
|
2386
|
+
applied: [],
|
|
2387
|
+
alreadyApplied: [],
|
|
2388
|
+
errors: [],
|
|
2389
|
+
totalMigrations: migrations.length
|
|
2390
|
+
};
|
|
2391
|
+
try {
|
|
2392
|
+
await pool.query(`CREATE TABLE IF NOT EXISTS _pg_migrations (
|
|
2393
|
+
id SERIAL PRIMARY KEY,
|
|
2394
|
+
version INT UNIQUE NOT NULL,
|
|
2395
|
+
applied_at TIMESTAMPTZ DEFAULT NOW()
|
|
2396
|
+
)`);
|
|
2397
|
+
const existing = await pool.query("SELECT version FROM _pg_migrations ORDER BY version");
|
|
2398
|
+
const appliedSet = new Set(existing.rows.map((row) => Number(row["version"])));
|
|
2399
|
+
for (let i = 0;i < migrations.length; i++) {
|
|
2400
|
+
if (appliedSet.has(i)) {
|
|
2401
|
+
result.alreadyApplied.push(i);
|
|
2402
|
+
continue;
|
|
2403
|
+
}
|
|
2404
|
+
try {
|
|
2405
|
+
await pool.query(migrations[i]);
|
|
2406
|
+
await pool.query("INSERT INTO _pg_migrations (version) VALUES ($1) ON CONFLICT DO NOTHING", [i]);
|
|
2407
|
+
result.applied.push(i);
|
|
2408
|
+
} catch (err) {
|
|
2409
|
+
result.errors.push(`Migration ${i}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2410
|
+
break;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
} finally {
|
|
2414
|
+
await pool.end().catch(() => {});
|
|
2415
|
+
}
|
|
2416
|
+
return result;
|
|
2417
|
+
}
|
|
2418
|
+
var init_pg_migrate = () => {};
|
|
2419
|
+
|
|
2251
2420
|
// src/db/pg-migrations.ts
|
|
2252
2421
|
var PG_MIGRATIONS;
|
|
2253
2422
|
var init_pg_migrations = __esm(() => {
|
|
@@ -2491,7 +2660,7 @@ __export(exports_cloud, {
|
|
|
2491
2660
|
cloudMigrations: () => cloudMigrations,
|
|
2492
2661
|
authClientFromPool: () => authClientFromPool
|
|
2493
2662
|
});
|
|
2494
|
-
import
|
|
2663
|
+
import pg2 from "pg";
|
|
2495
2664
|
function getCloudDatabaseUrl() {
|
|
2496
2665
|
return process.env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || process.env["ECONOMY_DATABASE_URL"]?.trim() || process.env["DATABASE_URL"]?.trim() || undefined;
|
|
2497
2666
|
}
|
|
@@ -2515,8 +2684,7 @@ function openCloudDatabase(dsn = getCloudDatabaseUrl()) {
|
|
|
2515
2684
|
function createCloudPool(dsn = getCloudDatabaseUrl()) {
|
|
2516
2685
|
if (!dsn)
|
|
2517
2686
|
throw new Error("economy cloud mode requires a Postgres DSN (HASNA_ECONOMY_DATABASE_URL)");
|
|
2518
|
-
const
|
|
2519
|
-
const pool = new pg.Pool({ connectionString: dsn, ssl, max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4 });
|
|
2687
|
+
const pool = new pg2.Pool({ connectionString: dsn, ssl: resolvePgSsl(dsn), max: Number(process.env["ECONOMY_PG_POOL_MAX"] ?? "5"), connectionTimeoutMillis: 1e4 });
|
|
2520
2688
|
pool.on("error", (err) => {
|
|
2521
2689
|
console.error(JSON.stringify({ evt: "pg_pool_error", message: err.message }));
|
|
2522
2690
|
});
|
|
@@ -2542,6 +2710,7 @@ function cloudMigrations() {
|
|
|
2542
2710
|
}
|
|
2543
2711
|
var init_cloud = __esm(() => {
|
|
2544
2712
|
init_sync_pg();
|
|
2713
|
+
init_pg_migrate();
|
|
2545
2714
|
init_pg_migrations();
|
|
2546
2715
|
});
|
|
2547
2716
|
|
|
@@ -2820,13 +2989,14 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
|
2820
2989
|
let imported = 0;
|
|
2821
2990
|
let skipped = 0;
|
|
2822
2991
|
for (const project of projects) {
|
|
2823
|
-
|
|
2992
|
+
const path = resolveProjectPath(project);
|
|
2993
|
+
if (!path) {
|
|
2824
2994
|
skipped++;
|
|
2825
2995
|
continue;
|
|
2826
2996
|
}
|
|
2827
2997
|
upsertProject(db, {
|
|
2828
2998
|
id: project.id,
|
|
2829
|
-
path
|
|
2999
|
+
path,
|
|
2830
3000
|
name: project.name,
|
|
2831
3001
|
description: project.description,
|
|
2832
3002
|
tags: project.tags ?? [],
|
|
@@ -2836,6 +3006,7 @@ async function syncOpenProjectsRegistry(db, listActiveProjects) {
|
|
|
2836
3006
|
}
|
|
2837
3007
|
return { imported, skipped };
|
|
2838
3008
|
}
|
|
3009
|
+
var resolveProjectPath = (project) => project.primary_path || project.path || "";
|
|
2839
3010
|
var init_open_projects = __esm(() => {
|
|
2840
3011
|
init_database();
|
|
2841
3012
|
});
|
|
@@ -3154,6 +3325,7 @@ function defaultCostBasisForAgent(agent) {
|
|
|
3154
3325
|
}
|
|
3155
3326
|
|
|
3156
3327
|
// src/lib/accounts.ts
|
|
3328
|
+
import { appliedProfileName, resolveStore } from "@hasna/accounts";
|
|
3157
3329
|
var AGENT_ACCOUNT_TOOLS = {
|
|
3158
3330
|
claude: ["claude"],
|
|
3159
3331
|
takumi: ["takumi", "claude"],
|
|
@@ -3218,57 +3390,35 @@ function envOverride(agent, env) {
|
|
|
3218
3390
|
account_source: "override"
|
|
3219
3391
|
};
|
|
3220
3392
|
}
|
|
3221
|
-
function
|
|
3222
|
-
try {
|
|
3223
|
-
return new Set(api.listTools().map((tool) => tool.id));
|
|
3224
|
-
} catch {
|
|
3225
|
-
return new Set;
|
|
3226
|
-
}
|
|
3227
|
-
}
|
|
3228
|
-
function profileForEnvDir(api, tool, env) {
|
|
3393
|
+
async function profileForEnvDir(store, tool, env) {
|
|
3229
3394
|
const configuredDir = env[tool.envVar];
|
|
3230
3395
|
if (!configuredDir)
|
|
3231
3396
|
return null;
|
|
3232
3397
|
const normalized = normalizeDir(configuredDir);
|
|
3233
|
-
|
|
3234
|
-
return api.listProfiles(tool.id).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
3235
|
-
} catch {
|
|
3236
|
-
return null;
|
|
3237
|
-
}
|
|
3398
|
+
return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
3238
3399
|
}
|
|
3239
3400
|
async function resolveAccountForAgent(agent, env = process.env) {
|
|
3240
3401
|
const override = envOverride(agent, env);
|
|
3241
3402
|
if (override)
|
|
3242
3403
|
return override;
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
api = await import("@hasna/accounts");
|
|
3246
|
-
} catch {
|
|
3247
|
-
return null;
|
|
3248
|
-
}
|
|
3249
|
-
const toolIds = knownToolIds(api);
|
|
3404
|
+
const store = resolveStore(env);
|
|
3405
|
+
const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
|
|
3250
3406
|
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
let tool;
|
|
3254
|
-
try {
|
|
3255
|
-
tool = api.getTool(toolId);
|
|
3256
|
-
} catch {
|
|
3407
|
+
const tool = tools.get(toolId);
|
|
3408
|
+
if (!tool)
|
|
3257
3409
|
continue;
|
|
3258
|
-
|
|
3259
|
-
const envProfile = profileForEnvDir(api, tool, env);
|
|
3410
|
+
const envProfile = await profileForEnvDir(store, tool, env);
|
|
3260
3411
|
if (envProfile)
|
|
3261
3412
|
return fromProfile(envProfile, "env");
|
|
3262
|
-
|
|
3263
|
-
|
|
3413
|
+
const appliedName = appliedProfileName(toolId);
|
|
3414
|
+
if (appliedName) {
|
|
3415
|
+
const applied = await store.findProfile(appliedName, toolId);
|
|
3264
3416
|
if (applied)
|
|
3265
3417
|
return fromProfile(applied, "applied");
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
return fromProfile(current, "current");
|
|
3271
|
-
} catch {}
|
|
3418
|
+
}
|
|
3419
|
+
const current = await store.currentProfile(toolId);
|
|
3420
|
+
if (current)
|
|
3421
|
+
return fromProfile(current, "current");
|
|
3272
3422
|
}
|
|
3273
3423
|
return null;
|
|
3274
3424
|
}
|
|
@@ -6446,6 +6596,15 @@ var openApiSpec = {
|
|
|
6446
6596
|
type: "string"
|
|
6447
6597
|
}
|
|
6448
6598
|
},
|
|
6599
|
+
{
|
|
6600
|
+
name: "since",
|
|
6601
|
+
in: "query",
|
|
6602
|
+
description: "ISO timestamp lower bound; takes precedence over period",
|
|
6603
|
+
schema: {
|
|
6604
|
+
type: "string",
|
|
6605
|
+
format: "date-time"
|
|
6606
|
+
}
|
|
6607
|
+
},
|
|
6449
6608
|
{
|
|
6450
6609
|
name: "machine",
|
|
6451
6610
|
in: "query",
|
|
@@ -7538,9 +7697,10 @@ function createHandler(db, options = {}) {
|
|
|
7538
7697
|
if (path === "/api/breakdown" && method === "GET") {
|
|
7539
7698
|
const by = url.searchParams.get("by") ?? "model";
|
|
7540
7699
|
const period = url.searchParams.get("period") ?? "all";
|
|
7700
|
+
const since = url.searchParams.get("since") ?? undefined;
|
|
7541
7701
|
const machine = url.searchParams.get("machine") ?? undefined;
|
|
7542
7702
|
if (by === "project")
|
|
7543
|
-
return ok(queryProjectBreakdown(db, period, machine));
|
|
7703
|
+
return ok(since ? queryProjectBreakdownSince(db, since, machine) : queryProjectBreakdown(db, period, machine));
|
|
7544
7704
|
if (by === "agent")
|
|
7545
7705
|
return ok(queryAgentBreakdown(db, period, machine));
|
|
7546
7706
|
if (by === "account")
|
|
@@ -7934,12 +8094,12 @@ function parsePort(raw, label) {
|
|
|
7934
8094
|
}
|
|
7935
8095
|
async function runMigrate() {
|
|
7936
8096
|
const { getCloudDatabaseUrl: getCloudDatabaseUrl2, createCloudPool: createCloudPool2, authClientFromPool: authClientFromPool2, cloudMigrations: cloudMigrations2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
|
|
7937
|
-
const { applyPgMigrations } = await
|
|
8097
|
+
const { applyPgMigrations: applyPgMigrations2 } = await Promise.resolve().then(() => (init_pg_migrate(), exports_pg_migrate));
|
|
7938
8098
|
const { ApiKeyStore: ApiKeyStore2 } = await import("@hasna/contracts/auth");
|
|
7939
8099
|
const dsn = getCloudDatabaseUrl2();
|
|
7940
8100
|
if (!dsn)
|
|
7941
8101
|
throw new Error("migrate requires a Postgres DSN: set HASNA_ECONOMY_DATABASE_URL (or ECONOMY_DATABASE_URL / DATABASE_URL)");
|
|
7942
|
-
const result = await
|
|
8102
|
+
const result = await applyPgMigrations2(dsn, cloudMigrations2(), "economy");
|
|
7943
8103
|
const pool = createCloudPool2(dsn);
|
|
7944
8104
|
try {
|
|
7945
8105
|
await new ApiKeyStore2(authClientFromPool2(pool)).ensureSchema();
|
package/dist/server/serve.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SqliteAdapter as Database } from '
|
|
1
|
+
import type { SqliteAdapter as Database } from '../db/sqlite-adapter.js';
|
|
2
2
|
/** The serve OpenAPI document (source of the generated SDK), version-synced. */
|
|
3
3
|
export declare function openApiDocument(): Record<string, unknown>;
|
|
4
4
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,IAAI,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AA8CxE,gFAAgF;AAChF,wBAAgB,eAAe,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAGzD;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,CACV,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,GAC7F,OAAO,CAAC;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/E;AAuBD,UAAU,kBAAkB;IAC1B,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAChC;AA6ED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAAE,YAAY,SAAwB,IACzF,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwB7D;AAQD,MAAM,WAAW,cAAc;IAC7B;;wEAEoE;IACpE,aAAa,CAAC,EAAE,gBAAgB,CAAA;IAChC,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAChE;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,GAAE,cAAmB,IACxC,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA+e/D;AAMD,wBAAgB,WAAW,CAAC,IAAI,SAAO,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CAAC,OAAO,GAAG,CAAC,KAAK,CAAC,CAkDvG"}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Economy documentation
|
|
2
|
+
|
|
3
|
+
Economy can run as an on-machine SQLite application or as a client of a shared self-hosted HTTP service. These guides describe the current command and network surfaces:
|
|
4
|
+
|
|
5
|
+
- [CLI reference](cli.md) — the `economy` command and the four installed binaries.
|
|
6
|
+
- [Ingestion](ingestion.md) — supported sources, default paths, sync behavior, billing, and account attribution.
|
|
7
|
+
- [Configuration and deployment](configuration.md) — data paths, environment variables, local/server modes, and authentication.
|
|
8
|
+
- [REST API](rest-api.md) — canonical `/v1` routes, request conventions, probes, and legacy aliases.
|
|
9
|
+
- [MCP server](mcp.md) — stdio/HTTP setup and the available tools.
|
|
10
|
+
- [OTLP sidecar](otel.md) — OTLP metrics and the simplified `/ingest` payload.
|
|
11
|
+
|
|
12
|
+
For generated API details, start `economy-serve` and open [`/openapi.json`](http://127.0.0.1:3456/openapi.json). The checked-in source is [`openapi/economy.json`](../openapi/economy.json).
|
package/docs/cli.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# CLI reference
|
|
2
|
+
|
|
3
|
+
Installing `@hasna/economy` provides four binaries:
|
|
4
|
+
|
|
5
|
+
| Binary | Purpose |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `economy` | Ingest, query, and manage Economy data. |
|
|
8
|
+
| `economy-mcp` | Run the MCP server over stdio or Streamable HTTP. |
|
|
9
|
+
| `economy-serve` | Serve the REST API and built dashboard, or migrate a self-hosted database. |
|
|
10
|
+
| `economy-otel` | Ingest OTLP/HTTP metrics or simplified cost events into local SQLite. |
|
|
11
|
+
|
|
12
|
+
Use `<binary> --help` and `economy <command> --help` for the exact help emitted by the installed version.
|
|
13
|
+
|
|
14
|
+
## Core commands
|
|
15
|
+
|
|
16
|
+
The supported coding-agent values are `claude`, `takumi`, `codex`, `gemini`, `opencode`, `cursor`, `pi`, and `hermes`.
|
|
17
|
+
|
|
18
|
+
| Command | Current behavior and principal options |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `economy sync` | Ingest every local source, or select sources with `--claude`, `--takumi`, `--codex`, `--gemini`, `--opencode`, `--cursor`, `--pi`, `--hermes`, or `--loops`. Maintenance flags are `--force`, `--backfill-machine`, and `--recalculate`; `--verbose` prints source details. |
|
|
21
|
+
| `economy today`, `week`, `month` | Auto-sync in local mode and print the corresponding summary. Auto-sync is skipped in cloud-client mode. |
|
|
22
|
+
| `economy sessions` | List sessions. Filters: `--agent`, `--project`, `--account`, `--machine`, `--since`, and `--search`; `--limit` defaults to 20; `--format` is `table`, `compact`, `csv`, or `json`. |
|
|
23
|
+
| `economy session <id>` | Show one session and its requests. IDs may be prefixes. `--limit` defaults to 20 and `--verbose` shows up to 50 requests. |
|
|
24
|
+
| `economy top` | Rank expensive sessions with `-n`, `--agent`, and `--since`. |
|
|
25
|
+
| `economy breakdown` | Group by `model`, `agent`, `project`, `account`, `cost-center`, `loop`, `app`, or `repo`; accepts `--since`, `--limit`, `--verbose`, and `--json`. |
|
|
26
|
+
| `economy accounts [period]` | Account/profile totals for `today`, `week`, `month`, `year`, or `all`; accepts `--limit`, `--verbose`, and `--json`. |
|
|
27
|
+
| `economy machines` | Machines represented in stored data; accepts `--limit` and `--verbose`. |
|
|
28
|
+
| `economy fleet` | Shared summary plus per-machine rows; accepts `--period`, `--limit`, `--verbose`, and `--json`. |
|
|
29
|
+
| `economy brief` | Fleet brief with `--since <24h|7d|ISO-date>`, `--machine <id|all>`, or `--json`. |
|
|
30
|
+
| `economy usage [period]` | Quota/usage snapshots with `--agent`, `--limit`, `--verbose`, or `--json`. |
|
|
31
|
+
| `economy savings [period]` | Subscription-versus-API-equivalent savings with `--agent` or `--json`. |
|
|
32
|
+
| `economy watch` | Poll recent costs, or use `--daemon` to sync watched local paths. Also accepts `--interval`, `--agent`, and macOS `--notify`. In cloud mode it streams the API and does not ingest local files. |
|
|
33
|
+
| `economy status` | Print one-line spend, fleet, storage, top-agent, and available quota status. |
|
|
34
|
+
| `economy doctor` | Check source paths/token availability, storage mode, pricing gaps, deduplication, and billing drift. |
|
|
35
|
+
| `economy init` | Print first-run local and cloud-client setup hints. |
|
|
36
|
+
|
|
37
|
+
Human output for high-cardinality commands is intentionally capped. Use the command's `--json`, `--verbose`, or `--limit` option when available. JSON output is complete; `--verbose` has command-specific semantics, so consult `--help`.
|
|
38
|
+
|
|
39
|
+
## Analysis and export
|
|
40
|
+
|
|
41
|
+
| Command | Behavior |
|
|
42
|
+
| --- | --- |
|
|
43
|
+
| `economy export` | Export `sessions` or `requests` as CSV. `--period` accepts `today`, `week`, `month`, or `all`; `--output` writes a file instead of stdout. |
|
|
44
|
+
| `economy compare <period1> <period2>` | Compare `today`, `yesterday`, `week`, `lastweek`, `month`, or `lastmonth`. |
|
|
45
|
+
| `economy forecast` | Project the current month's total from the observed burn rate. |
|
|
46
|
+
| `economy efficiency` | Show output/input ratio, cache-hit percentage, and cost per 1,000 output tokens by model. |
|
|
47
|
+
| `economy estimate --model <id>` | Estimate cost from `--input` and `--output` token counts using the active pricing table. |
|
|
48
|
+
| `economy tui` | Terminal dashboard; `--watch` refreshes at `--interval` seconds. |
|
|
49
|
+
| `economy waybar` | Emit a Waybar-compatible JSON status object. |
|
|
50
|
+
| `economy bar tui`, `bar waybar` | Aliases for the TUI and Waybar commands. |
|
|
51
|
+
|
|
52
|
+
## Data management
|
|
53
|
+
|
|
54
|
+
| Command group | Subcommands |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| `economy budget` | `set` (`--limit` required; optional `--project`, `--agent`, `--cost-center`, `--period`, `--alert`), `list` (`--limit`, `--verbose`), `remove <id>`. |
|
|
57
|
+
| `economy goal` | `set` (`--limit` required; optional `--period`, `--project`, `--agent`), `list`, `status`, `remove <id>`. List/status accept `--limit` and `--verbose`. |
|
|
58
|
+
| `economy project` | `add <path> [--name]`, `list`, `show <nameOrPath>`, `rename <path> <name>`, `remove <path>`. |
|
|
59
|
+
| `economy pricing` | `list`, `set <model>`, `remove <model>`. Set accepts `--input`, `--output`, `--cache-read`, `--cache-write`, `--cache-write-1h`, and `--cache-storage`. |
|
|
60
|
+
| `economy subscriptions` | `set` (requires `--provider` and `--plan`; optional `--agent`, `--fee`, `--included`), `list`, `remove <id>`. |
|
|
61
|
+
| `economy billing` | `sync` (`--days`, provider selectors), `show --period`, and `diff --period`. Provider sync is local-only. |
|
|
62
|
+
| `economy config` | Show config, `get <key>`, `set <key> <value>`, or `webhook-test`. See [configuration](configuration.md). |
|
|
63
|
+
| `economy remove <type> <id>` | Top-level alias for removing a `budget`, `project`, `goal`, or `pricing` row. Alias: `rm`. |
|
|
64
|
+
|
|
65
|
+
## Integrations and utilities
|
|
66
|
+
|
|
67
|
+
| Command | Behavior |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `economy serve --port <port>` | Start the REST API in-process. Equivalent server controls are documented under [`economy-serve`](configuration.md#rest-server). |
|
|
70
|
+
| `economy dashboard --port <port>` | Start the server when necessary and open the dashboard URL. |
|
|
71
|
+
| `economy mcp` | Print Claude Code, Codex, and Gemini MCP configuration; select one or use `--all`. |
|
|
72
|
+
| `economy completion <shell>` | Print completion for `bash`, `zsh`, or `fish`. |
|
|
73
|
+
| `economy menubar` | `install [--force]`, `start`, `stop`, or `uninstall` the macOS Economy Bar app. |
|
|
74
|
+
| `economy brains` | `gather`, `train`, `model [set|clear]`, and `status`. Fine-tuning requires the optional `@hasna/brains` package; gathering does not. |
|
|
75
|
+
| `economy events`, `economy webhooks` | Event emit/list/replay and event-subscription commands supplied by `@hasna/events`; use the nested `--help` for options. |
|
|
76
|
+
| `economy todos` | Display the bundled Economy roadmap, filter tasks, or show a task ID. This is project planning data, not live service status. |
|
|
77
|
+
|
|
78
|
+
## Other binary help
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
economy-mcp [--http] [--port <port>]
|
|
82
|
+
economy-serve [--port <port>]
|
|
83
|
+
economy-serve migrate
|
|
84
|
+
economy-serve version
|
|
85
|
+
economy-otel [--port <port>]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`economy-mcp` and `economy-serve` support `--version`. See [MCP](mcp.md), [configuration](configuration.md), and [OTLP](otel.md) for environment controls and endpoints.
|