omo-ai 5.1.0 → 5.1.2
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/category-coverage.js +5 -1
- package/bin/lib/claude-code-floor.js +8 -4
- package/bin/lib/daemon-args.js +54 -0
- package/bin/lib/daemon-doctor-report.js +30 -0
- package/bin/lib/daemon-operations.js +190 -0
- package/bin/lib/daemon-rollback.js +158 -0
- package/bin/lib/daemon-status.js +128 -0
- package/bin/lib/daemon.js +109 -66
- package/bin/lib/doctor-migration.js +22 -7
- package/bin/lib/doctor-pi-config.js +70 -0
- package/bin/lib/doctor.js +2 -0
- package/bin/lib/engine-dependency.js +23 -0
- package/bin/lib/engine-models.js +5 -2
- package/bin/lib/launcher.js +15 -0
- package/bin/lib/provider-map.json +1 -1
- package/bin/lib/setup-opencode-models.d.ts +53 -0
- package/bin/lib/setup-opencode-models.js +121 -34
- package/package.json +2 -3
- package/plugin/CHANGELOG.md +80 -0
- package/plugin/extensions/memory-run-supervisor.mjs +1 -1
- package/plugin/extensions/omo-computer-use.js +1 -1
- 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/package.json +1 -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/assets.generated.json +7 -0
- package/plugin/runtime/category-coverage/index.js +4 -4
- package/plugin/runtime/rollback-migrate.js +2 -0
- package/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/plugin/skills/hyperplan/SKILL.md +1 -0
- package/plugin/skills/init-deep/SKILL.md +1 -0
- package/plugin/skills/mass-ulw/SKILL.md +1 -0
- package/plugin/skills/refactor/SKILL.md +1 -0
- package/plugin/skills/remove-ai-slops/SKILL.md +1 -0
- package/plugin/skills/ulw-execute/SKILL.md +1 -0
- package/plugin/skills/ulw-loop/SKILL.md +1 -0
- package/plugin/skills/ulw-plan/SKILL.md +1 -0
- package/plugin/skills/ulw-research/SKILL.md +1 -0
package/bin/lib/daemon.js
CHANGED
|
@@ -1,45 +1,53 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs"
|
|
2
2
|
import { join } from "node:path"
|
|
3
|
+
import {
|
|
4
|
+
parseEngineLine,
|
|
5
|
+
parseStoreArgs,
|
|
6
|
+
hostCommandEnvironment,
|
|
7
|
+
runGc,
|
|
8
|
+
runHandoff,
|
|
9
|
+
runStatus,
|
|
10
|
+
runStopAll,
|
|
11
|
+
} from "./daemon-operations.js"
|
|
12
|
+
import { runRollbackPrepare } from "./daemon-rollback.js"
|
|
13
|
+
import { attachLaunchArgs, blockingPause, DAEMON_EXIT, readTimeoutSeconds } from "./daemon-args.js"
|
|
14
|
+
|
|
15
|
+
export { DAEMON_EXIT } from "./daemon-args.js"
|
|
16
|
+
export { daemonReportLines } from "./daemon-doctor-report.js"
|
|
3
17
|
|
|
4
18
|
/**
|
|
5
|
-
* `omo daemon` - the operator's view of
|
|
19
|
+
* `omo daemon` - the operator's view of every engine host in one agent dir: the operator daemon on
|
|
20
|
+
* `rpc.sock` (the only endpoint `run` and `attach` ensure), each session's task host (`p-*`), each
|
|
21
|
+
* Desktop thread host (`i-*`), and any other endpoint the engine enumerates. `status`, `gc`,
|
|
22
|
+
* `handoff`, `stop --all` and `rollback-prepare` cover all of them.
|
|
6
23
|
*
|
|
7
|
-
* Everything that decides WHO serves
|
|
24
|
+
* Everything that decides WHO serves a socket lives in the engine (`senpi host`): probing an
|
|
8
25
|
* existing host, comparing build ordinals, handing a generation over, refusing when the two sides
|
|
9
26
|
* cannot agree. This wrapper owns three much smaller things, and deliberately nothing else:
|
|
10
27
|
* omo's launch spec is the argv source, omo.json is where the policy comes from, and the caller
|
|
11
28
|
* gets an exit code it can branch on without reading prose.
|
|
12
29
|
*/
|
|
13
30
|
|
|
14
|
-
const SUBCOMMANDS = new Set(["run", "attach", "status", "stop", "handoff"])
|
|
15
|
-
/** Flags this wrapper consumes itself; anything else after `attach` belongs to the launch. */
|
|
16
|
-
const DAEMON_FLAGS = new Set(["--json", "--no-upgrade", "--persistent", "--foreground", "--include-workers", "--drain"])
|
|
31
|
+
const SUBCOMMANDS = new Set(["run", "attach", "status", "stop", "handoff", "gc", "rollback-prepare"])
|
|
17
32
|
/** The subcommands that can bring a host into existence, and therefore need omo's argv source. */
|
|
18
33
|
const NEEDS_SPEC = new Set(["run", "attach", "handoff"])
|
|
19
34
|
|
|
20
|
-
/** A named code per outcome, so a script never has to parse the message to know what happened. */
|
|
21
|
-
export const DAEMON_EXIT = {
|
|
22
|
-
ok: 0,
|
|
23
|
-
usage: 2,
|
|
24
|
-
notRunning: 3,
|
|
25
|
-
unsupported: 4,
|
|
26
|
-
engineRefused: 5,
|
|
27
|
-
}
|
|
28
|
-
|
|
29
35
|
const USAGE = [
|
|
30
|
-
"usage: omo daemon <run|attach|status|stop|handoff> [options]",
|
|
36
|
+
"usage: omo daemon <run|attach|status|stop|handoff|gc|rollback-prepare> [options]",
|
|
31
37
|
"",
|
|
32
38
|
" run ensure a daemon is serving this agent dir (start, reuse, or hand off)",
|
|
33
39
|
" attach print the environment a child needs to reach the daemon",
|
|
34
40
|
" status report who is serving and which sessions exist",
|
|
35
41
|
" stop end the daemon; --drain lets in-flight work finish first",
|
|
36
42
|
" handoff hand the socket to this build, keeping live sessions",
|
|
43
|
+
" gc remove dead endpoint state; --prune-store-index drops missing store paths",
|
|
44
|
+
" rollback-prepare migrate retained task records back to rpc.sock before downgrading",
|
|
37
45
|
"",
|
|
38
46
|
" --json print the engine's JSON line instead of a summary",
|
|
39
47
|
" --no-upgrade never hand off, even from an older build",
|
|
40
|
-
" --persistent outlive the process that started it",
|
|
41
|
-
" --foreground run the host in this process instead of detaching",
|
|
42
48
|
" --include-workers include subagent sessions in status",
|
|
49
|
+
" --all apply stop to every discovered endpoint",
|
|
50
|
+
" --wait with stop --drain --all, wait for generations and claims to end",
|
|
43
51
|
].join("\n")
|
|
44
52
|
|
|
45
53
|
/**
|
|
@@ -71,32 +79,16 @@ function readConfig(agentDir) {
|
|
|
71
79
|
*/
|
|
72
80
|
const ENGINE_SUBCOMMAND = { run: "ensure", attach: "ensure", status: "status", stop: "stop", handoff: "handoff" }
|
|
73
81
|
|
|
74
|
-
function buildArgs(subcommand, args, { specPath, policy
|
|
82
|
+
function buildArgs(subcommand, args, { specPath, policy }) {
|
|
75
83
|
const engineArgs = ["host", ENGINE_SUBCOMMAND[subcommand], "--json"]
|
|
76
84
|
if (subcommand === "run" || subcommand === "attach" || subcommand === "handoff") {
|
|
77
85
|
engineArgs.push("--launch-spec", specPath, "--policy", policy)
|
|
78
|
-
const idleExitMs = config?.task?.host_idle_exit_ms
|
|
79
|
-
if (typeof idleExitMs === "number" && Number.isFinite(idleExitMs)) {
|
|
80
|
-
engineArgs.push("--idle-exit-ms", String(Math.trunc(idleExitMs)))
|
|
81
|
-
}
|
|
82
|
-
if (args.includes("--persistent")) engineArgs.push("--persistent")
|
|
83
|
-
if (args.includes("--foreground")) engineArgs.push("--foreground")
|
|
84
86
|
}
|
|
85
87
|
if (subcommand === "stop" && args.includes("--drain")) engineArgs.push("--drain")
|
|
86
88
|
if (subcommand === "status" && args.includes("--include-workers")) engineArgs.push("--include-workers")
|
|
87
89
|
return engineArgs
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
function parseLine(stdout) {
|
|
91
|
-
const line = stdout.trim().split("\n").filter(Boolean).pop()
|
|
92
|
-
if (line === undefined) return undefined
|
|
93
|
-
try {
|
|
94
|
-
return JSON.parse(line)
|
|
95
|
-
} catch {
|
|
96
|
-
return undefined
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
92
|
/** What a child process needs in its environment to reach this daemon rather than start its own. */
|
|
101
93
|
function attachEnv(parsed, agentDir) {
|
|
102
94
|
return {
|
|
@@ -123,7 +115,7 @@ function summarize(subcommand, parsed, exitCode) {
|
|
|
123
115
|
* @returns the process exit code the launcher should use
|
|
124
116
|
*/
|
|
125
117
|
export function runDaemonCommand(args, options) {
|
|
126
|
-
const { engine, pluginRoot, agentDir, env, stdout, stderr, platform } = options
|
|
118
|
+
const { engine, migration, pluginRoot, agentDir, env, stdout, stderr, platform } = options
|
|
127
119
|
const subcommand = args[0]
|
|
128
120
|
|
|
129
121
|
if (subcommand === "--help" || subcommand === "-h") {
|
|
@@ -141,9 +133,13 @@ export function runDaemonCommand(args, options) {
|
|
|
141
133
|
// A named pipe is per-process on win32: there is no socket for a second client to attach to,
|
|
142
134
|
// so refusing here is honest, where pretending would strand the caller on a host it cannot reach.
|
|
143
135
|
if (platform === "win32") {
|
|
144
|
-
stderr.write("omo daemon: a
|
|
136
|
+
stderr.write("omo daemon: a task host needs a unix socket, which win32 does not provide\n")
|
|
145
137
|
return DAEMON_EXIT.unsupported
|
|
146
138
|
}
|
|
139
|
+
if (args.includes("--foreground")) {
|
|
140
|
+
stderr.write("omo daemon: --foreground is unsupported; the engine host always detaches\n")
|
|
141
|
+
return DAEMON_EXIT.usage
|
|
142
|
+
}
|
|
147
143
|
|
|
148
144
|
// Only the subcommands that may START something need the spec; asking who is serving, or
|
|
149
145
|
// asking it to stop, must still work on an install whose plugin payload was never built.
|
|
@@ -154,9 +150,82 @@ export function runDaemonCommand(args, options) {
|
|
|
154
150
|
}
|
|
155
151
|
|
|
156
152
|
const config = readConfig(agentDir)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
if (subcommand === "status") {
|
|
154
|
+
const status = runStatus({
|
|
155
|
+
engine,
|
|
156
|
+
agentDir,
|
|
157
|
+
env,
|
|
158
|
+
json: args.includes("--json"),
|
|
159
|
+
stdout,
|
|
160
|
+
stderr,
|
|
161
|
+
})
|
|
162
|
+
if (!status.legacy) return status.exitCode
|
|
163
|
+
}
|
|
164
|
+
if (subcommand === "gc") {
|
|
165
|
+
if (args.includes("--prune-store-index") && migration === undefined) {
|
|
166
|
+
stderr.write("omo daemon: rollback migration runtime is unavailable\n")
|
|
167
|
+
return DAEMON_EXIT.engineRefused
|
|
168
|
+
}
|
|
169
|
+
return runGc({
|
|
170
|
+
engine,
|
|
171
|
+
migration,
|
|
172
|
+
agentDir,
|
|
173
|
+
env,
|
|
174
|
+
json: args.includes("--json"),
|
|
175
|
+
pruneStoreIndex: args.includes("--prune-store-index"),
|
|
176
|
+
stdout,
|
|
177
|
+
stderr,
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
if (subcommand === "rollback-prepare") {
|
|
181
|
+
if (migration === undefined) {
|
|
182
|
+
stderr.write("omo daemon: rollback migration runtime is unavailable\n")
|
|
183
|
+
return DAEMON_EXIT.engineRefused
|
|
184
|
+
}
|
|
185
|
+
return runRollbackPrepare({
|
|
186
|
+
engine,
|
|
187
|
+
migration,
|
|
188
|
+
agentDir,
|
|
189
|
+
env,
|
|
190
|
+
explicitStores: parseStoreArgs(args),
|
|
191
|
+
allowMissingIndex: args.includes("--allow-missing-index"),
|
|
192
|
+
dryRun: args.includes("--dry-run"),
|
|
193
|
+
json: args.includes("--json"),
|
|
194
|
+
stdout,
|
|
195
|
+
stderr,
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
if (subcommand === "handoff") {
|
|
199
|
+
return runHandoff({
|
|
200
|
+
engine,
|
|
201
|
+
pluginRoot,
|
|
202
|
+
agentDir,
|
|
203
|
+
env,
|
|
204
|
+
policy: resolvePolicy(args, config),
|
|
205
|
+
config,
|
|
206
|
+
stdout,
|
|
207
|
+
stderr,
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
if (subcommand === "stop" && args.includes("--all")) {
|
|
211
|
+
const timeoutSeconds = readTimeoutSeconds(args)
|
|
212
|
+
const outcome = runStopAll({
|
|
213
|
+
engine,
|
|
214
|
+
agentDir,
|
|
215
|
+
env,
|
|
216
|
+
drain: args.includes("--drain"),
|
|
217
|
+
wait: args.includes("--wait"),
|
|
218
|
+
timeoutSeconds,
|
|
219
|
+
stdout,
|
|
220
|
+
stderr,
|
|
221
|
+
now: options.now ?? Date.now,
|
|
222
|
+
pause: options.pause ?? blockingPause,
|
|
223
|
+
})
|
|
224
|
+
if (outcome !== undefined) return outcome
|
|
225
|
+
}
|
|
226
|
+
const engineArgs = buildArgs(subcommand, args, { specPath, policy: resolvePolicy(args, config) })
|
|
227
|
+
const result = engine.run(engineArgs, { env: hostCommandEnvironment(env, agentDir, config) })
|
|
228
|
+
const parsed = parseEngineLine(result.stdout ?? "")
|
|
160
229
|
|
|
161
230
|
if (result.stderr) stderr.write(result.stderr)
|
|
162
231
|
|
|
@@ -165,7 +234,7 @@ export function runDaemonCommand(args, options) {
|
|
|
165
234
|
const daemonEnv = attachEnv(parsed, agentDir)
|
|
166
235
|
// `omo daemon attach --model x`: the trailing args are a normal omo launch that should run
|
|
167
236
|
// against the daemon, so the caller gets the merged environment back and continues with them.
|
|
168
|
-
const launchArgs = args
|
|
237
|
+
const launchArgs = attachLaunchArgs(args)
|
|
169
238
|
if (launchArgs.length > 0) return { passthrough: true, args: launchArgs, env: { ...env, ...daemonEnv } }
|
|
170
239
|
const payload = { ...(parsed ?? {}), env: daemonEnv }
|
|
171
240
|
if (args.includes("--json")) stdout.write(`${JSON.stringify(payload)}\n`)
|
|
@@ -177,29 +246,3 @@ export function runDaemonCommand(args, options) {
|
|
|
177
246
|
else stdout.write(`${summarize(subcommand, parsed, result.exitCode)}\n`)
|
|
178
247
|
return result.exitCode
|
|
179
248
|
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* The `omo doctor` view: one INFO line, never a FAIL - a machine without a daemon is healthy,
|
|
183
|
-
* it just has nothing shared to report. Returned as lines so doctor can place it with the rest.
|
|
184
|
-
*/
|
|
185
|
-
export function daemonReportLines({ engine, pluginRoot, agentDir, env, platform }) {
|
|
186
|
-
if (platform === "win32") return ["INFO Daemon: unavailable on win32 (no unix socket to share)"]
|
|
187
|
-
const stdout = { write() {} }
|
|
188
|
-
const captured = []
|
|
189
|
-
const exitCode = runDaemonCommand(["status", "--json"], {
|
|
190
|
-
engine, pluginRoot, agentDir, env, platform,
|
|
191
|
-
stdout: { write: (text) => void captured.push(text) },
|
|
192
|
-
stderr: stdout,
|
|
193
|
-
})
|
|
194
|
-
const parsed = parseLine(captured.join(""))
|
|
195
|
-
if (exitCode !== DAEMON_EXIT.ok || parsed === undefined) return ["INFO Daemon: not running"]
|
|
196
|
-
const sessions = parsed.sessions?.total ?? parsed.sessions?.length ?? 0
|
|
197
|
-
const parts = [
|
|
198
|
-
`pid ${parsed.pid}`,
|
|
199
|
-
parsed.instanceId === undefined ? undefined : `instance ${parsed.instanceId}`,
|
|
200
|
-
parsed.engineVersion === undefined ? undefined : `engine ${parsed.engineVersion}`,
|
|
201
|
-
`${sessions} session(s)`,
|
|
202
|
-
`zombies ${parsed.zombies ?? 0}`,
|
|
203
|
-
].filter(Boolean)
|
|
204
|
-
return [`INFO Daemon: running ${parts.join(" · ")}`]
|
|
205
|
-
}
|
|
@@ -4,6 +4,7 @@ import { delimiter, dirname, isAbsolute, join, win32 } from "node:path"
|
|
|
4
4
|
import { parseJsonc } from "./jsonc.js"
|
|
5
5
|
import { releaseChannel } from "./package-paths.js"
|
|
6
6
|
import { opencodeConfigSources } from "./setup-opencode-assets.js"
|
|
7
|
+
import { openCodeRoutingGap, openCodeRoutingNotice, readEditionRouting } from "./setup-opencode-models.js"
|
|
7
8
|
import { standaloneBinaryVersion } from "./standalone-binary.js"
|
|
8
9
|
|
|
9
10
|
// Migration leftovers from the OpenCode edition, reported and never touched: another `omo` ahead of
|
|
@@ -321,16 +322,30 @@ export function formatMigrationLines({ shadowing, nativeDirectory, legacyPackage
|
|
|
321
322
|
return lines
|
|
322
323
|
}
|
|
323
324
|
|
|
325
|
+
// The OpenCode edition's model settings Native does not use: the same line Native's first start
|
|
326
|
+
// prints (omo-senpi config-startup), for as long as the gap exists.
|
|
327
|
+
export function openCodeRoutingReport({ env, homeDir }) {
|
|
328
|
+
const path = ["omo.jsonc", "omo.json"].map((name) => join(homeDir, ".omo", name)).find((candidate) => readWholeFile(candidate) !== undefined)
|
|
329
|
+
let document = {}
|
|
330
|
+
if (path !== undefined) {
|
|
331
|
+
try {
|
|
332
|
+
document = parseJsonc(readWholeFile(path))
|
|
333
|
+
} catch {
|
|
334
|
+
return [] // The config loader reports an unreadable omo.jsonc itself.
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const gap = openCodeRoutingGap(document, readEditionRouting({ home: homeDir, env }))
|
|
338
|
+
return gap.length === 0 ? [] : [`INFO ${openCodeRoutingNotice(gap)}`]
|
|
339
|
+
}
|
|
340
|
+
|
|
324
341
|
/** The doctor's migration section. `options.env` / `homeDir` / `platform` keep tests off the real machine. */
|
|
325
342
|
export function migrationReport(options, restoreCommand) {
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
homeDir: options.homeDir ?? homedir(),
|
|
330
|
-
})
|
|
343
|
+
const env = options.env ?? process.env
|
|
344
|
+
const homeDir = options.homeDir ?? homedir()
|
|
345
|
+
const environment = resolveMigrationEnvironment({ env, platform: options.platform ?? process.platform, homeDir })
|
|
331
346
|
const bins = scanOmoBins(environment)
|
|
332
347
|
const omoInstalls = omoInstallsOnPath(bins)
|
|
333
|
-
return formatMigrationLines({
|
|
348
|
+
return [...formatMigrationLines({
|
|
334
349
|
shadowing: shadowingOmoBins(bins),
|
|
335
350
|
nativeDirectory: omoInstalls[0]?.directory ?? null,
|
|
336
351
|
legacyPackages: findLegacyPackages(environment),
|
|
@@ -339,5 +354,5 @@ export function migrationReport(options, restoreCommand) {
|
|
|
339
354
|
omoInstalls,
|
|
340
355
|
bunRoot: environment.bunRoot,
|
|
341
356
|
standalone: options.standalone === true,
|
|
342
|
-
})
|
|
357
|
+
}), ...openCodeRoutingReport({ env, homeDir })]
|
|
343
358
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { canonicalAgentDir, runtimeHome } from "./agent-dir.js"
|
|
4
|
+
|
|
5
|
+
// Mirrors the engine's legacy-pi-edits check (senpi `src/legacy-pi-edits.ts`), which this package
|
|
6
|
+
// cannot import synchronously: the same four files, the same `legacyPiAgentDir.copiedAt` record in
|
|
7
|
+
// migrations-state.json, and the same rule: newer than the recorded copy (or, before the time was
|
|
8
|
+
// recorded, than the agent copy's preserved mtime) and different in content. `~/.pi/agent` is only
|
|
9
|
+
// read (#9173).
|
|
10
|
+
export const PI_CONFIG_FILES = ["auth.json", "keybindings.json", "models.json", "settings.json"]
|
|
11
|
+
|
|
12
|
+
function fileStat(path) {
|
|
13
|
+
try {
|
|
14
|
+
const stat = statSync(path)
|
|
15
|
+
return stat.isFile() ? stat : undefined
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return undefined
|
|
18
|
+
throw error
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function recordedCopyTime(agentDir) {
|
|
23
|
+
let state
|
|
24
|
+
try {
|
|
25
|
+
state = JSON.parse(readFileSync(join(agentDir, "migrations-state.json"), "utf8"))
|
|
26
|
+
} catch {
|
|
27
|
+
// A missing or hand-broken state file means no recorded copy time; the rule falls back.
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
const copiedAt = state?.schemaVersion === 1 ? state.legacyPiAgentDir?.copiedAt : undefined
|
|
31
|
+
return typeof copiedAt === "number" && Number.isFinite(copiedAt) ? copiedAt : undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function findPiConfigEdits({ agentDir, homeDir }) {
|
|
35
|
+
const piAgentDir = join(homeDir, ".pi", "agent")
|
|
36
|
+
if (!existsSync(piAgentDir)) return []
|
|
37
|
+
if (existsSync(agentDir) && realpathSync(agentDir) === realpathSync(piAgentDir)) return []
|
|
38
|
+
const copiedAt = recordedCopyTime(agentDir)
|
|
39
|
+
const edits = []
|
|
40
|
+
for (const file of PI_CONFIG_FILES) {
|
|
41
|
+
const piPath = join(piAgentDir, file)
|
|
42
|
+
const agentPath = join(agentDir, file)
|
|
43
|
+
const pi = fileStat(piPath)
|
|
44
|
+
if (pi === undefined) continue
|
|
45
|
+
const agent = fileStat(agentPath)
|
|
46
|
+
if (pi.mtimeMs <= (copiedAt ?? agent?.mtimeMs ?? 0)) continue
|
|
47
|
+
if (agent !== undefined && readFileSync(piPath).equals(readFileSync(agentPath))) continue
|
|
48
|
+
edits.push({ file, piPath, agentPath })
|
|
49
|
+
}
|
|
50
|
+
return edits
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function formatPiConfigLines(agentDir, edits) {
|
|
54
|
+
return [
|
|
55
|
+
`INFO config dir: ${agentDir}`,
|
|
56
|
+
...edits.map((edit) =>
|
|
57
|
+
`WARN You edited ${edit.piPath} after omo moved to ${agentDir}; omo reads ${edit.agentPath}. Copy your change there (or run: omo config import-pi ${edit.file}).`),
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function piConfigReport(options = {}) {
|
|
62
|
+
const env = options.env ?? process.env
|
|
63
|
+
const agentDir = options.agentDir ?? canonicalAgentDir(env)
|
|
64
|
+
const homeDir = options.homeDir ?? runtimeHome(env)
|
|
65
|
+
try {
|
|
66
|
+
return formatPiConfigLines(agentDir, findPiConfigEdits({ agentDir, homeDir }))
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return [`INFO config dir: ${agentDir}`, `WARN could not check ${join(homeDir, ".pi", "agent")} for config edits: ${error.message}`]
|
|
69
|
+
}
|
|
70
|
+
}
|
package/bin/lib/doctor.js
CHANGED
|
@@ -6,6 +6,7 @@ import { canonicalAgentDir } from "./agent-dir.js"
|
|
|
6
6
|
import { packageManifest, packageRoot, readJson, releaseChannel, resolveSenpi, updateTarget } from "./package-paths.js"
|
|
7
7
|
import { daemonReportLines } from "./daemon.js"
|
|
8
8
|
import { migrationReport } from "./doctor-migration.js"
|
|
9
|
+
import { piConfigReport } from "./doctor-pi-config.js"
|
|
9
10
|
import { needsSetupSuggestion } from "./setup-detect.js"
|
|
10
11
|
|
|
11
12
|
const NPM_DIST_TAGS_URL = "https://registry.npmjs.org/-/package/omo-ai/dist-tags"
|
|
@@ -430,6 +431,7 @@ export function runDoctor(inventory, args = [], options = {}) {
|
|
|
430
431
|
lines.push(`INFO Update: ${updateTarget().command}`)
|
|
431
432
|
lines.push(...migrationReport(options, updateTarget().command))
|
|
432
433
|
lines.push(...warningsForSettings())
|
|
434
|
+
lines.push(...piConfigReport({ env: options.env, homeDir: options.homeDir }))
|
|
433
435
|
lines.push(...staleEngineReport(options))
|
|
434
436
|
lines.push(...retiredPayloadReport(options))
|
|
435
437
|
lines.push(...transientMemoryReport(options))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { realpathSync, statSync } from "node:fs"
|
|
2
|
+
import { basename, dirname, join } from "node:path"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The directory of `name` as the engine installed at `senpiRoot` resolves it: Node's
|
|
6
|
+
* node_modules walk from senpi's real path. That one walk covers every install shape - a
|
|
7
|
+
* bundled senpi keeps the dependency in its own `node_modules`, npm hoists it beside senpi,
|
|
8
|
+
* and bun's isolated store links it beside senpi's real path in `.bun/<key>/node_modules` -
|
|
9
|
+
* so no caller may assume where the dependency lives. Returns undefined when nothing on the
|
|
10
|
+
* walk provides `name`.
|
|
11
|
+
*/
|
|
12
|
+
export function engineDependencyDir(senpiRoot, name) {
|
|
13
|
+
let current = realpathSync(senpiRoot)
|
|
14
|
+
while (true) {
|
|
15
|
+
if (basename(current) !== "node_modules") {
|
|
16
|
+
const candidate = join(current, "node_modules", name)
|
|
17
|
+
if (statSync(candidate, { throwIfNoEntry: false })?.isDirectory()) return candidate
|
|
18
|
+
}
|
|
19
|
+
const parent = dirname(current)
|
|
20
|
+
if (parent === current) return undefined
|
|
21
|
+
current = parent
|
|
22
|
+
}
|
|
23
|
+
}
|
package/bin/lib/engine-models.js
CHANGED
|
@@ -11,13 +11,16 @@ import { existsSync, readFileSync } from "node:fs"
|
|
|
11
11
|
import { join } from "node:path"
|
|
12
12
|
import { pathToFileURL } from "node:url"
|
|
13
13
|
import { parseJsonc } from "./jsonc.js"
|
|
14
|
+
import { engineDependencyDir } from "./engine-dependency.js"
|
|
14
15
|
import { resolveSenpi } from "./package-paths.js"
|
|
15
16
|
|
|
16
17
|
// The same module the engine's model-runtime.js composes its builtins from
|
|
17
|
-
// (`builtinProviderCatalog.builtinProviders()`),
|
|
18
|
+
// (`builtinProviderCatalog.builtinProviders()`), in the pi-ai the engine itself resolves.
|
|
18
19
|
async function builtinCatalog() {
|
|
19
20
|
const { packageRoot } = resolveSenpi()
|
|
20
|
-
const
|
|
21
|
+
const piAiRoot = engineDependencyDir(packageRoot, "@earendil-works/pi-ai")
|
|
22
|
+
if (piAiRoot === undefined) throw new Error(`the engine model catalog is missing: @earendil-works/pi-ai is not resolvable from ${packageRoot}`)
|
|
23
|
+
const path = join(piAiRoot, "dist", "providers", "all.js")
|
|
21
24
|
if (!existsSync(path)) throw new Error(`the engine model catalog is missing at ${path}`)
|
|
22
25
|
return (await import(pathToFileURL(path).href)).builtinProviders()
|
|
23
26
|
}
|
package/bin/lib/launcher.js
CHANGED
|
@@ -210,6 +210,20 @@ export function engineHostCall(engineArgs, options) {
|
|
|
210
210
|
return { exitCode: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" }
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
export function rollbackMigrateCall(request) {
|
|
214
|
+
const runtime = join(packageRoot, "plugin", "runtime", "rollback-migrate.js")
|
|
215
|
+
const result = spawnSync(process.execPath, [runtime], {
|
|
216
|
+
encoding: "utf8",
|
|
217
|
+
input: JSON.stringify(request),
|
|
218
|
+
env: senpiEnvironment(preparedSenpi().packageRoot),
|
|
219
|
+
windowsHide: true,
|
|
220
|
+
})
|
|
221
|
+
if (result.status !== 0) {
|
|
222
|
+
throw new Error(result.stderr?.trim() || `rollback migration runtime exited ${result.status ?? 1}`)
|
|
223
|
+
}
|
|
224
|
+
return JSON.parse(result.stdout)
|
|
225
|
+
}
|
|
226
|
+
|
|
213
227
|
export async function runLauncher(args = process.argv.slice(2)) {
|
|
214
228
|
migrateLegacyBunGlobalManifest()
|
|
215
229
|
reportLegacyFlatAdoption()
|
|
@@ -226,6 +240,7 @@ export async function runLauncher(args = process.argv.slice(2)) {
|
|
|
226
240
|
if (command === "daemon") {
|
|
227
241
|
const outcome = runDaemonCommand(args.slice(1), {
|
|
228
242
|
engine: { run: engineHostCall },
|
|
243
|
+
migration: { run: rollbackMigrateCall },
|
|
229
244
|
pluginRoot: join(packageRoot, "plugin"),
|
|
230
245
|
agentDir: canonicalAgentDir(),
|
|
231
246
|
env: process.env,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Derived from @code-yeongyu/senpi 2026.9.
|
|
2
|
+
"_comment": "Derived from @code-yeongyu/senpi 2026.9.29-3 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",
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Types for setup-opencode-models.js, which omo-senpi's config-startup imports across the package
|
|
2
|
+
// boundary: omo-ai ships this module as plain JS, so it is the single source of the mapping.
|
|
3
|
+
|
|
4
|
+
export type ModelRegistry = {
|
|
5
|
+
readonly models: ReadonlyMap<string, ReadonlySet<string>>
|
|
6
|
+
readonly dynamic: ReadonlySet<string>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type EditionRouting = {
|
|
10
|
+
readonly agents: Readonly<Record<string, unknown>>
|
|
11
|
+
readonly categories: Readonly<Record<string, unknown>>
|
|
12
|
+
readonly notices: readonly string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ConvertedChoice = {
|
|
16
|
+
readonly name: string
|
|
17
|
+
readonly entry: Readonly<Record<string, unknown>>
|
|
18
|
+
readonly from: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const NATIVE_AGENT_NAMES: readonly string[]
|
|
22
|
+
|
|
23
|
+
export function readEditionRouting(options: { home: string; env: Readonly<Record<string, string | undefined>> }): EditionRouting
|
|
24
|
+
|
|
25
|
+
export function readModelChoices(options: {
|
|
26
|
+
home: string
|
|
27
|
+
env: Readonly<Record<string, string | undefined>>
|
|
28
|
+
opencodeBlock: unknown
|
|
29
|
+
}): Record<string, unknown>
|
|
30
|
+
|
|
31
|
+
export function hasModelChoices(raw: unknown): boolean
|
|
32
|
+
|
|
33
|
+
export function translateModelRef(
|
|
34
|
+
raw: unknown,
|
|
35
|
+
context: { registry?: ModelRegistry; oauthProviders: ReadonlySet<string> },
|
|
36
|
+
): { ref?: string; provider?: string; modelId?: string; reason?: string }
|
|
37
|
+
|
|
38
|
+
export function convertModelChoices(
|
|
39
|
+
raw: unknown,
|
|
40
|
+
registry: ModelRegistry,
|
|
41
|
+
oauthProviders?: ReadonlySet<string>,
|
|
42
|
+
): {
|
|
43
|
+
defaultModel?: { source: string; provider: string; modelId: string; ref: string }
|
|
44
|
+
categories: readonly ConvertedChoice[]
|
|
45
|
+
agents: readonly ConvertedChoice[]
|
|
46
|
+
dropped: readonly string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function nativeRoutingLine(section: string, name: string, value: Readonly<Record<string, unknown>>): string
|
|
50
|
+
|
|
51
|
+
export function openCodeRoutingGap(document: unknown, edition: EditionRouting): readonly string[]
|
|
52
|
+
|
|
53
|
+
export function openCodeRoutingNotice(lines: readonly string[]): string
|