@canaryai/cli 0.1.4 → 0.1.6

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,2059 +1,10 @@
1
1
  #!/usr/bin/env node
2
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
- }) : x)(function(x) {
5
- if (typeof require !== "undefined") return require.apply(this, arguments);
6
- throw Error('Dynamic require of "' + x + '" is not supported');
7
- });
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";
111
-
112
- // src/auth.ts
113
- import fs3 from "fs/promises";
114
- import os2 from "os";
115
- import path3 from "path";
116
- async function readStoredToken() {
117
- try {
118
- const filePath = path3.join(os2.homedir(), ".config", "canary-cli", "auth.json");
119
- const content = await fs3.readFile(filePath, "utf8");
120
- const parsed = JSON.parse(content);
121
- return typeof parsed.token === "string" ? parsed.token : null;
122
- } catch {
123
- return null;
124
- }
125
- }
126
-
127
- // src/local-run.ts
128
- function getArgValue(argv, key) {
129
- const index = argv.indexOf(key);
130
- if (index === -1) return void 0;
131
- return argv[index + 1];
132
- }
133
- async function runLocalTest(argv) {
134
- const apiUrl = getArgValue(argv, "--api-url") ?? process2.env.CANARY_API_URL ?? "https://api.trycanary.ai";
135
- const token = getArgValue(argv, "--token") ?? process2.env.CANARY_API_TOKEN ?? await readStoredToken();
136
- const title = getArgValue(argv, "--title");
137
- const featureSpec = getArgValue(argv, "--feature");
138
- const startUrl = getArgValue(argv, "--start-url");
139
- const tunnelUrl = getArgValue(argv, "--tunnel-url");
140
- if (!tunnelUrl && !startUrl) {
141
- console.error("Missing --tunnel-url or --start-url");
142
- process2.exit(1);
143
- }
144
- if (!token) {
145
- console.error("Missing token. Run `canary login` first or set CANARY_API_TOKEN.");
146
- process2.exit(1);
147
- }
148
- const result = await createLocalRun({
149
- apiUrl,
150
- token,
151
- title,
152
- featureSpec,
153
- startUrl,
154
- tunnelUrl
155
- });
156
- console.log(`Local test queued: ${result.runId}`);
157
- if (result.watchUrl) {
158
- console.log(`Watch: ${result.watchUrl}`);
159
- }
160
- }
161
- async function createLocalRun(input) {
162
- const body = {
163
- title: input.title ?? null,
164
- featureSpec: input.featureSpec ?? null,
165
- startUrl: input.startUrl ?? null,
166
- tunnelPublicUrl: input.tunnelUrl ?? null
167
- };
168
- const response = await fetch(`${input.apiUrl}/local-tests/runs`, {
169
- method: "POST",
170
- headers: {
171
- "content-type": "application/json",
172
- authorization: `Bearer ${input.token}`
173
- },
174
- body: JSON.stringify(body)
175
- });
176
- const json = await response.json();
177
- if (!response.ok || !json.ok || !json.runId) {
178
- throw new Error(json.error ?? response.statusText);
179
- }
180
- return { runId: json.runId, watchUrl: json.watchUrl };
181
- }
182
-
183
- // src/tunnel.ts
184
- import { createHash } from "crypto";
185
- import os3 from "os";
186
- import process3 from "process";
187
- function getArgValue2(argv, key) {
188
- const index = argv.indexOf(key);
189
- if (index === -1) return void 0;
190
- return argv[index + 1];
191
- }
192
- function toWebSocketUrl(apiUrl) {
193
- const url = new URL(apiUrl);
194
- url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
195
- return url.toString();
196
- }
197
- function createFingerprint() {
198
- const raw = `${os3.hostname()}-${os3.userInfo().username}-${process3.version}`;
199
- return createHash("sha256").update(raw).digest("hex").slice(0, 16);
200
- }
201
- async function runTunnel(argv) {
202
- const apiUrl = getArgValue2(argv, "--api-url") ?? process3.env.CANARY_API_URL ?? "https://api.trycanary.ai";
203
- const token = getArgValue2(argv, "--token") ?? process3.env.CANARY_API_TOKEN ?? await readStoredToken();
204
- const portRaw = getArgValue2(argv, "--port") ?? process3.env.CANARY_LOCAL_PORT;
205
- if (!portRaw) {
206
- console.error("Missing --port");
207
- process3.exit(1);
208
- }
209
- const port = Number(portRaw);
210
- if (Number.isNaN(port) || port <= 0) {
211
- console.error("Invalid --port value");
212
- process3.exit(1);
213
- }
214
- if (!token) {
215
- console.error("Missing token. Run `canary login` first or set CANARY_API_TOKEN.");
216
- process3.exit(1);
217
- }
218
- const maxReconnectAttempts = 10;
219
- const baseReconnectDelayMs = 1e3;
220
- let reconnectAttempts = 0;
221
- const connect = async () => {
222
- try {
223
- const data = await createTunnel({
224
- apiUrl,
225
- token,
226
- port
227
- });
228
- console.log(`Tunnel connected: ${data.publicUrl ?? data.tunnelId}`);
229
- if (data.publicUrl) {
230
- console.log(`Public URL: ${data.publicUrl}`);
231
- console.log("");
232
- console.log("To use this tunnel for sandbox agent callbacks, add to apps/api/.env:");
233
- console.log(` SANDBOX_AGENT_API_URL=${data.publicUrl}`);
234
- console.log("");
235
- }
236
- const ws = connectTunnel({
237
- apiUrl,
238
- tunnelId: data.tunnelId,
239
- token: data.token,
240
- port,
241
- onReady: () => {
242
- reconnectAttempts = 0;
243
- }
244
- });
245
- return new Promise((resolve, reject) => {
246
- ws.onclose = (event) => {
247
- console.log(`Tunnel closed (code: ${event.code})`);
248
- if (reconnectAttempts < maxReconnectAttempts) {
249
- const delay = Math.min(baseReconnectDelayMs * Math.pow(2, reconnectAttempts), 3e4);
250
- reconnectAttempts++;
251
- console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})...`);
252
- setTimeout(() => {
253
- connect().then(resolve).catch(reject);
254
- }, delay);
255
- } else {
256
- console.error("Max reconnection attempts reached. Exiting.");
257
- process3.exit(1);
258
- }
259
- };
260
- ws.onerror = (event) => {
261
- console.error("Tunnel error:", event);
262
- };
263
- });
264
- } catch (error) {
265
- if (reconnectAttempts < maxReconnectAttempts) {
266
- const delay = Math.min(baseReconnectDelayMs * Math.pow(2, reconnectAttempts), 3e4);
267
- reconnectAttempts++;
268
- console.error(`Failed to create tunnel: ${error}`);
269
- console.log(`Retrying in ${delay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})...`);
270
- await new Promise((resolve) => setTimeout(resolve, delay));
271
- return connect();
272
- } else {
273
- console.error("Max reconnection attempts reached. Exiting.");
274
- process3.exit(1);
275
- }
276
- }
277
- };
278
- await connect();
279
- }
280
- async function createTunnel(input) {
281
- const response = await fetch(`${input.apiUrl}/local-tests/tunnels`, {
282
- method: "POST",
283
- headers: {
284
- "content-type": "application/json",
285
- authorization: `Bearer ${input.token}`
286
- },
287
- body: JSON.stringify({
288
- requestedPort: input.port,
289
- clientFingerprint: createFingerprint()
290
- })
291
- });
292
- const data = await response.json();
293
- if (!response.ok || !data.ok || !data.tunnelId || !data.token) {
294
- throw new Error(data.error ?? response.statusText);
295
- }
296
- return { tunnelId: data.tunnelId, publicUrl: data.publicUrl, token: data.token };
297
- }
298
- function connectTunnel(input) {
299
- const wsUrl = toWebSocketUrl(
300
- `${input.apiUrl}/local-tests/tunnels/${input.tunnelId}/connect?token=${input.token}`
301
- );
302
- const ws = new WebSocket(wsUrl);
303
- const wsConnections = /* @__PURE__ */ new Map();
304
- const wsQueues = /* @__PURE__ */ new Map();
305
- ws.onopen = () => {
306
- input.onReady?.();
307
- };
308
- ws.onerror = (event) => {
309
- console.error("Tunnel error", event);
310
- };
311
- ws.onclose = () => {
312
- console.log("Tunnel closed");
313
- };
314
- ws.onmessage = async (event) => {
315
- try {
316
- const raw = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString();
317
- const payload = JSON.parse(raw);
318
- if (payload.type === "http_request") {
319
- const request = payload;
320
- const targetUrl = `http://localhost:${input.port}${request.path.startsWith("/") ? request.path : `/${request.path}`}`;
321
- const body = request.bodyBase64 ? Buffer.from(request.bodyBase64, "base64") : void 0;
322
- const headers = { ...request.headers };
323
- delete headers.host;
324
- delete headers["content-length"];
325
- try {
326
- const res = await fetch(targetUrl, {
327
- method: request.method,
328
- headers,
329
- body: body ?? void 0
330
- });
331
- const resBody = await res.arrayBuffer();
332
- const resHeaders = Object.fromEntries(res.headers.entries());
333
- delete resHeaders["set-cookie"];
334
- const getSetCookie = res.headers.getSetCookie;
335
- const setCookieValues = typeof getSetCookie === "function" ? getSetCookie.call(res.headers) : [];
336
- const fallbackSetCookie = res.headers.get("set-cookie");
337
- if (setCookieValues.length === 0 && fallbackSetCookie) {
338
- setCookieValues.push(fallbackSetCookie);
339
- }
340
- if (setCookieValues.length > 0) {
341
- resHeaders["set-cookie"] = setCookieValues;
342
- }
343
- const responsePayload = {
344
- type: "http_response",
345
- id: request.id,
346
- status: res.status,
347
- headers: resHeaders,
348
- bodyBase64: resBody.byteLength ? Buffer.from(resBody).toString("base64") : null
349
- };
350
- ws.send(JSON.stringify(responsePayload));
351
- } catch (error) {
352
- const responsePayload = {
353
- type: "http_response",
354
- id: request.id,
355
- status: 502,
356
- headers: { "content-type": "text/plain" },
357
- bodyBase64: Buffer.from(`Tunnel error: ${String(error)}`).toString("base64")
358
- };
359
- ws.send(JSON.stringify(responsePayload));
360
- }
361
- }
362
- if (payload.type === "ws_open") {
363
- const request = payload;
364
- const targetUrl = `ws://localhost:${input.port}${request.path.startsWith("/") ? request.path : `/${request.path}`}`;
365
- const protocolsHeader = request.headers["sec-websocket-protocol"] ?? request.headers["Sec-WebSocket-Protocol"];
366
- const protocols = protocolsHeader ? protocolsHeader.split(",").map((value) => value.trim()).filter(Boolean) : void 0;
367
- const localWs = new WebSocket(targetUrl, protocols);
368
- wsConnections.set(request.id, localWs);
369
- localWs.onopen = () => {
370
- ws.send(JSON.stringify({ type: "ws_ready", id: request.id }));
371
- const queued = wsQueues.get(request.id);
372
- if (queued) {
373
- for (const message of queued) {
374
- ws.send(JSON.stringify(message));
375
- }
376
- wsQueues.delete(request.id);
377
- }
378
- };
379
- localWs.onmessage = (event2) => {
380
- const data = typeof event2.data === "string" ? Buffer.from(event2.data) : Buffer.from(event2.data);
381
- const response = {
382
- type: "ws_message",
383
- id: request.id,
384
- dataBase64: data.toString("base64"),
385
- isBinary: typeof event2.data !== "string"
386
- };
387
- ws.send(JSON.stringify(response));
388
- };
389
- localWs.onclose = (event2) => {
390
- wsConnections.delete(request.id);
391
- const response = {
392
- type: "ws_close",
393
- id: request.id,
394
- code: event2.code,
395
- reason: event2.reason
396
- };
397
- ws.send(JSON.stringify(response));
398
- };
399
- localWs.onerror = () => {
400
- wsConnections.delete(request.id);
401
- const response = {
402
- type: "ws_close",
403
- id: request.id,
404
- code: 1011,
405
- reason: "local_ws_error"
406
- };
407
- ws.send(JSON.stringify(response));
408
- };
409
- }
410
- if (payload.type === "ws_message") {
411
- const message = payload;
412
- const localWs = wsConnections.get(message.id);
413
- const data = Buffer.from(message.dataBase64, "base64");
414
- if (!localWs || localWs.readyState !== WebSocket.OPEN) {
415
- const queued = wsQueues.get(message.id) ?? [];
416
- queued.push(message);
417
- wsQueues.set(message.id, queued);
418
- return;
419
- }
420
- if (message.isBinary) {
421
- localWs.send(data);
422
- } else {
423
- localWs.send(data.toString());
424
- }
425
- }
426
- if (payload.type === "ws_close") {
427
- const message = payload;
428
- const localWs = wsConnections.get(message.id);
429
- if (!localWs) {
430
- const queued = wsQueues.get(message.id) ?? [];
431
- queued.push(message);
432
- wsQueues.set(message.id, queued);
433
- return;
434
- }
435
- localWs.close(message.code ?? 1e3, message.reason ?? "");
436
- wsConnections.delete(message.id);
437
- }
438
- if (payload.type === "health_ping") {
439
- ws.send(JSON.stringify({ type: "health_pong" }));
440
- }
441
- } catch (error) {
442
- console.error("Tunnel message error", error);
443
- }
444
- };
445
- return ws;
446
- }
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
- }
539
- }
540
- }
541
-
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}`);
582
- }
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 {
673
- }
674
- this.context = null;
675
- }
676
- if (this.browser) {
677
- try {
678
- await this.browser.close();
679
- } catch {
680
- }
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");
709
- this.stopHeartbeat();
710
- this.ws = null;
711
- if (!this.isShuttingDown) {
712
- this.scheduleReconnect();
713
- }
714
- };
715
- setTimeout(() => {
716
- if (!this.ws) {
717
- reject(new Error("WebSocket connection timeout"));
718
- }
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");
740
- }
741
- } catch (error) {
742
- this.log("error", "Reconnection failed", error);
743
- this.scheduleReconnect();
744
- }
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));
761
- }
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) {
792
- 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");
798
- }
799
- }
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",
817
- id: crypto.randomUUID(),
818
- timestamp: Date.now(),
819
- direction: "pong"
820
- };
821
- this.ws?.send(JSON.stringify(pong));
822
- return;
823
- }
824
- if (message.type === "command") {
825
- this.handleCommand(message);
826
- return;
827
- }
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);
1078
- }
1079
- }
1080
- if (opts?.doubleClick) {
1081
- await page.mouse.dblclick(centerX, centerY);
1082
- } else {
1083
- await page.mouse.click(centerX, centerY);
1084
- }
1085
- if (opts?.modifiers?.length) {
1086
- for (const mod of opts.modifiers) {
1087
- await page.keyboard.up(mod);
1088
- }
1089
- }
1090
- } else {
1091
- if (opts?.doubleClick) {
1092
- await locator.dblclick({ timeout: opts?.timeoutMs ?? 3e4 });
1093
- } else {
1094
- await locator.click({ timeout: opts?.timeoutMs ?? 3e4 });
1095
- }
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 });
1148
- }
1149
- break;
1150
- }
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
- }
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();
1178
- }
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();
1233
- }
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];
1239
- }
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 }] };
1300
- }
1301
- function toolJson(data) {
1302
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1303
- }
1304
- async function runMcp(argv) {
1305
- const server = new Server(
1306
- { name: "canary-cli", version: "0.1.0" },
1307
- { capabilities: { tools: {} } }
1308
- );
1309
- server.setRequestHandler(ListToolsRequestSchema, async () => ({
1310
- tools: [
1311
- {
1312
- name: "local_run_tests",
1313
- description: "Start an async local test run. A tunnel is opened automatically. Returns runId and watchUrl.",
1314
- inputSchema: {
1315
- type: "object",
1316
- properties: {
1317
- port: { type: "number" },
1318
- instructions: { type: "string" },
1319
- title: { type: "string" }
1320
- },
1321
- required: ["port", "instructions"]
1322
- }
1323
- },
1324
- {
1325
- name: "local_wait_for_results",
1326
- description: "Wait for a local test run to complete. Streams until completion and returns a compact report.",
1327
- inputSchema: {
1328
- type: "object",
1329
- properties: {
1330
- runId: { type: "string" }
1331
- },
1332
- required: ["runId"]
1333
- }
1334
- },
1335
- {
1336
- name: "local_browser_start",
1337
- description: "Start a local browser session that connects to the cloud agent. The cloud agent can then control this browser to test local applications. Returns sessionId for tracking.",
1338
- inputSchema: {
1339
- type: "object",
1340
- properties: {
1341
- mode: {
1342
- type: "string",
1343
- enum: ["playwright", "cdp"],
1344
- description: "Browser mode: 'playwright' for fresh browser, 'cdp' to connect to existing Chrome"
1345
- },
1346
- cdpUrl: {
1347
- type: "string",
1348
- description: "CDP endpoint URL when mode is 'cdp' (e.g. http://localhost:9222)"
1349
- },
1350
- headless: {
1351
- type: "boolean",
1352
- description: "Run browser headless (default: true for playwright mode)"
1353
- },
1354
- storageStatePath: {
1355
- type: "string",
1356
- description: "Path to Playwright storage state JSON for pre-authenticated sessions"
1357
- },
1358
- instructions: {
1359
- type: "string",
1360
- description: "Instructions for the cloud agent on what to test"
1361
- }
1362
- }
1363
- }
1364
- },
1365
- {
1366
- name: "local_browser_status",
1367
- description: "Check the status of a local browser session.",
1368
- inputSchema: {
1369
- type: "object",
1370
- properties: {
1371
- sessionId: { type: "string" }
1372
- },
1373
- required: ["sessionId"]
1374
- }
1375
- },
1376
- {
1377
- name: "local_browser_stop",
1378
- description: "Stop a local browser session and close the browser.",
1379
- inputSchema: {
1380
- type: "object",
1381
- properties: {
1382
- sessionId: { type: "string" }
1383
- },
1384
- required: ["sessionId"]
1385
- }
1386
- },
1387
- {
1388
- name: "local_browser_list",
1389
- description: "List all active local browser sessions.",
1390
- inputSchema: {
1391
- type: "object",
1392
- properties: {}
1393
- }
1394
- },
1395
- {
1396
- name: "local_browser_run",
1397
- description: "Start a test run on an active local browser session. The cloud agent will control the local browser according to the instructions.",
1398
- inputSchema: {
1399
- type: "object",
1400
- properties: {
1401
- sessionId: {
1402
- type: "string",
1403
- description: "The session ID from local_browser_start"
1404
- },
1405
- instructions: {
1406
- type: "string",
1407
- description: "Instructions for the cloud agent on what to test"
1408
- },
1409
- startUrl: {
1410
- type: "string",
1411
- description: "Optional URL to navigate to before starting"
1412
- }
1413
- },
1414
- required: ["sessionId", "instructions"]
1415
- }
1416
- }
1417
- ]
1418
- }));
1419
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
1420
- const token = await resolveToken();
1421
- const tool = req.params.name;
1422
- if (tool === "local_run_tests") {
1423
- const input = req.params.arguments;
1424
- const apiUrl = resolveApiUrl();
1425
- const tunnel = await createTunnel({ apiUrl, token, port: input.port });
1426
- connectTunnel({
1427
- apiUrl,
1428
- tunnelId: tunnel.tunnelId,
1429
- token: tunnel.token,
1430
- port: input.port
1431
- });
1432
- const tunnelUrl = tunnel.publicUrl;
1433
- const run2 = await createLocalRun({
1434
- apiUrl,
1435
- token,
1436
- title: input.title ?? "Local MCP run",
1437
- featureSpec: input.instructions,
1438
- startUrl: void 0,
1439
- tunnelUrl
1440
- });
1441
- return toolJson({
1442
- runId: run2.runId,
1443
- watchUrl: run2.watchUrl,
1444
- tunnelUrl,
1445
- note: "Testing is asynchronous. Use local_wait_for_results with the runId to wait for completion."
1446
- });
1447
- }
1448
- if (tool === "local_wait_for_results") {
1449
- const input = req.params.arguments;
1450
- const apiUrl = resolveApiUrl();
1451
- const report = await waitForResult({ apiUrl, token, runId: input.runId });
1452
- return toolJson(report);
1453
- }
1454
- if (tool === "local_browser_start") {
1455
- const input = req.params.arguments;
1456
- const apiUrl = resolveApiUrl();
1457
- const mode = input.mode ?? "playwright";
1458
- const sessionResponse = await fetch(`${apiUrl}/local-browser/sessions`, {
1459
- method: "POST",
1460
- headers: {
1461
- "Content-Type": "application/json",
1462
- Authorization: `Bearer ${token}`
1463
- },
1464
- body: JSON.stringify({
1465
- browserMode: mode,
1466
- instructions: input.instructions ?? null
1467
- })
1468
- });
1469
- if (!sessionResponse.ok) {
1470
- const text = await sessionResponse.text();
1471
- return toolJson({ ok: false, error: `Failed to create session: ${text}` });
1472
- }
1473
- const session = await sessionResponse.json();
1474
- const host = new LocalBrowserHost({
1475
- apiUrl,
1476
- wsToken: session.wsToken,
1477
- sessionId: session.sessionId,
1478
- browserMode: mode,
1479
- cdpUrl: input.cdpUrl,
1480
- headless: input.headless ?? true,
1481
- storageStatePath: input.storageStatePath,
1482
- onLog: (level, message) => {
1483
- if (level === "error") {
1484
- console.error(`[LocalBrowser] ${message}`);
1485
- }
1486
- }
1487
- });
1488
- host.start().catch((err) => {
1489
- console.error("Failed to start local browser:", err);
1490
- browserSessions.delete(session.sessionId);
1491
- });
1492
- browserSessions.set(session.sessionId, {
1493
- sessionId: session.sessionId,
1494
- host,
1495
- startedAt: Date.now(),
1496
- mode
1497
- });
1498
- return toolJson({
1499
- ok: true,
1500
- sessionId: session.sessionId,
1501
- mode,
1502
- expiresAt: session.expiresAt,
1503
- note: "Browser session started. The cloud agent can now control this browser. Use local_browser_stop to end the session."
1504
- });
1505
- }
1506
- if (tool === "local_browser_status") {
1507
- const input = req.params.arguments;
1508
- const session = browserSessions.get(input.sessionId);
1509
- if (!session) {
1510
- return toolJson({ ok: false, error: "Session not found", sessionId: input.sessionId });
1511
- }
1512
- return toolJson({
1513
- ok: true,
1514
- sessionId: session.sessionId,
1515
- mode: session.mode,
1516
- startedAt: new Date(session.startedAt).toISOString(),
1517
- uptimeMs: Date.now() - session.startedAt
1518
- });
1519
- }
1520
- if (tool === "local_browser_stop") {
1521
- const input = req.params.arguments;
1522
- const session = browserSessions.get(input.sessionId);
1523
- if (!session) {
1524
- return toolJson({ ok: false, error: "Session not found", sessionId: input.sessionId });
1525
- }
1526
- await session.host.stop();
1527
- browserSessions.delete(input.sessionId);
1528
- return toolJson({
1529
- ok: true,
1530
- sessionId: input.sessionId,
1531
- note: "Browser session stopped and browser closed."
1532
- });
1533
- }
1534
- if (tool === "local_browser_list") {
1535
- const sessions = Array.from(browserSessions.values()).map((s) => ({
1536
- sessionId: s.sessionId,
1537
- mode: s.mode,
1538
- startedAt: new Date(s.startedAt).toISOString(),
1539
- uptimeMs: Date.now() - s.startedAt
1540
- }));
1541
- return toolJson({
1542
- ok: true,
1543
- count: sessions.length,
1544
- sessions
1545
- });
1546
- }
1547
- if (tool === "local_browser_run") {
1548
- const input = req.params.arguments;
1549
- const apiUrl = resolveApiUrl();
1550
- const session = browserSessions.get(input.sessionId);
1551
- if (!session) {
1552
- return toolJson({
1553
- ok: false,
1554
- error: "Session not found locally. Make sure you started it with local_browser_start.",
1555
- sessionId: input.sessionId
1556
- });
1557
- }
1558
- const response = await fetch(`${apiUrl}/local-browser/sessions/${input.sessionId}/run`, {
1559
- method: "POST",
1560
- headers: {
1561
- "Content-Type": "application/json",
1562
- Authorization: `Bearer ${token}`
1563
- },
1564
- body: JSON.stringify({
1565
- instructions: input.instructions,
1566
- startUrl: input.startUrl ?? null
1567
- })
1568
- });
1569
- if (!response.ok) {
1570
- const text = await response.text();
1571
- return toolJson({ ok: false, error: `Failed to start run: ${text}` });
1572
- }
1573
- const result = await response.json();
1574
- return toolJson({
1575
- ok: true,
1576
- jobId: result.jobId,
1577
- sessionId: result.sessionId,
1578
- note: "Test run started. The cloud agent is now controlling your local browser. You can watch the browser to see the test in action."
1579
- });
1580
- }
1581
- return toolText(`Unknown tool: ${tool}`);
1582
- });
1583
- const transport = new StdioServerTransport();
1584
- await server.connect(transport);
1585
- return new Promise(() => void 0);
1586
- }
1587
- async function waitForResult(input) {
1588
- await streamUntilComplete(input);
1589
- const response = await fetch(`${input.apiUrl}/local-tests/runs/${input.runId}`, {
1590
- credentials: "include",
1591
- headers: { authorization: `Bearer ${input.token}` }
1592
- });
1593
- const data = await response.json();
1594
- const run2 = data?.data?.run ?? data?.run ?? data?.data;
1595
- const summary = run2?.summaryJson;
1596
- return formatReport({ run: run2, summary });
1597
- }
1598
- async function streamUntilComplete(input) {
1599
- const response = await fetch(`${input.apiUrl}/local-tests/runs/${input.runId}/stream`, {
1600
- headers: { authorization: `Bearer ${input.token}` }
1601
- });
1602
- if (!response.body) return;
1603
- const reader = response.body.getReader();
1604
- const decoder = new TextDecoder();
1605
- const parser = createParser({
1606
- onEvent: (event) => {
1607
- if (event.event === "status") {
1608
- try {
1609
- const payload = JSON.parse(event.data);
1610
- if (payload?.status === "completed" || payload?.status === "failed") {
1611
- reader.cancel().catch(() => void 0);
1612
- }
1613
- } catch {
1614
- }
1615
- }
1616
- if (event.event === "complete" || event.event === "error") {
1617
- reader.cancel().catch(() => void 0);
1618
- }
1619
- }
1620
- });
1621
- while (true) {
1622
- const { done, value } = await reader.read();
1623
- if (done) break;
1624
- parser.feed(decoder.decode(value, { stream: true }));
1625
- }
1626
- }
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
- };
1634
- }
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
- }
1647
-
1648
- // src/remote-test.ts
1649
- import process7 from "process";
1650
- import { createParser as createParser2 } from "eventsource-parser";
1651
- function getArgValue5(argv, key) {
1652
- const index = argv.indexOf(key);
1653
- if (index === -1 || index >= argv.length - 1) return void 0;
1654
- return argv[index + 1];
1655
- }
1656
- function hasFlag(argv, ...flags) {
1657
- return flags.some((flag) => argv.includes(flag));
1658
- }
1659
- 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();
1662
- const tag = getArgValue5(argv, "--tag");
1663
- const namePattern = getArgValue5(argv, "--name-pattern");
1664
- const verbose = hasFlag(argv, "--verbose", "-v");
1665
- if (!token) {
1666
- console.error("Error: No API token found.");
1667
- console.error("");
1668
- console.error("Set CANARY_API_TOKEN environment variable or run:");
1669
- console.error(" canary login");
1670
- console.error("");
1671
- console.error("Or create an API key in Settings > API Keys and pass it:");
1672
- console.error(" canary test --remote --token cnry_...");
1673
- process7.exit(1);
1674
- }
1675
- console.log("Starting remote workflow tests...");
1676
- if (tag) console.log(` Filtering by tag: ${tag}`);
1677
- if (namePattern) console.log(` Filtering by name pattern: ${namePattern}`);
1678
- console.log("");
1679
- const queryParams = new URLSearchParams();
1680
- if (tag) queryParams.set("tag", tag);
1681
- if (namePattern) queryParams.set("namePattern", namePattern);
1682
- const triggerUrl = `${apiUrl}/workflows/test-runs${queryParams.toString() ? `?${queryParams}` : ""}`;
1683
- let triggerRes;
1684
- try {
1685
- triggerRes = await fetch(triggerUrl, {
1686
- method: "POST",
1687
- headers: {
1688
- Authorization: `Bearer ${token}`,
1689
- "Content-Type": "application/json"
1690
- }
1691
- });
1692
- } catch (err) {
1693
- console.error(`Failed to connect to API: ${err}`);
1694
- process7.exit(1);
1695
- }
1696
- if (!triggerRes.ok) {
1697
- const errorText = await triggerRes.text();
1698
- console.error(`Failed to start tests: ${triggerRes.status}`);
1699
- console.error(errorText);
1700
- process7.exit(1);
1701
- }
1702
- const triggerData = await triggerRes.json();
1703
- if (!triggerData.ok || !triggerData.suiteId) {
1704
- console.error(`Failed to start tests: ${triggerData.error ?? "Unknown error"}`);
1705
- process7.exit(1);
1706
- }
1707
- const { suiteId, jobId } = triggerData;
1708
- if (verbose) {
1709
- console.log(`Suite ID: ${suiteId}`);
1710
- console.log(`Job ID: ${jobId}`);
1711
- console.log("");
1712
- }
1713
- const streamUrl = `${apiUrl}/workflows/test-runs/stream?suiteId=${suiteId}`;
1714
- let streamRes;
1715
- try {
1716
- streamRes = await fetch(streamUrl, {
1717
- headers: {
1718
- Authorization: `Bearer ${token}`,
1719
- Accept: "text/event-stream"
1720
- }
1721
- });
1722
- } catch (err) {
1723
- console.error(`Failed to connect to event stream: ${err}`);
1724
- process7.exit(1);
1725
- }
1726
- if (!streamRes.ok || !streamRes.body) {
1727
- console.error(`Failed to connect to event stream: ${streamRes.status}`);
1728
- process7.exit(1);
1729
- }
1730
- let exitCode = 0;
1731
- let hasCompleted = false;
1732
- const workflowNames = /* @__PURE__ */ new Map();
1733
- let totalWorkflows = 0;
1734
- let completedWorkflows = 0;
1735
- let failedWorkflows = 0;
1736
- let successfulWorkflows = 0;
1737
- const parser = createParser2({
1738
- onEvent: (event) => {
1739
- if (!event.data) return;
1740
- try {
1741
- const data = JSON.parse(event.data);
1742
- if (verbose) {
1743
- console.log(`[${event.event}] ${JSON.stringify(data)}`);
1744
- }
1745
- if (event.event === "workflow-test") {
1746
- const testEvent = data;
1747
- const { status, workflowId, message, errorMessage } = testEvent;
1748
- const name = workflowNames.get(workflowId) || message?.replace(/^Flow "(.+)" .*$/, "$1") || workflowId;
1749
- if (message?.startsWith('Flow "')) {
1750
- const match = message.match(/^Flow "(.+?)" /);
1751
- if (match) workflowNames.set(workflowId, match[1]);
1752
- }
1753
- if (!verbose) {
1754
- if (status === "success") {
1755
- console.log(` \u2713 ${name}`);
1756
- } else if (status === "failed") {
1757
- console.log(` \u2717 ${name}`);
1758
- if (errorMessage) {
1759
- console.log(` Error: ${errorMessage.slice(0, 200)}`);
1760
- }
1761
- exitCode = 1;
1762
- } else if (status === "running") {
1763
- } else if (status === "waiting") {
1764
- console.log(` \u23F3 ${name} (waiting for scheduled time)`);
1765
- }
1766
- }
1767
- }
1768
- if (event.event === "workflow-test-suite") {
1769
- const suiteEvent = data;
1770
- totalWorkflows = suiteEvent.totalWorkflows;
1771
- completedWorkflows = suiteEvent.completedWorkflows;
1772
- failedWorkflows = suiteEvent.failedWorkflows;
1773
- successfulWorkflows = suiteEvent.successfulWorkflows;
1774
- if (suiteEvent.status === "completed") {
1775
- hasCompleted = true;
1776
- }
1777
- }
1778
- if (event.event === "error") {
1779
- const errorData = data;
1780
- console.error(`Stream error: ${errorData.error ?? "Unknown error"}`);
1781
- exitCode = 1;
1782
- }
1783
- } catch {
1784
- }
1785
- }
1786
- });
1787
- const reader = streamRes.body.getReader();
1788
- const decoder = new TextDecoder();
1789
- try {
1790
- while (!hasCompleted) {
1791
- const { done, value } = await reader.read();
1792
- if (done) break;
1793
- parser.feed(decoder.decode(value, { stream: true }));
1794
- }
1795
- } finally {
1796
- reader.releaseLock();
1797
- }
1798
- console.log("");
1799
- console.log("\u2500".repeat(50));
1800
- if (totalWorkflows === 0) {
1801
- console.log("No workflows found matching the filter criteria.");
1802
- process7.exit(0);
1803
- }
1804
- const passRate = totalWorkflows > 0 ? Math.round(successfulWorkflows / totalWorkflows * 100) : 0;
1805
- if (failedWorkflows > 0) {
1806
- console.log(`FAILED: ${failedWorkflows} of ${totalWorkflows} workflows failed (${passRate}% pass rate)`);
1807
- exitCode = 1;
1808
- } else {
1809
- console.log(`PASSED: ${successfulWorkflows} of ${totalWorkflows} workflows passed`);
1810
- }
1811
- const waitingWorkflows = totalWorkflows - completedWorkflows;
1812
- if (waitingWorkflows > 0) {
1813
- console.log(`Note: ${waitingWorkflows} workflow(s) are still waiting (scheduled for later)`);
1814
- }
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
- }
1945
- }
1946
-
1947
- // src/index.ts
1948
- var baseDir = typeof __dirname !== "undefined" ? __dirname : path5.dirname(fileURLToPath2(import.meta.url));
1949
- var preloadPath = path5.join(baseDir, "runner", "preload.js");
1950
- var requireFn = makeRequire();
1951
- function runPlaywrightTests(args) {
1952
- const playwrightCli = requireFn.resolve("@playwright/test/cli");
1953
- const { runnerBin, preloadFlag } = resolveRunner(preloadPath);
1954
- const nodeOptions = process9.env.NODE_OPTIONS && preloadFlag ? `${process9.env.NODE_OPTIONS} ${preloadFlag}` : preloadFlag ?? process9.env.NODE_OPTIONS;
1955
- const env = {
1956
- ...process9.env,
1957
- CANARY_ENABLED: process9.env.CANARY_ENABLED ?? "1",
1958
- CANARY_RUNNER: "canary",
1959
- ...nodeOptions ? { NODE_OPTIONS: nodeOptions } : {}
1960
- };
1961
- const result = spawnSync2(runnerBin, [playwrightCli, "test", ...args], {
1962
- env,
1963
- stdio: "inherit",
1964
- cwd: process9.cwd()
1965
- });
1966
- if (result.error) {
1967
- console.error("canary failed to launch Playwright:", result.error);
1968
- process9.exit(1);
1969
- }
1970
- process9.exit(result.status ?? 1);
1971
- }
1972
- function printHelp() {
1973
- console.log(
1974
- [
1975
- "canary: Local and remote testing CLI",
1976
- "",
1977
- "Usage:",
1978
- " canary test [playwright options] Run local Playwright tests",
1979
- " canary test --remote [options] Run remote workflow tests",
1980
- " canary local-run --tunnel-url <url> [options]",
1981
- " canary tunnel --port <localPort> [options]",
1982
- " canary run --port <localPort> [options]",
1983
- " canary mcp",
1984
- " canary browser [--mode playwright|cdp] [--cdp-url <url>] [--no-headless]",
1985
- " canary login [--app-url https://app.trycanary.ai] [--no-open]",
1986
- " canary help",
1987
- "",
1988
- "Remote test options:",
1989
- " --token <key> API key (or set CANARY_API_TOKEN)",
1990
- " --api-url <url> API URL (default: https://api.trycanary.ai)",
1991
- " --tag <tag> Filter workflows by tag",
1992
- " --name-pattern <pat> Filter workflows by name pattern",
1993
- " --verbose, -v Show all events",
1994
- "",
1995
- "Browser options:",
1996
- " --mode <playwright|cdp> Browser mode (default: playwright)",
1997
- " --cdp-url <url> CDP endpoint for existing Chrome",
1998
- " --no-headless Run browser with visible UI",
1999
- " --storage-state <path> Path to storage state JSON",
2000
- " --instructions <text> Instructions for the cloud agent",
2001
- "",
2002
- "Flags:",
2003
- " -h, --help Show help"
2004
- ].join("\n")
2005
- );
2006
- }
2007
- async function main(argv) {
2008
- if (argv.includes("--help") || argv.includes("-h")) {
2009
- printHelp();
2010
- return;
2011
- }
2012
- const [command, ...rest] = argv;
2013
- if (!command || command === "help") {
2014
- printHelp();
2015
- return;
2016
- }
2017
- if (command === "test") {
2018
- if (rest.includes("--remote")) {
2019
- const remoteArgs = rest.filter((arg) => arg !== "--remote");
2020
- await runRemoteTest(remoteArgs);
2021
- return;
2022
- }
2023
- runPlaywrightTests(rest);
2024
- return;
2025
- }
2026
- if (command === "local-run") {
2027
- await runLocalTest(rest);
2028
- return;
2029
- }
2030
- if (command === "run") {
2031
- await runLocalSession(rest);
2032
- return;
2033
- }
2034
- if (command === "mcp") {
2035
- await runMcp(rest);
2036
- return;
2037
- }
2038
- if (command === "tunnel") {
2039
- await runTunnel(rest);
2040
- return;
2041
- }
2042
- if (command === "login") {
2043
- await runLogin(rest);
2044
- return;
2045
- }
2046
- if (command === "browser") {
2047
- await runLocalBrowser(rest);
2048
- return;
2049
- }
2050
- console.log(`Unknown command "${command}".`);
2051
- printHelp();
2052
- process9.exit(1);
2053
- }
2054
- if (import.meta.url === pathToFileURL2(process9.argv[1]).href) {
2055
- void main(process9.argv.slice(2));
2056
- }
2
+ import {
3
+ main
4
+ } from "./chunk-YA43CE6P.js";
5
+ import "./chunk-Z6I3ZXZL.js";
6
+ import "./chunk-UBYYNMML.js";
7
+ import "./chunk-7OCVIDC7.js";
2057
8
 
2058
9
  // src/bin.ts
2059
10
  void main(process.argv.slice(2));