@canaryai/cli 0.2.8 → 0.2.12

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.
Files changed (58) hide show
  1. package/README.md +77 -92
  2. package/dist/chunk-CEW4BDXD.js +186 -0
  3. package/dist/chunk-CEW4BDXD.js.map +1 -0
  4. package/dist/chunk-ERSNYLMZ.js +229 -0
  5. package/dist/chunk-ERSNYLMZ.js.map +1 -0
  6. package/dist/{chunk-FK3EZADZ.js → chunk-MSMC6UXW.js} +2021 -873
  7. package/dist/chunk-MSMC6UXW.js.map +1 -0
  8. package/dist/{chunk-K2OB72B6.js → chunk-Q7WFBG5C.js} +2 -2
  9. package/dist/{debug-workflow-55G4Y6YT.js → debug-workflow-53ULOFJC.js} +57 -36
  10. package/dist/debug-workflow-53ULOFJC.js.map +1 -0
  11. package/dist/{docs-RPFT7ZJB.js → docs-BEE3LOCO.js} +2 -2
  12. package/dist/{feature-flag-2FDSKOVX.js → feature-flag-CYTDV4ZB.js} +3 -2
  13. package/dist/{feature-flag-2FDSKOVX.js.map → feature-flag-CYTDV4ZB.js.map} +1 -1
  14. package/dist/index.js +72 -137
  15. package/dist/index.js.map +1 -1
  16. package/dist/init-M6I3MG3D.js +146 -0
  17. package/dist/init-M6I3MG3D.js.map +1 -0
  18. package/dist/{issues-6ZDNDSD6.js → issues-NLM72HLU.js} +3 -2
  19. package/dist/{issues-6ZDNDSD6.js.map → issues-NLM72HLU.js.map} +1 -1
  20. package/dist/{knobs-MZRTYS3P.js → knobs-O35GAU5M.js} +3 -2
  21. package/dist/{knobs-MZRTYS3P.js.map → knobs-O35GAU5M.js.map} +1 -1
  22. package/dist/list-4K4EIGAT.js +57 -0
  23. package/dist/list-4K4EIGAT.js.map +1 -0
  24. package/dist/local-NHXXPHZ3.js +63 -0
  25. package/dist/local-NHXXPHZ3.js.map +1 -0
  26. package/dist/{local-browser-X7J27IGS.js → local-browser-VAZORCO3.js} +3 -3
  27. package/dist/login-ZLP64YQP.js +130 -0
  28. package/dist/login-ZLP64YQP.js.map +1 -0
  29. package/dist/mcp-ZF5G5DCB.js +377 -0
  30. package/dist/mcp-ZF5G5DCB.js.map +1 -0
  31. package/dist/{record-4OX7HXWQ.js → record-V6QKFFH3.js} +133 -72
  32. package/dist/record-V6QKFFH3.js.map +1 -0
  33. package/dist/{release-L4IXOHDF.js → release-7TI7EIGD.js} +8 -4
  34. package/dist/release-7TI7EIGD.js.map +1 -0
  35. package/dist/session-UGNJXRUW.js +819 -0
  36. package/dist/session-UGNJXRUW.js.map +1 -0
  37. package/dist/skill-ORWAPBDW.js +424 -0
  38. package/dist/skill-ORWAPBDW.js.map +1 -0
  39. package/dist/{src-I4EXB5OD.js → src-4VIDSK4A.js} +18 -2
  40. package/dist/start-E532F3BU.js +112 -0
  41. package/dist/start-E532F3BU.js.map +1 -0
  42. package/dist/workflow-HXIUXRFI.js +613 -0
  43. package/dist/workflow-HXIUXRFI.js.map +1 -0
  44. package/package.json +1 -1
  45. package/dist/chunk-6WWHXWCS.js +0 -65
  46. package/dist/chunk-6WWHXWCS.js.map +0 -1
  47. package/dist/chunk-DXIAHB72.js +0 -340
  48. package/dist/chunk-DXIAHB72.js.map +0 -1
  49. package/dist/chunk-FK3EZADZ.js.map +0 -1
  50. package/dist/debug-workflow-55G4Y6YT.js.map +0 -1
  51. package/dist/mcp-4JVLADZL.js +0 -688
  52. package/dist/mcp-4JVLADZL.js.map +0 -1
  53. package/dist/record-4OX7HXWQ.js.map +0 -1
  54. package/dist/release-L4IXOHDF.js.map +0 -1
  55. /package/dist/{chunk-K2OB72B6.js.map → chunk-Q7WFBG5C.js.map} +0 -0
  56. /package/dist/{docs-RPFT7ZJB.js.map → docs-BEE3LOCO.js.map} +0 -0
  57. /package/dist/{local-browser-X7J27IGS.js.map → local-browser-VAZORCO3.js.map} +0 -0
  58. /package/dist/{src-I4EXB5OD.js.map → src-4VIDSK4A.js.map} +0 -0
