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
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Anthropic cache placement for standalone side calls. pi-ai marks the final
3
+ * user question, which means every different question writes a fresh cache
4
+ * entry. Move that marker onto the preceding conversation message instead,
5
+ * without changing any message text. Call this from an `anthropic-messages`
6
+ * payload hook, or an `openai-completions` one: Chat Completions carries the
7
+ * same `cache_control` markers when the model takes Anthropic's format
8
+ * (OpenRouter's `anthropic/*`), and has none to move otherwise.
9
+ */
10
+
11
+ /** The APIs whose standalone side-call payload this placement applies to. */
12
+ export const SIDE_CALL_CACHE_APIS: ReadonlySet<string> = new Set(["anthropic-messages", "openai-completions"]);
13
+
14
+ /** Anthropic permits at most four cache-control markers per request. */
15
+ export const MAX_SIDE_CALL_CACHE_MARKERS = 4;
16
+
17
+ type WireRecord = Record<string, unknown>;
18
+
19
+ /**
20
+ * Move pi-ai's final-question cache marker to the last preceding message and
21
+ * remove the earliest markers past Anthropic's four-marker limit. Unsupported
22
+ * payload shapes are left unchanged rather than being normalised.
23
+ */
24
+ export function cacheSideCallConversation(payload: unknown): void {
25
+ if (!isRecord(payload) || !Array.isArray(payload.messages)) return;
26
+ const messages = payload.messages;
27
+ const questionIndex = messages.length - 1;
28
+ const question = asMessage(messages[questionIndex]);
29
+ if (!question || question.role !== "user") return;
30
+ const questionBlocks = blocks(question.content);
31
+ const sourceIndex = questionBlocks.findLastIndex((block) => block.cache_control !== undefined);
32
+ if (sourceIndex === -1) return;
33
+
34
+ const target = precedingCacheableBlock(messages, questionIndex);
35
+ if (!target) return;
36
+ const marker = questionBlocks[sourceIndex].cache_control;
37
+ const nextQuestionBlocks = [...questionBlocks];
38
+ const { cache_control: _, ...unmarked } = questionBlocks[sourceIndex];
39
+ nextQuestionBlocks[sourceIndex] = unmarked;
40
+ const nextTargetBlocks = [...target.blocks];
41
+ nextTargetBlocks[target.blockIndex] = { ...target.blocks[target.blockIndex], cache_control: marker };
42
+ const nextMessages = [...messages];
43
+ nextMessages[questionIndex] = { ...question, content: nextQuestionBlocks };
44
+ nextMessages[target.messageIndex] = { ...target.message, content: nextTargetBlocks };
45
+ payload.messages = nextMessages;
46
+ capMarkers(payload);
47
+ }
48
+
49
+ interface Target {
50
+ messageIndex: number;
51
+ message: WireRecord;
52
+ blocks: WireRecord[];
53
+ blockIndex: number;
54
+ }
55
+
56
+ /** The last prior wire block Anthropic accepts as a message cache breakpoint. */
57
+ function precedingCacheableBlock(messages: unknown[], questionIndex: number): Target | undefined {
58
+ for (let messageIndex = questionIndex - 1; messageIndex >= 0; messageIndex--) {
59
+ const message = asMessage(messages[messageIndex]);
60
+ if (!message) continue;
61
+ // pi-ai keeps earlier user text as a string, only converting the final
62
+ // marked message to blocks. Convert this target exactly as pi-ai does so
63
+ // the newest conversation turn, rather than an older block message, gets
64
+ // the cache boundary. The text itself remains byte-for-byte unchanged.
65
+ // Chat Completions also sends assistant and tool text as strings; an
66
+ // empty one (a tool-call-only reply) cannot carry a marker, as in pi-ai.
67
+ if (typeof message.content === "string") {
68
+ if (message.content.length === 0) continue;
69
+ return { messageIndex, message, blocks: [{ type: "text", text: message.content }], blockIndex: 0 };
70
+ }
71
+ const content = blocks(message.content);
72
+ const blockIndex = content.findLastIndex(isCacheableBlock);
73
+ if (blockIndex !== -1) return { messageIndex, message, blocks: content, blockIndex };
74
+ }
75
+ return undefined;
76
+ }
77
+
78
+ function asMessage(value: unknown): WireRecord | undefined {
79
+ return isRecord(value) && typeof value.role === "string" ? value : undefined;
80
+ }
81
+
82
+ function blocks(value: unknown): WireRecord[] {
83
+ return Array.isArray(value) && value.every(isRecord) ? value : [];
84
+ }
85
+
86
+ /** This is pi-ai's own list from its Anthropic message converter. */
87
+ function isCacheableBlock(block: WireRecord): boolean {
88
+ return (
89
+ block.type === "text" ||
90
+ block.type === "image" ||
91
+ block.type === "tool_result" ||
92
+ block.type === "tool_addition" ||
93
+ block.type === "tool_removal"
94
+ );
95
+ }
96
+
97
+ /** Drop the earliest actual Anthropic markers in wire order: tools, system, messages. */
98
+ function capMarkers(payload: WireRecord): void {
99
+ const markers = [
100
+ ...topLevelMarkers(payload.tools),
101
+ ...topLevelMarkers(payload.system),
102
+ ...messageMarkers(payload.messages),
103
+ ];
104
+ for (const marker of markers.slice(0, Math.max(0, markers.length - MAX_SIDE_CALL_CACHE_MARKERS))) delete marker.cache_control;
105
+ }
106
+
107
+ /** Tool definitions and top-level system blocks can each carry one marker. */
108
+ function topLevelMarkers(value: unknown): WireRecord[] {
109
+ return Array.isArray(value) ? value.filter((entry): entry is WireRecord => isRecord(entry) && entry.cache_control !== undefined) : [];
110
+ }
111
+
112
+ /** Only a message content block is an Anthropic conversation marker. Never inspect block payloads. */
113
+ function messageMarkers(value: unknown): WireRecord[] {
114
+ if (!Array.isArray(value)) return [];
115
+ return value.flatMap((message) => (isRecord(message) ? blocks(message.content).filter((block) => block.cache_control !== undefined) : []));
116
+ }
117
+
118
+ function isRecord(value: unknown): value is WireRecord {
119
+ return typeof value === "object" && value !== null && !Array.isArray(value);
120
+ }
121
+
122
+
123
+ /** The conversation cache placement a side call passes to `runSideCall` (`side-call-run.ts`). */
124
+ export const CONVERSATION_CACHE_PLACEMENT = { apis: SIDE_CALL_CACHE_APIS, place: cacheSideCallConversation } as const;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * One standalone side call: a single `completeSimple` request outside the main
3
+ * agent loop, shared by `/btw`, the recap and the web_fetch reader. The
4
+ * auto-mode classifier has its own transport and stays out of this.
5
+ *
6
+ * Every side call takes the same steps: splice the provider's base URL from the
7
+ * resolved auth into the model, combine the caller's abort signal with a
8
+ * per-attempt timeout, key the request to the session (`<session id>:<kind>`,
9
+ * stable across calls, so the provider's prompt cache is reused), apply the
10
+ * caller's cache placement on the APIs it covers, log the attempt
11
+ * (`CC_SIDE_CALL_LOG`), retry with thinking on when the model cannot disable
12
+ * it, and report usage for the footer cost. The request on the wire is exactly
13
+ * what each extension built inline before (`side-call-run-wire.test.ts`).
14
+ *
15
+ * Kept apart from `side-call.ts`, which stays free of runtime pi imports for
16
+ * the pure prompt modules that use it.
17
+ */
18
+
19
+ import { type Api, type AssistantMessage, type Context, cleanupSessionResources, type Model, type SimpleStreamOptions, type ThinkingLevel } from "@earendil-works/pi-ai";
20
+ import { completeSimple } from "@earendil-works/pi-ai/compat";
21
+ import { withReasoningFallback } from "./model-policy.ts";
22
+ import { SIDE_CALL_KINDS, type SideCallLogContext, logSideCallUsage } from "./side-call-usage.ts";
23
+
24
+ /** A payload hook that places cache breakpoints, and the APIs whose payload shape it handles. */
25
+ export interface SideCallCachePlacement {
26
+ apis: ReadonlySet<string>;
27
+ place: (payload: unknown) => void;
28
+ }
29
+
30
+ export interface SideCallRun {
31
+ /** Names the call in the cache key and the side-call log. */
32
+ kind: SideCallLogContext["kind"];
33
+ model: Model<Api>;
34
+ /** The model registry's resolved auth for `model` (the caller handles a failed lookup). */
35
+ auth: { apiKey?: string; headers?: SimpleStreamOptions["headers"]; env?: Record<string, string>; baseUrl?: string };
36
+ /** The session's id; the call's cache key is `<sessionId>:<kind>`. */
37
+ sessionId: string;
38
+ context: Context;
39
+ signal?: AbortSignal;
40
+ /** Applies to each attempt on its own, so a reasoning retry gets a full timeout. */
41
+ timeoutMs: number;
42
+ maxTokens: number;
43
+ /** Cache placement for the payload; none leaves pi-ai's own markers. */
44
+ cache?: SideCallCachePlacement;
45
+ /** Per-session memo of a model's required thinking level (`withReasoningFallback`). */
46
+ learnedReasoning?: Map<string, ThinkingLevel>;
47
+ /** Each attempt's usage, for the footer's all-in cost. */
48
+ onUsage: (usage: unknown) => void;
49
+ }
50
+
51
+ type SideCallKind = SideCallLogContext["kind"];
52
+
53
+ /** A side call's cache key, which pi-ai also takes as its `sessionId`. */
54
+ function sideCallKey(sessionId: string, kind: SideCallKind): string {
55
+ return `${sessionId}:${kind}`;
56
+ }
57
+
58
+ /**
59
+ * Close what pi-ai holds open under a session's side-call keys, every kind's
60
+ * (a key never used costs nothing). On openai-codex it keeps one WebSocket per
61
+ * `sessionId` with a 5-minute idle timer holding the process, and pi's own
62
+ * dispose closes only the session's id, so a `pi -p` run waited out the timer
63
+ * after its answer (findings §59). Every extension that runs a side call calls
64
+ * it as the session ends, so a new kind is released with no further change.
65
+ */
66
+ export function releaseSideCalls(sessionId: string): void {
67
+ for (const kind of SIDE_CALL_KINDS) cleanupSessionResources(sideCallKey(sessionId, kind));
68
+ }
69
+
70
+ /** Run the side call and return the final reply; a provider failure is in its `stopReason`, as with `completeSimple`. */
71
+ export async function runSideCall(run: SideCallRun): Promise<AssistantMessage> {
72
+ const { kind, model, auth, context } = run;
73
+ const sessionId = sideCallKey(run.sessionId, kind);
74
+ const target = auth.baseUrl ? ({ ...model, baseUrl: auth.baseUrl } as Model<Api>) : model;
75
+ const onPayload = run.cache?.apis.has(model.api) ? run.cache.place : undefined;
76
+ return withReasoningFallback(
77
+ model,
78
+ async (reasoning) => {
79
+ const timeout = AbortSignal.timeout(run.timeoutMs);
80
+ const reply = await completeSimple(target, context, {
81
+ apiKey: auth.apiKey,
82
+ headers: auth.headers,
83
+ env: auth.env,
84
+ signal: run.signal ? AbortSignal.any([run.signal, timeout]) : timeout,
85
+ maxTokens: run.maxTokens,
86
+ sessionId,
87
+ ...(onPayload ? { onPayload } : {}),
88
+ ...(reasoning ? { reasoning } : {}),
89
+ });
90
+ logSideCallUsage({ kind, model, sessionId, system: context.systemPrompt ?? "", messages: context.messages }, reply);
91
+ return reply;
92
+ },
93
+ run.learnedReasoning,
94
+ run.onUsage,
95
+ );
96
+ }
@@ -0,0 +1,38 @@
1
+ /** Opt-in side-call cache evidence, independent of the provider's transport. */
2
+ import { appendFileSync } from "node:fs";
3
+
4
+ /** Every side-call kind; releaseSideCalls closes each one's pi-ai key. */
5
+ export const SIDE_CALL_KINDS = ["reader", "btw", "recap"] as const;
6
+
7
+ export interface SideCallLogContext {
8
+ kind: (typeof SIDE_CALL_KINDS)[number];
9
+ model: { provider: string; id: string; api: string };
10
+ sessionId: string;
11
+ system: string;
12
+ messages: readonly unknown[];
13
+ }
14
+
15
+ /**
16
+ * CC_SIDE_CALL_LOG names a JSONL file containing prompt text and normalized
17
+ * usage, never credentials or response text. Like CC_AUTO_MODE_LOG, this is
18
+ * explicitly opt-in and never read back. Log every completed provider attempt,
19
+ * including reasoning retries; a probe must not mistake an error for a hit.
20
+ */
21
+ export function logSideCallUsage(call: SideCallLogContext, reply: { usage?: unknown; stopReason: string }): void {
22
+ const path = process.env.CC_SIDE_CALL_LOG;
23
+ if (!path) return;
24
+ try {
25
+ appendFileSync(path, `${JSON.stringify({
26
+ kind: call.kind,
27
+ model: `${call.model.provider}/${call.model.id}`,
28
+ api: call.model.api,
29
+ sessionId: call.sessionId,
30
+ system: call.system,
31
+ messages: call.messages,
32
+ usage: reply.usage,
33
+ stopReason: reply.stopReason,
34
+ })}\n`);
35
+ } catch {
36
+ // A diagnostic log that cannot be written must not fail a side call.
37
+ }
38
+ }
@@ -40,6 +40,18 @@ export function trimToTurnBoundary<T extends { role: string }>(messages: readonl
40
40
  return start === -1 ? [] : messages.slice(start);
41
41
  }
42
42
 
43
+ /**
44
+ * The conversation without pi's `system` transcript messages, for a capture of
45
+ * the `context` event. pi 0.86 hands `context` handlers a leading system message
46
+ * holding pi's own default prompt sections and every full tool schema; a side
47
+ * call that forwarded it had pi-ai replay those into its system prompt and tools
48
+ * (over the stubs), and it shifted every role-by-index comparison. pi 0.87 strips
49
+ * system messages before `context` handlers run, so there this is a no-op.
50
+ */
51
+ export function withoutSystemMessages<T extends { role: string }>(messages: readonly T[]): T[] {
52
+ return messages.filter((message) => message.role !== "system");
53
+ }
54
+
43
55
  /** The placeholder left in a message whose only content was an image we removed. */
44
56
  export const IMAGE_OMITTED_TEXT = "[image omitted]";
45
57
 
@@ -55,7 +67,7 @@ export const IMAGE_OMITTED_TEXT = "[image omitted]";
55
67
  * placeholder text block, so it stays a valid turn on strict providers. Purely
56
68
  * structural (any `{ role, content }` message) to stay free of runtime pi
57
69
  * imports; assistant messages carry no images and pass through untouched.
58
- * See `docs/decisions/model-policy.md`.
70
+ * See `working-docs/decisions/model-policy.md`.
59
71
  */
60
72
  export function stripImageBlocks<M extends { role: string; content: unknown }>(messages: readonly M[]): M[] {
61
73
  return messages.map((message) => {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * One in-flight run per key (pure): a call while a run for its key is pending
3
+ * shares that run's promise instead of starting another; once it settles, the
4
+ * next call starts fresh. Used where two overlapping callers would otherwise
5
+ * both pass an "is it there yet?" check across an await and duplicate work
6
+ * that must happen once (a language server spawn, a consent dialog).
7
+ */
8
+ export function singleFlight<T>(): (key: string, run: () => Promise<T>) => Promise<T> {
9
+ const pending = new Map<string, Promise<T>>();
10
+ return (key, run) => {
11
+ let current = pending.get(key);
12
+ if (!current) {
13
+ current = run().finally(() => pending.delete(key));
14
+ pending.set(key, current);
15
+ }
16
+ return current;
17
+ };
18
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Skills whose body is generated, not read from their SKILL.md: Claude Code's
3
+ * `/loop` builds a different prompt per argument shape (findings §21). The
4
+ * skill extension emits a `SkillBodyQuery` on SKILL_BODY_CHANNEL before it
5
+ * delivers a skill; the extension that owns the skill fills `body`
6
+ * synchronously, and the SKILL.md body is the fallback.
7
+ *
8
+ * A fired scheduled prompt that is a slash command (`/loop check the deploy`,
9
+ * `/babysit-prs`) runs the way Claude Code runs it, as if typed: the
10
+ * background extension emits a `SlashExpandQuery` on SLASH_EXPAND_CHANNEL
11
+ * and the skill extension fills `expanded` with the skill's block.
12
+ *
13
+ * Pure: no pi imports.
14
+ */
15
+
16
+ export const SKILL_BODY_CHANNEL = "one-code:skill-body";
17
+ export const SLASH_EXPAND_CHANNEL = "one-code:slash-expand";
18
+
19
+ export interface SkillBodyQuery {
20
+ /** The skill's name. */
21
+ skill: string;
22
+ /** Its SKILL.md, so a provider answers only for its own file. */
23
+ path: string;
24
+ args: string;
25
+ cwd: string;
26
+ /** Set by the provider; the arguments are then part of it. */
27
+ body?: string;
28
+ }
29
+
30
+ export interface SlashExpandQuery {
31
+ /** The fired prompt, starting with `/`. */
32
+ text: string;
33
+ cwd: string;
34
+ /** Set by the skill extension when the command is a skill. */
35
+ expanded?: string;
36
+ }
37
+
38
+ /** `/name rest` split, or undefined when `text` is not a slash command. */
39
+ export function parseSlashCommand(text: string): { name: string; args: string } | undefined {
40
+ const match = /^\/([^\s/][^\s]*)(?:\s+([\s\S]*))?$/.exec(text.trim());
41
+ return match ? { name: match[1], args: (match[2] ?? "").trim() } : undefined;
42
+ }
@@ -3,22 +3,26 @@
3
3
  * command registration (pure fs). (The /plugins Installed tab builds its rows
4
4
  * from discoverPlugins directly and no longer calls this.)
5
5
  *
6
- * Scans the SAME directories claude-compat feeds to pi, in pi's order —
7
- * `~/.claude/skills`, `~/.agents/skills`, `<cwd>/.claude/skills`,
8
- * `<cwd>/.agents/skills`, `<agentDir>/skills`, then the bundled catalog last
9
- * (so it loses a name collision, as it does in pi) — and merges the plugin
10
- * skills the caller resolved. Rows carry the SKILL.md path (for the ~token
6
+ * Scans the SAME directories pi loads, in pi's order — `<agentDir>/skills`
7
+ * (pi's own), then the ones claude-compat feeds it: `~/.claude/skills`,
8
+ * `~/.agents/skills`, `<cwd>/.claude/skills`, `<cwd>/.agents/skills`, and the
9
+ * bundled catalog last — and merges the plugin skills the caller resolved.
10
+ * As in pi, a name loads once (the first wins, so the bundled catalog loses a
11
+ * collision) and a file reached twice through a link counts once, so the
12
+ * banner, `/skills` and `/doctor` count the skills pi actually loads. Rows carry the SKILL.md path (for the ~token
11
13
  * size estimate) and the scope used by skill-overrides keys. Before the first
12
14
  * turn this scan is the only skill list One Code has (pi resolves skills per
13
15
  * turn), so a root missing here is a skill with no `/name` command at startup.
14
16
  */
15
17
 
16
- import { existsSync, readdirSync, statSync } from "node:fs";
18
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
17
19
  import { dirname, join } from "node:path";
18
20
  import { fileURLToPath } from "node:url";
19
21
  import type { PluginSkill } from "./plugins.ts";
20
22
  import type { SkillScope } from "./skill-overrides.ts";
21
- import { claudeUserDir } from "./paths.ts";
23
+ import { claudeSourcesOn, projectConfigDir, userConfigDir } from "./config-mode.ts";
24
+ import { claudeUserDir, isPathAtOrUnder, tryRealpath } from "./paths.ts";
25
+ import { parseFrontmatterLoosely } from "./frontmatter.ts";
22
26
 
23
27
  /** The skill catalog shipped in this package: `<package>/skills` (Claude Code's self-contained built-in skills). */
24
28
  export const BUNDLED_SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
@@ -31,16 +35,38 @@ export interface ScannedSkill {
31
35
 
32
36
  /**
33
37
  * The one scope-classification rule for a skill path — user skills live under
34
- * `~/.claude/skills` AND `<agentDir>/skills`; everything else is project
38
+ * `~/.claude/skills`, `~/.agents/skills` and `<agentDir>/skills`; everything else is project
35
39
  * scope. Both the scanner and the skill extension classify through this, so a
36
40
  * skill never shows different scopes in /plugins vs the skill tool.
37
41
  */
38
42
  export function scopeForPath(path: string, home: string, agentDir: string): SkillScope {
39
- if (path.startsWith(join(claudeUserDir(home), "skills")) || path.startsWith(join(agentDir, "skills"))) return "user";
40
- return "project";
43
+ const roots = [join(claudeUserDir(home), "skills"), join(home, ".agents", "skills"), join(agentDir, "skills")];
44
+ return roots.some((root) => isPathAtOrUnder(path, root)) ? "user" : "project";
41
45
  }
42
46
 
43
- function scanDir(dir: string, scope: SkillScope, into: Map<string, ScannedSkill>): void {
47
+ /**
48
+ * The skill folders One Code adds to pi's own (`<agentDir>/skills`, which pi
49
+ * scans itself), in load order: Claude Code's two only in Claude-compatible
50
+ * mode (lib/config-mode.ts), the cross-tool `.agents` pair in both. `claudeDir`
51
+ * defaults to `~/.claude` (`CLAUDE_CONFIG_DIR`); `~/.agents` is never moved.
52
+ */
53
+ export function skillSourceDirs(cwd: string, home: string, claudeDir: string = claudeUserDir(home)): Array<{ dir: string; scope: SkillScope }> {
54
+ const claude = claudeSourcesOn();
55
+ const dirs: Array<{ dir: string; scope: SkillScope } | false> = [
56
+ claude && { dir: join(claudeDir, "skills"), scope: "user" },
57
+ { dir: join(home, ".agents", "skills"), scope: "user" },
58
+ claude && { dir: join(cwd, ".claude", "skills"), scope: "project" },
59
+ { dir: join(cwd, ".agents", "skills"), scope: "project" },
60
+ ];
61
+ return dirs.filter((d): d is { dir: string; scope: SkillScope } => d !== false);
62
+ }
63
+
64
+ /** The prompt-template folders: `~/.claude/commands` and `.claude/commands`, or their `.onecode` twins in independent mode. */
65
+ export function commandDirs(cwd: string, home: string, claudeDir: string = claudeUserDir(home)): string[] {
66
+ return [join(claudeSourcesOn() ? claudeDir : userConfigDir(home), "commands"), join(projectConfigDir(cwd), "commands")];
67
+ }
68
+
69
+ function scanDir(dir: string, scope: SkillScope, into: Map<string, ScannedSkill>, seenFiles: Set<string>): void {
44
70
  if (!existsSync(dir)) return;
45
71
  let entries: string[];
46
72
  try {
@@ -51,9 +77,18 @@ function scanDir(dir: string, scope: SkillScope, into: Map<string, ScannedSkill>
51
77
  for (const entry of entries) {
52
78
  const skillFile = join(dir, entry, "SKILL.md");
53
79
  // existsSync follows symlinked skill directories, which isDirectory() would miss.
54
- if (existsSync(skillFile) && !into.has(`${scope}:${entry}`)) {
55
- into.set(`${scope}:${entry}`, { name: entry, path: skillFile, scope });
80
+ if (!existsSync(skillFile)) continue;
81
+ let name = entry;
82
+ try {
83
+ const declared = parseFrontmatterLoosely(readFileSync(skillFile, "utf-8")).frontmatter.name;
84
+ if (typeof declared === "string" && declared) name = declared;
85
+ } catch {
86
+ continue;
56
87
  }
88
+ const real = tryRealpath(skillFile) ?? skillFile;
89
+ if (into.has(name) || seenFiles.has(real)) continue;
90
+ seenFiles.add(real);
91
+ into.set(name, { name, path: skillFile, scope });
57
92
  }
58
93
  }
59
94
 
@@ -65,12 +100,10 @@ export function scanSkills(
65
100
  bundledSkillsDir?: string,
66
101
  ): ScannedSkill[] {
67
102
  const skills = new Map<string, ScannedSkill>();
68
- scanDir(join(claudeUserDir(home), "skills"), "user", skills);
69
- scanDir(join(home, ".agents", "skills"), "user", skills);
70
- scanDir(join(cwd, ".claude", "skills"), "project", skills);
71
- scanDir(join(cwd, ".agents", "skills"), "project", skills);
72
- scanDir(join(agentDir, "skills"), "user", skills);
73
- if (bundledSkillsDir) scanDir(bundledSkillsDir, scopeForPath(bundledSkillsDir, home, agentDir), skills);
103
+ const seenFiles = new Set<string>();
104
+ scanDir(join(agentDir, "skills"), "user", skills, seenFiles);
105
+ for (const { dir, scope } of skillSourceDirs(cwd, home)) scanDir(dir, scope, skills, seenFiles);
106
+ if (bundledSkillsDir) scanDir(bundledSkillsDir, scopeForPath(bundledSkillsDir, home, agentDir), skills, seenFiles);
74
107
  for (const skill of pluginSkills) {
75
108
  skills.set(`plugin:${skill.name}`, { name: skill.name, path: skill.path, scope: "plugin" });
76
109
  }
@@ -80,14 +113,20 @@ export function scanSkills(
80
113
  /**
81
114
  * Names of the `.claude/commands` prompt templates (`<name>.md`) pi will expose
82
115
  * as `/<name>` — the same dirs claude-compat feeds pi (`~/.claude/commands`,
83
- * `<cwd>/.claude/commands`) plus pi's own `<agentDir>/prompts`. pi resolves
116
+ * `<cwd>/.claude/commands`, or their `.onecode` twins in independent mode)
117
+ * plus pi's own `<agentDir>/prompts`. pi resolves
84
118
  * templates per turn, after `session_start`, so a bare skill command registered
85
119
  * then would silently shadow a same-named template for the whole session;
86
120
  * this pre-scan lets the registration skip those names.
87
121
  */
88
122
  export function promptTemplateNames(cwd: string, home: string, agentDir: string): string[] {
89
- const names = new Set<string>();
90
- for (const dir of [join(claudeUserDir(home), "commands"), join(cwd, ".claude", "commands"), join(agentDir, "prompts")]) {
123
+ return [...new Set(promptTemplateFiles(cwd, home, agentDir).map((file) => file.name))];
124
+ }
125
+
126
+ /** Every prompt-template file pi will resolve, with the command name each gives. */
127
+ export function promptTemplateFiles(cwd: string, home: string, agentDir: string): { name: string; path: string }[] {
128
+ const files: { name: string; path: string }[] = [];
129
+ for (const dir of [...commandDirs(cwd, home), join(agentDir, "prompts")]) {
91
130
  if (!existsSync(dir)) continue;
92
131
  let entries: string[];
93
132
  try {
@@ -95,9 +134,9 @@ export function promptTemplateNames(cwd: string, home: string, agentDir: string)
95
134
  } catch {
96
135
  continue;
97
136
  }
98
- for (const entry of entries) if (entry.endsWith(".md")) names.add(entry.slice(0, -3));
137
+ for (const entry of entries) if (entry.endsWith(".md")) files.push({ name: entry.slice(0, -3), path: join(dir, entry) });
99
138
  }
100
- return [...names];
139
+ return files;
101
140
  }
102
141
 
103
142
  /** Rough token estimate from the SKILL.md byte size (~4 bytes/token). */
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Keep pi's foreground shell spill file readable.
3
+ *
4
+ * When a foreground `bash` or `powershell` run prints more than pi keeps, pi
5
+ * writes the whole output to `<os.tmpdir()>/pi-<shell>-<16 hex>.log` and ends
6
+ * the text with `[… Full output: <path>]`, on success and on a failure: a
7
+ * non-zero exit is an error result from pi 0.99 on and a thrown error before
8
+ * it, and a timeout or an abort is always thrown. `os.tmpdir()` is outside
9
+ * every root the permission gate treats as readable, so the "full output is
10
+ * one read away" promise held only where outside reads skip the gate. So the
11
+ * file is moved into the session's persisted-results dir (a readable root)
12
+ * and the path in the text is rewritten to match.
13
+ *
14
+ * Only a file pi produced is moved: the path must sit directly in the temp
15
+ * dir and carry pi's name shape, so a command that prints a look-alike line
16
+ * cannot make the harness move some other file.
17
+ *
18
+ * The output is also cut Claude Code's way instead of pi's. pi
19
+ * keeps the last 2,000 lines or 50 KB and drops the head, so a batched
20
+ * command lost its first commands' output. Claude Code keeps the head: the
21
+ * whole output up to its inline cap, past it the persisted block naming the
22
+ * file, with the first 2 KB as the preview. pi's spill file holds the whole
23
+ * output, so the block names it and nothing is copied.
24
+ */
25
+
26
+ import { copyFileSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync } from "node:fs";
27
+ import { tmpdir } from "node:os";
28
+ import { basename, dirname, join, resolve } from "node:path";
29
+ import { persistedFilePreview, persistIfLarge } from "./persisted-output.ts";
30
+
31
+ const MARKER = "Full output: ";
32
+ const PI_SPILL_NAME = /^pi-[a-z]+-[0-9a-f]{16}\.log$/;
33
+ /** The note pi ends a cut output with: `\n\n[Showing lines …` or `\n\n[Showing last …`. */
34
+ const PI_NOTE = "\n\n[Showing ";
35
+ /** pi's status after the output of a run that did not exit 0. */
36
+ const PI_STATUS = /(?:^|\n\n)Command (?:exited with code -?\d+|timed out after \S+ seconds|aborted|terminated without an exit code)$/;
37
+
38
+ /** How a shell's output is cut: whole up to `inlineMax` bytes, past it persisted as `<id>.txt` when pi did not spill it. */
39
+ export interface OutputBound {
40
+ inlineMax: number;
41
+ id: string;
42
+ }
43
+
44
+ /** The spill path pi named at the end of `text`, when it is one pi produced in `tmp`. Pure. */
45
+ export function piSpillPath(text: string, tmp: string = tmpdir()): string | undefined {
46
+ const at = text.lastIndexOf(MARKER);
47
+ if (at === -1) return undefined;
48
+ const close = text.indexOf("]", at + MARKER.length);
49
+ if (close === -1) return undefined;
50
+ const path = text.slice(at + MARKER.length, close);
51
+ if (!PI_SPILL_NAME.test(basename(path))) return undefined;
52
+ if (resolve(dirname(path)) !== resolve(tmp)) return undefined;
53
+ return path;
54
+ }
55
+
56
+ /** Move `from` into `dir`, keeping its name; the new path, or undefined when it could not move. */
57
+ function moveInto(from: string, dir: string): string | undefined {
58
+ const to = join(dir, basename(from));
59
+ try {
60
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
61
+ try {
62
+ renameSync(from, to);
63
+ } catch {
64
+ // A different filesystem: copy, then drop the original.
65
+ copyFileSync(from, to);
66
+ unlinkSync(from);
67
+ }
68
+ return to;
69
+ } catch {
70
+ return undefined;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * `text` with its spill file moved and pi's tail cut replaced by Claude
76
+ * Code's head-first one. A spill file that cannot be moved keeps pi's text;
77
+ * one that cannot be read, or a text not in pi's shape, keeps pi's text with
78
+ * only the path rewritten.
79
+ */
80
+ export function boundSpillIn(text: string, resultsDir: string, bound: OutputBound, failed = true, tmp: string = tmpdir()): string {
81
+ // pi appends a status only to a run that did not exit 0; a successful
82
+ // command's own last line can look like one.
83
+ const status = failed ? PI_STATUS.exec(text) : null;
84
+ const end = status ? status.index : text.length;
85
+ const output = text.slice(0, end);
86
+ const from = piSpillPath(output, tmp);
87
+ if (!from) return persistIfLarge(output, { dir: resultsDir, id: bound.id, maxBytes: bound.inlineMax }) + text.slice(end);
88
+ const to = moveInto(from, resultsDir);
89
+ if (!to) return text;
90
+ // pi's note ends the output; anything else keeps pi's text.
91
+ const whole = output.includes(PI_NOTE) && output.endsWith(`${from}]`) ? wholeOutput(to, bound.inlineMax) : undefined;
92
+ return whole === undefined ? text.split(from).join(to) : whole + text.slice(end);
93
+ }
94
+
95
+ /** The spill file inline while it fits `inlineMax`, past it the persisted block; undefined when unreadable. */
96
+ function wholeOutput(path: string, inlineMax: number): string | undefined {
97
+ try {
98
+ return statSync(path).size <= inlineMax ? readFileSync(path, "utf8") : persistedFilePreview(path);
99
+ } catch {
100
+ return undefined;
101
+ }
102
+ }
103
+
104
+ type TextResult = { content: Array<{ type: string; text?: string }>; details?: unknown };
105
+
106
+ /**
107
+ * Run a foreground shell execute and keep its spill file readable: on a
108
+ * returned result the text and `details.fullOutputPath` are rewritten, and a
109
+ * thrown error is rethrown with its message rewritten, the output cut
110
+ * Claude Code's way (`bound`).
111
+ */
112
+ export async function keepSpillReadable<R extends TextResult>(run: () => Promise<R>, resultsDir: string, bound: OutputBound): Promise<R> {
113
+ const rewrite = (text: string, failed: boolean) => boundSpillIn(text, resultsDir, bound, failed);
114
+ let result: R;
115
+ try {
116
+ result = await run();
117
+ } catch (error) {
118
+ if (error instanceof Error) {
119
+ const message = rewrite(error.message, true);
120
+ if (message !== error.message) {
121
+ const moved = new Error(message);
122
+ moved.stack = error.stack;
123
+ throw moved;
124
+ }
125
+ }
126
+ throw error;
127
+ }
128
+ const details = result.details as { fullOutputPath?: unknown } | undefined;
129
+ const text = result.content.find((block) => block.type === "text" && typeof block.text === "string");
130
+ if (!text?.text) return result;
131
+ const rewritten = rewrite(text.text, (result as { isError?: boolean }).isError === true);
132
+ if (rewritten === text.text) return result;
133
+ const from = piSpillPath(text.text);
134
+ const to = from ? join(resultsDir, basename(from)) : undefined;
135
+ return {
136
+ ...result,
137
+ content: result.content.map((block) => (block === text ? { ...block, text: rewritten } : block)),
138
+ details: details && typeof details === "object" && to ? { ...details, fullOutputPath: to } : result.details,
139
+ };
140
+ }