@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 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
- // src/index.ts
10
- import { spawnSync as spawnSync2 } from "child_process";
11
- import process9 from "process";
12
- import path5 from "path";
13
- import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "url";
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
- // src/login.ts
449
- import fs4 from "fs/promises";
450
- import os4 from "os";
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/run-local.ts
543
- import process5 from "process";
544
- function getArgValue4(argv, key) {
545
- const index = argv.indexOf(key);
546
- if (index === -1) return void 0;
547
- return argv[index + 1];
548
- }
549
- async function runLocalSession(argv) {
550
- const apiUrl = getArgValue4(argv, "--api-url") ?? process5.env.CANARY_API_URL ?? "https://api.trycanary.ai";
551
- const token = getArgValue4(argv, "--token") ?? process5.env.CANARY_API_TOKEN ?? await readStoredToken();
552
- if (!token) {
553
- console.error("Missing token. Run `canary login` first or set CANARY_API_TOKEN.");
554
- process5.exit(1);
555
- }
556
- const portRaw = getArgValue4(argv, "--port") ?? process5.env.CANARY_LOCAL_PORT;
557
- const tunnelUrl = getArgValue4(argv, "--tunnel-url");
558
- const title = getArgValue4(argv, "--title");
559
- const featureSpec = getArgValue4(argv, "--feature");
560
- const startUrl = getArgValue4(argv, "--start-url");
561
- if (!tunnelUrl && !portRaw) {
562
- console.error("Missing --port or --tunnel-url");
563
- process5.exit(1);
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
- if (!publicUrl) {
586
- console.error("Failed to resolve tunnel URL");
587
- process5.exit(1);
588
- }
589
- const run2 = await createLocalRun({
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
- this.context = null;
675
- }
676
- if (this.browser) {
677
- try {
678
- await this.browser.close();
679
- } catch {
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
- this.browser = null;
682
- }
683
- this.page = null;
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 = null;
711
- if (!this.isShuttingDown) {
712
- this.scheduleReconnect();
420
+ if (this.ws) {
421
+ try {
422
+ this.ws.close(1e3, "Shutdown");
423
+ } catch {
424
+ }
425
+ this.ws = null;
713
426
  }
714
- };
715
- setTimeout(() => {
716
- if (!this.ws) {
717
- reject(new Error("WebSocket connection timeout"));
427
+ if (this.context) {
428
+ try {
429
+ await this.context.close();
430
+ } catch {
431
+ }
432
+ this.context = null;
718
433
  }
719
- }, 3e4);
720
- });
721
- }
722
- scheduleReconnect() {
723
- if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
724
- this.log("error", "Max reconnection attempts reached, giving up");
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
- } catch (error) {
742
- this.log("error", "Reconnection failed", error);
743
- this.scheduleReconnect();
441
+ this.page = null;
442
+ this.log("info", "Local browser host stopped");
744
443
  }
745
- }, delay);
746
- }
747
- // =========================================================================
748
- // Heartbeat
749
- // =========================================================================
750
- startHeartbeat() {
751
- this.stopHeartbeat();
752
- this.heartbeatTimer = setInterval(() => {
753
- if (this.ws?.readyState === WebSocket.OPEN) {
754
- const ping = {
755
- type: "heartbeat",
756
- id: crypto.randomUUID(),
757
- timestamp: Date.now(),
758
- direction: "pong"
759
- };
760
- this.ws.send(JSON.stringify(ping));
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
- }, HEARTBEAT_INTERVAL_MS);
763
- }
764
- stopHeartbeat() {
765
- if (this.heartbeatTimer) {
766
- clearInterval(this.heartbeatTimer);
767
- this.heartbeatTimer = null;
768
- }
769
- }
770
- // =========================================================================
771
- // Browser Management
772
- // =========================================================================
773
- async launchBrowser() {
774
- const { browserMode, cdpUrl, headless = true, storageStatePath } = this.options;
775
- if (browserMode === "cdp" && cdpUrl) {
776
- this.log("info", "Connecting to existing Chrome via CDP", { cdpUrl });
777
- this.browser = await chromium.connectOverCDP(cdpUrl);
778
- const contexts = this.browser.contexts();
779
- this.context = contexts[0] ?? await this.browser.newContext();
780
- const pages = this.context.pages();
781
- this.page = pages[0] ?? await this.context.newPage();
782
- } else {
783
- this.log("info", "Launching new Playwright browser", { headless });
784
- this.browser = await chromium.launch({
785
- headless,
786
- args: ["--no-sandbox"]
787
- });
788
- const contextOptions = {
789
- viewport: { width: 1920, height: 1080 }
790
- };
791
- if (storageStatePath) {
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
- await Bun.file(storageStatePath).exists();
794
- contextOptions.storageState = storageStatePath;
795
- this.log("info", "Loading storage state", { storageStatePath });
796
- } catch {
797
- this.log("debug", "Storage state file not found, starting fresh");
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
- this.context = await this.browser.newContext(contextOptions);
801
- this.page = await this.context.newPage();
802
- }
803
- this.page.on("dialog", (dialog) => {
804
- this.pendingDialogs.push(dialog);
805
- });
806
- this.log("info", "Browser ready");
807
- }
808
- // =========================================================================
809
- // Message Handling
810
- // =========================================================================
811
- handleMessage(data) {
812
- try {
813
- const message = JSON.parse(data);
814
- if (message.type === "heartbeat" && message.direction === "ping") {
815
- const pong = {
816
- type: "heartbeat",
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
- direction: "pong"
633
+ event,
634
+ browserMode: this.options.browserMode,
635
+ error
820
636
  };
821
- this.ws?.send(JSON.stringify(pong));
822
- return;
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
- if (message.type === "command") {
825
- this.handleCommand(message);
772
+ resolveRef(ref) {
773
+ return this.getPage().locator(`aria-ref=${ref}`);
774
+ }
775
+ async connect(_options) {
826
776
  return;
827
777
  }
828
- this.log("debug", "Received unknown message type", message);
829
- } catch (error) {
830
- this.log("error", "Failed to parse message", { error, data });
831
- }
832
- }
833
- async handleCommand(command) {
834
- const startTime = Date.now();
835
- this.log("debug", `Executing command: ${command.method}`, { id: command.id });
836
- try {
837
- const result = await this.executeMethod(command.method, command.args);
838
- const response = {
839
- type: "response",
840
- id: crypto.randomUUID(),
841
- timestamp: Date.now(),
842
- requestId: command.id,
843
- success: true,
844
- result
845
- };
846
- this.ws?.send(JSON.stringify(response));
847
- this.log("debug", `Command completed: ${command.method}`, {
848
- id: command.id,
849
- durationMs: Date.now() - startTime
850
- });
851
- } catch (error) {
852
- const errorMessage = error instanceof Error ? error.message : String(error);
853
- const response = {
854
- type: "response",
855
- id: crypto.randomUUID(),
856
- timestamp: Date.now(),
857
- requestId: command.id,
858
- success: false,
859
- error: errorMessage,
860
- stack: error instanceof Error ? error.stack : void 0
861
- };
862
- this.ws?.send(JSON.stringify(response));
863
- this.log("error", `Command failed: ${command.method}`, {
864
- id: command.id,
865
- error: errorMessage
866
- });
867
- }
868
- }
869
- sendSessionEvent(event, error) {
870
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
871
- const message = {
872
- type: "session",
873
- id: crypto.randomUUID(),
874
- timestamp: Date.now(),
875
- event,
876
- browserMode: this.options.browserMode,
877
- error
878
- };
879
- this.ws.send(JSON.stringify(message));
880
- }
881
- // =========================================================================
882
- // Method Execution
883
- // =========================================================================
884
- async executeMethod(method, args) {
885
- switch (method) {
886
- // Lifecycle
887
- case "connect":
888
- return this.connect(args[0]);
889
- case "disconnect":
890
- return this.disconnect();
891
- // Navigation
892
- case "navigate":
893
- return this.navigate(args[0], args[1]);
894
- case "navigateBack":
895
- return this.navigateBack(args[0]);
896
- // Page Inspection
897
- case "snapshot":
898
- return this.snapshot(args[0]);
899
- case "takeScreenshot":
900
- return this.takeScreenshot(args[0]);
901
- case "evaluate":
902
- return this.evaluate(args[0], args[1]);
903
- case "runCode":
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
- if (opts?.doubleClick) {
1081
- await page.mouse.dblclick(centerX, centerY);
1082
- } else {
1083
- await page.mouse.click(centerX, centerY);
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
- if (opts?.modifiers?.length) {
1086
- for (const mod of opts.modifiers) {
1087
- await page.keyboard.up(mod);
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
- } else {
1091
- if (opts?.doubleClick) {
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
- async clickAtCoordinates(x, y, _elementDesc, opts) {
1099
- const page = this.getPage();
1100
- if (opts?.doubleClick) {
1101
- await page.mouse.dblclick(x, y);
1102
- } else {
1103
- await page.mouse.click(x, y);
1104
- }
1105
- }
1106
- async moveToCoordinates(x, y, _elementDesc, _opts) {
1107
- await this.getPage().mouse.move(x, y);
1108
- }
1109
- async dragCoordinates(startX, startY, endX, endY, _elementDesc, _opts) {
1110
- const page = this.getPage();
1111
- await page.mouse.move(startX, startY);
1112
- await page.mouse.down();
1113
- await page.mouse.move(endX, endY);
1114
- await page.mouse.up();
1115
- }
1116
- async hover(ref, _elementDesc, opts) {
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
- async waitFor(opts) {
1182
- const page = this.getPage();
1183
- const timeout = opts?.timeout ?? opts?.timeoutMs ?? 3e4;
1184
- if (opts?.timeSec) {
1185
- await page.waitForTimeout(opts.timeSec * 1e3);
1186
- return;
1187
- }
1188
- if (opts?.text) {
1189
- await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
1190
- return;
1191
- }
1192
- if (opts?.textGone) {
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
- this.page = this.context.pages()[0] ?? null;
1235
- break;
1236
- case "select":
1237
- if (index !== void 0 && pages[index]) {
1238
- this.page = pages[index];
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
- break;
1241
- }
1242
- return null;
1243
- }
1244
- async getStorageState(_opts) {
1245
- if (!this.context) throw new Error("No context available");
1246
- return this.context.storageState();
1247
- }
1248
- async getCurrentUrl(_opts) {
1249
- return this.getPage().url();
1250
- }
1251
- async getTitle(_opts) {
1252
- return this.getPage().title();
1253
- }
1254
- async getLinks(_opts) {
1255
- const page = this.getPage();
1256
- return page.$$eval(
1257
- "a[href]",
1258
- (links) => links.map((a) => a.href).filter((h) => !!h && (h.startsWith("http://") || h.startsWith("https://")))
1259
- );
1260
- }
1261
- async getElementBoundingBox(ref, _opts) {
1262
- const locator = this.resolveRef(ref);
1263
- const box = await locator.boundingBox();
1264
- if (!box) return null;
1265
- return { x: box.x, y: box.y, width: box.width, height: box.height };
1266
- }
1267
- async startTracing(_opts) {
1268
- if (!this.context) throw new Error("No context available");
1269
- await this.context.tracing.start({ screenshots: true, snapshots: true });
1270
- }
1271
- async stopTracing(_opts) {
1272
- if (!this.context) throw new Error("No context available");
1273
- const tracePath = `/tmp/trace-${Date.now()}.zip`;
1274
- await this.context.tracing.stop({ path: tracePath });
1275
- return {
1276
- trace: tracePath,
1277
- network: "",
1278
- resources: "",
1279
- directory: null,
1280
- legend: `Trace saved to ${tracePath}`
1281
- };
1282
- }
1283
- };
1284
-
1285
- // src/mcp.ts
1286
- var browserSessions = /* @__PURE__ */ new Map();
1287
- var DEFAULT_API_URL = "https://api.trycanary.ai";
1288
- function resolveApiUrl(input) {
1289
- return input ?? process6.env.CANARY_API_URL ?? DEFAULT_API_URL;
1290
- }
1291
- async function resolveToken() {
1292
- const token = process6.env.CANARY_API_TOKEN ?? await readStoredToken();
1293
- if (!token) {
1294
- throw new Error("Missing token. Run `canary login` first or set CANARY_API_TOKEN.");
1295
- }
1296
- return token;
1297
- }
1298
- function toolText(text) {
1299
- return { content: [{ type: "text", text }] };
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 = createParser({
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
- const { done, value } = await reader.read();
1623
- if (done) break;
1624
- parser.feed(decoder.decode(value, { stream: true }));
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
- function formatReport(input) {
1628
- if (!input.summary) {
1629
- return {
1630
- runId: input.run?.id,
1631
- status: input.run?.status ?? "unknown",
1632
- summary: "No final report available."
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
- import process7 from "process";
1650
- import { createParser as createParser2 } from "eventsource-parser";
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") ?? process7.env.CANARY_API_URL ?? "https://api.trycanary.ai";
1661
- const token = getArgValue5(argv, "--token") ?? process7.env.CANARY_API_TOKEN ?? await readStoredToken();
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
- process7.exit(1);
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
- process7.exit(1);
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
- process7.exit(1);
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
- process7.exit(1);
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
- process7.exit(1);
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
- process7.exit(1);
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 = createParser2({
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
- process7.exit(0);
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
- process7.exit(exitCode);
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 runMcp(rest);
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 runLocalBrowser(rest);
2112
+ const runLocalBrowser2 = await loadLocalBrowser();
2113
+ await runLocalBrowser2(rest);
2048
2114
  return;
2049
2115
  }
2050
2116
  console.log(`Unknown command "${command}".`);