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.
@@ -20,12 +20,82 @@ function formatToolContent(result, log = () => {}) {
20
20
  return text(result.content);
21
21
  }
22
22
 
23
- // Handle ChatGPT/Gemini responses
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
+
54
+ // Handle Grok validation results
55
+ if (result.authenticated !== undefined && result.models !== undefined && result.expectedModels !== undefined) {
56
+ let output = "## Grok Validation Results\n\n";
57
+ output += `**Authenticated:** ${result.authenticated ? 'Yes' : 'No'}\n`;
58
+ output += `**Premium:** ${result.premium ? 'Yes' : 'No'}\n`;
59
+ output += `**Input Field:** ${result.inputFound ? 'Found' : 'Not Found'}\n`;
60
+ output += `**Send Button:** ${result.sendButtonFound ? 'Found' : 'Not Found'}\n\n`;
61
+
62
+ output += `**Available Models:** ${result.models.length > 0 ? result.models.join(', ') : 'None found'}\n`;
63
+ output += `**Expected Models:** ${result.expectedModels.join(', ')}\n`;
64
+ output += `**Model Mismatch:** ${result.modelMismatch ? 'Yes' : 'No'}\n\n`;
65
+
66
+ if (result.errors && result.errors.length > 0) {
67
+ output += `**Errors:**\n${result.errors.map(e => `- ${e}`).join('\n')}\n\n`;
68
+ }
69
+
70
+ if (result.savedModels) {
71
+ if (result.savedModels.success) {
72
+ output += `**Models saved to:** ${result.savedModels.path}\n`;
73
+ } else {
74
+ output += `**Failed to save models:** ${result.savedModels.error}\n`;
75
+ }
76
+ }
77
+
78
+ output += `\n*Config: ${result.configPath}*\n`;
79
+ output += `*Completed in ${result.tookMs}ms*`;
80
+
81
+ return text(output);
82
+ }
83
+
84
+ // Handle ChatGPT/Gemini/Grok responses
24
85
  if (result.response !== undefined && result.model !== undefined && result.tookMs !== undefined) {
25
86
  let output = result.response;
26
87
  if (result.imagePath) {
27
88
  output += `\n\n*Image saved to: ${result.imagePath}*`;
28
89
  }
90
+ if (result.thinkingTime) {
91
+ output += `\n\n*Grok thought for ${result.thinkingTime}s*`;
92
+ }
93
+ if (result.partial) {
94
+ output += `\n\n*Warning: Response was truncated due to timeout*`;
95
+ }
96
+ if (result.warnings && result.warnings.length > 0) {
97
+ output += `\n\n**Warnings:**\n${result.warnings.map(w => `- ${w}`).join('\n')}`;
98
+ }
29
99
  return text(output);
30
100
  }
31
101
 
@@ -815,6 +885,25 @@ function mapToolToMessage(tool, args, tabId) {
815
885
  value: a.value,
816
886
  ...baseMsg
817
887
  };
888
+ case "element.styles":
889
+ if (!a.selector) throw new Error("selector argument required");
890
+ return {
891
+ type: "GET_ELEMENT_STYLES",
892
+ selector: a.selector,
893
+ ...baseMsg
894
+ };
895
+ case "select": {
896
+ if (!a.selector) throw new Error("selector argument required");
897
+ const values = Array.isArray(a.values) ? a.values : (a.values ? [a.values] : []);
898
+ if (values.length === 0) throw new Error("at least one value required");
899
+ return {
900
+ type: "SELECT_OPTION",
901
+ selector: a.selector,
902
+ values,
903
+ by: a.by || "value", // value, label, or index
904
+ ...baseMsg
905
+ };
906
+ }
818
907
  case "ai":
819
908
  return { type: "AI_ANALYZE", query: a.query, act: a.act, mode: a.mode, ...baseMsg };
820
909
  case "wait":
@@ -963,6 +1052,24 @@ function mapToolToMessage(tool, args, tabId) {
963
1052
  timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 120000,
964
1053
  ...baseMsg
965
1054
  };
1055
+ case "grok":
1056
+ if (a.validate) {
1057
+ return {
1058
+ type: "GROK_VALIDATE",
1059
+ saveModels: a["save-models"] || a.saveModels || false,
1060
+ ...baseMsg
1061
+ };
1062
+ }
1063
+ if (!a.query) throw new Error("query required");
1064
+ return {
1065
+ type: "GROK_QUERY",
1066
+ query: a.query,
1067
+ model: a.model,
1068
+ deepSearch: a["deep-search"] || a.deepSearch || false,
1069
+ withPage: a["with-page"],
1070
+ timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 300000,
1071
+ ...baseMsg
1072
+ };
966
1073
  case "window.new":
967
1074
  return {
968
1075
  type: "WINDOW_NEW",
package/native/host.cjs CHANGED
@@ -9,6 +9,7 @@ const { GoogleGenerativeAI } = require("@google/generative-ai");
9
9
  const chatgptClient = require("./chatgpt-client.cjs");
10
10
  const geminiClient = require("./gemini-client.cjs");
11
11
  const perplexityClient = require("./perplexity-client.cjs");
12
+ const grokClient = require("./grok-client.cjs");
12
13
  const { mapToolToMessage, mapComputerAction, formatToolContent } = require("./host-helpers.cjs");
13
14
 
14
15
  const SOCKET_PATH = "/tmp/surf.sock";
@@ -713,6 +714,203 @@ function handleToolRequest(msg, socket) {
713
714
  return;
714
715
  }
715
716
 
717
+ if (extensionMsg.type === "GROK_QUERY") {
718
+ const { query, model, deepSearch, withPage, timeout } = extensionMsg;
719
+
720
+ queueAiRequest(async () => {
721
+ // 1. Get page context if requested
722
+ let pageContext = null;
723
+ if (withPage) {
724
+ const pageResult = await new Promise((resolve) => {
725
+ const pageId = ++requestCounter;
726
+ pendingToolRequests.set(pageId, {
727
+ socket: null,
728
+ originalId: null,
729
+ tool: "get_page_text",
730
+ onComplete: resolve
731
+ });
732
+ writeMessage({ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId, id: pageId });
733
+ });
734
+ if (pageResult && !pageResult.error) {
735
+ pageContext = {
736
+ url: pageResult.url,
737
+ text: pageResult.text || pageResult.pageContent || ""
738
+ };
739
+ }
740
+ }
741
+
742
+ // 2. Build full prompt
743
+ let fullPrompt = query || "";
744
+ if (pageContext) {
745
+ fullPrompt = `Page: ${pageContext.url}\n\n${pageContext.text}\n\n---\n\n${fullPrompt}`;
746
+ }
747
+
748
+ // 3. Call Grok client
749
+ const result = await grokClient.query({
750
+ prompt: fullPrompt,
751
+ model: model,
752
+ deepSearch: deepSearch || false,
753
+ timeout: timeout || 300000,
754
+ getCookies: () => new Promise((resolve) => {
755
+ const cookieId = ++requestCounter;
756
+ pendingToolRequests.set(cookieId, {
757
+ socket: null,
758
+ originalId: null,
759
+ tool: "get_cookies",
760
+ onComplete: (r) => resolve(r)
761
+ });
762
+ writeMessage({ type: "GET_TWITTER_COOKIES", id: cookieId });
763
+ }),
764
+ createTab: () => new Promise((resolve) => {
765
+ const tabCreateId = ++requestCounter;
766
+ pendingToolRequests.set(tabCreateId, {
767
+ socket: null,
768
+ originalId: null,
769
+ tool: "create_tab",
770
+ onComplete: (r) => resolve(r)
771
+ });
772
+ writeMessage({ type: "GROK_NEW_TAB", id: tabCreateId });
773
+ }),
774
+ closeTab: (tabIdToClose) => new Promise((resolve) => {
775
+ const tabCloseId = ++requestCounter;
776
+ pendingToolRequests.set(tabCloseId, {
777
+ socket: null,
778
+ originalId: null,
779
+ tool: "close_tab",
780
+ onComplete: (r) => resolve(r)
781
+ });
782
+ writeMessage({ type: "GROK_CLOSE_TAB", tabId: tabIdToClose, id: tabCloseId });
783
+ }),
784
+ cdpEvaluate: (tabId, expression) => new Promise((resolve) => {
785
+ const evalId = ++requestCounter;
786
+ pendingToolRequests.set(evalId, {
787
+ socket: null,
788
+ originalId: null,
789
+ tool: "cdp_evaluate",
790
+ onComplete: (r) => resolve(r)
791
+ });
792
+ writeMessage({ type: "GROK_EVALUATE", tabId, expression, id: evalId });
793
+ }),
794
+ cdpCommand: (tabId, method, params) => new Promise((resolve) => {
795
+ const cmdId = ++requestCounter;
796
+ pendingToolRequests.set(cmdId, {
797
+ socket: null,
798
+ originalId: null,
799
+ tool: "cdp_command",
800
+ onComplete: (r) => resolve(r)
801
+ });
802
+ writeMessage({ type: "GROK_CDP_COMMAND", tabId, method, params, id: cmdId });
803
+ }),
804
+ log: (msg) => log(`[grok] ${msg}`)
805
+ });
806
+
807
+ return result;
808
+ }).then((result) => {
809
+ const response = {
810
+ response: result.response,
811
+ model: result.model,
812
+ tookMs: result.tookMs
813
+ };
814
+ if (result.thinkingTime) {
815
+ response.thinkingTime = result.thinkingTime;
816
+ }
817
+ if (result.deepSearch) {
818
+ response.deepSearch = result.deepSearch;
819
+ }
820
+ if (result.partial) {
821
+ response.partial = true;
822
+ }
823
+ if (result.warnings && result.warnings.length > 0) {
824
+ response.warnings = result.warnings;
825
+ }
826
+ if (result.modelSelectionFailed) {
827
+ response.modelSelectionFailed = true;
828
+ }
829
+ sendToolResponse(socket, originalId, response, null);
830
+ }).catch((err) => {
831
+ sendToolResponse(socket, originalId, null, err.message);
832
+ });
833
+
834
+ return;
835
+ }
836
+
837
+ if (extensionMsg.type === "GROK_VALIDATE") {
838
+ const { saveModels } = extensionMsg;
839
+
840
+ queueAiRequest(async () => {
841
+ const result = await grokClient.validate({
842
+ getCookies: () => new Promise((resolve) => {
843
+ const cookieId = ++requestCounter;
844
+ pendingToolRequests.set(cookieId, {
845
+ socket: null,
846
+ originalId: null,
847
+ tool: "get_cookies",
848
+ onComplete: (r) => resolve(r)
849
+ });
850
+ writeMessage({ type: "GET_TWITTER_COOKIES", id: cookieId });
851
+ }),
852
+ createTab: () => new Promise((resolve) => {
853
+ const tabCreateId = ++requestCounter;
854
+ pendingToolRequests.set(tabCreateId, {
855
+ socket: null,
856
+ originalId: null,
857
+ tool: "create_tab",
858
+ onComplete: (r) => resolve(r)
859
+ });
860
+ writeMessage({ type: "GROK_NEW_TAB", id: tabCreateId });
861
+ }),
862
+ closeTab: (tabIdToClose) => new Promise((resolve) => {
863
+ const tabCloseId = ++requestCounter;
864
+ pendingToolRequests.set(tabCloseId, {
865
+ socket: null,
866
+ originalId: null,
867
+ tool: "close_tab",
868
+ onComplete: (r) => resolve(r)
869
+ });
870
+ writeMessage({ type: "GROK_CLOSE_TAB", tabId: tabIdToClose, id: tabCloseId });
871
+ }),
872
+ cdpEvaluate: (tabId, expression) => new Promise((resolve) => {
873
+ const evalId = ++requestCounter;
874
+ pendingToolRequests.set(evalId, {
875
+ socket: null,
876
+ originalId: null,
877
+ tool: "cdp_evaluate",
878
+ onComplete: (r) => resolve(r)
879
+ });
880
+ writeMessage({ type: "GROK_EVALUATE", tabId, expression, id: evalId });
881
+ }),
882
+ log: (msg) => log(`[grok:validate] ${msg}`)
883
+ });
884
+
885
+ return result;
886
+ }).then((result) => {
887
+ // If --save-models flag was passed and we found models, save them
888
+ if (saveModels && result.models && result.models.length > 0) {
889
+ // Convert scraped model names to our format
890
+ const modelMap = {};
891
+ result.models.forEach(name => {
892
+ const nameLower = name.toLowerCase();
893
+ // Match known model keywords to generate consistent short IDs
894
+ let shortId;
895
+ if (nameLower.includes('thinking')) shortId = 'thinking';
896
+ else if (nameLower.includes('expert')) shortId = 'expert';
897
+ else if (nameLower.includes('fast')) shortId = 'fast';
898
+ else if (nameLower.includes('auto')) shortId = 'auto';
899
+ else shortId = nameLower.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
900
+
901
+ modelMap[shortId] = { id: shortId, name: name, desc: "" };
902
+ });
903
+ const saveResult = grokClient.saveModels(modelMap);
904
+ result.savedModels = saveResult;
905
+ }
906
+ sendToolResponse(socket, originalId, result, null);
907
+ }).catch((err) => {
908
+ sendToolResponse(socket, originalId, null, err.message);
909
+ });
910
+
911
+ return;
912
+ }
913
+
716
914
  if (extensionMsg.type === "EXECUTE_KEY_REPEAT") {
717
915
  const { key, repeat, tabId: tid } = extensionMsg;
718
916
  let completed = 0;
@@ -193,22 +193,21 @@ async function selectModel(cdp, model, timeoutMs = 8000) {
193
193
 
194
194
  await delay(500);
195
195
 
196
- // Select from menu
196
+ // Select from menu - loop in Node.js to avoid CDP timeout issues
197
197
  const normalizedModel = model.toLowerCase().replace(/[^a-z0-9]/g, '');
198
+ const deadline = Date.now() + timeoutMs;
198
199
 
199
- const result = await evaluate(cdp, `(async () => {
200
- ${buildClickDispatcher()}
201
-
202
- const targetModel = ${JSON.stringify(normalizedModel)};
203
- const normalize = (text) => (text || '').toLowerCase().replace(/[^a-z0-9]/g, '');
204
- const deadline = Date.now() + ${timeoutMs};
205
-
206
- while (Date.now() < deadline) {
200
+ while (Date.now() < deadline) {
201
+ const result = await evaluate(cdp, `(() => {
202
+ ${buildClickDispatcher()}
203
+
204
+ const targetModel = ${JSON.stringify(normalizedModel)};
205
+ const normalize = (text) => (text || '').toLowerCase().replace(/[^a-z0-9]/g, '');
206
+
207
207
  const menuItems = document.querySelectorAll('[role=menuitem], [role=menuitemradio], [role=option]');
208
208
 
209
209
  if (menuItems.length === 0) {
210
- await new Promise(r => setTimeout(r, 100));
211
- continue;
210
+ return { found: false, waiting: true };
212
211
  }
213
212
 
214
213
  let bestMatch = null;
@@ -229,23 +228,28 @@ async function selectModel(cdp, model, timeoutMs = 8000) {
229
228
 
230
229
  if (bestMatch) {
231
230
  dispatchClickSequence(bestMatch);
232
- await new Promise(r => setTimeout(r, 200));
233
- return { success: true, model: bestMatch.textContent?.trim() };
231
+ return { found: true, success: true, model: bestMatch.textContent?.trim() };
234
232
  }
235
233
 
236
- await new Promise(r => setTimeout(r, 100));
234
+ return { found: true, success: false, error: 'No matching model in menu' };
235
+ })()`);
236
+
237
+ if (result && result.found) {
238
+ if (result.success) {
239
+ await delay(200);
240
+ return result.model;
241
+ }
242
+ // Items found but no match - close menu and throw
243
+ await evaluate(cdp, `document.body.click()`);
244
+ throw new Error(`Failed to select model: ${result?.error}`);
237
245
  }
238
246
 
239
- // Close menu by clicking elsewhere
240
- document.body.click();
241
- return { success: false, error: 'Model not found in menu' };
242
- })()`);
243
-
244
- if (!result || !result.success) {
245
- throw new Error(`Failed to select model: ${result?.error}`);
247
+ await delay(100);
246
248
  }
247
249
 
248
- return result.model;
250
+ // Timeout - close menu
251
+ await evaluate(cdp, `document.body.click()`);
252
+ throw new Error(`Failed to select model: timeout waiting for menu`);
249
253
  }
250
254
 
251
255
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "surf-cli",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
5
5
  "keywords": [
6
6
  "chrome",