@pasko70/pibo 1.13.0 → 1.15.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/apps/chat/static-assets.js +3 -1
- package/dist/apps/chat/web-app.js +10 -0
- package/dist/apps/{context-files-ui/assets/dist-BFRDpzcv.js → chat-ui/assets/dist-BFwSuVfD.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-XmG4qG-A.js → dist-BYTi90un.js} +1 -1
- package/dist/apps/{context-files-ui/assets/dist-B_zXPNMb.js → chat-ui/assets/dist-Bi74WdBU.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-UYBI2F8E.js → dist-CYMM1_bS.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DVPXynJE.js → dist-CuIwfDgF.js} +1 -1
- package/dist/apps/{context-files-ui/assets/dist-B5mpygOZ.js → chat-ui/assets/dist-DS1ScAMf.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-B_8EcoB6.js → dist-DUgrMhHX.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CVLKu7NV.js → dist-Dq3i1LVZ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DugRsLaG.js → dist-Dy5L1xOD.js} +1 -1
- package/dist/apps/{context-files-ui/assets/dist-D6pYe6d_.js → chat-ui/assets/dist-Zy3KnDp7.js} +1 -1
- package/dist/apps/chat-ui/assets/index-BZO4zpMi.js +237 -0
- package/dist/apps/chat-ui/assets/index-CoCl8FG8.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-V_pKPVAm.js +41 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/{chat-ui/assets/dist-CnX234jg.js → context-files-ui/assets/dist-B7gXtTZC.js} +1 -1
- package/dist/apps/{chat-ui/assets/dist-CELRUR8b.js → context-files-ui/assets/dist-BD262Ouw.js} +1 -1
- package/dist/apps/context-files-ui/assets/{dist-Em3LWmNi.js → dist-BKuGdcnX.js} +1 -1
- package/dist/apps/{chat-ui/assets/dist-CVmpu9FK.js → context-files-ui/assets/dist-C-Jm45P4.js} +1 -1
- package/dist/apps/context-files-ui/assets/{dist-DM6yvMZv.js → dist-CHdx1Jri.js} +1 -1
- package/dist/apps/context-files-ui/assets/{dist-BLI6sQZG.js → dist-CULMH-fB.js} +1 -1
- package/dist/apps/context-files-ui/assets/{dist-CLrlJjPP.js → dist-Cjq6RWEt.js} +1 -1
- package/dist/apps/context-files-ui/assets/{dist-D8ppb7gk.js → dist-CntqFySz.js} +1 -1
- package/dist/apps/context-files-ui/assets/{dist-SEP88Pg7.js → dist-DxEGebCR.js} +1 -1
- package/dist/apps/{chat-ui/assets/dist-KOXty1JB.js → context-files-ui/assets/dist-kPuQud7t.js} +1 -1
- package/dist/apps/context-files-ui/assets/index-DZWsbuvg.js +202 -0
- package/dist/apps/context-files-ui/index.html +1 -1
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.15.0.vsix +0 -0
- package/dist/cli-session/localSessionSource.js +2 -0
- package/dist/core/runtime.js +27 -5
- package/dist/core/session-router.js +3 -0
- package/dist/gateway/client.js +8 -0
- package/dist/gateway/server.js +18 -2
- package/dist/gateway/web.js +1 -0
- package/dist/local/extension.js +2 -0
- package/dist/loops/plugin.js +34 -1
- package/dist/loops/service.js +85 -0
- package/dist/mcp/config-command.js +5 -3
- package/dist/plugins/builtin.js +7 -0
- package/dist/plugins/context-files-store.js +287 -101
- package/dist/plugins/context-files.js +102 -154
- package/dist/session-ui/terminalRows.js +12 -2
- package/dist/shared/trace-live-reducer.js +6 -1
- package/dist/tools/browser-use-leases.js +16 -0
- package/dist/tools/codex-browser-node-repl.js +224 -0
- package/dist/tools/codex-browser-node-worker-source.js +403 -0
- package/dist/tools/codex-browser.js +461 -0
- package/dist/web/channel.js +105 -16
- package/dist/web/http.js +14 -2
- package/package.json +13 -7
- package/dist/apps/chat-ui/assets/index-DhFQ1th-.js +0 -237
- package/dist/apps/chat-ui/assets/index-hjygk9Ol.css +0 -1
- package/dist/apps/chat-vscode-web/assets/index-1wWonouX.js +0 -41
- package/dist/apps/context-files-ui/assets/index-C7X2UvWe.js +0 -202
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.13.0.vsix +0 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { StringEnum, Type } from "@earendil-works/pi-ai";
|
|
4
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { connectCdpTarget, listCdpTargets } from "./cdp-client.js";
|
|
6
|
+
import { browserPoolPaths, releaseBrowserPoolLease } from "./browser-pool.js";
|
|
7
|
+
import { browserUseLeaseEnvironment, findActiveBrowserUseLeaseForHolder } from "./browser-use-leases.js";
|
|
8
|
+
import { ensureBrowserUseWrapper } from "./browser-use-wrapper.js";
|
|
9
|
+
import { findCliToolEntry, getCliToolStatus } from "./registry.js";
|
|
10
|
+
import { CodexBrowserNodeRepl, } from "./codex-browser-node-repl.js";
|
|
11
|
+
export const BROWSER_USE_OPEN_TABS_TOOL_NAME = "browser_use_open_tabs";
|
|
12
|
+
export const BROWSER_USE_TAKE_SCREENSHOT_TOOL_NAME = "browser_use_take_screenshot";
|
|
13
|
+
export const BROWSER_USE_TOOL_NAME = "browser_use_browser_use";
|
|
14
|
+
export const NODE_REPL_JS_TOOL_NAME = "node_repl_js";
|
|
15
|
+
export const NODE_REPL_JS_RESET_TOOL_NAME = "node_repl_js_reset";
|
|
16
|
+
export const CODEX_BROWSER_TOOL_NAMES = [
|
|
17
|
+
BROWSER_USE_OPEN_TABS_TOOL_NAME,
|
|
18
|
+
BROWSER_USE_TAKE_SCREENSHOT_TOOL_NAME,
|
|
19
|
+
BROWSER_USE_TOOL_NAME,
|
|
20
|
+
NODE_REPL_JS_TOOL_NAME,
|
|
21
|
+
NODE_REPL_JS_RESET_TOOL_NAME,
|
|
22
|
+
];
|
|
23
|
+
export class CodexBrowserSessionController {
|
|
24
|
+
options;
|
|
25
|
+
sessionName;
|
|
26
|
+
browserPoolLeaseId;
|
|
27
|
+
authLeaseId;
|
|
28
|
+
status;
|
|
29
|
+
wrapperPath;
|
|
30
|
+
env;
|
|
31
|
+
runCommand;
|
|
32
|
+
cdpUrl;
|
|
33
|
+
nodeRepl;
|
|
34
|
+
browserLeaseUsed = false;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.options = options;
|
|
37
|
+
const entry = findCliToolEntry("browser-use");
|
|
38
|
+
if (!entry)
|
|
39
|
+
throw new Error("Pibo browser-use registry entry is unavailable");
|
|
40
|
+
this.status = options.status ?? getCliToolStatus(entry);
|
|
41
|
+
this.wrapperPath = this.status.installed
|
|
42
|
+
? ensureBrowserUseWrapper(this.status) ?? this.status.executablePath
|
|
43
|
+
: this.status.executablePath;
|
|
44
|
+
const baseEnv = options.env ?? process.env;
|
|
45
|
+
const explicitAuthLease = baseEnv.PIBO_BROWSER_USE_LEASE_ID?.trim();
|
|
46
|
+
const sessionId = options.piboSessionId?.trim() || "local";
|
|
47
|
+
const registeredLease = explicitAuthLease ? undefined : findRegisteredLease(this.status, sessionId);
|
|
48
|
+
const registeredLeaseEnv = registeredLease ? browserUseLeaseEnvironment(this.status, registeredLease) : undefined;
|
|
49
|
+
this.sessionName = explicitAuthLease && baseEnv.PIBO_BROWSER_USE_SESSION
|
|
50
|
+
? baseEnv.PIBO_BROWSER_USE_SESSION
|
|
51
|
+
: registeredLease?.sessionName ?? `pibo-${safeSessionName(sessionId)}`;
|
|
52
|
+
this.browserPoolLeaseId = explicitAuthLease && baseEnv.PIBO_BROWSER_POOL_LEASE_ID
|
|
53
|
+
? baseEnv.PIBO_BROWSER_POOL_LEASE_ID
|
|
54
|
+
: registeredLease?.browserPoolLeaseId ?? `browser-use:${this.sessionName}`;
|
|
55
|
+
this.authLeaseId = explicitAuthLease || registeredLease?.id;
|
|
56
|
+
this.env = {
|
|
57
|
+
...baseEnv,
|
|
58
|
+
...(registeredLeaseEnv ?? {}),
|
|
59
|
+
BROWSER_USE_HOME: this.status.homeDir,
|
|
60
|
+
PIBO_BROWSER_USE_SESSION: this.sessionName,
|
|
61
|
+
PIBO_BROWSER_POOL_LEASE_ID: this.browserPoolLeaseId,
|
|
62
|
+
PIBO_BROWSER_POOL_HOLDER: `pibo-session:${sessionId}`,
|
|
63
|
+
};
|
|
64
|
+
if (!this.authLeaseId) {
|
|
65
|
+
delete this.env.PIBO_BROWSER_USE_LEASE_ID;
|
|
66
|
+
delete this.env.PIBO_BROWSER_USE_CHROME_USER_DATA_DIR;
|
|
67
|
+
}
|
|
68
|
+
this.runCommand = options.runCommand ?? runBrowserUseCommand;
|
|
69
|
+
}
|
|
70
|
+
async openTabs() {
|
|
71
|
+
const cdpUrl = await this.ensureChrome();
|
|
72
|
+
const targets = await listCdpTargets({ cdpUrl });
|
|
73
|
+
return pageTargets(targets).map(toBrowserUseTab);
|
|
74
|
+
}
|
|
75
|
+
async takeScreenshot(input = {}) {
|
|
76
|
+
const timeoutMs = input.timeoutMs ?? 15_000;
|
|
77
|
+
const cdpUrl = await this.ensureChrome(timeoutMs);
|
|
78
|
+
const targets = pageTargets(await listCdpTargets({ cdpUrl, timeoutMs }));
|
|
79
|
+
const target = selectTarget(targets, input.tabId);
|
|
80
|
+
if (!target)
|
|
81
|
+
throw new Error(input.tabId ? `Browser tab "${input.tabId}" was not found` : "No open browser tab was found");
|
|
82
|
+
const client = await connectCdpTarget(target, Math.min(timeoutMs, 5_000));
|
|
83
|
+
try {
|
|
84
|
+
const data = await captureScreenshot(client, input.fullPage === true, timeoutMs);
|
|
85
|
+
return {
|
|
86
|
+
status: "ok",
|
|
87
|
+
sessionName: this.sessionName,
|
|
88
|
+
browserPoolLeaseId: this.browserPoolLeaseId,
|
|
89
|
+
...(this.authLeaseId ? { authLeaseId: this.authLeaseId } : {}),
|
|
90
|
+
tab: toBrowserUseTab(target, targets.indexOf(target)),
|
|
91
|
+
fullPage: input.fullPage === true,
|
|
92
|
+
mimeType: "image/png",
|
|
93
|
+
data,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
client.close();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async use(input, signal) {
|
|
101
|
+
const command = browserUseCommand(input);
|
|
102
|
+
const response = await this.execute(command, input.timeoutMs ?? 30_000, signal);
|
|
103
|
+
if (response.exitCode !== 0) {
|
|
104
|
+
throw new Error(response.stderr.trim() || response.stdout.trim() || `browser-use exited with code ${response.exitCode ?? "unknown"}`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
status: "ok",
|
|
108
|
+
sessionName: this.sessionName,
|
|
109
|
+
browserPoolLeaseId: this.browserPoolLeaseId,
|
|
110
|
+
...(this.authLeaseId ? { authLeaseId: this.authLeaseId } : {}),
|
|
111
|
+
command,
|
|
112
|
+
output: parseJsonOrText(response.stdout),
|
|
113
|
+
...(response.stderr.trim() ? { stderr: response.stderr.trim() } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async js(code, timeoutMs = 30_000) {
|
|
117
|
+
const repl = await this.getNodeRepl();
|
|
118
|
+
return repl.exec(code, timeoutMs);
|
|
119
|
+
}
|
|
120
|
+
async jsReset() {
|
|
121
|
+
if (!this.nodeRepl?.isAlive())
|
|
122
|
+
return { status: "ok", reset: true };
|
|
123
|
+
return this.nodeRepl.reset();
|
|
124
|
+
}
|
|
125
|
+
async dispose() {
|
|
126
|
+
this.nodeRepl?.dispose();
|
|
127
|
+
this.nodeRepl = undefined;
|
|
128
|
+
if (!this.browserLeaseUsed)
|
|
129
|
+
return;
|
|
130
|
+
const identity = {
|
|
131
|
+
workerId: this.env.PIBO_BROWSER_POOL_WORKER_ID || this.env.PIBO_COMPUTE_WORKER_ID || this.env.HOSTNAME || "local",
|
|
132
|
+
poolId: this.env.PIBO_BROWSER_POOL_ID || "default",
|
|
133
|
+
maxBrowserProcesses: positiveInteger(this.env.PIBO_BROWSER_POOL_MAX_PROCESSES, 1),
|
|
134
|
+
};
|
|
135
|
+
const rootDir = this.env.PIBO_BROWSER_POOL_ROOT || join(this.status.homeDir, "pibo-browser-pool");
|
|
136
|
+
try {
|
|
137
|
+
await releaseBrowserPoolLease(browserPoolPaths(rootDir, identity), identity, { leaseId: this.browserPoolLeaseId });
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Browser pool cleanup is best-effort during Pi session disposal.
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async getNodeRepl() {
|
|
144
|
+
if (this.nodeRepl?.isAlive())
|
|
145
|
+
return this.nodeRepl;
|
|
146
|
+
this.nodeRepl = await CodexBrowserNodeRepl.start({
|
|
147
|
+
openTabs: () => this.openTabs(),
|
|
148
|
+
use: (input) => this.use(requireBrowserUseInput(input)),
|
|
149
|
+
}, this.options.cwd);
|
|
150
|
+
return this.nodeRepl;
|
|
151
|
+
}
|
|
152
|
+
async ensureChrome(timeoutMs = 15_000) {
|
|
153
|
+
if (this.cdpUrl) {
|
|
154
|
+
try {
|
|
155
|
+
await listCdpTargets({ cdpUrl: this.cdpUrl, timeoutMs: Math.min(timeoutMs, 1_000) });
|
|
156
|
+
return this.cdpUrl;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
this.cdpUrl = undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const response = await this.execute(["--pibo-ensure-chrome"], timeoutMs);
|
|
163
|
+
if (response.exitCode !== 0) {
|
|
164
|
+
throw new Error(response.stderr.trim() || response.stdout.trim() || "browser-use could not start Chrome");
|
|
165
|
+
}
|
|
166
|
+
const cdpUrl = response.stdout.split(/\r?\n/).map((line) => line.trim()).findLast((line) => /^https?:\/\//.test(line));
|
|
167
|
+
if (!cdpUrl)
|
|
168
|
+
throw new Error(`browser-use did not return a CDP URL: ${response.stdout.trim()}`);
|
|
169
|
+
this.cdpUrl = cdpUrl.replace(/\/+$/, "");
|
|
170
|
+
return this.cdpUrl;
|
|
171
|
+
}
|
|
172
|
+
execute(command, timeoutMs, signal) {
|
|
173
|
+
if (!this.status.installed) {
|
|
174
|
+
throw new Error("browser-use is not installed. Run `pibo tools install browser-use`.");
|
|
175
|
+
}
|
|
176
|
+
const args = ["--session", this.sessionName, ...(command[0]?.startsWith("--") ? [] : ["--json"]), ...command];
|
|
177
|
+
this.browserLeaseUsed = true;
|
|
178
|
+
return this.runCommand({
|
|
179
|
+
executable: this.wrapperPath,
|
|
180
|
+
args,
|
|
181
|
+
cwd: this.options.cwd,
|
|
182
|
+
env: this.env,
|
|
183
|
+
timeoutMs,
|
|
184
|
+
signal,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export function createCodexBrowserToolProfiles() {
|
|
189
|
+
const descriptions = {
|
|
190
|
+
[BROWSER_USE_OPEN_TABS_TOOL_NAME]: "List tabs in the persistent Browser Use session (Codex-compatible label: browser_use.open_tabs).",
|
|
191
|
+
[BROWSER_USE_TAKE_SCREENSHOT_TOOL_NAME]: "Capture a browser tab screenshot (Codex-compatible label: browser_use.take_screenshot).",
|
|
192
|
+
[BROWSER_USE_TOOL_NAME]: "Control the persistent Browser Use session through structured actions (Codex-compatible label: browser_use.browser_use).",
|
|
193
|
+
[NODE_REPL_JS_TOOL_NAME]: "Run JavaScript in a persistent browser-bound Node REPL (Codex-compatible label: node_repl.js).",
|
|
194
|
+
[NODE_REPL_JS_RESET_TOOL_NAME]: "Reset the persistent browser-bound Node REPL (Codex-compatible label: node_repl.js_reset).",
|
|
195
|
+
};
|
|
196
|
+
return CODEX_BROWSER_TOOL_NAMES.map((name) => ({
|
|
197
|
+
name,
|
|
198
|
+
description: descriptions[name],
|
|
199
|
+
yieldable: name === BROWSER_USE_TOOL_NAME,
|
|
200
|
+
builtInPiboTool: "codex_browser",
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
export function createCodexBrowserToolDefinitions(controller, enabledNames = CODEX_BROWSER_TOOL_NAMES) {
|
|
204
|
+
const enabled = new Set(enabledNames);
|
|
205
|
+
const tools = [
|
|
206
|
+
[BROWSER_USE_OPEN_TABS_TOOL_NAME, defineTool({
|
|
207
|
+
name: BROWSER_USE_OPEN_TABS_TOOL_NAME,
|
|
208
|
+
label: "browser_use.open_tabs",
|
|
209
|
+
description: "List open tabs in the persistent Browser Use session. Returns stable target IDs, short tab IDs, titles, URLs, and tab indices.",
|
|
210
|
+
promptSnippet: "Use browser_use_open_tabs (browser_use.open_tabs) to discover tabs before targeting a screenshot or tab-specific action.",
|
|
211
|
+
executionMode: "parallel",
|
|
212
|
+
parameters: Type.Object({}),
|
|
213
|
+
async execute() {
|
|
214
|
+
return toolResult(await controller.openTabs());
|
|
215
|
+
},
|
|
216
|
+
})],
|
|
217
|
+
[BROWSER_USE_TAKE_SCREENSHOT_TOOL_NAME, defineTool({
|
|
218
|
+
name: BROWSER_USE_TAKE_SCREENSHOT_TOOL_NAME,
|
|
219
|
+
label: "browser_use.take_screenshot",
|
|
220
|
+
description: "Take a PNG screenshot from the persistent Browser Use session. Omit tabId to capture the first attachable page tab.",
|
|
221
|
+
promptSnippet: "Use browser_use_take_screenshot (browser_use.take_screenshot) when visual page state matters.",
|
|
222
|
+
executionMode: "parallel",
|
|
223
|
+
parameters: Type.Object({
|
|
224
|
+
tabId: Type.Optional(Type.String({ description: "Full target ID or short tab ID returned by browser_use_open_tabs." })),
|
|
225
|
+
fullPage: Type.Optional(Type.Boolean({ description: "Capture the full scrollable page instead of only the viewport." })),
|
|
226
|
+
timeoutMs: Type.Optional(Type.Number({ description: "Screenshot timeout in milliseconds." })),
|
|
227
|
+
}),
|
|
228
|
+
async execute(_toolCallId, params) {
|
|
229
|
+
const result = await controller.takeScreenshot(params);
|
|
230
|
+
const { data, ...details } = result;
|
|
231
|
+
return {
|
|
232
|
+
content: [
|
|
233
|
+
{ type: "text", text: JSON.stringify(details, null, 2) },
|
|
234
|
+
{ type: "image", data, mimeType: result.mimeType },
|
|
235
|
+
],
|
|
236
|
+
details,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
})],
|
|
240
|
+
[BROWSER_USE_TOOL_NAME, defineTool({
|
|
241
|
+
name: BROWSER_USE_TOOL_NAME,
|
|
242
|
+
label: "browser_use.browser_use",
|
|
243
|
+
description: "Control one persistent Browser Use session with a structured action. State and element indices persist through the same Pibo session and managed browser-pool lease.",
|
|
244
|
+
promptSnippet: "Use browser_use_browser_use (browser_use.browser_use) for structured navigation and interaction; call state before index-based clicks or input.",
|
|
245
|
+
executionMode: "sequential",
|
|
246
|
+
parameters: Type.Object({
|
|
247
|
+
action: StringEnum(["navigate", "state", "click", "input", "type", "scroll", "back", "switch_tab", "close_tab", "keys", "evaluate", "wait_selector", "wait_text"], { description: "Browser action." }),
|
|
248
|
+
url: Type.Optional(Type.String({ description: "URL for navigate." })),
|
|
249
|
+
index: Type.Optional(Type.Number({ description: "Interactive element index for click or input." })),
|
|
250
|
+
coordinateX: Type.Optional(Type.Number({ description: "Viewport X coordinate for click; use with coordinateY." })),
|
|
251
|
+
coordinateY: Type.Optional(Type.Number({ description: "Viewport Y coordinate for click; use with coordinateX." })),
|
|
252
|
+
text: Type.Optional(Type.String({ description: "Text for input, type, or wait_text." })),
|
|
253
|
+
direction: Type.Optional(StringEnum(["up", "down"], { description: "Scroll direction." })),
|
|
254
|
+
amount: Type.Optional(Type.Number({ description: "Scroll amount in pixels." })),
|
|
255
|
+
tab: Type.Optional(Type.Number({ description: "Browser Use tab index for switch_tab or close_tab." })),
|
|
256
|
+
keys: Type.Optional(Type.String({ description: "Keys to send, such as Enter or Control+a." })),
|
|
257
|
+
js: Type.Optional(Type.String({ description: "JavaScript for evaluate." })),
|
|
258
|
+
selector: Type.Optional(Type.String({ description: "CSS selector for wait_selector." })),
|
|
259
|
+
timeoutMs: Type.Optional(Type.Number({ description: "Action timeout in milliseconds." })),
|
|
260
|
+
}),
|
|
261
|
+
async execute(_toolCallId, params, signal) {
|
|
262
|
+
return toolResult(await controller.use(params, signal));
|
|
263
|
+
},
|
|
264
|
+
})],
|
|
265
|
+
[NODE_REPL_JS_TOOL_NAME, defineTool({
|
|
266
|
+
name: NODE_REPL_JS_TOOL_NAME,
|
|
267
|
+
label: "node_repl.js",
|
|
268
|
+
description: "Execute JavaScript in a persistent, session-scoped Node REPL with top-level await. Top-level bindings persist. The sandbox omits require and process, and exposes browser.openTabs() plus browser.use(action, params) for the same Browser Use session.",
|
|
269
|
+
promptSnippet: "Use node_repl_js (node_repl.js) for persistent JavaScript state and browser-bound analysis. Top-level await and reusable top-level bindings are supported.",
|
|
270
|
+
executionMode: "sequential",
|
|
271
|
+
parameters: Type.Object({
|
|
272
|
+
code: Type.String({ description: "JavaScript to execute. Top-level declarations persist across calls." }),
|
|
273
|
+
timeoutMs: Type.Optional(Type.Number({ description: "Execution timeout in milliseconds." })),
|
|
274
|
+
}),
|
|
275
|
+
async execute(_toolCallId, params) {
|
|
276
|
+
return nodeReplToolResult(await controller.js(params.code, params.timeoutMs));
|
|
277
|
+
},
|
|
278
|
+
})],
|
|
279
|
+
[NODE_REPL_JS_RESET_TOOL_NAME, defineTool({
|
|
280
|
+
name: NODE_REPL_JS_RESET_TOOL_NAME,
|
|
281
|
+
label: "node_repl.js_reset",
|
|
282
|
+
description: "Reset the persistent node_repl.js namespace for the current Pibo session.",
|
|
283
|
+
promptSnippet: "Use node_repl_js_reset (node_repl.js_reset) only when persistent JavaScript state should be discarded.",
|
|
284
|
+
executionMode: "sequential",
|
|
285
|
+
parameters: Type.Object({}),
|
|
286
|
+
async execute() {
|
|
287
|
+
return nodeReplToolResult(await controller.jsReset());
|
|
288
|
+
},
|
|
289
|
+
})],
|
|
290
|
+
];
|
|
291
|
+
return tools.filter(([name]) => enabled.has(name)).map(([, definition]) => definition);
|
|
292
|
+
}
|
|
293
|
+
function safeSessionName(value) {
|
|
294
|
+
return value.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 120) || "local";
|
|
295
|
+
}
|
|
296
|
+
function positiveInteger(value, fallback) {
|
|
297
|
+
const parsed = Number.parseInt(value ?? "", 10);
|
|
298
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
299
|
+
}
|
|
300
|
+
function findRegisteredLease(status, piboSessionId) {
|
|
301
|
+
try {
|
|
302
|
+
return findActiveBrowserUseLeaseForHolder(status, piboSessionId);
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function runBrowserUseCommand(request) {
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
const child = spawn(request.executable, request.args, {
|
|
311
|
+
cwd: request.cwd,
|
|
312
|
+
env: request.env,
|
|
313
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
314
|
+
});
|
|
315
|
+
let stdout = "";
|
|
316
|
+
let stderr = "";
|
|
317
|
+
let settled = false;
|
|
318
|
+
const abort = () => child.kill("SIGTERM");
|
|
319
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
320
|
+
const timer = setTimeout(() => child.kill("SIGTERM"), request.timeoutMs);
|
|
321
|
+
child.stdout.on("data", (chunk) => { stdout += String(chunk); });
|
|
322
|
+
child.stderr.on("data", (chunk) => { stderr += String(chunk); });
|
|
323
|
+
child.once("error", (error) => {
|
|
324
|
+
if (settled)
|
|
325
|
+
return;
|
|
326
|
+
settled = true;
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
request.signal?.removeEventListener("abort", abort);
|
|
329
|
+
reject(error);
|
|
330
|
+
});
|
|
331
|
+
child.once("close", (exitCode) => {
|
|
332
|
+
if (settled)
|
|
333
|
+
return;
|
|
334
|
+
settled = true;
|
|
335
|
+
clearTimeout(timer);
|
|
336
|
+
request.signal?.removeEventListener("abort", abort);
|
|
337
|
+
resolve({ exitCode, stdout, stderr });
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
function browserUseCommand(input) {
|
|
342
|
+
switch (input.action) {
|
|
343
|
+
case "navigate":
|
|
344
|
+
return ["open", requireText(input.url, "url")];
|
|
345
|
+
case "state":
|
|
346
|
+
return ["state"];
|
|
347
|
+
case "click":
|
|
348
|
+
if (input.index !== undefined)
|
|
349
|
+
return ["click", integerText(input.index, "index")];
|
|
350
|
+
if (input.coordinateX !== undefined && input.coordinateY !== undefined) {
|
|
351
|
+
return ["click", integerText(input.coordinateX, "coordinateX"), integerText(input.coordinateY, "coordinateY")];
|
|
352
|
+
}
|
|
353
|
+
throw new Error("browser_use.browser_use click requires index or coordinateX and coordinateY");
|
|
354
|
+
case "input":
|
|
355
|
+
return ["input", integerText(input.index, "index"), requireText(input.text, "text")];
|
|
356
|
+
case "type":
|
|
357
|
+
return ["type", requireText(input.text, "text")];
|
|
358
|
+
case "scroll":
|
|
359
|
+
return ["scroll", input.direction ?? "down", ...(input.amount === undefined ? [] : ["--amount", integerText(input.amount, "amount")])];
|
|
360
|
+
case "back":
|
|
361
|
+
return ["back"];
|
|
362
|
+
case "switch_tab":
|
|
363
|
+
return ["switch", integerText(input.tab, "tab")];
|
|
364
|
+
case "close_tab":
|
|
365
|
+
return ["close-tab", ...(input.tab === undefined ? [] : [integerText(input.tab, "tab")])];
|
|
366
|
+
case "keys":
|
|
367
|
+
return ["keys", requireText(input.keys, "keys")];
|
|
368
|
+
case "evaluate":
|
|
369
|
+
return ["eval", requireText(input.js, "js")];
|
|
370
|
+
case "wait_selector":
|
|
371
|
+
return ["wait", "selector", requireText(input.selector, "selector")];
|
|
372
|
+
case "wait_text":
|
|
373
|
+
return ["wait", "text", requireText(input.text, "text")];
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function requireBrowserUseInput(value) {
|
|
377
|
+
if (typeof value.action !== "string")
|
|
378
|
+
throw new Error("browser.use requires action");
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
function requireText(value, field) {
|
|
382
|
+
if (typeof value !== "string" || value.length === 0)
|
|
383
|
+
throw new Error(`browser_use.browser_use ${field} must be a non-empty string`);
|
|
384
|
+
return value;
|
|
385
|
+
}
|
|
386
|
+
function integerText(value, field) {
|
|
387
|
+
if (typeof value !== "number" || !Number.isInteger(value))
|
|
388
|
+
throw new Error(`browser_use.browser_use ${field} must be an integer`);
|
|
389
|
+
return String(value);
|
|
390
|
+
}
|
|
391
|
+
function pageTargets(targets) {
|
|
392
|
+
return targets.filter((target) => target.type === "page" && Boolean(target.webSocketDebuggerUrl));
|
|
393
|
+
}
|
|
394
|
+
function toBrowserUseTab(target, index = 0) {
|
|
395
|
+
return {
|
|
396
|
+
index,
|
|
397
|
+
tabId: target.id.slice(-4),
|
|
398
|
+
targetId: target.id,
|
|
399
|
+
title: target.title,
|
|
400
|
+
url: target.url,
|
|
401
|
+
type: target.type,
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
function selectTarget(targets, tabId) {
|
|
405
|
+
if (!tabId)
|
|
406
|
+
return targets.find((target) => target.url !== "about:blank") ?? targets[0];
|
|
407
|
+
return targets.find((target) => target.id === tabId || target.id.endsWith(tabId));
|
|
408
|
+
}
|
|
409
|
+
async function captureScreenshot(client, fullPage, timeoutMs) {
|
|
410
|
+
await client.send("Page.enable", undefined, timeoutMs);
|
|
411
|
+
const params = { format: "png", fromSurface: true, captureBeyondViewport: fullPage };
|
|
412
|
+
if (fullPage) {
|
|
413
|
+
const metrics = await client.send("Page.getLayoutMetrics", undefined, timeoutMs);
|
|
414
|
+
const width = metrics.cssContentSize?.width;
|
|
415
|
+
const height = metrics.cssContentSize?.height;
|
|
416
|
+
if (typeof width === "number" && typeof height === "number") {
|
|
417
|
+
params.clip = { x: 0, y: 0, width, height, scale: 1 };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const response = await client.send("Page.captureScreenshot", params, timeoutMs);
|
|
421
|
+
if (typeof response.data !== "string" || response.data.length === 0)
|
|
422
|
+
throw new Error("CDP screenshot returned no image data");
|
|
423
|
+
return response.data;
|
|
424
|
+
}
|
|
425
|
+
function parseJsonOrText(stdout) {
|
|
426
|
+
const text = stdout.trim();
|
|
427
|
+
if (!text)
|
|
428
|
+
return "";
|
|
429
|
+
try {
|
|
430
|
+
return JSON.parse(text);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
return text;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function toolResult(result) {
|
|
437
|
+
return {
|
|
438
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
439
|
+
details: result,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
function nodeReplToolResult(result) {
|
|
443
|
+
const lines = [`status: ${result.status}`];
|
|
444
|
+
if ("executionCount" in result)
|
|
445
|
+
lines.push(`executionCount: ${result.executionCount}`, `durationMs: ${result.durationMs}`);
|
|
446
|
+
if ("reset" in result)
|
|
447
|
+
lines.push(`reset: ${result.reset}`);
|
|
448
|
+
if ("stdout" in result && result.stdout)
|
|
449
|
+
lines.push("", "stdout:", result.stdout);
|
|
450
|
+
if ("stderr" in result && result.stderr)
|
|
451
|
+
lines.push("", "stderr:", result.stderr);
|
|
452
|
+
if ("result" in result && result.result !== undefined && result.result !== null)
|
|
453
|
+
lines.push("", "result:", JSON.stringify(result.result, null, 2));
|
|
454
|
+
if (result.error)
|
|
455
|
+
lines.push("", "error:", JSON.stringify(result.error, null, 2));
|
|
456
|
+
return {
|
|
457
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
458
|
+
details: result,
|
|
459
|
+
isError: result.status !== "ok",
|
|
460
|
+
};
|
|
461
|
+
}
|
package/dist/web/channel.js
CHANGED
|
@@ -6,6 +6,7 @@ import { PiboWebHttpError, nodeRequestToWebRequest, responseHtml, responseJson,
|
|
|
6
6
|
export const DEFAULT_WEB_CHANNEL_HOST = "127.0.0.1";
|
|
7
7
|
export const DEFAULT_WEB_CHANNEL_PORT = 4788;
|
|
8
8
|
export const WEB_CHANNEL_NAME = "web-host";
|
|
9
|
+
const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 5_000;
|
|
9
10
|
/**
|
|
10
11
|
* Internal header that carries the TCP socket peer address from the web host
|
|
11
12
|
* channel to the auth plugin. This is one of three independent safety layers
|
|
@@ -168,11 +169,44 @@ function createCanonicalRedirect(request, canonicalBaseURL) {
|
|
|
168
169
|
}
|
|
169
170
|
return redirect(new URL(`${url.pathname}${url.search}`, canonical.origin).toString());
|
|
170
171
|
}
|
|
172
|
+
function isEventStreamResponse(response) {
|
|
173
|
+
return response.headers.get("content-type")?.toLowerCase().startsWith("text/event-stream") === true;
|
|
174
|
+
}
|
|
175
|
+
async function waitForServerClose(closePromise, timeoutMs) {
|
|
176
|
+
return await new Promise((resolve, reject) => {
|
|
177
|
+
let settled = false;
|
|
178
|
+
const timeout = setTimeout(() => {
|
|
179
|
+
settled = true;
|
|
180
|
+
resolve(false);
|
|
181
|
+
}, timeoutMs);
|
|
182
|
+
closePromise.then(() => {
|
|
183
|
+
if (settled)
|
|
184
|
+
return;
|
|
185
|
+
settled = true;
|
|
186
|
+
clearTimeout(timeout);
|
|
187
|
+
resolve(true);
|
|
188
|
+
}, (error) => {
|
|
189
|
+
if (settled)
|
|
190
|
+
return;
|
|
191
|
+
settled = true;
|
|
192
|
+
clearTimeout(timeout);
|
|
193
|
+
reject(error);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
171
197
|
export function createWebHostChannel(options = {}) {
|
|
172
198
|
const host = options.host ?? DEFAULT_WEB_CHANNEL_HOST;
|
|
173
199
|
const port = options.port ?? DEFAULT_WEB_CHANNEL_PORT;
|
|
200
|
+
const shutdownDrainTimeoutMs = options.shutdownDrainTimeoutMs ?? DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS;
|
|
201
|
+
if (!Number.isFinite(shutdownDrainTimeoutMs) || shutdownDrainTimeoutMs < 0) {
|
|
202
|
+
throw new Error("Web channel shutdown drain timeout must be a non-negative finite number");
|
|
203
|
+
}
|
|
174
204
|
let server;
|
|
175
205
|
let context;
|
|
206
|
+
let stopPromise;
|
|
207
|
+
let shuttingDown = false;
|
|
208
|
+
const sockets = new Set();
|
|
209
|
+
const eventStreamControllers = new Map();
|
|
176
210
|
const requireContext = () => {
|
|
177
211
|
if (!context)
|
|
178
212
|
throw new Error("Web channel is not started");
|
|
@@ -185,6 +219,20 @@ export function createWebHostChannel(options = {}) {
|
|
|
185
219
|
}
|
|
186
220
|
return auth.handleRequest(request);
|
|
187
221
|
};
|
|
222
|
+
const sendResponse = async (nodeResponse, webResponse) => {
|
|
223
|
+
const eventStreamController = isEventStreamResponse(webResponse) ? new AbortController() : undefined;
|
|
224
|
+
if (eventStreamController)
|
|
225
|
+
eventStreamControllers.set(nodeResponse, eventStreamController);
|
|
226
|
+
try {
|
|
227
|
+
await sendWebResponse(nodeResponse, webResponse, { signal: eventStreamController?.signal });
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
if (eventStreamController)
|
|
231
|
+
eventStreamControllers.delete(nodeResponse);
|
|
232
|
+
if (shuttingDown)
|
|
233
|
+
server?.closeIdleConnections();
|
|
234
|
+
}
|
|
235
|
+
};
|
|
188
236
|
const handleRequest = async (nodeRequest, nodeResponse) => {
|
|
189
237
|
try {
|
|
190
238
|
const baseURL = createRequestBaseURL(nodeRequest, host, port);
|
|
@@ -198,51 +246,51 @@ export function createWebHostChannel(options = {}) {
|
|
|
198
246
|
const url = new URL(request.url);
|
|
199
247
|
const canonicalRedirect = createCanonicalRedirect(request, options.canonicalBaseURL);
|
|
200
248
|
if (canonicalRedirect) {
|
|
201
|
-
await
|
|
249
|
+
await sendResponse(nodeResponse, canonicalRedirect);
|
|
202
250
|
return;
|
|
203
251
|
}
|
|
204
252
|
if (url.pathname === "/health") {
|
|
205
|
-
await
|
|
253
|
+
await sendResponse(nodeResponse, responseJson({
|
|
206
254
|
status: "ok",
|
|
207
255
|
mode: process.env.PIBO_FALLBACK_MODE === "1" ? "fallback" : "main",
|
|
208
256
|
}));
|
|
209
257
|
return;
|
|
210
258
|
}
|
|
211
259
|
if (url.pathname === "/gateway/status") {
|
|
212
|
-
await
|
|
260
|
+
await sendResponse(nodeResponse, createGatewayStatusResponse(requireContext(), options));
|
|
213
261
|
return;
|
|
214
262
|
}
|
|
215
263
|
if (url.pathname.startsWith("/api/auth/")) {
|
|
216
264
|
const authResponse = stripSocketPeerHeaderFromResponse(await handleAuthRequest(request));
|
|
217
|
-
await
|
|
265
|
+
await sendResponse(nodeResponse, authResponse);
|
|
218
266
|
return;
|
|
219
267
|
}
|
|
220
268
|
const ctx = requireContext();
|
|
221
269
|
const simpleApiResponse = await handleSimpleAgentApiRequest(request, ctx);
|
|
222
270
|
if (simpleApiResponse) {
|
|
223
|
-
await
|
|
271
|
+
await sendResponse(nodeResponse, simpleApiResponse);
|
|
224
272
|
return;
|
|
225
273
|
}
|
|
226
274
|
const apps = ctx.getWebApps();
|
|
227
275
|
const app = apps.find((candidate) => matchPrefix(url.pathname, candidate.mountPath) || matchPrefix(url.pathname, candidate.apiPrefix));
|
|
228
276
|
if (app) {
|
|
229
277
|
const response = await app.handleRequest(request, createAppContext(ctx));
|
|
230
|
-
await
|
|
278
|
+
await sendResponse(nodeResponse, response ?? notFound());
|
|
231
279
|
return;
|
|
232
280
|
}
|
|
233
281
|
if (url.pathname === "/" && apps[0]) {
|
|
234
|
-
await
|
|
282
|
+
await sendResponse(nodeResponse, redirect(apps[0].mountPath));
|
|
235
283
|
return;
|
|
236
284
|
}
|
|
237
285
|
if (url.pathname === "/") {
|
|
238
|
-
await
|
|
286
|
+
await sendResponse(nodeResponse, responseHtml("<!doctype html><title>Pibo</title><p>No web apps registered.</p>"));
|
|
239
287
|
return;
|
|
240
288
|
}
|
|
241
|
-
await
|
|
289
|
+
await sendResponse(nodeResponse, notFound());
|
|
242
290
|
}
|
|
243
291
|
catch (error) {
|
|
244
292
|
const status = error instanceof PiboAuthError || error instanceof PiboWebHttpError ? error.statusCode : 500;
|
|
245
|
-
await
|
|
293
|
+
await sendResponse(nodeResponse, responseJson({ error: error instanceof Error ? error.message : String(error) }, { status }));
|
|
246
294
|
}
|
|
247
295
|
};
|
|
248
296
|
return {
|
|
@@ -251,12 +299,19 @@ export function createWebHostChannel(options = {}) {
|
|
|
251
299
|
description: "Same-origin HTTP host for pibo web apps and auth routes.",
|
|
252
300
|
auth: { mode: "required" },
|
|
253
301
|
async start(channelContext) {
|
|
302
|
+
if (stopPromise)
|
|
303
|
+
await stopPromise;
|
|
254
304
|
if (server)
|
|
255
305
|
return;
|
|
306
|
+
shuttingDown = false;
|
|
256
307
|
context = channelContext;
|
|
257
308
|
server = createServer((request, response) => {
|
|
258
309
|
void handleRequest(request, response);
|
|
259
310
|
});
|
|
311
|
+
server.on("connection", (socket) => {
|
|
312
|
+
sockets.add(socket);
|
|
313
|
+
socket.once("close", () => sockets.delete(socket));
|
|
314
|
+
});
|
|
260
315
|
await new Promise((resolve, reject) => {
|
|
261
316
|
server.once("error", reject);
|
|
262
317
|
server.listen(port, host, () => {
|
|
@@ -270,12 +325,46 @@ export function createWebHostChannel(options = {}) {
|
|
|
270
325
|
}
|
|
271
326
|
},
|
|
272
327
|
async stop() {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
328
|
+
if (stopPromise)
|
|
329
|
+
return await stopPromise;
|
|
330
|
+
if (!server) {
|
|
331
|
+
context = undefined;
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const closingServer = server;
|
|
335
|
+
shuttingDown = true;
|
|
336
|
+
stopPromise = (async () => {
|
|
337
|
+
try {
|
|
338
|
+
const closePromise = new Promise((resolve, reject) => {
|
|
339
|
+
closingServer.close((error) => (error ? reject(error) : resolve()));
|
|
340
|
+
});
|
|
341
|
+
const eventStreamCount = eventStreamControllers.size;
|
|
342
|
+
for (const controller of eventStreamControllers.values())
|
|
343
|
+
controller.abort();
|
|
344
|
+
closingServer.closeIdleConnections();
|
|
345
|
+
const drained = await waitForServerClose(closePromise, shutdownDrainTimeoutMs);
|
|
346
|
+
if (!drained) {
|
|
347
|
+
const connectionCount = sockets.size;
|
|
348
|
+
console.warn(`[web-host] graceful shutdown timed out after ${shutdownDrainTimeoutMs} ms; force-closing ${connectionCount} active connection(s)`);
|
|
349
|
+
for (const socket of sockets)
|
|
350
|
+
socket.destroy();
|
|
351
|
+
await closePromise;
|
|
352
|
+
}
|
|
353
|
+
else if (eventStreamCount > 0) {
|
|
354
|
+
console.error(`[web-host] graceful shutdown closed ${eventStreamCount} active event stream(s)`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
finally {
|
|
358
|
+
server = undefined;
|
|
359
|
+
context = undefined;
|
|
360
|
+
shuttingDown = false;
|
|
361
|
+
}
|
|
362
|
+
})();
|
|
363
|
+
try {
|
|
364
|
+
await stopPromise;
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
stopPromise = undefined;
|
|
279
368
|
}
|
|
280
369
|
},
|
|
281
370
|
getAddress() {
|