@oharness/engine 0.10.0 → 0.11.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 (94) hide show
  1. package/dist/acp/claude.d.ts +34 -0
  2. package/dist/acp/claude.js +122 -0
  3. package/dist/acp/claude.js.map +1 -0
  4. package/dist/acp/client.d.ts +193 -0
  5. package/dist/acp/client.js +279 -0
  6. package/dist/acp/client.js.map +1 -0
  7. package/dist/acp/runner.d.ts +80 -0
  8. package/dist/acp/runner.js +159 -0
  9. package/dist/acp/runner.js.map +1 -0
  10. package/dist/agent/agent.d.ts +51 -1
  11. package/dist/agent/agent.js +322 -1
  12. package/dist/agent/agent.js.map +1 -1
  13. package/dist/agent/hooks.js +6 -6
  14. package/dist/agent/hooks.js.map +1 -1
  15. package/dist/agent/session.d.ts +2 -0
  16. package/dist/agent/session.js +19 -2
  17. package/dist/agent/session.js.map +1 -1
  18. package/dist/agent/subagent.d.ts +9 -0
  19. package/dist/agent/subagent.js +6 -0
  20. package/dist/agent/subagent.js.map +1 -1
  21. package/dist/auth/index.d.ts +3 -1
  22. package/dist/auth/index.js +3 -1
  23. package/dist/auth/index.js.map +1 -1
  24. package/dist/auth/manager.js +4 -0
  25. package/dist/auth/manager.js.map +1 -1
  26. package/dist/auth/oauth.d.ts +33 -0
  27. package/dist/auth/oauth.js +59 -8
  28. package/dist/auth/oauth.js.map +1 -1
  29. package/dist/auth/providers/anthropic.d.ts +25 -5
  30. package/dist/auth/providers/anthropic.js +47 -6
  31. package/dist/auth/providers/anthropic.js.map +1 -1
  32. package/dist/auth/providers/chatgpt.d.ts +8 -0
  33. package/dist/auth/providers/chatgpt.js +180 -0
  34. package/dist/auth/providers/chatgpt.js.map +1 -0
  35. package/dist/auth/providers/openrouter.d.ts +2 -0
  36. package/dist/auth/providers/openrouter.js +101 -0
  37. package/dist/auth/providers/openrouter.js.map +1 -0
  38. package/dist/auth/types.d.ts +15 -0
  39. package/dist/auth/types.js.map +1 -1
  40. package/dist/browser/index.d.ts +1 -1
  41. package/dist/browser/plugin.d.ts +6 -1
  42. package/dist/browser/plugin.js.map +1 -1
  43. package/dist/browser/session.d.ts +47 -0
  44. package/dist/browser/session.js +177 -2
  45. package/dist/browser/session.js.map +1 -1
  46. package/dist/browser/tools.js +139 -18
  47. package/dist/browser/tools.js.map +1 -1
  48. package/dist/computer/cua-driver.d.ts +60 -1
  49. package/dist/computer/cua-driver.js +149 -5
  50. package/dist/computer/cua-driver.js.map +1 -1
  51. package/dist/computer/helper-client.d.ts +3 -1
  52. package/dist/computer/helper-client.js +3 -0
  53. package/dist/computer/helper-client.js.map +1 -1
  54. package/dist/computer/index.d.ts +3 -2
  55. package/dist/computer/index.js +1 -1
  56. package/dist/computer/index.js.map +1 -1
  57. package/dist/computer/mode.d.ts +15 -0
  58. package/dist/computer/mode.js +11 -1
  59. package/dist/computer/mode.js.map +1 -1
  60. package/dist/computer/session.d.ts +36 -7
  61. package/dist/computer/session.js +118 -17
  62. package/dist/computer/session.js.map +1 -1
  63. package/dist/computer/target.d.ts +19 -0
  64. package/dist/computer/target.js.map +1 -1
  65. package/dist/computer/tools.js +135 -8
  66. package/dist/computer/tools.js.map +1 -1
  67. package/dist/config.d.ts +7 -0
  68. package/dist/config.js.map +1 -1
  69. package/dist/harness.d.ts +52 -0
  70. package/dist/harness.js +113 -12
  71. package/dist/harness.js.map +1 -1
  72. package/dist/mcp/manager.d.ts +15 -0
  73. package/dist/mcp/manager.js +28 -1
  74. package/dist/mcp/manager.js.map +1 -1
  75. package/dist/providers/catalog.json +14 -3
  76. package/dist/providers/models-dev.d.ts +15 -0
  77. package/dist/providers/models-dev.js +14 -0
  78. package/dist/providers/models-dev.js.map +1 -1
  79. package/dist/providers/piai.d.ts +38 -1
  80. package/dist/providers/piai.js +70 -2
  81. package/dist/providers/piai.js.map +1 -1
  82. package/dist/providers/registry.d.ts +16 -1
  83. package/dist/providers/registry.js +42 -11
  84. package/dist/providers/registry.js.map +1 -1
  85. package/dist/runtime-plugins/builtin/studio.d.ts +7 -0
  86. package/dist/runtime-plugins/builtin/studio.js +69 -4
  87. package/dist/runtime-plugins/builtin/studio.js.map +1 -1
  88. package/dist/tools/builtin/bash.js +6 -10
  89. package/dist/tools/builtin/bash.js.map +1 -1
  90. package/dist/util/shell.d.ts +30 -0
  91. package/dist/util/shell.js +70 -0
  92. package/dist/util/shell.js.map +1 -1
  93. package/package.json +3 -3
  94. package/skills/media-generation/SKILL.md +22 -1
