@canaryai/cli 0.1.14 → 0.2.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/chunk-UEOXNF5X.js +371 -0
- package/dist/chunk-UEOXNF5X.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/{local-browser-SYPTG6IQ.js → local-browser-MKKPBTYI.js} +2 -2
- package/dist/{mcp-TMD2R5Z6.js → mcp-4F4HI7L2.js} +2 -2
- package/package.json +2 -1
- package/dist/chunk-L26U3BST.js +0 -770
- package/dist/chunk-L26U3BST.js.map +0 -1
- /package/dist/{local-browser-SYPTG6IQ.js.map → local-browser-MKKPBTYI.js.map} +0 -0
- /package/dist/{mcp-TMD2R5Z6.js.map → mcp-4F4HI7L2.js.map} +0 -0
package/dist/chunk-L26U3BST.js
DELETED
|
@@ -1,770 +0,0 @@
|
|
|
1
|
-
// src/local-browser/host.ts
|
|
2
|
-
import { chromium } from "playwright";
|
|
3
|
-
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
4
|
-
var RECONNECT_DELAY_MS = 1e3;
|
|
5
|
-
var MAX_RECONNECT_DELAY_MS = 3e4;
|
|
6
|
-
var MAX_RECONNECT_ATTEMPTS = 10;
|
|
7
|
-
var LocalBrowserHost = class _LocalBrowserHost {
|
|
8
|
-
options;
|
|
9
|
-
ws = null;
|
|
10
|
-
browser = null;
|
|
11
|
-
contexts = /* @__PURE__ */ new Map();
|
|
12
|
-
static DEFAULT_CONTEXT_ID = "__default__";
|
|
13
|
-
heartbeatTimer = null;
|
|
14
|
-
reconnectAttempts = 0;
|
|
15
|
-
isShuttingDown = false;
|
|
16
|
-
lastSnapshotYaml = "";
|
|
17
|
-
constructor(options) {
|
|
18
|
-
this.options = options;
|
|
19
|
-
}
|
|
20
|
-
log(level, message, data) {
|
|
21
|
-
if (this.options.onLog) {
|
|
22
|
-
this.options.onLog(level, message, data);
|
|
23
|
-
} else {
|
|
24
|
-
const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
25
|
-
fn(`[LocalBrowserHost] ${message}`, data ?? "");
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
// =========================================================================
|
|
29
|
-
// Lifecycle
|
|
30
|
-
// =========================================================================
|
|
31
|
-
async start() {
|
|
32
|
-
this.log("info", "Starting local browser host", {
|
|
33
|
-
browserMode: this.options.browserMode,
|
|
34
|
-
sessionId: this.options.sessionId
|
|
35
|
-
});
|
|
36
|
-
await this.connectWebSocket();
|
|
37
|
-
await this.launchBrowser();
|
|
38
|
-
this.sendSessionEvent("browser_ready");
|
|
39
|
-
}
|
|
40
|
-
async stop() {
|
|
41
|
-
this.isShuttingDown = true;
|
|
42
|
-
this.log("info", "Stopping local browser host");
|
|
43
|
-
this.stopHeartbeat();
|
|
44
|
-
if (this.ws) {
|
|
45
|
-
try {
|
|
46
|
-
this.ws.close(1e3, "Shutdown");
|
|
47
|
-
} catch {
|
|
48
|
-
}
|
|
49
|
-
this.ws = null;
|
|
50
|
-
}
|
|
51
|
-
for (const [id, slot] of this.contexts) {
|
|
52
|
-
try {
|
|
53
|
-
await slot.context.close();
|
|
54
|
-
} catch {
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
this.contexts.clear();
|
|
58
|
-
if (this.browser) {
|
|
59
|
-
try {
|
|
60
|
-
await this.browser.close();
|
|
61
|
-
} catch {
|
|
62
|
-
}
|
|
63
|
-
this.browser = null;
|
|
64
|
-
}
|
|
65
|
-
this.log("info", "Local browser host stopped");
|
|
66
|
-
}
|
|
67
|
-
// =========================================================================
|
|
68
|
-
// WebSocket Connection
|
|
69
|
-
// =========================================================================
|
|
70
|
-
async connectWebSocket() {
|
|
71
|
-
return new Promise((resolve, reject) => {
|
|
72
|
-
const wsUrl = `${this.options.apiUrl.replace("http", "ws")}/local-browser/sessions/${this.options.sessionId}/connect?token=${this.options.wsToken}`;
|
|
73
|
-
this.log("info", "Connecting to cloud API", { url: wsUrl.replace(/token=.*/, "token=***") });
|
|
74
|
-
const ws = new WebSocket(wsUrl);
|
|
75
|
-
ws.onopen = () => {
|
|
76
|
-
this.log("info", "Connected to cloud API");
|
|
77
|
-
this.ws = ws;
|
|
78
|
-
this.reconnectAttempts = 0;
|
|
79
|
-
this.startHeartbeat();
|
|
80
|
-
resolve();
|
|
81
|
-
};
|
|
82
|
-
ws.onmessage = (event) => {
|
|
83
|
-
this.handleMessage(event.data);
|
|
84
|
-
};
|
|
85
|
-
ws.onerror = (event) => {
|
|
86
|
-
this.log("error", "WebSocket error", event);
|
|
87
|
-
};
|
|
88
|
-
ws.onclose = () => {
|
|
89
|
-
this.log("info", "WebSocket closed");
|
|
90
|
-
this.stopHeartbeat();
|
|
91
|
-
this.ws = null;
|
|
92
|
-
if (!this.isShuttingDown) {
|
|
93
|
-
this.scheduleReconnect();
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
setTimeout(() => {
|
|
97
|
-
if (!this.ws) {
|
|
98
|
-
reject(new Error("WebSocket connection timeout"));
|
|
99
|
-
}
|
|
100
|
-
}, 3e4);
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
scheduleReconnect() {
|
|
104
|
-
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
105
|
-
this.log("error", "Max reconnection attempts reached, giving up");
|
|
106
|
-
this.stop();
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const delay = Math.min(
|
|
110
|
-
RECONNECT_DELAY_MS * Math.pow(2, this.reconnectAttempts),
|
|
111
|
-
MAX_RECONNECT_DELAY_MS
|
|
112
|
-
);
|
|
113
|
-
this.reconnectAttempts++;
|
|
114
|
-
this.log("info", `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
115
|
-
setTimeout(async () => {
|
|
116
|
-
try {
|
|
117
|
-
await this.connectWebSocket();
|
|
118
|
-
this.sendSessionEvent("connected");
|
|
119
|
-
if (this.page) {
|
|
120
|
-
this.sendSessionEvent("browser_ready");
|
|
121
|
-
}
|
|
122
|
-
} catch (error) {
|
|
123
|
-
this.log("error", "Reconnection failed", error);
|
|
124
|
-
this.scheduleReconnect();
|
|
125
|
-
}
|
|
126
|
-
}, delay);
|
|
127
|
-
}
|
|
128
|
-
// =========================================================================
|
|
129
|
-
// Heartbeat
|
|
130
|
-
// =========================================================================
|
|
131
|
-
startHeartbeat() {
|
|
132
|
-
this.stopHeartbeat();
|
|
133
|
-
this.heartbeatTimer = setInterval(() => {
|
|
134
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
135
|
-
const ping = {
|
|
136
|
-
type: "heartbeat",
|
|
137
|
-
id: crypto.randomUUID(),
|
|
138
|
-
timestamp: Date.now(),
|
|
139
|
-
direction: "pong"
|
|
140
|
-
};
|
|
141
|
-
this.ws.send(JSON.stringify(ping));
|
|
142
|
-
}
|
|
143
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
144
|
-
}
|
|
145
|
-
stopHeartbeat() {
|
|
146
|
-
if (this.heartbeatTimer) {
|
|
147
|
-
clearInterval(this.heartbeatTimer);
|
|
148
|
-
this.heartbeatTimer = null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
// =========================================================================
|
|
152
|
-
// Browser Management
|
|
153
|
-
// =========================================================================
|
|
154
|
-
async launchBrowser() {
|
|
155
|
-
const { browserMode, cdpUrl, headless = true, storageStatePath } = this.options;
|
|
156
|
-
if (browserMode === "cdp" && cdpUrl) {
|
|
157
|
-
this.log("info", "Connecting to existing Chrome via CDP", { cdpUrl });
|
|
158
|
-
this.browser = await chromium.connectOverCDP(cdpUrl);
|
|
159
|
-
const existingContexts = this.browser.contexts();
|
|
160
|
-
const ctx = existingContexts[0] ?? await this.browser.newContext();
|
|
161
|
-
const pages = ctx.pages();
|
|
162
|
-
const pg = pages[0] ?? await ctx.newPage();
|
|
163
|
-
const slot = { context: ctx, page: pg, pendingDialogs: [] };
|
|
164
|
-
pg.on("dialog", (dialog) => slot.pendingDialogs.push(dialog));
|
|
165
|
-
this.contexts.set(_LocalBrowserHost.DEFAULT_CONTEXT_ID, slot);
|
|
166
|
-
} else {
|
|
167
|
-
this.log("info", "Launching new Playwright browser", { headless });
|
|
168
|
-
this.browser = await chromium.launch({
|
|
169
|
-
headless,
|
|
170
|
-
args: ["--no-sandbox"]
|
|
171
|
-
});
|
|
172
|
-
const contextOptions = {
|
|
173
|
-
viewport: { width: 1920, height: 1080 }
|
|
174
|
-
};
|
|
175
|
-
if (storageStatePath) {
|
|
176
|
-
try {
|
|
177
|
-
await Bun.file(storageStatePath).exists();
|
|
178
|
-
contextOptions.storageState = storageStatePath;
|
|
179
|
-
this.log("info", "Loading storage state", { storageStatePath });
|
|
180
|
-
} catch {
|
|
181
|
-
this.log("debug", "Storage state file not found, starting fresh");
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const ctx = await this.browser.newContext(contextOptions);
|
|
185
|
-
const pg = await ctx.newPage();
|
|
186
|
-
const slot = { context: ctx, page: pg, pendingDialogs: [] };
|
|
187
|
-
pg.on("dialog", (dialog) => slot.pendingDialogs.push(dialog));
|
|
188
|
-
this.contexts.set(_LocalBrowserHost.DEFAULT_CONTEXT_ID, slot);
|
|
189
|
-
}
|
|
190
|
-
this.log("info", "Browser ready");
|
|
191
|
-
}
|
|
192
|
-
// =========================================================================
|
|
193
|
-
// Message Handling
|
|
194
|
-
// =========================================================================
|
|
195
|
-
handleMessage(data) {
|
|
196
|
-
try {
|
|
197
|
-
const message = JSON.parse(data);
|
|
198
|
-
if (message.type === "heartbeat" && message.direction === "ping") {
|
|
199
|
-
const pong = {
|
|
200
|
-
type: "heartbeat",
|
|
201
|
-
id: crypto.randomUUID(),
|
|
202
|
-
timestamp: Date.now(),
|
|
203
|
-
direction: "pong"
|
|
204
|
-
};
|
|
205
|
-
this.ws?.send(JSON.stringify(pong));
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
if (message.type === "command") {
|
|
209
|
-
this.handleCommand(message);
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
this.log("debug", "Received unknown message type", message);
|
|
213
|
-
} catch (error) {
|
|
214
|
-
this.log("error", "Failed to parse message", { error, data });
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
async handleCommand(command) {
|
|
218
|
-
const startTime = Date.now();
|
|
219
|
-
const contextId = command.contextId;
|
|
220
|
-
this.log("debug", `Executing command: ${command.method}`, { id: command.id, contextId });
|
|
221
|
-
try {
|
|
222
|
-
const result = await this.executeMethod(command.method, command.args, contextId);
|
|
223
|
-
const response = {
|
|
224
|
-
type: "response",
|
|
225
|
-
id: crypto.randomUUID(),
|
|
226
|
-
timestamp: Date.now(),
|
|
227
|
-
requestId: command.id,
|
|
228
|
-
success: true,
|
|
229
|
-
result,
|
|
230
|
-
contextId
|
|
231
|
-
};
|
|
232
|
-
this.ws?.send(JSON.stringify(response));
|
|
233
|
-
this.log("debug", `Command completed: ${command.method}`, {
|
|
234
|
-
id: command.id,
|
|
235
|
-
contextId,
|
|
236
|
-
durationMs: Date.now() - startTime
|
|
237
|
-
});
|
|
238
|
-
} catch (error) {
|
|
239
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
240
|
-
const response = {
|
|
241
|
-
type: "response",
|
|
242
|
-
id: crypto.randomUUID(),
|
|
243
|
-
timestamp: Date.now(),
|
|
244
|
-
requestId: command.id,
|
|
245
|
-
success: false,
|
|
246
|
-
error: errorMessage,
|
|
247
|
-
stack: error instanceof Error ? error.stack : void 0,
|
|
248
|
-
contextId
|
|
249
|
-
};
|
|
250
|
-
this.ws?.send(JSON.stringify(response));
|
|
251
|
-
this.log("error", `Command failed: ${command.method}`, {
|
|
252
|
-
id: command.id,
|
|
253
|
-
contextId,
|
|
254
|
-
error: errorMessage
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
sendSessionEvent(event, error) {
|
|
259
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
260
|
-
const message = {
|
|
261
|
-
type: "session",
|
|
262
|
-
id: crypto.randomUUID(),
|
|
263
|
-
timestamp: Date.now(),
|
|
264
|
-
event,
|
|
265
|
-
browserMode: this.options.browserMode,
|
|
266
|
-
error
|
|
267
|
-
};
|
|
268
|
-
this.ws.send(JSON.stringify(message));
|
|
269
|
-
}
|
|
270
|
-
// =========================================================================
|
|
271
|
-
// Method Execution
|
|
272
|
-
// =========================================================================
|
|
273
|
-
getSlot(contextId) {
|
|
274
|
-
const id = contextId ?? _LocalBrowserHost.DEFAULT_CONTEXT_ID;
|
|
275
|
-
const slot = this.contexts.get(id);
|
|
276
|
-
if (!slot) throw new Error(`Context not found: ${id}`);
|
|
277
|
-
return slot;
|
|
278
|
-
}
|
|
279
|
-
async createContextSlot(contextId, options) {
|
|
280
|
-
if (!this.browser) throw new Error("No browser available");
|
|
281
|
-
if (this.contexts.has(contextId)) throw new Error(`Context already exists: ${contextId}`);
|
|
282
|
-
const contextOptions = {
|
|
283
|
-
viewport: { width: 1920, height: 1080 }
|
|
284
|
-
};
|
|
285
|
-
if (options?.storageState) {
|
|
286
|
-
const tmpPath = `/tmp/storage-state-${crypto.randomUUID()}.json`;
|
|
287
|
-
await Bun.write(tmpPath, JSON.stringify(options.storageState));
|
|
288
|
-
contextOptions.storageState = tmpPath;
|
|
289
|
-
this.log("info", "Loaded inline storage state for new context", { contextId });
|
|
290
|
-
}
|
|
291
|
-
const ctx = await this.browser.newContext(contextOptions);
|
|
292
|
-
const pg = await ctx.newPage();
|
|
293
|
-
const slot = { context: ctx, page: pg, pendingDialogs: [] };
|
|
294
|
-
pg.on("dialog", (dialog) => slot.pendingDialogs.push(dialog));
|
|
295
|
-
this.contexts.set(contextId, slot);
|
|
296
|
-
this.log("info", "Created new context slot", { contextId });
|
|
297
|
-
}
|
|
298
|
-
async destroyContextSlot(contextId) {
|
|
299
|
-
if (contextId === _LocalBrowserHost.DEFAULT_CONTEXT_ID) {
|
|
300
|
-
throw new Error("Cannot destroy the default context");
|
|
301
|
-
}
|
|
302
|
-
const slot = this.contexts.get(contextId);
|
|
303
|
-
if (!slot) throw new Error(`Context not found: ${contextId}`);
|
|
304
|
-
try {
|
|
305
|
-
await slot.context.close();
|
|
306
|
-
} catch {
|
|
307
|
-
}
|
|
308
|
-
this.contexts.delete(contextId);
|
|
309
|
-
this.log("info", "Destroyed context slot", { contextId });
|
|
310
|
-
}
|
|
311
|
-
async executeMethod(method, args, contextId) {
|
|
312
|
-
switch (method) {
|
|
313
|
-
case "createContext":
|
|
314
|
-
return this.createContextSlot(args[0], args[1]);
|
|
315
|
-
case "destroyContext":
|
|
316
|
-
return this.destroyContextSlot(args[0]);
|
|
317
|
-
}
|
|
318
|
-
switch (method) {
|
|
319
|
-
// Lifecycle
|
|
320
|
-
case "connect":
|
|
321
|
-
return this.connect(args[0]);
|
|
322
|
-
case "disconnect":
|
|
323
|
-
return this.disconnect();
|
|
324
|
-
// Navigation
|
|
325
|
-
case "navigate":
|
|
326
|
-
return this.navigate(args[0], args[1], contextId);
|
|
327
|
-
case "navigateBack":
|
|
328
|
-
return this.navigateBack(args[0], contextId);
|
|
329
|
-
// Page Inspection
|
|
330
|
-
case "snapshot":
|
|
331
|
-
return this.snapshot(args[0], contextId);
|
|
332
|
-
case "takeScreenshot":
|
|
333
|
-
return this.takeScreenshot(args[0], contextId);
|
|
334
|
-
case "evaluate":
|
|
335
|
-
return this.evaluate(args[0], args[1], contextId);
|
|
336
|
-
case "runCode":
|
|
337
|
-
return this.runCode(args[0], args[1], contextId);
|
|
338
|
-
case "consoleMessages":
|
|
339
|
-
return this.consoleMessages(args[0]);
|
|
340
|
-
case "networkRequests":
|
|
341
|
-
return this.networkRequests(args[0]);
|
|
342
|
-
// Interaction
|
|
343
|
-
case "click":
|
|
344
|
-
return this.click(args[0], args[1], args[2], contextId);
|
|
345
|
-
case "clickAtCoordinates":
|
|
346
|
-
return this.clickAtCoordinates(
|
|
347
|
-
args[0],
|
|
348
|
-
args[1],
|
|
349
|
-
args[2],
|
|
350
|
-
args[3],
|
|
351
|
-
contextId
|
|
352
|
-
);
|
|
353
|
-
case "moveToCoordinates":
|
|
354
|
-
return this.moveToCoordinates(
|
|
355
|
-
args[0],
|
|
356
|
-
args[1],
|
|
357
|
-
args[2],
|
|
358
|
-
args[3],
|
|
359
|
-
contextId
|
|
360
|
-
);
|
|
361
|
-
case "dragCoordinates":
|
|
362
|
-
return this.dragCoordinates(
|
|
363
|
-
args[0],
|
|
364
|
-
args[1],
|
|
365
|
-
args[2],
|
|
366
|
-
args[3],
|
|
367
|
-
args[4],
|
|
368
|
-
args[5],
|
|
369
|
-
contextId
|
|
370
|
-
);
|
|
371
|
-
case "hover":
|
|
372
|
-
return this.hover(args[0], args[1], args[2], contextId);
|
|
373
|
-
case "drag":
|
|
374
|
-
return this.drag(
|
|
375
|
-
args[0],
|
|
376
|
-
args[1],
|
|
377
|
-
args[2],
|
|
378
|
-
args[3],
|
|
379
|
-
args[4],
|
|
380
|
-
contextId
|
|
381
|
-
);
|
|
382
|
-
case "type":
|
|
383
|
-
return this.type(
|
|
384
|
-
args[0],
|
|
385
|
-
args[1],
|
|
386
|
-
args[2],
|
|
387
|
-
args[3],
|
|
388
|
-
args[4],
|
|
389
|
-
contextId
|
|
390
|
-
);
|
|
391
|
-
case "pressKey":
|
|
392
|
-
return this.pressKey(args[0], args[1], contextId);
|
|
393
|
-
case "fillForm":
|
|
394
|
-
return this.fillForm(args[0], args[1], contextId);
|
|
395
|
-
case "selectOption":
|
|
396
|
-
return this.selectOption(
|
|
397
|
-
args[0],
|
|
398
|
-
args[1],
|
|
399
|
-
args[2],
|
|
400
|
-
args[3],
|
|
401
|
-
contextId
|
|
402
|
-
);
|
|
403
|
-
case "fileUpload":
|
|
404
|
-
return this.fileUpload(args[0], args[1], contextId);
|
|
405
|
-
// Dialogs
|
|
406
|
-
case "handleDialog":
|
|
407
|
-
return this.handleDialog(args[0], args[1], args[2], contextId);
|
|
408
|
-
// Waiting
|
|
409
|
-
case "waitFor":
|
|
410
|
-
return this.waitFor(args[0], contextId);
|
|
411
|
-
// Browser Management
|
|
412
|
-
case "close":
|
|
413
|
-
return this.closePage(args[0], contextId);
|
|
414
|
-
case "resize":
|
|
415
|
-
return this.resize(args[0], args[1], args[2], contextId);
|
|
416
|
-
case "tabs":
|
|
417
|
-
return this.tabs(args[0], args[1], args[2], contextId);
|
|
418
|
-
// Context Management
|
|
419
|
-
case "swapContext":
|
|
420
|
-
return this.handleSwapContext(args[0], contextId);
|
|
421
|
-
// Storage
|
|
422
|
-
case "getStorageState":
|
|
423
|
-
return this.getStorageState(args[0], contextId);
|
|
424
|
-
case "getCurrentUrl":
|
|
425
|
-
return this.getCurrentUrl(args[0], contextId);
|
|
426
|
-
case "getTitle":
|
|
427
|
-
return this.getTitle(args[0], contextId);
|
|
428
|
-
case "getLinks":
|
|
429
|
-
return this.getLinks(args[0], contextId);
|
|
430
|
-
case "getElementBoundingBox":
|
|
431
|
-
return this.getElementBoundingBox(args[0], args[1], contextId);
|
|
432
|
-
// Tracing
|
|
433
|
-
case "startTracing":
|
|
434
|
-
return this.startTracing(args[0], contextId);
|
|
435
|
-
case "stopTracing":
|
|
436
|
-
return this.stopTracing(args[0], contextId);
|
|
437
|
-
// Video
|
|
438
|
-
case "isVideoRecordingEnabled":
|
|
439
|
-
return false;
|
|
440
|
-
// Video not supported in CLI host currently
|
|
441
|
-
case "saveVideo":
|
|
442
|
-
return null;
|
|
443
|
-
case "getVideoPath":
|
|
444
|
-
return null;
|
|
445
|
-
default:
|
|
446
|
-
throw new Error(`Unknown method: ${method}`);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
// =========================================================================
|
|
450
|
-
// IBrowserClient Method Implementations
|
|
451
|
-
// =========================================================================
|
|
452
|
-
async handleSwapContext(options, contextId) {
|
|
453
|
-
if (!this.browser) throw new Error("No browser available");
|
|
454
|
-
const slotId = contextId ?? _LocalBrowserHost.DEFAULT_CONTEXT_ID;
|
|
455
|
-
const existing = this.contexts.get(slotId);
|
|
456
|
-
if (existing) {
|
|
457
|
-
await existing.context.close();
|
|
458
|
-
this.contexts.delete(slotId);
|
|
459
|
-
}
|
|
460
|
-
const contextOptions = {
|
|
461
|
-
viewport: { width: 1920, height: 1080 }
|
|
462
|
-
};
|
|
463
|
-
if (options.storageState) {
|
|
464
|
-
const tmpPath = `/tmp/storage-state-${crypto.randomUUID()}.json`;
|
|
465
|
-
await Bun.write(tmpPath, JSON.stringify(options.storageState));
|
|
466
|
-
contextOptions.storageState = tmpPath;
|
|
467
|
-
this.log("info", "Loaded inline storage state for context swap");
|
|
468
|
-
} else if (options.storageStatePath) {
|
|
469
|
-
try {
|
|
470
|
-
const exists = await Bun.file(options.storageStatePath).exists();
|
|
471
|
-
if (exists) {
|
|
472
|
-
contextOptions.storageState = options.storageStatePath;
|
|
473
|
-
this.log("info", "Loading storage state from file for context swap", {
|
|
474
|
-
storageStatePath: options.storageStatePath
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
} catch {
|
|
478
|
-
this.log("debug", "Storage state file not found, starting fresh context");
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
const ctx = await this.browser.newContext(contextOptions);
|
|
482
|
-
const pg = await ctx.newPage();
|
|
483
|
-
const slot = { context: ctx, page: pg, pendingDialogs: [] };
|
|
484
|
-
pg.on("dialog", (dialog) => slot.pendingDialogs.push(dialog));
|
|
485
|
-
this.contexts.set(slotId, slot);
|
|
486
|
-
this.log("info", "Browser context swapped successfully", { contextId: slotId });
|
|
487
|
-
}
|
|
488
|
-
getPage(contextId) {
|
|
489
|
-
return this.getSlot(contextId).page;
|
|
490
|
-
}
|
|
491
|
-
resolveRef(ref, contextId) {
|
|
492
|
-
return this.getPage(contextId).locator(`aria-ref=${ref}`);
|
|
493
|
-
}
|
|
494
|
-
async connect(_options) {
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
async disconnect() {
|
|
498
|
-
await this.stop();
|
|
499
|
-
}
|
|
500
|
-
async navigate(url, _opts, contextId) {
|
|
501
|
-
const page = this.getPage(contextId);
|
|
502
|
-
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
503
|
-
await page.waitForLoadState("load", { timeout: 5e3 }).catch(() => {
|
|
504
|
-
});
|
|
505
|
-
return this.captureSnapshot(contextId);
|
|
506
|
-
}
|
|
507
|
-
async navigateBack(_opts, contextId) {
|
|
508
|
-
await this.getPage(contextId).goBack();
|
|
509
|
-
return this.captureSnapshot(contextId);
|
|
510
|
-
}
|
|
511
|
-
async snapshot(_opts, contextId) {
|
|
512
|
-
return this.captureSnapshot(contextId);
|
|
513
|
-
}
|
|
514
|
-
async captureSnapshot(contextId) {
|
|
515
|
-
const page = this.getPage(contextId);
|
|
516
|
-
this.lastSnapshotYaml = await page._snapshotForAI({ mode: "full" });
|
|
517
|
-
return this.lastSnapshotYaml;
|
|
518
|
-
}
|
|
519
|
-
async takeScreenshot(opts, contextId) {
|
|
520
|
-
const page = this.getPage(contextId);
|
|
521
|
-
const buffer = await page.screenshot({
|
|
522
|
-
type: opts?.type ?? "jpeg",
|
|
523
|
-
fullPage: opts?.fullPage ?? false
|
|
524
|
-
});
|
|
525
|
-
const mime = opts?.type === "png" ? "image/png" : "image/jpeg";
|
|
526
|
-
return `data:${mime};base64,${buffer.toString("base64")}`;
|
|
527
|
-
}
|
|
528
|
-
async evaluate(fn, _opts, contextId) {
|
|
529
|
-
const page = this.getPage(contextId);
|
|
530
|
-
return page.evaluate(new Function(`return (${fn})()`));
|
|
531
|
-
}
|
|
532
|
-
async runCode(code, _opts, contextId) {
|
|
533
|
-
const page = this.getPage(contextId);
|
|
534
|
-
const fn = new Function("page", `return (async () => { ${code} })()`);
|
|
535
|
-
return fn(page);
|
|
536
|
-
}
|
|
537
|
-
async consoleMessages(_opts) {
|
|
538
|
-
return "Console message capture not implemented in CLI host";
|
|
539
|
-
}
|
|
540
|
-
async networkRequests(_opts) {
|
|
541
|
-
return "Network request capture not implemented in CLI host";
|
|
542
|
-
}
|
|
543
|
-
async click(ref, _elementDesc, opts, contextId) {
|
|
544
|
-
const locator = this.resolveRef(ref, contextId);
|
|
545
|
-
await locator.scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
|
|
546
|
-
});
|
|
547
|
-
const box = await locator.boundingBox();
|
|
548
|
-
if (box) {
|
|
549
|
-
const centerX = box.x + box.width / 2;
|
|
550
|
-
const centerY = box.y + box.height / 2;
|
|
551
|
-
const page = this.getPage(contextId);
|
|
552
|
-
if (opts?.modifiers?.length) {
|
|
553
|
-
for (const mod of opts.modifiers) {
|
|
554
|
-
await page.keyboard.down(mod);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
if (opts?.doubleClick) {
|
|
558
|
-
await page.mouse.dblclick(centerX, centerY);
|
|
559
|
-
} else {
|
|
560
|
-
await page.mouse.click(centerX, centerY);
|
|
561
|
-
}
|
|
562
|
-
if (opts?.modifiers?.length) {
|
|
563
|
-
for (const mod of opts.modifiers) {
|
|
564
|
-
await page.keyboard.up(mod);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
} else {
|
|
568
|
-
if (opts?.doubleClick) {
|
|
569
|
-
await locator.dblclick({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
570
|
-
} else {
|
|
571
|
-
await locator.click({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
async clickAtCoordinates(x, y, _elementDesc, opts, contextId) {
|
|
576
|
-
const page = this.getPage(contextId);
|
|
577
|
-
if (opts?.doubleClick) {
|
|
578
|
-
await page.mouse.dblclick(x, y);
|
|
579
|
-
} else {
|
|
580
|
-
await page.mouse.click(x, y);
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
async moveToCoordinates(x, y, _elementDesc, _opts, contextId) {
|
|
584
|
-
await this.getPage(contextId).mouse.move(x, y);
|
|
585
|
-
}
|
|
586
|
-
async dragCoordinates(startX, startY, endX, endY, _elementDesc, _opts, contextId) {
|
|
587
|
-
const page = this.getPage(contextId);
|
|
588
|
-
await page.mouse.move(startX, startY);
|
|
589
|
-
await page.mouse.down();
|
|
590
|
-
await page.mouse.move(endX, endY);
|
|
591
|
-
await page.mouse.up();
|
|
592
|
-
}
|
|
593
|
-
async hover(ref, _elementDesc, opts, contextId) {
|
|
594
|
-
await this.resolveRef(ref, contextId).hover({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
595
|
-
}
|
|
596
|
-
async drag(startRef, _startElement, endRef, _endElement, opts, contextId) {
|
|
597
|
-
const startLocator = this.resolveRef(startRef, contextId);
|
|
598
|
-
const endLocator = this.resolveRef(endRef, contextId);
|
|
599
|
-
await startLocator.dragTo(endLocator, { timeout: opts?.timeoutMs ?? 6e4 });
|
|
600
|
-
}
|
|
601
|
-
async type(ref, text, _elementDesc, submit, opts, contextId) {
|
|
602
|
-
const locator = this.resolveRef(ref, contextId);
|
|
603
|
-
await locator.clear();
|
|
604
|
-
await locator.pressSequentially(text, {
|
|
605
|
-
delay: opts?.delay ?? 0,
|
|
606
|
-
timeout: opts?.timeoutMs ?? 3e4
|
|
607
|
-
});
|
|
608
|
-
if (submit) {
|
|
609
|
-
await locator.press("Enter");
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
async pressKey(key, _opts, contextId) {
|
|
613
|
-
await this.getPage(contextId).keyboard.press(key);
|
|
614
|
-
}
|
|
615
|
-
async fillForm(fields, opts, contextId) {
|
|
616
|
-
for (const field of fields) {
|
|
617
|
-
const locator = this.resolveRef(field.ref, contextId);
|
|
618
|
-
const fieldType = field.type ?? "textbox";
|
|
619
|
-
switch (fieldType) {
|
|
620
|
-
case "checkbox": {
|
|
621
|
-
const isChecked = await locator.isChecked();
|
|
622
|
-
const shouldBeChecked = field.value === "true";
|
|
623
|
-
if (shouldBeChecked !== isChecked) {
|
|
624
|
-
await locator.click({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
625
|
-
}
|
|
626
|
-
break;
|
|
627
|
-
}
|
|
628
|
-
case "radio":
|
|
629
|
-
await locator.check({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
630
|
-
break;
|
|
631
|
-
case "combobox":
|
|
632
|
-
await locator.selectOption(field.value, { timeout: opts?.timeoutMs ?? 3e4 });
|
|
633
|
-
break;
|
|
634
|
-
default:
|
|
635
|
-
await locator.fill(field.value, { timeout: opts?.timeoutMs ?? 3e4 });
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
async selectOption(ref, value, _elementDesc, opts, contextId) {
|
|
640
|
-
await this.resolveRef(ref, contextId).selectOption(value, { timeout: opts?.timeoutMs ?? 3e4 });
|
|
641
|
-
}
|
|
642
|
-
async fileUpload(paths, opts, contextId) {
|
|
643
|
-
const fileChooser = await this.getPage(contextId).waitForEvent("filechooser", {
|
|
644
|
-
timeout: opts?.timeoutMs ?? 3e4
|
|
645
|
-
});
|
|
646
|
-
await fileChooser.setFiles(paths);
|
|
647
|
-
}
|
|
648
|
-
async handleDialog(action, promptText, _opts, contextId) {
|
|
649
|
-
const slot = this.getSlot(contextId);
|
|
650
|
-
const dialog = slot.pendingDialogs.shift();
|
|
651
|
-
if (dialog) {
|
|
652
|
-
if (action === "accept") {
|
|
653
|
-
await dialog.accept(promptText);
|
|
654
|
-
} else {
|
|
655
|
-
await dialog.dismiss();
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
async waitFor(opts, contextId) {
|
|
660
|
-
const page = this.getPage(contextId);
|
|
661
|
-
const timeout = opts?.timeout ?? opts?.timeoutMs ?? 3e4;
|
|
662
|
-
if (opts?.timeSec) {
|
|
663
|
-
await page.waitForTimeout(opts.timeSec * 1e3);
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
if (opts?.text) {
|
|
667
|
-
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
if (opts?.textGone) {
|
|
671
|
-
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
if (opts?.selector) {
|
|
675
|
-
await page.locator(opts.selector).waitFor({
|
|
676
|
-
state: opts.state ?? "visible",
|
|
677
|
-
timeout
|
|
678
|
-
});
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
async closePage(_opts, contextId) {
|
|
682
|
-
const slot = this.getSlot(contextId);
|
|
683
|
-
await slot.page.close();
|
|
684
|
-
const remaining = slot.context.pages();
|
|
685
|
-
if (remaining.length > 0) {
|
|
686
|
-
slot.page = remaining[0];
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
async resize(width, height, _opts, contextId) {
|
|
690
|
-
await this.getPage(contextId).setViewportSize({ width, height });
|
|
691
|
-
}
|
|
692
|
-
async tabs(action, index, _opts, contextId) {
|
|
693
|
-
const slot = this.getSlot(contextId);
|
|
694
|
-
const pages = slot.context.pages();
|
|
695
|
-
switch (action) {
|
|
696
|
-
case "list":
|
|
697
|
-
return Promise.all(
|
|
698
|
-
pages.map(async (p, i) => ({
|
|
699
|
-
index: i,
|
|
700
|
-
url: p.url(),
|
|
701
|
-
title: await p.title().catch(() => "")
|
|
702
|
-
}))
|
|
703
|
-
);
|
|
704
|
-
case "new": {
|
|
705
|
-
const newPage = await slot.context.newPage();
|
|
706
|
-
slot.page = newPage;
|
|
707
|
-
newPage.on("dialog", (dialog) => slot.pendingDialogs.push(dialog));
|
|
708
|
-
return { index: pages.length };
|
|
709
|
-
}
|
|
710
|
-
case "close":
|
|
711
|
-
if (index !== void 0 && pages[index]) {
|
|
712
|
-
await pages[index].close();
|
|
713
|
-
} else {
|
|
714
|
-
await slot.page.close();
|
|
715
|
-
}
|
|
716
|
-
slot.page = slot.context.pages()[0] ?? slot.page;
|
|
717
|
-
break;
|
|
718
|
-
case "select":
|
|
719
|
-
if (index !== void 0 && pages[index]) {
|
|
720
|
-
slot.page = pages[index];
|
|
721
|
-
}
|
|
722
|
-
break;
|
|
723
|
-
}
|
|
724
|
-
return null;
|
|
725
|
-
}
|
|
726
|
-
async getStorageState(_opts, contextId) {
|
|
727
|
-
const slot = this.getSlot(contextId);
|
|
728
|
-
return slot.context.storageState();
|
|
729
|
-
}
|
|
730
|
-
async getCurrentUrl(_opts, contextId) {
|
|
731
|
-
return this.getPage(contextId).url();
|
|
732
|
-
}
|
|
733
|
-
async getTitle(_opts, contextId) {
|
|
734
|
-
return this.getPage(contextId).title();
|
|
735
|
-
}
|
|
736
|
-
async getLinks(_opts, contextId) {
|
|
737
|
-
const page = this.getPage(contextId);
|
|
738
|
-
return page.$$eval(
|
|
739
|
-
"a[href]",
|
|
740
|
-
(links) => links.map((a) => a.href).filter((h) => !!h && (h.startsWith("http://") || h.startsWith("https://")))
|
|
741
|
-
);
|
|
742
|
-
}
|
|
743
|
-
async getElementBoundingBox(ref, _opts, contextId) {
|
|
744
|
-
const locator = this.resolveRef(ref, contextId);
|
|
745
|
-
const box = await locator.boundingBox();
|
|
746
|
-
if (!box) return null;
|
|
747
|
-
return { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
748
|
-
}
|
|
749
|
-
async startTracing(_opts, contextId) {
|
|
750
|
-
const slot = this.getSlot(contextId);
|
|
751
|
-
await slot.context.tracing.start({ screenshots: true, snapshots: true });
|
|
752
|
-
}
|
|
753
|
-
async stopTracing(_opts, contextId) {
|
|
754
|
-
const slot = this.getSlot(contextId);
|
|
755
|
-
const tracePath = `/tmp/trace-${Date.now()}.zip`;
|
|
756
|
-
await slot.context.tracing.stop({ path: tracePath });
|
|
757
|
-
return {
|
|
758
|
-
trace: tracePath,
|
|
759
|
-
network: "",
|
|
760
|
-
resources: "",
|
|
761
|
-
directory: null,
|
|
762
|
-
legend: `Trace saved to ${tracePath}`
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
};
|
|
766
|
-
|
|
767
|
-
export {
|
|
768
|
-
LocalBrowserHost
|
|
769
|
-
};
|
|
770
|
-
//# sourceMappingURL=chunk-L26U3BST.js.map
|