ctxman 3.1.0 → 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 +17 -10
- package/bin/cli.js +107 -8
- package/bin/cm-ask.js +74 -70
- package/bin/cm-audit.js +80 -7
- 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/examples/github-actions/ctxman-check.yml +1 -1
- package/index.d.ts +931 -1
- package/index.js +105 -1
- package/lib/analyzers/performance-dashboard.js +7 -3
- package/lib/analyzers/token-calculator.js +76 -4
- package/lib/api/mcp/server.js +713 -14
- 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/parsers/gitignore-parser.js +4 -1
- package/lib/policy/audit.js +22 -2
- package/lib/policy/index.js +6 -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 +291 -0
- package/lib/rag/sources/FileDataSource.js +6 -3
- 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/logger.js +5 -2
- 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/wizards/config-generator.js +20 -18
- package/lib/wizards/project-detector.js +2 -1
- package/lib/workspace/index.js +1274 -0
- package/package.json +2 -2
package/lib/api/mcp/server.js
CHANGED
|
@@ -19,11 +19,29 @@ 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';
|
|
26
|
-
import {
|
|
27
|
+
import { focusContext, describeFocus, DEFAULT_FOCUS_DEPTH } from '../../focus/index.js';
|
|
28
|
+
import { mapWorkspace, searchWorkspace, packWorkspace } from '../../workspace/index.js';
|
|
29
|
+
import {
|
|
30
|
+
loadPolicy,
|
|
31
|
+
guardExport,
|
|
32
|
+
PolicyConfigError,
|
|
33
|
+
auditLogFiles,
|
|
34
|
+
readAuditLog,
|
|
35
|
+
filterAuditEvents,
|
|
36
|
+
parseSince,
|
|
37
|
+
checkWorkspacePolicies,
|
|
38
|
+
readWorkspaceAuditLogs,
|
|
39
|
+
} from '../../policy/index.js';
|
|
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';
|
|
27
45
|
import fs from 'fs';
|
|
28
46
|
import path from 'path';
|
|
29
47
|
import { createRequire } from 'module';
|
|
@@ -33,6 +51,11 @@ const { version } = createRequire(import.meta.url)('../../../package.json');
|
|
|
33
51
|
// Tools whose `path` may also name a single file
|
|
34
52
|
const FILE_PATH_TOOLS = new Set(['list_methods']);
|
|
35
53
|
|
|
54
|
+
const DEFAULT_AUDIT_LIMIT = 50;
|
|
55
|
+
// Characters of each ask_codebase snippet (the index stores whole files)
|
|
56
|
+
const ASK_SNIPPET_CHARS = 2000;
|
|
57
|
+
const DEFAULT_GRAPH_LIMIT = 50;
|
|
58
|
+
|
|
36
59
|
class MCPServer {
|
|
37
60
|
/**
|
|
38
61
|
* @param {Object} [options]
|
|
@@ -56,14 +79,39 @@ class MCPServer {
|
|
|
56
79
|
}
|
|
57
80
|
);
|
|
58
81
|
|
|
59
|
-
this.analyzer = new Analyzer();
|
|
82
|
+
this.analyzer = new Analyzer({ cache: true });
|
|
60
83
|
this.contextBuilder = new ContextBuilder();
|
|
84
|
+
this.incrementalAnalyzers = new Map();
|
|
61
85
|
this.resourceProvider = new ResourceProvider(this.root ?? undefined);
|
|
62
86
|
this.promptProvider = new PromptProvider(this.analyzer, this.contextBuilder);
|
|
63
87
|
this.setupHandlers();
|
|
64
88
|
this.setupErrorHandling();
|
|
65
89
|
}
|
|
66
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
|
+
|
|
67
115
|
setupHandlers() {
|
|
68
116
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
69
117
|
return {
|
|
@@ -123,13 +171,17 @@ class MCPServer {
|
|
|
123
171
|
{
|
|
124
172
|
name: 'search_code',
|
|
125
173
|
description:
|
|
126
|
-
'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).',
|
|
127
175
|
inputSchema: {
|
|
128
176
|
type: 'object',
|
|
129
177
|
properties: {
|
|
130
178
|
path: { type: 'string', description: 'Project root' },
|
|
131
179
|
query: { type: 'string', description: 'Search query' },
|
|
132
|
-
type: {
|
|
180
|
+
type: {
|
|
181
|
+
type: 'string',
|
|
182
|
+
enum: ['regex', 'substring', 'semantic', 'ranked', 'hybrid'],
|
|
183
|
+
default: 'regex',
|
|
184
|
+
},
|
|
133
185
|
limit: {
|
|
134
186
|
type: 'number',
|
|
135
187
|
description: `ranked: maximum number of chunks (default ${DEFAULT_LIMIT})`,
|
|
@@ -142,6 +194,12 @@ class MCPServer {
|
|
|
142
194
|
type: 'boolean',
|
|
143
195
|
description: 'ranked: redact secrets in the returned source (default true)',
|
|
144
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
|
+
},
|
|
145
203
|
},
|
|
146
204
|
required: ['path', 'query'],
|
|
147
205
|
},
|
|
@@ -182,6 +240,12 @@ class MCPServer {
|
|
|
182
240
|
description:
|
|
183
241
|
'Project-relative files or folders to rank higher, with the files next to them in the import graph',
|
|
184
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
|
+
},
|
|
185
249
|
},
|
|
186
250
|
required: ['path'],
|
|
187
251
|
},
|
|
@@ -189,7 +253,7 @@ class MCPServer {
|
|
|
189
253
|
{
|
|
190
254
|
name: 'pr_context',
|
|
191
255
|
description:
|
|
192
|
-
'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)',
|
|
193
257
|
inputSchema: {
|
|
194
258
|
type: 'object',
|
|
195
259
|
properties: {
|
|
@@ -202,6 +266,12 @@ class MCPServer {
|
|
|
202
266
|
head: { type: 'string', description: 'Head branch or commit (default: HEAD)' },
|
|
203
267
|
budget: { type: 'number', description: 'Maximum tokens of the result' },
|
|
204
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
|
+
},
|
|
205
275
|
},
|
|
206
276
|
required: ['path'],
|
|
207
277
|
},
|
|
@@ -209,7 +279,7 @@ class MCPServer {
|
|
|
209
279
|
{
|
|
210
280
|
name: 'get_symbol',
|
|
211
281
|
description:
|
|
212
|
-
'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.',
|
|
213
283
|
inputSchema: {
|
|
214
284
|
type: 'object',
|
|
215
285
|
properties: {
|
|
@@ -217,12 +287,19 @@ class MCPServer {
|
|
|
217
287
|
name: { type: 'string', description: 'Symbol name, or Class.method' },
|
|
218
288
|
file: {
|
|
219
289
|
type: 'string',
|
|
220
|
-
description:
|
|
290
|
+
description:
|
|
291
|
+
'Optional: only look in this project-relative (or <alias>/<path>) file',
|
|
221
292
|
},
|
|
222
293
|
redact: {
|
|
223
294
|
type: 'boolean',
|
|
224
295
|
description: 'Redact secrets in the source (default true)',
|
|
225
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
|
+
},
|
|
226
303
|
},
|
|
227
304
|
required: ['path', 'name'],
|
|
228
305
|
},
|
|
@@ -253,7 +330,7 @@ class MCPServer {
|
|
|
253
330
|
{
|
|
254
331
|
name: 'explain_history',
|
|
255
332
|
description:
|
|
256
|
-
'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.',
|
|
257
334
|
inputSchema: {
|
|
258
335
|
type: 'object',
|
|
259
336
|
properties: {
|
|
@@ -264,7 +341,7 @@ class MCPServer {
|
|
|
264
341
|
target: {
|
|
265
342
|
type: 'string',
|
|
266
343
|
description:
|
|
267
|
-
'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"',
|
|
268
345
|
},
|
|
269
346
|
limit: {
|
|
270
347
|
type: 'number',
|
|
@@ -283,6 +360,32 @@ class MCPServer {
|
|
|
283
360
|
type: 'boolean',
|
|
284
361
|
description: "Include each commit's diff of the target (default false)",
|
|
285
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
|
+
},
|
|
286
389
|
},
|
|
287
390
|
required: ['path', 'target'],
|
|
288
391
|
},
|
|
@@ -336,6 +439,288 @@ class MCPServer {
|
|
|
336
439
|
type: 'string',
|
|
337
440
|
description: "Count with this model's tokenizer encoding and report the fit",
|
|
338
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
|
+
},
|
|
448
|
+
},
|
|
449
|
+
required: ['path'],
|
|
450
|
+
},
|
|
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
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: 'check_policy',
|
|
526
|
+
description:
|
|
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.',
|
|
528
|
+
inputSchema: {
|
|
529
|
+
type: 'object',
|
|
530
|
+
properties: {
|
|
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
|
+
},
|
|
538
|
+
},
|
|
539
|
+
required: ['path'],
|
|
540
|
+
},
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: 'audit_log',
|
|
544
|
+
description:
|
|
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.",
|
|
546
|
+
inputSchema: {
|
|
547
|
+
type: 'object',
|
|
548
|
+
properties: {
|
|
549
|
+
path: { type: 'string', description: 'Project root' },
|
|
550
|
+
since: {
|
|
551
|
+
type: 'string',
|
|
552
|
+
description:
|
|
553
|
+
'Only exports at or after this date (2026-10-01), ISO time or age (30m, 12h, 7d, 2w)',
|
|
554
|
+
},
|
|
555
|
+
limit: {
|
|
556
|
+
type: 'number',
|
|
557
|
+
description: `The last N events (default ${DEFAULT_AUDIT_LIMIT}; 0 = all)`,
|
|
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
|
+
},
|
|
565
|
+
},
|
|
566
|
+
required: ['path'],
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: 'ask_codebase',
|
|
571
|
+
description:
|
|
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.',
|
|
573
|
+
inputSchema: {
|
|
574
|
+
type: 'object',
|
|
575
|
+
properties: {
|
|
576
|
+
path: { type: 'string', description: 'Project root' },
|
|
577
|
+
query: { type: 'string', description: 'The question, in natural language' },
|
|
578
|
+
limit: {
|
|
579
|
+
type: 'number',
|
|
580
|
+
description: `How many files (default ${DEFAULT_ASK_LIMIT})`,
|
|
581
|
+
},
|
|
582
|
+
repos: {
|
|
583
|
+
type: 'array',
|
|
584
|
+
items: { type: 'string' },
|
|
585
|
+
description:
|
|
586
|
+
'Additional repository roots to include in multi-repository semantic search',
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
required: ['path', 'query'],
|
|
590
|
+
},
|
|
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
|
+
},
|
|
339
724
|
},
|
|
340
725
|
required: ['path'],
|
|
341
726
|
},
|
|
@@ -405,6 +790,21 @@ class MCPServer {
|
|
|
405
790
|
}),
|
|
406
791
|
};
|
|
407
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
|
+
}
|
|
408
808
|
|
|
409
809
|
switch (name) {
|
|
410
810
|
case 'analyze_codebase': {
|
|
@@ -502,6 +902,17 @@ class MCPServer {
|
|
|
502
902
|
}
|
|
503
903
|
case 'search_code': {
|
|
504
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
|
+
}
|
|
505
916
|
if (type === 'ranked') {
|
|
506
917
|
const result = searchProject(projectPath, query, {
|
|
507
918
|
limit: args.limit ?? DEFAULT_LIMIT,
|
|
@@ -510,6 +921,16 @@ class MCPServer {
|
|
|
510
921
|
});
|
|
511
922
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
512
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
|
+
}
|
|
513
934
|
let pattern = null;
|
|
514
935
|
if (type === 'regex') {
|
|
515
936
|
try {
|
|
@@ -572,7 +993,7 @@ class MCPServer {
|
|
|
572
993
|
}
|
|
573
994
|
|
|
574
995
|
// Use Analyzer with methodLevel: true
|
|
575
|
-
const methodAnalyzer = new Analyzer({ methodLevel: true });
|
|
996
|
+
const methodAnalyzer = new Analyzer({ methodLevel: true, cache: true });
|
|
576
997
|
const analysis = await methodAnalyzer.analyze(files);
|
|
577
998
|
|
|
578
999
|
const methods = [];
|
|
@@ -592,10 +1013,18 @@ class MCPServer {
|
|
|
592
1013
|
};
|
|
593
1014
|
}
|
|
594
1015
|
case 'repo_map': {
|
|
595
|
-
const map =
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
+
});
|
|
599
1028
|
const notes = map.warnings.map((warning) => `Note: ${warning}`).join('\n');
|
|
600
1029
|
return {
|
|
601
1030
|
content: [
|
|
@@ -608,6 +1037,7 @@ class MCPServer {
|
|
|
608
1037
|
}
|
|
609
1038
|
case 'pr_context': {
|
|
610
1039
|
const result = await buildPrContext(args.path, {
|
|
1040
|
+
repos: args.repos,
|
|
611
1041
|
base: args.base,
|
|
612
1042
|
head: args.head,
|
|
613
1043
|
budget: args.budget == null ? undefined : Math.floor(args.budget),
|
|
@@ -619,6 +1049,7 @@ class MCPServer {
|
|
|
619
1049
|
const result = getSymbol(args.path, args.name, {
|
|
620
1050
|
file: args.file,
|
|
621
1051
|
redact: args.redact !== false,
|
|
1052
|
+
repos: args.repos,
|
|
622
1053
|
});
|
|
623
1054
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
624
1055
|
}
|
|
@@ -636,15 +1067,47 @@ class MCPServer {
|
|
|
636
1067
|
}
|
|
637
1068
|
case 'explain_history': {
|
|
638
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;
|
|
639
1078
|
const result = await explainHistory(args.path, args.target, {
|
|
1079
|
+
repos: args.repos,
|
|
640
1080
|
limit: whole(args.limit),
|
|
641
1081
|
budget: whole(args.budget ?? 20000),
|
|
642
1082
|
since: args.since,
|
|
643
1083
|
patches: args.patches === true,
|
|
1084
|
+
discussions: args.discussions === true,
|
|
1085
|
+
maxDiscussions: whole(args.maxDiscussions),
|
|
1086
|
+
analyzer: args.analyzer === true,
|
|
1087
|
+
analyzerDb,
|
|
644
1088
|
});
|
|
645
1089
|
return { content: [{ type: 'text', text: result.text }] };
|
|
646
1090
|
}
|
|
647
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
|
+
}
|
|
648
1111
|
const result = await packContext(args.path, {
|
|
649
1112
|
include: args.include,
|
|
650
1113
|
exclude: args.exclude,
|
|
@@ -666,6 +1129,242 @@ class MCPServer {
|
|
|
666
1129
|
],
|
|
667
1130
|
};
|
|
668
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
|
+
}
|
|
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
|
+
}
|
|
1161
|
+
let result;
|
|
1162
|
+
try {
|
|
1163
|
+
result = { valid: true, ...loadPolicy(args.path).toJSON() };
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
if (!(error instanceof PolicyConfigError)) throw error;
|
|
1166
|
+
result = { valid: false, file: error.file, errors: error.errors };
|
|
1167
|
+
}
|
|
1168
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1169
|
+
}
|
|
1170
|
+
case 'audit_log': {
|
|
1171
|
+
const since = args.since === undefined ? null : parseSince(String(args.since));
|
|
1172
|
+
const limit =
|
|
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
|
+
}
|
|
1186
|
+
// Only the policy's own logs: a tool argument never names a file to read
|
|
1187
|
+
const { files, error } = auditLogFiles(args.path);
|
|
1188
|
+
const { events, skipped, missing } = readAuditLog(files);
|
|
1189
|
+
const matching = filterAuditEvents(events, { since });
|
|
1190
|
+
const result = {
|
|
1191
|
+
files: files.filter((file) => !missing.includes(file)),
|
|
1192
|
+
total: matching.length,
|
|
1193
|
+
events: filterAuditEvents(matching, { limit }),
|
|
1194
|
+
skippedLines: skipped,
|
|
1195
|
+
};
|
|
1196
|
+
if (error) result.policyError = error.message;
|
|
1197
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1198
|
+
}
|
|
1199
|
+
case 'ask_codebase': {
|
|
1200
|
+
const result = await askCodebase(args.path, args.query, {
|
|
1201
|
+
repos: args.repos,
|
|
1202
|
+
limit: Number.isInteger(args.limit) ? args.limit : undefined,
|
|
1203
|
+
maxChars: ASK_SNIPPET_CHARS,
|
|
1204
|
+
});
|
|
1205
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
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
|
+
}
|
|
669
1368
|
default:
|
|
670
1369
|
throw new Error(`Unknown tool: ${name}`);
|
|
671
1370
|
}
|