@cognistore/mcp-server 1.2.2 → 1.3.0
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/index.js +432 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -288,6 +288,34 @@ CREATE TABLE IF NOT EXISTS plan_tasks (
|
|
|
288
288
|
);
|
|
289
289
|
CREATE INDEX IF NOT EXISTS idx_plan_tasks_plan ON plan_tasks(plan_id);
|
|
290
290
|
CREATE INDEX IF NOT EXISTS idx_plan_tasks_status ON plan_tasks(status);
|
|
291
|
+
`,
|
|
292
|
+
"1.3.0": `
|
|
293
|
+
CREATE TABLE IF NOT EXISTS token_usage (
|
|
294
|
+
id TEXT PRIMARY KEY,
|
|
295
|
+
source TEXT NOT NULL,
|
|
296
|
+
model TEXT NOT NULL,
|
|
297
|
+
project TEXT,
|
|
298
|
+
session_id TEXT,
|
|
299
|
+
message_id TEXT,
|
|
300
|
+
occurred_at TEXT NOT NULL,
|
|
301
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
302
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
303
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
304
|
+
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
|
305
|
+
scanned_at TEXT NOT NULL
|
|
306
|
+
);
|
|
307
|
+
CREATE INDEX IF NOT EXISTS idx_token_usage_occurred ON token_usage (occurred_at);
|
|
308
|
+
CREATE INDEX IF NOT EXISTS idx_token_usage_project ON token_usage (project);
|
|
309
|
+
CREATE INDEX IF NOT EXISTS idx_token_usage_source_model ON token_usage (source, model);
|
|
310
|
+
|
|
311
|
+
CREATE TABLE IF NOT EXISTS scan_state (
|
|
312
|
+
source TEXT NOT NULL,
|
|
313
|
+
file_path TEXT NOT NULL,
|
|
314
|
+
last_offset INTEGER NOT NULL DEFAULT 0,
|
|
315
|
+
last_mtime TEXT NOT NULL,
|
|
316
|
+
last_scanned_at TEXT NOT NULL,
|
|
317
|
+
PRIMARY KEY (source, file_path)
|
|
318
|
+
);
|
|
291
319
|
`
|
|
292
320
|
};
|
|
293
321
|
function runMigrations(sqlite, migrationsDir) {
|
|
@@ -1288,6 +1316,371 @@ var KnowledgeService = class {
|
|
|
1288
1316
|
}
|
|
1289
1317
|
};
|
|
1290
1318
|
|
|
1319
|
+
// ../../packages/core/dist/repositories/token-usage.repository.js
|
|
1320
|
+
var RANGE_CLAUSE = "occurred_at >= ? AND occurred_at <= ?";
|
|
1321
|
+
function applyOptionalFilters(filter) {
|
|
1322
|
+
const params = [filter.from, filter.to];
|
|
1323
|
+
let sql2 = RANGE_CLAUSE;
|
|
1324
|
+
if (filter.source) {
|
|
1325
|
+
sql2 += " AND source = ?";
|
|
1326
|
+
params.push(filter.source);
|
|
1327
|
+
}
|
|
1328
|
+
if (filter.model) {
|
|
1329
|
+
sql2 += " AND model = ?";
|
|
1330
|
+
params.push(filter.model);
|
|
1331
|
+
}
|
|
1332
|
+
if (filter.project) {
|
|
1333
|
+
sql2 += " AND project = ?";
|
|
1334
|
+
params.push(filter.project);
|
|
1335
|
+
}
|
|
1336
|
+
return { sql: sql2, params };
|
|
1337
|
+
}
|
|
1338
|
+
var TokenUsageRepository = class {
|
|
1339
|
+
sqlite;
|
|
1340
|
+
constructor(sqlite) {
|
|
1341
|
+
this.sqlite = sqlite;
|
|
1342
|
+
}
|
|
1343
|
+
// ─── Writes ────────────────────────────────────────────────────
|
|
1344
|
+
/** Insert if new; never throw on conflict (deterministic id makes this idempotent). */
|
|
1345
|
+
insertMany(records) {
|
|
1346
|
+
if (records.length === 0)
|
|
1347
|
+
return 0;
|
|
1348
|
+
const stmt = this.sqlite.prepare(`
|
|
1349
|
+
INSERT OR IGNORE INTO token_usage
|
|
1350
|
+
(id, source, model, project, session_id, message_id, occurred_at,
|
|
1351
|
+
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, scanned_at)
|
|
1352
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1353
|
+
`);
|
|
1354
|
+
const scannedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1355
|
+
const tx = this.sqlite.transaction((rows) => {
|
|
1356
|
+
let inserted = 0;
|
|
1357
|
+
for (const r of rows) {
|
|
1358
|
+
const info = stmt.run(r.id, r.source, r.model, r.project, r.sessionId, r.messageId, r.occurredAt, r.inputTokens, r.outputTokens, r.cacheReadTokens, r.cacheCreationTokens, scannedAt);
|
|
1359
|
+
if (info.changes > 0)
|
|
1360
|
+
inserted++;
|
|
1361
|
+
}
|
|
1362
|
+
return inserted;
|
|
1363
|
+
});
|
|
1364
|
+
return tx(records);
|
|
1365
|
+
}
|
|
1366
|
+
// ─── Scan state ────────────────────────────────────────────────
|
|
1367
|
+
getScanState(source, filePath) {
|
|
1368
|
+
const row = this.sqlite.prepare(`
|
|
1369
|
+
SELECT source, file_path as filePath, last_offset as lastOffset, last_mtime as lastMtime
|
|
1370
|
+
FROM scan_state WHERE source = ? AND file_path = ?
|
|
1371
|
+
`).get(source, filePath);
|
|
1372
|
+
return row;
|
|
1373
|
+
}
|
|
1374
|
+
upsertScanState(state) {
|
|
1375
|
+
this.sqlite.prepare(`
|
|
1376
|
+
INSERT INTO scan_state (source, file_path, last_offset, last_mtime, last_scanned_at)
|
|
1377
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1378
|
+
ON CONFLICT(source, file_path) DO UPDATE SET
|
|
1379
|
+
last_offset = excluded.last_offset,
|
|
1380
|
+
last_mtime = excluded.last_mtime,
|
|
1381
|
+
last_scanned_at = excluded.last_scanned_at
|
|
1382
|
+
`).run(state.source, state.filePath, state.lastOffset, state.lastMtime, (/* @__PURE__ */ new Date()).toISOString());
|
|
1383
|
+
}
|
|
1384
|
+
// ─── Aggregations ──────────────────────────────────────────────
|
|
1385
|
+
totals(filter) {
|
|
1386
|
+
const { sql: sql2, params } = applyOptionalFilters(filter);
|
|
1387
|
+
const row = this.sqlite.prepare(`
|
|
1388
|
+
SELECT
|
|
1389
|
+
COALESCE(SUM(input_tokens), 0) as inputTokens,
|
|
1390
|
+
COALESCE(SUM(output_tokens), 0) as outputTokens,
|
|
1391
|
+
COALESCE(SUM(cache_read_tokens), 0) as cacheReadTokens,
|
|
1392
|
+
COALESCE(SUM(cache_creation_tokens), 0) as cacheCreationTokens
|
|
1393
|
+
FROM token_usage WHERE ${sql2}
|
|
1394
|
+
`).get(...params);
|
|
1395
|
+
return row;
|
|
1396
|
+
}
|
|
1397
|
+
byDay(filter) {
|
|
1398
|
+
const { sql: sql2, params } = applyOptionalFilters(filter);
|
|
1399
|
+
const rows = this.sqlite.prepare(`
|
|
1400
|
+
SELECT
|
|
1401
|
+
date(occurred_at) as date,
|
|
1402
|
+
COALESCE(SUM(input_tokens), 0) as inputTokens,
|
|
1403
|
+
COALESCE(SUM(output_tokens), 0) as outputTokens,
|
|
1404
|
+
COALESCE(SUM(cache_read_tokens), 0) as cacheReadTokens,
|
|
1405
|
+
COALESCE(SUM(cache_creation_tokens), 0) as cacheCreationTokens
|
|
1406
|
+
FROM token_usage WHERE ${sql2}
|
|
1407
|
+
GROUP BY date(occurred_at)
|
|
1408
|
+
ORDER BY date(occurred_at)
|
|
1409
|
+
`).all(...params);
|
|
1410
|
+
return rows;
|
|
1411
|
+
}
|
|
1412
|
+
byModel(filter) {
|
|
1413
|
+
const { sql: sql2, params } = applyOptionalFilters(filter);
|
|
1414
|
+
const rows = this.sqlite.prepare(`
|
|
1415
|
+
SELECT
|
|
1416
|
+
model,
|
|
1417
|
+
COALESCE(SUM(input_tokens), 0) as inputTokens,
|
|
1418
|
+
COALESCE(SUM(output_tokens), 0) as outputTokens,
|
|
1419
|
+
COALESCE(SUM(cache_read_tokens), 0) as cacheReadTokens,
|
|
1420
|
+
COALESCE(SUM(cache_creation_tokens), 0) as cacheCreationTokens,
|
|
1421
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_creation_tokens), 0) as totalTokens
|
|
1422
|
+
FROM token_usage WHERE ${sql2}
|
|
1423
|
+
GROUP BY model
|
|
1424
|
+
ORDER BY totalTokens DESC
|
|
1425
|
+
`).all(...params);
|
|
1426
|
+
return rows;
|
|
1427
|
+
}
|
|
1428
|
+
byProject(filter) {
|
|
1429
|
+
const { sql: sql2, params } = applyOptionalFilters(filter);
|
|
1430
|
+
const rows = this.sqlite.prepare(`
|
|
1431
|
+
SELECT
|
|
1432
|
+
COALESCE(project, '(unknown)') as project,
|
|
1433
|
+
COALESCE(SUM(input_tokens), 0) as inputTokens,
|
|
1434
|
+
COALESCE(SUM(output_tokens), 0) as outputTokens,
|
|
1435
|
+
COALESCE(SUM(cache_read_tokens), 0) as cacheReadTokens,
|
|
1436
|
+
COALESCE(SUM(cache_creation_tokens), 0) as cacheCreationTokens,
|
|
1437
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_creation_tokens), 0) as totalTokens
|
|
1438
|
+
FROM token_usage WHERE ${sql2}
|
|
1439
|
+
GROUP BY project
|
|
1440
|
+
ORDER BY totalTokens DESC
|
|
1441
|
+
`).all(...params);
|
|
1442
|
+
return rows;
|
|
1443
|
+
}
|
|
1444
|
+
byHourDay(filter) {
|
|
1445
|
+
const { sql: sql2, params } = applyOptionalFilters(filter);
|
|
1446
|
+
const rows = this.sqlite.prepare(`
|
|
1447
|
+
SELECT
|
|
1448
|
+
CAST(strftime('%w', occurred_at) AS INTEGER) as dayOfWeek,
|
|
1449
|
+
CAST(strftime('%H', occurred_at) AS INTEGER) as hour,
|
|
1450
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_creation_tokens), 0) as totalTokens
|
|
1451
|
+
FROM token_usage WHERE ${sql2}
|
|
1452
|
+
GROUP BY dayOfWeek, hour
|
|
1453
|
+
`).all(...params);
|
|
1454
|
+
return rows;
|
|
1455
|
+
}
|
|
1456
|
+
topSessions(filter, limit = 20) {
|
|
1457
|
+
const { sql: sql2, params } = applyOptionalFilters(filter);
|
|
1458
|
+
const rows = this.sqlite.prepare(`
|
|
1459
|
+
SELECT
|
|
1460
|
+
session_id as sessionId,
|
|
1461
|
+
MAX(project) as project,
|
|
1462
|
+
MAX(model) as model,
|
|
1463
|
+
MIN(occurred_at) as startedAt,
|
|
1464
|
+
MAX(occurred_at) as endedAt,
|
|
1465
|
+
COUNT(*) as messageCount,
|
|
1466
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_creation_tokens), 0) as totalTokens
|
|
1467
|
+
FROM token_usage WHERE ${sql2} AND session_id IS NOT NULL
|
|
1468
|
+
GROUP BY session_id
|
|
1469
|
+
ORDER BY totalTokens DESC
|
|
1470
|
+
LIMIT ?
|
|
1471
|
+
`).all(...params, limit);
|
|
1472
|
+
return rows;
|
|
1473
|
+
}
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
// ../../packages/core/dist/services/token-usage/scanner.js
|
|
1477
|
+
var BATCH_SIZE = 200;
|
|
1478
|
+
var TokenUsageScanner = class {
|
|
1479
|
+
repo;
|
|
1480
|
+
adapters;
|
|
1481
|
+
constructor(repo, adapters) {
|
|
1482
|
+
this.repo = repo;
|
|
1483
|
+
this.adapters = adapters;
|
|
1484
|
+
}
|
|
1485
|
+
async scanAll() {
|
|
1486
|
+
const bySource = {};
|
|
1487
|
+
let totalInserted = 0;
|
|
1488
|
+
let totalScanned = 0;
|
|
1489
|
+
for (const adapter of this.adapters) {
|
|
1490
|
+
const stats = { inserted: 0, scanned: 0 };
|
|
1491
|
+
bySource[adapter.name] = stats;
|
|
1492
|
+
let batch = [];
|
|
1493
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
1494
|
+
const flush = () => {
|
|
1495
|
+
if (batch.length === 0)
|
|
1496
|
+
return;
|
|
1497
|
+
stats.inserted += this.repo.insertMany(batch);
|
|
1498
|
+
batch = [];
|
|
1499
|
+
};
|
|
1500
|
+
for await (const { record, filePath, byteOffset, mtime } of adapter.scan((fp) => this.repo.getScanState(adapter.name, fp))) {
|
|
1501
|
+
batch.push(record);
|
|
1502
|
+
offsets.set(filePath, { offset: byteOffset, mtime });
|
|
1503
|
+
stats.scanned++;
|
|
1504
|
+
if (batch.length >= BATCH_SIZE)
|
|
1505
|
+
flush();
|
|
1506
|
+
}
|
|
1507
|
+
flush();
|
|
1508
|
+
for (const [filePath, { offset, mtime }] of offsets) {
|
|
1509
|
+
this.repo.upsertScanState({
|
|
1510
|
+
source: adapter.name,
|
|
1511
|
+
filePath,
|
|
1512
|
+
lastOffset: offset,
|
|
1513
|
+
lastMtime: mtime
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
totalInserted += stats.inserted;
|
|
1517
|
+
totalScanned += stats.scanned;
|
|
1518
|
+
}
|
|
1519
|
+
return { inserted: totalInserted, scanned: totalScanned, bySource };
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
// ../../packages/core/dist/services/token-usage/adapters/claude-code.js
|
|
1524
|
+
import { createHash } from "crypto";
|
|
1525
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, statSync, openSync, readSync, closeSync } from "fs";
|
|
1526
|
+
import { homedir as homedir2 } from "os";
|
|
1527
|
+
import { resolve as resolve3, basename } from "path";
|
|
1528
|
+
var SOURCE = "claude-code";
|
|
1529
|
+
function decodeProject(folderName) {
|
|
1530
|
+
const trimmed = folderName.replace(/^-+/, "");
|
|
1531
|
+
const parts = trimmed.split("-").filter(Boolean);
|
|
1532
|
+
return parts[parts.length - 1] ?? folderName;
|
|
1533
|
+
}
|
|
1534
|
+
function recordId(sessionId, messageId, occurredAt) {
|
|
1535
|
+
const key = `${SOURCE}|${sessionId ?? ""}|${messageId ?? ""}|${occurredAt}`;
|
|
1536
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 32);
|
|
1537
|
+
}
|
|
1538
|
+
function* readNewLines(filePath, fromOffset, fileSize) {
|
|
1539
|
+
if (fromOffset >= fileSize)
|
|
1540
|
+
return;
|
|
1541
|
+
const fd = openSync(filePath, "r");
|
|
1542
|
+
try {
|
|
1543
|
+
const CHUNK = 64 * 1024;
|
|
1544
|
+
const buf = Buffer.alloc(CHUNK);
|
|
1545
|
+
let offset = fromOffset;
|
|
1546
|
+
let pending = "";
|
|
1547
|
+
while (offset < fileSize) {
|
|
1548
|
+
const toRead = Math.min(CHUNK, fileSize - offset);
|
|
1549
|
+
const bytesRead = readSync(fd, buf, 0, toRead, offset);
|
|
1550
|
+
if (bytesRead === 0)
|
|
1551
|
+
break;
|
|
1552
|
+
pending += buf.subarray(0, bytesRead).toString("utf8");
|
|
1553
|
+
offset += bytesRead;
|
|
1554
|
+
let nl;
|
|
1555
|
+
let pendingStart = 0;
|
|
1556
|
+
while ((nl = pending.indexOf("\n", pendingStart)) !== -1) {
|
|
1557
|
+
const line = pending.slice(pendingStart, nl);
|
|
1558
|
+
pendingStart = nl + 1;
|
|
1559
|
+
if (line.length > 0) {
|
|
1560
|
+
yield { line, byteOffsetAfter: -1 };
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
pending = pending.slice(pendingStart);
|
|
1564
|
+
}
|
|
1565
|
+
void pending;
|
|
1566
|
+
} finally {
|
|
1567
|
+
closeSync(fd);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
function parseUsageLine(line) {
|
|
1571
|
+
let obj;
|
|
1572
|
+
try {
|
|
1573
|
+
obj = JSON.parse(line);
|
|
1574
|
+
} catch {
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1577
|
+
if (!obj || typeof obj !== "object")
|
|
1578
|
+
return null;
|
|
1579
|
+
const message = obj.message ?? obj;
|
|
1580
|
+
const usage = message?.usage;
|
|
1581
|
+
if (!usage || typeof usage !== "object")
|
|
1582
|
+
return null;
|
|
1583
|
+
const occurredAt = obj.timestamp ?? message?.timestamp;
|
|
1584
|
+
if (!occurredAt)
|
|
1585
|
+
return null;
|
|
1586
|
+
const model = message?.model ?? "unknown";
|
|
1587
|
+
const sessionId = obj.sessionId ?? message?.sessionId ?? null;
|
|
1588
|
+
const messageId = message?.id ?? obj.uuid ?? null;
|
|
1589
|
+
return {
|
|
1590
|
+
source: SOURCE,
|
|
1591
|
+
model,
|
|
1592
|
+
sessionId,
|
|
1593
|
+
messageId,
|
|
1594
|
+
occurredAt,
|
|
1595
|
+
inputTokens: Number(usage.input_tokens ?? 0) || 0,
|
|
1596
|
+
outputTokens: Number(usage.output_tokens ?? 0) || 0,
|
|
1597
|
+
cacheReadTokens: Number(usage.cache_read_input_tokens ?? 0) || 0,
|
|
1598
|
+
cacheCreationTokens: Number(usage.cache_creation_input_tokens ?? 0) || 0
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
var ClaudeCodeAdapter = class {
|
|
1602
|
+
name = SOURCE;
|
|
1603
|
+
projectsDir;
|
|
1604
|
+
constructor(options = {}) {
|
|
1605
|
+
this.projectsDir = options.projectsDir ?? resolve3(homedir2(), ".claude", "projects");
|
|
1606
|
+
}
|
|
1607
|
+
async *scan(getState) {
|
|
1608
|
+
if (!existsSync2(this.projectsDir))
|
|
1609
|
+
return;
|
|
1610
|
+
for (const folder of readdirSync2(this.projectsDir)) {
|
|
1611
|
+
const projectDir = resolve3(this.projectsDir, folder);
|
|
1612
|
+
let stat;
|
|
1613
|
+
try {
|
|
1614
|
+
stat = statSync(projectDir);
|
|
1615
|
+
} catch {
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
if (!stat.isDirectory())
|
|
1619
|
+
continue;
|
|
1620
|
+
const project = decodeProject(basename(folder));
|
|
1621
|
+
let files;
|
|
1622
|
+
try {
|
|
1623
|
+
files = readdirSync2(projectDir);
|
|
1624
|
+
} catch {
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
for (const fileName of files) {
|
|
1628
|
+
if (!fileName.endsWith(".jsonl"))
|
|
1629
|
+
continue;
|
|
1630
|
+
const filePath = resolve3(projectDir, fileName);
|
|
1631
|
+
let fstat;
|
|
1632
|
+
try {
|
|
1633
|
+
fstat = statSync(filePath);
|
|
1634
|
+
} catch {
|
|
1635
|
+
continue;
|
|
1636
|
+
}
|
|
1637
|
+
const fileSize = fstat.size;
|
|
1638
|
+
const mtime = fstat.mtime.toISOString();
|
|
1639
|
+
const state = getState(filePath);
|
|
1640
|
+
const fromOffset = state?.lastOffset ?? 0;
|
|
1641
|
+
if (fromOffset >= fileSize)
|
|
1642
|
+
continue;
|
|
1643
|
+
for (const { line } of readNewLines(filePath, fromOffset, fileSize)) {
|
|
1644
|
+
const parsed = parseUsageLine(line);
|
|
1645
|
+
if (!parsed)
|
|
1646
|
+
continue;
|
|
1647
|
+
const record = {
|
|
1648
|
+
...parsed,
|
|
1649
|
+
id: recordId(parsed.sessionId, parsed.messageId, parsed.occurredAt),
|
|
1650
|
+
project
|
|
1651
|
+
};
|
|
1652
|
+
yield { record, filePath, byteOffset: fileSize, mtime };
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
// ../../packages/core/dist/services/token-usage/service.js
|
|
1660
|
+
var DEFAULT_TOP_SESSIONS = 20;
|
|
1661
|
+
var TokenUsageService = class {
|
|
1662
|
+
repo;
|
|
1663
|
+
scanner;
|
|
1664
|
+
constructor(repo, adapters) {
|
|
1665
|
+
this.repo = repo;
|
|
1666
|
+
this.scanner = new TokenUsageScanner(repo, adapters ?? [new ClaudeCodeAdapter()]);
|
|
1667
|
+
}
|
|
1668
|
+
scan() {
|
|
1669
|
+
return this.scanner.scanAll();
|
|
1670
|
+
}
|
|
1671
|
+
getAggregates(filter) {
|
|
1672
|
+
const totals = this.repo.totals(filter);
|
|
1673
|
+
const byDay = this.repo.byDay(filter);
|
|
1674
|
+
const byModel = this.repo.byModel(filter);
|
|
1675
|
+
const byProject = this.repo.byProject(filter);
|
|
1676
|
+
const byHourDay = this.repo.byHourDay(filter);
|
|
1677
|
+
const topSessions = this.repo.topSessions(filter, DEFAULT_TOP_SESSIONS);
|
|
1678
|
+
const cacheDenom = totals.inputTokens + totals.cacheReadTokens + totals.cacheCreationTokens;
|
|
1679
|
+
const cacheEfficiency = cacheDenom > 0 ? totals.cacheReadTokens / cacheDenom : 0;
|
|
1680
|
+
return { totals, byDay, byModel, byProject, byHourDay, topSessions, cacheEfficiency };
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
|
|
1291
1684
|
// ../../packages/embeddings/dist/client.js
|
|
1292
1685
|
var OllamaEmbeddingClient = class {
|
|
1293
1686
|
host;
|
|
@@ -1494,6 +1887,7 @@ var KnowledgeSDK = class {
|
|
|
1494
1887
|
db = null;
|
|
1495
1888
|
sqlite = null;
|
|
1496
1889
|
service = null;
|
|
1890
|
+
tokenService = null;
|
|
1497
1891
|
ollamaClient;
|
|
1498
1892
|
initialized = false;
|
|
1499
1893
|
constructor(config) {
|
|
@@ -1523,6 +1917,8 @@ var KnowledgeSDK = class {
|
|
|
1523
1917
|
await this.detectAndMigrateDimensions();
|
|
1524
1918
|
const repository = new KnowledgeRepository(this.db, this.sqlite);
|
|
1525
1919
|
this.service = new KnowledgeService(repository, this.ollamaClient);
|
|
1920
|
+
const tokenRepo = new TokenUsageRepository(this.sqlite);
|
|
1921
|
+
this.tokenService = new TokenUsageService(tokenRepo);
|
|
1526
1922
|
this.initialized = true;
|
|
1527
1923
|
} catch (error) {
|
|
1528
1924
|
await this.cleanup();
|
|
@@ -1781,6 +2177,15 @@ var KnowledgeSDK = class {
|
|
|
1781
2177
|
return 0;
|
|
1782
2178
|
return this.service.cleanupOldOperations();
|
|
1783
2179
|
}
|
|
2180
|
+
// ─── Token usage ────────────────────────────────────────────
|
|
2181
|
+
async scanTokenUsage() {
|
|
2182
|
+
this.ensureInitialized();
|
|
2183
|
+
return this.tokenService.scan();
|
|
2184
|
+
}
|
|
2185
|
+
getTokenUsage(filter) {
|
|
2186
|
+
this.ensureInitialized();
|
|
2187
|
+
return this.tokenService.getAggregates(filter);
|
|
2188
|
+
}
|
|
1784
2189
|
cleanupCompletedPlanEmbeddings(maxAgeDays = 30) {
|
|
1785
2190
|
if (!this.initialized || !this.service)
|
|
1786
2191
|
return 0;
|
|
@@ -1857,6 +2262,7 @@ var KnowledgeSDK = class {
|
|
|
1857
2262
|
}
|
|
1858
2263
|
this.db = null;
|
|
1859
2264
|
this.service = null;
|
|
2265
|
+
this.tokenService = null;
|
|
1860
2266
|
}
|
|
1861
2267
|
/**
|
|
1862
2268
|
* Detect if stored embeddings have different dimensions than config.
|
|
@@ -2082,6 +2488,32 @@ function createServer(sdk) {
|
|
|
2082
2488
|
return { content: [{ type: "text", text: JSON.stringify(health, null, 2) }] };
|
|
2083
2489
|
}
|
|
2084
2490
|
);
|
|
2491
|
+
server.tool(
|
|
2492
|
+
"getTokenUsage",
|
|
2493
|
+
"Aggregated token usage for AI coding tools (input/output/cache reads/cache writes) for a date range, optionally filtered by source, model, or project.",
|
|
2494
|
+
{
|
|
2495
|
+
from: z2.string().describe('ISO date \u2014 start of range (e.g. "2025-05-01T00:00:00Z")'),
|
|
2496
|
+
to: z2.string().describe("ISO date \u2014 end of range"),
|
|
2497
|
+
source: z2.string().optional().describe('Filter by source (e.g. "claude-code")'),
|
|
2498
|
+
model: z2.string().optional().describe("Filter by model"),
|
|
2499
|
+
project: z2.string().optional().describe("Filter by project (decoded cwd basename)")
|
|
2500
|
+
},
|
|
2501
|
+
READ_ONLY,
|
|
2502
|
+
async (params) => {
|
|
2503
|
+
try {
|
|
2504
|
+
await sdk.scanTokenUsage();
|
|
2505
|
+
} catch {
|
|
2506
|
+
}
|
|
2507
|
+
const result = sdk.getTokenUsage({
|
|
2508
|
+
from: params.from,
|
|
2509
|
+
to: params.to,
|
|
2510
|
+
source: params.source,
|
|
2511
|
+
model: params.model,
|
|
2512
|
+
project: params.project
|
|
2513
|
+
});
|
|
2514
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2515
|
+
}
|
|
2516
|
+
);
|
|
2085
2517
|
server.tool(
|
|
2086
2518
|
"createPlan",
|
|
2087
2519
|
"Create a plan with tasks. Plan auto-activates when the first task starts. Returns planId \u2014 SAVE IT and pass to addKnowledge calls.",
|