@herbertgao/pi-extensions 2026.8.5 → 2026.8.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.
Files changed (155) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +48 -6
  6. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  7. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +10 -0
  8. package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
  9. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +5 -2
  10. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
  11. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
  12. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
  13. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
  14. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
  15. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  16. package/node_modules/@narumitw/pi-btw/README.md +24 -17
  17. package/node_modules/@narumitw/pi-btw/package.json +5 -5
  18. package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
  19. package/node_modules/@narumitw/pi-btw/src/menu.ts +33 -13
  20. package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
  21. package/node_modules/pi-lens/CHANGELOG.md +95 -0
  22. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
  23. package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
  24. package/node_modules/pi-lens/dist/clients/biome-client.js +121 -13
  25. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
  26. package/node_modules/pi-lens/dist/clients/bus-publish.js +11 -3
  27. package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
  28. package/node_modules/pi-lens/dist/clients/console-guard-install.js +16 -4
  29. package/node_modules/pi-lens/dist/clients/dead-code-client.js +135 -30
  30. package/node_modules/pi-lens/dist/clients/dependency-checker.js +19 -7
  31. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +6 -4
  32. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  33. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +114 -10
  34. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +101 -16
  35. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -4
  36. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +230 -50
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
  40. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  41. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
  42. package/node_modules/pi-lens/dist/clients/extension-log.js +296 -3
  43. package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
  44. package/node_modules/pi-lens/dist/clients/format-events-publish.js +39 -8
  45. package/node_modules/pi-lens/dist/clients/git-guard.js +18 -21
  46. package/node_modules/pi-lens/dist/clients/go-client.js +21 -39
  47. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +116 -7
  48. package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
  49. package/node_modules/pi-lens/dist/clients/installer/index.js +5 -1
  50. package/node_modules/pi-lens/dist/clients/jscpd-client.js +4 -9
  51. package/node_modules/pi-lens/dist/clients/knip-client.js +51 -14
  52. package/node_modules/pi-lens/dist/clients/latency-logger.js +50 -1
  53. package/node_modules/pi-lens/dist/clients/lens-events.js +56 -25
  54. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  55. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +45 -2
  56. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
  57. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +58 -13
  58. package/node_modules/pi-lens/dist/clients/lsp/client.js +169 -8
  59. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +29 -0
  60. package/node_modules/pi-lens/dist/clients/lsp/index.js +264 -66
  61. package/node_modules/pi-lens/dist/clients/lsp/server.js +247 -60
  62. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
  63. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
  64. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
  65. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +4 -0
  66. package/node_modules/pi-lens/dist/clients/mcp/session.js +30 -17
  67. package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
  68. package/node_modules/pi-lens/dist/clients/pipeline.js +29 -2
  69. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
  70. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
  71. package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
  72. package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
  73. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +153 -7
  74. package/node_modules/pi-lens/dist/clients/runtime-context.js +104 -11
  75. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +170 -22
  76. package/node_modules/pi-lens/dist/clients/runtime-session.js +28 -5
  77. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +124 -10
  78. package/node_modules/pi-lens/dist/clients/runtime-turn.js +388 -35
  79. package/node_modules/pi-lens/dist/clients/rust-client.js +21 -37
  80. package/node_modules/pi-lens/dist/clients/security-scan-client.js +88 -5
  81. package/node_modules/pi-lens/dist/clients/sg-runner.js +141 -23
  82. package/node_modules/pi-lens/dist/clients/smells-rollup.js +18 -11
  83. package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
  84. package/node_modules/pi-lens/dist/clients/test-runner-client.js +431 -24
  85. package/node_modules/pi-lens/dist/clients/tool-policy.js +2 -0
  86. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
  87. package/node_modules/pi-lens/dist/clients/warm-attach.js +17 -0
  88. package/node_modules/pi-lens/dist/clients/word-index.js +305 -33
  89. package/node_modules/pi-lens/dist/index.js +4827 -1633
  90. package/node_modules/pi-lens/dist/mcp/server.js +8 -2
  91. package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
  92. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
  93. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
  94. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +3 -2
  95. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +62 -7
  96. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
  97. package/node_modules/pi-lens/docs/agent-guide.md +38 -13
  98. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +11 -3
  99. package/node_modules/pi-lens/docs/features.md +14 -1
  100. package/node_modules/pi-lens/docs/globalconfig.md +11 -0
  101. package/node_modules/pi-lens/docs/servercapabilities.md +1 -1
  102. package/node_modules/pi-lens/docs/settings.md +6 -0
  103. package/node_modules/pi-lens/docs/usage.md +23 -5
  104. package/node_modules/pi-lens/docs/word-index.md +35 -0
  105. package/node_modules/pi-lens/package.json +1 -1
  106. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
  107. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
  108. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
  109. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
  110. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
  111. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
  112. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
  113. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
  114. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
  115. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
  116. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
  117. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
  118. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
  119. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
  120. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
  121. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
  122. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +79 -0
  123. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
  124. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
  125. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
  126. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
  127. package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
  128. package/node_modules/pi-mcp-adapter/CHANGELOG.md +13 -0
  129. package/node_modules/pi-mcp-adapter/README.md +4 -1
  130. package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
  131. package/node_modules/pi-mcp-adapter/cli.js +25 -4
  132. package/node_modules/pi-mcp-adapter/config.ts +4 -4
  133. package/node_modules/pi-mcp-adapter/direct-tools.ts +18 -15
  134. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
  135. package/node_modules/pi-mcp-adapter/metadata-cache.ts +18 -8
  136. package/node_modules/pi-mcp-adapter/package.json +2 -1
  137. package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
  138. package/node_modules/pi-mcp-adapter/server-manager.ts +5 -0
  139. package/node_modules/pi-mcp-adapter/tool-metadata.ts +38 -18
  140. package/node_modules/pi-mcp-adapter/types.ts +93 -10
  141. package/node_modules/pi-web-access/CHANGELOG.md +14 -0
  142. package/node_modules/pi-web-access/README.md +18 -12
  143. package/node_modules/pi-web-access/auth-fetch.ts +148 -0
  144. package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
  145. package/node_modules/pi-web-access/curator-page.ts +5 -3
  146. package/node_modules/pi-web-access/curator-server.ts +2 -1
  147. package/node_modules/pi-web-access/extract.ts +106 -34
  148. package/node_modules/pi-web-access/fetch-params.ts +17 -3
  149. package/node_modules/pi-web-access/firecrawl.ts +172 -12
  150. package/node_modules/pi-web-access/gemini-search.ts +18 -4
  151. package/node_modules/pi-web-access/index.ts +120 -48
  152. package/node_modules/pi-web-access/package.json +2 -2
  153. package/node_modules/pi-web-access/summary-review.ts +11 -5
  154. package/node_modules/pi-web-access/youtube-extract.ts +2 -2
  155. package/package.json +9 -9
