oh-my-opencode 5.1.13 → 5.1.15

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 (80) hide show
  1. package/dist/cli/index.js +18 -17
  2. package/dist/cli-node/index.js +18 -17
  3. package/dist/index.js +16 -16
  4. package/dist/skills/browser/SKILL.md +26 -2
  5. package/dist/skills/browser/references/commands.md +10 -1
  6. package/dist/skills/browser/references/install.md +7 -0
  7. package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
  8. package/dist/skills/browser/scripts/browser-engine-guard.mjs +506 -0
  9. package/dist/skills/browser/scripts/omowright.mjs +29 -2
  10. package/dist/tui.js +16 -16
  11. package/package.json +14 -14
  12. package/packages/lsp-core/src/post-edit/not-installed.test.ts +2 -2
  13. package/packages/lsp-core/src/post-edit/orchestration.test.ts +13 -13
  14. package/packages/lsp-core/src/post-edit/orchestration.ts +9 -3
  15. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  16. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  17. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  19. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  20. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  21. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  23. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +4 -4
  25. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +33 -19
  26. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +27 -17
  27. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  28. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  29. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +36 -18
  30. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-not-installed.test.ts +203 -0
  31. package/packages/omo-codex/plugin/components/rules/CHANGELOG.md +1 -0
  32. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-6.md +1 -1
  33. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  34. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  35. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  36. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  37. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  38. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  39. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  40. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  41. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  42. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  44. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  45. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  46. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  47. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  48. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  49. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  50. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  51. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  52. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  53. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  54. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  55. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  56. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  57. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  58. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  59. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  60. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  61. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  62. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  63. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  66. package/packages/omo-codex/plugin/package-lock.json +12 -12
  67. package/packages/omo-codex/plugin/package.json +1 -1
  68. package/packages/omo-codex/plugin/skills/browser/SKILL.md +26 -2
  69. package/packages/omo-codex/plugin/skills/browser/references/commands.md +10 -1
  70. package/packages/omo-codex/plugin/skills/browser/references/install.md +7 -0
  71. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  72. package/packages/omo-codex/plugin/skills/browser/scripts/browser-engine-guard.mjs +506 -0
  73. package/packages/omo-codex/plugin/skills/browser/scripts/omowright.mjs +29 -2
  74. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  75. package/packages/shared-skills/skills/browser/SKILL.md +26 -2
  76. package/packages/shared-skills/skills/browser/references/commands.md +10 -1
  77. package/packages/shared-skills/skills/browser/references/install.md +7 -0
  78. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  79. package/packages/shared-skills/skills/browser/scripts/browser-engine-guard.mjs +506 -0
  80. package/packages/shared-skills/skills/browser/scripts/omowright.mjs +29 -2
