gitnexus 1.6.11-rc.2 → 1.6.11-rc.21
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/README.md +43 -1
- package/dist/_shared/impact-risk.d.ts +37 -0
- package/dist/_shared/impact-risk.d.ts.map +1 -0
- package/dist/_shared/impact-risk.js +92 -0
- package/dist/_shared/impact-risk.js.map +1 -0
- package/dist/_shared/index.d.ts +2 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +2 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/cli/ai-context.js +4 -4
- package/dist/cli/analyze-config.d.ts +2 -0
- package/dist/cli/analyze-config.js +16 -0
- package/dist/cli/analyze-options.d.ts +4 -0
- package/dist/cli/analyze.d.ts +17 -0
- package/dist/cli/analyze.js +43 -7
- package/dist/cli/eval-server.js +22 -5
- package/dist/cli/group.js +7 -1
- package/dist/cli/help-i18n.js +2 -0
- package/dist/cli/i18n/en.d.ts +12 -2
- package/dist/cli/i18n/en.js +12 -2
- package/dist/cli/i18n/resources.d.ts +22 -2
- package/dist/cli/i18n/zh-CN.d.ts +10 -0
- package/dist/cli/i18n/zh-CN.js +12 -2
- package/dist/cli/index.js +11 -4
- package/dist/cli/status.js +91 -6
- package/dist/cli/watch-queue.d.ts +41 -0
- package/dist/cli/watch-queue.js +184 -0
- package/dist/cli/watch.d.ts +20 -0
- package/dist/cli/watch.js +372 -0
- package/dist/cli/wiki.js +15 -2
- package/dist/config/ignore-service.d.ts +11 -0
- package/dist/config/ignore-service.js +41 -4
- package/dist/config/repo-control-file.d.ts +3 -0
- package/dist/config/repo-control-file.js +115 -0
- package/dist/core/group/config-parser.js +20 -2
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +33 -2
- package/dist/core/group/extractors/fs-utils.d.ts +2 -0
- package/dist/core/group/extractors/fs-utils.js +86 -0
- package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
- package/dist/core/group/extractors/graphql-extractor.js +652 -0
- package/dist/core/group/extractors/java-workspace-extractor.js +244 -31
- package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
- package/dist/core/group/extractors/manifest-extractor.js +1 -1
- package/dist/core/group/matching.js +8 -1
- package/dist/core/group/service.js +5 -1
- package/dist/core/group/storage.js +1 -0
- package/dist/core/group/sync.d.ts +3 -1
- package/dist/core/group/sync.js +43 -5
- package/dist/core/group/types.d.ts +19 -5
- package/dist/core/incremental/derived-writeback.d.ts +36 -0
- package/dist/core/incremental/derived-writeback.js +68 -0
- package/dist/core/incremental/subgraph-extract.d.ts +6 -4
- package/dist/core/incremental/subgraph-extract.js +7 -5
- package/dist/core/index-content-drift.d.ts +54 -0
- package/dist/core/index-content-drift.js +127 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +15 -5
- package/dist/core/ingestion/filesystem-walker.js +20 -3
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.d.ts +22 -0
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.js +124 -0
- package/dist/core/ingestion/language-provider.d.ts +43 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.d.ts +62 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.js +954 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +3 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.js +6 -1
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +8 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/java/captures.js +14 -1
- package/dist/core/ingestion/languages/java/lombok-synthesizer.d.ts +42 -0
- package/dist/core/ingestion/languages/java/lombok-synthesizer.js +439 -0
- package/dist/core/ingestion/languages/java/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.js +62 -0
- package/dist/core/ingestion/languages/java.js +2 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.d.ts +99 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.js +173 -0
- package/dist/core/ingestion/languages/jvm/beanspec.d.ts +17 -0
- package/dist/core/ingestion/languages/jvm/beanspec.js +42 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +14 -1
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.d.ts +26 -0
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.js +417 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.js +77 -0
- package/dist/core/ingestion/languages/kotlin.js +2 -0
- package/dist/core/ingestion/pipeline-phases/di.js +47 -14
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +3 -1
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +57 -86
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/runner.d.ts +4 -1
- package/dist/core/ingestion/pipeline-phases/runner.js +34 -14
- package/dist/core/ingestion/pipeline-phases/scan.js +25 -13
- package/dist/core/ingestion/pipeline.d.ts +6 -0
- package/dist/core/ingestion/pipeline.js +44 -12
- package/dist/core/ingestion/scope-extractor.js +1 -0
- package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
- package/dist/core/ingestion/utils/symbol-labels.js +2 -2
- package/dist/core/ingestion/workers/parse-worker.js +28 -2
- package/dist/core/lbug/lbug-adapter.d.ts +59 -0
- package/dist/core/lbug/lbug-adapter.js +154 -1
- package/dist/core/run-analyze.d.ts +17 -0
- package/dist/core/run-analyze.js +231 -23
- package/dist/core/search/fts-indexes.d.ts +19 -1
- package/dist/core/search/fts-indexes.js +28 -1
- package/dist/core/wiki/generator.js +8 -0
- package/dist/core/wiki/grok-client.d.ts +21 -0
- package/dist/core/wiki/grok-client.js +287 -0
- package/dist/core/wiki/llm-client.d.ts +1 -1
- package/dist/core/wiki/llm-client.js +5 -2
- package/dist/core/wiki/local-cli-client.d.ts +11 -0
- package/dist/core/wiki/local-cli-client.js +22 -9
- package/dist/mcp/local/local-backend.d.ts +24 -7
- package/dist/mcp/local/local-backend.js +190 -81
- package/dist/mcp/local/pdg-impact.d.ts +8 -4
- package/dist/mcp/local/pdg-impact.js +7 -2
- package/dist/mcp/repository-policy.d.ts +5 -1
- package/dist/mcp/repository-policy.js +48 -4
- package/dist/mcp/resources.js +2 -1
- package/dist/mcp/server.js +6 -5
- package/dist/mcp/tools.js +31 -19
- package/dist/server/api.js +23 -64
- package/dist/server/grep-params.d.ts +18 -0
- package/dist/server/grep-params.js +83 -0
- package/dist/server/grep-scan.d.ts +23 -0
- package/dist/server/grep-scan.js +107 -0
- package/dist/server/grep-worker.d.ts +1 -0
- package/dist/server/grep-worker.js +12 -0
- package/dist/server/mcp-http.d.ts +8 -0
- package/dist/server/mcp-http.js +16 -1
- package/dist/storage/file-hash.d.ts +5 -0
- package/dist/storage/file-hash.js +16 -6
- package/dist/storage/fs-atomic.d.ts +24 -0
- package/dist/storage/fs-atomic.js +86 -2
- package/dist/storage/git.d.ts +19 -8
- package/dist/storage/git.js +83 -24
- package/dist/storage/gitnexus-managed-paths.d.ts +36 -0
- package/dist/storage/gitnexus-managed-paths.js +46 -0
- package/dist/storage/parse-cache.d.ts +22 -4
- package/dist/storage/parse-cache.js +106 -29
- package/dist/storage/parsedfile-store.d.ts +34 -60
- package/dist/storage/parsedfile-store.js +177 -171
- package/dist/storage/repo-manager.d.ts +11 -1
- package/dist/storage/repo-manager.js +23 -2
- package/dist/storage/repo-meta.d.ts +12 -0
- package/dist/storage/v8-sidecar.d.ts +48 -0
- package/dist/storage/v8-sidecar.js +347 -0
- package/dist/types/pipeline.d.ts +14 -0
- package/package.json +4 -1
- package/scripts/cross-platform-shard.ts +4 -2
- package/scripts/cross-platform-tests.ts +7 -1
- package/skills/gitnexus-cli.md +11 -3
- package/skills/gitnexus-impact-analysis.md +9 -0
- package/web/assets/{agent-Dr4l5EOp.js → agent-CFqT4hjR.js} +108 -104
- package/web/assets/{index-2zdvEdzg.js → index-BMIniRtX.js} +3 -3
- package/web/index.html +1 -1
package/dist/mcp/resources.js
CHANGED
|
@@ -254,7 +254,8 @@ async function getReposResource(backend) {
|
|
|
254
254
|
}
|
|
255
255
|
if (repos.length > 1) {
|
|
256
256
|
lines.push('');
|
|
257
|
-
lines.push('# Multiple repos indexed.
|
|
257
|
+
lines.push('# Multiple repos indexed. Read-only tools may omit repo when an MCP default is configured or GitNexus process.cwd() is inside one listed path without crossing an unindexed nested Git checkout.');
|
|
258
|
+
lines.push('# Otherwise—and for mutating tools without an MCP default—pass repo explicitly:');
|
|
258
259
|
lines.push(`# query({search_query: "auth", repo: "${repos[0].name}"})`);
|
|
259
260
|
}
|
|
260
261
|
return lines.join('\n');
|
package/dist/mcp/server.js
CHANGED
|
@@ -136,11 +136,11 @@ export function createMCPServer(backend, options = {}) {
|
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
});
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
139
|
+
// Make the effective routing contract machine-readable. Read-only tools may
|
|
140
|
+
// use a cwd-derived default; mutating rename remains explicit unless policy
|
|
141
|
+
// supplies a single/default repository.
|
|
142
142
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
143
|
-
const
|
|
143
|
+
const { readOnlyRequiresRepo, mutatingRequiresRepo } = await repositoryPolicy.toolSchemaRepoRequirements(backend);
|
|
144
144
|
return {
|
|
145
145
|
tools: GITNEXUS_TOOLS.filter((tool) => (!readOnly || MCP_READ_ONLY_TOOLS.has(tool.name)) &&
|
|
146
146
|
repositoryPolicy.toolAllowed(tool.name))
|
|
@@ -148,7 +148,8 @@ export function createMCPServer(backend, options = {}) {
|
|
|
148
148
|
.map((tool) => ({
|
|
149
149
|
name: tool.name,
|
|
150
150
|
description: tool.description,
|
|
151
|
-
inputSchema:
|
|
151
|
+
inputSchema: (tool.name === 'rename' ? mutatingRequiresRepo : readOnlyRequiresRepo) &&
|
|
152
|
+
REPO_SCOPED_TOOLS.has(tool.name)
|
|
152
153
|
? {
|
|
153
154
|
...tool.inputSchema,
|
|
154
155
|
required: [...new Set([...tool.inputSchema.required, 'repo'])],
|
package/dist/mcp/tools.js
CHANGED
|
@@ -49,6 +49,8 @@ export const PDG_QUERY_MAX_LIMIT = 200;
|
|
|
49
49
|
// Shared impact traversal depth cap. The MCP schema advertises this bound;
|
|
50
50
|
// PDG direct backend callers also enforce it before running traversal.
|
|
51
51
|
export const IMPACT_MAX_DEPTH = 32;
|
|
52
|
+
const CWD_AWARE_REPO_OMISSION = 'Omit when only one repo is indexed, an MCP default is configured, or the GitNexus process cwd is inside a registered path without crossing an unindexed nested Git checkout; otherwise specify it explicitly.';
|
|
53
|
+
const MUTATING_REPO_OMISSION = 'Omit only when one repo is indexed or an MCP default is configured; otherwise mutating tools require an explicit repo.';
|
|
52
54
|
export const GITNEXUS_TOOLS = [
|
|
53
55
|
{
|
|
54
56
|
name: 'list_repos',
|
|
@@ -61,8 +63,10 @@ PAGINATION: Results are paginated so a large registry is not truncated by MCP/LL
|
|
|
61
63
|
WHEN TO USE: First step when multiple repos are indexed, or to discover available repos.
|
|
62
64
|
AFTER THIS: READ gitnexus://repo/{name}/context for the repo you want to work with.
|
|
63
65
|
|
|
64
|
-
When multiple repos are indexed,
|
|
65
|
-
|
|
66
|
+
When multiple repos are indexed, repo-scoped read-only tools use the configured
|
|
67
|
+
MCP default or the registered path containing the GitNexus process cwd, unless
|
|
68
|
+
cwd has crossed into an unindexed nested Git checkout. If neither applies,
|
|
69
|
+
specify the "repo" parameter explicitly.`,
|
|
66
70
|
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
|
67
71
|
inputSchema: {
|
|
68
72
|
type: 'object',
|
|
@@ -148,7 +152,7 @@ SERVICE: optional monorepo path prefix (POSIX-style, case-sensitive segments). W
|
|
|
148
152
|
},
|
|
149
153
|
repo: {
|
|
150
154
|
type: 'string',
|
|
151
|
-
description:
|
|
155
|
+
description: `Indexed repository name or path, or group mode "@<groupName>" / "@<groupName>/<memberPath>" (member path keys from group.yaml). ${CWD_AWARE_REPO_OMISSION}`,
|
|
152
156
|
},
|
|
153
157
|
service: {
|
|
154
158
|
type: 'string',
|
|
@@ -227,7 +231,7 @@ TIPS:
|
|
|
227
231
|
},
|
|
228
232
|
repo: {
|
|
229
233
|
type: 'string',
|
|
230
|
-
description:
|
|
234
|
+
description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
|
|
231
235
|
},
|
|
232
236
|
},
|
|
233
237
|
required: ['statement'],
|
|
@@ -290,7 +294,7 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
|
|
|
290
294
|
},
|
|
291
295
|
repo: {
|
|
292
296
|
type: 'string',
|
|
293
|
-
description:
|
|
297
|
+
description: `Indexed repository name or path, or group mode "@<groupName>" / "@<groupName>/<memberPath>". ${CWD_AWARE_REPO_OMISSION}`,
|
|
294
298
|
},
|
|
295
299
|
service: {
|
|
296
300
|
type: 'string',
|
|
@@ -334,7 +338,7 @@ Returns: changed symbols, affected processes, and a risk summary.
|
|
|
334
338
|
},
|
|
335
339
|
repo: {
|
|
336
340
|
type: 'string',
|
|
337
|
-
description:
|
|
341
|
+
description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
|
|
338
342
|
},
|
|
339
343
|
},
|
|
340
344
|
required: [],
|
|
@@ -373,7 +377,7 @@ A graph too large to analyze at all returns \`{ error, truncated: true }\` with
|
|
|
373
377
|
},
|
|
374
378
|
repo: {
|
|
375
379
|
type: 'string',
|
|
376
|
-
description:
|
|
380
|
+
description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
|
|
377
381
|
},
|
|
378
382
|
},
|
|
379
383
|
required: [],
|
|
@@ -410,7 +414,7 @@ Handles disambiguation via context()'s payload verbatim: an ambiguous symbol_nam
|
|
|
410
414
|
},
|
|
411
415
|
repo: {
|
|
412
416
|
type: 'string',
|
|
413
|
-
description:
|
|
417
|
+
description: `Repository name or path. ${MUTATING_REPO_OMISSION}`,
|
|
414
418
|
},
|
|
415
419
|
},
|
|
416
420
|
required: ['new_name'],
|
|
@@ -425,17 +429,19 @@ MODE (opt-in): "callgraph" (default) walks symbol→symbol edges (CALLS/IMPORTS/
|
|
|
425
429
|
|
|
426
430
|
STATEMENT-ANCHORED PDG SLICE: with mode:'pdg', pass "line" (1-based source line within the target symbol) to seed the dependence slice on the statement at that line and return what depends on it in affectedStatements (line + text). Inter-procedural symbols are still reported through interproceduralByDepth/pdgInterprocedural and the compatibility byDepth bucket. Without "line", pdg returns whole-symbol inter-procedural reach plus local whole-symbol PDG diagnostics.
|
|
427
431
|
|
|
428
|
-
PDG OUTPUT CONTRACT: every mode:'pdg' result (success, empty, degraded, or error) carries pdgResultVersion:
|
|
432
|
+
PDG OUTPUT CONTRACT: every mode:'pdg' result (success, empty, degraded, or error) carries pdgResultVersion:3 — a stable discriminator for external consumers that bumps on any breaking change to the PDG result shape (distinct from the DB schema version). Successful PDG results include mode:'pdg', a full target envelope (id/name/type/filePath), affectedStatements, affectedStatementCount, interproceduralByDepth/pdgInterprocedural for cross-function reach, compatibility byDepth/byDepthCounts, risk:'UNKNOWN', and a note describing the unified contract. Degraded PDG results (no-layer, sub-layer-missing, unknown) keep mode:'pdg', pdgResultVersion:3, target metadata when the target resolves, risk:'UNKNOWN', note/remediation, and empty byDepth parity fields — never a false-safe zero. If depth and limit both bound the slice, truncatedByReasons reports both causes while truncatedBy remains scalar. Return-value-ascent coverage is published structurally at pdgEvidence.ascent — present iff the inter-procedural descent ran, including on an empty slice — with referencesScanned (DISTINCT callees scanned for a CALL_SUMMARY: a distinct-id tally, not a call-site count — two call sites to the same callee count once), returnFlowFound (whether the ascent fired anywhere in the slice), undecodableSummaryCount, examinedComplete (whether that scan covered every callee the index recorded a resolved id for on the visited blocks), incompleteReasons ('traversal-truncated' | 'callee-list-capped' | 'callee-ids-unrecorded'), and callSummaryLayerPresent. Read callSummaryLayerPresent FIRST: false ⇒ a pre-CALL_SUMMARY index, so {referencesScanned:N>0, returnFlowFound:false} is self-consistent and says nothing about the callees — the scan ran, but no layer existed in which a return-flow could be recorded (remedy: re-run gitnexus analyze --pdg). Branch on those fields; the note narrates the same facts in prose for humans and is not a stable contract.
|
|
429
433
|
|
|
430
434
|
WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break.
|
|
431
435
|
AFTER THIS: Review d=1 items (WILL BREAK). Use context() on high-risk symbols.
|
|
432
436
|
|
|
433
437
|
Output includes:
|
|
434
|
-
- risk: LOW / MEDIUM / HIGH / CRITICAL / UNKNOWN. An upstream walk that resolved ZERO callers reports UNKNOWN, never LOW, and carries riskNote: "safe to change" is a claim about callers and there were none to reason about, so the symbol is either genuinely unused OR reached only through a reference class the index does not record (plain-object property access, a bare-identifier read of a module-scope const). Confirm with a text search before acting on it. Downstream walks are unaffected — an empty downstream result reports resolved callees, not safety.
|
|
438
|
+
- risk: LOW / MEDIUM / HIGH / CRITICAL / UNKNOWN. This is the HIGH/CRITICAL edit-gate field. File targets lack process/community membership, so their risk is not directly comparable with symbol risk; use riskSharedAxes to compare the direct/total axes common to both. Group-mode (\`repo: "@…"\`) results lift the same fields to the top-level envelope. The web Graph-RAG impact tool expands File targets to in-file symbols before enrichment, so process/cluster axes remain comparable there. An upstream walk that resolved ZERO callers reports UNKNOWN, never LOW, and carries riskNote: "safe to change" is a claim about callers and there were none to reason about, so the symbol is either genuinely unused OR reached only through a reference class the index does not record (plain-object property access, a bare-identifier read of a module-scope const). Confirm with a text search before acting on it. Downstream walks are unaffected — an empty downstream result reports resolved callees, not safety.
|
|
439
|
+
- riskSharedAxes: single-repo risk computed only from direct and total impact. Group mode then applies the cross-repo crossing overlay to that local value. Suitable for comparing File and symbol targets within the same mode. Never substitute it for \`risk\` when deciding whether to warn before edits.
|
|
440
|
+
- riskScale: { comparableAcrossKinds, unusedAxes } — names process/module axes that were structurally unavailable, skipped, budget-exhausted (\`IMPACT_MAX_CHUNKS=0\`), truncated (sampled a subset of impacted symbols), or failed at query time. Failed-query and truncated-sample counts are lower bounds: known HIGH/CRITICAL warnings survive, otherwise risk is UNKNOWN. Group impact copies this metadata from the local leg.
|
|
435
441
|
- riskNote: string — present only when risk is UNKNOWN; states why the verdict is withheld.
|
|
436
442
|
- summary: direct callers, processes affected, modules affected
|
|
437
443
|
- affected_processes: which execution flows break and at which step
|
|
438
|
-
- affected_modules: which functional areas are hit (direct vs indirect)
|
|
444
|
+
- affected_modules: which functional areas are hit (direct vs indirect; classification-unavailable when that secondary query fails)
|
|
439
445
|
- byDepth: affected symbols grouped by traversal depth (paginated by limit/offset; omitted when summaryOnly:true — use byDepthCounts for totals per depth, pagination object when truncated). Each item includes a processes:[{id,label,processType,step}] field listing the execution flows that symbol participates in. Empty when the symbol has no process membership. Can ALSO be empty when partial:true is set — either the process-aggregation pass hit its cap before detecting affected processes, or per-symbol enrichment was capped on a very large page. When partial:true, do NOT treat processes:[] as proof of no participation; cross-check the top-level affected_processes list.
|
|
440
446
|
- epistemic: 'exact' | 'lower-bound' — whether impactedCount is the whole story. 'lower-bound' means the walk provably missed callers, so the count is a floor. Absent only on skipped probes (ambiguous-candidate lists, group fan-out).
|
|
441
447
|
- boundaries: string[] — one plain-language sentence per reason the count is short. Prose for humans; branch on causes instead.
|
|
@@ -540,7 +546,7 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
|
|
|
540
546
|
},
|
|
541
547
|
repo: {
|
|
542
548
|
type: 'string',
|
|
543
|
-
description:
|
|
549
|
+
description: `Indexed repository name or path, or group mode "@<groupName>" / "@<groupName>/<memberPath>". ${CWD_AWARE_REPO_OMISSION}`,
|
|
544
550
|
},
|
|
545
551
|
service: {
|
|
546
552
|
type: 'string',
|
|
@@ -628,7 +634,7 @@ Findings are deliberately NOT part of impact()'s traversal or the web schema —
|
|
|
628
634
|
},
|
|
629
635
|
repo: {
|
|
630
636
|
type: 'string',
|
|
631
|
-
description:
|
|
637
|
+
description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
|
|
632
638
|
},
|
|
633
639
|
},
|
|
634
640
|
required: [],
|
|
@@ -677,7 +683,7 @@ CONTRACT CAVEATS:
|
|
|
677
683
|
},
|
|
678
684
|
repo: {
|
|
679
685
|
type: 'string',
|
|
680
|
-
description:
|
|
686
|
+
description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
|
|
681
687
|
},
|
|
682
688
|
},
|
|
683
689
|
required: ['mode', 'target'],
|
|
@@ -701,7 +707,7 @@ Returns: route nodes with their handlers, middleware wrapper chains (e.g., withA
|
|
|
701
707
|
},
|
|
702
708
|
repo: {
|
|
703
709
|
type: 'string',
|
|
704
|
-
description:
|
|
710
|
+
description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
|
|
705
711
|
},
|
|
706
712
|
},
|
|
707
713
|
required: [],
|
|
@@ -719,7 +725,10 @@ Returns: tool nodes with their handler files and descriptions.`,
|
|
|
719
725
|
type: 'object',
|
|
720
726
|
properties: {
|
|
721
727
|
tool: { type: 'string', description: 'Filter by tool name. Omit for all tools.' },
|
|
722
|
-
repo: {
|
|
728
|
+
repo: {
|
|
729
|
+
type: 'string',
|
|
730
|
+
description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
|
|
731
|
+
},
|
|
723
732
|
},
|
|
724
733
|
required: [],
|
|
725
734
|
},
|
|
@@ -742,7 +751,7 @@ Returns routes that have both detected response keys AND consumers. Shows top-le
|
|
|
742
751
|
},
|
|
743
752
|
repo: {
|
|
744
753
|
type: 'string',
|
|
745
|
-
description:
|
|
754
|
+
description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
|
|
746
755
|
},
|
|
747
756
|
},
|
|
748
757
|
required: [],
|
|
@@ -767,7 +776,10 @@ Response shape is keyed on how many routes match, not on the data: exactly one m
|
|
|
767
776
|
type: 'string',
|
|
768
777
|
description: 'Optional HTTP verb — GET, POST, PUT, PATCH, DELETE, etc. — to narrow a multi-verb route or file lookup to a single method. Returns an error if no matched route uses that verb.',
|
|
769
778
|
},
|
|
770
|
-
repo: {
|
|
779
|
+
repo: {
|
|
780
|
+
type: 'string',
|
|
781
|
+
description: `Repository name or path. ${CWD_AWARE_REPO_OMISSION}`,
|
|
782
|
+
},
|
|
771
783
|
},
|
|
772
784
|
required: [],
|
|
773
785
|
},
|
|
@@ -877,7 +889,7 @@ DESTINATION TRACE (cross-repo): for an "@groupName" trace, OMIT to/to_uid/to_fil
|
|
|
877
889
|
},
|
|
878
890
|
repo: {
|
|
879
891
|
type: 'string',
|
|
880
|
-
description:
|
|
892
|
+
description: `Repository name or path, or "@groupName" / "@groupName/memberPath" for a cross-repo trace over a group. ${CWD_AWARE_REPO_OMISSION}`,
|
|
881
893
|
},
|
|
882
894
|
},
|
|
883
895
|
required: [],
|
package/dist/server/api.js
CHANGED
|
@@ -20,14 +20,16 @@ import { searchFTSFromLbug } from '../core/search/bm25-index.js';
|
|
|
20
20
|
import { hybridSearch } from '../core/search/hybrid-search.js';
|
|
21
21
|
import { ftsDegradedWarning } from '../core/search/fts-indexes.js';
|
|
22
22
|
import { LocalBackend } from '../mcp/local/local-backend.js';
|
|
23
|
-
import { mountMCPEndpoints } from './mcp-http.js';
|
|
23
|
+
import { installServeMcpAuth, mountMCPEndpoints } from './mcp-http.js';
|
|
24
24
|
import { fileURLToPath } from 'url';
|
|
25
25
|
import { isTerminalJobStatus, JobManager } from './analyze-job.js';
|
|
26
26
|
import { mountSSEProgress } from './sse-progress.js';
|
|
27
27
|
import { resolveEmbedRunOutcome, withMeasuredEmbeddingCount, } from './embed-run-outcome.js';
|
|
28
28
|
import { decideEmbeddingResume, mintInterruptedCheckpoint } from '../core/embedding-checkpoint.js';
|
|
29
29
|
import { measurePersistedEmbeddingCount, persistedEmbeddingCountOrUndefined, } from '../core/embedding-count.js';
|
|
30
|
-
import { assertString,
|
|
30
|
+
import { assertString, BadRequestError, createRouteLimiter } from './validation.js';
|
|
31
|
+
import { parseGrepQuery, GREP_TIME_BUDGET_MS } from './grep-params.js';
|
|
32
|
+
import { runGrepScanInWorker } from './grep-scan.js';
|
|
31
33
|
import { extractRepoName, getCloneDir, cloneOrPull, warnIfInsecureAzureConfig, GITHUB_TOKEN_HOSTS, } from './git-clone.js';
|
|
32
34
|
import { createAnalyzeUploadHandler } from './analyze-upload.js';
|
|
33
35
|
import { assertServeAuthForPublicOrigin, createPublicOriginMatcher, createWriteOriginGuard, logOriginPolicy, PUBLIC_ORIGIN_ENV, resolveTrustProxy, TRUST_PROXY_ENV, warnIfRateLimitKeysCollapse, } from './middleware.js';
|
|
@@ -616,6 +618,9 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
616
618
|
callback(null, isAllowedOrigin(origin));
|
|
617
619
|
},
|
|
618
620
|
}));
|
|
621
|
+
// Optional protocol-layer auth for the MCP route. Keep this before the
|
|
622
|
+
// global body parser so rejected requests do not consume the JSON budget.
|
|
623
|
+
installServeMcpAuth(app);
|
|
619
624
|
app.use(express.json({ limit: '10mb' }));
|
|
620
625
|
// Origin guard for write routes: loopback, the server's own bound host, and
|
|
621
626
|
// any configured public origin — prevents CSRF from other devices.
|
|
@@ -1126,75 +1131,29 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1126
1131
|
res.status(404).json({ error: 'Repository not found' });
|
|
1127
1132
|
return;
|
|
1128
1133
|
}
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
|
|
1133
|
-
const rawPattern = req.query.pattern;
|
|
1134
|
-
if (rawPattern === undefined) {
|
|
1135
|
-
res.status(400).json({ error: 'Missing "pattern" query parameter' });
|
|
1136
|
-
return;
|
|
1137
|
-
}
|
|
1138
|
-
const pattern = assertString(rawPattern, 'pattern');
|
|
1139
|
-
if (pattern.length === 0) {
|
|
1140
|
-
res.status(400).json({ error: 'Missing "pattern" query parameter' });
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
// Length cap: applies to both literal and regex modes as a defense-in-depth
|
|
1144
|
-
// bound against pathological input.
|
|
1145
|
-
if (pattern.length > 200) {
|
|
1146
|
-
res.status(400).json({ error: 'Pattern too long (max 200 characters)' });
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
// Treat user input as a literal substring in all cases to prevent
|
|
1150
|
-
// regex-injection/ReDoS via attacker-controlled regex syntax.
|
|
1151
|
-
const effectivePattern = escapeRegExp(pattern);
|
|
1152
|
-
// Validate regex syntax (catches both opt-in user regex and any escapeRegExp bug)
|
|
1153
|
-
let regex;
|
|
1154
|
-
try {
|
|
1155
|
-
regex = new RegExp(effectivePattern, 'gim');
|
|
1156
|
-
}
|
|
1157
|
-
catch {
|
|
1158
|
-
res.status(400).json({ error: 'Invalid regex pattern' });
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
const parsedLimit = Number(req.query.limit ?? 50);
|
|
1162
|
-
const limit = Number.isFinite(parsedLimit)
|
|
1163
|
-
? Math.max(1, Math.min(200, Math.trunc(parsedLimit)))
|
|
1164
|
-
: 50;
|
|
1165
|
-
const results = [];
|
|
1134
|
+
// Pattern parsing lives in grep-params.ts (unit-testable without
|
|
1135
|
+
// Express + LadybugDB). Matching runs in a worker so terminate() can
|
|
1136
|
+
// cut a stuck regex.test() when the wall-clock budget expires.
|
|
1137
|
+
const { regex, fileFilter, limit } = parseGrepQuery(req.query);
|
|
1166
1138
|
const repoRoot = path.resolve(entry.path);
|
|
1167
|
-
// Get file paths from the graph (lightweight — no content loaded)
|
|
1168
1139
|
const lbugPath = path.join(entry.storagePath, 'lbug');
|
|
1169
1140
|
const fileRows = await withLbugDb(lbugPath, () => executeQuery(`MATCH (n:File) WHERE n.content IS NOT NULL RETURN n.filePath AS filePath`), { readOnly: true });
|
|
1170
|
-
|
|
1141
|
+
const filePaths = [];
|
|
1171
1142
|
for (const row of fileRows) {
|
|
1172
|
-
if (results.length >= limit)
|
|
1173
|
-
break;
|
|
1174
1143
|
const filePath = row.filePath || '';
|
|
1175
|
-
|
|
1176
|
-
// Path traversal guard
|
|
1177
|
-
const safeRepoRoot = repoRoot.endsWith(path.sep) ? repoRoot : repoRoot + path.sep;
|
|
1178
|
-
if (!fullPath.startsWith(safeRepoRoot) && fullPath !== repoRoot)
|
|
1144
|
+
if (fileFilter && !filePath.toLowerCase().includes(fileFilter))
|
|
1179
1145
|
continue;
|
|
1180
|
-
|
|
1181
|
-
try {
|
|
1182
|
-
content = await fs.readFile(fullPath, 'utf-8');
|
|
1183
|
-
}
|
|
1184
|
-
catch {
|
|
1185
|
-
continue; // File may have been deleted since indexing
|
|
1186
|
-
}
|
|
1187
|
-
const lines = content.split('\n');
|
|
1188
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1189
|
-
if (results.length >= limit)
|
|
1190
|
-
break;
|
|
1191
|
-
if (regex.test(lines[i])) {
|
|
1192
|
-
results.push({ filePath, line: i + 1, text: lines[i].trim().slice(0, 200) });
|
|
1193
|
-
}
|
|
1194
|
-
regex.lastIndex = 0;
|
|
1195
|
-
}
|
|
1146
|
+
filePaths.push(filePath);
|
|
1196
1147
|
}
|
|
1197
|
-
|
|
1148
|
+
const { results, timedOut } = await runGrepScanInWorker({
|
|
1149
|
+
repoRoot,
|
|
1150
|
+
filePaths,
|
|
1151
|
+
pattern: regex.source,
|
|
1152
|
+
flags: regex.flags,
|
|
1153
|
+
limit,
|
|
1154
|
+
deadlineMs: Date.now() + GREP_TIME_BUDGET_MS,
|
|
1155
|
+
});
|
|
1156
|
+
res.json({ results, ...(timedOut ? { timedOut: true } : {}) });
|
|
1198
1157
|
}
|
|
1199
1158
|
catch (err) {
|
|
1200
1159
|
res.status(statusFromError(err)).json({ error: err.message || 'Grep failed' });
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Hard cap on pattern length — unchanged from the literal-only era. */
|
|
2
|
+
export declare const GREP_PATTERN_MAX_LENGTH = 200;
|
|
3
|
+
/** Wall-clock budget for one /api/grep call; parent terminate()s the scan worker. */
|
|
4
|
+
export declare const GREP_TIME_BUDGET_MS = 5000;
|
|
5
|
+
export declare const GREP_DEFAULT_LIMIT = 50;
|
|
6
|
+
export declare const GREP_MAX_LIMIT = 200;
|
|
7
|
+
export interface ParsedGrepQuery {
|
|
8
|
+
regex: RegExp;
|
|
9
|
+
/** Lowercased path substring; '' disables path filtering. */
|
|
10
|
+
fileFilter: string;
|
|
11
|
+
limit: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parse /api/grep query parameters into a ready-to-use regex + filters.
|
|
15
|
+
* Throws BadRequestError (mapped to HTTP 400 by statusFromError) on
|
|
16
|
+
* missing/over-long patterns or invalid regex syntax.
|
|
17
|
+
*/
|
|
18
|
+
export declare function parseGrepQuery(query: Record<string, unknown>): ParsedGrepQuery;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query-parameter parsing for GET /api/grep.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from api.ts so the contract is unit-testable without pulling
|
|
5
|
+
* Express + the LadybugDB native adapter into the test run (same rationale
|
|
6
|
+
* as the #2790 helper extraction).
|
|
7
|
+
*
|
|
8
|
+
* Contract fix (Patch 12): the grep tool schema in gitnexus-web has always
|
|
9
|
+
* promised regex search with an optional path-substring filter and
|
|
10
|
+
* case-sensitivity control, but the handler used to escapeRegExp() every
|
|
11
|
+
* pattern into a literal substring — an agent asking for "sign|Sign" got
|
|
12
|
+
* zero hits and concluded the code didn't exist, and the schema's own
|
|
13
|
+
* example ("console\.log") could never match. This restores the promised
|
|
14
|
+
* semantics. Matching runs in a worker_threads worker (`grep-worker.ts`) so
|
|
15
|
+
* `terminate()` can interrupt a catastrophic `regex.test()` when the
|
|
16
|
+
* wall-clock budget expires; the parent event loop stays responsive.
|
|
17
|
+
* Bounded mitigations:
|
|
18
|
+
* - pattern length cap (200 chars, unchanged from the literal-only era)
|
|
19
|
+
* - line-by-line matching (each regex.test call sees one source line)
|
|
20
|
+
* - a wall-clock budget the parent enforces via worker terminate()
|
|
21
|
+
* - result cap unchanged (limit, max 200)
|
|
22
|
+
* - literal=1 opt-out restores the old escaped-substring immunity
|
|
23
|
+
*/
|
|
24
|
+
import { assertString, escapeRegExp, BadRequestError } from './validation.js';
|
|
25
|
+
/** Hard cap on pattern length — unchanged from the literal-only era. */
|
|
26
|
+
export const GREP_PATTERN_MAX_LENGTH = 200;
|
|
27
|
+
/** Wall-clock budget for one /api/grep call; parent terminate()s the scan worker. */
|
|
28
|
+
export const GREP_TIME_BUDGET_MS = 5_000;
|
|
29
|
+
export const GREP_DEFAULT_LIMIT = 50;
|
|
30
|
+
export const GREP_MAX_LIMIT = 200;
|
|
31
|
+
const isFlagTrue = (value, name) => {
|
|
32
|
+
const s = assertString(value ?? '', name).toLowerCase();
|
|
33
|
+
return s === '1' || s === 'true';
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Parse /api/grep query parameters into a ready-to-use regex + filters.
|
|
37
|
+
* Throws BadRequestError (mapped to HTTP 400 by statusFromError) on
|
|
38
|
+
* missing/over-long patterns or invalid regex syntax.
|
|
39
|
+
*/
|
|
40
|
+
export function parseGrepQuery(query) {
|
|
41
|
+
if (query.pattern === undefined) {
|
|
42
|
+
throw new BadRequestError('Missing "pattern" query parameter');
|
|
43
|
+
}
|
|
44
|
+
const pattern = assertString(query.pattern, 'pattern');
|
|
45
|
+
if (pattern.length === 0) {
|
|
46
|
+
throw new BadRequestError('Missing "pattern" query parameter');
|
|
47
|
+
}
|
|
48
|
+
if (pattern.length > GREP_PATTERN_MAX_LENGTH) {
|
|
49
|
+
throw new BadRequestError(`Pattern too long (max ${GREP_PATTERN_MAX_LENGTH} characters)`);
|
|
50
|
+
}
|
|
51
|
+
// Regex semantics by default — what the tool schema always promised.
|
|
52
|
+
// literal=1 opts back into the escaped-substring behaviour of the
|
|
53
|
+
// literal-only era for callers that want it verbatim.
|
|
54
|
+
const caseSensitive = isFlagTrue(query.caseSensitive, 'caseSensitive');
|
|
55
|
+
const flags = caseSensitive ? '' : 'i';
|
|
56
|
+
let regex;
|
|
57
|
+
try {
|
|
58
|
+
// Deliberately no 'g' flag: the handler tests line-by-line and a
|
|
59
|
+
// stateful lastIndex across lines would skip matches (the old handler
|
|
60
|
+
// had to reset it manually). No 'm' either: each test receives a
|
|
61
|
+
// single line, so ^/$ already anchor at string boundaries — 'm'
|
|
62
|
+
// would be a no-op.
|
|
63
|
+
if (isFlagTrue(query.literal, 'literal')) {
|
|
64
|
+
regex = new RegExp(escapeRegExp(pattern), flags);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Intentional: /api/grep advertises real regex (see file header + SECURITY.md).
|
|
68
|
+
// ReDoS is mitigated by running the scan in a worker and terminate()-ing it.
|
|
69
|
+
// codeql[js/regex-injection]
|
|
70
|
+
regex = new RegExp(pattern, flags);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
throw new BadRequestError('Invalid regex pattern');
|
|
75
|
+
}
|
|
76
|
+
// Path-substring filter, case-insensitive ("Controller.java", "src/api").
|
|
77
|
+
const fileFilter = assertString(query.fileFilter ?? '', 'fileFilter').toLowerCase();
|
|
78
|
+
const parsedLimit = Number(query.limit ?? GREP_DEFAULT_LIMIT);
|
|
79
|
+
const limit = Number.isFinite(parsedLimit)
|
|
80
|
+
? Math.max(1, Math.min(GREP_MAX_LIMIT, Math.trunc(parsedLimit)))
|
|
81
|
+
: GREP_DEFAULT_LIMIT;
|
|
82
|
+
return { regex, fileFilter, limit };
|
|
83
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface GrepHit {
|
|
2
|
+
filePath: string;
|
|
3
|
+
line: number;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export interface GrepScanInput {
|
|
7
|
+
repoRoot: string;
|
|
8
|
+
/** Repo-relative paths already filtered by fileFilter. */
|
|
9
|
+
filePaths: string[];
|
|
10
|
+
pattern: string;
|
|
11
|
+
flags: string;
|
|
12
|
+
limit: number;
|
|
13
|
+
/** Absolute Date.now() deadline. */
|
|
14
|
+
deadlineMs: number;
|
|
15
|
+
}
|
|
16
|
+
export interface GrepScanResult {
|
|
17
|
+
results: GrepHit[];
|
|
18
|
+
timedOut: boolean;
|
|
19
|
+
}
|
|
20
|
+
export type GrepProgress = (partial: GrepScanResult) => void;
|
|
21
|
+
export declare function scanGrepFiles(input: GrepScanInput, onProgress?: GrepProgress): Promise<GrepScanResult>;
|
|
22
|
+
export declare function grepWorkerPath(): string;
|
|
23
|
+
export declare function runGrepScanInWorker(input: GrepScanInput): Promise<GrepScanResult>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem grep scan used by GET /api/grep.
|
|
3
|
+
*
|
|
4
|
+
* Matching runs in a worker_threads worker (see grep-worker.ts) so a
|
|
5
|
+
* catastrophic `regex.test()` can be killed with terminate() when the
|
|
6
|
+
* wall-clock budget expires. The parent event loop stays responsive.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
12
|
+
import { Worker } from 'node:worker_threads';
|
|
13
|
+
export async function scanGrepFiles(input, onProgress) {
|
|
14
|
+
const regex = new RegExp(input.pattern, input.flags);
|
|
15
|
+
const results = [];
|
|
16
|
+
const repoRoot = path.resolve(input.repoRoot);
|
|
17
|
+
const safeRepoRoot = repoRoot.endsWith(path.sep) ? repoRoot : repoRoot + path.sep;
|
|
18
|
+
let timedOut = false;
|
|
19
|
+
files: for (const filePath of input.filePaths) {
|
|
20
|
+
if (results.length >= input.limit)
|
|
21
|
+
break;
|
|
22
|
+
if (Date.now() > input.deadlineMs) {
|
|
23
|
+
timedOut = true;
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
const fullPath = path.resolve(repoRoot, filePath);
|
|
27
|
+
if (!fullPath.startsWith(safeRepoRoot) && fullPath !== repoRoot)
|
|
28
|
+
continue;
|
|
29
|
+
let content;
|
|
30
|
+
try {
|
|
31
|
+
content = await fs.readFile(fullPath, 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const lines = content.split('\n');
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
if (results.length >= input.limit)
|
|
39
|
+
break files;
|
|
40
|
+
if ((i & 255) === 0 && Date.now() > input.deadlineMs) {
|
|
41
|
+
timedOut = true;
|
|
42
|
+
break files;
|
|
43
|
+
}
|
|
44
|
+
regex.lastIndex = 0;
|
|
45
|
+
if (regex.test(lines[i])) {
|
|
46
|
+
results.push({ filePath, line: i + 1, text: lines[i].trim().slice(0, 200) });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
onProgress?.({ results: results.slice(), timedOut });
|
|
50
|
+
}
|
|
51
|
+
return { results, timedOut };
|
|
52
|
+
}
|
|
53
|
+
const _require = createRequire(import.meta.url);
|
|
54
|
+
export function grepWorkerPath() {
|
|
55
|
+
const callerPath = fileURLToPath(import.meta.url);
|
|
56
|
+
const isDev = callerPath.endsWith('.ts');
|
|
57
|
+
return path.join(path.dirname(callerPath), isDev ? 'grep-worker.ts' : 'grep-worker.js');
|
|
58
|
+
}
|
|
59
|
+
export function runGrepScanInWorker(input) {
|
|
60
|
+
const callerPath = fileURLToPath(import.meta.url);
|
|
61
|
+
const isDev = callerPath.endsWith('.ts');
|
|
62
|
+
const tsxHookArgs = isDev
|
|
63
|
+
? ['--import', pathToFileURL(_require.resolve('tsx/esm')).href]
|
|
64
|
+
: [];
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
let settled = false;
|
|
67
|
+
let latest = { results: [], timedOut: false };
|
|
68
|
+
const worker = new Worker(grepWorkerPath(), {
|
|
69
|
+
workerData: input,
|
|
70
|
+
execArgv: tsxHookArgs,
|
|
71
|
+
});
|
|
72
|
+
const finish = (result) => {
|
|
73
|
+
if (settled)
|
|
74
|
+
return;
|
|
75
|
+
settled = true;
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
void worker.terminate();
|
|
78
|
+
resolve(result);
|
|
79
|
+
};
|
|
80
|
+
const remain = Math.max(1, input.deadlineMs - Date.now());
|
|
81
|
+
const timer = setTimeout(() => {
|
|
82
|
+
finish({ results: latest.results, timedOut: true });
|
|
83
|
+
}, remain);
|
|
84
|
+
worker.on('message', (msg) => {
|
|
85
|
+
if (msg.type === 'progress') {
|
|
86
|
+
latest = { results: msg.results, timedOut: msg.timedOut };
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (msg.type === 'done') {
|
|
90
|
+
finish({ results: msg.results, timedOut: msg.timedOut });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
worker.on('error', (err) => {
|
|
94
|
+
if (settled)
|
|
95
|
+
return;
|
|
96
|
+
settled = true;
|
|
97
|
+
clearTimeout(timer);
|
|
98
|
+
void worker.terminate();
|
|
99
|
+
reject(err);
|
|
100
|
+
});
|
|
101
|
+
worker.on('exit', () => {
|
|
102
|
+
if (settled)
|
|
103
|
+
return;
|
|
104
|
+
finish({ results: latest.results, timedOut: true });
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
2
|
+
if (!parentPort) {
|
|
3
|
+
throw new Error('grep-worker must run as a worker_threads worker');
|
|
4
|
+
}
|
|
5
|
+
const ext = import.meta.url.endsWith('.ts') ? '.ts' : '.js';
|
|
6
|
+
const { scanGrepFiles } = await import(new URL(`./grep-scan${ext}`, import.meta.url).href);
|
|
7
|
+
const port = parentPort;
|
|
8
|
+
const input = workerData;
|
|
9
|
+
const out = await scanGrepFiles(input, (partial) => {
|
|
10
|
+
port.postMessage({ type: 'progress', ...partial });
|
|
11
|
+
});
|
|
12
|
+
port.postMessage({ type: 'done', ...out });
|
|
@@ -9,4 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { Express } from 'express';
|
|
11
11
|
import type { LocalBackend } from '../mcp/local/local-backend.js';
|
|
12
|
+
/**
|
|
13
|
+
* Protect serve's /api/mcp route when the shared MCP bearer token is configured.
|
|
14
|
+
*
|
|
15
|
+
* This middleware must be installed before Express's global JSON parser so an
|
|
16
|
+
* unauthenticated request body is rejected before it is parsed. The standalone
|
|
17
|
+
* `gitnexus mcp --http` server resolves the same environment variable.
|
|
18
|
+
*/
|
|
19
|
+
export declare function installServeMcpAuth(app: Express, env?: NodeJS.ProcessEnv): boolean;
|
|
12
20
|
export declare function mountMCPEndpoints(app: Express, backend: LocalBackend): Promise<() => Promise<void>>;
|
package/dist/server/mcp-http.js
CHANGED
|
@@ -7,9 +7,24 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Used by server/api.ts to wire up the full web server.
|
|
9
9
|
*/
|
|
10
|
-
import { createStreamableHttpHandler } from '../mcp/http-transport.js';
|
|
10
|
+
import { createAuthMiddleware, createStreamableHttpHandler, resolveAuthToken, } from '../mcp/http-transport.js';
|
|
11
11
|
import { createMcpRepositoryPolicy } from '../mcp/repository-policy.js';
|
|
12
12
|
import { logger } from '../core/logger.js';
|
|
13
|
+
/**
|
|
14
|
+
* Protect serve's /api/mcp route when the shared MCP bearer token is configured.
|
|
15
|
+
*
|
|
16
|
+
* This middleware must be installed before Express's global JSON parser so an
|
|
17
|
+
* unauthenticated request body is rejected before it is parsed. The standalone
|
|
18
|
+
* `gitnexus mcp --http` server resolves the same environment variable.
|
|
19
|
+
*/
|
|
20
|
+
export function installServeMcpAuth(app, env = process.env) {
|
|
21
|
+
const authToken = resolveAuthToken(undefined, env);
|
|
22
|
+
if (!authToken)
|
|
23
|
+
return false;
|
|
24
|
+
app.use('/api/mcp', createAuthMiddleware(authToken));
|
|
25
|
+
logger.info('Bearer authentication enabled for serve /api/mcp');
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
13
28
|
export async function mountMCPEndpoints(app, backend) {
|
|
14
29
|
const repositoryPolicy = await createMcpRepositoryPolicy(backend);
|
|
15
30
|
const { handler, cleanup } = createStreamableHttpHandler(backend, { repositoryPolicy });
|