omo-ai 5.0.0-0.beta.9 → 5.0.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 (228) hide show
  1. package/bin/lib/auth-store.js +57 -0
  2. package/bin/lib/bun-bin-shim.js +128 -0
  3. package/bin/lib/bun-runtime.js +227 -0
  4. package/bin/lib/category-coverage.js +212 -0
  5. package/bin/lib/child-process.js +95 -5
  6. package/bin/lib/claude-code-floor.js +53 -0
  7. package/bin/lib/compile-safe-engine.js +112 -0
  8. package/bin/lib/daemon.js +205 -0
  9. package/bin/lib/doctor-migration.js +289 -0
  10. package/bin/lib/doctor.js +350 -6
  11. package/bin/lib/engine-models.js +67 -0
  12. package/bin/lib/engine-prepare.js +39 -0
  13. package/bin/lib/jsonc-edit.js +142 -0
  14. package/bin/lib/jsonc.js +53 -0
  15. package/bin/lib/launcher.js +114 -17
  16. package/bin/lib/package-paths.js +64 -13
  17. package/bin/lib/provider-map.json +26 -6
  18. package/bin/lib/rpc-stream-errors.js +35 -0
  19. package/bin/lib/self-update.js +82 -0
  20. package/bin/lib/setup-assets-import.js +134 -0
  21. package/bin/lib/setup-credentials.js +197 -0
  22. package/bin/lib/setup-detect-cache.js +90 -0
  23. package/bin/lib/setup-detect-refresh.js +33 -0
  24. package/bin/lib/setup-detect.js +32 -12
  25. package/bin/lib/setup-guidance.js +32 -0
  26. package/bin/lib/setup-import.js +95 -203
  27. package/bin/lib/setup-model-choices-import.js +202 -0
  28. package/bin/lib/setup-models.js +7 -17
  29. package/bin/lib/setup-opencode-assets.js +239 -0
  30. package/bin/lib/setup-opencode-models.js +263 -0
  31. package/bin/lib/setup-opencode-providers.js +206 -0
  32. package/bin/lib/setup-providers-import.js +118 -0
  33. package/bin/lib/setup-summary.js +171 -0
  34. package/bin/lib/sqlite-rows.js +32 -0
  35. package/bin/omo.js +19 -2
  36. package/bin/senpi-patch.mjs +27 -0
  37. package/package.json +6 -3
  38. package/plugin/CHANGELOG.md +5313 -0
  39. package/plugin/README.md +10 -1
  40. package/plugin/daemon-launch-spec.json +18 -0
  41. package/plugin/extensions/dream-persona.md +16 -5
  42. package/plugin/extensions/facts-persona.md +3 -5
  43. package/plugin/extensions/kibitzer-persona.md +67 -0
  44. package/plugin/extensions/memory-run-supervisor.mjs +2 -2
  45. package/plugin/extensions/omo-init-deep-advisor.js +2 -2
  46. package/plugin/extensions/omo-member.js +2 -2
  47. package/plugin/extensions/omo-task.js +2 -2
  48. package/plugin/extensions/omo.js +2 -2
  49. package/plugin/extensions/reflection-persona.md +2 -1
  50. package/plugin/package.json +8 -6
  51. package/plugin/runtime/agent-toolkit-sdk/sdk.js +2 -0
  52. package/plugin/runtime/ast-grep-mcp/cli.js +32 -20
  53. package/plugin/runtime/ast-grep-mcp/manifest.json +2 -2
  54. package/plugin/runtime/category-coverage/index.js +332 -0
  55. package/plugin/runtime/dag/library.js +124 -0
  56. package/plugin/runtime/dag/sdk.js +103 -6
  57. package/plugin/runtime/lsp-daemon/dist/.omo-runtime-manifest.json +12 -12
  58. package/plugin/runtime/lsp-daemon/dist/cli.js +1421 -960
  59. package/plugin/runtime/lsp-daemon/dist/client.d.ts +10 -0
  60. package/plugin/runtime/lsp-daemon/dist/client.js +956 -498
  61. package/plugin/runtime/lsp-daemon/dist/daemon-client.d.ts +3 -0
  62. package/plugin/runtime/lsp-daemon/dist/daemon-client.js +23 -3
  63. package/plugin/runtime/lsp-daemon/dist/ensure-daemon.d.ts +6 -1
  64. package/plugin/runtime/lsp-daemon/dist/ensure-daemon.js +27 -5
  65. package/plugin/runtime/lsp-daemon/dist/index.js +860 -407
  66. package/plugin/runtime/lsp-daemon/dist/ownership.js +2 -2
  67. package/plugin/runtime/lsp-daemon/dist/proxy.js +3 -0
  68. package/plugin/runtime/lsp-daemon/dist/version-reap.js +1 -1
  69. package/plugin/scripts/install.mjs +62 -21
  70. package/plugin/skills/ast-grep/AGENTS.md +51 -0
  71. package/plugin/skills/ast-grep/SKILL.md +13 -1
  72. package/plugin/skills/browser/ATTRIBUTION.md +37 -0
  73. package/plugin/skills/browser/SKILL.md +109 -0
  74. package/plugin/skills/browser/references/commands.md +105 -0
  75. package/plugin/skills/browser/references/install.md +97 -0
  76. package/plugin/skills/browser/references/owned-engine/README.md +62 -0
  77. package/plugin/skills/browser/references/owned-engine/frames-and-humans.md +52 -0
  78. package/plugin/skills/browser/references/owned-engine/ladder.md +61 -0
  79. package/plugin/skills/browser/references/owned-engine/network.md +62 -0
  80. package/plugin/skills/browser/references/recipes/1password.md +61 -0
  81. package/plugin/skills/browser/references/remote.md +25 -0
  82. package/plugin/skills/browser/runtime/omowright/index.js +1534 -0
  83. package/plugin/skills/browser/runtime/omowright/manifest.json +9 -0
  84. package/plugin/skills/browser/runtime/omowright/page-bundle.js +1395 -0
  85. package/plugin/skills/browser/scripts/browser-doctor.mjs +57 -0
  86. package/plugin/skills/browser/scripts/browser-install.mjs +50 -0
  87. package/plugin/skills/browser/scripts/omowright.mjs +25 -0
  88. package/plugin/skills/coding-agent-sessions/AGENTS.md +62 -0
  89. package/plugin/skills/coding-agent-sessions/SKILL.md +1 -1
  90. package/plugin/skills/dag-library/SKILL.md +70 -0
  91. package/plugin/skills/data-scientist/SKILL.md +99 -239
  92. package/plugin/skills/data-scientist/references/execution-surfaces.md +91 -0
  93. package/plugin/skills/data-scientist/references/placement.md +74 -0
  94. package/plugin/skills/data-scientist/references/polars-lane.md +95 -0
  95. package/plugin/skills/data-scientist/references/uv-setup.md +1 -1
  96. package/plugin/skills/data-scientist/references/visualization.md +64 -0
  97. package/plugin/skills/data-scientist/scripts/ensure-js-deps.sh +28 -0
  98. package/plugin/skills/data-scientist/scripts/ensure-py-deps.sh +37 -0
  99. package/plugin/skills/debugging/SKILL.md +5 -3
  100. package/plugin/skills/debugging/references/methodology/00-setup.md +12 -0
  101. package/plugin/skills/debugging/references/methodology/02-investigate.md +28 -8
  102. package/plugin/skills/debugging/references/methodology/04-oracle-triple.md +5 -5
  103. package/plugin/skills/debugging/references/methodology/06-fix.md +3 -3
  104. package/plugin/skills/debugging/references/methodology/08-qa.md +1 -1
  105. package/plugin/skills/debugging/references/methodology/partial-runtime-evidence.md +3 -3
  106. package/plugin/skills/debugging/references/runtimes/go.md +15 -1
  107. package/plugin/skills/debugging/references/runtimes/native-binary.md +38 -0
  108. package/plugin/skills/debugging/references/runtimes/node.md +25 -0
  109. package/plugin/skills/debugging/references/runtimes/python.md +13 -0
  110. package/plugin/skills/debugging/references/runtimes/rust.md +41 -0
  111. package/plugin/skills/debugging/references/scripts/dap.mjs +267 -0
  112. package/plugin/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
  113. package/plugin/skills/debugging/references/tools/browser-qa.md +104 -0
  114. package/plugin/skills/debugging/references/tools/dap.md +103 -0
  115. package/plugin/skills/debugging/references/tools/frida.md +193 -0
  116. package/plugin/skills/frontend/ATTRIBUTION.md +10 -0
  117. package/plugin/skills/frontend/SKILL.md +8 -3
  118. package/plugin/skills/frontend/references/design/README.md +13 -4
  119. package/plugin/skills/frontend/references/design/_INDEX.md +1 -0
  120. package/plugin/skills/frontend/references/design/ambience-skill.md +146 -0
  121. package/plugin/skills/frontend/references/design/aside.md +1 -1
  122. package/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
  123. package/plugin/skills/frontend/references/design/design-system-architecture.md +30 -6
  124. package/plugin/skills/frontend/references/design/interaction-skill.md +19 -5
  125. package/plugin/skills/frontend/references/design/print-paged-media.md +76 -0
  126. package/plugin/skills/frontend/references/designpowers/README.md +1 -1
  127. package/plugin/skills/frontend/references/designpowers/lane-a-direction.md +1 -1
  128. package/plugin/skills/frontend/references/designpowers/lane-b-execution.md +11 -11
  129. package/plugin/skills/frontend/references/designpowers/lane-d-memory.md +3 -3
  130. package/plugin/skills/frontend/references/designpowers/orchestration.md +1 -1
  131. package/plugin/skills/frontend/references/designpowers/routing.md +5 -5
  132. package/plugin/skills/frontend/references/perfection/README.md +1 -0
  133. package/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
  134. package/plugin/skills/git-master/SKILL.md +1 -1
  135. package/plugin/skills/give-me-tips/SKILL.md +85 -3
  136. package/plugin/skills/hyperplan/SKILL.md +7 -7
  137. package/plugin/skills/init-deep/SKILL.md +98 -132
  138. package/plugin/skills/lsp-setup/SKILL.md +1 -1
  139. package/plugin/skills/mass-ulw/SKILL.md +42 -27
  140. package/plugin/skills/mass-ulw/references/planning.md +169 -0
  141. package/plugin/skills/onboarding/SKILL.md +60 -12
  142. package/plugin/skills/programming/SKILL.md +12 -18
  143. package/plugin/skills/programming/references/rust/README.md +43 -15
  144. package/plugin/skills/programming/references/rust/api-design.md +81 -0
  145. package/plugin/skills/programming/references/rust/async-tokio.md +60 -28
  146. package/plugin/skills/programming/references/rust/axum-stack.md +1 -13
  147. package/plugin/skills/programming/references/rust/cargo-strict.md +44 -8
  148. package/plugin/skills/programming/references/rust/clap-stack.md +8 -3
  149. package/plugin/skills/programming/references/rust/concurrency.md +66 -52
  150. package/plugin/skills/programming/references/rust/libraries.md +35 -25
  151. package/plugin/skills/programming/references/rust/macros.md +63 -0
  152. package/plugin/skills/programming/references/rust/one-liners.md +5 -3
  153. package/plugin/skills/programming/references/rust/proptest-insta.md +8 -0
  154. package/plugin/skills/programming/references/rust/type-state.md +50 -12
  155. package/plugin/skills/programming/references/rust/unsafe-discipline.md +34 -6
  156. package/plugin/skills/programming/references/rust/zero-cost-safety.md +62 -52
  157. package/plugin/skills/programming/references/rust-ub/README.md +2 -2
  158. package/plugin/skills/programming/references/rust-ub/miri-sanitizers-loom.md +1 -1
  159. package/plugin/skills/programming/references/rust-ub/ub-taxonomy.md +6 -3
  160. package/plugin/skills/programming/scripts/rust/check-no-excuse-rules.sh +86 -75
  161. package/plugin/skills/programming/scripts/rust/new-project.py +31 -28
  162. package/plugin/skills/refactor/SKILL.md +8 -9
  163. package/plugin/skills/remove-ai-slops/SKILL.md +7 -7
  164. package/plugin/skills/review-work/SKILL.md +103 -420
  165. package/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
  166. package/plugin/skills/ultimate-browsing/SKILL.md +40 -28
  167. package/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
  168. package/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
  169. package/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
  170. package/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
  171. package/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
  172. package/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
  173. package/plugin/skills/ultimate-browsing/references/chrome-stealth.md +34 -111
  174. package/plugin/skills/ultimate-browsing/references/insane-search/README.md +14 -7
  175. package/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +22 -36
  176. package/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
  177. package/plugin/skills/ultrawork/SKILL.md +196 -190
  178. package/plugin/skills/{start-work → ulw-execute}/SKILL.md +29 -27
  179. package/plugin/skills/ulw-loop/SKILL.md +51 -17
  180. package/plugin/skills/ulw-loop/references/define-goal.md +2 -3
  181. package/plugin/skills/ulw-loop/references/full-workflow.md +82 -106
  182. package/plugin/skills/ulw-plan/SKILL.md +24 -18
  183. package/plugin/skills/ulw-plan/references/full-workflow.md +34 -27
  184. package/plugin/skills/ulw-plan/references/intent-clear.md +8 -9
  185. package/plugin/skills/ulw-plan/references/intent-unclear.md +8 -8
  186. package/plugin/skills/ulw-plan/references/stance-calibration.md +95 -0
  187. package/plugin/skills/ulw-plan/scripts/plan-templates.mjs +171 -0
  188. package/plugin/skills/ulw-plan/scripts/scaffold-plan.mjs +5 -151
  189. package/plugin/skills/ulw-research/SKILL.md +65 -45
  190. package/plugin/skills/ulw-research/references/deliverable-phase.md +187 -0
  191. package/plugin/skills/ulw-research/references/report-gates.md +66 -0
  192. package/plugin/skills/ulw-research/scripts/cli-support.mjs +102 -0
  193. package/plugin/skills/ulw-research/scripts/contracts.mjs +162 -0
  194. package/plugin/skills/ulw-research/scripts/css-lite.mjs +204 -0
  195. package/plugin/skills/ulw-research/scripts/design-spec.mjs +161 -0
  196. package/plugin/skills/ulw-research/scripts/entities.mjs +39 -0
  197. package/plugin/skills/ulw-research/scripts/entry-guard.mjs +21 -0
  198. package/plugin/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
  199. package/plugin/skills/ulw-research/scripts/format-extract.mjs +229 -0
  200. package/plugin/skills/ulw-research/scripts/gates-figures.mjs +173 -0
  201. package/plugin/skills/ulw-research/scripts/gates-layout.mjs +146 -0
  202. package/plugin/skills/ulw-research/scripts/gates-static.mjs +167 -0
  203. package/plugin/skills/ulw-research/scripts/gates-structure.mjs +118 -0
  204. package/plugin/skills/ulw-research/scripts/gates-text.mjs +190 -0
  205. package/plugin/skills/ulw-research/scripts/html-lite.mjs +239 -0
  206. package/plugin/skills/ulw-research/scripts/layout-probe.mjs +83 -0
  207. package/plugin/skills/ulw-research/scripts/outcome.mjs +250 -0
  208. package/plugin/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
  209. package/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
  210. package/plugin/skills/ulw-research/scripts/report-tools.mjs +61 -0
  211. package/plugin/skills/visual-qa/AGENTS.md +58 -0
  212. package/plugin/skills/visual-qa/SKILL.md +9 -8
  213. package/plugin/skills/visual-qa/references/browser-setup.md +77 -0
  214. package/plugin/skills-conditional/x-search/SKILL.md +123 -0
  215. package/bin/omo-agent-toolkit.js +0 -14
  216. package/plugin/extensions/omo-memory-mcp.js +0 -3
  217. package/plugin/runtime/agent-toolkit/cli.js +0 -14
  218. package/plugin/runtime/agent-toolkit/directive.md +0 -477
  219. package/plugin/runtime/agent-toolkit/omo-agent-toolkit +0 -2
  220. package/plugin/runtime/agent-toolkit/omo-agent-toolkit.cmd +0 -2
  221. package/plugin/runtime/agent-toolkit/ulw-loop/cli.js +0 -3455
  222. package/plugin/skills/data-scientist/references/common-scenarios.md +0 -176
  223. package/plugin/skills/data-scientist/references/execution-templates.md +0 -197
  224. package/plugin/skills/data-scientist/references/integration-patterns.md +0 -153
  225. package/plugin/skills/data-scientist/references/performance-benchmarks.md +0 -37
  226. package/plugin/skills/debugging/references/tools/playwright-cli.md +0 -194
  227. package/plugin/skills/programming/scripts/rust/check-no-excuse-rules.py +0 -296
  228. package/plugin/skills/visual-qa/references/agent-browser-setup.md +0 -45
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Read and write the engine's `<agentDir>/auth.json`: every setup stage that imports a key goes
3
+ * through here, so they all escape, back up and write it the same way.
4
+ */
5
+
6
+ import { chmodSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs"
7
+ import { dirname } from "node:path"
8
+
9
+ // opencode keeps a pasted key verbatim and never interprets it, but the engine resolves every stored
10
+ // key as a config value: a leading `!` runs a shell command and `$NAME` / `${NAME}` interpolate the
11
+ // environment. `$$` and `$!` are the engine's literal escapes, so the engine reads back the exact
12
+ // bytes opencode held.
13
+ export function literalConfigValue(value) {
14
+ return value.replace(/[$!]/g, "$$$&")
15
+ }
16
+
17
+ export function readAuthStore(path) {
18
+ if (!existsSync(path)) return { entries: {}, bytes: undefined }
19
+ const bytes = readFileSync(path, "utf8")
20
+ try {
21
+ const entries = JSON.parse(bytes)
22
+ if (entries === null || typeof entries !== "object" || Array.isArray(entries)) throw new Error("expected object")
23
+ return { entries, bytes }
24
+ } catch {
25
+ return { malformed: true, bytes }
26
+ }
27
+ }
28
+
29
+ export function timestamp() {
30
+ return new Date().toISOString().replace(/[-:]/g, "")
31
+ }
32
+
33
+ // The credential and custom-provider stages of one run can both rewrite auth.json within the same
34
+ // millisecond. The first backup holds the older bytes, so a later one never replaces it.
35
+ function backup(path) {
36
+ try {
37
+ copyFileSync(path, `${path}.bak-${timestamp()}`, constants.COPYFILE_EXCL)
38
+ } catch (error) {
39
+ if (error.code !== "EEXIST") throw error
40
+ }
41
+ }
42
+
43
+ export function writeAuthStore(path, current, additions) {
44
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 })
45
+ if (current.bytes !== undefined) backup(path)
46
+ const next = { ...current.entries }
47
+ for (const item of additions) next[item.provider] = { type: "api_key", key: item.key }
48
+ const temporary = `${path}.tmp-${process.pid}`
49
+ try {
50
+ writeFileSync(temporary, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 0o600 })
51
+ chmodSync(temporary, 0o600)
52
+ renameSync(temporary, path)
53
+ chmodSync(path, 0o600)
54
+ } finally {
55
+ if (existsSync(temporary)) unlinkSync(temporary)
56
+ }
57
+ }
@@ -0,0 +1,128 @@
1
+ import { chmodSync, lstatSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs"
2
+ import { homedir as osHomedir } from "node:os"
3
+ import { join } from "node:path"
4
+ import { bunRoot, findBunBinary, isUnderBunGlobalTree } from "./bun-runtime.js"
5
+
6
+ // Bumping this rewrites every generated shim on its next node boot; the value is embedded so a
7
+ // stale shim is recognizable by content comparison alone.
8
+ const SHIM_VERSION = 1
9
+ const SHIM_MARKER = "omo-ai bun launcher shim"
10
+
11
+ // The two bins a POSIX bun-global install exposes for the same launcher. `bun add -g` links the
12
+ // package bin into BOTH, and a bun layout that puts the global node_modules/.bin on PATH first
13
+ // makes the second one the entry a typed `omo` actually reaches - so repairing only the first
14
+ // leaves those machines paying for a node boot on every launch. Each entry is judged on its own.
15
+ const BIN_ENTRIES = [
16
+ { label: "bin", segments: ["bin", "omo"] },
17
+ { label: "node-modules-bin", segments: ["install", "global", "node_modules", ".bin", "omo"] },
18
+ ]
19
+
20
+ function quoteForShell(value) {
21
+ return `'${value.replaceAll("'", "'\\''")}'`
22
+ }
23
+
24
+ /**
25
+ * The wrapper a bun-global install deserves: a two-branch sh script that execs bun on the real
26
+ * entrypoint so the launch never pays for a node boot. Both fallbacks land on the entrypoint's
27
+ * own `#!/usr/bin/env node` shebang, which is byte-identical to what the stock symlink did:
28
+ * - OMO_RUNTIME=node asks for node end to end, so bun is skipped on purpose;
29
+ * - a bun that moved or vanished falls back to the node path, and the next node boot rewrites
30
+ * the shim with the freshly discovered bun path.
31
+ */
32
+ export function bunBinShimScript(entryPath, bunPath) {
33
+ return [
34
+ "#!/bin/sh",
35
+ `# ${SHIM_MARKER} v${SHIM_VERSION} - generated by omo-ai; safe to delete, the next launch recreates it`,
36
+ `# entry: ${entryPath}`,
37
+ `if [ "$OMO_RUNTIME" != "node" ] && [ -x ${quoteForShell(bunPath)} ]; then exec ${quoteForShell(bunPath)} ${quoteForShell(entryPath)} "$@"; fi`,
38
+ `exec ${quoteForShell(entryPath)} "$@"`,
39
+ "",
40
+ ].join("\n")
41
+ }
42
+
43
+ /**
44
+ * Brings ONE bin entry to the shim, or explains why it was left as it was. Only bun's own link to
45
+ * THIS install, or a file already carrying the marker, is ever replaced; anything else - a link
46
+ * into another install, a hand-written file, a directory, a socket - is not ours to touch.
47
+ * Returns the labelled action; the caller owns the try/catch, so a broken entry cannot take the
48
+ * other one down with it.
49
+ */
50
+ function applyShim(binPath, script, io) {
51
+ let stats
52
+ try {
53
+ stats = io.lstat(binPath)
54
+ } catch {
55
+ return "absent-bin"
56
+ }
57
+ if (stats.isSymbolicLink()) {
58
+ if (io.realpath(binPath) !== io.scriptPath) return "foreign-link"
59
+ } else if (stats.isFile()) {
60
+ const current = io.readFile(binPath, "utf8")
61
+ if (current === script) return "current"
62
+ if (!current.includes(SHIM_MARKER)) return "foreign-file"
63
+ } else {
64
+ return "foreign-entry"
65
+ }
66
+
67
+ const temporary = `${binPath}.${io.pid}.tmp`
68
+ io.write(temporary, script, { mode: 0o755 })
69
+ // writeFileSync applies umask, and a 077 umask would ship a non-executable shim; chmod is exact.
70
+ io.chmod(temporary, 0o755)
71
+ io.rename(temporary, binPath)
72
+ return "repaired"
73
+ }
74
+
75
+ function repairBinEntry(entry, root, script, io) {
76
+ const path = join(root, ...entry.segments)
77
+ try {
78
+ return { label: entry.label, path, action: applyShim(path, script, io), error: undefined }
79
+ } catch (error) {
80
+ if (io.env.OMO_DEBUG) io.warn(`omo: bun launcher shim left untouched at ${path}: ${error.message}`)
81
+ return { label: entry.label, path, action: "failed", error: error.message }
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Keeps the user-facing bins of a POSIX bun-global install pointed at bun without a node boot.
87
+ * Called on every launcher start; it must stay cheap on the happy path (one lstat per entry plus,
88
+ * when an entry is already a regular file, reading a few hundred bytes) and must never break a
89
+ * launch: every failure path - foreign file, missing bin, unwritable bin dir - returns quietly,
90
+ * one entry's failure never stops the other from being repaired, and only OMO_DEBUG narrates.
91
+ * Repairs run under node only, because a bun process either arrived through a shim already or has
92
+ * no node boot to save.
93
+ *
94
+ * The answer is per entry (`entries`), with the primary `<bunRoot>/bin/omo` entry's action and
95
+ * error mirrored at the top level for callers that only ever cared about that one.
96
+ */
97
+ export function ensureBunBinShim(input) {
98
+ const env = input.env ?? process.env
99
+ const homedir = input.homedir ?? osHomedir
100
+ const platform = input.platform ?? process.platform
101
+ const versions = input.versions ?? process.versions
102
+ const io = {
103
+ env,
104
+ scriptPath: input.scriptPath,
105
+ lstat: input.lstat ?? lstatSync,
106
+ realpath: input.realpath ?? realpathSync,
107
+ readFile: input.readFile ?? readFileSync,
108
+ write: input.write ?? writeFileSync,
109
+ rename: input.rename ?? renameSync,
110
+ chmod: input.chmod ?? chmodSync,
111
+ pid: input.pid ?? process.pid,
112
+ warn: input.warn ?? ((message) => console.error(message)),
113
+ }
114
+
115
+ if (platform !== "darwin" && platform !== "linux") return { action: "skipped-platform", error: undefined, entries: [] }
116
+ if (versions.bun) return { action: "skipped-runtime", error: undefined, entries: [] }
117
+ if (!isUnderBunGlobalTree(input.scriptPath, { env, homedir, platform, realpath: io.realpath })) {
118
+ return { action: "skipped-install", error: undefined, entries: [] }
119
+ }
120
+ const bunPath = findBunBinary({ env, homedir, platform, exists: input.exists, realpath: io.realpath })
121
+ if (!bunPath) return { action: "skipped-no-bun", error: undefined, entries: [] }
122
+
123
+ const root = bunRoot(env, homedir, platform)
124
+ const script = bunBinShimScript(input.scriptPath, bunPath)
125
+ const entries = BIN_ENTRIES.map((entry) => repairBinEntry(entry, root, script, io))
126
+ const primary = entries[0]
127
+ return { action: primary.action, error: primary.error, entries }
128
+ }
@@ -0,0 +1,227 @@
1
+ import { execFile as nodeExecFile } from "node:child_process"
2
+ import { existsSync, realpathSync } from "node:fs"
3
+ import { homedir as osHomedir } from "node:os"
4
+ import { posix, win32 } from "node:path"
5
+ import { propagateResult, runChild } from "./child-process.js"
6
+
7
+ // A bun global install lives under <BUN_ROOT>/install/global/, and `bun add -g` links the launcher
8
+ // into <BUN_ROOT>/bin. The link TARGET is what identifies the install, so every comparison below
9
+ // runs on a real path.
10
+ const GLOBAL_TREE_MARKER = "/install/global/"
11
+
12
+ // The oldest bun the engine is known to run on: node:sqlite parity and the worker_threads compat the
13
+ // JS eval kernel relies on both landed in 1.4. A bun found lying around on an npm-installed machine
14
+ // is only trusted from here up; anything older leaves the launch on node, which always works.
15
+ export const BUN_MIN_VERSION = "1.4.0"
16
+
17
+ // `bun --version` answers in a few milliseconds; a probe that has not answered by now is a broken
18
+ // binary, and the launch simply proceeds on node.
19
+ const VERSION_PROBE_TIMEOUT_MS = 3_000
20
+
21
+ // Both sides of the tree comparison are reduced to one spelling: backslashes become forward
22
+ // slashes so Windows paths match, and repeated separators collapse because BUN_INSTALL is
23
+ // user-supplied and a value like `/tmp//bunroot` would otherwise never prefix-match a real path.
24
+ function normalize(path) {
25
+ return path.replaceAll("\\", "/").replaceAll(/\/{2,}/g, "/")
26
+ }
27
+
28
+ function pathApi(platform) {
29
+ return platform === "win32" ? win32 : posix
30
+ }
31
+
32
+ export function bunRoot(env, homedir, platform) {
33
+ return env.BUN_INSTALL ? env.BUN_INSTALL : pathApi(platform).join(homedir(), ".bun")
34
+ }
35
+
36
+ /**
37
+ * Node resolves the main module to its real path, so the script side of the comparison is already
38
+ * canonical. The root has to be canonicalized too or the two sides can name the same directory in
39
+ * different spellings - `/tmp` against `/private/tmp` on macOS, or any symlinked home - and a real
40
+ * bun install would silently fail to be recognized. A root that does not exist is used verbatim.
41
+ */
42
+ function canonicalRoot(env, homedir, platform, realpath) {
43
+ const root = bunRoot(env, homedir, platform)
44
+ try {
45
+ return realpath(root)
46
+ } catch {
47
+ return root
48
+ }
49
+ }
50
+
51
+ function binaryName(platform) {
52
+ return platform === "win32" ? "bun.exe" : "bun"
53
+ }
54
+
55
+ function pathDelimiter(platform) {
56
+ return platform === "win32" ? ";" : ":"
57
+ }
58
+
59
+ /**
60
+ * True when the executed script belongs to a Bun global install. The caller passes the script's
61
+ * REAL path: the launcher is reached through a symlink under the bun root's bin directory, and
62
+ * that link lives outside the global tree, so the link path itself never matches.
63
+ */
64
+ export function isUnderBunGlobalTree(scriptRealPath, options = {}) {
65
+ const env = options.env ?? process.env
66
+ const homedir = options.homedir ?? osHomedir
67
+ const platform = options.platform ?? process.platform
68
+ const realpath = options.realpath ?? realpathSync
69
+ const root = normalize(canonicalRoot(env, homedir, platform, realpath)).replace(/\/+$/, "")
70
+ return normalize(scriptRealPath).startsWith(`${root}${GLOBAL_TREE_MARKER}`)
71
+ }
72
+
73
+ // Only real executables qualify on win32. npm installs bun as a `bun.cmd` shim next to the real
74
+ // binary, and Node refuses to spawn .cmd/.bat files without a shell (spawn EINVAL, CVE-2024-27980),
75
+ // so a shim can neither answer the version probe nor host the re-exec. PATHEXT still decides which
76
+ // executable spellings exist; batch-file spellings are dropped. Lower-case first, so the path
77
+ // returned matches how the file is spelled on disk.
78
+ const WIN32_EXECUTABLE_EXTENSIONS = new Set([".exe", ".com"])
79
+
80
+ function pathExtensions(env, platform) {
81
+ if (platform !== "win32") return [""]
82
+ const configured = env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD"
83
+ const spellings = []
84
+ for (const raw of configured.split(";")) {
85
+ const extension = raw.trim()
86
+ if (!WIN32_EXECUTABLE_EXTENSIONS.has(extension.toLowerCase())) continue
87
+ for (const spelling of [extension.toLowerCase(), extension]) {
88
+ if (!spellings.includes(spelling)) spellings.push(spelling)
89
+ }
90
+ }
91
+ return spellings.length > 0 ? spellings : [...WIN32_EXECUTABLE_EXTENSIONS]
92
+ }
93
+
94
+ /**
95
+ * Locates the bun binary this machine would run. Absence is a normal answer - a machine without bun
96
+ * simply keeps the launcher on node.
97
+ */
98
+ export function findBunBinary(options = {}) {
99
+ const env = options.env ?? process.env
100
+ const homedir = options.homedir ?? osHomedir
101
+ const platform = options.platform ?? process.platform
102
+ const exists = options.exists ?? existsSync
103
+ const paths = pathApi(platform)
104
+ const name = binaryName(platform)
105
+
106
+ const candidates = [
107
+ paths.join(bunRoot(env, homedir, platform), "bin", name),
108
+ paths.join(homedir(), ".bun", "bin", name),
109
+ ]
110
+ for (const candidate of candidates) {
111
+ if (exists(candidate)) return candidate
112
+ }
113
+
114
+ const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path")
115
+ const entries = pathKey ? (env[pathKey] ?? "").split(pathDelimiter(platform)).filter(Boolean) : []
116
+ for (const entry of entries) {
117
+ for (const extension of pathExtensions(env, platform)) {
118
+ // On win32 the name already carries .exe; PATHEXT decides which other spellings are runnable.
119
+ const candidate = paths.join(entry, platform === "win32" ? `bun${extension}` : name)
120
+ if (exists(candidate)) return candidate
121
+ }
122
+ }
123
+ return undefined
124
+ }
125
+
126
+ /** True when a `bun --version` answer is at least BUN_MIN_VERSION; missing or unparseable never is. */
127
+ export function bunVersionSatisfies(version) {
128
+ if (typeof version !== "string") return false
129
+ const match = /^(\d+)\.(\d+)/.exec(version)
130
+ if (match === null) return false
131
+ const [floorMajor, floorMinor] = BUN_MIN_VERSION.split(".").map(Number)
132
+ const major = Number(match[1])
133
+ const minor = Number(match[2])
134
+ return major > floorMajor || (major === floorMajor && minor >= floorMinor)
135
+ }
136
+
137
+ /**
138
+ * Asks a bun binary for its version. Every failure - a binary that cannot start, prints nothing, or
139
+ * hangs past the bounded timeout - resolves to undefined, which the decision reads as "not a bun we
140
+ * can trust". Asynchronous like every other spawn in this launcher: nothing here may block the
141
+ * event loop, so a signal arriving mid-probe is still handled.
142
+ */
143
+ export function probeBunVersion(bunPath, options = {}) {
144
+ const execFile = options.execFile ?? nodeExecFile
145
+ const env = options.env ?? process.env
146
+ return new Promise((resolve) => {
147
+ try {
148
+ execFile(
149
+ bunPath,
150
+ ["--version"],
151
+ { encoding: "utf8", env, timeout: VERSION_PROBE_TIMEOUT_MS, windowsHide: true },
152
+ (error, stdout) => {
153
+ if (error) {
154
+ resolve(undefined)
155
+ return
156
+ }
157
+ const version = String(stdout).trim()
158
+ resolve(version === "" ? undefined : version)
159
+ },
160
+ )
161
+ } catch {
162
+ // Node throws synchronously for a batch file spawned without a shell (spawn EINVAL); that is
163
+ // "not a bun we can trust", never a launcher crash.
164
+ resolve(undefined)
165
+ }
166
+ })
167
+ }
168
+
169
+ /**
170
+ * The whole policy in one place, first match wins:
171
+ * 1. already on bun -> stay (the loop guard; without it a re-exec would recurse)
172
+ * 2. OMO_RUNTIME=node -> stay (explicit user override beats detection)
173
+ * 3. no bun binary anywhere -> stay (npm-only machines never notice this module)
174
+ * 4. OMO_RUNTIME=bun -> re-exec (explicit opt-in, no version floor)
175
+ * 5. bun global install -> re-exec (the bun that installed omo is the bun to run it)
176
+ * 6. any other install, bun >= 1.4 -> re-exec (a machine that has bun runs omo on bun)
177
+ * 7. an older bun -> stay
178
+ *
179
+ * Rules 5 and 6 differ only in cost: a bun-global install already proved its bun, so it never pays
180
+ * for the version probe, while an npm, project-local or bunx install probes the discovered binary
181
+ * once per node boot before trusting it.
182
+ */
183
+ export async function resolveBunReexec(input) {
184
+ const env = input.env ?? process.env
185
+ const versions = input.versions ?? process.versions
186
+ if (versions.bun) return { reexec: false }
187
+ const requested = env.OMO_RUNTIME
188
+ if (requested === "node") return { reexec: false }
189
+ const bunPath = findBunBinary(input)
190
+ if (!bunPath) return { reexec: false }
191
+ if (requested === "bun" || isUnderBunGlobalTree(input.scriptPath, input)) return { reexec: true, bunPath }
192
+ const probe = input.bunVersion ?? probeBunVersion
193
+ const version = await probe(bunPath, { env })
194
+ if (!bunVersionSatisfies(version)) return { reexec: false }
195
+ return { reexec: true, bunPath }
196
+ }
197
+
198
+ /**
199
+ * Runs the decision. Resolves true when bun took over the process, in which case the caller must
200
+ * return immediately. POSIX replaces the current image; unsupported or failed execve uses the
201
+ * asynchronous child path so signals still reach the engine and its exit status is propagated.
202
+ *
203
+ * Node's execArgv is deliberately dropped - node flags are not bun flags, and forwarding them
204
+ * would fail the very launch this re-exec is meant to make work.
205
+ */
206
+ export async function maybeReexecUnderBun(input) {
207
+ const decision = await resolveBunReexec(input)
208
+ if (!decision.reexec) return false
209
+ const run = input.spawn ?? runChild
210
+ const propagate = input.propagate ?? propagateResult
211
+ const argv = input.argv ?? process.argv
212
+ const execve = input.execve === undefined ? process.execve : input.execve
213
+ if ((input.platform ?? process.platform) !== "win32" && typeof execve === "function") {
214
+ try {
215
+ execve(decision.bunPath, [decision.bunPath, input.scriptPath, ...argv.slice(2)], process.env)
216
+ return true
217
+ } catch {
218
+ // Keep the same inherited environment and signal forwarding if replacement fails.
219
+ }
220
+ }
221
+ const result = await run(decision.bunPath, [input.scriptPath, ...argv.slice(2)], {
222
+ stdio: "inherit",
223
+ windowsHide: true,
224
+ })
225
+ propagate(result)
226
+ return true
227
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Which task categories the user's providers serve, for `omo doctor` and the `omo setup` summary.
3
+ *
4
+ * The model list is the pinned engine's own: its ModelRuntime, built the way the engine's
5
+ * `auth-check --no-refresh` builds it (read-only credentials, in-memory model cache, no network),
6
+ * so auth.json, models.json custom providers and provider env keys count exactly as a session
7
+ * counts them, and nothing is written. The classification is the spawn path's resolver, reached
8
+ * through the bundled `plugin/runtime/category-coverage/index.js` (category-coverage-entry.ts).
9
+ * Both are fail-open: any error yields no coverage, never a failed command.
10
+ */
11
+
12
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
13
+ import { tmpdir } from "node:os"
14
+ import { join } from "node:path"
15
+ import { pathToFileURL } from "node:url"
16
+ import { packageRoot, resolveSenpi } from "./package-paths.js"
17
+
18
+ export const COVERAGE_RUNTIME = join("plugin", "runtime", "category-coverage", "index.js")
19
+
20
+ async function loadEngine() {
21
+ const core = join(resolveSenpi().packageRoot, "dist", "core")
22
+ const load = (name) => import(pathToFileURL(join(core, name)).href)
23
+ const subscriptionDir = join("extensions", "builtin", "anthropic-subscription")
24
+ const [runtime, auth, store, settings, subscription, subscriptionSettings] = await Promise.all([
25
+ load("model-runtime.js"),
26
+ load("auth-storage.js"),
27
+ load("models-store.js"),
28
+ load("settings-manager.js"),
29
+ load(join(subscriptionDir, "index.js")),
30
+ load(join(subscriptionDir, "settings.js")),
31
+ ])
32
+ return {
33
+ ModelRuntime: runtime.ModelRuntime,
34
+ AuthStorage: auth.AuthStorage,
35
+ ReadOnlyAuthStorage: auth.ReadOnlyAuthStorage,
36
+ InMemoryCodingAgentModelsStore: store.InMemoryCodingAgentModelsStore,
37
+ SettingsManager: settings.SettingsManager,
38
+ registerAnthropicSubscription: subscription.registerAnthropicSubscriptionExtension,
39
+ loadAnthropicSubscriptionSettings: subscriptionSettings.loadAnthropicSubscriptionProviderSettings,
40
+ }
41
+ }
42
+
43
+ // anthropic-subscription is the one chain provider a builtin extension registers instead of the
44
+ // catalog. Its registration carries its own availability check (stored accounts,
45
+ // CLAUDE_CODE_OAUTH_TOKEN* env tokens, an opted-in ambient Claude login), so the provider config
46
+ // is captured from the extension itself and registered on the runtime, as a session does. The
47
+ // extension's commands and session hooks are not needed here and are dropped. Its settings are
48
+ // read from the inspected agent dir (the extension's default reads the engine's own default dir).
49
+ function extensionProviders(register) {
50
+ const providers = []
51
+ const capture = (name, config) => { providers.push([name, config]) }
52
+ register(new Proxy({}, { get: (_, key) => key === "registerProvider" ? capture : () => undefined }))
53
+ return providers
54
+ }
55
+
56
+ async function loadRuntime() {
57
+ return import(pathToFileURL(join(packageRoot, COVERAGE_RUNTIME)).href)
58
+ }
59
+
60
+ /**
61
+ * @typedef {{ provider: string, id: string }} EngineModel
62
+ * @typedef {{
63
+ * agentDir: string,
64
+ * authEntries?: Record<string, unknown>,
65
+ * modelsDocument?: Record<string, unknown>,
66
+ * cwd?: string,
67
+ * loadEngine?: () => Promise<any>,
68
+ * }} EngineModelsInput
69
+ * @param {EngineModelsInput} input `authEntries` / `modelsDocument` stand in for auth.json /
70
+ * models.json (a setup plan's post-import view); absent, the agent dir's files are read.
71
+ * @returns {Promise<EngineModel[]>}
72
+ */
73
+ export async function engineAvailableModels(input) {
74
+ const engine = await (input.loadEngine ?? loadEngine)()
75
+ const credentials = input.authEntries === undefined
76
+ ? new engine.ReadOnlyAuthStorage(join(input.agentDir, "auth.json"))
77
+ : engine.AuthStorage.inMemory(input.authEntries)
78
+ const scratch = input.modelsDocument === undefined ? undefined : mkdtempSync(join(tmpdir(), "omo-coverage-"))
79
+ try {
80
+ const modelsPath = scratch === undefined ? join(input.agentDir, "models.json") : join(scratch, "models.json")
81
+ if (scratch !== undefined) writeFileSync(modelsPath, JSON.stringify(input.modelsDocument), { mode: 0o600 })
82
+ const runtime = await engine.ModelRuntime.create({
83
+ credentials,
84
+ modelsPath,
85
+ modelsStore: new engine.InMemoryCodingAgentModelsStore(),
86
+ allowModelNetwork: false,
87
+ })
88
+ const cwd = input.cwd ?? process.cwd()
89
+ const readSettings = () => engine.loadAnthropicSubscriptionSettings(engine.SettingsManager.create(cwd, input.agentDir))
90
+ for (const [name, config] of extensionProviders((pi) => engine.registerAnthropicSubscription(pi, { readSettings }))) {
91
+ await runtime.registerProvider(name, config, { refresh: false })
92
+ }
93
+ return (await runtime.getAvailable()).map((model) => ({ provider: model.provider, id: model.id }))
94
+ } finally {
95
+ if (scratch !== undefined) rmSync(scratch, { recursive: true, force: true })
96
+ }
97
+ }
98
+
99
+ /**
100
+ * @typedef {{ name: string, providers: string[] }} UnusableCategory
101
+ * @typedef {{ usable: string[], unusable: UnusableCategory[] }} CategoryCoverage
102
+ * @typedef {{
103
+ * models: EngineModel[],
104
+ * cwd: string,
105
+ * env: Record<string, string | undefined>,
106
+ * pinnedCategories?: string[],
107
+ * loadRuntime?: () => Promise<any>,
108
+ * }} CoverageInput
109
+ * @param {CoverageInput} input
110
+ * @returns {Promise<CategoryCoverage>}
111
+ */
112
+ export async function categoryCoverage(input) {
113
+ const runtime = await (input.loadRuntime ?? loadRuntime)()
114
+ return runtime.categoryCoverage({ models: input.models, cwd: input.cwd, env: input.env, pinnedCategories: input.pinnedCategories ?? [] })
115
+ }
116
+
117
+ function total(coverage) {
118
+ return coverage.usable.length + coverage.unusable.length
119
+ }
120
+
121
+ // The category-unavailable notice's wording (omo-senpi components/task/category-unavailable-warning.ts).
122
+ function gapLine(gap) {
123
+ if (gap.providers.length === 0) {
124
+ return `Category "${gap.name}" has no usable model: your connected providers serve none of its fallback-chain models. Pin categories.${gap.name}.model in omo.json.`
125
+ }
126
+ return `Category "${gap.name}" has no usable model: none of its fallback-chain providers are connected (${gap.providers.join(", ")}). Connect one with /login <provider>, or pin categories.${gap.name}.model in omo.json.`
127
+ }
128
+
129
+ /** @param {CategoryCoverage} coverage */
130
+ export function formatDoctorCoverageLines(coverage) {
131
+ if (coverage.unusable.length === 0) return [`PASS task categories: all ${total(coverage)} usable with your connected providers`]
132
+ const usable = coverage.usable.length > 0 ? ` (${coverage.usable.join(", ")})` : ""
133
+ return [
134
+ `WARN task categories: ${coverage.usable.length} of ${total(coverage)} usable with your connected providers${usable}`,
135
+ ...coverage.unusable.map((gap) => `WARN ${gapLine(gap)}`),
136
+ ]
137
+ }
138
+
139
+ /** The setup summary's `categories` row parts. @param {CategoryCoverage} coverage */
140
+ export function coverageSummaryParts(coverage) {
141
+ if (coverage.unusable.length === 0) return [`all ${total(coverage)} usable with these providers`]
142
+ const usable = coverage.usable.length > 0 ? ` (${coverage.usable.join(", ")})` : ""
143
+ return [`${coverage.usable.length} of ${total(coverage)} usable with these providers${usable}`, ...coverage.unusable.map(gapLine)]
144
+ }
145
+
146
+ /**
147
+ * @typedef {{
148
+ * agentDir: string,
149
+ * cwd?: string,
150
+ * env?: Record<string, string | undefined>,
151
+ * loadEngine?: () => Promise<any>,
152
+ * loadRuntime?: () => Promise<any>,
153
+ * }} DoctorCoverageOptions
154
+ * @param {DoctorCoverageOptions} options
155
+ * @returns {Promise<string[]>}
156
+ */
157
+ export async function doctorCoverageLines(options) {
158
+ try {
159
+ // The runtime first: a payload without it fails fast, before the engine import.
160
+ const runtime = await (options.loadRuntime ?? loadRuntime)()
161
+ const cwd = options.cwd ?? process.cwd()
162
+ const models = await engineAvailableModels({ agentDir: options.agentDir, cwd, loadEngine: options.loadEngine })
163
+ const coverage = await categoryCoverage({ models, cwd, env: options.env ?? process.env, loadRuntime: async () => runtime })
164
+ return formatDoctorCoverageLines(coverage)
165
+ } catch {
166
+ return []
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Coverage for what setup's plan leaves behind: auth.json with the keys the credential and
172
+ * custom-provider stages add, models.json with the providers they add, omo.json with the category
173
+ * pins the model-choice stage writes. Undefined when that view cannot be composed or computed.
174
+ *
175
+ * @typedef {{
176
+ * agentDir: string,
177
+ * home: string,
178
+ * env: Record<string, string | undefined>,
179
+ * plans: any,
180
+ * cwd?: string,
181
+ * loadEngine?: () => Promise<any>,
182
+ * loadRuntime?: () => Promise<any>,
183
+ * }} SetupCoverageInput
184
+ * @param {SetupCoverageInput} input
185
+ * @returns {Promise<CategoryCoverage | undefined>}
186
+ */
187
+ export async function setupCoverage(input) {
188
+ try {
189
+ const { credentials, providers, modelChoices } = input.plans
190
+ if (credentials.malformed || providers.models.malformed) return undefined
191
+ const runtime = await (input.loadRuntime ?? loadRuntime)()
192
+ const authEntries = { ...credentials.current.entries }
193
+ for (const item of [...credentials.result.additions, ...providers.result.keys]) authEntries[item.provider] ??= { type: "api_key", key: item.key }
194
+ const added = providers.result.added
195
+ const modelsDocument = added.length === 0 ? undefined : {
196
+ ...providers.models.document,
197
+ providers: { ...providers.models.document.providers, ...Object.fromEntries(added.map((provider) => [provider.id, provider.config])) },
198
+ }
199
+ const cwd = input.cwd ?? process.cwd()
200
+ const models = await engineAvailableModels({ agentDir: input.agentDir, authEntries, modelsDocument, cwd, loadEngine: input.loadEngine })
201
+ const pinnedCategories = modelChoices.items.filter((item) => item.kind === "categories" && item.state === "pending").map((item) => item.name)
202
+ return await categoryCoverage({
203
+ models,
204
+ cwd,
205
+ env: { ...input.env, HOME: input.home },
206
+ pinnedCategories,
207
+ loadRuntime: async () => runtime,
208
+ })
209
+ } catch {
210
+ return undefined
211
+ }
212
+ }