@herbertgao/pi-extensions 2026.9.10 → 2026.9.12

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 (271) hide show
  1. package/README.md +1 -0
  2. package/THIRD_PARTY_NOTICES.md +26 -0
  3. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
  5. package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
  6. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -2
  7. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  8. package/node_modules/@herbertgao/pi-subagents/package.json +2 -2
  9. package/node_modules/@herbertgao/resume-from/package.json +1 -1
  10. package/node_modules/@herbertgao/sol-pi/README.md +3 -3
  11. package/node_modules/@herbertgao/sol-pi/THIRD_PARTY_NOTICES.md +4 -4
  12. package/node_modules/@herbertgao/sol-pi/agents-install.md +4 -4
  13. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +6 -6
  14. package/node_modules/@herbertgao/sol-pi/package.json +2 -2
  15. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  16. package/node_modules/@narumitw/pi-btw/dist/index.ts +39 -89
  17. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  18. package/node_modules/@narumitw/pi-btw/package.json +4 -4
  19. package/node_modules/@narumitw/pi-btw/src/btw.ts +28 -87
  20. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +8 -0
  21. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +40 -37
  22. package/node_modules/@narumitw/pi-caffeinate/README.md +21 -66
  23. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +10 -41
  24. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
  25. package/node_modules/@narumitw/pi-caffeinate/package.json +50 -51
  26. package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +637 -663
  27. package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +114 -120
  28. package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +29 -29
  29. package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +108 -126
  30. package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +124 -128
  31. package/node_modules/pi-mcp-adapter/CHANGELOG.md +62 -0
  32. package/node_modules/pi-mcp-adapter/README.md +75 -7
  33. package/node_modules/pi-mcp-adapter/bearer-command-resolver.ts +193 -0
  34. package/node_modules/pi-mcp-adapter/cli.js +56 -5
  35. package/node_modules/pi-mcp-adapter/commands.ts +96 -5
  36. package/node_modules/pi-mcp-adapter/config.ts +73 -6
  37. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +22 -28
  38. package/node_modules/pi-mcp-adapter/direct-tools.ts +13 -5
  39. package/node_modules/pi-mcp-adapter/dist/abort.d.ts +2 -0
  40. package/node_modules/pi-mcp-adapter/dist/abort.js +36 -0
  41. package/node_modules/pi-mcp-adapter/dist/abort.js.map +1 -0
  42. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.d.ts +6 -0
  43. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js +194 -0
  44. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js.map +1 -0
  45. package/node_modules/pi-mcp-adapter/dist/config.d.ts +5 -0
  46. package/node_modules/pi-mcp-adapter/dist/config.js +75 -6
  47. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  48. package/node_modules/pi-mcp-adapter/dist/consent-manager.d.ts +18 -0
  49. package/node_modules/pi-mcp-adapter/dist/consent-manager.js +88 -0
  50. package/node_modules/pi-mcp-adapter/dist/consent-manager.js.map +1 -0
  51. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.d.ts +17 -0
  52. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js +316 -0
  53. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js.map +1 -0
  54. package/node_modules/pi-mcp-adapter/dist/errors.d.ts +131 -0
  55. package/node_modules/pi-mcp-adapter/dist/errors.js +278 -0
  56. package/node_modules/pi-mcp-adapter/dist/errors.js.map +1 -0
  57. package/node_modules/pi-mcp-adapter/dist/http-ca.d.ts +8 -0
  58. package/node_modules/pi-mcp-adapter/dist/http-ca.js +88 -0
  59. package/node_modules/pi-mcp-adapter/dist/http-ca.js.map +1 -0
  60. package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +15 -0
  61. package/node_modules/pi-mcp-adapter/dist/jev-client.js +340 -0
  62. package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -0
  63. package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +77 -0
  64. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js +2 -0
  65. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js.map +1 -0
  66. package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +22 -0
  67. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +85 -0
  68. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -0
  69. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.d.ts +2 -0
  70. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +55 -0
  71. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -0
  72. package/node_modules/pi-mcp-adapter/dist/lifecycle.d.ts +56 -0
  73. package/node_modules/pi-mcp-adapter/dist/lifecycle.js +410 -0
  74. package/node_modules/pi-mcp-adapter/dist/lifecycle.js.map +1 -0
  75. package/node_modules/pi-mcp-adapter/dist/logger.d.ts +51 -0
  76. package/node_modules/pi-mcp-adapter/dist/logger.js +131 -0
  77. package/node_modules/pi-mcp-adapter/dist/logger.js.map +1 -0
  78. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.d.ts +26 -0
  79. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js +103 -0
  80. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js.map +1 -0
  81. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.d.ts +118 -0
  82. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1098 -0
  83. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -0
  84. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +168 -0
  85. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +1117 -0
  86. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -0
  87. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +7 -140
  88. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -1
  89. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.d.ts +53 -0
  90. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js +441 -0
  91. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js.map +1 -0
  92. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.d.ts +148 -0
  93. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js +689 -0
  94. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js.map +1 -0
  95. package/node_modules/pi-mcp-adapter/dist/mcp-probe.d.ts +6 -0
  96. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js +166 -0
  97. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js.map +1 -0
  98. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.d.ts +102 -0
  99. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js +369 -0
  100. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js.map +1 -0
  101. package/node_modules/pi-mcp-adapter/dist/mcp-trace.d.ts +95 -0
  102. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js +242 -0
  103. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js.map +1 -0
  104. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +2 -11
  105. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  106. package/node_modules/pi-mcp-adapter/dist/npx-resolver.d.ts +6 -0
  107. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js +505 -0
  108. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js.map +1 -0
  109. package/node_modules/pi-mcp-adapter/dist/request-headers-command.d.ts +10 -0
  110. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js +312 -0
  111. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js.map +1 -0
  112. package/node_modules/pi-mcp-adapter/dist/runtime-owner.d.ts +13 -0
  113. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js +97 -0
  114. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js.map +1 -0
  115. package/node_modules/pi-mcp-adapter/dist/sampling-handler.d.ts +17 -0
  116. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js +203 -0
  117. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js.map +1 -0
  118. package/node_modules/pi-mcp-adapter/dist/secure-keyring.d.ts +11 -0
  119. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js +172 -0
  120. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js.map +1 -0
  121. package/node_modules/pi-mcp-adapter/dist/server-manager.d.ts +157 -0
  122. package/node_modules/pi-mcp-adapter/dist/server-manager.js +1743 -0
  123. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -0
  124. package/node_modules/pi-mcp-adapter/dist/session-approvals.d.ts +34 -0
  125. package/node_modules/pi-mcp-adapter/dist/session-approvals.js +140 -0
  126. package/node_modules/pi-mcp-adapter/dist/session-approvals.js.map +1 -0
  127. package/node_modules/pi-mcp-adapter/dist/session-recovery.d.ts +26 -0
  128. package/node_modules/pi-mcp-adapter/dist/session-recovery.js +142 -0
  129. package/node_modules/pi-mcp-adapter/dist/session-recovery.js.map +1 -0
  130. package/node_modules/pi-mcp-adapter/dist/state.d.ts +73 -0
  131. package/node_modules/pi-mcp-adapter/dist/state.js +2 -0
  132. package/node_modules/pi-mcp-adapter/dist/state.js.map +1 -0
  133. package/node_modules/pi-mcp-adapter/dist/types.d.ts +36 -0
  134. package/node_modules/pi-mcp-adapter/dist/types.js +18 -0
  135. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  136. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.d.ts +17 -0
  137. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js +219 -0
  138. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js.map +1 -0
  139. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.d.ts +15 -0
  140. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js +85 -0
  141. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js.map +1 -0
  142. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +1 -0
  143. package/node_modules/pi-mcp-adapter/dist/utils.js +13 -0
  144. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  145. package/node_modules/pi-mcp-adapter/elicitation-handler.ts +29 -19
  146. package/node_modules/pi-mcp-adapter/examples/jev-accessibility-loop.mjs +85 -0
  147. package/node_modules/pi-mcp-adapter/examples/jev-semantic-filter.mjs +34 -0
  148. package/node_modules/pi-mcp-adapter/index.ts +129 -6
  149. package/node_modules/pi-mcp-adapter/init.ts +6 -19
  150. package/node_modules/pi-mcp-adapter/jev-client.ts +273 -0
  151. package/node_modules/pi-mcp-adapter/jev-contracts.ts +53 -0
  152. package/node_modules/pi-mcp-adapter/jev-key-store.ts +79 -0
  153. package/node_modules/pi-mcp-adapter/lifecycle.ts +14 -3
  154. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +7 -142
  155. package/node_modules/pi-mcp-adapter/mcp-code.ts +119 -11
  156. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +4 -0
  157. package/node_modules/pi-mcp-adapter/mcp-references.ts +5 -3
  158. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +5 -0
  159. package/node_modules/pi-mcp-adapter/mcp-tasks.ts +467 -0
  160. package/node_modules/pi-mcp-adapter/metadata-cache.ts +2 -13
  161. package/node_modules/pi-mcp-adapter/namespace-tools.ts +1 -1
  162. package/node_modules/pi-mcp-adapter/package.json +17 -9
  163. package/node_modules/pi-mcp-adapter/proxy-modes.ts +319 -166
  164. package/node_modules/pi-mcp-adapter/request-headers-command.ts +6 -3
  165. package/node_modules/pi-mcp-adapter/search-ranking.ts +38 -7
  166. package/node_modules/pi-mcp-adapter/secure-keyring.ts +170 -0
  167. package/node_modules/pi-mcp-adapter/semantic-search.ts +186 -0
  168. package/node_modules/pi-mcp-adapter/server-manager.ts +293 -50
  169. package/node_modules/pi-mcp-adapter/session-approvals.ts +14 -0
  170. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +2 -0
  171. package/node_modules/pi-mcp-adapter/state.ts +3 -1
  172. package/node_modules/pi-mcp-adapter/tool-approval.ts +26 -4
  173. package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -14
  174. package/node_modules/pi-mcp-adapter/tool-registrar.ts +8 -6
  175. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +4 -1
  176. package/node_modules/pi-mcp-adapter/types.ts +53 -0
  177. package/node_modules/pi-mcp-adapter/ui-server.ts +17 -9
  178. package/node_modules/pi-mcp-adapter/utils.ts +16 -0
  179. package/node_modules/pi-multi-account/CHANGELOG.md +1209 -0
  180. package/node_modules/pi-multi-account/CONTRIBUTING.md +61 -0
  181. package/node_modules/pi-multi-account/LICENSE +21 -0
  182. package/node_modules/pi-multi-account/README.md +197 -0
  183. package/node_modules/pi-multi-account/SECURITY.md +27 -0
  184. package/node_modules/pi-multi-account/auth-file-transaction.ts +56 -0
  185. package/node_modules/pi-multi-account/child-usability.ts +233 -0
  186. package/node_modules/pi-multi-account/compaction-summary.ts +32 -0
  187. package/node_modules/pi-multi-account/completion-route-planner.ts +224 -0
  188. package/node_modules/pi-multi-account/context-guard.ts +420 -0
  189. package/node_modules/pi-multi-account/cursor/LICENSE +21 -0
  190. package/node_modules/pi-multi-account/cursor/NOTICE +2 -0
  191. package/node_modules/pi-multi-account/cursor/auth.ts +165 -0
  192. package/node_modules/pi-multi-account/cursor/bridge-handle.ts +155 -0
  193. package/node_modules/pi-multi-account/cursor/conversation-registry.ts +104 -0
  194. package/node_modules/pi-multi-account/cursor/cursor-models-raw.json +611 -0
  195. package/node_modules/pi-multi-account/cursor/cursor-shared.ts +192 -0
  196. package/node_modules/pi-multi-account/cursor/h2-bridge.mjs +175 -0
  197. package/node_modules/pi-multi-account/cursor/index.ts +572 -0
  198. package/node_modules/pi-multi-account/cursor/message-parsing.ts +323 -0
  199. package/node_modules/pi-multi-account/cursor/prompt-usage.ts +53 -0
  200. package/node_modules/pi-multi-account/cursor/proto/agent_pb.ts +15294 -0
  201. package/node_modules/pi-multi-account/cursor/proxy.ts +2510 -0
  202. package/node_modules/pi-multi-account/cursor/session-lifecycle.ts +40 -0
  203. package/node_modules/pi-multi-account/cursor/sse-keepalive.ts +24 -0
  204. package/node_modules/pi-multi-account/cursor/stream-lifecycle.ts +193 -0
  205. package/node_modules/pi-multi-account/cursor/upstream-watchdog.ts +88 -0
  206. package/node_modules/pi-multi-account/cursor-bridge.ts +240 -0
  207. package/node_modules/pi-multi-account/cursor-model-name.ts +12 -0
  208. package/node_modules/pi-multi-account/index.ts +11825 -0
  209. package/node_modules/pi-multi-account/model-catalog.ts +354 -0
  210. package/node_modules/pi-multi-account/package.json +101 -0
  211. package/node_modules/pi-multi-account/pi-contract.ts +281 -0
  212. package/node_modules/pi-multi-account/provider-payload-stream.ts +44 -0
  213. package/node_modules/pi-multi-account/provider-priority.ts +189 -0
  214. package/node_modules/pi-multi-account/slot-proxy-auth.ts +167 -0
  215. package/node_modules/pi-multi-account/slot-proxy.ts +344 -0
  216. package/node_modules/pi-multi-account/state-file-transaction.ts +67 -0
  217. package/node_modules/pi-multi-account/usage.ts +1099 -0
  218. package/node_modules/pi-typesafe/README.md +6 -2
  219. package/node_modules/pi-typesafe/dist/client.d.ts +11 -0
  220. package/node_modules/pi-typesafe/dist/client.js +45 -10
  221. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  222. package/node_modules/pi-typesafe/dist/index.js +1 -1
  223. package/node_modules/pi-typesafe/package.json +2 -2
  224. package/node_modules/pi-web-access/CHANGELOG.md +36 -0
  225. package/node_modules/pi-web-access/README.md +75 -18
  226. package/node_modules/pi-web-access/anysearch.ts +4 -15
  227. package/node_modules/pi-web-access/bocha.ts +3 -22
  228. package/node_modules/pi-web-access/brave.ts +3 -21
  229. package/node_modules/pi-web-access/brightdata.ts +5 -32
  230. package/node_modules/pi-web-access/content-find.ts +168 -53
  231. package/node_modules/pi-web-access/curator-page.ts +4 -1
  232. package/node_modules/pi-web-access/curator-run.ts +2 -1
  233. package/node_modules/pi-web-access/curator-server.ts +1 -0
  234. package/node_modules/pi-web-access/dist/index.js +24620 -0
  235. package/node_modules/pi-web-access/domain-filter-normalization.ts +14 -0
  236. package/node_modules/pi-web-access/duckduckgo.ts +3 -21
  237. package/node_modules/pi-web-access/extract.ts +3 -1
  238. package/node_modules/pi-web-access/firecrawl.ts +5 -29
  239. package/node_modules/pi-web-access/gemini-search.ts +81 -32
  240. package/node_modules/pi-web-access/index.ts +149 -148
  241. package/node_modules/pi-web-access/jina-search.ts +4 -15
  242. package/node_modules/pi-web-access/kagi.ts +4 -13
  243. package/node_modules/pi-web-access/kimi-search.ts +5 -30
  244. package/node_modules/pi-web-access/mistral-search.ts +1 -15
  245. package/node_modules/pi-web-access/ollama.ts +2 -7
  246. package/node_modules/pi-web-access/openai-search.ts +174 -36
  247. package/node_modules/pi-web-access/opencode-session-headers.ts +24 -0
  248. package/node_modules/pi-web-access/package.json +10 -4
  249. package/node_modules/pi-web-access/page-query.ts +10 -2
  250. package/node_modules/pi-web-access/parallel.ts +1 -15
  251. package/node_modules/pi-web-access/pdf-extract.ts +3 -0
  252. package/node_modules/pi-web-access/querit.ts +5 -29
  253. package/node_modules/pi-web-access/search-answer-formatting.ts +11 -0
  254. package/node_modules/pi-web-access/search-result-count-normalization.ts +4 -0
  255. package/node_modules/pi-web-access/search1api.ts +5 -29
  256. package/node_modules/pi-web-access/searchinfinity.ts +5 -29
  257. package/node_modules/pi-web-access/searxng.ts +3 -21
  258. package/node_modules/pi-web-access/serpapi.ts +5 -28
  259. package/node_modules/pi-web-access/serpbase.ts +3 -22
  260. package/node_modules/pi-web-access/serpdive.ts +3 -21
  261. package/node_modules/pi-web-access/serper.ts +5 -28
  262. package/node_modules/pi-web-access/serply.ts +197 -0
  263. package/node_modules/pi-web-access/source-check.ts +11 -47
  264. package/node_modules/pi-web-access/summary-review.ts +7 -3
  265. package/node_modules/pi-web-access/tavily.ts +3 -21
  266. package/node_modules/pi-web-access/tinyfish.ts +5 -29
  267. package/node_modules/pi-web-access/utils.ts +9 -1
  268. package/node_modules/pi-web-access/valyu.ts +5 -28
  269. package/node_modules/pi-web-access/xai-search.ts +1 -15
  270. package/node_modules/pi-web-access/xcrawl.ts +5 -32
  271. package/package.json +23 -16
