sift-light 1.0.5-2 → 1.0.5-6
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/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/kimi.plugin.json +2 -2
- package/plugins/sift-light/omp-extension.mjs +78 -19
- package/plugins/sift-light/package.json +1 -1
- package/src/errors.ts +7 -1
- package/src/mcp-server.mjs +78 -19
- package/src/request-contract-catalog.ts +10 -6
- package/src/rg.ts +3 -1
- package/src/ripgrep-diagnostics.ts +86 -3
- package/src/scan-revisions.ts +3 -1
- package/src/tool-schema.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.5-6 — 2026-10-10
|
|
4
|
+
|
|
5
|
+
- Add structured `repairExamples` to invalid-regex and missing-path diagnostics. The examples preserve bounded search filters, never auto-convert regex input, and are omitted when redaction would make request data unsafe to echo.
|
|
6
|
+
- Extend model-facing guidance for `redact=true`, including its sensitive-search use cases and its boundary as display masking rather than filesystem sandboxing.
|
|
7
|
+
- Add regression coverage for the audited request failures and verify the same typed recovery details through the real MCP, ripgrep and packaged host paths.
|
|
8
|
+
|
|
3
9
|
## 1.0.5-1 — 2026-10-09
|
|
4
10
|
|
|
5
11
|
- Fix OMP plugin-cache searches when the generated bundle cannot resolve its platform ripgrep package. Bundled ripgrep remains preferred; an executable `rg` on `PATH` is now a bounded compatibility fallback, while explicit `SIFT_LIGHT_RG_PATH` values retain strict absolute-path and executable validation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sift-light",
|
|
3
|
-
"version": "1.0.5-
|
|
3
|
+
"version": "1.0.5-6",
|
|
4
4
|
"description": "Require sift-light for conventional local searches while keeping development tools available.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "baoer"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"args": [
|
|
14
14
|
"--yes",
|
|
15
15
|
"--package",
|
|
16
|
-
"sift-light-runtime@npm:sift-light@1.0.5-
|
|
16
|
+
"sift-light-runtime@npm:sift-light@1.0.5-6",
|
|
17
17
|
"sift-light-mcp",
|
|
18
18
|
"--stdio"
|
|
19
19
|
],
|
|
@@ -362,7 +362,7 @@ class RipgrepInputError extends SiftLightError {
|
|
|
362
362
|
code;
|
|
363
363
|
guidance;
|
|
364
364
|
details;
|
|
365
|
-
constructor(code, message, guidance) {
|
|
365
|
+
constructor(code, message, guidance, repairExamples = []) {
|
|
366
366
|
super(`${message} ${guidance}`);
|
|
367
367
|
this.name = "RipgrepInputError";
|
|
368
368
|
this.code = code;
|
|
@@ -375,7 +375,8 @@ class RipgrepInputError extends SiftLightError {
|
|
|
375
375
|
reason: guidance
|
|
376
376
|
}
|
|
377
377
|
],
|
|
378
|
-
recovery: { action: "manual", reason: guidance }
|
|
378
|
+
recovery: { action: "manual", reason: guidance },
|
|
379
|
+
...repairExamples.length > 0 ? { repairExamples } : {}
|
|
379
380
|
};
|
|
380
381
|
}
|
|
381
382
|
}
|
|
@@ -915,7 +916,7 @@ async function resolveRipgrepExecutable() {
|
|
|
915
916
|
}
|
|
916
917
|
|
|
917
918
|
// src/ripgrep-diagnostics.ts
|
|
918
|
-
import { resolve as resolve3 } from "path";
|
|
919
|
+
import { basename, resolve as resolve3 } from "path";
|
|
919
920
|
|
|
920
921
|
// src/redaction.ts
|
|
921
922
|
var PRIVATE_KEY = /-----BEGIN ([^-\r\n]*PRIVATE KEY)-----[\s\S]*?-----END \1-----/g;
|
|
@@ -1049,15 +1050,70 @@ function missingPathFromDiagnostics(stderr) {
|
|
|
1049
1050
|
}
|
|
1050
1051
|
return;
|
|
1051
1052
|
}
|
|
1052
|
-
|
|
1053
|
+
var RIPGREP_RECOVERY_FIELDS = [
|
|
1054
|
+
"path",
|
|
1055
|
+
"glob",
|
|
1056
|
+
"exclude",
|
|
1057
|
+
"literal",
|
|
1058
|
+
"ignoreCase",
|
|
1059
|
+
"hidden",
|
|
1060
|
+
"ignorePolicy",
|
|
1061
|
+
"wholeWord",
|
|
1062
|
+
"context",
|
|
1063
|
+
"scope",
|
|
1064
|
+
"redact"
|
|
1065
|
+
];
|
|
1066
|
+
function copyRipgrepRecoveryFields(request) {
|
|
1067
|
+
const result = {};
|
|
1068
|
+
for (const field of RIPGREP_RECOVERY_FIELDS) {
|
|
1069
|
+
const value = request[field];
|
|
1070
|
+
if (value !== undefined)
|
|
1071
|
+
result[field] = value;
|
|
1072
|
+
}
|
|
1073
|
+
return result;
|
|
1074
|
+
}
|
|
1075
|
+
function ripgrepRepairExamples(code, request, redact) {
|
|
1076
|
+
if (request === undefined || redact && containsSensitiveText(request))
|
|
1077
|
+
return [];
|
|
1078
|
+
if (code === "E_REGEX_INVALID") {
|
|
1079
|
+
return [
|
|
1080
|
+
{
|
|
1081
|
+
label: "\u6309\u539F\u6587\u67E5\u627E",
|
|
1082
|
+
request: {
|
|
1083
|
+
mode: "matches",
|
|
1084
|
+
...copyRipgrepRecoveryFields(request),
|
|
1085
|
+
pattern: request.pattern,
|
|
1086
|
+
literal: true
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
];
|
|
1090
|
+
}
|
|
1091
|
+
if (code === "E_SEARCH_PATH_NOT_FOUND" && request.path !== undefined) {
|
|
1092
|
+
const fileName = basename(request.path.replaceAll("\\", "/"));
|
|
1093
|
+
if (fileName === "." || fileName === "/" || fileName.length === 0)
|
|
1094
|
+
return [];
|
|
1095
|
+
return [
|
|
1096
|
+
{
|
|
1097
|
+
label: "\u6309\u6587\u4EF6\u540D\u67E5\u627E",
|
|
1098
|
+
request: {
|
|
1099
|
+
mode: "files",
|
|
1100
|
+
query: fileName,
|
|
1101
|
+
...request.redact === true ? { redact: true } : {}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
];
|
|
1105
|
+
}
|
|
1106
|
+
return [];
|
|
1107
|
+
}
|
|
1108
|
+
function createRipgrepInputError(stderr, redact = false, request) {
|
|
1053
1109
|
const code = classifyRipgrepInputFailure(stderr);
|
|
1054
1110
|
if (code === "E_REGEX_INVALID") {
|
|
1055
|
-
return new RipgrepInputError(code, "ripgrep regex parse error: pattern is not a valid regular expression.", "Correct pattern or set literal=true for source text; no automatic literal conversion was attempted.");
|
|
1111
|
+
return new RipgrepInputError(code, "ripgrep regex parse error: pattern is not a valid regular expression.", "Correct pattern or set literal=true for source text; no automatic literal conversion was attempted.", ripgrepRepairExamples(code, request, redact));
|
|
1056
1112
|
}
|
|
1057
1113
|
if (code === "E_SEARCH_PATH_NOT_FOUND") {
|
|
1058
1114
|
const path = missingPathFromDiagnostics(stderr);
|
|
1059
1115
|
const location = path ? ` (${JSON.stringify(boundedRipgrepDiagnostic(path, redact))})` : "";
|
|
1060
|
-
return new RipgrepInputError(code, `A searched path${location} does not exist or became unavailable.`, "Provide an existing exact path, or use mode=files with query to discover an unknown filename; no retry or scope expansion was attempted.");
|
|
1116
|
+
return new RipgrepInputError(code, `A searched path${location} does not exist or became unavailable.`, "Provide an existing exact path, or use mode=files with query to discover an unknown filename; no retry or scope expansion was attempted.", ripgrepRepairExamples(code, request, redact));
|
|
1061
1117
|
}
|
|
1062
1118
|
return;
|
|
1063
1119
|
}
|
|
@@ -1278,7 +1334,7 @@ async function captureBatch(paths, revisions, signal, onRevisionError) {
|
|
|
1278
1334
|
if (signal?.aborted)
|
|
1279
1335
|
throw abortError();
|
|
1280
1336
|
}
|
|
1281
|
-
async function captureCandidateRevisions(executable, args2, cwd, maxFiles, signal, redact = false) {
|
|
1337
|
+
async function captureCandidateRevisions(executable, args2, cwd, maxFiles, signal, redact = false, request) {
|
|
1282
1338
|
const revisions = new Map;
|
|
1283
1339
|
let candidateCount = 0;
|
|
1284
1340
|
let enumerationTruncated = false;
|
|
@@ -1332,7 +1388,7 @@ async function captureCandidateRevisions(executable, args2, cwd, maxFiles, signa
|
|
|
1332
1388
|
await captureBatch(batch, revisions, signal, recordMetadataFailure);
|
|
1333
1389
|
});
|
|
1334
1390
|
const diagnostics = classifyRipgrepDiagnostics(result.stderr);
|
|
1335
|
-
const inputError = createRipgrepInputError(result.stderr, redact);
|
|
1391
|
+
const inputError = createRipgrepInputError(result.stderr, redact, request);
|
|
1336
1392
|
const unreadable = [...diagnostics.unreadable, ...diagnostics.recoverable, ...metadataFailures];
|
|
1337
1393
|
if (inputError)
|
|
1338
1394
|
throw inputError;
|
|
@@ -1658,7 +1714,7 @@ function createRipgrepRunner(options = {}) {
|
|
|
1658
1714
|
...policy.ripgrepGlobArguments(validatedSearchPath),
|
|
1659
1715
|
"--",
|
|
1660
1716
|
searchTarget
|
|
1661
|
-
], ripgrepCwd, maxSourceRevisionFiles, signal, request.redact);
|
|
1717
|
+
], ripgrepCwd, maxSourceRevisionFiles, signal, request.redact, request);
|
|
1662
1718
|
let ignoredFileCount = 0;
|
|
1663
1719
|
let ignoredFileSamples = [];
|
|
1664
1720
|
let filesystemCoverage = "complete";
|
|
@@ -1688,7 +1744,7 @@ function createRipgrepRunner(options = {}) {
|
|
|
1688
1744
|
...policy.ripgrepGlobArguments(validatedSearchPath),
|
|
1689
1745
|
"--",
|
|
1690
1746
|
searchTarget
|
|
1691
|
-
], ripgrepCwd, maxSourceRevisionFiles, signal, request.redact);
|
|
1747
|
+
], ripgrepCwd, maxSourceRevisionFiles, signal, request.redact, request);
|
|
1692
1748
|
const ignored = [...allCandidates.revisions.keys()].filter((path) => !before.revisions.has(path));
|
|
1693
1749
|
ignoredFileCount = ignored.length;
|
|
1694
1750
|
ignoredFileSamples = ignored.slice(0, 20).map((path) => displayPath(path, cwd).displayPath);
|
|
@@ -1732,7 +1788,7 @@ function createRipgrepRunner(options = {}) {
|
|
|
1732
1788
|
}
|
|
1733
1789
|
}));
|
|
1734
1790
|
const diagnostics = classifyRipgrepDiagnostics(stderr);
|
|
1735
|
-
const inputError = createRipgrepInputError(stderr, request.redact);
|
|
1791
|
+
const inputError = createRipgrepInputError(stderr, request.redact, request);
|
|
1736
1792
|
if (inputError)
|
|
1737
1793
|
throw inputError;
|
|
1738
1794
|
const filesystemDiagnostics = [...diagnostics.unreadable, ...diagnostics.recoverable];
|
|
@@ -1969,7 +2025,7 @@ function createCtagsStructureProvider(options = {}) {
|
|
|
1969
2025
|
// package.json
|
|
1970
2026
|
var package_default = {
|
|
1971
2027
|
name: "sift-light",
|
|
1972
|
-
version: "1.0.5-
|
|
2028
|
+
version: "1.0.5-6",
|
|
1973
2029
|
description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
|
|
1974
2030
|
keywords: [
|
|
1975
2031
|
"ai-agent",
|
|
@@ -11333,6 +11389,7 @@ var MODE_FIELD_SUMMARY = MODE_SUMMARY_MODES.map((mode) => modeFieldSummary(mode)
|
|
|
11333
11389
|
var REQUEST_FIELD_GUIDANCE = {
|
|
11334
11390
|
pattern: "pattern is regex by default; literal=true matches source text exactly",
|
|
11335
11391
|
literal: "literal=true makes pattern exact source text; anyOf/allOf already use literal semantics",
|
|
11392
|
+
redact: "redact=true masks credential-like values and private-key bodies in displayed evidence; it does not sandbox search or change counts",
|
|
11336
11393
|
path: "path must be an existing exact file or root; use mode=files+query for unknown names",
|
|
11337
11394
|
query: "files+query matches known filename/path text (not glob patterns); omit files query to list every file under path; concept/hybrid query is natural language",
|
|
11338
11395
|
anyOf: "anyOf is case-sensitive exact-literal OR; omit pattern, allOf, literal, ignoreCase, roles",
|
|
@@ -11349,6 +11406,7 @@ function fieldGuidance(field) {
|
|
|
11349
11406
|
}
|
|
11350
11407
|
var GUIDANCE_FIELDS = [
|
|
11351
11408
|
"pattern",
|
|
11409
|
+
"redact",
|
|
11352
11410
|
"literal",
|
|
11353
11411
|
"path",
|
|
11354
11412
|
"query",
|
|
@@ -11370,12 +11428,13 @@ var REQUEST_USAGE_GUIDANCE = [
|
|
|
11370
11428
|
].join("; ");
|
|
11371
11429
|
var MODEL_USAGE_GUIDANCE = [
|
|
11372
11430
|
"pattern is regex by default; literal=true matches source text exactly",
|
|
11373
|
-
"
|
|
11374
|
-
"
|
|
11375
|
-
|
|
11376
|
-
|
|
11377
|
-
"outline
|
|
11378
|
-
|
|
11431
|
+
"redact=true masks credentials and private-key bodies in displayed evidence; use it for configs, logs or user directories; it does not sandbox search",
|
|
11432
|
+
"path is an existing exact file/root; use files+query for unknown names",
|
|
11433
|
+
"anyOf/allOf are exact-literal OR/AND variants; exclude pattern/literal",
|
|
11434
|
+
`limit/context are ordinary-search budgets (limit <= ${String(MAX_PAGE_SIZE)}); omit for hybrid/concept/outline/structure/inspect`,
|
|
11435
|
+
"outline needs a concrete JS/TS/TSX/Python-capable source file, not a directory; structure needs a nonempty AST pattern for JS/TS/TSX/Go, no lang; use capabilities before unfamiliar language operations",
|
|
11436
|
+
"imports/tests are static JS/TS/TSX candidates",
|
|
11437
|
+
"selectors: inspect, hybrid and capabilities use their cataloged fields",
|
|
11379
11438
|
"exact, any-of and max_results are not parameters"
|
|
11380
11439
|
].join("; ");
|
|
11381
11440
|
var MODE_CONTRACT_DESCRIPTION = [
|
|
@@ -22369,7 +22428,7 @@ var siftLightSchema = _Object_({
|
|
|
22369
22428
|
description: "Named exact-literal checks for mode=audit. Each finding is present, absent_with_complete_coverage, or unknown."
|
|
22370
22429
|
})),
|
|
22371
22430
|
redact: Optional(Boolean2({
|
|
22372
|
-
description: "Optional display-only masking for credential-like values and private-key bodies. Default false. It never changes searched files, admitted matches, counts
|
|
22431
|
+
description: "Optional display-only masking for credential-like values and private-key bodies. Default false. Use it for configuration, logs and user-directory searches when returned text may contain secrets. It never changes the search scope, searched files, admitted matches, counts or cursor completeness, and it is not a sandbox."
|
|
22373
22432
|
})),
|
|
22374
22433
|
modifiedAfter: Optional(Integer({
|
|
22375
22434
|
minimum: 0,
|
package/src/errors.ts
CHANGED
|
@@ -51,7 +51,12 @@ export class RipgrepInputError extends SiftLightError {
|
|
|
51
51
|
readonly guidance: string;
|
|
52
52
|
readonly details: SiftLightDiagnosticDetails;
|
|
53
53
|
|
|
54
|
-
constructor(
|
|
54
|
+
constructor(
|
|
55
|
+
code: RipgrepInputErrorCode,
|
|
56
|
+
message: string,
|
|
57
|
+
guidance: string,
|
|
58
|
+
repairExamples: readonly SiftLightDiagnosticRepairExample[] = [],
|
|
59
|
+
) {
|
|
55
60
|
super(`${message} ${guidance}`);
|
|
56
61
|
this.name = "RipgrepInputError";
|
|
57
62
|
this.code = code;
|
|
@@ -65,6 +70,7 @@ export class RipgrepInputError extends SiftLightError {
|
|
|
65
70
|
},
|
|
66
71
|
],
|
|
67
72
|
recovery: { action: "manual", reason: guidance },
|
|
73
|
+
...(repairExamples.length > 0 ? { repairExamples } : {}),
|
|
68
74
|
};
|
|
69
75
|
}
|
|
70
76
|
}
|
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.5-
|
|
10
|
+
version: "1.0.5-6",
|
|
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",
|
|
@@ -571,7 +571,7 @@ class RipgrepInputError extends SiftLightError {
|
|
|
571
571
|
code;
|
|
572
572
|
guidance;
|
|
573
573
|
details;
|
|
574
|
-
constructor(code, message, guidance) {
|
|
574
|
+
constructor(code, message, guidance, repairExamples = []) {
|
|
575
575
|
super(`${message} ${guidance}`);
|
|
576
576
|
this.name = "RipgrepInputError";
|
|
577
577
|
this.code = code;
|
|
@@ -584,7 +584,8 @@ class RipgrepInputError extends SiftLightError {
|
|
|
584
584
|
reason: guidance
|
|
585
585
|
}
|
|
586
586
|
],
|
|
587
|
-
recovery: { action: "manual", reason: guidance }
|
|
587
|
+
recovery: { action: "manual", reason: guidance },
|
|
588
|
+
...repairExamples.length > 0 ? { repairExamples } : {}
|
|
588
589
|
};
|
|
589
590
|
}
|
|
590
591
|
}
|
|
@@ -966,6 +967,7 @@ var MODE_FIELD_SUMMARY = MODE_SUMMARY_MODES.map((mode) => modeFieldSummary(mode)
|
|
|
966
967
|
var REQUEST_FIELD_GUIDANCE = {
|
|
967
968
|
pattern: "pattern is regex by default; literal=true matches source text exactly",
|
|
968
969
|
literal: "literal=true makes pattern exact source text; anyOf/allOf already use literal semantics",
|
|
970
|
+
redact: "redact=true masks credential-like values and private-key bodies in displayed evidence; it does not sandbox search or change counts",
|
|
969
971
|
path: "path must be an existing exact file or root; use mode=files+query for unknown names",
|
|
970
972
|
query: "files+query matches known filename/path text (not glob patterns); omit files query to list every file under path; concept/hybrid query is natural language",
|
|
971
973
|
anyOf: "anyOf is case-sensitive exact-literal OR; omit pattern, allOf, literal, ignoreCase, roles",
|
|
@@ -982,6 +984,7 @@ function fieldGuidance(field) {
|
|
|
982
984
|
}
|
|
983
985
|
var GUIDANCE_FIELDS = [
|
|
984
986
|
"pattern",
|
|
987
|
+
"redact",
|
|
985
988
|
"literal",
|
|
986
989
|
"path",
|
|
987
990
|
"query",
|
|
@@ -1003,12 +1006,13 @@ var REQUEST_USAGE_GUIDANCE = [
|
|
|
1003
1006
|
].join("; ");
|
|
1004
1007
|
var MODEL_USAGE_GUIDANCE = [
|
|
1005
1008
|
"pattern is regex by default; literal=true matches source text exactly",
|
|
1006
|
-
"
|
|
1007
|
-
"
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
"outline
|
|
1011
|
-
|
|
1009
|
+
"redact=true masks credentials and private-key bodies in displayed evidence; use it for configs, logs or user directories; it does not sandbox search",
|
|
1010
|
+
"path is an existing exact file/root; use files+query for unknown names",
|
|
1011
|
+
"anyOf/allOf are exact-literal OR/AND variants; exclude pattern/literal",
|
|
1012
|
+
`limit/context are ordinary-search budgets (limit <= ${String(MAX_PAGE_SIZE)}); omit for hybrid/concept/outline/structure/inspect`,
|
|
1013
|
+
"outline needs a concrete JS/TS/TSX/Python-capable source file, not a directory; structure needs a nonempty AST pattern for JS/TS/TSX/Go, no lang; use capabilities before unfamiliar language operations",
|
|
1014
|
+
"imports/tests are static JS/TS/TSX candidates",
|
|
1015
|
+
"selectors: inspect, hybrid and capabilities use their cataloged fields",
|
|
1012
1016
|
"exact, any-of and max_results are not parameters"
|
|
1013
1017
|
].join("; ");
|
|
1014
1018
|
var MODE_CONTRACT_DESCRIPTION = [
|
|
@@ -2030,7 +2034,7 @@ async function resolveRipgrepExecutable() {
|
|
|
2030
2034
|
}
|
|
2031
2035
|
|
|
2032
2036
|
// src/ripgrep-diagnostics.ts
|
|
2033
|
-
import { resolve as resolve3 } from "node:path";
|
|
2037
|
+
import { basename, resolve as resolve3 } from "node:path";
|
|
2034
2038
|
var UNREADABLE_SUFFIX = /:\s+Permission denied(?:\s+\(os error 13\))?\s*$/iu;
|
|
2035
2039
|
var UNREADABLE_CODE = /\(os error 13\)\s*$/iu;
|
|
2036
2040
|
var RECOVERABLE_FILESYSTEM_CODE = /\(os error (?:4|5|22)\)\s*$/iu;
|
|
@@ -2069,15 +2073,70 @@ function missingPathFromDiagnostics(stderr) {
|
|
|
2069
2073
|
}
|
|
2070
2074
|
return;
|
|
2071
2075
|
}
|
|
2072
|
-
|
|
2076
|
+
var RIPGREP_RECOVERY_FIELDS = [
|
|
2077
|
+
"path",
|
|
2078
|
+
"glob",
|
|
2079
|
+
"exclude",
|
|
2080
|
+
"literal",
|
|
2081
|
+
"ignoreCase",
|
|
2082
|
+
"hidden",
|
|
2083
|
+
"ignorePolicy",
|
|
2084
|
+
"wholeWord",
|
|
2085
|
+
"context",
|
|
2086
|
+
"scope",
|
|
2087
|
+
"redact"
|
|
2088
|
+
];
|
|
2089
|
+
function copyRipgrepRecoveryFields(request) {
|
|
2090
|
+
const result = {};
|
|
2091
|
+
for (const field of RIPGREP_RECOVERY_FIELDS) {
|
|
2092
|
+
const value = request[field];
|
|
2093
|
+
if (value !== undefined)
|
|
2094
|
+
result[field] = value;
|
|
2095
|
+
}
|
|
2096
|
+
return result;
|
|
2097
|
+
}
|
|
2098
|
+
function ripgrepRepairExamples(code, request, redact) {
|
|
2099
|
+
if (request === undefined || redact && containsSensitiveText(request))
|
|
2100
|
+
return [];
|
|
2101
|
+
if (code === "E_REGEX_INVALID") {
|
|
2102
|
+
return [
|
|
2103
|
+
{
|
|
2104
|
+
label: "按原文查找",
|
|
2105
|
+
request: {
|
|
2106
|
+
mode: "matches",
|
|
2107
|
+
...copyRipgrepRecoveryFields(request),
|
|
2108
|
+
pattern: request.pattern,
|
|
2109
|
+
literal: true
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
];
|
|
2113
|
+
}
|
|
2114
|
+
if (code === "E_SEARCH_PATH_NOT_FOUND" && request.path !== undefined) {
|
|
2115
|
+
const fileName = basename(request.path.replaceAll("\\", "/"));
|
|
2116
|
+
if (fileName === "." || fileName === "/" || fileName.length === 0)
|
|
2117
|
+
return [];
|
|
2118
|
+
return [
|
|
2119
|
+
{
|
|
2120
|
+
label: "按文件名查找",
|
|
2121
|
+
request: {
|
|
2122
|
+
mode: "files",
|
|
2123
|
+
query: fileName,
|
|
2124
|
+
...request.redact === true ? { redact: true } : {}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
];
|
|
2128
|
+
}
|
|
2129
|
+
return [];
|
|
2130
|
+
}
|
|
2131
|
+
function createRipgrepInputError(stderr, redact = false, request) {
|
|
2073
2132
|
const code = classifyRipgrepInputFailure(stderr);
|
|
2074
2133
|
if (code === "E_REGEX_INVALID") {
|
|
2075
|
-
return new RipgrepInputError(code, "ripgrep regex parse error: pattern is not a valid regular expression.", "Correct pattern or set literal=true for source text; no automatic literal conversion was attempted.");
|
|
2134
|
+
return new RipgrepInputError(code, "ripgrep regex parse error: pattern is not a valid regular expression.", "Correct pattern or set literal=true for source text; no automatic literal conversion was attempted.", ripgrepRepairExamples(code, request, redact));
|
|
2076
2135
|
}
|
|
2077
2136
|
if (code === "E_SEARCH_PATH_NOT_FOUND") {
|
|
2078
2137
|
const path = missingPathFromDiagnostics(stderr);
|
|
2079
2138
|
const location = path ? ` (${JSON.stringify(boundedRipgrepDiagnostic(path, redact))})` : "";
|
|
2080
|
-
return new RipgrepInputError(code, `A searched path${location} does not exist or became unavailable.`, "Provide an existing exact path, or use mode=files with query to discover an unknown filename; no retry or scope expansion was attempted.");
|
|
2139
|
+
return new RipgrepInputError(code, `A searched path${location} does not exist or became unavailable.`, "Provide an existing exact path, or use mode=files with query to discover an unknown filename; no retry or scope expansion was attempted.", ripgrepRepairExamples(code, request, redact));
|
|
2081
2140
|
}
|
|
2082
2141
|
return;
|
|
2083
2142
|
}
|
|
@@ -2298,7 +2357,7 @@ async function captureBatch(paths, revisions, signal, onRevisionError) {
|
|
|
2298
2357
|
if (signal?.aborted)
|
|
2299
2358
|
throw abortError();
|
|
2300
2359
|
}
|
|
2301
|
-
async function captureCandidateRevisions(executable, args, cwd, maxFiles, signal, redact = false) {
|
|
2360
|
+
async function captureCandidateRevisions(executable, args, cwd, maxFiles, signal, redact = false, request) {
|
|
2302
2361
|
const revisions = new Map;
|
|
2303
2362
|
let candidateCount = 0;
|
|
2304
2363
|
let enumerationTruncated = false;
|
|
@@ -2352,7 +2411,7 @@ async function captureCandidateRevisions(executable, args, cwd, maxFiles, signal
|
|
|
2352
2411
|
await captureBatch(batch, revisions, signal, recordMetadataFailure);
|
|
2353
2412
|
});
|
|
2354
2413
|
const diagnostics = classifyRipgrepDiagnostics(result.stderr);
|
|
2355
|
-
const inputError = createRipgrepInputError(result.stderr, redact);
|
|
2414
|
+
const inputError = createRipgrepInputError(result.stderr, redact, request);
|
|
2356
2415
|
const unreadable = [...diagnostics.unreadable, ...diagnostics.recoverable, ...metadataFailures];
|
|
2357
2416
|
if (inputError)
|
|
2358
2417
|
throw inputError;
|
|
@@ -2678,7 +2737,7 @@ function createRipgrepRunner(options = {}) {
|
|
|
2678
2737
|
...policy.ripgrepGlobArguments(validatedSearchPath),
|
|
2679
2738
|
"--",
|
|
2680
2739
|
searchTarget
|
|
2681
|
-
], ripgrepCwd, maxSourceRevisionFiles, signal, request.redact);
|
|
2740
|
+
], ripgrepCwd, maxSourceRevisionFiles, signal, request.redact, request);
|
|
2682
2741
|
let ignoredFileCount = 0;
|
|
2683
2742
|
let ignoredFileSamples = [];
|
|
2684
2743
|
let filesystemCoverage = "complete";
|
|
@@ -2708,7 +2767,7 @@ function createRipgrepRunner(options = {}) {
|
|
|
2708
2767
|
...policy.ripgrepGlobArguments(validatedSearchPath),
|
|
2709
2768
|
"--",
|
|
2710
2769
|
searchTarget
|
|
2711
|
-
], ripgrepCwd, maxSourceRevisionFiles, signal, request.redact);
|
|
2770
|
+
], ripgrepCwd, maxSourceRevisionFiles, signal, request.redact, request);
|
|
2712
2771
|
const ignored = [...allCandidates.revisions.keys()].filter((path) => !before.revisions.has(path));
|
|
2713
2772
|
ignoredFileCount = ignored.length;
|
|
2714
2773
|
ignoredFileSamples = ignored.slice(0, 20).map((path) => displayPath(path, cwd).displayPath);
|
|
@@ -2752,7 +2811,7 @@ function createRipgrepRunner(options = {}) {
|
|
|
2752
2811
|
}
|
|
2753
2812
|
}));
|
|
2754
2813
|
const diagnostics = classifyRipgrepDiagnostics(stderr);
|
|
2755
|
-
const inputError = createRipgrepInputError(stderr, request.redact);
|
|
2814
|
+
const inputError = createRipgrepInputError(stderr, request.redact, request);
|
|
2756
2815
|
if (inputError)
|
|
2757
2816
|
throw inputError;
|
|
2758
2817
|
const filesystemDiagnostics = [...diagnostics.unreadable, ...diagnostics.recoverable];
|
|
@@ -14858,7 +14917,7 @@ var siftLightSchema = Type.Object({
|
|
|
14858
14917
|
description: "Named exact-literal checks for mode=audit. Each finding is present, absent_with_complete_coverage, or unknown."
|
|
14859
14918
|
})),
|
|
14860
14919
|
redact: Type.Optional(Type.Boolean({
|
|
14861
|
-
description: "Optional display-only masking for credential-like values and private-key bodies. Default false. It never changes searched files, admitted matches, counts
|
|
14920
|
+
description: "Optional display-only masking for credential-like values and private-key bodies. Default false. Use it for configuration, logs and user-directory searches when returned text may contain secrets. It never changes the search scope, searched files, admitted matches, counts or cursor completeness, and it is not a sandbox."
|
|
14862
14921
|
})),
|
|
14863
14922
|
modifiedAfter: Type.Optional(Type.Integer({
|
|
14864
14923
|
minimum: 0,
|
|
@@ -169,6 +169,8 @@ export const REQUEST_FIELD_GUIDANCE: Partial<Record<RequestField, string>> = {
|
|
|
169
169
|
pattern: "pattern is regex by default; literal=true matches source text exactly",
|
|
170
170
|
literal:
|
|
171
171
|
"literal=true makes pattern exact source text; anyOf/allOf already use literal semantics",
|
|
172
|
+
redact:
|
|
173
|
+
"redact=true masks credential-like values and private-key bodies in displayed evidence; it does not sandbox search or change counts",
|
|
172
174
|
path: "path must be an existing exact file or root; use mode=files+query for unknown names",
|
|
173
175
|
query:
|
|
174
176
|
"files+query matches known filename/path text (not glob patterns); omit files query to list every file under path; concept/hybrid query is natural language",
|
|
@@ -193,6 +195,7 @@ export function fieldGuidance(field: RequestField): string {
|
|
|
193
195
|
|
|
194
196
|
const GUIDANCE_FIELDS = [
|
|
195
197
|
"pattern",
|
|
198
|
+
"redact",
|
|
196
199
|
"literal",
|
|
197
200
|
"path",
|
|
198
201
|
"query",
|
|
@@ -219,12 +222,13 @@ export const REQUEST_USAGE_GUIDANCE = [
|
|
|
219
222
|
|
|
220
223
|
export const MODEL_USAGE_GUIDANCE = [
|
|
221
224
|
"pattern is regex by default; literal=true matches source text exactly",
|
|
222
|
-
"
|
|
223
|
-
"
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
"outline
|
|
227
|
-
|
|
225
|
+
"redact=true masks credentials and private-key bodies in displayed evidence; use it for configs, logs or user directories; it does not sandbox search",
|
|
226
|
+
"path is an existing exact file/root; use files+query for unknown names",
|
|
227
|
+
"anyOf/allOf are exact-literal OR/AND variants; exclude pattern/literal",
|
|
228
|
+
`limit/context are ordinary-search budgets (limit <= ${String(MAX_PAGE_SIZE)}); omit for hybrid/concept/outline/structure/inspect`,
|
|
229
|
+
"outline needs a concrete JS/TS/TSX/Python-capable source file, not a directory; structure needs a nonempty AST pattern for JS/TS/TSX/Go, no lang; use capabilities before unfamiliar language operations",
|
|
230
|
+
"imports/tests are static JS/TS/TSX candidates",
|
|
231
|
+
"selectors: inspect, hybrid and capabilities use their cataloged fields",
|
|
228
232
|
"exact, any-of and max_results are not parameters",
|
|
229
233
|
].join("; ");
|
|
230
234
|
|
package/src/rg.ts
CHANGED
|
@@ -435,6 +435,7 @@ export function createRipgrepRunner(options: RipgrepRunnerOptions = {}) {
|
|
|
435
435
|
maxSourceRevisionFiles,
|
|
436
436
|
signal,
|
|
437
437
|
request.redact,
|
|
438
|
+
request,
|
|
438
439
|
);
|
|
439
440
|
let ignoredFileCount = 0;
|
|
440
441
|
let ignoredFileSamples: string[] = [];
|
|
@@ -475,6 +476,7 @@ export function createRipgrepRunner(options: RipgrepRunnerOptions = {}) {
|
|
|
475
476
|
maxSourceRevisionFiles,
|
|
476
477
|
signal,
|
|
477
478
|
request.redact,
|
|
479
|
+
request,
|
|
478
480
|
);
|
|
479
481
|
const ignored = [...allCandidates.revisions.keys()].filter(
|
|
480
482
|
(path) => !before.revisions.has(path),
|
|
@@ -528,7 +530,7 @@ export function createRipgrepRunner(options: RipgrepRunnerOptions = {}) {
|
|
|
528
530
|
}),
|
|
529
531
|
);
|
|
530
532
|
const diagnostics = classifyRipgrepDiagnostics(stderr);
|
|
531
|
-
const inputError = createRipgrepInputError(stderr, request.redact);
|
|
533
|
+
const inputError = createRipgrepInputError(stderr, request.redact, request);
|
|
532
534
|
if (inputError) throw inputError;
|
|
533
535
|
const filesystemDiagnostics = [...diagnostics.unreadable, ...diagnostics.recoverable];
|
|
534
536
|
if (hasRequestedRootUnreadable(diagnostics.unreadable, cwd, validatedSearchPath))
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
RipgrepInputError,
|
|
4
|
+
type RipgrepInputErrorCode,
|
|
5
|
+
type SiftLightDiagnosticRepairExample,
|
|
6
|
+
} from "./errors.js";
|
|
7
|
+
import { containsSensitiveText, redactDiagnosticText } from "./redaction.js";
|
|
8
|
+
import type { SearchRequest } from "./types.js";
|
|
4
9
|
|
|
5
10
|
export interface RipgrepUnreadableDiagnostic {
|
|
6
11
|
message: string;
|
|
@@ -63,9 +68,85 @@ function missingPathFromDiagnostics(stderr: string): string | undefined {
|
|
|
63
68
|
return undefined;
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
export type RipgrepRecoveryRequest = Pick<
|
|
72
|
+
SearchRequest,
|
|
73
|
+
| "pattern"
|
|
74
|
+
| "path"
|
|
75
|
+
| "glob"
|
|
76
|
+
| "exclude"
|
|
77
|
+
| "literal"
|
|
78
|
+
| "ignoreCase"
|
|
79
|
+
| "hidden"
|
|
80
|
+
| "ignorePolicy"
|
|
81
|
+
| "wholeWord"
|
|
82
|
+
| "context"
|
|
83
|
+
| "scope"
|
|
84
|
+
| "redact"
|
|
85
|
+
>;
|
|
86
|
+
|
|
87
|
+
const RIPGREP_RECOVERY_FIELDS = [
|
|
88
|
+
"path",
|
|
89
|
+
"glob",
|
|
90
|
+
"exclude",
|
|
91
|
+
"literal",
|
|
92
|
+
"ignoreCase",
|
|
93
|
+
"hidden",
|
|
94
|
+
"ignorePolicy",
|
|
95
|
+
"wholeWord",
|
|
96
|
+
"context",
|
|
97
|
+
"scope",
|
|
98
|
+
"redact",
|
|
99
|
+
] as const satisfies readonly (keyof RipgrepRecoveryRequest)[];
|
|
100
|
+
|
|
101
|
+
function copyRipgrepRecoveryFields(request: RipgrepRecoveryRequest): Record<string, unknown> {
|
|
102
|
+
const result: Record<string, unknown> = {};
|
|
103
|
+
for (const field of RIPGREP_RECOVERY_FIELDS) {
|
|
104
|
+
const value = request[field];
|
|
105
|
+
if (value !== undefined) result[field] = value;
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function ripgrepRepairExamples(
|
|
111
|
+
code: RipgrepInputErrorCode,
|
|
112
|
+
request: RipgrepRecoveryRequest | undefined,
|
|
113
|
+
redact: boolean,
|
|
114
|
+
): readonly SiftLightDiagnosticRepairExample[] {
|
|
115
|
+
if (request === undefined || (redact && containsSensitiveText(request))) return [];
|
|
116
|
+
if (code === "E_REGEX_INVALID") {
|
|
117
|
+
return [
|
|
118
|
+
{
|
|
119
|
+
label: "按原文查找",
|
|
120
|
+
request: {
|
|
121
|
+
mode: "matches",
|
|
122
|
+
...copyRipgrepRecoveryFields(request),
|
|
123
|
+
pattern: request.pattern,
|
|
124
|
+
literal: true,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
if (code === "E_SEARCH_PATH_NOT_FOUND" && request.path !== undefined) {
|
|
130
|
+
const fileName = basename(request.path.replaceAll("\\", "/"));
|
|
131
|
+
if (fileName === "." || fileName === "/" || fileName.length === 0) return [];
|
|
132
|
+
return [
|
|
133
|
+
{
|
|
134
|
+
label: "按文件名查找",
|
|
135
|
+
request: {
|
|
136
|
+
mode: "files",
|
|
137
|
+
query: fileName,
|
|
138
|
+
...(request.redact === true ? { redact: true } : {}),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
66
146
|
export function createRipgrepInputError(
|
|
67
147
|
stderr: string,
|
|
68
148
|
redact = false,
|
|
149
|
+
request?: RipgrepRecoveryRequest,
|
|
69
150
|
): RipgrepInputError | undefined {
|
|
70
151
|
const code = classifyRipgrepInputFailure(stderr);
|
|
71
152
|
if (code === "E_REGEX_INVALID") {
|
|
@@ -73,6 +154,7 @@ export function createRipgrepInputError(
|
|
|
73
154
|
code,
|
|
74
155
|
"ripgrep regex parse error: pattern is not a valid regular expression.",
|
|
75
156
|
"Correct pattern or set literal=true for source text; no automatic literal conversion was attempted.",
|
|
157
|
+
ripgrepRepairExamples(code, request, redact),
|
|
76
158
|
);
|
|
77
159
|
}
|
|
78
160
|
if (code === "E_SEARCH_PATH_NOT_FOUND") {
|
|
@@ -82,6 +164,7 @@ export function createRipgrepInputError(
|
|
|
82
164
|
code,
|
|
83
165
|
`A searched path${location} does not exist or became unavailable.`,
|
|
84
166
|
"Provide an existing exact path, or use mode=files with query to discover an unknown filename; no retry or scope expansion was attempted.",
|
|
167
|
+
ripgrepRepairExamples(code, request, redact),
|
|
85
168
|
);
|
|
86
169
|
}
|
|
87
170
|
// Known input failures above intentionally omit ripgrep's raw pattern and path echo.
|
package/src/scan-revisions.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
boundedRipgrepDiagnostic,
|
|
6
6
|
classifyRipgrepDiagnostics,
|
|
7
7
|
createRipgrepInputError,
|
|
8
|
+
type RipgrepRecoveryRequest,
|
|
8
9
|
type RipgrepUnreadableDiagnostic,
|
|
9
10
|
} from "./ripgrep-diagnostics.js";
|
|
10
11
|
import { getSourceRevision, sameSourceRevision } from "./source.js";
|
|
@@ -48,6 +49,7 @@ export async function captureCandidateRevisions(
|
|
|
48
49
|
maxFiles: number,
|
|
49
50
|
signal?: AbortSignal,
|
|
50
51
|
redact = false,
|
|
52
|
+
request?: RipgrepRecoveryRequest,
|
|
51
53
|
): Promise<{
|
|
52
54
|
revisions: Map<string, SourceRevision>;
|
|
53
55
|
unreadable: RipgrepUnreadableDiagnostic[];
|
|
@@ -111,7 +113,7 @@ export async function captureCandidateRevisions(
|
|
|
111
113
|
},
|
|
112
114
|
);
|
|
113
115
|
const diagnostics = classifyRipgrepDiagnostics(result.stderr);
|
|
114
|
-
const inputError = createRipgrepInputError(result.stderr, redact);
|
|
116
|
+
const inputError = createRipgrepInputError(result.stderr, redact, request);
|
|
115
117
|
const unreadable = [...diagnostics.unreadable, ...diagnostics.recoverable, ...metadataFailures];
|
|
116
118
|
if (inputError) throw inputError;
|
|
117
119
|
if (result.code === 2 && diagnostics.other.length === 0 && unreadable.length > 0)
|
package/src/tool-schema.ts
CHANGED
|
@@ -234,7 +234,7 @@ export const siftLightSchema = Type.Object({
|
|
|
234
234
|
redact: Type.Optional(
|
|
235
235
|
Type.Boolean({
|
|
236
236
|
description:
|
|
237
|
-
"Optional display-only masking for credential-like values and private-key bodies. Default false. It never changes searched files, admitted matches, counts
|
|
237
|
+
"Optional display-only masking for credential-like values and private-key bodies. Default false. Use it for configuration, logs and user-directory searches when returned text may contain secrets. It never changes the search scope, searched files, admitted matches, counts or cursor completeness, and it is not a sandbox.",
|
|
238
238
|
}),
|
|
239
239
|
),
|
|
240
240
|
modifiedAfter: Type.Optional(
|