omo-ai 5.1.38 → 5.1.40
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/provider-map.json +1 -1
- package/bin/lib/setup-credentials.js +7 -1
- package/bin/lib/setup-import.js +13 -63
- package/bin/lib/setup-model-choices-import.js +8 -5
- package/bin/lib/setup-providers-import.js +11 -4
- package/bin/lib/setup-stream-items.js +64 -0
- package/bin/lib/setup-stream.d.ts +94 -0
- package/bin/lib/setup-stream.js +238 -0
- package/package.json +2 -2
- package/plugin/CHANGELOG.md +44 -0
- package/plugin/extensions/CaseFolding.txt +1654 -0
- package/plugin/extensions/gateway-rules-extension.mjs +1 -1
- package/plugin/extensions/gateway-store-worker.mjs +1 -1
- package/plugin/extensions/loro_wasm_bg.wasm +0 -0
- package/plugin/extensions/memory-run-supervisor.mjs +2 -2
- package/plugin/extensions/omo-computer-use.js +1 -1
- package/plugin/extensions/omo-init-deep-advisor.js +1 -1
- package/plugin/extensions/omo-member.js +2 -2
- package/plugin/extensions/omo-memory-doctor.js +2 -2
- package/plugin/extensions/omo-memory-memfs.js +2 -2
- package/plugin/extensions/omo-memory-palace.js +2 -2
- package/plugin/extensions/omo-memory-sync.js +2 -0
- package/plugin/extensions/omo-onboarding-scan.js +2 -0
- package/plugin/extensions/omo-recall-store.js +2 -0
- package/plugin/extensions/omo-session-history.js +2 -0
- package/plugin/extensions/omo-side-panel.js +2 -2
- package/plugin/extensions/omo-task.js +2 -2
- package/plugin/extensions/omo-telemetry-posthog.js +2 -0
- package/plugin/extensions/omo-thread-client.js +1 -1
- package/plugin/extensions/omo.js +2 -2
- package/plugin/package.json +5 -1
- package/plugin/runtime/agent-toolkit-sdk/sdk.js +2 -2
- package/plugin/runtime/ast-grep-mcp/manifest.json +1 -1
- package/plugin/runtime/category-coverage/index.js +2 -2
- package/plugin/runtime/rollback-migrate.js +1 -1
- package/plugin/runtime/task-config/index.js +1 -1
- package/plugin/runtime/thread-sdk/sdk.js +2 -2
- package/plugin/scripts/install.mjs +6 -0
- package/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/plugin/skills/git-master/SKILL.md +1 -1
- package/plugin/skills/onboarding/SKILL.md +56 -23
- package/plugin/skills/ulw-plan/SKILL.md +1 -1
- package/plugin/skills/ulw-plan/references/full-workflow.md +6 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Derived from @code-yeongyu/senpi 2026.10.19 builtinProviders(). builtinProviderIds and oauthProviderIds are the engine registry verbatim; providers maps a source harness id onto the omo provider that serves the same endpoint; oauthLogins maps a source OAuth id onto the omo provider to run /login for. Re-derive all four whenever the senpi pin changes.",
|
|
2
|
+
"_comment": "Derived from @code-yeongyu/senpi 2026.10.19-2 builtinProviders(). builtinProviderIds and oauthProviderIds are the engine registry verbatim; providers maps a source harness id onto the omo provider that serves the same endpoint; oauthLogins maps a source OAuth id onto the omo provider to run /login for. Re-derive all four whenever the senpi pin changes.",
|
|
3
3
|
"builtinProviderIds": [
|
|
4
4
|
"alibaba-token-plan",
|
|
5
5
|
"amazon-bedrock",
|
|
@@ -165,7 +165,13 @@ export async function planCredentials(runtime, target, customProviderIds) {
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
|
|
168
|
+
/**
|
|
169
|
+
* The stage's one writer. With `options.writeAuth` (the engine's locked credential store) the
|
|
170
|
+
* additions merge into whatever the store holds under its lock; the CLI path keeps its direct
|
|
171
|
+
* backup-and-write.
|
|
172
|
+
*/
|
|
173
|
+
export function applyCredentials(plan, options = {}) {
|
|
174
|
+
if (options.writeAuth !== undefined) return options.writeAuth(plan.target, plan.result.additions)
|
|
169
175
|
writeAuthStore(plan.target, plan.current, plan.result.additions)
|
|
170
176
|
}
|
|
171
177
|
|
package/bin/lib/setup-import.js
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `omo setup`: every stage's plan is built first (nothing is written while
|
|
3
|
-
* summary renders them all, one consent covers the whole plan, then each stage
|
|
4
|
-
* `--ask-each` asks per stage instead; `--yes` consents; `--dry-run` prints the
|
|
2
|
+
* `omo setup`: every stage's plan is built first from the setup stream (nothing is written while
|
|
3
|
+
* planning), the one summary renders them all, one consent covers the whole plan, then each stage
|
|
4
|
+
* applies its part. `--ask-each` asks per stage instead; `--yes` consents; `--dry-run` prints the
|
|
5
|
+
* summary and exits.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { homedir } from "node:os"
|
|
8
|
-
import { dirname,
|
|
9
|
+
import { dirname, relative } from "node:path"
|
|
9
10
|
import { createInterface } from "node:readline/promises"
|
|
10
11
|
import { canonicalAgentDir } from "./agent-dir.js"
|
|
11
12
|
import { setupCoverage } from "./category-coverage.js"
|
|
12
|
-
import {
|
|
13
|
-
import { applyCredentials, credentialCounts, credentialPlanLines, credentialQuestion, opencodeOauthProviders, planCredentials } from "./setup-credentials.js"
|
|
14
|
-
import { applyAssets, assetCounts, assetPlanLines, assetQuestion, planAssets } from "./setup-assets-import.js"
|
|
15
|
-
import { planOpencodeProviders } from "./setup-opencode-providers.js"
|
|
16
|
-
import { applyProviders, planProviders, providerCounts, providerPlanLines, providerQuestion } from "./setup-providers-import.js"
|
|
17
|
-
import { applyModelChoices, modelChoiceCounts, modelChoicePlanLines, modelChoiceQuestion, planModelChoices } from "./setup-model-choices-import.js"
|
|
13
|
+
import { applySetupStages, collectSetupPlans, scanSetup, setupStages } from "./setup-stream.js"
|
|
18
14
|
import { displayPath, formatSetupSummary, TELEMETRY_NOTICE } from "./setup-summary.js"
|
|
19
15
|
|
|
20
16
|
function write(lines) {
|
|
@@ -37,17 +33,6 @@ async function ask(question, options, defaultYes) {
|
|
|
37
33
|
}
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
// In apply order: the model-choice stage runs last because a default model or category may name a
|
|
41
|
-
// custom provider the stage before it carries. `present` stages report counts after the run.
|
|
42
|
-
function stageList(plans) {
|
|
43
|
-
return [
|
|
44
|
-
{ plan: plans.credentials, present: !plans.credentials.malformed, lines: credentialPlanLines, counts: credentialCounts, question: credentialQuestion, apply: applyCredentials },
|
|
45
|
-
{ plan: plans.assets, present: plans.assets.present, lines: assetPlanLines, counts: assetCounts, question: assetQuestion, apply: applyAssets },
|
|
46
|
-
{ plan: plans.providers, present: plans.providers.present, lines: providerPlanLines, counts: providerCounts, question: providerQuestion, apply: applyProviders },
|
|
47
|
-
{ plan: plans.modelChoices, present: plans.modelChoices.present, lines: modelChoicePlanLines, counts: modelChoiceCounts, question: modelChoiceQuestion, apply: applyModelChoices },
|
|
48
|
-
]
|
|
49
|
-
}
|
|
50
|
-
|
|
51
36
|
// The directories the whole plan writes into, a directory inside another one folded into it.
|
|
52
37
|
function targetRoots(plans, agentDir, home) {
|
|
53
38
|
const choiceDirs = plans.modelChoices.items.filter((item) => item.state === "pending").map((item) => dirname(item.target.path))
|
|
@@ -56,51 +41,20 @@ function targetRoots(plans, agentDir, home) {
|
|
|
56
41
|
return roots.filter((path) => !roots.some((root) => inside(path, root))).map((path) => displayPath(path, home))
|
|
57
42
|
}
|
|
58
43
|
|
|
59
|
-
async function planAll(runtime, agentDir) {
|
|
60
|
-
const providerSource = planOpencodeProviders(runtime)
|
|
61
|
-
// A provider the custom-provider stage described (carried or refused with its reason) is reported there only.
|
|
62
|
-
const described = new Set([...providerSource.providers.map((item) => item.id), ...providerSource.skipped])
|
|
63
|
-
const credentials = await planCredentials(runtime, join(agentDir, "auth.json"), described)
|
|
64
|
-
const assets = planAssets({ runtime, agentDir })
|
|
65
|
-
const providers = planProviders(providerSource, agentDir)
|
|
66
|
-
const oauthProviders = opencodeOauthProviders(runtime)
|
|
67
|
-
const planChoices = (customProviders) => planModelChoices({ runtime, agentDir, customProviders, oauthProviders })
|
|
68
|
-
return { credentials, assets, providers, modelChoices: await planChoices(providers.result.added), planChoices }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Per-stage consent. A declined provider stage leaves models.json as it is, so the model choices
|
|
72
|
-
// are re-planned against it rather than against providers that will not exist.
|
|
73
|
-
async function applyEach(plans, confirm) {
|
|
74
|
-
const applied = new Set()
|
|
75
|
-
for (const [index, stage] of stageList(plans).entries()) {
|
|
76
|
-
if (index === 3 && plans.providers.pending > 0 && !applied.has(2)) {
|
|
77
|
-
const shown = new Set(plans.modelChoices.notices)
|
|
78
|
-
plans.modelChoices = await plans.planChoices([])
|
|
79
|
-
stage.plan = plans.modelChoices
|
|
80
|
-
write(plans.modelChoices.notices.filter((notice) => !shown.has(notice)))
|
|
81
|
-
}
|
|
82
|
-
if (stage.plan.pending === 0) continue
|
|
83
|
-
if (!await confirm(stage.question(stage.plan), false)) continue
|
|
84
|
-
stage.apply(stage.plan)
|
|
85
|
-
applied.add(index)
|
|
86
|
-
}
|
|
87
|
-
return applied
|
|
88
|
-
}
|
|
89
|
-
|
|
90
44
|
export async function runSetup(args = process.argv.slice(2), options = {}) {
|
|
91
45
|
const home = options.home ?? homedir()
|
|
92
46
|
const env = options.env ?? process.env
|
|
93
47
|
const agentDir = canonicalAgentDir(env, home)
|
|
94
48
|
const runtime = { stdin: process.stdin, stdout: process.stdout, ...options, home, env }
|
|
95
49
|
const dryRun = args.includes("--dry-run")
|
|
96
|
-
|
|
97
|
-
const plans = await
|
|
50
|
+
// The CLI has always carried OpenCode's custom providers; the onboarding RPC leaves them opt-in.
|
|
51
|
+
const { inventory, plans } = await collectSetupPlans(scanSetup(runtime, agentDir, { includeCustomProviders: true }))
|
|
98
52
|
if (dryRun) process.stdout.write("DRY RUN: no files will be written\n")
|
|
99
53
|
const categories = await setupCoverage({ agentDir, home, env, plans, loadRuntime: options.loadCoverageRuntime, loadEngine: options.loadCoverageEngine })
|
|
100
54
|
process.stdout.write(formatSetupSummary({ home, agentDir, inventory, ...plans, categories }))
|
|
101
55
|
process.stdout.write(`${TELEMETRY_NOTICE}\n`)
|
|
102
56
|
if (dryRun) {
|
|
103
|
-
write(
|
|
57
|
+
write(setupStages(plans).filter((stage) => stage.present).flatMap((stage) => stage.lines(stage.plan)))
|
|
104
58
|
return
|
|
105
59
|
}
|
|
106
60
|
const confirm = async (question, defaultYes) => {
|
|
@@ -108,19 +62,15 @@ export async function runSetup(args = process.argv.slice(2), options = {}) {
|
|
|
108
62
|
if (!accepted && runtime.stdin.isTTY === true) process.stdout.write("Import cancelled\n")
|
|
109
63
|
return accepted
|
|
110
64
|
}
|
|
111
|
-
const pending =
|
|
65
|
+
const pending = setupStages(plans).reduce((total, stage) => total + stage.plan.pending, 0)
|
|
112
66
|
let applied = new Set()
|
|
113
67
|
if (pending === 0) {
|
|
114
68
|
process.stdout.write("Nothing new to import.\n")
|
|
115
69
|
} else if (args.includes("--ask-each")) {
|
|
116
|
-
applied = await
|
|
70
|
+
applied = await applySetupStages(plans, (stage) => confirm(stage.question(stage.plan), false), (line) => write([line]))
|
|
117
71
|
} else {
|
|
118
72
|
if (!await confirm(`Import all of the above into ${targetRoots(plans, agentDir, home).join(" and ")}? [Y/n] `, true)) return
|
|
119
|
-
|
|
120
|
-
if (stage.plan.pending === 0) continue
|
|
121
|
-
stage.apply(stage.plan)
|
|
122
|
-
applied.add(index)
|
|
123
|
-
}
|
|
73
|
+
applied = await applySetupStages(plans, () => true, (line) => write([line]))
|
|
124
74
|
}
|
|
125
|
-
write(
|
|
75
|
+
write(setupStages(plans).filter((stage) => stage.present && (applied.has(stage.id) || stage.plan.pending === 0)).flatMap((stage) => stage.counts(stage.plan)))
|
|
126
76
|
}
|
|
@@ -151,7 +151,7 @@ function writeText(target, text) {
|
|
|
151
151
|
|
|
152
152
|
// Every edit is an insertion into the file's own text (comments, order and formatting survive);
|
|
153
153
|
// a file the edit cannot reproduce exactly is left untouched and reported.
|
|
154
|
-
function writeTarget(target, items) {
|
|
154
|
+
function writeTarget(target, items, report) {
|
|
155
155
|
try {
|
|
156
156
|
let text = target.text ?? "{}\n"
|
|
157
157
|
for (const item of items) {
|
|
@@ -160,7 +160,7 @@ function writeTarget(target, items) {
|
|
|
160
160
|
writeText(target, text)
|
|
161
161
|
return items.map((item) => item.label)
|
|
162
162
|
} catch (error) {
|
|
163
|
-
|
|
163
|
+
report(`WARN senpi: could not edit ${target.path} safely (${error.message}); these model choices were not written: ${items.map((item) => item.label).join(", ")}`)
|
|
164
164
|
return []
|
|
165
165
|
}
|
|
166
166
|
}
|
|
@@ -198,9 +198,12 @@ export async function planModelChoices(stage) {
|
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
/** The stage's one writer; records what it wrote as `plan.carried` for the counts.
|
|
202
|
-
|
|
201
|
+
/** The stage's one writer; records what it wrote as `plan.carried` for the counts. `options.report`
|
|
202
|
+
* receives the warning when a target cannot be edited (the CLI prints it; callers inside the
|
|
203
|
+
* engine route it to their own reply). */
|
|
204
|
+
export function applyModelChoices(plan, options = {}) {
|
|
205
|
+
const report = options.report ?? ((line) => process.stdout.write(`${line}\n`))
|
|
203
206
|
const pending = plan.items.filter((item) => item.state === "pending")
|
|
204
207
|
const targets = [...new Set(pending.map((item) => item.target))]
|
|
205
|
-
plan.carried = targets.flatMap((target) => writeTarget(target, pending.filter((item) => item.target === target)))
|
|
208
|
+
plan.carried = targets.flatMap((target) => writeTarget(target, pending.filter((item) => item.target === target), report))
|
|
206
209
|
}
|
|
@@ -101,18 +101,25 @@ function writeModels(path, target, added) {
|
|
|
101
101
|
export function planProviders(source, agentDir) {
|
|
102
102
|
const paths = { models: join(agentDir, "models.json"), auth: join(agentDir, "auth.json") }
|
|
103
103
|
const models = readModelsTarget(paths.models)
|
|
104
|
-
const
|
|
104
|
+
const auth = readAuthStore(paths.auth)
|
|
105
|
+
const result = classify(source, models, auth)
|
|
105
106
|
const notices = [...source.notices]
|
|
106
107
|
if (result.blocked.length > 0) notices.push(`WARN senpi: malformed models.json; these custom providers were not imported: ${result.blocked.join(", ")}`)
|
|
107
|
-
return { paths, models, notices, result, skipped: source.skipped, present: source.providers.length > 0, pending: result.added.length }
|
|
108
|
+
return { paths, models, authBytes: auth.bytes, notices, result, skipped: source.skipped, present: source.providers.length > 0, pending: result.added.length }
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
/**
|
|
112
|
+
* The stage's one writer. With `options.writeAuth` the provider keys merge into whatever the
|
|
113
|
+
* store holds under its lock; the CLI path keeps its direct backup-and-write.
|
|
114
|
+
*/
|
|
115
|
+
export function applyProviders(plan, options = {}) {
|
|
111
116
|
writeModels(plan.paths.models, plan.models, plan.result.added)
|
|
112
117
|
// Re-read: the credential stage of the same run may have just written auth.json, after this plan
|
|
113
118
|
// was classified. A mapped credential target can also be a custom provider id (claude-sdk-oauth ->
|
|
114
119
|
// anthropic-subscription), so a key written since the plan is kept, never overwritten.
|
|
115
120
|
const auth = readAuthStore(plan.paths.auth)
|
|
116
121
|
plan.result.keys = auth.malformed ? [] : plan.result.keys.filter((item) => !Object.hasOwn(auth.entries, item.provider))
|
|
117
|
-
if (plan.result.keys.length
|
|
122
|
+
if (plan.result.keys.length === 0) return
|
|
123
|
+
if (options.writeAuth !== undefined) return options.writeAuth(plan.paths.auth, plan.result.keys)
|
|
124
|
+
writeAuthStore(plan.paths.auth, auth, plan.result.keys)
|
|
118
125
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One `{id, kind, state, detail?}` row per thing a setup source found, built from the stage plan
|
|
3
|
+
* the summary renders. Rows travel over RPC, so they carry ids and states only: never a key.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const MODEL_CHOICE_STATES = { pending: "import", kept: "exists", carried: "carried", blocked: "blocked" }
|
|
7
|
+
|
|
8
|
+
function rows(ids, kind, state) {
|
|
9
|
+
return ids.map((id) => ({ id, kind, state }))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function detectItems(inventory) {
|
|
13
|
+
return inventory.harnesses
|
|
14
|
+
.filter((harness) => harness.installed)
|
|
15
|
+
.map((harness) => ({ id: harness.id, kind: "harness", state: "found", detail: `${harness.entryCount} login(s), ${harness.importableCount} importable` }))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function providerItems(plan) {
|
|
19
|
+
const { result } = plan
|
|
20
|
+
return [
|
|
21
|
+
...result.added.map((provider) => ({ id: provider.id, kind: "provider", state: "import", detail: `${provider.config.models.length} model(s)` })),
|
|
22
|
+
...rows(result.skippedExisting, "provider", "exists"),
|
|
23
|
+
...rows(result.blocked, "provider", "blocked"),
|
|
24
|
+
...rows(plan.skipped, "provider", "skipped"),
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function credentialItems(plan) {
|
|
29
|
+
if (plan.malformed) return []
|
|
30
|
+
const { result, guidance } = plan
|
|
31
|
+
return [
|
|
32
|
+
...result.additions.map((item) => ({ id: item.provider, kind: "api-key", state: "import", detail: `from ${item.source}` })),
|
|
33
|
+
...rows(result.skippedExisting, "api-key", "exists"),
|
|
34
|
+
...guidance.logins.map((login) => ({ id: login.provider, kind: "oauth", state: login.state, ...(login.target === undefined ? {} : { detail: `/login ${login.target}` }) })),
|
|
35
|
+
...rows(result.skippedUnmapped, "api-key", "unmapped"),
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assetItems({ result }) {
|
|
40
|
+
return [
|
|
41
|
+
...result.servers.added.map((server) => ({ id: server.name, kind: "mcp", state: "import" })),
|
|
42
|
+
...rows(result.servers.skippedExisting, "mcp", "exists"),
|
|
43
|
+
...rows(result.servers.blocked, "mcp", "blocked"),
|
|
44
|
+
...result.skills.added.map((skill) => ({ id: skill.name, kind: "skill", state: "import" })),
|
|
45
|
+
...rows(result.skills.skippedExisting, "skill", "exists"),
|
|
46
|
+
...rows(result.skills.skippedBundled, "skill", "bundled"),
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function modelChoiceItems(plan) {
|
|
51
|
+
return plan.items.map((item) => ({ id: item.label, kind: item.kind, state: MODEL_CHOICE_STATES[item.state] ?? item.state }))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ITEMS_BY_SOURCE = {
|
|
55
|
+
detect: detectItems,
|
|
56
|
+
providers: providerItems,
|
|
57
|
+
credentials: credentialItems,
|
|
58
|
+
assets: assetItems,
|
|
59
|
+
"model-choices": modelChoiceItems,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function setupStageItems(source, plan) {
|
|
63
|
+
return ITEMS_BY_SOURCE[source](plan)
|
|
64
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Types for setup-stream.js, which omo-senpi's onboarding scan imports across the package
|
|
2
|
+
// boundary: omo-ai ships this module as plain JS, so it is the single source of the setup stream.
|
|
3
|
+
|
|
4
|
+
export type SetupSource = "detect" | "providers" | "credentials" | "assets" | "model-choices"
|
|
5
|
+
export type SetupStageId = "credentials" | "assets" | "providers" | "model-choices"
|
|
6
|
+
|
|
7
|
+
export const SETUP_SOURCES: readonly SetupSource[]
|
|
8
|
+
|
|
9
|
+
export type SetupItem = {
|
|
10
|
+
readonly id: string
|
|
11
|
+
readonly kind: string
|
|
12
|
+
readonly state: string
|
|
13
|
+
readonly detail?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type SetupStagePlan = {
|
|
17
|
+
readonly pending: number
|
|
18
|
+
readonly notices?: readonly string[]
|
|
19
|
+
readonly [field: string]: unknown
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type SetupStageResult = {
|
|
23
|
+
readonly source: SetupSource
|
|
24
|
+
readonly items: readonly SetupItem[]
|
|
25
|
+
readonly notices: readonly string[]
|
|
26
|
+
readonly plan: SetupStagePlan
|
|
27
|
+
readonly disabled?: true
|
|
28
|
+
readonly error?: Error
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type SetupRuntime = {
|
|
32
|
+
readonly home: string
|
|
33
|
+
readonly env: Readonly<Record<string, string | undefined>>
|
|
34
|
+
readonly pluginRoot?: string
|
|
35
|
+
readonly loadModelCatalog?: () => Promise<readonly unknown[]>
|
|
36
|
+
readonly loadSqlite?: () => Promise<unknown>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type SetupScanOptions = {
|
|
40
|
+
readonly includeCustomProviders?: boolean
|
|
41
|
+
readonly onSourceStart?: (source: SetupSource) => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type SetupInventory = {
|
|
45
|
+
readonly harnesses: readonly { readonly id: string; readonly installed: boolean }[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type SetupPlans = { readonly [stage: string]: unknown }
|
|
49
|
+
|
|
50
|
+
export type SetupApplyStage = {
|
|
51
|
+
readonly id: SetupStageId
|
|
52
|
+
readonly plan: SetupStagePlan
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function canonicalAgentDir(env?: Readonly<Record<string, string | undefined>>, home?: string): string
|
|
56
|
+
|
|
57
|
+
export function scanSetup(runtime: SetupRuntime, agentDir: string, options?: SetupScanOptions): AsyncGenerator<SetupStageResult, void, undefined>
|
|
58
|
+
|
|
59
|
+
export function collectSetupPlans(stream: AsyncIterable<SetupStageResult> | Iterable<SetupStageResult>): Promise<{ inventory: SetupInventory; plans: SetupPlans }>
|
|
60
|
+
|
|
61
|
+
export type SetupAuthEntry = {
|
|
62
|
+
readonly provider: string
|
|
63
|
+
readonly key: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Per-apply writer options: how credential additions and notice lines travel. */
|
|
67
|
+
export type SetupApplyOptions = {
|
|
68
|
+
/** Routes credential additions through a locked credential store (the engine's AuthStorage) instead of the CLI's direct writer. */
|
|
69
|
+
readonly writeAuth?: (target: string, additions: readonly SetupAuthEntry[]) => Promise<void> | void
|
|
70
|
+
/** Receives the apply's notice lines (the CLI prints them; an RPC caller collects them into its reply). */
|
|
71
|
+
readonly report?: (line: string) => void
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Refused by `applySetupStages` when `staleGuard` finds a target that changed since planning. */
|
|
75
|
+
export class SetupStaleError extends Error {
|
|
76
|
+
readonly paths: readonly string[]
|
|
77
|
+
readonly applied: readonly SetupStageId[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The target paths of the given stages whose content changed since their plans read them (content hash, never mtime alone). */
|
|
81
|
+
export function staleSetupTargets(plans: SetupPlans, stageIds: Iterable<SetupStageId>, written?: ReadonlySet<string>): string[]
|
|
82
|
+
|
|
83
|
+
/** True when any of the given stages would write into a credential store. */
|
|
84
|
+
export function authWritesPending(plans: SetupPlans, stageIds: Iterable<SetupStageId>): boolean
|
|
85
|
+
|
|
86
|
+
export function applySetupStages(
|
|
87
|
+
plans: SetupPlans,
|
|
88
|
+
confirm: (stage: SetupApplyStage) => boolean | Promise<boolean>,
|
|
89
|
+
report?: (line: string) => void,
|
|
90
|
+
options?: SetupApplyOptions & {
|
|
91
|
+
/** Re-checked right before each stage writes; a non-empty result refuses that stage with `SetupStaleError`. */
|
|
92
|
+
readonly staleGuard?: (stageId: SetupStageId, written: ReadonlySet<string>) => readonly string[] | undefined
|
|
93
|
+
},
|
|
94
|
+
): Promise<Set<SetupStageId>>
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The detection and planning half of `omo setup` as one stream: each source is read, classified
|
|
3
|
+
* and yielded on its own, in the order later sources depend on. `omo setup` and the engine's
|
|
4
|
+
* onboarding scan RPC (omo-senpi `components/onboarding/scan-runtime.ts`) both consume it, so the
|
|
5
|
+
* import logic exists once. Nothing is written until a caller hands the collected plans to
|
|
6
|
+
* `applySetupStages`. No source opens a socket or calls a model: the model-choice stage reads the
|
|
7
|
+
* engine's offline model catalog only.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createHash } from "node:crypto"
|
|
11
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
12
|
+
import { join, resolve } from "node:path"
|
|
13
|
+
import { detectHarnesses } from "./setup-detect.js"
|
|
14
|
+
import { applyCredentials, credentialCounts, credentialPlanLines, credentialQuestion, opencodeOauthProviders, planCredentials } from "./setup-credentials.js"
|
|
15
|
+
import { applyAssets, assetCounts, assetPlanLines, assetQuestion, planAssets } from "./setup-assets-import.js"
|
|
16
|
+
import { planOpencodeProviders } from "./setup-opencode-providers.js"
|
|
17
|
+
import { applyProviders, planProviders, providerCounts, providerPlanLines, providerQuestion } from "./setup-providers-import.js"
|
|
18
|
+
import { applyModelChoices, modelChoiceCounts, modelChoicePlanLines, modelChoiceQuestion, planModelChoices } from "./setup-model-choices-import.js"
|
|
19
|
+
import { setupStageItems } from "./setup-stream-items.js"
|
|
20
|
+
|
|
21
|
+
export { canonicalAgentDir } from "./agent-dir.js"
|
|
22
|
+
|
|
23
|
+
/** Every source, in stream order: a later source reads what an earlier one decided. */
|
|
24
|
+
export const SETUP_SOURCES = ["detect", "providers", "credentials", "assets", "model-choices"]
|
|
25
|
+
|
|
26
|
+
const NO_PROVIDERS = { providers: [], skipped: [], notices: [] }
|
|
27
|
+
|
|
28
|
+
async function settle(work) {
|
|
29
|
+
try {
|
|
30
|
+
return { plan: await work() }
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return { error: error instanceof Error ? error : new Error(String(error)) }
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function stageResult(source, settled, disabled = false) {
|
|
37
|
+
if (settled.error !== undefined) return { source, items: [], notices: [], plan: { pending: 0 }, error: settled.error }
|
|
38
|
+
const plan = settled.plan
|
|
39
|
+
return { source, items: disabled ? [] : setupStageItems(source, plan), notices: plan.notices ?? [], plan, ...(disabled ? { disabled: true } : {}) }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Streams every source's plan. Custom providers (OpenCode `provider.<id>` blocks and catalog
|
|
44
|
+
* providers) are read only with `includeCustomProviders`; without it the provider stage yields
|
|
45
|
+
* disabled and empty, and a key for such a provider stays unmapped. `onSourceStart` is called
|
|
46
|
+
* right before an enabled source starts reading. A source that throws yields its `error` and the
|
|
47
|
+
* stream goes on with the sources after it.
|
|
48
|
+
*/
|
|
49
|
+
export async function* scanSetup(runtime, agentDir, options = {}) {
|
|
50
|
+
const includeCustomProviders = options.includeCustomProviders === true
|
|
51
|
+
const start = (source) => options.onSourceStart?.(source)
|
|
52
|
+
|
|
53
|
+
start("detect")
|
|
54
|
+
const detect = await settle(() => detectHarnesses(runtime))
|
|
55
|
+
yield stageResult("detect", detect)
|
|
56
|
+
|
|
57
|
+
if (includeCustomProviders) start("providers")
|
|
58
|
+
let providerSource = NO_PROVIDERS
|
|
59
|
+
const providers = await settle(() => {
|
|
60
|
+
if (includeCustomProviders) providerSource = planOpencodeProviders(runtime)
|
|
61
|
+
return planProviders(providerSource, agentDir)
|
|
62
|
+
})
|
|
63
|
+
if (providers.error !== undefined) providerSource = NO_PROVIDERS
|
|
64
|
+
yield stageResult("providers", providers, !includeCustomProviders)
|
|
65
|
+
|
|
66
|
+
start("credentials")
|
|
67
|
+
// A provider the custom-provider stage described (carried or refused with its reason) is reported there only.
|
|
68
|
+
const described = new Set([...providerSource.providers.map((item) => item.id), ...providerSource.skipped])
|
|
69
|
+
yield stageResult("credentials", await settle(() => planCredentials(runtime, join(agentDir, "auth.json"), described)))
|
|
70
|
+
|
|
71
|
+
start("assets")
|
|
72
|
+
yield stageResult("assets", await settle(() => planAssets({ runtime, agentDir })))
|
|
73
|
+
|
|
74
|
+
start("model-choices")
|
|
75
|
+
let planChoices
|
|
76
|
+
const modelChoices = await settle(() => {
|
|
77
|
+
const oauthProviders = opencodeOauthProviders(runtime)
|
|
78
|
+
planChoices = (customProviders) => planModelChoices({ runtime, agentDir, customProviders, oauthProviders })
|
|
79
|
+
return planChoices(providers.plan?.result.added ?? [])
|
|
80
|
+
})
|
|
81
|
+
yield { ...stageResult("model-choices", modelChoices), ...(planChoices === undefined ? {} : { replan: planChoices }) }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Drains a stream into the detect inventory and the stage plans `omo setup` renders and applies.
|
|
86
|
+
* The CLI cannot summarize a partial plan, so the first failed source is rethrown.
|
|
87
|
+
*/
|
|
88
|
+
export async function collectSetupPlans(stream) {
|
|
89
|
+
const plans = {}
|
|
90
|
+
let inventory = { harnesses: [] }
|
|
91
|
+
for await (const result of stream) {
|
|
92
|
+
if (result.error !== undefined) throw result.error
|
|
93
|
+
if (result.source === "detect") inventory = result.plan
|
|
94
|
+
else if (result.source === "model-choices") {
|
|
95
|
+
plans.modelChoices = result.plan
|
|
96
|
+
plans.planChoices = result.replan
|
|
97
|
+
} else plans[result.source] = result.plan
|
|
98
|
+
}
|
|
99
|
+
return { inventory, plans }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* In apply order: the model-choice stage runs last because a default model or category may name a
|
|
104
|
+
* custom provider the stage before it carries. `present` stages report counts after the run. A
|
|
105
|
+
* stage whose source failed has no plan and is never applied.
|
|
106
|
+
*/
|
|
107
|
+
export function setupStages(plans) {
|
|
108
|
+
return [
|
|
109
|
+
{ id: "credentials", plan: plans.credentials, present: plans.credentials !== undefined && !plans.credentials.malformed, lines: credentialPlanLines, counts: credentialCounts, question: credentialQuestion, apply: (plan, options) => applyCredentials(plan, options) },
|
|
110
|
+
{ id: "assets", plan: plans.assets, present: plans.assets?.present === true, lines: assetPlanLines, counts: assetCounts, question: assetQuestion, apply: (plan) => applyAssets(plan) },
|
|
111
|
+
{ id: "providers", plan: plans.providers, present: plans.providers?.present === true, lines: providerPlanLines, counts: providerCounts, question: providerQuestion, apply: (plan, options) => applyProviders(plan, options) },
|
|
112
|
+
{ id: "model-choices", plan: plans.modelChoices, present: plans.modelChoices?.present === true, lines: modelChoicePlanLines, counts: modelChoiceCounts, question: modelChoiceQuestion, apply: (plan, options) => applyModelChoices(plan, options) },
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Thrown by `applySetupStages` when its `staleGuard` finds a target that changed since planning. */
|
|
117
|
+
export class SetupStaleError extends Error {
|
|
118
|
+
constructor(paths, applied) {
|
|
119
|
+
super(`setup targets changed since planning: ${paths.join(", ")}`)
|
|
120
|
+
this.name = "SetupStaleError"
|
|
121
|
+
this.paths = paths
|
|
122
|
+
this.applied = applied
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Every plan holds the bytes its target had when it was read. A target whose content hash no longer
|
|
127
|
+
// matches would be overwritten with the scan-time copy, so it counts as stale; an absent file is its
|
|
128
|
+
// own expected state, and mtime alone never decides anything.
|
|
129
|
+
function targetDigest(bytes) {
|
|
130
|
+
return createHash("sha256").update(bytes === undefined ? "\0absent" : bytes).digest("hex")
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function staleTarget(path, expectedBytes) {
|
|
134
|
+
let current
|
|
135
|
+
try {
|
|
136
|
+
current = readFileSync(path, "utf8")
|
|
137
|
+
} catch {
|
|
138
|
+
current = undefined
|
|
139
|
+
}
|
|
140
|
+
return targetDigest(current) !== targetDigest(expectedBytes)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A skill directory planned as new is stale once anything exists at its path: it is a directory, so
|
|
144
|
+
// reading it as a file would always look absent (#10496 review).
|
|
145
|
+
function staleAbsentPath(path) {
|
|
146
|
+
return existsSync(path)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The target paths of the given stages whose content changed since their plans read them. Paths in
|
|
151
|
+
* `written` were rewritten by an earlier stage of the same apply (the credentials stage and the
|
|
152
|
+
* provider keys share auth.json), so they are not the user's change and do not count (#10496 review).
|
|
153
|
+
*/
|
|
154
|
+
export function staleSetupTargets(plans, stageIds, written = new Set()) {
|
|
155
|
+
const changed = new Set()
|
|
156
|
+
const mark = (path, expectedBytes) => {
|
|
157
|
+
if (written.has(resolve(path))) return
|
|
158
|
+
if (staleTarget(path, expectedBytes)) changed.add(path)
|
|
159
|
+
}
|
|
160
|
+
const markAbsent = (path) => {
|
|
161
|
+
if (written.has(resolve(path))) return
|
|
162
|
+
if (staleAbsentPath(path)) changed.add(path)
|
|
163
|
+
}
|
|
164
|
+
for (const id of stageIds) {
|
|
165
|
+
if (id === "credentials") {
|
|
166
|
+
const plan = plans.credentials
|
|
167
|
+
if (plan === undefined || plan.malformed || plan.pending === 0) continue
|
|
168
|
+
mark(plan.target, plan.current?.bytes)
|
|
169
|
+
} else if (id === "assets") {
|
|
170
|
+
const plan = plans.assets
|
|
171
|
+
if (plan?.present !== true || plan.pending === 0) continue
|
|
172
|
+
if (plan.result.target.malformed !== true) mark(plan.paths.mcp, plan.result.target.bytes)
|
|
173
|
+
for (const skill of plan.result.skills.added) markAbsent(join(plan.paths.skills, skill.name))
|
|
174
|
+
} else if (id === "providers") {
|
|
175
|
+
const plan = plans.providers
|
|
176
|
+
if (plan?.present !== true || plan.pending === 0) continue
|
|
177
|
+
if (plan.models.malformed !== true) mark(plan.paths.models, plan.models.bytes)
|
|
178
|
+
if ((plan.result.keys?.length ?? 0) > 0) mark(plan.paths.auth, plan.authBytes)
|
|
179
|
+
} else if (id === "model-choices") {
|
|
180
|
+
const plan = plans.modelChoices
|
|
181
|
+
if (plan?.present !== true || plan.pending === 0) continue
|
|
182
|
+
for (const item of plan.items) {
|
|
183
|
+
if (item.state === "pending" && item.target.malformed !== true) mark(item.target.path, item.target.text)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return [...changed]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The files a stage writes when it applies (resolved), so later stages of one apply can tell them apart. */
|
|
191
|
+
function stageWrittenPaths(plans, id) {
|
|
192
|
+
if (id === "credentials") return plans.credentials?.target === undefined ? [] : [plans.credentials.target]
|
|
193
|
+
if (id === "assets") return plans.assets?.paths?.mcp === undefined ? [] : [plans.assets.paths.mcp, ...(plans.assets.result?.skills?.added ?? []).map((skill) => join(plans.assets.paths.skills, skill.name))]
|
|
194
|
+
if (id === "providers") return [plans.providers?.paths?.models, plans.providers?.paths?.auth].filter((path) => path !== undefined)
|
|
195
|
+
if (id === "model-choices") return (plans.modelChoices?.items ?? []).map((item) => item.target?.path).filter((path) => path !== undefined)
|
|
196
|
+
return []
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** True when any of the given stages would write into a credential store. */
|
|
200
|
+
export function authWritesPending(plans, stageIds) {
|
|
201
|
+
const ids = new Set(stageIds)
|
|
202
|
+
const credentials = ids.has("credentials") && plans.credentials !== undefined && !plans.credentials.malformed && plans.credentials.pending > 0
|
|
203
|
+
const keys = ids.has("providers") && plans.providers !== undefined && (plans.providers.result?.keys?.length ?? 0) > 0
|
|
204
|
+
return credentials || keys
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Applies every stage `confirm` accepts and returns the ids it applied. A declined provider stage
|
|
209
|
+
* leaves models.json as it is, so the model choices are re-planned against it rather than against
|
|
210
|
+
* providers that will not exist; the notices that re-plan adds go to `report`. `options.writeAuth`
|
|
211
|
+
* routes credential additions through a locked credential store instead of the CLI's direct writer,
|
|
212
|
+
* and `options.staleGuard`, re-checked right before each write, refuses a stage whose target
|
|
213
|
+
* changed since planning by throwing `SetupStaleError`.
|
|
214
|
+
*/
|
|
215
|
+
export async function applySetupStages(plans, confirm, report = () => {}, options = {}) {
|
|
216
|
+
const applied = new Set()
|
|
217
|
+
// Files this apply already rewrote: a later stage's stale check must not mistake them for a
|
|
218
|
+
// user's change (the credentials stage and the provider keys share auth.json; #10496 review).
|
|
219
|
+
const written = new Set()
|
|
220
|
+
for (const stage of setupStages(plans)) {
|
|
221
|
+
if (stage.id === "model-choices" && stage.plan !== undefined && plans.planChoices !== undefined && (plans.providers?.pending ?? 0) > 0 && !applied.has("providers")) {
|
|
222
|
+
const shown = new Set(plans.modelChoices.notices)
|
|
223
|
+
plans.modelChoices = await plans.planChoices([])
|
|
224
|
+
stage.plan = plans.modelChoices
|
|
225
|
+
for (const notice of plans.modelChoices.notices) if (!shown.has(notice)) report(notice)
|
|
226
|
+
}
|
|
227
|
+
if (stage.plan === undefined || stage.plan.pending === 0) continue
|
|
228
|
+
if (!await confirm(stage)) continue
|
|
229
|
+
if (options.staleGuard !== undefined) {
|
|
230
|
+
const stale = options.staleGuard(stage.id, written)
|
|
231
|
+
if (stale !== undefined && stale.length > 0) throw new SetupStaleError(stale, [...applied])
|
|
232
|
+
}
|
|
233
|
+
await stage.apply(stage.plan, { ...options, report })
|
|
234
|
+
applied.add(stage.id)
|
|
235
|
+
for (const path of stageWrittenPaths(plans, stage.id)) written.add(resolve(path))
|
|
236
|
+
}
|
|
237
|
+
return applied
|
|
238
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omo-ai",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.40",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OmO Native - the standalone omo command with the OMO extension built in: bun add -g omo-ai",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"postinstall": "node bin/senpi-patch.mjs"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@code-yeongyu/senpi": "2026.10.19"
|
|
20
|
+
"@code-yeongyu/senpi": "2026.10.19-2"
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|