@@ -0,0 +1,193 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ const DEFAULT_TTL_MS = 5 * 60_000;
4
+ const ENV_TTL_MS = "PI_MCP_ADAPTER_BEARER_COMMAND_TTL_MS";
5
+ const COMMAND_TIMEOUT_MS = 10_000;
6
+ const COMMAND_MAX_OUTPUT_BYTES = 1024 * 1024;
7
+ const USE_PROCESS_GROUP = process.platform !== "win32";
8
+
9
+ function resolveDefaultTtlMs(): number {
10
+ const parsed = Number(process.env[ENV_TTL_MS] || DEFAULT_TTL_MS);
11
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_TTL_MS;
12
+ }
13
+
14
+ function abortReason(signal: AbortSignal): unknown {
15
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
16
+ }
17
+
18
+ function runCommand(command: string, context: string, signal: AbortSignal): Promise<string> {
19
+ return new Promise((resolve, reject) => {
20
+ let output = Buffer.alloc(0);
21
+ let settled = false;
22
+ let terminating = false;
23
+ const child = spawn(command.slice(1), {
24
+ shell: true,
25
+ stdio: ["ignore", "pipe", "ignore"],
26
+ windowsHide: true,
27
+ detached: USE_PROCESS_GROUP,
28
+ });
29
+
30
+ const kill = async (): Promise<void> => {
31
+ if (child.pid === undefined) return;
32
+ if (!USE_PROCESS_GROUP) {
33
+ await new Promise<void>((resolveKill, rejectKill) => {
34
+ const killer = spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
35
+ stdio: "ignore",
36
+ windowsHide: true,
37
+ });
38
+ killer.on("error", () => rejectKill(new Error("Failed to stop bearer token command")));
39
+ killer.on("close", code => {
40
+ if (code === 0 || code === 128) resolveKill();
41
+ else rejectKill(new Error(`Failed to stop bearer token command: taskkill exited with code ${code ?? "unknown"}`));
42
+ });
43
+ });
44
+ return;
45
+ }
46
+ try {
47
+ process.kill(-child.pid, "SIGKILL");
48
+ } catch (error) {
49
+ if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
50
+ }
51
+ };
52
+ const finish = (error: unknown, token?: string) => {
53
+ if (settled) return;
54
+ settled = true;
55
+ clearTimeout(timer);
56
+ signal.removeEventListener("abort", onAbort);
57
+ if (error !== undefined) reject(error);
58
+ else resolve(token!);
59
+ };
60
+ const terminate = async (error: unknown) => {
61
+ if (settled || terminating) return;
62
+ terminating = true;
63
+ clearTimeout(timer);
64
+ signal.removeEventListener("abort", onAbort);
65
+ try {
66
+ await kill();
67
+ finish(error);
68
+ } catch (cleanupError) {
69
+ finish(cleanupError);
70
+ }
71
+ };
72
+ const onAbort = () => {
73
+ void terminate(abortReason(signal));
74
+ };
75
+ const timer = setTimeout(() => {
76
+ void terminate(new Error(`Failed to resolve ${context}: command timed out after ${COMMAND_TIMEOUT_MS}ms`));
77
+ }, COMMAND_TIMEOUT_MS);
78
+
79
+ signal.addEventListener("abort", onAbort, { once: true });
80
+ if (signal.aborted) {
81
+ onAbort();
82
+ return;
83
+ }
84
+ child.on("error", () => {
85
+ if (!terminating) finish(new Error(`Failed to resolve ${context}: command failed to start`));
86
+ });
87
+ child.stdout.on("data", (chunk: Buffer | string) => {
88
+ if (settled || terminating) return;
89
+ output = Buffer.concat([output, Buffer.from(chunk)]);
90
+ if (output.byteLength > COMMAND_MAX_OUTPUT_BYTES) {
91
+ void terminate(new Error(`Failed to resolve ${context}: command output exceeded 1 MiB`));
92
+ }
93
+ });
94
+ child.on("close", code => {
95
+ if (settled || terminating) return;
96
+ if (code !== 0) {
97
+ finish(new Error(`Failed to resolve ${context}: command exited with code ${code ?? "unknown"}`));
98
+ return;
99
+ }
100
+ const token = output.toString("utf8").trim();
101
+ if (!token) {
102
+ finish(new Error(`Failed to resolve ${context}: command returned empty output`));
103
+ return;
104
+ }
105
+ finish(undefined, token);
106
+ });
107
+ });
108
+ }
109
+
110
+ type Inflight = {
111
+ controller: AbortController;
112
+ promise: Promise<string>;
113
+ waiters: number;
114
+ };
115
+
116
+ /** Resolve and periodically refresh one command-backed bearer token. */
117
+ export class BearerCommandResolver {
118
+ readonly #command: string;
119
+ readonly #context: string;
120
+ readonly #ttlMs: number;
121
+ #cached: { token: string; expiresAt: number } | undefined;
122
+ #failure: { error: unknown; retryAt: number } | undefined;
123
+ #inflight: Inflight | undefined;
124
+
125
+ constructor(command: string, context: string, ttlMs: number = resolveDefaultTtlMs()) {
126
+ this.#command = command;
127
+ this.#context = context;
128
+ this.#ttlMs = ttlMs;
129
+ }
130
+
131
+ resolve(signal?: AbortSignal): Promise<string> {
132
+ if (signal?.aborted) return Promise.reject(abortReason(signal));
133
+ const now = Date.now();
134
+ if (this.#cached !== undefined && now < this.#cached.expiresAt) {
135
+ return Promise.resolve(this.#cached.token);
136
+ }
137
+ if (this.#failure !== undefined && now < this.#failure.retryAt) {
138
+ return this.#cached !== undefined
139
+ ? Promise.resolve(this.#cached.token)
140
+ : Promise.reject(this.#failure.error);
141
+ }
142
+
143
+ let inflight = this.#inflight;
144
+ if (inflight === undefined) {
145
+ const controller = new AbortController();
146
+ let current: Inflight;
147
+ const promise = runCommand(this.#command, this.#context, controller.signal)
148
+ .then(token => {
149
+ this.#cached = { token, expiresAt: Date.now() + this.#ttlMs };
150
+ this.#failure = undefined;
151
+ return token;
152
+ })
153
+ .catch(error => {
154
+ // Request cancellation is not a helper outage. Do not let one
155
+ // cancelled request suppress refresh attempts for the next caller.
156
+ if (!controller.signal.aborted) {
157
+ this.#failure = { error, retryAt: Date.now() + this.#ttlMs };
158
+ }
159
+ if (this.#cached !== undefined) return this.#cached.token;
160
+ throw error;
161
+ })
162
+ .finally(() => {
163
+ if (this.#inflight === current) this.#inflight = undefined;
164
+ });
165
+ current = { controller, promise, waiters: 0 };
166
+ this.#inflight = current;
167
+ inflight = current;
168
+ }
169
+ return this.#wait(inflight, signal);
170
+ }
171
+
172
+ #wait(inflight: Inflight, signal?: AbortSignal): Promise<string> {
173
+ inflight.waiters++;
174
+ let onAbort: (() => void) | undefined;
175
+ const result = signal === undefined
176
+ ? inflight.promise
177
+ : Promise.race([
178
+ inflight.promise,
179
+ new Promise<string>((_resolve, reject) => {
180
+ onAbort = () => reject(abortReason(signal));
181
+ signal.addEventListener("abort", onAbort, { once: true });
182
+ }),
183
+ ]);
184
+ return result.finally(() => {
185
+ if (onAbort) signal!.removeEventListener("abort", onAbort);
186
+ inflight.waiters--;
187
+ if (this.#inflight === inflight && inflight.waiters === 0) {
188
+ this.#inflight = undefined;
189
+ inflight.controller.abort(signal?.reason);
190
+ }
191
+ });
192
+ }
193
+ }
@@ -78,6 +78,11 @@ function printHelp(log = console.log) {
78
78
  log(" pi-mcp-adapter token set <server> Store a token read from stdin (masked prompt or pipe; never argv)");
79
79
  log(" pi-mcp-adapter token status <server> Report whether a stored token matches the configured URL");
80
80
  log(" pi-mcp-adapter token remove <server> Remove the stored token");
81
+ log("");
82
+ log("TypeSafe API key storage:");
83
+ log(" pi-mcp-adapter key set typesafe Store a key read from stdin (masked prompt or pipe; never argv)");
84
+ log(" pi-mcp-adapter key status typesafe Report the effective credential source without revealing it");
85
+ log(" pi-mcp-adapter key remove typesafe Remove the stored key");
81
86
  }
82
87
 
83
88
  function readJsonFile(filePath) {
@@ -214,8 +219,8 @@ async function importTokenModules(error) {
214
219
  }
215
220
  }
216
221
 
217
- function readTokenFromStdin(stdin) {
218
- if (stdin.isTTY) return readTokenMasked(stdin);
222
+ function readSecretFromStdin(stdin, prompt = "Enter bearer token (input hidden): ") {
223
+ if (stdin.isTTY) return readSecretMasked(stdin, prompt);
219
224
  return new Promise((resolve, reject) => {
220
225
  let data = "";
221
226
  stdin.setEncoding("utf8");
@@ -228,9 +233,9 @@ function readTokenFromStdin(stdin) {
228
233
  }
229
234
 
230
235
  // Raw-mode masked prompt: the token never echoes to the terminal.
231
- function readTokenMasked(stdin) {
236
+ function readSecretMasked(stdin, prompt) {
232
237
  return new Promise((resolve, reject) => {
233
- process.stderr.write("Enter bearer token (input hidden): ");
238
+ process.stderr.write(prompt);
234
239
  stdin.setRawMode(true);
235
240
  stdin.resume();
236
241
  stdin.setEncoding("utf8");
@@ -306,7 +311,7 @@ async function runToken(argv, log, error, stdin) {
306
311
  }
307
312
 
308
313
  if (action === "set") {
309
- const token = await readTokenFromStdin(stdin);
314
+ const token = await readSecretFromStdin(stdin);
310
315
  if (!token) {
311
316
  error("No token provided on stdin.");
312
317
  return 1;
@@ -352,6 +357,48 @@ async function runToken(argv, log, error, stdin) {
352
357
  return 0;
353
358
  }
354
359
 
360
+ async function runKey(argv, log, error, stdin) {
361
+ const [action, provider, ...extra] = argv;
362
+ if (!["set", "status", "remove"].includes(action) || provider !== "typesafe") {
363
+ error("Usage: pi-mcp-adapter key <set|status|remove> typesafe");
364
+ error("`key set` reads the API key from stdin only. Never pass the key as an argument.");
365
+ return 1;
366
+ }
367
+ if (extra.length > 0) {
368
+ error("Unexpected extra arguments. The API key must not be passed on the command line; pipe it on stdin or use the interactive prompt.");
369
+ return 1;
370
+ }
371
+ let store;
372
+ try { store = await import("./dist/jev-key-store.js"); }
373
+ catch (err) {
374
+ error("Unable to load TypeSafe key command module.");
375
+ error(`Import failed: ${err instanceof Error ? err.message : String(err)}`);
376
+ return 1;
377
+ }
378
+
379
+ if (action === "set") {
380
+ const apiKey = await readSecretFromStdin(stdin, "Enter TypeSafe API key (input hidden): ");
381
+ if (!apiKey) { error("No API key provided on stdin."); return 1; }
382
+ try { store.saveJevApiKey(apiKey); }
383
+ catch (err) { error(err instanceof Error ? err.message : "TypeSafe API key could not be stored."); return 1; }
384
+ log("TypeSafe API key stored in the OS secure credential store.");
385
+ if (Object.hasOwn(process.env, "TYPESAFE_API_KEY")) log("Note: TYPESAFE_API_KEY is present and overrides the stored key.");
386
+ return 0;
387
+ }
388
+ if (action === "status") {
389
+ const status = store.resolveJevCredential();
390
+ if (status.status === "present") { log(`source=${status.source}`); return 0; }
391
+ if (status.status === "unavailable") { error(`unavailable: ${status.message}`); return 1; }
392
+ log("missing");
393
+ return 1;
394
+ }
395
+ try { store.removeJevApiKey(); }
396
+ catch (err) { error(err instanceof Error ? err.message : "TypeSafe API key could not be removed."); return 1; }
397
+ log("TypeSafe API key removed from the OS secure credential store.");
398
+ if (Object.hasOwn(process.env, "TYPESAFE_API_KEY")) log("TYPESAFE_API_KEY is still present and overrides the stored key.");
399
+ return 0;
400
+ }
401
+
355
402
  export async function main(argv = process.argv.slice(2), log = console.log, error = console.error, stdin = process.stdin) {
356
403
  const [command, ...rest] = argv;
357
404
 
@@ -364,6 +411,10 @@ export async function main(argv = process.argv.slice(2), log = console.log, erro
364
411
  return runToken(rest, log, error, stdin);
365
412
  }
366
413
 
414
+ if (command === "key") {
415
+ return runKey(rest, log, error, stdin);
416
+ }
417
+
367
418
  if (command === "install") {
368
419
  error("The custom downloader has been retired.");
369
420
  error("Use `pi install npm:pi-mcp-adapter` instead, then optionally run `pi-mcp-adapter init`.");
@@ -1,3 +1,4 @@
1
+ import { existsSync, readFileSync } from "node:fs";
1
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
3
  import type { OverlayHandle } from "@earendil-works/pi-tui";
3
4
  import type { McpExtensionState } from "./state.ts";
@@ -14,8 +15,10 @@ import {
14
15
  previewSharedServerEntry,
15
16
  previewStarterSharedConfig,
16
17
  writeDirectToolsConfig,
18
+ writeJevSemanticSearchConfig,
17
19
  writeProjectServerDisabledOverride,
18
20
  writeSharedServerEntry,
21
+ writeSharedConfigText,
19
22
  writeStarterSharedConfig,
20
23
  } from "./config.ts";
21
24
  import { markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateMetadataCache, updateStatusBar, getFailureAgeSeconds, getFailureMessage, clearFailure, recordFailure } from "./init.ts";
@@ -26,8 +29,9 @@ import { supportsOAuth, authenticate, removeAuth, type McpOAuthRuntime } from ".
26
29
  import { getAuthStorageOptions, inspectAuthForUrl } from "./mcp-auth.ts";
27
30
  import { inspectBearerTokenForUrl, removeBearerToken } from "./mcp-bearer-store.ts";
28
31
  import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markSharedConfigHintShown } from "./onboarding-state.ts";
29
- import { openPath, resolveServerUrl, sanitizeTerminalText } from "./utils.ts";
32
+ import { formatTerminalError, openPath, resolveServerUrl, sanitizeTerminalText } from "./utils.ts";
30
33
  import { isAbortError } from "./runtime-owner.ts";
34
+ import { resolveJevCredential } from "./jev-key-store.ts";
31
35
 
32
36
  function terminalHyperlink(label: string, url: string): string {
33
37
  return `\u001B]8;;${sanitizeTerminalText(url)}\u001B\\${sanitizeTerminalText(label)}\u001B]8;;\u001B\\`;
@@ -46,6 +50,93 @@ function canRenderPanel(ctx: ExtensionContext): boolean {
46
50
  return ctx.hasUI && ctx.mode === "tui";
47
51
  }
48
52
 
53
+ export async function editSharedConfig(ctx: ExtensionContext, target: SharedConfigTarget): Promise<boolean> {
54
+ if (!ctx.hasUI) return false;
55
+ const path = getSharedConfigPath(target, ctx.cwd);
56
+ const before = existsSync(path) ? readFileSync(path, "utf8") : '{\n "mcpServers": {}\n}\n';
57
+ const after = await ctx.ui.editor(`Edit ${path} (Ctrl+G opens $EDITOR)`, before);
58
+ if (after === undefined || after === before) return false;
59
+ try {
60
+ writeSharedConfigText(path, after);
61
+ } catch (error) {
62
+ ctx.ui.notify(`MCP: not saved: ${formatTerminalError(error)}`, "error");
63
+ return false;
64
+ }
65
+ return true;
66
+ }
67
+
68
+ export async function setupJevSemanticSearch(
69
+ state: McpExtensionState,
70
+ ctx: ExtensionContext,
71
+ configOverridePath?: string,
72
+ ): Promise<boolean> {
73
+ if (!ctx.hasUI) return false;
74
+ const credential = resolveJevCredential();
75
+ if (credential.status !== "present") {
76
+ const detail = credential.status === "unavailable" ? ` ${credential.message}` : "";
77
+ ctx.ui.notify(
78
+ `Jev needs a TypeSafe API key.${detail}\nRun \`pi-mcp-adapter key set typesafe\` in a terminal, then run \`/mcp jev setup\` again.`,
79
+ "error",
80
+ );
81
+ return false;
82
+ }
83
+
84
+ const servers = Object.keys(state.config.mcpServers)
85
+ .filter((name) => !isServerDisabled(state.config.mcpServers[name]))
86
+ .sort((a, b) => a.localeCompare(b));
87
+ if (servers.length === 0) {
88
+ ctx.ui.notify("Enable or add an MCP server before setting up Jev semantic search.", "error");
89
+ return false;
90
+ }
91
+ const configuredJev = state.config.settings?.jev;
92
+ const payloadDisclosure = configuredJev && configuredJev.scriptEvaluation
93
+ ? " Allowed servers can also be sources for script evaluations, which may send state and MCP results."
94
+ : " Semantic search does not send tool results.";
95
+
96
+ const choice = await ctx.ui.select("Configure Jev semantic search", [
97
+ `Use all ${servers.length} enabled servers (default)`,
98
+ "Restrict to selected servers",
99
+ "Cancel",
100
+ ]);
101
+ if (!choice || choice === "Cancel") return false;
102
+
103
+ let allowedServers: string[];
104
+ if (choice.startsWith("Use all ")) {
105
+ const confirmed = await ctx.ui.confirm(
106
+ "Share MCP tool metadata with Jev?",
107
+ `Semantic searches send the query text, server names, tool paths, tool names, and descriptions from ${servers.length} servers to TypeSafe.${payloadDisclosure}`,
108
+ );
109
+ if (!confirmed) return false;
110
+ allowedServers = servers;
111
+ } else {
112
+ allowedServers = [];
113
+ for (const server of servers) {
114
+ if (await ctx.ui.confirm(
115
+ `Allow ${server}?`,
116
+ `Semantic searches send the query text, server name, tool paths, tool names, and descriptions to TypeSafe.${payloadDisclosure}`,
117
+ )) allowedServers.push(server);
118
+ }
119
+ if (allowedServers.length === 0) {
120
+ ctx.ui.notify("Jev setup cancelled because no servers were allowed.", "info");
121
+ return false;
122
+ }
123
+ }
124
+
125
+ try {
126
+ const result = writeJevSemanticSearchConfig(configOverridePath, ctx.cwd, allowedServers, state.config.settings?.jev);
127
+ ctx.ui.notify(
128
+ result.changed
129
+ ? `Jev semantic search configured for ${allowedServers.length} server${allowedServers.length === 1 ? "" : "s"}. Reloading Pi…`
130
+ : "Jev semantic search is already configured for those servers.",
131
+ "info",
132
+ );
133
+ return result.changed;
134
+ } catch (error) {
135
+ ctx.ui.notify(`Jev setup failed: ${formatTerminalError(error)}`, "error");
136
+ return false;
137
+ }
138
+ }
139
+
49
140
  export async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
50
141
  if (!ctx.hasUI) return;
51
142
 
@@ -194,11 +285,11 @@ export async function reconnectServer(
194
285
  }
195
286
 
196
287
  try {
197
- await state.manager.close(name);
198
288
  state.owner?.throwIfInactive();
199
- const connection = signal
200
- ? await state.manager.connect(name, definition, signal)
201
- : await state.manager.connect(name, definition);
289
+ const current = state.manager.getConnection(name);
290
+ const connection = current
291
+ ? await state.manager.reconnect(name, definition, current, signal)
292
+ : await state.manager.connect(name, definition, signal);
202
293
  state.owner?.throwIfInactive();
203
294
  if (connection.status === "needs-auth") {
204
295
  if (ui) {
@@ -1,5 +1,5 @@
1
1
  // config.ts - Config loading with import support
2
- import { existsSync, readFileSync, realpathSync, statSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
2
+ import { chmodSync, existsSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { parse as parseToml } from "smol-toml";
@@ -9,6 +9,7 @@ import { getAgentPluginSummaries, loadAgentPluginConfigs, type AgentPluginSummar
9
9
  import { cloneBuiltInAgentPluginEntry, isBuiltInAgentPlugin, mergeBuiltInAgentPluginEntries } from "./agent-plugin-provenance.ts";
10
10
  import { loadClaudePluginBundles } from "./claude-plugin-loader.ts";
11
11
  import { loadPackageMcpConfigs } from "./package-mcp-loader.ts";
12
+ import { validateJevSettings } from "./jev-client.ts";
12
13
  import { formatServerNamespace, isServerDisabled, type ClaudePluginConfig, type HostConfigDiscovery, type McpConfig, type ServerEntry, type McpSettings, type ImportKind, type ServerProvenance } from "./types.ts";
13
14
  import { parseJsonWithComments, toStringRecord } from "./utils.ts";
14
15
 
@@ -846,11 +847,21 @@ function validateConfig(raw: unknown): McpConfig {
846
847
  return {
847
848
  mcpServers: toServerEntries(raw.mcpServers ?? raw["mcp-servers"]),
848
849
  ...(Array.isArray(raw.imports) ? { imports: raw.imports as ImportKind[] } : {}),
849
- ...(raw.settings !== undefined ? { settings: raw.settings as McpSettings } : {}),
850
+ ...(raw.settings !== undefined ? { settings: parseSettings(raw.settings) } : {}),
850
851
  ...(raw.claudePlugins !== undefined ? { claudePlugins: parseClaudePlugins(raw.claudePlugins) } : {}),
851
852
  };
852
853
  }
853
854
 
855
+ function parseSettings(value: unknown): McpSettings {
856
+ if (!isRecord(value)) throw new Error("settings must be an object");
857
+ const settings = { ...value } as McpSettings;
858
+ if (value.jev !== undefined) {
859
+ validateJevSettings(value.jev);
860
+ settings.jev = value.jev as NonNullable<McpSettings["jev"]>;
861
+ }
862
+ return settings;
863
+ }
864
+
854
865
  function parseClaudePlugins(value: unknown): ClaudePluginConfig[] {
855
866
  if (!Array.isArray(value)) {
856
867
  console.warn("Invalid claudePlugins config: expected an array");
@@ -1139,11 +1150,67 @@ function readRawConfigObject(filePath: string): Record<string, unknown> {
1139
1150
  }
1140
1151
  }
1141
1152
 
1153
+ function writeConfigText(writePath: string, text: string): void {
1154
+ let mode: number | undefined;
1155
+ try {
1156
+ writePath = realpathSync(writePath);
1157
+ mode = statSync(writePath).mode & 0o777;
1158
+ } catch {}
1159
+ mkdirSync(dirname(writePath), { recursive: true });
1160
+ const tmpPath = `${writePath}.${process.pid}.tmp`;
1161
+ rmSync(tmpPath, { force: true });
1162
+ try {
1163
+ writeFileSync(tmpPath, text, mode === undefined ? "utf-8" : { encoding: "utf-8", mode });
1164
+ if (mode !== undefined) chmodSync(tmpPath, mode);
1165
+ renameSync(tmpPath, writePath);
1166
+ } catch (error) {
1167
+ try { rmSync(tmpPath, { force: true }); } catch {}
1168
+ throw error;
1169
+ }
1170
+ }
1171
+
1142
1172
  function writeRawConfigObject(filePath: string, raw: Record<string, unknown>): void {
1143
- mkdirSync(dirname(filePath), { recursive: true });
1144
- const tmpPath = `${filePath}.${process.pid}.tmp`;
1145
- writeFileSync(tmpPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
1146
- renameSync(tmpPath, filePath);
1173
+ writeConfigText(filePath, `${JSON.stringify(raw, null, 2)}\n`);
1174
+ }
1175
+
1176
+ export function writeSharedConfigText(filePath: string, text: string): void {
1177
+ if (!isRecord(parseJsonWithComments(text))) throw new Error("top-level value must be an object");
1178
+ writeConfigText(filePath, text);
1179
+ }
1180
+
1181
+ export function writeJevSemanticSearchConfig(
1182
+ overridePath: string | undefined,
1183
+ cwd: string,
1184
+ allowedServers: string[],
1185
+ effectiveJev?: unknown,
1186
+ ): { path: string; changed: boolean } {
1187
+ const filePath = overridePath ? getPiGlobalConfigPath(overridePath) : getProjectPiConfigPath(cwd);
1188
+ let raw: Record<string, unknown> = {};
1189
+ if (existsSync(filePath)) {
1190
+ try {
1191
+ const parsed = parseJsonWithComments(readFileSync(filePath, "utf8"));
1192
+ if (!isRecord(parsed)) throw new Error("top-level value must be an object");
1193
+ raw = parsed;
1194
+ } catch (error) {
1195
+ throw new Error(`Failed to update Jev settings at ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
1196
+ }
1197
+ }
1198
+ if (raw.settings !== undefined && !isRecord(raw.settings)) {
1199
+ throw new Error(`Failed to update Jev settings at ${filePath}: settings must be an object`);
1200
+ }
1201
+ const settings = raw.settings as Record<string, unknown> | undefined;
1202
+ const currentJev = settings?.jev;
1203
+ if (currentJev !== undefined && currentJev !== false && !isRecord(currentJev)) {
1204
+ throw new Error(`Failed to update Jev settings at ${filePath}: settings.jev must be an object or false`);
1205
+ }
1206
+ const jev = isRecord(effectiveJev) ? effectiveJev : isRecord(currentJev) ? currentJev : {};
1207
+ const nextServers = [...new Set(allowedServers)].sort((a, b) => a.localeCompare(b));
1208
+ const nextJev = { ...jev, semanticSearch: true, allowedServers: nextServers };
1209
+ validateJevSettings(nextJev);
1210
+ if (isRecord(currentJev) && JSON.stringify(currentJev) === JSON.stringify(nextJev)) return { path: filePath, changed: false };
1211
+ raw.settings = { ...settings, jev: nextJev };
1212
+ writeRawConfigObject(filePath, raw);
1213
+ return { path: filePath, changed: true };
1147
1214
  }
1148
1215
 
1149
1216
  function getServersObject(raw: Record<string, unknown>): Record<string, ServerEntry> {
@@ -1,6 +1,6 @@
1
1
  import { Check, Errors } from "typebox/value";
2
2
  import type { DirectToolSpec, McpConfig, ToolPrefix } from "./types.ts";
3
- import { createToolSelectorCandidateIndex, formatToolName, getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
3
+ import { createToolSelectorCandidateIndex, formatToolName, getToolNameCandidates, isServerDisabled, isToolAllowed, resolveToolPrefix, resolveUniqueNameOwnership } from "./types.ts";
4
4
  import type { MetadataCache } from "./metadata-cache.ts";
5
5
  import { isServerCacheValid, parseDirectToolSelectors } from "./metadata-cache.ts";
6
6
  export { getMissingConfiguredDirectToolServers } from "./metadata-cache.ts";
@@ -10,6 +10,13 @@ import { resourceNameToToolName } from "./resource-tools.ts";
10
10
  const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
11
11
  export const DIRECT_TOOLS_ADVISORY_THRESHOLD = 75;
12
12
 
13
+ export function getLargeDirectToolsAdvisory(config: McpConfig, specs: readonly DirectToolSpec[]): string | undefined {
14
+ if (config.settings?.warnOnLargeDirectTools === false) return undefined;
15
+ const eagerCount = specs.filter((spec) => !spec.lazy).length;
16
+ if (eagerCount < DIRECT_TOOLS_ADVISORY_THRESHOLD) return undefined;
17
+ return `MCP: ${eagerCount} 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. Set settings.warnOnLargeDirectTools to false to hide this advisory.`;
18
+ }
19
+
13
20
  /**
14
21
  * Recover one model-emitted JSON layer for schema-declared object and array
15
22
  * properties, then validate the complete input against the same schema.
@@ -28,14 +35,14 @@ export function prepareDirectToolArguments(inputSchema: unknown, args: unknown):
28
35
  for (const [name, propertySchema] of Object.entries(properties)) {
29
36
  if (!Object.hasOwn(input, name) || typeof input[name] !== "string"
30
37
  || !propertySchema || typeof propertySchema !== "object" || Array.isArray(propertySchema)) continue;
31
- const expectedType = (propertySchema as Record<string, unknown>).type;
32
- if (expectedType !== "object" && expectedType !== "array") continue;
38
+ // A valid string may be intentional (for example, string | object).
39
+ // Only recover JSON when the advertised property rejects the raw value.
40
+ if (Check(propertySchema as never, input[name])) continue;
33
41
  try {
34
42
  const parsed: unknown = JSON.parse(input[name] as string);
35
- const matches = expectedType === "array"
36
- ? Array.isArray(parsed)
37
- : parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
38
- if (matches) {
43
+ const isContainer = Array.isArray(parsed)
44
+ || (parsed !== null && typeof parsed === "object");
45
+ if (isContainer && Check(propertySchema as never, parsed)) {
39
46
  prepared ??= { ...input };
40
47
  prepared[name] = parsed;
41
48
  }
@@ -73,8 +80,6 @@ export function resolveDirectTools(
73
80
  const specs: DirectToolSpec[] = [];
74
81
  if (!cache) return specs;
75
82
 
76
- const seenNames = new Set<string>();
77
-
78
83
  const envSelection = envOverride ? parseDirectToolSelectors(envOverride) : null;
79
84
  const globalDirect = config.settings?.directTools;
80
85
 
@@ -139,11 +144,6 @@ export function resolveDirectTools(
139
144
  console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
140
145
  continue;
141
146
  }
142
- if (seenNames.has(prefixedName)) {
143
- console.warn(`MCP: skipping duplicate direct tool "${prefixedName}" from "${serverName}"`);
144
- continue;
145
- }
146
- seenNames.add(prefixedName);
147
147
  specs.push({
148
148
  ...(lazy ? { lazy: true } : {}),
149
149
  serverName,
@@ -166,11 +166,6 @@ export function resolveDirectTools(
166
166
  console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
167
167
  continue;
168
168
  }
169
- if (seenNames.has(prefixedName)) {
170
- console.warn(`MCP: skipping duplicate direct resource tool "${prefixedName}" from "${serverName}"`);
171
- continue;
172
- }
173
- seenNames.add(prefixedName);
174
169
  specs.push({
175
170
  ...(lazy ? { lazy: true } : {}),
176
171
  serverName,
@@ -183,17 +178,16 @@ export function resolveDirectTools(
183
178
  }
184
179
  }
185
180
 
186
- for (const spec of specs) reservedNames?.add(spec.prefixedName);
181
+ const ownership = resolveUniqueNameOwnership(specs, (spec) => spec.prefixedName);
182
+ for (const [name, colliding] of ownership.collisions) {
183
+ console.warn(`MCP: skipping colliding direct name "${name}" from ${colliding.map((spec) => `"${spec.serverName}"`).join(", ")}`);
184
+ }
185
+ const uniqueSpecs = ownership.unique;
186
+ for (const spec of uniqueSpecs) reservedNames?.add(spec.prefixedName);
187
187
 
188
188
  const emittedSpecs = unavailableServers.size === 0
189
- ? specs
190
- : specs.filter((spec) => !unavailableServers.has(spec.serverName));
191
-
192
- // Lazy specs cost nothing at turn start, so they do not count toward the advisory.
193
- const eagerCount = emittedSpecs.filter((spec) => !spec.lazy).length;
194
- if (config.settings?.warnOnLargeDirectTools !== false && eagerCount >= DIRECT_TOOLS_ADVISORY_THRESHOLD) {
195
- console.warn(`MCP: ${eagerCount} 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. Set settings.warnOnLargeDirectTools to false to hide this advisory.`);
196
- }
189
+ ? uniqueSpecs
190
+ : uniqueSpecs.filter((spec) => !unavailableServers.has(spec.serverName));
197
191
 
198
192
  return emittedSpecs;
199
193
  }