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,513 @@
1
+ /**
2
+ * The public model catalogs the tier algorithm reads (`model-catalog.ts`):
3
+ * models.dev (release dates, vendor prices, canonical ids, tool support),
4
+ * OpenRouter's model list (Hugging Face repo ids, output modalities) and
5
+ * Hugging Face's safetensors metadata (total parameter counts). None needs a
6
+ * key. Pure: no pi imports.
7
+ *
8
+ * Each source is kept as its own API payload, trimmed to the fields the
9
+ * algorithm reads but in the API's own shape, so the copy bundled with a
10
+ * release (`model-catalog/*.json`, written by `scripts/gen-model-catalog.mjs`)
11
+ * and the copy an interactive session refreshes into the One Code state dir go
12
+ * through the same code. The newer copy of each source wins; parameter counts
13
+ * merge, since a refresh only looks up repos it has not seen.
14
+ *
15
+ * A refresh runs at most once a day, only from a session that outlives its
16
+ * turn, never awaited in `session_start` (the caller's job), and a failure
17
+ * keeps the copy already on disk and waits a day before the next try (a
18
+ * stamp in the cache dir). Each fetch is bounded, body included.
19
+ * `refreshModelCatalog: false` in
20
+ * `~/.onecode/settings.json` or pi's `PI_OFFLINE` turns it off; the copy on
21
+ * disk (or the bundled one) then decides.
22
+ * working-docs/decisions/model-tiers.md.
23
+ */
24
+
25
+ import { closeSync, openSync, readSync, rmSync, statSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+ import { readJsonFile, writeJsonAtomic } from "./atomic-write.ts";
29
+ import { fetchWithTimeout } from "./fetch-timeout.ts";
30
+
31
+ export const MODELS_DEV_URL = "https://models.dev/api.json";
32
+ export const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
33
+ export const HUGGING_FACE_MODELS_URL = "https://huggingface.co/api/models/";
34
+
35
+ export const CATALOG_REFRESH_AFTER_MS = 24 * 60 * 60 * 1000;
36
+ export const CATALOG_FETCH_TIMEOUT_MS = 30_000;
37
+ /** Hugging Face lookups per refresh: only repos new since the last one, so normally a handful. */
38
+ export const HUGGING_FACE_LOOKUPS_PER_REFRESH = 25;
39
+
40
+ /** pi providers models.dev lists under another name. */
41
+ export const MODELS_DEV_PROVIDER: Readonly<Record<string, string>> = { "vercel-ai-gateway": "vercel" };
42
+
43
+ /** A models.dev model row, trimmed to what the tier algorithm reads. */
44
+ export interface ModelsDevModel {
45
+ release_date?: string;
46
+ /** models.dev's lifecycle marker; only `deprecated` is acted on. */
47
+ status?: string;
48
+ tool_call?: boolean;
49
+ /** USD per million tokens. */
50
+ cost?: { input?: number; output?: number };
51
+ /** The vendor's own `provider/id` for the model a hosted or gateway row serves. */
52
+ canonical_model_id?: string;
53
+ /** Kept only when a model outputs something besides text (an image model). */
54
+ modalities?: { output?: string[] };
55
+ }
56
+
57
+ export type ModelsDevPayload = Record<string, { models: Record<string, ModelsDevModel> }>;
58
+
59
+ /** An OpenRouter model row, trimmed. */
60
+ export interface OpenRouterModel {
61
+ id: string;
62
+ /** Unix seconds the row was listed: a release date when models.dev has none. */
63
+ created?: number;
64
+ hugging_face_id?: string;
65
+ /** Kept only as `["tools"]` when present. */
66
+ supported_parameters?: string[];
67
+ architecture?: { output_modalities?: string[] };
68
+ /** USD per token, as strings, OpenRouter's own spelling. */
69
+ pricing?: { prompt?: string; completion?: string };
70
+ }
71
+
72
+ export interface OpenRouterPayload {
73
+ data: OpenRouterModel[];
74
+ }
75
+
76
+ /** Hugging Face repo id → total parameters from its safetensors metadata; null when the repo has none (gated, removed, no safetensors). */
77
+ export type ParameterCounts = Record<string, number | null>;
78
+
79
+ export interface CatalogFile<T> {
80
+ fetchedAt: string;
81
+ source: string;
82
+ payload: T;
83
+ }
84
+
85
+ export interface CatalogSources {
86
+ /** Whether the models.dev copy in use came from a runtime refresh rather than the release. */
87
+ refreshed?: boolean;
88
+ modelsDev: CatalogFile<ModelsDevPayload>;
89
+ openRouter: CatalogFile<OpenRouterPayload>;
90
+ huggingFace: CatalogFile<ParameterCounts>;
91
+ }
92
+
93
+ const FILES = { modelsDev: "models-dev.json", openRouter: "openrouter.json", huggingFace: "huggingface.json" } as const;
94
+ /** The last failed refresh, which holds the next one off for a day (`recentFailure`). */
95
+ const FAILURE_FILE = "refresh-failed.json";
96
+ type SourceName = keyof typeof FILES;
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Trimming (shared by the build script and the runtime refresh)
100
+ // ---------------------------------------------------------------------------
101
+
102
+ const record = (value: unknown): Record<string, unknown> | undefined =>
103
+ value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
104
+ const finite = (value: unknown): number | undefined => (typeof value === "number" && Number.isFinite(value) ? value : undefined);
105
+ const strings = (value: unknown): string[] => (Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === "string") : []);
106
+
107
+ function trimModelsDevRow(raw: Record<string, unknown>): ModelsDevModel {
108
+ const row: ModelsDevModel = {};
109
+ if (typeof raw.release_date === "string") row.release_date = raw.release_date;
110
+ if (typeof raw.status === "string") row.status = raw.status;
111
+ if (typeof raw.tool_call === "boolean") row.tool_call = raw.tool_call;
112
+ const cost = record(raw.cost);
113
+ if (cost && (finite(cost.input) !== undefined || finite(cost.output) !== undefined)) {
114
+ row.cost = {};
115
+ if (finite(cost.input) !== undefined) row.cost.input = finite(cost.input);
116
+ if (finite(cost.output) !== undefined) row.cost.output = finite(cost.output);
117
+ }
118
+ if (typeof raw.canonical_model_id === "string") row.canonical_model_id = raw.canonical_model_id;
119
+ const output = strings(record(raw.modalities)?.output);
120
+ if (output.some((modality) => modality !== "text")) row.modalities = { output };
121
+ return row;
122
+ }
123
+
124
+ /**
125
+ * models.dev's `api.json` reduced to the providers One Code can run on (pi's
126
+ * built-in providers, under models.dev's names) plus OpenRouter and every
127
+ * vendor those rows name as canonical, whole: a vendor's comparison set needs
128
+ * its other models too.
129
+ */
130
+ export function trimModelsDev(body: unknown, piProviders: Iterable<string>): ModelsDevPayload {
131
+ const all = record(body) ?? {};
132
+ const keep = new Set<string>(["openrouter"]);
133
+ for (const provider of piProviders) keep.add(MODELS_DEV_PROVIDER[provider] ?? provider);
134
+ const out: ModelsDevPayload = {};
135
+ const pending = [...keep];
136
+ while (pending.length > 0) {
137
+ const provider = pending.pop()!;
138
+ const models = record(record(all[provider])?.models);
139
+ if (!models || out[provider]) continue;
140
+ const trimmed: Record<string, ModelsDevModel> = {};
141
+ for (const [id, raw] of Object.entries(models)) {
142
+ const row = record(raw);
143
+ if (!row) continue;
144
+ trimmed[id] = trimModelsDevRow(row);
145
+ const vendor = trimmed[id].canonical_model_id?.split("/")[0];
146
+ if (vendor && !keep.has(vendor)) {
147
+ keep.add(vendor);
148
+ pending.push(vendor);
149
+ }
150
+ }
151
+ out[provider] = { models: trimmed };
152
+ }
153
+ return Object.fromEntries(Object.entries(out).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
154
+ }
155
+
156
+ /** OpenRouter's `/api/v1/models` reduced to the fields the algorithm reads. */
157
+ export function trimOpenRouter(body: unknown): OpenRouterPayload {
158
+ const data: OpenRouterModel[] = [];
159
+ for (const raw of Array.isArray(record(body)?.data) ? (record(body)!.data as unknown[]) : []) {
160
+ const row = record(raw);
161
+ if (!row || typeof row.id !== "string") continue;
162
+ const model: OpenRouterModel = { id: row.id };
163
+ if (finite(row.created) !== undefined) model.created = finite(row.created);
164
+ if (typeof row.hugging_face_id === "string" && row.hugging_face_id) model.hugging_face_id = row.hugging_face_id;
165
+ if (strings(row.supported_parameters).includes("tools")) model.supported_parameters = ["tools"];
166
+ const output = strings(record(row.architecture)?.output_modalities);
167
+ if (output.some((modality) => modality !== "text")) model.architecture = { output_modalities: output };
168
+ const pricing = record(row.pricing);
169
+ if (pricing && typeof pricing.prompt === "string" && typeof pricing.completion === "string") {
170
+ model.pricing = { prompt: pricing.prompt, completion: pricing.completion };
171
+ }
172
+ data.push(model);
173
+ }
174
+ data.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
175
+ return { data };
176
+ }
177
+
178
+ /** The total parameter count in a Hugging Face model response, or null. */
179
+ export function parameterCountFromResponse(body: unknown): number | null {
180
+ const total = finite(record(record(body)?.safetensors)?.total);
181
+ return total !== undefined && total > 0 ? total : null;
182
+ }
183
+
184
+ /** The repo ids an OpenRouter payload names that `known` has no entry for. */
185
+ export function unknownRepos(openRouter: OpenRouterPayload, known: ParameterCounts): string[] {
186
+ const repos = new Set<string>();
187
+ for (const row of openRouter.data) if (row.hugging_face_id && !(row.hugging_face_id in known)) repos.add(row.hugging_face_id);
188
+ return [...repos].sort();
189
+ }
190
+
191
+ /** One repo's parameter count; a failed lookup is undefined (retried next refresh), a repo without one is null. */
192
+ export async function fetchParameterCount(repo: string, fetchImpl?: typeof fetch, signal?: AbortSignal): Promise<number | null | undefined> {
193
+ const url = `${HUGGING_FACE_MODELS_URL}${repo.split("/").map(encodeURIComponent).join("/")}?expand%5B%5D=safetensors`;
194
+ try {
195
+ return await fetchWithTimeout(
196
+ url,
197
+ CATALOG_FETCH_TIMEOUT_MS,
198
+ async (response) => {
199
+ if (response.status === 401 || response.status === 403 || response.status === 404) return null;
200
+ if (!response.ok) return undefined;
201
+ return parameterCountFromResponse(await response.json());
202
+ },
203
+ { fetchImpl, signal },
204
+ );
205
+ } catch {
206
+ return undefined;
207
+ }
208
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // Loading: the bundled copy, the refreshed copy, the newer of the two
212
+ // ---------------------------------------------------------------------------
213
+
214
+ export function bundledCatalogDir(): string {
215
+ return fileURLToPath(new URL("./model-catalog/", import.meta.url));
216
+ }
217
+
218
+ export function catalogCacheDir(stateDir: string): string {
219
+ return join(stateDir, "cache", "model-catalog");
220
+ }
221
+
222
+ /**
223
+ * The refreshed catalog files and the failure stamp that holds a refresh off.
224
+ * They decide a model's tier, and the tier decides which auto-mode fast paths
225
+ * skip the classifier, so the safety floor guards them like the settings
226
+ * files (auto-mode/safety-floor.ts).
227
+ */
228
+ export function catalogCacheFiles(stateDir: string): string[] {
229
+ return [...Object.values(FILES), FAILURE_FILE].map((file) => join(catalogCacheDir(stateDir), file));
230
+ }
231
+
232
+ /**
233
+ * A refreshed catalog file under any literal `.onecode` directory, as a
234
+ * forward-slashed path tail: the floor's counterpart of `catalogCacheFiles`,
235
+ * for one in another home or spelled through a symlink the resolver missed.
236
+ */
237
+ export const CATALOG_CACHE_TAIL = new RegExp(`/\\.onecode/cache/model-catalog/(${[...Object.values(FILES), FAILURE_FILE].map((file) => file.replace(/[.]/g, "\\.")).join("|")})$`);
238
+
239
+ function isCatalogFile(value: unknown): value is CatalogFile<unknown> {
240
+ const file = record(value);
241
+ return !!file && typeof file.fetchedAt === "string" && !Number.isNaN(Date.parse(file.fetchedAt)) && record(file.payload) !== undefined;
242
+ }
243
+
244
+ function readCatalogFile<T>(path: string): CatalogFile<T> | undefined {
245
+ // Missing or corrupt: the other copy decides.
246
+ const parsed = readJsonFile<unknown>(path);
247
+ return isCatalogFile(parsed) ? (parsed as CatalogFile<T>) : undefined;
248
+ }
249
+
250
+ const EMPTY: CatalogSources = {
251
+ modelsDev: { fetchedAt: new Date(0).toISOString(), source: MODELS_DEV_URL, payload: {} },
252
+ openRouter: { fetchedAt: new Date(0).toISOString(), source: OPENROUTER_MODELS_URL, payload: { data: [] } },
253
+ huggingFace: { fetchedAt: new Date(0).toISOString(), source: HUGGING_FACE_MODELS_URL, payload: {} },
254
+ };
255
+
256
+ let bundled: CatalogSources | undefined;
257
+ /** The copy bundled with this release, ignoring any refresh. */
258
+ export function bundledSources(): CatalogSources {
259
+ if (bundled) return bundled;
260
+ const dir = bundledCatalogDir();
261
+ bundled = {
262
+ modelsDev: readCatalogFile<ModelsDevPayload>(join(dir, FILES.modelsDev)) ?? EMPTY.modelsDev,
263
+ openRouter: readCatalogFile<OpenRouterPayload>(join(dir, FILES.openRouter)) ?? EMPTY.openRouter,
264
+ huggingFace: readCatalogFile<ParameterCounts>(join(dir, FILES.huggingFace)) ?? EMPTY.huggingFace,
265
+ };
266
+ return bundled;
267
+ }
268
+
269
+ const newer = <T>(a: CatalogFile<T>, b: CatalogFile<T> | undefined): CatalogFile<T> =>
270
+ b && Date.parse(b.fetchedAt) > Date.parse(a.fetchedAt) ? b : a;
271
+
272
+ function combine(base: CatalogSources, cached: Partial<CatalogSources>): CatalogSources {
273
+ const huggingFace = newer(base.huggingFace, cached.huggingFace);
274
+ const modelsDev = newer(base.modelsDev, cached.modelsDev);
275
+ return {
276
+ refreshed: modelsDev !== base.modelsDev,
277
+ modelsDev,
278
+ openRouter: newer(base.openRouter, cached.openRouter),
279
+ // Counts never go stale, and each copy may hold repos the other lacks.
280
+ huggingFace: { ...huggingFace, payload: { ...base.huggingFace.payload, ...cached.huggingFace?.payload } },
281
+ };
282
+ }
283
+
284
+ /** How long a stat result is trusted before the cache files are stat'ed again. */
285
+ const RESTAT_AFTER_MS = 1000;
286
+ let memo: { dir: string; stamp: string; sources: CatalogSources; checkedAt: number } | undefined;
287
+ let pinned: { value: CatalogSources } | undefined;
288
+
289
+ /** Test seam: pin the sources every lookup sees; `undefined` reads the real files again. */
290
+ export function setCatalogSourcesForTest(sources: CatalogSources | undefined): void {
291
+ pinned = sources ? { value: sources } : undefined;
292
+ memo = undefined;
293
+ }
294
+
295
+ /** An empty catalog: every model falls back to the no-catalog rules. */
296
+ export function emptyCatalogSources(): CatalogSources {
297
+ return structuredClone(EMPTY);
298
+ }
299
+
300
+ /** Forget the disk memo so a lifecycle boundary sees another extension instance's refresh. */
301
+ export function invalidateCatalogCache(): void {
302
+ // Re-stat on the next load; unchanged files keep the same sources object, so
303
+ // the index memo (keyed by it) still hits.
304
+ if (memo) memo.checkedAt = 0;
305
+ }
306
+
307
+ function stampOf(dir: string): string {
308
+ return (Object.values(FILES) as string[])
309
+ .map((file) => {
310
+ try {
311
+ return String(statSync(join(dir, file)).mtimeMs);
312
+ } catch {
313
+ return "-";
314
+ }
315
+ })
316
+ .join("|");
317
+ }
318
+
319
+ /**
320
+ * The sources lookups read: for each, the newer of the bundled and the
321
+ * refreshed copy. The same object is returned until a cache file changes, so
322
+ * callers may memoize what they derive from it.
323
+ */
324
+ export function loadCatalogSources(stateDir: string): CatalogSources {
325
+ if (pinned) return pinned.value;
326
+ const dir = catalogCacheDir(stateDir);
327
+ const now = Date.now();
328
+ if (memo && memo.dir === dir && now - memo.checkedAt < RESTAT_AFTER_MS) return memo.sources;
329
+ const stamp = stampOf(dir);
330
+ if (memo && memo.dir === dir && memo.stamp === stamp) {
331
+ memo.checkedAt = now;
332
+ return memo.sources;
333
+ }
334
+ const cached: Partial<CatalogSources> = {
335
+ modelsDev: readCatalogFile<ModelsDevPayload>(join(dir, FILES.modelsDev)),
336
+ openRouter: readCatalogFile<OpenRouterPayload>(join(dir, FILES.openRouter)),
337
+ huggingFace: readCatalogFile<ParameterCounts>(join(dir, FILES.huggingFace)),
338
+ };
339
+ const sources = combine(bundledSources(), cached);
340
+ memo = { dir, stamp, sources, checkedAt: now };
341
+ return sources;
342
+ }
343
+
344
+ const olderThanRefresh = (stamp: string, now: Date): boolean => now.getTime() - Date.parse(stamp) > CATALOG_REFRESH_AFTER_MS;
345
+
346
+ /** The `fetchedAt` stamp at the head of a catalog file, without parsing its payload. */
347
+ function fetchedAtOf(path: string): string | undefined {
348
+ let fd: number | undefined;
349
+ try {
350
+ fd = openSync(path, "r");
351
+ const head = Buffer.alloc(256);
352
+ const read = readSync(fd, head, 0, head.length, 0);
353
+ const stamp = head.subarray(0, read).toString("utf8").match(/^\{\s*"fetchedAt"\s*:\s*"([^"]+)"/)?.[1];
354
+ if (stamp && !Number.isNaN(Date.parse(stamp))) return stamp;
355
+ } catch {
356
+ return undefined;
357
+ } finally {
358
+ if (fd !== undefined) closeSync(fd);
359
+ }
360
+ // Written some other way: the whole file decides.
361
+ return readCatalogFile<unknown>(path)?.fetchedAt;
362
+ }
363
+
364
+ /**
365
+ * When the models.dev copy in use was fetched (the newer of the bundled and
366
+ * the refreshed one, as `loadCatalogSources` picks), read from the files'
367
+ * heads: a session start checks this without parsing the catalogs.
368
+ */
369
+ export function catalogFetchedAt(stateDir: string): string {
370
+ if (pinned) return pinned.value.modelsDev.fetchedAt;
371
+ const stamps = [fetchedAtOf(join(bundledCatalogDir(), FILES.modelsDev)), fetchedAtOf(join(catalogCacheDir(stateDir), FILES.modelsDev))];
372
+ return stamps.reduce<string>((a, b) => (b && Date.parse(b) > Date.parse(a) ? b : a), EMPTY.modelsDev.fetchedAt);
373
+ }
374
+
375
+ /** The last refresh that failed, kept so the next one waits a day. */
376
+ interface RefreshFailure {
377
+ failedAt: string;
378
+ error: string;
379
+ }
380
+
381
+ function recentFailure(stateDir: string, now: Date): RefreshFailure | undefined {
382
+ const failure = readJsonFile<RefreshFailure>(join(catalogCacheDir(stateDir), FAILURE_FILE));
383
+ const failedAt = typeof failure?.failedAt === "string" ? Date.parse(failure.failedAt) : Number.NaN;
384
+ // A stamp later than now was not written by a refresh; it must not hold refreshes off.
385
+ return !Number.isNaN(failedAt) && failedAt <= now.getTime() && !olderThanRefresh(failure!.failedAt, now) ? failure : undefined;
386
+ }
387
+
388
+ /**
389
+ * Whether a refresh should run: the copy in use is over a day old, and no
390
+ * refresh failed within the last day (a failure writes nothing, so without
391
+ * the wait every start would fetch again). Cheap: no catalog is parsed.
392
+ */
393
+ export function catalogRefreshDue(stateDir: string, now: Date = new Date()): boolean {
394
+ return !pinned && olderThanRefresh(catalogFetchedAt(stateDir), now) && !recentFailure(stateDir, now);
395
+ }
396
+
397
+ // ---------------------------------------------------------------------------
398
+ // Refresh
399
+ // ---------------------------------------------------------------------------
400
+
401
+ export type CatalogRefreshOutcome =
402
+ | { status: "fresh" }
403
+ | { status: "refreshed"; models: number; repos: number }
404
+ | { status: "failed"; error: string }
405
+ /** A refresh failed within the last day; the next try waits for the day to pass. */
406
+ | { status: "backing-off"; error: string };
407
+
408
+ const inflight = new Map<string, Promise<CatalogRefreshOutcome>>();
409
+
410
+ /**
411
+ * Fetch both catalogs and any parameter counts not yet known, into the cache,
412
+ * when the copy in use is older than a day and no refresh failed within the
413
+ * last day (`force` skips both checks). Never throws; a failure leaves the
414
+ * cache as it was and is remembered for a day, unless `signal` cancelled it.
415
+ * Every fetch, bodies included, is bounded by `CATALOG_FETCH_TIMEOUT_MS`. One
416
+ * fetch per state dir at a time.
417
+ */
418
+ export async function refreshModelCatalog(options: {
419
+ stateDir: string;
420
+ piProviders: Iterable<string>;
421
+ fetchImpl?: typeof fetch;
422
+ now?: Date;
423
+ force?: boolean;
424
+ signal?: AbortSignal;
425
+ }): Promise<CatalogRefreshOutcome> {
426
+ const { stateDir, now = new Date(), force = false } = options;
427
+ // A pinned catalog (tests) is never replaced from the network.
428
+ if (pinned) return { status: "fresh" };
429
+ if (!force) {
430
+ if (!olderThanRefresh(catalogFetchedAt(stateDir), now)) return { status: "fresh" };
431
+ const failure = recentFailure(stateDir, now);
432
+ if (failure) return { status: "backing-off", error: failure.error };
433
+ }
434
+ const running = inflight.get(stateDir);
435
+ if (running) return running;
436
+ const attempt = fetchCatalog(options, now).finally(() => inflight.delete(stateDir));
437
+ inflight.set(stateDir, attempt);
438
+ return attempt;
439
+ }
440
+
441
+ function fetchJson(url: string, fetchImpl: typeof fetch | undefined, signal: AbortSignal | undefined): Promise<unknown> {
442
+ // models.dev answers a bare client with 403; a browser User-Agent is accepted.
443
+ const init: RequestInit = { headers: { accept: "application/json", "user-agent": "Mozilla/5.0 (compatible; one-code-model-catalog)" } };
444
+ return fetchWithTimeout(
445
+ url,
446
+ CATALOG_FETCH_TIMEOUT_MS,
447
+ async (response) => {
448
+ if (!response.ok) throw new Error(`${new URL(url).host} responded ${response.status}`);
449
+ return (await response.json()) as unknown;
450
+ },
451
+ { init, fetchImpl, signal },
452
+ );
453
+ }
454
+
455
+ async function fetchCatalog(
456
+ options: { stateDir: string; piProviders: Iterable<string>; fetchImpl?: typeof fetch; signal?: AbortSignal },
457
+ now: Date,
458
+ ): Promise<CatalogRefreshOutcome> {
459
+ const { stateDir, piProviders, fetchImpl, signal } = options;
460
+ const dir = catalogCacheDir(stateDir);
461
+ const fail = (error: string): CatalogRefreshOutcome => {
462
+ // Cancelled by the caller (a session ending) is not the network failing.
463
+ if (!signal?.aborted) {
464
+ try {
465
+ writeJsonAtomic(join(dir, FAILURE_FILE), { failedAt: now.toISOString(), error } satisfies RefreshFailure);
466
+ } catch {
467
+ // Unwritable: the next start tries again, as before.
468
+ }
469
+ }
470
+ return { status: "failed", error };
471
+ };
472
+ try {
473
+ const [modelsDevBody, openRouterBody] = await Promise.all([fetchJson(MODELS_DEV_URL, fetchImpl, signal), fetchJson(OPENROUTER_MODELS_URL, fetchImpl, signal)]);
474
+ const modelsDev = trimModelsDev(modelsDevBody, piProviders);
475
+ const openRouter = trimOpenRouter(openRouterBody);
476
+ const rows = Object.values(modelsDev).reduce((sum, provider) => sum + Object.keys(provider.models).length, 0);
477
+ if (rows === 0 || openRouter.data.length === 0) return fail("a model catalog came back empty");
478
+ const fetchedAt = now.toISOString();
479
+ const known = loadCatalogSources(stateDir).huggingFace.payload;
480
+ const counts: ParameterCounts = { ...readCatalogFile<ParameterCounts>(join(dir, FILES.huggingFace))?.payload };
481
+ const lookups = unknownRepos(openRouter, known).slice(0, HUGGING_FACE_LOOKUPS_PER_REFRESH);
482
+ const found = await Promise.all(lookups.map((repo) => fetchParameterCount(repo, fetchImpl, signal)));
483
+ if (signal?.aborted) throw signal.reason;
484
+ lookups.forEach((repo, index) => {
485
+ if (found[index] !== undefined) counts[repo] = found[index]!;
486
+ });
487
+ // models.dev last: its stamp is what marks the copy fresh, so a write
488
+ // that fails before it leaves the copy stale and retried, never a
489
+ // fresh-looking mix of old and new.
490
+ writeJsonAtomic(join(dir, FILES.openRouter), { fetchedAt, source: OPENROUTER_MODELS_URL, payload: openRouter });
491
+ writeJsonAtomic(join(dir, FILES.huggingFace), { fetchedAt, source: HUGGING_FACE_MODELS_URL, payload: counts });
492
+ writeJsonAtomic(join(dir, FILES.modelsDev), { fetchedAt, source: MODELS_DEV_URL, payload: modelsDev });
493
+ rmSync(join(dir, FAILURE_FILE), { force: true });
494
+ memo = undefined;
495
+ return { status: "refreshed", models: rows, repos: lookups.length };
496
+ } catch (error) {
497
+ return fail(error instanceof Error ? error.message : String(error));
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Whether the daily refresh may run: not with pi's `PI_OFFLINE` set, and not
503
+ * when the user turned it off in `~/.onecode/settings.json`.
504
+ */
505
+ export function catalogRefreshEnabled(settings: unknown, env: Record<string, string | undefined> = process.env): boolean {
506
+ if (/^(1|true|yes)$/i.test(String(env.PI_OFFLINE ?? "").trim())) return false;
507
+ return record(settings)?.refreshModelCatalog !== false;
508
+ }
509
+
510
+ /** The settings file reader the refresh switch uses, for callers without one. */
511
+ export function readCatalogRefreshEnabled(settingsPath: string, env: Record<string, string | undefined> = process.env): boolean {
512
+ return catalogRefreshEnabled(readJsonFile(settingsPath), env);
513
+ }