sift-light 1.0.3-3 → 1.0.3-5

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.3-5 — 2026-10-07
4
+
5
+ - Update the MCP SDK to 1.31.0 and Transformers.js to 4.3.1 to remove the dependency vulnerabilities reported in [#111](https://github.com/lightsifter/sift-light/issues/111), including the old ONNX proxy/logging dependency chain. Refresh compatible SDK transitive dependencies in the lockfile.
6
+ - Recompute cached text embeddings after the inference runtime upgrade; the pinned offline model and search interfaces remain unchanged.
7
+
8
+ ## 1.0.3-4 — 2026-10-05
9
+
10
+ - Allow recognized stdin-only filters at any pipeline position, including `cmd | grep pattern | head` and `cmd | grep pattern | wc -l`. Deny file operands, recursive search, file-sourced patterns, input redirection, and unsafe dynamic arguments even when a filter receives piped input ([#109](https://github.com/lightsifter/sift-light/issues/109)).
11
+ - Apply the same input-aware policy to Pi, OMP, and native plugin hooks, including PowerShell file-object pipeline handling.
12
+
3
13
  ## 1.0.3-3 — 2026-10-04
4
14
 
5
15
  - Keep explicit paths outside the MCP project isolated after zero matches by default, and evaluate content-search and file-discovery globs/exclusions from the selected external root. Explicit `scope: "expand"` still retries the MCP project ([#106](https://github.com/lightsifter/sift-light/issues/106)).
package/README.md CHANGED
@@ -179,7 +179,7 @@ Kimi Code's web mode can start plugin MCP servers from its installation director
179
179
 
180
180
  Restart after installation. Native hooks use hard enforcement by default. To keep the native plugin, MCP tool and model guidance without denying conventional searches, start the host with `SIFT_LIGHT_ENFORCE_SEARCH=prefer`; use `off` to disable only hook enforcement. Accepted values are `hard`, `prefer`, and `off`; an unsupported value fails closed instead of silently allowing a search. The setting is inherited from the host process, so a project cannot lower a user's policy merely by committing a repository configuration file. You can still disable the complete integration through Claude Code `/plugin`, Codex `/hooks`, or Kimi `/plugins disable sift-light` followed by `/reload`.
181
181
 
182
- In hard mode, a direct search such as `grep warning report.txt` is denied. A trailing filter such as `cat report.txt | grep warning` remains available because it filters output from a non-search producer; `find src | grep test` and `rg warning src | grep result` remain denied because their pipelines already contain a direct search producer. If one subcommand in a compound shell call is denied, the host executes none of that call. The denial identifies the detected search and tells the agent to retry non-search operations separately.
182
+ In hard mode, a direct search such as `grep warning report.txt` is denied. A recognized stdin-only filter such as `cat report.txt | grep warning | head` remains available at any pipeline position because it filters output from a non-search producer. A filter with a file operand, recursive option, file-sourced pattern, or input redirection is denied even when piped. Put grep options before the pattern and quote shell wildcard characters; unknown options and dynamic arguments fail closed. PowerShell `Select-String` is also denied when a known file-object producer such as `Get-Item` feeds it. `find src | grep test` and `rg warning src | grep result` remain denied because their pipelines contain a direct search producer. If one subcommand in a compound shell call is denied, the host executes none of that call. The denial identifies the detected search and tells the agent to retry non-search operations separately.
183
183
 
184
184
  Local searches stay on your machine. Only grant access to files your agent is allowed to read. HTTP deployments need an authenticated gateway before public exposure; see [Security](SECURITY.md).
185
185
 
package/README.zh-CN.md CHANGED
@@ -179,7 +179,7 @@ Kimi Code 的 web 模式可能从安装目录启动插件 MCP 服务。如果相
179
179
 
180
180
  安装后重启。原生钩子默认使用严格模式。如需保留原生插件、MCP 工具和模型指引,但不硬性拒绝其他搜索,请使用 `SIFT_LIGHT_ENFORCE_SEARCH=prefer` 启动宿主;设置为 `off` 只关闭钩子强制策略。可用值为 `hard`、`prefer` 和 `off`,非法值会安全拒绝并明确报错,不会静默放行。该设置由宿主进程继承,因此项目不能仅靠提交仓库配置文件降低用户的全局策略。仍可通过 Claude Code `/plugin`、Codex `/hooks`,或 Kimi `/plugins disable sift-light` 后执行 `/reload` 来关闭整个集成。
181
181
 
182
- 严格模式下,`grep warning report.txt` 这类直接搜索会被拒绝;`cat report.txt | grep warning` 仍可用,因为它只是过滤一个非搜索命令的输出。`find src | grep test` 和 `rg warning src | grep result` 仍会被拒绝,因为管道中已经包含直接搜索来源。复合 shell 调用中只要一个子命令被拒绝,宿主就不会执行其中任何操作;拒绝信息会指出检测到的搜索,并要求 Agent 单独重试非搜索操作。
182
+ 严格模式下,`grep warning report.txt` 这类直接搜索会被拒绝;`cat report.txt | grep warning | head` 这类可静态确认只读取标准输入的过滤命令可放在管道中的任意位置,因为它过滤的是非搜索命令的输出。即使位于管道中,带文件参数、递归选项、从文件读取模式或输入重定向的过滤命令仍会被拒绝。请把 grep 选项放在模式之前,并用引号保护 shell 通配符;无法识别的选项和动态参数会保守拦截。PowerShell 中,`Get-Item` 等已知会输出文件对象的命令接入 `Select-String` 时也会被拒绝。`find src | grep test` 和 `rg warning src | grep result` 仍会被拒绝,因为管道中包含直接搜索来源。复合 shell 调用中只要一个子命令被拒绝,宿主就不会执行其中任何操作;拒绝信息会指出检测到的搜索,并要求 Agent 单独重试非搜索操作。
183
183
 
184
184
  本地搜索在你的机器上进行。请只允许 Agent 读取已获授权的文件。HTTP 服务对外开放前需要认证网关,详见[安全说明](SECURITY.md)。
185
185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sift-light",
3
- "version": "1.0.3-3",
3
+ "version": "1.0.3-5",
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",
@@ -84,8 +84,8 @@
84
84
  "dependencies": {
85
85
  "@ast-grep/lang-go": "0.0.6",
86
86
  "@ast-grep/napi": "0.45.2",
87
- "@huggingface/transformers": "3.8.1",
88
- "@modelcontextprotocol/sdk": "1.30.0",
87
+ "@huggingface/transformers": "4.3.1",
88
+ "@modelcontextprotocol/sdk": "1.31.0",
89
89
  "@vscode/ripgrep": "1.18.0",
90
90
  "typebox": "1.3.19",
91
91
  "web-tree-sitter": "0.25.10",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sift-light",
3
- "version": "1.0.3-3",
3
+ "version": "1.0.3-5",
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.3-3",
3
+ "version": "1.0.3-5",
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.3-3",
8
+ "sift-light-runtime@npm:sift-light@1.0.3-5",
9
9
  "sift-light-mcp",
10
10
  "--stdio"
11
11
  ],
@@ -30,7 +30,7 @@ var MAX_CONCEPT_CHARS = 1000;
30
30
  var CONCEPT_MODEL_TOKENS = 256;
31
31
  var CONCEPT_WINDOW_OVERLAP_TOKENS = 64;
32
32
  var CONCEPT_EMBEDDING_DIMENSIONS = 384;
33
- var CONCEPT_CACHE_VERSION = 2;
33
+ var CONCEPT_CACHE_VERSION = 3;
34
34
  var CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
35
35
  var CONCEPT_TIMEOUT_MS = 10 * 60000;
36
36
  var MAX_CONCEPT_TIMEOUT_MS = 60 * 60000;
@@ -3779,9 +3779,182 @@ function classifyCommand(argv, language, depth = 0) {
3779
3779
  return classifyCommand(args2.slice(start2), language, depth + 1);
3780
3780
  }
3781
3781
 
3782
+ // src/search-policy-stdin-filter.ts
3783
+ var GREP_SWITCHES = new Set("abcEFGHhiILlnoPqsTUuvwxyzZ".split(""));
3784
+ var GREP_VALUE_OPTIONS = new Set(["A", "B", "C", "m"]);
3785
+ var GREP_LONG_SWITCHES = new Set([
3786
+ "--basic-regexp",
3787
+ "--extended-regexp",
3788
+ "--fixed-strings",
3789
+ "--perl-regexp",
3790
+ "--ignore-case",
3791
+ "--no-ignore-case",
3792
+ "--invert-match",
3793
+ "--word-regexp",
3794
+ "--line-regexp",
3795
+ "--count",
3796
+ "--files-with-matches",
3797
+ "--files-without-match",
3798
+ "--only-matching",
3799
+ "--quiet",
3800
+ "--silent",
3801
+ "--no-messages",
3802
+ "--byte-offset",
3803
+ "--line-number",
3804
+ "--with-filename",
3805
+ "--no-filename",
3806
+ "--initial-tab",
3807
+ "--null",
3808
+ "--null-data",
3809
+ "--text",
3810
+ "--binary",
3811
+ "--line-buffered",
3812
+ "--no-group-separator"
3813
+ ]);
3814
+ var GREP_LONG_VALUES = new Set([
3815
+ "--after-context",
3816
+ "--before-context",
3817
+ "--context",
3818
+ "--max-count",
3819
+ "--binary-files",
3820
+ "--label",
3821
+ "--group-separator",
3822
+ "--include",
3823
+ "--exclude",
3824
+ "--exclude-dir"
3825
+ ]);
3826
+ function isStdinOnlyGrep(args2) {
3827
+ const positionals = [];
3828
+ let hasPatternOption = false;
3829
+ let optionsEnded = false;
3830
+ for (let index = 0;index < args2.length; index += 1) {
3831
+ const arg = args2[index];
3832
+ if (arg === null || arg === undefined)
3833
+ return false;
3834
+ if (positionals.length > 0 && arg !== "-")
3835
+ return false;
3836
+ if (!optionsEnded && arg === "--") {
3837
+ optionsEnded = true;
3838
+ continue;
3839
+ }
3840
+ if (!optionsEnded && arg.startsWith("--")) {
3841
+ const separator = arg.indexOf("=");
3842
+ const option = separator < 0 ? arg : arg.slice(0, separator);
3843
+ const attached = separator < 0 ? undefined : arg.slice(separator + 1);
3844
+ if (option === "--regexp") {
3845
+ hasPatternOption = true;
3846
+ } else if (option === "--color" || option === "--colour") {
3847
+ continue;
3848
+ } else if (GREP_LONG_SWITCHES.has(option) && attached === undefined) {
3849
+ continue;
3850
+ } else if (!GREP_LONG_VALUES.has(option)) {
3851
+ return false;
3852
+ }
3853
+ if (attached === undefined) {
3854
+ index += 1;
3855
+ if (args2[index] === null || args2[index] === undefined)
3856
+ return false;
3857
+ }
3858
+ continue;
3859
+ }
3860
+ if (!optionsEnded && arg.startsWith("-") && arg !== "-") {
3861
+ for (let optionIndex = 1;optionIndex < arg.length; optionIndex += 1) {
3862
+ const option = arg[optionIndex];
3863
+ if (option === "e") {
3864
+ hasPatternOption = true;
3865
+ } else if (option !== undefined && GREP_SWITCHES.has(option)) {
3866
+ continue;
3867
+ } else if (option === undefined || !GREP_VALUE_OPTIONS.has(option)) {
3868
+ return false;
3869
+ }
3870
+ if (optionIndex === arg.length - 1) {
3871
+ index += 1;
3872
+ if (args2[index] === null || args2[index] === undefined)
3873
+ return false;
3874
+ }
3875
+ break;
3876
+ }
3877
+ continue;
3878
+ }
3879
+ positionals.push(arg);
3880
+ }
3881
+ return hasPatternOption ? positionals.every((operand) => operand === "-") : positionals.length > 0 && positionals.slice(1).every((operand) => operand === "-");
3882
+ }
3883
+ var SELECT_STRING_SWITCHES = new Set([
3884
+ "simplematch",
3885
+ "casesensitive",
3886
+ "quiet",
3887
+ "list",
3888
+ "noemphasis",
3889
+ "notmatch",
3890
+ "allmatches",
3891
+ "raw",
3892
+ "verbose",
3893
+ "debug"
3894
+ ]);
3895
+ var SELECT_STRING_VALUES = new Set([
3896
+ "culture",
3897
+ "encoding",
3898
+ "context",
3899
+ "erroraction",
3900
+ "warningaction",
3901
+ "informationaction",
3902
+ "outvariable",
3903
+ "pipelinevariable"
3904
+ ]);
3905
+ function isPipelineOnlySelectString(args2, namedParameters) {
3906
+ let hasPattern = false;
3907
+ for (let index = 0;index < args2.length; index += 1) {
3908
+ const arg = args2[index];
3909
+ if (arg === null || arg === undefined)
3910
+ return false;
3911
+ if (!namedParameters[index]) {
3912
+ if (hasPattern)
3913
+ return false;
3914
+ hasPattern = true;
3915
+ continue;
3916
+ }
3917
+ const separator = arg.indexOf(":");
3918
+ const option = (separator < 0 ? arg.slice(1) : arg.slice(1, separator)).toLowerCase();
3919
+ if (SELECT_STRING_SWITCHES.has(option) && separator < 0)
3920
+ continue;
3921
+ if (option !== "pattern" && !SELECT_STRING_VALUES.has(option))
3922
+ return false;
3923
+ if (option === "pattern") {
3924
+ if (hasPattern)
3925
+ return false;
3926
+ hasPattern = true;
3927
+ }
3928
+ if (separator < 0) {
3929
+ index += 1;
3930
+ if (args2[index] === null || args2[index] === undefined)
3931
+ return false;
3932
+ }
3933
+ }
3934
+ return hasPattern;
3935
+ }
3936
+ function isStdinOnlySearchFilter(words, language, namedParameters) {
3937
+ const executable = words[0];
3938
+ if (executable === null || executable === undefined)
3939
+ return false;
3940
+ const name2 = executableName(executable);
3941
+ if (language === "powershell")
3942
+ return ["select-string", "sls"].includes(name2.toLowerCase()) && isPipelineOnlySelectString(words.slice(1), namedParameters.slice(1));
3943
+ return ["grep", "egrep", "fgrep"].includes(name2) && isStdinOnlyGrep(words.slice(1));
3944
+ }
3945
+
3782
3946
  // src/search-policy-shell.ts
3783
3947
  var MAX_POLICY_COMMAND_BYTES = 64 * 1024;
3784
3948
  var MAX_SHELL_NESTING = 4;
3949
+ var POWERSHELL_FILE_OBJECT_PRODUCERS = new Set([
3950
+ "get-item",
3951
+ "gi",
3952
+ "get-childitem",
3953
+ "gci",
3954
+ "dir",
3955
+ "ls"
3956
+ ]);
3957
+ var POWERSHELL_STRING_CONVERTERS = new Set(["out-string", "oss", "convertto-json"]);
3785
3958
  function decodeBashDoubleQuoted(value) {
3786
3959
  let result = "";
3787
3960
  for (let index = 0;index < value.length; index += 1) {
@@ -3867,6 +4040,18 @@ function commandWordNodes(node, language) {
3867
4040
  function commandWords(node, language) {
3868
4041
  return commandWordNodes(node, language).map((word) => literalWord(word, language));
3869
4042
  }
4043
+ function hasBashWordExpansion(text) {
4044
+ if (text.startsWith("~") || text.startsWith("="))
4045
+ return true;
4046
+ for (let index = 0;index < text.length; index += 1) {
4047
+ if (text[index] === "\\") {
4048
+ index += 1;
4049
+ } else if ("*?[]{}".includes(text[index] ?? "")) {
4050
+ return true;
4051
+ }
4052
+ }
4053
+ return false;
4054
+ }
3870
4055
  function hasUntranslatedShellSyntax(node, language) {
3871
4056
  const text = node.text;
3872
4057
  if (language === "powershell") {
@@ -3879,30 +4064,70 @@ function hasUntranslatedShellSyntax(node, language) {
3879
4064
  if (["raw_string", "string", "string_content"].includes(node.type))
3880
4065
  return false;
3881
4066
  if (["word", "number", "command_name"].includes(node.type))
3882
- return /[*?[\]{}]/u.test(text) || text.startsWith("~") || text.startsWith("=");
4067
+ return hasBashWordExpansion(text);
3883
4068
  if (node.type === "concatenation")
3884
4069
  return node.namedChildren.some((child) => child !== null && hasUntranslatedShellSyntax(child, language));
3885
4070
  return false;
3886
4071
  }
4072
+ function hasInputRedirectNodes(owner) {
4073
+ return owner.descendantsOfType(["file_redirect", "herestring_redirect", "heredoc_redirect"]).some((redirect) => redirect !== null && /^(?:\d+)?</u.test(redirect.text));
4074
+ }
4075
+ function hasInputRedirect(node) {
4076
+ if (hasInputRedirectNodes(node))
4077
+ return true;
4078
+ for (let parent = node.parent;parent; parent = parent.parent) {
4079
+ if (parent.type !== "redirected_statement")
4080
+ continue;
4081
+ const body2 = parent.childForFieldName("body");
4082
+ const last = body2?.descendantsOfType("command").at(-1) ?? (body2?.type === "command" ? body2 : undefined);
4083
+ if (last?.startIndex === node.startIndex && last.endIndex === node.endIndex && parent.childrenForFieldName("redirect").some((redirect) => redirect !== null && hasInputRedirectNodes(redirect)))
4084
+ return true;
4085
+ }
4086
+ return false;
4087
+ }
3887
4088
  function isSafePipelineFilter(node, language, words) {
3888
- const executable = words[0];
3889
- if (executable === null || executable === undefined)
4089
+ const wordNodes = commandWordNodes(node, language);
4090
+ if (!isStdinOnlySearchFilter(words, language, wordNodes.map((word) => word.type === "command_parameter")))
3890
4091
  return false;
3891
- const name2 = executableName(executable);
3892
- const filterNames = language === "powershell" ? new Set(["select-string", "sls"]) : new Set(["grep", "egrep", "fgrep"]);
3893
- if (!filterNames.has(language === "powershell" ? name2.toLowerCase() : name2))
4092
+ if (wordNodes.some((word) => hasUntranslatedShellSyntax(word, language)))
4093
+ return false;
4094
+ if (hasInputRedirect(node))
3894
4095
  return false;
3895
4096
  const pipeline = node.parent;
3896
- if (!pipeline || pipeline.type !== "pipeline")
4097
+ if (!pipeline || !["pipeline", "pipeline_chain"].includes(pipeline.type))
3897
4098
  return false;
3898
- const commands = pipeline.namedChildren.filter((child) => child !== null && child.type === "command");
3899
- const last = commands.at(-1);
3900
- if (!last || last.startIndex !== node.startIndex || last.endIndex !== node.endIndex || commands.length < 2)
4099
+ const stages = pipeline.namedChildren.filter((child) => child !== null);
4100
+ const position = stages.findIndex((stage) => stage.startIndex <= node.startIndex && stage.endIndex >= node.endIndex);
4101
+ if (position < 1)
3901
4102
  return false;
3902
- return commands.slice(0, -1).every((candidate) => {
3903
- const decision = classifyCommand(commandWords(candidate, language), language);
3904
- return !decision.kind && !decision.nested;
3905
- });
4103
+ let fileObjectFlow = false;
4104
+ for (const [index, stage] of stages.entries()) {
4105
+ if (index === position)
4106
+ continue;
4107
+ const commands = stage.descendantsOfType("command");
4108
+ for (const candidate of commands) {
4109
+ if (candidate === null)
4110
+ continue;
4111
+ const candidateWords = commandWords(candidate, language);
4112
+ const decision = classifyCommand(candidateWords, language);
4113
+ if (decision.kind || decision.nested)
4114
+ return false;
4115
+ if (language !== "powershell" || index > position)
4116
+ continue;
4117
+ const executable = candidateWords[0];
4118
+ if (executable === null || executable === undefined)
4119
+ continue;
4120
+ const name2 = executableName(executable).toLowerCase();
4121
+ if (POWERSHELL_FILE_OBJECT_PRODUCERS.has(name2))
4122
+ fileObjectFlow = true;
4123
+ }
4124
+ if (language === "powershell" && index < position && stage.type === "command") {
4125
+ const executable = commandWords(stage, language)[0];
4126
+ if (executable !== null && executable !== undefined && POWERSHELL_STRING_CONVERTERS.has(executableName(executable).toLowerCase()))
4127
+ fileObjectFlow = false;
4128
+ }
4129
+ }
4130
+ return !fileObjectFlow;
3906
4131
  }
3907
4132
 
3908
4133
  class ShellSearchPolicy {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sift-light",
3
- "version": "1.0.3-3",
3
+ "version": "1.0.3-5",
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.3-3",
16
+ "sift-light-runtime@npm:sift-light@1.0.3-5",
17
17
  "sift-light-mcp",
18
18
  "--stdio"
19
19
  ],
@@ -1845,7 +1845,7 @@ function createCtagsStructureProvider(options = {}) {
1845
1845
  // package.json
1846
1846
  var package_default = {
1847
1847
  name: "sift-light",
1848
- version: "1.0.3-3",
1848
+ version: "1.0.3-5",
1849
1849
  description: "Context-efficient local search for files, documents, notes and logs across Pi, OMP and MCP clients",
1850
1850
  keywords: [
1851
1851
  "ai-agent",
@@ -1929,8 +1929,8 @@ var package_default = {
1929
1929
  dependencies: {
1930
1930
  "@ast-grep/lang-go": "0.0.6",
1931
1931
  "@ast-grep/napi": "0.45.2",
1932
- "@huggingface/transformers": "3.8.1",
1933
- "@modelcontextprotocol/sdk": "1.30.0",
1932
+ "@huggingface/transformers": "4.3.1",
1933
+ "@modelcontextprotocol/sdk": "1.31.0",
1934
1934
  "@vscode/ripgrep": "1.18.0",
1935
1935
  typebox: "1.3.19",
1936
1936
  "web-tree-sitter": "0.25.10",
@@ -2216,7 +2216,7 @@ var CONCEPT_MODEL = "Xenova/multilingual-e5-small";
2216
2216
  var CONCEPT_REVISION = "761b726dd34fb83930e26aab4e9ac3899aa1fa78";
2217
2217
  var MAX_CONCEPT_CHARS = 1000;
2218
2218
  var CONCEPT_PASSAGE_OVERLAP_CHARS = 160;
2219
- var CONCEPT_CACHE_VERSION = 2;
2219
+ var CONCEPT_CACHE_VERSION = 3;
2220
2220
  var CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
2221
2221
  var CONCEPT_TIMEOUT_MS = 10 * 60000;
2222
2222
  var MIN_CONCEPT_TIMEOUT_MS = 1000;
@@ -20458,9 +20458,182 @@ function classifyCommand(argv, language, depth = 0) {
20458
20458
  return classifyCommand(args2.slice(start2), language, depth + 1);
20459
20459
  }
20460
20460
 
20461
+ // src/search-policy-stdin-filter.ts
20462
+ var GREP_SWITCHES = new Set("abcEFGHhiILlnoPqsTUuvwxyzZ".split(""));
20463
+ var GREP_VALUE_OPTIONS = new Set(["A", "B", "C", "m"]);
20464
+ var GREP_LONG_SWITCHES = new Set([
20465
+ "--basic-regexp",
20466
+ "--extended-regexp",
20467
+ "--fixed-strings",
20468
+ "--perl-regexp",
20469
+ "--ignore-case",
20470
+ "--no-ignore-case",
20471
+ "--invert-match",
20472
+ "--word-regexp",
20473
+ "--line-regexp",
20474
+ "--count",
20475
+ "--files-with-matches",
20476
+ "--files-without-match",
20477
+ "--only-matching",
20478
+ "--quiet",
20479
+ "--silent",
20480
+ "--no-messages",
20481
+ "--byte-offset",
20482
+ "--line-number",
20483
+ "--with-filename",
20484
+ "--no-filename",
20485
+ "--initial-tab",
20486
+ "--null",
20487
+ "--null-data",
20488
+ "--text",
20489
+ "--binary",
20490
+ "--line-buffered",
20491
+ "--no-group-separator"
20492
+ ]);
20493
+ var GREP_LONG_VALUES = new Set([
20494
+ "--after-context",
20495
+ "--before-context",
20496
+ "--context",
20497
+ "--max-count",
20498
+ "--binary-files",
20499
+ "--label",
20500
+ "--group-separator",
20501
+ "--include",
20502
+ "--exclude",
20503
+ "--exclude-dir"
20504
+ ]);
20505
+ function isStdinOnlyGrep(args2) {
20506
+ const positionals = [];
20507
+ let hasPatternOption = false;
20508
+ let optionsEnded = false;
20509
+ for (let index = 0;index < args2.length; index += 1) {
20510
+ const arg = args2[index];
20511
+ if (arg === null || arg === undefined)
20512
+ return false;
20513
+ if (positionals.length > 0 && arg !== "-")
20514
+ return false;
20515
+ if (!optionsEnded && arg === "--") {
20516
+ optionsEnded = true;
20517
+ continue;
20518
+ }
20519
+ if (!optionsEnded && arg.startsWith("--")) {
20520
+ const separator = arg.indexOf("=");
20521
+ const option = separator < 0 ? arg : arg.slice(0, separator);
20522
+ const attached = separator < 0 ? undefined : arg.slice(separator + 1);
20523
+ if (option === "--regexp") {
20524
+ hasPatternOption = true;
20525
+ } else if (option === "--color" || option === "--colour") {
20526
+ continue;
20527
+ } else if (GREP_LONG_SWITCHES.has(option) && attached === undefined) {
20528
+ continue;
20529
+ } else if (!GREP_LONG_VALUES.has(option)) {
20530
+ return false;
20531
+ }
20532
+ if (attached === undefined) {
20533
+ index += 1;
20534
+ if (args2[index] === null || args2[index] === undefined)
20535
+ return false;
20536
+ }
20537
+ continue;
20538
+ }
20539
+ if (!optionsEnded && arg.startsWith("-") && arg !== "-") {
20540
+ for (let optionIndex = 1;optionIndex < arg.length; optionIndex += 1) {
20541
+ const option = arg[optionIndex];
20542
+ if (option === "e") {
20543
+ hasPatternOption = true;
20544
+ } else if (option !== undefined && GREP_SWITCHES.has(option)) {
20545
+ continue;
20546
+ } else if (option === undefined || !GREP_VALUE_OPTIONS.has(option)) {
20547
+ return false;
20548
+ }
20549
+ if (optionIndex === arg.length - 1) {
20550
+ index += 1;
20551
+ if (args2[index] === null || args2[index] === undefined)
20552
+ return false;
20553
+ }
20554
+ break;
20555
+ }
20556
+ continue;
20557
+ }
20558
+ positionals.push(arg);
20559
+ }
20560
+ return hasPatternOption ? positionals.every((operand) => operand === "-") : positionals.length > 0 && positionals.slice(1).every((operand) => operand === "-");
20561
+ }
20562
+ var SELECT_STRING_SWITCHES = new Set([
20563
+ "simplematch",
20564
+ "casesensitive",
20565
+ "quiet",
20566
+ "list",
20567
+ "noemphasis",
20568
+ "notmatch",
20569
+ "allmatches",
20570
+ "raw",
20571
+ "verbose",
20572
+ "debug"
20573
+ ]);
20574
+ var SELECT_STRING_VALUES = new Set([
20575
+ "culture",
20576
+ "encoding",
20577
+ "context",
20578
+ "erroraction",
20579
+ "warningaction",
20580
+ "informationaction",
20581
+ "outvariable",
20582
+ "pipelinevariable"
20583
+ ]);
20584
+ function isPipelineOnlySelectString(args2, namedParameters) {
20585
+ let hasPattern = false;
20586
+ for (let index = 0;index < args2.length; index += 1) {
20587
+ const arg = args2[index];
20588
+ if (arg === null || arg === undefined)
20589
+ return false;
20590
+ if (!namedParameters[index]) {
20591
+ if (hasPattern)
20592
+ return false;
20593
+ hasPattern = true;
20594
+ continue;
20595
+ }
20596
+ const separator = arg.indexOf(":");
20597
+ const option = (separator < 0 ? arg.slice(1) : arg.slice(1, separator)).toLowerCase();
20598
+ if (SELECT_STRING_SWITCHES.has(option) && separator < 0)
20599
+ continue;
20600
+ if (option !== "pattern" && !SELECT_STRING_VALUES.has(option))
20601
+ return false;
20602
+ if (option === "pattern") {
20603
+ if (hasPattern)
20604
+ return false;
20605
+ hasPattern = true;
20606
+ }
20607
+ if (separator < 0) {
20608
+ index += 1;
20609
+ if (args2[index] === null || args2[index] === undefined)
20610
+ return false;
20611
+ }
20612
+ }
20613
+ return hasPattern;
20614
+ }
20615
+ function isStdinOnlySearchFilter(words, language, namedParameters) {
20616
+ const executable = words[0];
20617
+ if (executable === null || executable === undefined)
20618
+ return false;
20619
+ const name2 = executableName(executable);
20620
+ if (language === "powershell")
20621
+ return ["select-string", "sls"].includes(name2.toLowerCase()) && isPipelineOnlySelectString(words.slice(1), namedParameters.slice(1));
20622
+ return ["grep", "egrep", "fgrep"].includes(name2) && isStdinOnlyGrep(words.slice(1));
20623
+ }
20624
+
20461
20625
  // src/search-policy-shell.ts
20462
20626
  var MAX_POLICY_COMMAND_BYTES = 64 * 1024;
20463
20627
  var MAX_SHELL_NESTING = 4;
20628
+ var POWERSHELL_FILE_OBJECT_PRODUCERS = new Set([
20629
+ "get-item",
20630
+ "gi",
20631
+ "get-childitem",
20632
+ "gci",
20633
+ "dir",
20634
+ "ls"
20635
+ ]);
20636
+ var POWERSHELL_STRING_CONVERTERS = new Set(["out-string", "oss", "convertto-json"]);
20464
20637
  function decodeBashDoubleQuoted(value) {
20465
20638
  let result = "";
20466
20639
  for (let index = 0;index < value.length; index += 1) {
@@ -20546,6 +20719,18 @@ function commandWordNodes(node, language) {
20546
20719
  function commandWords(node, language) {
20547
20720
  return commandWordNodes(node, language).map((word) => literalWord(word, language));
20548
20721
  }
20722
+ function hasBashWordExpansion(text) {
20723
+ if (text.startsWith("~") || text.startsWith("="))
20724
+ return true;
20725
+ for (let index = 0;index < text.length; index += 1) {
20726
+ if (text[index] === "\\") {
20727
+ index += 1;
20728
+ } else if ("*?[]{}".includes(text[index] ?? "")) {
20729
+ return true;
20730
+ }
20731
+ }
20732
+ return false;
20733
+ }
20549
20734
  function hasUntranslatedShellSyntax(node, language) {
20550
20735
  const text = node.text;
20551
20736
  if (language === "powershell") {
@@ -20558,30 +20743,70 @@ function hasUntranslatedShellSyntax(node, language) {
20558
20743
  if (["raw_string", "string", "string_content"].includes(node.type))
20559
20744
  return false;
20560
20745
  if (["word", "number", "command_name"].includes(node.type))
20561
- return /[*?[\]{}]/u.test(text) || text.startsWith("~") || text.startsWith("=");
20746
+ return hasBashWordExpansion(text);
20562
20747
  if (node.type === "concatenation")
20563
20748
  return node.namedChildren.some((child) => child !== null && hasUntranslatedShellSyntax(child, language));
20564
20749
  return false;
20565
20750
  }
20751
+ function hasInputRedirectNodes(owner) {
20752
+ return owner.descendantsOfType(["file_redirect", "herestring_redirect", "heredoc_redirect"]).some((redirect) => redirect !== null && /^(?:\d+)?</u.test(redirect.text));
20753
+ }
20754
+ function hasInputRedirect(node) {
20755
+ if (hasInputRedirectNodes(node))
20756
+ return true;
20757
+ for (let parent = node.parent;parent; parent = parent.parent) {
20758
+ if (parent.type !== "redirected_statement")
20759
+ continue;
20760
+ const body2 = parent.childForFieldName("body");
20761
+ const last = body2?.descendantsOfType("command").at(-1) ?? (body2?.type === "command" ? body2 : undefined);
20762
+ if (last?.startIndex === node.startIndex && last.endIndex === node.endIndex && parent.childrenForFieldName("redirect").some((redirect) => redirect !== null && hasInputRedirectNodes(redirect)))
20763
+ return true;
20764
+ }
20765
+ return false;
20766
+ }
20566
20767
  function isSafePipelineFilter(node, language, words) {
20567
- const executable = words[0];
20568
- if (executable === null || executable === undefined)
20768
+ const wordNodes = commandWordNodes(node, language);
20769
+ if (!isStdinOnlySearchFilter(words, language, wordNodes.map((word) => word.type === "command_parameter")))
20569
20770
  return false;
20570
- const name2 = executableName(executable);
20571
- const filterNames = language === "powershell" ? new Set(["select-string", "sls"]) : new Set(["grep", "egrep", "fgrep"]);
20572
- if (!filterNames.has(language === "powershell" ? name2.toLowerCase() : name2))
20771
+ if (wordNodes.some((word) => hasUntranslatedShellSyntax(word, language)))
20772
+ return false;
20773
+ if (hasInputRedirect(node))
20573
20774
  return false;
20574
20775
  const pipeline = node.parent;
20575
- if (!pipeline || pipeline.type !== "pipeline")
20776
+ if (!pipeline || !["pipeline", "pipeline_chain"].includes(pipeline.type))
20576
20777
  return false;
20577
- const commands = pipeline.namedChildren.filter((child) => child !== null && child.type === "command");
20578
- const last = commands.at(-1);
20579
- if (!last || last.startIndex !== node.startIndex || last.endIndex !== node.endIndex || commands.length < 2)
20778
+ const stages = pipeline.namedChildren.filter((child) => child !== null);
20779
+ const position = stages.findIndex((stage) => stage.startIndex <= node.startIndex && stage.endIndex >= node.endIndex);
20780
+ if (position < 1)
20580
20781
  return false;
20581
- return commands.slice(0, -1).every((candidate) => {
20582
- const decision = classifyCommand(commandWords(candidate, language), language);
20583
- return !decision.kind && !decision.nested;
20584
- });
20782
+ let fileObjectFlow = false;
20783
+ for (const [index, stage] of stages.entries()) {
20784
+ if (index === position)
20785
+ continue;
20786
+ const commands = stage.descendantsOfType("command");
20787
+ for (const candidate of commands) {
20788
+ if (candidate === null)
20789
+ continue;
20790
+ const candidateWords = commandWords(candidate, language);
20791
+ const decision = classifyCommand(candidateWords, language);
20792
+ if (decision.kind || decision.nested)
20793
+ return false;
20794
+ if (language !== "powershell" || index > position)
20795
+ continue;
20796
+ const executable = candidateWords[0];
20797
+ if (executable === null || executable === undefined)
20798
+ continue;
20799
+ const name2 = executableName(executable).toLowerCase();
20800
+ if (POWERSHELL_FILE_OBJECT_PRODUCERS.has(name2))
20801
+ fileObjectFlow = true;
20802
+ }
20803
+ if (language === "powershell" && index < position && stage.type === "command") {
20804
+ const executable = commandWords(stage, language)[0];
20805
+ if (executable !== null && executable !== undefined && POWERSHELL_STRING_CONVERTERS.has(executableName(executable).toLowerCase()))
20806
+ fileObjectFlow = false;
20807
+ }
20808
+ }
20809
+ return !fileObjectFlow;
20585
20810
  }
20586
20811
 
20587
20812
  class ShellSearchPolicy {
@@ -20663,7 +20888,7 @@ class ShellSearchPolicy {
20663
20888
  }
20664
20889
 
20665
20890
  // src/search-policy.ts
20666
- var SEARCH_POLICY_GUIDANCE = "Local content and filename searches must use sift-light. Built-in search tools and direct search commands are blocked before execution; filtering output from an unrelated producer at a pipeline tail remains available. Use pattern for contents or mode=files with query for filenames. Keep read/edit/write, tests and builds available. After a denial, call sift-light once with the stated repair; do not paste the denial into the request, repeat the blocked call, use another shell/custom script, or weaken the search mode.";
20891
+ var SEARCH_POLICY_GUIDANCE = "Local content and filename searches must use sift-light. Built-in search tools and direct search commands are blocked before execution; recognized stdin-only filtering of output from an unrelated producer remains available at any pipeline position. File operands, recursive search options, file-sourced patterns and input redirection are not filter exceptions. Use pattern for contents or mode=files with query for filenames. Keep read/edit/write, tests and builds available. After a denial, call sift-light once with the stated repair; do not paste the denial into the request, repeat the blocked call, use another shell/custom script, or weaken the search mode.";
20667
20892
  var PI_REPLACED_SEARCH_TOOLS = new Set(["grep", "find"]);
20668
20893
  var PREFERRED_SEARCH_GUIDANCE = "Prefer sift-light for local content and filename searches because it provides bounded evidence, coverage and continuation details. Conventional search entries remain available in advisory mode.";
20669
20894
  var contentTools = new Set(["grep", "Grep", "SearchFileContent"]);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sift-light",
3
- "version": "1.0.3-3",
3
+ "version": "1.0.3-5",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "omp": {
@@ -11,7 +11,8 @@ export const CONCEPT_PASSAGE_OVERLAP_CHARS = 160;
11
11
  export const CONCEPT_MODEL_TOKENS = 256;
12
12
  export const CONCEPT_WINDOW_OVERLAP_TOKENS = 64;
13
13
  export const CONCEPT_EMBEDDING_DIMENSIONS = 384;
14
- export const CONCEPT_CACHE_VERSION = 2;
14
+ // Transformers 4 / ONNX 1.30 embeddings must not reuse the previous runtime's vectors.
15
+ export const CONCEPT_CACHE_VERSION = 3;
15
16
  export const CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
16
17
  export const CONCEPT_TIMEOUT_MS = 10 * 60_000;
17
18
  export const MIN_CONCEPT_TIMEOUT_MS = 1_000;
@@ -30,7 +30,7 @@ var MAX_CONCEPT_CHARS = 1000;
30
30
  var CONCEPT_MODEL_TOKENS = 256;
31
31
  var CONCEPT_WINDOW_OVERLAP_TOKENS = 64;
32
32
  var CONCEPT_EMBEDDING_DIMENSIONS = 384;
33
- var CONCEPT_CACHE_VERSION = 2;
33
+ var CONCEPT_CACHE_VERSION = 3;
34
34
  var CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
35
35
  var CONCEPT_TIMEOUT_MS = 10 * 60000;
36
36
  var MAX_CONCEPT_TIMEOUT_MS = 60 * 60000;
@@ -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.3-3",
10
+ version: "1.0.3-5",
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",
@@ -91,8 +91,8 @@ var package_default = {
91
91
  dependencies: {
92
92
  "@ast-grep/lang-go": "0.0.6",
93
93
  "@ast-grep/napi": "0.45.2",
94
- "@huggingface/transformers": "3.8.1",
95
- "@modelcontextprotocol/sdk": "1.30.0",
94
+ "@huggingface/transformers": "4.3.1",
95
+ "@modelcontextprotocol/sdk": "1.31.0",
96
96
  "@vscode/ripgrep": "1.18.0",
97
97
  typebox: "1.3.19",
98
98
  "web-tree-sitter": "0.25.10",
@@ -2913,7 +2913,7 @@ var CONCEPT_MODEL = "Xenova/multilingual-e5-small";
2913
2913
  var CONCEPT_REVISION = "761b726dd34fb83930e26aab4e9ac3899aa1fa78";
2914
2914
  var MAX_CONCEPT_CHARS = 1000;
2915
2915
  var CONCEPT_PASSAGE_OVERLAP_CHARS = 160;
2916
- var CONCEPT_CACHE_VERSION = 2;
2916
+ var CONCEPT_CACHE_VERSION = 3;
2917
2917
  var CONCEPT_CACHE_MAX_BYTES = 512 * 1024 * 1024;
2918
2918
  var CONCEPT_TIMEOUT_MS = 10 * 60000;
2919
2919
  var MIN_CONCEPT_TIMEOUT_MS = 1000;
@@ -6,9 +6,19 @@ import {
6
6
  type SearchKind,
7
7
  type ShellLanguage,
8
8
  } from "./search-policy-commands.js";
9
+ import { isStdinOnlySearchFilter } from "./search-policy-stdin-filter.js";
9
10
 
10
11
  export const MAX_POLICY_COMMAND_BYTES = 64 * 1024;
11
12
  const MAX_SHELL_NESTING = 4;
13
+ const POWERSHELL_FILE_OBJECT_PRODUCERS = new Set([
14
+ "get-item",
15
+ "gi",
16
+ "get-childitem",
17
+ "gci",
18
+ "dir",
19
+ "ls",
20
+ ]);
21
+ const POWERSHELL_STRING_CONVERTERS = new Set(["out-string", "oss", "convertto-json"]);
12
22
 
13
23
  export interface ShellSearchMatch {
14
24
  kind: SearchKind;
@@ -116,6 +126,18 @@ function commandWords(node: Node, language: ShellLanguage): Array<string | null>
116
126
  return commandWordNodes(node, language).map((word) => literalWord(word, language));
117
127
  }
118
128
 
129
+ function hasBashWordExpansion(text: string): boolean {
130
+ if (text.startsWith("~") || text.startsWith("=")) return true;
131
+ for (let index = 0; index < text.length; index += 1) {
132
+ if (text[index] === "\\") {
133
+ index += 1;
134
+ } else if ("*?[]{}".includes(text[index] ?? "")) {
135
+ return true;
136
+ }
137
+ }
138
+ return false;
139
+ }
140
+
119
141
  function hasUntranslatedShellSyntax(node: Node, language: ShellLanguage): boolean {
120
142
  const text = node.text;
121
143
  if (language === "powershell") {
@@ -131,8 +153,7 @@ function hasUntranslatedShellSyntax(node: Node, language: ShellLanguage): boolea
131
153
  );
132
154
  }
133
155
  if (["raw_string", "string", "string_content"].includes(node.type)) return false;
134
- if (["word", "number", "command_name"].includes(node.type))
135
- return /[*?[\]{}]/u.test(text) || text.startsWith("~") || text.startsWith("=");
156
+ if (["word", "number", "command_name"].includes(node.type)) return hasBashWordExpansion(text);
136
157
  if (node.type === "concatenation")
137
158
  return node.namedChildren.some(
138
159
  (child) => child !== null && hasUntranslatedShellSyntax(child, language),
@@ -140,36 +161,80 @@ function hasUntranslatedShellSyntax(node: Node, language: ShellLanguage): boolea
140
161
  return false;
141
162
  }
142
163
 
164
+ function hasInputRedirectNodes(owner: Node): boolean {
165
+ return owner
166
+ .descendantsOfType(["file_redirect", "herestring_redirect", "heredoc_redirect"])
167
+ .some((redirect) => redirect !== null && /^(?:\d+)?</u.test(redirect.text));
168
+ }
169
+
170
+ function hasInputRedirect(node: Node): boolean {
171
+ if (hasInputRedirectNodes(node)) return true;
172
+ for (let parent = node.parent; parent; parent = parent.parent) {
173
+ if (parent.type !== "redirected_statement") continue;
174
+ const body = parent.childForFieldName("body");
175
+ const last =
176
+ body?.descendantsOfType("command").at(-1) ?? (body?.type === "command" ? body : undefined);
177
+ if (
178
+ last?.startIndex === node.startIndex &&
179
+ last.endIndex === node.endIndex &&
180
+ parent
181
+ .childrenForFieldName("redirect")
182
+ .some((redirect) => redirect !== null && hasInputRedirectNodes(redirect))
183
+ )
184
+ return true;
185
+ }
186
+ return false;
187
+ }
188
+
143
189
  function isSafePipelineFilter(
144
190
  node: Node,
145
191
  language: ShellLanguage,
146
192
  words: Array<string | null>,
147
193
  ): boolean {
148
- const executable = words[0];
149
- if (executable === null || executable === undefined) return false;
150
- const name = executableName(executable);
151
- const filterNames =
152
- language === "powershell"
153
- ? new Set(["select-string", "sls"])
154
- : new Set(["grep", "egrep", "fgrep"]);
155
- if (!filterNames.has(language === "powershell" ? name.toLowerCase() : name)) return false;
156
- const pipeline = node.parent;
157
- if (!pipeline || pipeline.type !== "pipeline") return false;
158
- const commands = pipeline.namedChildren.filter(
159
- (child): child is Node => child !== null && child.type === "command",
160
- );
161
- const last = commands.at(-1);
194
+ const wordNodes = commandWordNodes(node, language);
162
195
  if (
163
- !last ||
164
- last.startIndex !== node.startIndex ||
165
- last.endIndex !== node.endIndex ||
166
- commands.length < 2
196
+ !isStdinOnlySearchFilter(
197
+ words,
198
+ language,
199
+ wordNodes.map((word) => word.type === "command_parameter"),
200
+ )
167
201
  )
168
202
  return false;
169
- return commands.slice(0, -1).every((candidate) => {
170
- const decision = classifyCommand(commandWords(candidate, language), language);
171
- return !decision.kind && !decision.nested;
172
- });
203
+ if (wordNodes.some((word) => hasUntranslatedShellSyntax(word, language))) return false;
204
+ if (hasInputRedirect(node)) return false;
205
+ const pipeline = node.parent;
206
+ if (!pipeline || !["pipeline", "pipeline_chain"].includes(pipeline.type)) return false;
207
+ const stages = pipeline.namedChildren.filter((child): child is Node => child !== null);
208
+ const position = stages.findIndex(
209
+ (stage) => stage.startIndex <= node.startIndex && stage.endIndex >= node.endIndex,
210
+ );
211
+ if (position < 1) return false;
212
+ let fileObjectFlow = false;
213
+ for (const [index, stage] of stages.entries()) {
214
+ if (index === position) continue;
215
+ const commands = stage.descendantsOfType("command");
216
+ for (const candidate of commands) {
217
+ if (candidate === null) continue;
218
+ const candidateWords = commandWords(candidate, language);
219
+ const decision = classifyCommand(candidateWords, language);
220
+ if (decision.kind || decision.nested) return false;
221
+ if (language !== "powershell" || index > position) continue;
222
+ const executable = candidateWords[0];
223
+ if (executable === null || executable === undefined) continue;
224
+ const name = executableName(executable).toLowerCase();
225
+ if (POWERSHELL_FILE_OBJECT_PRODUCERS.has(name)) fileObjectFlow = true;
226
+ }
227
+ if (language === "powershell" && index < position && stage.type === "command") {
228
+ const executable = commandWords(stage, language)[0];
229
+ if (
230
+ executable !== null &&
231
+ executable !== undefined &&
232
+ POWERSHELL_STRING_CONVERTERS.has(executableName(executable).toLowerCase())
233
+ )
234
+ fileObjectFlow = false;
235
+ }
236
+ }
237
+ return !fileObjectFlow;
173
238
  }
174
239
 
175
240
  /** WASM grammars are shipped with the hook; parsing never reads shell scripts or executes code. */
@@ -0,0 +1,174 @@
1
+ import { executableName, type ShellLanguage } from "./search-policy-commands.js";
2
+
3
+ // Only options whose operands cannot name an input file are accepted here. Unknown
4
+ // options fail closed, since grep variants and PowerShell aliases can add file inputs.
5
+ const GREP_SWITCHES = new Set("abcEFGHhiILlnoPqsTUuvwxyzZ".split(""));
6
+ const GREP_VALUE_OPTIONS = new Set(["A", "B", "C", "m"]);
7
+ const GREP_LONG_SWITCHES = new Set([
8
+ "--basic-regexp",
9
+ "--extended-regexp",
10
+ "--fixed-strings",
11
+ "--perl-regexp",
12
+ "--ignore-case",
13
+ "--no-ignore-case",
14
+ "--invert-match",
15
+ "--word-regexp",
16
+ "--line-regexp",
17
+ "--count",
18
+ "--files-with-matches",
19
+ "--files-without-match",
20
+ "--only-matching",
21
+ "--quiet",
22
+ "--silent",
23
+ "--no-messages",
24
+ "--byte-offset",
25
+ "--line-number",
26
+ "--with-filename",
27
+ "--no-filename",
28
+ "--initial-tab",
29
+ "--null",
30
+ "--null-data",
31
+ "--text",
32
+ "--binary",
33
+ "--line-buffered",
34
+ "--no-group-separator",
35
+ ]);
36
+ const GREP_LONG_VALUES = new Set([
37
+ "--after-context",
38
+ "--before-context",
39
+ "--context",
40
+ "--max-count",
41
+ "--binary-files",
42
+ "--label",
43
+ "--group-separator",
44
+ "--include",
45
+ "--exclude",
46
+ "--exclude-dir",
47
+ ]);
48
+
49
+ function isStdinOnlyGrep(args: readonly (string | null)[]): boolean {
50
+ const positionals: string[] = [];
51
+ let hasPatternOption = false;
52
+ let optionsEnded = false;
53
+ for (let index = 0; index < args.length; index += 1) {
54
+ const arg = args[index];
55
+ if (arg === null || arg === undefined) return false;
56
+ // Some grep implementations (and GNU grep with POSIXLY_CORRECT) stop option
57
+ // parsing at the first operand. Later option-looking words can be file names.
58
+ if (positionals.length > 0 && arg !== "-") return false;
59
+ if (!optionsEnded && arg === "--") {
60
+ optionsEnded = true;
61
+ continue;
62
+ }
63
+ if (!optionsEnded && arg.startsWith("--")) {
64
+ const separator = arg.indexOf("=");
65
+ const option = separator < 0 ? arg : arg.slice(0, separator);
66
+ const attached = separator < 0 ? undefined : arg.slice(separator + 1);
67
+ if (option === "--regexp") {
68
+ hasPatternOption = true;
69
+ } else if (option === "--color" || option === "--colour") {
70
+ continue;
71
+ } else if (GREP_LONG_SWITCHES.has(option) && attached === undefined) {
72
+ continue;
73
+ } else if (!GREP_LONG_VALUES.has(option)) {
74
+ return false;
75
+ }
76
+ if (attached === undefined) {
77
+ index += 1;
78
+ if (args[index] === null || args[index] === undefined) return false;
79
+ }
80
+ continue;
81
+ }
82
+ if (!optionsEnded && arg.startsWith("-") && arg !== "-") {
83
+ for (let optionIndex = 1; optionIndex < arg.length; optionIndex += 1) {
84
+ const option = arg[optionIndex];
85
+ if (option === "e") {
86
+ hasPatternOption = true;
87
+ } else if (option !== undefined && GREP_SWITCHES.has(option)) {
88
+ continue;
89
+ } else if (option === undefined || !GREP_VALUE_OPTIONS.has(option)) {
90
+ return false;
91
+ }
92
+ if (optionIndex === arg.length - 1) {
93
+ index += 1;
94
+ if (args[index] === null || args[index] === undefined) return false;
95
+ }
96
+ break;
97
+ }
98
+ continue;
99
+ }
100
+ positionals.push(arg);
101
+ }
102
+ return hasPatternOption
103
+ ? positionals.every((operand) => operand === "-")
104
+ : positionals.length > 0 && positionals.slice(1).every((operand) => operand === "-");
105
+ }
106
+
107
+ const SELECT_STRING_SWITCHES = new Set([
108
+ "simplematch",
109
+ "casesensitive",
110
+ "quiet",
111
+ "list",
112
+ "noemphasis",
113
+ "notmatch",
114
+ "allmatches",
115
+ "raw",
116
+ "verbose",
117
+ "debug",
118
+ ]);
119
+ const SELECT_STRING_VALUES = new Set([
120
+ "culture",
121
+ "encoding",
122
+ "context",
123
+ "erroraction",
124
+ "warningaction",
125
+ "informationaction",
126
+ "outvariable",
127
+ "pipelinevariable",
128
+ ]);
129
+
130
+ function isPipelineOnlySelectString(
131
+ args: readonly (string | null)[],
132
+ namedParameters: readonly boolean[],
133
+ ): boolean {
134
+ let hasPattern = false;
135
+ for (let index = 0; index < args.length; index += 1) {
136
+ const arg = args[index];
137
+ if (arg === null || arg === undefined) return false;
138
+ if (!namedParameters[index]) {
139
+ if (hasPattern) return false; // Further positional values can bind to -Path.
140
+ hasPattern = true;
141
+ continue;
142
+ }
143
+ const separator = arg.indexOf(":");
144
+ const option = (separator < 0 ? arg.slice(1) : arg.slice(1, separator)).toLowerCase();
145
+ if (SELECT_STRING_SWITCHES.has(option) && separator < 0) continue;
146
+ if (option !== "pattern" && !SELECT_STRING_VALUES.has(option)) return false;
147
+ if (option === "pattern") {
148
+ if (hasPattern) return false;
149
+ hasPattern = true;
150
+ }
151
+ if (separator < 0) {
152
+ index += 1;
153
+ if (args[index] === null || args[index] === undefined) return false;
154
+ }
155
+ }
156
+ return hasPattern;
157
+ }
158
+
159
+ /** The exception is for filters consuming the pipeline, never for file operands. */
160
+ export function isStdinOnlySearchFilter(
161
+ words: readonly (string | null)[],
162
+ language: ShellLanguage,
163
+ namedParameters: readonly boolean[],
164
+ ): boolean {
165
+ const executable = words[0];
166
+ if (executable === null || executable === undefined) return false;
167
+ const name = executableName(executable);
168
+ if (language === "powershell")
169
+ return (
170
+ ["select-string", "sls"].includes(name.toLowerCase()) &&
171
+ isPipelineOnlySelectString(words.slice(1), namedParameters.slice(1))
172
+ );
173
+ return ["grep", "egrep", "fgrep"].includes(name) && isStdinOnlyGrep(words.slice(1));
174
+ }
@@ -3,7 +3,7 @@ import { recoverShellSearch } from "./search-policy-recovery.js";
3
3
  import { ShellSearchPolicy, type ShellSearchMatch } from "./search-policy-shell.js";
4
4
 
5
5
  export const SEARCH_POLICY_GUIDANCE =
6
- "Local content and filename searches must use sift-light. Built-in search tools and direct search commands are blocked before execution; filtering output from an unrelated producer at a pipeline tail remains available. Use pattern for contents or mode=files with query for filenames. Keep read/edit/write, tests and builds available. After a denial, call sift-light once with the stated repair; do not paste the denial into the request, repeat the blocked call, use another shell/custom script, or weaken the search mode.";
6
+ "Local content and filename searches must use sift-light. Built-in search tools and direct search commands are blocked before execution; recognized stdin-only filtering of output from an unrelated producer remains available at any pipeline position. File operands, recursive search options, file-sourced patterns and input redirection are not filter exceptions. Use pattern for contents or mode=files with query for filenames. Keep read/edit/write, tests and builds available. After a denial, call sift-light once with the stated repair; do not paste the denial into the request, repeat the blocked call, use another shell/custom script, or weaken the search mode.";
7
7
  export const PI_REPLACED_SEARCH_TOOLS = new Set(["grep", "find"]);
8
8
  export const PREFERRED_SEARCH_GUIDANCE =
9
9
  "Prefer sift-light for local content and filename searches because it provides bounded evidence, coverage and continuation details. Conventional search entries remain available in advisory mode.";