@oh-my-pi/pi-coding-agent 17.2.3 → 17.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/{CHANGELOG-c5hpqt9r.md → CHANGELOG-q2w43aw3.md} +92 -0
  3. package/dist/cli.js +3979 -3548
  4. package/dist/{template-8vdv6xb4.js → template-c2hyaytt.js} +8 -26
  5. package/dist/types/advisor/advise-tool.d.ts +6 -0
  6. package/dist/types/advisor/runtime.d.ts +4 -4
  7. package/dist/types/capability/mcp.d.ts +9 -0
  8. package/dist/types/cli/args.d.ts +4 -1
  9. package/dist/types/cli/browser-relay-cli.d.ts +15 -0
  10. package/dist/types/cli/command-help.d.ts +99 -0
  11. package/dist/types/cli/help-extra.d.ts +2 -0
  12. package/dist/types/cli/models-cli.d.ts +1 -1
  13. package/dist/types/cli/thinking-levels.d.ts +5 -0
  14. package/dist/types/cli/update-cli.d.ts +13 -0
  15. package/dist/types/commands/browser-relay.d.ts +38 -0
  16. package/dist/types/commands/commit.d.ts +3 -0
  17. package/dist/types/commands/complete.d.ts +1 -1
  18. package/dist/types/commands/completions.d.ts +6 -0
  19. package/dist/types/commands/grep.d.ts +3 -0
  20. package/dist/types/commands/join.d.ts +4 -0
  21. package/dist/types/commands/launch-help.d.ts +171 -0
  22. package/dist/types/commands/launch.d.ts +5 -1
  23. package/dist/types/commands/models.d.ts +3 -0
  24. package/dist/types/commands/say.d.ts +10 -0
  25. package/dist/types/commands/worktree.d.ts +3 -0
  26. package/dist/types/config/keybindings.d.ts +6 -1
  27. package/dist/types/config/model-discovery.d.ts +2 -2
  28. package/dist/types/config/models-config-schema-bundle.d.ts +3 -0
  29. package/dist/types/config/models-config-schema.d.ts +2 -0
  30. package/dist/types/config/models-config.d.ts +1 -0
  31. package/dist/types/config/service-tier.d.ts +2 -0
  32. package/dist/types/config/settings-schema.d.ts +70 -22
  33. package/dist/types/cursor-bridge-tools.d.ts +1 -1
  34. package/dist/types/cursor.d.ts +7 -4
  35. package/dist/types/discovery/helpers.d.ts +6 -0
  36. package/dist/types/discovery/omp-extension-roots.d.ts +21 -2
  37. package/dist/types/edit/index.d.ts +3 -3
  38. package/dist/types/edit/modes/replace.d.ts +22 -15
  39. package/dist/types/edit/renderer.d.ts +1 -1
  40. package/dist/types/eval/py/kernel.d.ts +3 -1
  41. package/dist/types/eval/py/spawn-options.d.ts +12 -16
  42. package/dist/types/extensibility/extensions/loader.d.ts +6 -2
  43. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +8 -1
  44. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +1 -1
  45. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -0
  46. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +2 -2
  47. package/dist/types/launch/broker.d.ts +1 -1
  48. package/dist/types/launch/client.d.ts +6 -3
  49. package/dist/types/launch/ensure.d.ts +8 -0
  50. package/dist/types/launch/paths.d.ts +3 -2
  51. package/dist/types/launch/protocol.d.ts +2 -2
  52. package/dist/types/lsp/client.d.ts +2 -0
  53. package/dist/types/lsp/mux/daemon.d.ts +15 -0
  54. package/dist/types/lsp/mux/protocol.d.ts +57 -0
  55. package/dist/types/lsp/mux/server.d.ts +18 -0
  56. package/dist/types/lsp/types.d.ts +22 -2
  57. package/dist/types/main.d.ts +2 -0
  58. package/dist/types/mcp/request-id.d.ts +19 -0
  59. package/dist/types/mcp/types.d.ts +14 -0
  60. package/dist/types/memory-backend/index.d.ts +1 -0
  61. package/dist/types/memory-backend/messages.d.ts +15 -0
  62. package/dist/types/mnemopi/embed-client.d.ts +1 -1
  63. package/dist/types/mnemopi/state.d.ts +0 -23
  64. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  65. package/dist/types/modes/components/custom-editor.d.ts +2 -1
  66. package/dist/types/modes/components/model-browser.d.ts +7 -5
  67. package/dist/types/modes/components/model-picker.d.ts +9 -3
  68. package/dist/types/modes/components/read-tool-group.d.ts +11 -0
  69. package/dist/types/modes/components/stripped-tool-calls-placeholder.d.ts +12 -0
  70. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  71. package/dist/types/modes/controllers/input-controller.d.ts +1 -0
  72. package/dist/types/modes/interactive-mode.d.ts +1 -0
  73. package/dist/types/modes/types.d.ts +2 -1
  74. package/dist/types/modes/workflow.d.ts +2 -1
  75. package/dist/types/registry/agent-lifecycle.d.ts +14 -5
  76. package/dist/types/registry/agent-registry.d.ts +4 -3
  77. package/dist/types/sdk.d.ts +4 -2
  78. package/dist/types/secrets/index.d.ts +7 -9
  79. package/dist/types/session/agent-session-types.d.ts +3 -1
  80. package/dist/types/session/agent-session.d.ts +6 -1
  81. package/dist/types/session/indexed-session-storage.d.ts +1 -0
  82. package/dist/types/session/model-controls.d.ts +0 -1
  83. package/dist/types/session/session-advisors.d.ts +8 -1
  84. package/dist/types/session/session-context.d.ts +2 -1
  85. package/dist/types/session/session-maintenance.d.ts +2 -0
  86. package/dist/types/session/session-manager.d.ts +16 -5
  87. package/dist/types/session/session-stats.d.ts +10 -0
  88. package/dist/types/session/session-storage.d.ts +15 -3
  89. package/dist/types/system-prompt.d.ts +6 -1
  90. package/dist/types/task/index.d.ts +2 -1
  91. package/dist/types/task/spawn-policy.d.ts +5 -0
  92. package/dist/types/thinking.d.ts +1 -7
  93. package/dist/types/tools/ast-grep.d.ts +1 -1
  94. package/dist/types/tools/browser/registry.d.ts +2 -1
  95. package/dist/types/tools/browser/relay/bridge.d.ts +35 -0
  96. package/dist/types/tools/browser/relay/daemon.d.ts +16 -0
  97. package/dist/types/tools/browser/relay/kind.d.ts +19 -0
  98. package/dist/types/tools/browser/relay/protocol.d.ts +97 -0
  99. package/dist/types/tools/browser/relay/server.d.ts +35 -0
  100. package/dist/types/tools/browser/render.d.ts +1 -0
  101. package/dist/types/tools/browser.d.ts +3 -0
  102. package/dist/types/tools/computer/exposure.d.ts +9 -7
  103. package/dist/types/tools/computer/protocol.d.ts +70 -17
  104. package/dist/types/tools/computer/supervisor.d.ts +23 -10
  105. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  106. package/dist/types/tools/computer/worker.d.ts +42 -7
  107. package/dist/types/tools/computer-renderer.d.ts +7 -5
  108. package/dist/types/tools/computer.d.ts +25 -44
  109. package/dist/types/tools/context.d.ts +3 -0
  110. package/dist/types/tools/default-renderer.d.ts +3 -0
  111. package/dist/types/tools/glob.d.ts +1 -1
  112. package/dist/types/tools/grep.d.ts +1 -1
  113. package/dist/types/tools/render-utils.d.ts +2 -0
  114. package/dist/types/tools/{browser/run-cancellation.d.ts → run-scope.d.ts} +8 -8
  115. package/dist/types/tools/tool-timeouts.d.ts +5 -0
  116. package/dist/types/tools/xdev.d.ts +8 -0
  117. package/dist/types/utils/clipboard.d.ts +2 -2
  118. package/dist/types/web/kagi.d.ts +1 -0
  119. package/dist/types/web/search/index.d.ts +1 -1
  120. package/dist/types/web/search/providers/anthropic.d.ts +1 -0
  121. package/dist/types/web/search/providers/base.d.ts +2 -0
  122. package/dist/types/web/search/providers/brave.d.ts +1 -0
  123. package/dist/types/web/search/providers/browser-page.d.ts +1 -0
  124. package/dist/types/web/search/providers/codex.d.ts +1 -0
  125. package/dist/types/web/search/providers/exa.d.ts +1 -0
  126. package/dist/types/web/search/providers/firecrawl.d.ts +1 -0
  127. package/dist/types/web/search/providers/gemini.d.ts +1 -0
  128. package/dist/types/web/search/providers/jina.d.ts +1 -0
  129. package/dist/types/web/search/providers/kagi.d.ts +1 -0
  130. package/dist/types/web/search/providers/kimi.d.ts +1 -0
  131. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  132. package/dist/types/web/search/providers/perplexity.d.ts +1 -0
  133. package/dist/types/web/search/providers/searxng.d.ts +1 -0
  134. package/dist/types/web/search/providers/tavily.d.ts +1 -0
  135. package/dist/types/web/search/providers/tinyfish.d.ts +1 -0
  136. package/dist/types/web/search/providers/utils.d.ts +6 -9
  137. package/dist/types/web/search/providers/zai.d.ts +1 -0
  138. package/dist/types/web/search/types.d.ts +4 -0
  139. package/package.json +12 -12
  140. package/scripts/legacy-pi-virtual-module.ts +12 -2
  141. package/src/advisor/advise-tool.ts +18 -0
  142. package/src/advisor/runtime.ts +6 -6
  143. package/src/capability/mcp.ts +7 -0
  144. package/src/cli/args.ts +7 -87
  145. package/src/cli/browser-relay-cli.ts +119 -0
  146. package/src/cli/command-help.ts +103 -0
  147. package/src/cli/completion-gen.ts +5 -5
  148. package/src/cli/flag-tables.ts +9 -0
  149. package/src/cli/help-extra.ts +89 -0
  150. package/src/cli/models-cli.ts +13 -16
  151. package/src/cli/setup-cli.ts +13 -21
  152. package/src/cli/thinking-levels.ts +7 -0
  153. package/src/cli/update-cli.ts +26 -7
  154. package/src/cli-commands.ts +170 -33
  155. package/src/cli.ts +17 -5
  156. package/src/collab/host.ts +1 -1
  157. package/src/commands/acp.ts +3 -1
  158. package/src/commands/agents.ts +3 -2
  159. package/src/commands/auth-broker.ts +3 -2
  160. package/src/commands/auth-gateway.ts +3 -2
  161. package/src/commands/bench.ts +2 -3
  162. package/src/commands/browser-relay.ts +53 -0
  163. package/src/commands/cleanse.ts +2 -2
  164. package/src/commands/commit.ts +3 -2
  165. package/src/commands/complete.ts +2 -1
  166. package/src/commands/completions.ts +3 -2
  167. package/src/commands/config.ts +3 -2
  168. package/src/commands/dry-balance.ts +2 -2
  169. package/src/commands/gallery.ts +3 -2
  170. package/src/commands/gc.ts +3 -2
  171. package/src/commands/grep.ts +3 -2
  172. package/src/commands/grievances.ts +3 -2
  173. package/src/commands/install.ts +2 -2
  174. package/src/commands/join.ts +3 -2
  175. package/src/commands/launch-help.ts +116 -0
  176. package/src/commands/launch.ts +7 -191
  177. package/src/commands/models.ts +3 -2
  178. package/src/commands/plugin.ts +3 -2
  179. package/src/commands/read.ts +3 -2
  180. package/src/commands/say.ts +3 -2
  181. package/src/commands/setup.ts +3 -2
  182. package/src/commands/shell.ts +3 -2
  183. package/src/commands/ssh.ts +3 -2
  184. package/src/commands/stats.ts +3 -2
  185. package/src/commands/tiny-models.ts +2 -2
  186. package/src/commands/token.ts +2 -2
  187. package/src/commands/ttsr.ts +2 -2
  188. package/src/commands/update.ts +3 -2
  189. package/src/commands/usage.ts +3 -2
  190. package/src/commands/web-search.ts +3 -2
  191. package/src/commands/worktree.ts +3 -2
  192. package/src/config/keybindings.ts +19 -9
  193. package/src/config/mcp-schema.json +5 -0
  194. package/src/config/model-discovery.ts +30 -13
  195. package/src/config/model-registry.ts +16 -6
  196. package/src/config/models-config-schema-bundle.ts +9 -0
  197. package/src/config/resolve-config-value.ts +2 -1
  198. package/src/config/service-tier.ts +5 -0
  199. package/src/config/settings-schema.ts +73 -21
  200. package/src/cursor-bridge-tools.ts +1 -1
  201. package/src/cursor.ts +10 -14
  202. package/src/discovery/builtin.ts +10 -0
  203. package/src/discovery/helpers.ts +10 -0
  204. package/src/discovery/mcp-json.ts +11 -1
  205. package/src/discovery/omp-extension-roots.ts +79 -20
  206. package/src/discovery/omp-plugins.ts +10 -1
  207. package/src/edit/diff.ts +17 -8
  208. package/src/edit/index.ts +20 -7
  209. package/src/edit/modes/replace.ts +93 -40
  210. package/src/edit/renderer.ts +1 -1
  211. package/src/edit/streaming.ts +14 -17
  212. package/src/eval/py/kernel.ts +2 -1
  213. package/src/eval/py/spawn-options.ts +24 -29
  214. package/src/export/html/template.js +8 -26
  215. package/src/extensibility/extensions/loader.ts +66 -28
  216. package/src/extensibility/extensions/wrapper.ts +1 -0
  217. package/src/extensibility/legacy-pi-ai-shim.ts +22 -1
  218. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -6
  219. package/src/extensibility/plugins/legacy-pi-compat.ts +124 -57
  220. package/src/extensibility/plugins/marketplace/registry.ts +4 -8
  221. package/src/extensibility/skills.ts +11 -0
  222. package/src/extensibility/utils.ts +10 -2
  223. package/src/launch/broker.ts +1 -1
  224. package/src/launch/client.ts +32 -13
  225. package/src/launch/ensure.ts +73 -0
  226. package/src/launch/paths.ts +3 -6
  227. package/src/launch/protocol.ts +2 -2
  228. package/src/lsp/client.ts +23 -2
  229. package/src/lsp/index.ts +8 -0
  230. package/src/lsp/mux/daemon.ts +348 -0
  231. package/src/lsp/mux/protocol.ts +96 -0
  232. package/src/lsp/mux/server.ts +797 -0
  233. package/src/lsp/types.ts +28 -2
  234. package/src/main.ts +31 -24
  235. package/src/mcp/config.ts +1 -0
  236. package/src/mcp/oauth-flow.ts +11 -1
  237. package/src/mcp/request-id.ts +24 -0
  238. package/src/mcp/transports/http.ts +4 -2
  239. package/src/mcp/transports/sse.ts +4 -2
  240. package/src/mcp/transports/stdio.ts +4 -2
  241. package/src/mcp/types.ts +15 -0
  242. package/src/memory-backend/index.ts +1 -0
  243. package/src/memory-backend/messages.ts +19 -0
  244. package/src/mnemopi/embed-client.ts +49 -2
  245. package/src/mnemopi/state.ts +57 -23
  246. package/src/modes/components/agent-hub.ts +1 -1
  247. package/src/modes/components/assistant-message.ts +11 -0
  248. package/src/modes/components/chat-transcript-builder.ts +4 -0
  249. package/src/modes/components/custom-editor.ts +10 -1
  250. package/src/modes/components/model-browser.ts +25 -17
  251. package/src/modes/components/model-picker.ts +10 -6
  252. package/src/modes/components/read-tool-group.ts +36 -0
  253. package/src/modes/components/status-line/component.ts +1 -1
  254. package/src/modes/components/stripped-tool-calls-placeholder.ts +35 -0
  255. package/src/modes/components/tool-execution.ts +85 -20
  256. package/src/modes/components/tree-selector.ts +46 -48
  257. package/src/modes/controllers/command-controller.ts +2 -2
  258. package/src/modes/controllers/event-controller.ts +199 -10
  259. package/src/modes/controllers/input-controller.ts +39 -1
  260. package/src/modes/controllers/mcp-command-controller.ts +11 -5
  261. package/src/modes/controllers/selector-controller.ts +47 -6
  262. package/src/modes/interactive-mode.ts +2 -0
  263. package/src/modes/types.ts +6 -1
  264. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  265. package/src/modes/utils/interactive-context-helpers.ts +1 -0
  266. package/src/modes/utils/ui-helpers.ts +6 -10
  267. package/src/modes/workflow.ts +8 -2
  268. package/src/prompts/agents/init.md +1 -1
  269. package/src/prompts/system/plan-mode-active.md +2 -2
  270. package/src/prompts/system/system-prompt.md +26 -52
  271. package/src/prompts/system/workflow-notice.md +3 -3
  272. package/src/prompts/tools/ast-grep.md +1 -1
  273. package/src/prompts/tools/bash.md +1 -1
  274. package/src/prompts/tools/browser.md +1 -0
  275. package/src/prompts/tools/computer.md +21 -21
  276. package/src/prompts/tools/glob.md +1 -1
  277. package/src/prompts/tools/grep.md +1 -1
  278. package/src/prompts/tools/replace.md +5 -4
  279. package/src/prompts/tools/task.md +4 -4
  280. package/src/registry/agent-lifecycle.ts +36 -11
  281. package/src/registry/agent-registry.ts +12 -5
  282. package/src/sdk.ts +102 -35
  283. package/src/secrets/index.ts +16 -18
  284. package/src/session/agent-session-types.ts +3 -1
  285. package/src/session/agent-session.ts +23 -2
  286. package/src/session/agent-storage.ts +6 -4
  287. package/src/session/history-storage.ts +4 -2
  288. package/src/session/indexed-session-storage.ts +9 -0
  289. package/src/session/messages.ts +31 -7
  290. package/src/session/model-controls.ts +0 -1
  291. package/src/session/prewalk.ts +24 -2
  292. package/src/session/session-advisors.ts +21 -3
  293. package/src/session/session-context.ts +16 -12
  294. package/src/session/session-dump-format.ts +1 -1
  295. package/src/session/session-maintenance.ts +41 -4
  296. package/src/session/session-manager.ts +181 -94
  297. package/src/session/session-paths.ts +118 -55
  298. package/src/session/session-stats.ts +48 -3
  299. package/src/session/session-storage.ts +30 -22
  300. package/src/session/session-tools.ts +25 -23
  301. package/src/session/stream-guards.ts +1 -1
  302. package/src/slash-commands/builtin-registry.ts +29 -26
  303. package/src/system-prompt.ts +14 -4
  304. package/src/task/executor.ts +8 -1
  305. package/src/task/index.ts +23 -8
  306. package/src/task/spawn-policy.ts +14 -0
  307. package/src/thinking.ts +2 -8
  308. package/src/tiny/text.ts +12 -0
  309. package/src/tools/ast-grep.ts +10 -4
  310. package/src/tools/browser/cmux/cmux-tab.ts +5 -10
  311. package/src/tools/browser/cmux/rpc.ts +2 -8
  312. package/src/tools/browser/registry.ts +53 -2
  313. package/src/tools/browser/relay/bridge.ts +945 -0
  314. package/src/tools/browser/relay/daemon.ts +117 -0
  315. package/src/tools/browser/relay/extension-assets/background.js.txt +242 -0
  316. package/src/tools/browser/relay/extension-assets/manifest.json.txt +14 -0
  317. package/src/tools/browser/relay/extension-assets/options.html.txt +53 -0
  318. package/src/tools/browser/relay/extension-assets/options.js.txt +23 -0
  319. package/src/tools/browser/relay/kind.ts +41 -0
  320. package/src/tools/browser/relay/protocol.ts +54 -0
  321. package/src/tools/browser/relay/server.ts +141 -0
  322. package/src/tools/browser/render.ts +4 -1
  323. package/src/tools/browser/shared-daemon.ts +7 -58
  324. package/src/tools/browser/tab-supervisor.ts +5 -4
  325. package/src/tools/browser/tab-worker.ts +33 -12
  326. package/src/tools/browser.ts +26 -1
  327. package/src/tools/computer/exposure.ts +11 -35
  328. package/src/tools/computer/protocol.ts +51 -10
  329. package/src/tools/computer/supervisor.ts +227 -124
  330. package/src/tools/computer/worker-entry.ts +11 -6
  331. package/src/tools/computer/worker.ts +695 -81
  332. package/src/tools/computer-renderer.ts +104 -65
  333. package/src/tools/computer.ts +134 -428
  334. package/src/tools/context.ts +3 -0
  335. package/src/tools/default-renderer.ts +19 -4
  336. package/src/tools/glob.ts +9 -2
  337. package/src/tools/grep.ts +12 -6
  338. package/src/tools/index.ts +10 -6
  339. package/src/tools/inspect-image.ts +32 -6
  340. package/src/tools/render-utils.ts +2 -0
  341. package/src/tools/{browser/run-cancellation.ts → run-scope.ts} +11 -11
  342. package/src/tools/tool-timeouts.ts +1 -0
  343. package/src/tools/xdev.ts +29 -2
  344. package/src/utils/clipboard.ts +40 -14
  345. package/src/utils/command-args.ts +26 -28
  346. package/src/utils/tool-choice.ts +0 -13
  347. package/src/web/kagi.ts +2 -1
  348. package/src/web/search/index.ts +18 -2
  349. package/src/web/search/providers/anthropic.ts +7 -2
  350. package/src/web/search/providers/base.ts +2 -0
  351. package/src/web/search/providers/brave.ts +3 -1
  352. package/src/web/search/providers/browser-page.ts +6 -4
  353. package/src/web/search/providers/codex.ts +4 -1
  354. package/src/web/search/providers/duckduckgo.ts +2 -1
  355. package/src/web/search/providers/ecosia.ts +2 -1
  356. package/src/web/search/providers/exa.ts +4 -2
  357. package/src/web/search/providers/firecrawl.ts +3 -1
  358. package/src/web/search/providers/gemini.ts +8 -2
  359. package/src/web/search/providers/google.ts +2 -1
  360. package/src/web/search/providers/jina.ts +12 -2
  361. package/src/web/search/providers/kagi.ts +3 -0
  362. package/src/web/search/providers/kimi.ts +5 -1
  363. package/src/web/search/providers/mojeek.ts +2 -1
  364. package/src/web/search/providers/parallel.ts +5 -1
  365. package/src/web/search/providers/perplexity.ts +6 -3
  366. package/src/web/search/providers/public.ts +1 -1
  367. package/src/web/search/providers/searxng.ts +11 -5
  368. package/src/web/search/providers/startpage.ts +10 -4
  369. package/src/web/search/providers/synthetic.ts +11 -5
  370. package/src/web/search/providers/tavily.ts +3 -1
  371. package/src/web/search/providers/tinyfish.ts +3 -1
  372. package/src/web/search/providers/utils.ts +12 -10
  373. package/src/web/search/providers/xai.ts +2 -2
  374. package/src/web/search/providers/zai.ts +18 -3
  375. package/src/web/search/types.ts +6 -0
@@ -31,6 +31,7 @@ import {
31
31
  expandEnvVarsDeep,
32
32
  getExtensionNameFromPath,
33
33
  loadFilesFromDir,
34
+ parseRequestIdFormat,
34
35
  SOURCE_PATHS,
35
36
  scanSkillsFromDir,
36
37
  } from "./helpers";
@@ -154,10 +155,19 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
154
155
  timeout = undefined;
155
156
  }
156
157
 
158
+ // Validate requestIdFormat: only the two documented encodings
159
+ const requestIdFormat = parseRequestIdFormat(serverConfig.requestIdFormat);
160
+ if (requestIdFormat === undefined && serverConfig.requestIdFormat != null) {
161
+ logger.warn(
162
+ `MCP server "${serverName}": invalid requestIdFormat ${JSON.stringify(serverConfig.requestIdFormat)}, ignoring`,
163
+ );
164
+ }
165
+
157
166
  result.push({
158
167
  name: serverName,
159
168
  enabled,
160
169
  timeout,
170
+ requestIdFormat,
161
171
  command: serverConfig.command as string | undefined,
162
172
  args: serverConfig.args as string[] | undefined,
163
173
  env: serverConfig.env as Record<string, string> | undefined,
@@ -16,6 +16,7 @@ import { invalidate as invalidateFsCache, readDirEntries, readFile } from "../ca
16
16
  import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
17
17
  import type { Skill, SkillFrontmatter } from "../capability/skill";
18
18
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
19
+ import type { MCPRequestIdFormat } from "../mcp/types";
19
20
  import { type ConfiguredThinkingLevel, parseConfiguredThinkingLevel } from "../thinking";
20
21
  import { normalizeToolNames } from "../tools/builtin-names";
21
22
 
@@ -140,6 +141,15 @@ export function parseBoolean(value: unknown): boolean | undefined {
140
141
  return undefined;
141
142
  }
142
143
 
144
+ /**
145
+ * Parse an MCP `requestIdFormat` value. Unrecognized values are dropped so a typo
146
+ * degrades to the default integer ids rather than reaching a transport.
147
+ */
148
+ export function parseRequestIdFormat(value: unknown): MCPRequestIdFormat | undefined {
149
+ if (value === "string" || value === "number") return value;
150
+ return undefined;
151
+ }
152
+
143
153
  /**
144
154
  * Parse a comma-separated string into an array of trimmed, non-empty strings.
145
155
  */
@@ -12,7 +12,7 @@ import { registerProvider } from "../capability";
12
12
  import { readFile } from "../capability/fs";
13
13
  import { type MCPServer, mcpCapability } from "../capability/mcp";
14
14
  import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
15
- import { createSourceMeta, expandEnvVarsDeep } from "./helpers";
15
+ import { createSourceMeta, expandEnvVarsDeep, parseRequestIdFormat } from "./helpers";
16
16
 
17
17
  const PROVIDER_ID = "mcp-json";
18
18
  const DISPLAY_NAME = "MCP Config";
@@ -26,6 +26,7 @@ interface MCPConfigFile {
26
26
  {
27
27
  enabled?: boolean;
28
28
  timeout?: number;
29
+ requestIdFormat?: "string" | "number";
29
30
  command?: string;
30
31
  args?: string[];
31
32
  env?: Record<string, string>;
@@ -83,10 +84,19 @@ function transformMCPConfig(config: MCPConfigFile, source: SourceMeta): MCPServe
83
84
  }
84
85
  }
85
86
 
87
+ const requestIdFormat = parseRequestIdFormat(serverConfig.requestIdFormat);
88
+ if (requestIdFormat === undefined && serverConfig.requestIdFormat !== undefined) {
89
+ logger.warn("MCP server has invalid 'requestIdFormat' value, ignoring", {
90
+ name,
91
+ value: serverConfig.requestIdFormat,
92
+ });
93
+ }
94
+
86
95
  const server: MCPServer = {
87
96
  name,
88
97
  enabled,
89
98
  timeout,
99
+ requestIdFormat,
90
100
  command: serverConfig.command,
91
101
  args: serverConfig.args,
92
102
  env: serverConfig.env,
@@ -15,6 +15,7 @@
15
15
  * @see ./omp-plugins.ts
16
16
  * @see ./builtin.ts `loadExtensionModules`
17
17
  */
18
+ import { AsyncLocalStorage } from "node:async_hooks";
18
19
  import * as fs from "node:fs/promises";
19
20
  import * as path from "node:path";
20
21
  import { getAgentDir, isEnoent, logger, tryParseJson } from "@oh-my-pi/pi-utils";
@@ -36,10 +37,48 @@ export interface OmpExtensionRoot {
36
37
 
37
38
  interface InjectedRoot {
38
39
  path: string;
40
+ /** Relative CLI spelling, rebound against the active project on discovery. */
41
+ relativePath?: string;
39
42
  level: "user" | "project";
40
43
  }
41
44
 
45
+ export type OmpExtensionRootMode = "merge" | "explicit-only";
46
+
47
+ interface InvocationRootScope {
48
+ /** Raw SDK spellings, resolved against the LoadContext that performs discovery. */
49
+ paths: readonly string[];
50
+ mode: OmpExtensionRootMode;
51
+ }
52
+
53
+ const invocationRootScope = new AsyncLocalStorage<InvocationRootScope>();
54
+
42
55
  let injectedCliRoots: InjectedRoot[] = [];
56
+ let injectedCliRootMode: OmpExtensionRootMode = "merge";
57
+
58
+ export interface InjectOmpExtensionCliRootOptions {
59
+ /**
60
+ * `explicit-only` exposes only roots named by this CLI invocation. Use it
61
+ * with `--no-extensions` so configured and installed packages cannot
62
+ * contribute sibling capabilities through the `omp-plugins` provider.
63
+ */
64
+ mode?: OmpExtensionRootMode;
65
+ /** Replace roots from an earlier invocation instead of extending them. */
66
+ replace?: boolean;
67
+ }
68
+
69
+ /**
70
+ * Run one SDK invocation with its own extension-package roots. Async resources
71
+ * started inside `callback` retain this scope, including discovery deliberately
72
+ * deferred until the end of session startup. Raw relative paths are resolved by
73
+ * {@link listOmpExtensionRoots} against that invocation's active cwd.
74
+ */
75
+ export function withOmpExtensionRootScope<T>(
76
+ paths: readonly string[],
77
+ mode: OmpExtensionRootMode,
78
+ callback: () => T,
79
+ ): T {
80
+ return invocationRootScope.run({ paths: [...paths], mode }, callback);
81
+ }
43
82
 
44
83
  /**
45
84
  * Register CLI-provided extension package paths (e.g. from `--extension`/`-e`)
@@ -50,17 +89,27 @@ let injectedCliRoots: InjectedRoot[] = [];
50
89
  * Call once during startup before any capability load. Repeated calls extend
51
90
  * the registered set; {@link clearOmpExtensionCliRoots} resets for tests.
52
91
  */
53
- export function injectOmpExtensionCliRoots(paths: readonly string[], home: string, cwd: string): void {
92
+ export function injectOmpExtensionCliRoots(
93
+ paths: readonly string[],
94
+ home: string,
95
+ cwd: string,
96
+ options: InjectOmpExtensionCliRootOptions = {},
97
+ ): void {
98
+ if (options.mode) injectedCliRootMode = options.mode;
99
+ if (options.replace) injectedCliRoots = [];
54
100
  if (paths.length === 0) return;
55
101
  const expanded = paths.map(raw => {
56
102
  const tilde = expandTilde(raw, home);
57
- return path.isAbsolute(tilde) ? tilde : path.resolve(cwd, tilde);
103
+ return {
104
+ path: path.isAbsolute(tilde) ? tilde : path.resolve(cwd, tilde),
105
+ relativePath: path.isAbsolute(tilde) ? undefined : tilde,
106
+ };
58
107
  });
59
108
  const merged = new Map<string, InjectedRoot>();
60
109
  for (const root of injectedCliRoots) merged.set(root.path, root);
61
- for (const resolved of expanded) {
110
+ for (const { path: resolved, relativePath } of expanded) {
62
111
  // CLI scope mirrors how `--extension` is treated elsewhere — user-level overrides win.
63
- if (!merged.has(resolved)) merged.set(resolved, { path: resolved, level: "user" });
112
+ if (!merged.has(resolved)) merged.set(resolved, { path: resolved, relativePath, level: "user" });
64
113
  }
65
114
  injectedCliRoots = [...merged.values()];
66
115
  }
@@ -68,6 +117,7 @@ export function injectOmpExtensionCliRoots(paths: readonly string[], home: strin
68
117
  /** Drop every CLI-injected root. Tests use this between cases. */
69
118
  export function clearOmpExtensionCliRoots(): void {
70
119
  injectedCliRoots = [];
120
+ injectedCliRootMode = "merge";
71
121
  }
72
122
 
73
123
  /** Inspect currently-injected CLI roots (read-only). Exposed for diagnostics + tests. */
@@ -121,7 +171,8 @@ async function isDirectory(p: string): Promise<boolean> {
121
171
  * Sources, in order of precedence (later entries with the same absolute path
122
172
  * are dropped):
123
173
  *
124
- * 1. CLI roots injected via {@link injectOmpExtensionCliRoots}
174
+ * 1. Invocation-scoped SDK roots, when present; otherwise CLI roots injected
175
+ * via {@link injectOmpExtensionCliRoots}
125
176
  * 2. Project `<cwd>/.omp/settings.json#extensions`
126
177
  * 3. User `~/.omp/agent/settings.json#extensions`
127
178
  * 4. Enabled npm/link plugins installed under `<plugins>/node_modules/` (for
@@ -134,21 +185,29 @@ async function isDirectory(p: string): Promise<boolean> {
134
185
  * other sources still surface.
135
186
  */
136
187
  export async function listOmpExtensionRoots(ctx: LoadContext): Promise<OmpExtensionRoot[]> {
137
- const { project, user } = scopeDirs(ctx);
138
- const [projectExtensions, userExtensions, installedPlugins] = await Promise.all([
139
- readSettingsExtensions(path.join(project, "settings.json")),
140
- readSettingsExtensions(path.join(user, "settings.json")),
141
- listInstalledPluginRoots(ctx),
142
- ]);
143
-
144
- const candidates: InjectedRoot[] = [
145
- ...injectedCliRoots,
146
- ...projectExtensions.map((raw): InjectedRoot => ({ path: resolveAgainst(raw, ctx), level: "project" })),
147
- ...userExtensions.map((raw): InjectedRoot => ({ path: resolveAgainst(raw, ctx), level: "user" })),
148
- ...installedPlugins,
149
- ];
150
-
151
- // First-seen-wins dedup preserves CLI > project-settings > user-settings > installed precedence.
188
+ const scopedRoots = invocationRootScope.getStore();
189
+ const rootMode = scopedRoots?.mode ?? injectedCliRootMode;
190
+ let candidates: InjectedRoot[] = scopedRoots
191
+ ? scopedRoots.paths.map(raw => ({ path: resolveAgainst(raw, ctx), level: "user" }))
192
+ : injectedCliRoots.map(root =>
193
+ root.relativePath ? { ...root, path: path.resolve(ctx.cwd, root.relativePath) } : root,
194
+ );
195
+ if (rootMode === "merge") {
196
+ const { project, user } = scopeDirs(ctx);
197
+ const [projectExtensions, userExtensions, installedPlugins] = await Promise.all([
198
+ readSettingsExtensions(path.join(project, "settings.json")),
199
+ readSettingsExtensions(path.join(user, "settings.json")),
200
+ listInstalledPluginRoots(ctx),
201
+ ]);
202
+ candidates = [
203
+ ...candidates,
204
+ ...projectExtensions.map((raw): InjectedRoot => ({ path: resolveAgainst(raw, ctx), level: "project" })),
205
+ ...userExtensions.map((raw): InjectedRoot => ({ path: resolveAgainst(raw, ctx), level: "user" })),
206
+ ...installedPlugins,
207
+ ];
208
+ }
209
+
210
+ // First-seen-wins dedup preserves invocation/CLI > project-settings > user-settings > installed precedence.
152
211
  const seen = new Set<string>();
153
212
  const unique: InjectedRoot[] = [];
154
213
  for (const candidate of candidates) {
@@ -28,7 +28,13 @@ import { type Skill, skillCapability } from "../capability/skill";
28
28
  import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
29
29
  import { type CustomTool, toolCapability } from "../capability/tool";
30
30
  import type { LoadContext, LoadResult } from "../capability/types";
31
- import { buildRuleFromMarkdown, createSourceMeta, loadFilesFromDir, scanSkillsFromDir } from "./helpers";
31
+ import {
32
+ buildRuleFromMarkdown,
33
+ createSourceMeta,
34
+ loadFilesFromDir,
35
+ parseRequestIdFormat,
36
+ scanSkillsFromDir,
37
+ } from "./helpers";
32
38
  import { listOmpExtensionRoots, type OmpExtensionRoot } from "./omp-extension-roots";
33
39
  import { resolvePluginStdioPaths } from "./substitute-plugin-root";
34
40
 
@@ -257,6 +263,7 @@ const MCP_FILENAMES = [".mcp.json", "mcp.json"] as const;
257
263
  interface RawMcpServer {
258
264
  enabled?: boolean;
259
265
  timeout?: number;
266
+ requestIdFormat?: unknown;
260
267
  command?: string;
261
268
  args?: string[];
262
269
  env?: Record<string, string>;
@@ -305,10 +312,12 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
305
312
  // Root relative command/cwd at the plugin's config directory, not the
306
313
  // session cwd (MCP stdio spawning resolves relative values there).
307
314
  const rooted = resolvePluginStdioPaths({ command: cfg.command, cwd: cfg.cwd }, root.path);
315
+ const requestIdFormat = parseRequestIdFormat(cfg.requestIdFormat);
308
316
  items.push({
309
317
  name: serverName,
310
318
  ...(cfg.enabled !== undefined && { enabled: cfg.enabled }),
311
319
  ...(cfg.timeout !== undefined && { timeout: cfg.timeout }),
320
+ ...(requestIdFormat !== undefined && { requestIdFormat }),
312
321
  ...(rooted.command !== undefined && { command: rooted.command }),
313
322
  ...(cfg.args !== undefined && { args: cfg.args }),
314
323
  ...(cfg.env !== undefined && { env: cfg.env }),
package/src/edit/diff.ts CHANGED
@@ -855,7 +855,6 @@ export function replaceText(content: string, oldText: string, newText: string, o
855
855
  let normalizedContent = normalizeToLF(content);
856
856
  const normalizedOldText = normalizeToLF(oldText);
857
857
  const normalizedNewText = normalizeToLF(newText);
858
- let count = 0;
859
858
 
860
859
  if (options.all) {
861
860
  // Check for exact matches first
@@ -867,11 +866,13 @@ export function replaceText(content: string, oldText: string, newText: string, o
867
866
  };
868
867
  }
869
868
 
870
- // No exact matches - try fuzzy matching iteratively
869
+ // Match against the immutable source so inserted replacement text cannot become a later candidate.
870
+ const replacements: Array<{ startIndex: number; endIndex: number; text: string }> = [];
871
871
  while (true) {
872
872
  const matchOutcome = findMatch(normalizedContent, normalizedOldText, {
873
873
  allowFuzzy: options.fuzzy,
874
874
  threshold,
875
+ excludedRanges: replacements,
875
876
  });
876
877
 
877
878
  const shouldUseClosest =
@@ -888,14 +889,22 @@ export function replaceText(content: string, oldText: string, newText: string, o
888
889
  if (adjustedNewText === match.actualText) {
889
890
  break;
890
891
  }
891
- normalizedContent =
892
- normalizedContent.substring(0, match.startIndex) +
893
- adjustedNewText +
894
- normalizedContent.substring(match.startIndex + match.actualText.length);
895
- count++;
892
+ replacements.push({
893
+ startIndex: match.startIndex,
894
+ endIndex: match.startIndex + Math.max(match.actualText.length, 1),
895
+ text: adjustedNewText,
896
+ });
896
897
  }
897
898
 
898
- return { content: normalizedContent, count };
899
+ replacements.sort((a, b) => a.startIndex - b.startIndex);
900
+ const parts: string[] = [];
901
+ let sourceIndex = 0;
902
+ for (const replacement of replacements) {
903
+ parts.push(normalizedContent.substring(sourceIndex, replacement.startIndex), replacement.text);
904
+ sourceIndex = replacement.endIndex;
905
+ }
906
+ parts.push(normalizedContent.substring(sourceIndex));
907
+ return { content: parts.join(""), count: replacements.length };
899
908
  }
900
909
 
901
910
  // Single replacement mode
package/src/edit/index.ts CHANGED
@@ -19,7 +19,7 @@ import { executeHashlineSingle, hashlineEditParamsSchema } from "./hashline";
19
19
  import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
20
20
  import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
21
21
  import { executePatchSingle, type PatchEditEntry, type PatchParams, patchEditSchema } from "./modes/patch";
22
- import { executeReplaceSingle, type ReplaceEditEntry, type ReplaceParams, replaceEditSchema } from "./modes/replace";
22
+ import { executeReplace, type ReplaceBatchParams, type ReplaceParams, replaceEditSchema } from "./modes/replace";
23
23
  import { type EditToolDetails, type EditToolPerFileResult, getLspBatchRequest, type LspBatchRequest } from "./renderer";
24
24
  import { pruneOversizedEditSnapshots } from "./snapshot-details";
25
25
  import { EDIT_MODE_STRATEGIES } from "./streaming";
@@ -46,7 +46,7 @@ type TInput =
46
46
 
47
47
  type HashlineParams = typeof hashlineEditParamsSchema.infer;
48
48
 
49
- type EditParams = ReplaceParams | PatchParams | HashlineParams | ApplyPatchParams;
49
+ type EditParams = ReplaceParams | ReplaceBatchParams | PatchParams | HashlineParams | ApplyPatchParams;
50
50
 
51
51
  type EditModeDefinition = {
52
52
  description: (session: ToolSession) => string;
@@ -402,7 +402,7 @@ export class EditTool implements AgentTool<TInput> {
402
402
  /**
403
403
  * `mode` pins the edit variant for this instance, for callers whose protocol
404
404
  * fixes the shape of an edit. The Cursor `pi_edit` frame carries
405
- * `old_text`/`new_text` pairs, which only `replace` accepts — under the
405
+ * `old_string`/`new_string` args, which only `replace` accepts — under the
406
406
  * default `hashline` mode those args do not match the schema at all. Left
407
407
  * unset, the env/settings resolution applies as before.
408
408
  */
@@ -662,11 +662,24 @@ export class EditTool implements AgentTool<TInput> {
662
662
  batchRequest: LspBatchRequest | undefined,
663
663
  onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
664
664
  ) => {
665
- const { edits, path } = params as ReplaceParams;
666
- const targetPath = await resolveEditPath(tool.session, path, { mustExist: true, signal });
667
- const runs = (edits as ReplaceEditEntry[]).map(
665
+ // `edits` is the internal `ReplaceBatchParams` form only the Cursor
666
+ // exec bridge produces (multi-replacement `pi_edit` frames run as one
667
+ // lifecycle); model calls always arrive in the single-edit schema shape.
668
+ const replaceParams = params as ReplaceParams | ReplaceBatchParams;
669
+ const entries =
670
+ "edits" in replaceParams
671
+ ? replaceParams.edits
672
+ : [
673
+ {
674
+ old_string: replaceParams.old_string,
675
+ new_string: replaceParams.new_string,
676
+ replace_all: replaceParams.replace_all,
677
+ },
678
+ ];
679
+ const targetPath = await resolveEditPath(tool.session, replaceParams.path, { mustExist: true, signal });
680
+ const runs = entries.map(
668
681
  entry => (br: LspBatchRequest | undefined) =>
669
- executeReplaceSingle({
682
+ executeReplace({
670
683
  session: tool.session,
671
684
  path: targetPath,
672
685
  params: entry,
@@ -258,23 +258,41 @@ function formatPreviewWindow(lines: string[], centerIndex: number, options: Prev
258
258
  .join("\n");
259
259
  }
260
260
 
261
- function findExactMatchOutcome(content: string, target: string): MatchOutcome | undefined {
262
- const exactIndex = content.indexOf(target);
263
- if (exactIndex === -1) {
261
+ function findExactMatchOutcome(
262
+ content: string,
263
+ target: string,
264
+ excludedRanges: readonly { startIndex: number; endIndex: number }[],
265
+ ): MatchOutcome | undefined {
266
+ let firstIndex: number | undefined;
267
+ let occurrences = 0;
268
+ const recordedIndices: number[] = [];
269
+ let searchStart = 0;
270
+
271
+ while (searchStart <= content.length - target.length) {
272
+ const index = content.indexOf(target, searchStart);
273
+ if (index === -1) break;
274
+
275
+ const endIndex = index + target.length;
276
+ if (!excludedRanges.some(range => index < range.endIndex && endIndex > range.startIndex)) {
277
+ firstIndex ??= index;
278
+ occurrences++;
279
+ if (recordedIndices.length < MAX_RECORDED_MATCHES) {
280
+ recordedIndices.push(index);
281
+ }
282
+ }
283
+ searchStart = endIndex;
284
+ }
285
+
286
+ if (firstIndex === undefined) {
264
287
  return undefined;
265
288
  }
266
289
 
267
- const occurrences = content.split(target).length - 1;
268
290
  if (occurrences > 1) {
269
291
  const contentLines = content.split("\n");
270
292
  const occurrenceLines: number[] = [];
271
293
  const occurrencePreviews: string[] = [];
272
- let searchStart = 0;
273
-
274
- for (let i = 0; i < MAX_RECORDED_MATCHES; i++) {
275
- const idx = content.indexOf(target, searchStart);
276
- if (idx === -1) break;
277
- const lineNumber = content.slice(0, idx).split("\n").length;
294
+ for (const index of recordedIndices) {
295
+ const lineNumber = content.slice(0, index).split("\n").length;
278
296
  occurrenceLines.push(lineNumber);
279
297
  occurrencePreviews.push(
280
298
  formatPreviewWindow(contentLines, lineNumber - 1, {
@@ -282,17 +300,16 @@ function findExactMatchOutcome(content: string, target: string): MatchOutcome |
282
300
  maxLen: OCCURRENCE_PREVIEW_MAX_LEN,
283
301
  }),
284
302
  );
285
- searchStart = idx + 1;
286
303
  }
287
304
 
288
305
  return { occurrences, occurrenceLines, occurrencePreviews };
289
306
  }
290
307
 
291
- const startLine = content.slice(0, exactIndex).split("\n").length;
308
+ const startLine = content.slice(0, firstIndex).split("\n").length;
292
309
  return {
293
310
  match: {
294
311
  actualText: target,
295
- startIndex: exactIndex,
312
+ startIndex: firstIndex,
296
313
  startLine,
297
314
  confidence: 1,
298
315
  },
@@ -408,6 +425,7 @@ function findBestFuzzyMatchCore(
408
425
  offsets: number[],
409
426
  threshold: number,
410
427
  includeDepth: boolean,
428
+ excludedRanges: readonly { startIndex: number; endIndex: number }[],
411
429
  ): BestFuzzyMatchResult {
412
430
  const targetNormalized = normalizeLines(targetLines, includeDepth);
413
431
 
@@ -417,6 +435,12 @@ function findBestFuzzyMatchCore(
417
435
  let aboveThresholdCount = 0;
418
436
 
419
437
  for (let start = 0; start <= contentLines.length - targetLines.length; start++) {
438
+ const startIndex = offsets[start];
439
+ const endLine = start + targetLines.length - 1;
440
+ const endIndex = Math.max(startIndex + 1, offsets[endLine] + contentLines[endLine].length);
441
+ if (excludedRanges.some(range => startIndex < range.endIndex && endIndex > range.startIndex)) {
442
+ continue;
443
+ }
420
444
  const windowLines = contentLines.slice(start, start + targetLines.length);
421
445
  const windowNormalized = normalizeLines(windowLines, includeDepth);
422
446
  let score = 0;
@@ -434,7 +458,7 @@ function findBestFuzzyMatchCore(
434
458
  bestScore = score;
435
459
  best = {
436
460
  actualText: windowLines.join("\n"),
437
- startIndex: offsets[start],
461
+ startIndex,
438
462
  startLine: start + 1,
439
463
  confidence: score,
440
464
  };
@@ -446,7 +470,12 @@ function findBestFuzzyMatchCore(
446
470
  return { best, aboveThresholdCount, secondBestScore };
447
471
  }
448
472
 
449
- function findBestFuzzyMatch(content: string, target: string, threshold: number): BestFuzzyMatchResult {
473
+ function findBestFuzzyMatch(
474
+ content: string,
475
+ target: string,
476
+ threshold: number,
477
+ excludedRanges: readonly { startIndex: number; endIndex: number }[],
478
+ ): BestFuzzyMatchResult {
450
479
  const contentLines = content.split("\n");
451
480
  const targetLines = target.split("\n");
452
481
 
@@ -458,11 +487,18 @@ function findBestFuzzyMatch(content: string, target: string, threshold: number):
458
487
  }
459
488
 
460
489
  const offsets = computeLineOffsets(contentLines);
461
- let result = findBestFuzzyMatchCore(contentLines, targetLines, offsets, threshold, true);
490
+ let result = findBestFuzzyMatchCore(contentLines, targetLines, offsets, threshold, true, excludedRanges);
462
491
 
463
492
  // Retry without indent depth if match is close but below threshold
464
493
  if (result.best && result.best.confidence < threshold && result.best.confidence >= FALLBACK_THRESHOLD) {
465
- const noDepthResult = findBestFuzzyMatchCore(contentLines, targetLines, offsets, threshold, false);
494
+ const noDepthResult = findBestFuzzyMatchCore(
495
+ contentLines,
496
+ targetLines,
497
+ offsets,
498
+ threshold,
499
+ false,
500
+ excludedRanges,
501
+ );
466
502
  if (noDepthResult.best && noDepthResult.best.confidence > result.best.confidence) {
467
503
  result = noDepthResult;
468
504
  }
@@ -473,25 +509,35 @@ function findBestFuzzyMatch(content: string, target: string, threshold: number):
473
509
 
474
510
  /**
475
511
  * Find a match for target text within content.
476
- * Used primarily for replace-mode edits.
512
+ * Used primarily for replace-mode edits; excluded ranges remain invisible to exact and fuzzy matching.
477
513
  */
478
514
  export function findMatch(
479
515
  content: string,
480
516
  target: string,
481
- options: { allowFuzzy: boolean; threshold?: number },
517
+ options: {
518
+ allowFuzzy: boolean;
519
+ threshold?: number;
520
+ excludedRanges?: readonly { startIndex: number; endIndex: number }[];
521
+ },
482
522
  ): MatchOutcome {
483
523
  if (target.length === 0) {
484
524
  return {};
485
525
  }
526
+ const excludedRanges = options.excludedRanges ?? [];
486
527
 
487
- const exactMatch = findExactMatchOutcome(content, target);
528
+ const exactMatch = findExactMatchOutcome(content, target, excludedRanges);
488
529
  if (exactMatch) {
489
530
  return exactMatch;
490
531
  }
491
532
 
492
533
  // Try fuzzy match
493
534
  const threshold = options.threshold ?? DEFAULT_FUZZY_THRESHOLD;
494
- const { best, aboveThresholdCount, secondBestScore } = findBestFuzzyMatch(content, target, threshold);
535
+ const { best, aboveThresholdCount, secondBestScore } = findBestFuzzyMatch(
536
+ content,
537
+ target,
538
+ threshold,
539
+ excludedRanges,
540
+ );
495
541
 
496
542
  if (!best) {
497
543
  return {};
@@ -1012,24 +1058,31 @@ export function findContextLine(
1012
1058
  return { index: undefined, confidence: bestScore };
1013
1059
  }
1014
1060
 
1015
- export const replaceEditEntrySchema = type({
1016
- old_text: "string",
1017
- new_text: "string",
1018
- "all?": "boolean",
1019
- });
1020
-
1021
1061
  export const replaceEditSchema = type({
1022
1062
  path: "string",
1023
- edits: replaceEditEntrySchema.array(),
1063
+ old_string: "string",
1064
+ new_string: "string",
1065
+ "replace_all?": "boolean",
1024
1066
  });
1025
1067
 
1026
- export type ReplaceEditEntry = typeof replaceEditEntrySchema.infer;
1027
1068
  export type ReplaceParams = typeof replaceEditSchema.infer;
1028
1069
 
1029
- export interface ExecuteReplaceSingleOptions {
1070
+ /**
1071
+ * Internal batch form of {@link ReplaceParams}, produced only by the Cursor
1072
+ * exec bridge: a `pi_edit` frame carries several replacements against one
1073
+ * path but must run as a single tool lifecycle with one aggregate result.
1074
+ * Never advertised to the model — the agent loop validates model calls
1075
+ * against {@link replaceEditSchema}, which has no `edits` field.
1076
+ */
1077
+ export interface ReplaceBatchParams {
1078
+ path: string;
1079
+ edits: Omit<ReplaceParams, "path">[];
1080
+ }
1081
+
1082
+ export interface ExecuteReplaceOptions {
1030
1083
  session: ToolSession;
1031
1084
  path: string;
1032
- params: ReplaceEditEntry;
1085
+ params: Omit<ReplaceParams, "path">;
1033
1086
  signal?: AbortSignal;
1034
1087
  batchRequest?: LspBatchRequest;
1035
1088
  allowFuzzy: boolean;
@@ -1038,9 +1091,9 @@ export interface ExecuteReplaceSingleOptions {
1038
1091
  beginDeferredDiagnosticsForPath: (path: string) => WritethroughDeferredHandle;
1039
1092
  }
1040
1093
 
1041
- export async function executeReplaceSingle(
1042
- options: ExecuteReplaceSingleOptions,
1043
- ): Promise<AgentToolResult<EditToolDetails, ReplaceEditEntry>> {
1094
+ export async function executeReplace(
1095
+ options: ExecuteReplaceOptions,
1096
+ ): Promise<AgentToolResult<EditToolDetails, ReplaceParams>> {
1044
1097
  const {
1045
1098
  session,
1046
1099
  path,
@@ -1052,12 +1105,12 @@ export async function executeReplaceSingle(
1052
1105
  writethrough,
1053
1106
  beginDeferredDiagnosticsForPath,
1054
1107
  } = options;
1055
- const { old_text, new_text, all } = params;
1108
+ const { old_string, new_string, replace_all } = params;
1056
1109
 
1057
1110
  enforcePlanModeWrite(session, path);
1058
1111
 
1059
- if (old_text.length === 0) {
1060
- throw new Error("old_text must not be empty.");
1112
+ if (old_string.length === 0) {
1113
+ throw new Error("old_string must not be empty.");
1061
1114
  }
1062
1115
 
1063
1116
  const absolutePath = resolvePlanPath(session, path);
@@ -1065,12 +1118,12 @@ export async function executeReplaceSingle(
1065
1118
  const { bom, text: content } = stripBom(rawContent);
1066
1119
  const originalEnding = detectLineEnding(content);
1067
1120
  const normalizedContent = normalizeToLF(content);
1068
- const normalizedOldText = normalizeToLF(old_text);
1069
- const normalizedNewText = normalizeToLF(new_text);
1121
+ const normalizedOldText = normalizeToLF(old_string);
1122
+ const normalizedNewText = normalizeToLF(new_string);
1070
1123
 
1071
1124
  const result = replaceText(normalizedContent, normalizedOldText, normalizedNewText, {
1072
1125
  fuzzy: allowFuzzy,
1073
- all: all ?? false,
1126
+ all: replace_all ?? false,
1074
1127
  threshold: fuzzyThreshold,
1075
1128
  });
1076
1129
 
@@ -115,7 +115,7 @@ interface EditRenderArgs {
115
115
  patch?: string;
116
116
  input?: string;
117
117
  _input?: string;
118
- all?: boolean;
118
+ replace_all?: boolean;
119
119
  // Patch mode fields
120
120
  op?: Operation;
121
121
  rename?: unknown;