surf-cli 2.1.0 → 2.2.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/native/cli.cjs CHANGED
@@ -349,6 +349,37 @@ const TOOLS = {
349
349
  },
350
350
  }
351
351
  },
352
+ element: {
353
+ desc: "Element inspection",
354
+ commands: {
355
+ "element.styles": {
356
+ desc: "Get computed styles from element(s)",
357
+ args: ["ref_or_selector"],
358
+ examples: [
359
+ { cmd: "element.styles e5", desc: "Get styles by ref" },
360
+ { cmd: 'element.styles ".header"', desc: "Get styles by selector (can return multiple)" },
361
+ ]
362
+ },
363
+ }
364
+ },
365
+ forms: {
366
+ desc: "Form interactions",
367
+ commands: {
368
+ "select": {
369
+ desc: "Select option(s) in dropdown",
370
+ args: ["ref_or_selector", "values..."],
371
+ opts: {
372
+ by: "Match by: value (default), label, index"
373
+ },
374
+ examples: [
375
+ { cmd: 'select e5 "US"', desc: "Select by value" },
376
+ { cmd: 'select e5 "opt1" "opt2"', desc: "Multi-select" },
377
+ { cmd: 'select e5 --by label "United States"', desc: "Select by visible text" },
378
+ { cmd: 'select e5 --by index 0', desc: "Select first option" },
379
+ ]
380
+ },
381
+ }
382
+ },
352
383
  wait: {
353
384
  desc: "Waiting",
354
385
  commands: {
@@ -1809,6 +1840,8 @@ const PRIMARY_ARG_MAP = {
1809
1840
  "locate.label": "label",
1810
1841
  "emulate.device": "device",
1811
1842
  "frame.js": "code",
1843
+ "element.styles": "selector",
1844
+ "select": "selector",
1812
1845
  };
1813
1846
 
1814
1847
  const toolArgs = { ...options };
@@ -1845,6 +1878,17 @@ if (tool === "js" && toolArgs.file) {
1845
1878
  }
1846
1879
  }
1847
1880
 
1881
+ // Handle select command: capture multiple values after selector
1882
+ if (tool === "select" && positional.length > 2) {
1883
+ const values = positional.slice(2); // All args after "select <selector>"
1884
+ toolArgs.values = values.length === 1 ? values[0] : values;
1885
+ } else if (tool === "select" && positional.length === 2) {
1886
+ // Only selector provided, no values
1887
+ console.error("Error: select requires at least one value");
1888
+ console.error("Usage: surf select <selector> <value...>");
1889
+ process.exit(1);
1890
+ }
1891
+
1848
1892
  if (toolArgs.into && !toolArgs.selector) {
1849
1893
  toolArgs.selector = toolArgs.into;
1850
1894
  delete toolArgs.into;
@@ -20,6 +20,37 @@ function formatToolContent(result, log = () => {}) {
20
20
  return text(result.content);
21
21
  }
22
22
 
23
+ // Handle element.styles response
24
+ if (result.styles && Array.isArray(result.styles)) {
25
+ const output = result.styles.map(el => {
26
+ const lines = [`<${el.tag}>${el.text ? ` "${el.text.slice(0, 50)}${el.text.length > 50 ? '...' : ''}"` : ''}`];
27
+ if (el.box) lines.push(` box: ${el.box.x},${el.box.y} ${el.box.width}x${el.box.height}`);
28
+ const s = el.styles;
29
+ if (s) {
30
+ if (s.fontSize) lines.push(` font: ${s.fontSize} ${s.fontWeight} ${s.fontFamily}`);
31
+ if (s.color) lines.push(` color: ${s.color}`);
32
+ if (s.backgroundColor && s.backgroundColor !== 'rgba(0, 0, 0, 0)') lines.push(` bg: ${s.backgroundColor}`);
33
+ if (s.borderRadius && s.borderRadius !== '0px') lines.push(` radius: ${s.borderRadius}`);
34
+ if (s.border) lines.push(` border: ${s.border}`);
35
+ if (s.boxShadow) lines.push(` shadow: ${s.boxShadow}`);
36
+ if (s.padding && s.padding !== '0px') lines.push(` padding: ${s.padding}`);
37
+ }
38
+ return lines.join('\n');
39
+ }).join('\n\n');
40
+ return text(output || "No elements found");
41
+ }
42
+
43
+ // Handle select response
44
+ if (result.selected !== undefined) {
45
+ let output = Array.isArray(result.selected)
46
+ ? `Selected: ${result.selected.join(', ')}`
47
+ : `Selected: ${result.selected}`;
48
+ if (result.warning) {
49
+ output += `\n[Warning: ${result.warning}]`;
50
+ }
51
+ return text(output);
52
+ }
53
+
23
54
  // Handle ChatGPT/Gemini responses
24
55
  if (result.response !== undefined && result.model !== undefined && result.tookMs !== undefined) {
25
56
  let output = result.response;
@@ -815,6 +846,25 @@ function mapToolToMessage(tool, args, tabId) {
815
846
  value: a.value,
816
847
  ...baseMsg
817
848
  };
849
+ case "element.styles":
850
+ if (!a.selector) throw new Error("selector argument required");
851
+ return {
852
+ type: "GET_ELEMENT_STYLES",
853
+ selector: a.selector,
854
+ ...baseMsg
855
+ };
856
+ case "select": {
857
+ if (!a.selector) throw new Error("selector argument required");
858
+ const values = Array.isArray(a.values) ? a.values : (a.values ? [a.values] : []);
859
+ if (values.length === 0) throw new Error("at least one value required");
860
+ return {
861
+ type: "SELECT_OPTION",
862
+ selector: a.selector,
863
+ values,
864
+ by: a.by || "value", // value, label, or index
865
+ ...baseMsg
866
+ };
867
+ }
818
868
  case "ai":
819
869
  return { type: "AI_ANALYZE", query: a.query, act: a.act, mode: a.mode, ...baseMsg };
820
870
  case "wait":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surf-cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
5
5
  "keywords": [
6
6
  "chrome",
@@ -1,415 +0,0 @@
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>