@canaryai/cli 0.1.4 → 0.1.5
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/bin.js +1157 -1091
- package/dist/bin.js.map +1 -1
- package/dist/index.js +1443 -1377
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1,113 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
4
|
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
5
|
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
6
|
}) : x)(function(x) {
|
|
5
7
|
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
8
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
9
|
});
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
// src/runner/common.ts
|
|
16
|
-
import { spawnSync } from "child_process";
|
|
17
|
-
import fs from "fs";
|
|
18
|
-
import path from "path";
|
|
19
|
-
import { createRequire } from "module";
|
|
20
|
-
import { pathToFileURL } from "url";
|
|
21
|
-
function makeRequire() {
|
|
22
|
-
try {
|
|
23
|
-
return createRequire(import.meta.url);
|
|
24
|
-
} catch {
|
|
25
|
-
try {
|
|
26
|
-
return createRequire(process.cwd());
|
|
27
|
-
} catch {
|
|
28
|
-
return typeof __require !== "undefined" ? __require : createRequire(".");
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function resolveRunner(preloadPath2) {
|
|
33
|
-
const { bin, version } = pickNodeBinary();
|
|
34
|
-
const supportsImport = typeof version === "number" && version >= 18;
|
|
35
|
-
if (supportsImport && preloadPath2 && fs.existsSync(preloadPath2)) {
|
|
36
|
-
return { runnerBin: bin, preloadFlag: `--import=${pathToFileURL(preloadPath2).href}` };
|
|
37
|
-
}
|
|
38
|
-
if (preloadPath2) {
|
|
39
|
-
console.warn("[canary] Warning: no preload module found; instrumentation may be disabled.");
|
|
40
|
-
}
|
|
41
|
-
return { runnerBin: bin };
|
|
42
|
-
}
|
|
43
|
-
function pickNodeBinary() {
|
|
44
|
-
const candidates = collectNodeCandidates();
|
|
45
|
-
let best;
|
|
46
|
-
let fallback;
|
|
47
|
-
for (const bin of candidates) {
|
|
48
|
-
const version = getNodeMajor(bin);
|
|
49
|
-
if (!version) continue;
|
|
50
|
-
const current = { bin, version };
|
|
51
|
-
if (version >= 18 && !fallback) {
|
|
52
|
-
fallback = current;
|
|
53
|
-
}
|
|
54
|
-
if (!best || version > (best.version ?? 0)) {
|
|
55
|
-
best = current;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
if (fallback) return fallback;
|
|
59
|
-
if (best) return best;
|
|
60
|
-
return { bin: candidates[0] ?? "node" };
|
|
61
|
-
}
|
|
62
|
-
function collectNodeCandidates() {
|
|
63
|
-
const seen = /* @__PURE__ */ new Set();
|
|
64
|
-
const push = (value) => {
|
|
65
|
-
if (!value) return;
|
|
66
|
-
if (seen.has(value)) return;
|
|
67
|
-
seen.add(value);
|
|
68
|
-
};
|
|
69
|
-
const isBun = path.basename(process.execPath).includes("bun");
|
|
70
|
-
push(process.env.CANARY_NODE_BIN);
|
|
71
|
-
push(isBun ? void 0 : process.execPath);
|
|
72
|
-
push("node");
|
|
73
|
-
try {
|
|
74
|
-
const which = spawnSync("which", ["-a", "node"], { encoding: "utf-8" });
|
|
75
|
-
which.stdout?.toString().split("\n").map((line) => line.trim()).forEach((line) => push(line));
|
|
76
|
-
} catch {
|
|
77
|
-
}
|
|
78
|
-
const nvmDir = process.env.NVM_DIR || (process.env.HOME ? path.join(process.env.HOME, ".nvm") : void 0);
|
|
79
|
-
if (nvmDir) {
|
|
80
|
-
const versionsDir = path.join(nvmDir, "versions", "node");
|
|
81
|
-
if (fs.existsSync(versionsDir)) {
|
|
82
|
-
try {
|
|
83
|
-
const versions = fs.readdirSync(versionsDir);
|
|
84
|
-
versions.sort((a, b) => a > b ? -1 : 1).forEach((v) => push(path.join(versionsDir, v, "bin", "node")));
|
|
85
|
-
} catch {
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return Array.from(seen);
|
|
90
|
-
}
|
|
91
|
-
function getNodeMajor(bin) {
|
|
92
|
-
try {
|
|
93
|
-
const result = spawnSync(bin, ["-v"], { encoding: "utf-8" });
|
|
94
|
-
const output = (result.stdout || result.stderr || "").toString().trim();
|
|
95
|
-
const match = output.match(/^v(\d+)/);
|
|
96
|
-
if (match) return Number(match[1]);
|
|
97
|
-
} catch {
|
|
98
|
-
}
|
|
99
|
-
return void 0;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// src/run.ts
|
|
103
|
-
import { spawn } from "child_process";
|
|
104
|
-
import fs2 from "fs";
|
|
105
|
-
import os from "os";
|
|
106
|
-
import path2 from "path";
|
|
107
|
-
import { fileURLToPath } from "url";
|
|
108
|
-
|
|
109
|
-
// src/local-run.ts
|
|
110
|
-
import process2 from "process";
|
|
10
|
+
var __esm = (fn, res) => function __init() {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
};
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
111
17
|
|
|
112
18
|
// src/auth.ts
|
|
113
19
|
import fs3 from "fs/promises";
|
|
@@ -123,8 +29,14 @@ async function readStoredToken() {
|
|
|
123
29
|
return null;
|
|
124
30
|
}
|
|
125
31
|
}
|
|
32
|
+
var init_auth = __esm({
|
|
33
|
+
"src/auth.ts"() {
|
|
34
|
+
"use strict";
|
|
35
|
+
}
|
|
36
|
+
});
|
|
126
37
|
|
|
127
38
|
// src/local-run.ts
|
|
39
|
+
import process2 from "process";
|
|
128
40
|
function getArgValue(argv, key) {
|
|
129
41
|
const index = argv.indexOf(key);
|
|
130
42
|
if (index === -1) return void 0;
|
|
@@ -179,6 +91,12 @@ async function createLocalRun(input) {
|
|
|
179
91
|
}
|
|
180
92
|
return { runId: json.runId, watchUrl: json.watchUrl };
|
|
181
93
|
}
|
|
94
|
+
var init_local_run = __esm({
|
|
95
|
+
"src/local-run.ts"() {
|
|
96
|
+
"use strict";
|
|
97
|
+
init_auth();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
182
100
|
|
|
183
101
|
// src/tunnel.ts
|
|
184
102
|
import { createHash } from "crypto";
|
|
@@ -444,859 +362,708 @@ function connectTunnel(input) {
|
|
|
444
362
|
};
|
|
445
363
|
return ws;
|
|
446
364
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
import path4 from "path";
|
|
452
|
-
import process4 from "process";
|
|
453
|
-
import { spawn as spawn2 } from "child_process";
|
|
454
|
-
var DEFAULT_APP_URL = "https://app.trycanary.ai";
|
|
455
|
-
function getArgValue3(argv, key) {
|
|
456
|
-
const index = argv.indexOf(key);
|
|
457
|
-
if (index === -1) return void 0;
|
|
458
|
-
return argv[index + 1];
|
|
459
|
-
}
|
|
460
|
-
function shouldOpenBrowser(argv) {
|
|
461
|
-
return !argv.includes("--no-open");
|
|
462
|
-
}
|
|
463
|
-
function openUrl(url) {
|
|
464
|
-
const platform = process4.platform;
|
|
465
|
-
if (platform === "darwin") {
|
|
466
|
-
spawn2("open", [url], { stdio: "ignore" });
|
|
467
|
-
return;
|
|
468
|
-
}
|
|
469
|
-
if (platform === "win32") {
|
|
470
|
-
spawn2("cmd", ["/c", "start", "", url], { stdio: "ignore" });
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
spawn2("xdg-open", [url], { stdio: "ignore" });
|
|
474
|
-
}
|
|
475
|
-
async function writeToken(token) {
|
|
476
|
-
const dir = path4.join(os4.homedir(), ".config", "canary-cli");
|
|
477
|
-
const filePath = path4.join(dir, "auth.json");
|
|
478
|
-
await fs4.mkdir(dir, { recursive: true });
|
|
479
|
-
await fs4.writeFile(filePath, JSON.stringify({ token }, null, 2), "utf8");
|
|
480
|
-
return filePath;
|
|
481
|
-
}
|
|
482
|
-
async function runLogin(argv) {
|
|
483
|
-
const apiUrl = getArgValue3(argv, "--api-url") ?? process4.env.CANARY_API_URL ?? "https://api.trycanary.ai";
|
|
484
|
-
const appUrl = getArgValue3(argv, "--app-url") ?? process4.env.CANARY_APP_URL ?? DEFAULT_APP_URL;
|
|
485
|
-
const startRes = await fetch(`${apiUrl}/cli-login/start`, {
|
|
486
|
-
method: "POST",
|
|
487
|
-
headers: { "content-type": "application/json" },
|
|
488
|
-
body: JSON.stringify({ appUrl })
|
|
489
|
-
});
|
|
490
|
-
const startJson = await startRes.json();
|
|
491
|
-
if (!startRes.ok || !startJson.ok || !startJson.deviceCode || !startJson.userCode) {
|
|
492
|
-
console.error("Login start failed", startJson.error ?? startRes.statusText);
|
|
493
|
-
process4.exit(1);
|
|
494
|
-
}
|
|
495
|
-
console.log("Login required.");
|
|
496
|
-
console.log(`User code: ${startJson.userCode}`);
|
|
497
|
-
if (startJson.verificationUrl) {
|
|
498
|
-
console.log(`Open: ${startJson.verificationUrl}`);
|
|
499
|
-
if (shouldOpenBrowser(argv)) {
|
|
500
|
-
try {
|
|
501
|
-
openUrl(startJson.verificationUrl);
|
|
502
|
-
} catch {
|
|
503
|
-
console.log("Unable to open browser automatically. Please open the URL manually.");
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
const intervalMs = (startJson.intervalSeconds ?? 3) * 1e3;
|
|
508
|
-
const expiresAt = startJson.expiresAt ? new Date(startJson.expiresAt).getTime() : null;
|
|
509
|
-
while (true) {
|
|
510
|
-
if (expiresAt && Date.now() > expiresAt) {
|
|
511
|
-
console.error("Login code expired.");
|
|
512
|
-
process4.exit(1);
|
|
513
|
-
}
|
|
514
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
515
|
-
const pollRes = await fetch(`${apiUrl}/cli-login/poll`, {
|
|
516
|
-
method: "POST",
|
|
517
|
-
headers: { "content-type": "application/json" },
|
|
518
|
-
body: JSON.stringify({ deviceCode: startJson.deviceCode })
|
|
519
|
-
});
|
|
520
|
-
const pollJson = await pollRes.json();
|
|
521
|
-
if (!pollRes.ok || !pollJson.ok) {
|
|
522
|
-
console.error("Login poll failed", pollJson.error ?? pollRes.statusText);
|
|
523
|
-
process4.exit(1);
|
|
524
|
-
}
|
|
525
|
-
if (pollJson.status === "approved" && pollJson.accessToken) {
|
|
526
|
-
const filePath = await writeToken(pollJson.accessToken);
|
|
527
|
-
console.log(`Login successful. Token saved to ${filePath}`);
|
|
528
|
-
console.log("Set CANARY_API_TOKEN to use the CLI without re-login.");
|
|
529
|
-
return;
|
|
530
|
-
}
|
|
531
|
-
if (pollJson.status === "rejected") {
|
|
532
|
-
console.error("Login rejected.");
|
|
533
|
-
process4.exit(1);
|
|
534
|
-
}
|
|
535
|
-
if (pollJson.status === "expired") {
|
|
536
|
-
console.error("Login expired.");
|
|
537
|
-
process4.exit(1);
|
|
538
|
-
}
|
|
365
|
+
var init_tunnel = __esm({
|
|
366
|
+
"src/tunnel.ts"() {
|
|
367
|
+
"use strict";
|
|
368
|
+
init_auth();
|
|
539
369
|
}
|
|
540
|
-
}
|
|
370
|
+
});
|
|
541
371
|
|
|
542
|
-
// src/
|
|
543
|
-
import
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
let publicUrl = tunnelUrl;
|
|
566
|
-
let ws = null;
|
|
567
|
-
if (!publicUrl && portRaw) {
|
|
568
|
-
const port = Number(portRaw);
|
|
569
|
-
if (Number.isNaN(port) || port <= 0) {
|
|
570
|
-
console.error("Invalid --port value");
|
|
571
|
-
process5.exit(1);
|
|
572
|
-
}
|
|
573
|
-
const tunnel = await createTunnel({ apiUrl, token, port });
|
|
574
|
-
publicUrl = tunnel.publicUrl;
|
|
575
|
-
ws = connectTunnel({
|
|
576
|
-
apiUrl,
|
|
577
|
-
tunnelId: tunnel.tunnelId,
|
|
578
|
-
token: tunnel.token,
|
|
579
|
-
port,
|
|
580
|
-
onReady: () => {
|
|
581
|
-
console.log(`Tunnel connected: ${publicUrl ?? tunnel.tunnelId}`);
|
|
372
|
+
// src/local-browser/host.ts
|
|
373
|
+
import { chromium } from "playwright";
|
|
374
|
+
var HEARTBEAT_INTERVAL_MS, RECONNECT_DELAY_MS, MAX_RECONNECT_DELAY_MS, MAX_RECONNECT_ATTEMPTS, LocalBrowserHost;
|
|
375
|
+
var init_host = __esm({
|
|
376
|
+
"src/local-browser/host.ts"() {
|
|
377
|
+
"use strict";
|
|
378
|
+
HEARTBEAT_INTERVAL_MS = 3e4;
|
|
379
|
+
RECONNECT_DELAY_MS = 1e3;
|
|
380
|
+
MAX_RECONNECT_DELAY_MS = 3e4;
|
|
381
|
+
MAX_RECONNECT_ATTEMPTS = 10;
|
|
382
|
+
LocalBrowserHost = class {
|
|
383
|
+
options;
|
|
384
|
+
ws = null;
|
|
385
|
+
browser = null;
|
|
386
|
+
context = null;
|
|
387
|
+
page = null;
|
|
388
|
+
pendingDialogs = [];
|
|
389
|
+
heartbeatTimer = null;
|
|
390
|
+
reconnectAttempts = 0;
|
|
391
|
+
isShuttingDown = false;
|
|
392
|
+
lastSnapshotYaml = "";
|
|
393
|
+
constructor(options) {
|
|
394
|
+
this.options = options;
|
|
582
395
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
apiUrl,
|
|
591
|
-
token,
|
|
592
|
-
title,
|
|
593
|
-
featureSpec,
|
|
594
|
-
startUrl,
|
|
595
|
-
tunnelUrl: publicUrl
|
|
596
|
-
});
|
|
597
|
-
console.log(`Local test queued: ${run2.runId}`);
|
|
598
|
-
if (run2.watchUrl) {
|
|
599
|
-
console.log(`Watch: ${run2.watchUrl}`);
|
|
600
|
-
}
|
|
601
|
-
if (ws) {
|
|
602
|
-
console.log("Tunnel active. Press Ctrl+C to stop.");
|
|
603
|
-
process5.on("SIGINT", () => {
|
|
604
|
-
ws?.close();
|
|
605
|
-
process5.exit(0);
|
|
606
|
-
});
|
|
607
|
-
await new Promise(() => void 0);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// src/mcp.ts
|
|
612
|
-
import process6 from "process";
|
|
613
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
614
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
615
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
616
|
-
import { createParser } from "eventsource-parser";
|
|
617
|
-
|
|
618
|
-
// src/local-browser/host.ts
|
|
619
|
-
import { chromium } from "playwright";
|
|
620
|
-
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
621
|
-
var RECONNECT_DELAY_MS = 1e3;
|
|
622
|
-
var MAX_RECONNECT_DELAY_MS = 3e4;
|
|
623
|
-
var MAX_RECONNECT_ATTEMPTS = 10;
|
|
624
|
-
var LocalBrowserHost = class {
|
|
625
|
-
options;
|
|
626
|
-
ws = null;
|
|
627
|
-
browser = null;
|
|
628
|
-
context = null;
|
|
629
|
-
page = null;
|
|
630
|
-
pendingDialogs = [];
|
|
631
|
-
heartbeatTimer = null;
|
|
632
|
-
reconnectAttempts = 0;
|
|
633
|
-
isShuttingDown = false;
|
|
634
|
-
lastSnapshotYaml = "";
|
|
635
|
-
constructor(options) {
|
|
636
|
-
this.options = options;
|
|
637
|
-
}
|
|
638
|
-
log(level, message, data) {
|
|
639
|
-
if (this.options.onLog) {
|
|
640
|
-
this.options.onLog(level, message, data);
|
|
641
|
-
} else {
|
|
642
|
-
const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
643
|
-
fn(`[LocalBrowserHost] ${message}`, data ?? "");
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
// =========================================================================
|
|
647
|
-
// Lifecycle
|
|
648
|
-
// =========================================================================
|
|
649
|
-
async start() {
|
|
650
|
-
this.log("info", "Starting local browser host", {
|
|
651
|
-
browserMode: this.options.browserMode,
|
|
652
|
-
sessionId: this.options.sessionId
|
|
653
|
-
});
|
|
654
|
-
await this.connectWebSocket();
|
|
655
|
-
await this.launchBrowser();
|
|
656
|
-
this.sendSessionEvent("browser_ready");
|
|
657
|
-
}
|
|
658
|
-
async stop() {
|
|
659
|
-
this.isShuttingDown = true;
|
|
660
|
-
this.log("info", "Stopping local browser host");
|
|
661
|
-
this.stopHeartbeat();
|
|
662
|
-
if (this.ws) {
|
|
663
|
-
try {
|
|
664
|
-
this.ws.close(1e3, "Shutdown");
|
|
665
|
-
} catch {
|
|
666
|
-
}
|
|
667
|
-
this.ws = null;
|
|
668
|
-
}
|
|
669
|
-
if (this.context) {
|
|
670
|
-
try {
|
|
671
|
-
await this.context.close();
|
|
672
|
-
} catch {
|
|
396
|
+
log(level, message, data) {
|
|
397
|
+
if (this.options.onLog) {
|
|
398
|
+
this.options.onLog(level, message, data);
|
|
399
|
+
} else {
|
|
400
|
+
const fn = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
|
|
401
|
+
fn(`[LocalBrowserHost] ${message}`, data ?? "");
|
|
402
|
+
}
|
|
673
403
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
404
|
+
// =========================================================================
|
|
405
|
+
// Lifecycle
|
|
406
|
+
// =========================================================================
|
|
407
|
+
async start() {
|
|
408
|
+
this.log("info", "Starting local browser host", {
|
|
409
|
+
browserMode: this.options.browserMode,
|
|
410
|
+
sessionId: this.options.sessionId
|
|
411
|
+
});
|
|
412
|
+
await this.connectWebSocket();
|
|
413
|
+
await this.launchBrowser();
|
|
414
|
+
this.sendSessionEvent("browser_ready");
|
|
680
415
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
this.log("info", "Local browser host stopped");
|
|
685
|
-
}
|
|
686
|
-
// =========================================================================
|
|
687
|
-
// WebSocket Connection
|
|
688
|
-
// =========================================================================
|
|
689
|
-
async connectWebSocket() {
|
|
690
|
-
return new Promise((resolve, reject) => {
|
|
691
|
-
const wsUrl = `${this.options.apiUrl.replace("http", "ws")}/local-browser/sessions/${this.options.sessionId}/connect?token=${this.options.wsToken}`;
|
|
692
|
-
this.log("info", "Connecting to cloud API", { url: wsUrl.replace(/token=.*/, "token=***") });
|
|
693
|
-
const ws = new WebSocket(wsUrl);
|
|
694
|
-
ws.onopen = () => {
|
|
695
|
-
this.log("info", "Connected to cloud API");
|
|
696
|
-
this.ws = ws;
|
|
697
|
-
this.reconnectAttempts = 0;
|
|
698
|
-
this.startHeartbeat();
|
|
699
|
-
resolve();
|
|
700
|
-
};
|
|
701
|
-
ws.onmessage = (event) => {
|
|
702
|
-
this.handleMessage(event.data);
|
|
703
|
-
};
|
|
704
|
-
ws.onerror = (event) => {
|
|
705
|
-
this.log("error", "WebSocket error", event);
|
|
706
|
-
};
|
|
707
|
-
ws.onclose = () => {
|
|
708
|
-
this.log("info", "WebSocket closed");
|
|
416
|
+
async stop() {
|
|
417
|
+
this.isShuttingDown = true;
|
|
418
|
+
this.log("info", "Stopping local browser host");
|
|
709
419
|
this.stopHeartbeat();
|
|
710
|
-
this.ws
|
|
711
|
-
|
|
712
|
-
|
|
420
|
+
if (this.ws) {
|
|
421
|
+
try {
|
|
422
|
+
this.ws.close(1e3, "Shutdown");
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
this.ws = null;
|
|
713
426
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
427
|
+
if (this.context) {
|
|
428
|
+
try {
|
|
429
|
+
await this.context.close();
|
|
430
|
+
} catch {
|
|
431
|
+
}
|
|
432
|
+
this.context = null;
|
|
718
433
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
this.stop();
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
const delay = Math.min(
|
|
729
|
-
RECONNECT_DELAY_MS * Math.pow(2, this.reconnectAttempts),
|
|
730
|
-
MAX_RECONNECT_DELAY_MS
|
|
731
|
-
);
|
|
732
|
-
this.reconnectAttempts++;
|
|
733
|
-
this.log("info", `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
734
|
-
setTimeout(async () => {
|
|
735
|
-
try {
|
|
736
|
-
await this.connectWebSocket();
|
|
737
|
-
this.sendSessionEvent("connected");
|
|
738
|
-
if (this.page) {
|
|
739
|
-
this.sendSessionEvent("browser_ready");
|
|
434
|
+
if (this.browser) {
|
|
435
|
+
try {
|
|
436
|
+
await this.browser.close();
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
this.browser = null;
|
|
740
440
|
}
|
|
741
|
-
|
|
742
|
-
this.log("
|
|
743
|
-
this.scheduleReconnect();
|
|
441
|
+
this.page = null;
|
|
442
|
+
this.log("info", "Local browser host stopped");
|
|
744
443
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
444
|
+
// =========================================================================
|
|
445
|
+
// WebSocket Connection
|
|
446
|
+
// =========================================================================
|
|
447
|
+
async connectWebSocket() {
|
|
448
|
+
return new Promise((resolve, reject) => {
|
|
449
|
+
const wsUrl = `${this.options.apiUrl.replace("http", "ws")}/local-browser/sessions/${this.options.sessionId}/connect?token=${this.options.wsToken}`;
|
|
450
|
+
this.log("info", "Connecting to cloud API", { url: wsUrl.replace(/token=.*/, "token=***") });
|
|
451
|
+
const ws = new WebSocket(wsUrl);
|
|
452
|
+
ws.onopen = () => {
|
|
453
|
+
this.log("info", "Connected to cloud API");
|
|
454
|
+
this.ws = ws;
|
|
455
|
+
this.reconnectAttempts = 0;
|
|
456
|
+
this.startHeartbeat();
|
|
457
|
+
resolve();
|
|
458
|
+
};
|
|
459
|
+
ws.onmessage = (event) => {
|
|
460
|
+
this.handleMessage(event.data);
|
|
461
|
+
};
|
|
462
|
+
ws.onerror = (event) => {
|
|
463
|
+
this.log("error", "WebSocket error", event);
|
|
464
|
+
};
|
|
465
|
+
ws.onclose = () => {
|
|
466
|
+
this.log("info", "WebSocket closed");
|
|
467
|
+
this.stopHeartbeat();
|
|
468
|
+
this.ws = null;
|
|
469
|
+
if (!this.isShuttingDown) {
|
|
470
|
+
this.scheduleReconnect();
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
setTimeout(() => {
|
|
474
|
+
if (!this.ws) {
|
|
475
|
+
reject(new Error("WebSocket connection timeout"));
|
|
476
|
+
}
|
|
477
|
+
}, 3e4);
|
|
478
|
+
});
|
|
761
479
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
480
|
+
scheduleReconnect() {
|
|
481
|
+
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
482
|
+
this.log("error", "Max reconnection attempts reached, giving up");
|
|
483
|
+
this.stop();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const delay = Math.min(
|
|
487
|
+
RECONNECT_DELAY_MS * Math.pow(2, this.reconnectAttempts),
|
|
488
|
+
MAX_RECONNECT_DELAY_MS
|
|
489
|
+
);
|
|
490
|
+
this.reconnectAttempts++;
|
|
491
|
+
this.log("info", `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
492
|
+
setTimeout(async () => {
|
|
493
|
+
try {
|
|
494
|
+
await this.connectWebSocket();
|
|
495
|
+
this.sendSessionEvent("connected");
|
|
496
|
+
if (this.page) {
|
|
497
|
+
this.sendSessionEvent("browser_ready");
|
|
498
|
+
}
|
|
499
|
+
} catch (error) {
|
|
500
|
+
this.log("error", "Reconnection failed", error);
|
|
501
|
+
this.scheduleReconnect();
|
|
502
|
+
}
|
|
503
|
+
}, delay);
|
|
504
|
+
}
|
|
505
|
+
// =========================================================================
|
|
506
|
+
// Heartbeat
|
|
507
|
+
// =========================================================================
|
|
508
|
+
startHeartbeat() {
|
|
509
|
+
this.stopHeartbeat();
|
|
510
|
+
this.heartbeatTimer = setInterval(() => {
|
|
511
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
512
|
+
const ping = {
|
|
513
|
+
type: "heartbeat",
|
|
514
|
+
id: crypto.randomUUID(),
|
|
515
|
+
timestamp: Date.now(),
|
|
516
|
+
direction: "pong"
|
|
517
|
+
};
|
|
518
|
+
this.ws.send(JSON.stringify(ping));
|
|
519
|
+
}
|
|
520
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
521
|
+
}
|
|
522
|
+
stopHeartbeat() {
|
|
523
|
+
if (this.heartbeatTimer) {
|
|
524
|
+
clearInterval(this.heartbeatTimer);
|
|
525
|
+
this.heartbeatTimer = null;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
// =========================================================================
|
|
529
|
+
// Browser Management
|
|
530
|
+
// =========================================================================
|
|
531
|
+
async launchBrowser() {
|
|
532
|
+
const { browserMode, cdpUrl, headless = true, storageStatePath } = this.options;
|
|
533
|
+
if (browserMode === "cdp" && cdpUrl) {
|
|
534
|
+
this.log("info", "Connecting to existing Chrome via CDP", { cdpUrl });
|
|
535
|
+
this.browser = await chromium.connectOverCDP(cdpUrl);
|
|
536
|
+
const contexts = this.browser.contexts();
|
|
537
|
+
this.context = contexts[0] ?? await this.browser.newContext();
|
|
538
|
+
const pages = this.context.pages();
|
|
539
|
+
this.page = pages[0] ?? await this.context.newPage();
|
|
540
|
+
} else {
|
|
541
|
+
this.log("info", "Launching new Playwright browser", { headless });
|
|
542
|
+
this.browser = await chromium.launch({
|
|
543
|
+
headless,
|
|
544
|
+
args: ["--no-sandbox"]
|
|
545
|
+
});
|
|
546
|
+
const contextOptions = {
|
|
547
|
+
viewport: { width: 1920, height: 1080 }
|
|
548
|
+
};
|
|
549
|
+
if (storageStatePath) {
|
|
550
|
+
try {
|
|
551
|
+
await Bun.file(storageStatePath).exists();
|
|
552
|
+
contextOptions.storageState = storageStatePath;
|
|
553
|
+
this.log("info", "Loading storage state", { storageStatePath });
|
|
554
|
+
} catch {
|
|
555
|
+
this.log("debug", "Storage state file not found, starting fresh");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
559
|
+
this.page = await this.context.newPage();
|
|
560
|
+
}
|
|
561
|
+
this.page.on("dialog", (dialog) => {
|
|
562
|
+
this.pendingDialogs.push(dialog);
|
|
563
|
+
});
|
|
564
|
+
this.log("info", "Browser ready");
|
|
565
|
+
}
|
|
566
|
+
// =========================================================================
|
|
567
|
+
// Message Handling
|
|
568
|
+
// =========================================================================
|
|
569
|
+
handleMessage(data) {
|
|
792
570
|
try {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
571
|
+
const message = JSON.parse(data);
|
|
572
|
+
if (message.type === "heartbeat" && message.direction === "ping") {
|
|
573
|
+
const pong = {
|
|
574
|
+
type: "heartbeat",
|
|
575
|
+
id: crypto.randomUUID(),
|
|
576
|
+
timestamp: Date.now(),
|
|
577
|
+
direction: "pong"
|
|
578
|
+
};
|
|
579
|
+
this.ws?.send(JSON.stringify(pong));
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (message.type === "command") {
|
|
583
|
+
this.handleCommand(message);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
this.log("debug", "Received unknown message type", message);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
this.log("error", "Failed to parse message", { error, data });
|
|
798
589
|
}
|
|
799
590
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
591
|
+
async handleCommand(command) {
|
|
592
|
+
const startTime = Date.now();
|
|
593
|
+
this.log("debug", `Executing command: ${command.method}`, { id: command.id });
|
|
594
|
+
try {
|
|
595
|
+
const result = await this.executeMethod(command.method, command.args);
|
|
596
|
+
const response = {
|
|
597
|
+
type: "response",
|
|
598
|
+
id: crypto.randomUUID(),
|
|
599
|
+
timestamp: Date.now(),
|
|
600
|
+
requestId: command.id,
|
|
601
|
+
success: true,
|
|
602
|
+
result
|
|
603
|
+
};
|
|
604
|
+
this.ws?.send(JSON.stringify(response));
|
|
605
|
+
this.log("debug", `Command completed: ${command.method}`, {
|
|
606
|
+
id: command.id,
|
|
607
|
+
durationMs: Date.now() - startTime
|
|
608
|
+
});
|
|
609
|
+
} catch (error) {
|
|
610
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
611
|
+
const response = {
|
|
612
|
+
type: "response",
|
|
613
|
+
id: crypto.randomUUID(),
|
|
614
|
+
timestamp: Date.now(),
|
|
615
|
+
requestId: command.id,
|
|
616
|
+
success: false,
|
|
617
|
+
error: errorMessage,
|
|
618
|
+
stack: error instanceof Error ? error.stack : void 0
|
|
619
|
+
};
|
|
620
|
+
this.ws?.send(JSON.stringify(response));
|
|
621
|
+
this.log("error", `Command failed: ${command.method}`, {
|
|
622
|
+
id: command.id,
|
|
623
|
+
error: errorMessage
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
sendSessionEvent(event, error) {
|
|
628
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
|
|
629
|
+
const message = {
|
|
630
|
+
type: "session",
|
|
817
631
|
id: crypto.randomUUID(),
|
|
818
632
|
timestamp: Date.now(),
|
|
819
|
-
|
|
633
|
+
event,
|
|
634
|
+
browserMode: this.options.browserMode,
|
|
635
|
+
error
|
|
820
636
|
};
|
|
821
|
-
this.ws
|
|
822
|
-
|
|
637
|
+
this.ws.send(JSON.stringify(message));
|
|
638
|
+
}
|
|
639
|
+
// =========================================================================
|
|
640
|
+
// Method Execution
|
|
641
|
+
// =========================================================================
|
|
642
|
+
async executeMethod(method, args) {
|
|
643
|
+
switch (method) {
|
|
644
|
+
// Lifecycle
|
|
645
|
+
case "connect":
|
|
646
|
+
return this.connect(args[0]);
|
|
647
|
+
case "disconnect":
|
|
648
|
+
return this.disconnect();
|
|
649
|
+
// Navigation
|
|
650
|
+
case "navigate":
|
|
651
|
+
return this.navigate(args[0], args[1]);
|
|
652
|
+
case "navigateBack":
|
|
653
|
+
return this.navigateBack(args[0]);
|
|
654
|
+
// Page Inspection
|
|
655
|
+
case "snapshot":
|
|
656
|
+
return this.snapshot(args[0]);
|
|
657
|
+
case "takeScreenshot":
|
|
658
|
+
return this.takeScreenshot(args[0]);
|
|
659
|
+
case "evaluate":
|
|
660
|
+
return this.evaluate(args[0], args[1]);
|
|
661
|
+
case "runCode":
|
|
662
|
+
return this.runCode(args[0], args[1]);
|
|
663
|
+
case "consoleMessages":
|
|
664
|
+
return this.consoleMessages(args[0]);
|
|
665
|
+
case "networkRequests":
|
|
666
|
+
return this.networkRequests(args[0]);
|
|
667
|
+
// Interaction
|
|
668
|
+
case "click":
|
|
669
|
+
return this.click(args[0], args[1], args[2]);
|
|
670
|
+
case "clickAtCoordinates":
|
|
671
|
+
return this.clickAtCoordinates(
|
|
672
|
+
args[0],
|
|
673
|
+
args[1],
|
|
674
|
+
args[2],
|
|
675
|
+
args[3]
|
|
676
|
+
);
|
|
677
|
+
case "moveToCoordinates":
|
|
678
|
+
return this.moveToCoordinates(
|
|
679
|
+
args[0],
|
|
680
|
+
args[1],
|
|
681
|
+
args[2],
|
|
682
|
+
args[3]
|
|
683
|
+
);
|
|
684
|
+
case "dragCoordinates":
|
|
685
|
+
return this.dragCoordinates(
|
|
686
|
+
args[0],
|
|
687
|
+
args[1],
|
|
688
|
+
args[2],
|
|
689
|
+
args[3],
|
|
690
|
+
args[4],
|
|
691
|
+
args[5]
|
|
692
|
+
);
|
|
693
|
+
case "hover":
|
|
694
|
+
return this.hover(args[0], args[1], args[2]);
|
|
695
|
+
case "drag":
|
|
696
|
+
return this.drag(
|
|
697
|
+
args[0],
|
|
698
|
+
args[1],
|
|
699
|
+
args[2],
|
|
700
|
+
args[3],
|
|
701
|
+
args[4]
|
|
702
|
+
);
|
|
703
|
+
case "type":
|
|
704
|
+
return this.type(
|
|
705
|
+
args[0],
|
|
706
|
+
args[1],
|
|
707
|
+
args[2],
|
|
708
|
+
args[3],
|
|
709
|
+
args[4]
|
|
710
|
+
);
|
|
711
|
+
case "pressKey":
|
|
712
|
+
return this.pressKey(args[0], args[1]);
|
|
713
|
+
case "fillForm":
|
|
714
|
+
return this.fillForm(args[0], args[1]);
|
|
715
|
+
case "selectOption":
|
|
716
|
+
return this.selectOption(
|
|
717
|
+
args[0],
|
|
718
|
+
args[1],
|
|
719
|
+
args[2],
|
|
720
|
+
args[3]
|
|
721
|
+
);
|
|
722
|
+
case "fileUpload":
|
|
723
|
+
return this.fileUpload(args[0], args[1]);
|
|
724
|
+
// Dialogs
|
|
725
|
+
case "handleDialog":
|
|
726
|
+
return this.handleDialog(args[0], args[1], args[2]);
|
|
727
|
+
// Waiting
|
|
728
|
+
case "waitFor":
|
|
729
|
+
return this.waitFor(args[0]);
|
|
730
|
+
// Browser Management
|
|
731
|
+
case "close":
|
|
732
|
+
return this.closePage(args[0]);
|
|
733
|
+
case "resize":
|
|
734
|
+
return this.resize(args[0], args[1], args[2]);
|
|
735
|
+
case "tabs":
|
|
736
|
+
return this.tabs(args[0], args[1], args[2]);
|
|
737
|
+
// Storage
|
|
738
|
+
case "getStorageState":
|
|
739
|
+
return this.getStorageState(args[0]);
|
|
740
|
+
case "getCurrentUrl":
|
|
741
|
+
return this.getCurrentUrl(args[0]);
|
|
742
|
+
case "getTitle":
|
|
743
|
+
return this.getTitle(args[0]);
|
|
744
|
+
case "getLinks":
|
|
745
|
+
return this.getLinks(args[0]);
|
|
746
|
+
case "getElementBoundingBox":
|
|
747
|
+
return this.getElementBoundingBox(args[0], args[1]);
|
|
748
|
+
// Tracing
|
|
749
|
+
case "startTracing":
|
|
750
|
+
return this.startTracing(args[0]);
|
|
751
|
+
case "stopTracing":
|
|
752
|
+
return this.stopTracing(args[0]);
|
|
753
|
+
// Video
|
|
754
|
+
case "isVideoRecordingEnabled":
|
|
755
|
+
return false;
|
|
756
|
+
// Video not supported in CLI host currently
|
|
757
|
+
case "saveVideo":
|
|
758
|
+
return null;
|
|
759
|
+
case "getVideoPath":
|
|
760
|
+
return null;
|
|
761
|
+
default:
|
|
762
|
+
throw new Error(`Unknown method: ${method}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
// =========================================================================
|
|
766
|
+
// IBrowserClient Method Implementations
|
|
767
|
+
// =========================================================================
|
|
768
|
+
getPage() {
|
|
769
|
+
if (!this.page) throw new Error("No page available");
|
|
770
|
+
return this.page;
|
|
823
771
|
}
|
|
824
|
-
|
|
825
|
-
this.
|
|
772
|
+
resolveRef(ref) {
|
|
773
|
+
return this.getPage().locator(`aria-ref=${ref}`);
|
|
774
|
+
}
|
|
775
|
+
async connect(_options) {
|
|
826
776
|
return;
|
|
827
777
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
return this.runCode(args[0], args[1]);
|
|
905
|
-
case "consoleMessages":
|
|
906
|
-
return this.consoleMessages(args[0]);
|
|
907
|
-
case "networkRequests":
|
|
908
|
-
return this.networkRequests(args[0]);
|
|
909
|
-
// Interaction
|
|
910
|
-
case "click":
|
|
911
|
-
return this.click(args[0], args[1], args[2]);
|
|
912
|
-
case "clickAtCoordinates":
|
|
913
|
-
return this.clickAtCoordinates(
|
|
914
|
-
args[0],
|
|
915
|
-
args[1],
|
|
916
|
-
args[2],
|
|
917
|
-
args[3]
|
|
918
|
-
);
|
|
919
|
-
case "moveToCoordinates":
|
|
920
|
-
return this.moveToCoordinates(
|
|
921
|
-
args[0],
|
|
922
|
-
args[1],
|
|
923
|
-
args[2],
|
|
924
|
-
args[3]
|
|
925
|
-
);
|
|
926
|
-
case "dragCoordinates":
|
|
927
|
-
return this.dragCoordinates(
|
|
928
|
-
args[0],
|
|
929
|
-
args[1],
|
|
930
|
-
args[2],
|
|
931
|
-
args[3],
|
|
932
|
-
args[4],
|
|
933
|
-
args[5]
|
|
934
|
-
);
|
|
935
|
-
case "hover":
|
|
936
|
-
return this.hover(args[0], args[1], args[2]);
|
|
937
|
-
case "drag":
|
|
938
|
-
return this.drag(
|
|
939
|
-
args[0],
|
|
940
|
-
args[1],
|
|
941
|
-
args[2],
|
|
942
|
-
args[3],
|
|
943
|
-
args[4]
|
|
944
|
-
);
|
|
945
|
-
case "type":
|
|
946
|
-
return this.type(
|
|
947
|
-
args[0],
|
|
948
|
-
args[1],
|
|
949
|
-
args[2],
|
|
950
|
-
args[3],
|
|
951
|
-
args[4]
|
|
952
|
-
);
|
|
953
|
-
case "pressKey":
|
|
954
|
-
return this.pressKey(args[0], args[1]);
|
|
955
|
-
case "fillForm":
|
|
956
|
-
return this.fillForm(args[0], args[1]);
|
|
957
|
-
case "selectOption":
|
|
958
|
-
return this.selectOption(
|
|
959
|
-
args[0],
|
|
960
|
-
args[1],
|
|
961
|
-
args[2],
|
|
962
|
-
args[3]
|
|
963
|
-
);
|
|
964
|
-
case "fileUpload":
|
|
965
|
-
return this.fileUpload(args[0], args[1]);
|
|
966
|
-
// Dialogs
|
|
967
|
-
case "handleDialog":
|
|
968
|
-
return this.handleDialog(args[0], args[1], args[2]);
|
|
969
|
-
// Waiting
|
|
970
|
-
case "waitFor":
|
|
971
|
-
return this.waitFor(args[0]);
|
|
972
|
-
// Browser Management
|
|
973
|
-
case "close":
|
|
974
|
-
return this.closePage(args[0]);
|
|
975
|
-
case "resize":
|
|
976
|
-
return this.resize(args[0], args[1], args[2]);
|
|
977
|
-
case "tabs":
|
|
978
|
-
return this.tabs(args[0], args[1], args[2]);
|
|
979
|
-
// Storage
|
|
980
|
-
case "getStorageState":
|
|
981
|
-
return this.getStorageState(args[0]);
|
|
982
|
-
case "getCurrentUrl":
|
|
983
|
-
return this.getCurrentUrl(args[0]);
|
|
984
|
-
case "getTitle":
|
|
985
|
-
return this.getTitle(args[0]);
|
|
986
|
-
case "getLinks":
|
|
987
|
-
return this.getLinks(args[0]);
|
|
988
|
-
case "getElementBoundingBox":
|
|
989
|
-
return this.getElementBoundingBox(args[0], args[1]);
|
|
990
|
-
// Tracing
|
|
991
|
-
case "startTracing":
|
|
992
|
-
return this.startTracing(args[0]);
|
|
993
|
-
case "stopTracing":
|
|
994
|
-
return this.stopTracing(args[0]);
|
|
995
|
-
// Video
|
|
996
|
-
case "isVideoRecordingEnabled":
|
|
997
|
-
return false;
|
|
998
|
-
// Video not supported in CLI host currently
|
|
999
|
-
case "saveVideo":
|
|
1000
|
-
return null;
|
|
1001
|
-
case "getVideoPath":
|
|
1002
|
-
return null;
|
|
1003
|
-
default:
|
|
1004
|
-
throw new Error(`Unknown method: ${method}`);
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
// =========================================================================
|
|
1008
|
-
// IBrowserClient Method Implementations
|
|
1009
|
-
// =========================================================================
|
|
1010
|
-
getPage() {
|
|
1011
|
-
if (!this.page) throw new Error("No page available");
|
|
1012
|
-
return this.page;
|
|
1013
|
-
}
|
|
1014
|
-
resolveRef(ref) {
|
|
1015
|
-
return this.getPage().locator(`aria-ref=${ref}`);
|
|
1016
|
-
}
|
|
1017
|
-
async connect(_options) {
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
async disconnect() {
|
|
1021
|
-
await this.stop();
|
|
1022
|
-
}
|
|
1023
|
-
async navigate(url, _opts) {
|
|
1024
|
-
const page = this.getPage();
|
|
1025
|
-
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
1026
|
-
await page.waitForLoadState("load", { timeout: 5e3 }).catch(() => {
|
|
1027
|
-
});
|
|
1028
|
-
return this.captureSnapshot();
|
|
1029
|
-
}
|
|
1030
|
-
async navigateBack(_opts) {
|
|
1031
|
-
await this.getPage().goBack();
|
|
1032
|
-
return this.captureSnapshot();
|
|
1033
|
-
}
|
|
1034
|
-
async snapshot(_opts) {
|
|
1035
|
-
return this.captureSnapshot();
|
|
1036
|
-
}
|
|
1037
|
-
async captureSnapshot() {
|
|
1038
|
-
const page = this.getPage();
|
|
1039
|
-
this.lastSnapshotYaml = await page._snapshotForAI({ mode: "full" });
|
|
1040
|
-
return this.lastSnapshotYaml;
|
|
1041
|
-
}
|
|
1042
|
-
async takeScreenshot(opts) {
|
|
1043
|
-
const page = this.getPage();
|
|
1044
|
-
const buffer = await page.screenshot({
|
|
1045
|
-
type: opts?.type ?? "jpeg",
|
|
1046
|
-
fullPage: opts?.fullPage ?? false
|
|
1047
|
-
});
|
|
1048
|
-
const mime = opts?.type === "png" ? "image/png" : "image/jpeg";
|
|
1049
|
-
return `data:${mime};base64,${buffer.toString("base64")}`;
|
|
1050
|
-
}
|
|
1051
|
-
async evaluate(fn, _opts) {
|
|
1052
|
-
const page = this.getPage();
|
|
1053
|
-
return page.evaluate(new Function(`return (${fn})()`));
|
|
1054
|
-
}
|
|
1055
|
-
async runCode(code, _opts) {
|
|
1056
|
-
const page = this.getPage();
|
|
1057
|
-
const fn = new Function("page", `return (async () => { ${code} })()`);
|
|
1058
|
-
return fn(page);
|
|
1059
|
-
}
|
|
1060
|
-
async consoleMessages(_opts) {
|
|
1061
|
-
return "Console message capture not implemented in CLI host";
|
|
1062
|
-
}
|
|
1063
|
-
async networkRequests(_opts) {
|
|
1064
|
-
return "Network request capture not implemented in CLI host";
|
|
1065
|
-
}
|
|
1066
|
-
async click(ref, _elementDesc, opts) {
|
|
1067
|
-
const locator = this.resolveRef(ref);
|
|
1068
|
-
await locator.scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
|
|
1069
|
-
});
|
|
1070
|
-
const box = await locator.boundingBox();
|
|
1071
|
-
if (box) {
|
|
1072
|
-
const centerX = box.x + box.width / 2;
|
|
1073
|
-
const centerY = box.y + box.height / 2;
|
|
1074
|
-
const page = this.getPage();
|
|
1075
|
-
if (opts?.modifiers?.length) {
|
|
1076
|
-
for (const mod of opts.modifiers) {
|
|
1077
|
-
await page.keyboard.down(mod);
|
|
778
|
+
async disconnect() {
|
|
779
|
+
await this.stop();
|
|
780
|
+
}
|
|
781
|
+
async navigate(url, _opts) {
|
|
782
|
+
const page = this.getPage();
|
|
783
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
784
|
+
await page.waitForLoadState("load", { timeout: 5e3 }).catch(() => {
|
|
785
|
+
});
|
|
786
|
+
return this.captureSnapshot();
|
|
787
|
+
}
|
|
788
|
+
async navigateBack(_opts) {
|
|
789
|
+
await this.getPage().goBack();
|
|
790
|
+
return this.captureSnapshot();
|
|
791
|
+
}
|
|
792
|
+
async snapshot(_opts) {
|
|
793
|
+
return this.captureSnapshot();
|
|
794
|
+
}
|
|
795
|
+
async captureSnapshot() {
|
|
796
|
+
const page = this.getPage();
|
|
797
|
+
this.lastSnapshotYaml = await page._snapshotForAI({ mode: "full" });
|
|
798
|
+
return this.lastSnapshotYaml;
|
|
799
|
+
}
|
|
800
|
+
async takeScreenshot(opts) {
|
|
801
|
+
const page = this.getPage();
|
|
802
|
+
const buffer = await page.screenshot({
|
|
803
|
+
type: opts?.type ?? "jpeg",
|
|
804
|
+
fullPage: opts?.fullPage ?? false
|
|
805
|
+
});
|
|
806
|
+
const mime = opts?.type === "png" ? "image/png" : "image/jpeg";
|
|
807
|
+
return `data:${mime};base64,${buffer.toString("base64")}`;
|
|
808
|
+
}
|
|
809
|
+
async evaluate(fn, _opts) {
|
|
810
|
+
const page = this.getPage();
|
|
811
|
+
return page.evaluate(new Function(`return (${fn})()`));
|
|
812
|
+
}
|
|
813
|
+
async runCode(code, _opts) {
|
|
814
|
+
const page = this.getPage();
|
|
815
|
+
const fn = new Function("page", `return (async () => { ${code} })()`);
|
|
816
|
+
return fn(page);
|
|
817
|
+
}
|
|
818
|
+
async consoleMessages(_opts) {
|
|
819
|
+
return "Console message capture not implemented in CLI host";
|
|
820
|
+
}
|
|
821
|
+
async networkRequests(_opts) {
|
|
822
|
+
return "Network request capture not implemented in CLI host";
|
|
823
|
+
}
|
|
824
|
+
async click(ref, _elementDesc, opts) {
|
|
825
|
+
const locator = this.resolveRef(ref);
|
|
826
|
+
await locator.scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
|
|
827
|
+
});
|
|
828
|
+
const box = await locator.boundingBox();
|
|
829
|
+
if (box) {
|
|
830
|
+
const centerX = box.x + box.width / 2;
|
|
831
|
+
const centerY = box.y + box.height / 2;
|
|
832
|
+
const page = this.getPage();
|
|
833
|
+
if (opts?.modifiers?.length) {
|
|
834
|
+
for (const mod of opts.modifiers) {
|
|
835
|
+
await page.keyboard.down(mod);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (opts?.doubleClick) {
|
|
839
|
+
await page.mouse.dblclick(centerX, centerY);
|
|
840
|
+
} else {
|
|
841
|
+
await page.mouse.click(centerX, centerY);
|
|
842
|
+
}
|
|
843
|
+
if (opts?.modifiers?.length) {
|
|
844
|
+
for (const mod of opts.modifiers) {
|
|
845
|
+
await page.keyboard.up(mod);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
} else {
|
|
849
|
+
if (opts?.doubleClick) {
|
|
850
|
+
await locator.dblclick({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
851
|
+
} else {
|
|
852
|
+
await locator.click({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
853
|
+
}
|
|
1078
854
|
}
|
|
1079
855
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
856
|
+
async clickAtCoordinates(x, y, _elementDesc, opts) {
|
|
857
|
+
const page = this.getPage();
|
|
858
|
+
if (opts?.doubleClick) {
|
|
859
|
+
await page.mouse.dblclick(x, y);
|
|
860
|
+
} else {
|
|
861
|
+
await page.mouse.click(x, y);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async moveToCoordinates(x, y, _elementDesc, _opts) {
|
|
865
|
+
await this.getPage().mouse.move(x, y);
|
|
866
|
+
}
|
|
867
|
+
async dragCoordinates(startX, startY, endX, endY, _elementDesc, _opts) {
|
|
868
|
+
const page = this.getPage();
|
|
869
|
+
await page.mouse.move(startX, startY);
|
|
870
|
+
await page.mouse.down();
|
|
871
|
+
await page.mouse.move(endX, endY);
|
|
872
|
+
await page.mouse.up();
|
|
873
|
+
}
|
|
874
|
+
async hover(ref, _elementDesc, opts) {
|
|
875
|
+
await this.resolveRef(ref).hover({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
1084
876
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
877
|
+
async drag(startRef, _startElement, endRef, _endElement, opts) {
|
|
878
|
+
const startLocator = this.resolveRef(startRef);
|
|
879
|
+
const endLocator = this.resolveRef(endRef);
|
|
880
|
+
await startLocator.dragTo(endLocator, { timeout: opts?.timeoutMs ?? 6e4 });
|
|
881
|
+
}
|
|
882
|
+
async type(ref, text, _elementDesc, submit, opts) {
|
|
883
|
+
const locator = this.resolveRef(ref);
|
|
884
|
+
await locator.clear();
|
|
885
|
+
await locator.pressSequentially(text, {
|
|
886
|
+
delay: opts?.delay ?? 0,
|
|
887
|
+
timeout: opts?.timeoutMs ?? 3e4
|
|
888
|
+
});
|
|
889
|
+
if (submit) {
|
|
890
|
+
await locator.press("Enter");
|
|
1088
891
|
}
|
|
1089
892
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
await locator.dblclick({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
1093
|
-
} else {
|
|
1094
|
-
await locator.click({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
893
|
+
async pressKey(key, _opts) {
|
|
894
|
+
await this.getPage().keyboard.press(key);
|
|
1095
895
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
await this.resolveRef(ref).hover({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
1118
|
-
}
|
|
1119
|
-
async drag(startRef, _startElement, endRef, _endElement, opts) {
|
|
1120
|
-
const startLocator = this.resolveRef(startRef);
|
|
1121
|
-
const endLocator = this.resolveRef(endRef);
|
|
1122
|
-
await startLocator.dragTo(endLocator, { timeout: opts?.timeoutMs ?? 6e4 });
|
|
1123
|
-
}
|
|
1124
|
-
async type(ref, text, _elementDesc, submit, opts) {
|
|
1125
|
-
const locator = this.resolveRef(ref);
|
|
1126
|
-
await locator.clear();
|
|
1127
|
-
await locator.pressSequentially(text, {
|
|
1128
|
-
delay: opts?.delay ?? 0,
|
|
1129
|
-
timeout: opts?.timeoutMs ?? 3e4
|
|
1130
|
-
});
|
|
1131
|
-
if (submit) {
|
|
1132
|
-
await locator.press("Enter");
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
async pressKey(key, _opts) {
|
|
1136
|
-
await this.getPage().keyboard.press(key);
|
|
1137
|
-
}
|
|
1138
|
-
async fillForm(fields, opts) {
|
|
1139
|
-
for (const field of fields) {
|
|
1140
|
-
const locator = this.resolveRef(field.ref);
|
|
1141
|
-
const fieldType = field.type ?? "textbox";
|
|
1142
|
-
switch (fieldType) {
|
|
1143
|
-
case "checkbox": {
|
|
1144
|
-
const isChecked = await locator.isChecked();
|
|
1145
|
-
const shouldBeChecked = field.value === "true";
|
|
1146
|
-
if (shouldBeChecked !== isChecked) {
|
|
1147
|
-
await locator.click({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
896
|
+
async fillForm(fields, opts) {
|
|
897
|
+
for (const field of fields) {
|
|
898
|
+
const locator = this.resolveRef(field.ref);
|
|
899
|
+
const fieldType = field.type ?? "textbox";
|
|
900
|
+
switch (fieldType) {
|
|
901
|
+
case "checkbox": {
|
|
902
|
+
const isChecked = await locator.isChecked();
|
|
903
|
+
const shouldBeChecked = field.value === "true";
|
|
904
|
+
if (shouldBeChecked !== isChecked) {
|
|
905
|
+
await locator.click({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
906
|
+
}
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
case "radio":
|
|
910
|
+
await locator.check({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
911
|
+
break;
|
|
912
|
+
case "combobox":
|
|
913
|
+
await locator.selectOption(field.value, { timeout: opts?.timeoutMs ?? 3e4 });
|
|
914
|
+
break;
|
|
915
|
+
default:
|
|
916
|
+
await locator.fill(field.value, { timeout: opts?.timeoutMs ?? 3e4 });
|
|
1148
917
|
}
|
|
1149
|
-
break;
|
|
1150
918
|
}
|
|
1151
|
-
case "radio":
|
|
1152
|
-
await locator.check({ timeout: opts?.timeoutMs ?? 3e4 });
|
|
1153
|
-
break;
|
|
1154
|
-
case "combobox":
|
|
1155
|
-
await locator.selectOption(field.value, { timeout: opts?.timeoutMs ?? 3e4 });
|
|
1156
|
-
break;
|
|
1157
|
-
default:
|
|
1158
|
-
await locator.fill(field.value, { timeout: opts?.timeoutMs ?? 3e4 });
|
|
1159
919
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
async selectOption(ref, value, _elementDesc, opts) {
|
|
1163
|
-
await this.resolveRef(ref).selectOption(value, { timeout: opts?.timeoutMs ?? 3e4 });
|
|
1164
|
-
}
|
|
1165
|
-
async fileUpload(paths, opts) {
|
|
1166
|
-
const fileChooser = await this.getPage().waitForEvent("filechooser", {
|
|
1167
|
-
timeout: opts?.timeoutMs ?? 3e4
|
|
1168
|
-
});
|
|
1169
|
-
await fileChooser.setFiles(paths);
|
|
1170
|
-
}
|
|
1171
|
-
async handleDialog(action, promptText, _opts) {
|
|
1172
|
-
const dialog = this.pendingDialogs.shift();
|
|
1173
|
-
if (dialog) {
|
|
1174
|
-
if (action === "accept") {
|
|
1175
|
-
await dialog.accept(promptText);
|
|
1176
|
-
} else {
|
|
1177
|
-
await dialog.dismiss();
|
|
920
|
+
async selectOption(ref, value, _elementDesc, opts) {
|
|
921
|
+
await this.resolveRef(ref).selectOption(value, { timeout: opts?.timeoutMs ?? 3e4 });
|
|
1178
922
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
1194
|
-
return;
|
|
1195
|
-
}
|
|
1196
|
-
if (opts?.selector) {
|
|
1197
|
-
await page.locator(opts.selector).waitFor({
|
|
1198
|
-
state: opts.state ?? "visible",
|
|
1199
|
-
timeout
|
|
1200
|
-
});
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
async closePage(_opts) {
|
|
1204
|
-
await this.getPage().close();
|
|
1205
|
-
this.page = null;
|
|
1206
|
-
}
|
|
1207
|
-
async resize(width, height, _opts) {
|
|
1208
|
-
await this.getPage().setViewportSize({ width, height });
|
|
1209
|
-
}
|
|
1210
|
-
async tabs(action, index, _opts) {
|
|
1211
|
-
if (!this.context) throw new Error("No context available");
|
|
1212
|
-
const pages = this.context.pages();
|
|
1213
|
-
switch (action) {
|
|
1214
|
-
case "list":
|
|
1215
|
-
return Promise.all(
|
|
1216
|
-
pages.map(async (p, i) => ({
|
|
1217
|
-
index: i,
|
|
1218
|
-
url: p.url(),
|
|
1219
|
-
title: await p.title().catch(() => "")
|
|
1220
|
-
}))
|
|
1221
|
-
);
|
|
1222
|
-
case "new": {
|
|
1223
|
-
const newPage = await this.context.newPage();
|
|
1224
|
-
this.page = newPage;
|
|
1225
|
-
newPage.on("dialog", (dialog) => this.pendingDialogs.push(dialog));
|
|
1226
|
-
return { index: pages.length };
|
|
1227
|
-
}
|
|
1228
|
-
case "close":
|
|
1229
|
-
if (index !== void 0 && pages[index]) {
|
|
1230
|
-
await pages[index].close();
|
|
1231
|
-
} else {
|
|
1232
|
-
await this.page?.close();
|
|
923
|
+
async fileUpload(paths, opts) {
|
|
924
|
+
const fileChooser = await this.getPage().waitForEvent("filechooser", {
|
|
925
|
+
timeout: opts?.timeoutMs ?? 3e4
|
|
926
|
+
});
|
|
927
|
+
await fileChooser.setFiles(paths);
|
|
928
|
+
}
|
|
929
|
+
async handleDialog(action, promptText, _opts) {
|
|
930
|
+
const dialog = this.pendingDialogs.shift();
|
|
931
|
+
if (dialog) {
|
|
932
|
+
if (action === "accept") {
|
|
933
|
+
await dialog.accept(promptText);
|
|
934
|
+
} else {
|
|
935
|
+
await dialog.dismiss();
|
|
936
|
+
}
|
|
1233
937
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
938
|
+
}
|
|
939
|
+
async waitFor(opts) {
|
|
940
|
+
const page = this.getPage();
|
|
941
|
+
const timeout = opts?.timeout ?? opts?.timeoutMs ?? 3e4;
|
|
942
|
+
if (opts?.timeSec) {
|
|
943
|
+
await page.waitForTimeout(opts.timeSec * 1e3);
|
|
944
|
+
return;
|
|
1239
945
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
946
|
+
if (opts?.text) {
|
|
947
|
+
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
if (opts?.textGone) {
|
|
951
|
+
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
if (opts?.selector) {
|
|
955
|
+
await page.locator(opts.selector).waitFor({
|
|
956
|
+
state: opts.state ?? "visible",
|
|
957
|
+
timeout
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
async closePage(_opts) {
|
|
962
|
+
await this.getPage().close();
|
|
963
|
+
this.page = null;
|
|
964
|
+
}
|
|
965
|
+
async resize(width, height, _opts) {
|
|
966
|
+
await this.getPage().setViewportSize({ width, height });
|
|
967
|
+
}
|
|
968
|
+
async tabs(action, index, _opts) {
|
|
969
|
+
if (!this.context) throw new Error("No context available");
|
|
970
|
+
const pages = this.context.pages();
|
|
971
|
+
switch (action) {
|
|
972
|
+
case "list":
|
|
973
|
+
return Promise.all(
|
|
974
|
+
pages.map(async (p, i) => ({
|
|
975
|
+
index: i,
|
|
976
|
+
url: p.url(),
|
|
977
|
+
title: await p.title().catch(() => "")
|
|
978
|
+
}))
|
|
979
|
+
);
|
|
980
|
+
case "new": {
|
|
981
|
+
const newPage = await this.context.newPage();
|
|
982
|
+
this.page = newPage;
|
|
983
|
+
newPage.on("dialog", (dialog) => this.pendingDialogs.push(dialog));
|
|
984
|
+
return { index: pages.length };
|
|
985
|
+
}
|
|
986
|
+
case "close":
|
|
987
|
+
if (index !== void 0 && pages[index]) {
|
|
988
|
+
await pages[index].close();
|
|
989
|
+
} else {
|
|
990
|
+
await this.page?.close();
|
|
991
|
+
}
|
|
992
|
+
this.page = this.context.pages()[0] ?? null;
|
|
993
|
+
break;
|
|
994
|
+
case "select":
|
|
995
|
+
if (index !== void 0 && pages[index]) {
|
|
996
|
+
this.page = pages[index];
|
|
997
|
+
}
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
async getStorageState(_opts) {
|
|
1003
|
+
if (!this.context) throw new Error("No context available");
|
|
1004
|
+
return this.context.storageState();
|
|
1005
|
+
}
|
|
1006
|
+
async getCurrentUrl(_opts) {
|
|
1007
|
+
return this.getPage().url();
|
|
1008
|
+
}
|
|
1009
|
+
async getTitle(_opts) {
|
|
1010
|
+
return this.getPage().title();
|
|
1011
|
+
}
|
|
1012
|
+
async getLinks(_opts) {
|
|
1013
|
+
const page = this.getPage();
|
|
1014
|
+
return page.$$eval(
|
|
1015
|
+
"a[href]",
|
|
1016
|
+
(links) => links.map((a) => a.href).filter((h) => !!h && (h.startsWith("http://") || h.startsWith("https://")))
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
async getElementBoundingBox(ref, _opts) {
|
|
1020
|
+
const locator = this.resolveRef(ref);
|
|
1021
|
+
const box = await locator.boundingBox();
|
|
1022
|
+
if (!box) return null;
|
|
1023
|
+
return { x: box.x, y: box.y, width: box.width, height: box.height };
|
|
1024
|
+
}
|
|
1025
|
+
async startTracing(_opts) {
|
|
1026
|
+
if (!this.context) throw new Error("No context available");
|
|
1027
|
+
await this.context.tracing.start({ screenshots: true, snapshots: true });
|
|
1028
|
+
}
|
|
1029
|
+
async stopTracing(_opts) {
|
|
1030
|
+
if (!this.context) throw new Error("No context available");
|
|
1031
|
+
const tracePath = `/tmp/trace-${Date.now()}.zip`;
|
|
1032
|
+
await this.context.tracing.stop({ path: tracePath });
|
|
1033
|
+
return {
|
|
1034
|
+
trace: tracePath,
|
|
1035
|
+
network: "",
|
|
1036
|
+
resources: "",
|
|
1037
|
+
directory: null,
|
|
1038
|
+
legend: `Trace saved to ${tracePath}`
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
// src/mcp.ts
|
|
1046
|
+
var mcp_exports = {};
|
|
1047
|
+
__export(mcp_exports, {
|
|
1048
|
+
runMcp: () => runMcp
|
|
1049
|
+
});
|
|
1050
|
+
import process7 from "process";
|
|
1051
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1052
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1053
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
1054
|
+
import { createParser as createParser2 } from "eventsource-parser";
|
|
1055
|
+
function resolveApiUrl(input) {
|
|
1056
|
+
return input ?? process7.env.CANARY_API_URL ?? DEFAULT_API_URL;
|
|
1057
|
+
}
|
|
1058
|
+
async function resolveToken() {
|
|
1059
|
+
const token = process7.env.CANARY_API_TOKEN ?? await readStoredToken();
|
|
1060
|
+
if (!token) {
|
|
1061
|
+
throw new Error("Missing token. Run `canary login` first or set CANARY_API_TOKEN.");
|
|
1062
|
+
}
|
|
1063
|
+
return token;
|
|
1064
|
+
}
|
|
1065
|
+
function toolText(text) {
|
|
1066
|
+
return { content: [{ type: "text", text }] };
|
|
1300
1067
|
}
|
|
1301
1068
|
function toolJson(data) {
|
|
1302
1069
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
@@ -1602,7 +1369,7 @@ async function streamUntilComplete(input) {
|
|
|
1602
1369
|
if (!response.body) return;
|
|
1603
1370
|
const reader = response.body.getReader();
|
|
1604
1371
|
const decoder = new TextDecoder();
|
|
1605
|
-
const parser =
|
|
1372
|
+
const parser = createParser2({
|
|
1606
1373
|
onEvent: (event) => {
|
|
1607
1374
|
if (event.event === "status") {
|
|
1608
1375
|
try {
|
|
@@ -1617,37 +1384,461 @@ async function streamUntilComplete(input) {
|
|
|
1617
1384
|
reader.cancel().catch(() => void 0);
|
|
1618
1385
|
}
|
|
1619
1386
|
}
|
|
1620
|
-
});
|
|
1387
|
+
});
|
|
1388
|
+
while (true) {
|
|
1389
|
+
const { done, value } = await reader.read();
|
|
1390
|
+
if (done) break;
|
|
1391
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
function formatReport(input) {
|
|
1395
|
+
if (!input.summary) {
|
|
1396
|
+
return {
|
|
1397
|
+
runId: input.run?.id,
|
|
1398
|
+
status: input.run?.status ?? "unknown",
|
|
1399
|
+
summary: "No final report available."
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
const tested = Array.isArray(input.summary.testedItems) ? input.summary.testedItems : [];
|
|
1403
|
+
const status = input.summary.status ?? input.run?.status ?? "unknown";
|
|
1404
|
+
const issues = status === "issues_found" ? input.summary.notes ? [input.summary.notes] : ["Issues reported."] : [];
|
|
1405
|
+
return {
|
|
1406
|
+
runId: input.run?.id,
|
|
1407
|
+
status,
|
|
1408
|
+
summary: input.summary.summary ?? "Run completed.",
|
|
1409
|
+
testedItems: tested,
|
|
1410
|
+
issues,
|
|
1411
|
+
notes: input.summary.notes ?? null
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
var browserSessions, DEFAULT_API_URL;
|
|
1415
|
+
var init_mcp = __esm({
|
|
1416
|
+
"src/mcp.ts"() {
|
|
1417
|
+
"use strict";
|
|
1418
|
+
init_auth();
|
|
1419
|
+
init_local_run();
|
|
1420
|
+
init_tunnel();
|
|
1421
|
+
init_host();
|
|
1422
|
+
browserSessions = /* @__PURE__ */ new Map();
|
|
1423
|
+
DEFAULT_API_URL = "https://api.trycanary.ai";
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
// src/local-browser/index.ts
|
|
1428
|
+
var local_browser_exports = {};
|
|
1429
|
+
__export(local_browser_exports, {
|
|
1430
|
+
runLocalBrowser: () => runLocalBrowser
|
|
1431
|
+
});
|
|
1432
|
+
import process8 from "process";
|
|
1433
|
+
function parseArgs(args) {
|
|
1434
|
+
const options = {
|
|
1435
|
+
mode: "playwright",
|
|
1436
|
+
headless: true,
|
|
1437
|
+
apiUrl: process8.env.CANARY_API_URL ?? DEFAULT_API_URL2
|
|
1438
|
+
};
|
|
1439
|
+
for (let i = 0; i < args.length; i++) {
|
|
1440
|
+
const arg = args[i];
|
|
1441
|
+
const nextArg = args[i + 1];
|
|
1442
|
+
switch (arg) {
|
|
1443
|
+
case "--mode":
|
|
1444
|
+
if (nextArg === "playwright" || nextArg === "cdp") {
|
|
1445
|
+
options.mode = nextArg;
|
|
1446
|
+
i++;
|
|
1447
|
+
}
|
|
1448
|
+
break;
|
|
1449
|
+
case "--cdp-url":
|
|
1450
|
+
options.cdpUrl = nextArg;
|
|
1451
|
+
options.mode = "cdp";
|
|
1452
|
+
i++;
|
|
1453
|
+
break;
|
|
1454
|
+
case "--headless":
|
|
1455
|
+
options.headless = true;
|
|
1456
|
+
break;
|
|
1457
|
+
case "--no-headless":
|
|
1458
|
+
options.headless = false;
|
|
1459
|
+
break;
|
|
1460
|
+
case "--storage-state":
|
|
1461
|
+
options.storageStatePath = nextArg;
|
|
1462
|
+
i++;
|
|
1463
|
+
break;
|
|
1464
|
+
case "--api-url":
|
|
1465
|
+
options.apiUrl = nextArg;
|
|
1466
|
+
i++;
|
|
1467
|
+
break;
|
|
1468
|
+
case "--instructions":
|
|
1469
|
+
options.instructions = nextArg;
|
|
1470
|
+
i++;
|
|
1471
|
+
break;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return options;
|
|
1475
|
+
}
|
|
1476
|
+
async function resolveToken2() {
|
|
1477
|
+
const token = process8.env.CANARY_API_TOKEN ?? await readStoredToken();
|
|
1478
|
+
if (!token) {
|
|
1479
|
+
throw new Error("Missing token. Run `canary login` first or set CANARY_API_TOKEN.");
|
|
1480
|
+
}
|
|
1481
|
+
return token;
|
|
1482
|
+
}
|
|
1483
|
+
async function createSession(apiUrl, token, options) {
|
|
1484
|
+
const response = await fetch(`${apiUrl}/local-browser/sessions`, {
|
|
1485
|
+
method: "POST",
|
|
1486
|
+
headers: {
|
|
1487
|
+
"Content-Type": "application/json",
|
|
1488
|
+
Authorization: `Bearer ${token}`
|
|
1489
|
+
},
|
|
1490
|
+
body: JSON.stringify({
|
|
1491
|
+
browserMode: options.mode,
|
|
1492
|
+
instructions: options.instructions ?? null
|
|
1493
|
+
})
|
|
1494
|
+
});
|
|
1495
|
+
if (!response.ok) {
|
|
1496
|
+
const text = await response.text();
|
|
1497
|
+
throw new Error(`Failed to create session: ${response.status} ${text}`);
|
|
1498
|
+
}
|
|
1499
|
+
return response.json();
|
|
1500
|
+
}
|
|
1501
|
+
async function runLocalBrowser(args) {
|
|
1502
|
+
const options = parseArgs(args);
|
|
1503
|
+
console.log("Starting local browser...");
|
|
1504
|
+
console.log(` Mode: ${options.mode}`);
|
|
1505
|
+
if (options.cdpUrl) {
|
|
1506
|
+
console.log(` CDP URL: ${options.cdpUrl}`);
|
|
1507
|
+
}
|
|
1508
|
+
console.log(` Headless: ${options.headless}`);
|
|
1509
|
+
console.log(` API URL: ${options.apiUrl}`);
|
|
1510
|
+
console.log();
|
|
1511
|
+
const token = await resolveToken2();
|
|
1512
|
+
console.log("Creating session with cloud API...");
|
|
1513
|
+
const session = await createSession(options.apiUrl, token, options);
|
|
1514
|
+
if (!session.ok) {
|
|
1515
|
+
throw new Error(`Failed to create session: ${session.error}`);
|
|
1516
|
+
}
|
|
1517
|
+
console.log(`Session created: ${session.sessionId}`);
|
|
1518
|
+
console.log(`Expires at: ${session.expiresAt}`);
|
|
1519
|
+
console.log();
|
|
1520
|
+
const host = new LocalBrowserHost({
|
|
1521
|
+
apiUrl: options.apiUrl,
|
|
1522
|
+
wsToken: session.wsToken,
|
|
1523
|
+
sessionId: session.sessionId,
|
|
1524
|
+
browserMode: options.mode,
|
|
1525
|
+
cdpUrl: options.cdpUrl,
|
|
1526
|
+
headless: options.headless,
|
|
1527
|
+
storageStatePath: options.storageStatePath,
|
|
1528
|
+
onLog: (level, message, data) => {
|
|
1529
|
+
const prefix = `[${level.toUpperCase()}]`;
|
|
1530
|
+
if (data) {
|
|
1531
|
+
console.log(prefix, message, data);
|
|
1532
|
+
} else {
|
|
1533
|
+
console.log(prefix, message);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
const shutdown = async () => {
|
|
1538
|
+
console.log("\nShutting down...");
|
|
1539
|
+
await host.stop();
|
|
1540
|
+
process8.exit(0);
|
|
1541
|
+
};
|
|
1542
|
+
process8.on("SIGINT", shutdown);
|
|
1543
|
+
process8.on("SIGTERM", shutdown);
|
|
1544
|
+
try {
|
|
1545
|
+
await host.start();
|
|
1546
|
+
console.log();
|
|
1547
|
+
console.log("Local browser is ready and connected to cloud.");
|
|
1548
|
+
console.log("Press Ctrl+C to stop.");
|
|
1549
|
+
console.log();
|
|
1550
|
+
await new Promise(() => {
|
|
1551
|
+
});
|
|
1552
|
+
} catch (error) {
|
|
1553
|
+
console.error("Failed to start local browser:", error);
|
|
1554
|
+
await host.stop();
|
|
1555
|
+
process8.exit(1);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
var DEFAULT_API_URL2;
|
|
1559
|
+
var init_local_browser = __esm({
|
|
1560
|
+
"src/local-browser/index.ts"() {
|
|
1561
|
+
"use strict";
|
|
1562
|
+
init_auth();
|
|
1563
|
+
init_host();
|
|
1564
|
+
DEFAULT_API_URL2 = "https://api.trycanary.ai";
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
// src/index.ts
|
|
1569
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
1570
|
+
import process9 from "process";
|
|
1571
|
+
import path5 from "path";
|
|
1572
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "url";
|
|
1573
|
+
|
|
1574
|
+
// src/runner/common.ts
|
|
1575
|
+
import { spawnSync } from "child_process";
|
|
1576
|
+
import fs from "fs";
|
|
1577
|
+
import path from "path";
|
|
1578
|
+
import { createRequire } from "module";
|
|
1579
|
+
import { pathToFileURL } from "url";
|
|
1580
|
+
function makeRequire() {
|
|
1581
|
+
try {
|
|
1582
|
+
return createRequire(import.meta.url);
|
|
1583
|
+
} catch {
|
|
1584
|
+
try {
|
|
1585
|
+
return createRequire(process.cwd());
|
|
1586
|
+
} catch {
|
|
1587
|
+
return typeof __require !== "undefined" ? __require : createRequire(".");
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
function resolveRunner(preloadPath2) {
|
|
1592
|
+
const { bin, version } = pickNodeBinary();
|
|
1593
|
+
const supportsImport = typeof version === "number" && version >= 18;
|
|
1594
|
+
if (supportsImport && preloadPath2 && fs.existsSync(preloadPath2)) {
|
|
1595
|
+
return { runnerBin: bin, preloadFlag: `--import=${pathToFileURL(preloadPath2).href}` };
|
|
1596
|
+
}
|
|
1597
|
+
if (preloadPath2) {
|
|
1598
|
+
console.warn("[canary] Warning: no preload module found; instrumentation may be disabled.");
|
|
1599
|
+
}
|
|
1600
|
+
return { runnerBin: bin };
|
|
1601
|
+
}
|
|
1602
|
+
function pickNodeBinary() {
|
|
1603
|
+
const candidates = collectNodeCandidates();
|
|
1604
|
+
let best;
|
|
1605
|
+
let fallback;
|
|
1606
|
+
for (const bin of candidates) {
|
|
1607
|
+
const version = getNodeMajor(bin);
|
|
1608
|
+
if (!version) continue;
|
|
1609
|
+
const current = { bin, version };
|
|
1610
|
+
if (version >= 18 && !fallback) {
|
|
1611
|
+
fallback = current;
|
|
1612
|
+
}
|
|
1613
|
+
if (!best || version > (best.version ?? 0)) {
|
|
1614
|
+
best = current;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (fallback) return fallback;
|
|
1618
|
+
if (best) return best;
|
|
1619
|
+
return { bin: candidates[0] ?? "node" };
|
|
1620
|
+
}
|
|
1621
|
+
function collectNodeCandidates() {
|
|
1622
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1623
|
+
const push = (value) => {
|
|
1624
|
+
if (!value) return;
|
|
1625
|
+
if (seen.has(value)) return;
|
|
1626
|
+
seen.add(value);
|
|
1627
|
+
};
|
|
1628
|
+
const isBun = path.basename(process.execPath).includes("bun");
|
|
1629
|
+
push(process.env.CANARY_NODE_BIN);
|
|
1630
|
+
push(isBun ? void 0 : process.execPath);
|
|
1631
|
+
push("node");
|
|
1632
|
+
try {
|
|
1633
|
+
const which = spawnSync("which", ["-a", "node"], { encoding: "utf-8" });
|
|
1634
|
+
which.stdout?.toString().split("\n").map((line) => line.trim()).forEach((line) => push(line));
|
|
1635
|
+
} catch {
|
|
1636
|
+
}
|
|
1637
|
+
const nvmDir = process.env.NVM_DIR || (process.env.HOME ? path.join(process.env.HOME, ".nvm") : void 0);
|
|
1638
|
+
if (nvmDir) {
|
|
1639
|
+
const versionsDir = path.join(nvmDir, "versions", "node");
|
|
1640
|
+
if (fs.existsSync(versionsDir)) {
|
|
1641
|
+
try {
|
|
1642
|
+
const versions = fs.readdirSync(versionsDir);
|
|
1643
|
+
versions.sort((a, b) => a > b ? -1 : 1).forEach((v) => push(path.join(versionsDir, v, "bin", "node")));
|
|
1644
|
+
} catch {
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return Array.from(seen);
|
|
1649
|
+
}
|
|
1650
|
+
function getNodeMajor(bin) {
|
|
1651
|
+
try {
|
|
1652
|
+
const result = spawnSync(bin, ["-v"], { encoding: "utf-8" });
|
|
1653
|
+
const output = (result.stdout || result.stderr || "").toString().trim();
|
|
1654
|
+
const match = output.match(/^v(\d+)/);
|
|
1655
|
+
if (match) return Number(match[1]);
|
|
1656
|
+
} catch {
|
|
1657
|
+
}
|
|
1658
|
+
return void 0;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/run.ts
|
|
1662
|
+
import { spawn } from "child_process";
|
|
1663
|
+
import fs2 from "fs";
|
|
1664
|
+
import os from "os";
|
|
1665
|
+
import path2 from "path";
|
|
1666
|
+
import { fileURLToPath } from "url";
|
|
1667
|
+
|
|
1668
|
+
// src/index.ts
|
|
1669
|
+
init_local_run();
|
|
1670
|
+
init_tunnel();
|
|
1671
|
+
|
|
1672
|
+
// src/login.ts
|
|
1673
|
+
import fs4 from "fs/promises";
|
|
1674
|
+
import os4 from "os";
|
|
1675
|
+
import path4 from "path";
|
|
1676
|
+
import process4 from "process";
|
|
1677
|
+
import { spawn as spawn2 } from "child_process";
|
|
1678
|
+
var DEFAULT_APP_URL = "https://app.trycanary.ai";
|
|
1679
|
+
function getArgValue3(argv, key) {
|
|
1680
|
+
const index = argv.indexOf(key);
|
|
1681
|
+
if (index === -1) return void 0;
|
|
1682
|
+
return argv[index + 1];
|
|
1683
|
+
}
|
|
1684
|
+
function shouldOpenBrowser(argv) {
|
|
1685
|
+
return !argv.includes("--no-open");
|
|
1686
|
+
}
|
|
1687
|
+
function openUrl(url) {
|
|
1688
|
+
const platform = process4.platform;
|
|
1689
|
+
if (platform === "darwin") {
|
|
1690
|
+
spawn2("open", [url], { stdio: "ignore" });
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
1693
|
+
if (platform === "win32") {
|
|
1694
|
+
spawn2("cmd", ["/c", "start", "", url], { stdio: "ignore" });
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
spawn2("xdg-open", [url], { stdio: "ignore" });
|
|
1698
|
+
}
|
|
1699
|
+
async function writeToken(token) {
|
|
1700
|
+
const dir = path4.join(os4.homedir(), ".config", "canary-cli");
|
|
1701
|
+
const filePath = path4.join(dir, "auth.json");
|
|
1702
|
+
await fs4.mkdir(dir, { recursive: true });
|
|
1703
|
+
await fs4.writeFile(filePath, JSON.stringify({ token }, null, 2), "utf8");
|
|
1704
|
+
return filePath;
|
|
1705
|
+
}
|
|
1706
|
+
async function runLogin(argv) {
|
|
1707
|
+
const apiUrl = getArgValue3(argv, "--api-url") ?? process4.env.CANARY_API_URL ?? "https://api.trycanary.ai";
|
|
1708
|
+
const appUrl = getArgValue3(argv, "--app-url") ?? process4.env.CANARY_APP_URL ?? DEFAULT_APP_URL;
|
|
1709
|
+
const startRes = await fetch(`${apiUrl}/cli-login/start`, {
|
|
1710
|
+
method: "POST",
|
|
1711
|
+
headers: { "content-type": "application/json" },
|
|
1712
|
+
body: JSON.stringify({ appUrl })
|
|
1713
|
+
});
|
|
1714
|
+
const startJson = await startRes.json();
|
|
1715
|
+
if (!startRes.ok || !startJson.ok || !startJson.deviceCode || !startJson.userCode) {
|
|
1716
|
+
console.error("Login start failed", startJson.error ?? startRes.statusText);
|
|
1717
|
+
process4.exit(1);
|
|
1718
|
+
}
|
|
1719
|
+
console.log("Login required.");
|
|
1720
|
+
console.log(`User code: ${startJson.userCode}`);
|
|
1721
|
+
if (startJson.verificationUrl) {
|
|
1722
|
+
console.log(`Open: ${startJson.verificationUrl}`);
|
|
1723
|
+
if (shouldOpenBrowser(argv)) {
|
|
1724
|
+
try {
|
|
1725
|
+
openUrl(startJson.verificationUrl);
|
|
1726
|
+
} catch {
|
|
1727
|
+
console.log("Unable to open browser automatically. Please open the URL manually.");
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
const intervalMs = (startJson.intervalSeconds ?? 3) * 1e3;
|
|
1732
|
+
const expiresAt = startJson.expiresAt ? new Date(startJson.expiresAt).getTime() : null;
|
|
1621
1733
|
while (true) {
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1734
|
+
if (expiresAt && Date.now() > expiresAt) {
|
|
1735
|
+
console.error("Login code expired.");
|
|
1736
|
+
process4.exit(1);
|
|
1737
|
+
}
|
|
1738
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1739
|
+
const pollRes = await fetch(`${apiUrl}/cli-login/poll`, {
|
|
1740
|
+
method: "POST",
|
|
1741
|
+
headers: { "content-type": "application/json" },
|
|
1742
|
+
body: JSON.stringify({ deviceCode: startJson.deviceCode })
|
|
1743
|
+
});
|
|
1744
|
+
const pollJson = await pollRes.json();
|
|
1745
|
+
if (!pollRes.ok || !pollJson.ok) {
|
|
1746
|
+
console.error("Login poll failed", pollJson.error ?? pollRes.statusText);
|
|
1747
|
+
process4.exit(1);
|
|
1748
|
+
}
|
|
1749
|
+
if (pollJson.status === "approved" && pollJson.accessToken) {
|
|
1750
|
+
const filePath = await writeToken(pollJson.accessToken);
|
|
1751
|
+
console.log(`Login successful. Token saved to ${filePath}`);
|
|
1752
|
+
console.log("Set CANARY_API_TOKEN to use the CLI without re-login.");
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (pollJson.status === "rejected") {
|
|
1756
|
+
console.error("Login rejected.");
|
|
1757
|
+
process4.exit(1);
|
|
1758
|
+
}
|
|
1759
|
+
if (pollJson.status === "expired") {
|
|
1760
|
+
console.error("Login expired.");
|
|
1761
|
+
process4.exit(1);
|
|
1762
|
+
}
|
|
1625
1763
|
}
|
|
1626
1764
|
}
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1765
|
+
|
|
1766
|
+
// src/run-local.ts
|
|
1767
|
+
init_auth();
|
|
1768
|
+
init_local_run();
|
|
1769
|
+
init_tunnel();
|
|
1770
|
+
import process5 from "process";
|
|
1771
|
+
function getArgValue4(argv, key) {
|
|
1772
|
+
const index = argv.indexOf(key);
|
|
1773
|
+
if (index === -1) return void 0;
|
|
1774
|
+
return argv[index + 1];
|
|
1775
|
+
}
|
|
1776
|
+
async function runLocalSession(argv) {
|
|
1777
|
+
const apiUrl = getArgValue4(argv, "--api-url") ?? process5.env.CANARY_API_URL ?? "https://api.trycanary.ai";
|
|
1778
|
+
const token = getArgValue4(argv, "--token") ?? process5.env.CANARY_API_TOKEN ?? await readStoredToken();
|
|
1779
|
+
if (!token) {
|
|
1780
|
+
console.error("Missing token. Run `canary login` first or set CANARY_API_TOKEN.");
|
|
1781
|
+
process5.exit(1);
|
|
1782
|
+
}
|
|
1783
|
+
const portRaw = getArgValue4(argv, "--port") ?? process5.env.CANARY_LOCAL_PORT;
|
|
1784
|
+
const tunnelUrl = getArgValue4(argv, "--tunnel-url");
|
|
1785
|
+
const title = getArgValue4(argv, "--title");
|
|
1786
|
+
const featureSpec = getArgValue4(argv, "--feature");
|
|
1787
|
+
const startUrl = getArgValue4(argv, "--start-url");
|
|
1788
|
+
if (!tunnelUrl && !portRaw) {
|
|
1789
|
+
console.error("Missing --port or --tunnel-url");
|
|
1790
|
+
process5.exit(1);
|
|
1791
|
+
}
|
|
1792
|
+
let publicUrl = tunnelUrl;
|
|
1793
|
+
let ws = null;
|
|
1794
|
+
if (!publicUrl && portRaw) {
|
|
1795
|
+
const port = Number(portRaw);
|
|
1796
|
+
if (Number.isNaN(port) || port <= 0) {
|
|
1797
|
+
console.error("Invalid --port value");
|
|
1798
|
+
process5.exit(1);
|
|
1799
|
+
}
|
|
1800
|
+
const tunnel = await createTunnel({ apiUrl, token, port });
|
|
1801
|
+
publicUrl = tunnel.publicUrl;
|
|
1802
|
+
ws = connectTunnel({
|
|
1803
|
+
apiUrl,
|
|
1804
|
+
tunnelId: tunnel.tunnelId,
|
|
1805
|
+
token: tunnel.token,
|
|
1806
|
+
port,
|
|
1807
|
+
onReady: () => {
|
|
1808
|
+
console.log(`Tunnel connected: ${publicUrl ?? tunnel.tunnelId}`);
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
if (!publicUrl) {
|
|
1813
|
+
console.error("Failed to resolve tunnel URL");
|
|
1814
|
+
process5.exit(1);
|
|
1815
|
+
}
|
|
1816
|
+
const run2 = await createLocalRun({
|
|
1817
|
+
apiUrl,
|
|
1818
|
+
token,
|
|
1819
|
+
title,
|
|
1820
|
+
featureSpec,
|
|
1821
|
+
startUrl,
|
|
1822
|
+
tunnelUrl: publicUrl
|
|
1823
|
+
});
|
|
1824
|
+
console.log(`Local test queued: ${run2.runId}`);
|
|
1825
|
+
if (run2.watchUrl) {
|
|
1826
|
+
console.log(`Watch: ${run2.watchUrl}`);
|
|
1827
|
+
}
|
|
1828
|
+
if (ws) {
|
|
1829
|
+
console.log("Tunnel active. Press Ctrl+C to stop.");
|
|
1830
|
+
process5.on("SIGINT", () => {
|
|
1831
|
+
ws?.close();
|
|
1832
|
+
process5.exit(0);
|
|
1833
|
+
});
|
|
1834
|
+
await new Promise(() => void 0);
|
|
1634
1835
|
}
|
|
1635
|
-
const tested = Array.isArray(input.summary.testedItems) ? input.summary.testedItems : [];
|
|
1636
|
-
const status = input.summary.status ?? input.run?.status ?? "unknown";
|
|
1637
|
-
const issues = status === "issues_found" ? input.summary.notes ? [input.summary.notes] : ["Issues reported."] : [];
|
|
1638
|
-
return {
|
|
1639
|
-
runId: input.run?.id,
|
|
1640
|
-
status,
|
|
1641
|
-
summary: input.summary.summary ?? "Run completed.",
|
|
1642
|
-
testedItems: tested,
|
|
1643
|
-
issues,
|
|
1644
|
-
notes: input.summary.notes ?? null
|
|
1645
|
-
};
|
|
1646
1836
|
}
|
|
1647
1837
|
|
|
1648
1838
|
// src/remote-test.ts
|
|
1649
|
-
|
|
1650
|
-
import
|
|
1839
|
+
init_auth();
|
|
1840
|
+
import process6 from "process";
|
|
1841
|
+
import { createParser } from "eventsource-parser";
|
|
1651
1842
|
function getArgValue5(argv, key) {
|
|
1652
1843
|
const index = argv.indexOf(key);
|
|
1653
1844
|
if (index === -1 || index >= argv.length - 1) return void 0;
|
|
@@ -1657,8 +1848,8 @@ function hasFlag(argv, ...flags) {
|
|
|
1657
1848
|
return flags.some((flag) => argv.includes(flag));
|
|
1658
1849
|
}
|
|
1659
1850
|
async function runRemoteTest(argv) {
|
|
1660
|
-
const apiUrl = getArgValue5(argv, "--api-url") ??
|
|
1661
|
-
const token = getArgValue5(argv, "--token") ??
|
|
1851
|
+
const apiUrl = getArgValue5(argv, "--api-url") ?? process6.env.CANARY_API_URL ?? "https://api.trycanary.ai";
|
|
1852
|
+
const token = getArgValue5(argv, "--token") ?? process6.env.CANARY_API_TOKEN ?? await readStoredToken();
|
|
1662
1853
|
const tag = getArgValue5(argv, "--tag");
|
|
1663
1854
|
const namePattern = getArgValue5(argv, "--name-pattern");
|
|
1664
1855
|
const verbose = hasFlag(argv, "--verbose", "-v");
|
|
@@ -1670,7 +1861,7 @@ async function runRemoteTest(argv) {
|
|
|
1670
1861
|
console.error("");
|
|
1671
1862
|
console.error("Or create an API key in Settings > API Keys and pass it:");
|
|
1672
1863
|
console.error(" canary test --remote --token cnry_...");
|
|
1673
|
-
|
|
1864
|
+
process6.exit(1);
|
|
1674
1865
|
}
|
|
1675
1866
|
console.log("Starting remote workflow tests...");
|
|
1676
1867
|
if (tag) console.log(` Filtering by tag: ${tag}`);
|
|
@@ -1691,18 +1882,18 @@ async function runRemoteTest(argv) {
|
|
|
1691
1882
|
});
|
|
1692
1883
|
} catch (err) {
|
|
1693
1884
|
console.error(`Failed to connect to API: ${err}`);
|
|
1694
|
-
|
|
1885
|
+
process6.exit(1);
|
|
1695
1886
|
}
|
|
1696
1887
|
if (!triggerRes.ok) {
|
|
1697
1888
|
const errorText = await triggerRes.text();
|
|
1698
1889
|
console.error(`Failed to start tests: ${triggerRes.status}`);
|
|
1699
1890
|
console.error(errorText);
|
|
1700
|
-
|
|
1891
|
+
process6.exit(1);
|
|
1701
1892
|
}
|
|
1702
1893
|
const triggerData = await triggerRes.json();
|
|
1703
1894
|
if (!triggerData.ok || !triggerData.suiteId) {
|
|
1704
1895
|
console.error(`Failed to start tests: ${triggerData.error ?? "Unknown error"}`);
|
|
1705
|
-
|
|
1896
|
+
process6.exit(1);
|
|
1706
1897
|
}
|
|
1707
1898
|
const { suiteId, jobId } = triggerData;
|
|
1708
1899
|
if (verbose) {
|
|
@@ -1721,11 +1912,11 @@ async function runRemoteTest(argv) {
|
|
|
1721
1912
|
});
|
|
1722
1913
|
} catch (err) {
|
|
1723
1914
|
console.error(`Failed to connect to event stream: ${err}`);
|
|
1724
|
-
|
|
1915
|
+
process6.exit(1);
|
|
1725
1916
|
}
|
|
1726
1917
|
if (!streamRes.ok || !streamRes.body) {
|
|
1727
1918
|
console.error(`Failed to connect to event stream: ${streamRes.status}`);
|
|
1728
|
-
|
|
1919
|
+
process6.exit(1);
|
|
1729
1920
|
}
|
|
1730
1921
|
let exitCode = 0;
|
|
1731
1922
|
let hasCompleted = false;
|
|
@@ -1734,7 +1925,7 @@ async function runRemoteTest(argv) {
|
|
|
1734
1925
|
let completedWorkflows = 0;
|
|
1735
1926
|
let failedWorkflows = 0;
|
|
1736
1927
|
let successfulWorkflows = 0;
|
|
1737
|
-
const parser =
|
|
1928
|
+
const parser = createParser({
|
|
1738
1929
|
onEvent: (event) => {
|
|
1739
1930
|
if (!event.data) return;
|
|
1740
1931
|
try {
|
|
@@ -1799,7 +1990,7 @@ async function runRemoteTest(argv) {
|
|
|
1799
1990
|
console.log("\u2500".repeat(50));
|
|
1800
1991
|
if (totalWorkflows === 0) {
|
|
1801
1992
|
console.log("No workflows found matching the filter criteria.");
|
|
1802
|
-
|
|
1993
|
+
process6.exit(0);
|
|
1803
1994
|
}
|
|
1804
1995
|
const passRate = totalWorkflows > 0 ? Math.round(successfulWorkflows / totalWorkflows * 100) : 0;
|
|
1805
1996
|
if (failedWorkflows > 0) {
|
|
@@ -1812,139 +2003,12 @@ async function runRemoteTest(argv) {
|
|
|
1812
2003
|
if (waitingWorkflows > 0) {
|
|
1813
2004
|
console.log(`Note: ${waitingWorkflows} workflow(s) are still waiting (scheduled for later)`);
|
|
1814
2005
|
}
|
|
1815
|
-
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
// src/local-browser/index.ts
|
|
1819
|
-
import process8 from "process";
|
|
1820
|
-
var DEFAULT_API_URL2 = "https://api.trycanary.ai";
|
|
1821
|
-
function parseArgs(args) {
|
|
1822
|
-
const options = {
|
|
1823
|
-
mode: "playwright",
|
|
1824
|
-
headless: true,
|
|
1825
|
-
apiUrl: process8.env.CANARY_API_URL ?? DEFAULT_API_URL2
|
|
1826
|
-
};
|
|
1827
|
-
for (let i = 0; i < args.length; i++) {
|
|
1828
|
-
const arg = args[i];
|
|
1829
|
-
const nextArg = args[i + 1];
|
|
1830
|
-
switch (arg) {
|
|
1831
|
-
case "--mode":
|
|
1832
|
-
if (nextArg === "playwright" || nextArg === "cdp") {
|
|
1833
|
-
options.mode = nextArg;
|
|
1834
|
-
i++;
|
|
1835
|
-
}
|
|
1836
|
-
break;
|
|
1837
|
-
case "--cdp-url":
|
|
1838
|
-
options.cdpUrl = nextArg;
|
|
1839
|
-
options.mode = "cdp";
|
|
1840
|
-
i++;
|
|
1841
|
-
break;
|
|
1842
|
-
case "--headless":
|
|
1843
|
-
options.headless = true;
|
|
1844
|
-
break;
|
|
1845
|
-
case "--no-headless":
|
|
1846
|
-
options.headless = false;
|
|
1847
|
-
break;
|
|
1848
|
-
case "--storage-state":
|
|
1849
|
-
options.storageStatePath = nextArg;
|
|
1850
|
-
i++;
|
|
1851
|
-
break;
|
|
1852
|
-
case "--api-url":
|
|
1853
|
-
options.apiUrl = nextArg;
|
|
1854
|
-
i++;
|
|
1855
|
-
break;
|
|
1856
|
-
case "--instructions":
|
|
1857
|
-
options.instructions = nextArg;
|
|
1858
|
-
i++;
|
|
1859
|
-
break;
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
return options;
|
|
1863
|
-
}
|
|
1864
|
-
async function resolveToken2() {
|
|
1865
|
-
const token = process8.env.CANARY_API_TOKEN ?? await readStoredToken();
|
|
1866
|
-
if (!token) {
|
|
1867
|
-
throw new Error("Missing token. Run `canary login` first or set CANARY_API_TOKEN.");
|
|
1868
|
-
}
|
|
1869
|
-
return token;
|
|
1870
|
-
}
|
|
1871
|
-
async function createSession(apiUrl, token, options) {
|
|
1872
|
-
const response = await fetch(`${apiUrl}/local-browser/sessions`, {
|
|
1873
|
-
method: "POST",
|
|
1874
|
-
headers: {
|
|
1875
|
-
"Content-Type": "application/json",
|
|
1876
|
-
Authorization: `Bearer ${token}`
|
|
1877
|
-
},
|
|
1878
|
-
body: JSON.stringify({
|
|
1879
|
-
browserMode: options.mode,
|
|
1880
|
-
instructions: options.instructions ?? null
|
|
1881
|
-
})
|
|
1882
|
-
});
|
|
1883
|
-
if (!response.ok) {
|
|
1884
|
-
const text = await response.text();
|
|
1885
|
-
throw new Error(`Failed to create session: ${response.status} ${text}`);
|
|
1886
|
-
}
|
|
1887
|
-
return response.json();
|
|
1888
|
-
}
|
|
1889
|
-
async function runLocalBrowser(args) {
|
|
1890
|
-
const options = parseArgs(args);
|
|
1891
|
-
console.log("Starting local browser...");
|
|
1892
|
-
console.log(` Mode: ${options.mode}`);
|
|
1893
|
-
if (options.cdpUrl) {
|
|
1894
|
-
console.log(` CDP URL: ${options.cdpUrl}`);
|
|
1895
|
-
}
|
|
1896
|
-
console.log(` Headless: ${options.headless}`);
|
|
1897
|
-
console.log(` API URL: ${options.apiUrl}`);
|
|
1898
|
-
console.log();
|
|
1899
|
-
const token = await resolveToken2();
|
|
1900
|
-
console.log("Creating session with cloud API...");
|
|
1901
|
-
const session = await createSession(options.apiUrl, token, options);
|
|
1902
|
-
if (!session.ok) {
|
|
1903
|
-
throw new Error(`Failed to create session: ${session.error}`);
|
|
1904
|
-
}
|
|
1905
|
-
console.log(`Session created: ${session.sessionId}`);
|
|
1906
|
-
console.log(`Expires at: ${session.expiresAt}`);
|
|
1907
|
-
console.log();
|
|
1908
|
-
const host = new LocalBrowserHost({
|
|
1909
|
-
apiUrl: options.apiUrl,
|
|
1910
|
-
wsToken: session.wsToken,
|
|
1911
|
-
sessionId: session.sessionId,
|
|
1912
|
-
browserMode: options.mode,
|
|
1913
|
-
cdpUrl: options.cdpUrl,
|
|
1914
|
-
headless: options.headless,
|
|
1915
|
-
storageStatePath: options.storageStatePath,
|
|
1916
|
-
onLog: (level, message, data) => {
|
|
1917
|
-
const prefix = `[${level.toUpperCase()}]`;
|
|
1918
|
-
if (data) {
|
|
1919
|
-
console.log(prefix, message, data);
|
|
1920
|
-
} else {
|
|
1921
|
-
console.log(prefix, message);
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
1924
|
-
});
|
|
1925
|
-
const shutdown = async () => {
|
|
1926
|
-
console.log("\nShutting down...");
|
|
1927
|
-
await host.stop();
|
|
1928
|
-
process8.exit(0);
|
|
1929
|
-
};
|
|
1930
|
-
process8.on("SIGINT", shutdown);
|
|
1931
|
-
process8.on("SIGTERM", shutdown);
|
|
1932
|
-
try {
|
|
1933
|
-
await host.start();
|
|
1934
|
-
console.log();
|
|
1935
|
-
console.log("Local browser is ready and connected to cloud.");
|
|
1936
|
-
console.log("Press Ctrl+C to stop.");
|
|
1937
|
-
console.log();
|
|
1938
|
-
await new Promise(() => {
|
|
1939
|
-
});
|
|
1940
|
-
} catch (error) {
|
|
1941
|
-
console.error("Failed to start local browser:", error);
|
|
1942
|
-
await host.stop();
|
|
1943
|
-
process8.exit(1);
|
|
1944
|
-
}
|
|
2006
|
+
process6.exit(exitCode);
|
|
1945
2007
|
}
|
|
1946
2008
|
|
|
1947
2009
|
// src/index.ts
|
|
2010
|
+
var loadMcp = () => Promise.resolve().then(() => (init_mcp(), mcp_exports)).then((m) => m.runMcp);
|
|
2011
|
+
var loadLocalBrowser = () => Promise.resolve().then(() => (init_local_browser(), local_browser_exports)).then((m) => m.runLocalBrowser);
|
|
1948
2012
|
var baseDir = typeof __dirname !== "undefined" ? __dirname : path5.dirname(fileURLToPath2(import.meta.url));
|
|
1949
2013
|
var preloadPath = path5.join(baseDir, "runner", "preload.js");
|
|
1950
2014
|
var requireFn = makeRequire();
|
|
@@ -2032,7 +2096,8 @@ async function main(argv) {
|
|
|
2032
2096
|
return;
|
|
2033
2097
|
}
|
|
2034
2098
|
if (command === "mcp") {
|
|
2035
|
-
await
|
|
2099
|
+
const runMcp2 = await loadMcp();
|
|
2100
|
+
await runMcp2(rest);
|
|
2036
2101
|
return;
|
|
2037
2102
|
}
|
|
2038
2103
|
if (command === "tunnel") {
|
|
@@ -2044,7 +2109,8 @@ async function main(argv) {
|
|
|
2044
2109
|
return;
|
|
2045
2110
|
}
|
|
2046
2111
|
if (command === "browser") {
|
|
2047
|
-
await
|
|
2112
|
+
const runLocalBrowser2 = await loadLocalBrowser();
|
|
2113
|
+
await runLocalBrowser2(rest);
|
|
2048
2114
|
return;
|
|
2049
2115
|
}
|
|
2050
2116
|
console.log(`Unknown command "${command}".`);
|