@oharness/engine 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent.d.ts +1 -0
- package/dist/agent/agent.js +5 -0
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/permissions.js +8 -0
- package/dist/agent/permissions.js.map +1 -1
- package/dist/browser/cdp.d.ts +75 -0
- package/dist/browser/cdp.js +136 -0
- package/dist/browser/cdp.js.map +1 -0
- package/dist/browser/chrome.d.ts +19 -0
- package/dist/browser/chrome.js +306 -0
- package/dist/browser/chrome.js.map +1 -0
- package/dist/browser/extension.d.ts +35 -0
- package/dist/browser/extension.js +297 -0
- package/dist/browser/extension.js.map +1 -0
- package/dist/browser/host-manifest.d.ts +78 -0
- package/dist/browser/host-manifest.js +166 -0
- package/dist/browser/host-manifest.js.map +1 -0
- package/dist/browser/host.d.ts +27 -0
- package/dist/browser/host.js +311 -0
- package/dist/browser/host.js.map +1 -0
- package/dist/browser/index.d.ts +26 -0
- package/dist/browser/index.js +15 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/login-import.d.ts +98 -0
- package/dist/browser/login-import.js +244 -0
- package/dist/browser/login-import.js.map +1 -0
- package/dist/browser/plugin.d.ts +109 -0
- package/dist/browser/plugin.js +22 -0
- package/dist/browser/plugin.js.map +1 -0
- package/dist/browser/presence.d.ts +29 -0
- package/dist/browser/presence.js +54 -0
- package/dist/browser/presence.js.map +1 -0
- package/dist/browser/provider.d.ts +17 -0
- package/dist/browser/provider.js +13 -0
- package/dist/browser/provider.js.map +1 -0
- package/dist/browser/screencast.d.ts +42 -0
- package/dist/browser/screencast.js +99 -0
- package/dist/browser/screencast.js.map +1 -0
- package/dist/browser/session.d.ts +320 -0
- package/dist/browser/session.js +837 -0
- package/dist/browser/session.js.map +1 -0
- package/dist/browser/snapshot.d.ts +49 -0
- package/dist/browser/snapshot.js +145 -0
- package/dist/browser/snapshot.js.map +1 -0
- package/dist/browser/socket.d.ts +15 -0
- package/dist/browser/socket.js +32 -0
- package/dist/browser/socket.js.map +1 -0
- package/dist/browser/target.d.ts +112 -0
- package/dist/browser/target.js +20 -0
- package/dist/browser/target.js.map +1 -0
- package/dist/browser/tools.d.ts +3 -0
- package/dist/browser/tools.js +537 -0
- package/dist/browser/tools.js.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js.map +1 -1
- package/dist/core/errors.d.ts +12 -0
- package/dist/core/errors.js +14 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/harness.d.ts +49 -0
- package/dist/harness.js +97 -1
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/piai.js +7 -2
- package/dist/providers/piai.js.map +1 -1
- package/dist/tools/define.d.ts +1 -0
- package/dist/tools/define.js +3 -0
- package/dist/tools/define.js.map +1 -1
- package/dist/tools/types.d.ts +11 -0
- package/dist/tools/types.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, mkdir } from "node:fs/promises";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { BrowserError } from "../core/errors.js";
|
|
5
|
+
import { userPath } from "../paths.js";
|
|
6
|
+
import { CdpConnection, NulFramer } from "./cdp.js";
|
|
7
|
+
import { ScreencastHub } from "./screencast.js";
|
|
8
|
+
/**
|
|
9
|
+
* A Chrome the harness starts and owns.
|
|
10
|
+
*
|
|
11
|
+
* Two decisions worth stating, because both look like details and neither is.
|
|
12
|
+
*
|
|
13
|
+
* It runs against its own `--user-data-dir`, never the person's. That keeps the
|
|
14
|
+
* agent out of their logged-in sessions, and it is also the only way this works
|
|
15
|
+
* at all: since Chrome 136 remote debugging is refused on the default profile,
|
|
16
|
+
* so a build that tried to drive the everyday browser would simply fail to
|
|
17
|
+
* attach. Reaching a logged-in session is a different feature with a different
|
|
18
|
+
* mechanism — an extension — and it is deliberately not this one.
|
|
19
|
+
*
|
|
20
|
+
* It talks over `--remote-debugging-pipe` rather than a debugging port. A port
|
|
21
|
+
* is a TCP listener on the loopback interface that any local process may
|
|
22
|
+
* connect to, and what it grants is complete control of a browser: reading
|
|
23
|
+
* every page, typing into it, and executing script in it. The pipe is two file
|
|
24
|
+
* descriptors held by this process alone.
|
|
25
|
+
*/
|
|
26
|
+
const DEFAULT_ARGS = [
|
|
27
|
+
"--remote-debugging-pipe",
|
|
28
|
+
"--no-first-run",
|
|
29
|
+
"--no-default-browser-check",
|
|
30
|
+
"--disable-background-networking",
|
|
31
|
+
"--disable-sync",
|
|
32
|
+
"--disable-features=Translate,OptimizationHints",
|
|
33
|
+
// The agent's browser is not where anybody wants to be asked about
|
|
34
|
+
// notifications, and a permission bubble is a modal the model cannot see.
|
|
35
|
+
"--deny-permission-prompts",
|
|
36
|
+
];
|
|
37
|
+
/** Where each platform keeps the browsers, most preferred first. */
|
|
38
|
+
const CANDIDATES = {
|
|
39
|
+
darwin: [
|
|
40
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
41
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
42
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
43
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
44
|
+
],
|
|
45
|
+
linux: [
|
|
46
|
+
"/usr/bin/google-chrome",
|
|
47
|
+
"/usr/bin/google-chrome-stable",
|
|
48
|
+
"/usr/bin/chromium",
|
|
49
|
+
"/usr/bin/chromium-browser",
|
|
50
|
+
"/usr/bin/microsoft-edge",
|
|
51
|
+
],
|
|
52
|
+
win32: [
|
|
53
|
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
54
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
55
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Find a Chromium to drive.
|
|
60
|
+
*
|
|
61
|
+
* `OHARNESS_BROWSER_PATH` wins, so a person on a distribution nobody listed —
|
|
62
|
+
* or one who wants a specific channel — is never stuck arguing with a hardcoded
|
|
63
|
+
* array.
|
|
64
|
+
*/
|
|
65
|
+
export async function findChrome(platform = process.platform) {
|
|
66
|
+
const override = process.env["OHARNESS_BROWSER_PATH"];
|
|
67
|
+
if (override)
|
|
68
|
+
return (await executable(override)) ? override : undefined;
|
|
69
|
+
for (const candidate of CANDIDATES[platform] ?? []) {
|
|
70
|
+
if (await executable(candidate))
|
|
71
|
+
return candidate;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
async function executable(file) {
|
|
76
|
+
try {
|
|
77
|
+
await access(file, constants.X_OK);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function launchChrome(opts = {}) {
|
|
85
|
+
const bin = opts.executablePath ?? (await findChrome());
|
|
86
|
+
if (!bin) {
|
|
87
|
+
throw new BrowserError("no Chrome, Chromium, Edge or Brave found. Install one, or set OHARNESS_BROWSER_PATH to its executable.");
|
|
88
|
+
}
|
|
89
|
+
const profile = opts.userDataDir ?? userPath("browser-profile");
|
|
90
|
+
await mkdir(profile, { recursive: true });
|
|
91
|
+
const args = [...DEFAULT_ARGS, `--user-data-dir=${profile}`];
|
|
92
|
+
// The new headless mode is a real Chrome with no window, not the old
|
|
93
|
+
// separate binary that behaved differently enough to make a page work in
|
|
94
|
+
// one and fail in the other.
|
|
95
|
+
if (opts.headless !== false)
|
|
96
|
+
args.push("--headless=new");
|
|
97
|
+
args.push("about:blank");
|
|
98
|
+
const child = spawn(bin, args, {
|
|
99
|
+
// fds 3 and 4 are the protocol: Chrome reads commands from 3 and writes
|
|
100
|
+
// events to 4. stdout and stderr stay separate so a crash message is not
|
|
101
|
+
// mistaken for a protocol frame.
|
|
102
|
+
stdio: ["ignore", "pipe", "pipe", "pipe", "pipe"],
|
|
103
|
+
});
|
|
104
|
+
const toBrowser = child.stdio[3];
|
|
105
|
+
const fromBrowser = child.stdio[4];
|
|
106
|
+
if (!toBrowser || !fromBrowser) {
|
|
107
|
+
child.kill();
|
|
108
|
+
throw new BrowserError("chrome started without a protocol pipe");
|
|
109
|
+
}
|
|
110
|
+
const connection = new CdpConnection((message) => {
|
|
111
|
+
toBrowser.write(JSON.stringify(message) + "\0");
|
|
112
|
+
});
|
|
113
|
+
const framer = new NulFramer();
|
|
114
|
+
fromBrowser.setEncoding("utf8");
|
|
115
|
+
fromBrowser.on("data", (chunk) => framer.push(chunk, (raw) => connection.receive(raw)));
|
|
116
|
+
let stderr = "";
|
|
117
|
+
child.stderr?.setEncoding("utf8");
|
|
118
|
+
child.stderr?.on("data", (chunk) => {
|
|
119
|
+
// Kept only to explain a failure to start; a running Chrome is chatty and
|
|
120
|
+
// none of it is interesting.
|
|
121
|
+
if (stderr.length < 4000)
|
|
122
|
+
stderr += chunk;
|
|
123
|
+
});
|
|
124
|
+
const exited = new Promise((_, reject) => {
|
|
125
|
+
child.once("error", (err) => {
|
|
126
|
+
connection.fail(err.message);
|
|
127
|
+
reject(new BrowserError(`could not start ${bin}`, err));
|
|
128
|
+
});
|
|
129
|
+
child.once("exit", (code) => {
|
|
130
|
+
const reason = `browser exited${code === null ? "" : ` with code ${code}`}`;
|
|
131
|
+
connection.fail(reason);
|
|
132
|
+
reject(new BrowserError(stderr.trim() ? `${reason}: ${stderr.trim().split("\n").at(-1)}` : reason));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
// Nothing awaits this until something has already gone wrong; without a
|
|
136
|
+
// sink an early exit would be an unhandled rejection.
|
|
137
|
+
exited.catch(() => { });
|
|
138
|
+
const target = new ChromeTarget(child, connection, opts.logger);
|
|
139
|
+
// Prove the protocol works before handing the target out, so a broken launch
|
|
140
|
+
// is one error here rather than a confusing failure inside the first tool.
|
|
141
|
+
await Promise.race([target.handshake(), exited]);
|
|
142
|
+
return target;
|
|
143
|
+
}
|
|
144
|
+
class ChromeTarget {
|
|
145
|
+
kind = "chrome";
|
|
146
|
+
attached = new Map();
|
|
147
|
+
closing;
|
|
148
|
+
child;
|
|
149
|
+
cdp;
|
|
150
|
+
logger;
|
|
151
|
+
constructor(child, cdp, logger) {
|
|
152
|
+
this.child = child;
|
|
153
|
+
this.cdp = cdp;
|
|
154
|
+
this.logger = logger;
|
|
155
|
+
}
|
|
156
|
+
async handshake() {
|
|
157
|
+
await this.cdp.send("Target.setDiscoverTargets", { discover: true });
|
|
158
|
+
}
|
|
159
|
+
async pages() {
|
|
160
|
+
const result = await this.cdp.send("Target.getTargets");
|
|
161
|
+
const infos = Array.isArray(result["targetInfos"]) ? result["targetInfos"] : [];
|
|
162
|
+
const pages = [];
|
|
163
|
+
for (const info of infos) {
|
|
164
|
+
const record = info;
|
|
165
|
+
if (record["type"] !== "page")
|
|
166
|
+
continue;
|
|
167
|
+
const targetId = String(record["targetId"]);
|
|
168
|
+
pages.push(this.attached.get(targetId) ?? (await this.attach(targetId)));
|
|
169
|
+
}
|
|
170
|
+
return pages;
|
|
171
|
+
}
|
|
172
|
+
async pageInfos() {
|
|
173
|
+
const result = await this.cdp.send("Target.getTargets");
|
|
174
|
+
const infos = Array.isArray(result["targetInfos"]) ? result["targetInfos"] : [];
|
|
175
|
+
return infos.flatMap((info) => {
|
|
176
|
+
const record = info;
|
|
177
|
+
if (record["type"] !== "page")
|
|
178
|
+
return [];
|
|
179
|
+
const title = typeof record["title"] === "string" ? record["title"].trim() : "";
|
|
180
|
+
return [{
|
|
181
|
+
id: String(record["targetId"]),
|
|
182
|
+
url: typeof record["url"] === "string" ? record["url"] : "",
|
|
183
|
+
...(title ? { title } : {}),
|
|
184
|
+
}];
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
async pageById(id) {
|
|
188
|
+
const info = (await this.pageInfos()).find((page) => page.id === id);
|
|
189
|
+
return info ? this.attach(id) : undefined;
|
|
190
|
+
}
|
|
191
|
+
async newPage(url = "about:blank") {
|
|
192
|
+
const result = await this.cdp.send("Target.createTarget", { url });
|
|
193
|
+
return this.attach(String(result["targetId"]));
|
|
194
|
+
}
|
|
195
|
+
async activate(page) {
|
|
196
|
+
await this.cdp.send("Target.activateTarget", { targetId: page.id });
|
|
197
|
+
}
|
|
198
|
+
async attach(targetId) {
|
|
199
|
+
const existing = this.attached.get(targetId);
|
|
200
|
+
if (existing)
|
|
201
|
+
return existing;
|
|
202
|
+
const result = await this.cdp.send("Target.attachToTarget", { targetId, flatten: true });
|
|
203
|
+
const sessionId = String(result["sessionId"]);
|
|
204
|
+
const page = new ChromePage(targetId, sessionId, this.cdp, () => {
|
|
205
|
+
this.attached.delete(targetId);
|
|
206
|
+
});
|
|
207
|
+
this.attached.set(targetId, page);
|
|
208
|
+
await page.prepare();
|
|
209
|
+
return page;
|
|
210
|
+
}
|
|
211
|
+
close() {
|
|
212
|
+
return (this.closing ??= this.shutdown());
|
|
213
|
+
}
|
|
214
|
+
async shutdown() {
|
|
215
|
+
// Ask first: a browser told to close flushes its profile, and a profile
|
|
216
|
+
// killed mid-write is one Chrome complains about on the next start.
|
|
217
|
+
try {
|
|
218
|
+
await Promise.race([
|
|
219
|
+
this.cdp.send("Browser.close"),
|
|
220
|
+
new Promise((resolve) => setTimeout(resolve, 2000)),
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
/* already gone */
|
|
225
|
+
}
|
|
226
|
+
this.cdp.fail("closed");
|
|
227
|
+
if (this.child.exitCode === null)
|
|
228
|
+
this.child.kill();
|
|
229
|
+
this.logger?.debug?.("browser closed");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
class ChromePage {
|
|
233
|
+
id;
|
|
234
|
+
sessionId;
|
|
235
|
+
cdp;
|
|
236
|
+
forget;
|
|
237
|
+
screencast;
|
|
238
|
+
constructor(id, sessionId, cdp, forget) {
|
|
239
|
+
this.id = id;
|
|
240
|
+
this.sessionId = sessionId;
|
|
241
|
+
this.cdp = cdp;
|
|
242
|
+
this.forget = forget;
|
|
243
|
+
this.screencast = new ScreencastHub(this);
|
|
244
|
+
}
|
|
245
|
+
/** The domains every tool assumes are on. */
|
|
246
|
+
async prepare() {
|
|
247
|
+
await Promise.all([
|
|
248
|
+
this.send("Page.enable"),
|
|
249
|
+
this.send("Runtime.enable"),
|
|
250
|
+
this.send("DOM.enable"),
|
|
251
|
+
this.send("Log.enable"),
|
|
252
|
+
]);
|
|
253
|
+
}
|
|
254
|
+
send(method, params = {}) {
|
|
255
|
+
return this.cdp.send(method, params, this.sessionId);
|
|
256
|
+
}
|
|
257
|
+
on(event, handler) {
|
|
258
|
+
return this.cdp.on(event, handler, this.sessionId);
|
|
259
|
+
}
|
|
260
|
+
/** Frames, shared with every other watcher of this page. */
|
|
261
|
+
frames(opts = {}) {
|
|
262
|
+
return this.screencast.frames(opts);
|
|
263
|
+
}
|
|
264
|
+
async input(event) {
|
|
265
|
+
if (event.type === "text") {
|
|
266
|
+
await this.send("Input.insertText", { text: event.text });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (event.type === "key") {
|
|
270
|
+
const method = event.action === "char" ? "char" : event.action === "down" ? "keyDown" : "keyUp";
|
|
271
|
+
await this.send("Input.dispatchKeyEvent", {
|
|
272
|
+
type: method,
|
|
273
|
+
...(event.key ? { key: event.key } : {}),
|
|
274
|
+
...(event.code ? { code: event.code } : {}),
|
|
275
|
+
...(event.text ? { text: event.text } : {}),
|
|
276
|
+
modifiers: event.modifiers ?? 0,
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (event.action === "wheel") {
|
|
281
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
282
|
+
type: "mouseWheel",
|
|
283
|
+
x: event.x,
|
|
284
|
+
y: event.y,
|
|
285
|
+
deltaX: event.deltaX ?? 0,
|
|
286
|
+
deltaY: event.deltaY ?? 0,
|
|
287
|
+
modifiers: event.modifiers ?? 0,
|
|
288
|
+
});
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
292
|
+
type: event.action === "move" ? "mouseMoved" : event.action === "down" ? "mousePressed" : "mouseReleased",
|
|
293
|
+
x: event.x,
|
|
294
|
+
y: event.y,
|
|
295
|
+
button: event.button ?? "left",
|
|
296
|
+
clickCount: event.clickCount ?? (event.action === "move" ? 0 : 1),
|
|
297
|
+
modifiers: event.modifiers ?? 0,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
async close() {
|
|
301
|
+
this.forget();
|
|
302
|
+
this.screencast.close();
|
|
303
|
+
await this.cdp.send("Target.closeTarget", { targetId: this.id }).catch(() => { });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
//# sourceMappingURL=chrome.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chrome.js","sourceRoot":"","sources":["../../src/browser/chrome.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { BrowserTarget, BrowserTargetContext } from "./target.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The user's own Chrome, reached through an extension.
|
|
4
|
+
*
|
|
5
|
+
* This is the only way to drive a browser somebody is already signed into.
|
|
6
|
+
* Chrome has refused remote debugging on the default profile since 136, so
|
|
7
|
+
* attaching to the everyday browser is not a thing a build can do however it
|
|
8
|
+
* asks — and that refusal is the right one, because a debugging port is
|
|
9
|
+
* unauthenticated control of every logged-in session on the machine. An
|
|
10
|
+
* extension is the sanctioned path: the person installs it, Chrome shows a
|
|
11
|
+
* banner for as long as it is driving, and `chrome.debugger` gives the same CDP
|
|
12
|
+
* everything else here already speaks.
|
|
13
|
+
*
|
|
14
|
+
* So the extension is a relay and almost nothing else. It forwards CDP in both
|
|
15
|
+
* directions and answers a handful of control messages that CDP has no browser
|
|
16
|
+
* half for — listing tabs, opening or activating one, attaching. The same `CdpConnection` that
|
|
17
|
+
* drives a piped Chrome drives this, with the tab id standing in for the session
|
|
18
|
+
* id, which is what it already is.
|
|
19
|
+
*
|
|
20
|
+
* The bridge in between is a local socket rather than an HTTP endpoint on the
|
|
21
|
+
* harness server. Chrome spawns the native messaging host, so the host is the
|
|
22
|
+
* process that exists first; having it listen and the harness connect means the
|
|
23
|
+
* feature needs no server running, no token to plumb, and works the same from
|
|
24
|
+
* the TUI as from the desktop app.
|
|
25
|
+
*/
|
|
26
|
+
/** What the target needs of the thing between it and the extension. */
|
|
27
|
+
export interface ExtensionChannel {
|
|
28
|
+
send(message: unknown): void;
|
|
29
|
+
onMessage(handler: (message: unknown) => void): void;
|
|
30
|
+
onClose(handler: (reason?: string) => void): void;
|
|
31
|
+
close(): void;
|
|
32
|
+
}
|
|
33
|
+
/** Open the socket the native messaging host listens on. */
|
|
34
|
+
export declare function connectExtensionChannel(path?: string): Promise<ExtensionChannel>;
|
|
35
|
+
export declare function extensionTarget(channel?: ExtensionChannel, context?: BrowserTargetContext): Promise<BrowserTarget>;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { connect } from "node:net";
|
|
2
|
+
import { BrowserError } from "../core/errors.js";
|
|
3
|
+
import { CdpConnection } from "./cdp.js";
|
|
4
|
+
import { ScreencastHub } from "./screencast.js";
|
|
5
|
+
import { extensionSocketPath } from "./socket.js";
|
|
6
|
+
/** Open the socket the native messaging host listens on. */
|
|
7
|
+
export function connectExtensionChannel(path = extensionSocketPath()) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const socket = connect(path);
|
|
10
|
+
const messageHandlers = new Set();
|
|
11
|
+
const closeHandlers = new Set();
|
|
12
|
+
let buffer = "";
|
|
13
|
+
socket.setEncoding("utf8");
|
|
14
|
+
socket.on("data", (chunk) => {
|
|
15
|
+
buffer += chunk;
|
|
16
|
+
let newline;
|
|
17
|
+
while ((newline = buffer.indexOf("\n")) !== -1) {
|
|
18
|
+
const line = buffer.slice(0, newline).trim();
|
|
19
|
+
buffer = buffer.slice(newline + 1);
|
|
20
|
+
if (line === "")
|
|
21
|
+
continue;
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = JSON.parse(line);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
for (const handler of messageHandlers)
|
|
30
|
+
handler(parsed);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
socket.once("error", (err) => {
|
|
34
|
+
reject(new BrowserError(`no browser extension is connected. Add the OHarness extension to Chrome, Edge or Brave and keep that browser open; outside the desktop app, also run \`oharness-browser-host install\` once. (${err.message})`, err));
|
|
35
|
+
});
|
|
36
|
+
socket.on("close", () => {
|
|
37
|
+
for (const handler of closeHandlers)
|
|
38
|
+
handler("the extension disconnected");
|
|
39
|
+
});
|
|
40
|
+
socket.once("connect", () => {
|
|
41
|
+
socket.removeAllListeners("error");
|
|
42
|
+
socket.on("error", () => {
|
|
43
|
+
for (const handler of closeHandlers)
|
|
44
|
+
handler("the extension connection failed");
|
|
45
|
+
});
|
|
46
|
+
resolve({
|
|
47
|
+
send: (message) => socket.write(`${JSON.stringify(message)}\n`),
|
|
48
|
+
onMessage: (handler) => void messageHandlers.add(handler),
|
|
49
|
+
onClose: (handler) => void closeHandlers.add(handler),
|
|
50
|
+
close: () => socket.end(),
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export async function extensionTarget(channel, context = {}) {
|
|
56
|
+
return new ExtensionTarget(channel ?? (await connectExtensionChannel()), context);
|
|
57
|
+
}
|
|
58
|
+
class ExtensionTarget {
|
|
59
|
+
kind = "extension";
|
|
60
|
+
channel;
|
|
61
|
+
cdp;
|
|
62
|
+
attached = new Map();
|
|
63
|
+
/** Tabs this conversation opened, as against the person's own it was pointed at. */
|
|
64
|
+
openedTabs = new Set();
|
|
65
|
+
control = new Map();
|
|
66
|
+
nextControlId = 1;
|
|
67
|
+
context;
|
|
68
|
+
constructor(channel, context = {}) {
|
|
69
|
+
this.channel = channel;
|
|
70
|
+
this.context = context;
|
|
71
|
+
this.cdp = new CdpConnection((message) => channel.send({ t: "cdp", ...message }));
|
|
72
|
+
channel.onMessage((message) => {
|
|
73
|
+
if (typeof message !== "object" || message === null)
|
|
74
|
+
return;
|
|
75
|
+
const envelope = message;
|
|
76
|
+
if (envelope["t"] === "cdp") {
|
|
77
|
+
// The person cancelled the debugging banner, or the tab went away. The
|
|
78
|
+
// page is no longer ours; the next use of it attaches again.
|
|
79
|
+
if (envelope["method"] === "Inspector.detached" && envelope["sessionId"] !== undefined) {
|
|
80
|
+
this.attached.delete(String(envelope["sessionId"]));
|
|
81
|
+
}
|
|
82
|
+
const { t: _ignored, ...rest } = envelope;
|
|
83
|
+
this.cdp.deliver(rest);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (envelope["t"] === "ctl") {
|
|
87
|
+
const reply = envelope;
|
|
88
|
+
const pending = this.control.get(reply.id);
|
|
89
|
+
if (!pending)
|
|
90
|
+
return;
|
|
91
|
+
this.control.delete(reply.id);
|
|
92
|
+
if (reply.error)
|
|
93
|
+
pending.reject(new BrowserError(reply.error));
|
|
94
|
+
else
|
|
95
|
+
pending.resolve(reply.result);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
channel.onClose((reason) => {
|
|
99
|
+
this.cdp.fail(reason ?? "the extension disconnected");
|
|
100
|
+
for (const pending of this.control.values()) {
|
|
101
|
+
pending.reject(new BrowserError(reason ?? "the extension disconnected"));
|
|
102
|
+
}
|
|
103
|
+
this.control.clear();
|
|
104
|
+
this.attached.clear();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Ask the extension something CDP has no browser-level answer for.
|
|
109
|
+
*
|
|
110
|
+
* `chrome.debugger` is scoped to a tab, so listing and opening tabs go through
|
|
111
|
+
* `chrome.tabs` on the extension side and come back here as control replies.
|
|
112
|
+
*/
|
|
113
|
+
ask(op, params = {}) {
|
|
114
|
+
const id = this.nextControlId++;
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
this.control.set(id, { resolve, reject });
|
|
117
|
+
const timer = setTimeout(() => {
|
|
118
|
+
if (!this.control.delete(id))
|
|
119
|
+
return;
|
|
120
|
+
reject(new BrowserError(`the extension did not answer ${op}`));
|
|
121
|
+
}, 15_000);
|
|
122
|
+
timer.unref?.();
|
|
123
|
+
try {
|
|
124
|
+
this.channel.send({ t: "ctl", id, op, ...params });
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
this.control.delete(id);
|
|
128
|
+
reject(new BrowserError(`could not reach the extension`, err));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The tabs this conversation is driving — never the rest of the window.
|
|
134
|
+
*
|
|
135
|
+
* Every other target answers with all of its pages, because all of them are
|
|
136
|
+
* the agent's. Here the rest are the person's, and several conversations may
|
|
137
|
+
* be using the browser at once: attaching everything would put a banner on
|
|
138
|
+
* every tab and have each conversation start on whichever tab came first.
|
|
139
|
+
* The others are listed by `pageInfos` and attached when one is chosen.
|
|
140
|
+
*/
|
|
141
|
+
async pages() {
|
|
142
|
+
const open = new Set((await this.tabRecords()).map((entry) => String(entry.tabId)));
|
|
143
|
+
return [...this.attached.values()].filter((page) => open.has(page.id));
|
|
144
|
+
}
|
|
145
|
+
async tabRecords() {
|
|
146
|
+
const result = await this.ask("tabs");
|
|
147
|
+
return Array.isArray(result) ? result : [];
|
|
148
|
+
}
|
|
149
|
+
async pageInfos() {
|
|
150
|
+
return (await this.tabRecords()).flatMap((entry) => {
|
|
151
|
+
if (typeof entry.tabId !== "number" && typeof entry.tabId !== "string")
|
|
152
|
+
return [];
|
|
153
|
+
const title = typeof entry.title === "string" ? entry.title.trim() : "";
|
|
154
|
+
return [{
|
|
155
|
+
id: String(entry.tabId),
|
|
156
|
+
url: typeof entry.url === "string" ? entry.url : "",
|
|
157
|
+
...(title ? { title } : {}),
|
|
158
|
+
}];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async pageById(id) {
|
|
162
|
+
const known = (await this.pageInfos()).some((page) => page.id === id);
|
|
163
|
+
return known ? this.attach(id) : undefined;
|
|
164
|
+
}
|
|
165
|
+
async newPage(url = "about:blank") {
|
|
166
|
+
// Grouped per conversation, so the person can see which tabs belong to
|
|
167
|
+
// which piece of work, and several conversations never share a tab.
|
|
168
|
+
const title = this.context.label?.()?.trim();
|
|
169
|
+
const result = (await this.ask("newTab", {
|
|
170
|
+
url,
|
|
171
|
+
group: {
|
|
172
|
+
key: this.context.sessionId ?? "oharness",
|
|
173
|
+
...(title ? { title } : {}),
|
|
174
|
+
// So the side panel, finding the person in this group, can open the
|
|
175
|
+
// conversation in its project.
|
|
176
|
+
...(this.context.cwd ? { cwd: this.context.cwd } : {}),
|
|
177
|
+
},
|
|
178
|
+
}));
|
|
179
|
+
const tabId = result?.tabId;
|
|
180
|
+
if (typeof tabId !== "number" && typeof tabId !== "string") {
|
|
181
|
+
throw new BrowserError("the extension opened no tab");
|
|
182
|
+
}
|
|
183
|
+
this.openedTabs.add(String(tabId));
|
|
184
|
+
return this.attach(String(tabId));
|
|
185
|
+
}
|
|
186
|
+
opened(id) {
|
|
187
|
+
return this.openedTabs.has(id);
|
|
188
|
+
}
|
|
189
|
+
/** Let go of one tab — the banner goes, the tab stays where the person had it. */
|
|
190
|
+
async release(page) {
|
|
191
|
+
this.attached.delete(page.id);
|
|
192
|
+
await this.ask("detach", { tabId: page.id });
|
|
193
|
+
}
|
|
194
|
+
async activate(page) {
|
|
195
|
+
await this.ask("activate", { tabId: page.id });
|
|
196
|
+
}
|
|
197
|
+
/** The browser the extension runs in. An extension older than `info` cannot say. */
|
|
198
|
+
async describe() {
|
|
199
|
+
const result = (await this.ask("info"));
|
|
200
|
+
const name = typeof result?.name === "string" && result.name.trim() ? result.name.trim() : "Chrome";
|
|
201
|
+
return { name, ...(typeof result?.version === "string" ? { version: result.version } : {}) };
|
|
202
|
+
}
|
|
203
|
+
async attach(tabId) {
|
|
204
|
+
const existing = this.attached.get(tabId);
|
|
205
|
+
if (existing)
|
|
206
|
+
return existing;
|
|
207
|
+
await this.ask("attach", { tabId });
|
|
208
|
+
const page = new ExtensionPage(tabId, this.cdp, (op, params) => this.ask(op, params), () => {
|
|
209
|
+
this.attached.delete(tabId);
|
|
210
|
+
this.openedTabs.delete(tabId);
|
|
211
|
+
});
|
|
212
|
+
this.attached.set(tabId, page);
|
|
213
|
+
await page.prepare();
|
|
214
|
+
return page;
|
|
215
|
+
}
|
|
216
|
+
async close() {
|
|
217
|
+
// Detach rather than close: these are the person's own tabs, and shutting
|
|
218
|
+
// the harness down must not take their browsing with it.
|
|
219
|
+
for (const tabId of this.attached.keys()) {
|
|
220
|
+
await this.ask("detach", { tabId }).catch(() => { });
|
|
221
|
+
}
|
|
222
|
+
this.attached.clear();
|
|
223
|
+
this.channel.close();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
class ExtensionPage {
|
|
227
|
+
id;
|
|
228
|
+
cdp;
|
|
229
|
+
ask;
|
|
230
|
+
forget;
|
|
231
|
+
screencast;
|
|
232
|
+
constructor(id, cdp, ask, forget) {
|
|
233
|
+
this.id = id;
|
|
234
|
+
this.cdp = cdp;
|
|
235
|
+
this.ask = ask;
|
|
236
|
+
this.forget = forget;
|
|
237
|
+
this.screencast = new ScreencastHub(this);
|
|
238
|
+
}
|
|
239
|
+
async prepare() {
|
|
240
|
+
for (const domain of ["Page.enable", "Runtime.enable", "DOM.enable", "Log.enable"]) {
|
|
241
|
+
await this.send(domain).catch(() => { });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
send(method, params = {}) {
|
|
245
|
+
return this.cdp.send(method, params, this.id);
|
|
246
|
+
}
|
|
247
|
+
on(event, handler) {
|
|
248
|
+
return this.cdp.on(event, handler, this.id);
|
|
249
|
+
}
|
|
250
|
+
/** Frames, shared with every other watcher of this tab. */
|
|
251
|
+
frames(opts = {}) {
|
|
252
|
+
return this.screencast.frames(opts);
|
|
253
|
+
}
|
|
254
|
+
async input(event) {
|
|
255
|
+
if (event.type === "text") {
|
|
256
|
+
await this.send("Input.insertText", { text: event.text });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (event.type === "key") {
|
|
260
|
+
const type = event.action === "char" ? "char" : event.action === "down" ? "keyDown" : "keyUp";
|
|
261
|
+
await this.send("Input.dispatchKeyEvent", {
|
|
262
|
+
type,
|
|
263
|
+
...(event.key ? { key: event.key } : {}),
|
|
264
|
+
...(event.code ? { code: event.code } : {}),
|
|
265
|
+
...(event.text ? { text: event.text } : {}),
|
|
266
|
+
modifiers: event.modifiers ?? 0,
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (event.action === "wheel") {
|
|
271
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
272
|
+
type: "mouseWheel",
|
|
273
|
+
x: event.x,
|
|
274
|
+
y: event.y,
|
|
275
|
+
deltaX: event.deltaX ?? 0,
|
|
276
|
+
deltaY: event.deltaY ?? 0,
|
|
277
|
+
modifiers: event.modifiers ?? 0,
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
282
|
+
type: event.action === "move" ? "mouseMoved" : event.action === "down" ? "mousePressed" : "mouseReleased",
|
|
283
|
+
x: event.x,
|
|
284
|
+
y: event.y,
|
|
285
|
+
button: event.button ?? "left",
|
|
286
|
+
clickCount: event.clickCount ?? (event.action === "move" ? 0 : 1),
|
|
287
|
+
modifiers: event.modifiers ?? 0,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
/** Explicit tab close. Target shutdown still only detaches the person's other tabs. */
|
|
291
|
+
async close() {
|
|
292
|
+
this.forget();
|
|
293
|
+
this.screencast.close();
|
|
294
|
+
await this.ask("closeTab", { tabId: this.id });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=extension.js.map
|