surf-cli 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -170,16 +170,20 @@ surf scroll.bottom # Scroll to bottom
170
170
 
171
171
  ### Screenshots
172
172
 
173
- Screenshots are optimized for AI consumption by default:
173
+ Screenshots auto-save to `/tmp` by default (optimized for AI agents):
174
174
 
175
175
  ```bash
176
- surf screenshot --output /tmp/shot.png # Auto-resized to 1200px max
177
- surf screenshot --full --output /tmp/hd.png # Full resolution
178
- surf screenshot --annotate --output /tmp/labeled.png # With element labels
179
- surf screenshot --fullpage --output /tmp/full.png # Entire page
180
- surf snap # Quick save to /tmp
176
+ surf screenshot # Auto-saves to /tmp/surf-snap-*.png
177
+ surf screenshot --output /tmp/shot.png # Save to specific path
178
+ surf screenshot --full --output /tmp/hd.png # Full resolution (skip resize)
179
+ surf screenshot --annotate # With element labels
180
+ surf screenshot --fullpage # Entire page
181
+ surf screenshot --no-save # Return base64 + ID only (no file)
182
+ surf snap # Alias for screenshot
181
183
  ```
182
184
 
185
+ To disable auto-save globally, set `autoSaveScreenshots: false` in `surf.json`.
186
+
183
187
  Actions like `click`, `type`, and `scroll` automatically capture a screenshot after execution - no extra command needed.
184
188
 
185
189
  ### Tabs
package/native/cli.cjs CHANGED
@@ -234,7 +234,7 @@ const TOOLS = {
234
234
  examples: [{ cmd: "forward", desc: "Browser forward" }]
235
235
  },
