ctxman 3.1.1 → 3.2.7
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/.ctxman/llm-profiles.json +6 -6
- package/README.md +16 -10
- package/bin/cli.js +107 -8
- package/bin/cm-ask.js +64 -6
- package/bin/cm-audit.js +76 -0
- package/bin/cm-diagnose.js +199 -0
- package/bin/cm-focus.js +322 -0
- package/bin/cm-map.js +71 -18
- package/bin/cm-pack.js +36 -5
- package/bin/cm-policy.js +107 -2
- package/bin/cm-pr.js +18 -3
- package/bin/cm-search.js +84 -19
- package/bin/cm-task.js +192 -0
- package/bin/cm-who.js +119 -0
- package/bin/cm-why.js +41 -5
- package/index.d.ts +931 -1
- package/index.js +105 -1
- package/lib/analyzers/token-calculator.js +76 -4
- package/lib/api/mcp/server.js +618 -16
- package/lib/cache/CacheManager.js +161 -47
- package/lib/core/Analyzer.js +61 -5
- package/lib/diagnose/index.js +712 -0
- package/lib/focus/index.js +790 -0
- package/lib/map/call-graph.js +570 -0
- package/lib/map/graph.js +30 -5
- package/lib/map/index.js +12 -4
- package/lib/map/rank.js +45 -7
- package/lib/map/render.js +89 -28
- package/lib/pack/index.js +20 -1
- package/lib/pack/select.js +8 -1
- package/lib/policy/audit.js +3 -1
- package/lib/policy/index.js +5 -0
- package/lib/policy/workspace.js +254 -0
- package/lib/pr/pr-context.js +292 -31
- package/lib/pr/pr-renderer.js +25 -10
- package/lib/rag/EmbeddingProvider.js +139 -2
- package/lib/rag/VectorStoreFactory.js +10 -4
- package/lib/rag/ask.js +186 -17
- package/lib/rag/stores/SqliteVectorStore.js +595 -0
- package/lib/search/hybrid.js +324 -0
- package/lib/search/symbols.js +75 -11
- package/lib/task/index.js +630 -0
- package/lib/utils/file-utils.js +1 -0
- package/lib/utils/multi-repo-manager.js +4 -1
- package/lib/watch/FileWatcher.js +121 -31
- package/lib/watch/IncrementalAnalyzer.js +964 -16
- package/lib/who/index.js +893 -0
- package/lib/why/analyzer-bridge.js +223 -0
- package/lib/why/discussions.js +314 -0
- package/lib/why/index.js +356 -30
- package/lib/why/render.js +154 -9
- package/lib/workspace/index.js +1274 -0
- package/package.json +1 -1
package/lib/api/mcp/server.js
CHANGED
|
@@ -19,10 +19,13 @@ import { PromptProvider } from './prompts.js';
|
|
|
19
19
|
import { buildPrContext } from '../../pr/pr-context.js';
|
|
20
20
|
import { resolveRootDir, resolveSandboxPath } from './sandbox.js';
|
|
21
21
|
import { searchProject, DEFAULT_LIMIT, DEFAULT_MAX_TOKENS } from '../../search/bm25.js';
|
|
22
|
+
import { hybridSearch } from '../../search/hybrid.js';
|
|
22
23
|
import { getSymbol } from '../../search/symbols.js';
|
|
23
24
|
import { countForModel } from '../../count/index.js';
|
|
24
25
|
import { explainHistory } from '../../why/index.js';
|
|
25
26
|
import { packContext, describePack, TOOL_DEFAULT_BUDGET } from '../../pack/index.js';
|
|
27
|
+
import { focusContext, describeFocus, DEFAULT_FOCUS_DEPTH } from '../../focus/index.js';
|
|
28
|
+
import { mapWorkspace, searchWorkspace, packWorkspace } from '../../workspace/index.js';
|
|
26
29
|
import {
|
|
27
30
|
loadPolicy,
|
|
28
31
|
guardExport,
|
|
@@ -31,8 +34,14 @@ import {
|
|
|
31
34
|
readAuditLog,
|
|
32
35
|
filterAuditEvents,
|
|
33
36
|
parseSince,
|
|
37
|
+
checkWorkspacePolicies,
|
|
38
|
+
readWorkspaceAuditLogs,
|
|
34
39
|
} from '../../policy/index.js';
|
|
35
40
|
import { askCodebase, DEFAULT_ASK_LIMIT } from '../../rag/ask.js';
|
|
41
|
+
import { buildTaskContext, DEFAULT_TASK_BUDGET } from '../../task/index.js';
|
|
42
|
+
import { diagnoseFailure, DEFAULT_DIAGNOSE_BUDGET } from '../../diagnose/index.js';
|
|
43
|
+
import { findExperts, DEFAULT_WHO_LIMIT } from '../../who/index.js';
|
|
44
|
+
import { IncrementalAnalyzer } from '../../watch/IncrementalAnalyzer.js';
|
|
36
45
|
import fs from 'fs';
|
|
37
46
|
import path from 'path';
|
|
38
47
|
import { createRequire } from 'module';
|
|
@@ -45,6 +54,7 @@ const FILE_PATH_TOOLS = new Set(['list_methods']);
|
|
|
45
54
|
const DEFAULT_AUDIT_LIMIT = 50;
|
|
46
55
|
// Characters of each ask_codebase snippet (the index stores whole files)
|
|
47
56
|
const ASK_SNIPPET_CHARS = 2000;
|
|
57
|
+
const DEFAULT_GRAPH_LIMIT = 50;
|
|
48
58
|
|
|
49
59
|
class MCPServer {
|
|
50
60
|
/**
|
|
@@ -69,14 +79,39 @@ class MCPServer {
|
|
|
69
79
|
}
|
|
70
80
|
);
|
|
71
81
|
|
|
72
|
-
this.analyzer = new Analyzer();
|
|
82
|
+
this.analyzer = new Analyzer({ cache: true });
|
|
73
83
|
this.contextBuilder = new ContextBuilder();
|
|
84
|
+
this.incrementalAnalyzers = new Map();
|
|
74
85
|
this.resourceProvider = new ResourceProvider(this.root ?? undefined);
|
|
75
86
|
this.promptProvider = new PromptProvider(this.analyzer, this.contextBuilder);
|
|
76
87
|
this.setupHandlers();
|
|
77
88
|
this.setupErrorHandling();
|
|
78
89
|
}
|
|
79
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Get or create a cached IncrementalAnalyzer instance bound to the project's SQLite knowledge graph.
|
|
93
|
+
* @param {string} projectPath
|
|
94
|
+
* @returns {IncrementalAnalyzer}
|
|
95
|
+
*/
|
|
96
|
+
getIncrementalAnalyzer(projectPath, repos = []) {
|
|
97
|
+
const resolved = path.resolve(projectPath);
|
|
98
|
+
const extraRepos = Array.isArray(repos)
|
|
99
|
+
? repos.filter(Boolean).map((r) => path.resolve(r))
|
|
100
|
+
: [];
|
|
101
|
+
const cacheKey = [resolved, ...extraRepos].join('::');
|
|
102
|
+
let inc = this.incrementalAnalyzers.get(cacheKey);
|
|
103
|
+
if (!inc) {
|
|
104
|
+
inc = new IncrementalAnalyzer({
|
|
105
|
+
projectRoot: resolved,
|
|
106
|
+
repos: extraRepos,
|
|
107
|
+
sync: true,
|
|
108
|
+
hydrateGraphOnStart: false,
|
|
109
|
+
});
|
|
110
|
+
this.incrementalAnalyzers.set(cacheKey, inc);
|
|
111
|
+
}
|
|
112
|
+
return inc;
|
|
113
|
+
}
|
|
114
|
+
|
|
80
115
|
setupHandlers() {
|
|
81
116
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
82
117
|
return {
|
|
@@ -136,13 +171,17 @@ class MCPServer {
|
|
|
136
171
|
{
|
|
137
172
|
name: 'search_code',
|
|
138
173
|
description:
|
|
139
|
-
'Search source lines by regular expression (default) or plain substring
|
|
174
|
+
'Search source lines by regular expression (default) or plain substring. "ranked" returns BM25-ranked methods/chunks; "semantic" runs vector embedding search projected onto chunks (with BM25 fallback); "hybrid" fuses BM25 and semantic embeddings via Reciprocal Rank Fusion (RRF).',
|
|
140
175
|
inputSchema: {
|
|
141
176
|
type: 'object',
|
|
142
177
|
properties: {
|
|
143
178
|
path: { type: 'string', description: 'Project root' },
|
|
144
179
|
query: { type: 'string', description: 'Search query' },
|
|
145
|
-
type: {
|
|
180
|
+
type: {
|
|
181
|
+
type: 'string',
|
|
182
|
+
enum: ['regex', 'substring', 'semantic', 'ranked', 'hybrid'],
|
|
183
|
+
default: 'regex',
|
|
184
|
+
},
|
|
146
185
|
limit: {
|
|
147
186
|
type: 'number',
|
|
148
187
|
description: `ranked: maximum number of chunks (default ${DEFAULT_LIMIT})`,
|
|
@@ -155,6 +194,12 @@ class MCPServer {
|
|
|
155
194
|
type: 'boolean',
|
|
156
195
|
description: 'ranked: redact secrets in the returned source (default true)',
|
|
157
196
|
},
|
|
197
|
+
repos: {
|
|
198
|
+
type: 'array',
|
|
199
|
+
items: { type: 'string' },
|
|
200
|
+
description:
|
|
201
|
+
'ranked/hybrid: additional repository roots to search across under one shared token budget',
|
|
202
|
+
},
|
|
158
203
|
},
|
|
159
204
|
required: ['path', 'query'],
|
|
160
205
|
},
|
|
@@ -195,6 +240,12 @@ class MCPServer {
|
|
|
195
240
|
description:
|
|
196
241
|
'Project-relative files or folders to rank higher, with the files next to them in the import graph',
|
|
197
242
|
},
|
|
243
|
+
repos: {
|
|
244
|
+
type: 'array',
|
|
245
|
+
items: { type: 'string' },
|
|
246
|
+
description:
|
|
247
|
+
'Additional repository roots to include in a multi-repository workspace map under one shared token budget',
|
|
248
|
+
},
|
|
198
249
|
},
|
|
199
250
|
required: ['path'],
|
|
200
251
|
},
|
|
@@ -202,7 +253,7 @@ class MCPServer {
|
|
|
202
253
|
{
|
|
203
254
|
name: 'pr_context',
|
|
204
255
|
description:
|
|
205
|
-
'Review context for a branch: summary, commit log, changed files, full source of changed methods, unified diff and related tests (secrets redacted)',
|
|
256
|
+
'Review context for a branch (single-repo or multi-repo workspace): summary, commit log, changed files, full source of changed methods, unified diff and related tests (secrets redacted)',
|
|
206
257
|
inputSchema: {
|
|
207
258
|
type: 'object',
|
|
208
259
|
properties: {
|
|
@@ -215,6 +266,12 @@ class MCPServer {
|
|
|
215
266
|
head: { type: 'string', description: 'Head branch or commit (default: HEAD)' },
|
|
216
267
|
budget: { type: 'number', description: 'Maximum tokens of the result' },
|
|
217
268
|
style: { type: 'string', enum: ['markdown', 'xml', 'json'], default: 'markdown' },
|
|
269
|
+
repos: {
|
|
270
|
+
type: 'array',
|
|
271
|
+
items: { type: 'string' },
|
|
272
|
+
description:
|
|
273
|
+
'Additional repository roots to include in multi-repository PR review context under one shared token budget',
|
|
274
|
+
},
|
|
218
275
|
},
|
|
219
276
|
required: ['path'],
|
|
220
277
|
},
|
|
@@ -222,7 +279,7 @@ class MCPServer {
|
|
|
222
279
|
{
|
|
223
280
|
name: 'get_symbol',
|
|
224
281
|
description:
|
|
225
|
-
'Full source of a method, function or class by name, with file and line range. Exact matches first, then case-insensitive; similar names are suggested when nothing matches. "Class.method" narrows to methods of a class.',
|
|
282
|
+
'Full source of a method, function or class by name, with file and line range (single-repo or multi-repo workspace). Exact matches first, then case-insensitive; similar names are suggested when nothing matches. "Class.method" narrows to methods of a class.',
|
|
226
283
|
inputSchema: {
|
|
227
284
|
type: 'object',
|
|
228
285
|
properties: {
|
|
@@ -230,12 +287,19 @@ class MCPServer {
|
|
|
230
287
|
name: { type: 'string', description: 'Symbol name, or Class.method' },
|
|
231
288
|
file: {
|
|
232
289
|
type: 'string',
|
|
233
|
-
description:
|
|
290
|
+
description:
|
|
291
|
+
'Optional: only look in this project-relative (or <alias>/<path>) file',
|
|
234
292
|
},
|
|
235
293
|
redact: {
|
|
236
294
|
type: 'boolean',
|
|
237
295
|
description: 'Redact secrets in the source (default true)',
|
|
238
296
|
},
|
|
297
|
+
repos: {
|
|
298
|
+
type: 'array',
|
|
299
|
+
items: { type: 'string' },
|
|
300
|
+
description:
|
|
301
|
+
'Additional repository roots to search for symbols across under one shared token budget',
|
|
302
|
+
},
|
|
239
303
|
},
|
|
240
304
|
required: ['path', 'name'],
|
|
241
305
|
},
|
|
@@ -266,7 +330,7 @@ class MCPServer {
|
|
|
266
330
|
{
|
|
267
331
|
name: 'explain_history',
|
|
268
332
|
description:
|
|
269
|
-
'Explain why code looks the way it does, from the local git history (offline): commits that changed a file, symbol or line range with their messages, referenced issues/PRs/tickets, blame ownership, files that change together with it, related docs/ADRs/comments, TODO notes and tests. Returns Markdown.',
|
|
333
|
+
'Explain why code looks the way it does, from the local git history (offline, single-repo or multi-repo workspace): commits that changed a file, symbol or line range with their messages, referenced issues/PRs/tickets, institutional memory citations (.plansmith/analyzer.sqlite), blame ownership, files that change together with it, related docs/ADRs/comments, TODO notes and tests. Returns Markdown.',
|
|
270
334
|
inputSchema: {
|
|
271
335
|
type: 'object',
|
|
272
336
|
properties: {
|
|
@@ -277,7 +341,7 @@ class MCPServer {
|
|
|
277
341
|
target: {
|
|
278
342
|
type: 'string',
|
|
279
343
|
description:
|
|
280
|
-
'File relative to path, optionally with a symbol or line range: "src/cart.js", "src/cart.js:total", "src/cart.js:Cart.total", "src/cart.js#L10-L40"',
|
|
344
|
+
'File relative to path (or <alias>/<path> in multi-repo mode), optionally with a symbol or line range: "src/cart.js", "src/cart.js:total", "src/cart.js:Cart.total", "src/cart.js#L10-L40"',
|
|
281
345
|
},
|
|
282
346
|
limit: {
|
|
283
347
|
type: 'number',
|
|
@@ -296,6 +360,32 @@ class MCPServer {
|
|
|
296
360
|
type: 'boolean',
|
|
297
361
|
description: "Include each commit's diff of the target (default false)",
|
|
298
362
|
},
|
|
363
|
+
discussions: {
|
|
364
|
+
type: 'boolean',
|
|
365
|
+
description:
|
|
366
|
+
'Fetch referenced GitHub/GitLab issue, PR and MR discussions over HTTPS (default false; uses GITHUB_TOKEN/GH_TOKEN or GITLAB_TOKEN when set)',
|
|
367
|
+
},
|
|
368
|
+
maxDiscussions: {
|
|
369
|
+
type: 'number',
|
|
370
|
+
description:
|
|
371
|
+
'Maximum remote discussions to fetch when discussions is true (default 10)',
|
|
372
|
+
},
|
|
373
|
+
analyzer: {
|
|
374
|
+
type: 'boolean',
|
|
375
|
+
description:
|
|
376
|
+
'Query local @plansmithai/analyzer SQLite database (.plansmith/analyzer.sqlite) for institutional memory citations',
|
|
377
|
+
},
|
|
378
|
+
analyzerDb: {
|
|
379
|
+
type: 'string',
|
|
380
|
+
description:
|
|
381
|
+
'Explicit path to @plansmithai/analyzer SQLite database file or directory',
|
|
382
|
+
},
|
|
383
|
+
repos: {
|
|
384
|
+
type: 'array',
|
|
385
|
+
items: { type: 'string' },
|
|
386
|
+
description:
|
|
387
|
+
'Additional repository roots to include in multi-repository history explanation and cross-repo ADR/doc/test discovery',
|
|
388
|
+
},
|
|
299
389
|
},
|
|
300
390
|
required: ['path', 'target'],
|
|
301
391
|
},
|
|
@@ -349,18 +439,102 @@ class MCPServer {
|
|
|
349
439
|
type: 'string',
|
|
350
440
|
description: "Count with this model's tokenizer encoding and report the fit",
|
|
351
441
|
},
|
|
442
|
+
repos: {
|
|
443
|
+
type: 'array',
|
|
444
|
+
items: { type: 'string' },
|
|
445
|
+
description:
|
|
446
|
+
'Additional repository roots to pack into one workspace document under one shared token budget',
|
|
447
|
+
},
|
|
352
448
|
},
|
|
353
449
|
required: ['path'],
|
|
354
450
|
},
|
|
355
451
|
},
|
|
452
|
+
{
|
|
453
|
+
name: 'focus_context',
|
|
454
|
+
description:
|
|
455
|
+
'Focused neighborhood context around a target file (or file:symbol): packs the target file together with its outgoing imports/callees and incoming importers/callers up to `depth` hops in the repository import and AST symbol call graph (directional traversal avoids shared-hub explosion), honouring .gitignore/.contextignore and export policy, with secrets redacted and trimmed to a token budget (target file kept first).',
|
|
456
|
+
inputSchema: {
|
|
457
|
+
type: 'object',
|
|
458
|
+
properties: {
|
|
459
|
+
path: { type: 'string', description: 'Project root' },
|
|
460
|
+
file: {
|
|
461
|
+
type: 'string',
|
|
462
|
+
description:
|
|
463
|
+
'Project-relative (or <alias>/<path>) file or file:symbol to focus on (e.g. "src/auth/login.ts" or "src/auth/login.ts:authenticate")',
|
|
464
|
+
},
|
|
465
|
+
symbol: {
|
|
466
|
+
type: 'string',
|
|
467
|
+
description:
|
|
468
|
+
'Optional function or method name inside `file` to traverse the cross-file AST symbol call graph around',
|
|
469
|
+
},
|
|
470
|
+
depth: {
|
|
471
|
+
type: 'number',
|
|
472
|
+
description: `Import/call graph hops (default ${DEFAULT_FOCUS_DEPTH}; 0 = target file only)`,
|
|
473
|
+
},
|
|
474
|
+
direction: {
|
|
475
|
+
type: 'string',
|
|
476
|
+
enum: ['both', 'imports', 'importers', 'calls', 'callers'],
|
|
477
|
+
default: 'both',
|
|
478
|
+
},
|
|
479
|
+
callGraph: {
|
|
480
|
+
type: 'boolean',
|
|
481
|
+
description:
|
|
482
|
+
'Include cross-file AST symbol call graph edges alongside module imports (default true)',
|
|
483
|
+
},
|
|
484
|
+
includeTests: {
|
|
485
|
+
type: 'boolean',
|
|
486
|
+
description: 'Include test files in the neighborhood (default true)',
|
|
487
|
+
},
|
|
488
|
+
style: {
|
|
489
|
+
type: 'string',
|
|
490
|
+
enum: ['xml', 'markdown', 'plain', 'json'],
|
|
491
|
+
default: 'xml',
|
|
492
|
+
},
|
|
493
|
+
compress: {
|
|
494
|
+
type: 'boolean',
|
|
495
|
+
description: 'Keep signatures, replace function bodies with a placeholder',
|
|
496
|
+
},
|
|
497
|
+
lineNumbers: { type: 'boolean', description: 'Number every line' },
|
|
498
|
+
tree: {
|
|
499
|
+
type: 'boolean',
|
|
500
|
+
description: 'Directory tree at the top (default true)',
|
|
501
|
+
},
|
|
502
|
+
instructions: {
|
|
503
|
+
type: 'string',
|
|
504
|
+
description: 'Text placed at the end, after the files',
|
|
505
|
+
},
|
|
506
|
+
budget: {
|
|
507
|
+
type: 'number',
|
|
508
|
+
description: `Maximum tokens of the document (default ${TOOL_DEFAULT_BUDGET})`,
|
|
509
|
+
},
|
|
510
|
+
targetModel: {
|
|
511
|
+
type: 'string',
|
|
512
|
+
description: "Count with this model's tokenizer encoding and report the fit",
|
|
513
|
+
},
|
|
514
|
+
repos: {
|
|
515
|
+
type: 'array',
|
|
516
|
+
items: { type: 'string' },
|
|
517
|
+
description:
|
|
518
|
+
'Additional repository roots to include in the workspace import/call graph neighborhood under one shared token budget',
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
required: ['path', 'file'],
|
|
522
|
+
},
|
|
523
|
+
},
|
|
356
524
|
{
|
|
357
525
|
name: 'check_policy',
|
|
358
526
|
description:
|
|
359
|
-
'The effective export policy of a project (.ctxman/policy.json plus the organisation policy named by CTXMAN_POLICY): denied paths, redaction and token limits, allowed target models, AI-tool ignore files and audit logging. Reports whether the policy files are valid and, if not, every error.',
|
|
527
|
+
'The effective export policy of a project or multi-repository workspace (.ctxman/policy.json plus the organisation policy named by CTXMAN_POLICY): denied paths, redaction and token limits, allowed target models, AI-tool ignore files and audit logging. Reports whether the policy files are valid and, if not, every error.',
|
|
360
528
|
inputSchema: {
|
|
361
529
|
type: 'object',
|
|
362
530
|
properties: {
|
|
363
531
|
path: { type: 'string', description: 'Project root' },
|
|
532
|
+
repos: {
|
|
533
|
+
type: 'array',
|
|
534
|
+
items: { type: 'string' },
|
|
535
|
+
description:
|
|
536
|
+
'Additional repository roots to validate and summarize together in a multi-repository workspace',
|
|
537
|
+
},
|
|
364
538
|
},
|
|
365
539
|
required: ['path'],
|
|
366
540
|
},
|
|
@@ -368,7 +542,7 @@ class MCPServer {
|
|
|
368
542
|
{
|
|
369
543
|
name: 'audit_log',
|
|
370
544
|
description:
|
|
371
|
-
"Exports recorded in the project's audit log while the export policy enables audit: when, by whom, which command, where to, which files, token count and a SHA-256 of the exported text. Newest last.",
|
|
545
|
+
"Exports recorded in the project's (or multi-repository workspace's) audit log while the export policy enables audit: when, by whom, which command, where to, which files, token count and a SHA-256 of the exported text. Newest last.",
|
|
372
546
|
inputSchema: {
|
|
373
547
|
type: 'object',
|
|
374
548
|
properties: {
|
|
@@ -382,6 +556,12 @@ class MCPServer {
|
|
|
382
556
|
type: 'number',
|
|
383
557
|
description: `The last N events (default ${DEFAULT_AUDIT_LIMIT}; 0 = all)`,
|
|
384
558
|
},
|
|
559
|
+
repos: {
|
|
560
|
+
type: 'array',
|
|
561
|
+
items: { type: 'string' },
|
|
562
|
+
description:
|
|
563
|
+
'Additional repository roots whose export audit logs should be merged chronologically with repo attribution',
|
|
564
|
+
},
|
|
385
565
|
},
|
|
386
566
|
required: ['path'],
|
|
387
567
|
},
|
|
@@ -389,7 +569,7 @@ class MCPServer {
|
|
|
389
569
|
{
|
|
390
570
|
name: 'ask_codebase',
|
|
391
571
|
description:
|
|
392
|
-
'Semantic search: the files whose content is closest in meaning to a question, from a local embedding index (.ctxman/self-knowledge
|
|
572
|
+
'Semantic search: the files whose content is closest in meaning to a question, from a local SQLite embedding index (.ctxman/self-knowledge/index.sqlite via built-in node:sqlite, single-repo or multi-repo workspace). Uses @xenova/transformers when installed and automatically falls back to deterministic offline LocalHashEmbeddingProvider otherwise. For exact names or keywords, search_code is faster.',
|
|
393
573
|
inputSchema: {
|
|
394
574
|
type: 'object',
|
|
395
575
|
properties: {
|
|
@@ -399,10 +579,152 @@ class MCPServer {
|
|
|
399
579
|
type: 'number',
|
|
400
580
|
description: `How many files (default ${DEFAULT_ASK_LIMIT})`,
|
|
401
581
|
},
|
|
582
|
+
repos: {
|
|
583
|
+
type: 'array',
|
|
584
|
+
items: { type: 'string' },
|
|
585
|
+
description:
|
|
586
|
+
'Additional repository roots to include in multi-repository semantic search',
|
|
587
|
+
},
|
|
402
588
|
},
|
|
403
589
|
required: ['path', 'query'],
|
|
404
590
|
},
|
|
405
591
|
},
|
|
592
|
+
{
|
|
593
|
+
name: 'task_context',
|
|
594
|
+
description:
|
|
595
|
+
'Task-driven automatic context builder: fuses hybrid search, PageRank import/call neighborhood, and architectural map for a task description within a token budget (single-repo or multi-repo workspace).',
|
|
596
|
+
inputSchema: {
|
|
597
|
+
type: 'object',
|
|
598
|
+
properties: {
|
|
599
|
+
path: { type: 'string', description: 'Project root' },
|
|
600
|
+
task: { type: 'string', description: 'Natural-language task or issue description' },
|
|
601
|
+
budget: {
|
|
602
|
+
type: 'number',
|
|
603
|
+
description: `Maximum tokens (default ${DEFAULT_TASK_BUDGET})`,
|
|
604
|
+
},
|
|
605
|
+
format: { type: 'string', enum: ['xml', 'markdown', 'json'], default: 'xml' },
|
|
606
|
+
model: { type: 'string', description: 'Optional target LLM model ID' },
|
|
607
|
+
callGraph: {
|
|
608
|
+
type: 'boolean',
|
|
609
|
+
description:
|
|
610
|
+
'Include cross-file AST symbol call graph edges alongside module imports (default true)',
|
|
611
|
+
},
|
|
612
|
+
repos: {
|
|
613
|
+
type: 'array',
|
|
614
|
+
items: { type: 'string' },
|
|
615
|
+
description:
|
|
616
|
+
'Additional repository roots to include in a multi-repository task context bundle under one shared token budget',
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
required: ['path', 'task'],
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
name: 'diagnose_failure',
|
|
624
|
+
description:
|
|
625
|
+
'Diagnose a stack trace, test failure, or error log: parses implicated repo files/lines (single-repo or multi-repo workspace), packs enclosing symbols and 1-hop graph neighbors, and surfaces recent commits that touched them.',
|
|
626
|
+
inputSchema: {
|
|
627
|
+
type: 'object',
|
|
628
|
+
properties: {
|
|
629
|
+
path: { type: 'string', description: 'Project root' },
|
|
630
|
+
trace: { type: 'string', description: 'Error stack trace or failure log text' },
|
|
631
|
+
budget: {
|
|
632
|
+
type: 'number',
|
|
633
|
+
description: `Maximum tokens (default ${DEFAULT_DIAGNOSE_BUDGET})`,
|
|
634
|
+
},
|
|
635
|
+
format: { type: 'string', enum: ['xml', 'markdown', 'json'], default: 'xml' },
|
|
636
|
+
callGraph: {
|
|
637
|
+
type: 'boolean',
|
|
638
|
+
description:
|
|
639
|
+
'Include cross-file AST symbol call graph edges alongside module imports (default true)',
|
|
640
|
+
},
|
|
641
|
+
repos: {
|
|
642
|
+
type: 'array',
|
|
643
|
+
items: { type: 'string' },
|
|
644
|
+
description:
|
|
645
|
+
'Additional repository roots to resolve stack frames and 1-hop neighbors across under one shared token budget',
|
|
646
|
+
},
|
|
647
|
+
},
|
|
648
|
+
required: ['path', 'trace'],
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: 'find_experts',
|
|
653
|
+
description:
|
|
654
|
+
'Expert Finder ("Who knows about this?"): ranks code and domain experts for a file, symbol, directory, or natural-language topic (single-repo or multi-repo workspace) by fusing surviving git blame ownership, recency-decayed commit/churn history, and CODEOWNERS stewardship.',
|
|
655
|
+
inputSchema: {
|
|
656
|
+
type: 'object',
|
|
657
|
+
properties: {
|
|
658
|
+
path: { type: 'string', description: 'Project root inside a git repository' },
|
|
659
|
+
target: {
|
|
660
|
+
type: 'string',
|
|
661
|
+
description:
|
|
662
|
+
'File path (<alias>/<path>), file:symbol, file#L10-L40, directory, or natural-language topic query',
|
|
663
|
+
},
|
|
664
|
+
limit: {
|
|
665
|
+
type: 'number',
|
|
666
|
+
description: `Maximum ranked experts to return (default ${DEFAULT_WHO_LIMIT})`,
|
|
667
|
+
},
|
|
668
|
+
halfLifeDays: {
|
|
669
|
+
type: 'number',
|
|
670
|
+
description: 'Recency exponential decay half-life in days (default 90)',
|
|
671
|
+
},
|
|
672
|
+
style: {
|
|
673
|
+
type: 'string',
|
|
674
|
+
enum: ['markdown', 'xml', 'json'],
|
|
675
|
+
default: 'markdown',
|
|
676
|
+
},
|
|
677
|
+
repos: {
|
|
678
|
+
type: 'array',
|
|
679
|
+
items: { type: 'string' },
|
|
680
|
+
description:
|
|
681
|
+
'Additional repository roots inside git repositories to include in multi-repository expert ranking',
|
|
682
|
+
},
|
|
683
|
+
},
|
|
684
|
+
required: ['path', 'target'],
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
name: 'query_knowledge_graph',
|
|
689
|
+
description:
|
|
690
|
+
'Query the persistent SQLite knowledge graph (.ctxman/self-knowledge/index.sqlite) or IncrementalAnalyzer cache for file nodes, method declarations, and import/dependency edges.',
|
|
691
|
+
inputSchema: {
|
|
692
|
+
type: 'object',
|
|
693
|
+
properties: {
|
|
694
|
+
path: { type: 'string', description: 'Project root' },
|
|
695
|
+
query: {
|
|
696
|
+
type: 'string',
|
|
697
|
+
description:
|
|
698
|
+
'Optional substring or symbol query to filter graph nodes and method declarations',
|
|
699
|
+
},
|
|
700
|
+
file: {
|
|
701
|
+
type: 'string',
|
|
702
|
+
description:
|
|
703
|
+
'Optional project-relative file path to inspect along with its dependency neighborhood',
|
|
704
|
+
},
|
|
705
|
+
depth: {
|
|
706
|
+
type: 'number',
|
|
707
|
+
description: 'Neighborhood hop depth when file is provided (default: 1, max: 4)',
|
|
708
|
+
},
|
|
709
|
+
limit: {
|
|
710
|
+
type: 'number',
|
|
711
|
+
description: `Maximum number of graph nodes to return (default ${DEFAULT_GRAPH_LIMIT})`,
|
|
712
|
+
},
|
|
713
|
+
refresh: {
|
|
714
|
+
type: 'boolean',
|
|
715
|
+
description:
|
|
716
|
+
'Re-synchronize the IncrementalAnalyzer cache and SQLite knowledge graph before querying',
|
|
717
|
+
},
|
|
718
|
+
repos: {
|
|
719
|
+
type: 'array',
|
|
720
|
+
items: { type: 'string' },
|
|
721
|
+
description:
|
|
722
|
+
'Optional additional repository roots to include in a multi-repository knowledge graph under <alias>/<path>',
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
required: ['path'],
|
|
726
|
+
},
|
|
727
|
+
},
|
|
406
728
|
],
|
|
407
729
|
};
|
|
408
730
|
});
|
|
@@ -468,6 +790,21 @@ class MCPServer {
|
|
|
468
790
|
}),
|
|
469
791
|
};
|
|
470
792
|
}
|
|
793
|
+
if (args.repos !== undefined) {
|
|
794
|
+
if (!Array.isArray(args.repos)) {
|
|
795
|
+
throw new Error('repos must be an array of directory paths');
|
|
796
|
+
}
|
|
797
|
+
args = {
|
|
798
|
+
...args,
|
|
799
|
+
repos: args.repos.map((repoPath) =>
|
|
800
|
+
resolveSandboxPath(repoPath, {
|
|
801
|
+
root: this.root,
|
|
802
|
+
expect: 'directory',
|
|
803
|
+
name: 'repos',
|
|
804
|
+
})
|
|
805
|
+
),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
471
808
|
|
|
472
809
|
switch (name) {
|
|
473
810
|
case 'analyze_codebase': {
|
|
@@ -565,6 +902,17 @@ class MCPServer {
|
|
|
565
902
|
}
|
|
566
903
|
case 'search_code': {
|
|
567
904
|
const { path: projectPath, query, type = 'regex' } = args;
|
|
905
|
+
if (
|
|
906
|
+
args.repos?.length > 0 &&
|
|
907
|
+
(type === 'ranked' || type === 'hybrid' || type === 'semantic')
|
|
908
|
+
) {
|
|
909
|
+
const result = searchWorkspace([projectPath, ...args.repos], query, {
|
|
910
|
+
limit: args.limit ?? DEFAULT_LIMIT,
|
|
911
|
+
maxTokens: args.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
912
|
+
redact: args.redact !== false,
|
|
913
|
+
});
|
|
914
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
915
|
+
}
|
|
568
916
|
if (type === 'ranked') {
|
|
569
917
|
const result = searchProject(projectPath, query, {
|
|
570
918
|
limit: args.limit ?? DEFAULT_LIMIT,
|
|
@@ -573,6 +921,16 @@ class MCPServer {
|
|
|
573
921
|
});
|
|
574
922
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
575
923
|
}
|
|
924
|
+
if (type === 'hybrid' || type === 'semantic') {
|
|
925
|
+
const result = await hybridSearch(projectPath, query, {
|
|
926
|
+
mode: type,
|
|
927
|
+
fallbackToBm25: true,
|
|
928
|
+
limit: args.limit ?? DEFAULT_LIMIT,
|
|
929
|
+
maxTokens: args.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
930
|
+
redact: args.redact !== false,
|
|
931
|
+
});
|
|
932
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
933
|
+
}
|
|
576
934
|
let pattern = null;
|
|
577
935
|
if (type === 'regex') {
|
|
578
936
|
try {
|
|
@@ -635,7 +993,7 @@ class MCPServer {
|
|
|
635
993
|
}
|
|
636
994
|
|
|
637
995
|
// Use Analyzer with methodLevel: true
|
|
638
|
-
const methodAnalyzer = new Analyzer({ methodLevel: true });
|
|
996
|
+
const methodAnalyzer = new Analyzer({ methodLevel: true, cache: true });
|
|
639
997
|
const analysis = await methodAnalyzer.analyze(files);
|
|
640
998
|
|
|
641
999
|
const methods = [];
|
|
@@ -655,10 +1013,18 @@ class MCPServer {
|
|
|
655
1013
|
};
|
|
656
1014
|
}
|
|
657
1015
|
case 'repo_map': {
|
|
658
|
-
const map =
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
1016
|
+
const map =
|
|
1017
|
+
args.repos?.length > 0
|
|
1018
|
+
? mapWorkspace([args.path, ...args.repos], {
|
|
1019
|
+
tokens: args.tokens,
|
|
1020
|
+
focus: args.focus,
|
|
1021
|
+
callGraph: args.callGraph,
|
|
1022
|
+
})
|
|
1023
|
+
: renderRepoMap(args.path, {
|
|
1024
|
+
tokens: args.tokens,
|
|
1025
|
+
focus: args.focus,
|
|
1026
|
+
callGraph: args.callGraph,
|
|
1027
|
+
});
|
|
662
1028
|
const notes = map.warnings.map((warning) => `Note: ${warning}`).join('\n');
|
|
663
1029
|
return {
|
|
664
1030
|
content: [
|
|
@@ -671,6 +1037,7 @@ class MCPServer {
|
|
|
671
1037
|
}
|
|
672
1038
|
case 'pr_context': {
|
|
673
1039
|
const result = await buildPrContext(args.path, {
|
|
1040
|
+
repos: args.repos,
|
|
674
1041
|
base: args.base,
|
|
675
1042
|
head: args.head,
|
|
676
1043
|
budget: args.budget == null ? undefined : Math.floor(args.budget),
|
|
@@ -682,6 +1049,7 @@ class MCPServer {
|
|
|
682
1049
|
const result = getSymbol(args.path, args.name, {
|
|
683
1050
|
file: args.file,
|
|
684
1051
|
redact: args.redact !== false,
|
|
1052
|
+
repos: args.repos,
|
|
685
1053
|
});
|
|
686
1054
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
687
1055
|
}
|
|
@@ -699,15 +1067,47 @@ class MCPServer {
|
|
|
699
1067
|
}
|
|
700
1068
|
case 'explain_history': {
|
|
701
1069
|
const whole = (value) => (typeof value === 'number' ? Math.floor(value) : value);
|
|
1070
|
+
const analyzerDb =
|
|
1071
|
+
args.analyzerDb !== undefined
|
|
1072
|
+
? resolveSandboxPath(args.analyzerDb, {
|
|
1073
|
+
root: this.root,
|
|
1074
|
+
expect: 'any',
|
|
1075
|
+
name: 'analyzerDb',
|
|
1076
|
+
})
|
|
1077
|
+
: undefined;
|
|
702
1078
|
const result = await explainHistory(args.path, args.target, {
|
|
1079
|
+
repos: args.repos,
|
|
703
1080
|
limit: whole(args.limit),
|
|
704
1081
|
budget: whole(args.budget ?? 20000),
|
|
705
1082
|
since: args.since,
|
|
706
1083
|
patches: args.patches === true,
|
|
1084
|
+
discussions: args.discussions === true,
|
|
1085
|
+
maxDiscussions: whole(args.maxDiscussions),
|
|
1086
|
+
analyzer: args.analyzer === true,
|
|
1087
|
+
analyzerDb,
|
|
707
1088
|
});
|
|
708
1089
|
return { content: [{ type: 'text', text: result.text }] };
|
|
709
1090
|
}
|
|
710
1091
|
case 'pack_context': {
|
|
1092
|
+
if (args.repos?.length > 0) {
|
|
1093
|
+
const result = await packWorkspace([args.path, ...args.repos], {
|
|
1094
|
+
include: args.include,
|
|
1095
|
+
exclude: args.exclude,
|
|
1096
|
+
style: args.style,
|
|
1097
|
+
compress: args.compress,
|
|
1098
|
+
lineNumbers: args.lineNumbers,
|
|
1099
|
+
tree: args.tree,
|
|
1100
|
+
instructions: args.instructions,
|
|
1101
|
+
budget: args.budget == null ? TOOL_DEFAULT_BUDGET : Math.floor(args.budget),
|
|
1102
|
+
targetModel: args.targetModel,
|
|
1103
|
+
});
|
|
1104
|
+
return {
|
|
1105
|
+
content: [
|
|
1106
|
+
{ type: 'text', text: result.text },
|
|
1107
|
+
{ type: 'text', text: describePack(result) },
|
|
1108
|
+
],
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
711
1111
|
const result = await packContext(args.path, {
|
|
712
1112
|
include: args.include,
|
|
713
1113
|
exclude: args.exclude,
|
|
@@ -729,7 +1129,35 @@ class MCPServer {
|
|
|
729
1129
|
],
|
|
730
1130
|
};
|
|
731
1131
|
}
|
|
1132
|
+
case 'focus_context': {
|
|
1133
|
+
const result = await focusContext(args.path, args.file ?? args.target, {
|
|
1134
|
+
repos: args.repos,
|
|
1135
|
+
symbol: args.symbol,
|
|
1136
|
+
depth: args.depth == null ? undefined : Math.floor(args.depth),
|
|
1137
|
+
direction: args.direction,
|
|
1138
|
+
callGraph: args.callGraph,
|
|
1139
|
+
includeTests: args.includeTests,
|
|
1140
|
+
cwd: args.path,
|
|
1141
|
+
style: args.style,
|
|
1142
|
+
compress: args.compress,
|
|
1143
|
+
lineNumbers: args.lineNumbers,
|
|
1144
|
+
tree: args.tree,
|
|
1145
|
+
instructions: args.instructions,
|
|
1146
|
+
budget: args.budget == null ? TOOL_DEFAULT_BUDGET : Math.floor(args.budget),
|
|
1147
|
+
targetModel: args.targetModel,
|
|
1148
|
+
});
|
|
1149
|
+
return {
|
|
1150
|
+
content: [
|
|
1151
|
+
{ type: 'text', text: result.text },
|
|
1152
|
+
{ type: 'text', text: describeFocus(result) },
|
|
1153
|
+
],
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
732
1156
|
case 'check_policy': {
|
|
1157
|
+
if (args.repos?.length > 0) {
|
|
1158
|
+
const result = checkWorkspacePolicies(args.path, args.repos);
|
|
1159
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1160
|
+
}
|
|
733
1161
|
let result;
|
|
734
1162
|
try {
|
|
735
1163
|
result = { valid: true, ...loadPolicy(args.path).toJSON() };
|
|
@@ -743,6 +1171,18 @@ class MCPServer {
|
|
|
743
1171
|
const since = args.since === undefined ? null : parseSince(String(args.since));
|
|
744
1172
|
const limit =
|
|
745
1173
|
Number.isInteger(args.limit) && args.limit >= 0 ? args.limit : DEFAULT_AUDIT_LIMIT;
|
|
1174
|
+
if (args.repos?.length > 0) {
|
|
1175
|
+
const ws = readWorkspaceAuditLogs(args.path, args.repos, { since, limit });
|
|
1176
|
+
const result = {
|
|
1177
|
+
repos: ws.repos,
|
|
1178
|
+
files: ws.files,
|
|
1179
|
+
total: ws.total,
|
|
1180
|
+
events: ws.events,
|
|
1181
|
+
skippedLines: ws.skippedLines,
|
|
1182
|
+
};
|
|
1183
|
+
if (ws.policyErrors.length > 0) result.policyErrors = ws.policyErrors;
|
|
1184
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1185
|
+
}
|
|
746
1186
|
// Only the policy's own logs: a tool argument never names a file to read
|
|
747
1187
|
const { files, error } = auditLogFiles(args.path);
|
|
748
1188
|
const { events, skipped, missing } = readAuditLog(files);
|
|
@@ -758,11 +1198,173 @@ class MCPServer {
|
|
|
758
1198
|
}
|
|
759
1199
|
case 'ask_codebase': {
|
|
760
1200
|
const result = await askCodebase(args.path, args.query, {
|
|
1201
|
+
repos: args.repos,
|
|
761
1202
|
limit: Number.isInteger(args.limit) ? args.limit : undefined,
|
|
762
1203
|
maxChars: ASK_SNIPPET_CHARS,
|
|
763
1204
|
});
|
|
764
1205
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
765
1206
|
}
|
|
1207
|
+
case 'task_context': {
|
|
1208
|
+
const result = await buildTaskContext(
|
|
1209
|
+
args.path ?? args.dir ?? process.cwd(),
|
|
1210
|
+
args.task ?? args.query ?? '',
|
|
1211
|
+
{
|
|
1212
|
+
repos: args.repos,
|
|
1213
|
+
budget: args.budget == null ? DEFAULT_TASK_BUDGET : Math.floor(args.budget),
|
|
1214
|
+
format: args.format,
|
|
1215
|
+
model: args.model,
|
|
1216
|
+
callGraph: args.callGraph,
|
|
1217
|
+
}
|
|
1218
|
+
);
|
|
1219
|
+
return {
|
|
1220
|
+
content: [
|
|
1221
|
+
{ type: 'text', text: result.text },
|
|
1222
|
+
{
|
|
1223
|
+
type: 'text',
|
|
1224
|
+
text: `Task Context: ${result.files.length} file(s), ${result.totalTokens}/${result.budget} tokens (${result.format}).`,
|
|
1225
|
+
},
|
|
1226
|
+
],
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
case 'diagnose_failure': {
|
|
1230
|
+
const result = await diagnoseFailure(
|
|
1231
|
+
args.path ?? args.dir ?? process.cwd(),
|
|
1232
|
+
args.trace ?? args.log ?? '',
|
|
1233
|
+
{
|
|
1234
|
+
repos: args.repos,
|
|
1235
|
+
budget: args.budget == null ? DEFAULT_DIAGNOSE_BUDGET : Math.floor(args.budget),
|
|
1236
|
+
format: args.format,
|
|
1237
|
+
callGraph: args.callGraph,
|
|
1238
|
+
}
|
|
1239
|
+
);
|
|
1240
|
+
return {
|
|
1241
|
+
content: [
|
|
1242
|
+
{ type: 'text', text: result.text },
|
|
1243
|
+
{
|
|
1244
|
+
type: 'text',
|
|
1245
|
+
text: `Diagnosed: ${result.implicatedFiles.length} implicated file(s), ${result.totalTokens}/${result.budget} tokens.`,
|
|
1246
|
+
},
|
|
1247
|
+
],
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
case 'find_experts': {
|
|
1251
|
+
const result = await findExperts(
|
|
1252
|
+
args.path ?? process.cwd(),
|
|
1253
|
+
args.target ?? args.query ?? '',
|
|
1254
|
+
{
|
|
1255
|
+
repos: args.repos,
|
|
1256
|
+
limit: Number.isInteger(args.limit) ? args.limit : undefined,
|
|
1257
|
+
halfLifeDays: typeof args.halfLifeDays === 'number' ? args.halfLifeDays : undefined,
|
|
1258
|
+
style: args.style || 'markdown',
|
|
1259
|
+
}
|
|
1260
|
+
);
|
|
1261
|
+
return {
|
|
1262
|
+
content: [{ type: 'text', text: result.text }],
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
case 'query_knowledge_graph': {
|
|
1266
|
+
const projectPath = args.path ?? process.cwd();
|
|
1267
|
+
const extraRepos = Array.isArray(args.repos) ? args.repos : [];
|
|
1268
|
+
const scanner = new Scanner(projectPath);
|
|
1269
|
+
scanner.getPolicy();
|
|
1270
|
+
for (const repoPath of extraRepos) {
|
|
1271
|
+
new Scanner(repoPath).getPolicy();
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const inc = this.getIncrementalAnalyzer(projectPath, extraRepos);
|
|
1275
|
+
await inc.ensureSyncReady(projectPath);
|
|
1276
|
+
const store = inc.vectorStore;
|
|
1277
|
+
const initialCounts = store.graphCounts();
|
|
1278
|
+
|
|
1279
|
+
let bootstrapped = false;
|
|
1280
|
+
if (args.refresh === true || initialCounts.nodes === 0) {
|
|
1281
|
+
await inc.bootstrapSync(projectPath, { repos: extraRepos });
|
|
1282
|
+
bootstrapped = true;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
const rawGraph = await inc.getGraph();
|
|
1286
|
+
let nodes = rawGraph.nodes.filter((n) => !inc.isPathDenied(n.path));
|
|
1287
|
+
let edges = rawGraph.edges.filter(
|
|
1288
|
+
(e) => !inc.isPathDenied(e.from) && !inc.isPathDenied(e.to)
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
if (typeof args.file === 'string' && args.file.trim()) {
|
|
1292
|
+
const targetFile = args.file.trim().replace(/\\/g, '/').replace(/^\.\//, '');
|
|
1293
|
+
const maxDepth = Math.max(
|
|
1294
|
+
1,
|
|
1295
|
+
Math.min(4, Number.isFinite(args.depth) ? Math.floor(args.depth) : 1)
|
|
1296
|
+
);
|
|
1297
|
+
const visited = new Set([targetFile]);
|
|
1298
|
+
let frontier = new Set([targetFile]);
|
|
1299
|
+
|
|
1300
|
+
for (let hop = 0; hop < maxDepth && frontier.size > 0; hop++) {
|
|
1301
|
+
const nextFrontier = new Set();
|
|
1302
|
+
for (const edge of edges) {
|
|
1303
|
+
if (frontier.has(edge.from) && !visited.has(edge.to)) {
|
|
1304
|
+
visited.add(edge.to);
|
|
1305
|
+
nextFrontier.add(edge.to);
|
|
1306
|
+
}
|
|
1307
|
+
if (frontier.has(edge.to) && !visited.has(edge.from)) {
|
|
1308
|
+
visited.add(edge.from);
|
|
1309
|
+
nextFrontier.add(edge.from);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
frontier = nextFrontier;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
nodes = nodes.filter((n) => visited.has(n.path));
|
|
1316
|
+
edges = edges.filter((e) => visited.has(e.from) && visited.has(e.to));
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
if (typeof args.query === 'string' && args.query.trim()) {
|
|
1320
|
+
const needle = args.query.trim().toLowerCase();
|
|
1321
|
+
nodes = nodes.filter((n) => {
|
|
1322
|
+
if (n.path.toLowerCase().includes(needle)) return true;
|
|
1323
|
+
if (Array.isArray(n.methods)) {
|
|
1324
|
+
return n.methods.some((m) => {
|
|
1325
|
+
const name = typeof m === 'string' ? m : m?.name || '';
|
|
1326
|
+
const sig = typeof m === 'object' ? m?.signature || '' : '';
|
|
1327
|
+
return (
|
|
1328
|
+
name.toLowerCase().includes(needle) || sig.toLowerCase().includes(needle)
|
|
1329
|
+
);
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
return false;
|
|
1333
|
+
});
|
|
1334
|
+
const matchedPaths = new Set(nodes.map((n) => n.path));
|
|
1335
|
+
edges = edges.filter((e) => matchedPaths.has(e.from) || matchedPaths.has(e.to));
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const limit = Math.max(
|
|
1339
|
+
1,
|
|
1340
|
+
Math.min(
|
|
1341
|
+
500,
|
|
1342
|
+
Number.isFinite(args.limit) ? Math.floor(args.limit) : DEFAULT_GRAPH_LIMIT
|
|
1343
|
+
)
|
|
1344
|
+
);
|
|
1345
|
+
const limitedNodes = nodes.slice(0, limit);
|
|
1346
|
+
const keptPaths = new Set(limitedNodes.map((n) => n.path));
|
|
1347
|
+
const matchingEdges = edges.filter((e) => keptPaths.has(e.from) || keptPaths.has(e.to));
|
|
1348
|
+
|
|
1349
|
+
const counts = store.graphCounts();
|
|
1350
|
+
const payload = {
|
|
1351
|
+
path: projectPath,
|
|
1352
|
+
bootstrapped,
|
|
1353
|
+
cached: inc.fileCache.size > 0 || inc.graphNodes.size > 0,
|
|
1354
|
+
stats: {
|
|
1355
|
+
totalNodes: counts.nodes,
|
|
1356
|
+
totalEdges: counts.edges,
|
|
1357
|
+
vectorDocuments: store.count(),
|
|
1358
|
+
cachedFiles: inc.fileCache.size,
|
|
1359
|
+
},
|
|
1360
|
+
nodes: limitedNodes,
|
|
1361
|
+
edges: matchingEdges,
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
return {
|
|
1365
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
766
1368
|
default:
|
|
767
1369
|
throw new Error(`Unknown tool: ${name}`);
|
|
768
1370
|
}
|