ctxo-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,786 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ RevertDetector,
4
+ SimpleGitAdapter,
5
+ SqliteStorageAdapter
6
+ } from "./chunk-P7JUSY3I.js";
7
+ import {
8
+ DetailLevelSchema,
9
+ JsonIndexReader
10
+ } from "./chunk-54ETLIQX.js";
11
+
12
+ // src/index.ts
13
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import { z as z6 } from "zod";
16
+ import { existsSync, readFileSync } from "fs";
17
+ import { join } from "path";
18
+
19
+ // src/core/masking/masking-pipeline.ts
20
+ var DEFAULT_PATTERNS = [
21
+ // AWS Access Key IDs (AKIA...)
22
+ { regex: /AKIA[0-9A-Z]{16}/g, label: "AWS_KEY" },
23
+ // AWS Secret Access Keys (40 char base64 containing / or + — excludes hex-only git hashes)
24
+ { regex: /(?<![A-Za-z0-9/+])(?=[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=]))(?=.*[/+])[A-Za-z0-9/+=]{40}/g, label: "AWS_SECRET" },
25
+ // JWT tokens (eyJ...)
26
+ { regex: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, label: "JWT" },
27
+ // Private IPv4 (10.x.x.x, 172.16-31.x.x, 192.168.x.x)
28
+ { regex: /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g, label: "PRIVATE_IP" },
29
+ // Private IPv6 (fc00::/7)
30
+ { regex: /\b[fF][cCdD][0-9a-fA-F]{0,2}(?::[0-9a-fA-F]{1,4}){1,7}\b/g, label: "PRIVATE_IPV6" },
31
+ // Environment variable assignments for secrets
32
+ { regex: /(?:_SECRET|_KEY|_TOKEN|_PASSWORD)\s*=\s*['"]?[^\s'"]{8,}['"]?/g, label: "ENV_SECRET" },
33
+ // GCP service account keys
34
+ { regex: /"private_key"\s*:\s*"-----BEGIN[^"]+"/g, label: "GCP_KEY" },
35
+ // Azure connection strings
36
+ { regex: /AccountKey=[A-Za-z0-9+/=]{20,}/g, label: "AZURE_KEY" }
37
+ ];
38
+ var MaskingPipeline = class _MaskingPipeline {
39
+ patterns;
40
+ constructor(additionalPatterns = []) {
41
+ this.patterns = [...DEFAULT_PATTERNS, ...additionalPatterns].map(({ regex, label }) => ({
42
+ regex: new RegExp(regex.source, regex.flags),
43
+ label
44
+ }));
45
+ }
46
+ static fromConfig(configPatterns) {
47
+ const additional = [];
48
+ for (const { pattern, flags, label } of configPatterns) {
49
+ try {
50
+ additional.push({ regex: new RegExp(pattern, flags ?? "g"), label });
51
+ } catch (err) {
52
+ console.error(`[ctxo:masking] Invalid regex pattern "${pattern}": ${err.message}`);
53
+ }
54
+ }
55
+ return new _MaskingPipeline(additional);
56
+ }
57
+ mask(text) {
58
+ if (text.length === 0) return text;
59
+ let result = text;
60
+ for (const { regex, label } of this.patterns) {
61
+ result = result.replace(regex, `[REDACTED:${label}]`);
62
+ }
63
+ return result;
64
+ }
65
+ };
66
+
67
+ // src/adapters/mcp/get-logic-slice.ts
68
+ import { z } from "zod";
69
+
70
+ // src/core/graph/symbol-graph.ts
71
+ var SymbolGraph = class {
72
+ nodes = /* @__PURE__ */ new Map();
73
+ nodesByFileAndName = /* @__PURE__ */ new Map();
74
+ forwardEdges = /* @__PURE__ */ new Map();
75
+ reverseEdges = /* @__PURE__ */ new Map();
76
+ edgeSet = /* @__PURE__ */ new Set();
77
+ addNode(node) {
78
+ this.nodes.set(node.symbolId, node);
79
+ const parts = node.symbolId.split("::");
80
+ if (parts.length >= 2) {
81
+ this.nodesByFileAndName.set(`${parts[0]}::${parts[1]}`, node);
82
+ }
83
+ }
84
+ addEdge(edge) {
85
+ const resolvedFrom = this.resolveNodeId(edge.from);
86
+ const resolvedTo = this.resolveNodeId(edge.to);
87
+ const resolvedEdge = { from: resolvedFrom, to: resolvedTo, kind: edge.kind };
88
+ const edgeKey = `${resolvedEdge.from}|${resolvedEdge.to}|${resolvedEdge.kind}`;
89
+ if (this.edgeSet.has(edgeKey)) return;
90
+ this.edgeSet.add(edgeKey);
91
+ const forward = this.forwardEdges.get(resolvedEdge.from) ?? [];
92
+ forward.push(resolvedEdge);
93
+ this.forwardEdges.set(resolvedEdge.from, forward);
94
+ const reverse = this.reverseEdges.get(resolvedEdge.to) ?? [];
95
+ reverse.push(resolvedEdge);
96
+ this.reverseEdges.set(resolvedEdge.to, reverse);
97
+ }
98
+ getNode(symbolId) {
99
+ return this.nodes.get(symbolId) ?? this.resolveNodeFuzzy(symbolId);
100
+ }
101
+ resolveNodeId(id) {
102
+ if (this.nodes.has(id)) return id;
103
+ const parts = id.split("::");
104
+ if (parts.length >= 2) {
105
+ const fuzzyKey = `${parts[0]}::${parts[1]}`;
106
+ const match = this.nodesByFileAndName.get(fuzzyKey);
107
+ if (match) return match.symbolId;
108
+ }
109
+ return id;
110
+ }
111
+ resolveNodeFuzzy(id) {
112
+ const parts = id.split("::");
113
+ if (parts.length >= 2) {
114
+ return this.nodesByFileAndName.get(`${parts[0]}::${parts[1]}`);
115
+ }
116
+ return void 0;
117
+ }
118
+ getForwardEdges(symbolId) {
119
+ return this.forwardEdges.get(symbolId) ?? [];
120
+ }
121
+ getReverseEdges(symbolId) {
122
+ return this.reverseEdges.get(symbolId) ?? [];
123
+ }
124
+ hasNode(symbolId) {
125
+ return this.nodes.has(symbolId);
126
+ }
127
+ get nodeCount() {
128
+ return this.nodes.size;
129
+ }
130
+ get edgeCount() {
131
+ return this.edgeSet.size;
132
+ }
133
+ allNodes() {
134
+ return [...this.nodes.values()];
135
+ }
136
+ allEdges() {
137
+ const edges = [];
138
+ for (const list of this.forwardEdges.values()) {
139
+ edges.push(...list);
140
+ }
141
+ return edges;
142
+ }
143
+ };
144
+
145
+ // src/core/logic-slice/logic-slice-query.ts
146
+ var LogicSliceQuery = class {
147
+ getLogicSlice(graph, symbolId, maxDepth = Infinity) {
148
+ const root = graph.getNode(symbolId);
149
+ if (!root) return void 0;
150
+ const visited = /* @__PURE__ */ new Set([symbolId]);
151
+ const dependencies = [];
152
+ const collectedEdges = [];
153
+ const queue = [{ id: symbolId, depth: 0 }];
154
+ while (queue.length > 0) {
155
+ const current = queue.shift();
156
+ if (current.depth >= maxDepth) continue;
157
+ const edges = graph.getForwardEdges(current.id);
158
+ for (const edge of edges) {
159
+ const depNode = graph.getNode(edge.to);
160
+ if (!depNode) continue;
161
+ collectedEdges.push(edge);
162
+ if (visited.has(edge.to)) continue;
163
+ visited.add(edge.to);
164
+ dependencies.push(depNode);
165
+ queue.push({ id: edge.to, depth: current.depth + 1 });
166
+ }
167
+ }
168
+ return { root, dependencies, edges: collectedEdges };
169
+ }
170
+ };
171
+
172
+ // src/core/detail-levels/detail-formatter.ts
173
+ var L1_MAX_LINES = 150;
174
+ var L4_TOKEN_BUDGET = 8e3;
175
+ var DetailFormatter = class {
176
+ format(slice, level) {
177
+ switch (level) {
178
+ case 1:
179
+ return this.formatL1(slice);
180
+ case 2:
181
+ return this.formatL2(slice);
182
+ case 3:
183
+ return this.formatL3(slice);
184
+ case 4:
185
+ return this.formatL4(slice);
186
+ }
187
+ }
188
+ formatL1(slice) {
189
+ const root = slice.root;
190
+ const lineCount = root.endLine - root.startLine + 1;
191
+ const clampedRoot = lineCount > L1_MAX_LINES ? { ...root, endLine: root.startLine + L1_MAX_LINES - 1 } : root;
192
+ return {
193
+ root: clampedRoot,
194
+ dependencies: [],
195
+ edges: [],
196
+ level: 1,
197
+ levelDescription: "L1: Root symbol signature only (no dependencies)",
198
+ ...lineCount > L1_MAX_LINES ? { truncation: { truncated: true, reason: "token_budget_exceeded" } } : {}
199
+ };
200
+ }
201
+ formatL2(slice) {
202
+ const directEdges = slice.edges.filter((e) => e.from === slice.root.symbolId);
203
+ const directDepIds = new Set(directEdges.map((e) => e.to));
204
+ const directDeps = slice.dependencies.filter((d) => directDepIds.has(d.symbolId));
205
+ return {
206
+ root: slice.root,
207
+ dependencies: directDeps,
208
+ edges: directEdges,
209
+ level: 2,
210
+ levelDescription: "L2: Root + direct dependencies (depth 1)"
211
+ };
212
+ }
213
+ formatL3(slice) {
214
+ return {
215
+ root: slice.root,
216
+ dependencies: slice.dependencies,
217
+ edges: slice.edges,
218
+ level: 3,
219
+ levelDescription: "L3: Full transitive dependency closure"
220
+ };
221
+ }
222
+ formatL4(slice) {
223
+ const estimatedTokens = this.estimateTokens(slice);
224
+ if (estimatedTokens > L4_TOKEN_BUDGET) {
225
+ const truncated = this.truncateToTokenBudget(slice, L4_TOKEN_BUDGET);
226
+ return {
227
+ ...truncated,
228
+ level: 4,
229
+ levelDescription: "L4: Full closure with 8K token budget (truncated)",
230
+ truncation: {
231
+ truncated: true,
232
+ reason: "token_budget_exceeded"
233
+ }
234
+ };
235
+ }
236
+ return {
237
+ root: slice.root,
238
+ dependencies: slice.dependencies,
239
+ edges: slice.edges,
240
+ level: 4,
241
+ levelDescription: "L4: Full closure with 8K token budget"
242
+ };
243
+ }
244
+ estimateTokens(slice) {
245
+ let chars = this.symbolCharEstimate(slice.root);
246
+ for (const dep of slice.dependencies) {
247
+ chars += this.symbolCharEstimate(dep);
248
+ }
249
+ for (const edge of slice.edges) {
250
+ chars += edge.from.length + edge.to.length + edge.kind.length + 10;
251
+ }
252
+ return Math.ceil(chars / 4);
253
+ }
254
+ symbolCharEstimate(node) {
255
+ return (node.endLine - node.startLine + 1) * 40;
256
+ }
257
+ truncateToTokenBudget(slice, budget) {
258
+ let currentTokens = this.symbolCharEstimate(slice.root) / 4;
259
+ const deps = [];
260
+ const includedIds = /* @__PURE__ */ new Set([slice.root.symbolId]);
261
+ for (const dep of slice.dependencies) {
262
+ const depTokens = this.symbolCharEstimate(dep) / 4;
263
+ if (currentTokens + depTokens > budget) break;
264
+ currentTokens += depTokens;
265
+ deps.push(dep);
266
+ includedIds.add(dep.symbolId);
267
+ }
268
+ const edges = slice.edges.filter(
269
+ (e) => includedIds.has(e.from) && includedIds.has(e.to)
270
+ );
271
+ return { root: slice.root, dependencies: deps, edges };
272
+ }
273
+ };
274
+
275
+ // src/adapters/mcp/get-logic-slice.ts
276
+ var InputSchema = z.object({
277
+ symbolId: z.string().min(1).optional(),
278
+ symbolIds: z.array(z.string().min(1)).optional(),
279
+ level: DetailLevelSchema.optional().default(3)
280
+ }).refine(
281
+ (data) => data.symbolId || data.symbolIds && data.symbolIds.length > 0,
282
+ { message: "Either symbolId or symbolIds must be provided" }
283
+ );
284
+ function buildGraphFromStorage(storage) {
285
+ const graph = new SymbolGraph();
286
+ for (const sym of storage.getAllSymbols()) {
287
+ graph.addNode(sym);
288
+ }
289
+ for (const edge of storage.getAllEdges()) {
290
+ graph.addEdge(edge);
291
+ }
292
+ return graph;
293
+ }
294
+ function buildGraphFromJsonIndex(ctxoRoot) {
295
+ const reader = new JsonIndexReader(ctxoRoot);
296
+ const indices = reader.readAll();
297
+ const graph = new SymbolGraph();
298
+ for (const fileIndex of indices) {
299
+ for (const sym of fileIndex.symbols) {
300
+ graph.addNode(sym);
301
+ }
302
+ }
303
+ for (const fileIndex of indices) {
304
+ for (const edge of fileIndex.edges) {
305
+ graph.addEdge(edge);
306
+ }
307
+ }
308
+ return graph;
309
+ }
310
+ function handleGetLogicSlice(storage, masking, staleness, ctxoRoot = ".ctxo") {
311
+ const query = new LogicSliceQuery();
312
+ const formatter = new DetailFormatter();
313
+ const getGraph = () => {
314
+ const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
315
+ if (jsonGraph.nodeCount > 0) return jsonGraph;
316
+ return buildGraphFromStorage(storage);
317
+ };
318
+ const handler = (args) => {
319
+ try {
320
+ const parsed = InputSchema.safeParse(args);
321
+ if (!parsed.success) {
322
+ return {
323
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
324
+ };
325
+ }
326
+ const { symbolId, symbolIds, level } = parsed.data;
327
+ const ids = symbolIds ?? (symbolId ? [symbolId] : []);
328
+ const graph = getGraph();
329
+ const results = [];
330
+ for (const id of ids) {
331
+ const slice = query.getLogicSlice(graph, id);
332
+ if (slice) {
333
+ results.push(formatter.format(slice, level));
334
+ } else {
335
+ results.push({ found: false, symbolId: id, hint: 'Symbol not found. Run "ctxo index".' });
336
+ }
337
+ }
338
+ const responseData = ids.length === 1 ? results[0] : { batch: true, results };
339
+ const payload = masking.mask(JSON.stringify(responseData));
340
+ const content = [];
341
+ if (staleness) {
342
+ const warning = staleness.check(storage.listIndexedFiles());
343
+ if (warning) {
344
+ content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
345
+ }
346
+ }
347
+ content.push({ type: "text", text: payload });
348
+ return { content };
349
+ } catch (err) {
350
+ return {
351
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
352
+ };
353
+ }
354
+ };
355
+ return handler;
356
+ }
357
+
358
+ // src/adapters/mcp/get-why-context.ts
359
+ import { z as z2 } from "zod";
360
+ var InputSchema2 = z2.object({
361
+ symbolId: z2.string().min(1)
362
+ });
363
+ function handleGetWhyContext(storage, git, masking, staleness, ctxoRoot = ".ctxo") {
364
+ const revertDetector = new RevertDetector();
365
+ const indexReader = new JsonIndexReader(ctxoRoot);
366
+ return async (args) => {
367
+ try {
368
+ const parsed = InputSchema2.safeParse(args);
369
+ if (!parsed.success) {
370
+ return {
371
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
372
+ };
373
+ }
374
+ const { symbolId } = parsed.data;
375
+ const symbol = storage.getSymbolById(symbolId);
376
+ if (!symbol) {
377
+ return {
378
+ content: [{ type: "text", text: JSON.stringify({ found: false, hint: 'Symbol not found. Run "ctxo index" to build the codebase index.' }) }]
379
+ };
380
+ }
381
+ const filePath = symbolId.split("::")[0];
382
+ const indices = indexReader.readAll();
383
+ const fileIndex = indices.find((i) => i.file === filePath);
384
+ let commitHistory;
385
+ let antiPatterns;
386
+ if (fileIndex && fileIndex.intent.length > 0) {
387
+ commitHistory = fileIndex.intent;
388
+ antiPatterns = fileIndex.antiPatterns;
389
+ } else {
390
+ const commits = await git.getCommitHistory(filePath);
391
+ commitHistory = commits.map((c) => ({
392
+ hash: c.hash,
393
+ message: c.message,
394
+ date: c.date,
395
+ kind: "commit"
396
+ }));
397
+ antiPatterns = revertDetector.detect(commits);
398
+ }
399
+ const responsePayload = {
400
+ commitHistory,
401
+ antiPatternWarnings: antiPatterns
402
+ };
403
+ if (antiPatterns.length > 0) {
404
+ responsePayload.warningBadge = "\u26A0 Anti-pattern detected";
405
+ }
406
+ const payload = masking.mask(JSON.stringify(responsePayload));
407
+ const content = [];
408
+ if (staleness) {
409
+ const warning = staleness.check(storage.listIndexedFiles());
410
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
411
+ }
412
+ content.push({ type: "text", text: payload });
413
+ return { content };
414
+ } catch (err) {
415
+ return {
416
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
417
+ };
418
+ }
419
+ };
420
+ }
421
+
422
+ // src/adapters/mcp/get-change-intelligence.ts
423
+ import { z as z3 } from "zod";
424
+
425
+ // src/core/change-intelligence/churn-analyzer.ts
426
+ var ChurnAnalyzer = class {
427
+ normalize(commitCount, maxCommitCount) {
428
+ if (maxCommitCount <= 0) return 0;
429
+ if (commitCount < 0) {
430
+ throw new Error(`Invalid commit count: ${commitCount}`);
431
+ }
432
+ return Math.min(commitCount / maxCommitCount, 1);
433
+ }
434
+ };
435
+
436
+ // src/core/change-intelligence/health-scorer.ts
437
+ var HealthScorer = class {
438
+ score(symbolId, complexity, churn) {
439
+ const composite = complexity * churn;
440
+ const band = this.toBand(composite);
441
+ return { symbolId, complexity, churn, composite, band };
442
+ }
443
+ toBand(composite) {
444
+ if (composite < 0.3) return "low";
445
+ if (composite < 0.7) return "medium";
446
+ return "high";
447
+ }
448
+ };
449
+
450
+ // src/adapters/mcp/get-change-intelligence.ts
451
+ var InputSchema3 = z3.object({
452
+ symbolId: z3.string().min(1)
453
+ });
454
+ function handleGetChangeIntelligence(storage, git, masking, staleness, ctxoRoot = ".ctxo") {
455
+ const churnAnalyzer = new ChurnAnalyzer();
456
+ const healthScorer = new HealthScorer();
457
+ const indexReader = new JsonIndexReader(ctxoRoot);
458
+ return async (args) => {
459
+ try {
460
+ const parsed = InputSchema3.safeParse(args);
461
+ if (!parsed.success) {
462
+ return {
463
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
464
+ };
465
+ }
466
+ const { symbolId } = parsed.data;
467
+ const symbol = storage.getSymbolById(symbolId);
468
+ if (!symbol) {
469
+ return {
470
+ content: [{ type: "text", text: JSON.stringify({ found: false, hint: 'Symbol not found. Run "ctxo index" to build the codebase index.' }) }]
471
+ };
472
+ }
473
+ const filePath = symbolId.split("::")[0];
474
+ const normalizePath = (p) => p.replace(/\\/g, "/");
475
+ const allFiles = storage.listIndexedFiles();
476
+ const churnResults = await Promise.all(allFiles.map((f) => git.getFileChurn(f)));
477
+ const maxChurn = Math.max(1, ...churnResults.map((c) => c.commitCount));
478
+ const targetChurn = churnResults.find((c) => normalizePath(c.filePath) === normalizePath(filePath));
479
+ const commitCount = targetChurn?.commitCount ?? 0;
480
+ const normalizedChurn = churnAnalyzer.normalize(commitCount, maxChurn);
481
+ const indices = indexReader.readAll();
482
+ const fileIndex = indices.find((i) => i.file === filePath);
483
+ const complexityEntries = fileIndex?.complexity ?? [];
484
+ let cyclomatic = complexityEntries.find((c) => c.symbolId === symbolId)?.cyclomatic;
485
+ if (cyclomatic === void 0) {
486
+ const symbolName = symbolId.split("::")[1] ?? "";
487
+ const relatedEntries = complexityEntries.filter(
488
+ (c) => c.symbolId.startsWith(`${filePath}::${symbolName}.`)
489
+ );
490
+ cyclomatic = relatedEntries.length > 0 ? Math.max(...relatedEntries.map((c) => c.cyclomatic)) : 1;
491
+ }
492
+ const normalizedComplexity = Math.min((cyclomatic - 1) / 9, 1);
493
+ const score = healthScorer.score(symbolId, normalizedComplexity, normalizedChurn);
494
+ const payload = masking.mask(JSON.stringify(score));
495
+ const content = [];
496
+ if (staleness) {
497
+ const warning = staleness.check(storage.listIndexedFiles());
498
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
499
+ }
500
+ content.push({ type: "text", text: payload });
501
+ return { content };
502
+ } catch (err) {
503
+ return {
504
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
505
+ };
506
+ }
507
+ };
508
+ }
509
+
510
+ // src/adapters/mcp/get-blast-radius.ts
511
+ import { z as z4 } from "zod";
512
+
513
+ // src/core/blast-radius/blast-radius-calculator.ts
514
+ var BlastRadiusCalculator = class {
515
+ calculate(graph, symbolId) {
516
+ if (!graph.hasNode(symbolId)) {
517
+ return { impactedSymbols: [], directDependentsCount: 0, overallRiskScore: 0 };
518
+ }
519
+ const visited = /* @__PURE__ */ new Set([symbolId]);
520
+ const entries = [];
521
+ const queue = [{ id: symbolId, depth: 0 }];
522
+ while (queue.length > 0) {
523
+ const current = queue.shift();
524
+ const reverseEdges = graph.getReverseEdges(current.id);
525
+ for (const edge of reverseEdges) {
526
+ if (visited.has(edge.from)) continue;
527
+ visited.add(edge.from);
528
+ if (!graph.hasNode(edge.from)) continue;
529
+ const depth = current.depth + 1;
530
+ const riskScore = 1 / Math.pow(depth, 0.7);
531
+ entries.push({
532
+ symbolId: edge.from,
533
+ depth,
534
+ dependentCount: graph.getReverseEdges(edge.from).length,
535
+ riskScore: Math.round(riskScore * 1e3) / 1e3
536
+ });
537
+ queue.push({ id: edge.from, depth });
538
+ }
539
+ }
540
+ entries.sort((a, b) => a.depth - b.depth);
541
+ const directDependentsCount = entries.filter((e) => e.depth === 1).length;
542
+ const totalRisk = entries.reduce((sum, e) => sum + e.riskScore, 0);
543
+ const maxPossibleRisk = entries.length > 0 ? entries.length : 1;
544
+ const overallRiskScore = Math.round(Math.min(totalRisk / maxPossibleRisk, 1) * 1e3) / 1e3;
545
+ return { impactedSymbols: entries, directDependentsCount, overallRiskScore };
546
+ }
547
+ };
548
+
549
+ // src/adapters/mcp/get-blast-radius.ts
550
+ var InputSchema4 = z4.object({
551
+ symbolId: z4.string().min(1)
552
+ });
553
+ function handleGetBlastRadius(storage, masking, staleness, ctxoRoot = ".ctxo") {
554
+ const calculator = new BlastRadiusCalculator();
555
+ const getGraph = () => {
556
+ const jsonGraph = buildGraphFromJsonIndex(ctxoRoot);
557
+ if (jsonGraph.nodeCount > 0) return jsonGraph;
558
+ return buildGraphFromStorage(storage);
559
+ };
560
+ return (args) => {
561
+ try {
562
+ const parsed = InputSchema4.safeParse(args);
563
+ if (!parsed.success) {
564
+ return {
565
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
566
+ };
567
+ }
568
+ const { symbolId } = parsed.data;
569
+ const graph = getGraph();
570
+ if (!graph.hasNode(symbolId)) {
571
+ return {
572
+ content: [{ type: "text", text: JSON.stringify({ found: false, hint: 'Symbol not found. Run "ctxo index" to build the codebase index.' }) }]
573
+ };
574
+ }
575
+ const result = calculator.calculate(graph, symbolId);
576
+ const payload = masking.mask(JSON.stringify({
577
+ symbolId,
578
+ impactScore: result.impactedSymbols.length,
579
+ directDependentsCount: result.directDependentsCount,
580
+ overallRiskScore: result.overallRiskScore,
581
+ impactedSymbols: result.impactedSymbols
582
+ }));
583
+ const content = [];
584
+ if (staleness) {
585
+ const warning = staleness.check(storage.listIndexedFiles());
586
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
587
+ }
588
+ content.push({ type: "text", text: payload });
589
+ return { content };
590
+ } catch (err) {
591
+ return {
592
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
593
+ };
594
+ }
595
+ };
596
+ }
597
+
598
+ // src/adapters/mcp/get-architectural-overlay.ts
599
+ import { z as z5 } from "zod";
600
+
601
+ // src/core/overlay/architectural-overlay.ts
602
+ var DEFAULT_RULES = [
603
+ // Test layer (matched first — __tests__, .test.ts, tests/, fixtures)
604
+ { pattern: /__tests__/, layer: "Test" },
605
+ { pattern: /\.test\.ts$/, layer: "Test" },
606
+ { pattern: /\btests\b/, layer: "Test" },
607
+ { pattern: /\bfixtures?\b/, layer: "Test" },
608
+ // Composition root
609
+ { pattern: /src\/index\.ts$/, layer: "Composition" },
610
+ // Domain
611
+ { pattern: /\bcore\b/, layer: "Domain" },
612
+ { pattern: /\bports?\b/, layer: "Domain" },
613
+ // Adapter
614
+ { pattern: /\badapters?\b/, layer: "Adapter" },
615
+ { pattern: /\bcli\b/, layer: "Adapter" },
616
+ // Infrastructure
617
+ { pattern: /\binfra\b/, layer: "Infrastructure" },
618
+ { pattern: /\bdb\b/, layer: "Infrastructure" },
619
+ { pattern: /\bqueue\b/, layer: "Infrastructure" },
620
+ // Config files
621
+ { pattern: /\.(config|rc)\.(ts|js|json)$/, layer: "Configuration" }
622
+ ];
623
+ var ArchitecturalOverlay = class {
624
+ rules;
625
+ constructor(customRules) {
626
+ this.rules = (customRules ?? DEFAULT_RULES).map(({ pattern, layer }) => ({
627
+ pattern: new RegExp(pattern.source, pattern.flags),
628
+ layer
629
+ }));
630
+ }
631
+ classify(filePaths) {
632
+ const layers = {};
633
+ for (const filePath of filePaths) {
634
+ const layer = this.matchLayer(filePath);
635
+ const list = layers[layer] ?? [];
636
+ list.push(filePath);
637
+ layers[layer] = list;
638
+ }
639
+ return { layers };
640
+ }
641
+ matchLayer(filePath) {
642
+ for (const rule of this.rules) {
643
+ if (rule.pattern.test(filePath)) {
644
+ return rule.layer;
645
+ }
646
+ }
647
+ return "Unknown";
648
+ }
649
+ };
650
+
651
+ // src/adapters/mcp/get-architectural-overlay.ts
652
+ var InputSchema5 = z5.object({
653
+ layer: z5.string().optional()
654
+ });
655
+ function handleGetArchitecturalOverlay(storage, masking, staleness) {
656
+ const overlay = new ArchitecturalOverlay();
657
+ return (args) => {
658
+ try {
659
+ const parsed = InputSchema5.safeParse(args);
660
+ if (!parsed.success) {
661
+ return {
662
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: parsed.error.message }) }]
663
+ };
664
+ }
665
+ const files = storage.listIndexedFiles();
666
+ const result = overlay.classify(files);
667
+ const buildContent = (payloadStr) => {
668
+ const content = [];
669
+ if (staleness) {
670
+ const warning = staleness.check(files);
671
+ if (warning) content.push({ type: "text", text: `\u26A0\uFE0F ${warning.message}` });
672
+ }
673
+ content.push({ type: "text", text: payloadStr });
674
+ return content;
675
+ };
676
+ if (parsed.data.layer) {
677
+ const filtered = result.layers[parsed.data.layer];
678
+ const payload2 = masking.mask(JSON.stringify({ layer: parsed.data.layer, files: filtered ?? [] }));
679
+ return { content: buildContent(payload2) };
680
+ }
681
+ const payload = masking.mask(JSON.stringify(result));
682
+ return { content: buildContent(payload) };
683
+ } catch (err) {
684
+ return {
685
+ content: [{ type: "text", text: JSON.stringify({ error: true, message: err.message }) }]
686
+ };
687
+ }
688
+ };
689
+ }
690
+
691
+ // src/index.ts
692
+ function loadMaskingConfig(ctxoRoot) {
693
+ const jsonConfigPath = join(ctxoRoot, "masking.json");
694
+ if (existsSync(jsonConfigPath)) {
695
+ try {
696
+ const raw = readFileSync(jsonConfigPath, "utf-8");
697
+ const patterns = JSON.parse(raw);
698
+ console.error(`[ctxo] Loaded ${patterns.length} custom masking pattern(s)`);
699
+ return MaskingPipeline.fromConfig(patterns);
700
+ } catch (err) {
701
+ console.error(`[ctxo] Failed to load masking config: ${err.message}`);
702
+ }
703
+ }
704
+ return new MaskingPipeline();
705
+ }
706
+ async function main() {
707
+ const args = process.argv.slice(2);
708
+ if (args.length > 0) {
709
+ const { CliRouter } = await import("./cli-router-PIWHLS5F.js");
710
+ const router = new CliRouter(process.cwd());
711
+ await router.route(args);
712
+ return;
713
+ }
714
+ const ctxoRoot = ".ctxo";
715
+ const storage = new SqliteStorageAdapter(ctxoRoot);
716
+ await storage.init();
717
+ const masking = loadMaskingConfig(ctxoRoot);
718
+ const git = new SimpleGitAdapter(process.cwd());
719
+ const server = new McpServer({ name: "ctxo", version: "0.1.0" });
720
+ const { StalenessDetector } = await import("./staleness-detector-5AN223FM.js");
721
+ const staleness = new StalenessDetector(process.cwd(), ctxoRoot);
722
+ const logicSliceHandler = handleGetLogicSlice(storage, masking, staleness, ctxoRoot);
723
+ const whyContextHandler = handleGetWhyContext(storage, git, masking, staleness, ctxoRoot);
724
+ const changeIntelligenceHandler = handleGetChangeIntelligence(storage, git, masking, staleness, ctxoRoot);
725
+ server.registerTool(
726
+ "get_logic_slice",
727
+ {
728
+ description: "Retrieve a Logic-Slice for a named symbol \u2014 the symbol plus all transitive dependencies",
729
+ inputSchema: {
730
+ symbolId: z6.string().optional().describe("Single symbol ID (format: file::name::kind)"),
731
+ symbolIds: z6.array(z6.string()).optional().describe("Batch: array of symbol IDs"),
732
+ level: z6.number().min(1).max(4).optional().default(3).describe("Detail level (L1=signature, L2=direct deps, L3=full closure, L4=with token budget)")
733
+ }
734
+ },
735
+ (args2) => logicSliceHandler(args2)
736
+ );
737
+ server.registerTool(
738
+ "get_why_context",
739
+ {
740
+ description: "Retrieve git commit intent, anti-pattern warnings from revert history for a symbol",
741
+ inputSchema: {
742
+ symbolId: z6.string().min(1).describe("The symbol ID (format: file::name::kind)")
743
+ }
744
+ },
745
+ (args2) => whyContextHandler(args2)
746
+ );
747
+ server.registerTool(
748
+ "get_change_intelligence",
749
+ {
750
+ description: "Retrieve complexity x churn composite score for a symbol",
751
+ inputSchema: {
752
+ symbolId: z6.string().min(1).describe("The symbol ID (format: file::name::kind)")
753
+ }
754
+ },
755
+ (args2) => changeIntelligenceHandler(args2)
756
+ );
757
+ const blastRadiusHandler = handleGetBlastRadius(storage, masking, staleness, ctxoRoot);
758
+ server.registerTool(
759
+ "get_blast_radius",
760
+ {
761
+ description: "Retrieve the blast radius for a symbol \u2014 symbols that would break if it changed",
762
+ inputSchema: {
763
+ symbolId: z6.string().min(1).describe("The symbol ID (format: file::name::kind)")
764
+ }
765
+ },
766
+ (args2) => blastRadiusHandler(args2)
767
+ );
768
+ const overlayHandler = handleGetArchitecturalOverlay(storage, masking, staleness);
769
+ server.registerTool(
770
+ "get_architectural_overlay",
771
+ {
772
+ description: "Retrieve an architectural overlay \u2014 layer map identifying Domain, Infrastructure, and Adapter boundaries",
773
+ inputSchema: {
774
+ layer: z6.string().optional().describe("Filter by specific layer name")
775
+ }
776
+ },
777
+ (args2) => overlayHandler(args2)
778
+ );
779
+ const transport = new StdioServerTransport();
780
+ await server.connect(transport);
781
+ }
782
+ main().catch((err) => {
783
+ console.error("[ctxo] Fatal:", err.message);
784
+ process.exit(1);
785
+ });
786
+ //# sourceMappingURL=index.js.map