@hasna/economy 0.2.11 → 0.2.13
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 +367 -32
- 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 +81 -26
- package/dist/ingest/claude.d.ts +5 -0
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/mcp/index.js +207 -39
- package/dist/server/index.js +90 -28
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -108,8 +108,17 @@ var init_pricing = __esm(() => {
|
|
|
108
108
|
// src/db/database.ts
|
|
109
109
|
import { SqliteAdapter as Database } from "@hasna/cloud";
|
|
110
110
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
111
|
+
import { hostname } from "os";
|
|
111
112
|
import { homedir } from "os";
|
|
112
113
|
import { join } from "path";
|
|
114
|
+
function getMachineId() {
|
|
115
|
+
if (process.env["ECONOMY_MACHINE_ID"])
|
|
116
|
+
return process.env["ECONOMY_MACHINE_ID"];
|
|
117
|
+
const h = hostname().toLowerCase();
|
|
118
|
+
if (h.startsWith("spark") || h.startsWith("apple"))
|
|
119
|
+
return h.split(".")[0];
|
|
120
|
+
return h.split(".")[0];
|
|
121
|
+
}
|
|
113
122
|
function getDataDir() {
|
|
114
123
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
115
124
|
const newDir = join(home, ".hasna", "economy");
|
|
@@ -142,6 +151,7 @@ function openDatabase(dbPath, skipSeed = false) {
|
|
|
142
151
|
}
|
|
143
152
|
const db = new Database(path);
|
|
144
153
|
db.exec("PRAGMA journal_mode = WAL");
|
|
154
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
145
155
|
db.exec("PRAGMA foreign_keys = ON");
|
|
146
156
|
initSchema(db);
|
|
147
157
|
if (!skipSeed) {
|
|
@@ -163,7 +173,8 @@ function initSchema(db) {
|
|
|
163
173
|
cost_usd REAL NOT NULL DEFAULT 0,
|
|
164
174
|
duration_ms INTEGER DEFAULT 0,
|
|
165
175
|
timestamp TEXT NOT NULL,
|
|
166
|
-
source_request_id TEXT
|
|
176
|
+
source_request_id TEXT,
|
|
177
|
+
machine_id TEXT DEFAULT ''
|
|
167
178
|
);
|
|
168
179
|
|
|
169
180
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -175,7 +186,8 @@ function initSchema(db) {
|
|
|
175
186
|
ended_at TEXT,
|
|
176
187
|
total_cost_usd REAL DEFAULT 0,
|
|
177
188
|
total_tokens INTEGER DEFAULT 0,
|
|
178
|
-
request_count INTEGER DEFAULT 0
|
|
189
|
+
request_count INTEGER DEFAULT 0,
|
|
190
|
+
machine_id TEXT DEFAULT ''
|
|
179
191
|
);
|
|
180
192
|
|
|
181
193
|
CREATE TABLE IF NOT EXISTS projects (
|
|
@@ -241,6 +253,15 @@ function initSchema(db) {
|
|
|
241
253
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
242
254
|
);
|
|
243
255
|
`);
|
|
256
|
+
const cols = db.prepare(`PRAGMA table_info(requests)`).all();
|
|
257
|
+
if (!cols.some((c) => c.name === "machine_id")) {
|
|
258
|
+
db.exec(`ALTER TABLE requests ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
259
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN machine_id TEXT DEFAULT ''`);
|
|
260
|
+
}
|
|
261
|
+
db.exec(`
|
|
262
|
+
CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id);
|
|
263
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id);
|
|
264
|
+
`);
|
|
244
265
|
}
|
|
245
266
|
function periodWhere(period) {
|
|
246
267
|
switch (period) {
|
|
@@ -279,17 +300,17 @@ function upsertRequest(db, req) {
|
|
|
279
300
|
INSERT OR REPLACE INTO requests
|
|
280
301
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
281
302
|
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);
|
|
303
|
+
timestamp, source_request_id, machine_id)
|
|
304
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
305
|
+
`).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
306
|
}
|
|
286
307
|
function upsertSession(db, session) {
|
|
287
308
|
db.prepare(`
|
|
288
309
|
INSERT OR REPLACE INTO sessions
|
|
289
310
|
(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);
|
|
311
|
+
total_cost_usd, total_tokens, request_count, machine_id)
|
|
312
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
313
|
+
`).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
314
|
}
|
|
294
315
|
function rollupSession(db, sessionId) {
|
|
295
316
|
db.prepare(`
|
|
@@ -319,6 +340,10 @@ function querySessions(db, filter = {}) {
|
|
|
319
340
|
conditions.push("started_at >= ?");
|
|
320
341
|
params.push(filter.since);
|
|
321
342
|
}
|
|
343
|
+
if (filter.machine) {
|
|
344
|
+
conditions.push("machine_id = ?");
|
|
345
|
+
params.push(filter.machine);
|
|
346
|
+
}
|
|
322
347
|
if (filter.search) {
|
|
323
348
|
const q = `%${filter.search}%`;
|
|
324
349
|
conditions.push("(project_name LIKE ? OR agent LIKE ? OR id LIKE ?)");
|
|
@@ -337,24 +362,25 @@ function queryTopSessions(db, n = 10, agent) {
|
|
|
337
362
|
}
|
|
338
363
|
return db.prepare(`SELECT * FROM sessions ORDER BY total_cost_usd DESC LIMIT ?`).all(n);
|
|
339
364
|
}
|
|
340
|
-
function querySummary(db, period) {
|
|
365
|
+
function querySummary(db, period, machine) {
|
|
341
366
|
const rWhere = periodWhere(period);
|
|
342
367
|
const sWhere = sessionPeriodWhere(period);
|
|
368
|
+
const machineClause = machine ? ` AND machine_id = '${machine.replace(/'/g, "''")}'` : "";
|
|
343
369
|
const r = db.prepare(`
|
|
344
370
|
SELECT COALESCE(SUM(cost_usd), 0) as total_usd,
|
|
345
371
|
COUNT(*) as requests,
|
|
346
372
|
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) as tokens
|
|
347
|
-
FROM requests WHERE ${rWhere}
|
|
373
|
+
FROM requests WHERE ${rWhere}${machineClause}
|
|
348
374
|
`).get();
|
|
349
375
|
const codexTotals = db.prepare(`
|
|
350
376
|
SELECT COALESCE(SUM(total_cost_usd), 0) as cost_usd,
|
|
351
377
|
COALESCE(SUM(total_tokens), 0) as tokens,
|
|
352
378
|
COUNT(*) as sessions
|
|
353
379
|
FROM sessions
|
|
354
|
-
WHERE ${sWhere}
|
|
380
|
+
WHERE ${sWhere}${machineClause}
|
|
355
381
|
AND id NOT IN (SELECT DISTINCT session_id FROM requests)
|
|
356
382
|
`).get();
|
|
357
|
-
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}`).get();
|
|
383
|
+
const sessionCount = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE ${sWhere}${machineClause}`).get();
|
|
358
384
|
return {
|
|
359
385
|
total_usd: r.total_usd + codexTotals.cost_usd,
|
|
360
386
|
requests: r.requests,
|
|
@@ -477,6 +503,20 @@ function getIngestState(db, source, key) {
|
|
|
477
503
|
function setIngestState(db, source, key, value) {
|
|
478
504
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
479
505
|
}
|
|
506
|
+
function listMachines(db) {
|
|
507
|
+
return db.prepare(`
|
|
508
|
+
SELECT
|
|
509
|
+
s.machine_id,
|
|
510
|
+
COUNT(DISTINCT s.id) as sessions,
|
|
511
|
+
COALESCE((SELECT COUNT(*) FROM requests r WHERE r.machine_id = s.machine_id), 0) as requests,
|
|
512
|
+
COALESCE(SUM(s.total_cost_usd), 0) as total_cost_usd,
|
|
513
|
+
MAX(s.started_at) as last_active
|
|
514
|
+
FROM sessions s
|
|
515
|
+
WHERE s.machine_id != ''
|
|
516
|
+
GROUP BY s.machine_id
|
|
517
|
+
ORDER BY total_cost_usd DESC
|
|
518
|
+
`).all();
|
|
519
|
+
}
|
|
480
520
|
function upsertModelPricing(db, p) {
|
|
481
521
|
db.prepare(`
|
|
482
522
|
INSERT OR REPLACE INTO model_pricing
|
|
@@ -513,6 +553,95 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
513
553
|
import { registerCloudTools } from "@hasna/cloud";
|
|
514
554
|
import { z } from "zod";
|
|
515
555
|
|
|
556
|
+
// src/db/pg-migrations.ts
|
|
557
|
+
var PG_MIGRATIONS = [
|
|
558
|
+
`CREATE TABLE IF NOT EXISTS requests (
|
|
559
|
+
id TEXT PRIMARY KEY,
|
|
560
|
+
agent TEXT NOT NULL,
|
|
561
|
+
session_id TEXT NOT NULL,
|
|
562
|
+
model TEXT NOT NULL,
|
|
563
|
+
input_tokens INTEGER DEFAULT 0,
|
|
564
|
+
output_tokens INTEGER DEFAULT 0,
|
|
565
|
+
cache_read_tokens INTEGER DEFAULT 0,
|
|
566
|
+
cache_create_tokens INTEGER DEFAULT 0,
|
|
567
|
+
cost_usd REAL NOT NULL DEFAULT 0,
|
|
568
|
+
duration_ms INTEGER DEFAULT 0,
|
|
569
|
+
timestamp TEXT NOT NULL,
|
|
570
|
+
source_request_id TEXT,
|
|
571
|
+
machine_id TEXT DEFAULT ''
|
|
572
|
+
)`,
|
|
573
|
+
`CREATE TABLE IF NOT EXISTS sessions (
|
|
574
|
+
id TEXT PRIMARY KEY,
|
|
575
|
+
agent TEXT NOT NULL,
|
|
576
|
+
project_path TEXT DEFAULT '',
|
|
577
|
+
project_name TEXT DEFAULT '',
|
|
578
|
+
started_at TEXT NOT NULL,
|
|
579
|
+
ended_at TEXT,
|
|
580
|
+
total_cost_usd REAL DEFAULT 0,
|
|
581
|
+
total_tokens INTEGER DEFAULT 0,
|
|
582
|
+
request_count INTEGER DEFAULT 0,
|
|
583
|
+
machine_id TEXT DEFAULT ''
|
|
584
|
+
)`,
|
|
585
|
+
`CREATE TABLE IF NOT EXISTS projects (
|
|
586
|
+
id TEXT PRIMARY KEY,
|
|
587
|
+
path TEXT UNIQUE NOT NULL,
|
|
588
|
+
name TEXT NOT NULL,
|
|
589
|
+
description TEXT,
|
|
590
|
+
tags TEXT DEFAULT '[]',
|
|
591
|
+
created_at TEXT NOT NULL
|
|
592
|
+
)`,
|
|
593
|
+
`CREATE TABLE IF NOT EXISTS budgets (
|
|
594
|
+
id TEXT PRIMARY KEY,
|
|
595
|
+
project_path TEXT,
|
|
596
|
+
agent TEXT,
|
|
597
|
+
period TEXT NOT NULL,
|
|
598
|
+
limit_usd REAL NOT NULL,
|
|
599
|
+
alert_at_percent INTEGER DEFAULT 80,
|
|
600
|
+
created_at TEXT NOT NULL,
|
|
601
|
+
updated_at TEXT NOT NULL
|
|
602
|
+
)`,
|
|
603
|
+
`CREATE TABLE IF NOT EXISTS goals (
|
|
604
|
+
id TEXT PRIMARY KEY,
|
|
605
|
+
period TEXT NOT NULL,
|
|
606
|
+
project_path TEXT,
|
|
607
|
+
agent TEXT,
|
|
608
|
+
limit_usd REAL NOT NULL,
|
|
609
|
+
created_at TEXT NOT NULL,
|
|
610
|
+
updated_at TEXT NOT NULL
|
|
611
|
+
)`,
|
|
612
|
+
`CREATE TABLE IF NOT EXISTS ingest_state (
|
|
613
|
+
source TEXT NOT NULL,
|
|
614
|
+
key TEXT NOT NULL,
|
|
615
|
+
value TEXT NOT NULL,
|
|
616
|
+
PRIMARY KEY (source, key)
|
|
617
|
+
)`,
|
|
618
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id)`,
|
|
619
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)`,
|
|
620
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_agent ON requests(agent)`,
|
|
621
|
+
`CREATE INDEX IF NOT EXISTS idx_requests_machine ON requests(machine_id)`,
|
|
622
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_agent ON sessions(agent)`,
|
|
623
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path)`,
|
|
624
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_started ON sessions(started_at)`,
|
|
625
|
+
`CREATE INDEX IF NOT EXISTS idx_sessions_machine ON sessions(machine_id)`,
|
|
626
|
+
`CREATE TABLE IF NOT EXISTS model_pricing (
|
|
627
|
+
model TEXT PRIMARY KEY,
|
|
628
|
+
input_per_1m REAL NOT NULL DEFAULT 0,
|
|
629
|
+
output_per_1m REAL NOT NULL DEFAULT 0,
|
|
630
|
+
cache_read_per_1m REAL NOT NULL DEFAULT 0,
|
|
631
|
+
cache_write_per_1m REAL NOT NULL DEFAULT 0,
|
|
632
|
+
updated_at TEXT NOT NULL
|
|
633
|
+
)`,
|
|
634
|
+
`CREATE TABLE IF NOT EXISTS feedback (
|
|
635
|
+
id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
|
|
636
|
+
message TEXT NOT NULL,
|
|
637
|
+
email TEXT,
|
|
638
|
+
category TEXT DEFAULT 'general',
|
|
639
|
+
version TEXT,
|
|
640
|
+
machine_id TEXT,
|
|
641
|
+
created_at TEXT NOT NULL DEFAULT NOW()::text
|
|
642
|
+
)`
|
|
643
|
+
];
|
|
644
|
+
|
|
516
645
|
// src/ingest/claude.ts
|
|
517
646
|
init_database();
|
|
518
647
|
init_pricing();
|
|
@@ -522,7 +651,8 @@ import { join as join2, basename } from "path";
|
|
|
522
651
|
function autoDetectProject(cwd, projects) {
|
|
523
652
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
524
653
|
}
|
|
525
|
-
var
|
|
654
|
+
var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
|
|
655
|
+
var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
|
|
526
656
|
function dirNameToPath(dirName) {
|
|
527
657
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
528
658
|
}
|
|
@@ -542,29 +672,36 @@ function collectJsonlFiles(projectDir) {
|
|
|
542
672
|
return files;
|
|
543
673
|
}
|
|
544
674
|
async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
545
|
-
|
|
675
|
+
return ingestJsonlProjects(db, CLAUDE_PROJECTS_DIR, "claude", verbose);
|
|
676
|
+
}
|
|
677
|
+
async function ingestTakumi(db, verbose = false) {
|
|
678
|
+
return ingestJsonlProjects(db, TAKUMI_PROJECTS_DIR, "takumi", verbose);
|
|
679
|
+
}
|
|
680
|
+
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
681
|
+
if (!existsSync2(projectsDir)) {
|
|
546
682
|
if (verbose)
|
|
547
|
-
console.log(
|
|
683
|
+
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
548
684
|
return { files: 0, requests: 0, sessions: 0 };
|
|
549
685
|
}
|
|
686
|
+
const machineId = getMachineId();
|
|
550
687
|
let totalFiles = 0;
|
|
551
688
|
let totalRequests = 0;
|
|
552
689
|
const touchedSessions = new Set;
|
|
553
690
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
554
|
-
const projectDirs = readdirSync2(
|
|
691
|
+
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
555
692
|
for (const projectDirEntry of projectDirs) {
|
|
556
|
-
const projectDirPath = join2(
|
|
693
|
+
const projectDirPath = join2(projectsDir, projectDirEntry.name);
|
|
557
694
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
558
695
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
559
696
|
for (const filePath of jsonlFiles) {
|
|
560
|
-
const stateKey = filePath.replace(
|
|
697
|
+
const stateKey = filePath.replace(projectsDir, "");
|
|
561
698
|
let fileMtime = "0";
|
|
562
699
|
try {
|
|
563
700
|
fileMtime = statSync2(filePath).mtimeMs.toString();
|
|
564
701
|
} catch {
|
|
565
702
|
continue;
|
|
566
703
|
}
|
|
567
|
-
const processed = getIngestState(db,
|
|
704
|
+
const processed = getIngestState(db, agentName, stateKey);
|
|
568
705
|
if (processed === fileMtime)
|
|
569
706
|
continue;
|
|
570
707
|
let lines;
|
|
@@ -605,10 +742,10 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
605
742
|
if (inputTokens + outputTokens + cacheWriteTokens === 0)
|
|
606
743
|
continue;
|
|
607
744
|
const costUsd = computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
608
|
-
const reqId =
|
|
745
|
+
const reqId = `${agentName}-${sessionId}-${timestamp}`;
|
|
609
746
|
upsertRequest(db, {
|
|
610
747
|
id: reqId,
|
|
611
|
-
agent:
|
|
748
|
+
agent: agentName,
|
|
612
749
|
session_id: sessionId,
|
|
613
750
|
model,
|
|
614
751
|
input_tokens: inputTokens,
|
|
@@ -618,7 +755,8 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
618
755
|
cost_usd: costUsd,
|
|
619
756
|
duration_ms: 0,
|
|
620
757
|
timestamp,
|
|
621
|
-
source_request_id: reqId
|
|
758
|
+
source_request_id: reqId,
|
|
759
|
+
machine_id: machineId
|
|
622
760
|
});
|
|
623
761
|
if (!touchedSessions.has(sessionId)) {
|
|
624
762
|
const existing = db.prepare(`SELECT id FROM sessions WHERE id = ?`).get(sessionId);
|
|
@@ -627,14 +765,15 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
627
765
|
const detectedProject = autoDetectProject(effectiveCwd, registeredProjects);
|
|
628
766
|
const session = {
|
|
629
767
|
id: sessionId,
|
|
630
|
-
agent:
|
|
768
|
+
agent: agentName,
|
|
631
769
|
project_path: detectedProject ? detectedProject.path : effectiveCwd,
|
|
632
770
|
project_name: detectedProject ? detectedProject.name : "",
|
|
633
771
|
started_at: timestamp,
|
|
634
772
|
ended_at: null,
|
|
635
773
|
total_cost_usd: 0,
|
|
636
774
|
total_tokens: 0,
|
|
637
|
-
request_count: 0
|
|
775
|
+
request_count: 0,
|
|
776
|
+
machine_id: machineId
|
|
638
777
|
};
|
|
639
778
|
upsertSession(db, session);
|
|
640
779
|
}
|
|
@@ -642,7 +781,7 @@ async function ingestClaude(db, verbose = false, _telemetryDir) {
|
|
|
642
781
|
}
|
|
643
782
|
totalRequests++;
|
|
644
783
|
}
|
|
645
|
-
setIngestState(db,
|
|
784
|
+
setIngestState(db, agentName, stateKey, fileMtime);
|
|
646
785
|
totalFiles++;
|
|
647
786
|
}
|
|
648
787
|
}
|
|
@@ -666,6 +805,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
666
805
|
console.log("Codex DB not found:", CODEX_DB_PATH);
|
|
667
806
|
return { sessions: 0 };
|
|
668
807
|
}
|
|
808
|
+
const machineId = getMachineId();
|
|
669
809
|
let codexDb = null;
|
|
670
810
|
let ingested = 0;
|
|
671
811
|
try {
|
|
@@ -690,7 +830,8 @@ async function ingestCodex(db, verbose = false) {
|
|
|
690
830
|
ended_at: endedAt,
|
|
691
831
|
total_cost_usd: costUsd,
|
|
692
832
|
total_tokens: thread.tokens_used,
|
|
693
|
-
request_count: 1
|
|
833
|
+
request_count: 1,
|
|
834
|
+
machine_id: machineId
|
|
694
835
|
});
|
|
695
836
|
setIngestState(db, "codex", stateKey, "done");
|
|
696
837
|
ingested++;
|
|
@@ -715,6 +856,7 @@ async function ingestGemini(db, verbose) {
|
|
|
715
856
|
console.log("Gemini tmp dir not found:", GEMINI_TMP_DIR);
|
|
716
857
|
return { sessions: 0 };
|
|
717
858
|
}
|
|
859
|
+
const machineId = getMachineId();
|
|
718
860
|
let totalSessions = 0;
|
|
719
861
|
const touchedSessions = new Set;
|
|
720
862
|
let projectHashDirs = [];
|
|
@@ -765,7 +907,8 @@ async function ingestGemini(db, verbose) {
|
|
|
765
907
|
ended_at: chatData.lastUpdated ?? null,
|
|
766
908
|
total_cost_usd: 0,
|
|
767
909
|
total_tokens: 0,
|
|
768
|
-
request_count: 0
|
|
910
|
+
request_count: 0,
|
|
911
|
+
machine_id: machineId
|
|
769
912
|
};
|
|
770
913
|
upsertSession(db, session);
|
|
771
914
|
touchedSessions.add(sessionId);
|
|
@@ -838,6 +981,7 @@ var TOOL_NAMES = [
|
|
|
838
981
|
"get_goals",
|
|
839
982
|
"set_goal",
|
|
840
983
|
"remove_goal",
|
|
984
|
+
"list_machines",
|
|
841
985
|
"register_agent",
|
|
842
986
|
"heartbeat",
|
|
843
987
|
"set_focus",
|
|
@@ -845,9 +989,10 @@ var TOOL_NAMES = [
|
|
|
845
989
|
"send_feedback"
|
|
846
990
|
];
|
|
847
991
|
var TOOL_DESCRIPTIONS = {
|
|
848
|
-
get_cost_summary: "period(today|week|month|year|all) -> {total_usd, sessions, requests, tokens, summary}",
|
|
849
|
-
get_sessions: "agent(claude|codex|gemini), project(partial), limit(20) -> compact session table",
|
|
992
|
+
get_cost_summary: "period(today|week|month|year|all), machine?(hostname) -> {total_usd, sessions, requests, tokens, summary}",
|
|
993
|
+
get_sessions: "agent(claude|codex|gemini), project(partial), machine?(hostname), limit(20) -> compact session table",
|
|
850
994
|
get_top_sessions: "n(10), agent(claude|codex|gemini) -> top sessions by cost",
|
|
995
|
+
list_machines: "no params -> machine_id, sessions, requests, cost, last_active",
|
|
851
996
|
get_model_breakdown: "no params -> model, requests, tokens, cost",
|
|
852
997
|
get_project_breakdown: "no params -> project_name, sessions, cost",
|
|
853
998
|
get_budget_status: "no params -> budget limits, current spend, percent_used, is_over_alert",
|
|
@@ -891,27 +1036,30 @@ server.tool("describe_tools", "Get param hints for specific tools by name.", { n
|
|
|
891
1036
|
`);
|
|
892
1037
|
return text(result);
|
|
893
1038
|
});
|
|
894
|
-
server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
1039
|
+
server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all. machine: filter by hostname.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), machine: z.string().optional() }, async ({ period, machine }) => {
|
|
895
1040
|
const resolved = period ?? "today";
|
|
896
|
-
const s = querySummary(db, resolved);
|
|
1041
|
+
const s = querySummary(db, resolved, machine);
|
|
1042
|
+
const machineLabel = machine ? ` on ${machine}` : "";
|
|
897
1043
|
return text([
|
|
898
|
-
`period: ${resolved}`,
|
|
1044
|
+
`period: ${resolved}${machineLabel}`,
|
|
899
1045
|
`cost: ${fmtUsd(s.total_usd)}`,
|
|
900
1046
|
`sessions: ${s.sessions}`,
|
|
901
1047
|
`requests: ${s.requests.toLocaleString()}`,
|
|
902
1048
|
`tokens: ${fmtTok(s.tokens)}`,
|
|
903
|
-
`summary: You've spent ${fmtUsd(s.total_usd)} ${resolved === "all" ? "total" : resolved} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
|
|
1049
|
+
`summary: You've spent ${fmtUsd(s.total_usd)} ${resolved === "all" ? "total" : resolved}${machineLabel} across ${s.sessions} sessions (${s.requests.toLocaleString()} requests, ${fmtTok(s.tokens)} tokens)`
|
|
904
1050
|
].join(`
|
|
905
1051
|
`));
|
|
906
1052
|
});
|
|
907
|
-
server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, limit(20)", {
|
|
908
|
-
agent: z.enum(["claude", "codex", "gemini"]).optional(),
|
|
1053
|
+
server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, machine, limit(20)", {
|
|
1054
|
+
agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional(),
|
|
909
1055
|
project: z.string().optional(),
|
|
1056
|
+
machine: z.string().optional(),
|
|
910
1057
|
limit: z.number().int().positive().max(100).optional()
|
|
911
|
-
}, async ({ agent, project, limit }) => {
|
|
1058
|
+
}, async ({ agent, project, machine, limit }) => {
|
|
912
1059
|
const sessions = querySessions(db, {
|
|
913
1060
|
agent,
|
|
914
1061
|
project,
|
|
1062
|
+
machine,
|
|
915
1063
|
limit: limit ?? 20
|
|
916
1064
|
});
|
|
917
1065
|
const lines = ["id agent cost tokens project"];
|
|
@@ -922,7 +1070,7 @@ server.tool("get_sessions", "List sessions. Returns compact table. Params: agent
|
|
|
922
1070
|
});
|
|
923
1071
|
server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
|
|
924
1072
|
n: z.number().int().positive().max(100).optional(),
|
|
925
|
-
agent: z.enum(["claude", "codex", "gemini"]).optional()
|
|
1073
|
+
agent: z.enum(["claude", "takumi", "codex", "gemini"]).optional()
|
|
926
1074
|
}, async ({ n, agent }) => {
|
|
927
1075
|
const sessions = queryTopSessions(db, n ?? 10, agent);
|
|
928
1076
|
const lines = ["rank id agent cost tokens project"];
|
|
@@ -1003,13 +1151,17 @@ server.tool("get_session_detail", "Per-request breakdown of a single session. Pa
|
|
|
1003
1151
|
return text(lines.join(`
|
|
1004
1152
|
`));
|
|
1005
1153
|
});
|
|
1006
|
-
server.tool("sync", "Ingest new cost data. sources: all|claude|codex|gemini", { sources: z.enum(["all", "claude", "codex", "gemini"]).optional() }, async ({ sources }) => {
|
|
1154
|
+
server.tool("sync", "Ingest new cost data. sources: all|claude|takumi|codex|gemini", { sources: z.enum(["all", "claude", "takumi", "codex", "gemini"]).optional() }, async ({ sources }) => {
|
|
1007
1155
|
const selected = sources ?? "all";
|
|
1008
1156
|
const parts = [];
|
|
1009
1157
|
if (selected === "all" || selected === "claude") {
|
|
1010
1158
|
const result = await ingestClaude(db);
|
|
1011
1159
|
parts.push(`claude: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
|
|
1012
1160
|
}
|
|
1161
|
+
if (selected === "all" || selected === "takumi") {
|
|
1162
|
+
const result = await ingestTakumi(db);
|
|
1163
|
+
parts.push(`takumi: ${result["files"]} files, ${result["requests"]} requests, ${result["sessions"]} sessions`);
|
|
1164
|
+
}
|
|
1013
1165
|
if (selected === "all" || selected === "codex") {
|
|
1014
1166
|
const result = await ingestCodex(db);
|
|
1015
1167
|
parts.push(`codex: ${result["sessions"]} sessions`);
|
|
@@ -1057,6 +1209,19 @@ server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({
|
|
|
1057
1209
|
deleteGoal(db, id);
|
|
1058
1210
|
return text("Goal removed.");
|
|
1059
1211
|
});
|
|
1212
|
+
server.tool("list_machines", "List all machines that have synced data. No params.", {}, async () => {
|
|
1213
|
+
const machines = listMachines(db);
|
|
1214
|
+
if (machines.length === 0)
|
|
1215
|
+
return text(`No machine data yet. Current machine: ${getMachineId()}`);
|
|
1216
|
+
const lines = ["machine sessions requests cost last_active"];
|
|
1217
|
+
for (const m of machines) {
|
|
1218
|
+
lines.push(`${m.machine_id.padEnd(17)}${String(m.sessions).padEnd(10)}${String(m.requests).padEnd(10)}${fmtUsd(m.total_cost_usd).padEnd(12)}${m.last_active?.substring(0, 16) ?? "\u2014"}`);
|
|
1219
|
+
}
|
|
1220
|
+
lines.push(`
|
|
1221
|
+
current machine: ${getMachineId()}`);
|
|
1222
|
+
return text(lines.join(`
|
|
1223
|
+
`));
|
|
1224
|
+
});
|
|
1060
1225
|
server.tool("register_agent", "Register agent session.", { name: z.string(), session_id: z.string().optional() }, async ({ name }) => {
|
|
1061
1226
|
const existing = [..._econAgents.values()].find((agent2) => agent2.name === name);
|
|
1062
1227
|
if (existing) {
|
|
@@ -1096,5 +1261,8 @@ server.tool("send_feedback", "Send feedback about this service.", {
|
|
|
1096
1261
|
}
|
|
1097
1262
|
});
|
|
1098
1263
|
var transport = new StdioServerTransport;
|
|
1099
|
-
registerCloudTools(server, "economy"
|
|
1264
|
+
registerCloudTools(server, "economy", {
|
|
1265
|
+
dbPath: getDbPath(),
|
|
1266
|
+
migrations: PG_MIGRATIONS
|
|
1267
|
+
});
|
|
1100
1268
|
await server.connect(transport);
|