76 lines
2.8 KiB
TypeScript
76 lines
2.8 KiB
TypeScript
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
import { Type } from "typebox";
|
|
import { execFileSync } from "child_process";
|
|
|
|
const CDP_EVAL_SCRIPT = "/opt/open-computer/interface-service/utils/cdp-eval.js";
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
pi.registerTool({
|
|
name: "browser_cdp",
|
|
label: "Browser CDP",
|
|
description:
|
|
"Execute JavaScript in the browser page via Chrome DevTools Protocol. " +
|
|
"The code runs in the ACTUAL browser page context (like the Chrome console), so " +
|
|
"document, window, fetch, etc. are all available. " +
|
|
"The browser MUST already be open (use open_browser first and wait for it to complete). " +
|
|
"Returns the result of the last expression. " +
|
|
"Examples: " +
|
|
" document.title | " +
|
|
" document.body.innerText.slice(0, 3000) | " +
|
|
" JSON.stringify([...document.querySelectorAll('a')].map(a=>({text:a.innerText,href:a.href}))) | " +
|
|
" document.querySelector('#search').value = 'test'",
|
|
parameters: Type.Object({
|
|
code: Type.String({
|
|
description:
|
|
"JavaScript to execute in the browser page context. " +
|
|
"Use document, window, fetch, etc. as if typing in Chrome DevTools console. " +
|
|
"The result of the last expression is returned.",
|
|
}),
|
|
target_url: Type.Optional(
|
|
Type.String({
|
|
description:
|
|
"Optional: match a specific tab by URL substring (e.g. 'ycombinator'). " +
|
|
"If omitted, uses the most recently opened page tab.",
|
|
})
|
|
),
|
|
}),
|
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
try {
|
|
const args = [CDP_EVAL_SCRIPT, params.code];
|
|
if (params.target_url) args.push(params.target_url);
|
|
|
|
const result = execFileSync("node", args, {
|
|
encoding: "utf8",
|
|
timeout: 30000,
|
|
maxBuffer: 1024 * 1024,
|
|
env: { ...process.env, NODE_PATH: "/opt/open-computer/node_modules" },
|
|
});
|
|
|
|
const text = result.length > 10000
|
|
? result.slice(0, 10000) + "\n... (truncated)"
|
|
: result;
|
|
|
|
return {
|
|
content: [{ type: "text", text: text || "(no return value)" }],
|
|
details: {},
|
|
};
|
|
} catch (err: unknown) {
|
|
const error = err as { stderr?: string; message?: string };
|
|
const msg = error.stderr?.trim() || error.message || "Unknown error";
|
|
|
|
if (msg.includes("ECONNREFUSED") || msg.includes("No browser pages")) {
|
|
return {
|
|
content: [{ type: "text", text: `Error: ${msg}\nMake sure the browser is open (use open_browser first).` }],
|
|
details: {},
|
|
};
|
|
}
|
|
|
|
return {
|
|
content: [{ type: "text", text: msg }],
|
|
details: {},
|
|
};
|
|
}
|
|
},
|
|
});
|
|
}
|