@hasna/economy 0.2.7 → 0.2.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/dist/cli/index.js +595 -208
- package/dist/index.js +1010 -0
- package/dist/mcp/index.js +126 -12
- package/dist/server/index.js +940 -0
- package/package.json +2 -1
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 "
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
}
|