one-code-extension 0.4.0 → 0.5.0

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 (419) hide show
  1. package/README.md +219 -219
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +32 -23
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +27 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +66 -34
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +123 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +515 -0
  56. package/extensions/background/index.ts +514 -236
  57. package/extensions/background/loop-fire.ts +136 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +52 -0
  63. package/extensions/background/wakeup.ts +159 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +88 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +229 -71
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/claude-compat/index.ts +7 -11
  80. package/extensions/claude-context/index.ts +446 -58
  81. package/extensions/compaction/cut.ts +157 -0
  82. package/extensions/compaction/index.ts +216 -75
  83. package/extensions/compaction/kept-thinking.ts +62 -0
  84. package/extensions/compaction/prompt.ts +15 -12
  85. package/extensions/context-budget/index.ts +24 -5
  86. package/extensions/context-management/index.ts +25 -0
  87. package/extensions/doctor/build.ts +59 -8
  88. package/extensions/doctor/builtins.ts +84 -0
  89. package/extensions/doctor/cli.ts +7 -1
  90. package/extensions/doctor/compat.ts +88 -23
  91. package/extensions/doctor/dependencies.ts +41 -5
  92. package/extensions/doctor/fix-prompt.ts +9 -2
  93. package/extensions/doctor/index.ts +102 -58
  94. package/extensions/doctor/models.ts +102 -63
  95. package/extensions/doctor/presets.ts +42 -27
  96. package/extensions/doctor/report.ts +19 -2
  97. package/extensions/doctor/viewer.ts +49 -27
  98. package/extensions/effort/index.ts +72 -19
  99. package/extensions/effort/settings.ts +21 -0
  100. package/extensions/effort/slider.ts +24 -20
  101. package/extensions/exit/index.ts +32 -1
  102. package/extensions/file-tracker/index.ts +337 -26
  103. package/extensions/file-tracker/replay.ts +96 -10
  104. package/extensions/file-tracker/restore.ts +63 -0
  105. package/extensions/file-tracker/results.ts +66 -0
  106. package/extensions/file-tracker/shell-reads.ts +211 -0
  107. package/extensions/file-tracker/tracker.ts +32 -15
  108. package/extensions/footer/footer-line.ts +45 -24
  109. package/extensions/footer/index.ts +47 -2
  110. package/extensions/footer/pr.ts +34 -5
  111. package/extensions/hooks/compaction.ts +7 -0
  112. package/extensions/hooks/executor.ts +32 -30
  113. package/extensions/hooks/index.ts +198 -31
  114. package/extensions/hooks/matcher.ts +3 -0
  115. package/extensions/hooks/protocol.ts +41 -3
  116. package/extensions/hooks/settings.ts +50 -19
  117. package/extensions/hooks/subagent-bridge.ts +2 -0
  118. package/extensions/hooks/trust.ts +13 -5
  119. package/extensions/idle-compact/index.ts +119 -0
  120. package/extensions/idle-compact/policy.ts +143 -0
  121. package/extensions/init/index.ts +11 -10
  122. package/extensions/init/prompt.ts +25 -32
  123. package/extensions/interrupted/index.ts +13 -1
  124. package/extensions/kept-thinking/index.ts +27 -0
  125. package/extensions/lib/agent-cron.ts +130 -0
  126. package/extensions/lib/agent-loader.ts +15 -1
  127. package/extensions/lib/agent-view.ts +85 -0
  128. package/extensions/lib/anthropic-payload.ts +82 -1
  129. package/extensions/lib/anthropic-server-call.ts +230 -0
  130. package/extensions/lib/anthropic-server-tools.ts +485 -0
  131. package/extensions/lib/app-launch.mjs +94 -0
  132. package/extensions/lib/argument-hints.ts +190 -0
  133. package/extensions/lib/atomic-write.ts +6 -2
  134. package/extensions/lib/auto-memory.ts +75 -0
  135. package/extensions/lib/bash-parser.ts +84 -0
  136. package/extensions/lib/btw-fork.ts +93 -0
  137. package/extensions/lib/child-extensions.ts +42 -0
  138. package/extensions/lib/child-writes.ts +45 -0
  139. package/extensions/lib/claude-context.ts +515 -100
  140. package/extensions/lib/claude-external-includes.ts +68 -0
  141. package/extensions/lib/claude-id.ts +22 -0
  142. package/extensions/lib/claude-rules.ts +323 -0
  143. package/extensions/lib/claude-settings.ts +76 -4
  144. package/extensions/lib/compaction-boundary.ts +36 -0
  145. package/extensions/lib/compaction-keep.mjs +83 -0
  146. package/extensions/lib/config-mode.ts +112 -0
  147. package/extensions/lib/consent-dialogs.ts +63 -0
  148. package/extensions/lib/consent-preview.ts +5 -1
  149. package/extensions/lib/consent-stores.ts +48 -0
  150. package/extensions/lib/context-facts.ts +78 -0
  151. package/extensions/lib/context-stack.ts +81 -0
  152. package/extensions/lib/deferred.ts +219 -25
  153. package/extensions/lib/dock-rows.ts +60 -0
  154. package/extensions/lib/empty-tool-result.ts +27 -0
  155. package/extensions/lib/environment-block.ts +76 -0
  156. package/extensions/lib/fetch-timeout.ts +24 -6
  157. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  158. package/extensions/lib/git.ts +75 -13
  159. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  160. package/extensions/lib/grammars/README.md +2 -0
  161. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  162. package/extensions/lib/guide-docs.ts +45 -0
  163. package/extensions/lib/headless-output.ts +46 -0
  164. package/extensions/lib/hook-gate.ts +7 -7
  165. package/extensions/lib/interrupt.ts +20 -4
  166. package/extensions/lib/key-input.ts +455 -0
  167. package/extensions/lib/keys.ts +14 -1
  168. package/extensions/lib/local-command.ts +47 -13
  169. package/extensions/lib/mcp-overrides.ts +1 -1
  170. package/extensions/lib/mcp-share.ts +46 -0
  171. package/extensions/lib/memory-content.ts +146 -0
  172. package/extensions/lib/memory-index.ts +18 -0
  173. package/extensions/lib/memory.ts +134 -123
  174. package/extensions/lib/model-catalog/huggingface.json +174 -0
  175. package/extensions/lib/model-catalog/models-dev.json +18297 -0
  176. package/extensions/lib/model-catalog/openrouter.json +5252 -0
  177. package/extensions/lib/model-catalog-data.ts +513 -0
  178. package/extensions/lib/model-catalog.ts +462 -0
  179. package/extensions/lib/model-choice-warnings.ts +137 -0
  180. package/extensions/lib/model-pairings.ts +152 -0
  181. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  182. package/extensions/lib/model-policy.ts +112 -15
  183. package/extensions/lib/model-tier.ts +283 -355
  184. package/extensions/lib/model-unusable.ts +1 -1
  185. package/extensions/lib/newer-model.ts +100 -0
  186. package/extensions/lib/notices.ts +87 -0
  187. package/extensions/lib/notifications.ts +308 -64
  188. package/extensions/lib/one-code-settings.ts +74 -8
  189. package/extensions/lib/open-browser.ts +63 -0
  190. package/extensions/lib/openrouter-generation.ts +94 -0
  191. package/extensions/lib/operation-deadline.ts +30 -0
  192. package/extensions/lib/original-command.ts +4 -4
  193. package/extensions/lib/package-version.ts +1 -1
  194. package/extensions/lib/panel-focus.ts +134 -0
  195. package/extensions/lib/paths.ts +17 -2
  196. package/extensions/lib/permission-gate.ts +72 -6
  197. package/extensions/lib/persisted-output.ts +60 -8
  198. package/extensions/lib/pi-ai-estimate.ts +19 -3
  199. package/extensions/lib/pi-version.ts +21 -2
  200. package/extensions/lib/plan-mode-channels.ts +20 -5
  201. package/extensions/lib/plugins.ts +26 -17
  202. package/extensions/lib/powershell-parse-script.ts +208 -0
  203. package/extensions/lib/powershell-parser.ts +344 -0
  204. package/extensions/lib/process-tree.ts +131 -8
  205. package/extensions/lib/prompt-options.ts +82 -0
  206. package/extensions/lib/queued-delivery.ts +44 -0
  207. package/extensions/lib/reminders.ts +364 -58
  208. package/extensions/lib/replaced-builtins.mjs +199 -0
  209. package/extensions/lib/replay-call.ts +70 -0
  210. package/extensions/lib/request-replay.ts +439 -0
  211. package/extensions/lib/scratchpad.ts +81 -14
  212. package/extensions/lib/send-now-channel.ts +7 -0
  213. package/extensions/lib/session-model-tier.ts +51 -0
  214. package/extensions/lib/session-work.ts +64 -0
  215. package/extensions/lib/settings-channels.ts +4 -4
  216. package/extensions/lib/shell-exit-semantics.ts +170 -0
  217. package/extensions/lib/shell-quote.ts +11 -0
  218. package/extensions/lib/shell-result.ts +70 -0
  219. package/extensions/lib/shell-spawn.ts +70 -6
  220. package/extensions/lib/shell-tool.ts +75 -25
  221. package/extensions/lib/side-call-cache.ts +124 -0
  222. package/extensions/lib/side-call-run.ts +96 -0
  223. package/extensions/lib/side-call-usage.ts +38 -0
  224. package/extensions/lib/side-call.ts +13 -1
  225. package/extensions/lib/single-flight.ts +18 -0
  226. package/extensions/lib/skill-body.ts +42 -0
  227. package/extensions/lib/skill-scan.ts +63 -24
  228. package/extensions/lib/spill-file.ts +140 -0
  229. package/extensions/lib/subagent-handback.ts +113 -0
  230. package/extensions/lib/system-role.ts +156 -0
  231. package/extensions/lib/terminal-text.ts +114 -0
  232. package/extensions/lib/text-width.ts +47 -16
  233. package/extensions/lib/timer-ops.ts +20 -0
  234. package/extensions/lib/tool-additions.ts +149 -0
  235. package/extensions/lib/tool-call-corruption.ts +134 -0
  236. package/extensions/lib/tool-path.ts +66 -0
  237. package/extensions/lib/tool-variants.ts +69 -0
  238. package/extensions/lib/tui-render.ts +255 -27
  239. package/extensions/lib/turn-budget-layout.ts +143 -0
  240. package/extensions/lib/update-check.mjs +15 -2
  241. package/extensions/lib/usage-bus.ts +1 -1
  242. package/extensions/lib/user-denial.ts +38 -0
  243. package/extensions/lib/workspace-channel.ts +16 -0
  244. package/extensions/lib/worktree-channel.ts +31 -3
  245. package/extensions/lib/worktree-isolation.ts +40 -10
  246. package/extensions/lsp/client.ts +8 -9
  247. package/extensions/lsp/index.ts +70 -4
  248. package/extensions/lsp/keep-alive.ts +1 -1
  249. package/extensions/lsp/trust.ts +119 -0
  250. package/extensions/lsp/watcher.ts +1 -1
  251. package/extensions/mcp/announce.ts +87 -5
  252. package/extensions/mcp/client.ts +161 -48
  253. package/extensions/mcp/config.ts +132 -8
  254. package/extensions/mcp/discovery.ts +19 -0
  255. package/extensions/mcp/index.ts +209 -71
  256. package/extensions/mcp/oauth/browser.ts +38 -17
  257. package/extensions/mcp/oauth/flow.ts +25 -17
  258. package/extensions/mcp/oauth/provider.ts +14 -8
  259. package/extensions/mcp/oauth/store.ts +34 -16
  260. package/extensions/mcp/panel/keys.ts +12 -11
  261. package/extensions/mcp/panel/render.ts +9 -6
  262. package/extensions/mcp/schema.ts +104 -5
  263. package/extensions/mcp/trust.ts +151 -67
  264. package/extensions/memory/entries.ts +23 -14
  265. package/extensions/memory/index.ts +134 -28
  266. package/extensions/memory/open-external.ts +33 -18
  267. package/extensions/memory/panel.ts +135 -26
  268. package/extensions/newer-model/index.ts +31 -0
  269. package/extensions/notebook/description.ts +18 -0
  270. package/extensions/notebook/index.ts +6 -5
  271. package/extensions/notebook/notebook.ts +1 -1
  272. package/extensions/permissions/index.ts +1300 -381
  273. package/extensions/permissions/matcher.ts +425 -74
  274. package/extensions/permissions/modes.ts +6 -4
  275. package/extensions/permissions/panel/host.ts +130 -0
  276. package/extensions/permissions/panel/keys.ts +48 -0
  277. package/extensions/permissions/panel/render.ts +450 -0
  278. package/extensions/permissions/panel/state.ts +550 -0
  279. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  280. package/extensions/permissions/powershell-paths.ts +197 -0
  281. package/extensions/permissions/powershell-rules.ts +207 -280
  282. package/extensions/permissions/powershell-tree.ts +557 -0
  283. package/extensions/permissions/project-trust.ts +27 -11
  284. package/extensions/permissions/protected-paths.ts +14 -2
  285. package/extensions/permissions/session-grant.ts +6 -0
  286. package/extensions/permissions/settings.ts +166 -31
  287. package/extensions/permissions/subagent-gate.ts +25 -0
  288. package/extensions/permissions/workspace.ts +78 -0
  289. package/extensions/plan-mode/index.ts +156 -69
  290. package/extensions/plan-mode/reminder.ts +89 -21
  291. package/extensions/plan-mode/texts.ts +177 -0
  292. package/extensions/plan-mode/viewer.ts +92 -27
  293. package/extensions/plugins/counts.ts +3 -3
  294. package/extensions/plugins/index.ts +61 -46
  295. package/extensions/plugins/install/install.ts +16 -0
  296. package/extensions/plugins/marketplace/sync.ts +9 -3
  297. package/extensions/plugins/marketplace/types.ts +3 -1
  298. package/extensions/plugins/panel/keys.ts +21 -24
  299. package/extensions/plugins/panel/render.ts +12 -6
  300. package/extensions/plugins/shell-expand.ts +107 -0
  301. package/extensions/plugins/template.ts +32 -15
  302. package/extensions/powershell/child.ts +7 -0
  303. package/extensions/powershell/description.ts +38 -6
  304. package/extensions/powershell/guards.ts +1 -1
  305. package/extensions/powershell/index.ts +12 -7
  306. package/extensions/recap/index.ts +65 -60
  307. package/extensions/recap/prompt.ts +2 -2
  308. package/extensions/recap/scheduler.ts +3 -5
  309. package/extensions/search-tools/grep-merge.ts +39 -0
  310. package/extensions/search-tools/index.ts +0 -0
  311. package/extensions/send-now/chord.ts +62 -0
  312. package/extensions/send-now/index.ts +133 -0
  313. package/extensions/session-title/index.ts +3 -0
  314. package/extensions/session-title/title.ts +4 -1
  315. package/extensions/skill/code-review.ts +97 -0
  316. package/extensions/skill/index.ts +421 -101
  317. package/extensions/skill/invoke.ts +128 -18
  318. package/extensions/skill/listing.ts +133 -9
  319. package/extensions/skill/panel/keys.ts +15 -16
  320. package/extensions/skill/panel/render.ts +5 -2
  321. package/extensions/skill/panel/state.ts +12 -1
  322. package/extensions/subagents/agent-args.ts +39 -0
  323. package/extensions/subagents/agent-description.ts +132 -0
  324. package/extensions/subagents/agents.ts +116 -23
  325. package/extensions/subagents/announce.ts +69 -0
  326. package/extensions/subagents/default-model.ts +2 -3
  327. package/extensions/subagents/delegation-steer.ts +4 -4
  328. package/extensions/subagents/fork-cache.ts +76 -0
  329. package/extensions/subagents/guide-agent.ts +189 -0
  330. package/extensions/subagents/hand-back-review.ts +3 -0
  331. package/extensions/subagents/index.ts +1493 -824
  332. package/extensions/subagents/live-runs.ts +26 -6
  333. package/extensions/subagents/model-select.ts +38 -25
  334. package/extensions/subagents/outcome.ts +2 -2
  335. package/extensions/subagents/panel-keys.ts +73 -31
  336. package/extensions/subagents/panel-render.ts +120 -96
  337. package/extensions/subagents/panel-widget.ts +60 -89
  338. package/extensions/subagents/runner.ts +108 -80
  339. package/extensions/subagents/runs.ts +39 -5
  340. package/extensions/subagents/send-to-main-tool.ts +1 -1
  341. package/extensions/subagents/session-turns.ts +32 -8
  342. package/extensions/subagents/shell-panel.ts +36 -178
  343. package/extensions/subagents/tasks-dialog.ts +170 -0
  344. package/extensions/subagents/viewport-mirror.ts +95 -0
  345. package/extensions/subagents/worktree.ts +57 -13
  346. package/extensions/system-prompt/environment.ts +19 -19
  347. package/extensions/system-prompt/idle-turn.ts +65 -0
  348. package/extensions/system-prompt/index.ts +131 -47
  349. package/extensions/system-prompt/template.ts +24 -50
  350. package/extensions/system-prompt/tiers/common.ts +23 -64
  351. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  352. package/extensions/system-prompt/tiers/low.ts +67 -45
  353. package/extensions/system-prompt/tiers/mid.ts +96 -38
  354. package/extensions/system-reminder/index.ts +175 -13
  355. package/extensions/tasks/descriptions.ts +190 -0
  356. package/extensions/tasks/index.ts +120 -41
  357. package/extensions/tasks/store.ts +83 -16
  358. package/extensions/tool-call-corruption/child.ts +7 -0
  359. package/extensions/tool-call-corruption/index.ts +237 -0
  360. package/extensions/tool-search/announce.ts +16 -0
  361. package/extensions/tool-search/index.ts +189 -41
  362. package/extensions/tool-style/index.ts +68 -10
  363. package/extensions/tool-style/write-description.ts +40 -0
  364. package/extensions/tool-style/write-render.ts +237 -0
  365. package/extensions/turn-duration/index.ts +7 -3
  366. package/extensions/turn-duration/line.ts +47 -4
  367. package/extensions/web/backends.ts +6 -9
  368. package/extensions/web/description.ts +50 -0
  369. package/extensions/web/index.ts +118 -53
  370. package/extensions/web/native-call.ts +15 -0
  371. package/extensions/web/native-results.ts +41 -0
  372. package/extensions/web-fetch/description.ts +42 -0
  373. package/extensions/web-fetch/extract.ts +53 -6
  374. package/extensions/web-fetch/index.ts +125 -76
  375. package/extensions/web-fetch/page-content.ts +19 -0
  376. package/extensions/web-fetch/response.ts +49 -0
  377. package/extensions/web-fetch/summarize.ts +9 -10
  378. package/extensions/workflow/agent-session.ts +105 -18
  379. package/extensions/workflow/args.ts +24 -0
  380. package/extensions/workflow/description.ts +67 -0
  381. package/extensions/workflow/globals.ts +17 -3
  382. package/extensions/workflow/index.ts +145 -162
  383. package/extensions/workflow/records.ts +1 -0
  384. package/extensions/workflow/run-manager.ts +29 -6
  385. package/extensions/workflow/saved-workflows.ts +3 -2
  386. package/extensions/workflow/types.ts +22 -1
  387. package/extensions/workflow/viewer.ts +54 -24
  388. package/extensions/workflow/widget.ts +20 -0
  389. package/extensions/worktree/descriptions.ts +91 -0
  390. package/extensions/worktree/guards.ts +316 -54
  391. package/extensions/worktree/index.ts +121 -39
  392. package/extensions/worktree/powershell-guards.ts +167 -0
  393. package/extensions/worktree/rewrite.ts +14 -16
  394. package/package.json +19 -7
  395. package/skills/artifact-design/SKILL.md +85 -0
  396. package/skills/code-review/SKILL.md +20 -20
  397. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  398. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  399. package/skills/code-review/cells/high-8-inline.md +122 -0
  400. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  401. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  402. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  403. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  404. package/skills/code-review/cells/minimal-prompt.md +32 -0
  405. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  406. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  407. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  408. package/skills/loop/SKILL.md +11 -0
  409. package/skills/security-review/SKILL.md +2 -5
  410. package/skills/simplify/SKILL.md +7 -5
  411. package/skills/workflow-authoring/SKILL.md +143 -0
  412. package/themes/onecode-light.json +1 -1
  413. package/themes/onecode.json +1 -1
  414. package/types/app.d.ts +58 -0
  415. package/types/community-packages.d.ts +26 -1
  416. package/extensions/auto-mode/recoverability.ts +0 -229
  417. package/extensions/lib/capability-index.ts +0 -466
  418. package/extensions/lib/model-facts.generated.json +0 -2675
  419. package/extensions/lib/model-facts.ts +0 -162
