@cognistore/mcp-server 1.1.0 → 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.
Files changed (2) hide show
  1. package/dist/index.js +434 -1
  2. package/package.json +2 -2
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) {
@@ -407,6 +435,7 @@ function createPlansEmbeddingsTable(sqlite, dimensions = DEFAULT_EMBEDDING_DIMEN
407
435
 
408
436
  // ../../packages/core/dist/repositories/knowledge.repository.js
409
437
  import { eq, ne, sql, and, or, isNull } from "drizzle-orm";
438
+ var OPERATIONS_RETENTION_DAYS = 30;
410
439
  var KnowledgeRepository = class {
411
440
  db;
412
441
  sqlite;
@@ -796,7 +825,7 @@ var KnowledgeRepository = class {
796
825
  return Object.entries(map).map(([date, counts]) => ({ date, ...counts }));
797
826
  }
798
827
  cleanupOldOperations() {
799
- const cutoff = new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3).toISOString();
828
+ const cutoff = new Date(Date.now() - OPERATIONS_RETENTION_DAYS * 24 * 60 * 60 * 1e3).toISOString();
800
829
  return this.sqlite.prepare("DELETE FROM operations_log WHERE created_at < ?").run(cutoff).changes;
801
830
  }
802
831
  /**
@@ -1287,6 +1316,371 @@ var KnowledgeService = class {
1287
1316
  }
1288
1317
  };
1289
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
+
1290
1684
  // ../../packages/embeddings/dist/client.js
1291
1685
  var OllamaEmbeddingClient = class {
1292
1686
  host;
@@ -1493,6 +1887,7 @@ var KnowledgeSDK = class {
1493
1887
  db = null;
1494
1888
  sqlite = null;
1495
1889
  service = null;
1890
+ tokenService = null;
1496
1891
  ollamaClient;
1497
1892
  initialized = false;
1498
1893
  constructor(config) {
@@ -1522,6 +1917,8 @@ var KnowledgeSDK = class {
1522
1917
  await this.detectAndMigrateDimensions();
1523
1918
  const repository = new KnowledgeRepository(this.db, this.sqlite);
1524
1919
  this.service = new KnowledgeService(repository, this.ollamaClient);
1920
+ const tokenRepo = new TokenUsageRepository(this.sqlite);
1921
+ this.tokenService = new TokenUsageService(tokenRepo);
1525
1922
  this.initialized = true;
1526
1923
  } catch (error) {
1527
1924
  await this.cleanup();
@@ -1780,6 +2177,15 @@ var KnowledgeSDK = class {
1780
2177
  return 0;
1781
2178
  return this.service.cleanupOldOperations();
1782
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
+ }
1783
2189
  cleanupCompletedPlanEmbeddings(maxAgeDays = 30) {
1784
2190
  if (!this.initialized || !this.service)
1785
2191
  return 0;
@@ -1856,6 +2262,7 @@ var KnowledgeSDK = class {
1856
2262
  }
1857
2263
  this.db = null;
1858
2264
  this.service = null;
2265
+ this.tokenService = null;
1859
2266
  }
1860
2267
  /**
1861
2268
  * Detect if stored embeddings have different dimensions than config.
@@ -2081,6 +2488,32 @@ function createServer(sdk) {
2081
2488
  return { content: [{ type: "text", text: JSON.stringify(health, null, 2) }] };
2082
2489
  }
2083
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
+ );
2084
2517
  server.tool(
2085
2518
  "createPlan",
2086
2519
  "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,10 +1,10 @@
1
1
  {
2
2
  "name": "@cognistore/mcp-server",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "MCP server for CogniStore — integrates with Claude Code and GitHub Copilot",
7
- "license": "BUSL-1.1",
7
+ "license": "MIT",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/Sithion/cognistore.git",