@hasna/economy 0.2.7 → 0.2.9

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/mcp/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
3
  var __defProp = Object.defineProperty;
4
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
4
5
  var __returnValue = (v) => v;
5
6
  function __exportSetter(name, newValue) {
6
7
  this[name] = __returnValue.bind(null, newValue);
@@ -107,12 +108,32 @@ var init_pricing = __esm(() => {
107
108
  });
108
109
 
109
110
  // src/db/database.ts
110
- import { Database } from "bun:sqlite";
111
- import { existsSync, mkdirSync } from "fs";
111
+ import { SqliteAdapter as Database } from "@hasna/cloud";
112
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
112
113
  import { homedir } from "os";
113
114
  import { join } from "path";
115
+ function getDataDir() {
116
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
117
+ const newDir = join(home, ".hasna", "economy");
118
+ const oldDir = join(home, ".economy");
119
+ if (existsSync(oldDir) && !existsSync(newDir)) {
120
+ mkdirSync(newDir, { recursive: true });
121
+ for (const file of readdirSync(oldDir)) {
122
+ const oldPath = join(oldDir, file);
123
+ if (statSync(oldPath).isFile()) {
124
+ copyFileSync(oldPath, join(newDir, file));
125
+ }
126
+ }
127
+ }
128
+ mkdirSync(newDir, { recursive: true });
129
+ return newDir;
130
+ }
114
131
  function getDbPath() {
115
- return process.env["ECONOMY_DB"] ?? join(homedir(), ".economy", "economy.db");
132
+ if (process.env["HASNA_ECONOMY_DB_PATH"])
133
+ return process.env["HASNA_ECONOMY_DB_PATH"];
134
+ if (process.env["ECONOMY_DB"])
135
+ return process.env["ECONOMY_DB"];
136
+ return join(getDataDir(), "economy.db");
116
137
  }
117
138
  function openDatabase(dbPath, skipSeed = false) {
118
139
  const path = dbPath ?? getDbPath();
@@ -211,12 +232,24 @@ function initSchema(db) {
211
232
  cache_write_per_1m REAL NOT NULL DEFAULT 0,
212
233
  updated_at TEXT NOT NULL
213
234
  );
235
+
236
+ CREATE TABLE IF NOT EXISTS feedback (
237
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
238
+ message TEXT NOT NULL,
239
+ email TEXT,
240
+ category TEXT DEFAULT 'general',
241
+ version TEXT,
242
+ machine_id TEXT,
243
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
244
+ );
214
245
  `);
215
246
  }
216
247
  function periodWhere(period) {
217
248
  switch (period) {
218
249
  case "today":
219
250
  return `DATE(timestamp) = DATE('now')`;
251
+ case "yesterday":
252
+ return `DATE(timestamp) = DATE('now', '-1 day')`;
220
253
  case "week":
221
254
  return `timestamp >= DATE('now', '-7 days')`;
222
255
  case "month":
@@ -231,6 +264,8 @@ function sessionPeriodWhere(period) {
231
264
  switch (period) {
232
265
  case "today":
233
266
  return `DATE(started_at) = DATE('now')`;
267
+ case "yesterday":
268
+ return `DATE(started_at) = DATE('now', '-1 day')`;
234
269
  case "week":
235
270
  return `started_at >= DATE('now', '-7 days')`;
236
271
  case "month":
@@ -472,6 +507,75 @@ function seedModelPricing(db, defaults) {
472
507
  }
473
508
  var init_database = () => {};
474
509
 
510
+ // package.json
511
+ var require_package = __commonJS((exports, module) => {
512
+ module.exports = {
513
+ name: "@hasna/economy",
514
+ version: "0.2.8",
515
+ description: "AI coding cost tracker \u2014 CLI + MCP server + REST API + web dashboard for Claude Code, Codex, and Gemini",
516
+ type: "module",
517
+ main: "dist/index.js",
518
+ types: "dist/index.d.ts",
519
+ bin: {
520
+ economy: "dist/cli/index.js",
521
+ "economy-mcp": "dist/mcp/index.js",
522
+ "economy-serve": "dist/server/index.js"
523
+ },
524
+ exports: {
525
+ ".": {
526
+ types: "./dist/index.d.ts",
527
+ import: "./dist/index.js"
528
+ }
529
+ },
530
+ files: [
531
+ "dist",
532
+ "LICENSE"
533
+ ],
534
+ scripts: {
535
+ build: "cd dashboard && bun run build && cd .. && bun build src/cli/index.ts --outdir dist/cli --target bun --packages external && bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/index.ts --outdir dist --target bun --packages external && tsc --emitDeclarationOnly --outDir dist",
536
+ "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
537
+ "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
538
+ "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
539
+ "build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
540
+ "build:dashboard": "cd dashboard && bun run build",
541
+ typecheck: "tsc --noEmit",
542
+ test: "bun test",
543
+ "dev:cli": "bun run src/cli/index.ts",
544
+ "dev:mcp": "bun run src/mcp/index.ts",
545
+ "dev:serve": "bun run src/server/index.ts"
546
+ },
547
+ keywords: [
548
+ "economy",
549
+ "cost",
550
+ "ai",
551
+ "claude",
552
+ "codex",
553
+ "gemini",
554
+ "mcp",
555
+ "cli",
556
+ "budget",
557
+ "tracking"
558
+ ],
559
+ author: "hasna",
560
+ license: "Apache-2.0",
561
+ publishConfig: {
562
+ registry: "https://registry.npmjs.org",
563
+ access: "public"
564
+ },
565
+ dependencies: {
566
+ "@hasna/cloud": "^0.1.0",
567
+ "@modelcontextprotocol/sdk": "^1.12.1",
568
+ chalk: "^5.4.1",
569
+ commander: "^13.1.0"
570
+ },
571
+ devDependencies: {
572
+ "@types/bun": "latest",
573
+ "bun-types": "latest",
574
+ typescript: "^5.7.2"
575
+ }
576
+ };
577
+ });
578
+
475
579
  // src/mcp/index.ts
476
580
  init_database();
477
581
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -481,7 +585,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
481
585
  // src/ingest/claude.ts
482
586
  init_database();
483
587
  init_pricing();
484
- import { readdirSync, readFileSync, existsSync as existsSync2, statSync } from "fs";
588
+ import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
485
589
  import { homedir as homedir2 } from "os";
486
590
  import { join as join2, basename } from "path";
487
591
  function autoDetectProject(cwd, projects) {
@@ -495,7 +599,7 @@ function collectJsonlFiles(projectDir) {
495
599
  const files = [];
496
600
  function walk(dir) {
497
601
  try {
498
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
602
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
499
603
  if (entry.isDirectory())
500
604
  walk(join2(dir, entry.name));
501
605
  else if (entry.name.endsWith(".jsonl"))
@@ -516,7 +620,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
516
620
  let totalRequests = 0;
517
621
  const touchedSessions = new Set;
518
622
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
519
- const projectDirs = readdirSync(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
623
+ const projectDirs = readdirSync2(PROJECTS_DIR, { withFileTypes: true }).filter((d) => d.isDirectory());
520
624
  for (const projectDirEntry of projectDirs) {
521
625
  const projectDirPath = join2(PROJECTS_DIR, projectDirEntry.name);
522
626
  const projectPath = dirNameToPath(projectDirEntry.name);
@@ -525,7 +629,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
525
629
  const stateKey = filePath.replace(PROJECTS_DIR, "");
526
630
  let fileMtime = "0";
527
631
  try {
528
- fileMtime = statSync(filePath).mtimeMs.toString();
632
+ fileMtime = statSync2(filePath).mtimeMs.toString();
529
633
  } catch {
530
634
  continue;
531
635
  }
@@ -670,7 +774,7 @@ async function ingestCodex(db, verbose = false) {
670
774
 
671
775
  // src/ingest/gemini.ts
672
776
  init_database();
673
- import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync2 } from "fs";
777
+ import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
674
778
  import { homedir as homedir4 } from "os";
675
779
  import { join as join4 } from "path";
676
780
  var GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
@@ -684,7 +788,7 @@ async function ingestGemini(db, verbose) {
684
788
  const touchedSessions = new Set;
685
789
  let projectHashDirs = [];
686
790
  try {
687
- projectHashDirs = readdirSync2(GEMINI_TMP_DIR, { withFileTypes: true }).filter((d) => d.isDirectory() && /^[0-9a-f]{64}$/.test(d.name)).map((d) => join4(GEMINI_TMP_DIR, d.name));
791
+ 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));
688
792
  } catch {
689
793
  return { sessions: 0 };
690
794
  }
@@ -694,7 +798,7 @@ async function ingestGemini(db, verbose) {
694
798
  continue;
695
799
  let chatFiles = [];
696
800
  try {
697
- chatFiles = readdirSync2(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join4(chatsDir, f));
801
+ chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join4(chatsDir, f));
698
802
  } catch {
699
803
  continue;
700
804
  }
@@ -702,7 +806,7 @@ async function ingestGemini(db, verbose) {
702
806
  const stateKey = filePath.replace(homedir4(), "~");
703
807
  let fileMtime = "0";
704
808
  try {
705
- fileMtime = statSync2(filePath).mtimeMs.toString();
809
+ fileMtime = statSync3(filePath).mtimeMs.toString();
706
810
  } catch {
707
811
  continue;
708
812
  }
@@ -777,7 +881,8 @@ var TOOLS = [
777
881
  { name: "remove_goal", description: "Delete a goal by id.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
778
882
  { name: "register_agent", description: "Register agent session.", inputSchema: { type: "object", properties: { name: { type: "string" }, session_id: { type: "string" } }, required: ["name"] } },
779
883
  { name: "heartbeat", description: "Update last_seen_at.", inputSchema: { type: "object", properties: { agent_id: { type: "string" } }, required: ["agent_id"] } },
780
- { name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } }
884
+ { name: "set_focus", description: "Set active project context.", inputSchema: { type: "object", properties: { agent_id: { type: "string" }, project_id: { type: "string" } }, required: ["agent_id"] } },
885
+ { name: "send_feedback", description: "Send feedback about this service.", inputSchema: { type: "object", properties: { message: { type: "string" }, email: { type: "string" }, category: { type: "string", enum: ["bug", "feature", "general"] } }, required: ["message"] } }
781
886
  ];
782
887
  var TOOL_DESCRIPTIONS = {
783
888
  get_cost_summary: "period(today|week|month|year|all) \u2192 {total_usd, sessions, requests, tokens, summary}",
@@ -995,6 +1100,15 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
995
1100
  ag["project_id"] = args["project_id"];
996
1101
  return { content: [{ type: "text", text: String(args["project_id"] ? `Focus: ${args["project_id"]}` : "Focus cleared") }] };
997
1102
  }
1103
+ case "send_feedback": {
1104
+ try {
1105
+ const pkg = require_package();
1106
+ db.prepare("INSERT INTO feedback (message, email, category, version) VALUES (?, ?, ?, ?)").run(String(a["message"]), a["email"] || null, a["category"] || "general", pkg.version);
1107
+ return { content: [{ type: "text", text: "Feedback saved. Thank you!" }] };
1108
+ } catch (e) {
1109
+ return { content: [{ type: "text", text: String(e) }], isError: true };
1110
+ }
1111
+ }
998
1112
  default:
999
1113
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
1000
1114
  }