ctxo-mcp 0.3.1 → 0.4.2

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 CHANGED
@@ -1,20 +1,94 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  RevertDetector,
4
+ SessionRecorderAdapter,
4
5
  SimpleGitAdapter,
5
6
  SqliteStorageAdapter,
6
7
  createLogger,
7
8
  loadCoChangeMap
8
- } from "./chunk-XSHNN6PU.js";
9
+ } from "./chunk-WZKXGKKI.js";
9
10
  import {
10
11
  DetailLevelSchema,
11
12
  JsonIndexReader
12
- } from "./chunk-N6GPODUY.js";
13
+ } from "./chunk-4OI75JDS.js";
13
14
  import "./chunk-JIDIH7DS.js";
14
15
 
15
16
  // src/index.ts
16
17
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
17
18
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
+
20
+ // src/adapters/transport/http-server-transport.ts
21
+ import { createServer } from "http";
22
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
23
+ import { randomUUID } from "crypto";
24
+ var log = createLogger("ctxo:http");
25
+ async function startHttpTransport(serverFactory, port) {
26
+ const sessions = /* @__PURE__ */ new Map();
27
+ const httpServer = createServer(async (req, res) => {
28
+ res.setHeader("Access-Control-Allow-Origin", "*");
29
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
30
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id");
31
+ res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
32
+ if (req.method === "OPTIONS") {
33
+ res.writeHead(204);
34
+ res.end();
35
+ return;
36
+ }
37
+ const sessionId = req.headers["mcp-session-id"];
38
+ if (sessionId && sessions.has(sessionId)) {
39
+ try {
40
+ await sessions.get(sessionId).transport.handleRequest(req, res);
41
+ } catch (err) {
42
+ log.error("Request error: %s", err.message);
43
+ if (!res.headersSent) {
44
+ res.writeHead(500);
45
+ res.end();
46
+ }
47
+ }
48
+ return;
49
+ }
50
+ if (sessionId && !sessions.has(sessionId)) {
51
+ res.writeHead(404, { "Content-Type": "application/json" });
52
+ res.end(JSON.stringify({ jsonrpc: "2.0", error: { code: -32001, message: "Session not found" }, id: null }));
53
+ return;
54
+ }
55
+ try {
56
+ const server = await serverFactory();
57
+ const transport = new StreamableHTTPServerTransport({
58
+ sessionIdGenerator: () => randomUUID()
59
+ });
60
+ transport.onclose = () => {
61
+ if (transport.sessionId) {
62
+ sessions.delete(transport.sessionId);
63
+ log.info("Session closed: %s", transport.sessionId);
64
+ }
65
+ };
66
+ await server.connect(transport);
67
+ await transport.handleRequest(req, res);
68
+ if (transport.sessionId) {
69
+ sessions.set(transport.sessionId, { transport, server });
70
+ log.info("New session: %s (total: %d)", transport.sessionId, sessions.size);
71
+ }
72
+ } catch (err) {
73
+ log.error("Session creation error: %s", err.message);
74
+ if (!res.headersSent) {
75
+ res.writeHead(500);
76
+ res.end();
77
+ }
78
+ }
79
+ });
80
+ return new Promise((resolve, reject) => {
81
+ httpServer.on("error", reject);
82
+ httpServer.listen(port, () => {
83
+ log.info("MCP HTTP server listening on http://localhost:%d", port);
84
+ process.stderr.write(`[ctxo] MCP HTTP server running at http://localhost:${port}
85
+ `);
86
+ resolve({ httpServer });
87
+ });
88
+ });
89
+ }
90
+
91
+ // src/index.ts
18
92
  import { z as z15 } from "zod";
19
93
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
20
94
  import { join as join2 } from "path";
