@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.
@@ -1,2 +1,3 @@
1
+ #!/usr/bin/env bun
1
2
  export {};
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -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 Database2 } from "bun:sqlite";
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 Database2(CODEX_DB_PATH, { readonly: true });
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, { agent: agent ?? undefined, project, limit, offset, since });
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"] ?? "monthly",
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: existsSync4 } = await import("fs");
920
- if (existsSync4(dashboardDir)) {
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 (existsSync4(fullPath)) {
1025
+ if (existsSync5(fullPath)) {
924
1026
  return new Response(Bun.file(fullPath));
925
1027
  }
926
1028
  const indexPath = dashboardDir + "/index.html";
927
- if (existsSync4(indexPath)) {
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
- var port = Number(process.env["ECONOMY_PORT"] ?? 3456);
940
- startServer(port);
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
+ }
@@ -1,4 +1,4 @@
1
- import type { Database } from 'bun:sqlite';
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,YAAY,CAAA;AA2C1C,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,IACV,KAAK,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC,CA+K/D;AAED,wBAAgB,WAAW,CAAC,IAAI,SAAO,GAAG,IAAI,CAuC7C"}
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.10",
4
- "description": "AI coding cost tracker \u2014 CLI + MCP server + REST API + web dashboard for Claude Code, Codex, and Gemini",
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.0",
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
+ }