omo-ai 5.0.0 → 5.1.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/computer-use-doctor.js +90 -0
- package/bin/lib/doctor-migration.js +69 -15
- package/bin/lib/doctor.js +2 -2
- package/bin/lib/launcher.js +47 -6
- package/bin/lib/package-paths.js +22 -5
- package/bin/lib/provider-map.json +1 -1
- package/bin/lib/standalone-binary.js +67 -0
- package/package.json +2 -2
- package/plugin/CHANGELOG.md +139 -0
- package/plugin/extensions/assets.generated.json +7 -0
- package/plugin/extensions/memory-run-supervisor.mjs +2 -2
- package/plugin/extensions/omo-computer-use.js +2 -0
- 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 +2 -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 +9 -3
- package/plugin/runtime/lsp-daemon/dist/.omo-runtime-manifest.json +1 -1
- package/plugin/scripts/install.mjs +2 -0
- package/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/plugin/skills/coding-agent-sessions/SKILL.md +1 -1
- package/plugin/skills/coding-agent-sessions/scripts/agent_sessions/aside_scanner.py +3 -0
- package/plugin/skills/coding-agent-sessions/scripts/agent_sessions/cli.py +4 -0
- package/plugin/skills/coding-agent-sessions/scripts/agent_sessions/file_scanners.py +6 -2
- package/plugin/skills/coding-agent-sessions/scripts/agent_sessions/scanners.py +7 -1
- package/plugin/skills/coding-agent-sessions/scripts/agent_sessions/transcript.py +3 -1
- package/plugin/skills/coding-agent-sessions/scripts/agent_sessions/types.py +3 -1
- package/plugin/skills/coding-agent-sessions/scripts/find-agent-sessions.py +4 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { pathToFileURL } from "node:url"
|
|
3
|
+
import { packageManifest, packageRoot } from "./package-paths.js"
|
|
4
|
+
|
|
5
|
+
export const COMPUTER_USE_RUNTIME = join("plugin", "runtime", "category-coverage", "index.js")
|
|
6
|
+
|
|
7
|
+
async function loadRuntime() {
|
|
8
|
+
return import(pathToFileURL(join(packageRoot, COMPUTER_USE_RUNTIME)).href)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function baseLine(report) {
|
|
12
|
+
return `INFO computer use: enabled=${report.enabled} supported=${report.supported} host=${report.host}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readyLines(report) {
|
|
16
|
+
const capabilities = report.capabilities
|
|
17
|
+
const displayServer = capabilities.displayServer ? ` (${capabilities.displayServer})` : ""
|
|
18
|
+
const backendLevel = capabilities.backend === "unavailable" ? "WARN" : "PASS"
|
|
19
|
+
const permissionLevel = capabilities.capture && capabilities.input && capabilities.ax ? "PASS" : "WARN"
|
|
20
|
+
const displayLevel = capabilities.displayCount > 0 && !capabilities.screenLocked ? "PASS" : "WARN"
|
|
21
|
+
const stopLevel = capabilities.stopPath === "none" ? "WARN" : "PASS"
|
|
22
|
+
const stopReason = capabilities.stopReason ? ` reason=${capabilities.stopReason}` : ""
|
|
23
|
+
// The engine arms the stop chord only when a session starts input (`stopPath.start`), so a pre-session
|
|
24
|
+
// probe always sees `none` / `no-global-listener`; that is the normal idle state, not a failure.
|
|
25
|
+
const stopLine =
|
|
26
|
+
capabilities.stopPath === "none" && capabilities.stopReason === "no-global-listener"
|
|
27
|
+
? "INFO computer use stop path: not armed until computer use starts input (checked when a session activates)"
|
|
28
|
+
: `${stopLevel} computer use stop path: ${capabilities.stopPath}${stopReason}`
|
|
29
|
+
return [
|
|
30
|
+
baseLine(report),
|
|
31
|
+
`PASS computer use engine: ${report.enginePath} (version ${report.hello.engineVersion}, ABI ${report.hello.abi}, protocol ${report.hello.protocolVersion})`,
|
|
32
|
+
`${backendLevel} computer use backend: ${capabilities.backend}${displayServer}`,
|
|
33
|
+
`${permissionLevel} computer use permissions: capture=${capabilities.capturePermission} input=${capabilities.inputPermission} accessibility=${capabilities.axPermission}`,
|
|
34
|
+
`${displayLevel} computer use display: count=${capabilities.displayCount} screenLocked=${capabilities.screenLocked}`,
|
|
35
|
+
stopLine,
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatComputerUseDoctorLines(report) {
|
|
40
|
+
switch (report.kind) {
|
|
41
|
+
case "ready":
|
|
42
|
+
return readyLines(report)
|
|
43
|
+
case "skipped":
|
|
44
|
+
return [
|
|
45
|
+
baseLine(report),
|
|
46
|
+
report.reason === "disabled"
|
|
47
|
+
? "INFO computer use probe: skipped because computer.enabled=false"
|
|
48
|
+
: `INFO computer use probe: skipped because ${report.host} is unsupported`,
|
|
49
|
+
]
|
|
50
|
+
case "unavailable":
|
|
51
|
+
return [
|
|
52
|
+
baseLine(report),
|
|
53
|
+
`FAIL computer use engine: ${report.diagnostic.code}: ${report.diagnostic.message} ${report.diagnostic.cause}`.trimEnd(),
|
|
54
|
+
`INFO computer use engine paths tried: ${report.diagnostic.attemptedPaths.join(", ") || "<none>"}`,
|
|
55
|
+
]
|
|
56
|
+
case "not-installed":
|
|
57
|
+
return [
|
|
58
|
+
baseLine(report),
|
|
59
|
+
"INFO computer use engine: not installed yet; it is downloaded the first time computer use starts",
|
|
60
|
+
`INFO computer use engine paths checked: ${report.attemptedPaths.join(", ") || "<none>"}`,
|
|
61
|
+
]
|
|
62
|
+
case "failed":
|
|
63
|
+
return [
|
|
64
|
+
baseLine(report),
|
|
65
|
+
`FAIL computer use engine: ${report.code}: ${report.message}`,
|
|
66
|
+
`INFO computer use engine path: ${report.enginePath}`,
|
|
67
|
+
]
|
|
68
|
+
default:
|
|
69
|
+
throw new TypeError(`unknown computer use doctor report kind: ${String(report.kind)}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function doctorComputerUseLines(options = {}) {
|
|
74
|
+
try {
|
|
75
|
+
const runtime = await (options.loadRuntime ?? loadRuntime)()
|
|
76
|
+
const report = await runtime.computerUseDoctorReport({
|
|
77
|
+
cwd: options.cwd ?? process.cwd(),
|
|
78
|
+
env: options.env ?? process.env,
|
|
79
|
+
version: packageManifest().version,
|
|
80
|
+
packageRoot,
|
|
81
|
+
platform: options.platform ?? process.platform,
|
|
82
|
+
arch: options.arch ?? process.arch,
|
|
83
|
+
timeoutMs: options.timeoutMs,
|
|
84
|
+
})
|
|
85
|
+
return formatComputerUseDoctorLines(report)
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
88
|
+
return [`WARN computer use: diagnostics unavailable: ${message}`]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -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 { standaloneBinaryVersion } from "./standalone-binary.js"
|
|
7
8
|
|
|
8
9
|
// Migration leftovers from the OpenCode edition, reported and never touched: another `omo` ahead of
|
|
9
10
|
// omo-ai's on PATH, the legacy package still installed globally, and the OpenCode plugin still
|
|
@@ -51,7 +52,7 @@ export function resolveMigrationEnvironment({ env, platform, homeDir }) {
|
|
|
51
52
|
...opencode.files,
|
|
52
53
|
...opencode.directories.flatMap((directory) => TUI_CONFIG_FILES.map((name) => join(directory, name))),
|
|
53
54
|
]
|
|
54
|
-
return { isWindows, pathDirectories, npmPrefixes, bunRoot, opencodeConfigFiles }
|
|
55
|
+
return { isWindows, homeDir, pathDirectories, npmPrefixes, bunRoot, opencodeConfigFiles }
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
function npmrcPrefix(homeDir) {
|
|
@@ -72,15 +73,27 @@ export function scanOmoBins(environment) {
|
|
|
72
73
|
const binPath = suffixes.map((suffix) => join(directory, `omo${suffix}`)).find(pathExists)
|
|
73
74
|
if (binPath === undefined) continue
|
|
74
75
|
const owner = resolveOwner(binPath)
|
|
75
|
-
|
|
76
|
+
const kind = classify(owner)
|
|
77
|
+
const standalone = kind === "foreign" ? standaloneBinaryVersion(binPath, environment.homeDir, environment.isWindows) : null
|
|
78
|
+
entries.push(standalone === null
|
|
79
|
+
? { binPath, directory, owner, kind }
|
|
80
|
+
: { binPath, directory, owner: { name: STANDALONE_LABEL, version: standalone }, kind: "standalone" })
|
|
76
81
|
}
|
|
77
82
|
return entries
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
|
|
85
|
+
const STANDALONE_LABEL = "standalone omo binary"
|
|
86
|
+
const isOmoInstall = (entry) => entry.kind === "native" || entry.kind === "standalone"
|
|
87
|
+
|
|
88
|
+
/** The entries a typed `omo` reaches before OmO's own (omo-ai or a standalone binary); all of them when neither is on PATH. */
|
|
81
89
|
export function shadowingOmoBins(entries) {
|
|
82
|
-
const
|
|
83
|
-
return (
|
|
90
|
+
const firstOmo = entries.findIndex(isOmoInstall)
|
|
91
|
+
return (firstOmo < 0 ? entries : entries.slice(0, firstOmo)).filter((entry) => !isOmoInstall(entry))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** omo-ai and standalone binaries on PATH, in PATH order; only the first one runs. */
|
|
95
|
+
export function omoInstallsOnPath(entries) {
|
|
96
|
+
return entries.filter(isOmoInstall)
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
function classify(owner) {
|
|
@@ -105,13 +118,16 @@ function resolveOwner(binPath) {
|
|
|
105
118
|
const fromLink = isInsideNodeModules(real) ? ownerOfFile(real) : null
|
|
106
119
|
if (fromLink !== null) return fromLink
|
|
107
120
|
const shim = readFileHead(binPath, SHIM_READ_LIMIT)
|
|
108
|
-
if (shim
|
|
109
|
-
if (shim.includes(CODEX_LIGHT_WRAPPER_MARKER)) {
|
|
121
|
+
if (shim?.includes(CODEX_LIGHT_WRAPPER_MARKER)) {
|
|
110
122
|
const version = shim.match(CODEX_LIGHT_CACHE_VERSION)?.[1]
|
|
111
123
|
if (version !== undefined) return { name: "lazycodex", version }
|
|
112
124
|
}
|
|
113
|
-
// Ownership comes from an installed package the shim really launches, never
|
|
114
|
-
|
|
125
|
+
// Ownership comes from an installed package the shim or its Bun sidecar really launches, never text alone.
|
|
126
|
+
const entries = [
|
|
127
|
+
...(shim === undefined ? [] : shimEntryPaths(shim, dirname(binPath))),
|
|
128
|
+
...bunxEntryPaths(binPath),
|
|
129
|
+
]
|
|
130
|
+
for (const entry of entries) {
|
|
115
131
|
if (!isInsideNodeModules(entry) || !pathExists(entry)) continue
|
|
116
132
|
const owner = ownerOfFile(entry)
|
|
117
133
|
if (owner !== null) return owner
|
|
@@ -132,6 +148,24 @@ function shimEntryPaths(shim, shimDirectory) {
|
|
|
132
148
|
return paths
|
|
133
149
|
}
|
|
134
150
|
|
|
151
|
+
// Bun's Windows bin is a copied `omo.exe` plus an `omo.bunx` sidecar: UTF-16LE, the target path up to
|
|
152
|
+
// a `"` and a NUL. Bun writes that path relative to the bin dir's parent (`..\node_modules\...` or
|
|
153
|
+
// `install\global\node_modules\...` from `~/.bun`), and its shim resolves it against that same dir.
|
|
154
|
+
function bunxEntryPaths(binPath) {
|
|
155
|
+
if (!/\.exe$/i.test(binPath)) return []
|
|
156
|
+
const sidecar = `${binPath.slice(0, -4)}.bunx`
|
|
157
|
+
let contents
|
|
158
|
+
try {
|
|
159
|
+
if (!statSync(sidecar).isFile()) return []
|
|
160
|
+
contents = readFileSync(sidecar).toString("utf16le")
|
|
161
|
+
} catch {
|
|
162
|
+
return []
|
|
163
|
+
}
|
|
164
|
+
const end = contents.indexOf('"\0')
|
|
165
|
+
if (end <= 0) return []
|
|
166
|
+
return [join(dirname(dirname(binPath)), contents.slice(0, end).replace(/\\/g, "/"))]
|
|
167
|
+
}
|
|
168
|
+
|
|
135
169
|
function isInsideNodeModules(path) {
|
|
136
170
|
return /(?:^|[\\/])node_modules[\\/]/.test(path)
|
|
137
171
|
}
|
|
@@ -243,23 +277,39 @@ function isPluginEntryFor(entry, name) {
|
|
|
243
277
|
|
|
244
278
|
function ownerLabel(owner) {
|
|
245
279
|
if (owner === null) return "unknown owner"
|
|
280
|
+
if (owner.name === STANDALONE_LABEL) return `${owner.name} ${owner.version}`
|
|
246
281
|
return owner.version === null ? owner.name : `${owner.name}@${owner.version}`
|
|
247
282
|
}
|
|
248
283
|
|
|
249
|
-
|
|
284
|
+
function omoInstallRemoval(entry, bunRoot) {
|
|
285
|
+
if (entry.kind === "standalone") return `remove ${entry.binPath}`
|
|
286
|
+
return realPathOf(entry.binPath).startsWith(realPathOf(bunRoot)) ? "bun remove -g omo-ai" : "npm uninstall -g omo-ai"
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function formatMigrationLines({ shadowing, nativeDirectory, legacyPackages, registrations, restoreCommand, omoInstalls = [], bunRoot = "", standalone = false }) {
|
|
250
290
|
const lines = []
|
|
291
|
+
const first = omoInstalls[0]
|
|
292
|
+
const target = first?.kind === "standalone" || (first === undefined && standalone) ? "the standalone omo binary" : "omo-ai"
|
|
251
293
|
for (const entry of shadowing) {
|
|
252
|
-
const fix = entry.kind === "legacy"
|
|
294
|
+
const fix = entry.kind === "legacy" && target === "omo-ai"
|
|
253
295
|
? `${repairCommand()} (repairs it), or remove that file.`
|
|
254
296
|
: nativeDirectory === null
|
|
255
|
-
?
|
|
297
|
+
? `remove that file, or put ${target === "omo-ai" ? "omo-ai's bin dir" : "the omo binary's directory"} ahead of it on PATH.`
|
|
256
298
|
: `remove that file, or move ${nativeDirectory} ahead of ${entry.directory} on PATH.`
|
|
257
|
-
lines.push(`WARN another omo precedes
|
|
299
|
+
lines.push(`WARN another omo precedes ${target} on PATH: ${entry.binPath} (${ownerLabel(entry.owner)}). Fix: ${fix}`)
|
|
300
|
+
}
|
|
301
|
+
const shadowedOmo = omoInstalls.slice(1).filter((entry) => entry.kind !== first.kind || entry.kind === "standalone")
|
|
302
|
+
if (shadowedOmo.length > 0) {
|
|
303
|
+
const others = shadowedOmo.map((entry) => `${entry.binPath} (${ownerLabel(entry.owner)})`).join(", ")
|
|
304
|
+
const removals = shadowedOmo.map((entry) => omoInstallRemoval(entry, bunRoot)).join(" and ")
|
|
305
|
+
lines.push(`WARN more than one OmO install is on PATH: ${first.binPath} (${ownerLabel(first.owner)}) runs when you type omo; ${others} never runs. Keep one: ${removals}, or ${omoInstallRemoval(first, bunRoot)} to use the other.`)
|
|
258
306
|
}
|
|
259
307
|
for (const found of legacyPackages) {
|
|
260
308
|
const label = ownerLabel({ name: found.name, version: found.version })
|
|
261
309
|
const remove = found.manager === "npm"
|
|
262
|
-
?
|
|
310
|
+
? restoreCommand === null
|
|
311
|
+
? `npm uninstall -g ${found.name}`
|
|
312
|
+
: `npm uninstall -g ${found.name}, then re-run ${restoreCommand} if omo disappears.`
|
|
263
313
|
: `bun remove -g ${found.name}`
|
|
264
314
|
lines.push(`WARN legacy package ${label} is still installed globally (${found.manager}: ${found.packageDir}). Remove: ${remove}`)
|
|
265
315
|
}
|
|
@@ -279,11 +329,15 @@ export function migrationReport(options, restoreCommand) {
|
|
|
279
329
|
homeDir: options.homeDir ?? homedir(),
|
|
280
330
|
})
|
|
281
331
|
const bins = scanOmoBins(environment)
|
|
332
|
+
const omoInstalls = omoInstallsOnPath(bins)
|
|
282
333
|
return formatMigrationLines({
|
|
283
334
|
shadowing: shadowingOmoBins(bins),
|
|
284
|
-
nativeDirectory:
|
|
335
|
+
nativeDirectory: omoInstalls[0]?.directory ?? null,
|
|
285
336
|
legacyPackages: findLegacyPackages(environment),
|
|
286
337
|
registrations: findOpenCodeRegistrations(environment.opencodeConfigFiles),
|
|
287
338
|
restoreCommand,
|
|
339
|
+
omoInstalls,
|
|
340
|
+
bunRoot: environment.bunRoot,
|
|
341
|
+
standalone: options.standalone === true,
|
|
288
342
|
})
|
|
289
343
|
}
|
package/bin/lib/doctor.js
CHANGED
|
@@ -433,8 +433,8 @@ export function runDoctor(inventory, args = [], options = {}) {
|
|
|
433
433
|
lines.push(...staleEngineReport(options))
|
|
434
434
|
lines.push(...retiredPayloadReport(options))
|
|
435
435
|
lines.push(...transientMemoryReport(options))
|
|
436
|
-
lines.push(...daemonReport(options))
|
|
437
|
-
|
|
436
|
+
lines.push(...daemonReport(options), ...(options.computerUse ?? []), ...(options.categoryCoverage ?? []))
|
|
437
|
+
if ((options.computerUse ?? []).some((line) => line.startsWith("FAIL "))) failed = true
|
|
438
438
|
if (needsSetupSuggestion(inventory)) {
|
|
439
439
|
lines.push("INFO no credentials found; run omo setup to review sibling stores")
|
|
440
440
|
}
|
package/bin/lib/launcher.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process"
|
|
2
|
-
import { existsSync } from "node:fs"
|
|
3
|
-
import { delimiter, join } from "node:path"
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs"
|
|
3
|
+
import { delimiter, isAbsolute, join, relative, sep } from "node:path"
|
|
4
4
|
import { spawnNode } from "./child-process.js"
|
|
5
5
|
import { doctorCoverageLines } from "./category-coverage.js"
|
|
6
|
+
import { doctorComputerUseLines } from "./computer-use-doctor.js"
|
|
6
7
|
import { runDaemonCommand } from "./daemon.js"
|
|
7
8
|
import { runDoctor } from "./doctor.js"
|
|
8
9
|
import { ensureEnginePrepared } from "./engine-prepare.js"
|
|
9
10
|
import { migrateLegacyBunGlobalManifest } from "./legacy-bun-global-migration.js"
|
|
10
11
|
import { adoptLegacyFlatState, canonicalAgentDir } from "./agent-dir.js"
|
|
11
|
-
import { nearestNodeBin, packageManifest, packageRoot, readJson, releaseChannel, resolveSenpi, updateTarget } from "./package-paths.js"
|
|
12
|
+
import { nearestNodeBin, packageManifest, packageRoot, readJson, releaseBanner, releaseChannel, resolveSenpi, updateTarget } from "./package-paths.js"
|
|
12
13
|
import { runSelfUpdate } from "./self-update.js"
|
|
13
14
|
import { detectHarnesses } from "./setup-detect.js"
|
|
14
15
|
import { readSetupSuggestionCache, spawnSetupSuggestionRefresh } from "./setup-detect-cache.js"
|
|
@@ -78,10 +79,35 @@ function engineVersion() {
|
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
function canonicalPath(path) {
|
|
83
|
+
try {
|
|
84
|
+
return realpathSync.native(path)
|
|
85
|
+
} catch {
|
|
86
|
+
return path
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function containsPath(root, target) {
|
|
91
|
+
const rel = relative(canonicalPath(root), canonicalPath(target))
|
|
92
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// A compiled omo session exports its own payload as the brand-scoped package dir, and every shell it
|
|
96
|
+
// spawns inherits that. The engine reads these names before the legacy PI_PACKAGE_DIR, so a release
|
|
97
|
+
// omo started from such a shell would run on the foreign payload. A deliberate relocation names this
|
|
98
|
+
// install's engine and survives; a root that does not contain the engine belongs to another install.
|
|
99
|
+
function dropForeignPackageDirs(env, senpiRoot) {
|
|
100
|
+
for (const name of ["OMO_PACKAGE_DIR", "SENPI_PACKAGE_DIR"]) {
|
|
101
|
+
const root = env[name]
|
|
102
|
+
if (root && !containsPath(root, senpiRoot)) delete env[name]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
81
106
|
function senpiEnvironment(senpiRoot) {
|
|
82
107
|
const env = { ...process.env }
|
|
83
108
|
delete env.OMO_BIN
|
|
84
109
|
delete env.SENPI_BIN
|
|
110
|
+
dropForeignPackageDirs(env, senpiRoot)
|
|
85
111
|
// One directory for every surface. The legacy name travels too, so a bare senpi spawned by a
|
|
86
112
|
// tool inherits the same state instead of falling back to its own home.
|
|
87
113
|
const agentDir = canonicalAgentDir(env)
|
|
@@ -220,8 +246,17 @@ export async function runLauncher(args = process.argv.slice(2)) {
|
|
|
220
246
|
return
|
|
221
247
|
}
|
|
222
248
|
if (command === "doctor") {
|
|
223
|
-
const categoryCoverage = args[1] === "--reap"
|
|
224
|
-
|
|
249
|
+
const [categoryCoverage, computerUse] = args[1] === "--reap"
|
|
250
|
+
? [[], []]
|
|
251
|
+
: await Promise.all([
|
|
252
|
+
doctorCoverageLines({ agentDir: canonicalAgentDir() }),
|
|
253
|
+
doctorComputerUseLines(),
|
|
254
|
+
])
|
|
255
|
+
runDoctor(await detectHarnesses(), args.slice(1), {
|
|
256
|
+
daemonEngine: { run: engineHostCall },
|
|
257
|
+
categoryCoverage,
|
|
258
|
+
computerUse,
|
|
259
|
+
})
|
|
225
260
|
return
|
|
226
261
|
}
|
|
227
262
|
if (command === "setup") {
|
|
@@ -240,12 +275,18 @@ export async function runLauncher(args = process.argv.slice(2)) {
|
|
|
240
275
|
process.exitCode = await runSelfUpdate(args)
|
|
241
276
|
return
|
|
242
277
|
}
|
|
278
|
+
// app-server takes the plugin after its subcommand: a leading --extension never reaches the
|
|
279
|
+
// engine's app-server dispatch. It loads into every thread, including the daemon's.
|
|
280
|
+
if (command === "app-server") {
|
|
281
|
+
await spawnSenpi(args.includes("--no-extensions") ? args : [...args, "--extension", join(packageRoot, "plugin")], false)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
243
284
|
if (earlyCommands.has(command) || command === "update") {
|
|
244
285
|
await spawnSenpi(args, false)
|
|
245
286
|
return
|
|
246
287
|
}
|
|
247
288
|
if (isInteractiveDefault(args)) {
|
|
248
|
-
console.error(
|
|
289
|
+
console.error(releaseBanner())
|
|
249
290
|
if (process.stdout.isTTY === true && setupSuggestionForLaunch()) {
|
|
250
291
|
console.error("omo: sibling credentials detected; run `omo setup` to review them")
|
|
251
292
|
}
|
package/bin/lib/package-paths.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { homedir } from "node:os"
|
|
2
3
|
import { basename, dirname, join, parse } from "node:path"
|
|
3
4
|
import { fileURLToPath } from "node:url"
|
|
4
5
|
|
|
@@ -19,6 +20,11 @@ export function releaseChannel(version = packageManifest().version) {
|
|
|
19
20
|
return typeof version === "string" && version.includes("-") ? "beta" : "latest"
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
/** The startup banner line: a prerelease names its beta channel, a stable release does not. */
|
|
24
|
+
export function releaseBanner(version = packageManifest().version) {
|
|
25
|
+
return releaseChannel(version) === "beta" ? `omo (omo-ai beta ${version})` : `omo (omo-ai ${version})`
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
/** The package spec that installs this build's channel: `omo-ai@beta` or the bare `omo-ai`. */
|
|
23
29
|
export function channelPackageSpec(version = packageManifest().version) {
|
|
24
30
|
return releaseChannel(version) === "beta" ? "omo-ai@beta" : "omo-ai"
|
|
@@ -32,21 +38,32 @@ function installedVersion(root) {
|
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
export function updateTarget(
|
|
41
|
+
export function updateTarget(
|
|
42
|
+
root = packageRoot,
|
|
43
|
+
platform = process.platform,
|
|
44
|
+
version = installedVersion(root),
|
|
45
|
+
homeDir = process.env.HOME || process.env.USERPROFILE || homedir(),
|
|
46
|
+
exists = existsSync,
|
|
47
|
+
) {
|
|
36
48
|
const spec = channelPackageSpec(version)
|
|
37
49
|
const updateCwd = dirname(join(root, "package.json"))
|
|
38
50
|
const normalizedRoot = updateCwd.replaceAll("\\", "/")
|
|
39
|
-
|
|
51
|
+
const normalizedHome = homeDir.replaceAll("\\", "/").replace(/\/+$/, "")
|
|
52
|
+
const bunInstall = normalizedRoot.endsWith(BUN_GLOBAL_PACKAGE_SUFFIX)
|
|
53
|
+
? normalizedRoot.slice(0, -BUN_GLOBAL_PACKAGE_SUFFIX.length)
|
|
54
|
+
: undefined
|
|
55
|
+
const isLegacyBunGlobal = normalizedRoot === `${normalizedHome}/node_modules/omo-ai`
|
|
56
|
+
&& (exists(join(homeDir, "bun.lock")) || exists(join(homeDir, "bun.lockb")))
|
|
57
|
+
if (bunInstall !== undefined || isLegacyBunGlobal) {
|
|
40
58
|
// `--cwd` into this package dir does not retarget `bun add -g`; bun still installs into
|
|
41
59
|
// `$BUN_INSTALL/install/global` (or `~/.bun` when that env is unset). The prefix is the
|
|
42
60
|
// ancestor of `/install/global/`, and the spawn overlays it so this install is the one that
|
|
43
|
-
// moves.
|
|
44
|
-
const bunInstall = normalizedRoot.slice(0, -BUN_GLOBAL_PACKAGE_SUFFIX.length)
|
|
61
|
+
// moves. A legacy Bun home-root install must carry Bun's lockfile and keeps its ambient configuration.
|
|
45
62
|
return {
|
|
46
63
|
manager: "bun",
|
|
47
64
|
command: `bun add -g ${spec}`,
|
|
48
65
|
argv: ["bun", "add", "-g", spec],
|
|
49
|
-
env: { BUN_INSTALL: bunInstall },
|
|
66
|
+
...(bunInstall === undefined ? {} : { env: { BUN_INSTALL: bunInstall } }),
|
|
50
67
|
}
|
|
51
68
|
}
|
|
52
69
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Derived from @code-yeongyu/senpi 2026.9.
|
|
2
|
+
"_comment": "Derived from @code-yeongyu/senpi 2026.9.28-7 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,67 @@
|
|
|
1
|
+
import { closeSync, openSync, readdirSync, readSync, realpathSync, statSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
|
|
4
|
+
const SAMPLE_BYTES = 64 * 1024
|
|
5
|
+
|
|
6
|
+
// A compiled omo copies itself to ~/.omo/binary-runtime/<version>/omo on its first run
|
|
7
|
+
// (materializeProvisionedExecutable in compile-runtime.ts), so a standalone binary that has run
|
|
8
|
+
// once is that file or a byte-identical copy of it. Copies are compared by size and a bounded
|
|
9
|
+
// head+tail sample: these binaries are ~100 MB and the doctor must stay fast.
|
|
10
|
+
export function standaloneBinaryVersion(binPath, homeDir, isWindows) {
|
|
11
|
+
const root = join(homeDir, ".omo", "binary-runtime")
|
|
12
|
+
let versions
|
|
13
|
+
try {
|
|
14
|
+
versions = readdirSync(root)
|
|
15
|
+
} catch {
|
|
16
|
+
return null
|
|
17
|
+
}
|
|
18
|
+
const real = realPathOf(binPath)
|
|
19
|
+
const size = regularFileSize(real)
|
|
20
|
+
if (size === undefined) return null
|
|
21
|
+
for (const version of versions) {
|
|
22
|
+
const provisioned = realPathOf(join(root, version, isWindows ? "omo.exe" : "omo"))
|
|
23
|
+
if (provisioned === real) return version
|
|
24
|
+
if (regularFileSize(provisioned) === size && sameSamples(real, provisioned, size)) return version
|
|
25
|
+
}
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function realPathOf(path) {
|
|
30
|
+
try {
|
|
31
|
+
return realpathSync(path)
|
|
32
|
+
} catch {
|
|
33
|
+
return path
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function regularFileSize(path) {
|
|
38
|
+
try {
|
|
39
|
+
const stats = statSync(path)
|
|
40
|
+
return stats.isFile() ? stats.size : undefined
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sameSamples(left, right, size) {
|
|
47
|
+
const length = Math.min(SAMPLE_BYTES, size)
|
|
48
|
+
const tail = Math.max(0, size - length)
|
|
49
|
+
const a = readSamples(left, length, tail)
|
|
50
|
+
const b = readSamples(right, length, tail)
|
|
51
|
+
return a !== undefined && b !== undefined && a.equals(b)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readSamples(path, length, tailOffset) {
|
|
55
|
+
let descriptor
|
|
56
|
+
try {
|
|
57
|
+
descriptor = openSync(path, "r")
|
|
58
|
+
const buffer = Buffer.alloc(length * 2)
|
|
59
|
+
readSync(descriptor, buffer, 0, length, 0)
|
|
60
|
+
readSync(descriptor, buffer, length, length, tailOffset)
|
|
61
|
+
return buffer
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined
|
|
64
|
+
} finally {
|
|
65
|
+
if (descriptor !== undefined) closeSync(descriptor)
|
|
66
|
+
}
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omo-ai",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.0",
|
|
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": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@babel/parser": "8.0.4",
|
|
21
|
-
"@code-yeongyu/senpi": "2026.9.
|
|
21
|
+
"@code-yeongyu/senpi": "2026.9.28-7"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|