@@ -0,0 +1,2 @@
1
+ import type { AuthProvider } from "../types.ts";
2
+ export declare const openrouterAuth: AuthProvider;
@@ -0,0 +1,101 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createPkce, listenForCallback } from "../oauth.js";
3
+ import { parseJson } from "../../util/json.js";
4
+ /**
5
+ * OpenRouter, with a sign-in that ends in an ordinary key.
6
+ *
7
+ * OpenRouter's OAuth is a public PKCE flow built for apps like this one: no
8
+ * client id to register, and what the code is exchanged for is not an
9
+ * expiring token but a permanent API key the user owns, can see and can
10
+ * revoke on openrouter.ai. So the sign-in is only a way of not copying a key
11
+ * by hand — the result is stored and sent exactly as a pasted key would be,
12
+ * and spends the user's own OpenRouter credits.
13
+ */
14
+ const AUTHORIZE_URL = "https://openrouter.ai/auth";
15
+ const KEY_URL = "https://openrouter.ai/api/v1/auth/keys";
16
+ const ENV_VAR = "OPENROUTER_API_KEY";
17
+ async function exchangeForKey(code, verifier) {
18
+ const response = await fetch(KEY_URL, {
19
+ method: "POST",
20
+ headers: { accept: "application/json", "content-type": "application/json" },
21
+ body: JSON.stringify({ code, code_verifier: verifier, code_challenge_method: "S256" }),
22
+ });
23
+ const text = await response.text();
24
+ const parsed = parseJson(text);
25
+ if (!response.ok) {
26
+ const error = parsed.ok ? parsed.value.error : undefined;
27
+ const detail = typeof error === "string" ? error : (error?.message ?? text.slice(0, 200));
28
+ throw new Error(`OpenRouter refused the sign-in (${response.status}): ${detail}`);
29
+ }
30
+ const key = parsed.ok ? parsed.value.key : undefined;
31
+ if (typeof key !== "string" || !key)
32
+ throw new Error("OpenRouter returned no key");
33
+ return key;
34
+ }
35
+ async function signIn() {
36
+ const pkce = createPkce();
37
+ // A path nobody can guess stands in for the state OpenRouter does not echo
38
+ // back; the PKCE verifier is what actually ties the code to this login.
39
+ const path = `/callback/${randomUUID()}`;
40
+ const { server, port, waitForCallback, deliver } = await listenForCallback({ path, state: null });
41
+ const url = new URL(AUTHORIZE_URL);
42
+ url.search = new URLSearchParams({
43
+ callback_url: `http://127.0.0.1:${port}${path}`,
44
+ code_challenge: pkce.challenge,
45
+ code_challenge_method: "S256",
46
+ }).toString();
47
+ return {
48
+ method: "auto",
49
+ url: url.toString(),
50
+ instructions: "Sign in to OpenRouter in your browser; this continues on its own.",
51
+ paste: deliver,
52
+ async callback() {
53
+ try {
54
+ const params = await waitForCallback;
55
+ const key = await exchangeForKey(params.get("code"), pkce.verifier);
56
+ return { type: "success", key, metadata: { source: "oauth" } };
57
+ }
58
+ catch (err) {
59
+ return { type: "failed", error: err instanceof Error ? err.message : String(err) };
60
+ }
61
+ finally {
62
+ server.close();
63
+ server.closeAllConnections();
64
+ }
65
+ },
66
+ };
67
+ }
68
+ function bearer(key, source) {
69
+ return { headers: { authorization: `Bearer ${key}` }, source };
70
+ }
71
+ export const openrouterAuth = {
72
+ id: "openrouter",
73
+ label: "OpenRouter",
74
+ methods: [
75
+ {
76
+ type: "oauth",
77
+ label: "Sign in with OpenRouter",
78
+ browser: true,
79
+ authorize: signIn,
80
+ },
81
+ {
82
+ type: "api",
83
+ label: "API key",
84
+ prompts: [{ type: "text", key: "key", message: "OpenRouter API key", placeholder: "sk-or-…", secret: true }],
85
+ },
86
+ ],
87
+ async loader(credential) {
88
+ const envKey = process.env[ENV_VAR];
89
+ if (envKey)
90
+ return bearer(envKey, `env:${ENV_VAR}`);
91
+ if (credential?.type === "api") {
92
+ return bearer(credential.key, credential.metadata?.["source"] === "oauth" ? "stored:oauth" : "stored:api");
93
+ }
94
+ return undefined;
95
+ },
96
+ async discover() {
97
+ const envKey = process.env[ENV_VAR];
98
+ return envKey ? { credential: { type: "api", key: envKey }, source: `env:${ENV_VAR}` } : undefined;
99
+ },
100
+ };
101
+ //# sourceMappingURL=openrouter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openrouter.js","sourceRoot":"","sources":["../../../src/auth/providers/openrouter.ts"],"names":[],"mappings":"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"}
@@ -79,6 +79,14 @@ export type AuthorizeResult = {
79
79
  */
80
80
  method: "auto";
81
81
  callback(): Promise<CallbackResult>;
82
+ /**
83
+ * Finish the flow from the address the browser ended on, pasted back
84
+ * by hand — for when the browser runs on another machine (an SSH
85
+ * session) and its redirect to this one's loopback never arrives.
86
+ * Returns why the paste was not accepted, or nothing when it was, in
87
+ * which case `callback()` resolves as if the redirect had come in.
88
+ */
89
+ paste?(input: string): string | undefined;
82
90
  } | {
83
91
  /** The user copies a code out of the browser and pastes it back. */
84
92
  method: "code";
@@ -95,6 +103,13 @@ export type CallbackResult = ({
95
103
  export interface OAuthMethod {
96
104
  type: "oauth";
97
105
  label: string;
106
+ /**
107
+ * A vendor's own browser sign-in that asks nothing up front and finishes on
108
+ * a loopback redirect — Sign in with ChatGPT, OpenRouter. Marked so a UI
109
+ * can tell it from the OHarness account sign-ins, which ask for an email
110
+ * and go through the website, and offer it as the one button it is.
111
+ */
112
+ browser?: boolean;
98
113
  prompts?: Prompt[];
99
114
  authorize(inputs: Record<string, string>): Promise<AuthorizeResult>;
100
115
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAoLH,MAAM,UAAU,SAAS,CAAC,UAA2B,EAAE,MAAM,GAAG,MAAM;IACpE,OAAO,UAAU,CAAC,OAAO,GAAG,CAAC,IAAI,UAAU,CAAC,OAAO,GAAG,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;AAC7E,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAmMH,MAAM,UAAU,SAAS,CAAC,UAA2B,EAAE,MAAM,GAAG,MAAM;IACpE,OAAO,UAAU,CAAC,OAAO,GAAG,CAAC,IAAI,UAAU,CAAC,OAAO,GAAG,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;AAC7E,CAAC"}
@@ -1,5 +1,5 @@
1
1
  export { BrowserSession, originOf } from "./session.ts";
2
- export type { BrowserSessionOptions, ConsoleEntry, NetworkEntry } from "./session.ts";
2
+ export type { BrowserSessionOptions, BrowserTurnShot, ConsoleEntry, NetworkEntry } from "./session.ts";
3
3
  export { createBrowserTools } from "./tools.ts";
4
4
  export { findChrome, launchChrome } from "./chrome.ts";
5
5
  export type { LaunchChromeOptions } from "./chrome.ts";
@@ -1,6 +1,6 @@
1
1
  import type { Logger } from "../util/logger.ts";
2
2
  import type { Tool } from "../tools/types.ts";
3
- import type { BrowserSessionOptions } from "./session.ts";
3
+ import type { BrowserSessionOptions, BrowserTurnShot } from "./session.ts";
4
4
  import type { BrowserProviderPlugin } from "./provider.ts";
5
5
  import type { ExternalSite, ImportedSite } from "./login-import.ts";
6
6
  import type { BrowserInputEvent, FrameOptions } from "./target.ts";
@@ -57,6 +57,11 @@ export interface BrowserUseSession {
57
57
  running: boolean;
58
58
  }>;
59
59
  close(): Promise<void>;
60
+ /**
61
+ * The turn ended: pictures of the tabs it left that the agent did not look
62
+ * at, or none when `capture` is false. Optional for third-party feature plugins.
63
+ */
64
+ endTurn?(capture: boolean): Promise<BrowserTurnShot[]>;
60
65
  /** Name the conversation this browser works for; see BrowserTargetContext.label. */
61
66
  setLabel?(label: () => string | undefined): void;
62
67
  /** Move to the other browser; the next call opens it. Optional for third-party feature plugins. */
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/browser/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AA2EhD,MAAM,UAAU,sBAAsB,CAAC,OAAuC,EAAE;IAC9E,OAAO;QACL,EAAE,EAAE,aAAa;QACjB,aAAa,CAAC,OAAO;YACnB,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC;gBACjC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/C,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACxD,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrD,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClD,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC7C,CAAC,CAAC;YACH,IAAI,OAAO,CAAC,SAAS;gBAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YACvD,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,kBAAkB,CAAC,OAAyB,CAAC;KAClE,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/browser/plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAgFhD,MAAM,UAAU,sBAAsB,CAAC,OAAuC,EAAE;IAC9E,OAAO;QACL,EAAE,EAAE,aAAa;QACjB,aAAa,CAAC,OAAO;YACnB,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC;gBACjC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC/C,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACxD,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrD,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAClD,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAC7C,CAAC,CAAC;YACH,IAAI,OAAO,CAAC,SAAS;gBAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YACvD,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,kBAAkB,CAAC,OAAyB,CAAC;KAClE,CAAC;AACJ,CAAC"}
@@ -4,6 +4,13 @@ import { LoginImportMemory } from "./login-import.ts";
4
4
  import type { ExternalSite, ImportedSite } from "./login-import.ts";
5
5
  import type { Snapshot, SnapshotOptions } from "./snapshot.ts";
6
6
  import type { BrowserInputEvent, BrowserPage, BrowserTargetFactory, FrameOptions } from "./target.ts";
7
+ /** One tab as a turn left it, for the person reading the turn back. */
8
+ export interface BrowserTurnShot {
9
+ mimeType: string;
10
+ data: string;
11
+ title?: string;
12
+ url?: string;
13
+ }
7
14
  export interface ConsoleEntry {
8
15
  level: string;
9
16
  text: string;
@@ -80,6 +87,17 @@ export declare class BrowserSession {
80
87
  private importing;
81
88
  private probing?;
82
89
  private readonly suggestions;
90
+ /**
91
+ * Tabs the tools acted on this turn whose last state the agent has not
92
+ * looked at in a screenshot, oldest first. A tab whose last step was a
93
+ * screenshot is already pictured in the transcript.
94
+ */
95
+ private readonly unseen;
96
+ /**
97
+ * Tabs wearing the working glow this turn, with the browser they are in and
98
+ * the script that puts the glow back after a navigation.
99
+ */
100
+ private readonly glowing;
83
101
  constructor(opts?: BrowserSessionOptions);
84
102
  /** Name the conversation this browser works for. Read lazily, so it may change. */
85
103
  setLabel(label: () => string | undefined): void;
@@ -148,6 +166,13 @@ export declare class BrowserSession {
148
166
  x: number;
149
167
  y: number;
150
168
  }>;
169
+ /** The border box of a ref, in viewport CSS pixels, after scrolling it into view. */
170
+ boxOf(ref: string): Promise<{
171
+ x: number;
172
+ y: number;
173
+ width: number;
174
+ height: number;
175
+ }>;
151
176
  resolveObject(ref: string): Promise<string>;
152
177
  /** Every open tab. Starts the browser if nothing has yet. */
153
178
  tabs(): Promise<BrowserPage[]>;
@@ -241,6 +266,28 @@ export declare class BrowserSession {
241
266
  forward(event: BrowserInputEvent): Promise<void>;
242
267
  consoleEntries(): readonly ConsoleEntry[];
243
268
  networkEntries(): readonly NetworkEntry[];
269
+ /**
270
+ * Edge the tab in use with the working glow until the turn ends, so a person
271
+ * glancing at their browser can tell the agent is in it. Not the in-app
272
+ * browser: the pane around it already says so. Never seen by the model —
273
+ * screenshots are taken without it, and it is hidden from the
274
+ * accessibility tree.
275
+ */
276
+ glow(): Promise<void>;
277
+ /** Run `fn` with the glow out of the picture on `page`. */
278
+ withoutGlow<T>(page: BrowserPage, fn: () => Promise<T>): Promise<T>;
279
+ /** Take the glow off, from every tab or only those in one browser. */
280
+ private unglow;
281
+ /** A tool ran against the tab in use; `sawPage` when its last step was a screenshot. */
282
+ noteTool(sawPage: boolean): void;
283
+ /**
284
+ * The turn ended: a picture of each tab it left unseen, for the transcript.
285
+ *
286
+ * The agent works from the accessibility tree and rarely looks, so without
287
+ * this a browser turn is often read back with no picture of the page it
288
+ * left. `capture` false only forgets the turn's tabs.
289
+ */
290
+ endTurn(capture: boolean): Promise<BrowserTurnShot[]>;
244
291
  /** Let go of both browsers: the built-in one closes, the person's is detached. */
245
292
  close(): Promise<void>;
246
293
  /** Let go of one browser, leaving the other as it is. */
@@ -20,6 +20,14 @@ import { LoginImportMemory, externalBrowserName, hostInSite, listExternalSites,
20
20
  */
21
21
  const CONSOLE_LIMIT = 200;
22
22
  const NETWORK_LIMIT = 200;
23
+ /**
24
+ * At most this many tabs get a picture when a turn ends: a turn that went
25
+ * through ten pages is read back by its last few, and each is a capture.
26
+ * The page shows one row of them and opens the rest from it.
27
+ */
28
+ const TURN_SHOT_MAX_TABS = 6;
29
+ /** A tab that does not answer a capture in this long is left out, not waited on. */
30
+ const TURN_SHOT_TIMEOUT_MS = 5_000;
23
31
  function emptySide() {
24
32
  return { refs: new Map(), wired: new WeakSet() };
25
33
  }
@@ -65,6 +73,17 @@ export class BrowserSession {
65
73
  importing = false;
66
74
  probing;
67
75
  suggestions = new Map();
76
+ /**
77
+ * Tabs the tools acted on this turn whose last state the agent has not
78
+ * looked at in a screenshot, oldest first. A tab whose last step was a
79
+ * screenshot is already pictured in the transcript.
80
+ */
81
+ unseen = new Map();
82
+ /**
83
+ * Tabs wearing the working glow this turn, with the browser they are in and
84
+ * the script that puts the glow back after a navigation.
85
+ */
86
+ glowing = new Map();
68
87
  constructor(opts = {}) {
69
88
  this.opts = opts;
70
89
  this.policy = opts.mode === "extension" || opts.mode === "auto" ? opts.mode : "builtin";
@@ -375,6 +394,25 @@ export class BrowserSession {
375
394
  y: (quad[1] + quad[3] + quad[5] + quad[7]) / 4,
376
395
  };
377
396
  }
397
+ /** The border box of a ref, in viewport CSS pixels, after scrolling it into view. */
398
+ async boxOf(ref) {
399
+ const page = await this.page();
400
+ const backendNodeId = this.refs.get(ref);
401
+ if (backendNodeId === undefined) {
402
+ throw new BrowserError(`no such element ${ref}. Call browser_read_page to get current refs — they are replaced on every snapshot.`);
403
+ }
404
+ await page.send("DOM.scrollIntoViewIfNeeded", { backendNodeId }).catch(() => { });
405
+ const box = await page.send("DOM.getBoxModel", { backendNodeId }).catch(() => undefined);
406
+ const model = (box?.["model"] ?? {});
407
+ const quad = Array.isArray(model["border"]) ? model["border"] : Array.isArray(model["content"]) ? model["content"] : undefined;
408
+ if (!quad || quad.length < 8)
409
+ throw new BrowserError(`${ref} is not visible on the page`);
410
+ const xs = [quad[0], quad[2], quad[4], quad[6]];
411
+ const ys = [quad[1], quad[3], quad[5], quad[7]];
412
+ const x = Math.min(...xs);
413
+ const y = Math.min(...ys);
414
+ return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
415
+ }
378
416
  async resolveObject(ref) {
379
417
  const page = await this.page();
380
418
  const backendNodeId = this.refs.get(ref);
@@ -500,10 +538,14 @@ export class BrowserSession {
500
538
  throw new BrowserError(`no tab ${id}`);
501
539
  const wasActive = page === this.active;
502
540
  const released = Boolean(target.opened && target.release && !target.opened(id));
503
- if (released)
541
+ if (released) {
542
+ await this.unglow(undefined, page);
504
543
  await target.release(page);
505
- else
544
+ }
545
+ else {
546
+ this.glowing.delete(page);
506
547
  await page.close();
548
+ }
507
549
  if (wasActive)
508
550
  this.active = undefined;
509
551
  // What is left to work in: in the person's own browser only this
@@ -595,8 +637,81 @@ export class BrowserSession {
595
637
  networkEntries() {
596
638
  return this.network;
597
639
  }
640
+ /**
641
+ * Edge the tab in use with the working glow until the turn ends, so a person
642
+ * glancing at their browser can tell the agent is in it. Not the in-app
643
+ * browser: the pane around it already says so. Never seen by the model —
644
+ * screenshots are taken without it, and it is hidden from the
645
+ * accessibility tree.
646
+ */
647
+ async glow() {
648
+ const page = this.active;
649
+ if (!page || !this.target || this.target.kind === "electron" || this.glowing.has(page))
650
+ return;
651
+ const entry = { side: this.mode };
652
+ this.glowing.set(page, entry);
653
+ const added = await page.send("Page.addScriptToEvaluateOnNewDocument", { source: GLOW_ON }).catch(() => undefined);
654
+ if (typeof added?.["identifier"] === "string")
655
+ entry.script = added["identifier"];
656
+ await page.send("Runtime.evaluate", { expression: GLOW_ON }).catch(() => { });
657
+ }
658
+ /** Run `fn` with the glow out of the picture on `page`. */
659
+ async withoutGlow(page, fn) {
660
+ if (!this.glowing.has(page))
661
+ return fn();
662
+ await page.send("Runtime.evaluate", { expression: glowVisibility("hidden") }).catch(() => { });
663
+ try {
664
+ return await fn();
665
+ }
666
+ finally {
667
+ await page.send("Runtime.evaluate", { expression: glowVisibility("visible") }).catch(() => { });
668
+ }
669
+ }
670
+ /** Take the glow off, from every tab or only those in one browser. */
671
+ async unglow(side, only) {
672
+ const pages = [...this.glowing].filter(([page, entry]) => (!side || entry.side === side) && (!only || page === only));
673
+ await Promise.all(pages.map(async ([page, entry]) => {
674
+ this.glowing.delete(page);
675
+ if (entry.script)
676
+ await page.send("Page.removeScriptToEvaluateOnNewDocument", { identifier: entry.script }).catch(() => { });
677
+ await page.send("Runtime.evaluate", { expression: GLOW_OFF }).catch(() => { });
678
+ }));
679
+ }
680
+ /** A tool ran against the tab in use; `sawPage` when its last step was a screenshot. */
681
+ noteTool(sawPage) {
682
+ const page = this.active;
683
+ if (!page)
684
+ return;
685
+ this.unseen.delete(page.id);
686
+ if (!sawPage)
687
+ this.unseen.set(page.id, page);
688
+ }
689
+ /**
690
+ * The turn ended: a picture of each tab it left unseen, for the transcript.
691
+ *
692
+ * The agent works from the accessibility tree and rarely looks, so without
693
+ * this a browser turn is often read back with no picture of the page it
694
+ * left. `capture` false only forgets the turn's tabs.
695
+ */
696
+ async endTurn(capture) {
697
+ const pages = [...this.unseen.values()].slice(-TURN_SHOT_MAX_TABS);
698
+ this.unseen.clear();
699
+ // Off before the pictures: the transcript shows the page, not the glow.
700
+ await this.unglow();
701
+ if (!capture)
702
+ return [];
703
+ const shots = [];
704
+ for (const page of pages) {
705
+ const shot = await withTimeout(capturePage(page), TURN_SHOT_TIMEOUT_MS).catch(() => undefined);
706
+ if (shot)
707
+ shots.push(shot);
708
+ }
709
+ return shots;
710
+ }
598
711
  /** Let go of both browsers: the built-in one closes, the person's is detached. */
599
712
  async close() {
713
+ this.unseen.clear();
714
+ await this.unglow();
600
715
  this.consoleLog.length = 0;
601
716
  this.network.length = 0;
602
717
  await Promise.all([this.closeSide("builtin"), this.closeSide("extension")]);
@@ -605,6 +720,8 @@ export class BrowserSession {
605
720
  async closeSide(side) {
606
721
  const target = this.sides[side].target;
607
722
  this.sides[side] = emptySide();
723
+ // The person's tabs outlive the connection; the glow must not.
724
+ await this.unglow(side);
608
725
  await target?.close().catch(() => { });
609
726
  }
610
727
  /**
@@ -834,4 +951,62 @@ export function originOf(url) {
834
951
  function errorText(err) {
835
952
  return err instanceof Error ? err.message : String(err);
836
953
  }
954
+ /**
955
+ * The working glow, put into a page from outside it.
956
+ *
957
+ * Built for pages that are not ours: a closed shadow root so the page's CSS
958
+ * cannot reach it, styles through the CSSOM and constructed sheets so a
959
+ * Content-Security-Policy or Trusted Types does not refuse them, top frame
960
+ * only, and nothing a click or a screen reader can land on.
961
+ */
962
+ const GLOW_TAG = "oharness-glow";
963
+ const GLOW_CSS = ".g,.g::after{position:fixed;inset:0;pointer-events:none}" +
964
+ ".g{box-shadow:inset 0 0 0 2px rgba(198,244,50,.9),inset 0 0 16px 2px rgba(198,244,50,.5);animation:in .5s ease}" +
965
+ ".g::after{content:'';box-shadow:inset 0 0 60px 12px rgba(198,244,50,.3),inset 0 0 140px 24px rgba(107,92,255,.12);animation:breathe 2.6s ease-in-out infinite}" +
966
+ "@keyframes in{from{opacity:0}}@keyframes breathe{0%,100%{opacity:.55}50%{opacity:1}}" +
967
+ "@media (prefers-reduced-motion:reduce){.g,.g::after{animation:none}}";
968
+ const GLOW_ON = `(() => {
969
+ if (window.top !== window) return;
970
+ const put = () => {
971
+ const root = document.documentElement;
972
+ if (!root || root.querySelector(":scope > ${GLOW_TAG}")) return;
973
+ const host = document.createElement("${GLOW_TAG}");
974
+ host.setAttribute("aria-hidden", "true");
975
+ for (const [key, value] of [["all", "initial"], ["display", "block"], ["position", "fixed"], ["inset", "0"], ["pointer-events", "none"], ["z-index", "2147483647"]]) host.style.setProperty(key, value, "important");
976
+ const shadow = host.attachShadow({ mode: "closed" });
977
+ const sheet = new CSSStyleSheet();
978
+ sheet.replaceSync(${JSON.stringify(GLOW_CSS)});
979
+ shadow.adoptedStyleSheets = [sheet];
980
+ const ring = document.createElement("div");
981
+ ring.className = "g";
982
+ shadow.appendChild(ring);
983
+ root.appendChild(host);
984
+ };
985
+ if (document.documentElement) put(); else document.addEventListener("readystatechange", put, { once: true });
986
+ })()`;
987
+ const GLOW_OFF = `document.querySelector(${JSON.stringify(GLOW_TAG)})?.remove()`;
988
+ function glowVisibility(value) {
989
+ return `document.querySelector(${JSON.stringify(GLOW_TAG)})?.style.setProperty("visibility", ${JSON.stringify(value)}, "important")`;
990
+ }
991
+ /** A tab as it is now, with what it is called. Undefined for a tab with nothing to show. */
992
+ async function capturePage(page) {
993
+ const result = await page.send("Page.captureScreenshot", { format: "jpeg", quality: 70 });
994
+ const data = result["data"];
995
+ if (typeof data !== "string" || !data)
996
+ return undefined;
997
+ const about = await page
998
+ .send("Runtime.evaluate", { expression: "({ title: document.title, url: location.href })", returnByValue: true })
999
+ .then((r) => (r["result"] ?? {})["value"])
1000
+ .catch(() => undefined);
1001
+ const title = typeof about?.title === "string" ? about.title.trim() : "";
1002
+ const url = typeof about?.url === "string" ? about.url : "";
1003
+ return { mimeType: "image/jpeg", data, ...(title ? { title } : {}), ...(url ? { url } : {}) };
1004
+ }
1005
+ function withTimeout(promise, ms) {
1006
+ let timer;
1007
+ return Promise.race([
1008
+ promise,
1009
+ new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("timed out")), ms); }),
1010
+ ]).finally(() => clearTimeout(timer));
1011
+ }
837
1012
  //# sourceMappingURL=session.js.map