@@ -1342,6 +1416,9 @@ var ContextAssembler = class {
1342
1416
  isInterfaceOrType(kind) {
1343
1417
  return kind === "interface" || kind === "type";
1344
1418
  }
1419
+ estimateTokensPublic(node) {
1420
+ return this.estimateTokens(node);
1421
+ }
1345
1422
  estimateTokens(node) {
1346
1423
  if (node.startOffset !== void 0 && node.endOffset !== void 0) {
1347
1424
  return Math.ceil((node.endOffset - node.startOffset) / 4);
@@ -1400,18 +1477,575 @@ function handleGetContextForTask(storage, masking, staleness, ctxoRoot = ".ctxo"
1400
1477
 
1401
1478
  // src/adapters/mcp/get-ranked-context.ts
1402
1479
  import { z as z8 } from "zod";
1480
+
1481
+ // src/core/search/symbol-tokenizer.ts
1482
+ var STOP_WORDS = /* @__PURE__ */ new Set([
1483
+ "get",
1484
+ "set",
1485
+ "is",
1486
+ "has",
1487
+ "can",
1488
+ "do",
1489
+ "the",
1490
+ "a",
1491
+ "an",
1492
+ "of",
1493
+ "to",
1494
+ "in",
1495
+ "for",
1496
+ "on",
1497
+ "with",
1498
+ "at",
1499
+ "by",
1500
+ "from",
1501
+ "as",
1502
+ "into",
1503
+ "not",
1504
+ "no",
1505
+ "or"
1506
+ ]);
1507
+ var DEFAULT_OPTIONS = {
1508
+ includeStopWords: false,
1509
+ includeOriginal: true,
1510
+ includeFilePath: false
1511
+ };
1512
+ var SymbolTokenizer = class _SymbolTokenizer {
1513
+ options;
1514
+ constructor(options) {
1515
+ this.options = { ...DEFAULT_OPTIONS, ...options };
1516
+ }
1517
+ /**
1518
+ * Tokenize a symbol name into searchable tokens.
1519
+ *
1520
+ * Examples:
1521
+ * "getCoChangeMetrics" → ["get", "co", "change", "metrics"] (or without stop words: ["co", "change", "metrics"])
1522
+ * "SqliteStorageAdapter" → ["sqlite", "storage", "adapter"]
1523
+ * "i_storage_port" → ["i", "storage", "port"]
1524
+ * "BM25Scorer" → ["bm", "25", "scorer"]
1525
+ * "DB_PASSWORD" → ["db", "password"]
1526
+ */
1527
+ tokenize(symbolName, filePath) {
1528
+ const splitTokens = _SymbolTokenizer.splitName(symbolName);
1529
+ const lowered = splitTokens.map((t) => t.toLowerCase());
1530
+ const tokens = this.options.includeStopWords ? lowered : lowered.filter((t) => !STOP_WORDS.has(t));
1531
+ if (this.options.includeOriginal) {
1532
+ const original = symbolName.toLowerCase();
1533
+ if (!tokens.includes(original)) {
1534
+ tokens.unshift(original);
1535
+ }
1536
+ }
1537
+ if (this.options.includeFilePath && filePath) {
1538
+ const pathTokens = _SymbolTokenizer.tokenizeFilePath(filePath);
1539
+ for (const pt of pathTokens) {
1540
+ if (!tokens.includes(pt)) {
1541
+ tokens.push(pt);
1542
+ }
1543
+ }
1544
+ }
1545
+ return tokens;
1546
+ }
1547
+ /**
1548
+ * Tokenize a search query. Queries use stop words and don't include originals.
1549
+ */
1550
+ tokenizeQuery(query) {
1551
+ const words = query.trim().split(/\s+/);
1552
+ const tokens = [];
1553
+ for (const word of words) {
1554
+ const split = _SymbolTokenizer.splitName(word);
1555
+ for (const t of split) {
1556
+ const lowered = t.toLowerCase();
1557
+ if (lowered.length > 0 && !tokens.includes(lowered)) {
1558
+ tokens.push(lowered);
1559
+ }
1560
+ }
1561
+ }
1562
+ return tokens;
1563
+ }
1564
+ /**
1565
+ * Split a name into constituent words based on naming convention boundaries.
1566
+ *
1567
+ * Rules:
1568
+ * 1. Split on underscores and hyphens (snake_case, kebab-case)
1569
+ * 2. Split on camelCase/PascalCase boundaries (uppercase after lowercase)
1570
+ * 3. Split acronym boundaries (e.g., "HTMLParser" → ["HTML", "Parser"])
1571
+ * 4. Split on digit ↔ letter boundaries
1572
+ */
1573
+ static splitName(name) {
1574
+ if (!name || name.length === 0) return [];
1575
+ const segments = name.split(/[_\-./\\]+/).filter((s) => s.length > 0);
1576
+ const tokens = [];
1577
+ for (const segment of segments) {
1578
+ tokens.push(..._SymbolTokenizer.splitCamelCase(segment));
1579
+ }
1580
+ return tokens.filter((t) => t.length > 0);
1581
+ }
1582
+ /**
1583
+ * Split a single segment on camelCase/PascalCase/digit boundaries.
1584
+ */
1585
+ static splitCamelCase(segment) {
1586
+ const tokens = [];
1587
+ let current = "";
1588
+ for (let i = 0; i < segment.length; i++) {
1589
+ const ch = segment[i];
1590
+ const prev = i > 0 ? segment[i - 1] : "";
1591
+ if (current.length > 0) {
1592
+ const prevIsDigit = isDigit(prev);
1593
+ const currIsDigit = isDigit(ch);
1594
+ if (prevIsDigit !== currIsDigit) {
1595
+ tokens.push(current);
1596
+ current = ch;
1597
+ continue;
1598
+ }
1599
+ }
1600
+ if (current.length > 0 && isLower(prev) && isUpper(ch)) {
1601
+ tokens.push(current);
1602
+ current = ch;
1603
+ continue;
1604
+ }
1605
+ if (current.length > 1 && isUpper(ch) === false && isUpper(prev) && !isDigit(ch)) {
1606
+ const lastChar = current[current.length - 1];
1607
+ if (isUpper(lastChar)) {
1608
+ tokens.push(current.slice(0, -1));
1609
+ current = lastChar + ch;
1610
+ continue;
1611
+ }
1612
+ }
1613
+ current += ch;
1614
+ }
1615
+ if (current.length > 0) {
1616
+ tokens.push(current);
1617
+ }
1618
+ return tokens;
1619
+ }
1620
+ /**
1621
+ * Extract searchable tokens from a file path.
1622
+ * "src/core/search/symbol-tokenizer.ts" → ["src", "core", "search", "symbol", "tokenizer", "ts"]
1623
+ */
1624
+ static tokenizeFilePath(filePath) {
1625
+ const withoutExt = filePath.replace(/\.[^.]+$/, "");
1626
+ const segments = withoutExt.split(/[/\\]+/);
1627
+ const tokens = [];
1628
+ for (const seg of segments) {
1629
+ const parts = seg.split(/[_\-.]+/).filter((s) => s.length > 0);
1630
+ for (const p of parts) {
1631
+ const lowered = p.toLowerCase();
1632
+ if (lowered.length > 0 && !tokens.includes(lowered)) {
1633
+ tokens.push(lowered);
1634
+ }
1635
+ }
1636
+ }
1637
+ return tokens;
1638
+ }
1639
+ /** Check if a token is a stop word */
1640
+ static isStopWord(token) {
1641
+ return STOP_WORDS.has(token.toLowerCase());
1642
+ }
1643
+ };
1644
+ function isUpper(ch) {
1645
+ return ch >= "A" && ch <= "Z";
1646
+ }
1647
+ function isLower(ch) {
1648
+ return ch >= "a" && ch <= "z";
1649
+ }
1650
+ function isDigit(ch) {
1651
+ return ch >= "0" && ch <= "9";
1652
+ }
1653
+
1654
+ // src/core/search/fuzzy-corrector.ts
1655
+ var FuzzyCorrector = class {
1656
+ /** term → frequency (for tie-breaking) */
1657
+ vocabulary = /* @__PURE__ */ new Map();
1658
+ buildVocabulary(termFrequencies) {
1659
+ this.vocabulary = new Map(termFrequencies);
1660
+ }
1661
+ /**
1662
+ * Attempt to correct query tokens.
1663
+ * Returns null if no correction found or no improvement possible.
1664
+ */
1665
+ correct(queryTokens) {
1666
+ if (this.vocabulary.size === 0) return null;
1667
+ const corrections = [];
1668
+ const correctedTokens = [];
1669
+ let anyChanged = false;
1670
+ for (const token of queryTokens) {
1671
+ if (token.length <= 2) {
1672
+ correctedTokens.push(token);
1673
+ continue;
1674
+ }
1675
+ if (this.vocabulary.has(token)) {
1676
+ correctedTokens.push(token);
1677
+ continue;
1678
+ }
1679
+ const maxDist = token.length <= 5 ? 1 : 2;
1680
+ const best = this.findClosest(token, maxDist);
1681
+ if (best) {
1682
+ corrections.push({
1683
+ original: token,
1684
+ corrected: best.term,
1685
+ distance: best.distance
1686
+ });
1687
+ correctedTokens.push(best.term);
1688
+ anyChanged = true;
1689
+ } else {
1690
+ correctedTokens.push(token);
1691
+ }
1692
+ }
1693
+ if (!anyChanged) return null;
1694
+ return {
1695
+ originalQuery: queryTokens.join(" "),
1696
+ correctedQuery: correctedTokens.join(" "),
1697
+ corrections
1698
+ };
1699
+ }
1700
+ /**
1701
+ * Find the closest vocabulary term within maxDistance.
1702
+ * Tie-breaks by frequency (higher = preferred).
1703
+ */
1704
+ findClosest(token, maxDistance) {
1705
+ let bestTerm = null;
1706
+ let bestDist = maxDistance + 1;
1707
+ let bestFreq = -1;
1708
+ for (const [vocabTerm, freq] of this.vocabulary) {
1709
+ if (Math.abs(vocabTerm.length - token.length) > maxDistance) continue;
1710
+ const dist = damerauLevenshtein(token, vocabTerm);
1711
+ if (dist <= maxDistance) {
1712
+ if (dist < bestDist || dist === bestDist && freq > bestFreq) {
1713
+ bestTerm = vocabTerm;
1714
+ bestDist = dist;
1715
+ bestFreq = freq;
1716
+ }
1717
+ }
1718
+ }
1719
+ return bestTerm !== null ? { term: bestTerm, distance: bestDist } : null;
1720
+ }
1721
+ };
1722
+ function damerauLevenshtein(a, b) {
1723
+ const lenA = a.length;
1724
+ const lenB = b.length;
1725
+ if (lenA === 0) return lenB;
1726
+ if (lenB === 0) return lenA;
1727
+ const d = Array.from({ length: lenA + 1 }, () => new Array(lenB + 1).fill(0));
1728
+ for (let i = 0; i <= lenA; i++) d[i][0] = i;
1729
+ for (let j = 0; j <= lenB; j++) d[0][j] = j;
1730
+ for (let i = 1; i <= lenA; i++) {
1731
+ for (let j = 1; j <= lenB; j++) {
1732
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
1733
+ d[i][j] = Math.min(
1734
+ d[i - 1][j] + 1,
1735
+ // deletion
1736
+ d[i][j - 1] + 1,
1737
+ // insertion
1738
+ d[i - 1][j - 1] + cost
1739
+ // substitution
1740
+ );
1741
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
1742
+ d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost);
1743
+ }
1744
+ }
1745
+ }
1746
+ return d[lenA][lenB];
1747
+ }
1748
+
1749
+ // src/core/search/search-engine.ts
1750
+ var log2 = createLogger("ctxo:search");
1751
+ var DEFAULT_CONFIG = {
1752
+ bm25: { k1: 1.2, b: 0.25 },
1753
+ pageRankWeight: 0.5,
1754
+ trigramPenalty: 0.8,
1755
+ phase2Threshold: 3,
1756
+ fuzzyThreshold: 3,
1757
+ maxPerPhase: 50
1758
+ };
1759
+ var SearchEngine = class {
1760
+ config;
1761
+ tokenizer;
1762
+ fuzzy;
1763
+ // Primary index (exact + tokenized)
1764
+ documents = [];
1765
+ primaryIndex = /* @__PURE__ */ new Map();
1766
+ avgDocLength = 0;
1767
+ // Trigram index (character 3-grams)
1768
+ trigramIndex = /* @__PURE__ */ new Map();
1769
+ // PageRank scores
1770
+ pageRankScores = /* @__PURE__ */ new Map();
1771
+ constructor(config) {
1772
+ this.config = { ...DEFAULT_CONFIG, ...config };
1773
+ this.tokenizer = new SymbolTokenizer({ includeOriginal: true, includeFilePath: true });
1774
+ this.fuzzy = new FuzzyCorrector();
1775
+ }
1776
+ getTier() {
1777
+ return "in-memory";
1778
+ }
1779
+ buildIndex(symbols, pageRankScores) {
1780
+ const start = performance.now();
1781
+ this.documents = [];
1782
+ this.primaryIndex = /* @__PURE__ */ new Map();
1783
+ this.trigramIndex = /* @__PURE__ */ new Map();
1784
+ this.pageRankScores = pageRankScores ?? /* @__PURE__ */ new Map();
1785
+ let totalLength = 0;
1786
+ const vocabFreq = /* @__PURE__ */ new Map();
1787
+ for (const sym of symbols) {
1788
+ const filePath = sym.symbolId.split("::")[0] ?? "";
1789
+ const tokens = this.tokenizer.tokenize(sym.name, filePath);
1790
+ const doc = {
1791
+ symbolId: sym.symbolId,
1792
+ name: sym.name,
1793
+ kind: sym.kind,
1794
+ filePath,
1795
+ tokens,
1796
+ length: tokens.length
1797
+ };
1798
+ const docIndex = this.documents.length;
1799
+ this.documents.push(doc);
1800
+ totalLength += tokens.length;
1801
+ const termFreqs = /* @__PURE__ */ new Map();
1802
+ for (const token of tokens) {
1803
+ termFreqs.set(token, (termFreqs.get(token) ?? 0) + 1);
1804
+ vocabFreq.set(token, (vocabFreq.get(token) ?? 0) + 1);
1805
+ }
1806
+ for (const [term, tf] of termFreqs) {
1807
+ let postings = this.primaryIndex.get(term);
1808
+ if (!postings) {
1809
+ postings = [];
1810
+ this.primaryIndex.set(term, postings);
1811
+ }
1812
+ postings.push({ docIndex, termFrequency: tf });
1813
+ }
1814
+ const nameLower = sym.name.toLowerCase();
1815
+ const trigrams = this.extractTrigrams(nameLower);
1816
+ const trigramFreqs = /* @__PURE__ */ new Map();
1817
+ for (const tri of trigrams) {
1818
+ trigramFreqs.set(tri, (trigramFreqs.get(tri) ?? 0) + 1);
1819
+ }
1820
+ for (const [tri, tf] of trigramFreqs) {
1821
+ let postings = this.trigramIndex.get(tri);
1822
+ if (!postings) {
1823
+ postings = [];
1824
+ this.trigramIndex.set(tri, postings);
1825
+ }
1826
+ postings.push({ docIndex, termFrequency: tf });
1827
+ }
1828
+ }
1829
+ this.avgDocLength = this.documents.length > 0 ? totalLength / this.documents.length : 1;
1830
+ this.fuzzy.buildVocabulary(vocabFreq);
1831
+ const elapsed = performance.now() - start;
1832
+ log2.info(`Index built: ${symbols.length} symbols, ${this.primaryIndex.size} terms, ${this.trigramIndex.size} trigrams (${elapsed.toFixed(0)}ms)`);
1833
+ }
1834
+ search(query, limit = 50) {
1835
+ const start = performance.now();
1836
+ const queryTokens = this.tokenizer.tokenizeQuery(query);
1837
+ if (queryTokens.length === 0) {
1838
+ return this.emptyResponse(query, start);
1839
+ }
1840
+ const metrics = {
1841
+ porterHits: 0,
1842
+ trigramHits: 0,
1843
+ phase2Activated: false,
1844
+ fuzzyApplied: false,
1845
+ latencyMs: 0
1846
+ };
1847
+ const phase1Scores = this.bm25Search(queryTokens, this.primaryIndex);
1848
+ metrics.porterHits = phase1Scores.size;
1849
+ let allScores = phase1Scores;
1850
+ if (phase1Scores.size < this.config.phase2Threshold) {
1851
+ metrics.phase2Activated = true;
1852
+ const queryLower = query.toLowerCase();
1853
+ if (queryLower.length >= 3) {
1854
+ const queryTrigrams = this.extractTrigrams(queryLower);
1855
+ const phase2Scores = this.bm25Search(queryTrigrams, this.trigramIndex);
1856
+ metrics.trigramHits = phase2Scores.size;
1857
+ allScores = this.mergeScores(phase1Scores, phase2Scores, this.config.trigramPenalty);
1858
+ }
1859
+ }
1860
+ let fuzzyCorrection;
1861
+ if (allScores.size < this.config.fuzzyThreshold) {
1862
+ const correction = this.fuzzy.correct(queryTokens);
1863
+ if (correction) {
1864
+ metrics.fuzzyApplied = true;
1865
+ fuzzyCorrection = correction;
1866
+ const correctedTokens = this.tokenizer.tokenizeQuery(correction.correctedQuery);
1867
+ const fuzzyScores = this.bm25Search(correctedTokens, this.primaryIndex);
1868
+ allScores = this.mergeScores(allScores, fuzzyScores, 0.9);
1869
+ if (allScores.size < this.config.fuzzyThreshold && correction.correctedQuery.length >= 3) {
1870
+ const correctedTrigrams = this.extractTrigrams(correction.correctedQuery.toLowerCase());
1871
+ const fuzzyTrigramScores = this.bm25Search(correctedTrigrams, this.trigramIndex);
1872
+ allScores = this.mergeScores(allScores, fuzzyTrigramScores, this.config.trigramPenalty * 0.9);
1873
+ }
1874
+ }
1875
+ }
1876
+ const rawRelevance = new Map(allScores);
1877
+ const effectiveQueryTerms = fuzzyCorrection ? this.tokenizer.tokenizeQuery(fuzzyCorrection.correctedQuery) : queryTokens;
1878
+ if (effectiveQueryTerms.length >= 2) {
1879
+ for (const [docIdx, score] of allScores) {
1880
+ const doc = this.documents[docIdx];
1881
+ const boost = this.bigramBoost(effectiveQueryTerms, doc.tokens);
1882
+ allScores.set(docIdx, score * boost);
1883
+ }
1884
+ }
1885
+ for (const [docIdx, score] of allScores) {
1886
+ const doc = this.documents[docIdx];
1887
+ const pr = this.pageRankScores.get(doc.symbolId) ?? 0;
1888
+ allScores.set(docIdx, score * (1 + this.config.pageRankWeight * pr));
1889
+ }
1890
+ metrics.latencyMs = performance.now() - start;
1891
+ const results = this.buildResults(allScores, rawRelevance, limit);
1892
+ const phaseInfo = metrics.phase2Activated ? "phase2=yes" : "phase2=no";
1893
+ if (fuzzyCorrection) {
1894
+ log2.info(`search "${query}" \u2192 fuzzy "${fuzzyCorrection.correctedQuery}": ${results.length} results (${metrics.latencyMs.toFixed(0)}ms)`);
1895
+ } else {
1896
+ log2.info(`search "${query}": ${results.length} results, porter=${metrics.porterHits} trigram=${metrics.trigramHits} ${phaseInfo} (${metrics.latencyMs.toFixed(0)}ms)`);
1897
+ }
1898
+ return { query, results, metrics, ...fuzzyCorrection ? { fuzzyCorrection } : {} };
1899
+ }
1900
+ updateFile(filePath, symbols) {
1901
+ const oldIndices = /* @__PURE__ */ new Set();
1902
+ for (let i = 0; i < this.documents.length; i++) {
1903
+ if (this.documents[i].filePath === filePath) {
1904
+ oldIndices.add(i);
1905
+ }
1906
+ }
1907
+ if (oldIndices.size === 0 && symbols.length === 0) return;
1908
+ const allSymbols = [];
1909
+ for (let i = 0; i < this.documents.length; i++) {
1910
+ if (!oldIndices.has(i)) {
1911
+ const doc = this.documents[i];
1912
+ allSymbols.push({
1913
+ symbolId: doc.symbolId,
1914
+ name: doc.name,
1915
+ kind: doc.kind,
1916
+ startLine: 0,
1917
+ endLine: 0
1918
+ });
1919
+ }
1920
+ }
1921
+ allSymbols.push(...symbols);
1922
+ this.buildIndex(allSymbols, this.pageRankScores);
1923
+ }
1924
+ /**
1925
+ * BM25 scoring over an inverted index.
1926
+ * Returns Map<docIndex, score>
1927
+ */
1928
+ bm25Search(queryTerms, index) {
1929
+ const scores = /* @__PURE__ */ new Map();
1930
+ const N = this.documents.length;
1931
+ const { k1, b } = this.config.bm25;
1932
+ for (const term of queryTerms) {
1933
+ const postings = index.get(term);
1934
+ if (!postings) continue;
1935
+ const df = postings.length;
1936
+ const idf = Math.log((N - df + 0.5) / (df + 0.5) + 1);
1937
+ for (const { docIndex, termFrequency } of postings) {
1938
+ const doc = this.documents[docIndex];
1939
+ const tf = termFrequency;
1940
+ const docLen = doc.length;
1941
+ const numerator = tf * (k1 + 1);
1942
+ const denominator = tf + k1 * (1 - b + b * (docLen / this.avgDocLength));
1943
+ const termScore = idf * (numerator / denominator);
1944
+ scores.set(docIndex, (scores.get(docIndex) ?? 0) + termScore);
1945
+ }
1946
+ }
1947
+ const queryJoined = queryTerms.join("");
1948
+ for (const [docIdx, score] of scores) {
1949
+ const doc = this.documents[docIdx];
1950
+ const nameLower = doc.name.toLowerCase();
1951
+ if (nameLower === queryJoined || nameLower === queryTerms.join(" ")) {
1952
+ scores.set(docIdx, score * 5);
1953
+ }
1954
+ }
1955
+ return scores;
1956
+ }
1957
+ /**
1958
+ * Merge two score maps. Phase 2 scores are penalized.
1959
+ */
1960
+ mergeScores(primary, secondary, penalty) {
1961
+ const merged = new Map(primary);
1962
+ for (const [docIdx, score] of secondary) {
1963
+ const existing = merged.get(docIdx);
1964
+ const penalized = score * penalty;
1965
+ if (existing !== void 0) {
1966
+ merged.set(docIdx, Math.max(existing, penalized));
1967
+ } else {
1968
+ merged.set(docIdx, penalized);
1969
+ }
1970
+ }
1971
+ return merged;
1972
+ }
1973
+ /**
1974
+ * Bigram boost for multi-word queries.
1975
+ * Each adjacent query term pair found adjacent in symbol tokens → 2x boost.
1976
+ */
1977
+ bigramBoost(queryTerms, symbolTokens) {
1978
+ if (queryTerms.length < 2) return 1;
1979
+ let adjacentPairs = 0;
1980
+ for (let i = 0; i < queryTerms.length - 1; i++) {
1981
+ const idxA = symbolTokens.findIndex((t) => t.includes(queryTerms[i]));
1982
+ const idxB = symbolTokens.findIndex((t) => t.includes(queryTerms[i + 1]));
1983
+ if (idxA >= 0 && idxB >= 0 && Math.abs(idxA - idxB) === 1) {
1984
+ adjacentPairs++;
1985
+ }
1986
+ }
1987
+ return 1 + adjacentPairs * 2;
1988
+ }
1989
+ /**
1990
+ * Extract character trigrams from a string.
1991
+ * "sqlite" → ["sql", "qli", "lit", "ite"]
1992
+ */
1993
+ extractTrigrams(text) {
1994
+ const trigrams = [];
1995
+ for (let i = 0; i <= text.length - 3; i++) {
1996
+ trigrams.push(text.substring(i, i + 3));
1997
+ }
1998
+ return trigrams;
1999
+ }
2000
+ buildResults(combinedScores, rawRelevance, limit) {
2001
+ const entries = [...combinedScores.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit);
2002
+ return entries.map(([docIdx, combined]) => {
2003
+ const doc = this.documents[docIdx];
2004
+ const pr = this.pageRankScores.get(doc.symbolId) ?? 0;
2005
+ return {
2006
+ symbolId: doc.symbolId,
2007
+ name: doc.name,
2008
+ kind: doc.kind,
2009
+ filePath: doc.filePath,
2010
+ relevanceScore: rawRelevance.get(docIdx) ?? 0,
2011
+ importanceScore: pr,
2012
+ combinedScore: combined
2013
+ };
2014
+ });
2015
+ }
2016
+ emptyResponse(query, start) {
2017
+ return {
2018
+ query,
2019
+ results: [],
2020
+ metrics: {
2021
+ porterHits: 0,
2022
+ trigramHits: 0,
2023
+ phase2Activated: false,
2024
+ fuzzyApplied: false,
2025
+ latencyMs: performance.now() - start
2026
+ }
2027
+ };
2028
+ }
2029
+ };
2030
+
2031
+ // src/adapters/mcp/get-ranked-context.ts
2032
+ var log3 = createLogger("ctxo:search");
1403
2033
  var InputSchema8 = z8.object({
1404
2034
  query: z8.string().min(1),
1405
2035
  tokenBudget: z8.number().min(100).optional().default(4e3),
1406
- strategy: z8.enum(["combined", "dependency", "importance"]).optional().default("combined")
2036
+ strategy: z8.enum(["combined", "dependency", "importance"]).optional().default("combined"),
2037
+ fuzzy: z8.boolean().optional().default(true),
2038
+ searchMode: z8.enum(["fts", "legacy"]).optional().default("fts")
1407
2039
  });
1408
- function handleGetRankedContext(storage, masking, staleness, ctxoRoot = ".ctxo") {
2040
+ function handleGetRankedContext(storage, masking, staleness, ctxoRoot = ".ctxo", searchEngine) {
1409
2041
  const assembler = new ContextAssembler();
2042
+ const engine = searchEngine ?? new SearchEngine();
1410
2043
  const getGraph = () => {
1411
2044
  const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
1412
2045
  if (jsonGraph.nodeCount > 0) return jsonGraph;
1413
2046
  return buildGraphFromStorage(storage);
1414
2047
  };
2048
+ let lastNodeCount = -1;
1415
2049
  return (args) => {
1416
2050
  try {
1417
2051
  const parsed = InputSchema8.safeParse(args);
@@ -1420,8 +2054,62 @@ function handleGetRankedContext(storage, masking, staleness, ctxoRoot = ".ctxo")
1420
2054
  content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
1421
2055
  };
1422
2056
  }
1423
- const { query, tokenBudget, strategy } = parsed.data;
2057
+ const { query, tokenBudget, strategy, searchMode } = parsed.data;
1424
2058
  const graph = getGraph();
2059
+ if (searchMode === "fts" && strategy !== "importance") {
2060
+ const currentNodeCount = graph.nodeCount;
2061
+ if (currentNodeCount !== lastNodeCount) {
2062
+ const allNodes = graph.allNodes();
2063
+ const pageRankScores = /* @__PURE__ */ new Map();
2064
+ let maxReverseEdges = 1;
2065
+ for (const node of allNodes) {
2066
+ const count = graph.getReverseEdges(node.symbolId).length;
2067
+ if (count > maxReverseEdges) maxReverseEdges = count;
2068
+ }
2069
+ for (const node of allNodes) {
2070
+ const count = graph.getReverseEdges(node.symbolId).length;
2071
+ pageRankScores.set(node.symbolId, count / maxReverseEdges);
2072
+ }
2073
+ engine.buildIndex(allNodes, pageRankScores);
2074
+ lastNodeCount = currentNodeCount;
2075
+ log3.info(`Search engine: ${engine.getTier()} (${allNodes.length} symbols)`);
2076
+ }
2077
+ const searchResult = engine.search(query, 100);
2078
+ const results = [];
2079
+ let totalTokens = 0;
2080
+ for (const sr of searchResult.results) {
2081
+ const node = graph.getNode(sr.symbolId);
2082
+ const tokens = node ? assembler.estimateTokensPublic(node) : 90;
2083
+ if (totalTokens + tokens > tokenBudget) continue;
2084
+ results.push({
2085
+ symbolId: sr.symbolId,
2086
+ name: sr.name,
2087
+ kind: sr.kind,
2088
+ file: sr.filePath,
2089
+ relevanceScore: Math.round(sr.relevanceScore * 1e3) / 1e3,
2090
+ importanceScore: Math.round(sr.importanceScore * 1e3) / 1e3,
2091
+ combinedScore: Math.round(sr.combinedScore * 1e3) / 1e3,
2092
+ tokens
2093
+ });
2094
+ totalTokens += tokens;
2095
+ }
2096
+ const payload2 = masking.mask(JSON.stringify(wrapResponse({
2097
+ query,
2098
+ strategy,
2099
+ results,
2100
+ totalTokens,
2101
+ tokenBudget,
2102
+ searchMetrics: searchResult.metrics,
2103
+ ...searchResult.fuzzyCorrection ? { fuzzyCorrection: searchResult.fuzzyCorrection } : {}
2104
+ })));
2105
+ const content2 = [];
2106
+ if (staleness) {
2107
+ const warning = staleness.check(storage.listIndexedFiles());
2108
+ if (warning) content2.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
2109
+ }
2110
+ content2.push({ type: "text", text: payload2 });
2111
+ return { content: content2 };
2112
+ }
1425
2113
  const result = assembler.assembleRanked(graph, query, strategy, tokenBudget);
1426
2114
  const payload = masking.mask(JSON.stringify(wrapResponse(result)));
1427
2115
  const content = [];
@@ -1445,9 +2133,12 @@ var InputSchema9 = z9.object({
1445
2133
  pattern: z9.string().min(1),
1446
2134
  kind: z9.enum(["function", "class", "interface", "method", "variable", "type"]).optional(),
1447
2135
  filePattern: z9.string().optional(),
1448
- limit: z9.number().int().min(1).max(100).optional().default(25)
2136
+ limit: z9.number().int().min(1).max(100).optional().default(25),
2137
+ mode: z9.enum(["regex", "fts"]).optional().default("regex")
1449
2138
  });
1450
- function handleSearchSymbols(storage, masking, staleness, ctxoRoot = ".ctxo") {
2139
+ function handleSearchSymbols(storage, masking, staleness, ctxoRoot = ".ctxo", searchEngine) {
2140
+ const engine = searchEngine ?? new SearchEngine();
2141
+ let lastFtsNodeCount = -1;
1451
2142
  const getGraph = () => {
1452
2143
  const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
1453
2144
  if (jsonGraph.nodeCount > 0) return jsonGraph;
@@ -1461,9 +2152,49 @@ function handleSearchSymbols(storage, masking, staleness, ctxoRoot = ".ctxo") {
1461
2152
  content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
1462
2153
  };
1463
2154
  }
1464
- const { pattern, kind, filePattern, limit } = parsed.data;
2155
+ const { pattern, kind, filePattern, limit, mode } = parsed.data;
1465
2156
  const graph = getGraph();
1466
2157
  const allNodes = graph.allNodes();
2158
+ if (mode === "fts") {
2159
+ if (allNodes.length !== lastFtsNodeCount) {
2160
+ engine.buildIndex(allNodes);
2161
+ lastFtsNodeCount = allNodes.length;
2162
+ }
2163
+ const searchResult = engine.search(pattern, limit);
2164
+ let ftsResults = searchResult.results;
2165
+ if (kind) {
2166
+ ftsResults = ftsResults.filter((r) => r.kind === kind);
2167
+ }
2168
+ if (filePattern) {
2169
+ const fp = filePattern.toLowerCase();
2170
+ ftsResults = ftsResults.filter((r) => r.filePath.toLowerCase().includes(fp));
2171
+ }
2172
+ const results2 = ftsResults.slice(0, limit).map((r) => {
2173
+ const node = graph.getNode(r.symbolId);
2174
+ return {
2175
+ symbolId: r.symbolId,
2176
+ name: r.name,
2177
+ kind: r.kind,
2178
+ file: r.filePath,
2179
+ startLine: node?.startLine ?? 0,
2180
+ endLine: node?.endLine ?? 0,
2181
+ relevanceScore: Math.round(r.relevanceScore * 1e3) / 1e3
2182
+ };
2183
+ });
2184
+ const payload2 = masking.mask(JSON.stringify(wrapResponse({
2185
+ totalMatches: ftsResults.length,
2186
+ results: results2,
2187
+ searchMetrics: searchResult.metrics,
2188
+ ...searchResult.fuzzyCorrection ? { fuzzyCorrection: searchResult.fuzzyCorrection } : {}
2189
+ })));
2190
+ const content2 = [];
2191
+ if (staleness) {
2192
+ const warning = staleness.check(storage.listIndexedFiles());
2193
+ if (warning) content2.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
2194
+ }
2195
+ content2.push({ type: "text", text: payload2 });
2196
+ return { content: content2 };
2197
+ }
1467
2198
  let matcher;
1468
2199
  try {
1469
2200
  const regex = new RegExp(pattern, "i");
@@ -2119,26 +2850,85 @@ function handleGetPrImpact(storage, git, masking, staleness, ctxoRoot = ".ctxo")
2119
2850
  };
2120
2851
  }
2121
2852
 
2853
+ // src/adapters/stats/with-recording.ts
2854
+ function withRecording(toolName, handler, recorder) {
2855
+ if (!recorder) return handler;
2856
+ return (args) => {
2857
+ const start = performance.now();
2858
+ const resultOrPromise = handler(args);
2859
+ if (resultOrPromise instanceof Promise) {
2860
+ return resultOrPromise.then((result) => {
2861
+ recordEvent(toolName, args, result, performance.now() - start, recorder);
2862
+ return result;
2863
+ });
2864
+ }
2865
+ const latencyMs = performance.now() - start;
2866
+ recordEvent(toolName, args, resultOrPromise, latencyMs, recorder);
2867
+ return resultOrPromise;
2868
+ };
2869
+ }
2870
+ function recordEvent(toolName, args, result, latencyMs, recorder) {
2871
+ try {
2872
+ const responseText = result.content.filter((c) => c.type === "text").map((c) => c.text).join("");
2873
+ let truncated = false;
2874
+ try {
2875
+ const parsed = JSON.parse(responseText);
2876
+ truncated = parsed?._meta?.truncated ?? false;
2877
+ } catch {
2878
+ }
2879
+ const responseBytes = Buffer.byteLength(responseText, "utf-8");
2880
+ const rawLevel = args["level"];
2881
+ let detailLevel = null;
2882
+ if (typeof rawLevel === "number" && rawLevel >= 1 && rawLevel <= 4) {
2883
+ detailLevel = `L${rawLevel}`;
2884
+ }
2885
+ const event = {
2886
+ tool: toolName,
2887
+ symbolId: typeof args["symbolId"] === "string" ? args["symbolId"] : null,
2888
+ detailLevel,
2889
+ responseTokens: Math.ceil(responseBytes / 4),
2890
+ responseBytes,
2891
+ latencyMs,
2892
+ truncated
2893
+ };
2894
+ recorder.record(event);
2895
+ } catch {
2896
+ }
2897
+ }
2898
+
2122
2899
  // src/index.ts
2123
- var log = createLogger("ctxo:mcp");
2900
+ var log4 = createLogger("ctxo:mcp");
2124
2901
  function loadMaskingConfig(ctxoRoot) {
2125
2902
  const jsonConfigPath = join2(ctxoRoot, "masking.json");
2126
2903
  if (existsSync2(jsonConfigPath)) {
2127
2904
  try {
2128
2905
  const raw = readFileSync2(jsonConfigPath, "utf-8");
2129
2906
  const patterns = JSON.parse(raw);
2130
- log.info(`Loaded ${patterns.length} custom masking pattern(s)`);
2907
+ log4.info(`Loaded ${patterns.length} custom masking pattern(s)`);
2131
2908
  return MaskingPipeline.fromConfig(patterns);
2132
2909
  } catch (err) {
2133
- log.error(`Failed to load masking config: ${err.message}`);
2910
+ log4.error(`Failed to load masking config: ${err.message}`);
2134
2911
  }
2135
2912
  }
2136
2913
  return new MaskingPipeline();
2137
2914
  }
2915
+ function readStatsEnabled(ctxoRoot) {
2916
+ const configPath = join2(ctxoRoot, "config.yaml");
2917
+ if (!existsSync2(configPath)) return true;
2918
+ try {
2919
+ const raw = readFileSync2(configPath, "utf-8");
2920
+ const match = raw.match(/stats:\s*\n\s*enabled:\s*(true|false)/);
2921
+ if (match) return match[1] !== "false";
2922
+ return true;
2923
+ } catch {
2924
+ return true;
2925
+ }
2926
+ }
2138
2927
  async function main() {
2139
2928
  const args = process.argv.slice(2);
2140
- if (args.length > 0) {
2141
- const { CliRouter } = await import("./cli-router-NRUGPICL.js");
2929
+ const httpPortStr = process.env.CTXO_HTTP_PORT || (args.includes("--http") ? args[args.indexOf("--port") + 1] || "3001" : null);
2930
+ if (!httpPortStr && args.length > 0) {
2931
+ const { CliRouter } = await import("./cli-router-H557TJAM.js");
2142
2932
  const router = new CliRouter(process.cwd());
2143
2933
  await router.route(args);
2144
2934
  return;
@@ -2148,18 +2938,32 @@ async function main() {
2148
2938
  await storage.init();
2149
2939
  const masking = loadMaskingConfig(ctxoRoot);
2150
2940
  const git = new SimpleGitAdapter(process.cwd());
2941
+ const recorder = readStatsEnabled(ctxoRoot) ? new SessionRecorderAdapter(storage.getDb(), () => storage.persist()) : null;
2151
2942
  const server = new McpServer({ name: "ctxo", version: "0.1.0" });
2152
2943
  const { StalenessDetector } = await import("./staleness-detector-VSDPTPX7.js");
2153
2944
  const staleness = new StalenessDetector(process.cwd(), ctxoRoot);
2945
+ registerTools(server, storage, masking, git, staleness, recorder, ctxoRoot);
2946
+ if (httpPortStr) {
2947
+ await startHttpTransport(async () => {
2948
+ const s = new McpServer({ name: "ctxo", version: "0.1.0" });
2949
+ registerTools(s, storage, masking, git, staleness, recorder, ctxoRoot);
2950
+ return s;
2951
+ }, parseInt(httpPortStr, 10));
2952
+ } else {
2953
+ const transport = new StdioServerTransport();
2954
+ await server.connect(transport);
2955
+ }
2956
+ }
2957
+ function registerTools(server, storage, masking, git, staleness, recorder, ctxoRoot = ".ctxo") {
2154
2958
  const toolAnnotations = {
2155
2959
  readOnlyHint: true,
2156
2960
  destructiveHint: false,
2157
2961
  idempotentHint: true,
2158
2962
  openWorldHint: false
2159
2963
  };
2160
- const logicSliceHandler = handleGetLogicSlice(storage, masking, staleness, ctxoRoot);
2161
- const whyContextHandler = handleGetWhyContext(storage, git, masking, staleness, ctxoRoot);
2162
- const changeIntelligenceHandler = handleGetChangeIntelligence(storage, git, masking, staleness, ctxoRoot);
2964
+ const logicSliceHandler = withRecording("get_logic_slice", handleGetLogicSlice(storage, masking, staleness, ctxoRoot), recorder);
2965
+ const whyContextHandler = withRecording("get_why_context", handleGetWhyContext(storage, git, masking, staleness, ctxoRoot), recorder);
2966
+ const changeIntelligenceHandler = withRecording("get_change_intelligence", handleGetChangeIntelligence(storage, git, masking, staleness, ctxoRoot), recorder);
2163
2967
  server.registerTool(
2164
2968
  "get_logic_slice",
2165
2969
  {
@@ -2172,7 +2976,7 @@ async function main() {
2172
2976
  },
2173
2977
  annotations: toolAnnotations
2174
2978
  },
2175
- (args2) => logicSliceHandler(args2)
2979
+ (args) => logicSliceHandler(args)
2176
2980
  );
2177
2981
  server.registerTool(
2178
2982
  "get_why_context",
@@ -2184,7 +2988,7 @@ async function main() {
2184
2988
  },
2185
2989
  annotations: toolAnnotations
2186
2990
  },
2187
- (args2) => whyContextHandler(args2)
2991
+ (args) => whyContextHandler(args)
2188
2992
  );
2189
2993
  server.registerTool(
2190
2994
  "get_change_intelligence",
@@ -2195,9 +2999,9 @@ async function main() {
2195
2999
  },
2196
3000
  annotations: toolAnnotations
2197
3001
  },
2198
- (args2) => changeIntelligenceHandler(args2)
3002
+ (args) => changeIntelligenceHandler(args)
2199
3003
  );
2200
- const blastRadiusHandler = handleGetBlastRadius(storage, masking, staleness, ctxoRoot);
3004
+ const blastRadiusHandler = withRecording("get_blast_radius", handleGetBlastRadius(storage, masking, staleness, ctxoRoot), recorder);
2201
3005
  server.registerTool(
2202
3006
  "get_blast_radius",
2203
3007
  {
@@ -2209,9 +3013,9 @@ async function main() {
2209
3013
  },
2210
3014
  annotations: toolAnnotations
2211
3015
  },
2212
- (args2) => blastRadiusHandler(args2)
3016
+ (args) => blastRadiusHandler(args)
2213
3017
  );
2214
- const overlayHandler = handleGetArchitecturalOverlay(storage, masking, staleness);
3018
+ const overlayHandler = withRecording("get_architectural_overlay", handleGetArchitecturalOverlay(storage, masking, staleness), recorder);
2215
3019
  server.registerTool(
2216
3020
  "get_architectural_overlay",
2217
3021
  {
@@ -2221,9 +3025,9 @@ async function main() {
2221
3025
  },
2222
3026
  annotations: toolAnnotations
2223
3027
  },
2224
- (args2) => overlayHandler(args2)
3028
+ (args) => overlayHandler(args)
2225
3029
  );
2226
- const deadCodeHandler = handleFindDeadCode(storage, masking, staleness, ctxoRoot);
3030
+ const deadCodeHandler = withRecording("find_dead_code", handleFindDeadCode(storage, masking, staleness, ctxoRoot), recorder);
2227
3031
  server.registerTool(
2228
3032
  "find_dead_code",
2229
3033
  {
@@ -2234,9 +3038,9 @@ async function main() {
2234
3038
  },
2235
3039
  annotations: toolAnnotations
2236
3040
  },
2237
- (args2) => deadCodeHandler(args2)
3041
+ (args) => deadCodeHandler(args)
2238
3042
  );
2239
- const contextForTaskHandler = handleGetContextForTask(storage, masking, staleness, ctxoRoot);
3043
+ const contextForTaskHandler = withRecording("get_context_for_task", handleGetContextForTask(storage, masking, staleness, ctxoRoot), recorder);
2240
3044
  server.registerTool(
2241
3045
  "get_context_for_task",
2242
3046
  {
@@ -2248,9 +3052,9 @@ async function main() {
2248
3052
  },
2249
3053
  annotations: toolAnnotations
2250
3054
  },
2251
- (args2) => contextForTaskHandler(args2)
3055
+ (args) => contextForTaskHandler(args)
2252
3056
  );
2253
- const rankedContextHandler = handleGetRankedContext(storage, masking, staleness, ctxoRoot);
3057
+ const rankedContextHandler = withRecording("get_ranked_context", handleGetRankedContext(storage, masking, staleness, ctxoRoot), recorder);
2254
3058
  server.registerTool(
2255
3059
  "get_ranked_context",
2256
3060
  {
@@ -2262,9 +3066,9 @@ async function main() {
2262
3066
  },
2263
3067
  annotations: toolAnnotations
2264
3068
  },
2265
- (args2) => rankedContextHandler(args2)
3069
+ (args) => rankedContextHandler(args)
2266
3070
  );
2267
- const searchSymbolsHandler = handleSearchSymbols(storage, masking, staleness, ctxoRoot);
3071
+ const searchSymbolsHandler = withRecording("search_symbols", handleSearchSymbols(storage, masking, staleness, ctxoRoot), recorder);
2268
3072
  server.registerTool(
2269
3073
  "search_symbols",
2270
3074
  {
@@ -2277,9 +3081,9 @@ async function main() {
2277
3081
  },
2278
3082
  annotations: toolAnnotations
2279
3083
  },
2280
- (args2) => searchSymbolsHandler(args2)
3084
+ (args) => searchSymbolsHandler(args)
2281
3085
  );
2282
- const changedSymbolsHandler = handleGetChangedSymbols(storage, git, masking, staleness, ctxoRoot);
3086
+ const changedSymbolsHandler = withRecording("get_changed_symbols", handleGetChangedSymbols(storage, git, masking, staleness, ctxoRoot), recorder);
2283
3087
  server.registerTool(
2284
3088
  "get_changed_symbols",
2285
3089
  {
@@ -2290,9 +3094,9 @@ async function main() {
2290
3094
  },
2291
3095
  annotations: toolAnnotations
2292
3096
  },
2293
- (args2) => changedSymbolsHandler(args2)
3097
+ (args) => changedSymbolsHandler(args)
2294
3098
  );
2295
- const findImportersHandler = handleFindImporters(storage, masking, staleness, ctxoRoot);
3099
+ const findImportersHandler = withRecording("find_importers", handleFindImporters(storage, masking, staleness, ctxoRoot), recorder);
2296
3100
  server.registerTool(
2297
3101
  "find_importers",
2298
3102
  {
@@ -2306,9 +3110,9 @@ async function main() {
2306
3110
  },
2307
3111
  annotations: toolAnnotations
2308
3112
  },
2309
- (args2) => findImportersHandler(args2)
3113
+ (args) => findImportersHandler(args)
2310
3114
  );
2311
- const classHierarchyHandler = handleGetClassHierarchy(storage, masking, staleness, ctxoRoot);
3115
+ const classHierarchyHandler = withRecording("get_class_hierarchy", handleGetClassHierarchy(storage, masking, staleness, ctxoRoot), recorder);
2312
3116
  server.registerTool(
2313
3117
  "get_class_hierarchy",
2314
3118
  {
@@ -2319,9 +3123,9 @@ async function main() {
2319
3123
  },
2320
3124
  annotations: toolAnnotations
2321
3125
  },
2322
- (args2) => classHierarchyHandler(args2)
3126
+ (args) => classHierarchyHandler(args)
2323
3127
  );
2324
- const symbolImportanceHandler = handleGetSymbolImportance(storage, masking, staleness, ctxoRoot);
3128
+ const symbolImportanceHandler = withRecording("get_symbol_importance", handleGetSymbolImportance(storage, masking, staleness, ctxoRoot), recorder);
2325
3129
  server.registerTool(
2326
3130
  "get_symbol_importance",
2327
3131
  {
@@ -2334,9 +3138,9 @@ async function main() {
2334
3138
  },
2335
3139
  annotations: toolAnnotations
2336
3140
  },
2337
- (args2) => symbolImportanceHandler(args2)
3141
+ (args) => symbolImportanceHandler(args)
2338
3142
  );
2339
- const prImpactHandler = handleGetPrImpact(storage, git, masking, staleness, ctxoRoot);
3143
+ const prImpactHandler = withRecording("get_pr_impact", handleGetPrImpact(storage, git, masking, staleness, ctxoRoot), recorder);
2340
3144
  server.registerTool(
2341
3145
  "get_pr_impact",
2342
3146
  {
@@ -2348,16 +3152,14 @@ async function main() {
2348
3152
  },
2349
3153
  annotations: toolAnnotations
2350
3154
  },
2351
- (args2) => prImpactHandler(args2)
3155
+ (args) => prImpactHandler(args)
2352
3156
  );
2353
3157
  server.resource("ctxo-status", "ctxo://status", async (uri) => ({
2354
3158
  contents: [{ uri: uri.href, text: "Ctxo MCP server is running." }]
2355
3159
  }));
2356
- const transport = new StdioServerTransport();
2357
- await server.connect(transport);
2358
3160
  }
2359
3161
  main().catch((err) => {
2360
- log.error("Fatal: %s", err.message);
3162
+ log4.error("Fatal: %s", err.message);
2361
3163
  process.exit(1);
2362
3164
  });
2363
3165
  //# sourceMappingURL=index.js.map