@@ -0,0 +1,506 @@
1
+ export const BROWSER_CONFIRMATION_POLICY = "ask-before-irreversible"
2
+ export const BROWSER_ENGINE_ENV = "OMO_BROWSER_ENGINE"
3
+ export const BROWSER_STATE_EVENT = "omo.browser.state"
4
+
5
+ const BRIDGE_TOOL = "omo_browser_bridge"
6
+ const ENGINES = new Set(["connected", "builtin", "none"])
7
+ const NOT_CONNECTED_CODES = new Set(["no_browser_connected", "no_daemon", "unsupported"])
8
+ const CLASSIFY_HTML_BYTES = 4096
9
+ const QUESTION_HEADER = "Browser"
10
+ const ALLOW = "Allow"
11
+ const DECLINE = "Don't allow"
12
+ const READ_ONLY_RAW_TOOLS = new Set(["observe", "snapshot", "get_html", "screenshot", "tab_list", "console", "network"])
13
+
14
+ const OWNED_SESSION_CREATORS = new Set(["connect", "connectPipe", "connectCloakProfile"])
15
+ const PASSTHROUGH_FUNCTIONS = new Set([
16
+ "bskDoctor",
17
+ "bskOnboard",
18
+ "installBskCli",
19
+ "readDaemonInfo",
20
+ "resolveBskHome",
21
+ "listBrowsers",
22
+ "catalogBrowsers",
23
+ "detectBrowsers",
24
+ "identifyBrowser",
25
+ "probeBrowserSignals",
26
+ "findCloakBrowserPath",
27
+ "resolveCloakProfile",
28
+ "registerExternalExtension",
29
+ "unregisterExternalExtension",
30
+ "externalExtensionEntry",
31
+ "buildSnapshotExpression",
32
+ "buildCloakBrowserArgs",
33
+ "compactSnapshot",
34
+ "snapshotTokens",
35
+ "describeLayers",
36
+ "layersHeader",
37
+ "decodeFrames",
38
+ "encodeFrame",
39
+ "reconcileFrames",
40
+ "normalizeDialogPolicy",
41
+ "resolveDialogAction",
42
+ "sanitizeCookies",
43
+ "toHar",
44
+ "BskRpcError",
45
+ "BridgeProtocolError",
46
+ "BridgeUnavailableError",
47
+ "BrowserCdpCommandError",
48
+ "UnsupportedOperationError",
49
+ ])
50
+
51
+ class CodedError extends Error {
52
+ constructor(name, code, message, options) {
53
+ super(message, options)
54
+ this.name = name
55
+ this.code = code
56
+ }
57
+ }
58
+
59
+ export class BrowserEngineRefusal extends CodedError {
60
+ constructor(code, message) {
61
+ super("BrowserEngineRefusal", code, message)
62
+ }
63
+ }
64
+
65
+ export class BrowserNotConnectedError extends CodedError {
66
+ constructor(message, options) {
67
+ super("BrowserNotConnectedError", "browser_not_connected", message, options)
68
+ }
69
+ }
70
+
71
+ export class BrowserUserStoppedError extends CodedError {
72
+ constructor(message = "The user stopped browser use. Report that and do not start another browser session this turn.") {
73
+ super("BrowserUserStoppedError", "user_stopped", message)
74
+ }
75
+ }
76
+
77
+ export class BrowserActionDeclinedError extends CodedError {
78
+ constructor(message, code = "user_declined") {
79
+ super("BrowserActionDeclinedError", code, message)
80
+ }
81
+ }
82
+
83
+ const IRREVERSIBLE_LABELS = [
84
+ /\b(send|post|publish|tweet|reply)\b/i,
85
+ /\b(pay|purchase|buy|subscribe|donate|checkout|submit|confirm|transfer|approve|merge)\b/i,
86
+ /\b(e-?sign|sign\s+(the\s+|this\s+|your\s+)?(document|contract|agreement|transaction|petition|form))\b/i,
87
+ /\border\b(?!\s+(history|status|details|number|tracking))/i,
88
+ /\b(delete|remove|unsubscribe|deactivate)\b/i,
89
+ /\bcancel\s+(my\s+|your\s+)?(subscription|membership|plan|account)\b/i,
90
+ /\bclose\s+(my\s+|your\s+)?account\b/i,
91
+ /결제|구매|주문|구독|삭제|제거|탈퇴|전송|발송|게시|발행|제출|승인|송금|이체|병합/,
92
+ ]
93
+ const IRREVERSIBLE_ACTION_PATH = /\/(send|post|publish|pay|purchase|order|subscribe|delete|remove)\b/i
94
+ const SCRIPT_DRIVES_UI = /\.(click|submit|requestSubmit)\s*\(|\bdispatchEvent\b/
95
+ const SCRIPT_BEACON = /\bsendBeacon\s*\(/
96
+ const SCRIPT_REACHES_NETWORK = /\b(fetch|XMLHttpRequest)\b/
97
+ const SCRIPT_WRITES = /["'`](POST|PUT|PATCH|DELETE)["'`]/i
98
+ const ENTER_KEY = /(^|\+)(Enter|Return)$/i
99
+ const SENDING_CHORD = /(^|\+)(Control|Ctrl|Meta|Cmd|Command)\+/i
100
+
101
+ export function isIrreversibleLabel(label) {
102
+ return typeof label === "string" && IRREVERSIBLE_LABELS.some((pattern) => pattern.test(label))
103
+ }
104
+
105
+ function labelsOf(descriptor) {
106
+ return [descriptor?.text, descriptor?.ariaLabel, descriptor?.title, descriptor?.value].filter((label) => typeof label === "string" && label.length > 0)
107
+ }
108
+
109
+ function markupIsEditable(html) {
110
+ return /\bcontenteditable\b(?!\s*=\s*["']?false)|\brole\s*=\s*["']textbox["']|<textarea\b/i.test(html)
111
+ }
112
+
113
+ function labelsFromMarkup(html) {
114
+ const labels = []
115
+ for (const match of html.matchAll(/\b(?:aria-label|title|value|alt)\s*=\s*"([^"]*)"/gi)) labels.push(match[1])
116
+ labels.push(html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim())
117
+ return labels.filter((label) => label.length > 0)
118
+ }
119
+
120
+ function scriptNeedsConfirmation(expression) {
121
+ if (typeof expression !== "string") return false
122
+ if (SCRIPT_DRIVES_UI.test(expression) || SCRIPT_BEACON.test(expression)) return true
123
+ return SCRIPT_REACHES_NETWORK.test(expression) && SCRIPT_WRITES.test(expression)
124
+ }
125
+
126
+ function describeElementExpression(selector) {
127
+ return `(function (selector) {
128
+ const el = selector ? document.querySelector(selector) : document.activeElement
129
+ if (!el || el === document.body || el === document.documentElement) return null
130
+ const text = (el.innerText || el.textContent || "").trim().slice(0, 200)
131
+ const form = el.form || (el.closest ? el.closest("form") : null)
132
+ return {
133
+ tag: el.localName,
134
+ editable: el.isContentEditable === true || el.localName === "textarea" || el.getAttribute("role") === "textbox",
135
+ text,
136
+ ariaLabel: el.getAttribute("aria-label") || undefined,
137
+ title: el.getAttribute("title") || undefined,
138
+ value: el.localName === "button" || (el.localName === "input" && /^(submit|button|image)$/i.test(el.type)) ? el.value : undefined,
139
+ form: form ? {
140
+ action: form.getAttribute("action") || "",
141
+ method: (form.getAttribute("method") || "get").toLowerCase(),
142
+ submitLabels: Array.from(form.querySelectorAll("button, input[type=submit], input[type=image]")).filter((b) => (b.type || "submit") === "submit").map((b) => (b.innerText || b.value || b.getAttribute("aria-label") || "").trim().slice(0, 100)),
143
+ } : undefined,
144
+ }
145
+ })(${JSON.stringify(selector ?? null)})`
146
+ }
147
+
148
+ function codeOf(error) {
149
+ return typeof error?.code === "string" ? error.code : error instanceof Error ? error.name : "error"
150
+ }
151
+
152
+ function targetKind(target) {
153
+ if (typeof target === "string") return /^@?e\d+$/.test(target) ? { kind: "ref", ref: target } : { kind: "selector", selector: target }
154
+ if (target && typeof target === "object") {
155
+ if (typeof target.ref === "string") return { kind: "ref", ref: target.ref }
156
+ if (typeof target.selector === "string") return { kind: "selector", selector: target.selector }
157
+ }
158
+ return { kind: "point" }
159
+ }
160
+
161
+ function createBridge(host, context) {
162
+ const call = async (args) => {
163
+ if (host === undefined) return undefined
164
+ try {
165
+ const result = await host.callTool(BRIDGE_TOOL, args)
166
+ return result?.hasError === true ? undefined : result
167
+ } catch {
168
+ return undefined
169
+ }
170
+ }
171
+ return {
172
+ async emit(status, extra = {}) {
173
+ const data = { engine: context.engine, status }
174
+ if (context.sessionId !== undefined) data.session = context.sessionId
175
+ for (const [key, value] of Object.entries(extra)) if (value !== undefined) data[key] = value
176
+ await call({ op: "state", data })
177
+ },
178
+ async stopped() {
179
+ return (await call({ op: "status" }))?.details?.stopped === true
180
+ },
181
+ async markStopped() {
182
+ await call({ op: "stopped" })
183
+ },
184
+ }
185
+ }
186
+
187
+ function describeTab(tab) {
188
+ if (!tab || typeof tab !== "object") return undefined
189
+ const favicon = tab.favicon_url ?? tab.faviconUrl ?? tab.favIconUrl ?? tab.favicon
190
+ const described = { id: tab.id, url: tab.url, title: tab.title }
191
+ if (typeof favicon === "string") described.favicon = favicon
192
+ return described
193
+ }
194
+
195
+ async function currentTab(rawSession) {
196
+ try {
197
+ const { tabs } = await rawSession.tabList({ scope: "agent" })
198
+ return describeTab(tabs?.find((tab) => tab.active) ?? tabs?.[0])
199
+ } catch {
200
+ return undefined
201
+ }
202
+ }
203
+
204
+ function questionFor(variant, description) {
205
+ const options = [
206
+ { label: ALLOW, description: "Do it this once." },
207
+ { label: DECLINE, description: "Stop this action and tell the agent." },
208
+ ]
209
+ const question = `Allow the agent to ${description}?`
210
+ if (variant === "ask_user_question") {
211
+ return { waitForAnswer: true, questions: [{ header: QUESTION_HEADER, question, multiSelect: false, options }] }
212
+ }
213
+ return { wait_for_answer: true, questions: [{ id: "confirm_browser_action", header: QUESTION_HEADER, question, options }] }
214
+ }
215
+
216
+ function chosenAnswer(variant, details) {
217
+ if (details?.status !== "answered") return undefined
218
+ const entries = Object.values(details.answers ?? {})
219
+ if (entries.length !== 1) return undefined
220
+ return variant === "ask_user_question" ? entries[0] : entries[0]?.answers?.[0]
221
+ }
222
+
223
+ async function confirmWithUser({ host, bridge, rawSession, description, action }) {
224
+ const decline = (unavailableReason) =>
225
+ unavailableReason === undefined
226
+ ? new BrowserActionDeclinedError(`The user declined: ${description}. Nothing was done; report that and do not retry it.`)
227
+ : new BrowserActionDeclinedError(`Nothing was done: ${description}. ${unavailableReason}`, "confirmation_unavailable")
228
+ let variant
229
+ try {
230
+ const names = host === undefined ? [] : await host.listTools()
231
+ variant = ["ask_user_question", "request_user_input"].find((name) => names.includes(name))
232
+ } catch {
233
+ variant = undefined
234
+ }
235
+ if (host === undefined || variant === undefined) {
236
+ throw decline("The user could not be asked because this session has no question tool.")
237
+ }
238
+ const tab = await currentTab(rawSession)
239
+ await bridge.emit("awaiting_confirmation", { action, tab })
240
+ let answer
241
+ try {
242
+ const result = await host.callTool(variant, questionFor(variant, description))
243
+ answer = result?.hasError === true ? undefined : chosenAnswer(variant, result?.details)
244
+ } catch {
245
+ answer = undefined
246
+ }
247
+ if (answer !== ALLOW) {
248
+ await bridge.emit("failed", { action, reason: "user_declined", tab })
249
+ throw decline()
250
+ }
251
+ }
252
+
253
+ function hostOf(url) {
254
+ try {
255
+ return new URL(url).host
256
+ } catch {
257
+ return undefined
258
+ }
259
+ }
260
+
261
+ async function describeForConfirmation(rawSession, label, rawSessionTab) {
262
+ const site = hostOf(rawSessionTab?.url)
263
+ return site === undefined ? label : `${label} on ${site}`
264
+ }
265
+
266
+ function wrapSession(rawSession, context, bridge, host) {
267
+ const nowStopped = async () => {
268
+ if (!(await bridge.stopped())) return
269
+ try {
270
+ await rawSession.stop()
271
+ } catch {
272
+ // The daemon may already have ended the session.
273
+ }
274
+ throw new BrowserUserStoppedError()
275
+ }
276
+
277
+ const confirm = async (action, label) => {
278
+ const tab = await currentTab(rawSession)
279
+ await confirmWithUser({ host, bridge, rawSession, description: await describeForConfirmation(rawSession, label, tab), action })
280
+ }
281
+
282
+ const readTarget = async (target) => {
283
+ const kind = targetKind(target)
284
+ if (kind.kind === "point") return { unknown: true, labels: [] }
285
+ try {
286
+ if (kind.kind === "ref") {
287
+ const reply = await rawSession.getHtml({ ref: kind.ref, maxBytes: CLASSIFY_HTML_BYTES })
288
+ const html = String(reply?.html ?? "")
289
+ return { unknown: false, labels: labelsFromMarkup(html), editable: markupIsEditable(html) }
290
+ }
291
+ const reply = await rawSession.evaluate(describeElementExpression(kind.selector), { returnByValue: true, awaitPromise: false })
292
+ if (!reply || reply.ok === false) return { unknown: true, labels: [] }
293
+ if (reply.value === null || reply.value === undefined) return { unknown: false, labels: [], missing: true }
294
+ return { unknown: false, labels: labelsOf(reply.value), descriptor: reply.value }
295
+ } catch {
296
+ return { unknown: true, labels: [] }
297
+ }
298
+ }
299
+
300
+ const preflightClick = async (target) => {
301
+ const read = await readTarget(target)
302
+ if (read.unknown) return confirm("click", "click a control whose label could not be read")
303
+ const label = read.labels.find(isIrreversibleLabel)
304
+ if (label !== undefined) return confirm("click", `click "${label}"`)
305
+ return undefined
306
+ }
307
+
308
+ const preflightPress = async (key, options) => {
309
+ if (typeof key !== "string" || !ENTER_KEY.test(key)) return undefined
310
+ if (SENDING_CHORD.test(key)) return confirm("press", `press ${key}, which sends in many apps`)
311
+ if (/(^|\+)Shift\+/i.test(key)) return undefined
312
+ const sendsFromMessageBox = () => confirm("press", "press Enter in a message box, which sends in many apps")
313
+ const target = options?.target
314
+ if (target !== undefined && targetKind(target).kind !== "selector") {
315
+ const read = await readTarget(target)
316
+ if (read.unknown) return confirm("press", "press Enter on a control that could not be read")
317
+ if (read.editable) return sendsFromMessageBox()
318
+ const label = read.labels.find(isIrreversibleLabel)
319
+ return label === undefined ? undefined : confirm("press", `press Enter on "${label}"`)
320
+ }
321
+ let described
322
+ try {
323
+ const reply = await rawSession.evaluate(describeElementExpression(target === undefined ? undefined : targetKind(target).selector), {
324
+ returnByValue: true,
325
+ awaitPromise: false,
326
+ })
327
+ if (!reply || reply.ok === false) return confirm("press", "press Enter in a field that could not be read")
328
+ described = reply.value
329
+ } catch {
330
+ return confirm("press", "press Enter in a field that could not be read")
331
+ }
332
+ if (described === null || described === undefined) return undefined
333
+ const form = described.form
334
+ if (described.editable === true && (form === undefined || form.method !== "get")) return sendsFromMessageBox()
335
+ const own = labelsOf(described).find(isIrreversibleLabel)
336
+ if (own !== undefined && described.tag !== "input") return confirm("press", `press Enter on "${own}"`)
337
+ if (form === undefined) return undefined
338
+ const submit = (form.submitLabels ?? []).find(isIrreversibleLabel)
339
+ if (submit !== undefined) return confirm("press", `submit a form with "${submit}"`)
340
+ if (form.method === "post" && IRREVERSIBLE_ACTION_PATH.test(String(form.action))) return confirm("press", "submit a form that sends")
341
+ return undefined
342
+ }
343
+
344
+ const act = async (kind, run, { navigates = false, preflight } = {}) => {
345
+ await nowStopped()
346
+ if (preflight !== undefined) await preflight()
347
+ await bridge.emit("acting", { action: kind })
348
+ let result
349
+ try {
350
+ result = await run()
351
+ } catch (error) {
352
+ if (codeOf(error) === "user_aborted") {
353
+ await bridge.markStopped()
354
+ await bridge.emit("user_stopped", { action: kind })
355
+ try {
356
+ await rawSession.stop()
357
+ } catch {
358
+ // The daemon already ended it.
359
+ }
360
+ throw new BrowserUserStoppedError()
361
+ }
362
+ await bridge.emit("failed", { action: kind, reason: codeOf(error) })
363
+ throw error
364
+ }
365
+ if (navigates) await bridge.emit("navigated", { action: kind, tab: await currentTab(rawSession) })
366
+ await bridge.emit("idle", { action: kind })
367
+ return result
368
+ }
369
+
370
+ const plain = (method, navigates) => (...args) => act(method, () => rawSession[method](...args), { navigates })
371
+ const handlers = {
372
+ navigate: plain("navigate", true),
373
+ back: plain("back", true),
374
+ forward: plain("forward", true),
375
+ reload: plain("reload", true),
376
+ waitForNavigation: plain("waitForNavigation", true),
377
+ tabCreate: plain("tabCreate", true),
378
+ tabSelect: plain("tabSelect", true),
379
+ tabClose: plain("tabClose", true),
380
+ tabBorrow: plain("tabBorrow", true),
381
+ tabReturn: plain("tabReturn", true),
382
+ fill: plain("fill", false),
383
+ select: plain("select", false),
384
+ hover: plain("hover", false),
385
+ focus: plain("focus", false),
386
+ blur: plain("blur", false),
387
+ scrollTo: plain("scrollTo", false),
388
+ wheel: plain("wheel", false),
389
+ click: (target, options) => act("click", () => rawSession.click(target, options), { preflight: () => preflightClick(target) }),
390
+ press: (key, options) => act("press", () => rawSession.press(key, options), { preflight: () => preflightPress(key, options) }),
391
+ evaluate: (expression, options) =>
392
+ act("evaluate", () => rawSession.evaluate(expression, options), {
393
+ preflight: () => (scriptNeedsConfirmation(expression) ? confirm("evaluate", "run a script that can click, submit or send in your browser") : undefined),
394
+ }),
395
+ tool: async (name, params) => {
396
+ if (!READ_ONLY_RAW_TOOLS.has(name)) {
397
+ throw new BrowserEngineRefusal("browser_tool_blocked", `session.tool("${name}") skips the confirmation policy; only reads go through it. Call the matching session method instead.`)
398
+ }
399
+ return await rawSession.tool(name, params)
400
+ },
401
+ async stop() {
402
+ if (rawSession.stopped) return null
403
+ try {
404
+ return await rawSession.stop()
405
+ } finally {
406
+ await bridge.emit("stopped")
407
+ }
408
+ },
409
+ }
410
+
411
+ return new Proxy(rawSession, {
412
+ get(target, property) {
413
+ if (Object.hasOwn(handlers, property)) return handlers[property]
414
+ const value = Reflect.get(target, property, target)
415
+ return typeof value === "function" ? value.bind(target) : value
416
+ },
417
+ })
418
+ }
419
+
420
+ const GUARDED_SESSIONS = new WeakMap()
421
+
422
+ function connectErrorFor(error) {
423
+ if (NOT_CONNECTED_CODES.has(codeOf(error))) {
424
+ return new BrowserNotConnectedError(
425
+ "Connect your browser: this session is set to drive the user's own browser, and none is connected. Ask the user to open the browser that has the BrowserSkill extension enabled, then try again. Do not use or launch another browser.",
426
+ { cause: error },
427
+ )
428
+ }
429
+ return error
430
+ }
431
+
432
+ export function guardOmowright(raw, { env = process.env, host } = {}) {
433
+ const engine = env[BROWSER_ENGINE_ENV]
434
+ if (engine === undefined || engine === "") return raw
435
+
436
+ const connectBrowserSkill = async (options) => {
437
+ if (!ENGINES.has(engine)) {
438
+ throw new BrowserEngineRefusal("browser_engine_unsupported", `${BROWSER_ENGINE_ENV} is "${engine}", which is not connected, builtin or none; browser use is refused.`)
439
+ }
440
+ if (engine === "none") {
441
+ throw new BrowserEngineRefusal("browser_engine_none", "Agent browser access is off for this project. Tell the user; do not try another browser.")
442
+ }
443
+ if (engine === "builtin") {
444
+ throw new BrowserEngineRefusal("browser_engine_builtin", "This session uses the app's built-in browser. Do not call connectBrowserSkill(); use the app's in-app browser tools.")
445
+ }
446
+ const context = { engine, sessionId: undefined }
447
+ const bridge = createBridge(host, context)
448
+ if (await bridge.stopped()) throw new BrowserUserStoppedError()
449
+ let rawSession
450
+ try {
451
+ rawSession = await raw.connectBrowserSkill(options)
452
+ } catch (error) {
453
+ await bridge.emit("connect_failed", { reason: codeOf(error) })
454
+ throw connectErrorFor(error)
455
+ }
456
+ context.sessionId = rawSession.sessionId
457
+ const session = wrapSession(rawSession, context, bridge, host)
458
+ GUARDED_SESSIONS.set(session, rawSession)
459
+ await bridge.emit("started")
460
+ return session
461
+ }
462
+
463
+ const bskSnapshot = (session, options) => raw.bskSnapshot(GUARDED_SESSIONS.get(session) ?? session, options)
464
+
465
+ return allowlistedLibrary(raw, engine, { connectBrowserSkill, bskSnapshot })
466
+ }
467
+
468
+ function ownedBrowserRefusal(engine) {
469
+ const why =
470
+ engine === "none"
471
+ ? "Agent browser access is off for this project."
472
+ : engine === "builtin"
473
+ ? "This session uses the app's built-in browser."
474
+ : engine === "connected"
475
+ ? "This session drives the user's own browser only."
476
+ : `${BROWSER_ENGINE_ENV} is "${engine}", which is not connected, builtin or none.`
477
+ const refusal = new BrowserEngineRefusal("browser_engine_owned_blocked", `${why} Do not launch an owned browser; tell the user instead.`)
478
+ refusal.engine = engine
479
+ return refusal
480
+ }
481
+
482
+ function blockedExport(name, engine) {
483
+ const blocked = function () {
484
+ const refusal = new BrowserEngineRefusal(
485
+ OWNED_SESSION_CREATORS.has(name) ? "browser_engine_owned_blocked" : "browser_engine_export_blocked",
486
+ OWNED_SESSION_CREATORS.has(name)
487
+ ? ownedBrowserRefusal(engine).message
488
+ : `omowright.${name} is not available while ${BROWSER_ENGINE_ENV} is set; it can act on a browser without the engine and confirmation policy.`,
489
+ )
490
+ refusal.engine = engine
491
+ if (new.target !== undefined) throw refusal
492
+ return Promise.reject(refusal)
493
+ }
494
+ Object.defineProperty(blocked, "name", { value: name })
495
+ return blocked
496
+ }
497
+
498
+ function allowlistedLibrary(raw, engine, guarded) {
499
+ const library = {}
500
+ for (const [name, value] of Object.entries(raw)) {
501
+ if (typeof value !== "function") library[name] = value
502
+ else if (PASSTHROUGH_FUNCTIONS.has(name)) library[name] = value
503
+ else library[name] = blockedExport(name, engine)
504
+ }
505
+ return Object.assign(library, guarded)
506
+ }
@@ -1,6 +1,18 @@
1
1
  import { existsSync } from "node:fs"
2
2
  import { dirname, join } from "node:path"
3
3
  import { fileURLToPath, pathToFileURL } from "node:url"
4
+ import { guardOmowright } from "./browser-engine-guard.mjs"
5
+
6
+ export {
7
+ BROWSER_CONFIRMATION_POLICY,
8
+ BROWSER_ENGINE_ENV,
9
+ BROWSER_STATE_EVENT,
10
+ BrowserActionDeclinedError,
11
+ BrowserEngineRefusal,
12
+ BrowserNotConnectedError,
13
+ BrowserUserStoppedError,
14
+ guardOmowright,
15
+ } from "./browser-engine-guard.mjs"
4
16
 
5
17
  const scriptDir = dirname(fileURLToPath(import.meta.url))
6
18
  const skillRoot = dirname(scriptDir)
@@ -14,12 +26,27 @@ export function resolveOmowrightEntry(env = process.env) {
14
26
  return candidates.find((candidate) => existsSync(candidate))
15
27
  }
16
28
 
17
- export async function loadOmowright(env = process.env) {
29
+ function kernelHost() {
30
+ const { tool, tool_schema: toolSchema } = globalThis
31
+ if (typeof toolSchema !== "function" || tool === undefined || tool === null) return undefined
32
+ return {
33
+ async listTools() {
34
+ const listed = await toolSchema()
35
+ return Array.isArray(listed?.tools) ? listed.tools : []
36
+ },
37
+ async callTool(name, args) {
38
+ return await tool[name](args)
39
+ },
40
+ }
41
+ }
42
+
43
+ export async function loadOmowright(env = process.env, host = kernelHost()) {
18
44
  const entry = resolveOmowrightEntry(env)
19
45
  if (entry === undefined) {
20
46
  throw new Error(
21
47
  "omowright is not staged in this skill; run `node packages/shared-skills/stage-omowright-runtime.mjs` in a checkout, or set OMOWRIGHT_ROOT to a directory holding its bundled index.js",
22
48
  )
23
49
  }
24
- return { entry, omowright: await import(pathToFileURL(entry).href) }
50
+ const raw = await import(pathToFileURL(entry).href)
51
+ return { entry, omowright: guardOmowright(raw, { env, host }) }
25
52
  }
package/dist/tui.js CHANGED
@@ -5296,7 +5296,7 @@ You are working on tasks that don't fit specific categories but require moderate
5296
5296
  </Category_Context>`, UNSPECIFIED_LOW_CATEGORY_CALLER_GUIDANCE = `<Selection_Gate>Use only when no specialist category fits, effort is moderate, and scope stays within a few files/modules. Prefer any matching specialist category.</Selection_Gate>
5297
5297
  <Caller_Warning>Provide explicit must-do steps, forbidden scope, and concrete success criteria.</Caller_Warning>`, UNSPECIFIED_HIGH_CATEGORY_PROMPT_APPEND = `<Category_Context>
5298
5298
  You are working on tasks that don't fit specific categories but require substantial effort.
5299
- </Category_Context>`, UNSPECIFIED_HIGH_CATEGORY_CALLER_GUIDANCE = `<Selection_Gate>Use only when no specialist category fits and substantial effort spans systems/modules with broad impact. Use unspecified-low for contained moderate work.</Selection_Gate>`, DEEP_LOW_CATEGORY_CALLER_GUIDANCE = `<Selection_Gate>Route here when one subsystem plus its callers holds the mechanism and the evidence, once read, leaves one right answer. Wide but mechanical work belongs here or in a quick batch. When unsure, choose deep-low: a misrouted child returns \`ESCALATE: deep-high\` after one cheap attempt; re-spawn the same brief as deep-high with its findings.</Selection_Gate>`, DEEP_HIGH_CATEGORY_CALLER_GUIDANCE = `<Selection_Gate>Route here only when you can name the decision evidence cannot settle: a trade-off with no single right answer, a contract change crossing a package or process boundary, a mechanism with no in-repo pattern to copy, or correctness argued from invariants rather than observed in a test. Wide scope with easy decisions is deep-low or unspecified-high; reasoning as the deliverable is ultrabrain.</Selection_Gate>`, DEEP_LOW_GATE_MODELS, DEEP_HIGH_GATE_MODEL = "gpt-6-astra", OPENAI_CATEGORIES;
5299
+ </Category_Context>`, UNSPECIFIED_HIGH_CATEGORY_CALLER_GUIDANCE = `<Selection_Gate>Use only when no specialist category fits and substantial effort spans systems/modules with broad impact. Use unspecified-low for contained moderate work.</Selection_Gate>`, DEEP_LOW_CATEGORY_CALLER_GUIDANCE = `<Selection_Gate>Of the two deep lanes this is the default: the child settles its decisions from what it reads. When unsure, choose deep-low; a misrouted child returns \`ESCALATE: deep-high\` after one cheap attempt, and you re-spawn the same brief as deep-high with its findings.</Selection_Gate>`, DEEP_HIGH_CATEGORY_CALLER_GUIDANCE = `<Selection_Gate>Route here only when you can name the decision evidence cannot settle: a trade-off with no single right answer, a contract change crossing a package or process boundary, a mechanism with no in-repo pattern to copy, or correctness argued from invariants rather than observed in a test. Wide scope with easy decisions is deep-low or unspecified-high; reasoning as the deliverable is ultrabrain.</Selection_Gate>`, DEEP_LOW_GATE_MODELS, DEEP_HIGH_GATE_MODEL = "gpt-6-astra", OPENAI_CATEGORIES;
5300
5300
  var init_openai_categories = __esm(() => {
5301
5301
  init_types();
5302
5302
  DEEP_LOW_GATE_MODELS = ["gpt-6.1-sol", "gpt-6.1-sol-fast", "gpt-5.6-sol-fast", "gpt-5.6-sol"];
@@ -5311,7 +5311,7 @@ var init_openai_categories = __esm(() => {
5311
5311
  {
5312
5312
  name: "deep-low",
5313
5313
  config: { model: "openai/gpt-6.1-sol", variant: "medium" },
5314
- description: "Default deep lane: one goal, one deliverable, decisions the child can settle from what it reads. **3D graphics, computer/browser use, CAPTCHA, multimodal, backend, logic, and algorithm work is routed here.** Multiple goals fan out as parallel calls.",
5314
+ description: "Default deep lane: one goal, one deliverable, decisions the child can settle from what it reads; preferred over deep-high for 3D graphics, computer/browser use, CAPTCHA, multimodal, backend, logic, and algorithm work. Multiple goals fan out as parallel calls.",
5315
5315
  callerGuidance: DEEP_LOW_CATEGORY_CALLER_GUIDANCE,
5316
5316
  promptAppend: DEEP_LOW_CATEGORY_PROMPT_APPEND,
5317
5317
  resolvePromptAppend: resolveDeepLowCategoryPromptAppend,
@@ -74609,7 +74609,7 @@ init_shared();
74609
74609
  // package.json
74610
74610
  var package_default = {
74611
74611
  name: "oh-my-opencode",
74612
- version: "5.1.13",
74612
+ version: "5.1.15",
74613
74613
  description: "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
74614
74614
  main: "./dist/index.js",
74615
74615
  types: "dist/index.d.ts",
@@ -74786,7 +74786,7 @@ var package_default = {
74786
74786
  },
74787
74787
  devDependencies: {
74788
74788
  "@clack/prompts": "^1.8.1",
74789
- "@code-yeongyu/senpi": "2026.10.3",
74789
+ "@code-yeongyu/senpi": "2026.10.6",
74790
74790
  "@modelcontextprotocol/sdk": "^1.30.0",
74791
74791
  "@oh-my-opencode/agents-md-core": "workspace:*",
74792
74792
  "@oh-my-opencode/ast-grep-mcp": "workspace:*",
@@ -74844,18 +74844,18 @@ var package_default = {
74844
74844
  typescript: "^7.0.2"
74845
74845
  },
74846
74846
  optionalDependencies: {
74847
- "oh-my-opencode-darwin-arm64": "5.1.13",
74848
- "oh-my-opencode-darwin-x64": "5.1.13",
74849
- "oh-my-opencode-darwin-x64-baseline": "5.1.13",
74850
- "oh-my-opencode-linux-arm64": "5.1.13",
74851
- "oh-my-opencode-linux-arm64-musl": "5.1.13",
74852
- "oh-my-opencode-linux-x64": "5.1.13",
74853
- "oh-my-opencode-linux-x64-baseline": "5.1.13",
74854
- "oh-my-opencode-linux-x64-musl": "5.1.13",
74855
- "oh-my-opencode-linux-x64-musl-baseline": "5.1.13",
74856
- "oh-my-opencode-windows-arm64": "5.1.13",
74857
- "oh-my-opencode-windows-x64": "5.1.13",
74858
- "oh-my-opencode-windows-x64-baseline": "5.1.13"
74847
+ "oh-my-opencode-darwin-arm64": "5.1.15",
74848
+ "oh-my-opencode-darwin-x64": "5.1.15",
74849
+ "oh-my-opencode-darwin-x64-baseline": "5.1.15",
74850
+ "oh-my-opencode-linux-arm64": "5.1.15",
74851
+ "oh-my-opencode-linux-arm64-musl": "5.1.15",
74852
+ "oh-my-opencode-linux-x64": "5.1.15",
74853
+ "oh-my-opencode-linux-x64-baseline": "5.1.15",
74854
+ "oh-my-opencode-linux-x64-musl": "5.1.15",
74855
+ "oh-my-opencode-linux-x64-musl-baseline": "5.1.15",
74856
+ "oh-my-opencode-windows-arm64": "5.1.15",
74857
+ "oh-my-opencode-windows-x64": "5.1.15",
74858
+ "oh-my-opencode-windows-x64-baseline": "5.1.15"
74859
74859
  },
74860
74860
  overrides: {
74861
74861
  hono: "^4.13.8",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "5.1.13",
3
+ "version": "5.1.15",
4
4
  "description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
5
5
  "main": "./dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -177,7 +177,7 @@
177
177
  },
178
178
  "devDependencies": {
179
179
  "@clack/prompts": "^1.8.1",
180
- "@code-yeongyu/senpi": "2026.10.3",
180
+ "@code-yeongyu/senpi": "2026.10.6",
181
181
  "@modelcontextprotocol/sdk": "^1.30.0",
182
182
  "@oh-my-opencode/agents-md-core": "workspace:*",
183
183
  "@oh-my-opencode/ast-grep-mcp": "workspace:*",
@@ -235,18 +235,18 @@
235
235
  "typescript": "^7.0.2"
236
236
  },
237
237
  "optionalDependencies": {
238
- "oh-my-opencode-darwin-arm64": "5.1.13",
239
- "oh-my-opencode-darwin-x64": "5.1.13",
240
- "oh-my-opencode-darwin-x64-baseline": "5.1.13",
241
- "oh-my-opencode-linux-arm64": "5.1.13",
242
- "oh-my-opencode-linux-arm64-musl": "5.1.13",
243
- "oh-my-opencode-linux-x64": "5.1.13",
244
- "oh-my-opencode-linux-x64-baseline": "5.1.13",
245
- "oh-my-opencode-linux-x64-musl": "5.1.13",
246
- "oh-my-opencode-linux-x64-musl-baseline": "5.1.13",
247
- "oh-my-opencode-windows-arm64": "5.1.13",
248
- "oh-my-opencode-windows-x64": "5.1.13",
249
- "oh-my-opencode-windows-x64-baseline": "5.1.13"
238
+ "oh-my-opencode-darwin-arm64": "5.1.15",
239
+ "oh-my-opencode-darwin-x64": "5.1.15",
240
+ "oh-my-opencode-darwin-x64-baseline": "5.1.15",
241
+ "oh-my-opencode-linux-arm64": "5.1.15",
242
+ "oh-my-opencode-linux-arm64-musl": "5.1.15",
243
+ "oh-my-opencode-linux-x64": "5.1.15",
244
+ "oh-my-opencode-linux-x64-baseline": "5.1.15",
245
+ "oh-my-opencode-linux-x64-musl": "5.1.15",
246
+ "oh-my-opencode-linux-x64-musl-baseline": "5.1.15",
247
+ "oh-my-opencode-windows-arm64": "5.1.15",
248
+ "oh-my-opencode-windows-x64": "5.1.15",
249
+ "oh-my-opencode-windows-x64-baseline": "5.1.15"
250
250
  },
251
251
  "overrides": {
252
252
  "hono": "^4.13.8",