@hasna/economy 0.2.10 → 0.2.11
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 +127 -82
- package/dist/index.js +2 -2
- 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 +305 -331
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +168 -10
- package/dist/server/serve.d.ts +1 -1
- package/dist/server/serve.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;
|
|
@@ -680,7 +681,7 @@ init_database();
|
|
|
680
681
|
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
|
|
681
682
|
import { homedir as homedir3 } from "os";
|
|
682
683
|
import { join as join3, basename as basename2 } from "path";
|
|
683
|
-
import { Database as
|
|
684
|
+
import { Database as BunDatabase } from "bun:sqlite";
|
|
684
685
|
var CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
685
686
|
var CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
686
687
|
async function ingestCodex(db, verbose = false) {
|
|
@@ -692,7 +693,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
692
693
|
let codexDb = null;
|
|
693
694
|
let ingested = 0;
|
|
694
695
|
try {
|
|
695
|
-
codexDb = new
|
|
696
|
+
codexDb = new BunDatabase(CODEX_DB_PATH, { readonly: true });
|
|
696
697
|
const threads = codexDb.prepare(`SELECT id, cwd, created_at, updated_at, tokens_used, title FROM threads WHERE tokens_used > 0`).all();
|
|
697
698
|
for (const thread of threads) {
|
|
698
699
|
const stateKey = thread.id;
|
|
@@ -726,6 +727,83 @@ async function ingestCodex(db, verbose = false) {
|
|
|
726
727
|
return { sessions: ingested };
|
|
727
728
|
}
|
|
728
729
|
|
|
730
|
+
// src/ingest/gemini.ts
|
|
731
|
+
init_database();
|
|
732
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
|
|
733
|
+
import { homedir as homedir4 } from "os";
|
|
734
|
+
import { join as join4 } from "path";
|
|
735
|
+
var GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
|
|
736
|
+
async function ingestGemini(db, verbose) {
|
|
737
|
+
if (!existsSync4(GEMINI_TMP_DIR)) {
|
|
738
|
+
if (verbose)
|
|
739
|
+
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
740
|
+
return { sessions: 0 };
|
|
741
|
+
}
|
|
742
|
+
let totalSessions = 0;
|
|
743
|
+
const touchedSessions = new Set;
|
|
744
|
+
let projectHashDirs = [];
|
|
745
|
+
try {
|
|
746
|
+
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));
|
|
747
|
+
} catch {
|
|
748
|
+
return { sessions: 0 };
|
|
749
|
+
}
|
|
750
|
+
for (const projectDir of projectHashDirs) {
|
|
751
|
+
const chatsDir = join4(projectDir, "chats");
|
|
752
|
+
if (!existsSync4(chatsDir))
|
|
753
|
+
continue;
|
|
754
|
+
let chatFiles = [];
|
|
755
|
+
try {
|
|
756
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join4(chatsDir, f));
|
|
757
|
+
} catch {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
for (const filePath of chatFiles) {
|
|
761
|
+
const stateKey = filePath.replace(homedir4(), "~");
|
|
762
|
+
let fileMtime = "0";
|
|
763
|
+
try {
|
|
764
|
+
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
765
|
+
} catch {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
const processed = getIngestState(db, "gemini", stateKey);
|
|
769
|
+
if (processed === fileMtime)
|
|
770
|
+
continue;
|
|
771
|
+
let chatData;
|
|
772
|
+
try {
|
|
773
|
+
chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
774
|
+
} catch {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const sessionId = chatData.sessionId;
|
|
778
|
+
if (!sessionId)
|
|
779
|
+
continue;
|
|
780
|
+
const startTime = chatData.startTime ?? new Date().toISOString();
|
|
781
|
+
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
782
|
+
if (!existing) {
|
|
783
|
+
const session = {
|
|
784
|
+
id: sessionId,
|
|
785
|
+
agent: "gemini",
|
|
786
|
+
project_path: "",
|
|
787
|
+
project_name: "",
|
|
788
|
+
started_at: startTime,
|
|
789
|
+
ended_at: chatData.lastUpdated ?? null,
|
|
790
|
+
total_cost_usd: 0,
|
|
791
|
+
total_tokens: 0,
|
|
792
|
+
request_count: 0
|
|
793
|
+
};
|
|
794
|
+
upsertSession(db, session);
|
|
795
|
+
touchedSessions.add(sessionId);
|
|
796
|
+
totalSessions++;
|
|
797
|
+
}
|
|
798
|
+
setIngestState(db, "gemini", stateKey, fileMtime);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
for (const sessionId of touchedSessions) {
|
|
802
|
+
rollupSession(db, sessionId);
|
|
803
|
+
}
|
|
804
|
+
return { sessions: totalSessions };
|
|
805
|
+
}
|
|
806
|
+
|
|
729
807
|
// src/server/serve.ts
|
|
730
808
|
init_pricing();
|
|
731
809
|
import { randomUUID } from "crypto";
|
|
@@ -746,6 +824,20 @@ function ok(data, meta) {
|
|
|
746
824
|
function err(message, status = 400) {
|
|
747
825
|
return json({ error: message }, status);
|
|
748
826
|
}
|
|
827
|
+
function normalizeBudgetPeriod(value) {
|
|
828
|
+
switch (value) {
|
|
829
|
+
case "day":
|
|
830
|
+
case "daily":
|
|
831
|
+
return "daily";
|
|
832
|
+
case "week":
|
|
833
|
+
case "weekly":
|
|
834
|
+
return "weekly";
|
|
835
|
+
case "month":
|
|
836
|
+
case "monthly":
|
|
837
|
+
default:
|
|
838
|
+
return "monthly";
|
|
839
|
+
}
|
|
840
|
+
}
|
|
749
841
|
function applyFields(obj, fields) {
|
|
750
842
|
if (!fields || fields.length === 0)
|
|
751
843
|
return obj;
|
|
@@ -771,12 +863,20 @@ function createHandler(db) {
|
|
|
771
863
|
if (path === "/api/sessions" && method === "GET") {
|
|
772
864
|
const agent = url.searchParams.get("agent");
|
|
773
865
|
const project = url.searchParams.get("project") ?? undefined;
|
|
866
|
+
const search = url.searchParams.get("search") ?? undefined;
|
|
774
867
|
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
775
868
|
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
776
869
|
const since = url.searchParams.get("since") ?? undefined;
|
|
777
870
|
const fieldsParam = url.searchParams.get("fields");
|
|
778
871
|
const fields = fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
779
|
-
const sessions = querySessions(db, {
|
|
872
|
+
const sessions = querySessions(db, {
|
|
873
|
+
agent: agent ?? undefined,
|
|
874
|
+
project,
|
|
875
|
+
search,
|
|
876
|
+
limit,
|
|
877
|
+
offset,
|
|
878
|
+
since
|
|
879
|
+
});
|
|
780
880
|
return ok(fields ? sessions.map((s) => applyFields(s, fields)) : sessions, { limit, offset });
|
|
781
881
|
}
|
|
782
882
|
if (path === "/api/top" && method === "GET") {
|
|
@@ -804,7 +904,7 @@ function createHandler(db) {
|
|
|
804
904
|
id: randomUUID(),
|
|
805
905
|
project_path: body["project_path"] ?? null,
|
|
806
906
|
agent: body["agent"] ?? null,
|
|
807
|
-
period: body["period"]
|
|
907
|
+
period: normalizeBudgetPeriod(body["period"]),
|
|
808
908
|
limit_usd: Number(body["limit_usd"]),
|
|
809
909
|
alert_at_percent: Number(body["alert_at_percent"] ?? 80),
|
|
810
910
|
created_at: now,
|
|
@@ -867,6 +967,8 @@ function createHandler(db) {
|
|
|
867
967
|
results["claude"] = await ingestClaude(db);
|
|
868
968
|
if (sources === "all" || sources === "codex")
|
|
869
969
|
results["codex"] = await ingestCodex(db);
|
|
970
|
+
if (sources === "all" || sources === "gemini")
|
|
971
|
+
results["gemini"] = await ingestGemini(db);
|
|
870
972
|
return ok(results);
|
|
871
973
|
}
|
|
872
974
|
const sessionRequestsMatch = path.match(/^\/api\/sessions\/([^/]+)\/requests$/);
|
|
@@ -916,15 +1018,15 @@ function startServer(port = 3456) {
|
|
|
916
1018
|
return apiHandler(req);
|
|
917
1019
|
}
|
|
918
1020
|
try {
|
|
919
|
-
const { existsSync:
|
|
920
|
-
if (
|
|
1021
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
1022
|
+
if (existsSync5(dashboardDir)) {
|
|
921
1023
|
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
922
1024
|
const fullPath = dashboardDir + filePath;
|
|
923
|
-
if (
|
|
1025
|
+
if (existsSync5(fullPath)) {
|
|
924
1026
|
return new Response(Bun.file(fullPath));
|
|
925
1027
|
}
|
|
926
1028
|
const indexPath = dashboardDir + "/index.html";
|
|
927
|
-
if (
|
|
1029
|
+
if (existsSync5(indexPath)) {
|
|
928
1030
|
return new Response(Bun.file(indexPath));
|
|
929
1031
|
}
|
|
930
1032
|
}
|
|
@@ -935,6 +1037,62 @@ function startServer(port = 3456) {
|
|
|
935
1037
|
console.log(`economy-serve listening on http://localhost:${port}`);
|
|
936
1038
|
}
|
|
937
1039
|
|
|
1040
|
+
// src/lib/package-metadata.ts
|
|
1041
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1042
|
+
var cachedMetadata = null;
|
|
1043
|
+
function getPackageMetadata() {
|
|
1044
|
+
if (cachedMetadata)
|
|
1045
|
+
return cachedMetadata;
|
|
1046
|
+
const raw = readFileSync4(new URL("../../package.json", import.meta.url), "utf8");
|
|
1047
|
+
const parsed = JSON.parse(raw);
|
|
1048
|
+
cachedMetadata = {
|
|
1049
|
+
name: parsed.name ?? "@hasna/economy",
|
|
1050
|
+
version: parsed.version ?? "0.0.0"
|
|
1051
|
+
};
|
|
1052
|
+
return cachedMetadata;
|
|
1053
|
+
}
|
|
1054
|
+
var packageMetadata = getPackageMetadata();
|
|
1055
|
+
|
|
938
1056
|
// src/server/index.ts
|
|
939
|
-
|
|
940
|
-
|
|
1057
|
+
function printHelp() {
|
|
1058
|
+
console.log(`Usage: economy-serve [options]
|
|
1059
|
+
|
|
1060
|
+
REST API server for ${packageMetadata.name}
|
|
1061
|
+
|
|
1062
|
+
Options:
|
|
1063
|
+
-p, --port <port> Port to bind (default: ECONOMY_PORT or 3456)
|
|
1064
|
+
-V, --version output the version number
|
|
1065
|
+
-h, --help display help for command`);
|
|
1066
|
+
}
|
|
1067
|
+
function resolvePort(argv) {
|
|
1068
|
+
for (let i = 0;i < argv.length; i++) {
|
|
1069
|
+
const arg = argv[i];
|
|
1070
|
+
if ((arg === "--port" || arg === "-p") && argv[i + 1]) {
|
|
1071
|
+
const value2 = Number(argv[i + 1]);
|
|
1072
|
+
if (!Number.isFinite(value2) || value2 <= 0) {
|
|
1073
|
+
throw new Error(`Invalid port: ${argv[i + 1]}`);
|
|
1074
|
+
}
|
|
1075
|
+
return value2;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
const value = Number(process.env["ECONOMY_PORT"] ?? 3456);
|
|
1079
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1080
|
+
throw new Error(`Invalid ECONOMY_PORT: ${process.env["ECONOMY_PORT"]}`);
|
|
1081
|
+
}
|
|
1082
|
+
return value;
|
|
1083
|
+
}
|
|
1084
|
+
var args = process.argv.slice(2);
|
|
1085
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1086
|
+
printHelp();
|
|
1087
|
+
process.exit(0);
|
|
1088
|
+
}
|
|
1089
|
+
if (args.includes("--version") || args.includes("-V")) {
|
|
1090
|
+
console.log(packageMetadata.version);
|
|
1091
|
+
process.exit(0);
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
startServer(resolvePort(args));
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1097
|
+
process.exit(1);
|
|
1098
|
+
}
|
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;AA2D7D,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CAwL/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,GAAG,IAAI,CAuC7C"}
|
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.11",
|
|
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
|
+
}
|