scythe-context-mcp 0.1.3 → 0.1.4
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/CHANGELOG.md +6 -0
- package/README.en.md +2 -0
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/dist/cli.js +1 -1
- package/dist/indexing/hybridSearch.js +9 -0
- package/dist/tools/registerTools.js +120 -25
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ This project follows semantic versioning before npm publication where practical.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.4] - 2026-06-14
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Add keyword-only fallback for hybrid search/context-pack calls when query embedding is unavailable, while preserving explicit `embedding_unavailable` diagnostics for semantic-only mode.
|
|
14
|
+
|
|
9
15
|
## [0.1.3] - 2026-06-14
|
|
10
16
|
|
|
11
17
|
### Changed
|
package/README.en.md
CHANGED
|
@@ -244,6 +244,8 @@ Use `PWD/p` only if you intentionally run a Windows Node process and need WSL to
|
|
|
244
244
|
| `repo_related_files` | Shows symbols, imports, and importedBy for one file. |
|
|
245
245
|
| `gemini_embedding_probe` | Tests Gemini or proxy compatibility and returns endpoint, latency, error classification, and remediation hints. |
|
|
246
246
|
|
|
247
|
+
`repo_context_pack(mode="hybrid")` and `repo_semantic_search(mode="hybrid")` degrade to keyword-only results when query embedding is unavailable, returning `effectiveMode: "keyword"` and `fallback.reason: "embedding_unavailable"`. `mode="semantic"` does not degrade and returns `status: "embedding_unavailable"` because pure semantic search requires query embedding. Use `rg` / direct file reads for exact strings, known paths, or small targeted checks.
|
|
248
|
+
|
|
247
249
|
## Feature Status
|
|
248
250
|
|
|
249
251
|
Implemented: repo scanning, chunking, SQLite metadata, SQLite FTS5, sqlite-vec, Gemini Embedding 2 provider, semantic/keyword/hybrid search, lightweight symbol/dependency graph, related-file lookup, `repo_context_pack`, provider diagnostics, and index freshness diagnostics.
|
package/README.md
CHANGED
|
@@ -244,6 +244,8 @@ GEMINI_OUTPUT_DIMENSIONALITY = "1536"
|
|
|
244
244
|
| `repo_related_files` | 查看單一檔案的 symbols、imports、importedBy。 |
|
|
245
245
|
| `gemini_embedding_probe` | 測試 Gemini 或 proxy 相容性,回傳 endpoint、latency、錯誤分類與可修復建議。 |
|
|
246
246
|
|
|
247
|
+
`repo_context_pack(mode="hybrid")` 和 `repo_semantic_search(mode="hybrid")` 在 query embedding 不可用時會降級成 keyword-only 結果,並回傳 `effectiveMode: "keyword"` 與 `fallback.reason: "embedding_unavailable"`。`mode="semantic"` 不會降級,會回傳 `status: "embedding_unavailable"`,因為純 semantic search 必須有 query embedding。精確字串、已知路徑或小範圍檢查仍建議直接用 `rg` / 直接讀檔。
|
|
248
|
+
|
|
247
249
|
## 功能狀態
|
|
248
250
|
|
|
249
251
|
已完成:repo 掃描、chunking、SQLite metadata、SQLite FTS5、sqlite-vec、Gemini Embedding 2 provider、semantic/keyword/hybrid search、輕量 symbol/dependency graph、related-file lookup、`repo_context_pack`、provider diagnostics、index freshness diagnostics。
|
package/README.zh-CN.md
CHANGED
|
@@ -244,6 +244,8 @@ GEMINI_OUTPUT_DIMENSIONALITY = "1536"
|
|
|
244
244
|
| `repo_related_files` | 查看单一文件的 symbols、imports、importedBy。 |
|
|
245
245
|
| `gemini_embedding_probe` | 测试 Gemini 或 proxy 兼容性,返回 endpoint、latency、错误分类与可修复建议。 |
|
|
246
246
|
|
|
247
|
+
`repo_context_pack(mode="hybrid")` 和 `repo_semantic_search(mode="hybrid")` 在 query embedding 不可用时会降级成 keyword-only 结果,并返回 `effectiveMode: "keyword"` 与 `fallback.reason: "embedding_unavailable"`。`mode="semantic"` 不会降级,会返回 `status: "embedding_unavailable"`,因为纯 semantic search 必须有 query embedding。精确字符串、已知路径或小范围检查仍建议直接用 `rg` / 直接读文件。
|
|
248
|
+
|
|
247
249
|
## 功能状态
|
|
248
250
|
|
|
249
251
|
已完成:repo 扫描、chunking、SQLite metadata、SQLite FTS5、sqlite-vec、Gemini Embedding 2 provider、semantic/keyword/hybrid search、轻量 symbol/dependency graph、related-file lookup、`repo_context_pack`、provider diagnostics、index freshness diagnostics。
|
package/dist/cli.js
CHANGED
|
@@ -65,3 +65,12 @@ export function searchHybrid(options) {
|
|
|
65
65
|
});
|
|
66
66
|
return mergeHybridResults(semanticResults, keywordResults, options.maxResults);
|
|
67
67
|
}
|
|
68
|
+
export function searchKeywordOnly(options) {
|
|
69
|
+
const keywordResults = searchByKeyword({
|
|
70
|
+
dbPath: options.dbPath,
|
|
71
|
+
query: options.query,
|
|
72
|
+
maxResults: Math.max(options.maxResults * 2, options.maxResults),
|
|
73
|
+
maxSnippetChars: options.maxSnippetChars,
|
|
74
|
+
});
|
|
75
|
+
return mergeHybridResults([], keywordResults, options.maxResults);
|
|
76
|
+
}
|
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { buildContextPack } from "../indexing/contextPack.js";
|
|
5
5
|
import { reindexDryRun } from "../indexing/dryRun.js";
|
|
6
6
|
import { indexMissingEmbeddings } from "../indexing/embeddingWriter.js";
|
|
7
|
-
import { searchHybrid } from "../indexing/hybridSearch.js";
|
|
7
|
+
import { searchHybrid, searchKeywordOnly } from "../indexing/hybridSearch.js";
|
|
8
8
|
import { readDetailedIndexStatus, readIndexFreshness, recommendedNextActions } from "../indexing/indexStatus.js";
|
|
9
9
|
import { persistentReindexMetadata } from "../indexing/indexWriter.js";
|
|
10
10
|
import { readRelatedFileGraph, readRelatedFiles } from "../indexing/relatedFiles.js";
|
|
@@ -35,6 +35,37 @@ function searchIndexedChunks(options) {
|
|
|
35
35
|
maxSnippetChars: options.maxSnippetChars,
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
+
function searchKeywordOnlyChunks(options) {
|
|
39
|
+
return searchKeywordOnly({
|
|
40
|
+
dbPath: options.dbPath,
|
|
41
|
+
query: options.query,
|
|
42
|
+
maxResults: options.maxResults,
|
|
43
|
+
maxSnippetChars: options.maxSnippetChars,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function embeddingFailureDetails(error) {
|
|
47
|
+
const geminiError = error instanceof GeminiEmbeddingError ? error : undefined;
|
|
48
|
+
return {
|
|
49
|
+
type: error instanceof Error ? error.name : "UnknownError",
|
|
50
|
+
message: error instanceof Error ? error.message : String(error),
|
|
51
|
+
httpStatus: geminiError?.status,
|
|
52
|
+
retryable: geminiError?.retryable ?? false,
|
|
53
|
+
bodySnippet: geminiError?.bodySnippet,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function embeddingUnavailablePayload(error) {
|
|
57
|
+
return {
|
|
58
|
+
status: "embedding_unavailable",
|
|
59
|
+
fallbackAvailable: "Use mode=hybrid to allow keyword-only fallback, or use rg/direct file reads for exact strings and known paths.",
|
|
60
|
+
error: embeddingFailureDetails(error),
|
|
61
|
+
recommendedNextActions: [
|
|
62
|
+
"Run gemini_embedding_probe with a short test string.",
|
|
63
|
+
"Verify GEMINI_API_KEY, GEMINI_BASE_URL, GEMINI_AUTH_MODE, and GEMINI_OUTPUT_DIMENSIONALITY.",
|
|
64
|
+
"Use repo_context_pack with mode=hybrid for keyword-only degraded results while embeddings are unavailable.",
|
|
65
|
+
"Use rg/direct file reads for exact strings, known paths, or small targeted checks.",
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
38
69
|
function buildGeminiDiagnostics(config, expectedDimensions) {
|
|
39
70
|
const diagnostics = {
|
|
40
71
|
baseUrl: config.baseUrl,
|
|
@@ -223,20 +254,50 @@ export function registerTools(server, config) {
|
|
|
223
254
|
message: "Run repo_reindex with dry_run=false and index_embeddings=true before semantic search.",
|
|
224
255
|
});
|
|
225
256
|
}
|
|
226
|
-
const queryEmbedding = await embeddingProvider.embed({ kind: "query", text: query });
|
|
227
257
|
const dimensions = expectedDimensions;
|
|
228
|
-
|
|
229
|
-
|
|
258
|
+
let effectiveMode = mode;
|
|
259
|
+
let fallback;
|
|
260
|
+
let rawResults;
|
|
261
|
+
try {
|
|
262
|
+
const queryEmbedding = await embeddingProvider.embed({ kind: "query", text: query });
|
|
263
|
+
if (queryEmbedding.dimensions !== dimensions) {
|
|
264
|
+
throw new Error(`Query embedding dimensions mismatch: expected ${dimensions}, got ${queryEmbedding.dimensions}`);
|
|
265
|
+
}
|
|
266
|
+
rawResults = searchIndexedChunks({
|
|
267
|
+
dbPath,
|
|
268
|
+
query,
|
|
269
|
+
dimensions,
|
|
270
|
+
queryVector: queryEmbedding.vector,
|
|
271
|
+
maxResults: max_results,
|
|
272
|
+
maxSnippetChars: max_snippet_chars,
|
|
273
|
+
mode,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
if (mode === "semantic") {
|
|
278
|
+
return asJsonText({
|
|
279
|
+
query,
|
|
280
|
+
projectPath,
|
|
281
|
+
dbPath,
|
|
282
|
+
dimensions,
|
|
283
|
+
mode,
|
|
284
|
+
...embeddingUnavailablePayload(error),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
effectiveMode = "keyword";
|
|
288
|
+
fallback = {
|
|
289
|
+
reason: "embedding_unavailable",
|
|
290
|
+
fromMode: mode,
|
|
291
|
+
toMode: "keyword",
|
|
292
|
+
error: embeddingFailureDetails(error),
|
|
293
|
+
};
|
|
294
|
+
rawResults = searchKeywordOnlyChunks({
|
|
295
|
+
dbPath,
|
|
296
|
+
query,
|
|
297
|
+
maxResults: max_results,
|
|
298
|
+
maxSnippetChars: max_snippet_chars,
|
|
299
|
+
});
|
|
230
300
|
}
|
|
231
|
-
const rawResults = searchIndexedChunks({
|
|
232
|
-
dbPath,
|
|
233
|
-
query,
|
|
234
|
-
dimensions,
|
|
235
|
-
queryVector: queryEmbedding.vector,
|
|
236
|
-
maxResults: max_results,
|
|
237
|
-
maxSnippetChars: max_snippet_chars,
|
|
238
|
-
mode,
|
|
239
|
-
});
|
|
240
301
|
const formatted = formatSearchResults(query, rawResults, { maxContextChars: max_context_chars });
|
|
241
302
|
return asJsonText({
|
|
242
303
|
query,
|
|
@@ -244,6 +305,8 @@ export function registerTools(server, config) {
|
|
|
244
305
|
dbPath,
|
|
245
306
|
dimensions,
|
|
246
307
|
mode,
|
|
308
|
+
effectiveMode,
|
|
309
|
+
fallback,
|
|
247
310
|
results: formatted.results,
|
|
248
311
|
context: formatted.summary,
|
|
249
312
|
resultCount: rawResults.length,
|
|
@@ -308,20 +371,50 @@ export function registerTools(server, config) {
|
|
|
308
371
|
message: "Run repo_reindex with dry_run=false and index_embeddings=true before building a context pack.",
|
|
309
372
|
});
|
|
310
373
|
}
|
|
311
|
-
const queryEmbedding = await embeddingProvider.embed({ kind: "query", text: query });
|
|
312
374
|
const dimensions = expectedDimensions;
|
|
313
|
-
|
|
314
|
-
|
|
375
|
+
let effectiveMode = mode;
|
|
376
|
+
let fallback;
|
|
377
|
+
let rawResults;
|
|
378
|
+
try {
|
|
379
|
+
const queryEmbedding = await embeddingProvider.embed({ kind: "query", text: query });
|
|
380
|
+
if (queryEmbedding.dimensions !== dimensions) {
|
|
381
|
+
throw new Error(`Query embedding dimensions mismatch: expected ${dimensions}, got ${queryEmbedding.dimensions}`);
|
|
382
|
+
}
|
|
383
|
+
rawResults = searchIndexedChunks({
|
|
384
|
+
dbPath,
|
|
385
|
+
query,
|
|
386
|
+
dimensions,
|
|
387
|
+
queryVector: queryEmbedding.vector,
|
|
388
|
+
maxResults: max_results,
|
|
389
|
+
maxSnippetChars: max_snippet_chars,
|
|
390
|
+
mode,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
if (mode === "semantic") {
|
|
395
|
+
return asJsonText({
|
|
396
|
+
query,
|
|
397
|
+
projectPath,
|
|
398
|
+
dbPath,
|
|
399
|
+
dimensions,
|
|
400
|
+
mode,
|
|
401
|
+
...embeddingUnavailablePayload(error),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
effectiveMode = "keyword";
|
|
405
|
+
fallback = {
|
|
406
|
+
reason: "embedding_unavailable",
|
|
407
|
+
fromMode: mode,
|
|
408
|
+
toMode: "keyword",
|
|
409
|
+
error: embeddingFailureDetails(error),
|
|
410
|
+
};
|
|
411
|
+
rawResults = searchKeywordOnlyChunks({
|
|
412
|
+
dbPath,
|
|
413
|
+
query,
|
|
414
|
+
maxResults: max_results,
|
|
415
|
+
maxSnippetChars: max_snippet_chars,
|
|
416
|
+
});
|
|
315
417
|
}
|
|
316
|
-
const rawResults = searchIndexedChunks({
|
|
317
|
-
dbPath,
|
|
318
|
-
query,
|
|
319
|
-
dimensions,
|
|
320
|
-
queryVector: queryEmbedding.vector,
|
|
321
|
-
maxResults: max_results,
|
|
322
|
-
maxSnippetChars: max_snippet_chars,
|
|
323
|
-
mode,
|
|
324
|
-
});
|
|
325
418
|
const relatedPaths = Array.from(new Set(rawResults.map((result) => result.path))).slice(0, Math.min(max_seed_files, max_related_files));
|
|
326
419
|
const relatedFiles = readRelatedFileGraph({
|
|
327
420
|
dbPath,
|
|
@@ -355,6 +448,8 @@ export function registerTools(server, config) {
|
|
|
355
448
|
dbPath,
|
|
356
449
|
dimensions,
|
|
357
450
|
mode,
|
|
451
|
+
effectiveMode,
|
|
452
|
+
fallback,
|
|
358
453
|
relatedDepth: related_depth,
|
|
359
454
|
relatedSeedCount: relatedPaths.length,
|
|
360
455
|
includeRelatedSnippets: include_related_snippets,
|