@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.
Files changed (2) hide show
  1. package/dist/index.js +628 -14
  2. 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(ne(knowledgeEntries.type, "system")).groupBy(knowledgeEntries.type);
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(ne(knowledgeEntries.type, "system")).groupBy(knowledgeEntries.scope);
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.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cognistore/mcp-server",
3
- "version": "1.2.2",
3
+ "version": "1.4.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "MCP server for CogniStore — integrates with Claude Code and GitHub Copilot",