sift-light 1.0.0 → 1.0.1
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 +19 -0
- package/README.md +8 -1
- package/README.zh-CN.md +8 -1
- package/package.json +1 -1
- package/plugins/sift-light/.claude-plugin/plugin.json +1 -1
- package/plugins/sift-light/.codex-plugin/plugin.json +1 -1
- package/plugins/sift-light/.mcp.json +1 -1
- package/plugins/sift-light/concept-worker.mjs +39 -12
- package/plugins/sift-light/kimi.plugin.json +2 -2
- package/plugins/sift-light/omp-extension.mjs +68 -28
- package/plugins/sift-light/package.json +1 -1
- package/src/concept-inference.ts +44 -11
- package/src/concept-model.ts +2 -2
- package/src/concept-search.ts +12 -2
- package/src/concept-worker.mjs +39 -12
- package/src/concept-worker.ts +1 -1
- package/src/config-reader.ts +11 -3
- package/src/hybrid-search.ts +13 -0
- package/src/index.ts +4 -10
- package/src/mcp-model-output.ts +41 -0
- package/src/mcp-semantic-judge.ts +19 -8
- package/src/mcp-server.mjs +262 -191
- package/src/mcp-server.ts +11 -13
- package/src/mcp.ts +2 -0
- package/src/model-error.ts +6 -16
- package/src/omp-index.ts +4 -6
- package/src/prompt-guidelines.ts +5 -5
- package/src/request-contract.ts +41 -5
- package/src/semantic-judge.ts +17 -3
- package/src/service.ts +8 -0
- package/src/tool-schema.ts +3 -3
package/src/mcp-server.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { URL as URL2 } from "node:url";
|
|
|
7
7
|
// package.json
|
|
8
8
|
var package_default = {
|
|
9
9
|
name: "sift-light",
|
|
10
|
-
version: "1.0.
|
|
10
|
+
version: "1.0.1",
|
|
11
11
|
description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
|
|
12
12
|
keywords: [
|
|
13
13
|
"ai-agent",
|
|
@@ -292,7 +292,33 @@ function analysisExtraGroups(counts, termCounts, items) {
|
|
|
292
292
|
function isRecord(value) {
|
|
293
293
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
294
294
|
}
|
|
295
|
+
function compactSemanticMetadata(details, analysis) {
|
|
296
|
+
const counts = analysis.counts;
|
|
297
|
+
const stats = analysis.stats;
|
|
298
|
+
const judge = analysis.semanticJudge;
|
|
299
|
+
const coverage = analysis.coverage;
|
|
300
|
+
return [
|
|
301
|
+
...counts || stats ? [
|
|
302
|
+
`Search: ${String(counts?.filesAdmitted ?? stats?.filesAdmitted ?? 0)} files, ${String(stats?.passagesRanked ?? counts?.passagesQueued ?? 0)} passages; ${String(stats?.elapsedMs ?? 0)} ms; peak inference RSS ${String(stats?.inferencePeakRssBytes ?? 0)} bytes; cache ${String(stats?.conceptCacheHits ?? 0)} hits/${String(stats?.conceptCacheMisses ?? 0)} misses.`
|
|
303
|
+
] : [],
|
|
304
|
+
...coverage ? [
|
|
305
|
+
`Coverage: ${Object.entries(coverage).map(([name, status]) => `${name}=${status}`).join(", ")}.`
|
|
306
|
+
] : [],
|
|
307
|
+
...analysis.scope ? [`Scope: ${analysis.scope.path}; ignore=${analysis.scope.ignorePolicy}.`] : [],
|
|
308
|
+
...analysis.sourceGeneration ? [
|
|
309
|
+
`Source: ${analysis.sourceGeneration.verification}; ${String(analysis.sourceGeneration.filesUnavailable)} unavailable.`
|
|
310
|
+
] : [],
|
|
311
|
+
...judge ? [
|
|
312
|
+
`Semantic judge: ${judge.status}; ${String(judge.judgedCandidates)}/${String(judge.candidatesConsidered)} judged, ${String(judge.candidatesUnjudged)} unjudged; batches ${String(judge.batchesCompleted)}/${String(judge.batchesAttempted)} completed; classes ${JSON.stringify(judge.classificationCounts)}${judge.reason ? `; ${judge.reason}` : ""}.`
|
|
313
|
+
] : [],
|
|
314
|
+
...details.operation ? [`Operation: ${details.operation.state}; id=${details.operation.id}.`] : [],
|
|
315
|
+
...analysis.reasons.map((reason) => `[${reason}]`),
|
|
316
|
+
...details.redactionApplied ? ["[Display redaction applied.]"] : []
|
|
317
|
+
];
|
|
318
|
+
}
|
|
295
319
|
function compactMetadata(details, analysis) {
|
|
320
|
+
if (analysis.kind === "concept" || analysis.kind === "hybrid")
|
|
321
|
+
return compactSemanticMetadata(details, analysis);
|
|
296
322
|
return [
|
|
297
323
|
...analysis.statistics ? formatStatistics(analysis.statistics) : [],
|
|
298
324
|
analysis.counts ? `Counts: ${JSON.stringify(analysis.counts)}` : undefined,
|
|
@@ -996,15 +1022,23 @@ function schemaError(input, field, reason) {
|
|
|
996
1022
|
recovery: { action: "manual", reason }
|
|
997
1023
|
}, `${field} is invalid: ${reason}`);
|
|
998
1024
|
}
|
|
1025
|
+
function plainFilePatternRecovery(mode, input, invalid) {
|
|
1026
|
+
return mode === "files" && invalid.includes("pattern") && input.query === undefined && typeof input.pattern === "string" && input.pattern.trim().length > 0 && input.pattern.length <= 256 && input.pattern.isWellFormed() && !/[\\^$.*+?()[\]{}|\r\n\0]/u.test(input.pattern);
|
|
1027
|
+
}
|
|
999
1028
|
function safeNextRequest(input, mode, invalid) {
|
|
1000
1029
|
if (input.redact === true && containsSensitiveText(input))
|
|
1001
1030
|
return;
|
|
1002
1031
|
const safe = new Set(SAFE_DROP_FIELDS[mode] ?? []);
|
|
1032
|
+
const plainFilePattern = plainFilePatternRecovery(mode, input, invalid);
|
|
1033
|
+
if (plainFilePattern)
|
|
1034
|
+
safe.add("pattern");
|
|
1003
1035
|
if (invalid.some((field) => !safe.has(field)))
|
|
1004
1036
|
return;
|
|
1005
1037
|
const next = { ...input };
|
|
1006
1038
|
for (const field of invalid)
|
|
1007
1039
|
delete next[field];
|
|
1040
|
+
if (plainFilePattern)
|
|
1041
|
+
next.query = input.pattern;
|
|
1008
1042
|
if (selectorIssuesFor(next, mode).length > 0)
|
|
1009
1043
|
return;
|
|
1010
1044
|
try {
|
|
@@ -1033,20 +1067,26 @@ function fieldsError(input, mode, invalid, selectorIssues = []) {
|
|
|
1033
1067
|
});
|
|
1034
1068
|
const visibleFields = visibleInvalid.map((field) => boundedField(field)).join(", ");
|
|
1035
1069
|
const reason = omitted > 0 ? `mode=${mode} does not accept ${visibleFields} and ${String(omitted)} additional field(s)` : `mode=${mode} does not accept ${visibleFields}`;
|
|
1036
|
-
const
|
|
1070
|
+
const nestedModeObject = invalid.includes(mode);
|
|
1071
|
+
const flatRule = nestedModeObject ? `Fields are flat: pass them at the top level, not inside a per-mode object. mode=${mode} accepts: ${modeFields(mode).join(", ")}.` : "";
|
|
1072
|
+
const filesPatternRule = mode === "files" && invalid.includes("pattern") ? "For filename or path discovery, put the literal name text in query; pattern is a content-search regex, so check any regex syntax before copying it." : "";
|
|
1037
1073
|
return new RequestContractError({
|
|
1038
1074
|
code: "E_MODE_FIELDS",
|
|
1039
1075
|
mode,
|
|
1040
1076
|
issues,
|
|
1041
1077
|
recovery: nextRequest ? {
|
|
1042
1078
|
action: "retry",
|
|
1043
|
-
reason: `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
|
|
1079
|
+
reason: plainFilePatternRecovery(mode, input, invalid) ? "For filename discovery, move the plain text from pattern to query and copy nextRequest; all filters are preserved." : `Remove only ${visibleFields} and copy the exact nextRequest; all other fields are preserved.`,
|
|
1044
1080
|
nextRequest
|
|
1045
1081
|
} : {
|
|
1046
1082
|
action: "manual",
|
|
1047
|
-
reason:
|
|
1083
|
+
reason: [
|
|
1084
|
+
reason,
|
|
1085
|
+
filesPatternRule || "Choose the mode explicitly or remove unsupported fields without changing the requested scope.",
|
|
1086
|
+
flatRule
|
|
1087
|
+
].filter(Boolean).join(" ")
|
|
1048
1088
|
}
|
|
1049
|
-
}, nextRequest ? `${reason};
|
|
1089
|
+
}, nextRequest ? `${reason}; ${plainFilePatternRecovery(mode, input, invalid) ? "move plain filename text to query" : "retry the exact nextRequest"}.` : `${reason}; no semantics-preserving automatic request is available. ${filesPatternRule || flatRule || "Check the mode's accepted fields."}`);
|
|
1050
1090
|
}
|
|
1051
1091
|
function selectorIssuesFor(input, mode) {
|
|
1052
1092
|
if ((mode === "auto" || mode === "summary" || mode === "matches") && typeof input.cursor === "string" && input.cursor.trim().length > 0 && !input.cursor.includes(".analysis")) {
|
|
@@ -1282,25 +1322,23 @@ function modelErrorText(error) {
|
|
|
1282
1322
|
function requestContractErrorText(error) {
|
|
1283
1323
|
return requestContractProjection(error).text;
|
|
1284
1324
|
}
|
|
1285
|
-
function projectRequestContract(projected, serialized
|
|
1286
|
-
const prefix = `sift-light failed: request rejected [${projected.code}]: ${message}`;
|
|
1325
|
+
function projectRequestContract(projected, serialized) {
|
|
1287
1326
|
const recovery = projected.recovery;
|
|
1288
1327
|
const next = recovery.nextRequest ? `
|
|
1289
1328
|
Copy the nested recovery.nextRequest object unchanged; do not repeat the original query.` : `
|
|
1290
|
-
Recovery action: ${recovery.action}
|
|
1329
|
+
Recovery action: ${recovery.action}.`;
|
|
1291
1330
|
return `
|
|
1292
|
-
|
|
1293
|
-
${
|
|
1331
|
+
sift-light failed: request rejected [${projected.code}].
|
|
1332
|
+
Error details: ${serialized}${next}`;
|
|
1294
1333
|
}
|
|
1295
1334
|
function requestContractProjection(error) {
|
|
1296
1335
|
const details = boundedRequestContractDetails(error.details);
|
|
1297
1336
|
const serializedDetails = JSON.stringify(details);
|
|
1298
|
-
const
|
|
1299
|
-
const result = projectRequestContract(details, serializedDetails, boundedMessage);
|
|
1337
|
+
const result = projectRequestContract(details, serializedDetails);
|
|
1300
1338
|
if (Buffer.byteLength(result) <= MAX_REQUEST_RECOVERY_BYTES)
|
|
1301
1339
|
return { details, text: result };
|
|
1302
1340
|
const compact = {
|
|
1303
|
-
code:
|
|
1341
|
+
code: details.code,
|
|
1304
1342
|
...details.mode ? { mode: details.mode } : {},
|
|
1305
1343
|
issues: [
|
|
1306
1344
|
{
|
|
@@ -1313,7 +1351,7 @@ function requestContractProjection(error) {
|
|
|
1313
1351
|
reason: "Exact recovery was omitted; correct the request explicitly."
|
|
1314
1352
|
}
|
|
1315
1353
|
};
|
|
1316
|
-
const compactText = projectRequestContract(compact, JSON.stringify(compact)
|
|
1354
|
+
const compactText = projectRequestContract(compact, JSON.stringify(compact));
|
|
1317
1355
|
return { details: compact, text: compactText };
|
|
1318
1356
|
}
|
|
1319
1357
|
|
|
@@ -2819,7 +2857,7 @@ var CONCEPT_MODEL = "Xenova/multilingual-e5-small";
|
|
|
2819
2857
|
var CONCEPT_REVISION = "761b726dd34fb83930e26aab4e9ac3899aa1fa78";
|
|
2820
2858
|
var MAX_CONCEPT_CHARS = 1000;
|
|
2821
2859
|
var CONCEPT_PASSAGE_OVERLAP_CHARS = 160;
|
|
2822
|
-
var CONCEPT_CACHE_VERSION =
|
|
2860
|
+
var CONCEPT_CACHE_VERSION = 2;
|
|
2823
2861
|
var CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
|
|
2824
2862
|
var CONCEPT_TIMEOUT_MS = 10 * 60000;
|
|
2825
2863
|
var MIN_CONCEPT_TIMEOUT_MS = 1000;
|
|
@@ -5142,6 +5180,11 @@ function scoreProfile(scores) {
|
|
|
5142
5180
|
spread: top - min
|
|
5143
5181
|
};
|
|
5144
5182
|
}
|
|
5183
|
+
function conceptRankingScore(cosine, passageLength) {
|
|
5184
|
+
const referenceLength = 500;
|
|
5185
|
+
const maximumCorrection = 0.02;
|
|
5186
|
+
return cosine - maximumCorrection * (1 - Math.sqrt(Math.min(passageLength / referenceLength, 1)));
|
|
5187
|
+
}
|
|
5145
5188
|
function passage(document, start) {
|
|
5146
5189
|
let end = Math.min(document.text.length, start + MAX_CONCEPT_CHARS);
|
|
5147
5190
|
if (end < document.text.length) {
|
|
@@ -5400,6 +5443,7 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
|
|
|
5400
5443
|
const similarity = inferred.scores[index];
|
|
5401
5444
|
if (similarity === undefined)
|
|
5402
5445
|
throw new Error("Missing concept similarity");
|
|
5446
|
+
const rankingScore = conceptRankingScore(similarity, item.text.length);
|
|
5403
5447
|
const evidence = rangeEvidence(item.document, item.range);
|
|
5404
5448
|
return {
|
|
5405
5449
|
path: item.document.path,
|
|
@@ -5412,7 +5456,8 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
|
|
|
5412
5456
|
kind: "concept-candidate",
|
|
5413
5457
|
certainty: "candidate",
|
|
5414
5458
|
score: similarity,
|
|
5415
|
-
|
|
5459
|
+
rankingScore,
|
|
5460
|
+
rankingReason: "local multilingual E5 cosine similarity with bounded short-passage rank correction; relevance candidate, no binding or execution claim",
|
|
5416
5461
|
model: CONCEPT_MODEL,
|
|
5417
5462
|
revision: CONCEPT_REVISION,
|
|
5418
5463
|
tokenTruncated: false,
|
|
@@ -5421,7 +5466,7 @@ async function runConceptSearch(input, access, infer, onProgress, options = {})
|
|
|
5421
5466
|
}
|
|
5422
5467
|
};
|
|
5423
5468
|
});
|
|
5424
|
-
const ranked = [...result.items, ...batchItems].toSorted((a, b) => Number(b.details?.
|
|
5469
|
+
const ranked = [...result.items, ...batchItems].toSorted((a, b) => Number(b.details?.rankingScore) - Number(a.details?.rankingScore) || a.path.localeCompare(b.path) || a.line - b.line);
|
|
5425
5470
|
if (ranked.length > MAX_ANALYSIS_RESULTS)
|
|
5426
5471
|
retentionTruncated = true;
|
|
5427
5472
|
result.items = ranked.slice(0, MAX_ANALYSIS_RESULTS);
|
|
@@ -9517,6 +9562,160 @@ function parsePythonOutline(document) {
|
|
|
9517
9562
|
// src/hybrid-search.ts
|
|
9518
9563
|
import { resolve as resolve20 } from "node:path";
|
|
9519
9564
|
|
|
9565
|
+
// src/config-reader.ts
|
|
9566
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
9567
|
+
var SIFT_LIGHT_CONFIG_ENV = "SIFT_LIGHT_CONFIG";
|
|
9568
|
+
var SEMANTIC_JUDGE_API_KEY_ENVS = ["TYPESAFE_API_KEY", "SIFT_LIGHT_JEV_API_KEY"];
|
|
9569
|
+
var DEFAULT_SEMANTIC_JUDGE_CONFIG = {
|
|
9570
|
+
enabled: false,
|
|
9571
|
+
provider: "jev",
|
|
9572
|
+
endpoint: "https://api.typesafe.ai/v1/systemone",
|
|
9573
|
+
apiKeyEnv: SEMANTIC_JUDGE_API_KEY_ENVS[0],
|
|
9574
|
+
model: "jev-latest",
|
|
9575
|
+
timeoutMs: 120000,
|
|
9576
|
+
maxCandidates: 20,
|
|
9577
|
+
maxRetries: 2
|
|
9578
|
+
};
|
|
9579
|
+
var DEFAULT_SIFT_LIGHT_CONFIG = {
|
|
9580
|
+
locale: "en",
|
|
9581
|
+
enforceSearch: "hard",
|
|
9582
|
+
vectorSearchEnabled: false,
|
|
9583
|
+
semanticJudge: DEFAULT_SEMANTIC_JUDGE_CONFIG
|
|
9584
|
+
};
|
|
9585
|
+
function hasErrorCode(error, codes) {
|
|
9586
|
+
return error instanceof Error && "code" in error && codes.includes(String(error.code));
|
|
9587
|
+
}
|
|
9588
|
+
function isMissingFile(error) {
|
|
9589
|
+
return hasErrorCode(error, ["ENOENT"]);
|
|
9590
|
+
}
|
|
9591
|
+
function isRawSiftLightConfig(value) {
|
|
9592
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9593
|
+
}
|
|
9594
|
+
function isRawSemanticJudgeConfig(value) {
|
|
9595
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9596
|
+
}
|
|
9597
|
+
function boundedInteger2(value, fallback, minimum, maximum, field) {
|
|
9598
|
+
const candidate = value ?? fallback;
|
|
9599
|
+
if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < minimum || candidate > maximum) {
|
|
9600
|
+
throw new Error(`Invalid sift-light ${field}: expected an integer from ${String(minimum)} through ${String(maximum)}`);
|
|
9601
|
+
}
|
|
9602
|
+
return candidate;
|
|
9603
|
+
}
|
|
9604
|
+
function parseSemanticJudge(value, path) {
|
|
9605
|
+
if (value === undefined)
|
|
9606
|
+
return { ...DEFAULT_SEMANTIC_JUDGE_CONFIG };
|
|
9607
|
+
if (!isRawSemanticJudgeConfig(value)) {
|
|
9608
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge must be an object`);
|
|
9609
|
+
}
|
|
9610
|
+
const unknown = Object.keys(value).filter((key) => ![
|
|
9611
|
+
"enabled",
|
|
9612
|
+
"provider",
|
|
9613
|
+
"endpoint",
|
|
9614
|
+
"apiKeyEnv",
|
|
9615
|
+
"model",
|
|
9616
|
+
"timeoutMs",
|
|
9617
|
+
"maxCandidates",
|
|
9618
|
+
"maxRetries"
|
|
9619
|
+
].includes(key));
|
|
9620
|
+
if (unknown.length > 0) {
|
|
9621
|
+
throw new Error(`Invalid sift-light config at ${path}: unsupported semanticJudge fields; accepted fields are enabled, provider, endpoint, apiKeyEnv, model, timeoutMs, maxCandidates and maxRetries`);
|
|
9622
|
+
}
|
|
9623
|
+
const enabled = value.enabled ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.enabled;
|
|
9624
|
+
if (typeof enabled !== "boolean") {
|
|
9625
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.enabled must be a boolean`);
|
|
9626
|
+
}
|
|
9627
|
+
const provider = value.provider ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.provider;
|
|
9628
|
+
if (provider !== "jev") {
|
|
9629
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.provider must be "jev"`);
|
|
9630
|
+
}
|
|
9631
|
+
const endpoint = value.endpoint ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.endpoint;
|
|
9632
|
+
if (typeof endpoint !== "string" || endpoint.length === 0 || endpoint.length > 2048) {
|
|
9633
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a nonempty URL`);
|
|
9634
|
+
}
|
|
9635
|
+
let parsedEndpoint;
|
|
9636
|
+
try {
|
|
9637
|
+
parsedEndpoint = new URL(endpoint);
|
|
9638
|
+
} catch (error) {
|
|
9639
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a URL`, {
|
|
9640
|
+
cause: error
|
|
9641
|
+
});
|
|
9642
|
+
}
|
|
9643
|
+
const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
9644
|
+
const secureEndpoint = parsedEndpoint.protocol === "https:" || parsedEndpoint.protocol === "http:" && loopbackHosts.has(parsedEndpoint.hostname);
|
|
9645
|
+
if (!secureEndpoint || parsedEndpoint.username || parsedEndpoint.password) {
|
|
9646
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must use HTTPS, except that HTTP is allowed for localhost loopback development; URL credentials are not allowed`);
|
|
9647
|
+
}
|
|
9648
|
+
const apiKeyEnv = value.apiKeyEnv ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.apiKeyEnv;
|
|
9649
|
+
if (typeof apiKeyEnv !== "string" || !/^[A-Z][A-Z0-9_]{0,127}$/u.test(apiKeyEnv)) {
|
|
9650
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.apiKeyEnv must be an uppercase environment variable name`);
|
|
9651
|
+
}
|
|
9652
|
+
const model = value.model ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.model;
|
|
9653
|
+
if (typeof model !== "string" || model.length === 0 || model.length > 128 || /[\r\n\0]/u.test(model)) {
|
|
9654
|
+
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.model must be bounded single-line text`);
|
|
9655
|
+
}
|
|
9656
|
+
return {
|
|
9657
|
+
enabled,
|
|
9658
|
+
provider,
|
|
9659
|
+
endpoint,
|
|
9660
|
+
apiKeyEnv,
|
|
9661
|
+
model,
|
|
9662
|
+
timeoutMs: boundedInteger2(value.timeoutMs, DEFAULT_SEMANTIC_JUDGE_CONFIG.timeoutMs, 1000, 1200000, `config at ${path}: semanticJudge.timeoutMs`),
|
|
9663
|
+
maxCandidates: boundedInteger2(value.maxCandidates, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxCandidates, 1, 20, `config at ${path}: semanticJudge.maxCandidates`),
|
|
9664
|
+
maxRetries: boundedInteger2(value.maxRetries, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxRetries, 0, 5, `config at ${path}: semanticJudge.maxRetries`)
|
|
9665
|
+
};
|
|
9666
|
+
}
|
|
9667
|
+
function parseConfig(value, path) {
|
|
9668
|
+
if (!isRawSiftLightConfig(value)) {
|
|
9669
|
+
throw new Error(`Invalid sift-light config at ${path}: expected a JSON object`);
|
|
9670
|
+
}
|
|
9671
|
+
const unknown = Object.keys(value).filter((key) => !["locale", "enforceSearch", "vectorSearchEnabled", "semanticJudge"].includes(key));
|
|
9672
|
+
if (unknown.length > 0) {
|
|
9673
|
+
throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch, vectorSearchEnabled and semanticJudge are accepted`);
|
|
9674
|
+
}
|
|
9675
|
+
const { locale, enforceSearch, vectorSearchEnabled } = value;
|
|
9676
|
+
if (locale !== undefined && locale !== "en" && locale !== "zh-CN") {
|
|
9677
|
+
throw new Error(`Invalid sift-light config at ${path}: locale must be "en" or "zh-CN"`);
|
|
9678
|
+
}
|
|
9679
|
+
if (vectorSearchEnabled !== undefined && typeof vectorSearchEnabled !== "boolean") {
|
|
9680
|
+
throw new Error(`Invalid sift-light config at ${path}: vectorSearchEnabled must be a boolean`);
|
|
9681
|
+
}
|
|
9682
|
+
const enforcement = normalizeSearchEnforcement(enforceSearch, `config at ${path}`);
|
|
9683
|
+
return {
|
|
9684
|
+
locale: locale ?? DEFAULT_SIFT_LIGHT_CONFIG.locale,
|
|
9685
|
+
enforceSearch: enforcement,
|
|
9686
|
+
vectorSearchEnabled: vectorSearchEnabled ?? DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled ?? false,
|
|
9687
|
+
semanticJudge: parseSemanticJudge(value.semanticJudge, path)
|
|
9688
|
+
};
|
|
9689
|
+
}
|
|
9690
|
+
function normalizeSearchEnforcement(value, source) {
|
|
9691
|
+
if (value === undefined || value === "hard")
|
|
9692
|
+
return "hard";
|
|
9693
|
+
if (value === "prefer")
|
|
9694
|
+
return "prefer";
|
|
9695
|
+
if (value === "off")
|
|
9696
|
+
return "off";
|
|
9697
|
+
throw new Error(`Invalid sift-light ${source}: enforceSearch must be "hard", "prefer", or "off"`);
|
|
9698
|
+
}
|
|
9699
|
+
async function readSiftLightConfigFile(path, options = {}) {
|
|
9700
|
+
try {
|
|
9701
|
+
const content = await readFile2(path, "utf8");
|
|
9702
|
+
return parseConfig(JSON.parse(content), path);
|
|
9703
|
+
} catch (error) {
|
|
9704
|
+
if (isMissingFile(error)) {
|
|
9705
|
+
if (options.missing === "error") {
|
|
9706
|
+
throw new Error(`sift-light config was not found at ${path}; create it or unset ${SIFT_LIGHT_CONFIG_ENV}`, { cause: error });
|
|
9707
|
+
}
|
|
9708
|
+
return { ...DEFAULT_SIFT_LIGHT_CONFIG };
|
|
9709
|
+
}
|
|
9710
|
+
if (error instanceof SyntaxError) {
|
|
9711
|
+
throw new Error(`Invalid sift-light config at ${path}: ${error.message}`, {
|
|
9712
|
+
cause: error
|
|
9713
|
+
});
|
|
9714
|
+
}
|
|
9715
|
+
throw error;
|
|
9716
|
+
}
|
|
9717
|
+
}
|
|
9718
|
+
|
|
9520
9719
|
// src/semantic-judge-batches.ts
|
|
9521
9720
|
var MAX_SEMANTIC_JUDGE_BATCH_CANDIDATES = 8;
|
|
9522
9721
|
var MAX_SEMANTIC_JUDGE_REQUEST_BYTES = 64 * 1024;
|
|
@@ -9664,7 +9863,7 @@ function requestBody(query, candidates, model) {
|
|
|
9664
9863
|
for (const candidate of candidates) {
|
|
9665
9864
|
questions[candidate.id] = {
|
|
9666
9865
|
type: "choice",
|
|
9667
|
-
instructions:
|
|
9866
|
+
instructions: `Classify only candidate ${candidate.id} in state.candidates for state.query. Judge that candidate's excerpt by its actual behavior, not by matching words or the other candidates.`,
|
|
9668
9867
|
criteria: {
|
|
9669
9868
|
"implementation-candidate": "The excerpt appears to implement the requested behavior or its core decision/side effect.",
|
|
9670
9869
|
"caller-candidate": "The excerpt invokes or wires an implementation but does not implement the behavior itself.",
|
|
@@ -9827,6 +10026,10 @@ function createSemanticJudgeIntegration(config, environment = process.env, fetch
|
|
|
9827
10026
|
}
|
|
9828
10027
|
return { config, runner: createJevRunner(config, key, fetcher) };
|
|
9829
10028
|
}
|
|
10029
|
+
function createConfiguredSemanticJudgeIntegration(config, environment = process.env) {
|
|
10030
|
+
const judge = config.semanticJudge ?? DEFAULT_SEMANTIC_JUDGE_CONFIG;
|
|
10031
|
+
return config.vectorSearchEnabled === true ? createSemanticJudgeIntegration(judge, environment) : createDisabledSemanticJudgeIntegration(judge);
|
|
10032
|
+
}
|
|
9830
10033
|
function baseDetails(config) {
|
|
9831
10034
|
return {
|
|
9832
10035
|
enabled: config.enabled,
|
|
@@ -10133,6 +10336,9 @@ async function combineHybridSearch(scan, execution, access, conceptLimit, query,
|
|
|
10133
10336
|
const deduplicationCoverage = scan.snapshotComplete && literal.sourceCoverage === "complete" && conceptSourceCoverage === "complete" ? "complete" : "partial";
|
|
10134
10337
|
const partial = !scan.snapshotComplete || concept.partial || conceptCoverage === "skipped" || conceptSourceCoverage === "partial" || literal.sourceCoverage === "partial" || deduplicationCoverage === "partial" || judgedConcept.semanticJudge?.status === "failed" || judgedConcept.semanticJudge?.status === "partial";
|
|
10135
10338
|
const selectionReason = conceptCandidatesOmitted ? `Hybrid concept limit retained the top ${String(selectedConcept.length)} of ${String(eligibleConcept.length)} non-overlapping semantic candidates` : undefined;
|
|
10339
|
+
const firstScore = Number(eligibleConcept[0]?.details?.rankingScore ?? eligibleConcept[0]?.details?.score);
|
|
10340
|
+
const secondScore = Number(eligibleConcept[1]?.details?.rankingScore ?? eligibleConcept[1]?.details?.score);
|
|
10341
|
+
const closeRanking = Number.isFinite(firstScore) && Number.isFinite(secondScore) && firstScore - secondScore < 0.01;
|
|
10136
10342
|
return {
|
|
10137
10343
|
kind: "hybrid",
|
|
10138
10344
|
unit: "evidence-items",
|
|
@@ -10144,6 +10350,9 @@ async function combineHybridSearch(scan, execution, access, conceptLimit, query,
|
|
|
10144
10350
|
...literal.reasons,
|
|
10145
10351
|
...execution.sourceGeneration.reasons,
|
|
10146
10352
|
...selectionReason ? [selectionReason] : [],
|
|
10353
|
+
...closeRanking ? [
|
|
10354
|
+
"Semantic ranks are close; verify the leading candidates with source inspection or literal terms."
|
|
10355
|
+
] : [],
|
|
10147
10356
|
...judgedConcept.semanticJudge?.reason ? [judgedConcept.semanticJudge.reason] : []
|
|
10148
10357
|
],
|
|
10149
10358
|
filesRead: (concept.filesRead ?? 0) + access.filesRead,
|
|
@@ -11036,7 +11245,7 @@ import { resolve as resolve24 } from "node:path";
|
|
|
11036
11245
|
var DISCOVERY_MODE_REQUIRED_ERROR = 'query requires an explicit discovery mode: use mode=files for filename/path discovery or mode=concept for semantic discovery; for example {"mode":"files","query":"<filename-or-path>"}';
|
|
11037
11246
|
|
|
11038
11247
|
// src/format.ts
|
|
11039
|
-
import { readFile as
|
|
11248
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
11040
11249
|
var RESULT_METADATA_RESERVE_BYTES = 1024;
|
|
11041
11250
|
var RESULT_METADATA_RESERVE_CHARACTERS = 512;
|
|
11042
11251
|
|
|
@@ -11170,7 +11379,7 @@ async function loadContextLines(match, expectedRevision, cache, signal) {
|
|
|
11170
11379
|
cache.set(match.absolutePath, changed);
|
|
11171
11380
|
return changed;
|
|
11172
11381
|
}
|
|
11173
|
-
const content = await
|
|
11382
|
+
const content = await readFile3(match.absolutePath, { encoding: "utf8", signal });
|
|
11174
11383
|
const afterRevision = await getSourceRevision(match.absolutePath);
|
|
11175
11384
|
if (!afterRevision || !sameSourceRevision(expectedRevision, afterRevision)) {
|
|
11176
11385
|
const changed = { status: "changed" };
|
|
@@ -12601,6 +12810,7 @@ class SiftLightService {
|
|
|
12601
12810
|
#runRipgrep;
|
|
12602
12811
|
#snapshots;
|
|
12603
12812
|
#summaryFileLimit;
|
|
12813
|
+
#vectorSearchEnabled;
|
|
12604
12814
|
#capabilities = new LanguageCapabilityCatalog;
|
|
12605
12815
|
#evidence;
|
|
12606
12816
|
#lifecycle = new AbortController;
|
|
@@ -12611,12 +12821,16 @@ class SiftLightService {
|
|
|
12611
12821
|
this.#runRipgrep = options.runRipgrep;
|
|
12612
12822
|
this.#snapshots = options.snapshots ?? new SnapshotStore;
|
|
12613
12823
|
this.#summaryFileLimit = options.summaryFileLimit ?? DEFAULT_SUMMARY_FILE_LIMIT;
|
|
12824
|
+
this.#vectorSearchEnabled = options.vectorSearchEnabled ?? options.conceptSearch !== undefined;
|
|
12614
12825
|
this.#operations = new OperationLifecycle({ deadlineMs: resolveConceptTimeoutMs() });
|
|
12615
12826
|
this.#evidence = new EvidenceService(this.#runRipgrep, this.#snapshots, options.structure, options.conceptSearch, options.semanticJudge);
|
|
12616
12827
|
}
|
|
12617
12828
|
async search(input, cwd, signal, options = {}) {
|
|
12618
12829
|
validateRawSearchInput(input);
|
|
12619
12830
|
validateRequestContract(input);
|
|
12831
|
+
if (!this.#vectorSearchEnabled && (input.mode === "concept" || input.mode === "hybrid")) {
|
|
12832
|
+
throw new SiftLightError(`${input.mode} search is disabled; set vectorSearchEnabled to true in sift-light.json and restart the host`);
|
|
12833
|
+
}
|
|
12620
12834
|
let request;
|
|
12621
12835
|
if (input.mode === "await" || input.mode === "cancel") {
|
|
12622
12836
|
request = this.#operationCommand(input, cwd, signal);
|
|
@@ -13015,160 +13229,11 @@ ${page.body}${rangeNote}${contextNote}${missingSelectionNote}
|
|
|
13015
13229
|
}
|
|
13016
13230
|
}
|
|
13017
13231
|
|
|
13018
|
-
// src/config-reader.ts
|
|
13019
|
-
import { readFile as readFile3 } from "node:fs/promises";
|
|
13020
|
-
var SIFT_LIGHT_CONFIG_ENV = "SIFT_LIGHT_CONFIG";
|
|
13021
|
-
var SEMANTIC_JUDGE_API_KEY_ENVS = ["TYPESAFE_API_KEY", "SIFT_LIGHT_JEV_API_KEY"];
|
|
13022
|
-
var DEFAULT_SEMANTIC_JUDGE_CONFIG = {
|
|
13023
|
-
enabled: false,
|
|
13024
|
-
provider: "jev",
|
|
13025
|
-
endpoint: "https://api.typesafe.ai/v1/systemone",
|
|
13026
|
-
apiKeyEnv: SEMANTIC_JUDGE_API_KEY_ENVS[0],
|
|
13027
|
-
model: "jev-latest",
|
|
13028
|
-
timeoutMs: 120000,
|
|
13029
|
-
maxCandidates: 20,
|
|
13030
|
-
maxRetries: 2
|
|
13031
|
-
};
|
|
13032
|
-
var DEFAULT_SIFT_LIGHT_CONFIG = {
|
|
13033
|
-
locale: "en",
|
|
13034
|
-
enforceSearch: "hard",
|
|
13035
|
-
semanticJudge: DEFAULT_SEMANTIC_JUDGE_CONFIG
|
|
13036
|
-
};
|
|
13037
|
-
function hasErrorCode(error, codes) {
|
|
13038
|
-
return error instanceof Error && "code" in error && codes.includes(String(error.code));
|
|
13039
|
-
}
|
|
13040
|
-
function isMissingFile(error) {
|
|
13041
|
-
return hasErrorCode(error, ["ENOENT"]);
|
|
13042
|
-
}
|
|
13043
|
-
function isRawSiftLightConfig(value) {
|
|
13044
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13045
|
-
}
|
|
13046
|
-
function isRawSemanticJudgeConfig(value) {
|
|
13047
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13048
|
-
}
|
|
13049
|
-
function boundedInteger2(value, fallback, minimum, maximum, field) {
|
|
13050
|
-
const candidate = value ?? fallback;
|
|
13051
|
-
if (typeof candidate !== "number" || !Number.isSafeInteger(candidate) || candidate < minimum || candidate > maximum) {
|
|
13052
|
-
throw new Error(`Invalid sift-light ${field}: expected an integer from ${String(minimum)} through ${String(maximum)}`);
|
|
13053
|
-
}
|
|
13054
|
-
return candidate;
|
|
13055
|
-
}
|
|
13056
|
-
function parseSemanticJudge(value, path) {
|
|
13057
|
-
if (value === undefined)
|
|
13058
|
-
return { ...DEFAULT_SEMANTIC_JUDGE_CONFIG };
|
|
13059
|
-
if (!isRawSemanticJudgeConfig(value)) {
|
|
13060
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge must be an object`);
|
|
13061
|
-
}
|
|
13062
|
-
const unknown = Object.keys(value).filter((key) => ![
|
|
13063
|
-
"enabled",
|
|
13064
|
-
"provider",
|
|
13065
|
-
"endpoint",
|
|
13066
|
-
"apiKeyEnv",
|
|
13067
|
-
"model",
|
|
13068
|
-
"timeoutMs",
|
|
13069
|
-
"maxCandidates",
|
|
13070
|
-
"maxRetries"
|
|
13071
|
-
].includes(key));
|
|
13072
|
-
if (unknown.length > 0) {
|
|
13073
|
-
throw new Error(`Invalid sift-light config at ${path}: unsupported semanticJudge fields; accepted fields are enabled, provider, endpoint, apiKeyEnv, model, timeoutMs, maxCandidates and maxRetries`);
|
|
13074
|
-
}
|
|
13075
|
-
const enabled = value.enabled ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.enabled;
|
|
13076
|
-
if (typeof enabled !== "boolean") {
|
|
13077
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.enabled must be a boolean`);
|
|
13078
|
-
}
|
|
13079
|
-
const provider = value.provider ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.provider;
|
|
13080
|
-
if (provider !== "jev") {
|
|
13081
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.provider must be "jev"`);
|
|
13082
|
-
}
|
|
13083
|
-
const endpoint = value.endpoint ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.endpoint;
|
|
13084
|
-
if (typeof endpoint !== "string" || endpoint.length === 0 || endpoint.length > 2048) {
|
|
13085
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a nonempty URL`);
|
|
13086
|
-
}
|
|
13087
|
-
let parsedEndpoint;
|
|
13088
|
-
try {
|
|
13089
|
-
parsedEndpoint = new URL(endpoint);
|
|
13090
|
-
} catch (error) {
|
|
13091
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must be a URL`, {
|
|
13092
|
-
cause: error
|
|
13093
|
-
});
|
|
13094
|
-
}
|
|
13095
|
-
const loopbackHosts = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
13096
|
-
const secureEndpoint = parsedEndpoint.protocol === "https:" || parsedEndpoint.protocol === "http:" && loopbackHosts.has(parsedEndpoint.hostname);
|
|
13097
|
-
if (!secureEndpoint || parsedEndpoint.username || parsedEndpoint.password) {
|
|
13098
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.endpoint must use HTTPS, except that HTTP is allowed for localhost loopback development; URL credentials are not allowed`);
|
|
13099
|
-
}
|
|
13100
|
-
const apiKeyEnv = value.apiKeyEnv ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.apiKeyEnv;
|
|
13101
|
-
if (typeof apiKeyEnv !== "string" || !/^[A-Z][A-Z0-9_]{0,127}$/u.test(apiKeyEnv)) {
|
|
13102
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.apiKeyEnv must be an uppercase environment variable name`);
|
|
13103
|
-
}
|
|
13104
|
-
const model = value.model ?? DEFAULT_SEMANTIC_JUDGE_CONFIG.model;
|
|
13105
|
-
if (typeof model !== "string" || model.length === 0 || model.length > 128 || /[\r\n\0]/u.test(model)) {
|
|
13106
|
-
throw new Error(`Invalid sift-light config at ${path}: semanticJudge.model must be bounded single-line text`);
|
|
13107
|
-
}
|
|
13108
|
-
return {
|
|
13109
|
-
enabled,
|
|
13110
|
-
provider,
|
|
13111
|
-
endpoint,
|
|
13112
|
-
apiKeyEnv,
|
|
13113
|
-
model,
|
|
13114
|
-
timeoutMs: boundedInteger2(value.timeoutMs, DEFAULT_SEMANTIC_JUDGE_CONFIG.timeoutMs, 1000, 1200000, `config at ${path}: semanticJudge.timeoutMs`),
|
|
13115
|
-
maxCandidates: boundedInteger2(value.maxCandidates, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxCandidates, 1, 20, `config at ${path}: semanticJudge.maxCandidates`),
|
|
13116
|
-
maxRetries: boundedInteger2(value.maxRetries, DEFAULT_SEMANTIC_JUDGE_CONFIG.maxRetries, 0, 5, `config at ${path}: semanticJudge.maxRetries`)
|
|
13117
|
-
};
|
|
13118
|
-
}
|
|
13119
|
-
function parseConfig(value, path) {
|
|
13120
|
-
if (!isRawSiftLightConfig(value)) {
|
|
13121
|
-
throw new Error(`Invalid sift-light config at ${path}: expected a JSON object`);
|
|
13122
|
-
}
|
|
13123
|
-
const unknown = Object.keys(value).filter((key) => !["locale", "enforceSearch", "semanticJudge"].includes(key));
|
|
13124
|
-
if (unknown.length > 0) {
|
|
13125
|
-
throw new Error(`Invalid sift-light config at ${path}: unsupported configuration fields; only locale, enforceSearch and semanticJudge are accepted`);
|
|
13126
|
-
}
|
|
13127
|
-
const { locale, enforceSearch } = value;
|
|
13128
|
-
if (locale !== undefined && locale !== "en" && locale !== "zh-CN") {
|
|
13129
|
-
throw new Error(`Invalid sift-light config at ${path}: locale must be "en" or "zh-CN"`);
|
|
13130
|
-
}
|
|
13131
|
-
const enforcement = normalizeSearchEnforcement(enforceSearch, `config at ${path}`);
|
|
13132
|
-
return {
|
|
13133
|
-
locale: locale ?? DEFAULT_SIFT_LIGHT_CONFIG.locale,
|
|
13134
|
-
enforceSearch: enforcement,
|
|
13135
|
-
semanticJudge: parseSemanticJudge(value.semanticJudge, path)
|
|
13136
|
-
};
|
|
13137
|
-
}
|
|
13138
|
-
function normalizeSearchEnforcement(value, source) {
|
|
13139
|
-
if (value === undefined || value === "hard")
|
|
13140
|
-
return "hard";
|
|
13141
|
-
if (value === "prefer")
|
|
13142
|
-
return "prefer";
|
|
13143
|
-
if (value === "off")
|
|
13144
|
-
return "off";
|
|
13145
|
-
throw new Error(`Invalid sift-light ${source}: enforceSearch must be "hard", "prefer", or "off"`);
|
|
13146
|
-
}
|
|
13147
|
-
async function readSiftLightConfigFile(path, options = {}) {
|
|
13148
|
-
try {
|
|
13149
|
-
const content = await readFile3(path, "utf8");
|
|
13150
|
-
return parseConfig(JSON.parse(content), path);
|
|
13151
|
-
} catch (error) {
|
|
13152
|
-
if (isMissingFile(error)) {
|
|
13153
|
-
if (options.missing === "error") {
|
|
13154
|
-
throw new Error(`sift-light config was not found at ${path}; create it or unset ${SIFT_LIGHT_CONFIG_ENV}`, { cause: error });
|
|
13155
|
-
}
|
|
13156
|
-
return { ...DEFAULT_SIFT_LIGHT_CONFIG };
|
|
13157
|
-
}
|
|
13158
|
-
if (error instanceof SyntaxError) {
|
|
13159
|
-
throw new Error(`Invalid sift-light config at ${path}: ${error.message}`, {
|
|
13160
|
-
cause: error
|
|
13161
|
-
});
|
|
13162
|
-
}
|
|
13163
|
-
throw error;
|
|
13164
|
-
}
|
|
13165
|
-
}
|
|
13166
|
-
|
|
13167
13232
|
// src/prompt-guidelines.ts
|
|
13168
13233
|
var SOURCE_OUTPUT_GUIDANCE = "Auto/summary text may include bounded source excerpts; ordinary matches text is metadata-only. Inspect may return bounded source windows covering an entire small file. Analysis text may include semantic passages; structured details may retain excerpts, names and signatures. Follow output limits, coverage and continuations.";
|
|
13169
13234
|
function siftLightPromptGuidelines(structuredOutput = true) {
|
|
13170
13235
|
return [
|
|
13171
|
-
`Use sift-light for read-only content search. ${SOURCE_OUTPUT_GUIDANCE} Omit mode and limit for automatic detail/summary selection; use mode="matches" for ordinary match metadata.`,
|
|
13236
|
+
`Use sift-light for read-only content search. ${SOURCE_OUTPUT_GUIDANCE} For routine development searches, start with fast exact content, filename or applicable structural modes when the request has a usable name, symbol, error text or other literal clue. Omitted mode is ordinary exact search and never loads the local embedding model. Vector search is disabled by default; concept/hybrid require vectorSearchEnabled:true in sift-light.json and an installed model. They can take tens of seconds on an uncached scope, so select them only when semantic recall is needed. Omit mode and limit for automatic detail/summary selection; use mode="matches" for ordinary match metadata.`,
|
|
13172
13237
|
`An omitted path searches the project cwd. Use scope:"strict" for a question restricted to one path; otherwise, if an explicit subpath has zero matches, ordinary and content-analysis searches retry from cwd and return project-wide counts with an expansion notice. Explicit absolute paths and .. traversal can search outside cwd, except protected external system areas and .git internals. Git changes mode remains cwd-scoped.`,
|
|
13173
13238
|
`Search output includes counts, categories, ranked paths, coverage and continuation metadata. Source excerpts may contain the searched text. Use mode="inspect" or the host read capability when exact source is required for an edit or verification.`,
|
|
13174
13239
|
`Use file and directory distributions to choose evidence. Reuse the visible cursor with path or paths for match metadata; mode="summary" pages the remaining file statistics. Match counts are not relevance scores.`,
|
|
@@ -13177,7 +13242,7 @@ function siftLightPromptGuidelines(structuredOutput = true) {
|
|
|
13177
13242
|
`Use anyOf:["term1","term2"] when every exact occurrence of 2-64 literals is needed in one version-bound result. It is case-sensitive, reports anonymized condition counts, and runs requests above eight terms as bounded parallel chunks. Large condition inventories have separate continuation pages; copy those requests to retrieve the complete counts.`,
|
|
13178
13243
|
`For a changed-code question, add changes:{base:"HEAD",scope:"lines",side:"new"}; omit target for the working tree, use side:"old" for deleted-side statistics. Copy returned continuation requests to preserve source versions.`,
|
|
13179
13244
|
`Use mode:"capabilities" when the language or requested operation is unclear to get a compact lazy inventory. Use mode:"outline" with a concrete source file path for symbol counts and locations, mode:"imports" for static relationships, and mode:"tests" for related-test candidates. Their text pages summarize metadata; structured details can retain source evidence.`,
|
|
13180
|
-
`Use mode:"files" plus query for unknown filenames and fuzzy paths. Multi-word filename queries require each word literally in the path; business concepts
|
|
13245
|
+
`Use mode:"files" plus query for unknown filenames and fuzzy paths. Multi-word filename queries require each word literally in the path; use hybrid/concept only for business concepts that cannot be located by a literal clue. Use wholeWord:true for a single-pattern whole-word search. exclude contains file globs, not content negation.`,
|
|
13181
13246
|
`Use mode:"structure" only for JS/TS/TSX/Go, with a required nonempty ast-grep pattern such as "compare($X, $X)" or "send()" for code shapes across whitespace; the text page reports structural counts and locations; structured details can retain matched source evidence.`,
|
|
13182
13247
|
`Use mode:"concept" plus a natural-language query when names are unknown. It runs a pinned local multilingual model only after explicit installation; no search downloads weights or sends code to a remote model. Results expose candidate counts, score statistics, paths and the bounded ranked passage behind each candidate. Similarity scores identify candidates, not correctness.`,
|
|
13183
13248
|
`Use mode:"hybrid" plus query when wording may differ from the source. It reports exact and semantic counts separately, removes overlap, and retains one pageable evidence snapshot. conceptLimit changes only the semantic candidate count; retained candidates keep their bounded source passages.`,
|
|
@@ -13188,9 +13253,9 @@ function siftLightPromptGuidelines(structuredOutput = true) {
|
|
|
13188
13253
|
}
|
|
13189
13254
|
function siftLightModelGuidelines() {
|
|
13190
13255
|
return [
|
|
13191
|
-
`Search with pattern and optional path. ${SOURCE_OUTPUT_GUIDANCE}
|
|
13192
|
-
`Use ranked paths and
|
|
13193
|
-
`Modes:
|
|
13256
|
+
`Search with pattern and optional path. ${SOURCE_OUTPUT_GUIDANCE} Default search is exact and model-free. concept/hybrid require vectorSearchEnabled:true in sift-light.json plus an installed model; uncached runs may take tens of seconds. Omit mode/limit for auto pages.`,
|
|
13257
|
+
`Use ranked paths and counts; Cursor continuation pages retained results. Use mode="inspect" for exact source before editing.`,
|
|
13258
|
+
`Modes: files+query for filenames; anyOf/allOf for literals; outline/imports/tests for static code; structure for AST; concept/hybrid for semantic candidates. Similarity is not proof.`,
|
|
13194
13259
|
`On rejection keep the strongest applicable mode and apply the stated repair once. Do not repeat the rejected request or include its error. Only explicit capability-unavailable permits a visibly partial alternative.`
|
|
13195
13260
|
];
|
|
13196
13261
|
}
|
|
@@ -13213,12 +13278,12 @@ function stringEnum(values, options) {
|
|
|
13213
13278
|
...options?.description ? { description: options.description } : {}
|
|
13214
13279
|
});
|
|
13215
13280
|
}
|
|
13216
|
-
var SIFT_LIGHT_DESCRIPTION = `Search and navigate code with bounded, verifiable evidence. Ordinary pattern searches use auto detail/summary; pattern is regex by default and literal=true matches source text exactly. A path selects an existing exact file or root; use mode=files with query to discover an unknown name. scope=strict prevents zero-result path expansion and wholeWord requires word boundaries. mode=capabilities returns a compact names-only project language inventory and the modes available for each detected language; capability providers are loaded only when the requested analysis runs. It never starts a parser, compiler, model or language server. mode=concept accepts a natural-language query, path and source filters; mode=hybrid uses one natural-language query for exact and local concept evidence, ranks exact evidence first, and retains a bounded semantic supplement. An explicitly enabled semantic judge may classify hybrid candidates, but it is disabled by default and never turns classification into a runtime proof. Slow concept/hybrid requests return status=waiting or running with operationId, progress, and an exact nextRequest using mode=await; copy that request unchanged to continue the same computation. Await expiry never downgrades evidence to literal-only or partial, and final results remain stable for the operation retention window. mode=cancel explicitly stops one operation. allOf and anyOf are explicit literal variants and cannot be mixed with pattern/literal; limit and context are output intent and are never silently dropped. modifiedAfter/modifiedBefore filter worktree files by inclusive/exclusive modification-time bounds in Unix milliseconds. structure requires a nonempty AST pattern and JS/TS/TSX/Go sources; lang is not a field. Outline uses a concrete source file path (not a directory) or retained cursor+matchIndex and follows declared syntax capabilities. imports/tests return bounded static module and related-test candidates without proving runtime execution. validate checks saved source evidence against its recorded origin. Partial coverage stays explicit. ${REQUEST_USAGE_GUIDANCE}`;
|
|
13217
|
-
var SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
|
|
13281
|
+
var SIFT_LIGHT_DESCRIPTION = `Search and navigate code with bounded, verifiable evidence. Routine searches should use exact content, filenames or applicable structural modes first. Omitted mode is ordinary exact search and does not load the embedding model; concept/hybrid require vectorSearchEnabled:true in sift-light.json plus an installed model and may take tens of seconds on an uncached scope. Ordinary pattern searches use auto detail/summary; pattern is regex by default and literal=true matches source text exactly. A path selects an existing exact file or root; use mode=files with query to discover an unknown name. scope=strict prevents zero-result path expansion and wholeWord requires word boundaries. mode=capabilities returns a compact names-only project language inventory and the modes available for each detected language; capability providers are loaded only when the requested analysis runs. It never starts a parser, compiler, model or language server. mode=concept accepts a natural-language query, path and source filters; mode=hybrid uses one natural-language query for exact and local concept evidence, ranks exact evidence first, and retains a bounded semantic supplement. An explicitly enabled semantic judge may classify hybrid candidates, but it is disabled by default and never turns classification into a runtime proof. Slow concept/hybrid requests return status=waiting or running with operationId, progress, and an exact nextRequest using mode=await; copy that request unchanged to continue the same computation. Await expiry never downgrades evidence to literal-only or partial, and final results remain stable for the operation retention window. mode=cancel explicitly stops one operation. allOf and anyOf are explicit literal variants and cannot be mixed with pattern/literal; limit and context are output intent and are never silently dropped. modifiedAfter/modifiedBefore filter worktree files by inclusive/exclusive modification-time bounds in Unix milliseconds. structure requires a nonempty AST pattern and JS/TS/TSX/Go sources; lang is not a field. Outline uses a concrete source file path (not a directory) or retained cursor+matchIndex and follows declared syntax capabilities. imports/tests return bounded static module and related-test candidates without proving runtime execution. validate checks saved source evidence against its recorded origin. Partial coverage stays explicit. ${REQUEST_USAGE_GUIDANCE}`;
|
|
13282
|
+
var SIFT_LIGHT_MODEL_DESCRIPTION = `Bounded local evidence search. Default exact search is model-free; concept/hybrid require vectorSearchEnabled:true in sift-light.json and a model. ${MODEL_USAGE_GUIDANCE}. Copy cursors; analysis is evidence, not proof.`;
|
|
13218
13283
|
var siftLightSchema = Type.Object({
|
|
13219
13284
|
query: Type.Optional(Type.String({
|
|
13220
13285
|
maxLength: 256,
|
|
13221
|
-
description: `${fieldGuidance("query")}. Hybrid uses the same query as exact literal text and as the local concept query. Discovery modes preserve their requested path. Concept and hybrid require an explicitly installed local model. A semantic judge is optional and remains disabled unless the active configuration explicitly enables it.`
|
|
13286
|
+
description: `${fieldGuidance("query")}. Hybrid uses the same query as exact literal text and as the local concept query. Discovery modes preserve their requested path. Concept and hybrid require vectorSearchEnabled:true in sift-light.json and an explicitly installed local model. A semantic judge is optional and remains disabled unless the active configuration explicitly enables it.`
|
|
13222
13287
|
})),
|
|
13223
13288
|
scope: Type.Optional(stringEnum(["strict", "expand"], {
|
|
13224
13289
|
description: `${fieldGuidance("scope")}; expand (default) retries ordinary content search from project cwd. Applies to ordinary, multi-term and role searches.`
|
|
@@ -13421,11 +13486,12 @@ function siftLightTool(outputMode) {
|
|
|
13421
13486
|
tool.outputSchema = SIFT_LIGHT_OUTPUT_SCHEMA;
|
|
13422
13487
|
return tool;
|
|
13423
13488
|
}
|
|
13424
|
-
function createDefaultSiftLightMcpService(semanticJudge) {
|
|
13489
|
+
function createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled = false) {
|
|
13425
13490
|
const resolvedSemanticJudge = semanticJudge ?? createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG);
|
|
13426
13491
|
return new SiftLightService({
|
|
13427
13492
|
runRipgrep: createRipgrepRunner(),
|
|
13428
13493
|
structure: createCtagsStructureProvider(),
|
|
13494
|
+
vectorSearchEnabled,
|
|
13429
13495
|
semanticJudge: resolvedSemanticJudge
|
|
13430
13496
|
});
|
|
13431
13497
|
}
|
|
@@ -13848,12 +13914,18 @@ function configuredPath(environment) {
|
|
|
13848
13914
|
const value = environment[SIFT_LIGHT_CONFIG_ENV]?.trim();
|
|
13849
13915
|
return value || undefined;
|
|
13850
13916
|
}
|
|
13851
|
-
async function
|
|
13917
|
+
async function createMcpSearchFeatures(environment = process.env) {
|
|
13852
13918
|
const path = configuredPath(environment);
|
|
13853
13919
|
if (!path)
|
|
13854
|
-
return
|
|
13920
|
+
return {
|
|
13921
|
+
semanticJudge: createDisabledSemanticJudgeIntegration(DEFAULT_SEMANTIC_JUDGE_CONFIG),
|
|
13922
|
+
vectorSearchEnabled: DEFAULT_SIFT_LIGHT_CONFIG.vectorSearchEnabled === true
|
|
13923
|
+
};
|
|
13855
13924
|
const config = await readSiftLightConfigFile(path, { missing: "error" });
|
|
13856
|
-
return
|
|
13925
|
+
return {
|
|
13926
|
+
semanticJudge: createConfiguredSemanticJudgeIntegration(config, environment),
|
|
13927
|
+
vectorSearchEnabled: config.vectorSearchEnabled === true
|
|
13928
|
+
};
|
|
13857
13929
|
}
|
|
13858
13930
|
function mcpSemanticJudgeConfigSource(environment = process.env) {
|
|
13859
13931
|
return configuredPath(environment) ? "explicit-config" : "not-configured";
|
|
@@ -13967,20 +14039,17 @@ function environmentInteger(name, fallback, minimum, maximum) {
|
|
|
13967
14039
|
function allowedOrigins() {
|
|
13968
14040
|
return (process.env.SIFT_LIGHT_MCP_ALLOWED_ORIGINS ?? "").split(",").map((origin) => origin.trim()).filter((origin) => origin.length > 0);
|
|
13969
14041
|
}
|
|
13970
|
-
async function configuredSemanticJudge() {
|
|
13971
|
-
return createMcpSemanticJudgeIntegration();
|
|
13972
|
-
}
|
|
13973
14042
|
function logSemanticJudgeStatus(integration) {
|
|
13974
14043
|
const status = integration.config.enabled ? "enabled" : "disabled";
|
|
13975
14044
|
process.stderr.write(`sift-light MCP semantic judge: ${status}; source=${mcpSemanticJudgeConfigSource()}
|
|
13976
14045
|
`);
|
|
13977
14046
|
}
|
|
13978
|
-
async function runHttpServer(outputMode, semanticJudge) {
|
|
14047
|
+
async function runHttpServer(outputMode, semanticJudge, vectorSearchEnabled) {
|
|
13979
14048
|
const running = await startSiftLightMcpServer({
|
|
13980
14049
|
cwd: process.env.SIFT_LIGHT_MCP_CWD ?? process.cwd(),
|
|
13981
14050
|
host: process.env.SIFT_LIGHT_MCP_HOST ?? DEFAULT_MCP_HOST,
|
|
13982
14051
|
port: environmentInteger("SIFT_LIGHT_MCP_PORT", DEFAULT_MCP_PORT, 0, 65535),
|
|
13983
|
-
createService: () => createDefaultSiftLightMcpService(semanticJudge),
|
|
14052
|
+
createService: () => createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled),
|
|
13984
14053
|
maxSessions: environmentInteger("SIFT_LIGHT_MCP_MAX_SESSIONS", DEFAULT_MCP_MAX_SESSIONS, 1, Number.MAX_SAFE_INTEGER),
|
|
13985
14054
|
sessionIdleTimeoutMs: environmentInteger("SIFT_LIGHT_MCP_SESSION_IDLE_MS", DEFAULT_MCP_SESSION_IDLE_TIMEOUT_MS, 1, Number.MAX_SAFE_INTEGER),
|
|
13986
14055
|
allowedOrigins: allowedOrigins(),
|
|
@@ -14014,11 +14083,11 @@ async function runHttpServer(outputMode, semanticJudge) {
|
|
|
14014
14083
|
process.once("SIGINT", shutdown);
|
|
14015
14084
|
process.once("SIGTERM", shutdown);
|
|
14016
14085
|
}
|
|
14017
|
-
async function runStdioServer(outputMode, semanticJudge) {
|
|
14086
|
+
async function runStdioServer(outputMode, semanticJudge, vectorSearchEnabled) {
|
|
14018
14087
|
const running = await startSiftLightMcpStdioServer({
|
|
14019
14088
|
cwd: process.env.SIFT_LIGHT_MCP_CWD ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
|
|
14020
14089
|
outputMode,
|
|
14021
|
-
createService: () => createDefaultSiftLightMcpService(semanticJudge)
|
|
14090
|
+
createService: () => createDefaultSiftLightMcpService(semanticJudge, vectorSearchEnabled)
|
|
14022
14091
|
});
|
|
14023
14092
|
process.stderr.write(`sift-light MCP serving one local client over stdio
|
|
14024
14093
|
`);
|
|
@@ -14047,13 +14116,15 @@ async function main() {
|
|
|
14047
14116
|
return;
|
|
14048
14117
|
}
|
|
14049
14118
|
const outputMode = parseSiftLightMcpOutputMode(process.env.SIFT_LIGHT_MCP_OUTPUT_MODE);
|
|
14050
|
-
const semanticJudge = await
|
|
14119
|
+
const { semanticJudge, vectorSearchEnabled } = await createMcpSearchFeatures();
|
|
14051
14120
|
logSemanticJudgeStatus(semanticJudge);
|
|
14121
|
+
process.stderr.write(`sift-light MCP vector search: ${vectorSearchEnabled ? "enabled" : "disabled"}
|
|
14122
|
+
`);
|
|
14052
14123
|
if (transport === "stdio") {
|
|
14053
|
-
await runStdioServer(outputMode, semanticJudge);
|
|
14124
|
+
await runStdioServer(outputMode, semanticJudge, vectorSearchEnabled);
|
|
14054
14125
|
return;
|
|
14055
14126
|
}
|
|
14056
|
-
await runHttpServer(outputMode, semanticJudge);
|
|
14127
|
+
await runHttpServer(outputMode, semanticJudge, vectorSearchEnabled);
|
|
14057
14128
|
}
|
|
14058
14129
|
try {
|
|
14059
14130
|
await main();
|