@different-ai/opencode-browser 4.3.2 → 4.5.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/.opencode/skill/browser-automation/SKILL.md +7 -1
- package/README.md +75 -11
- package/bin/agent-gateway.cjs +129 -0
- package/bin/broker.cjs +158 -11
- package/bin/cli.js +254 -31
- package/bin/tool-test.ts +35 -0
- package/dist/plugin.js +803 -46
- package/extension/background.js +180 -35
- package/extension/manifest.json +4 -1
- package/package.json +8 -2
package/dist/plugin.js
CHANGED
|
@@ -12330,22 +12330,700 @@ function tool(input) {
|
|
|
12330
12330
|
}
|
|
12331
12331
|
tool.schema = exports_external;
|
|
12332
12332
|
// src/plugin.ts
|
|
12333
|
+
import net2 from "net";
|
|
12334
|
+
|
|
12335
|
+
// src/agent-backend.ts
|
|
12333
12336
|
import net from "net";
|
|
12334
|
-
import {
|
|
12335
|
-
import {
|
|
12336
|
-
import {
|
|
12337
|
+
import { readFileSync } from "fs";
|
|
12338
|
+
import { tmpdir } from "os";
|
|
12339
|
+
import { join } from "path";
|
|
12337
12340
|
import { spawn } from "child_process";
|
|
12341
|
+
import { createRequire } from "module";
|
|
12342
|
+
var agentRequire = createRequire(import.meta.url);
|
|
12343
|
+
var REQUEST_TIMEOUT_MS = 60000;
|
|
12344
|
+
var DEFAULT_PAGE_TEXT_LIMIT = 20000;
|
|
12345
|
+
var DEFAULT_LIST_LIMIT = 50;
|
|
12346
|
+
var DEFAULT_POLL_MS = 200;
|
|
12347
|
+
function createJsonLineParser(onMessage) {
|
|
12348
|
+
let buffer = "";
|
|
12349
|
+
return (chunk) => {
|
|
12350
|
+
buffer += chunk.toString("utf8");
|
|
12351
|
+
while (true) {
|
|
12352
|
+
const idx = buffer.indexOf(`
|
|
12353
|
+
`);
|
|
12354
|
+
if (idx === -1)
|
|
12355
|
+
return;
|
|
12356
|
+
const line = buffer.slice(0, idx);
|
|
12357
|
+
buffer = buffer.slice(idx + 1);
|
|
12358
|
+
if (!line.trim())
|
|
12359
|
+
continue;
|
|
12360
|
+
try {
|
|
12361
|
+
onMessage(JSON.parse(line));
|
|
12362
|
+
} catch {}
|
|
12363
|
+
}
|
|
12364
|
+
};
|
|
12365
|
+
}
|
|
12366
|
+
function writeJsonLine(socket, msg) {
|
|
12367
|
+
socket.write(JSON.stringify(msg) + `
|
|
12368
|
+
`);
|
|
12369
|
+
}
|
|
12370
|
+
async function sleep(ms) {
|
|
12371
|
+
return await new Promise((resolve) => setTimeout(resolve, ms));
|
|
12372
|
+
}
|
|
12373
|
+
function parseEnvNumber(value) {
|
|
12374
|
+
if (!value)
|
|
12375
|
+
return null;
|
|
12376
|
+
const parsed = Number(value);
|
|
12377
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
12378
|
+
}
|
|
12379
|
+
function getAgentSession(sessionId) {
|
|
12380
|
+
const override = process.env.OPENCODE_BROWSER_AGENT_SESSION?.trim();
|
|
12381
|
+
if (override)
|
|
12382
|
+
return override;
|
|
12383
|
+
return `opencode-${sessionId}`;
|
|
12384
|
+
}
|
|
12385
|
+
function getAgentPortForSession(session) {
|
|
12386
|
+
let hash2 = 0;
|
|
12387
|
+
for (let i = 0;i < session.length; i++) {
|
|
12388
|
+
hash2 = (hash2 << 5) - hash2 + session.charCodeAt(i);
|
|
12389
|
+
hash2 |= 0;
|
|
12390
|
+
}
|
|
12391
|
+
return 49152 + Math.abs(hash2) % 16383;
|
|
12392
|
+
}
|
|
12393
|
+
function getAgentConnectionInfo(session) {
|
|
12394
|
+
const socketOverride = process.env.OPENCODE_BROWSER_AGENT_SOCKET?.trim();
|
|
12395
|
+
if (socketOverride) {
|
|
12396
|
+
return { type: "unix", path: socketOverride };
|
|
12397
|
+
}
|
|
12398
|
+
const hostOverride = process.env.OPENCODE_BROWSER_AGENT_HOST?.trim();
|
|
12399
|
+
const portOverride = parseEnvNumber(process.env.OPENCODE_BROWSER_AGENT_PORT);
|
|
12400
|
+
const transportOverride = process.env.OPENCODE_BROWSER_AGENT_TRANSPORT?.toLowerCase();
|
|
12401
|
+
const forceTcp = transportOverride === "tcp" || process.env.OPENCODE_BROWSER_AGENT_TCP === "1";
|
|
12402
|
+
if (hostOverride || portOverride !== null || forceTcp || process.platform === "win32") {
|
|
12403
|
+
const host = hostOverride || "127.0.0.1";
|
|
12404
|
+
const port = portOverride ?? getAgentPortForSession(session);
|
|
12405
|
+
return { type: "tcp", host, port };
|
|
12406
|
+
}
|
|
12407
|
+
return { type: "unix", path: join(tmpdir(), `agent-browser-${session}.sock`) };
|
|
12408
|
+
}
|
|
12409
|
+
function isLocalHost(host) {
|
|
12410
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1";
|
|
12411
|
+
}
|
|
12412
|
+
function resolveAgentDaemonPath() {
|
|
12413
|
+
const override = process.env.OPENCODE_BROWSER_AGENT_DAEMON?.trim();
|
|
12414
|
+
if (override)
|
|
12415
|
+
return override;
|
|
12416
|
+
try {
|
|
12417
|
+
return agentRequire.resolve("agent-browser/dist/daemon.js");
|
|
12418
|
+
} catch {
|
|
12419
|
+
return null;
|
|
12420
|
+
}
|
|
12421
|
+
}
|
|
12422
|
+
function resolveAgentNodePath() {
|
|
12423
|
+
const override = process.env.OPENCODE_BROWSER_AGENT_NODE?.trim();
|
|
12424
|
+
return override || process.execPath;
|
|
12425
|
+
}
|
|
12426
|
+
function getAgentPackageVersion() {
|
|
12427
|
+
try {
|
|
12428
|
+
const pkgPath = agentRequire.resolve("agent-browser/package.json");
|
|
12429
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
12430
|
+
return typeof pkg?.version === "string" ? pkg.version : null;
|
|
12431
|
+
} catch {
|
|
12432
|
+
return null;
|
|
12433
|
+
}
|
|
12434
|
+
}
|
|
12435
|
+
function shouldAutoStartAgent(connection) {
|
|
12436
|
+
const autoStart = process.env.OPENCODE_BROWSER_AGENT_AUTOSTART?.toLowerCase();
|
|
12437
|
+
if (autoStart && ["0", "false", "no"].includes(autoStart))
|
|
12438
|
+
return false;
|
|
12439
|
+
if (connection.type === "unix")
|
|
12440
|
+
return true;
|
|
12441
|
+
return connection.type === "tcp" && process.platform === "win32" && isLocalHost(connection.host);
|
|
12442
|
+
}
|
|
12443
|
+
async function maybeStartAgentDaemon(connection, session) {
|
|
12444
|
+
if (!shouldAutoStartAgent(connection))
|
|
12445
|
+
return;
|
|
12446
|
+
const daemonPath = resolveAgentDaemonPath();
|
|
12447
|
+
if (!daemonPath) {
|
|
12448
|
+
throw new Error("agent-browser dependency not found. Install agent-browser or set OPENCODE_BROWSER_AGENT_DAEMON.");
|
|
12449
|
+
}
|
|
12450
|
+
try {
|
|
12451
|
+
const child = spawn(resolveAgentNodePath(), [daemonPath], {
|
|
12452
|
+
detached: true,
|
|
12453
|
+
stdio: "ignore",
|
|
12454
|
+
env: {
|
|
12455
|
+
...process.env,
|
|
12456
|
+
AGENT_BROWSER_SESSION: session,
|
|
12457
|
+
AGENT_BROWSER_DAEMON: "1"
|
|
12458
|
+
}
|
|
12459
|
+
});
|
|
12460
|
+
child.unref();
|
|
12461
|
+
} catch {}
|
|
12462
|
+
}
|
|
12463
|
+
function buildEvalScript(body) {
|
|
12464
|
+
return `(() => { ${body} })()`;
|
|
12465
|
+
}
|
|
12466
|
+
function buildAgentTypeScript(selector, indexValue, text, clear) {
|
|
12467
|
+
const payload = { selector, index: indexValue, text, clear };
|
|
12468
|
+
return buildEvalScript(`
|
|
12469
|
+
const payload = ${JSON.stringify(payload)};
|
|
12470
|
+
let matches = [];
|
|
12471
|
+
try {
|
|
12472
|
+
matches = Array.from(document.querySelectorAll(payload.selector));
|
|
12473
|
+
} catch {
|
|
12474
|
+
return { ok: false, error: "Invalid selector" };
|
|
12475
|
+
}
|
|
12476
|
+
const element = matches[payload.index];
|
|
12477
|
+
if (!element) return { ok: false, error: "Element not found" };
|
|
12478
|
+
const tag = element.tagName ? element.tagName.toUpperCase() : "";
|
|
12479
|
+
if (tag === "INPUT" || tag === "TEXTAREA") {
|
|
12480
|
+
if (payload.clear) element.value = "";
|
|
12481
|
+
element.value = (element.value || "") + payload.text;
|
|
12482
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
12483
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
12484
|
+
return { ok: true };
|
|
12485
|
+
}
|
|
12486
|
+
if (element.isContentEditable) {
|
|
12487
|
+
if (payload.clear) element.textContent = "";
|
|
12488
|
+
element.textContent = (element.textContent || "") + payload.text;
|
|
12489
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
12490
|
+
return { ok: true };
|
|
12491
|
+
}
|
|
12492
|
+
return { ok: false, error: "Element is not typable" };
|
|
12493
|
+
`);
|
|
12494
|
+
}
|
|
12495
|
+
function buildAgentSelectScript(selector, indexValue, value, label, optionIndex) {
|
|
12496
|
+
const payload = {
|
|
12497
|
+
selector,
|
|
12498
|
+
index: indexValue,
|
|
12499
|
+
value: value ?? null,
|
|
12500
|
+
label: label ?? null,
|
|
12501
|
+
optionIndex: Number.isFinite(optionIndex) ? optionIndex : null
|
|
12502
|
+
};
|
|
12503
|
+
return buildEvalScript(`
|
|
12504
|
+
const payload = ${JSON.stringify(payload)};
|
|
12505
|
+
let matches = [];
|
|
12506
|
+
try {
|
|
12507
|
+
matches = Array.from(document.querySelectorAll(payload.selector));
|
|
12508
|
+
} catch {
|
|
12509
|
+
return { ok: false, error: "Invalid selector" };
|
|
12510
|
+
}
|
|
12511
|
+
const element = matches[payload.index];
|
|
12512
|
+
if (!element) return { ok: false, error: "Element not found" };
|
|
12513
|
+
if (!element.tagName || element.tagName.toUpperCase() !== "SELECT") {
|
|
12514
|
+
return { ok: false, error: "Element is not a select" };
|
|
12515
|
+
}
|
|
12516
|
+
const options = Array.from(element.options || []);
|
|
12517
|
+
let chosen = null;
|
|
12518
|
+
if (payload.value !== null) {
|
|
12519
|
+
chosen = options.find((option) => option.value === payload.value) || null;
|
|
12520
|
+
}
|
|
12521
|
+
if (!chosen && payload.label !== null) {
|
|
12522
|
+
const target = payload.label.trim();
|
|
12523
|
+
chosen = options.find((option) => (option.label || option.textContent || "").trim() === target) || null;
|
|
12524
|
+
}
|
|
12525
|
+
if (!chosen && payload.optionIndex !== null) {
|
|
12526
|
+
chosen = options[payload.optionIndex] || null;
|
|
12527
|
+
}
|
|
12528
|
+
if (!chosen) return { ok: false, error: "Option not found" };
|
|
12529
|
+
element.value = chosen.value;
|
|
12530
|
+
chosen.selected = true;
|
|
12531
|
+
element.dispatchEvent(new Event("input", { bubbles: true }));
|
|
12532
|
+
element.dispatchEvent(new Event("change", { bubbles: true }));
|
|
12533
|
+
return {
|
|
12534
|
+
ok: true,
|
|
12535
|
+
value: element.value,
|
|
12536
|
+
label: (chosen.label || chosen.textContent || "").trim(),
|
|
12537
|
+
};
|
|
12538
|
+
`);
|
|
12539
|
+
}
|
|
12540
|
+
function buildAgentPageTextScript(limit, pattern, flags) {
|
|
12541
|
+
const payload = { limit, pattern, flags };
|
|
12542
|
+
return buildEvalScript(`
|
|
12543
|
+
const payload = ${JSON.stringify(payload)};
|
|
12544
|
+
const safeString = (value) => (typeof value === "string" ? value : "");
|
|
12545
|
+
const bodyText = safeString(document.body ? document.body.innerText : "");
|
|
12546
|
+
const inputText = Array.from(document.querySelectorAll("input, textarea, [contenteditable='true']"))
|
|
12547
|
+
.map((element) => {
|
|
12548
|
+
if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") {
|
|
12549
|
+
return safeString(element.value);
|
|
12550
|
+
}
|
|
12551
|
+
return safeString(element.textContent);
|
|
12552
|
+
})
|
|
12553
|
+
.filter(Boolean)
|
|
12554
|
+
.join("
|
|
12555
|
+
");
|
|
12556
|
+
const combined = [bodyText, inputText].filter(Boolean).join("
|
|
12557
|
+
|
|
12558
|
+
");
|
|
12559
|
+
const maxSize = Number.isFinite(payload.limit) ? payload.limit : ${DEFAULT_PAGE_TEXT_LIMIT};
|
|
12560
|
+
const text = combined.slice(0, Math.max(0, maxSize));
|
|
12561
|
+
let matches = [];
|
|
12562
|
+
if (payload.pattern) {
|
|
12563
|
+
try {
|
|
12564
|
+
const re = new RegExp(payload.pattern, payload.flags || "i");
|
|
12565
|
+
let match;
|
|
12566
|
+
while ((match = re.exec(text)) && matches.length < 50) {
|
|
12567
|
+
matches.push(match[0]);
|
|
12568
|
+
if (!re.global) break;
|
|
12569
|
+
}
|
|
12570
|
+
} catch {
|
|
12571
|
+
matches = [];
|
|
12572
|
+
}
|
|
12573
|
+
}
|
|
12574
|
+
return {
|
|
12575
|
+
url: location.href,
|
|
12576
|
+
title: document.title,
|
|
12577
|
+
text,
|
|
12578
|
+
matches,
|
|
12579
|
+
};
|
|
12580
|
+
`);
|
|
12581
|
+
}
|
|
12582
|
+
function buildAgentListScript(selector, limit) {
|
|
12583
|
+
const payload = { selector, limit };
|
|
12584
|
+
return buildEvalScript(`
|
|
12585
|
+
const payload = ${JSON.stringify(payload)};
|
|
12586
|
+
let nodes = [];
|
|
12587
|
+
try {
|
|
12588
|
+
nodes = Array.from(document.querySelectorAll(payload.selector));
|
|
12589
|
+
} catch {
|
|
12590
|
+
return { ok: false, error: "Invalid selector" };
|
|
12591
|
+
}
|
|
12592
|
+
const maxItems = Math.min(Math.max(1, payload.limit || ${DEFAULT_LIST_LIMIT}), 200);
|
|
12593
|
+
const items = nodes.slice(0, maxItems).map((element) => ({
|
|
12594
|
+
text: (element.innerText || element.textContent || "").trim().slice(0, 200),
|
|
12595
|
+
tag: (element.tagName || "").toLowerCase(),
|
|
12596
|
+
ariaLabel: element.getAttribute ? element.getAttribute("aria-label") : null,
|
|
12597
|
+
}));
|
|
12598
|
+
return { ok: true, value: { items, count: nodes.length } };
|
|
12599
|
+
`);
|
|
12600
|
+
}
|
|
12601
|
+
function buildAgentNthValueScript(selector, indexValue) {
|
|
12602
|
+
const payload = { selector, index: indexValue };
|
|
12603
|
+
return buildEvalScript(`
|
|
12604
|
+
const payload = ${JSON.stringify(payload)};
|
|
12605
|
+
let nodes = [];
|
|
12606
|
+
try {
|
|
12607
|
+
nodes = Array.from(document.querySelectorAll(payload.selector));
|
|
12608
|
+
} catch {
|
|
12609
|
+
return { ok: false, error: "Invalid selector" };
|
|
12610
|
+
}
|
|
12611
|
+
const element = nodes[payload.index];
|
|
12612
|
+
if (!element) return { ok: false, error: "Element not found" };
|
|
12613
|
+
const value = element.value !== undefined ? element.value : "";
|
|
12614
|
+
return { ok: true, value: typeof value === "string" ? value : String(value ?? "") };
|
|
12615
|
+
`);
|
|
12616
|
+
}
|
|
12617
|
+
function buildAgentNthAttributeScript(selector, indexValue, attribute) {
|
|
12618
|
+
const payload = { selector, index: indexValue, attribute };
|
|
12619
|
+
return buildEvalScript(`
|
|
12620
|
+
const payload = ${JSON.stringify(payload)};
|
|
12621
|
+
let nodes = [];
|
|
12622
|
+
try {
|
|
12623
|
+
nodes = Array.from(document.querySelectorAll(payload.selector));
|
|
12624
|
+
} catch {
|
|
12625
|
+
return { ok: false, error: "Invalid selector" };
|
|
12626
|
+
}
|
|
12627
|
+
const element = nodes[payload.index];
|
|
12628
|
+
if (!element) return { ok: false, error: "Element not found" };
|
|
12629
|
+
const value = element.getAttribute ? element.getAttribute(payload.attribute) : null;
|
|
12630
|
+
return { ok: true, value };
|
|
12631
|
+
`);
|
|
12632
|
+
}
|
|
12633
|
+
function buildAgentNthPropertyScript(selector, indexValue, property) {
|
|
12634
|
+
const payload = { selector, index: indexValue, property };
|
|
12635
|
+
return buildEvalScript(`
|
|
12636
|
+
const payload = ${JSON.stringify(payload)};
|
|
12637
|
+
let nodes = [];
|
|
12638
|
+
try {
|
|
12639
|
+
nodes = Array.from(document.querySelectorAll(payload.selector));
|
|
12640
|
+
} catch {
|
|
12641
|
+
return { ok: false, error: "Invalid selector" };
|
|
12642
|
+
}
|
|
12643
|
+
const element = nodes[payload.index];
|
|
12644
|
+
if (!element) return { ok: false, error: "Element not found" };
|
|
12645
|
+
return { ok: true, value: element[payload.property] };
|
|
12646
|
+
`);
|
|
12647
|
+
}
|
|
12648
|
+
function buildAgentOuterHtmlScript(selector, indexValue) {
|
|
12649
|
+
const payload = { selector, index: indexValue };
|
|
12650
|
+
return buildEvalScript(`
|
|
12651
|
+
const payload = ${JSON.stringify(payload)};
|
|
12652
|
+
let nodes = [];
|
|
12653
|
+
try {
|
|
12654
|
+
nodes = Array.from(document.querySelectorAll(payload.selector));
|
|
12655
|
+
} catch {
|
|
12656
|
+
return { ok: false, error: "Invalid selector" };
|
|
12657
|
+
}
|
|
12658
|
+
const element = nodes[payload.index];
|
|
12659
|
+
if (!element) return { ok: false, error: "Element not found" };
|
|
12660
|
+
return { ok: true, value: element.outerHTML };
|
|
12661
|
+
`);
|
|
12662
|
+
}
|
|
12663
|
+
function ensureEvalResult(result, fallback) {
|
|
12664
|
+
if (!result || typeof result !== "object" || result.ok !== true) {
|
|
12665
|
+
const message = typeof result?.error === "string" ? result.error : fallback;
|
|
12666
|
+
throw new Error(message);
|
|
12667
|
+
}
|
|
12668
|
+
return result.value;
|
|
12669
|
+
}
|
|
12670
|
+
function createAgentBackend(sessionId) {
|
|
12671
|
+
const session = getAgentSession(sessionId);
|
|
12672
|
+
const connection = getAgentConnectionInfo(session);
|
|
12673
|
+
let agentSocket = null;
|
|
12674
|
+
let agentReqId = 0;
|
|
12675
|
+
const agentPending = new Map;
|
|
12676
|
+
async function connectToAgent() {
|
|
12677
|
+
return await new Promise((resolve, reject) => {
|
|
12678
|
+
const socket = connection.type === "unix" ? net.createConnection(connection.path) : net.createConnection({ host: connection.host, port: connection.port });
|
|
12679
|
+
socket.once("connect", () => resolve(socket));
|
|
12680
|
+
socket.once("error", (err) => reject(err));
|
|
12681
|
+
});
|
|
12682
|
+
}
|
|
12683
|
+
async function ensureAgentSocket() {
|
|
12684
|
+
if (agentSocket && !agentSocket.destroyed)
|
|
12685
|
+
return agentSocket;
|
|
12686
|
+
try {
|
|
12687
|
+
agentSocket = await connectToAgent();
|
|
12688
|
+
} catch {
|
|
12689
|
+
await maybeStartAgentDaemon(connection, session);
|
|
12690
|
+
for (let attempt = 0;attempt < 20; attempt++) {
|
|
12691
|
+
await sleep(100);
|
|
12692
|
+
try {
|
|
12693
|
+
agentSocket = await connectToAgent();
|
|
12694
|
+
break;
|
|
12695
|
+
} catch {}
|
|
12696
|
+
}
|
|
12697
|
+
}
|
|
12698
|
+
if (!agentSocket || agentSocket.destroyed) {
|
|
12699
|
+
const target = connection.type === "unix" ? connection.path : `${connection.host}:${connection.port}`;
|
|
12700
|
+
throw new Error(`Could not connect to agent-browser daemon at ${target}.`);
|
|
12701
|
+
}
|
|
12702
|
+
agentSocket.setNoDelay(true);
|
|
12703
|
+
agentSocket.on("data", createJsonLineParser((msg) => {
|
|
12704
|
+
if (!msg || msg.id === undefined)
|
|
12705
|
+
return;
|
|
12706
|
+
const messageId = typeof msg.id === "string" ? msg.id : String(msg.id);
|
|
12707
|
+
const pending = agentPending.get(messageId);
|
|
12708
|
+
if (!pending)
|
|
12709
|
+
return;
|
|
12710
|
+
agentPending.delete(messageId);
|
|
12711
|
+
const res = msg;
|
|
12712
|
+
if (!res.success)
|
|
12713
|
+
pending.reject(new Error(res.error || "Agent browser error"));
|
|
12714
|
+
else
|
|
12715
|
+
pending.resolve(res.data);
|
|
12716
|
+
}));
|
|
12717
|
+
agentSocket.on("close", () => {
|
|
12718
|
+
for (const pending of agentPending.values()) {
|
|
12719
|
+
pending.reject(new Error("Agent browser connection closed"));
|
|
12720
|
+
}
|
|
12721
|
+
agentPending.clear();
|
|
12722
|
+
agentSocket = null;
|
|
12723
|
+
});
|
|
12724
|
+
agentSocket.on("error", () => {
|
|
12725
|
+
agentSocket = null;
|
|
12726
|
+
});
|
|
12727
|
+
return agentSocket;
|
|
12728
|
+
}
|
|
12729
|
+
async function agentRequest(action, payload) {
|
|
12730
|
+
const socket = await ensureAgentSocket();
|
|
12731
|
+
const id = `a${++agentReqId}`;
|
|
12732
|
+
return await new Promise((resolve, reject) => {
|
|
12733
|
+
agentPending.set(id, { resolve, reject });
|
|
12734
|
+
writeJsonLine(socket, { id, action, ...payload });
|
|
12735
|
+
setTimeout(() => {
|
|
12736
|
+
if (!agentPending.has(id))
|
|
12737
|
+
return;
|
|
12738
|
+
agentPending.delete(id);
|
|
12739
|
+
reject(new Error("Timed out waiting for agent-browser response"));
|
|
12740
|
+
}, REQUEST_TIMEOUT_MS);
|
|
12741
|
+
});
|
|
12742
|
+
}
|
|
12743
|
+
async function agentCommand(action, payload) {
|
|
12744
|
+
return await agentRequest(action, payload);
|
|
12745
|
+
}
|
|
12746
|
+
async function withTab(tabId, action) {
|
|
12747
|
+
if (!Number.isFinite(tabId))
|
|
12748
|
+
return await action();
|
|
12749
|
+
await agentCommand("tab_switch", { index: tabId });
|
|
12750
|
+
return await action();
|
|
12751
|
+
}
|
|
12752
|
+
async function agentEvaluate(script) {
|
|
12753
|
+
const data = await agentCommand("evaluate", { script });
|
|
12754
|
+
return data?.result;
|
|
12755
|
+
}
|
|
12756
|
+
async function waitForCount(selector, minimum, timeoutMs, pollMs) {
|
|
12757
|
+
const timeout = Math.max(0, timeoutMs);
|
|
12758
|
+
const poll = Math.max(0, pollMs || DEFAULT_POLL_MS);
|
|
12759
|
+
const start = Date.now();
|
|
12760
|
+
while (true) {
|
|
12761
|
+
const data = await agentCommand("count", { selector });
|
|
12762
|
+
const count = Number(data?.count ?? 0);
|
|
12763
|
+
if (count >= minimum)
|
|
12764
|
+
return count;
|
|
12765
|
+
if (!timeout || Date.now() - start >= timeout)
|
|
12766
|
+
return count;
|
|
12767
|
+
await sleep(poll);
|
|
12768
|
+
}
|
|
12769
|
+
}
|
|
12770
|
+
async function agentQuery(args) {
|
|
12771
|
+
const selector = typeof args.selector === "string" ? args.selector : undefined;
|
|
12772
|
+
const mode = typeof args.mode === "string" && args.mode ? args.mode : "text";
|
|
12773
|
+
const indexValue = Number.isFinite(args.index) ? args.index : 0;
|
|
12774
|
+
const limitValue = Number.isFinite(args.limit) ? args.limit : mode === "page_text" ? DEFAULT_PAGE_TEXT_LIMIT : DEFAULT_LIST_LIMIT;
|
|
12775
|
+
const timeoutValue = Number.isFinite(args.timeoutMs) ? args.timeoutMs : 0;
|
|
12776
|
+
const pollValue = Number.isFinite(args.pollMs) ? args.pollMs : DEFAULT_POLL_MS;
|
|
12777
|
+
const pattern = typeof args.pattern === "string" ? args.pattern : null;
|
|
12778
|
+
const flags = typeof args.flags === "string" ? args.flags : "i";
|
|
12779
|
+
if (mode === "page_text") {
|
|
12780
|
+
if (selector && timeoutValue > 0) {
|
|
12781
|
+
await waitForCount(selector, 1, timeoutValue, pollValue);
|
|
12782
|
+
}
|
|
12783
|
+
const pageText = await agentEvaluate(buildAgentPageTextScript(limitValue, pattern, flags));
|
|
12784
|
+
return { content: JSON.stringify({ ok: true, value: pageText }, null, 2) };
|
|
12785
|
+
}
|
|
12786
|
+
if (!selector)
|
|
12787
|
+
throw new Error("selector is required");
|
|
12788
|
+
if (mode === "exists") {
|
|
12789
|
+
const count2 = await waitForCount(selector, 1, timeoutValue, pollValue);
|
|
12790
|
+
return {
|
|
12791
|
+
content: JSON.stringify({ ok: true, value: { exists: count2 > 0, count: count2 } }, null, 2)
|
|
12792
|
+
};
|
|
12793
|
+
}
|
|
12794
|
+
const count = await waitForCount(selector, indexValue + 1, timeoutValue, pollValue);
|
|
12795
|
+
if (count <= indexValue) {
|
|
12796
|
+
throw new Error(`No matches for selector: ${selector}`);
|
|
12797
|
+
}
|
|
12798
|
+
if (mode === "text") {
|
|
12799
|
+
const data = indexValue > 0 ? await agentCommand("nth", { selector, index: indexValue, subaction: "text" }) : await agentCommand("innertext", { selector });
|
|
12800
|
+
return { content: typeof data?.text === "string" ? data.text : "" };
|
|
12801
|
+
}
|
|
12802
|
+
if (mode === "value") {
|
|
12803
|
+
if (indexValue > 0) {
|
|
12804
|
+
const result = ensureEvalResult(await agentEvaluate(buildAgentNthValueScript(selector, indexValue)), "Value lookup failed");
|
|
12805
|
+
return { content: typeof result === "string" ? result : JSON.stringify(result) };
|
|
12806
|
+
}
|
|
12807
|
+
const data = await agentCommand("inputvalue", { selector });
|
|
12808
|
+
return { content: typeof data?.value === "string" ? data.value : "" };
|
|
12809
|
+
}
|
|
12810
|
+
if (mode === "attribute") {
|
|
12811
|
+
if (!args.attribute)
|
|
12812
|
+
throw new Error("attribute is required");
|
|
12813
|
+
if (indexValue > 0) {
|
|
12814
|
+
const result = ensureEvalResult(await agentEvaluate(buildAgentNthAttributeScript(selector, indexValue, args.attribute)), "Attribute lookup failed");
|
|
12815
|
+
return { content: typeof result === "string" ? result : JSON.stringify(result) };
|
|
12816
|
+
}
|
|
12817
|
+
const data = await agentCommand("getattribute", { selector, attribute: args.attribute });
|
|
12818
|
+
return { content: typeof data?.value === "string" ? data.value : JSON.stringify(data?.value) };
|
|
12819
|
+
}
|
|
12820
|
+
if (mode === "property") {
|
|
12821
|
+
if (!args.property)
|
|
12822
|
+
throw new Error("property is required");
|
|
12823
|
+
const result = ensureEvalResult(await agentEvaluate(buildAgentNthPropertyScript(selector, indexValue, args.property)), "Property lookup failed");
|
|
12824
|
+
return { content: typeof result === "string" ? result : JSON.stringify(result) };
|
|
12825
|
+
}
|
|
12826
|
+
if (mode === "html") {
|
|
12827
|
+
const result = ensureEvalResult(await agentEvaluate(buildAgentOuterHtmlScript(selector, indexValue)), "HTML lookup failed");
|
|
12828
|
+
return { content: typeof result === "string" ? result : JSON.stringify(result) };
|
|
12829
|
+
}
|
|
12830
|
+
if (mode === "list") {
|
|
12831
|
+
const listResult = ensureEvalResult(await agentEvaluate(buildAgentListScript(selector, limitValue)), "List lookup failed");
|
|
12832
|
+
return { content: JSON.stringify({ ok: true, value: listResult }, null, 2) };
|
|
12833
|
+
}
|
|
12834
|
+
throw new Error(`Unknown mode: ${mode}`);
|
|
12835
|
+
}
|
|
12836
|
+
async function requestTool(tool3, args) {
|
|
12837
|
+
switch (tool3) {
|
|
12838
|
+
case "get_tabs": {
|
|
12839
|
+
const data = await agentCommand("tab_list", {});
|
|
12840
|
+
const tabs = Array.isArray(data?.tabs) ? data.tabs : [];
|
|
12841
|
+
const mapped = tabs.map((tab) => ({
|
|
12842
|
+
id: tab.index,
|
|
12843
|
+
url: tab.url,
|
|
12844
|
+
title: tab.title,
|
|
12845
|
+
active: tab.active,
|
|
12846
|
+
windowId: tab.windowId ?? 0
|
|
12847
|
+
}));
|
|
12848
|
+
return { content: JSON.stringify(mapped, null, 2) };
|
|
12849
|
+
}
|
|
12850
|
+
case "open_tab": {
|
|
12851
|
+
const active = args.active;
|
|
12852
|
+
let previousActive = null;
|
|
12853
|
+
if (active === false) {
|
|
12854
|
+
const list = await agentCommand("tab_list", {});
|
|
12855
|
+
if (Number.isFinite(list?.active))
|
|
12856
|
+
previousActive = list.active;
|
|
12857
|
+
}
|
|
12858
|
+
const created = await agentCommand("tab_new", {});
|
|
12859
|
+
if (args.url) {
|
|
12860
|
+
await agentCommand("navigate", { url: args.url });
|
|
12861
|
+
}
|
|
12862
|
+
if (active === false && previousActive !== null) {
|
|
12863
|
+
await agentCommand("tab_switch", { index: previousActive });
|
|
12864
|
+
}
|
|
12865
|
+
return { content: { tabId: created.index, url: args.url, active: active !== false } };
|
|
12866
|
+
}
|
|
12867
|
+
case "navigate": {
|
|
12868
|
+
return await withTab(args.tabId, async () => {
|
|
12869
|
+
if (!args.url)
|
|
12870
|
+
throw new Error("URL is required");
|
|
12871
|
+
await agentCommand("navigate", { url: args.url });
|
|
12872
|
+
return { content: `Navigated to ${args.url}` };
|
|
12873
|
+
});
|
|
12874
|
+
}
|
|
12875
|
+
case "click": {
|
|
12876
|
+
return await withTab(args.tabId, async () => {
|
|
12877
|
+
if (!args.selector)
|
|
12878
|
+
throw new Error("Selector is required");
|
|
12879
|
+
const indexValue = Number.isFinite(args.index) ? args.index : 0;
|
|
12880
|
+
if (indexValue) {
|
|
12881
|
+
await agentCommand("nth", { selector: args.selector, index: indexValue, subaction: "click" });
|
|
12882
|
+
} else {
|
|
12883
|
+
await agentCommand("click", { selector: args.selector });
|
|
12884
|
+
}
|
|
12885
|
+
return { content: `Clicked ${args.selector}` };
|
|
12886
|
+
});
|
|
12887
|
+
}
|
|
12888
|
+
case "type": {
|
|
12889
|
+
return await withTab(args.tabId, async () => {
|
|
12890
|
+
if (!args.selector)
|
|
12891
|
+
throw new Error("Selector is required");
|
|
12892
|
+
if (args.text === undefined)
|
|
12893
|
+
throw new Error("Text is required");
|
|
12894
|
+
const indexValue = Number.isFinite(args.index) ? args.index : 0;
|
|
12895
|
+
if (!indexValue) {
|
|
12896
|
+
await agentCommand("type", {
|
|
12897
|
+
selector: args.selector,
|
|
12898
|
+
text: String(args.text),
|
|
12899
|
+
clear: args.clear
|
|
12900
|
+
});
|
|
12901
|
+
} else {
|
|
12902
|
+
const result = await agentEvaluate(buildAgentTypeScript(args.selector, indexValue, String(args.text), !!args.clear));
|
|
12903
|
+
if (!result?.ok) {
|
|
12904
|
+
throw new Error(result?.error || "Type failed");
|
|
12905
|
+
}
|
|
12906
|
+
}
|
|
12907
|
+
return { content: `Typed "${args.text}" into ${args.selector}` };
|
|
12908
|
+
});
|
|
12909
|
+
}
|
|
12910
|
+
case "select": {
|
|
12911
|
+
return await withTab(args.tabId, async () => {
|
|
12912
|
+
if (!args.selector)
|
|
12913
|
+
throw new Error("Selector is required");
|
|
12914
|
+
if (args.value === undefined && args.label === undefined && args.optionIndex === undefined) {
|
|
12915
|
+
throw new Error("value, label, or optionIndex is required");
|
|
12916
|
+
}
|
|
12917
|
+
const indexValue = Number.isFinite(args.index) ? args.index : 0;
|
|
12918
|
+
let selectedValue = args.value;
|
|
12919
|
+
let selectedLabel = args.label;
|
|
12920
|
+
if (indexValue || args.label !== undefined || args.optionIndex !== undefined) {
|
|
12921
|
+
const result = await agentEvaluate(buildAgentSelectScript(args.selector, indexValue, args.value, args.label, args.optionIndex));
|
|
12922
|
+
if (!result?.ok) {
|
|
12923
|
+
throw new Error(result?.error || "Select failed");
|
|
12924
|
+
}
|
|
12925
|
+
selectedValue = result.value;
|
|
12926
|
+
selectedLabel = result.label;
|
|
12927
|
+
} else if (args.value !== undefined) {
|
|
12928
|
+
await agentCommand("select", { selector: args.selector, values: args.value });
|
|
12929
|
+
}
|
|
12930
|
+
const valueText = selectedValue ? String(selectedValue) : "";
|
|
12931
|
+
const labelText = selectedLabel ? String(selectedLabel) : "";
|
|
12932
|
+
const summary = labelText && valueText && labelText !== valueText ? `${labelText} (${valueText})` : labelText || valueText || "option";
|
|
12933
|
+
return { content: `Selected ${summary} in ${args.selector}` };
|
|
12934
|
+
});
|
|
12935
|
+
}
|
|
12936
|
+
case "screenshot": {
|
|
12937
|
+
return await withTab(args.tabId, async () => {
|
|
12938
|
+
const data = await agentCommand("screenshot", { format: "png" });
|
|
12939
|
+
const base643 = data?.base64 ? String(data.base64) : "";
|
|
12940
|
+
if (!base643)
|
|
12941
|
+
throw new Error("Screenshot failed");
|
|
12942
|
+
return { content: `data:image/png;base64,${base643}` };
|
|
12943
|
+
});
|
|
12944
|
+
}
|
|
12945
|
+
case "snapshot": {
|
|
12946
|
+
return await withTab(args.tabId, async () => {
|
|
12947
|
+
const data = await agentCommand("snapshot", {});
|
|
12948
|
+
const payload = {
|
|
12949
|
+
snapshot: data?.snapshot ?? "",
|
|
12950
|
+
refs: data?.refs ?? {}
|
|
12951
|
+
};
|
|
12952
|
+
return { content: JSON.stringify(payload, null, 2) };
|
|
12953
|
+
});
|
|
12954
|
+
}
|
|
12955
|
+
case "query": {
|
|
12956
|
+
return await withTab(args.tabId, async () => {
|
|
12957
|
+
return await agentQuery(args);
|
|
12958
|
+
});
|
|
12959
|
+
}
|
|
12960
|
+
case "scroll": {
|
|
12961
|
+
return await withTab(args.tabId, async () => {
|
|
12962
|
+
const x = Number.isFinite(args.x) ? args.x : 0;
|
|
12963
|
+
const y = Number.isFinite(args.y) ? args.y : 0;
|
|
12964
|
+
await agentCommand("scroll", {
|
|
12965
|
+
selector: args.selector,
|
|
12966
|
+
x,
|
|
12967
|
+
y
|
|
12968
|
+
});
|
|
12969
|
+
const target = args.selector ? `to ${args.selector}` : `by (${x}, ${y})`;
|
|
12970
|
+
return { content: `Scrolled ${target}` };
|
|
12971
|
+
});
|
|
12972
|
+
}
|
|
12973
|
+
case "wait": {
|
|
12974
|
+
return await withTab(args.tabId, async () => {
|
|
12975
|
+
const ms = Number.isFinite(args.ms) ? args.ms : 1000;
|
|
12976
|
+
await agentCommand("wait", { timeout: ms });
|
|
12977
|
+
return { content: `Waited ${ms}ms` };
|
|
12978
|
+
});
|
|
12979
|
+
}
|
|
12980
|
+
default:
|
|
12981
|
+
throw new Error(`Unsupported tool for agent backend: ${tool3}`);
|
|
12982
|
+
}
|
|
12983
|
+
}
|
|
12984
|
+
async function status() {
|
|
12985
|
+
let connected = false;
|
|
12986
|
+
let error45;
|
|
12987
|
+
try {
|
|
12988
|
+
await ensureAgentSocket();
|
|
12989
|
+
connected = true;
|
|
12990
|
+
} catch (err) {
|
|
12991
|
+
error45 = err instanceof Error ? err.message : String(err);
|
|
12992
|
+
}
|
|
12993
|
+
return {
|
|
12994
|
+
backend: "agent-browser",
|
|
12995
|
+
session,
|
|
12996
|
+
connection,
|
|
12997
|
+
connected,
|
|
12998
|
+
error: error45,
|
|
12999
|
+
agentBrowserVersion: getAgentPackageVersion()
|
|
13000
|
+
};
|
|
13001
|
+
}
|
|
13002
|
+
return {
|
|
13003
|
+
mode: "agent",
|
|
13004
|
+
session,
|
|
13005
|
+
connection,
|
|
13006
|
+
getVersion: getAgentPackageVersion,
|
|
13007
|
+
status,
|
|
13008
|
+
requestTool
|
|
13009
|
+
};
|
|
13010
|
+
}
|
|
13011
|
+
|
|
13012
|
+
// src/plugin.ts
|
|
13013
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2 } from "fs";
|
|
13014
|
+
import { homedir } from "os";
|
|
13015
|
+
import { dirname, join as join2 } from "path";
|
|
13016
|
+
import { spawn as spawn2 } from "child_process";
|
|
12338
13017
|
import { fileURLToPath } from "url";
|
|
12339
|
-
console.log("[opencode-browser] Plugin loading...", { pid: process.pid });
|
|
12340
13018
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
12341
13019
|
var __dirname2 = dirname(__filename2);
|
|
12342
|
-
var PACKAGE_JSON_PATH =
|
|
13020
|
+
var PACKAGE_JSON_PATH = join2(__dirname2, "..", "package.json");
|
|
12343
13021
|
var cachedVersion = null;
|
|
12344
13022
|
function getPackageVersion() {
|
|
12345
13023
|
if (cachedVersion)
|
|
12346
13024
|
return cachedVersion;
|
|
12347
13025
|
try {
|
|
12348
|
-
const pkg = JSON.parse(
|
|
13026
|
+
const pkg = JSON.parse(readFileSync2(PACKAGE_JSON_PATH, "utf8"));
|
|
12349
13027
|
if (typeof pkg?.version === "string") {
|
|
12350
13028
|
cachedVersion = pkg.version;
|
|
12351
13029
|
return cachedVersion;
|
|
@@ -12355,10 +13033,10 @@ function getPackageVersion() {
|
|
|
12355
13033
|
return cachedVersion;
|
|
12356
13034
|
}
|
|
12357
13035
|
var { schema } = tool;
|
|
12358
|
-
var BASE_DIR =
|
|
12359
|
-
var SOCKET_PATH =
|
|
13036
|
+
var BASE_DIR = join2(homedir(), ".opencode-browser");
|
|
13037
|
+
var SOCKET_PATH = join2(BASE_DIR, "broker.sock");
|
|
12360
13038
|
mkdirSync(BASE_DIR, { recursive: true });
|
|
12361
|
-
function
|
|
13039
|
+
function createJsonLineParser2(onMessage) {
|
|
12362
13040
|
let buffer = "";
|
|
12363
13041
|
return (chunk) => {
|
|
12364
13042
|
buffer += chunk.toString("utf8");
|
|
@@ -12377,33 +13055,36 @@ function createJsonLineParser(onMessage) {
|
|
|
12377
13055
|
}
|
|
12378
13056
|
};
|
|
12379
13057
|
}
|
|
12380
|
-
function
|
|
13058
|
+
function writeJsonLine2(socket, msg) {
|
|
12381
13059
|
socket.write(JSON.stringify(msg) + `
|
|
12382
13060
|
`);
|
|
12383
13061
|
}
|
|
12384
13062
|
function maybeStartBroker() {
|
|
12385
|
-
const brokerPath =
|
|
13063
|
+
const brokerPath = join2(BASE_DIR, "broker.cjs");
|
|
12386
13064
|
if (!existsSync(brokerPath))
|
|
12387
13065
|
return;
|
|
12388
13066
|
try {
|
|
12389
|
-
const child =
|
|
13067
|
+
const child = spawn2(process.execPath, [brokerPath], { detached: true, stdio: "ignore" });
|
|
12390
13068
|
child.unref();
|
|
12391
13069
|
} catch {}
|
|
12392
13070
|
}
|
|
12393
13071
|
async function connectToBroker() {
|
|
12394
13072
|
return await new Promise((resolve, reject) => {
|
|
12395
|
-
const socket =
|
|
13073
|
+
const socket = net2.createConnection(SOCKET_PATH);
|
|
12396
13074
|
socket.once("connect", () => resolve(socket));
|
|
12397
13075
|
socket.once("error", (err) => reject(err));
|
|
12398
13076
|
});
|
|
12399
13077
|
}
|
|
12400
|
-
async function
|
|
13078
|
+
async function sleep2(ms) {
|
|
12401
13079
|
return await new Promise((r) => setTimeout(r, ms));
|
|
12402
13080
|
}
|
|
13081
|
+
var BACKEND_MODE = (process.env.OPENCODE_BROWSER_BACKEND ?? process.env.OPENCODE_BROWSER_MODE ?? "extension").toLowerCase().trim();
|
|
13082
|
+
var USE_AGENT_BACKEND = ["agent", "agent-browser", "agentbrowser"].includes(BACKEND_MODE);
|
|
12403
13083
|
var socket = null;
|
|
12404
13084
|
var sessionId = Math.random().toString(36).slice(2);
|
|
12405
13085
|
var reqId = 0;
|
|
12406
13086
|
var pending = new Map;
|
|
13087
|
+
var agentBackend = USE_AGENT_BACKEND ? createAgentBackend(sessionId) : null;
|
|
12407
13088
|
async function ensureBrokerSocket() {
|
|
12408
13089
|
if (socket && !socket.destroyed)
|
|
12409
13090
|
return socket;
|
|
@@ -12412,7 +13093,7 @@ async function ensureBrokerSocket() {
|
|
|
12412
13093
|
} catch {
|
|
12413
13094
|
maybeStartBroker();
|
|
12414
13095
|
for (let i = 0;i < 20; i++) {
|
|
12415
|
-
await
|
|
13096
|
+
await sleep2(100);
|
|
12416
13097
|
try {
|
|
12417
13098
|
socket = await connectToBroker();
|
|
12418
13099
|
break;
|
|
@@ -12423,7 +13104,7 @@ async function ensureBrokerSocket() {
|
|
|
12423
13104
|
throw new Error("Could not connect to local broker. Run `npx @different-ai/opencode-browser install` and ensure the extension is loaded.");
|
|
12424
13105
|
}
|
|
12425
13106
|
socket.setNoDelay(true);
|
|
12426
|
-
socket.on("data",
|
|
13107
|
+
socket.on("data", createJsonLineParser2((msg) => {
|
|
12427
13108
|
if (msg?.type !== "response" || typeof msg.id !== "number")
|
|
12428
13109
|
return;
|
|
12429
13110
|
const p = pending.get(msg.id);
|
|
@@ -12442,7 +13123,7 @@ async function ensureBrokerSocket() {
|
|
|
12442
13123
|
socket.on("error", () => {
|
|
12443
13124
|
socket = null;
|
|
12444
13125
|
});
|
|
12445
|
-
|
|
13126
|
+
writeJsonLine2(socket, { type: "hello", role: "plugin", sessionId, pid: process.pid });
|
|
12446
13127
|
return socket;
|
|
12447
13128
|
}
|
|
12448
13129
|
async function brokerRequest(op, payload) {
|
|
@@ -12450,7 +13131,7 @@ async function brokerRequest(op, payload) {
|
|
|
12450
13131
|
const id = ++reqId;
|
|
12451
13132
|
return await new Promise((resolve, reject) => {
|
|
12452
13133
|
pending.set(id, { resolve, reject });
|
|
12453
|
-
|
|
13134
|
+
writeJsonLine2(s, { type: "request", id, op, ...payload });
|
|
12454
13135
|
setTimeout(() => {
|
|
12455
13136
|
if (!pending.has(id))
|
|
12456
13137
|
return;
|
|
@@ -12459,6 +13140,12 @@ async function brokerRequest(op, payload) {
|
|
|
12459
13140
|
}, 60000);
|
|
12460
13141
|
});
|
|
12461
13142
|
}
|
|
13143
|
+
async function brokerOnlyRequest(op, payload) {
|
|
13144
|
+
if (USE_AGENT_BACKEND) {
|
|
13145
|
+
throw new Error("Tab claims are not supported with agent-browser backend");
|
|
13146
|
+
}
|
|
13147
|
+
return await brokerRequest(op, payload);
|
|
13148
|
+
}
|
|
12462
13149
|
function toolResultText(data, fallback) {
|
|
12463
13150
|
if (typeof data?.content === "string")
|
|
12464
13151
|
return data.content;
|
|
@@ -12468,8 +13155,29 @@ function toolResultText(data, fallback) {
|
|
|
12468
13155
|
return JSON.stringify(data.content);
|
|
12469
13156
|
return fallback;
|
|
12470
13157
|
}
|
|
13158
|
+
async function toolRequest(toolName, args) {
|
|
13159
|
+
if (USE_AGENT_BACKEND) {
|
|
13160
|
+
if (!agentBackend) {
|
|
13161
|
+
throw new Error("Agent backend unavailable: configuration failed to initialize");
|
|
13162
|
+
}
|
|
13163
|
+
return await agentBackend.requestTool(toolName, args);
|
|
13164
|
+
}
|
|
13165
|
+
return await brokerRequest("tool", { tool: toolName, args });
|
|
13166
|
+
}
|
|
13167
|
+
async function statusRequest() {
|
|
13168
|
+
if (USE_AGENT_BACKEND) {
|
|
13169
|
+
if (!agentBackend) {
|
|
13170
|
+
return {
|
|
13171
|
+
backend: "agent-browser",
|
|
13172
|
+
connected: false,
|
|
13173
|
+
error: "Agent backend unavailable: configuration failed to initialize"
|
|
13174
|
+
};
|
|
13175
|
+
}
|
|
13176
|
+
return await agentBackend.status();
|
|
13177
|
+
}
|
|
13178
|
+
return await brokerRequest("status", {});
|
|
13179
|
+
}
|
|
12471
13180
|
var plugin = async (ctx) => {
|
|
12472
|
-
console.log("[opencode-browser] Plugin loading...", { pid: process.pid });
|
|
12473
13181
|
return {
|
|
12474
13182
|
tool: {
|
|
12475
13183
|
browser_debug: tool({
|
|
@@ -12481,6 +13189,10 @@ var plugin = async (ctx) => {
|
|
|
12481
13189
|
loaded: true,
|
|
12482
13190
|
sessionId,
|
|
12483
13191
|
pid: process.pid,
|
|
13192
|
+
backend: USE_AGENT_BACKEND ? "agent-browser" : "extension",
|
|
13193
|
+
agentSession: agentBackend?.session ?? null,
|
|
13194
|
+
agentConnection: agentBackend?.connection ?? null,
|
|
13195
|
+
agentBrowserVersion: agentBackend?.getVersion?.() ?? null,
|
|
12484
13196
|
pluginVersion: getPackageVersion(),
|
|
12485
13197
|
timestamp: new Date().toISOString()
|
|
12486
13198
|
});
|
|
@@ -12494,15 +13206,17 @@ var plugin = async (ctx) => {
|
|
|
12494
13206
|
name: "@different-ai/opencode-browser",
|
|
12495
13207
|
version: getPackageVersion(),
|
|
12496
13208
|
sessionId,
|
|
12497
|
-
pid: process.pid
|
|
13209
|
+
pid: process.pid,
|
|
13210
|
+
backend: USE_AGENT_BACKEND ? "agent-browser" : "extension",
|
|
13211
|
+
agentBrowserVersion: agentBackend?.getVersion?.() ?? null
|
|
12498
13212
|
});
|
|
12499
13213
|
}
|
|
12500
13214
|
}),
|
|
12501
13215
|
browser_status: tool({
|
|
12502
|
-
description: "Check
|
|
13216
|
+
description: "Check backend connection status and current tab claims.",
|
|
12503
13217
|
args: {},
|
|
12504
13218
|
async execute(args, ctx2) {
|
|
12505
|
-
const data = await
|
|
13219
|
+
const data = await statusRequest();
|
|
12506
13220
|
return JSON.stringify(data);
|
|
12507
13221
|
}
|
|
12508
13222
|
}),
|
|
@@ -12510,10 +13224,39 @@ var plugin = async (ctx) => {
|
|
|
12510
13224
|
description: "List all open browser tabs",
|
|
12511
13225
|
args: {},
|
|
12512
13226
|
async execute(args, ctx2) {
|
|
12513
|
-
const data = await
|
|
13227
|
+
const data = await toolRequest("get_tabs", {});
|
|
12514
13228
|
return toolResultText(data, "ok");
|
|
12515
13229
|
}
|
|
12516
13230
|
}),
|
|
13231
|
+
browser_list_claims: tool({
|
|
13232
|
+
description: "List tab ownership claims",
|
|
13233
|
+
args: {},
|
|
13234
|
+
async execute(args, ctx2) {
|
|
13235
|
+
const data = await brokerOnlyRequest("list_claims", {});
|
|
13236
|
+
return JSON.stringify(data);
|
|
13237
|
+
}
|
|
13238
|
+
}),
|
|
13239
|
+
browser_claim_tab: tool({
|
|
13240
|
+
description: "Claim a browser tab for this session",
|
|
13241
|
+
args: {
|
|
13242
|
+
tabId: schema.number(),
|
|
13243
|
+
force: schema.boolean().optional()
|
|
13244
|
+
},
|
|
13245
|
+
async execute({ tabId, force }, ctx2) {
|
|
13246
|
+
const data = await brokerOnlyRequest("claim_tab", { tabId, force });
|
|
13247
|
+
return JSON.stringify(data);
|
|
13248
|
+
}
|
|
13249
|
+
}),
|
|
13250
|
+
browser_release_tab: tool({
|
|
13251
|
+
description: "Release a claimed browser tab",
|
|
13252
|
+
args: {
|
|
13253
|
+
tabId: schema.number()
|
|
13254
|
+
},
|
|
13255
|
+
async execute({ tabId }, ctx2) {
|
|
13256
|
+
const data = await brokerOnlyRequest("release_tab", { tabId });
|
|
13257
|
+
return JSON.stringify(data);
|
|
13258
|
+
}
|
|
13259
|
+
}),
|
|
12517
13260
|
browser_open_tab: tool({
|
|
12518
13261
|
description: "Open a new browser tab",
|
|
12519
13262
|
args: {
|
|
@@ -12521,7 +13264,7 @@ var plugin = async (ctx) => {
|
|
|
12521
13264
|
active: schema.boolean().optional()
|
|
12522
13265
|
},
|
|
12523
13266
|
async execute({ url: url2, active }, ctx2) {
|
|
12524
|
-
const data = await
|
|
13267
|
+
const data = await toolRequest("open_tab", { url: url2, active });
|
|
12525
13268
|
return toolResultText(data, "Opened new tab");
|
|
12526
13269
|
}
|
|
12527
13270
|
}),
|
|
@@ -12532,7 +13275,7 @@ var plugin = async (ctx) => {
|
|
|
12532
13275
|
tabId: schema.number().optional()
|
|
12533
13276
|
},
|
|
12534
13277
|
async execute({ url: url2, tabId }, ctx2) {
|
|
12535
|
-
const data = await
|
|
13278
|
+
const data = await toolRequest("navigate", { url: url2, tabId });
|
|
12536
13279
|
return toolResultText(data, `Navigated to ${url2}`);
|
|
12537
13280
|
}
|
|
12538
13281
|
}),
|
|
@@ -12541,10 +13284,12 @@ var plugin = async (ctx) => {
|
|
|
12541
13284
|
args: {
|
|
12542
13285
|
selector: schema.string(),
|
|
12543
13286
|
index: schema.number().optional(),
|
|
12544
|
-
tabId: schema.number().optional()
|
|
13287
|
+
tabId: schema.number().optional(),
|
|
13288
|
+
timeoutMs: schema.number().optional(),
|
|
13289
|
+
pollMs: schema.number().optional()
|
|
12545
13290
|
},
|
|
12546
|
-
async execute({ selector, index, tabId }, ctx2) {
|
|
12547
|
-
const data = await
|
|
13291
|
+
async execute({ selector, index, tabId, timeoutMs, pollMs }, ctx2) {
|
|
13292
|
+
const data = await toolRequest("click", { selector, index, tabId, timeoutMs, pollMs });
|
|
12548
13293
|
return toolResultText(data, `Clicked ${selector}`);
|
|
12549
13294
|
}
|
|
12550
13295
|
}),
|
|
@@ -12555,10 +13300,12 @@ var plugin = async (ctx) => {
|
|
|
12555
13300
|
text: schema.string(),
|
|
12556
13301
|
clear: schema.boolean().optional(),
|
|
12557
13302
|
index: schema.number().optional(),
|
|
12558
|
-
tabId: schema.number().optional()
|
|
13303
|
+
tabId: schema.number().optional(),
|
|
13304
|
+
timeoutMs: schema.number().optional(),
|
|
13305
|
+
pollMs: schema.number().optional()
|
|
12559
13306
|
},
|
|
12560
|
-
async execute({ selector, text, clear, index, tabId }, ctx2) {
|
|
12561
|
-
const data = await
|
|
13307
|
+
async execute({ selector, text, clear, index, tabId, timeoutMs, pollMs }, ctx2) {
|
|
13308
|
+
const data = await toolRequest("type", { selector, text, clear, index, tabId, timeoutMs, pollMs });
|
|
12562
13309
|
return toolResultText(data, `Typed "${text}" into ${selector}`);
|
|
12563
13310
|
}
|
|
12564
13311
|
}),
|
|
@@ -12570,13 +13317,12 @@ var plugin = async (ctx) => {
|
|
|
12570
13317
|
label: schema.string().optional(),
|
|
12571
13318
|
optionIndex: schema.number().optional(),
|
|
12572
13319
|
index: schema.number().optional(),
|
|
12573
|
-
tabId: schema.number().optional()
|
|
13320
|
+
tabId: schema.number().optional(),
|
|
13321
|
+
timeoutMs: schema.number().optional(),
|
|
13322
|
+
pollMs: schema.number().optional()
|
|
12574
13323
|
},
|
|
12575
|
-
async execute({ selector, value, label, optionIndex, index, tabId }, ctx2) {
|
|
12576
|
-
const data = await
|
|
12577
|
-
tool: "select",
|
|
12578
|
-
args: { selector, value, label, optionIndex, index, tabId }
|
|
12579
|
-
});
|
|
13324
|
+
async execute({ selector, value, label, optionIndex, index, tabId, timeoutMs, pollMs }, ctx2) {
|
|
13325
|
+
const data = await toolRequest("select", { selector, value, label, optionIndex, index, tabId, timeoutMs, pollMs });
|
|
12580
13326
|
const summary = value ?? label ?? (optionIndex != null ? String(optionIndex) : "option");
|
|
12581
13327
|
return toolResultText(data, `Selected ${summary} in ${selector}`);
|
|
12582
13328
|
}
|
|
@@ -12587,7 +13333,7 @@ var plugin = async (ctx) => {
|
|
|
12587
13333
|
tabId: schema.number().optional()
|
|
12588
13334
|
},
|
|
12589
13335
|
async execute({ tabId }, ctx2) {
|
|
12590
|
-
const data = await
|
|
13336
|
+
const data = await toolRequest("screenshot", { tabId });
|
|
12591
13337
|
return toolResultText(data, "Screenshot failed");
|
|
12592
13338
|
}
|
|
12593
13339
|
}),
|
|
@@ -12597,7 +13343,7 @@ var plugin = async (ctx) => {
|
|
|
12597
13343
|
tabId: schema.number().optional()
|
|
12598
13344
|
},
|
|
12599
13345
|
async execute({ tabId }, ctx2) {
|
|
12600
|
-
const data = await
|
|
13346
|
+
const data = await toolRequest("snapshot", { tabId });
|
|
12601
13347
|
return toolResultText(data, "Snapshot failed");
|
|
12602
13348
|
}
|
|
12603
13349
|
}),
|
|
@@ -12607,10 +13353,12 @@ var plugin = async (ctx) => {
|
|
|
12607
13353
|
selector: schema.string().optional(),
|
|
12608
13354
|
x: schema.number().optional(),
|
|
12609
13355
|
y: schema.number().optional(),
|
|
12610
|
-
tabId: schema.number().optional()
|
|
13356
|
+
tabId: schema.number().optional(),
|
|
13357
|
+
timeoutMs: schema.number().optional(),
|
|
13358
|
+
pollMs: schema.number().optional()
|
|
12611
13359
|
},
|
|
12612
|
-
async execute({ selector, x, y, tabId }, ctx2) {
|
|
12613
|
-
const data = await
|
|
13360
|
+
async execute({ selector, x, y, tabId, timeoutMs, pollMs }, ctx2) {
|
|
13361
|
+
const data = await toolRequest("scroll", { selector, x, y, tabId, timeoutMs, pollMs });
|
|
12614
13362
|
return toolResultText(data, "Scrolled");
|
|
12615
13363
|
}
|
|
12616
13364
|
}),
|
|
@@ -12621,7 +13369,7 @@ var plugin = async (ctx) => {
|
|
|
12621
13369
|
tabId: schema.number().optional()
|
|
12622
13370
|
},
|
|
12623
13371
|
async execute({ ms, tabId }, ctx2) {
|
|
12624
|
-
const data = await
|
|
13372
|
+
const data = await toolRequest("wait", { ms, tabId });
|
|
12625
13373
|
return toolResultText(data, "Waited");
|
|
12626
13374
|
}
|
|
12627
13375
|
}),
|
|
@@ -12641,9 +13389,18 @@ var plugin = async (ctx) => {
|
|
|
12641
13389
|
tabId: schema.number().optional()
|
|
12642
13390
|
},
|
|
12643
13391
|
async execute({ selector, mode, attribute, property, index, limit, timeoutMs, pollMs, pattern, flags, tabId }, ctx2) {
|
|
12644
|
-
const data = await
|
|
12645
|
-
|
|
12646
|
-
|
|
13392
|
+
const data = await toolRequest("query", {
|
|
13393
|
+
selector,
|
|
13394
|
+
mode,
|
|
13395
|
+
attribute,
|
|
13396
|
+
property,
|
|
13397
|
+
index,
|
|
13398
|
+
limit,
|
|
13399
|
+
timeoutMs,
|
|
13400
|
+
pollMs,
|
|
13401
|
+
pattern,
|
|
13402
|
+
flags,
|
|
13403
|
+
tabId
|
|
12647
13404
|
});
|
|
12648
13405
|
return toolResultText(data, "Query failed");
|
|
12649
13406
|
}
|