aboutsummaryrefslogtreecommitdiff
path: root/src/tools.ts
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmal.com>2026-04-30 17:52:26 +0530
committeramankumarsingh77 <amankumarsingh7702@gmal.com>2026-04-30 17:52:26 +0530
commit28578d1d6237cdec3f613a3ed9fa6e888dd71a6b (patch)
treec7f69ac2fb286a4551c5f7ecc0e67225b674f945 /src/tools.ts
parentb6d752222947229112132ac0fbd87f355fc78974 (diff)
Replace dynamic tool registration with browser_run_script
Remove register_tool/remove_tool/list_dynamic_tools and the entire dynamic-tools.ts module. Replace with a single browser_run_script tool that executes temporary scripts from disk with daemon access. Scripts are now written to disk before execution, making them auditable and re-runnable. Same bindings (daemon, require, params, etc.) as before via the same AsyncFunction mechanism.
Diffstat (limited to 'src/tools.ts')
-rw-r--r--src/tools.ts132
1 files changed, 131 insertions, 1 deletions
diff --git a/src/tools.ts b/src/tools.ts
index 6b3bb8d..822854c 100644
--- a/src/tools.ts
+++ b/src/tools.ts
@@ -15,12 +15,16 @@ import {
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Text } from "@mariozechner/pi-tui";
import { Type } from "typebox";
-import { mkdtemp, rm, writeFile } from "node:fs/promises";
+import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { BrowserDaemon } from "./daemon";
import type { TabInfo } from "./protocol";
+// ── AsyncFunction constructor ────────────────────────────────────────────────
+
+const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as new (...args: string[]) => (...args: unknown[]) => Promise<unknown>;
+
// ── Key code map ─────────────────────────────────────────────────────────────
const SPECIAL_KEYS = [
@@ -1225,4 +1229,130 @@ export function registerTools(pi: ExtensionAPI, daemon: BrowserDaemon): void {
);
},
});
+
+ // ═══════════════════════════════════════════════════════════════════════════
+ // browser_run_script
+ // ═══════════════════════════════════════════════════════════════════════════
+ pi.registerTool({
+ name: "browser_run_script",
+ label: "Browser Run Script",
+ description:
+ "Execute a temporary JavaScript script file with access to the browser daemon " +
+ "and Node.js APIs. Use this when the built-in browser_* tools are insufficient " +
+ "for a multi-step workflow — write a script to disk, then run it with this tool. " +
+ "The script receives these bindings in scope:\n" +
+ " params — the arguments passed to this tool\n" +
+ " daemon — the browser daemon (daemon.cdp(), daemon.evaluateJS(), daemon.getPageInfo(), etc.)\n" +
+ " require — Node.js require() for builtins and installed packages\n" +
+ " signal — AbortSignal for cancellation\n" +
+ " onUpdate — progress callback: onUpdate({ content: [{ type: 'text', text: '...' }] })\n" +
+ " ctx — ExtensionContext with cwd, sessionManager, ui, signal, etc.\n" +
+ " console, fetch, JSON, Buffer, setTimeout, clearTimeout\n\n" +
+ "The script MUST return { content: [{ type: 'text', text: '...' }], details?: {...} }.\n" +
+ "For errors, throw: throw new Error('something went wrong').",
+ promptSnippet: "Run a temporary script with browser daemon access (write script to disk first)",
+ promptGuidelines: [
+ "Use write to create a temporary script file, then browser_run_script to execute it — no dynamic tool registration needed.",
+ "Scripts are written to disk, making them auditable and re-runnable. The user can inspect them.",
+ "The script has access to the browser daemon via `daemon` — use daemon.cdp(), daemon.evaluateJS(), daemon.getPageInfo(), etc.",
+ "For pure data processing or file operations, use require() to access Node.js modules like fs, path, crypto.",
+ "If the script only needs built-in browser_* tools, break those into separate tool calls before or after — do not try to invoke them from within the script.",
+ ],
+ parameters: Type.Object({
+ path: Type.String({ description: "Path to the temporary script file (.js or .mjs) to execute" }),
+ params: Type.Optional(Type.Object({}, { additionalProperties: true, description: "Optional parameters to pass to the script as `params`" })),
+ }),
+ async execute(_id, p, signal, onUpdate, ctx) {
+ const { path: scriptPath, params: scriptParams } = p as {
+ path: string;
+ params?: Record<string, unknown>;
+ };
+
+ // ── Read the script from disk ────────────────────────────────────
+ let source: string;
+ try {
+ source = await readFile(scriptPath, "utf8");
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : String(err);
+ return {
+ isError: true,
+ content: [{
+ type: "text" as const,
+ text: `Failed to read script: ${msg}\n\nPath: ${scriptPath}\nCheck that the file exists and is readable.`,
+ }],
+ details: { error: "read_failed", message: msg },
+ };
+ }
+
+ if (source.trim().length === 0) {
+ return {
+ isError: true,
+ content: [{ type: "text" as const, text: `Script is empty: ${scriptPath}` }],
+ details: { error: "empty_script" },
+ };
+ }
+
+ // ── Compile ─────────────────────────────────────────────────────
+ let executeFn: (...args: unknown[]) => Promise<unknown>;
+ try {
+ const wrapped = `"use strict";\n${source}`;
+ executeFn = new AsyncFunction(
+ "params", "daemon", "require", "signal", "onUpdate", "ctx",
+ "console", "fetch", "JSON", "Buffer", "setTimeout", "clearTimeout",
+ wrapped,
+ );
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : String(err);
+ return {
+ isError: true,
+ content: [{
+ type: "text" as const,
+ text: `Syntax error in script: ${msg}\n\nFile: ${scriptPath}\nFix the JavaScript syntax and try again.`,
+ }],
+ details: { error: "syntax_error", message: msg },
+ };
+ }
+
+ // ── Execute ─────────────────────────────────────────────────────
+ try {
+ const result = await executeFn(
+ scriptParams ?? {},
+ daemon,
+ require,
+ signal,
+ onUpdate ?? ((_update: unknown) => {}),
+ ctx ?? { cwd: process.cwd() },
+ console,
+ fetch,
+ JSON,
+ Buffer,
+ setTimeout,
+ clearTimeout,
+ );
+
+ if (!result || typeof result !== "object" || !Array.isArray((result as any).content)) {
+ return {
+ isError: true,
+ content: [{
+ type: "text" as const,
+ text: `Script must return { content: [{ type: 'text', text: '...' }], details?: {...} }.\nGot: ${JSON.stringify(result)}\n\nFile: ${scriptPath}`,
+ }],
+ details: { error: "invalid_return" },
+ };
+ }
+
+ return result as { content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> | undefined };
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : String(err);
+ return {
+ isError: true,
+ content: [{
+ type: "text" as const,
+ text: `Script execution failed: ${msg}\n\nFile: ${scriptPath}\nCheck for runtime errors, undefined variables, or broken require() calls.`,
+ }],
+ details: { error: "execution_failed", message: msg },
+ };
+ }
+ },
+ });
}