omo-ai 5.0.1 → 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.
Files changed (31) hide show
  1. package/bin/lib/computer-use-doctor.js +90 -0
  2. package/bin/lib/doctor-migration.js +69 -15
  3. package/bin/lib/doctor.js +2 -2
  4. package/bin/lib/launcher.js +45 -4
  5. package/bin/lib/package-paths.js +17 -5
  6. package/bin/lib/provider-map.json +1 -1
  7. package/bin/lib/standalone-binary.js +67 -0
  8. package/package.json +2 -2
  9. package/plugin/CHANGELOG.md +117 -0
  10. package/plugin/extensions/assets.generated.json +7 -0
  11. package/plugin/extensions/memory-run-supervisor.mjs +2 -2
  12. package/plugin/extensions/omo-computer-use.js +2 -0
  13. package/plugin/extensions/omo-init-deep-advisor.js +2 -2
  14. package/plugin/extensions/omo-member.js +2 -2
  15. package/plugin/extensions/omo-task.js +2 -2
  16. package/plugin/extensions/omo.js +2 -2
  17. package/plugin/package.json +2 -1
  18. package/plugin/runtime/agent-toolkit-sdk/sdk.js +2 -2
  19. package/plugin/runtime/ast-grep-mcp/manifest.json +1 -1
  20. package/plugin/runtime/category-coverage/index.js +8 -2
  21. package/plugin/runtime/lsp-daemon/dist/.omo-runtime-manifest.json +1 -1
  22. package/plugin/scripts/install.mjs +2 -0
  23. package/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  24. package/plugin/skills/coding-agent-sessions/SKILL.md +1 -1
  25. package/plugin/skills/coding-agent-sessions/scripts/agent_sessions/aside_scanner.py +3 -0
  26. package/plugin/skills/coding-agent-sessions/scripts/agent_sessions/cli.py +4 -0
  27. package/plugin/skills/coding-agent-sessions/scripts/agent_sessions/file_scanners.py +6 -2
  28. package/plugin/skills/coding-agent-sessions/scripts/agent_sessions/scanners.py +7 -1
  29. package/plugin/skills/coding-agent-sessions/scripts/agent_sessions/transcript.py +3 -1
  30. package/plugin/skills/coding-agent-sessions/scripts/agent_sessions/types.py +3 -1
  31. 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