@@ -16,11 +16,13 @@
16
16
  */
17
17
 
18
18
  import os from "node:os";
19
- import { getAgentDir, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
19
+ import { isDeepStrictEqual } from "node:util";
20
+ import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
20
21
  import { Type } from "typebox";
21
22
  import { DEFER_CHANNEL } from "../lib/deferred.ts";
22
23
  import { MCP_TOOLS_CHANNEL } from "../lib/mcp-share.ts";
23
24
  import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
25
+ import { emptyToolResultContent } from "../lib/empty-tool-result.ts";
24
26
  import { ccToolRenderers } from "../lib/tui-render.ts";
25
27
  import {
26
28
  callTool,
@@ -47,14 +49,15 @@ import { readDisabledMcpServers, setMcpServerDisabled } from "../lib/mcp-overrid
47
49
  import { boundedDockHeight, safeThemeBold, safeThemePaint, truncateLine } from "../lib/tui-render.ts";
48
50
  import { authenticate as runOAuthFlow, silentProvider } from "./oauth/flow.ts";
49
51
  import { hasStoredTokens } from "./oauth/store.ts";
50
- import { announcedFrom, applyMcpDelta, emptyAnnounced, mcpChangeNotices, mcpDelta, type McpSnapshot } from "./announce.ts";
52
+ import { announcedFrom, applyMcpDelta, emptyAnnounced, mcpAnnouncedBaseline, mcpAnnouncedFromReminders, mcpChangeNotices, mcpDelta, serializeMcpAnnounced, type McpAnnounced, type McpSnapshot, mcpStartupNotices } from "./announce.ts";
53
+ import { CONTEXT_BASELINE_CHANNEL, restoredContext } from "../lib/context-stack.ts";
51
54
  import { decodeMcpKey } from "./panel/keys.ts";
52
55
  import { type McpEntry, type McpEntryStatus } from "./panel/model.ts";
53
56
  import { renderMcpPanel, type McpPaint } from "./panel/render.ts";
54
57
  import { applyMcpKey, initialMcpState, type McpEffect } from "./panel/state.ts";
55
58
  import { sessionAlive } from "../lib/session-lifecycle.ts";
56
- import { loadServers, type McpServer } from "./config.ts";
57
- import { approveMcpServers, persistApproval, projectRootOf } from "./trust.ts";
59
+ import { loadServers, type McpServer, noMcpServersMessage, piMcpConfigPaths, registeredMcpServers } from "./config.ts";
60
+ import { approveMcpServers, type McpTrustDeps, persistApproval, projectRootOf, reconnectRefusal } from "./trust.ts";
58
61
  import {
59
62
  capDescription,
60
63
  describeContent,
@@ -65,6 +68,11 @@ import {
65
68
  namespacedToolName,
66
69
  } from "./schema.ts";
67
70
  import { registerLocalCommand } from "../lib/local-command.ts";
71
+ import { canShowCustomUi, notifyRpcReadOnly } from "../lib/headless-output.ts";
72
+ import { claudeSourcesOn } from "../lib/config-mode.ts";
73
+ import { claudeJsonPath } from "../lib/paths.ts";
74
+ import { consentDialog, startupConsentReady } from "../lib/consent-dialogs.ts";
75
+ import { panelUi } from "../lib/panel-focus.ts";
68
76
 
69
77
  /** Anthropic's hard limit on a tool name; a longer one fails the whole request. */
70
78
  const MAX_TOOL_NAME_LENGTH = 128;
@@ -78,10 +86,13 @@ function unknownServerError(name: string) {
78
86
  };
79
87
  }
80
88
 
89
+ /** pi's own `mcp.json` files, read because our `/mcp` replaces pi 0.99's built-in MCP (config.ts). */
90
+ const piMcpConfigs = (cwd: string) => piMcpConfigPaths(getAgentDir(), cwd, CONFIG_DIR_NAME);
91
+
81
92
  export default function mcpExtension(pi: ExtensionAPI) {
82
93
  const connections = new Map<string, Connection>();
83
94
  let failures: FailedConnection[] = [];
84
- const registered = new Set<string>();
95
+ const registered = new Map<string, { server: string; tool: string }>();
85
96
  let servers: McpServer[] = [];
86
97
  // http servers that returned a 401 with no stored tokens — authNeeded via OAuth,
87
98
  // distinct from a missing-env authNeeded (which Authenticate cannot fix).
@@ -93,6 +104,13 @@ export default function mcpExtension(pi: ExtensionAPI) {
93
104
  const withheldNames = new Map<string, string>();
94
105
  // Config file paths contributed by plugins, so the panel can group them.
95
106
  let pluginConfigPaths = new Set<string>();
107
+ // pi 0.99's registry of servers other extensions register; absent on older pi.
108
+ // Registered after the session started: our servers are read once per session.
109
+ pi.on("mcp_servers_change", (_event, ctx) => {
110
+ const text = "An extension changed its MCP servers; run /reload to connect the change.";
111
+ if (ctx.hasUI) ctx.ui.notify(text, "info");
112
+ else process.stderr.write(`${text}\n`);
113
+ });
96
114
  const home = os.homedir();
97
115
  // Live tool definitions, shared with in-process subagents so they reach MCP
98
116
  // through these same open connections instead of connecting their own.
@@ -102,6 +120,8 @@ export default function mcpExtension(pi: ExtensionAPI) {
102
120
  // those blocks are frozen and later changes ride one-shot notices (./announce.ts).
103
121
  let requestSent = false;
104
122
  let announced = emptyAnnounced();
123
+ /** A resumed snapshot from before structured baselines: its MCP blocks, until the server names are known. */
124
+ let legacyBlocks: { instructions?: string; failures?: string } | undefined;
105
125
  pi.on("context", () => {
106
126
  requestSent = true;
107
127
  });
@@ -114,6 +134,8 @@ export default function mcpExtension(pi: ExtensionAPI) {
114
134
  const resultsDir = (ctx: ExtensionContext | undefined): string => sessionResultsDir(ctx);
115
135
 
116
136
  const registerToolsFor = (connection: Connection) => {
137
+ // Names other extensions own, read once per batch (getAllTools builds every definition).
138
+ let taken: Set<string> | undefined;
117
139
  for (const tool of connection.tools) {
118
140
  const name = namespacedToolName(connection.server.name, tool.name);
119
141
  // Anthropic caps a tool name at 128 characters and rejects the whole
@@ -128,8 +150,12 @@ export default function mcpExtension(pi: ExtensionAPI) {
128
150
  });
129
151
  continue;
130
152
  }
131
- if (registered.has(name)) {
132
- // Two tools sanitise to one name (`a-b` and `a_b`, or a server
153
+ const owner = registered.get(name);
154
+ // An unchanged tool on reconnect keeps its exact definition and loaded
155
+ // state. Only a different raw server/tool pair is a name collision.
156
+ if (owner?.server === connection.server.name && owner.tool === tool.name) continue;
157
+ if (owner || (taken ??= new Set(pi.getAllTools().map((existing) => existing.name))).has(name)) {
158
+ // Two tools sanitise to one name (`a.b` and `a_b`, or a server
133
159
  // reconnecting under a stale registration). Skipping silently would
134
160
  // make the second tool unreachable with no trace; record it where
135
161
  // the connection summary reports it.
@@ -139,7 +165,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
139
165
  });
140
166
  continue;
141
167
  }
142
- registered.add(name);
168
+ registered.set(name, { server: connection.server.name, tool: tool.name });
143
169
 
144
170
  const def: ToolDefinition = {
145
171
  name,
@@ -147,7 +173,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
147
173
  ...ccToolRenderers(`${connection.server.name}: ${tool.name}`),
148
174
  description: capDescription(tool.description) ?? `MCP tool "${tool.name}" from server "${connection.server.name}".`,
149
175
  parameters: jsonSchemaToTypeBox(tool.inputSchema),
150
- async execute(toolCallId, params, _signal, _onUpdate, ctx) {
176
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
151
177
  const live = connections.get(connection.server.name);
152
178
  if (!live) {
153
179
  return {
@@ -156,17 +182,27 @@ export default function mcpExtension(pi: ExtensionAPI) {
156
182
  isError: true,
157
183
  };
158
184
  }
185
+ const current = live.tools.find((candidate) => candidate.name === tool.name);
186
+ if (!current || !isDeepStrictEqual(current.inputSchema, tool.inputSchema)) {
187
+ return {
188
+ content: [{ type: "text", text: !current
189
+ ? `MCP server "${connection.server.name}" no longer exposes tool "${tool.name}".`
190
+ : `MCP tool "${name}" changed its input schema. Run /reload to load the new definition; this call was not sent.` }],
191
+ details: {} as Record<string, unknown>,
192
+ isError: true,
193
+ };
194
+ }
159
195
  try {
160
- const result = await callTool(live, tool.name, (params ?? {}) as Record<string, unknown>);
161
- const { text, images } = describeContent(result.content as McpContentBlock[] | undefined, connection.server.name);
196
+ const result = await callTool(live, tool.name, (params ?? {}) as Record<string, unknown>, signal);
197
+ const { text: contentText, images } = describeContent(result.content as McpContentBlock[] | undefined, connection.server.name);
198
+ const structuredText = result.structuredContent !== undefined ? JSON.stringify(result.structuredContent, null, 2) : "";
199
+ const text = [contentText, structuredText].filter(Boolean).join("\n");
200
+ const content = [
201
+ ...(text ? [{ type: "text" as const, text: persistIfLarge(text, { dir: resultsDir(ctx), id: toolCallId }) }] : []),
202
+ ...images.map((image) => ({ type: "image" as const, data: image.data, mimeType: image.mimeType })),
203
+ ];
162
204
  return {
163
- content: [
164
- {
165
- type: "text",
166
- text: text ? persistIfLarge(text, { dir: resultsDir(ctx), id: toolCallId }) : "(no output)",
167
- },
168
- ...images.map((image) => ({ type: "image" as const, data: image.data, mimeType: image.mimeType })),
169
- ],
205
+ content: emptyToolResultContent(content, name) ?? content,
170
206
  details: { server: connection.server.name, tool: tool.name } as Record<string, unknown>,
171
207
  isError: result.isError === true,
172
208
  };
@@ -232,15 +268,35 @@ export default function mcpExtension(pi: ExtensionAPI) {
232
268
  });
233
269
  emitInstructions();
234
270
  };
271
+ let previousTools = connection.tools;
272
+ connection.onToolsChanged = (error) => {
273
+ if (!alive() || connection.closing || connections.get(server.name) !== connection) return;
274
+ if (error) {
275
+ failures.push({ server, error: error.message });
276
+ pi.events.emit(REMINDER_CHANNEL, { text: `MCP server "${server.name}" could not refresh its tool list: ${error.message}` });
277
+ return;
278
+ }
279
+ const unavailable = previousTools.filter((old) => {
280
+ const current = connection.tools.find((tool) => tool.name === old.name);
281
+ return !current || !isDeepStrictEqual(current.inputSchema, old.inputSchema);
282
+ });
283
+ previousTools = connection.tools;
284
+ registerToolsFor(connection);
285
+ if (unavailable.length > 0) {
286
+ pi.events.emit(REMINDER_CHANNEL, {
287
+ text: `MCP server "${server.name}" removed or changed these tools: ${unavailable.map((tool) => namespacedToolName(server.name, tool.name)).join(", ")}. Their old definitions cannot be used. Run /reload to refresh changed schemas.`,
288
+ });
289
+ }
290
+ };
235
291
  registerToolsFor(connection);
236
292
  for (const warning of connection.warnings) failures.push({ server, error: warning });
237
- if (connection.resources.length > 0) registerResourceTools();
293
+ if (connection.resources.length > 0 || connection.resourceTemplates?.length) registerResourceTools();
238
294
  };
239
295
 
240
296
  const connectOne = async (server: McpServer): Promise<void> => {
241
297
  failures = failures.filter((f) => f.server.name !== server.name);
242
298
  oauthNeeded.delete(server.name);
243
- const provider = server.kind === "http" && hasStoredTokens(server.name) ? silentProvider(server.name) : undefined;
299
+ const provider = server.kind === "http" && hasStoredTokens(server) ? silentProvider(server) : undefined;
244
300
  try {
245
301
  const connection = await connect(server, provider);
246
302
  if (!alive()) {
@@ -265,6 +321,9 @@ export default function mcpExtension(pi: ExtensionAPI) {
265
321
  * message 1 are (re)written; after it they are frozen and only the changes are
266
322
  * announced (./announce.ts).
267
323
  */
324
+ const publishBaseline = (value: McpAnnounced) => {
325
+ pi.events.emit(CONTEXT_BASELINE_CHANNEL, { key: "mcp", value: serializeMcpAnnounced(value) });
326
+ };
268
327
  const emitInstructions = () => {
269
328
  const live = [...connections.values()];
270
329
  const snapshot: McpSnapshot = {
@@ -274,6 +333,8 @@ export default function mcpExtension(pi: ExtensionAPI) {
274
333
 
275
334
  if (!requestSent) {
276
335
  // Nothing is cached yet: (re)write the standing message-1 blocks.
336
+ const nextAnnounced = announcedFrom(snapshot);
337
+ publishBaseline(nextAnnounced);
277
338
  if (snapshot.failed.length === 0) {
278
339
  pi.events.emit(REMINDER_CHANNEL, { scope: "every-turn", key: "mcp-failures", text: "", remove: true });
279
340
  } else {
@@ -300,35 +361,75 @@ export default function mcpExtension(pi: ExtensionAPI) {
300
361
  // being told to use tools that are gone.
301
362
  pi.events.emit(REMINDER_CHANNEL, { scope: "every-turn", key: "mcp-instructions", text: "", remove: true });
302
363
  }
303
- announced = announcedFrom(snapshot);
364
+ announced = nextAnnounced;
304
365
  return;
305
366
  }
306
367
 
307
368
  // Message 1 is frozen (rewriting it would re-cache the whole conversation):
308
369
  // tell the model only what changed, as one-shots where it reads next.
309
370
  const delta = mcpDelta(announced, snapshot);
310
- for (const text of mcpChangeNotices(delta)) pi.events.emit(REMINDER_CHANNEL, { text });
371
+ const notices = mcpChangeNotices(delta);
372
+ if (notices.length > 0) {
373
+ const nextAnnounced = { instructed: new Map(announced.instructed), failed: new Map(announced.failed) };
374
+ applyMcpDelta(nextAnnounced, delta);
375
+ publishBaseline(nextAnnounced);
376
+ }
377
+ for (const text of notices) pi.events.emit(REMINDER_CHANNEL, { text });
311
378
  applyMcpDelta(announced, delta);
312
379
  };
313
380
 
314
381
  /** Config files that exist but failed to parse (review M11) — shown at startup and in /mcp. */
315
382
  let configErrors: string[] = [];
316
383
 
384
+ /** How the consent check asks, tells and records a "No", for startup and for Reconnect alike. */
385
+ const consentDeps = (ctx: ExtensionContext): McpTrustDeps => ({
386
+ hasUI: ctx.hasUI,
387
+ select: (title, options) => consentDialog(pi.events, (signal) => ctx.ui.select(title, options, { signal })),
388
+ notify: (message) => {
389
+ if (ctx.hasUI) ctx.ui.notify(message, "warning");
390
+ else process.stderr.write(`${message}\n`);
391
+ },
392
+ disable: (server) => {
393
+ setMcpServerDisabled(server.name, true, "project", ctx.cwd, home);
394
+ disabledNames.add(server.name);
395
+ },
396
+ });
397
+
317
398
  const connectAll = async (ctx: ExtensionContext) => {
318
399
  const plugins = discoverPlugins(defaultDiscoverRoots(getAgentDir(), ctx.cwd));
319
400
  pluginConfigPaths = new Set(plugins.mcpConfigs);
320
401
  configErrors = [];
402
+ const configNotes: string[] = [];
321
403
  servers = loadServers(ctx.cwd, home, process.env, [...pluginConfigPaths], {
322
404
  pluginNames: plugins.mcpConfigPlugins,
323
405
  onError: (path, message) => configErrors.push(`${path}: ${message}`),
406
+ piConfigs: piMcpConfigs(ctx.cwd),
407
+ onNote: (message) => configNotes.push(message),
408
+ registered: registeredMcpServers(pi),
324
409
  });
325
- if (configErrors.length > 0) {
326
- const text = `MCP config could not be parsed (ignored): ${configErrors.join("; ")}`;
410
+ // An older snapshot stored only the rendered blocks: read them again by the
411
+ // configured names, before any delta is taken against them.
412
+ if (legacyBlocks) {
413
+ announced = mcpAnnouncedFromReminders(legacyBlocks.instructions, legacyBlocks.failures, servers.map((server) => server.name));
414
+ legacyBlocks = undefined;
415
+ }
416
+ const configWarnings = [
417
+ ...(configErrors.length > 0 ? [`MCP config could not be parsed (ignored): ${configErrors.join("; ")}`] : []),
418
+ ...configNotes,
419
+ ];
420
+ for (const text of configWarnings) {
327
421
  if (ctx.hasUI) ctx.ui.notify(text, "warning");
328
422
  else process.stderr.write(`${text}\n`);
329
423
  }
330
424
  disabledNames = readDisabledMcpServers(ctx.cwd, home);
331
- if (servers.length === 0) return;
425
+ // A resumed first-prepend may describe a server whose configuration was
426
+ // removed entirely. There is no connection attempt in that case, so make
427
+ // the frozen-prefix delta explicit rather than leaving the stale capability
428
+ // silently standing. Fresh empty sessions keep their byte-identical no-op.
429
+ if (servers.length === 0) {
430
+ if (requestSent) emitInstructions();
431
+ return;
432
+ }
332
433
 
333
434
  // Disabled servers are listed but never connected; a server with an unset
334
435
  // credential env var stays "needs authentication" and is not attempted
@@ -336,19 +437,13 @@ export default function mcpExtension(pi: ExtensionAPI) {
336
437
  // server needs the user's consent first (trust.ts — a cloned repo must not
337
438
  // run commands at startup). Everything else connects.
338
439
  const candidates = servers.filter((server) => !disabledNames.has(server.name) && !server.missingEnv?.length);
339
- const consent = await approveMcpServers(candidates, pluginConfigPaths, ctx.cwd, home, {
340
- hasUI: ctx.hasUI,
341
- select: (title, options) => ctx.ui.select(title, options),
342
- notify: (message) => {
343
- if (ctx.hasUI) ctx.ui.notify(message, "warning");
344
- else process.stderr.write(`${message}\n`);
345
- },
346
- disable: (server) => {
347
- setMcpServerDisabled(server.name, true, "project", ctx.cwd, home);
348
- disabledNames.add(server.name);
349
- },
350
- });
351
- if (!alive()) return;
440
+ // Reserve the modal position before the asynchronous policy read, so the
441
+ // external-includes dialog cannot jump ahead of MCP's startup consent.
442
+ const consent = await consentDialog(pi.events, (signal) => approveMcpServers(candidates, pluginConfigPaths, ctx.cwd, home, {
443
+ ...consentDeps(ctx),
444
+ select: (title, options) => ctx.ui.select(title, options, { signal }),
445
+ }));
446
+ if (!consent || !alive()) return;
352
447
  withheldNames.clear();
353
448
  for (const { server, reason } of consent.withheld) {
354
449
  withheldNames.set(
@@ -364,16 +459,15 @@ export default function mcpExtension(pi: ExtensionAPI) {
364
459
 
365
460
  if (!alive()) return;
366
461
 
367
- if (failures.length > 0) {
368
- const text = failures.map((f) => `MCP server "${f.server.name}" failed: ${f.error}`).join("\n") + " (/mcp for status)";
369
- if (ctx.hasUI) ctx.ui.notify(text, "warning");
370
- else process.stderr.write(`${text}\n`);
371
- }
372
- if (oauthNeeded.size > 0 && ctx.hasUI) {
373
- ctx.ui.notify(
374
- `${oauthNeeded.size} MCP server${oauthNeeded.size === 1 ? "" : "s"} need authentication — run /mcp to authenticate.`,
375
- "warning",
376
- );
462
+ // Warnings from servers that did connect are shown in /mcp, not as failures.
463
+ const failed = failures.filter((f) => !connections.has(f.server.name));
464
+ if (ctx.hasUI) {
465
+ // Needs auth: an OAuth sign-in, or a credential variable that is not set (never attempted).
466
+ const needsAuth = new Set([...oauthNeeded, ...servers.filter((server) => !disabledNames.has(server.name) && server.missingEnv?.length).map((server) => server.name)]).size;
467
+ for (const text of mcpStartupNotices(new Set(failed.map((f) => f.server.name)).size, needsAuth)) ctx.ui.notify(text, "warning");
468
+ } else if (failed.length > 0) {
469
+ // Headless runs have no /mcp, so they keep each server's error on stderr.
470
+ process.stderr.write(`${failed.map((f) => `MCP server "${f.server.name}" failed: ${f.error}`).join("\n")}\n`);
377
471
  }
378
472
 
379
473
  emitInstructions();
@@ -384,12 +478,17 @@ export default function mcpExtension(pi: ExtensionAPI) {
384
478
  if (resourceToolsRegistered) return;
385
479
  resourceToolsRegistered = true;
386
480
 
387
- pi.registerTool({
481
+ const registerResourceTool: ExtensionAPI["registerTool"] = (definition) => {
482
+ pi.registerTool(definition);
483
+ sharedTools.push(definition as ToolDefinition);
484
+ };
485
+
486
+ registerResourceTool({
388
487
  name: "list_mcp_resources",
389
488
  label: "MCP Resources",
390
489
  ...ccToolRenderers("MCP Resources"),
391
490
  description:
392
- "List resources exposed by connected MCP servers. Resources are readable documents or data the server offers, addressed by uri.",
491
+ "List resources and URI templates exposed by connected MCP servers. Resources are readable documents or data the server offers, addressed by uri. Substitute template variables to construct a resource uri.",
393
492
  parameters: Type.Object({
394
493
  server: Type.Optional(Type.String({ description: "Limit to one server by name" })),
395
494
  }),
@@ -417,12 +516,15 @@ export default function mcpExtension(pi: ExtensionAPI) {
417
516
  `${connection.server.name} ${resource.uri}${resource.name ? ` — ${resource.name}` : ""}${resource.description ? `: ${resource.description}` : ""}`,
418
517
  );
419
518
  }
519
+ for (const template of connection.resourceTemplates ?? []) {
520
+ lines.push(`${connection.server.name} ${template.uriTemplate} (URI template)${template.name ? ` — ${template.name}` : ""}${template.description ? `: ${template.description}` : ""}`);
521
+ }
420
522
  }
421
523
  return {
422
524
  content: [
423
525
  {
424
526
  type: "text",
425
- text: lines.length ? persistIfLarge(lines.join("\n"), { dir: resultsDir(ctx), id: toolCallId }) : "No MCP resources available.",
527
+ text: lines.length ? persistIfLarge(lines.join("\n"), { dir: resultsDir(ctx), id: toolCallId }) : "No resources found. MCP servers may still provide tools even if they have no resources.",
426
528
  },
427
529
  ],
428
530
  details: { count: lines.length },
@@ -430,7 +532,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
430
532
  },
431
533
  });
432
534
 
433
- pi.registerTool({
535
+ registerResourceTool({
434
536
  name: "read_mcp_resource",
435
537
  label: "Read MCP Resource",
436
538
  ...ccToolRenderers("Read MCP Resource"),
@@ -439,17 +541,17 @@ export default function mcpExtension(pi: ExtensionAPI) {
439
541
  server: Type.String({ description: "Server name that owns the resource" }),
440
542
  uri: Type.String({ description: "Resource uri" }),
441
543
  }),
442
- async execute(toolCallId, params, _signal, _onUpdate, ctx) {
544
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
443
545
  const connection = connections.get(params.server);
444
546
  if (!connection) return unknownServerError(params.server);
445
547
  try {
446
- const result = await readResource(connection, params.uri);
447
- const text = describeResourceContents(result.contents as McpResourceContents[] | undefined);
548
+ const result = await readResource(connection, params.uri, signal);
549
+ const text = describeResourceContents(result.contents as McpResourceContents[] | undefined) || JSON.stringify({ contents: result.contents ?? [] });
448
550
  return {
449
551
  content: [
450
552
  {
451
553
  type: "text",
452
- text: text ? persistIfLarge(text, { dir: resultsDir(ctx), id: toolCallId }) : "(empty resource)",
554
+ text: persistIfLarge(text, { dir: resultsDir(ctx), id: toolCallId }),
453
555
  },
454
556
  ],
455
557
  details: { server: params.server, uri: params.uri },
@@ -464,7 +566,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
464
566
  },
465
567
  });
466
568
 
467
- pi.registerTool({
569
+ registerResourceTool({
468
570
  name: "read_mcp_resource_dir",
469
571
  label: "List MCP Resource Directory",
470
572
  ...ccToolRenderers("List MCP Resource Directory"),
@@ -474,18 +576,18 @@ export default function mcpExtension(pi: ExtensionAPI) {
474
576
  server: Type.String({ description: "Server name that owns the directory" }),
475
577
  uri: Type.String({ description: "The directory resource uri to list" }),
476
578
  }),
477
- async execute(toolCallId, params, _signal, _onUpdate, ctx) {
579
+ async execute(toolCallId, params, signal, _onUpdate, ctx) {
478
580
  const connection = connections.get(params.server);
479
581
  if (!connection) return unknownServerError(params.server);
480
582
  try {
481
- const result = await readResourceDir(connection, params.uri);
583
+ const result = await readResourceDir(connection, params.uri, signal);
482
584
  const entries = (result.resources ?? result.entries ?? []) as Array<{ uri?: string; name?: string; mimeType?: string }>;
483
585
  const lines = entries.map((e) => `${e.uri ?? "(no uri)"}${e.name ? ` — ${e.name}` : ""}${e.mimeType ? ` (${e.mimeType})` : ""}`);
484
586
  return {
485
587
  content: [
486
588
  {
487
589
  type: "text",
488
- text: lines.length ? persistIfLarge(lines.join("\n"), { dir: resultsDir(ctx), id: toolCallId }) : "(empty directory)",
590
+ text: lines.length ? persistIfLarge(lines.join("\n"), { dir: resultsDir(ctx), id: toolCallId }) : 'Directory is empty.\n{"resources":[]}',
489
591
  },
490
592
  ],
491
593
  details: { server: params.server, uri: params.uri },
@@ -503,13 +605,19 @@ export default function mcpExtension(pi: ExtensionAPI) {
503
605
  for (const name of ["list_mcp_resources", "read_mcp_resource", "read_mcp_resource_dir"]) {
504
606
  pi.events.emit(DEFER_CHANNEL, { name, keywords: ["mcp", "resource", "document", "uri", "directory"] });
505
607
  }
608
+ pi.events.emit(MCP_TOOLS_CHANNEL, { tools: [...sharedTools] });
506
609
  };
507
610
 
508
611
  pi.on("session_start", async (_event, ctx) => {
509
- // A new session (/clear, resume in a new process) has no cached prefix yet:
510
- // its message-1 blocks are written fresh (same reset as tool-search).
511
- requestSent = false;
512
- announced = emptyAnnounced();
612
+ const restored = restoredContext(pi.events);
613
+ // The restored stack owns message 1. Its structured state records the exact
614
+ // arbitrary server text that was shown; a fresh session keeps prior behavior.
615
+ requestSent = restored !== undefined;
616
+ const stackText = (key: string) => restored?.stack.find((entry) => (entry as { key?: string }).key === key)?.text;
617
+ const structured = mcpAnnouncedBaseline(restored?.baselines.mcp);
618
+ legacyBlocks = structured ? undefined : { instructions: stackText("mcp-instructions"), failures: stackText("mcp-failures") };
619
+ announced = structured ?? mcpAnnouncedFromReminders(legacyBlocks?.instructions, legacyBlocks?.failures);
620
+ if (!restored) publishBaseline(announced);
513
621
  // pi awaits session_start handlers serially before the prompt opens, and
514
622
  // remote servers take seconds to answer — awaiting here was the entire
515
623
  // "slow startup" (4.9s → 0.24s measured, findings §15). In the interactive
@@ -524,6 +632,9 @@ export default function mcpExtension(pi: ExtensionAPI) {
524
632
  // `failures`, so this only catches setup bugs — surface them loud.
525
633
  if (ctx.hasUI) ctx.ui.notify(`MCP startup failed: ${(error as Error).message}`, "error");
526
634
  });
635
+ // The external-includes question follows the startup hooks/MCP consent,
636
+ // but does not wait for slow server connections.
637
+ startupConsentReady(pi.events);
527
638
  connecting.finally(() => {
528
639
  connectSettled = true;
529
640
  // The session may be gone by now (/clear while a remote server was still
@@ -600,12 +711,18 @@ export default function mcpExtension(pi: ExtensionAPI) {
600
711
  if (pluginConfigPaths.has(server.source)) {
601
712
  return { rank: 3, group: "Plugin MCPs", configLocation: "Plugin configuration", scope: "project" };
602
713
  }
603
- if (server.source === join(home, ".claude.json")) {
714
+ if (server.source === claudeJsonPath(home) || server.piOrigin === "user") {
604
715
  return { rank: 0, group: `User MCPs (${shorten(server.source)})`, configLocation: shorten(server.source), scope: "user" };
605
716
  }
606
717
  if (server.source.endsWith(join(".claude", "settings.local.json"))) {
607
718
  return { rank: 2, group: "Project MCPs (.claude/settings.local.json)", configLocation: shorten(server.source), scope: "project" };
608
719
  }
720
+ if (server.piOrigin === "extension") {
721
+ return { rank: 3, group: "Extension MCPs", configLocation: shorten(server.source), scope: "user" };
722
+ }
723
+ if (server.piOrigin === "project") {
724
+ return { rank: 1, group: `Project MCPs (${join(CONFIG_DIR_NAME, "mcp.json")})`, configLocation: shorten(server.source), scope: "project" };
725
+ }
609
726
  return { rank: 1, group: `Project MCPs (${shorten(server.source)})`, configLocation: shorten(server.source), scope: "project" };
610
727
  };
611
728
 
@@ -626,7 +743,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
626
743
  // honestly from whether tokens are stored — not just "failed http".
627
744
  const authState =
628
745
  status === "failed" && server.kind === "http"
629
- ? hasStoredTokens(server.name)
746
+ ? hasStoredTokens(server)
630
747
  ? ("authenticated" as const)
631
748
  : ("notAuthenticated" as const)
632
749
  : undefined;
@@ -656,7 +773,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
656
773
  const MCP_PANEL_MAX_HEIGHT = 22;
657
774
 
658
775
  const openMcpPanel = async (ctx: ExtensionContext): Promise<void> => {
659
- await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
776
+ await panelUi(pi.events, ctx.ui).custom<null>((tui, theme, _keybindings, done) => {
660
777
  const paint: McpPaint = { fg: safeThemePaint(theme), bold: safeThemeBold(theme) };
661
778
  const state = initialMcpState();
662
779
  let notices: string[] = [];
@@ -692,7 +809,24 @@ export default function mcpExtension(pi: ExtensionAPI) {
692
809
  const runReconnect = async (entry: McpEntry) => {
693
810
  const server = servers.find((s) => s.name === entry.name);
694
811
  if (!server || busy.has(entry.name)) return;
812
+ const refusal = reconnectRefusal(server);
813
+ if (refusal) {
814
+ notices = [refusal];
815
+ syncRepaint();
816
+ return;
817
+ }
695
818
  busy.add(entry.name);
819
+ // Reconnect spawns the command again, so a project server needs the
820
+ // same consent startup asked for (a stored or session approval
821
+ // passes without a prompt).
822
+ const consent = await approveMcpServers([server], pluginConfigPaths, ctx.cwd, home, consentDeps(ctx));
823
+ if (consent.approved.length === 0) {
824
+ busy.delete(entry.name);
825
+ if (!alive()) return;
826
+ notices = [`"${entry.name}" was not approved, so it was not reconnected.`];
827
+ syncRepaint();
828
+ return;
829
+ }
696
830
  notices = [`Reconnecting to "${entry.name}"…`];
697
831
  syncRepaint();
698
832
  const existing = connections.get(entry.name);
@@ -844,16 +978,19 @@ export default function mcpExtension(pi: ExtensionAPI) {
844
978
  };
845
979
 
846
980
  registerLocalCommand(pi, "mcp", {
981
+ reportsResult: true,
847
982
  description: "Manage MCP servers (status, reconnect, authenticate, enable/disable)",
848
983
  handler: async (args, ctx) => {
849
984
  if (servers.length === 0) {
850
- ctx.ui.notify("No MCP servers configured. Add them to .mcp.json or ~/.claude.json.", "info");
851
- return;
985
+ const stdout = noMcpServersMessage(claudeSourcesOn());
986
+ ctx.ui.notify(stdout, "info");
987
+ return stdout;
852
988
  }
853
- if (ctx.hasUI) {
989
+ if (canShowCustomUi(ctx)) {
854
990
  await openMcpPanel(ctx);
855
- return;
991
+ return "MCP dialog dismissed";
856
992
  }
993
+ const readOnly = notifyRpcReadOnly(ctx, "/mcp", "reconnect, authenticate, enable or disable servers");
857
994
  // Non-interactive fallback: a flat status listing, with tool/resource
858
995
  // counts and any warning for connected servers.
859
996
  const lines = servers.map((server) => {
@@ -869,6 +1006,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
869
1006
  return `${status.padEnd(12)} ${server.name}${suffix} (${server.source})`;
870
1007
  });
871
1008
  ctx.ui.notify(lines.join("\n"), "info");
1009
+ return [readOnly, ...lines].filter(Boolean).join("\n");
872
1010
  },
873
1011
  });
874
1012
  }
@@ -1,26 +1,47 @@
1
1
  /**
2
- * Open a URL in the user's default browser (no dependency — platform command).
2
+ * Open a sign-in URL in the user's default browser (`lib/open-browser.ts`),
3
+ * falling back to printing the URL for manual paste when no browser starts.
3
4
  *
4
- * Detached and unref'd so the auth flow never blocks on the browser process,
5
- * and errors are swallowed to a boolean: a headless box has no browser, in which
6
- * case the caller falls back to printing the URL for manual paste.
5
+ * The authorization URL is the server's choice (its metadata names the
6
+ * endpoint, the SDK appends `&`-joined parameters), so only `https:`, or
7
+ * `http:` on a loopback host, is opened: `open` and `xdg-open` hand any other
8
+ * scheme (`file:`, a custom handler) to whatever the desktop registered for it.
7
9
  */
8
10
 
9
- import { spawn } from "node:child_process";
11
+ import { launchOpener, type OpenerPlan, openerPlan } from "../../lib/open-browser.ts";
10
12
 
11
- export function openBrowser(url: string): boolean {
12
- const [command, args] =
13
- process.platform === "darwin"
14
- ? (["open", [url]] as const)
15
- : process.platform === "win32"
16
- ? (["cmd", ["/c", "start", "", url]] as const)
17
- : (["xdg-open", [url]] as const);
13
+ export type BrowserPlan = OpenerPlan | { refused: string };
14
+
15
+ const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
16
+
17
+ /** Why a URL must not be opened, or undefined when it may. */
18
+ export function refuseAuthorizationUrl(url: string): string | undefined {
19
+ let parsed: URL;
18
20
  try {
19
- const child = spawn(command, [...args], { stdio: "ignore", detached: true });
20
- child.on("error", () => {});
21
- child.unref();
22
- return true;
21
+ parsed = new URL(url);
23
22
  } catch {
24
- return false;
23
+ return "it is not a valid URL";
25
24
  }
25
+ if (parsed.protocol === "https:") return undefined;
26
+ if (parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname)) return undefined;
27
+ return `its scheme is ${parsed.protocol} on ${parsed.hostname || "no host"}; a sign-in URL must be https (or http on a loopback host)`;
28
+ }
29
+
30
+ /** The command and argv that open `url` on `platform`, or why it is refused. Pure. */
31
+ export function browserOpenPlan(url: string, platform: NodeJS.Platform = process.platform, env: Record<string, string | undefined> = process.env): BrowserPlan {
32
+ const refused = refuseAuthorizationUrl(url);
33
+ if (refused) return { refused };
34
+ return openerPlan(url, platform, env);
35
+ }
36
+
37
+ /**
38
+ * Open `url`, or throw (synchronously) when it is refused: a sign-in the
39
+ * server pointed at a non-web scheme is a hostile or broken server, and the
40
+ * error names why. Resolves false when the opener could not start (no browser
41
+ * on the box).
42
+ */
43
+ export function openBrowser(url: string): Promise<boolean> {
44
+ const plan = browserOpenPlan(url);
45
+ if ("refused" in plan) throw new Error(`refusing to open the authorization URL: ${plan.refused}`);
46
+ return launchOpener(plan);
26
47
  }