@cognistore/mcp-server 1.2.2 → 1.4.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 +628 -14
- 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) {
|
|
@@ -509,11 +537,28 @@ var KnowledgeRepository = class {
|
|
|
509
537
|
conditions.push(sql`${knowledgeEntries.scope} = ${filters.scope}`);
|
|
510
538
|
return this.db.select().from(knowledgeEntries).where(and(...conditions)).orderBy(sql`${knowledgeEntries.createdAt} DESC`).limit(limit);
|
|
511
539
|
}
|
|
512
|
-
async listTags() {
|
|
540
|
+
async listTags(opts = {}) {
|
|
541
|
+
const { from, to } = opts;
|
|
542
|
+
if (from && to) {
|
|
543
|
+
const result2 = await this.db.all(sql`SELECT DISTINCT value FROM knowledge_entries, json_each(knowledge_entries.tags)
|
|
544
|
+
WHERE knowledge_entries.type != 'system'
|
|
545
|
+
AND knowledge_entries.created_at >= ${from}
|
|
546
|
+
AND knowledge_entries.created_at < ${to}`);
|
|
547
|
+
return result2.map((r) => r.value);
|
|
548
|
+
}
|
|
513
549
|
const result = await this.db.all(sql`SELECT DISTINCT value FROM knowledge_entries, json_each(knowledge_entries.tags) WHERE knowledge_entries.type != 'system'`);
|
|
514
550
|
return result.map((r) => r.value);
|
|
515
551
|
}
|
|
516
|
-
async topTags(limit = 10) {
|
|
552
|
+
async topTags(limit = 10, opts = {}) {
|
|
553
|
+
const { from, to } = opts;
|
|
554
|
+
if (from && to) {
|
|
555
|
+
const result2 = await this.db.all(sql`SELECT value as tag, COUNT(*) as count FROM knowledge_entries, json_each(knowledge_entries.tags)
|
|
556
|
+
WHERE knowledge_entries.type != 'system'
|
|
557
|
+
AND knowledge_entries.created_at >= ${from}
|
|
558
|
+
AND knowledge_entries.created_at < ${to}
|
|
559
|
+
GROUP BY value ORDER BY count DESC LIMIT ${limit}`);
|
|
560
|
+
return result2;
|
|
561
|
+
}
|
|
517
562
|
const result = await this.db.all(sql`SELECT value as tag, COUNT(*) as count FROM knowledge_entries, json_each(knowledge_entries.tags) WHERE knowledge_entries.type != 'system' GROUP BY value ORDER BY count DESC LIMIT ${limit}`);
|
|
518
563
|
return result;
|
|
519
564
|
}
|
|
@@ -525,18 +570,30 @@ var KnowledgeRepository = class {
|
|
|
525
570
|
const result = await this.db.all(sql`SELECT MAX(updated_at) as latest FROM knowledge_entries`);
|
|
526
571
|
return result[0]?.latest ?? null;
|
|
527
572
|
}
|
|
528
|
-
async countByType() {
|
|
573
|
+
async countByType(opts = {}) {
|
|
574
|
+
const { from, to } = opts;
|
|
575
|
+
const conditions = [ne(knowledgeEntries.type, "system")];
|
|
576
|
+
if (from && to) {
|
|
577
|
+
conditions.push(sql`${knowledgeEntries.createdAt} >= ${from}`);
|
|
578
|
+
conditions.push(sql`${knowledgeEntries.createdAt} < ${to}`);
|
|
579
|
+
}
|
|
529
580
|
const results = await this.db.select({
|
|
530
581
|
type: knowledgeEntries.type,
|
|
531
582
|
count: sql`count(*)`
|
|
532
|
-
}).from(knowledgeEntries).where(
|
|
583
|
+
}).from(knowledgeEntries).where(and(...conditions)).groupBy(knowledgeEntries.type);
|
|
533
584
|
return results.map((r) => ({ type: r.type, count: Number(r.count) }));
|
|
534
585
|
}
|
|
535
|
-
async countByScope() {
|
|
586
|
+
async countByScope(opts = {}) {
|
|
587
|
+
const { from, to } = opts;
|
|
588
|
+
const conditions = [ne(knowledgeEntries.type, "system")];
|
|
589
|
+
if (from && to) {
|
|
590
|
+
conditions.push(sql`${knowledgeEntries.createdAt} >= ${from}`);
|
|
591
|
+
conditions.push(sql`${knowledgeEntries.createdAt} < ${to}`);
|
|
592
|
+
}
|
|
536
593
|
const results = await this.db.select({
|
|
537
594
|
scope: knowledgeEntries.scope,
|
|
538
595
|
count: sql`count(*)`
|
|
539
|
-
}).from(knowledgeEntries).where(
|
|
596
|
+
}).from(knowledgeEntries).where(and(...conditions)).groupBy(knowledgeEntries.scope);
|
|
540
597
|
return results.map((r) => ({ scope: r.scope, count: Number(r.count) }));
|
|
541
598
|
}
|
|
542
599
|
async listAll() {
|
|
@@ -1015,11 +1072,11 @@ var KnowledgeService = class {
|
|
|
1015
1072
|
const entries = await this.repository.listRecent(limit, filters);
|
|
1016
1073
|
return entries.map((e) => this.toKnowledgeEntry(e));
|
|
1017
1074
|
}
|
|
1018
|
-
async topTags(limit = 10) {
|
|
1019
|
-
return this.repository.topTags(limit);
|
|
1075
|
+
async topTags(limit = 10, opts = {}) {
|
|
1076
|
+
return this.repository.topTags(limit, opts);
|
|
1020
1077
|
}
|
|
1021
|
-
async listTags() {
|
|
1022
|
-
return this.repository.listTags();
|
|
1078
|
+
async listTags(opts = {}) {
|
|
1079
|
+
return this.repository.listTags(opts);
|
|
1023
1080
|
}
|
|
1024
1081
|
async getStats() {
|
|
1025
1082
|
const [count, byType, byScope, lastUpdatedAt] = await Promise.all([
|
|
@@ -1030,6 +1087,12 @@ var KnowledgeService = class {
|
|
|
1030
1087
|
]);
|
|
1031
1088
|
return { total: count, byType, byScope, lastUpdatedAt };
|
|
1032
1089
|
}
|
|
1090
|
+
async countByType(opts = {}) {
|
|
1091
|
+
return this.repository.countByType(opts);
|
|
1092
|
+
}
|
|
1093
|
+
async countByScope(opts = {}) {
|
|
1094
|
+
return this.repository.countByScope(opts);
|
|
1095
|
+
}
|
|
1033
1096
|
// ─── Plans (separate entity) ────────────────────────────────
|
|
1034
1097
|
async createPlan(input) {
|
|
1035
1098
|
const { tasks, skipDedup, ...planInput } = input;
|
|
@@ -1288,6 +1351,502 @@ var KnowledgeService = class {
|
|
|
1288
1351
|
}
|
|
1289
1352
|
};
|
|
1290
1353
|
|
|
1354
|
+
// ../../packages/core/dist/repositories/token-usage.repository.js
|
|
1355
|
+
var RANGE_CLAUSE = "occurred_at >= ? AND occurred_at <= ?";
|
|
1356
|
+
function applyOptionalFilters(filter) {
|
|
1357
|
+
const params = [filter.from, filter.to];
|
|
1358
|
+
let sql2 = RANGE_CLAUSE;
|
|
1359
|
+
if (filter.source) {
|
|
1360
|
+
sql2 += " AND source = ?";
|
|
1361
|
+
params.push(filter.source);
|
|
1362
|
+
}
|
|
1363
|
+
if (filter.model) {
|
|
1364
|
+
sql2 += " AND model = ?";
|
|
1365
|
+
params.push(filter.model);
|
|
1366
|
+
}
|
|
1367
|
+
if (filter.project) {
|
|
1368
|
+
sql2 += " AND project = ?";
|
|
1369
|
+
params.push(filter.project);
|
|
1370
|
+
}
|
|
1371
|
+
return { sql: sql2, params };
|
|
1372
|
+
}
|
|
1373
|
+
var TokenUsageRepository = class {
|
|
1374
|
+
sqlite;
|
|
1375
|
+
constructor(sqlite) {
|
|
1376
|
+
this.sqlite = sqlite;
|
|
1377
|
+
}
|
|
1378
|
+
// ─── Writes ────────────────────────────────────────────────────
|
|
1379
|
+
/** Insert if new; never throw on conflict (deterministic id makes this idempotent). */
|
|
1380
|
+
insertMany(records) {
|
|
1381
|
+
if (records.length === 0)
|
|
1382
|
+
return 0;
|
|
1383
|
+
const stmt = this.sqlite.prepare(`
|
|
1384
|
+
INSERT OR IGNORE INTO token_usage
|
|
1385
|
+
(id, source, model, project, session_id, message_id, occurred_at,
|
|
1386
|
+
input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, scanned_at)
|
|
1387
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1388
|
+
`);
|
|
1389
|
+
const scannedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1390
|
+
const tx = this.sqlite.transaction((rows) => {
|
|
1391
|
+
let inserted = 0;
|
|
1392
|
+
for (const r of rows) {
|
|
1393
|
+
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);
|
|
1394
|
+
if (info.changes > 0)
|
|
1395
|
+
inserted++;
|
|
1396
|
+
}
|
|
1397
|
+
return inserted;
|
|
1398
|
+
});
|
|
1399
|
+
return tx(records);
|
|
1400
|
+
}
|
|
1401
|
+
// ─── Scan state ────────────────────────────────────────────────
|
|
1402
|
+
getScanState(source, filePath) {
|
|
1403
|
+
const row = this.sqlite.prepare(`
|
|
1404
|
+
SELECT source, file_path as filePath, last_offset as lastOffset, last_mtime as lastMtime
|
|
1405
|
+
FROM scan_state WHERE source = ? AND file_path = ?
|
|
1406
|
+
`).get(source, filePath);
|
|
1407
|
+
return row;
|
|
1408
|
+
}
|
|
1409
|
+
upsertScanState(state) {
|
|
1410
|
+
this.sqlite.prepare(`
|
|
1411
|
+
INSERT INTO scan_state (source, file_path, last_offset, last_mtime, last_scanned_at)
|
|
1412
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1413
|
+
ON CONFLICT(source, file_path) DO UPDATE SET
|
|
1414
|
+
last_offset = excluded.last_offset,
|
|
1415
|
+
last_mtime = excluded.last_mtime,
|
|
1416
|
+
last_scanned_at = excluded.last_scanned_at
|
|
1417
|
+
`).run(state.source, state.filePath, state.lastOffset, state.lastMtime, (/* @__PURE__ */ new Date()).toISOString());
|
|
1418
|
+
}
|
|
1419
|
+
// ─── Aggregations ──────────────────────────────────────────────
|
|
1420
|
+
totals(filter) {
|
|
1421
|
+
const { sql: sql2, params } = applyOptionalFilters(filter);
|
|
1422
|
+
const row = this.sqlite.prepare(`
|
|
1423
|
+
SELECT
|
|
1424
|
+
COALESCE(SUM(input_tokens), 0) as inputTokens,
|
|
1425
|
+
COALESCE(SUM(output_tokens), 0) as outputTokens,
|
|
1426
|
+
COALESCE(SUM(cache_read_tokens), 0) as cacheReadTokens,
|
|
1427
|
+
COALESCE(SUM(cache_creation_tokens), 0) as cacheCreationTokens
|
|
1428
|
+
FROM token_usage WHERE ${sql2}
|
|
1429
|
+
`).get(...params);
|
|
1430
|
+
return row;
|
|
1431
|
+
}
|
|
1432
|
+
byDay(filter) {
|
|
1433
|
+
const { sql: sql2, params } = applyOptionalFilters(filter);
|
|
1434
|
+
const rows = this.sqlite.prepare(`
|
|
1435
|
+
SELECT
|
|
1436
|
+
date(occurred_at) as date,
|
|
1437
|
+
COALESCE(SUM(input_tokens), 0) as inputTokens,
|
|
1438
|
+
COALESCE(SUM(output_tokens), 0) as outputTokens,
|
|
1439
|
+
COALESCE(SUM(cache_read_tokens), 0) as cacheReadTokens,
|
|
1440
|
+
COALESCE(SUM(cache_creation_tokens), 0) as cacheCreationTokens
|
|
1441
|
+
FROM token_usage WHERE ${sql2}
|
|
1442
|
+
GROUP BY date(occurred_at)
|
|
1443
|
+
ORDER BY date(occurred_at)
|
|
1444
|
+
`).all(...params);
|
|
1445
|
+
return rows;
|
|
1446
|
+
}
|
|
1447
|
+
byModel(filter) {
|
|
1448
|
+
const { sql: sql2, params } = applyOptionalFilters(filter);
|
|
1449
|
+
const rows = this.sqlite.prepare(`
|
|
1450
|
+
SELECT
|
|
1451
|
+
model,
|
|
1452
|
+
COALESCE(SUM(input_tokens), 0) as inputTokens,
|
|
1453
|
+
COALESCE(SUM(output_tokens), 0) as outputTokens,
|
|
1454
|
+
COALESCE(SUM(cache_read_tokens), 0) as cacheReadTokens,
|
|
1455
|
+
COALESCE(SUM(cache_creation_tokens), 0) as cacheCreationTokens,
|
|
1456
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_creation_tokens), 0) as totalTokens
|
|
1457
|
+
FROM token_usage WHERE ${sql2}
|
|
1458
|
+
GROUP BY model
|
|
1459
|
+
ORDER BY totalTokens DESC
|
|
1460
|
+
`).all(...params);
|
|
1461
|
+
return rows;
|
|
1462
|
+
}
|
|
1463
|
+
byProject(filter) {
|
|
1464
|
+
const { sql: sql2, params } = applyOptionalFilters(filter);
|
|
1465
|
+
const rows = this.sqlite.prepare(`
|
|
1466
|
+
SELECT
|
|
1467
|
+
COALESCE(project, '(unknown)') as project,
|
|
1468
|
+
COALESCE(SUM(input_tokens), 0) as inputTokens,
|
|
1469
|
+
COALESCE(SUM(output_tokens), 0) as outputTokens,
|
|
1470
|
+
COALESCE(SUM(cache_read_tokens), 0) as cacheReadTokens,
|
|
1471
|
+
COALESCE(SUM(cache_creation_tokens), 0) as cacheCreationTokens,
|
|
1472
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_creation_tokens), 0) as totalTokens
|
|
1473
|
+
FROM token_usage WHERE ${sql2}
|
|
1474
|
+
GROUP BY project
|
|
1475
|
+
ORDER BY totalTokens DESC
|
|
1476
|
+
`).all(...params);
|
|
1477
|
+
return rows;
|
|
1478
|
+
}
|
|
1479
|
+
byHourDay(filter) {
|
|
1480
|
+
const { sql: sql2, params } = applyOptionalFilters(filter);
|
|
1481
|
+
const rows = this.sqlite.prepare(`
|
|
1482
|
+
SELECT
|
|
1483
|
+
CAST(strftime('%w', occurred_at) AS INTEGER) as dayOfWeek,
|
|
1484
|
+
CAST(strftime('%H', occurred_at) AS INTEGER) as hour,
|
|
1485
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_creation_tokens), 0) as totalTokens
|
|
1486
|
+
FROM token_usage WHERE ${sql2}
|
|
1487
|
+
GROUP BY dayOfWeek, hour
|
|
1488
|
+
`).all(...params);
|
|
1489
|
+
return rows;
|
|
1490
|
+
}
|
|
1491
|
+
topSessions(filter, limit = 20) {
|
|
1492
|
+
const { sql: sql2, params } = applyOptionalFilters(filter);
|
|
1493
|
+
const rows = this.sqlite.prepare(`
|
|
1494
|
+
SELECT
|
|
1495
|
+
session_id as sessionId,
|
|
1496
|
+
MAX(project) as project,
|
|
1497
|
+
MAX(model) as model,
|
|
1498
|
+
MIN(occurred_at) as startedAt,
|
|
1499
|
+
MAX(occurred_at) as endedAt,
|
|
1500
|
+
COUNT(*) as messageCount,
|
|
1501
|
+
COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_creation_tokens), 0) as totalTokens
|
|
1502
|
+
FROM token_usage WHERE ${sql2} AND session_id IS NOT NULL
|
|
1503
|
+
GROUP BY session_id
|
|
1504
|
+
ORDER BY totalTokens DESC
|
|
1505
|
+
LIMIT ?
|
|
1506
|
+
`).all(...params, limit);
|
|
1507
|
+
return rows;
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1511
|
+
// ../../packages/core/dist/services/token-usage/scanner.js
|
|
1512
|
+
var BATCH_SIZE = 200;
|
|
1513
|
+
var TokenUsageScanner = class {
|
|
1514
|
+
repo;
|
|
1515
|
+
adapters;
|
|
1516
|
+
constructor(repo, adapters) {
|
|
1517
|
+
this.repo = repo;
|
|
1518
|
+
this.adapters = adapters;
|
|
1519
|
+
}
|
|
1520
|
+
async scanAll() {
|
|
1521
|
+
const bySource = {};
|
|
1522
|
+
let totalInserted = 0;
|
|
1523
|
+
let totalScanned = 0;
|
|
1524
|
+
for (const adapter of this.adapters) {
|
|
1525
|
+
const stats = { inserted: 0, scanned: 0 };
|
|
1526
|
+
bySource[adapter.name] = stats;
|
|
1527
|
+
let batch = [];
|
|
1528
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
1529
|
+
const flush = () => {
|
|
1530
|
+
if (batch.length === 0)
|
|
1531
|
+
return;
|
|
1532
|
+
stats.inserted += this.repo.insertMany(batch);
|
|
1533
|
+
batch = [];
|
|
1534
|
+
};
|
|
1535
|
+
for await (const { record, filePath, byteOffset, mtime } of adapter.scan((fp) => this.repo.getScanState(adapter.name, fp))) {
|
|
1536
|
+
if (record) {
|
|
1537
|
+
batch.push(record);
|
|
1538
|
+
if (batch.length >= BATCH_SIZE)
|
|
1539
|
+
flush();
|
|
1540
|
+
}
|
|
1541
|
+
offsets.set(filePath, { offset: byteOffset, mtime });
|
|
1542
|
+
stats.scanned++;
|
|
1543
|
+
}
|
|
1544
|
+
flush();
|
|
1545
|
+
for (const [filePath, { offset, mtime }] of offsets) {
|
|
1546
|
+
this.repo.upsertScanState({
|
|
1547
|
+
source: adapter.name,
|
|
1548
|
+
filePath,
|
|
1549
|
+
lastOffset: offset,
|
|
1550
|
+
lastMtime: mtime
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
totalInserted += stats.inserted;
|
|
1554
|
+
totalScanned += stats.scanned;
|
|
1555
|
+
}
|
|
1556
|
+
return { inserted: totalInserted, scanned: totalScanned, bySource };
|
|
1557
|
+
}
|
|
1558
|
+
};
|
|
1559
|
+
|
|
1560
|
+
// ../../packages/core/dist/services/token-usage/adapters/claude-code.js
|
|
1561
|
+
import { createHash } from "crypto";
|
|
1562
|
+
import { existsSync as existsSync2, readdirSync as readdirSync2, statSync, openSync, readSync, closeSync } from "fs";
|
|
1563
|
+
import { homedir as homedir2 } from "os";
|
|
1564
|
+
import { resolve as resolve3, basename } from "path";
|
|
1565
|
+
var SOURCE = "claude-code";
|
|
1566
|
+
function decodeProject(folderName) {
|
|
1567
|
+
const trimmed = folderName.replace(/^-+/, "");
|
|
1568
|
+
const parts = trimmed.split("-").filter(Boolean);
|
|
1569
|
+
return parts[parts.length - 1] ?? folderName;
|
|
1570
|
+
}
|
|
1571
|
+
function recordId(sessionId, messageId, occurredAt) {
|
|
1572
|
+
const key = `${SOURCE}|${sessionId ?? ""}|${messageId ?? ""}|${occurredAt}`;
|
|
1573
|
+
return createHash("sha256").update(key).digest("hex").slice(0, 32);
|
|
1574
|
+
}
|
|
1575
|
+
function* readNewLines(filePath, fromOffset, fileSize) {
|
|
1576
|
+
if (fromOffset >= fileSize)
|
|
1577
|
+
return;
|
|
1578
|
+
const fd = openSync(filePath, "r");
|
|
1579
|
+
try {
|
|
1580
|
+
const CHUNK = 64 * 1024;
|
|
1581
|
+
const buf = Buffer.alloc(CHUNK);
|
|
1582
|
+
let offset = fromOffset;
|
|
1583
|
+
let pending = "";
|
|
1584
|
+
while (offset < fileSize) {
|
|
1585
|
+
const toRead = Math.min(CHUNK, fileSize - offset);
|
|
1586
|
+
const bytesRead = readSync(fd, buf, 0, toRead, offset);
|
|
1587
|
+
if (bytesRead === 0)
|
|
1588
|
+
break;
|
|
1589
|
+
pending += buf.subarray(0, bytesRead).toString("utf8");
|
|
1590
|
+
offset += bytesRead;
|
|
1591
|
+
let nl;
|
|
1592
|
+
let pendingStart = 0;
|
|
1593
|
+
while ((nl = pending.indexOf("\n", pendingStart)) !== -1) {
|
|
1594
|
+
const line = pending.slice(pendingStart, nl);
|
|
1595
|
+
pendingStart = nl + 1;
|
|
1596
|
+
if (line.length > 0) {
|
|
1597
|
+
yield { line, byteOffsetAfter: -1 };
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
pending = pending.slice(pendingStart);
|
|
1601
|
+
}
|
|
1602
|
+
void pending;
|
|
1603
|
+
} finally {
|
|
1604
|
+
closeSync(fd);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function parseUsageLine(line) {
|
|
1608
|
+
let obj;
|
|
1609
|
+
try {
|
|
1610
|
+
obj = JSON.parse(line);
|
|
1611
|
+
} catch {
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
if (!obj || typeof obj !== "object")
|
|
1615
|
+
return null;
|
|
1616
|
+
const message = obj.message ?? obj;
|
|
1617
|
+
const usage = message?.usage;
|
|
1618
|
+
if (!usage || typeof usage !== "object")
|
|
1619
|
+
return null;
|
|
1620
|
+
const occurredAt = obj.timestamp ?? message?.timestamp;
|
|
1621
|
+
if (!occurredAt)
|
|
1622
|
+
return null;
|
|
1623
|
+
const model = message?.model ?? "unknown";
|
|
1624
|
+
const sessionId = obj.sessionId ?? message?.sessionId ?? null;
|
|
1625
|
+
const messageId = message?.id ?? obj.uuid ?? null;
|
|
1626
|
+
return {
|
|
1627
|
+
source: SOURCE,
|
|
1628
|
+
model,
|
|
1629
|
+
sessionId,
|
|
1630
|
+
messageId,
|
|
1631
|
+
occurredAt,
|
|
1632
|
+
inputTokens: Number(usage.input_tokens ?? 0) || 0,
|
|
1633
|
+
outputTokens: Number(usage.output_tokens ?? 0) || 0,
|
|
1634
|
+
cacheReadTokens: Number(usage.cache_read_input_tokens ?? 0) || 0,
|
|
1635
|
+
cacheCreationTokens: Number(usage.cache_creation_input_tokens ?? 0) || 0
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
var ClaudeCodeAdapter = class {
|
|
1639
|
+
name = SOURCE;
|
|
1640
|
+
projectsDir;
|
|
1641
|
+
constructor(options = {}) {
|
|
1642
|
+
this.projectsDir = options.projectsDir ?? resolve3(homedir2(), ".claude", "projects");
|
|
1643
|
+
}
|
|
1644
|
+
async *scan(getState) {
|
|
1645
|
+
if (!existsSync2(this.projectsDir))
|
|
1646
|
+
return;
|
|
1647
|
+
for (const folder of readdirSync2(this.projectsDir)) {
|
|
1648
|
+
const projectDir = resolve3(this.projectsDir, folder);
|
|
1649
|
+
let stat;
|
|
1650
|
+
try {
|
|
1651
|
+
stat = statSync(projectDir);
|
|
1652
|
+
} catch {
|
|
1653
|
+
continue;
|
|
1654
|
+
}
|
|
1655
|
+
if (!stat.isDirectory())
|
|
1656
|
+
continue;
|
|
1657
|
+
const project = decodeProject(basename(folder));
|
|
1658
|
+
let files;
|
|
1659
|
+
try {
|
|
1660
|
+
files = readdirSync2(projectDir);
|
|
1661
|
+
} catch {
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
for (const fileName of files) {
|
|
1665
|
+
if (!fileName.endsWith(".jsonl"))
|
|
1666
|
+
continue;
|
|
1667
|
+
const filePath = resolve3(projectDir, fileName);
|
|
1668
|
+
let fstat;
|
|
1669
|
+
try {
|
|
1670
|
+
fstat = statSync(filePath);
|
|
1671
|
+
} catch {
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1674
|
+
const fileSize = fstat.size;
|
|
1675
|
+
const mtime = fstat.mtime.toISOString();
|
|
1676
|
+
const state = getState(filePath);
|
|
1677
|
+
const fromOffset = state?.lastOffset ?? 0;
|
|
1678
|
+
if (fromOffset >= fileSize)
|
|
1679
|
+
continue;
|
|
1680
|
+
for (const { line } of readNewLines(filePath, fromOffset, fileSize)) {
|
|
1681
|
+
const parsed = parseUsageLine(line);
|
|
1682
|
+
if (!parsed)
|
|
1683
|
+
continue;
|
|
1684
|
+
const record = {
|
|
1685
|
+
...parsed,
|
|
1686
|
+
id: recordId(parsed.sessionId, parsed.messageId, parsed.occurredAt),
|
|
1687
|
+
project
|
|
1688
|
+
};
|
|
1689
|
+
yield { record, filePath, byteOffset: fileSize, mtime };
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
|
|
1696
|
+
// ../../packages/core/dist/services/token-usage/adapters/copilot-cli.js
|
|
1697
|
+
import { createHash as createHash2 } from "crypto";
|
|
1698
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync2 } from "fs";
|
|
1699
|
+
import { homedir as homedir3 } from "os";
|
|
1700
|
+
import { resolve as resolve4, basename as basename2 } from "path";
|
|
1701
|
+
var SOURCE2 = "copilot-cli";
|
|
1702
|
+
function recordId2(sessionId, model, occurredAt) {
|
|
1703
|
+
const key = `${SOURCE2}|${sessionId ?? ""}|${model}|${occurredAt}`;
|
|
1704
|
+
return createHash2("sha256").update(key).digest("hex").slice(0, 32);
|
|
1705
|
+
}
|
|
1706
|
+
function parseEventsFile(filePath) {
|
|
1707
|
+
let raw;
|
|
1708
|
+
try {
|
|
1709
|
+
raw = readFileSync2(filePath, "utf8");
|
|
1710
|
+
} catch {
|
|
1711
|
+
return null;
|
|
1712
|
+
}
|
|
1713
|
+
let cwd = null;
|
|
1714
|
+
let sessionId = null;
|
|
1715
|
+
let shutdown = null;
|
|
1716
|
+
for (const line of raw.split("\n")) {
|
|
1717
|
+
if (!line)
|
|
1718
|
+
continue;
|
|
1719
|
+
let evt;
|
|
1720
|
+
try {
|
|
1721
|
+
evt = JSON.parse(line);
|
|
1722
|
+
} catch {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
if (!evt || typeof evt !== "object")
|
|
1726
|
+
continue;
|
|
1727
|
+
if (evt.type === "session.start") {
|
|
1728
|
+
const data = evt.data ?? {};
|
|
1729
|
+
sessionId = data.sessionId ?? sessionId;
|
|
1730
|
+
const ctx = data.context ?? {};
|
|
1731
|
+
cwd = ctx.cwd ?? cwd;
|
|
1732
|
+
} else if (evt.type === "session.shutdown") {
|
|
1733
|
+
const data = evt.data ?? {};
|
|
1734
|
+
const modelMetrics = data.modelMetrics;
|
|
1735
|
+
if (!modelMetrics || typeof modelMetrics !== "object")
|
|
1736
|
+
continue;
|
|
1737
|
+
const perModel = [];
|
|
1738
|
+
for (const [model, payload] of Object.entries(modelMetrics)) {
|
|
1739
|
+
const usage = payload?.usage;
|
|
1740
|
+
if (!usage || typeof usage !== "object")
|
|
1741
|
+
continue;
|
|
1742
|
+
perModel.push({
|
|
1743
|
+
model,
|
|
1744
|
+
inputTokens: Number(usage.inputTokens ?? 0) || 0,
|
|
1745
|
+
outputTokens: Number(usage.outputTokens ?? 0) || 0,
|
|
1746
|
+
cacheReadTokens: Number(usage.cacheReadTokens ?? 0) || 0,
|
|
1747
|
+
cacheCreationTokens: Number(usage.cacheWriteTokens ?? 0) || 0
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
if (perModel.length === 0)
|
|
1751
|
+
continue;
|
|
1752
|
+
shutdown = {
|
|
1753
|
+
occurredAt: evt.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1754
|
+
sessionId,
|
|
1755
|
+
cwd,
|
|
1756
|
+
perModel
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
return shutdown;
|
|
1761
|
+
}
|
|
1762
|
+
var CopilotCliAdapter = class {
|
|
1763
|
+
name = SOURCE2;
|
|
1764
|
+
sessionStateDir;
|
|
1765
|
+
constructor(options = {}) {
|
|
1766
|
+
this.sessionStateDir = options.sessionStateDir ?? resolve4(homedir3(), ".copilot", "session-state");
|
|
1767
|
+
}
|
|
1768
|
+
async *scan(getState) {
|
|
1769
|
+
if (!existsSync3(this.sessionStateDir))
|
|
1770
|
+
return;
|
|
1771
|
+
let entries;
|
|
1772
|
+
try {
|
|
1773
|
+
entries = readdirSync3(this.sessionStateDir);
|
|
1774
|
+
} catch {
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
for (const entry of entries) {
|
|
1778
|
+
const sessionDir = resolve4(this.sessionStateDir, entry);
|
|
1779
|
+
let stat;
|
|
1780
|
+
try {
|
|
1781
|
+
stat = statSync2(sessionDir);
|
|
1782
|
+
} catch {
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
if (!stat.isDirectory())
|
|
1786
|
+
continue;
|
|
1787
|
+
const filePath = resolve4(sessionDir, "events.jsonl");
|
|
1788
|
+
let fstat;
|
|
1789
|
+
try {
|
|
1790
|
+
fstat = statSync2(filePath);
|
|
1791
|
+
} catch {
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
const fileSize = fstat.size;
|
|
1795
|
+
const mtime = fstat.mtime.toISOString();
|
|
1796
|
+
const state = getState(filePath);
|
|
1797
|
+
if (state && state.lastOffset >= fileSize)
|
|
1798
|
+
continue;
|
|
1799
|
+
const parsed = parseEventsFile(filePath);
|
|
1800
|
+
if (!parsed) {
|
|
1801
|
+
yield { record: null, filePath, byteOffset: fileSize, mtime };
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
const project = parsed.cwd ? basename2(parsed.cwd) : null;
|
|
1805
|
+
for (const m of parsed.perModel) {
|
|
1806
|
+
const record = {
|
|
1807
|
+
id: recordId2(parsed.sessionId, m.model, parsed.occurredAt),
|
|
1808
|
+
source: SOURCE2,
|
|
1809
|
+
model: m.model,
|
|
1810
|
+
project,
|
|
1811
|
+
sessionId: parsed.sessionId,
|
|
1812
|
+
messageId: null,
|
|
1813
|
+
occurredAt: parsed.occurredAt,
|
|
1814
|
+
inputTokens: m.inputTokens,
|
|
1815
|
+
outputTokens: m.outputTokens,
|
|
1816
|
+
cacheReadTokens: m.cacheReadTokens,
|
|
1817
|
+
cacheCreationTokens: m.cacheCreationTokens
|
|
1818
|
+
};
|
|
1819
|
+
yield { record, filePath, byteOffset: fileSize, mtime };
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
|
|
1825
|
+
// ../../packages/core/dist/services/token-usage/service.js
|
|
1826
|
+
var DEFAULT_TOP_SESSIONS = 20;
|
|
1827
|
+
var TokenUsageService = class {
|
|
1828
|
+
repo;
|
|
1829
|
+
scanner;
|
|
1830
|
+
constructor(repo, adapters) {
|
|
1831
|
+
this.repo = repo;
|
|
1832
|
+
this.scanner = new TokenUsageScanner(repo, adapters ?? [new ClaudeCodeAdapter(), new CopilotCliAdapter()]);
|
|
1833
|
+
}
|
|
1834
|
+
scan() {
|
|
1835
|
+
return this.scanner.scanAll();
|
|
1836
|
+
}
|
|
1837
|
+
getAggregates(filter) {
|
|
1838
|
+
const totals = this.repo.totals(filter);
|
|
1839
|
+
const byDay = this.repo.byDay(filter);
|
|
1840
|
+
const byModel = this.repo.byModel(filter);
|
|
1841
|
+
const byProject = this.repo.byProject(filter);
|
|
1842
|
+
const byHourDay = this.repo.byHourDay(filter);
|
|
1843
|
+
const topSessions = this.repo.topSessions(filter, DEFAULT_TOP_SESSIONS);
|
|
1844
|
+
const cacheDenom = totals.inputTokens + totals.cacheReadTokens + totals.cacheCreationTokens;
|
|
1845
|
+
const cacheEfficiency = cacheDenom > 0 ? totals.cacheReadTokens / cacheDenom : 0;
|
|
1846
|
+
return { totals, byDay, byModel, byProject, byHourDay, topSessions, cacheEfficiency };
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1291
1850
|
// ../../packages/embeddings/dist/client.js
|
|
1292
1851
|
var OllamaEmbeddingClient = class {
|
|
1293
1852
|
host;
|
|
@@ -1494,6 +2053,7 @@ var KnowledgeSDK = class {
|
|
|
1494
2053
|
db = null;
|
|
1495
2054
|
sqlite = null;
|
|
1496
2055
|
service = null;
|
|
2056
|
+
tokenService = null;
|
|
1497
2057
|
ollamaClient;
|
|
1498
2058
|
initialized = false;
|
|
1499
2059
|
constructor(config) {
|
|
@@ -1523,6 +2083,8 @@ var KnowledgeSDK = class {
|
|
|
1523
2083
|
await this.detectAndMigrateDimensions();
|
|
1524
2084
|
const repository = new KnowledgeRepository(this.db, this.sqlite);
|
|
1525
2085
|
this.service = new KnowledgeService(repository, this.ollamaClient);
|
|
2086
|
+
const tokenRepo = new TokenUsageRepository(this.sqlite);
|
|
2087
|
+
this.tokenService = new TokenUsageService(tokenRepo);
|
|
1526
2088
|
this.initialized = true;
|
|
1527
2089
|
} catch (error) {
|
|
1528
2090
|
await this.cleanup();
|
|
@@ -1605,22 +2167,38 @@ var KnowledgeSDK = class {
|
|
|
1605
2167
|
throw this.wrapError(error, "Failed to list recent knowledge");
|
|
1606
2168
|
}
|
|
1607
2169
|
}
|
|
1608
|
-
async getTopTags(limit = 10) {
|
|
2170
|
+
async getTopTags(limit = 10, opts = {}) {
|
|
1609
2171
|
this.ensureInitialized();
|
|
1610
2172
|
try {
|
|
1611
|
-
return await this.service.topTags(limit);
|
|
2173
|
+
return await this.service.topTags(limit, opts);
|
|
1612
2174
|
} catch (error) {
|
|
1613
2175
|
throw this.wrapError(error, "Failed to get top tags");
|
|
1614
2176
|
}
|
|
1615
2177
|
}
|
|
1616
|
-
async listTags() {
|
|
2178
|
+
async listTags(opts = {}) {
|
|
1617
2179
|
this.ensureInitialized();
|
|
1618
2180
|
try {
|
|
1619
|
-
return await this.service.listTags();
|
|
2181
|
+
return await this.service.listTags(opts);
|
|
1620
2182
|
} catch (error) {
|
|
1621
2183
|
throw this.wrapError(error, "Failed to list tags");
|
|
1622
2184
|
}
|
|
1623
2185
|
}
|
|
2186
|
+
async countByType(opts = {}) {
|
|
2187
|
+
this.ensureInitialized();
|
|
2188
|
+
try {
|
|
2189
|
+
return await this.service.countByType(opts);
|
|
2190
|
+
} catch (error) {
|
|
2191
|
+
throw this.wrapError(error, "Failed to count by type");
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
async countByScope(opts = {}) {
|
|
2195
|
+
this.ensureInitialized();
|
|
2196
|
+
try {
|
|
2197
|
+
return await this.service.countByScope(opts);
|
|
2198
|
+
} catch (error) {
|
|
2199
|
+
throw this.wrapError(error, "Failed to count by scope");
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
1624
2202
|
async getStats() {
|
|
1625
2203
|
this.ensureInitialized();
|
|
1626
2204
|
try {
|
|
@@ -1781,6 +2359,15 @@ var KnowledgeSDK = class {
|
|
|
1781
2359
|
return 0;
|
|
1782
2360
|
return this.service.cleanupOldOperations();
|
|
1783
2361
|
}
|
|
2362
|
+
// ─── Token usage ────────────────────────────────────────────
|
|
2363
|
+
async scanTokenUsage() {
|
|
2364
|
+
this.ensureInitialized();
|
|
2365
|
+
return this.tokenService.scan();
|
|
2366
|
+
}
|
|
2367
|
+
getTokenUsage(filter) {
|
|
2368
|
+
this.ensureInitialized();
|
|
2369
|
+
return this.tokenService.getAggregates(filter);
|
|
2370
|
+
}
|
|
1784
2371
|
cleanupCompletedPlanEmbeddings(maxAgeDays = 30) {
|
|
1785
2372
|
if (!this.initialized || !this.service)
|
|
1786
2373
|
return 0;
|
|
@@ -1857,6 +2444,7 @@ var KnowledgeSDK = class {
|
|
|
1857
2444
|
}
|
|
1858
2445
|
this.db = null;
|
|
1859
2446
|
this.service = null;
|
|
2447
|
+
this.tokenService = null;
|
|
1860
2448
|
}
|
|
1861
2449
|
/**
|
|
1862
2450
|
* Detect if stored embeddings have different dimensions than config.
|
|
@@ -2082,6 +2670,32 @@ function createServer(sdk) {
|
|
|
2082
2670
|
return { content: [{ type: "text", text: JSON.stringify(health, null, 2) }] };
|
|
2083
2671
|
}
|
|
2084
2672
|
);
|
|
2673
|
+
server.tool(
|
|
2674
|
+
"getTokenUsage",
|
|
2675
|
+
"Aggregated token usage for AI coding tools (input/output/cache reads/cache writes) for a date range, optionally filtered by source, model, or project.",
|
|
2676
|
+
{
|
|
2677
|
+
from: z2.string().describe('ISO date \u2014 start of range (e.g. "2025-05-01T00:00:00Z")'),
|
|
2678
|
+
to: z2.string().describe("ISO date \u2014 end of range"),
|
|
2679
|
+
source: z2.string().optional().describe('Filter by source (e.g. "claude-code")'),
|
|
2680
|
+
model: z2.string().optional().describe("Filter by model"),
|
|
2681
|
+
project: z2.string().optional().describe("Filter by project (decoded cwd basename)")
|
|
2682
|
+
},
|
|
2683
|
+
READ_ONLY,
|
|
2684
|
+
async (params) => {
|
|
2685
|
+
try {
|
|
2686
|
+
await sdk.scanTokenUsage();
|
|
2687
|
+
} catch {
|
|
2688
|
+
}
|
|
2689
|
+
const result = sdk.getTokenUsage({
|
|
2690
|
+
from: params.from,
|
|
2691
|
+
to: params.to,
|
|
2692
|
+
source: params.source,
|
|
2693
|
+
model: params.model,
|
|
2694
|
+
project: params.project
|
|
2695
|
+
});
|
|
2696
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
2697
|
+
}
|
|
2698
|
+
);
|
|
2085
2699
|
server.tool(
|
|
2086
2700
|
"createPlan",
|
|
2087
2701
|
"Create a plan with tasks. Plan auto-activates when the first task starts. Returns planId \u2014 SAVE IT and pass to addKnowledge calls.",
|