@@ -0,0 +1,377 @@
1
+ import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
2
+ import {
3
+ LocalBrowserHost
4
+ } from "./chunk-Q7WFBG5C.js";
5
+ import {
6
+ callTool,
7
+ createSession,
8
+ deleteAllSessions,
9
+ deleteSession,
10
+ resolveTargetSession
11
+ } from "./chunk-CEW4BDXD.js";
12
+ import {
13
+ readStoredToken
14
+ } from "./chunk-PWWQGYFG.js";
15
+ import {
16
+ getBrowserToolDefinitionsWithLifecycle
17
+ } from "./chunk-MSMC6UXW.js";
18
+ import "./chunk-XAA5VQ5N.js";
19
+ import "./chunk-P5Z2Y5VV.js";
20
+ import "./chunk-VKVL7WBN.js";
21
+
22
+ // src/mcp/index.ts
23
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
24
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
25
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
26
+
27
+ // src/mcp/tool-helpers.ts
28
+ function toolText(text) {
29
+ return { content: [{ type: "text", text }] };
30
+ }
31
+ function toolJson(data) {
32
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
33
+ }
34
+ function formatMCPContent(result) {
35
+ if (typeof result === "string") {
36
+ return { content: [{ type: "text", text: result }] };
37
+ }
38
+ const content = [];
39
+ if (result.text) {
40
+ content.push({ type: "text", text: result.text });
41
+ }
42
+ for (const img of result.images ?? []) {
43
+ content.push({ type: "image", data: img.data, mimeType: img.mimeType });
44
+ }
45
+ return { content };
46
+ }
47
+
48
+ // src/mcp/session-tools.ts
49
+ import process from "process";
50
+ var browserSessions = /* @__PURE__ */ new Map();
51
+ var DEFAULT_API_URL = "https://api.trycanary.ai";
52
+ function resolveApiUrl(input) {
53
+ return input ?? process.env.CANARY_API_URL ?? DEFAULT_API_URL;
54
+ }
55
+ async function resolveToken() {
56
+ const token = process.env.CANARY_API_TOKEN ?? await readStoredToken();
57
+ if (!token) {
58
+ throw new Error("Missing token. Run `canary login` first or set CANARY_API_TOKEN.");
59
+ }
60
+ return token;
61
+ }
62
+ function getSessionToolDefinitions() {
63
+ return [
64
+ {
65
+ name: "local_browser_start",
66
+ 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.",
67
+ inputSchema: {
68
+ type: "object",
69
+ properties: {
70
+ mode: {
71
+ type: "string",
72
+ enum: ["playwright", "cdp"],
73
+ description: "Browser mode: 'playwright' for fresh browser, 'cdp' to connect to existing Chrome"
74
+ },
75
+ cdpUrl: {
76
+ type: "string",
77
+ description: "CDP endpoint URL when mode is 'cdp' (e.g. http://localhost:9222)"
78
+ },
79
+ headless: {
80
+ type: "boolean",
81
+ description: "Run browser headless (default: true for playwright mode)"
82
+ },
83
+ storageStatePath: {
84
+ type: "string",
85
+ description: "Path to Playwright storage state JSON for pre-authenticated sessions"
86
+ },
87
+ instructions: {
88
+ type: "string",
89
+ description: "Instructions for the cloud agent on what to test"
90
+ }
91
+ }
92
+ }
93
+ },
94
+ {
95
+ name: "local_browser_status",
96
+ description: "Check the status of a local browser session.",
97
+ inputSchema: {
98
+ type: "object",
99
+ properties: {
100
+ sessionId: { type: "string" }
101
+ },
102
+ required: ["sessionId"]
103
+ }
104
+ },
105
+ {
106
+ name: "local_browser_stop",
107
+ description: "Stop a local browser session and close the browser.",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {
111
+ sessionId: { type: "string" }
112
+ },
113
+ required: ["sessionId"]
114
+ }
115
+ },
116
+ {
117
+ name: "local_browser_list",
118
+ description: "List all active local browser sessions.",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {}
122
+ }
123
+ },
124
+ {
125
+ name: "local_browser_run",
126
+ description: "Start a test run on an active local browser session. The cloud agent will control the local browser according to the instructions.",
127
+ inputSchema: {
128
+ type: "object",
129
+ properties: {
130
+ sessionId: {
131
+ type: "string",
132
+ description: "The session ID from local_browser_start"
133
+ },
134
+ instructions: {
135
+ type: "string",
136
+ description: "Instructions for the cloud agent on what to test"
137
+ },
138
+ startUrl: {
139
+ type: "string",
140
+ description: "Optional URL to navigate to before starting"
141
+ }
142
+ },
143
+ required: ["sessionId", "instructions"]
144
+ }
145
+ }
146
+ ];
147
+ }
148
+ async function handleSessionTool(tool, args) {
149
+ const token = await resolveToken();
150
+ if (tool === "local_browser_start") {
151
+ const input = args;
152
+ const apiUrl = resolveApiUrl();
153
+ const mode = input.mode ?? "playwright";
154
+ const sessionResponse = await fetch(`${apiUrl}/local-browser/sessions`, {
155
+ method: "POST",
156
+ headers: {
157
+ "Content-Type": "application/json",
158
+ Authorization: `Bearer ${token}`
159
+ },
160
+ body: JSON.stringify({
161
+ browserMode: mode,
162
+ instructions: input.instructions ?? null
163
+ })
164
+ });
165
+ if (!sessionResponse.ok) {
166
+ const text = await sessionResponse.text();
167
+ return toolJson({ ok: false, error: `Failed to create session: ${text}` });
168
+ }
169
+ const session = await sessionResponse.json();
170
+ const host = new LocalBrowserHost({
171
+ apiUrl,
172
+ wsToken: session.wsToken,
173
+ sessionId: session.sessionId,
174
+ browserMode: mode,
175
+ cdpUrl: input.cdpUrl,
176
+ headless: input.headless ?? true,
177
+ storageStatePath: input.storageStatePath,
178
+ onLog: (level, message) => {
179
+ if (level === "error") {
180
+ console.error(`[LocalBrowser] ${message}`);
181
+ }
182
+ }
183
+ });
184
+ host.start().catch((err) => {
185
+ console.error("Failed to start local browser:", err);
186
+ browserSessions.delete(session.sessionId);
187
+ });
188
+ browserSessions.set(session.sessionId, {
189
+ sessionId: session.sessionId,
190
+ host,
191
+ startedAt: Date.now(),
192
+ mode
193
+ });
194
+ return toolJson({
195
+ ok: true,
196
+ sessionId: session.sessionId,
197
+ mode,
198
+ expiresAt: session.expiresAt,
199
+ note: "Browser session started. The cloud agent can now control this browser. Use local_browser_stop to end the session."
200
+ });
201
+ }
202
+ if (tool === "local_browser_status") {
203
+ const input = args;
204
+ const session = browserSessions.get(input.sessionId);
205
+ if (!session) {
206
+ return toolJson({ ok: false, error: "Session not found", sessionId: input.sessionId });
207
+ }
208
+ return toolJson({
209
+ ok: true,
210
+ sessionId: session.sessionId,
211
+ mode: session.mode,
212
+ startedAt: new Date(session.startedAt).toISOString(),
213
+ uptimeMs: Date.now() - session.startedAt
214
+ });
215
+ }
216
+ if (tool === "local_browser_stop") {
217
+ const input = args;
218
+ const session = browserSessions.get(input.sessionId);
219
+ if (!session) {
220
+ return toolJson({ ok: false, error: "Session not found", sessionId: input.sessionId });
221
+ }
222
+ await session.host.stop();
223
+ browserSessions.delete(input.sessionId);
224
+ return toolJson({
225
+ ok: true,
226
+ sessionId: input.sessionId,
227
+ note: "Browser session stopped and browser closed."
228
+ });
229
+ }
230
+ if (tool === "local_browser_list") {
231
+ const sessions = Array.from(browserSessions.values()).map((s) => ({
232
+ sessionId: s.sessionId,
233
+ mode: s.mode,
234
+ startedAt: new Date(s.startedAt).toISOString(),
235
+ uptimeMs: Date.now() - s.startedAt
236
+ }));
237
+ return toolJson({
238
+ ok: true,
239
+ count: sessions.length,
240
+ sessions
241
+ });
242
+ }
243
+ if (tool === "local_browser_run") {
244
+ const input = args;
245
+ const apiUrl = resolveApiUrl();
246
+ const session = browserSessions.get(input.sessionId);
247
+ if (!session) {
248
+ return toolJson({
249
+ ok: false,
250
+ error: "Session not found locally. Make sure you started it with local_browser_start.",
251
+ sessionId: input.sessionId
252
+ });
253
+ }
254
+ const response = await fetch(`${apiUrl}/local-browser/sessions/${input.sessionId}/run`, {
255
+ method: "POST",
256
+ headers: {
257
+ "Content-Type": "application/json",
258
+ Authorization: `Bearer ${token}`
259
+ },
260
+ body: JSON.stringify({
261
+ instructions: input.instructions,
262
+ startUrl: input.startUrl ?? null
263
+ })
264
+ });
265
+ if (!response.ok) {
266
+ const text = await response.text();
267
+ return toolJson({ ok: false, error: `Failed to start run: ${text}` });
268
+ }
269
+ const result = await response.json();
270
+ return toolJson({
271
+ ok: true,
272
+ jobId: result.jobId,
273
+ sessionId: result.sessionId,
274
+ note: "Test run started. The cloud agent is now controlling your local browser. You can watch the browser to see the test in action."
275
+ });
276
+ }
277
+ return null;
278
+ }
279
+
280
+ // src/mcp/browser-tools.ts
281
+ var activeSessionId = null;
282
+ function getBrowserMCPToolDefinitions() {
283
+ return getBrowserToolDefinitionsWithLifecycle();
284
+ }
285
+ async function handleBrowserTool(name, args) {
286
+ if (name === "browser_start") {
287
+ try {
288
+ const result = await createSession({
289
+ headless: args.headless ?? false,
290
+ storageStatePath: args.storageStatePath
291
+ });
292
+ if (!result.ok) {
293
+ return toolJson({ ok: false, error: result.error });
294
+ }
295
+ activeSessionId = result.data.id;
296
+ return toolJson({
297
+ ok: true,
298
+ sessionId: activeSessionId,
299
+ note: "Browser started. You can now use browser_navigate, browser_click, and other browser tools."
300
+ });
301
+ } catch (err) {
302
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
303
+ }
304
+ }
305
+ if (name === "browser_stop") {
306
+ try {
307
+ if (activeSessionId) {
308
+ await deleteSession(activeSessionId);
309
+ activeSessionId = null;
310
+ } else {
311
+ await deleteAllSessions();
312
+ }
313
+ return toolJson({ ok: true, note: "Browser stopped and cleaned up." });
314
+ } catch (err) {
315
+ return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });
316
+ }
317
+ }
318
+ if (!name.startsWith("browser_")) return null;
319
+ try {
320
+ let sessionId = activeSessionId;
321
+ if (!sessionId) {
322
+ try {
323
+ const target = await resolveTargetSession();
324
+ sessionId = target.id;
325
+ activeSessionId = sessionId;
326
+ } catch {
327
+ return toolJson({
328
+ ok: false,
329
+ error: "No active browser session. Start one with browser_start first."
330
+ });
331
+ }
332
+ }
333
+ const toolName = name.replace(/^browser_/, "");
334
+ const result = await callTool(sessionId, toolName, args);
335
+ if (!result.ok) {
336
+ if (result.error?.includes("not found")) {
337
+ activeSessionId = null;
338
+ }
339
+ return toolJson({ ok: false, error: result.error, tool: name });
340
+ }
341
+ const toolResult = result.images?.length ? { text: result.text ?? "", images: result.images } : result.text ?? "";
342
+ return formatMCPContent(toolResult);
343
+ } catch (err) {
344
+ const message = err instanceof Error ? err.message : String(err);
345
+ return toolJson({ ok: false, error: message, tool: name });
346
+ }
347
+ }
348
+
349
+ // src/mcp/index.ts
350
+ async function runMcp(argv) {
351
+ const server = new Server(
352
+ { name: "canary-cli", version: "0.1.0" },
353
+ { capabilities: { tools: {} } }
354
+ );
355
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
356
+ tools: [
357
+ ...getSessionToolDefinitions(),
358
+ ...getBrowserMCPToolDefinitions()
359
+ ]
360
+ }));
361
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
362
+ const tool = req.params.name;
363
+ const args = req.params.arguments ?? {};
364
+ const browserResult = await handleBrowserTool(tool, args);
365
+ if (browserResult) return browserResult;
366
+ const sessionResult = await handleSessionTool(tool, args);
367
+ if (sessionResult) return sessionResult;
368
+ return toolText(`Unknown tool: ${tool}`);
369
+ });
370
+ const transport = new StdioServerTransport();
371
+ await server.connect(transport);
372
+ return new Promise(() => void 0);
373
+ }
374
+ export {
375
+ runMcp
376
+ };
377
+ //# sourceMappingURL=mcp-ZF5G5DCB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mcp/index.ts","../src/mcp/tool-helpers.ts","../src/mcp/session-tools.ts","../src/mcp/browser-tools.ts"],"sourcesContent":["/**\n * MCP Server for Canary CLI.\n *\n * Combines cloud-relay session tools and direct browser tools\n * into a single MCP server.\n *\n * @module\n */\n\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { toolText } from './tool-helpers';\nimport { getSessionToolDefinitions, handleSessionTool } from './session-tools';\nimport { getBrowserMCPToolDefinitions, handleBrowserTool } from './browser-tools';\n\nexport async function runMcp(argv: string[]) {\n const server = new Server(\n { name: 'canary-cli', version: '0.1.0' },\n { capabilities: { tools: {} } }\n );\n\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: [\n ...getSessionToolDefinitions(),\n ...getBrowserMCPToolDefinitions(),\n ],\n }));\n\n server.setRequestHandler(CallToolRequestSchema, async (req) => {\n const tool = req.params.name;\n const args = (req.params.arguments ?? {}) as Record<string, unknown>;\n\n // Try browser tools first (browser_* prefix)\n const browserResult = await handleBrowserTool(tool, args);\n if (browserResult) return browserResult;\n\n // Try session tools (local_* prefix)\n const sessionResult = await handleSessionTool(tool, args);\n if (sessionResult) return sessionResult;\n\n return toolText(`Unknown tool: ${tool}`);\n });\n\n const transport = new StdioServerTransport();\n await server.connect(transport);\n return new Promise<void>(() => undefined);\n}\n","/**\n * MCP tool response helpers.\n *\n * @module\n */\n\nimport type { ToolResult } from '@chatsdet/browser-core';\n\nexport function toolText(text: string) {\n return { content: [{ type: 'text' as const, text }] };\n}\n\nexport function toolJson(data: unknown) {\n return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };\n}\n\n/**\n * Convert a BrowserToolExecutor result to MCP SDK content blocks.\n */\nexport function formatMCPContent(result: ToolResult) {\n if (typeof result === 'string') {\n return { content: [{ type: 'text' as const, text: result }] };\n }\n\n // MCPContentResult with text and optional images\n const content: Array<{ type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }> = [];\n\n if (result.text) {\n content.push({ type: 'text', text: result.text });\n }\n\n for (const img of result.images ?? []) {\n content.push({ type: 'image', data: img.data, mimeType: img.mimeType });\n }\n\n return { content };\n}\n","/**\n * Cloud-relay session tools for the MCP server.\n *\n * These tools proxy browser control through the cloud API via WebSocket.\n *\n * @module\n */\n\nimport { readStoredToken } from '../auth';\nimport { LocalBrowserHost } from '../local-browser/host';\nimport type { LocalBrowserMode } from '../local-browser/protocol';\nimport { toolText, toolJson } from './tool-helpers';\n\nimport process from 'node:process';\n\ntype LocalBrowserStartInput = {\n mode?: LocalBrowserMode;\n cdpUrl?: string;\n headless?: boolean;\n storageStatePath?: string;\n instructions?: string;\n};\n\ntype LocalBrowserStopInput = {\n sessionId: string;\n};\n\ntype LocalBrowserStatusInput = {\n sessionId: string;\n};\n\ninterface BrowserSession {\n sessionId: string;\n host: LocalBrowserHost;\n startedAt: number;\n mode: LocalBrowserMode;\n}\n\n// Global browser sessions managed by MCP\nconst browserSessions = new Map<string, BrowserSession>();\n\nconst DEFAULT_API_URL = 'https://api.trycanary.ai';\n\nfunction resolveApiUrl(input?: string): string {\n return input ?? process.env.CANARY_API_URL ?? DEFAULT_API_URL;\n}\n\nasync function resolveToken(): Promise<string> {\n const token = process.env.CANARY_API_TOKEN ?? (await readStoredToken());\n if (!token) {\n throw new Error('Missing token. Run `canary login` first or set CANARY_API_TOKEN.');\n }\n return token;\n}\n\nexport function getSessionToolDefinitions() {\n return [\n {\n name: 'local_browser_start',\n description:\n '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.',\n inputSchema: {\n type: 'object',\n properties: {\n mode: {\n type: 'string',\n enum: ['playwright', 'cdp'],\n description: \"Browser mode: 'playwright' for fresh browser, 'cdp' to connect to existing Chrome\",\n },\n cdpUrl: {\n type: 'string',\n description: 'CDP endpoint URL when mode is \\'cdp\\' (e.g. http://localhost:9222)',\n },\n headless: {\n type: 'boolean',\n description: 'Run browser headless (default: true for playwright mode)',\n },\n storageStatePath: {\n type: 'string',\n description: 'Path to Playwright storage state JSON for pre-authenticated sessions',\n },\n instructions: {\n type: 'string',\n description: 'Instructions for the cloud agent on what to test',\n },\n },\n },\n },\n {\n name: 'local_browser_status',\n description: 'Check the status of a local browser session.',\n inputSchema: {\n type: 'object',\n properties: {\n sessionId: { type: 'string' },\n },\n required: ['sessionId'],\n },\n },\n {\n name: 'local_browser_stop',\n description: 'Stop a local browser session and close the browser.',\n inputSchema: {\n type: 'object',\n properties: {\n sessionId: { type: 'string' },\n },\n required: ['sessionId'],\n },\n },\n {\n name: 'local_browser_list',\n description: 'List all active local browser sessions.',\n inputSchema: {\n type: 'object',\n properties: {},\n },\n },\n {\n name: 'local_browser_run',\n description:\n 'Start a test run on an active local browser session. The cloud agent will control the local browser according to the instructions.',\n inputSchema: {\n type: 'object',\n properties: {\n sessionId: {\n type: 'string',\n description: 'The session ID from local_browser_start',\n },\n instructions: {\n type: 'string',\n description: 'Instructions for the cloud agent on what to test',\n },\n startUrl: {\n type: 'string',\n description: 'Optional URL to navigate to before starting',\n },\n },\n required: ['sessionId', 'instructions'],\n },\n },\n ];\n}\n\nexport async function handleSessionTool(tool: string, args: Record<string, unknown>) {\n const token = await resolveToken();\n\n if (tool === 'local_browser_start') {\n const input = args as unknown as LocalBrowserStartInput;\n const apiUrl = resolveApiUrl();\n const mode = input.mode ?? 'playwright';\n\n const sessionResponse = await fetch(`${apiUrl}/local-browser/sessions`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({\n browserMode: mode,\n instructions: input.instructions ?? null,\n }),\n });\n\n if (!sessionResponse.ok) {\n const text = await sessionResponse.text();\n return toolJson({ ok: false, error: `Failed to create session: ${text}` });\n }\n\n const session = (await sessionResponse.json()) as {\n ok: boolean;\n sessionId: string;\n wsToken: string;\n expiresAt: string;\n };\n\n const host = new LocalBrowserHost({\n apiUrl,\n wsToken: session.wsToken,\n sessionId: session.sessionId,\n browserMode: mode,\n cdpUrl: input.cdpUrl,\n headless: input.headless ?? true,\n storageStatePath: input.storageStatePath,\n onLog: (level, message) => {\n if (level === 'error') {\n console.error(`[LocalBrowser] ${message}`);\n }\n },\n });\n\n host.start().catch((err) => {\n console.error('Failed to start local browser:', err);\n browserSessions.delete(session.sessionId);\n });\n\n browserSessions.set(session.sessionId, {\n sessionId: session.sessionId,\n host,\n startedAt: Date.now(),\n mode,\n });\n\n return toolJson({\n ok: true,\n sessionId: session.sessionId,\n mode,\n expiresAt: session.expiresAt,\n note: 'Browser session started. The cloud agent can now control this browser. Use local_browser_stop to end the session.',\n });\n }\n\n if (tool === 'local_browser_status') {\n const input = args as unknown as LocalBrowserStatusInput;\n const session = browserSessions.get(input.sessionId);\n\n if (!session) {\n return toolJson({ ok: false, error: 'Session not found', sessionId: input.sessionId });\n }\n\n return toolJson({\n ok: true,\n sessionId: session.sessionId,\n mode: session.mode,\n startedAt: new Date(session.startedAt).toISOString(),\n uptimeMs: Date.now() - session.startedAt,\n });\n }\n\n if (tool === 'local_browser_stop') {\n const input = args as unknown as LocalBrowserStopInput;\n const session = browserSessions.get(input.sessionId);\n\n if (!session) {\n return toolJson({ ok: false, error: 'Session not found', sessionId: input.sessionId });\n }\n\n await session.host.stop();\n browserSessions.delete(input.sessionId);\n\n return toolJson({\n ok: true,\n sessionId: input.sessionId,\n note: 'Browser session stopped and browser closed.',\n });\n }\n\n if (tool === 'local_browser_list') {\n const sessions = Array.from(browserSessions.values()).map((s) => ({\n sessionId: s.sessionId,\n mode: s.mode,\n startedAt: new Date(s.startedAt).toISOString(),\n uptimeMs: Date.now() - s.startedAt,\n }));\n\n return toolJson({\n ok: true,\n count: sessions.length,\n sessions,\n });\n }\n\n if (tool === 'local_browser_run') {\n const input = args as unknown as {\n sessionId: string;\n instructions: string;\n startUrl?: string;\n };\n const apiUrl = resolveApiUrl();\n\n const session = browserSessions.get(input.sessionId);\n if (!session) {\n return toolJson({\n ok: false,\n error: 'Session not found locally. Make sure you started it with local_browser_start.',\n sessionId: input.sessionId,\n });\n }\n\n const response = await fetch(`${apiUrl}/local-browser/sessions/${input.sessionId}/run`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${token}`,\n },\n body: JSON.stringify({\n instructions: input.instructions,\n startUrl: input.startUrl ?? null,\n }),\n });\n\n if (!response.ok) {\n const text = await response.text();\n return toolJson({ ok: false, error: `Failed to start run: ${text}` });\n }\n\n const result = (await response.json()) as { ok: boolean; jobId: string; sessionId: string };\n\n return toolJson({\n ok: true,\n jobId: result.jobId,\n sessionId: result.sessionId,\n note: 'Test run started. The cloud agent is now controlling your local browser. You can watch the browser to see the test in action.',\n });\n }\n\n return null; // Not handled\n}\n","/**\n * Browser MCP tool registration — daemon adapter.\n *\n * Thin layer that maps MCP tool calls to the session daemon,\n * ensuring the CLI gets the SAME semantic diff, snapshot formatting,\n * click validation, auto-snapshot, and recovery notices as the cloud agent.\n *\n * The daemon owns all browser sessions. MCP is just a protocol adapter.\n *\n * @module\n */\n\nimport { getBrowserToolDefinitionsWithLifecycle } from '@chatsdet/browser-core';\nimport {\n createSession,\n deleteSession,\n deleteAllSessions,\n callTool,\n resolveTargetSession,\n listSessions,\n} from '../session/daemon-client.js';\nimport { formatMCPContent, toolJson } from './tool-helpers';\nimport type { ToolResult } from '@chatsdet/browser-core';\n\n/** Track the active session ID within this MCP server instance */\nlet activeSessionId: string | null = null;\n\n/**\n * Get tool definitions for browser MCP tools (including lifecycle).\n */\nexport function getBrowserMCPToolDefinitions() {\n return getBrowserToolDefinitionsWithLifecycle();\n}\n\n/**\n * Handle a browser tool call.\n * Returns null if the tool name is not a browser tool.\n */\nexport async function handleBrowserTool(name: string, args: Record<string, unknown>) {\n // Lifecycle tools\n if (name === 'browser_start') {\n try {\n const result = await createSession({\n headless: (args.headless as boolean) ?? false,\n storageStatePath: args.storageStatePath as string | undefined,\n });\n\n if (!result.ok) {\n return toolJson({ ok: false, error: result.error });\n }\n\n activeSessionId = result.data!.id;\n return toolJson({\n ok: true,\n sessionId: activeSessionId,\n note: 'Browser started. You can now use browser_navigate, browser_click, and other browser tools.',\n });\n } catch (err) {\n return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });\n }\n }\n\n if (name === 'browser_stop') {\n try {\n if (activeSessionId) {\n await deleteSession(activeSessionId);\n activeSessionId = null;\n } else {\n // Stop all sessions if no specific one tracked\n await deleteAllSessions();\n }\n return toolJson({ ok: true, note: 'Browser stopped and cleaned up.' });\n } catch (err) {\n return toolJson({ ok: false, error: err instanceof Error ? err.message : String(err) });\n }\n }\n\n // All other browser_* tools require an active session\n if (!name.startsWith('browser_')) return null;\n\n try {\n // Resolve target session: prefer tracked MCP session, then auto-resolve\n let sessionId = activeSessionId;\n if (!sessionId) {\n try {\n const target = await resolveTargetSession();\n sessionId = target.id;\n activeSessionId = sessionId;\n } catch {\n return toolJson({\n ok: false,\n error: 'No active browser session. Start one with browser_start first.',\n });\n }\n }\n\n // Strip 'browser_' prefix for the daemon tool name\n const toolName = name.replace(/^browser_/, '');\n const result = await callTool(sessionId, toolName, args);\n\n if (!result.ok) {\n // If session not found, clear tracking and report error\n if (result.error?.includes('not found')) {\n activeSessionId = null;\n }\n return toolJson({ ok: false, error: result.error, tool: name });\n }\n\n // Convert daemon response back to ToolResult format for MCP formatting\n const toolResult: ToolResult = result.images?.length\n ? { text: result.text ?? '', images: result.images }\n : result.text ?? '';\n\n return formatMCPContent(toolResult);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n return toolJson({ ok: false, error: message, tool: name });\n }\n}\n"],"mappings":"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","names":[]}