@@ -67,7 +67,7 @@ constraints: # metavariable regex constraints work
67
67
 
68
68
  ⚠ **`has`/`inside` default to the immediate child/parent (`stopBy: neighbor`).** For a
69
69
  recursive descendant/ancestor search add `stopBy: end`. This is the #1 migration
70
- gotcha — see the `has` note below.
70
+ gotcha — see the `has` note in `reference.md`.
71
71
 
72
72
  ## YAML quoting — REQUIRED (js-yaml will reject the rule otherwise)
73
73
 
@@ -100,120 +100,14 @@ The parser is a real YAML parser, so unquoted special chars throw and the rule i
100
100
  ✅ Docs: docs/custom-rules.md
101
101
  ```
102
102
 
103
- ## Hard-won gotchas (NAPI runner specifics — verified)
103
+ ## Reference doc — read before writing a NAPI-runner-specific or hard-to-express rule
104
104
 
105
- ```
106
- ⚠ `has`/`inside` default to DIRECT child/parent — add `stopBy: end` for a recursive search.
107
- This cuts BOTH ways, so think about where the target node actually lives:
108
- - Target is a grandchild+ → you MUST add `stopBy: end` or the `has` never matches.
109
- `switch-without-default` = `switch_statement` not has `switch_default`: the default
110
- lives under `switch_body`, so without `stopBy: end` it matches nothing and every
111
- switch (even ones WITH a default) is flagged. Same for `nested-ternary` catching a
112
- parenthesized `a ? (b ? c : d) : e`.
113
- - Target is the direct child → leave it at `neighbor` (default). Adding `stopBy: end`
114
- OVER-reports: `throw_statement` has `string` + `stopBy: end` flags `throw new
115
- Error("x")` (the string is nested), and `expression_statement` has `new_expression`
116
- + `stopBy: end` flags `fn(new Error())` as a discarded error. Keep these direct.
117
- napi's `has` never matches the node itself, so a self-referential `kind: X` has
118
- `kind: X` (with `stopBy: end`) correctly flags only genuinely-nested X.
119
-
120
- ✅ Prefer `regex` on the matched node's OWN text over `has` when you only need to
121
- inspect the node — avoids recursive-descendant false positives:
122
- kind: export_statement
123
- regex: '^export\s+(let|var)\b' # precise; no has-recursion FP
124
- (NAPI evaluates `regex` with JS RegExp on node.text() — keep it LINEAR so the
125
- detector can't itself ReDoS.)
126
-
127
- ⚠ String-literal regexes match SOURCE text, not the runtime string value.
128
- Inspect the exact node text before writing constraints:
129
- ast-grep run --kind string --lang ts sample.ts --json=compact
130
- Example: source `"\\|"` is node text `"\\\\|"` in JSON; to match a
131
- source-level escaped backslash (`\\`) followed by a non-backslash, the rule
132
- regex needs FOUR regex backslashes, preferably in a YAML block scalar:
133
- regex: >-
134
- ^["'`]\\\\[^\\A-Za-z0-9$]
135
- This is how `incomplete-string-escaping` catches both `"\\|"` and
136
- `'\\"'`. Avoid shell here-doc probes for this class — shell/JSON escaping
137
- can silently eat a backslash and make the rule look broken.
138
-
139
- ⚠ `-js` twins: remember there are TWO execution surfaces.
140
- - ast-grep CLI/LSP language-gates by `language:`. A `language: TypeScript`
141
- rule is not enough for standalone `.js` coverage, so shipped user-facing
142
- TS/JS rules that should fire under the ast-grep LSP usually need a `-js`
143
- twin with `language: JavaScript` plus its own fixture.
144
- - the in-process NAPI fallback (`ast-grep-napi.ts`) parses the target file's
145
- own grammar and currently runs both TS and JS rules on every jsts file. A
146
- grammar-agnostic twin can therefore duplicate in fallback mode.
147
- - **Decide explicitly:** if the rule must cover `.js` through the ast-grep
148
- CLI/LSP baseline, ship the twin and test both. If a rule is NAPI-only or
149
- fallback duplication is unacceptable, fix runner dedup/normalization before
150
- relying on a single TypeScript rule for JS coverage.
151
- - **Grammar-divergent bodies** still need separate variants regardless:
152
- e.g. `no-flag-argument` uses `required_parameter` in TS and
153
- `assignment_pattern` in JS.
154
-
155
- ✅ Node-kind facts (tree-sitter-typescript grammar — NOT the TS compiler / Roslyn):
156
- - let / const → `lexical_declaration` (var is NOT here)
157
- - var → `variable_declaration`
158
- - a regex literal's pattern text → `regex_pattern`
159
- - x[i] index access → `subscript_expression` (NOT element_access_expression)
160
- - obj.prop access → `member_expression` (NOT property_access_expression)
161
- - !x / -x / typeof x → `unary_expression`
162
- - a ? b : c → `ternary_expression`
163
-
164
- ❌ Wrong-grammar kind names = silent dead rule. `element_access_expression`,
165
- `property_access_expression`, `binary_operator`, etc. are TS-compiler/Roslyn names, not
166
- tree-sitter's. napi REJECTS the whole rule ("invalid kind matcher") so it never runs.
167
- Verify a kind exists before shipping:
168
- node -e 'import("@ast-grep/napi").then(s=>{const r=s.ts.parse("x[i]").root();
169
- const f=(n,k)=>{let c=n.kind()===k?1:0;for(const x of n.children())c+=f(x,k);return c};
170
- console.log(f(r,"subscript_expression"))})' # >0 means the kind is real
171
-
172
- ✅ Test through the REAL runner from the repo root — it loads the actual shipped
173
- rules from rules/ast-grep-rules/rules. Assert on diagnostic `rule` ids:
174
- const res = await runner.run(ctx); // ctx.filePath = temp .ts, cwd = repo
175
- For pattern/kind/regex-only rules (CLI-identical semantics) `ast-grep scan` is fine.
176
-
177
- ✅ Before shipping any text/regex detector, FP-scan the codebase:
178
- ast-grep scan -r <rule>.yml clients tools
179
- Real safe variants bite (e.g. ReDoS: (ba+)+ is safe — a mandatory prefix makes
180
- the partition unique; flag only a single quantified atom inside the group).
181
- ```
182
-
183
- ## Matching things a pattern can't express (#305)
184
-
185
- ```
186
- ❌ A parameter default is NOT a `$X = false` pattern. `pattern: $FLAG = false` parses as
187
- an `assignment_expression` (statement context) and never matches a function parameter.
188
- Match the PARAM NODE + its child literal instead, capturing the name for reuse:
189
- # TS grammar
190
- - kind: required_parameter
191
- all:
192
- - has: { field: pattern, pattern: $FLAG }
193
- - has: { any: [ { kind: "true" }, { kind: "false" } ] }
194
- # JS grammar (assignment_pattern, with fields left/right)
195
- - kind: assignment_pattern
196
- all:
197
- - has: { field: left, pattern: $FLAG }
198
- - has: { field: right, any: [ { kind: "true" }, { kind: "false" } ] }
199
-
200
- ✅ Metavar consistency works ACROSS sibling clauses of an `all` — a metavar bound in one
201
- `has` must match the SAME text everywhere it reappears. Use it to CORRELATE nodes, which
202
- is what makes a structural rule precise:
203
- all:
204
- - has: { stopBy: end, kind: required_parameter, has: { field: pattern, pattern: $FLAG } }
205
- - has: { stopBy: end, any: [ { pattern: "if ($FLAG) $$$" }, { pattern: "if (!$FLAG) $$$" } ] }
206
- This fires ONLY when the function branches on the SAME param it declared boolean — a
207
- boolean default that's never branched on, or a branch on a different var, won't match.
208
-
209
- ❌ Two `has:` keys in one mapping silently OVERWRITE (YAML: last key wins). For multiple
210
- descendant constraints use `all:` with a LIST of `has` entries, never repeated `has:`.
211
-
212
- ✅ Prefer a high-precision structural guard over an unbounded denylist. Message-chain
213
- (Demeter) floods on fluent/promise/builder chains; rather than denylist every fluent
214
- method name, REQUIRE the chain's first calls to be accessors (`get*`/`is*`/`has*`) via
215
- `constraints` regex — promise/fluent/builder methods aren't accessor-named, so they're
216
- excluded by construction. Precision over recall.
105
+ `reference.md` (same directory) covers: ReDoS-safe regex authoring, node-text
106
+ string-escape quirks, `has`/`inside` `stopBy` defaults (and when to override
107
+ them), the `-js` twin dedup behavior (#657), boolean-parameter matching
108
+ across the TS/JS grammars, and precision-over-recall heuristics for
109
+ denylist-shaped rules. Read it when a rule isn't matching (or over-matching)
110
+ the way you expect, or before shipping a `regex`/`has`-heavy rule.
217
111
 
218
112
  ## Validating a candidate rule against the REAL engine (not the warm MCP cache)
219
113
 
@@ -0,0 +1,129 @@
1
+ # pi-lens ast-grep Rule Reference
2
+
3
+ Deep-dive companion to `SKILL.md`. Read this when you're debugging a rule
4
+ that isn't matching (or over-matching) the way you expect: ReDoS-safe regex
5
+ authoring, string-escape quirks in node text, `has`/`inside` `stopBy`
6
+ defaults and when to override them, boolean-parameter matching across two
7
+ grammars, and precision-over-recall heuristics for denylist-shaped rules.
8
+
9
+ ## Hard-won gotchas (NAPI runner specifics — verified)
10
+
11
+ ```
12
+ ⚠ `has`/`inside` default to DIRECT child/parent — add `stopBy: end` for a recursive search.
13
+ This cuts BOTH ways, so think about where the target node actually lives:
14
+ - Target is a grandchild+ → you MUST add `stopBy: end` or the `has` never matches.
15
+ `switch-without-default` = `switch_statement` not has `switch_default`: the default
16
+ lives under `switch_body`, so without `stopBy: end` it matches nothing and every
17
+ switch (even ones WITH a default) is flagged. Same for `nested-ternary` catching a
18
+ parenthesized `a ? (b ? c : d) : e`.
19
+ - Target is the direct child → leave it at `neighbor` (default). Adding `stopBy: end`
20
+ OVER-reports: `throw_statement` has `string` + `stopBy: end` flags `throw new
21
+ Error("x")` (the string is nested), and `expression_statement` has `new_expression`
22
+ + `stopBy: end` flags `fn(new Error())` as a discarded error. Keep these direct.
23
+ napi's `has` never matches the node itself, so a self-referential `kind: X` has
24
+ `kind: X` (with `stopBy: end`) correctly flags only genuinely-nested X.
25
+
26
+ ✅ Prefer `regex` on the matched node's OWN text over `has` when you only need to
27
+ inspect the node — avoids recursive-descendant false positives:
28
+ kind: export_statement
29
+ regex: '^export\s+(let|var)\b' # precise; no has-recursion FP
30
+ (NAPI evaluates `regex` with JS RegExp on node.text() — keep it LINEAR so the
31
+ detector can't itself ReDoS.)
32
+
33
+ ⚠ String-literal regexes match SOURCE text, not the runtime string value.
34
+ Inspect the exact node text before writing constraints:
35
+ ast-grep run --kind string --lang ts sample.ts --json=compact
36
+ Example: source `"\\|"` is node text `"\\\\|"` in JSON; to match a
37
+ source-level escaped backslash (`\\`) followed by a non-backslash, the rule
38
+ regex needs FOUR regex backslashes, preferably in a YAML block scalar:
39
+ regex: >-
40
+ ^["'`]\\\\[^\\A-Za-z0-9$]
41
+ This is how `incomplete-string-escaping` catches both `"\\|"` and
42
+ `'\\"'`. Avoid shell here-doc probes for this class — shell/JSON escaping
43
+ can silently eat a backslash and make the rule look broken.
44
+
45
+ ⚠ `-js` twins: remember there are TWO execution surfaces.
46
+ <!-- verified: clients/dispatch/runners/ast-grep-napi.ts:205-216,450-455, c170d94b -->
47
+ - ast-grep CLI/LSP language-gates by `language:`. A `language: TypeScript`
48
+ rule is not enough for standalone `.js` coverage, so shipped user-facing
49
+ TS/JS rules that should fire under the ast-grep LSP usually need a `-js`
50
+ twin with `language: JavaScript` plus its own fixture.
51
+ - the in-process NAPI fallback (`ast-grep-napi.ts` — pi-lens source checkout
52
+ only, not present in the installed package) already DEDUPES by grammar
53
+ (#657): `ruleLanguageForFile` (`ast-grep-napi.ts:205-216`) maps each file
54
+ extension to its actual grammar, and the matcher (`:450-455`) skips any
55
+ `language:`-tagged rule whose tag doesn't match that grammar. A TS rule
56
+ and its JS twin no longer double-fire on the same file in fallback mode.
57
+ - **`-js` twins are needed only when the rule body itself is
58
+ grammar-divergent** (different node kinds/fields between the TS and JS
59
+ grammars). A grammar-agnostic body doesn't need a twin for fallback
60
+ coverage — the dedup above already scopes it correctly; ship a twin only
61
+ when standalone `.js` coverage through the ast-grep CLI/LSP baseline is
62
+ required (see the first bullet).
63
+ - **Grammar-divergent bodies** still need separate variants regardless:
64
+ e.g. `no-flag-argument` uses `required_parameter` in TS and
65
+ `assignment_pattern` in JS.
66
+
67
+ ✅ Node-kind facts (tree-sitter-typescript grammar — NOT the TS compiler / Roslyn):
68
+ - let / const → `lexical_declaration` (var is NOT here)
69
+ - var → `variable_declaration`
70
+ - a regex literal's pattern text → `regex_pattern`
71
+ - x[i] index access → `subscript_expression` (NOT element_access_expression)
72
+ - obj.prop access → `member_expression` (NOT property_access_expression)
73
+ - !x / -x / typeof x → `unary_expression`
74
+ - a ? b : c → `ternary_expression`
75
+
76
+ ❌ Wrong-grammar kind names = silent dead rule. `element_access_expression`,
77
+ `property_access_expression`, `binary_operator`, etc. are TS-compiler/Roslyn names, not
78
+ tree-sitter's. napi REJECTS the whole rule ("invalid kind matcher") so it never runs.
79
+ Verify a kind exists before shipping:
80
+ node -e 'import("@ast-grep/napi").then(s=>{const r=s.ts.parse("x[i]").root();
81
+ const f=(n,k)=>{let c=n.kind()===k?1:0;for(const x of n.children())c+=f(x,k);return c};
82
+ console.log(f(r,"subscript_expression"))})' # >0 means the kind is real
83
+
84
+ ✅ Test through the REAL runner from the pi-lens source checkout's repo root — it loads the actual shipped
85
+ rules from rules/ast-grep-rules/rules. Assert on diagnostic `rule` ids:
86
+ const res = await runner.run(ctx); // ctx.filePath = temp .ts, cwd = repo (pi-lens source checkout only — not present in the installed package)
87
+ For pattern/kind/regex-only rules (CLI-identical semantics) `ast-grep scan` is fine.
88
+
89
+ ✅ Before shipping any text/regex detector, FP-scan the codebase:
90
+ ast-grep scan -r <rule>.yml clients tools
91
+ Real safe variants bite (e.g. ReDoS: (ba+)+ is safe — a mandatory prefix makes
92
+ the partition unique; flag only a single quantified atom inside the group).
93
+ ```
94
+
95
+ ## Matching things a pattern can't express (#305)
96
+
97
+ ```
98
+ ❌ A parameter default is NOT a `$X = false` pattern. `pattern: $FLAG = false` parses as
99
+ an `assignment_expression` (statement context) and never matches a function parameter.
100
+ Match the PARAM NODE + its child literal instead, capturing the name for reuse:
101
+ # TS grammar
102
+ - kind: required_parameter
103
+ all:
104
+ - has: { field: pattern, pattern: $FLAG }
105
+ - has: { any: [ { kind: "true" }, { kind: "false" } ] }
106
+ # JS grammar (assignment_pattern, with fields left/right)
107
+ - kind: assignment_pattern
108
+ all:
109
+ - has: { field: left, pattern: $FLAG }
110
+ - has: { field: right, any: [ { kind: "true" }, { kind: "false" } ] }
111
+
112
+ ✅ Metavar consistency works ACROSS sibling clauses of an `all` — a metavar bound in one
113
+ `has` must match the SAME text everywhere it reappears. Use it to CORRELATE nodes, which
114
+ is what makes a structural rule precise:
115
+ all:
116
+ - has: { stopBy: end, kind: required_parameter, has: { field: pattern, pattern: $FLAG } }
117
+ - has: { stopBy: end, any: [ { pattern: "if ($FLAG) $$$" }, { pattern: "if (!$FLAG) $$$" } ] }
118
+ This fires ONLY when the function branches on the SAME param it declared boolean — a
119
+ boolean default that's never branched on, or a branch on a different var, won't match.
120
+
121
+ ❌ Two `has:` keys in one mapping silently OVERWRITE (YAML: last key wins). For multiple
122
+ descendant constraints use `all:` with a LIST of `has` entries, never repeated `has:`.
123
+
124
+ ✅ Prefer a high-precision structural guard over an unbounded denylist. Message-chain
125
+ (Demeter) floods on fluent/promise/builder chains; rather than denylist every fluent
126
+ method name, REQUIRE the chain's first calls to be accessors (`get*`/`is*`/`has*`) via
127
+ `constraints` regex — promise/fluent/builder methods aren't accessor-named, so they're
128
+ excluded by construction. Precision over recall.
129
+ ```
@@ -6,7 +6,7 @@ description: Use when writing a new pi-lens tree-sitter query rule YAML file —
6
6
  # Writing a pi-lens tree-sitter Rule
7
7
 
8
8
  Drop path: `rules/tree-sitter-queries/<language>/<id>.yml`
9
- Language dir is **lowercase**: `typescript` `javascript` `tsx` `python` `go` `rust` `java` `csharp` `kotlin` `ruby` `cpp` `c` `css`
9
+ Language dir is **lowercase**: `typescript` `javascript` `tsx` `python` `go` `rust` `java` `csharp` `kotlin` `php` `ruby` `cpp` `c` `css`
10
10
 
11
11
  Project rules merge with built-ins (both run). To disable a language's built-ins: rename dir to `<lang>-disabled/`.
12
12
 
@@ -115,6 +115,8 @@ predicates:
115
115
  treeSitterRunner.run() via makeRealRunnerCtx (tests/support/real-runner-ctx.ts) —
116
116
  runQueryOnFile bypasses dispatch (skip_test_files, tiers, delta, rule cache).
117
117
  Template: tests/clients/dispatch/runners/tree-sitter-skip-test-files.test.ts.
118
+ (These test paths exist only in a pi-lens source checkout — the installed
119
+ npm package ships no tests/ directory.)
118
120
 
119
121
  ✅ JS files also run typescript/ rules (shared grammar) — one rule in
120
122
  rules/tree-sitter-queries/typescript/ covers BOTH .ts and .js. No -js copy needed.
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.26.0] - 2026-08-14
11
+
12
+ ### Added
13
+ - Added per-server `requestHeadersCommand` support for deriving fail-closed HTTP headers from the exact outbound request on every Streamable HTTP or SSE call. Thanks @kgreen18 for PR #353.
14
+ - Added `settings.warnOnLargeDirectTools` to suppress the advisory for 75 or more resolved direct tools. Thanks @Roshvan for issue #358.
15
+
16
+ ### Changed
17
+ - Refined request-header command result handling types without changing runtime behavior.
18
+
19
+ ### Fixed
20
+ - Matched adapter-owned config and state paths to the host agent directory when Pi is rebranded, including its environment override and config directory. Thanks @mindplay-dk for issue #356.
21
+ - Avoided O(tools²) cross-server tool-name collision scans at startup by skipping collision candidates when selectors are absent and sharing one indexed candidate set when `includeTools` or `excludeTools` is configured. Thanks @mjlbach for PR #357 and @cataldoc for issue #354.
22
+
10
23
  ## [2.25.0] - 2026-08-13
11
24
 
12
25
  ### Added
@@ -207,6 +207,7 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
207
207
  | `cwd` | Working directory; supports `${VAR}`, `$env:VAR`, and `~` expansion |
208
208
  | `url` | HTTP endpoint (StreamableHTTP with SSE fallback); supports raw `${VAR}` and `$env:VAR` interpolation, and missing URL variables fail before any request is sent |
209
209
  | `headers` | HTTP headers; supports `${VAR}` and `$env:VAR` interpolation. A value beginning with `!` runs a command when the HTTP server connects or OAuth authenticates; use `!!` for a literal leading `!`. |
210
+ | `requestHeadersCommand` | Trusted executable run for every HTTP request. It receives a versioned JSON envelope containing `method`, `url`, and the exact `bodyBase64` on stdin, and must return a JSON object of headers on stdout. `command`, `args`, and `env` support environment interpolation. Use for caller-bound request signatures; failures stop the request. |
210
211
  | `auth` | `"bearer"` or `"oauth"` |
211
212
  | `oauth.grantType` | `"authorization_code"` (default) or `"client_credentials"` for non-interactive machine auth |
212
213
  | `oauth.clientId` | Pre-registered OAuth client ID. MCP 2026 prefers pre-registered clients or Client ID Metadata Documents; this adapter falls back to Dynamic Client Registration when the ID is omitted and the server supports it. |
@@ -310,6 +311,7 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
310
311
  "toolResultRendering": "compact",
311
312
  "collapsedResultLines": 1,
312
313
  "notifyOnStartupConnect": true,
314
+ "warnOnLargeDirectTools": true,
313
315
  "hostConfigDiscovery": "off",
314
316
  "approveTools": ["github_delete_*", "notion_update_*"],
315
317
  "oauthDir": ".pi/mcp-oauth",
@@ -340,6 +342,7 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
340
342
  | `oauthDir` | Legacy OAuth `tokens.json` import directory for this MCP config. Relative paths resolve from the active project cwd. `MCP_OAUTH_DIR` still wins when set. Persistent OAuth credentials are stored in the OS credential store, not this directory. |
341
343
  | `mcpServers.<name>.oauth.authorizationParams` | Extra authorization URL parameters for provider-specific OAuth extensions. Flow-owned parameters such as `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge`, `response_type`, and `resource` cannot be overridden. |
342
344
  | `directTools` | Global default for all servers (default: false). Per-server overrides this. |
345
+ | `warnOnLargeDirectTools` | Show the advisory when 75 or more direct tools resolve (default: `true`). Set to `false` to suppress only this advisory. |
343
346
  | `freezeDirectTools` | Keep direct-tool registration stable after the initial sync so automatic reconnects and list-change notifications do not rebuild the system prompt. Use `mcp({ connect: "server" })` or `/mcp reconnect <server>` to refresh deliberately. Default: false. |
344
347
  | `scriptMode` | Register the MCP-only `mcpScript` plain-JavaScript tool (default: true). Set to `false` to hide it. |
345
348
  | `disableProxyTool` | Hide the `mcp` proxy tool once configured direct tools are fully available from cache. |
@@ -544,7 +547,7 @@ To hide specific tools while still using `directTools: true`, add `excludeTools`
544
547
 
545
548
  `includeTools` and `excludeTools` filter direct tools, proxy search/list/describe, and the `/mcp` panel view.
546
549
 
547
- Each direct tool costs ~150-300 tokens in the system prompt (name + description + schema). Good for targeted sets of 5-20 tools. For servers with 75+ tools, stick with the proxy or pick specific tools with a `string[]`. If 75+ direct tools resolve, the adapter prints a warning but still registers the tools you configured.
550
+ Each direct tool costs ~150-300 tokens in the system prompt (name + description + schema). Good for targeted sets of 5-20 tools. For servers with 75+ tools, stick with the proxy or pick specific tools with a `string[]`. If 75+ direct tools resolve, the adapter prints an advisory but still registers the tools you configured. Set `settings.warnOnLargeDirectTools` to `false` to suppress this advisory.
548
551
 
549
552
  Direct tools register from the metadata cache in the Pi agent dir (`~/.pi/agent/mcp-cache.json` by default, or `$PI_CODING_AGENT_DIR/mcp-cache.json` when set), so no server connections are needed at startup. On the first session after adding `directTools` to a new server, the cache won't exist yet — tools fall back to proxy-only while the cache populates, then the extension hot-loads the refreshed direct tools into the current session. Servers that advertise MCP list-change notifications refresh the current session when their tool or resource list changes. On Pi versions that expose `pi.unregisterTool()`, stale direct tools are removed from the registry during refresh; older Pi versions still deactivate them from the active tool set. To force a refresh: `/mcp reconnect <server>`.
550
553
 
@@ -2,10 +2,18 @@ import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join, resolve } from "node:path";
4
4
 
5
+ export function getConfigDirName(): string {
6
+ const configDir = readPiConfig()?.configDir;
7
+ return typeof configDir === "string" && configDir.trim() ? configDir.trim() : ".pi";
8
+ }
9
+
5
10
  export function getAgentDir(): string {
6
- const configured = process.env.PI_CODING_AGENT_DIR?.trim();
11
+ const piConfig = readPiConfig();
12
+ const name = piConfig?.name;
13
+ const appName = typeof name === "string" && name.trim() ? name.trim() : "pi";
14
+ const configured = process.env[`${appName.toUpperCase()}_CODING_AGENT_DIR`]?.trim();
7
15
  if (!configured) {
8
- return join(homedir(), ".pi", "agent");
16
+ return join(homedir(), getConfigDirName(), "agent");
9
17
  }
10
18
  if (configured === "~") {
11
19
  return homedir();
@@ -31,12 +39,12 @@ export function getAgentPath(...segments: string[]): string {
31
39
  *
32
40
  * Falls back to "pi", which is what pi's own APP_NAME resolves to.
33
41
  */
34
- function readPiConfig(): { name?: unknown; clientUri?: unknown } | undefined {
42
+ function readPiConfig(): { name?: unknown; configDir?: unknown; clientUri?: unknown } | undefined {
35
43
  const dir = process.env.PI_PACKAGE_DIR?.trim()
36
44
  if (!dir) return undefined
37
45
  try {
38
46
  const manifest = JSON.parse(readFileSync(join(resolve(dir), "package.json"), "utf8")) as {
39
- piConfig?: { name?: unknown; clientUri?: unknown }
47
+ piConfig?: { name?: unknown; configDir?: unknown; clientUri?: unknown }
40
48
  }
41
49
  return manifest.piConfig
42
50
  } catch {
@@ -14,15 +14,36 @@ function expandHome(input) {
14
14
  return path.resolve(input);
15
15
  }
16
16
 
17
- const AGENT_DIR = process.env.PI_CODING_AGENT_DIR?.trim()
18
- ? expandHome(process.env.PI_CODING_AGENT_DIR.trim())
19
- : path.join(HOME, ".pi", "agent");
17
+ function readPiConfig() {
18
+ const dir = process.env.PI_PACKAGE_DIR?.trim();
19
+ if (!dir) return undefined;
20
+ try {
21
+ return JSON.parse(fs.readFileSync(path.join(path.resolve(dir), "package.json"), "utf8")).piConfig;
22
+ } catch {
23
+ return undefined;
24
+ }
25
+ }
26
+
27
+ function getConfigDirName() {
28
+ const configDir = readPiConfig()?.configDir;
29
+ return typeof configDir === "string" && configDir.trim() ? configDir.trim() : ".pi";
30
+ }
31
+
32
+ function getAgentDir() {
33
+ const piConfig = readPiConfig();
34
+ const appName = typeof piConfig?.name === "string" && piConfig.name.trim() ? piConfig.name.trim() : "pi";
35
+ const configured = process.env[`${appName.toUpperCase()}_CODING_AGENT_DIR`]?.trim();
36
+ if (configured) return expandHome(configured);
37
+ return path.join(HOME, getConfigDirName(), "agent");
38
+ }
39
+
40
+ const AGENT_DIR = getAgentDir();
20
41
  const PI_CONFIG_PATH = path.join(AGENT_DIR, "mcp.json");
21
42
  const GENERIC_GLOBAL_CONFIG_PATH = path.join(HOME, ".config", "mcp", "mcp.json");
22
43
  const AGENTS_GLOBAL_CONFIG_PATH = path.join(HOME, ".agents", "mcp.json");
23
44
  const AGENTS_NESTED_GLOBAL_CONFIG_PATH = path.join(HOME, ".agents", "mcp", "mcp.json");
24
45
  const PROJECT_CONFIG_PATH = path.resolve(process.cwd(), ".mcp.json");
25
- const PROJECT_PI_CONFIG_PATH = path.resolve(process.cwd(), ".pi", "mcp.json");
46
+ const PROJECT_PI_CONFIG_PATH = path.resolve(process.cwd(), getConfigDirName(), "mcp.json");
26
47
 
27
48
  const IMPORT_PATHS = {
28
49
  cursor: [path.join(HOME, ".cursor", "mcp.json")],
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { parse as parseToml } from "smol-toml";
6
6
  import stripJsonComments from "strip-json-comments";
7
- import { getAgentPath } from "./agent-dir.ts";
7
+ import { getAgentPath, getConfigDirName } from "./agent-dir.ts";
8
8
  import { getAgentPluginSummaries, loadAgentPluginConfigs, type AgentPluginSummary } from "./agent-plugin-loader.ts";
9
9
  import { isServerDisabled, type HostConfigDiscovery, type McpConfig, type ServerEntry, type McpSettings, type ImportKind, type ServerProvenance } from "./types.ts";
10
10
  import { toStringRecord } from "./utils.ts";
@@ -15,7 +15,7 @@ const AGENTS_GLOBAL_CONFIG_PATHS = [
15
15
  join(homedir(), ".agents", "mcp", "mcp.json"),
16
16
  ] as const;
17
17
  const PROJECT_CONFIG_NAME = ".mcp.json";
18
- const PROJECT_PI_CONFIG_NAME = ".pi/mcp.json";
18
+ const PROJECT_PI_CONFIG_NAME = "mcp.json";
19
19
  const REPOPROMPT_BINARY_CANDIDATES = [
20
20
  join(homedir(), "RepoPrompt", "repoprompt_cli"),
21
21
  "/Applications/Repo Prompt.app/Contents/MacOS/repoprompt-mcp",
@@ -177,7 +177,7 @@ export function getProjectConfigPath(cwd = process.cwd()): string {
177
177
  }
178
178
 
179
179
  export function getProjectPiConfigPath(cwd = process.cwd()): string {
180
- return resolve(cwd, PROJECT_PI_CONFIG_NAME);
180
+ return resolve(cwd, getConfigDirName(), PROJECT_PI_CONFIG_NAME);
181
181
  }
182
182
 
183
183
  export function getConfigDiscoveryPaths(overridePath?: string, cwd = process.cwd()): ConfigDiscoveryPath[] {
@@ -471,7 +471,7 @@ function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
471
471
  // different url, these MUST NOT be inherited from the lower-precedence entry —
472
472
  // otherwise the original endpoint's credentials would be shipped to the new
473
473
  // url. See the SECURITY note in mergeServerMaps.
474
- const URL_BOUND_AUTH_FIELDS = ["headers", "bearerToken", "bearerTokenEnv"] as const;
474
+ const URL_BOUND_AUTH_FIELDS = ["headers", "bearerToken", "bearerTokenEnv", "requestHeadersCommand"] as const;
475
475
 
476
476
  function mergeServerMaps(
477
477
  base: Record<string, ServerEntry>,
@@ -11,7 +11,7 @@ import { formatSchema } from "./tool-metadata.ts";
11
11
  import { resolveMcpResultContent, transformMcpContent, transformMcpResourceContents } from "./tool-registrar.ts";
12
12
  import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.ts";
13
13
  import { maybeStartUiSession, summarizeUiSessionResult, type UiSessionRuntime } from "./ui-session.ts";
14
- import { formatToolName, getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
14
+ import { createToolSelectorCandidateIndex, formatToolName, getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
15
15
  import { isUiToolVisibleToModel } from "./ui-tool-visibility.ts";
16
16
  import { resourceNameToToolName } from "./resource-tools.ts";
17
17
  import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
@@ -150,7 +150,10 @@ export function resolveDirectTools(
150
150
  if (!toolFilter) continue;
151
151
 
152
152
  const effectivePrefix = resolveToolPrefix(definition, prefix);
153
- const getOtherCurrentCandidates = (toolName: string): Set<string> => {
153
+ const hasToolFilters =
154
+ (Array.isArray(definition.includeTools) && definition.includeTools.length > 0) ||
155
+ (Array.isArray(definition.excludeTools) && definition.excludeTools.length > 0);
156
+ const selectorCandidateIndex = hasToolFilters ? (() => {
154
157
  const candidates = new Set<string>();
155
158
  for (const [otherServerName, otherDefinition] of Object.entries(config.mcpServers)) {
156
159
  const otherCache = cache.servers[otherServerName];
@@ -167,14 +170,13 @@ export function resolveDirectTools(
167
170
  }
168
171
  }
169
172
  }
170
- for (const candidate of getToolNameCandidates(toolName, serverName, effectivePrefix, false)) candidates.delete(candidate);
171
- return candidates;
172
- };
173
+ return createToolSelectorCandidateIndex(candidates);
174
+ })() : undefined;
173
175
 
174
176
  for (const tool of serverCache.tools ?? []) {
175
177
  if (!isUiToolVisibleToModel(tool.uiVisibility)) continue;
176
178
  if (toolFilter !== true && !toolFilter.includes(tool.name)) continue;
177
- if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(tool.name))) continue;
179
+ if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) continue;
178
180
  const prefixedName = formatToolName(tool.name, serverName, effectivePrefix);
179
181
  if (BUILTIN_NAMES.has(prefixedName)) {
180
182
  console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
@@ -200,7 +202,7 @@ export function resolveDirectTools(
200
202
  for (const resource of serverCache.resources ?? []) {
201
203
  const baseName = `read_${resourceNameToToolName(resource.name)}`;
202
204
  if (toolFilter !== true && !toolFilter.includes(baseName)) continue;
203
- if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(baseName))) continue;
205
+ if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) continue;
204
206
  const prefixedName = formatToolName(baseName, serverName, effectivePrefix);
205
207
  if (BUILTIN_NAMES.has(prefixedName)) {
206
208
  console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
@@ -222,7 +224,7 @@ export function resolveDirectTools(
222
224
  }
223
225
  }
224
226
 
225
- if (specs.length >= DIRECT_TOOLS_ADVISORY_THRESHOLD) {
227
+ if (config.settings?.warnOnLargeDirectTools !== false && specs.length >= DIRECT_TOOLS_ADVISORY_THRESHOLD) {
226
228
  console.warn(`MCP: ${specs.length} direct tools resolved. Each direct tool adds prompt context; README guidance recommends targeted sets of 5-20 tools and using the proxy or an explicit string[] when 75+ direct tools would be registered.`);
227
229
  }
228
230
 
@@ -255,8 +257,10 @@ export function buildProxyDescription(
255
257
  const cachedEntry = cache?.servers?.[serverName];
256
258
  const entry = cachedEntry && isServerCacheValid(cachedEntry, definition) ? cachedEntry : undefined;
257
259
  const effectivePrefix = resolveToolPrefix(definition, prefix);
258
- const getOtherCurrentCandidates = (toolName: string): Set<string> | undefined => {
259
- if (!cache) return undefined;
260
+ const hasToolFilters =
261
+ (Array.isArray(definition.includeTools) && definition.includeTools.length > 0) ||
262
+ (Array.isArray(definition.excludeTools) && definition.excludeTools.length > 0);
263
+ const selectorCandidateIndex = hasToolFilters && cache ? (() => {
260
264
  const candidates = new Set<string>();
261
265
  for (const [otherServerName, otherDefinition] of Object.entries(config.mcpServers)) {
262
266
  const otherEntry = cache.servers[otherServerName];
@@ -273,17 +277,16 @@ export function buildProxyDescription(
273
277
  }
274
278
  }
275
279
  }
276
- for (const candidate of getToolNameCandidates(toolName, serverName, effectivePrefix, false)) candidates.delete(candidate);
277
- return candidates;
278
- };
280
+ return createToolSelectorCandidateIndex(candidates);
281
+ })() : undefined;
279
282
  const toolCount = (entry?.tools ?? []).filter(
280
283
  (tool) => isUiToolVisibleToModel(tool.uiVisibility)
281
- && isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(tool.name)),
284
+ && isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex),
282
285
  ).length;
283
286
  const resourceCount = definition?.exposeResources !== false
284
287
  ? (entry?.resources ?? []).filter((resource) => {
285
288
  const baseName = `read_${resourceNameToToolName(resource.name)}`;
286
- return isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(baseName));
289
+ return isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex);
287
290
  }).length
288
291
  : 0;
289
292
  const totalItems = toolCount + resourceCount;
@@ -1,6 +1,7 @@
1
1
  import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
2
  import { createPanelKeys, type PanelKeybindings, type PanelKeys } from "./panel-keys.ts";
3
3
  import type { ImportKind } from "./types.ts";
4
+ import { getConfigDirName } from "./agent-dir.ts";
4
5
  import { KNOWN_SERVER_PRESETS, type ConfigWritePreview, type KnownServerPreset, type McpDiscoverySummary } from "./config.ts";
5
6
  import type { McpOnboardingState } from "./onboarding-state.ts";
6
7
 
@@ -556,7 +557,7 @@ export class McpSetupPanel {
556
557
  "3. ~/.agents/mcp/mcp.json",
557
558
  "4. <Pi agent dir>/mcp.json",
558
559
  "5. .mcp.json",
559
- "6. .pi/mcp.json",
560
+ `6. ${getConfigDirName()}/mcp.json`,
560
561
  `Host discovery: ${this.discovery.hostConfigDiscovery}. Conflicts reported: ${this.discovery.conflicts.length}.`,
561
562
  ...this.discovery.conflicts.slice(0, 8).map((conflict) =>
562
563
  `${conflict.serverName}: ${conflict.sources.map((source) => source.path).join(" -> ")} (winner: ${conflict.winner.path})`,
@@ -19,11 +19,12 @@ import type {
19
19
  ToolMetadata,
20
20
  PromptMetadata,
21
21
  } from "./types.ts";
22
- import { formatPromptCommandName, formatToolName, getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix, type ToolPrefix } from "./types.ts";
22
+ import { createToolSelectorCandidateIndex, formatPromptCommandName, formatToolName, getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix, type ToolPrefix } from "./types.ts";
23
23
  import { resourceNameToToolName } from "./resource-tools.ts";
24
24
  import {
25
25
  extractToolUiStreamMode,
26
26
  interpolateEnvRecord,
27
+ interpolateEnvVars,
27
28
  resolveBearerToken,
28
29
  resolveConfigPath,
29
30
  resolveServerUrl,
@@ -90,6 +91,14 @@ export function computeServerHash(definition: ServerEntry): string {
90
91
  cwd: resolveConfigPath(definition.cwd),
91
92
  url: resolveServerUrl(definition),
92
93
  headers: interpolateEnvRecord(definition.headers),
94
+ requestHeadersCommand: definition.requestHeadersCommand
95
+ ? {
96
+ command: interpolateEnvVars(definition.requestHeadersCommand.command),
97
+ args: definition.requestHeadersCommand.args?.map(interpolateEnvVars),
98
+ env: interpolateEnvRecord(definition.requestHeadersCommand.env),
99
+ timeoutMs: definition.requestHeadersCommand.timeoutMs,
100
+ }
101
+ : undefined,
93
102
  auth: definition.auth,
94
103
  protocolVersion: definition.protocolVersion,
95
104
  bearerToken: resolveBearerToken(definition),
@@ -184,8 +193,10 @@ export function reconstructToolMetadata(
184
193
  const metadata: ToolMetadata[] = [];
185
194
  const seenNames = new Set<string>();
186
195
  const effectivePrefix = resolveToolPrefix(definition, prefix);
187
- const getOtherCurrentCandidates = (toolName: string): Set<string> | undefined => {
188
- if (!configuredServers || !cache) return undefined;
196
+ const hasToolFilters =
197
+ (Array.isArray(definition.includeTools) && definition.includeTools.length > 0) ||
198
+ (Array.isArray(definition.excludeTools) && definition.excludeTools.length > 0);
199
+ const selectorCandidateIndex = hasToolFilters && configuredServers && cache ? (() => {
189
200
  const candidates = new Set<string>();
190
201
  for (const [otherServerName, otherDefinition] of Object.entries(configuredServers)) {
191
202
  const otherEntry = cache.servers[otherServerName];
@@ -202,16 +213,15 @@ export function reconstructToolMetadata(
202
213
  }
203
214
  }
204
215
  }
205
- for (const candidate of getToolNameCandidates(toolName, serverName, effectivePrefix, false)) candidates.delete(candidate);
206
- return candidates;
207
- };
216
+ return createToolSelectorCandidateIndex(candidates);
217
+ })() : undefined;
208
218
 
209
219
  for (const tool of entry.tools ?? []) {
210
220
  if (!tool?.name) continue;
211
221
  if (!isUiToolVisibleToModel(tool.uiVisibility)) {
212
222
  continue;
213
223
  }
214
- if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(tool.name))) {
224
+ if (!isToolAllowed(tool.name, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) {
215
225
  continue;
216
226
  }
217
227
 
@@ -236,7 +246,7 @@ export function reconstructToolMetadata(
236
246
  for (const resource of entry.resources ?? []) {
237
247
  if (!resource?.name || !resource?.uri) continue;
238
248
  const baseName = `read_${resourceNameToToolName(resource.name)}`;
239
- if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, getOtherCurrentCandidates(baseName))) {
249
+ if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools, selectorCandidateIndex)) {
240
250
  continue;
241
251
  }
242
252