- entries.push({ binPath, directory, owner, kind: classify(owner) })
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
- /** The entries a typed `omo` reaches before omo-ai's own; all of them when omo-ai is not on PATH. */
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 nativeIndex = entries.findIndex((entry) => entry.kind === "native")
83
- return (nativeIndex < 0 ? entries : entries.slice(0, nativeIndex)).filter((entry) => entry.kind !== "native")
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 === undefined) return null
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 from its text alone.
114
- for (const entry of shimEntryPaths(shim, dirname(binPath))) {
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
- export function formatMigrationLines({ shadowing, nativeDirectory, legacyPackages, registrations, restoreCommand }) {
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
- ? "remove that file, or put omo-ai's bin dir ahead of it on PATH."
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 omo-ai on PATH: ${entry.binPath} (${ownerLabel(entry.owner)}). Fix: ${fix}`)
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
- ? `npm uninstall -g ${found.name}, then re-run ${restoreCommand} if omo disappears.`
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: bins.find((entry) => entry.kind === "native")?.directory ?? null,
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
- lines.push(...(options.categoryCoverage ?? []))
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
  }
@@ -1,8 +1,9 @@
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"
@@ -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" ? [] : await doctorCoverageLines({ agentDir: canonicalAgentDir() })
224
- runDoctor(await detectHarnesses(), args.slice(1), { daemonEngine: { run: engineHostCall }, categoryCoverage })
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,6 +275,12 @@ 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
@@ -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
 
@@ -37,21 +38,32 @@ function installedVersion(root) {
37
38
  }
38
39
  }
39
40
 
40
- export function updateTarget(root = packageRoot, platform = process.platform, version = installedVersion(root)) {
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
+ ) {
41
48
  const spec = channelPackageSpec(version)
42
49
  const updateCwd = dirname(join(root, "package.json"))
43
50
  const normalizedRoot = updateCwd.replaceAll("\\", "/")
44
- if (normalizedRoot.endsWith(BUN_GLOBAL_PACKAGE_SUFFIX)) {
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) {
45
58
  // `--cwd` into this package dir does not retarget `bun add -g`; bun still installs into
46
59
  // `$BUN_INSTALL/install/global` (or `~/.bun` when that env is unset). The prefix is the
47
60
  // ancestor of `/install/global/`, and the spawn overlays it so this install is the one that
48
- // moves. `platform` stays on the signature because callers pass the host they are describing.
49
- 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.
50
62
  return {
51
63
  manager: "bun",
52
64
  command: `bun add -g ${spec}`,
53
65
  argv: ["bun", "add", "-g", spec],
54
- env: { BUN_INSTALL: bunInstall },
66
+ ...(bunInstall === undefined ? {} : { env: { BUN_INSTALL: bunInstall } }),
55
67
  }
56
68
  }
57
69
  return {
@@ -1,5 +1,5 @@
1
1
  {
2
- "_comment": "Derived from @code-yeongyu/senpi 2026.9.27 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.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.0.1",
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.27"
21
+ "@code-yeongyu/senpi": "2026.9.28-7"
22
22
  },
23
23
  "repository": {
24
24
  "type": "git",
@@ -7,6 +7,123 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [5.1.0] - 2026-09-28
11
+
12
+ 5.1.0 adds computer use to OmO Native as an experimental feature, and fixes Claude subscription logins across many sessions, GitHub Copilot tokens and hosts, goals that looped on a rejected login, and a long list of background task problems. `omo update` installs it, or run:
13
+
14
+ ```bash
15
+ bun add -g omo-ai
16
+ ```
17
+
18
+ ### Computer use (experimental)
19
+
20
+ - **Experimental:** OmO Native agents can now drive your real desktop. They take screenshots, list windows, click, type, scroll, press keys, read the accessibility tree and use the clipboard, through a `computer` tool and a `computer` global in the eval kernels. Behaviour may still change, so please send feedback and bug reports.
21
+ - **Per OS:**
22
+ - macOS (arm64, x64): background input by default, so your frontmost app, focus and cursor stay put.
23
+ - Windows x64: background input varies by app, and nothing can reach elevated apps.
24
+ - Linux x64 on X11: XTEST/XSendEvent plus AT-SPI.
25
+ - Linux x64 on Wayland: portals and libei, with no single-window capture.
26
+ - No engine ships yet for Linux arm64 or Windows arm64.
27
+ - **Turning it on:** it is on by default on supported hosts, but its code and native engine load only on first use, so sessions that never use it pay nothing at startup. The agent finds `computer` through tool search, or you run `/computer on`. `/computer status` shows the engine, the stop path and the OS permissions. Set `computer.enabled: false` to remove it entirely. macOS needs Screen Recording and Accessibility.
28
+ - **Scrolling:** `dx`/`dy` are pixels on every OS (one wheel notch is 40 px), and a positive `dy` scrolls down everywhere, macOS included. ([#9101](https://github.com/code-yeongyu/oh-my-openagent/issues/9101), [#9055](https://github.com/code-yeongyu/oh-my-openagent/issues/9055))
29
+ - **Kill switch:** Control+Option+Command+Escape on macOS, or Ctrl+Alt+Shift+Escape on Linux and Windows, stops all computer input at once, including typing already in progress. Input stays stopped until you run `/computer resume`; the agent cannot resume it.
30
+ - **Safety:** screenshots need `computer:read` and input needs `computer:exec`. Before input, OmO checks the stop path, the screen lock, the OS permissions and that the target point is uncovered.
31
+ - **Known limits:** some toolkits ignore synthetic background input; OmO then reports it instead of pretending it worked. X11 background input doesn't reach GTK, Qt or Chromium yet (#9108). Wayland's stop chord needs the GlobalShortcuts portal. Full guide: [`docs/guide/computer-use.md`](https://github.com/code-yeongyu/oh-my-openagent/blob/dev/docs/guide/computer-use.md).
32
+
33
+ ### Added
34
+
35
+ - `omo app-server` threads load the OmO plugin, so they get `task`, `workpool`, `memory`, `lsp_*` and the other OmO tools, and `app-server daemon restart` keeps it. ([#9117](https://github.com/code-yeongyu/oh-my-openagent/issues/9117), [#9122](https://github.com/code-yeongyu/oh-my-openagent/pull/9122), [senpi#2313](https://github.com/code-yeongyu/senpi/issues/2313))
36
+ - Durable scheduled prompts: the `schedule_prompt` tool stores a reminder, follow-up or recurring prompt, and `schedule run` fires it later, also for `--print` runs. ([senpi#2216](https://github.com/code-yeongyu/senpi/issues/2216))
37
+ - Sessions follow a moved or re-cloned repository: `--session`, `--resume` and `/resume` offer to move them here, keeping their history, goal, loops and monitors. ([senpi#2181](https://github.com/code-yeongyu/senpi/issues/2181), [senpi#2184](https://github.com/code-yeongyu/senpi/issues/2184))
38
+ - `models discover <provider>` fills an OpenAI-compatible provider's model list in `models.json` from its `/models` endpoint, including the reasoning levels it advertises. ([senpi#2196](https://github.com/code-yeongyu/senpi/issues/2196))
39
+ - Eval kernels say when they hold a lot of memory, name the largest globals, and restart once past a ceiling (`memory.noticeMb`, `memory.ceilingMb`). ([senpi#2261](https://github.com/code-yeongyu/senpi/issues/2261))
40
+ - `host status --all` lists every RPC host endpoint, `host gc` clears the state of hosts that are provably gone, and `host status` shows `memory_pressure`. ([senpi#2226](https://github.com/code-yeongyu/senpi/issues/2226), [senpi#2245](https://github.com/code-yeongyu/senpi/issues/2245))
41
+ - A native crash of an interactive or print session leaves one entry in `process-crashes/crashes.jsonl` on the next start. ([senpi#2194](https://github.com/code-yeongyu/senpi/issues/2194))
42
+
43
+ ### Changed
44
+
45
+ - OmO Native moves from senpi 2026.9.27 to senpi 2026.9.28-7; the engine fixes below come with it.
46
+ - A parent no longer runs out of room for its task children: `task.residency_max_children` defaults to `"unlimited"`. Set a number in `omo.json` to keep a bound. ([#8999](https://github.com/code-yeongyu/oh-my-openagent/issues/8999))
47
+ - Memory extracts facts from one very long conversation entry (up to 512 KiB) in a bounded extra run instead of parking it forever, which can mean extra model calls. ([#8984](https://github.com/code-yeongyu/oh-my-openagent/issues/8984), contributed by @deadcode-walker in [#8985](https://github.com/code-yeongyu/oh-my-openagent/pull/8985))
48
+ - After an account-wide usage limit, a task's fallback tries the next provider's models first and keeps the spent provider as the last resort; a limit on one model or family still moves to the next model in order. ([#8296](https://github.com/code-yeongyu/oh-my-openagent/issues/8296))
49
+ - A fallback-chain model that keeps failing is skipped for a cooldown (60 s, doubling up to 30 minutes) by every session in the process, and a provider's `Retry-After` is honored. ([senpi#2198](https://github.com/code-yeongyu/senpi/issues/2198))
50
+ - GPT-6 Astra, Sol and Luna stay on the stated goal and report unrelated errors in the final message instead of chasing them. ([senpi#2256](https://github.com/code-yeongyu/senpi/issues/2256))
51
+ - OpenCode Go and Fireworks default to `kimi-k3`, since their old Kimi defaults left the catalogs. ([senpi#2295](https://github.com/code-yeongyu/senpi/issues/2295), [senpi#2175](https://github.com/code-yeongyu/senpi/issues/2175))
52
+
53
+ ### Fixed
54
+
55
+ **Providers and logins**
56
+
57
+ - Claude subscription sessions no longer hit `401 OAuth access token has been revoked` and end up "blocked until re-login" when several sessions run at once; they pick up the refreshed token instead. ([#8762](https://github.com/code-yeongyu/oh-my-openagent/issues/8762), [senpi#2225](https://github.com/code-yeongyu/senpi/issues/2225), [senpi#2281](https://github.com/code-yeongyu/senpi/issues/2281), [senpi#2282](https://github.com/code-yeongyu/senpi/issues/2282))
58
+ - `/login` into a blocked Claude account now replaces the revoked token instead of keeping it. ([senpi#2222](https://github.com/code-yeongyu/senpi/issues/2222))
59
+ - A Claude subscription turn no longer fails with "pre-replay buffer overflow" while Claude Code runs a background turn of its own. ([senpi#2192](https://github.com/code-yeongyu/senpi/issues/2192))
60
+ - GitHub Copilot recovers when GitHub revokes its cached token: the token is re-exchanged once and the request re-sent, so every model no longer fails with HTTP 403 until `/login`. ([senpi#2297](https://github.com/code-yeongyu/senpi/issues/2297), [senpi#2302](https://github.com/code-yeongyu/senpi/issues/2302))
61
+ - GitHub Copilot Business and Enterprise accounts reach their own API host instead of failing with `421 Misdirected Request`, for chat, `/btw` and web search. ([#8662](https://github.com/code-yeongyu/oh-my-openagent/issues/8662), [senpi#2309](https://github.com/code-yeongyu/senpi/issues/2309), [senpi#2310](https://github.com/code-yeongyu/senpi/issues/2310))
62
+ - Copilot turns no longer fail with a bare HTTP 400 when more than 128 tools are exposed; the extras are left out and you're told how many. ([senpi#2298](https://github.com/code-yeongyu/senpi/issues/2298))
63
+ - Copilot models compact before Copilot's own prompt limit instead of failing the turn. ([senpi#2299](https://github.com/code-yeongyu/senpi/issues/2299))
64
+ - A Copilot 402, 403 or quota 429 says whether it's a quota or a refusal and includes the GitHub request id. ([senpi#2297](https://github.com/code-yeongyu/senpi/issues/2297))
65
+ - A goal stops on the first 401 or 403 a provider keeps returning and tells you what to do (for example `Run /login github-copilot ...`) instead of looping to the continuation cap; your next message resumes it. ([senpi#2293](https://github.com/code-yeongyu/senpi/issues/2293))
66
+ - The first message no longer fails on providers that only accept automatic tool choice, for example Kiro behind a proxy; `compat.supportsForcedToolChoice: false` skips the forced choice entirely. ([#9096](https://github.com/code-yeongyu/oh-my-openagent/issues/9096), [senpi#2218](https://github.com/code-yeongyu/senpi/issues/2218), [senpi#2224](https://github.com/code-yeongyu/senpi/issues/2224))
67
+ - Devin SWE-2 lists only the lanes Devin serves (`swe-2-medium`, `swe-2-high`, `swe-2-max`), `devin/swe-2-medium` gets the SWE-2 prompt preset, and a config naming `swe-2`, `swe-2-low` or `swe-2-high-lite` gets a startup warning. ([#9111](https://github.com/code-yeongyu/oh-my-openagent/issues/9111), [senpi#2306](https://github.com/code-yeongyu/senpi/issues/2306), [senpi#2308](https://github.com/code-yeongyu/senpi/issues/2308))
68
+
69
+ **Memory**
70
+
71
+ - Kibitzer and the other memory sidecars fall back to their category's builtin models when a pinned model refuses, instead of failing every wake. ([#9111](https://github.com/code-yeongyu/oh-my-openagent/issues/9111))
72
+ - The Kibitzer failure notice names the model that failed and the `omo.json` setting to change. ([#9111](https://github.com/code-yeongyu/oh-my-openagent/issues/9111))
73
+ - Memory reflection moves to the next model when one hits a spent usage or quota limit, instead of ending the run. ([#8296](https://github.com/code-yeongyu/oh-my-openagent/issues/8296), [#6808](https://github.com/code-yeongyu/oh-my-openagent/issues/6808))
74
+ - Memory dreaming sees which skills and memory files you actually read again. ([#8864](https://github.com/code-yeongyu/oh-my-openagent/issues/8864), reported by @katamana, fixed by @MoerAI in [#8865](https://github.com/code-yeongyu/oh-my-openagent/pull/8865))
75
+
76
+ **Tasks and subagents**
77
+
78
+ - Task children wait for a busy task host instead of failing with "Task runner failed to start." within 30 s. ([#9067](https://github.com/code-yeongyu/oh-my-openagent/issues/9067), [senpi#2209](https://github.com/code-yeongyu/senpi/issues/2209))
79
+ - A task child that kept working through a host hiccup or handoff is no longer left "suspended" until the parent restarts. ([#9069](https://github.com/code-yeongyu/oh-my-openagent/issues/9069))
80
+ - A task waiting for a free slot on its fallback model shows as queued with its position, not as running. ([#9069](https://github.com/code-yeongyu/oh-my-openagent/issues/9069))
81
+ - A task's status follows its child: a nudged child no longer shows as failed, and a child that wakes up on its own sends a `task completion (resumed turn)`. ([#9069](https://github.com/code-yeongyu/oh-my-openagent/issues/9069))
82
+ - A delegated task no longer hangs in `running` (and `omo -p` no longer stays open) when its first model fails and it moves to the next one.
83
+ - A task child whose host restarted mid-turn continues its interrupted turn when the parent resumes. ([#9003](https://github.com/code-yeongyu/oh-my-openagent/issues/9003))
84
+ - A suspended task child frees its concurrency slot, so `task_send` no longer answers `lane_capacity` after a few restarts. ([#8973](https://github.com/code-yeongyu/oh-my-openagent/issues/8973))
85
+ - Task start failures say why the child couldn't start instead of only `Task runner failed to start.` ([#8960](https://github.com/code-yeongyu/oh-my-openagent/issues/8960))
86
+ - A child whose host dies while it opens reports `host_unreachable`. ([#9020](https://github.com/code-yeongyu/oh-my-openagent/issues/9020))
87
+ - A re-prompted child on a loaded host no longer prints `Timeout waiting for response to prompt` over the parent's screen. ([#9093](https://github.com/code-yeongyu/oh-my-openagent/issues/9093))
88
+ - A task whose whole fallback chain failed says so in its transcript. ([#8301](https://github.com/code-yeongyu/oh-my-openagent/issues/8301), contributed by @MoerAI in [#9022](https://github.com/code-yeongyu/oh-my-openagent/pull/9022))
89
+ - Live task progress, stats and transcripts keep updating after a child reconnects to its host. ([#8983](https://github.com/code-yeongyu/oh-my-openagent/issues/8983), contributed by @deadcode-walker in [#8978](https://github.com/code-yeongyu/oh-my-openagent/pull/8978))
90
+ - DAG nodes finish when their child task finishes, also in projects reached through a symlink. ([#8932](https://github.com/code-yeongyu/oh-my-openagent/issues/8932), reported by @ayalcoh)
91
+ - A cancel, a cleanup sweep or a reload during a model fallback no longer ends a revived run, loses a lane slot, or starts the next model beside a child that's still closing.
92
+
93
+ **Task host**
94
+
95
+ - The shared host keeps opening sessions after a session's directory is deleted, instead of failing every new task child with `ENOENT`. ([senpi#2206](https://github.com/code-yeongyu/senpi/issues/2206))
96
+ - The shared host no longer refuses task children when its memory is high, and it leaves memory pressure once memory is actually freed. ([#8960](https://github.com/code-yeongyu/oh-my-openagent/issues/8960), [senpi#2207](https://github.com/code-yeongyu/senpi/issues/2207), [senpi#2261](https://github.com/code-yeongyu/senpi/issues/2261))
97
+ - A transient task daemon idle-exits again while omo runs, and polling `host status` no longer keeps hosts alive. ([#9041](https://github.com/code-yeongyu/oh-my-openagent/issues/9041), [senpi#2242](https://github.com/code-yeongyu/senpi/issues/2242), [senpi#2227](https://github.com/code-yeongyu/senpi/issues/2227))
98
+ - An old host generation exits after a handoff even when one of its sessions misbehaves, and a handoff no longer inherits the calling session's identity or model. ([senpi#2285](https://github.com/code-yeongyu/senpi/issues/2285), [senpi#2208](https://github.com/code-yeongyu/senpi/issues/2208))
99
+
100
+ **Commands and sessions**
101
+
102
+ - `/ulw-execute <plan>` and every bundled skill name work as bare commands, like `/skill:<name>`. ([#9042](https://github.com/code-yeongyu/oh-my-openagent/issues/9042))
103
+ - A bare `/skill` or `/skill:` opens the skill list instead of going to the model. ([senpi#2249](https://github.com/code-yeongyu/senpi/issues/2249), [senpi#2251](https://github.com/code-yeongyu/senpi/issues/2251))
104
+ - `/thinking` works again, `/sessions` opens the session picker, and `/help` says how to change the thinking level and reopen a session. ([senpi#1437](https://github.com/code-yeongyu/senpi/issues/1437))
105
+ - Answering `y` to "Fork this session into current directory?" forks it instead of printing `Aborted.` ([senpi#2180](https://github.com/code-yeongyu/senpi/issues/2180), [senpi#1042](https://github.com/code-yeongyu/senpi/pull/1042) by @beengineer500)
106
+ - Starting omo no longer empties an upstream pi install, and a `~/.pi/agent` an earlier start drained gets its files copied back. ([#8039](https://github.com/code-yeongyu/oh-my-openagent/issues/8039), [senpi#2215](https://github.com/code-yeongyu/senpi/issues/2215))
107
+ - MCP tools register once per session instead of twice. ([senpi#2177](https://github.com/code-yeongyu/senpi/issues/2177))
108
+ - Unhandled errors go to the debug log instead of over the TUI, and a fatal crash prints one line with the log path. ([senpi#2284](https://github.com/code-yeongyu/senpi/issues/2284))
109
+ - ulw plan progress counts `T1.2`, `F1`-style task ids and blocked `- [~]` rows, and a plan with no countable tasks no longer triggers a `0/0` continuation. ([#9019](https://github.com/code-yeongyu/oh-my-openagent/issues/9019), [#6233](https://github.com/code-yeongyu/oh-my-openagent/issues/6233))
110
+ - Session search in the `coding-agent-sessions` skill finds prompts from the middle of a session. ([#9012](https://github.com/code-yeongyu/oh-my-openagent/issues/9012))
111
+
112
+ **Edits and eval**
113
+
114
+ - When the formatter rewrites a file after `write`, `edit` or `apply_patch`, the model always gets the "auto-formatted, re-read before exact-text edits" notice, also when diagnostics come back clean. ([#9123](https://github.com/code-yeongyu/oh-my-openagent/issues/9123))
115
+ - The comment checker replaces a stale cached binary with the pinned release, and warns in the session when it can't run instead of going quiet. ([#8850](https://github.com/code-yeongyu/oh-my-openagent/issues/8850), contributed by @cynkai in [#8877](https://github.com/code-yeongyu/oh-my-openagent/pull/8877) and [#8880](https://github.com/code-yeongyu/oh-my-openagent/pull/8880))
116
+ - `new Bun.WebView()` works in eval cells on Windows and Linux, and with `backend: "chrome"` on macOS, and no longer leaves Chrome running. ([senpi#2248](https://github.com/code-yeongyu/senpi/issues/2248), [senpi#2272](https://github.com/code-yeongyu/senpi/issues/2272))
117
+ - Eval memory goes back to the machine without a reset, settled cells no longer pin their images in memory, and retired workers don't leave zombie processes. ([senpi#2259](https://github.com/code-yeongyu/senpi/issues/2259), [senpi#2260](https://github.com/code-yeongyu/senpi/issues/2260), [senpi#2261](https://github.com/code-yeongyu/senpi/issues/2261), [senpi#1962](https://github.com/code-yeongyu/senpi/issues/1962))
118
+ - Eval cells that print a lot of output finish about 4x faster. ([senpi#2262](https://github.com/code-yeongyu/senpi/issues/2262))
119
+
120
+ **Install, update and doctor**
121
+
122
+ - `omo update` on a compiled binary picks the newest release on its own channel and the asset it was built as, and prints a command that replaces the running binary; the TUI update notice says `omo update`.
123
+ - `omo doctor` treats a standalone omo binary as an OmO install instead of an unknown file to delete, and warns once when it and omo-ai are both on PATH.
124
+ - On Windows, `omo doctor` and `install --platform=native` recognize a Bun-installed omo, and `omo update` uses `bun add -g` for a legacy Bun home install. ([#8909](https://github.com/code-yeongyu/oh-my-openagent/issues/8909))
125
+ - The computer use engine for Intel Macs starts: it was built for macOS 10.12 and could not load its Swift libraries, and the release now runs its selftest under Rosetta. ([#9139](https://github.com/code-yeongyu/oh-my-openagent/pull/9139))
126
+
10
127
  ## [5.0.1] - 2026-09-27
11
128
 
12
129
  A patch release for problems people hit on 5.0.0. `omo update` installs it, or run: