surf-cli 2.1.0 → 2.3.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.
@@ -150,22 +150,23 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
150
150
  })()`
151
151
  );
152
152
  await delay(300);
153
+ // Select from menu - loop in Node.js to avoid CDP timeout issues
153
154
  const normalizedModel = desiredModel.toLowerCase().replace(/[^a-z0-9]/g, "");
154
- const result = await evaluate(
155
- cdp,
156
- `(async () => {
157
- ${buildClickDispatcher()}
158
- const TIMEOUT_MS = ${timeoutMs};
159
- const targetModel = ${JSON.stringify(normalizedModel)};
160
- const menuSelector = '${SELECTORS.menuContainer}';
161
- const itemSelector = '${SELECTORS.menuItem}';
162
- const normalize = (text) => (text || '').toLowerCase().replace(/[^a-z0-9]/g, '');
163
- const deadline = Date.now() + TIMEOUT_MS;
164
- while (Date.now() < deadline) {
155
+ const deadline = Date.now() + timeoutMs;
156
+
157
+ while (Date.now() < deadline) {
158
+ const result = await evaluate(
159
+ cdp,
160
+ `(() => {
161
+ ${buildClickDispatcher()}
162
+ const targetModel = ${JSON.stringify(normalizedModel)};
163
+ const menuSelector = '${SELECTORS.menuContainer}';
164
+ const itemSelector = '${SELECTORS.menuItem}';
165
+ const normalize = (text) => (text || '').toLowerCase().replace(/[^a-z0-9]/g, '');
166
+
165
167
  const menu = document.querySelector(menuSelector);
166
168
  if (!menu) {
167
- await new Promise(r => setTimeout(r, 100));
168
- continue;
169
+ return { found: false, waiting: true };
169
170
  }
170
171
  const items = Array.from(menu.querySelectorAll(itemSelector));
171
172
  let bestMatch = null;
@@ -183,18 +184,24 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000) {
183
184
  }
184
185
  if (bestMatch) {
185
186
  dispatchClickSequence(bestMatch);
186
- await new Promise(r => setTimeout(r, 200));
187
- return { success: true, label: bestMatch.textContent?.trim() };
187
+ return { found: true, success: true, label: bestMatch.textContent?.trim() };
188
188
  }
189
- await new Promise(r => setTimeout(r, 100));
189
+ return { found: true, success: false, error: 'No matching model in menu' };
190
+ })()`
191
+ );
192
+
193
+ if (result && result.found) {
194
+ if (result.success) {
195
+ await delay(200);
196
+ return result.label;
190
197
  }
191
- return { success: false, error: 'Model option not found' };
192
- })()`
193
- );
194
- if (!result || !result.success) {
195
- throw new Error(`Model not found: ${desiredModel}`);
198
+ throw new Error(`Model not found: ${desiredModel}`);
199
+ }
200
+
201
+ await delay(100);
196
202
  }
197
- return result.label;
203
+
204
+ throw new Error(`Model not found: ${desiredModel} (timeout)`);
198
205
  }
199
206
 
200
207
  async function typePrompt(cdp, inputCdp, prompt) {
package/native/cli.cjs CHANGED
@@ -146,6 +146,27 @@ const TOOLS = {
146
146
  { cmd: 'perplexity "latest AI news" --model sonar', desc: "Specify model (Pro)" },
147
147
  ]
148
148
  },
149
+ "grok": {
150
+ desc: "Query Grok AI with real-time X/Twitter data access (uses browser session)",
151
+ args: ["query"],
152
+ opts: {
153
+ "with-page": "Include current page context",
154
+ model: "Model: auto, fast, expert, thinking (default)",
155
+ "deep-search": "Enable DeepSearch for X post searching",
156
+ timeout: "Timeout in seconds (default: 300 for thinking models)",
157
+ validate: "Check Grok UI and scrape available models (no query sent)",
158
+ "save-models": "Save discovered models to surf.json config"
159
+ },
160
+ examples: [
161
+ { cmd: 'grok "what are the latest AI agent trends on X"', desc: "Search X posts" },
162
+ { cmd: 'grok "analyze @username recent activity"', desc: "Profile analysis" },
163
+ { cmd: 'grok "summarize this page" --with-page', desc: "With page context" },
164
+ { cmd: 'grok "find viral AI posts" --deep-search', desc: "DeepSearch mode" },
165
+ { cmd: 'grok "quick question" --model fast', desc: "Faster model" },
166
+ { cmd: 'grok --validate', desc: "Check UI and list available models" },
167
+ { cmd: 'grok --validate --save-models', desc: "Save discovered models to settings" },
168
+ ]
169
+ },
149
170
  "ai": {
150
171
  desc: "Analyze page with AI (requires GOOGLE_API_KEY)",
151
172
  args: ["query"],
@@ -349,6 +370,37 @@ const TOOLS = {
349
370
  },
350
371
  }
351
372
  },
373
+ element: {
374
+ desc: "Element inspection",
375
+ commands: {
376
+ "element.styles": {
377
+ desc: "Get computed styles from element(s)",
378
+ args: ["ref_or_selector"],
379
+ examples: [
380
+ { cmd: "element.styles e5", desc: "Get styles by ref" },
381
+ { cmd: 'element.styles ".header"', desc: "Get styles by selector (can return multiple)" },
382
+ ]
383
+ },
384
+ }
385
+ },
386
+ forms: {
387
+ desc: "Form interactions",
388
+ commands: {
389
+ "select": {
390
+ desc: "Select option(s) in dropdown",
391
+ args: ["ref_or_selector", "values..."],
392
+ opts: {
393
+ by: "Match by: value (default), label, index"
394
+ },
395
+ examples: [
396
+ { cmd: 'select e5 "US"', desc: "Select by value" },
397
+ { cmd: 'select e5 "opt1" "opt2"', desc: "Multi-select" },
398
+ { cmd: 'select e5 --by label "United States"', desc: "Select by visible text" },
399
+ { cmd: 'select e5 --by index 0', desc: "Select first option" },
400
+ ]
401
+ },
402
+ }
403
+ },
352
404
  wait: {
353
405
  desc: "Waiting",
354
406
  commands: {
@@ -1771,6 +1823,7 @@ const PRIMARY_ARG_MAP = {
1771
1823
  gemini: "query",
1772
1824
  chatgpt: "query",
1773
1825
  perplexity: "query",
1826
+ grok: "query",
1774
1827
  navigate: "url",
1775
1828
  go: "url",
1776
1829
  js: "code",
@@ -1809,6 +1862,8 @@ const PRIMARY_ARG_MAP = {
1809
1862
  "locate.label": "label",
1810
1863
  "emulate.device": "device",
1811
1864
  "frame.js": "code",
1865
+ "element.styles": "selector",
1866
+ "select": "selector",
1812
1867
  };
1813
1868
 
1814
1869
  const toolArgs = { ...options };
@@ -1845,6 +1900,17 @@ if (tool === "js" && toolArgs.file) {
1845
1900
  }
1846
1901
  }
1847
1902
 
1903
+ // Handle select command: capture multiple values after selector
1904
+ if (tool === "select" && positional.length > 2) {
1905
+ const values = positional.slice(2); // All args after "select <selector>"
1906
+ toolArgs.values = values.length === 1 ? values[0] : values;
1907
+ } else if (tool === "select" && positional.length === 2) {
1908
+ // Only selector provided, no values
1909
+ console.error("Error: select requires at least one value");
1910
+ console.error("Usage: surf select <selector> <value...>");
1911
+ process.exit(1);
1912
+ }
1913
+
1848
1914
  if (toolArgs.into && !toolArgs.selector) {
1849
1915
  toolArgs.selector = toolArgs.into;
1850
1916
  delete toolArgs.into;
@@ -2124,7 +2190,7 @@ const socket = net.createConnection(SOCKET_PATH, () => {
2124
2190
  socket.write(JSON.stringify(request) + "\n");
2125
2191
  });
2126
2192
 
2127
- const AI_TOOLS = ["smoke", "chatgpt", "gemini", "perplexity", "ai"];
2193
+ const AI_TOOLS = ["smoke", "chatgpt", "gemini", "perplexity", "grok", "ai"];
2128
2194
  const requestTimeout = AI_TOOLS.includes(tool) ? 300000 : 30000;
2129
2195
  const timeout = setTimeout(() => {
2130
2196
  console.error(`Error: Request timed out (${requestTimeout / 1000}s)`);
package/native/config.cjs CHANGED
@@ -21,6 +21,18 @@ const STARTER_CONFIG = {
21
21
  }
22
22
  };
23
23
 
24
+ // Grok models can be customized in surf.json if X.com UI changes:
25
+ // {
26
+ // "grok": {
27
+ // "models": {
28
+ // "thinking": { "id": "thinking", "name": "Grok 4.1 Thinking" },
29
+ // "auto": { "id": "auto", "name": "Auto" },
30
+ // "fast": { "id": "fast", "name": "Fast" },
31
+ // "expert": { "id": "expert", "name": "Expert" }
32
+ // }
33
+ // }
34
+ // }
35
+
24
36
  function findConfigPath() {
25
37
  const cwdPath = path.join(process.cwd(), CONFIG_NAME);
26
38
  if (fs.existsSync(cwdPath)) {