236
236
  "screenshot": {
237
- desc: "Capture screenshot (auto-resized for LLM by default)",
237
+ desc: "Capture screenshot (auto-saves to /tmp by default)",
238
238
  args: [],
239
239
  opts: {
240
240
  output: "Save to file",
@@ -243,14 +243,15 @@ const TOOLS = {
243
243
  fullpage: "Capture full page",
244
244
  "max-height": "Max height for fullpage (default: 4000)",
245
245
  full: "Skip resize, save at full resolution",
246
- "max-size": "Max dimension in px (default: 1200)"
246
+ "max-size": "Max dimension in px (default: 1200)",
247
+ "no-save": "Don't auto-save, return base64 + ID (saves context)"
247
248
  },
248
249
  examples: [
249
- { cmd: "screenshot --output /tmp/shot.png", desc: "Save to file (auto-resized)" },
250
- { cmd: "screenshot --full --output /tmp/shot.png", desc: "Full resolution" },
251
- { cmd: "screenshot --max-size 800 --output /tmp/small.png", desc: "Custom max size" },
252
- { cmd: "screenshot --annotate --output /tmp/annotated.png", desc: "With element labels" },
253
- { cmd: "snap", desc: "Auto-save to /tmp (resized)" },
250
+ { cmd: "screenshot", desc: "Auto-save to /tmp (default)" },
251
+ { cmd: "screenshot --output /tmp/shot.png", desc: "Save to specific file" },
252
+ { cmd: "screenshot --no-save", desc: "Return base64 without saving" },
253
+ { cmd: "screenshot --annotate", desc: "With element labels" },
254
+ { cmd: "snap", desc: "Alias for screenshot" },
254
255
  ]
255
256
  },
256
257
  "snap": { desc: "Alias for screenshot (auto-saves to /tmp)", args: [], alias: "screenshot" },
@@ -1672,7 +1673,7 @@ if (args.includes("--script")) {
1672
1673
  return;
1673
1674
  }
1674
1675
 
1675
- const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl"];
1676
+ const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl", "no-save"];
1676
1677
 
1677
1678
  const AUTO_SCREENSHOT_TOOLS = ["click", "type", "key", "smart_type", "form.fill", "form_input", "drag", "hover", "scroll", "scroll.top", "scroll.bottom", "scroll.to", "dialog.accept", "dialog.dismiss", "js", "eval"];
1678
1679
 
@@ -1727,10 +1728,14 @@ if (REMOVED_COMMANDS[tool]) {
1727
1728
  process.exit(1);
1728
1729
  }
1729
1730
 
1730
- const wasSnap = tool === "snap";
1731
1731
  tool = ALIASES[tool] || tool;
1732
1732
 
1733
- if (wasSnap && !options.output && !options.savePath) {
1733
+ // Auto-save screenshots to temp file when no --output specified
1734
+ // This ensures agents always get a usable file path, not just an in-memory ID
1735
+ // Can be disabled with --no-save flag or autoSaveScreenshots: false in surf.json
1736
+ const config = loadConfig();
1737
+ const autoSaveEnabled = config.autoSaveScreenshots !== false && !options["no-save"];
1738
+ if (tool === "screenshot" && !options.output && !options.savePath && autoSaveEnabled) {
1734
1739
  options.savePath = `/tmp/surf-snap-${Date.now()}.png`;
1735
1740
  }
1736
1741
 
@@ -1887,7 +1892,7 @@ if (!noScreenshot && AUTO_SCREENSHOT_TOOLS.includes(tool)) {
1887
1892
  const outputPath = toolArgs.output;
1888
1893
  delete toolArgs.output;
1889
1894
 
1890
- if ((tool === "screenshot" || tool === "snap") && outputPath) {
1895
+ if (tool === "screenshot" && outputPath) {
1891
1896
  if (typeof outputPath !== "string") {
1892
1897
  console.error("Error: --output requires a file path");
1893
1898
  process.exit(1);
@@ -2208,7 +2213,7 @@ async function handleResponse(response) {
2208
2213
  process.exit(0);
2209
2214
  }
2210
2215
 
2211
- if ((tool === "screenshot" || tool === "snap") && data?.base64 && (outputPath || toolArgs.savePath)) {
2216
+ if (tool === "screenshot" && data?.base64 && (outputPath || toolArgs.savePath)) {
2212
2217
  const saveTo = outputPath || toolArgs.savePath;
2213
2218
  fs.writeFileSync(saveTo, Buffer.from(data.base64, "base64"));
2214
2219
 
@@ -2227,8 +2232,11 @@ async function handleResponse(response) {
2227
2232
  } else {
2228
2233
  console.log(`Saved to ${saveTo} (${origWidth}x${origHeight})`);
2229
2234
  }
2230
- } else if ((tool === "screenshot" || tool === "snap") && data?.message) {
2235
+ } else if (tool === "screenshot" && data?.message) {
2231
2236
  console.log(data.message);
2237
+ if (data.screenshotId) {
2238
+ console.log(`[Screenshot ID: ${data.screenshotId}]`);
2239
+ }
2232
2240
  } else if (tool === "tab.list") {
2233
2241
  const tabs = data?.tabs || data || [];
2234
2242
  if (Array.isArray(tabs)) {
package/native/config.cjs CHANGED
@@ -8,6 +8,9 @@ let cachedConfig = null;
8
8
  let cachedConfigPath = null;
9
9
 
10
10
  const STARTER_CONFIG = {
11
+ // Set to false to disable auto-saving screenshots to /tmp
12
+ // When disabled, screenshots return base64 + ID instead of file path
13
+ autoSaveScreenshots: true,
11
14
  routes: {
12
15
  main: ["http://localhost:3000"]
13
16
  },
@@ -84,7 +84,17 @@ function formatToolContent(result, log = () => {}) {
84
84
  return text(result.output);
85
85
  }
86
86
 
87
- if (result.screenshotId) {
87
+ // Screenshot saved to file (has path but no base64)
88
+ if (result.path && result.message && !result.base64) {
89
+ let msg = result.message;
90
+ if (result.screenshotId) {
91
+ msg += `\n[Screenshot ID: ${result.screenshotId} - use with upload_image]`;
92
+ }
93
+ return text(msg);
94
+ }
95
+
96
+ // Screenshot with inline base64 (MCP flow or no savePath)
97
+ if (result.screenshotId && result.base64) {
88
98
  const dims = result.width && result.height
89
99
  ? `${result.width}x${result.height}`
90
100
  : "unknown dimensions";
@@ -489,7 +499,7 @@ function mapToolToMessage(tool, args, tabId) {
489
499
  case "screenshot":
490
500
  return {
491
501
  type: "EXECUTE_SCREENSHOT",
492
- savePath: a.savePath,
502
+ savePath: a.savePath || a.output, // Accept both savePath (CLI) and output (MCP)
493
503
  annotate: a.annotate || false,
494
504
  fullpage: a.fullpage || false,
495
505
  maxHeight: a["max-height"] || 4000,
package/native/host.cjs CHANGED
@@ -996,7 +996,9 @@ function processInput() {
996
996
  }
997
997
  }
998
998
  sendToolResponse(socket, originalId, {
999
- message: `Saved to ${savePath} (${finalDims})`
999
+ message: `Saved to ${savePath} (${finalDims})`,
1000
+ path: savePath,
1001
+ screenshotId: msg.screenshotId, // Preserve for upload_image workflow
1000
1002
  }, null);
1001
1003
  } catch (e) {
1002
1004
  sendToolResponse(socket, originalId, null, `Failed to save: ${e.message}`);
@@ -0,0 +1,415 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Surf Stream Viewer</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
16
+ background: #1a1a1a;
17
+ color: #e0e0e0;
18
+ display: flex;
19
+ flex-direction: column;
20
+ height: 100vh;
21
+ overflow: hidden;
22
+ }
23
+
24
+ #status {
25
+ background: #2d2d2d;
26
+ padding: 12px 20px;
27
+ border-bottom: 1px solid #404040;
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 20px;
31
+ font-size: 14px;
32
+ flex-shrink: 0;
33
+ }
34
+
35
+ #status.connected {
36
+ border-bottom-color: #4caf50;
37
+ }
38
+
39
+ #status.disconnected {
40
+ border-bottom-color: #f44336;
41
+ }
42
+
43
+ .status-indicator {
44
+ width: 10px;
45
+ height: 10px;
46
+ border-radius: 50%;
47
+ background: #666;
48
+ }
49
+
50
+ .status-indicator.connected {
51
+ background: #4caf50;
52
+ box-shadow: 0 0 8px #4caf50;
53
+ }
54
+
55
+ .status-indicator.disconnected {
56
+ background: #f44336;
57
+ }
58
+
59
+ #viewport-container {
60
+ flex: 1;
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ overflow: auto;
65
+ background: #0a0a0a;
66
+ position: relative;
67
+ }
68
+
69
+ #viewport {
70
+ display: block;
71
+ max-width: 100%;
72
+ max-height: 100%;
73
+ cursor: crosshair;
74
+ image-rendering: crisp-edges;
75
+ image-rendering: pixelated;
76
+ }
77
+
78
+ #viewport:not(.ready) {
79
+ opacity: 0.5;
80
+ }
81
+
82
+ #loading {
83
+ position: absolute;
84
+ top: 50%;
85
+ left: 50%;
86
+ transform: translate(-50%, -50%);
87
+ text-align: center;
88
+ color: #888;
89
+ }
90
+
91
+ .spinner {
92
+ border: 3px solid #333;
93
+ border-top: 3px solid #4caf50;
94
+ border-radius: 50%;
95
+ width: 40px;
96
+ height: 40px;
97
+ animation: spin 1s linear infinite;
98
+ margin: 0 auto 10px;
99
+ }
100
+
101
+ @keyframes spin {
102
+ 0% { transform: rotate(0deg); }
103
+ 100% { transform: rotate(360deg); }
104
+ }
105
+ </style>
106
+ </head>
107
+ <body>
108
+ <div id="status" class="disconnected">
109
+ <div class="status-indicator disconnected"></div>
110
+ <span id="status-text">Connecting...</span>
111
+ <span id="viewport-info" style="margin-left: auto;"></span>
112
+ </div>
113
+ <div id="viewport-container">
114
+ <div id="loading">
115
+ <div class="spinner"></div>
116
+ <div>Waiting for stream...</div>
117
+ </div>
118
+ <canvas id="viewport"></canvas>
119
+ </div>
120
+
121
+ <script>
122
+ const canvas = document.getElementById('viewport');
123
+ const ctx = canvas.getContext('2d');
124
+ const statusEl = document.getElementById('status');
125
+ const statusText = document.getElementById('status-text');
126
+ const viewportInfo = document.getElementById('viewport-info');
127
+ const loadingEl = document.getElementById('loading');
128
+
129
+ let ws = null;
130
+ let frameCount = 0;
131
+ let lastFrameTime = Date.now();
132
+ let fps = 0;
133
+ let isMouseDown = false;
134
+ let activeButton = 'left';
135
+ let activeTouches = new Map();
136
+
137
+ function connect() {
138
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
139
+ const wsUrl = `${protocol}//${window.location.host}`;
140
+
141
+ ws = new WebSocket(wsUrl);
142
+
143
+ ws.onopen = () => {
144
+ statusEl.className = 'connected';
145
+ statusEl.querySelector('.status-indicator').className = 'status-indicator connected';
146
+ statusText.textContent = 'Connected';
147
+ loadingEl.style.display = 'none';
148
+ };
149
+
150
+ ws.onmessage = (event) => {
151
+ try {
152
+ const message = JSON.parse(event.data);
153
+
154
+ if (message.type === 'frame') {
155
+ drawFrame(message);
156
+ } else if (message.type === 'status') {
157
+ updateStatus(message);
158
+ } else if (message.type === 'error') {
159
+ statusText.textContent = `Error: ${message.message}`;
160
+ statusEl.className = 'disconnected';
161
+ statusEl.querySelector('.status-indicator').className = 'status-indicator disconnected';
162
+ }
163
+ } catch (e) {
164
+ console.error('Error parsing message:', e);
165
+ }
166
+ };
167
+
168
+ ws.onerror = (error) => {
169
+ console.error('WebSocket error:', error);
170
+ statusText.textContent = 'Connection error';
171
+ statusEl.className = 'disconnected';
172
+ statusEl.querySelector('.status-indicator').className = 'status-indicator disconnected';
173
+ };
174
+
175
+ ws.onclose = () => {
176
+ statusText.textContent = 'Disconnected';
177
+ statusEl.className = 'disconnected';
178
+ statusEl.querySelector('.status-indicator').className = 'status-indicator disconnected';
179
+ loadingEl.style.display = 'block';
180
+
181
+ setTimeout(connect, 2000);
182
+ };
183
+ }
184
+
185
+ function drawFrame(message) {
186
+ const img = new Image();
187
+ img.onload = () => {
188
+ canvas.width = img.width;
189
+ canvas.height = img.height;
190
+ ctx.drawImage(img, 0, 0);
191
+ canvas.classList.add('ready');
192
+
193
+ frameCount++;
194
+ const now = Date.now();
195
+ if (now - lastFrameTime >= 1000) {
196
+ fps = frameCount;
197
+ frameCount = 0;
198
+ lastFrameTime = now;
199
+ }
200
+
201
+ updateViewportInfo(img.width, img.height, fps);
202
+ };
203
+ const mimeType = message.data.startsWith('iVBORw0K') ? 'image/png' : 'image/jpeg';
204
+ img.src = `data:${mimeType};base64,${message.data}`;
205
+ }
206
+
207
+ function updateStatus(message) {
208
+ if (message.viewportWidth && message.viewportHeight) {
209
+ updateViewportInfo(message.viewportWidth, message.viewportHeight, fps);
210
+ }
211
+ }
212
+
213
+ function updateViewportInfo(width, height, fps) {
214
+ viewportInfo.textContent = `${width}×${height} | ${fps} fps`;
215
+ }
216
+
217
+ function getCanvasCoordinates(e) {
218
+ const rect = canvas.getBoundingClientRect();
219
+ const scaleX = canvas.width / rect.width;
220
+ const scaleY = canvas.height / rect.height;
221
+
222
+ const x = Math.round((e.clientX - rect.left) * scaleX);
223
+ const y = Math.round((e.clientY - rect.top) * scaleY);
224
+
225
+ // Check if click is within canvas bounds
226
+ if (x < 0 || x > canvas.width || y < 0 || y > canvas.height) {
227
+ return null;
228
+ }
229
+
230
+ return { x, y };
231
+ }
232
+
233
+ function getModifiers(e) {
234
+ return (e.altKey ? 1 : 0) | (e.ctrlKey ? 2 : 0) | (e.metaKey ? 4 : 0) | (e.shiftKey ? 8 : 0);
235
+ }
236
+
237
+ function sendMouseEvent(eventType, e, button = 'left') {
238
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
239
+
240
+ const coords = getCanvasCoordinates(e);
241
+ if (!coords) return; // Click outside canvas bounds
242
+
243
+ ws.send(JSON.stringify({
244
+ type: 'input_mouse',
245
+ eventType,
246
+ x: coords.x,
247
+ y: coords.y,
248
+ button,
249
+ clickCount: 1,
250
+ modifiers: getModifiers(e),
251
+ }));
252
+ }
253
+
254
+ canvas.addEventListener('mousedown', (e) => {
255
+ e.preventDefault();
256
+ isMouseDown = true;
257
+ activeButton = e.button === 2 ? 'right' : e.button === 1 ? 'middle' : 'left';
258
+ // Send mouseMoved first to ensure element has hover state
259
+ sendMouseEvent('mouseMoved', e, 'none');
260
+ sendMouseEvent('mousePressed', e, activeButton);
261
+ });
262
+
263
+ canvas.addEventListener('mouseup', (e) => {
264
+ e.preventDefault();
265
+ const button = e.button === 2 ? 'right' : e.button === 1 ? 'middle' : 'left';
266
+ sendMouseEvent('mouseReleased', e, button);
267
+ isMouseDown = false;
268
+ activeButton = 'left';
269
+ });
270
+
271
+ canvas.addEventListener('mousemove', (e) => {
272
+ e.preventDefault();
273
+ if (isMouseDown) {
274
+ sendMouseEvent('mouseMoved', e, activeButton);
275
+ }
276
+ });
277
+
278
+ canvas.addEventListener('mouseleave', (e) => {
279
+ if (isMouseDown) {
280
+ sendMouseEvent('mouseReleased', e, activeButton);
281
+ isMouseDown = false;
282
+ activeButton = 'left';
283
+ }
284
+ });
285
+
286
+ canvas.addEventListener('contextmenu', (e) => {
287
+ e.preventDefault();
288
+ });
289
+
290
+ canvas.addEventListener('wheel', (e) => {
291
+ e.preventDefault();
292
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
293
+
294
+ const coords = getCanvasCoordinates(e);
295
+ let deltaX = e.deltaX;
296
+ let deltaY = e.deltaY;
297
+ if (e.deltaMode === 1) {
298
+ deltaX *= 40;
299
+ deltaY *= 40;
300
+ } else if (e.deltaMode === 2) {
301
+ deltaX *= window.innerHeight;
302
+ deltaY *= window.innerHeight;
303
+ }
304
+ ws.send(JSON.stringify({
305
+ type: 'input_mouse',
306
+ eventType: 'mouseWheel',
307
+ x: coords.x,
308
+ y: coords.y,
309
+ deltaX,
310
+ deltaY,
311
+ modifiers: getModifiers(e),
312
+ }));
313
+ });
314
+
315
+ function getTouchCoordinates(touch) {
316
+ const rect = canvas.getBoundingClientRect();
317
+ const scaleX = canvas.width / rect.width;
318
+ const scaleY = canvas.height / rect.height;
319
+ return {
320
+ x: Math.round((touch.clientX - rect.left) * scaleX),
321
+ y: Math.round((touch.clientY - rect.top) * scaleY),
322
+ id: touch.identifier,
323
+ };
324
+ }
325
+
326
+ function sendTouchEvent(eventType, touches) {
327
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
328
+ ws.send(JSON.stringify({
329
+ type: 'input_touch',
330
+ eventType,
331
+ touchPoints: touches,
332
+ modifiers: 0,
333
+ }));
334
+ }
335
+
336
+ canvas.addEventListener('touchstart', (e) => {
337
+ e.preventDefault();
338
+ for (const touch of e.changedTouches) {
339
+ const coords = getTouchCoordinates(touch);
340
+ activeTouches.set(touch.identifier, coords);
341
+ }
342
+ sendTouchEvent('touchStart', Array.from(activeTouches.values()));
343
+ });
344
+
345
+ canvas.addEventListener('touchend', (e) => {
346
+ e.preventDefault();
347
+ for (const touch of e.changedTouches) {
348
+ activeTouches.delete(touch.identifier);
349
+ }
350
+ sendTouchEvent('touchEnd', Array.from(activeTouches.values()));
351
+ });
352
+
353
+ canvas.addEventListener('touchmove', (e) => {
354
+ e.preventDefault();
355
+ for (const touch of e.changedTouches) {
356
+ const coords = getTouchCoordinates(touch);
357
+ activeTouches.set(touch.identifier, coords);
358
+ }
359
+ sendTouchEvent('touchMove', Array.from(activeTouches.values()));
360
+ });
361
+
362
+ canvas.addEventListener('touchcancel', (e) => {
363
+ e.preventDefault();
364
+ for (const touch of e.changedTouches) {
365
+ activeTouches.delete(touch.identifier);
366
+ }
367
+ sendTouchEvent('touchCancel', Array.from(activeTouches.values()));
368
+ });
369
+
370
+ document.addEventListener('keydown', (e) => {
371
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
372
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
373
+
374
+ e.preventDefault();
375
+ const modifiers = (e.altKey ? 1 : 0) | (e.ctrlKey ? 2 : 0) | (e.metaKey ? 4 : 0) | (e.shiftKey ? 8 : 0);
376
+
377
+ // Send keyDown event
378
+ ws.send(JSON.stringify({
379
+ type: 'input_keyboard',
380
+ eventType: 'keyDown',
381
+ key: e.key,
382
+ code: e.code,
383
+ modifiers,
384
+ }));
385
+
386
+ // For printable characters, also send a char event
387
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
388
+ ws.send(JSON.stringify({
389
+ type: 'input_keyboard',
390
+ eventType: 'char',
391
+ text: e.key,
392
+ modifiers,
393
+ }));
394
+ }
395
+ });
396
+
397
+ document.addEventListener('keyup', (e) => {
398
+ if (!ws || ws.readyState !== WebSocket.OPEN) return;
399
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
400
+
401
+ e.preventDefault();
402
+
403
+ ws.send(JSON.stringify({
404
+ type: 'input_keyboard',
405
+ eventType: 'keyUp',
406
+ key: e.key,
407
+ code: e.code,
408
+ modifiers: (e.altKey ? 1 : 0) | (e.ctrlKey ? 2 : 0) | (e.metaKey ? 4 : 0) | (e.shiftKey ? 8 : 0),
409
+ }));
410
+ });
411
+
412
+ connect();
413
+ </script>
414
+ </body>
415
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surf-cli",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
5
5
  "keywords": [
6
6
  "chrome",
@@ -49,14 +49,14 @@
49
49
  "uninstall:native": "node scripts/uninstall-native-host.cjs"
50
50
  },
51
51
  "dependencies": {
52
- "@google/generative-ai": "^0.21.0",
52
+ "@google/generative-ai": "^0.24.1",
53
53
  "@modelcontextprotocol/sdk": "^1.7.0",
54
54
  "buffer": "^6.0.3",
55
55
  "crypto-browserify": "^3.12.1",
56
56
  "events": "^3.3.0",
57
57
  "stream-browserify": "^3.0.0",
58
58
  "vite-plugin-node-polyfills": "^0.24.0",
59
- "zod": "^3.24.0"
59
+ "zod": "^4.3.5"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@biomejs/biome": "^2.3.11",
@@ -64,7 +64,7 @@
64
64
  "@vitest/coverage-v8": "^4.0.16",
65
65
  "@vitest/ui": "^4.0.16",
66
66
  "typescript": "^5.7.2",
67
- "vite": "^6.0.0",
67
+ "vite": "^7.3.1",
68
68
  "vitest": "^4.0.16"
69
69
  }
70
70
  }