kiri-mcp-server 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -5
- package/config/default.example.yml +9 -0
- package/config/scoring-profiles.yml +11 -6
- package/dist/config/default.example.yml +9 -0
- package/dist/config/scoring-profiles.yml +11 -6
- package/dist/package.json +1 -1
- package/dist/server/context.js +0 -1
- package/dist/server/handlers.js +547 -79
- package/dist/server/scoring.js +8 -3
- package/dist/shared/duckdb.js +0 -2
- package/dist/shared/embedding.js +15 -2
- package/dist/shared/tokenizer.js +0 -1
- package/dist/shared/utils/simpleYaml.js +0 -1
- package/dist/src/indexer/cli.d.ts +1 -0
- package/dist/src/indexer/cli.d.ts.map +1 -1
- package/dist/src/indexer/cli.js +197 -11
- package/dist/src/indexer/cli.js.map +1 -1
- package/dist/src/indexer/watch.d.ts +4 -3
- package/dist/src/indexer/watch.d.ts.map +1 -1
- package/dist/src/indexer/watch.js +11 -7
- package/dist/src/indexer/watch.js.map +1 -1
- package/dist/src/server/handlers.d.ts.map +1 -1
- package/dist/src/server/handlers.js +234 -26
- package/dist/src/server/handlers.js.map +1 -1
- package/dist/src/server/rpc.d.ts.map +1 -1
- package/dist/src/server/rpc.js +9 -3
- package/dist/src/server/rpc.js.map +1 -1
- package/dist/src/server/scoring.d.ts +2 -0
- package/dist/src/server/scoring.d.ts.map +1 -1
- package/dist/src/server/scoring.js +13 -1
- package/dist/src/server/scoring.js.map +1 -1
- package/dist/src/shared/duckdb.d.ts +1 -0
- package/dist/src/shared/duckdb.d.ts.map +1 -1
- package/dist/src/shared/duckdb.js +54 -3
- package/dist/src/shared/duckdb.js.map +1 -1
- package/dist/src/shared/embedding.d.ts.map +1 -1
- package/dist/src/shared/embedding.js +2 -8
- package/dist/src/shared/embedding.js.map +1 -1
- package/dist/src/shared/tokenizer.d.ts +18 -0
- package/dist/src/shared/tokenizer.d.ts.map +1 -1
- package/dist/src/shared/tokenizer.js +35 -0
- package/dist/src/shared/tokenizer.js.map +1 -1
- package/package.json +1 -1
package/dist/server/handlers.js
CHANGED
|
@@ -4,14 +4,14 @@ import { encode as encodeGPT } from "../shared/tokenizer.js";
|
|
|
4
4
|
import { coerceProfileName, loadScoringProfile } from "./scoring.js";
|
|
5
5
|
const DEFAULT_SEARCH_LIMIT = 50;
|
|
6
6
|
const DEFAULT_SNIPPET_WINDOW = 150;
|
|
7
|
-
const DEFAULT_BUNDLE_LIMIT = 12
|
|
7
|
+
const DEFAULT_BUNDLE_LIMIT = 7; // Reduced from 12 to optimize token usage
|
|
8
8
|
const MAX_BUNDLE_LIMIT = 20;
|
|
9
9
|
const MAX_KEYWORDS = 12;
|
|
10
10
|
const MAX_MATCHES_PER_KEYWORD = 40;
|
|
11
11
|
const MAX_DEPENDENCY_SEEDS = 8;
|
|
12
12
|
const MAX_DEPENDENCY_SEEDS_QUERY_LIMIT = 100; // SQL injection防御用の上限
|
|
13
13
|
const NEARBY_LIMIT = 6;
|
|
14
|
-
const FALLBACK_SNIPPET_WINDOW = 120
|
|
14
|
+
const FALLBACK_SNIPPET_WINDOW = 40; // Reduced from 120 to optimize token usage
|
|
15
15
|
const MAX_RERANK_LIMIT = 50;
|
|
16
16
|
const STOP_WORDS = new Set([
|
|
17
17
|
"the",
|
|
@@ -73,22 +73,119 @@ function normalizeBundleLimit(limit) {
|
|
|
73
73
|
}
|
|
74
74
|
return Math.min(Math.max(1, Math.floor(limit)), MAX_BUNDLE_LIMIT);
|
|
75
75
|
}
|
|
76
|
-
|
|
76
|
+
/**
|
|
77
|
+
* トークン化戦略を取得
|
|
78
|
+
* 環境変数またはデフォルト値から決定
|
|
79
|
+
*/
|
|
80
|
+
function getTokenizationStrategy() {
|
|
81
|
+
const strategy = process.env.KIRI_TOKENIZATION_STRATEGY?.toLowerCase();
|
|
82
|
+
if (strategy === "legacy" || strategy === "hybrid") {
|
|
83
|
+
return strategy;
|
|
84
|
+
}
|
|
85
|
+
return "phrase-aware"; // デフォルト
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 引用符で囲まれたフレーズを抽出
|
|
89
|
+
* 例: 'search "page-agent handler" test' → ["page-agent handler"]
|
|
90
|
+
*/
|
|
91
|
+
function extractQuotedPhrases(text) {
|
|
92
|
+
const phrases = [];
|
|
93
|
+
const quotePattern = /"([^"]+)"|'([^']+)'/g;
|
|
94
|
+
let match;
|
|
95
|
+
let remaining = text;
|
|
96
|
+
// eslint-disable-next-line no-cond-assign
|
|
97
|
+
while ((match = quotePattern.exec(text)) !== null) {
|
|
98
|
+
const phrase = (match[1] || match[2] || "").trim().toLowerCase();
|
|
99
|
+
if (phrase.length >= 3) {
|
|
100
|
+
phrases.push(phrase);
|
|
101
|
+
}
|
|
102
|
+
remaining = remaining.replace(match[0], " ");
|
|
103
|
+
}
|
|
104
|
+
return { phrases, remaining };
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* ハイフン区切り用語を抽出
|
|
108
|
+
* 例: "page-agent lambda-handler" → ["page-agent", "lambda-handler"]
|
|
109
|
+
*/
|
|
110
|
+
function extractHyphenatedTerms(text) {
|
|
111
|
+
// マッチ条件: 英数字 + ハイフン + 英数字(少なくとも3文字以上)
|
|
112
|
+
const hyphenPattern = /\b[a-z0-9]+(?:-[a-z0-9]+)+\b/gi;
|
|
113
|
+
const matches = text.match(hyphenPattern) || [];
|
|
114
|
+
return matches
|
|
115
|
+
.map((term) => term.toLowerCase())
|
|
116
|
+
.filter((term) => term.length >= 3 && !STOP_WORDS.has(term));
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* パスライクな用語を抽出
|
|
120
|
+
* 例: "lambda/page-agent/handler" → ["lambda", "page-agent", "handler"]
|
|
121
|
+
*/
|
|
122
|
+
function extractPathSegments(text) {
|
|
123
|
+
const pathPattern = /\b[a-z0-9_-]+(?:\/[a-z0-9_-]+)+\b/gi;
|
|
124
|
+
const matches = text.match(pathPattern) || [];
|
|
125
|
+
const segments = [];
|
|
126
|
+
for (const path of matches) {
|
|
127
|
+
const parts = path.toLowerCase().split("/");
|
|
128
|
+
for (const part of parts) {
|
|
129
|
+
if (part.length >= 3 && !STOP_WORDS.has(part) && !segments.includes(part)) {
|
|
130
|
+
segments.push(part);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return segments;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 通常の単語を抽出(レガシーロジック)
|
|
138
|
+
*/
|
|
139
|
+
function extractRegularWords(text, strategy) {
|
|
140
|
+
const splitPattern = strategy === "legacy" ? /[^a-z0-9_]+/iu : /[^a-z0-9_-]+/iu;
|
|
77
141
|
const words = text
|
|
78
142
|
.toLowerCase()
|
|
79
|
-
.split(
|
|
143
|
+
.split(splitPattern)
|
|
80
144
|
.map((word) => word.trim())
|
|
81
145
|
.filter((word) => word.length >= 3 && !STOP_WORDS.has(word));
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
146
|
+
return words;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* テキストからキーワード、フレーズ、パスセグメントを抽出
|
|
150
|
+
* トークン化戦略に基づいて、ハイフン区切り用語の処理方法を変更
|
|
151
|
+
*/
|
|
152
|
+
function extractKeywords(text) {
|
|
153
|
+
const strategy = getTokenizationStrategy();
|
|
154
|
+
const result = {
|
|
155
|
+
phrases: [],
|
|
156
|
+
keywords: [],
|
|
157
|
+
pathSegments: [],
|
|
158
|
+
};
|
|
159
|
+
// Phase 1: 引用符で囲まれたフレーズを抽出
|
|
160
|
+
const { phrases: quotedPhrases, remaining: afterQuotes } = extractQuotedPhrases(text);
|
|
161
|
+
result.phrases.push(...quotedPhrases);
|
|
162
|
+
// Phase 2: パスセグメントを抽出
|
|
163
|
+
const pathSegments = extractPathSegments(afterQuotes);
|
|
164
|
+
result.pathSegments.push(...pathSegments);
|
|
165
|
+
// Phase 3: ハイフン区切り用語を抽出(phrase-aware または hybrid モード)
|
|
166
|
+
if (strategy === "phrase-aware" || strategy === "hybrid") {
|
|
167
|
+
const hyphenatedTerms = extractHyphenatedTerms(afterQuotes);
|
|
168
|
+
result.phrases.push(...hyphenatedTerms);
|
|
169
|
+
// hybrid モードの場合、ハイフン区切り用語を分割したキーワードも追加
|
|
170
|
+
if (strategy === "hybrid") {
|
|
171
|
+
for (const term of hyphenatedTerms) {
|
|
172
|
+
const parts = term.split("-").filter((part) => part.length >= 3 && !STOP_WORDS.has(part));
|
|
173
|
+
result.keywords.push(...parts);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Phase 4: 通常の単語を抽出
|
|
178
|
+
const regularWords = extractRegularWords(afterQuotes, strategy);
|
|
179
|
+
// 重複を除去しながら、最大キーワード数まで追加
|
|
180
|
+
for (const word of regularWords) {
|
|
181
|
+
if (!result.keywords.includes(word) && !result.phrases.includes(word)) {
|
|
182
|
+
result.keywords.push(word);
|
|
183
|
+
if (result.keywords.length >= MAX_KEYWORDS) {
|
|
87
184
|
break;
|
|
88
185
|
}
|
|
89
186
|
}
|
|
90
187
|
}
|
|
91
|
-
return
|
|
188
|
+
return result;
|
|
92
189
|
}
|
|
93
190
|
function ensureCandidate(map, filePath) {
|
|
94
191
|
let candidate = map.get(filePath);
|
|
@@ -216,10 +313,10 @@ function buildSnippetPreview(content, startLine, endLine) {
|
|
|
216
313
|
const startIndex = Math.max(0, Math.min(startLine - 1, lines.length));
|
|
217
314
|
const endIndex = Math.max(startIndex, Math.min(endLine, lines.length));
|
|
218
315
|
const snippet = lines.slice(startIndex, endIndex).join("\n");
|
|
219
|
-
if (snippet.length <=
|
|
316
|
+
if (snippet.length <= 240) {
|
|
220
317
|
return snippet;
|
|
221
318
|
}
|
|
222
|
-
return `${snippet.slice(0,
|
|
319
|
+
return `${snippet.slice(0, 239)}…`;
|
|
223
320
|
}
|
|
224
321
|
/**
|
|
225
322
|
* トークン数を推定(コンテンツベース)
|
|
@@ -246,53 +343,339 @@ function estimateTokensFromContent(content, startLine, endLine) {
|
|
|
246
343
|
return Math.max(1, Math.ceil(text.length / 4));
|
|
247
344
|
}
|
|
248
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* 複数単語クエリを単語分割してOR検索条件を構築
|
|
348
|
+
* @param query - 検索クエリ文字列
|
|
349
|
+
* @returns 単語配列(2文字以下を除外)
|
|
350
|
+
*/
|
|
351
|
+
function splitQueryWords(query) {
|
|
352
|
+
// 空白、スラッシュ、ハイフン、アンダースコアで分割
|
|
353
|
+
const words = query.split(/[\s/\-_]+/).filter((w) => w.length > 2);
|
|
354
|
+
return words.length > 0 ? words : [query]; // 全て除外された場合は元のクエリを使用
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* ファイルタイプに基づいてスコアをブーストする
|
|
358
|
+
* プロファイルに応じて実装ファイルまたはドキュメントを優遇
|
|
359
|
+
* @param path - ファイルパス
|
|
360
|
+
* @param baseScore - 元のスコア
|
|
361
|
+
* @param profile - ブーストプロファイル ("default" | "docs" | "none")
|
|
362
|
+
* @returns ブースト適用後のスコア
|
|
363
|
+
*/
|
|
364
|
+
function applyFileTypeBoost(path, baseScore, profile = "default") {
|
|
365
|
+
// Blacklisted directories that are almost always irrelevant for code context
|
|
366
|
+
const blacklistedDirs = [
|
|
367
|
+
".cursor/",
|
|
368
|
+
".devcontainer/",
|
|
369
|
+
".serena/",
|
|
370
|
+
"__mocks__/",
|
|
371
|
+
"docs/",
|
|
372
|
+
".git/",
|
|
373
|
+
"node_modules/",
|
|
374
|
+
];
|
|
375
|
+
if (blacklistedDirs.some((dir) => path.startsWith(dir))) {
|
|
376
|
+
return -100; // Effectively remove it
|
|
377
|
+
}
|
|
378
|
+
if (profile === "none") {
|
|
379
|
+
return baseScore;
|
|
380
|
+
}
|
|
381
|
+
if (profile === "docs") {
|
|
382
|
+
if (path.endsWith(".md") || path.endsWith(".yaml") || path.endsWith(".yml")) {
|
|
383
|
+
return baseScore * 1.8; // Stronger boost for docs
|
|
384
|
+
}
|
|
385
|
+
if (path.startsWith("src/") &&
|
|
386
|
+
(path.endsWith(".ts") || path.endsWith(".js") || path.endsWith(".tsx"))) {
|
|
387
|
+
return baseScore * 0.5; // Stronger penalty for implementation files
|
|
388
|
+
}
|
|
389
|
+
return baseScore;
|
|
390
|
+
}
|
|
391
|
+
// Default profile: prioritize implementation files, heavily penalize docs
|
|
392
|
+
const docExtensions = [".md", ".yaml", ".yml", ".mdc", ".json"];
|
|
393
|
+
if (docExtensions.some((ext) => path.endsWith(ext))) {
|
|
394
|
+
return baseScore * 0.1; // Heavy penalty for docs
|
|
395
|
+
}
|
|
396
|
+
if (path.startsWith("src/app/")) {
|
|
397
|
+
return baseScore * 1.8;
|
|
398
|
+
}
|
|
399
|
+
if (path.startsWith("src/components/")) {
|
|
400
|
+
return baseScore * 1.7;
|
|
401
|
+
}
|
|
402
|
+
if (path.startsWith("src/lib/")) {
|
|
403
|
+
return baseScore * 1.6;
|
|
404
|
+
}
|
|
405
|
+
if (path.startsWith("src/") &&
|
|
406
|
+
(path.endsWith(".ts") || path.endsWith(".js") || path.endsWith(".tsx"))) {
|
|
407
|
+
return baseScore * 1.5;
|
|
408
|
+
}
|
|
409
|
+
if (path.startsWith("tests/") || path.startsWith("test/")) {
|
|
410
|
+
return baseScore * 0.2; // Also penalize tests in default mode
|
|
411
|
+
}
|
|
412
|
+
return baseScore;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* contextBundle専用のブーストプロファイル適用
|
|
416
|
+
* candidateのスコアと理由を直接変更する
|
|
417
|
+
* @param candidate - スコアリング対象の候補
|
|
418
|
+
* @param row - ファイル情報(path, ext)
|
|
419
|
+
* @param profile - ブーストプロファイル
|
|
420
|
+
*/
|
|
421
|
+
function applyBoostProfile(candidate, row, profile, extractedTerms, pathMatchWeight) {
|
|
422
|
+
if (profile === "none") {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const { path, ext } = row;
|
|
426
|
+
const lowerPath = path.toLowerCase();
|
|
427
|
+
const fileName = path.split("/").pop() ?? "";
|
|
428
|
+
// パスベースのスコアリング: goalのキーワード/フレーズがファイルパスに含まれる場合にブースト
|
|
429
|
+
if (extractedTerms && pathMatchWeight && pathMatchWeight > 0) {
|
|
430
|
+
// フレーズがパスに完全一致する場合(最高の重み)
|
|
431
|
+
for (const phrase of extractedTerms.phrases) {
|
|
432
|
+
if (lowerPath.includes(phrase)) {
|
|
433
|
+
candidate.score += pathMatchWeight * 1.5; // 1.5倍のブースト
|
|
434
|
+
candidate.reasons.add(`path-phrase:${phrase}`);
|
|
435
|
+
break; // 最初のマッチのみ適用
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// パスセグメントがマッチする場合(中程度の重み)
|
|
439
|
+
const pathParts = lowerPath.split("/");
|
|
440
|
+
for (const segment of extractedTerms.pathSegments) {
|
|
441
|
+
if (pathParts.includes(segment)) {
|
|
442
|
+
candidate.score += pathMatchWeight;
|
|
443
|
+
candidate.reasons.add(`path-segment:${segment}`);
|
|
444
|
+
break; // 最初のマッチのみ適用
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
// 通常のキーワードがパスに含まれる場合(低い重み)
|
|
448
|
+
for (const keyword of extractedTerms.keywords) {
|
|
449
|
+
if (lowerPath.includes(keyword)) {
|
|
450
|
+
candidate.score += pathMatchWeight * 0.5; // 0.5倍のブースト
|
|
451
|
+
candidate.reasons.add(`path-keyword:${keyword}`);
|
|
452
|
+
break; // 最初のマッチのみ適用
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Blacklisted directories that are almost always irrelevant for code context
|
|
457
|
+
const blacklistedDirs = [
|
|
458
|
+
".cursor/",
|
|
459
|
+
".devcontainer/",
|
|
460
|
+
".serena/",
|
|
461
|
+
"__mocks__/",
|
|
462
|
+
"docs/",
|
|
463
|
+
"test/",
|
|
464
|
+
"tests/",
|
|
465
|
+
".git/",
|
|
466
|
+
"node_modules/",
|
|
467
|
+
"db/migrate/",
|
|
468
|
+
"db/migrations/",
|
|
469
|
+
"config/",
|
|
470
|
+
"dist/",
|
|
471
|
+
"build/",
|
|
472
|
+
"out/",
|
|
473
|
+
"coverage/",
|
|
474
|
+
".vscode/",
|
|
475
|
+
".idea/",
|
|
476
|
+
"tmp/",
|
|
477
|
+
"temp/",
|
|
478
|
+
];
|
|
479
|
+
if (blacklistedDirs.some((dir) => path.startsWith(dir))) {
|
|
480
|
+
candidate.score = -100; // Effectively remove it
|
|
481
|
+
candidate.reasons.add("penalty:blacklisted-dir");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
// Penalize test files explicitly (even if outside test directories)
|
|
485
|
+
const testPatterns = [".spec.ts", ".spec.js", ".test.ts", ".test.js", ".spec.tsx", ".test.tsx"];
|
|
486
|
+
if (testPatterns.some((pattern) => lowerPath.endsWith(pattern))) {
|
|
487
|
+
candidate.score -= 2.0; // Strong penalty for test files
|
|
488
|
+
candidate.reasons.add("penalty:test-file");
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
// Penalize lock files and package manifests
|
|
492
|
+
const lockFiles = [
|
|
493
|
+
"package-lock.json",
|
|
494
|
+
"pnpm-lock.yaml",
|
|
495
|
+
"yarn.lock",
|
|
496
|
+
"bun.lockb",
|
|
497
|
+
"Gemfile.lock",
|
|
498
|
+
"Cargo.lock",
|
|
499
|
+
"poetry.lock",
|
|
500
|
+
];
|
|
501
|
+
if (lockFiles.some((lockFile) => fileName === lockFile)) {
|
|
502
|
+
candidate.score -= 3.0; // Very strong penalty for lock files
|
|
503
|
+
candidate.reasons.add("penalty:lock-file");
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
// Penalize configuration files
|
|
507
|
+
const configPatterns = [
|
|
508
|
+
".config.js",
|
|
509
|
+
".config.ts",
|
|
510
|
+
".config.mjs",
|
|
511
|
+
".config.cjs",
|
|
512
|
+
"tsconfig.json",
|
|
513
|
+
"jsconfig.json",
|
|
514
|
+
"package.json",
|
|
515
|
+
".eslintrc",
|
|
516
|
+
".prettierrc",
|
|
517
|
+
"jest.config",
|
|
518
|
+
"vite.config",
|
|
519
|
+
"vitest.config",
|
|
520
|
+
"webpack.config",
|
|
521
|
+
"rollup.config",
|
|
522
|
+
];
|
|
523
|
+
if (configPatterns.some((pattern) => lowerPath.endsWith(pattern) || fileName.startsWith(".env")) ||
|
|
524
|
+
fileName === "Dockerfile" ||
|
|
525
|
+
fileName === "docker-compose.yml" ||
|
|
526
|
+
fileName === "docker-compose.yaml") {
|
|
527
|
+
candidate.score -= 1.5; // Strong penalty for config files
|
|
528
|
+
candidate.reasons.add("penalty:config-file");
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
// Penalize migration files (by path content)
|
|
532
|
+
if (lowerPath.includes("migrate") || lowerPath.includes("migration")) {
|
|
533
|
+
candidate.score -= 2.0; // Strong penalty for migrations
|
|
534
|
+
candidate.reasons.add("penalty:migration-file");
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (profile === "docs") {
|
|
538
|
+
// DOCS PROFILE: Boost docs, penalize code
|
|
539
|
+
if (path.endsWith(".md") || path.endsWith(".yaml") || path.endsWith(".yml")) {
|
|
540
|
+
candidate.score += 0.8;
|
|
541
|
+
candidate.reasons.add("boost:doc-file");
|
|
542
|
+
}
|
|
543
|
+
else if (path.startsWith("src/") && (ext === ".ts" || ext === ".tsx" || ext === ".js")) {
|
|
544
|
+
candidate.score -= 0.5;
|
|
545
|
+
candidate.reasons.add("penalty:impl-file");
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
else if (profile === "default") {
|
|
549
|
+
// DEFAULT PROFILE: Penalize docs heavily, boost implementation files.
|
|
550
|
+
// Penalize documentation and other non-code files
|
|
551
|
+
const docExtensions = [".md", ".yaml", ".yml", ".mdc", ".json"];
|
|
552
|
+
if (docExtensions.some((docExt) => path.endsWith(docExt))) {
|
|
553
|
+
candidate.score -= 1.0; // Strong penalty to overcome structural similarity
|
|
554
|
+
candidate.reasons.add("penalty:doc-file");
|
|
555
|
+
}
|
|
556
|
+
// Boost implementation files, with more specific paths getting higher scores
|
|
557
|
+
if (path.startsWith("src/app/")) {
|
|
558
|
+
candidate.score += 0.8;
|
|
559
|
+
candidate.reasons.add("boost:app-file");
|
|
560
|
+
}
|
|
561
|
+
else if (path.startsWith("src/components/")) {
|
|
562
|
+
candidate.score += 0.7;
|
|
563
|
+
candidate.reasons.add("boost:component-file");
|
|
564
|
+
}
|
|
565
|
+
else if (path.startsWith("src/lib/")) {
|
|
566
|
+
candidate.score += 0.6;
|
|
567
|
+
candidate.reasons.add("boost:lib-file");
|
|
568
|
+
}
|
|
569
|
+
else if (path.startsWith("src/")) {
|
|
570
|
+
if (ext === ".ts" || ext === ".tsx" || ext === ".js") {
|
|
571
|
+
candidate.score += 0.5;
|
|
572
|
+
candidate.reasons.add("boost:impl-file");
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
249
577
|
export async function filesSearch(context, params) {
|
|
250
578
|
const { db, repoId } = context;
|
|
251
579
|
const { query } = params;
|
|
252
580
|
if (!query || query.trim().length === 0) {
|
|
253
|
-
throw new Error("
|
|
581
|
+
throw new Error("files_search requires a non-empty query. Provide a search keyword to continue.");
|
|
254
582
|
}
|
|
255
583
|
const limit = normalizeLimit(params.limit);
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
584
|
+
const hasFTS = context.features?.fts ?? false;
|
|
585
|
+
let sql;
|
|
586
|
+
let values;
|
|
587
|
+
if (hasFTS) {
|
|
588
|
+
// FTS拡張利用可能: fts_main_blob.match_bm25 を使用
|
|
589
|
+
const conditions = ["f.repo_id = ?"];
|
|
590
|
+
values = [repoId];
|
|
591
|
+
// 言語・拡張子フィルタ
|
|
592
|
+
if (params.lang) {
|
|
593
|
+
conditions.push("COALESCE(f.lang, '') = ?");
|
|
594
|
+
values.push(params.lang);
|
|
595
|
+
}
|
|
596
|
+
if (params.ext) {
|
|
597
|
+
conditions.push("COALESCE(f.ext, '') = ?");
|
|
598
|
+
values.push(params.ext);
|
|
599
|
+
}
|
|
600
|
+
if (params.path_prefix) {
|
|
601
|
+
conditions.push("f.path LIKE ?");
|
|
602
|
+
values.push(`${params.path_prefix}%`);
|
|
603
|
+
}
|
|
604
|
+
// FTS検索(BM25スコアリング)
|
|
605
|
+
sql = `
|
|
606
|
+
SELECT f.path, f.lang, f.ext, b.content, fts.score
|
|
607
|
+
FROM file f
|
|
608
|
+
JOIN blob b ON b.hash = f.blob_hash
|
|
609
|
+
JOIN (
|
|
610
|
+
SELECT hash, fts_main_blob.match_bm25(hash, ?) AS score
|
|
611
|
+
FROM blob
|
|
612
|
+
WHERE score IS NOT NULL
|
|
613
|
+
) fts ON fts.hash = b.hash
|
|
614
|
+
WHERE ${conditions.join(" AND ")}
|
|
615
|
+
ORDER BY fts.score DESC
|
|
616
|
+
LIMIT ?
|
|
617
|
+
`;
|
|
618
|
+
values.unshift(query); // FTSクエリを先頭に追加
|
|
619
|
+
values.push(limit);
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
// FTS拡張利用不可: ILIKE検索(Phase 1の単語分割ロジック)
|
|
623
|
+
const conditions = ["f.repo_id = ?", "b.content IS NOT NULL"];
|
|
624
|
+
values = [repoId];
|
|
625
|
+
const words = splitQueryWords(query);
|
|
626
|
+
if (words.length === 1) {
|
|
627
|
+
conditions.push("b.content ILIKE '%' || ? || '%'");
|
|
628
|
+
values.push(query);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
const wordConditions = words.map(() => "b.content ILIKE '%' || ? || '%'");
|
|
632
|
+
conditions.push(`(${wordConditions.join(" OR ")})`);
|
|
633
|
+
values.push(...words);
|
|
634
|
+
}
|
|
635
|
+
if (params.lang) {
|
|
636
|
+
conditions.push("COALESCE(f.lang, '') = ?");
|
|
637
|
+
values.push(params.lang);
|
|
638
|
+
}
|
|
639
|
+
if (params.ext) {
|
|
640
|
+
conditions.push("COALESCE(f.ext, '') = ?");
|
|
641
|
+
values.push(params.ext);
|
|
642
|
+
}
|
|
643
|
+
if (params.path_prefix) {
|
|
644
|
+
conditions.push("f.path LIKE ?");
|
|
645
|
+
values.push(`${params.path_prefix}%`);
|
|
646
|
+
}
|
|
647
|
+
sql = `
|
|
648
|
+
SELECT f.path, f.lang, f.ext, b.content
|
|
649
|
+
FROM file f
|
|
650
|
+
JOIN blob b ON b.hash = f.blob_hash
|
|
651
|
+
WHERE ${conditions.join(" AND ")}
|
|
652
|
+
ORDER BY f.path
|
|
653
|
+
LIMIT ?
|
|
654
|
+
`;
|
|
655
|
+
values.push(limit);
|
|
656
|
+
}
|
|
279
657
|
const rows = await db.all(sql, values);
|
|
280
|
-
|
|
658
|
+
const boostProfile = params.boost_profile ?? "default";
|
|
659
|
+
return rows
|
|
660
|
+
.map((row) => {
|
|
281
661
|
const { preview, line } = buildPreview(row.content ?? "", query);
|
|
662
|
+
const baseScore = row.score ?? 1.0; // FTS時はBM25スコア、ILIKE時は1.0
|
|
663
|
+
const boostedScore = applyFileTypeBoost(row.path, baseScore, boostProfile);
|
|
282
664
|
return {
|
|
283
665
|
path: row.path,
|
|
284
666
|
preview,
|
|
285
667
|
matchLine: line,
|
|
286
668
|
lang: row.lang,
|
|
287
669
|
ext: row.ext,
|
|
288
|
-
score:
|
|
670
|
+
score: boostedScore,
|
|
289
671
|
};
|
|
290
|
-
})
|
|
672
|
+
})
|
|
673
|
+
.sort((a, b) => b.score - a.score); // スコアの高い順に再ソート
|
|
291
674
|
}
|
|
292
675
|
export async function snippetsGet(context, params) {
|
|
293
676
|
const { db, repoId } = context;
|
|
294
677
|
if (!params.path) {
|
|
295
|
-
throw new Error("
|
|
678
|
+
throw new Error("snippets_get requires a file path. Specify a tracked text file path to continue.");
|
|
296
679
|
}
|
|
297
680
|
const rows = await db.all(`
|
|
298
681
|
SELECT f.path, f.lang, f.ext, f.is_binary, b.content
|
|
@@ -372,7 +755,7 @@ export async function contextBundle(context, params) {
|
|
|
372
755
|
const { db, repoId } = context;
|
|
373
756
|
const goal = params.goal?.trim() ?? "";
|
|
374
757
|
if (goal.length === 0) {
|
|
375
|
-
throw new Error("
|
|
758
|
+
throw new Error("context_bundle requires a non-empty goal. Describe your objective to receive context.");
|
|
376
759
|
}
|
|
377
760
|
const limit = normalizeBundleLimit(params.limit);
|
|
378
761
|
const artifacts = params.artifacts ?? {};
|
|
@@ -391,18 +774,68 @@ export async function contextBundle(context, params) {
|
|
|
391
774
|
}
|
|
392
775
|
const semanticSeed = keywordSources.join(" ");
|
|
393
776
|
const queryEmbedding = generateEmbedding(semanticSeed)?.values ?? null;
|
|
394
|
-
|
|
395
|
-
|
|
777
|
+
const extractedTerms = extractKeywords(semanticSeed);
|
|
778
|
+
// フォールバック: editing_pathからキーワードを抽出
|
|
779
|
+
if (extractedTerms.phrases.length === 0 &&
|
|
780
|
+
extractedTerms.keywords.length === 0 &&
|
|
781
|
+
artifacts.editing_path) {
|
|
396
782
|
const pathSegments = artifacts.editing_path
|
|
397
783
|
.split(/[/_.-]/)
|
|
398
784
|
.map((segment) => segment.toLowerCase())
|
|
399
785
|
.filter((segment) => segment.length >= 3 && !STOP_WORDS.has(segment));
|
|
400
|
-
|
|
786
|
+
extractedTerms.pathSegments.push(...pathSegments.slice(0, MAX_KEYWORDS));
|
|
401
787
|
}
|
|
402
788
|
const candidates = new Map();
|
|
403
789
|
const stringMatchSeeds = new Set();
|
|
404
790
|
const fileCache = new Map();
|
|
405
|
-
|
|
791
|
+
// フレーズマッチング(高い重み: textMatch × 2)
|
|
792
|
+
for (const phrase of extractedTerms.phrases) {
|
|
793
|
+
const rows = await db.all(`
|
|
794
|
+
SELECT f.path, f.lang, f.ext, f.is_binary, b.content, fe.vector_json, fe.dims AS vector_dims
|
|
795
|
+
FROM file f
|
|
796
|
+
JOIN blob b ON b.hash = f.blob_hash
|
|
797
|
+
LEFT JOIN file_embedding fe
|
|
798
|
+
ON fe.repo_id = f.repo_id
|
|
799
|
+
AND fe.path = f.path
|
|
800
|
+
WHERE f.repo_id = ?
|
|
801
|
+
AND f.is_binary = FALSE
|
|
802
|
+
AND b.content ILIKE '%' || ? || '%'
|
|
803
|
+
ORDER BY f.path
|
|
804
|
+
LIMIT ?
|
|
805
|
+
`, [repoId, phrase, MAX_MATCHES_PER_KEYWORD]);
|
|
806
|
+
for (const row of rows) {
|
|
807
|
+
if (row.content === null) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
const candidate = ensureCandidate(candidates, row.path);
|
|
811
|
+
// フレーズマッチは通常の2倍のスコア
|
|
812
|
+
candidate.score += weights.textMatch * 2.0;
|
|
813
|
+
candidate.reasons.add(`phrase:${phrase}`);
|
|
814
|
+
// Apply boost profile to prioritize/penalize files based on type and location
|
|
815
|
+
const boostProfile = params.boost_profile ?? "default";
|
|
816
|
+
applyBoostProfile(candidate, row, boostProfile, extractedTerms, weights.pathMatch);
|
|
817
|
+
const { line } = buildPreview(row.content, phrase);
|
|
818
|
+
candidate.matchLine =
|
|
819
|
+
candidate.matchLine === null ? line : Math.min(candidate.matchLine, line);
|
|
820
|
+
candidate.content ?? (candidate.content = row.content);
|
|
821
|
+
candidate.lang ?? (candidate.lang = row.lang);
|
|
822
|
+
candidate.ext ?? (candidate.ext = row.ext);
|
|
823
|
+
candidate.totalLines ?? (candidate.totalLines = row.content.length === 0 ? 0 : row.content.split(/\r?\n/).length);
|
|
824
|
+
candidate.embedding ?? (candidate.embedding = parseEmbedding(row.vector_json ?? null, row.vector_dims ?? null));
|
|
825
|
+
stringMatchSeeds.add(row.path);
|
|
826
|
+
if (!fileCache.has(row.path)) {
|
|
827
|
+
fileCache.set(row.path, {
|
|
828
|
+
content: row.content,
|
|
829
|
+
lang: row.lang,
|
|
830
|
+
ext: row.ext,
|
|
831
|
+
totalLines: candidate.totalLines ?? 0,
|
|
832
|
+
embedding: candidate.embedding,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
// キーワードマッチング(通常の重み)
|
|
838
|
+
for (const keyword of extractedTerms.keywords) {
|
|
406
839
|
const rows = await db.all(`
|
|
407
840
|
SELECT f.path, f.lang, f.ext, f.is_binary, b.content, fe.vector_json, fe.dims AS vector_dims
|
|
408
841
|
FROM file f
|
|
@@ -423,14 +856,17 @@ export async function contextBundle(context, params) {
|
|
|
423
856
|
const candidate = ensureCandidate(candidates, row.path);
|
|
424
857
|
candidate.score += weights.textMatch;
|
|
425
858
|
candidate.reasons.add(`text:${keyword}`);
|
|
859
|
+
// Apply boost profile to prioritize/penalize files based on type and location
|
|
860
|
+
const boostProfile = params.boost_profile ?? "default";
|
|
861
|
+
applyBoostProfile(candidate, row, boostProfile, extractedTerms, weights.pathMatch);
|
|
426
862
|
const { line } = buildPreview(row.content, keyword);
|
|
427
863
|
candidate.matchLine =
|
|
428
864
|
candidate.matchLine === null ? line : Math.min(candidate.matchLine, line);
|
|
429
|
-
candidate.content
|
|
430
|
-
candidate.lang
|
|
431
|
-
candidate.ext
|
|
432
|
-
candidate.totalLines
|
|
433
|
-
candidate.embedding
|
|
865
|
+
candidate.content ?? (candidate.content = row.content);
|
|
866
|
+
candidate.lang ?? (candidate.lang = row.lang);
|
|
867
|
+
candidate.ext ?? (candidate.ext = row.ext);
|
|
868
|
+
candidate.totalLines ?? (candidate.totalLines = row.content.length === 0 ? 0 : row.content.split(/\r?\n/).length);
|
|
869
|
+
candidate.embedding ?? (candidate.embedding = parseEmbedding(row.vector_json ?? null, row.vector_dims ?? null));
|
|
434
870
|
stringMatchSeeds.add(row.path);
|
|
435
871
|
if (!fileCache.has(row.path)) {
|
|
436
872
|
fileCache.set(row.path, {
|
|
@@ -447,7 +883,7 @@ export async function contextBundle(context, params) {
|
|
|
447
883
|
const editingCandidate = ensureCandidate(candidates, artifacts.editing_path);
|
|
448
884
|
editingCandidate.score += weights.editingPath;
|
|
449
885
|
editingCandidate.reasons.add("artifact:editing_path");
|
|
450
|
-
editingCandidate.matchLine
|
|
886
|
+
editingCandidate.matchLine ?? (editingCandidate.matchLine = 1);
|
|
451
887
|
}
|
|
452
888
|
// SQL injection防御: ファイルパスの検証パターン
|
|
453
889
|
const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_.\-/]+$/;
|
|
@@ -546,6 +982,7 @@ export async function contextBundle(context, params) {
|
|
|
546
982
|
}
|
|
547
983
|
applyStructuralScores(materializedCandidates, queryEmbedding, weights.structural);
|
|
548
984
|
const sortedCandidates = materializedCandidates
|
|
985
|
+
.filter((candidate) => candidate.score > 0) // Filter out candidates with negative or zero scores
|
|
549
986
|
.sort((a, b) => {
|
|
550
987
|
if (b.score === a.score) {
|
|
551
988
|
return a.path.localeCompare(b.path);
|
|
@@ -586,19 +1023,22 @@ export async function contextBundle(context, params) {
|
|
|
586
1023
|
if (endLine < startLine) {
|
|
587
1024
|
endLine = startLine;
|
|
588
1025
|
}
|
|
589
|
-
const preview = buildSnippetPreview(candidate.content, startLine, endLine);
|
|
590
1026
|
const reasons = new Set(candidate.reasons);
|
|
591
1027
|
if (selected && selected.symbol_name) {
|
|
592
1028
|
reasons.add(`symbol:${selected.symbol_name}`);
|
|
593
1029
|
}
|
|
594
1030
|
const normalizedScore = maxScore > 0 ? candidate.score / maxScore : 0;
|
|
595
|
-
|
|
1031
|
+
const item = {
|
|
596
1032
|
path: candidate.path,
|
|
597
1033
|
range: [startLine, endLine],
|
|
598
|
-
preview,
|
|
599
1034
|
why: Array.from(reasons).sort(),
|
|
600
1035
|
score: Number.isFinite(normalizedScore) ? normalizedScore : 0,
|
|
601
|
-
}
|
|
1036
|
+
};
|
|
1037
|
+
// Add preview only if not in compact mode
|
|
1038
|
+
if (!params.compact) {
|
|
1039
|
+
item.preview = buildSnippetPreview(candidate.content, startLine, endLine);
|
|
1040
|
+
}
|
|
1041
|
+
results.push(item);
|
|
602
1042
|
}
|
|
603
1043
|
// コンテンツベースのトークン推定を使用(より正確)
|
|
604
1044
|
const tokensEstimate = results.reduce((acc, item) => {
|
|
@@ -615,7 +1055,7 @@ export async function contextBundle(context, params) {
|
|
|
615
1055
|
export async function semanticRerank(context, params) {
|
|
616
1056
|
const text = params.text?.trim() ?? "";
|
|
617
1057
|
if (text.length === 0) {
|
|
618
|
-
throw new Error("
|
|
1058
|
+
throw new Error("semantic_rerank requires non-empty text. Describe the intent to compute semantic similarity.");
|
|
619
1059
|
}
|
|
620
1060
|
if (!Array.isArray(params.candidates) || params.candidates.length === 0) {
|
|
621
1061
|
return { candidates: [] };
|
|
@@ -683,11 +1123,9 @@ export async function semanticRerank(context, params) {
|
|
|
683
1123
|
export async function depsClosure(context, params) {
|
|
684
1124
|
const { db, repoId } = context;
|
|
685
1125
|
if (!params.path) {
|
|
686
|
-
throw new Error("
|
|
687
|
-
}
|
|
688
|
-
if (params.direction && params.direction !== "outbound") {
|
|
689
|
-
throw new Error("deps.closure currently supports only outbound direction. Use outbound.");
|
|
1126
|
+
throw new Error("deps_closure requires a file path. Provide a tracked source file path to continue.");
|
|
690
1127
|
}
|
|
1128
|
+
const direction = params.direction ?? "outbound";
|
|
691
1129
|
const maxDepth = params.max_depth ?? 3;
|
|
692
1130
|
const includePackages = params.include_packages ?? true;
|
|
693
1131
|
const dependencyRows = await db.all(`
|
|
@@ -695,12 +1133,23 @@ export async function depsClosure(context, params) {
|
|
|
695
1133
|
FROM dependency
|
|
696
1134
|
WHERE repo_id = ?
|
|
697
1135
|
`, [repoId]);
|
|
1136
|
+
// outbound: このファイルが使用する依存関係
|
|
698
1137
|
const outbound = new Map();
|
|
1138
|
+
// inbound: このファイルを使用しているファイル
|
|
1139
|
+
const inbound = new Map();
|
|
699
1140
|
for (const row of dependencyRows) {
|
|
1141
|
+
// outbound マップ構築
|
|
700
1142
|
if (!outbound.has(row.src_path)) {
|
|
701
1143
|
outbound.set(row.src_path, []);
|
|
702
1144
|
}
|
|
703
1145
|
outbound.get(row.src_path)?.push(row);
|
|
1146
|
+
// inbound マップ構築(dst が path の場合のみ)
|
|
1147
|
+
if (row.dst_kind === "path") {
|
|
1148
|
+
if (!inbound.has(row.dst)) {
|
|
1149
|
+
inbound.set(row.dst, []);
|
|
1150
|
+
}
|
|
1151
|
+
inbound.get(row.dst)?.push(row);
|
|
1152
|
+
}
|
|
704
1153
|
}
|
|
705
1154
|
const queue = [{ path: params.path, depth: 0 }];
|
|
706
1155
|
const visitedPaths = new Set([params.path]);
|
|
@@ -726,32 +1175,52 @@ export async function depsClosure(context, params) {
|
|
|
726
1175
|
if (current.depth >= maxDepth) {
|
|
727
1176
|
continue;
|
|
728
1177
|
}
|
|
729
|
-
|
|
1178
|
+
// direction に応じて使用するマップを選択
|
|
1179
|
+
const edgeMap = direction === "inbound" ? inbound : outbound;
|
|
1180
|
+
const edges = edgeMap.get(current.path) ?? [];
|
|
730
1181
|
for (const edge of edges) {
|
|
731
1182
|
const nextDepth = current.depth + 1;
|
|
732
|
-
if (
|
|
1183
|
+
if (direction === "inbound") {
|
|
1184
|
+
// inbound: edge.src_path がこのファイルを使用している
|
|
733
1185
|
recordEdge({
|
|
734
|
-
from:
|
|
735
|
-
to:
|
|
1186
|
+
from: edge.src_path,
|
|
1187
|
+
to: current.path,
|
|
736
1188
|
kind: "path",
|
|
737
1189
|
rel: edge.rel,
|
|
738
1190
|
depth: nextDepth,
|
|
739
1191
|
});
|
|
740
|
-
recordNode({ kind: "path", target: edge.
|
|
741
|
-
if (!visitedPaths.has(edge.
|
|
742
|
-
visitedPaths.add(edge.
|
|
743
|
-
queue.push({ path: edge.
|
|
1192
|
+
recordNode({ kind: "path", target: edge.src_path, depth: nextDepth });
|
|
1193
|
+
if (!visitedPaths.has(edge.src_path)) {
|
|
1194
|
+
visitedPaths.add(edge.src_path);
|
|
1195
|
+
queue.push({ path: edge.src_path, depth: nextDepth });
|
|
744
1196
|
}
|
|
745
1197
|
}
|
|
746
|
-
else
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1198
|
+
else {
|
|
1199
|
+
// outbound: このファイルが edge.dst を使用している
|
|
1200
|
+
if (edge.dst_kind === "path") {
|
|
1201
|
+
recordEdge({
|
|
1202
|
+
from: current.path,
|
|
1203
|
+
to: edge.dst,
|
|
1204
|
+
kind: "path",
|
|
1205
|
+
rel: edge.rel,
|
|
1206
|
+
depth: nextDepth,
|
|
1207
|
+
});
|
|
1208
|
+
recordNode({ kind: "path", target: edge.dst, depth: nextDepth });
|
|
1209
|
+
if (!visitedPaths.has(edge.dst)) {
|
|
1210
|
+
visitedPaths.add(edge.dst);
|
|
1211
|
+
queue.push({ path: edge.dst, depth: nextDepth });
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
else if (edge.dst_kind === "package" && includePackages) {
|
|
1215
|
+
recordEdge({
|
|
1216
|
+
from: current.path,
|
|
1217
|
+
to: edge.dst,
|
|
1218
|
+
kind: "package",
|
|
1219
|
+
rel: edge.rel,
|
|
1220
|
+
depth: nextDepth,
|
|
1221
|
+
});
|
|
1222
|
+
recordNode({ kind: "package", target: edge.dst, depth: nextDepth });
|
|
1223
|
+
}
|
|
755
1224
|
}
|
|
756
1225
|
}
|
|
757
1226
|
}
|
|
@@ -773,7 +1242,7 @@ export async function depsClosure(context, params) {
|
|
|
773
1242
|
});
|
|
774
1243
|
return {
|
|
775
1244
|
root: params.path,
|
|
776
|
-
direction
|
|
1245
|
+
direction,
|
|
777
1246
|
nodes,
|
|
778
1247
|
edges,
|
|
779
1248
|
};
|
|
@@ -797,4 +1266,3 @@ export async function resolveRepoId(db, repoRoot) {
|
|
|
797
1266
|
throw error;
|
|
798
1267
|
}
|
|
799
1268
|
}
|
|
800
|
-
//# sourceMappingURL=handlers.js.map
|