surf-cli 2.2.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.
@@ -51,12 +51,51 @@ function formatToolContent(result, log = () => {}) {
51
51
  return text(output);
52
52
  }
53
53
 
54
- // Handle ChatGPT/Gemini responses
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
55
85
  if (result.response !== undefined && result.model !== undefined && result.tookMs !== undefined) {
56
86
  let output = result.response;
57
87
  if (result.imagePath) {
58
88
  output += `\n\n*Image saved to: ${result.imagePath}*`;
59
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
+ }
60
99
  return text(output);
61
100
  }
62
101
 
@@ -1013,6 +1052,24 @@ function mapToolToMessage(tool, args, tabId) {
1013
1052
  timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 120000,
1014
1053
  ...baseMsg
1015
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
+ };
1016
1073
  case "window.new":
1017
1074
  return {
1018
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.2.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",