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 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-2",
3
+ "version": "1.0.5-6",
4
4
  "description": "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sift-light",
3
- "version": "1.0.5-2",
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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sift-light",
3
- "version": "1.0.5-2",
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"
@@ -5,7 +5,7 @@
5
5
  "args": [
6
6
  "--yes",
7
7
  "--package",
8
- "sift-light-runtime@npm:sift-light@1.0.5-2",
8
+ "sift-light-runtime@npm:sift-light@1.0.5-6",
9
9
  "sift-light-mcp",
10
10
  "--stdio"
11
11
  ],
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sift-light",
3
- "version": "1.0.5-2",
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-2",
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
- function createRipgrepInputError(stderr, redact = false) {
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-2",
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
- "path is an existing exact file or root; use files+query for an unknown name",
11374
- "anyOf/allOf are exact-literal OR/AND variants and exclude pattern/literal",
11375
- `limit/context are ordinary-search output budgets; limit <= ${String(MAX_PAGE_SIZE)}; omit both for hybrid/concept/outline/structure/inspect`,
11376
- "outline requires a concrete source file, not a directory; structure requires a nonempty AST pattern and JS/TS/TSX/Go sources, no lang field; use capabilities before unfamiliar language operations",
11377
- "outline supports JS/TS/TSX and bounded Python syntax; imports/tests are static candidates for JS/TS/TSX",
11378
- `selectors: ${modeFieldSummary("inspect")}; ${modeFieldSummary("hybrid")}; ${modeFieldSummary("capabilities")}`,
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, or cursor completeness."
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,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sift-light",
3
- "version": "1.0.5-2",
3
+ "version": "1.0.5-6",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "omp": {
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(code: RipgrepInputErrorCode, message: string, guidance: string) {
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
  }
@@ -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-2",
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
- "path is an existing exact file or root; use files+query for an unknown name",
1007
- "anyOf/allOf are exact-literal OR/AND variants and exclude pattern/literal",
1008
- `limit/context are ordinary-search output budgets; limit <= ${String(MAX_PAGE_SIZE)}; omit both for hybrid/concept/outline/structure/inspect`,
1009
- "outline requires a concrete source file, not a directory; structure requires a nonempty AST pattern and JS/TS/TSX/Go sources, no lang field; use capabilities before unfamiliar language operations",
1010
- "outline supports JS/TS/TSX and bounded Python syntax; imports/tests are static candidates for JS/TS/TSX",
1011
- `selectors: ${modeFieldSummary("inspect")}; ${modeFieldSummary("hybrid")}; ${modeFieldSummary("capabilities")}`,
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
- function createRipgrepInputError(stderr, redact = false) {
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, or cursor completeness."
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
- "path is an existing exact file or root; use files+query for an unknown name",
223
- "anyOf/allOf are exact-literal OR/AND variants and exclude pattern/literal",
224
- `limit/context are ordinary-search output budgets; limit <= ${String(MAX_PAGE_SIZE)}; omit both for hybrid/concept/outline/structure/inspect`,
225
- "outline requires a concrete source file, not a directory; structure requires a nonempty AST pattern and JS/TS/TSX/Go sources, no lang field; use capabilities before unfamiliar language operations",
226
- "outline supports JS/TS/TSX and bounded Python syntax; imports/tests are static candidates for JS/TS/TSX",
227
- `selectors: ${modeFieldSummary("inspect")}; ${modeFieldSummary("hybrid")}; ${modeFieldSummary("capabilities")}`,
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 { RipgrepInputError, type RipgrepInputErrorCode } from "./errors.js";
3
- import { redactDiagnosticText } from "./redaction.js";
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.
@@ -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)
@@ -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, or cursor completeness.",
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(