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.
- package/bin/lib/auth-store.js +57 -0
- package/bin/lib/bun-bin-shim.js +128 -0
- package/bin/lib/bun-runtime.js +227 -0
- package/bin/lib/category-coverage.js +212 -0
- package/bin/lib/child-process.js +95 -5
- package/bin/lib/claude-code-floor.js +53 -0
- package/bin/lib/compile-safe-engine.js +112 -0
- package/bin/lib/daemon.js +205 -0
- package/bin/lib/doctor-migration.js +289 -0
- package/bin/lib/doctor.js +350 -6
- package/bin/lib/engine-models.js +67 -0
- package/bin/lib/engine-prepare.js +39 -0
- package/bin/lib/jsonc-edit.js +142 -0
- package/bin/lib/jsonc.js +53 -0
- package/bin/lib/launcher.js +114 -17
- package/bin/lib/package-paths.js +64 -13
- package/bin/lib/provider-map.json +26 -6
- package/bin/lib/rpc-stream-errors.js +35 -0
- package/bin/lib/self-update.js +82 -0
- package/bin/lib/setup-assets-import.js +134 -0
- package/bin/lib/setup-credentials.js +197 -0
- package/bin/lib/setup-detect-cache.js +90 -0
- package/bin/lib/setup-detect-refresh.js +33 -0
- package/bin/lib/setup-detect.js +32 -12
- package/bin/lib/setup-guidance.js +32 -0
- package/bin/lib/setup-import.js +95 -203
- package/bin/lib/setup-model-choices-import.js +202 -0
- package/bin/lib/setup-models.js +7 -17
- package/bin/lib/setup-opencode-assets.js +239 -0
- package/bin/lib/setup-opencode-models.js +263 -0
- package/bin/lib/setup-opencode-providers.js +206 -0
- package/bin/lib/setup-providers-import.js +118 -0
- package/bin/lib/setup-summary.js +171 -0
- package/bin/lib/sqlite-rows.js +32 -0
- package/bin/omo.js +19 -2
- package/bin/senpi-patch.mjs +27 -0
- package/package.json +6 -3
- package/plugin/CHANGELOG.md +5313 -0
- package/plugin/README.md +10 -1
- package/plugin/daemon-launch-spec.json +18 -0
- package/plugin/extensions/dream-persona.md +16 -5
- package/plugin/extensions/facts-persona.md +3 -5
- package/plugin/extensions/kibitzer-persona.md +67 -0
- package/plugin/extensions/memory-run-supervisor.mjs +2 -2
- package/plugin/extensions/omo-init-deep-advisor.js +2 -2
- package/plugin/extensions/omo-member.js +2 -2
- package/plugin/extensions/omo-task.js +2 -2
- package/plugin/extensions/omo.js +2 -2
- package/plugin/extensions/reflection-persona.md +2 -1
- package/plugin/package.json +8 -6
- package/plugin/runtime/agent-toolkit-sdk/sdk.js +2 -0
- package/plugin/runtime/ast-grep-mcp/cli.js +32 -20
- package/plugin/runtime/ast-grep-mcp/manifest.json +2 -2
- package/plugin/runtime/category-coverage/index.js +332 -0
- package/plugin/runtime/dag/library.js +124 -0
- package/plugin/runtime/dag/sdk.js +103 -6
- package/plugin/runtime/lsp-daemon/dist/.omo-runtime-manifest.json +12 -12
- package/plugin/runtime/lsp-daemon/dist/cli.js +1421 -960
- package/plugin/runtime/lsp-daemon/dist/client.d.ts +10 -0
- package/plugin/runtime/lsp-daemon/dist/client.js +956 -498
- package/plugin/runtime/lsp-daemon/dist/daemon-client.d.ts +3 -0
- package/plugin/runtime/lsp-daemon/dist/daemon-client.js +23 -3
- package/plugin/runtime/lsp-daemon/dist/ensure-daemon.d.ts +6 -1
- package/plugin/runtime/lsp-daemon/dist/ensure-daemon.js +27 -5
- package/plugin/runtime/lsp-daemon/dist/index.js +860 -407
- package/plugin/runtime/lsp-daemon/dist/ownership.js +2 -2
- package/plugin/runtime/lsp-daemon/dist/proxy.js +3 -0
- package/plugin/runtime/lsp-daemon/dist/version-reap.js +1 -1
- package/plugin/scripts/install.mjs +62 -21
- package/plugin/skills/ast-grep/AGENTS.md +51 -0
- package/plugin/skills/ast-grep/SKILL.md +13 -1
- package/plugin/skills/browser/ATTRIBUTION.md +37 -0
- package/plugin/skills/browser/SKILL.md +109 -0
- package/plugin/skills/browser/references/commands.md +105 -0
- package/plugin/skills/browser/references/install.md +97 -0
- package/plugin/skills/browser/references/owned-engine/README.md +62 -0
- package/plugin/skills/browser/references/owned-engine/frames-and-humans.md +52 -0
- package/plugin/skills/browser/references/owned-engine/ladder.md +61 -0
- package/plugin/skills/browser/references/owned-engine/network.md +62 -0
- package/plugin/skills/browser/references/recipes/1password.md +61 -0
- package/plugin/skills/browser/references/remote.md +25 -0
- package/plugin/skills/browser/runtime/omowright/index.js +1534 -0
- package/plugin/skills/browser/runtime/omowright/manifest.json +9 -0
- package/plugin/skills/browser/runtime/omowright/page-bundle.js +1395 -0
- package/plugin/skills/browser/scripts/browser-doctor.mjs +57 -0
- package/plugin/skills/browser/scripts/browser-install.mjs +50 -0
- package/plugin/skills/browser/scripts/omowright.mjs +25 -0
- package/plugin/skills/coding-agent-sessions/AGENTS.md +62 -0
- package/plugin/skills/coding-agent-sessions/SKILL.md +1 -1
- package/plugin/skills/dag-library/SKILL.md +70 -0
- package/plugin/skills/data-scientist/SKILL.md +99 -239
- package/plugin/skills/data-scientist/references/execution-surfaces.md +91 -0
- package/plugin/skills/data-scientist/references/placement.md +74 -0
- package/plugin/skills/data-scientist/references/polars-lane.md +95 -0
- package/plugin/skills/data-scientist/references/uv-setup.md +1 -1
- package/plugin/skills/data-scientist/references/visualization.md +64 -0
- package/plugin/skills/data-scientist/scripts/ensure-js-deps.sh +28 -0
- package/plugin/skills/data-scientist/scripts/ensure-py-deps.sh +37 -0
- package/plugin/skills/debugging/SKILL.md +5 -3
- package/plugin/skills/debugging/references/methodology/00-setup.md +12 -0
- package/plugin/skills/debugging/references/methodology/02-investigate.md +28 -8
- package/plugin/skills/debugging/references/methodology/04-oracle-triple.md +5 -5
- package/plugin/skills/debugging/references/methodology/06-fix.md +3 -3
- package/plugin/skills/debugging/references/methodology/08-qa.md +1 -1
- package/plugin/skills/debugging/references/methodology/partial-runtime-evidence.md +3 -3
- package/plugin/skills/debugging/references/runtimes/go.md +15 -1
- package/plugin/skills/debugging/references/runtimes/native-binary.md +38 -0
- package/plugin/skills/debugging/references/runtimes/node.md +25 -0
- package/plugin/skills/debugging/references/runtimes/python.md +13 -0
- package/plugin/skills/debugging/references/runtimes/rust.md +41 -0
- package/plugin/skills/debugging/references/scripts/dap.mjs +267 -0
- package/plugin/skills/debugging/references/scripts/fixture-adapter.mjs +59 -0
- package/plugin/skills/debugging/references/tools/browser-qa.md +104 -0
- package/plugin/skills/debugging/references/tools/dap.md +103 -0
- package/plugin/skills/debugging/references/tools/frida.md +193 -0
- package/plugin/skills/frontend/ATTRIBUTION.md +10 -0
- package/plugin/skills/frontend/SKILL.md +8 -3
- package/plugin/skills/frontend/references/design/README.md +13 -4
- package/plugin/skills/frontend/references/design/_INDEX.md +1 -0
- package/plugin/skills/frontend/references/design/ambience-skill.md +146 -0
- package/plugin/skills/frontend/references/design/aside.md +1 -1
- package/plugin/skills/frontend/references/design/clone-from-url.md +1 -1
- package/plugin/skills/frontend/references/design/design-system-architecture.md +30 -6
- package/plugin/skills/frontend/references/design/interaction-skill.md +19 -5
- package/plugin/skills/frontend/references/design/print-paged-media.md +76 -0
- package/plugin/skills/frontend/references/designpowers/README.md +1 -1
- package/plugin/skills/frontend/references/designpowers/lane-a-direction.md +1 -1
- package/plugin/skills/frontend/references/designpowers/lane-b-execution.md +11 -11
- package/plugin/skills/frontend/references/designpowers/lane-d-memory.md +3 -3
- package/plugin/skills/frontend/references/designpowers/orchestration.md +1 -1
- package/plugin/skills/frontend/references/designpowers/routing.md +5 -5
- package/plugin/skills/frontend/references/perfection/README.md +1 -0
- package/plugin/skills/frontend/scripts/perfection/lighthouse-audit.py +2 -2
- package/plugin/skills/git-master/SKILL.md +1 -1
- package/plugin/skills/give-me-tips/SKILL.md +85 -3
- package/plugin/skills/hyperplan/SKILL.md +7 -7
- package/plugin/skills/init-deep/SKILL.md +98 -132
- package/plugin/skills/lsp-setup/SKILL.md +1 -1
- package/plugin/skills/mass-ulw/SKILL.md +42 -27
- package/plugin/skills/mass-ulw/references/planning.md +169 -0
- package/plugin/skills/onboarding/SKILL.md +60 -12
- package/plugin/skills/programming/SKILL.md +12 -18
- package/plugin/skills/programming/references/rust/README.md +43 -15
- package/plugin/skills/programming/references/rust/api-design.md +81 -0
- package/plugin/skills/programming/references/rust/async-tokio.md +60 -28
- package/plugin/skills/programming/references/rust/axum-stack.md +1 -13
- package/plugin/skills/programming/references/rust/cargo-strict.md +44 -8
- package/plugin/skills/programming/references/rust/clap-stack.md +8 -3
- package/plugin/skills/programming/references/rust/concurrency.md +66 -52
- package/plugin/skills/programming/references/rust/libraries.md +35 -25
- package/plugin/skills/programming/references/rust/macros.md +63 -0
- package/plugin/skills/programming/references/rust/one-liners.md +5 -3
- package/plugin/skills/programming/references/rust/proptest-insta.md +8 -0
- package/plugin/skills/programming/references/rust/type-state.md +50 -12
- package/plugin/skills/programming/references/rust/unsafe-discipline.md +34 -6
- package/plugin/skills/programming/references/rust/zero-cost-safety.md +62 -52
- package/plugin/skills/programming/references/rust-ub/README.md +2 -2
- package/plugin/skills/programming/references/rust-ub/miri-sanitizers-loom.md +1 -1
- package/plugin/skills/programming/references/rust-ub/ub-taxonomy.md +6 -3
- package/plugin/skills/programming/scripts/rust/check-no-excuse-rules.sh +86 -75
- package/plugin/skills/programming/scripts/rust/new-project.py +31 -28
- package/plugin/skills/refactor/SKILL.md +8 -9
- package/plugin/skills/remove-ai-slops/SKILL.md +7 -7
- package/plugin/skills/review-work/SKILL.md +103 -420
- package/plugin/skills/ultimate-browsing/ATTRIBUTION.md +1 -82
- package/plugin/skills/ultimate-browsing/SKILL.md +40 -28
- package/plugin/skills/ultimate-browsing/engine/AGENTS.md +2 -1
- package/plugin/skills/ultimate-browsing/engine/templates/package.json +2 -2
- package/plugin/skills/ultimate-browsing/engine/templates/playwright_mobile_chrome.js +5 -2
- package/plugin/skills/ultimate-browsing/engine/templates/playwright_real_chrome.js +8 -6
- package/plugin/skills/ultimate-browsing/engine/tests/test_playwright_stealth.py +86 -0
- package/plugin/skills/ultimate-browsing/engine/tests/test_playwright_templates.py +4 -7
- package/plugin/skills/ultimate-browsing/references/chrome-stealth.md +34 -111
- package/plugin/skills/ultimate-browsing/references/insane-search/README.md +14 -7
- package/plugin/skills/ultimate-browsing/references/insane-search/playwright.md +22 -36
- package/plugin/skills/ultimate-browsing/scripts/extract_cookies.py +3 -3
- package/plugin/skills/ultrawork/SKILL.md +196 -190
- package/plugin/skills/{start-work → ulw-execute}/SKILL.md +29 -27
- package/plugin/skills/ulw-loop/SKILL.md +51 -17
- package/plugin/skills/ulw-loop/references/define-goal.md +2 -3
- package/plugin/skills/ulw-loop/references/full-workflow.md +82 -106
- package/plugin/skills/ulw-plan/SKILL.md +24 -18
- package/plugin/skills/ulw-plan/references/full-workflow.md +34 -27
- package/plugin/skills/ulw-plan/references/intent-clear.md +8 -9
- package/plugin/skills/ulw-plan/references/intent-unclear.md +8 -8
- package/plugin/skills/ulw-plan/references/stance-calibration.md +95 -0
- package/plugin/skills/ulw-plan/scripts/plan-templates.mjs +171 -0
- package/plugin/skills/ulw-plan/scripts/scaffold-plan.mjs +5 -151
- package/plugin/skills/ulw-research/SKILL.md +65 -45
- package/plugin/skills/ulw-research/references/deliverable-phase.md +187 -0
- package/plugin/skills/ulw-research/references/report-gates.md +66 -0
- package/plugin/skills/ulw-research/scripts/cli-support.mjs +102 -0
- package/plugin/skills/ulw-research/scripts/contracts.mjs +162 -0
- package/plugin/skills/ulw-research/scripts/css-lite.mjs +204 -0
- package/plugin/skills/ulw-research/scripts/design-spec.mjs +161 -0
- package/plugin/skills/ulw-research/scripts/entities.mjs +39 -0
- package/plugin/skills/ulw-research/scripts/entry-guard.mjs +21 -0
- package/plugin/skills/ulw-research/scripts/format-extract-css.mjs +122 -0
- package/plugin/skills/ulw-research/scripts/format-extract.mjs +229 -0
- package/plugin/skills/ulw-research/scripts/gates-figures.mjs +173 -0
- package/plugin/skills/ulw-research/scripts/gates-layout.mjs +146 -0
- package/plugin/skills/ulw-research/scripts/gates-static.mjs +167 -0
- package/plugin/skills/ulw-research/scripts/gates-structure.mjs +118 -0
- package/plugin/skills/ulw-research/scripts/gates-text.mjs +190 -0
- package/plugin/skills/ulw-research/scripts/html-lite.mjs +239 -0
- package/plugin/skills/ulw-research/scripts/layout-probe.mjs +83 -0
- package/plugin/skills/ulw-research/scripts/outcome.mjs +250 -0
- package/plugin/skills/ulw-research/scripts/repair-tracker.mjs +171 -0
- package/plugin/skills/ulw-research/scripts/report-tools-commands.mjs +214 -0
- package/plugin/skills/ulw-research/scripts/report-tools.mjs +61 -0
- package/plugin/skills/visual-qa/AGENTS.md +58 -0
- package/plugin/skills/visual-qa/SKILL.md +9 -8
- package/plugin/skills/visual-qa/references/browser-setup.md +77 -0
- package/plugin/skills-conditional/x-search/SKILL.md +123 -0
- package/bin/omo-agent-toolkit.js +0 -14
- package/plugin/extensions/omo-memory-mcp.js +0 -3
- package/plugin/runtime/agent-toolkit/cli.js +0 -14
- package/plugin/runtime/agent-toolkit/directive.md +0 -477
- package/plugin/runtime/agent-toolkit/omo-agent-toolkit +0 -2
- package/plugin/runtime/agent-toolkit/omo-agent-toolkit.cmd +0 -2
- package/plugin/runtime/agent-toolkit/ulw-loop/cli.js +0 -3455
- package/plugin/skills/data-scientist/references/common-scenarios.md +0 -176
- package/plugin/skills/data-scientist/references/execution-templates.md +0 -197
- package/plugin/skills/data-scientist/references/integration-patterns.md +0 -153
- package/plugin/skills/data-scientist/references/performance-benchmarks.md +0 -37
- package/plugin/skills/debugging/references/tools/playwright-cli.md +0 -194
- package/plugin/skills/programming/scripts/rust/check-no-excuse-rules.py +0 -296
- 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
|
+
}
|