@hasna/economy 0.2.10 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -45
- package/dist/cli/index.js +228 -100
- 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 +64 -17
- package/dist/ingest/claude.d.ts +1 -1
- package/dist/ingest/claude.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/gemini.d.ts +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/lib/package-metadata.d.ts +8 -0
- package/dist/lib/package-metadata.d.ts.map +1 -0
- package/dist/lib/pricing.d.ts +1 -1
- package/dist/lib/pricing.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 +386 -347
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +237 -26
- package/dist/server/serve.d.ts +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +6 -5
package/dist/server/index.d.ts
CHANGED
package/dist/server/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
1
2
|
// @bun
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __returnValue = (v) => v;
|
|
@@ -108,8 +109,17 @@ var init_pricing = __esm(() => {
|
|
|
108
109
|
// src/db/database.ts
|
|
109
110
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
110
111
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
112
|
+
import { hostname } from "os";
|
|
111
113
|
import { homedir } from "os";
|
|
112
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
|
+
}
|
|
113
123
|
function getDataDir() {
|
|
114
124
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
115
125
|
const newDir = join(home, ".hasna", "economy");
|
|
@@ -142,6 +152,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
142
152
|
}
|
|
143
153
|
const db = new Database(path);
|
|
144
154
|
db.exec("PRAGMA journal_mode = WAL");
|
|
155
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
145
156
|
db.exec("PRAGMA foreign_keys = ON");
|
|
146
157
|
initSchema(db);
|
|
147
158
|
if (!skipSeed) {
|
|
@@ -163,7 +174,8 @@ function initSchema(db) {
|
|
|
163
174
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
164
175
|
duration_ms INTEGER DEFAULT 0,
|
|
165
176
|
timestamp TEXT NOT NULL,
|
|
166
|
-
source_request_id TEXT
|
|
177
|
+
source_request_id TEXT,
|
|
178
|
+
machine_id TEXT DEFAULT ''
|
|
167
179
|
);
|
|
168
180
|
|
|
169
181
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -175,7 +187,8 @@ function initSchema(db) {
|
|
|
175
187
|
ended_at TEXT,
|
|
176
188
|
total_cost_usd REAL DEFAULT 0,
|
|
177
189
|
total_tokens INTEGER DEFAULT 0,
|
|
178
|
-
request_count INTEGER DEFAULT 0
|
|
190
|
+
request_count INTEGER DEFAULT 0,
|
|
191
|
+
machine_id TEXT DEFAULT ''
|
|
179
192
|
);
|
|
180
193
|
|
|
181
194
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -241,6 +254,15 @@ function initSchema(db) {
|
|
|
241
254
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
242
255
|
);
|
|
243
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
|
+
`);
|
|
244
266
|
}
|
|
245
267
|
function periodWhere(period) {
|
|
246
268
|
switch (period) {
|
|
@@ -279,17 +301,17 @@ function upsertRequest(db, req) {
|
|
|
279
301
|
INSERT OR REPLACE INTO requests
|
|
280
302
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
281
303
|
cache_read_tokens, cache_create_tokens, cost_usd, duration_ms,
|
|
282
|
-
timestamp, source_request_id)
|
|
283
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
284
|
-
`).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 ?? "");
|
|
285
307
|
}
|
|
286
308
|
function upsertSession(db, session) {
|
|
287
309
|
db.prepare(`
|
|
288
310
|
INSERT OR REPLACE INTO sessions
|
|
289
311
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
290
|
-
total_cost_usd, total_tokens, request_count)
|
|
291
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
292
|
-
`).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 ?? "");
|
|
293
315
|
}
|
|
294
316
|
function rollupSession(db, sessionId) {
|
|
295
317
|
db.prepare(`
|
|
@@ -319,6 +341,10 @@ function querySessions(db, filter = {}) {
|
|
|
319
341
|
conditions.push("started_at >= ?");
|
|
320
342
|
params.push(filter.since);
|
|
321
343
|
}
|
|
344
|
+
if (filter.machine) {
|
|
345
|
+
conditions.push("machine_id = ?");
|
|
346
|
+
params.push(filter.machine);
|
|
347
|
+
}
|
|
322
348
|
if (filter.search) {
|
|
323
349
|
const q = `%${filter.search}%`;
|
|
324
350
|
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
@@ -337,24 +363,25 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
337
363
|
}
|
|
338
364
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
339
365
|
}
|
|
340
|
-
function querySummary(db, period) {
|
|
366
|
+
function querySummary(db, period, machine) {
|
|
341
367
|
const rWhere = periodWhere(period);
|
|
342
368
|
const sWhere = sessionPeriodWhere(period);
|
|
369
|
+
const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
343
370
|
const r = db.prepare(`
|
|
344
371
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
345
372
|
COUNT(*) as requests,
|
|
346
373
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
347
|
-
FROM requests WHERE ${rWhere}
|
|
374
|
+
FROM requests WHERE ${rWhere}${machineClause}
|
|
348
375
|
`).get();
|
|
349
376
|
const codexTotals = db.prepare(`
|
|
350
377
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
351
378
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
352
379
|
COUNT(*) as sessions
|
|
353
380
|
FROM sessions
|
|
354
|
-
WHERE ${sWhere}
|
|
381
|
+
WHERE ${sWhere}${machineClause}
|
|
355
382
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
356
383
|
`).get();
|
|
357
|
-
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();
|
|
358
385
|
return {
|
|
359
386
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
360
387
|
requests: r.requests,
|
|
@@ -499,6 +526,20 @@ function getIngestState(db, source, key) {
|
|
|
499
526
|
function setIngestState(db, source, key, value) {
|
|
500
527
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
501
528
|
}
|
|
529
|
+
function listMachines(db) {
|
|
530
|
+
return db.prepare(`
|
|
531
|
+
SELECT
|
|
532
|
+
s.machine_id,
|
|
533
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
534
|
+
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
535
|
+
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
536
|
+
MAX(s.started_at) as last_active
|
|
537
|
+
FROM sessions s
|
|
538
|
+
WHERE s.machine_id != ''
|
|
539
|
+
GROUP BY s.machine_id
|
|
540
|
+
ORDER BY total_cost_usd DESC
|
|
541
|
+
`).all();
|
|
542
|
+
}
|
|
502
543
|
function upsertModelPricing(db, p) {
|
|
503
544
|
db.prepare(`
|
|
504
545
|
INSERT OR REPLACE INTO model_pricing
|
|
@@ -570,6 +611,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
570
611
|
console.log("Claude projects dir not found:", PROJECTS_DIR);
|
|
571
612
|
return { files: 0, requests: 0, sessions: 0 };
|
|
572
613
|
}
|
|
614
|
+
const machineId = getMachineId();
|
|
573
615
|
let totalFiles = 0;
|
|
574
616
|
let totalRequests = 0;
|
|
575
617
|
const touchedSessions = new Set;
|
|
@@ -641,7 +683,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
641
683
|
cost_usd: costUsd,
|
|
642
684
|
duration_ms: 0,
|
|
643
685
|
timestamp,
|
|
644
|
-
source_request_id: reqId
|
|
686
|
+
source_request_id: reqId,
|
|
687
|
+
machine_id: machineId
|
|
645
688
|
});
|
|
646
689
|
if (!touchedSessions.has(sessionId)) {
|
|
647
690
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
@@ -657,7 +700,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
657
700
|
ended_at: null,
|
|
658
701
|
total_cost_usd: 0,
|
|
659
702
|
total_tokens: 0,
|
|
660
|
-
request_count: 0
|
|
703
|
+
request_count: 0,
|
|
704
|
+
machine_id: machineId
|
|
661
705
|
};
|
|
662
706
|
upsertSession(db, session);
|
|
663
707
|
}
|
|
@@ -680,7 +724,7 @@ init_database();
|
|
|
680
724
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
681
725
|
import { homedir as homedir3 } from "os";
|
|
682
726
|
import { join as join3, basename as basename2 } from "path";
|
|
683
|
-
import { Database as
|
|
727
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
684
728
|
var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
685
729
|
var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
686
730
|
async function ingestCodex(db, verbose = false) {
|
|
@@ -689,10 +733,11 @@ async function ingestCodex(db, verbose = false) {
|
|
|
689
733
|
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
690
734
|
return { sessions: 0 };
|
|
691
735
|
}
|
|
736
|
+
const machineId = getMachineId();
|
|
692
737
|
let codexDb = null;
|
|
693
738
|
let ingested = 0;
|
|
694
739
|
try {
|
|
695
|
-
codexDb = new
|
|
740
|
+
codexDb = new BunDatabase(CODEX_DB_PATH, { readonly: true });
|
|
696
741
|
const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
|
|
697
742
|
for (const thread of threads) {
|
|
698
743
|
const stateKey = thread.id;
|
|
@@ -713,7 +758,8 @@ async function ingestCodex(db, verbose = false) {
|
|
|
713
758
|
ended_at: endedAt,
|
|
714
759
|
total_cost_usd: costUsd,
|
|
715
760
|
total_tokens: thread.tokens_used,
|
|
716
|
-
request_count: 1
|
|
761
|
+
request_count: 1,
|
|
762
|
+
machine_id: machineId
|
|
717
763
|
});
|
|
718
764
|
setIngestState(db, "codex", stateKey, "done");
|
|
719
765
|
ingested++;
|
|
@@ -726,6 +772,85 @@ async function ingestCodex(db, verbose = false) {
|
|
|
726
772
|
return { sessions: ingested };
|
|
727
773
|
}
|
|
728
774
|
|
|
775
|
+
// src/ingest/gemini.ts
|
|
776
|
+
init_database();
|
|
777
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
|
|
778
|
+
import { homedir as homedir4 } from "os";
|
|
779
|
+
import { join as join4 } from "path";
|
|
780
|
+
var GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
|
|
781
|
+
async function ingestGemini(db, verbose) {
|
|
782
|
+
if (!existsSync4(GEMINI_TMP_DIR)) {
|
|
783
|
+
if (verbose)
|
|
784
|
+
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
785
|
+
return { sessions: 0 };
|
|
786
|
+
}
|
|
787
|
+
const machineId = getMachineId();
|
|
788
|
+
let totalSessions = 0;
|
|
789
|
+
const touchedSessions = new Set;
|
|
790
|
+
let projectHashDirs = [];
|
|
791
|
+
try {
|
|
792
|
+
projectHashDirs = readdirSync3(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join4(GEMINI_TMP_DIR, d.name));
|
|
793
|
+
} catch {
|
|
794
|
+
return { sessions: 0 };
|
|
795
|
+
}
|
|
796
|
+
for (const projectDir of projectHashDirs) {
|
|
797
|
+
const chatsDir = join4(projectDir, "chats");
|
|
798
|
+
if (!existsSync4(chatsDir))
|
|
799
|
+
continue;
|
|
800
|
+
let chatFiles = [];
|
|
801
|
+
try {
|
|
802
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join4(chatsDir, f));
|
|
803
|
+
} catch {
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
for (const filePath of chatFiles) {
|
|
807
|
+
const stateKey = filePath.replace(homedir4(), "~");
|
|
808
|
+
let fileMtime = "0";
|
|
809
|
+
try {
|
|
810
|
+
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
811
|
+
} catch {
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const processed = getIngestState(db, "gemini", stateKey);
|
|
815
|
+
if (processed === fileMtime)
|
|
816
|
+
continue;
|
|
817
|
+
let chatData;
|
|
818
|
+
try {
|
|
819
|
+
chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
820
|
+
} catch {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const sessionId = chatData.sessionId;
|
|
824
|
+
if (!sessionId)
|
|
825
|
+
continue;
|
|
826
|
+
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
827
|
+
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
828
|
+
if (!existing) {
|
|
829
|
+
const session = {
|
|
830
|
+
id: sessionId,
|
|
831
|
+
agent: "gemini",
|
|
832
|
+
project_path: "",
|
|
833
|
+
project_name: "",
|
|
834
|
+
started_at: startTime,
|
|
835
|
+
ended_at: chatData.lastUpdated ?? null,
|
|
836
|
+
total_cost_usd: 0,
|
|
837
|
+
total_tokens: 0,
|
|
838
|
+
request_count: 0,
|
|
839
|
+
machine_id: machineId
|
|
840
|
+
};
|
|
841
|
+
upsertSession(db, session);
|
|
842
|
+
touchedSessions.add(sessionId);
|
|
843
|
+
totalSessions++;
|
|
844
|
+
}
|
|
845
|
+
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
for (const sessionId of touchedSessions) {
|
|
849
|
+
rollupSession(db, sessionId);
|
|
850
|
+
}
|
|
851
|
+
return { sessions: totalSessions };
|
|
852
|
+
}
|
|
853
|
+
|
|
729
854
|
// src/server/serve.ts
|
|
730
855
|
init_pricing();
|
|
731
856
|
import { randomUUID } from "crypto";
|
|
@@ -746,6 +871,20 @@ function ok(data, meta) {
|
|
|
746
871
|
function err(message, status = 400) {
|
|
747
872
|
return json({ error: message }, status);
|
|
748
873
|
}
|
|
874
|
+
function normalizeBudgetPeriod(value) {
|
|
875
|
+
switch (value) {
|
|
876
|
+
case "day":
|
|
877
|
+
case "daily":
|
|
878
|
+
return "daily";
|
|
879
|
+
case "week":
|
|
880
|
+
case "weekly":
|
|
881
|
+
return "weekly";
|
|
882
|
+
case "month":
|
|
883
|
+
case "monthly":
|
|
884
|
+
default:
|
|
885
|
+
return "monthly";
|
|
886
|
+
}
|
|
887
|
+
}
|
|
749
888
|
function applyFields(obj, fields) {
|
|
750
889
|
if (!fields || fields.length === 0)
|
|
751
890
|
return obj;
|
|
@@ -762,7 +901,11 @@ function createHandler(db) {
|
|
|
762
901
|
return ok({ status: "ok", ts: new Date().toISOString() });
|
|
763
902
|
if (path === "/api/summary" && method === "GET") {
|
|
764
903
|
const period = url.searchParams.get("period") ?? "today";
|
|
765
|
-
|
|
904
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
905
|
+
return ok(querySummary(db, period, machine));
|
|
906
|
+
}
|
|
907
|
+
if (path === "/api/machines" && method === "GET") {
|
|
908
|
+
return ok(listMachines(db), { current_machine: getMachineId() });
|
|
766
909
|
}
|
|
767
910
|
if (path === "/api/daily" && method === "GET") {
|
|
768
911
|
const days = Number(url.searchParams.get("days") ?? 30);
|
|
@@ -771,12 +914,22 @@ function createHandler(db) {
|
|
|
771
914
|
if (path === "/api/sessions" && method === "GET") {
|
|
772
915
|
const agent = url.searchParams.get("agent");
|
|
773
916
|
const project = url.searchParams.get("project") ?? undefined;
|
|
917
|
+
const search = url.searchParams.get("search") ?? undefined;
|
|
918
|
+
const machine = url.searchParams.get("machine") ?? undefined;
|
|
774
919
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
775
920
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
776
921
|
const since = url.searchParams.get("since") ?? undefined;
|
|
777
922
|
const fieldsParam = url.searchParams.get("fields");
|
|
778
923
|
const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
779
|
-
const sessions = querySessions(db, {
|
|
924
|
+
const sessions = querySessions(db, {
|
|
925
|
+
agent: agent ?? undefined,
|
|
926
|
+
project,
|
|
927
|
+
search,
|
|
928
|
+
machine,
|
|
929
|
+
limit,
|
|
930
|
+
offset,
|
|
931
|
+
since
|
|
932
|
+
});
|
|
780
933
|
return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
|
|
781
934
|
}
|
|
782
935
|
if (path === "/api/top" && method === "GET") {
|
|
@@ -804,7 +957,7 @@ function createHandler(db) {
|
|
|
804
957
|
id: randomUUID(),
|
|
805
958
|
project_path: body["project_path"] ?? null,
|
|
806
959
|
agent: body["agent"] ?? null,
|
|
807
|
-
period: body["period"]
|
|
960
|
+
period: normalizeBudgetPeriod(body["period"]),
|
|
808
961
|
limit_usd: Number(body["limit_usd"]),
|
|
809
962
|
alert_at_percent: Number(body["alert_at_percent"] ?? 80),
|
|
810
963
|
created_at: now,
|
|
@@ -867,6 +1020,8 @@ function createHandler(db) {
|
|
|
867
1020
|
results["claude"] = await ingestClaude(db);
|
|
868
1021
|
if (sources === "all" || sources === "codex")
|
|
869
1022
|
results["codex"] = await ingestCodex(db);
|
|
1023
|
+
if (sources === "all" || sources === "gemini")
|
|
1024
|
+
results["gemini"] = await ingestGemini(db);
|
|
870
1025
|
return ok(results);
|
|
871
1026
|
}
|
|
872
1027
|
const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
|
|
@@ -916,15 +1071,15 @@ function startServer(port = 3456) {
|
|
|
916
1071
|
return apiHandler(req);
|
|
917
1072
|
}
|
|
918
1073
|
try {
|
|
919
|
-
const { existsSync:
|
|
920
|
-
if (
|
|
1074
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
1075
|
+
if (existsSync5(dashboardDir)) {
|
|
921
1076
|
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
922
1077
|
const fullPath = dashboardDir + filePath;
|
|
923
|
-
if (
|
|
1078
|
+
if (existsSync5(fullPath)) {
|
|
924
1079
|
return new Response(Bun.file(fullPath));
|
|
925
1080
|
}
|
|
926
1081
|
const indexPath = dashboardDir + "/index.html";
|
|
927
|
-
if (
|
|
1082
|
+
if (existsSync5(indexPath)) {
|
|
928
1083
|
return new Response(Bun.file(indexPath));
|
|
929
1084
|
}
|
|
930
1085
|
}
|
|
@@ -935,6 +1090,62 @@ function startServer(port = 3456) {
|
|
|
935
1090
|
console.log(`economy-serve listening on http://localhost:${port}`);
|
|
936
1091
|
}
|
|
937
1092
|
|
|
1093
|
+
// src/lib/package-metadata.ts
|
|
1094
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1095
|
+
var cachedMetadata = null;
|
|
1096
|
+
function getPackageMetadata() {
|
|
1097
|
+
if (cachedMetadata)
|
|
1098
|
+
return cachedMetadata;
|
|
1099
|
+
const raw = readFileSync4(new URL("../../package.json", import.meta.url), "utf8");
|
|
1100
|
+
const parsed = JSON.parse(raw);
|
|
1101
|
+
cachedMetadata = {
|
|
1102
|
+
name: parsed.name ?? "@hasna/economy",
|
|
1103
|
+
version: parsed.version ?? "0.0.0"
|
|
1104
|
+
};
|
|
1105
|
+
return cachedMetadata;
|
|
1106
|
+
}
|
|
1107
|
+
var packageMetadata = getPackageMetadata();
|
|
1108
|
+
|
|
938
1109
|
// src/server/index.ts
|
|
939
|
-
|
|
940
|
-
|
|
1110
|
+
function printHelp() {
|
|
1111
|
+
console.log(`Usage: economy-serve [options]
|
|
1112
|
+
|
|
1113
|
+
REST API server for ${packageMetadata.name}
|
|
1114
|
+
|
|
1115
|
+
Options:
|
|
1116
|
+
-p, --port <port> Port to bind (default: ECONOMY_PORT or 3456)
|
|
1117
|
+
-V, --version output the version number
|
|
1118
|
+
-h, --help display help for command`);
|
|
1119
|
+
}
|
|
1120
|
+
function resolvePort(argv) {
|
|
1121
|
+
for (let i = 0;i < argv.length; i++) {
|
|
1122
|
+
const arg = argv[i];
|
|
1123
|
+
if ((arg === "--port" || arg === "-p") && argv[i + 1]) {
|
|
1124
|
+
const value2 = Number(argv[i + 1]);
|
|
1125
|
+
if (!Number.isFinite(value2) || value2 <= 0) {
|
|
1126
|
+
throw new Error(`Invalid port: ${argv[i + 1]}`);
|
|
1127
|
+
}
|
|
1128
|
+
return value2;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const value = Number(process.env["ECONOMY_PORT"] ?? 3456);
|
|
1132
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1133
|
+
throw new Error(`Invalid ECONOMY_PORT: ${process.env["ECONOMY_PORT"]}`);
|
|
1134
|
+
}
|
|
1135
|
+
return value;
|
|
1136
|
+
}
|
|
1137
|
+
var args = process.argv.slice(2);
|
|
1138
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1139
|
+
printHelp();
|
|
1140
|
+
process.exit(0);
|
|
1141
|
+
}
|
|
1142
|
+
if (args.includes("--version") || args.includes("-V")) {
|
|
1143
|
+
console.log(packageMetadata.version);
|
|
1144
|
+
process.exit(0);
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
startServer(resolvePort(args));
|
|
1148
|
+
} catch (error) {
|
|
1149
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
}
|
package/dist/server/serve.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Database } from '
|
|
1
|
+
import type { SqliteAdapter as Database } from '@hasna/cloud';
|
|
2
2
|
export declare function createHandler(db: Database): (req: Request) => Promise<Response>;
|
|
3
3
|
export declare function startServer(port?: number): void;
|
|
4
4
|
//# sourceMappingURL=serve.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/server/serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,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,cAAc,CAAA;AA4D7D,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAgM/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,GAAG,IAAI,CAuC7C"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export interface EconomyRequest {
|
|
|
13
13
|
duration_ms: number;
|
|
14
14
|
timestamp: string;
|
|
15
15
|
source_request_id: string;
|
|
16
|
+
machine_id?: string;
|
|
16
17
|
}
|
|
17
18
|
export interface EconomySession {
|
|
18
19
|
id: string;
|
|
@@ -24,6 +25,7 @@ export interface EconomySession {
|
|
|
24
25
|
total_cost_usd: number;
|
|
25
26
|
total_tokens: number;
|
|
26
27
|
request_count: number;
|
|
28
|
+
machine_id?: string;
|
|
27
29
|
}
|
|
28
30
|
export interface EconomyProject {
|
|
29
31
|
id: string;
|
|
@@ -97,5 +99,6 @@ export interface SessionFilter {
|
|
|
97
99
|
offset?: number;
|
|
98
100
|
since?: string;
|
|
99
101
|
search?: string;
|
|
102
|
+
machine?: string;
|
|
100
103
|
}
|
|
101
104
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEjD,MAAM,MAAM,MAAM,GAAG,OAAO,GAAG,WAAW,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAA;AAE9E,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,KAAK,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,iBAAiB,EAAE,MAAM,CAAA;IACzB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/economy",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "AI coding cost tracker
|
|
3
|
+
"version": "0.2.12",
|
|
4
|
+
"description": "AI coding cost tracker — CLI + MCP server + REST API + web dashboard for Claude Code, Codex, and Gemini",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -53,14 +53,15 @@
|
|
|
53
53
|
"access": "public"
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
|
-
"@hasna/cloud": "^0.1.
|
|
56
|
+
"@hasna/cloud": "^0.1.24",
|
|
57
57
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
58
58
|
"chalk": "^5.4.1",
|
|
59
|
-
"commander": "^13.1.0"
|
|
59
|
+
"commander": "^13.1.0",
|
|
60
|
+
"zod": "^3.24.2"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@types/bun": "latest",
|
|
63
64
|
"bun-types": "latest",
|
|
64
65
|
"typescript": "^5.7.2"
|
|
65
66
|
}
|
|
66
|
-
}
|
|
67
|
+
}
|