surf-cli 2.2.0 → 2.4.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 +125 -3
- package/dist/service-worker/index.js +11 -11
- package/dist/service-worker/index.js.map +1 -1
- package/native/chatgpt-client.cjs +29 -22
- package/native/cli.cjs +184 -3
- package/native/config.cjs +12 -0
- package/native/do-executor.cjs +273 -0
- package/native/do-parser.cjs +232 -0
- package/native/grok-client.cjs +906 -0
- package/native/host-helpers.cjs +58 -1
- package/native/host.cjs +198 -0
- package/native/perplexity-client.cjs +26 -22
- package/package.json +1 -1
- package/native/CHANGELOG.md +0 -136
- package/native/README.md +0 -141
package/native/host-helpers.cjs
CHANGED
|
@@ -51,12 +51,51 @@ function formatToolContent(result, log = () => {}) {
|
|
|
51
51
|
return text(output);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
// Handle
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
return { success: true, model: bestMatch.textContent?.trim() };
|
|
231
|
+
return { found: true, success: true, model: bestMatch.textContent?.trim() };
|
|
234
232
|
}
|
|
235
233
|
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/native/CHANGELOG.md
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to surf CLI will be documented in this file.
|
|
4
|
-
|
|
5
|
-
## [2.2.0] - 2026-01-07
|
|
6
|
-
|
|
7
|
-
### Added
|
|
8
|
-
- Network capture: `network`, `network.stats`, `network.origins`, `network.get`, `network.clear`, `network.export`
|
|
9
|
-
- Filtering by method, status, URL, content-type
|
|
10
|
-
- Export formats: curl, raw JSON, URL list
|
|
11
|
-
- Persistence to `/tmp/surf/` (configurable via `SURF_NETWORK_PATH`)
|
|
12
|
-
|
|
13
|
-
## [2.1.0] - 2025-12-30
|
|
14
|
-
|
|
15
|
-
### Added
|
|
16
|
-
|
|
17
|
-
**ChatGPT Integration**
|
|
18
|
-
- `chatgpt <query>` - Send prompt to ChatGPT using browser cookies (no API key)
|
|
19
|
-
- `--with-page` - Include current page context
|
|
20
|
-
- `--model` - Specify model (gpt-4o, o1, etc.)
|
|
21
|
-
- `--timeout` - Custom timeout (default: 45 minutes)
|
|
22
|
-
- File attachments coming soon
|
|
23
|
-
|
|
24
|
-
**Gemini Integration (Coming Soon)**
|
|
25
|
-
- `gemini <query>` - Command structure ready, implementation pending
|
|
26
|
-
|
|
27
|
-
**Request Queue**
|
|
28
|
-
- AI requests are queued sequentially with 2s minimum delay between requests
|
|
29
|
-
- Prevents rate limiting when making multiple AI queries
|
|
30
|
-
|
|
31
|
-
### Technical Changes
|
|
32
|
-
- New `chatgpt-client.cjs` module for ChatGPT browser automation
|
|
33
|
-
- Extension: `GET_CHATGPT_COOKIES`, `GET_GOOGLE_COOKIES` handlers
|
|
34
|
-
- Extension: `CHATGPT_NEW_TAB`, `CHATGPT_CLOSE_TAB`, `CHATGPT_CDP_COMMAND`, `CHATGPT_EVALUATE` handlers
|
|
35
|
-
- CDP controller: Added public `sendCommand()` method
|
|
36
|
-
|
|
37
|
-
## [2.0.0] - 2025-12-27
|
|
38
|
-
|
|
39
|
-
### Breaking Changes
|
|
40
|
-
- Removed snake_case command aliases (use dot-notation instead)
|
|
41
|
-
- `read_page` -> `page.read`
|
|
42
|
-
- `list_tabs` -> `tab.list`
|
|
43
|
-
- `wait_for_element` -> `wait.element`
|
|
44
|
-
- `javascript_tool` -> `js`
|
|
45
|
-
- And others (see REMOVED_COMMANDS in cli.cjs for full list)
|
|
46
|
-
- Removed all single-letter short flags for consistency
|
|
47
|
-
- Use `--output` instead of `-o`
|
|
48
|
-
- Use `--ref` instead of `-r`
|
|
49
|
-
- Use `--annotate` instead of `-a`
|
|
50
|
-
- Use `--fullpage` instead of `-f`
|
|
51
|
-
- Migration hints shown when using old command names
|
|
52
|
-
|
|
53
|
-
### Added
|
|
54
|
-
|
|
55
|
-
**Navigation**
|
|
56
|
-
- `back` - Go back in browser history
|
|
57
|
-
- `forward` - Go forward in browser history
|
|
58
|
-
- `tab.reload` - Reload tab (with `--hard` for cache bypass)
|
|
59
|
-
|
|
60
|
-
**Tab Groups**
|
|
61
|
-
- `tab.group` - Create or add to tab group
|
|
62
|
-
- `tab.ungroup` - Remove tabs from group
|
|
63
|
-
- `tab.groups` - List all tab groups
|
|
64
|
-
|
|
65
|
-
**Zoom Control**
|
|
66
|
-
- `zoom` - Get current zoom level
|
|
67
|
-
- `zoom <level>` - Set zoom (e.g., `zoom 1.5` for 150%)
|
|
68
|
-
- `zoom --reset` - Reset to default zoom
|
|
69
|
-
|
|
70
|
-
**Cookies**
|
|
71
|
-
- `cookie.list` - List cookies for current domain
|
|
72
|
-
- `cookie.get` - Get specific cookie by name
|
|
73
|
-
- `cookie.set` - Set a cookie
|
|
74
|
-
- `cookie.clear` - Clear specific cookie or all (`--all`)
|
|
75
|
-
|
|
76
|
-
**Search**
|
|
77
|
-
- `search <term>` - Search for text in page (alias: `find`)
|
|
78
|
-
- Returns match refs, context, and element associations
|
|
79
|
-
|
|
80
|
-
**Batch Execution**
|
|
81
|
-
- `batch --actions '[...]'` - Execute multiple actions
|
|
82
|
-
- `batch --file workflow.json` - Load actions from file
|
|
83
|
-
|
|
84
|
-
**Bookmarks**
|
|
85
|
-
- `bookmark.add` - Bookmark current page
|
|
86
|
-
- `bookmark.remove` - Remove bookmark for current page
|
|
87
|
-
- `bookmark.list` - List bookmarks
|
|
88
|
-
|
|
89
|
-
**History**
|
|
90
|
-
- `history.list` - Recent browser history
|
|
91
|
-
- `history.search <query>` - Search history
|
|
92
|
-
|
|
93
|
-
**Screenshot Enhancements**
|
|
94
|
-
- `--annotate` - Draw element labels on screenshot
|
|
95
|
-
- `--fullpage` - Capture entire scrollable page
|
|
96
|
-
- `--max-height` - Limit fullpage capture height (default: 4000px)
|
|
97
|
-
- Extension UI automatically hidden during capture
|
|
98
|
-
|
|
99
|
-
**Aliases**
|
|
100
|
-
- `snap` -> `screenshot` (auto-saves to /tmp if no output specified)
|
|
101
|
-
- `read` -> `page.read`
|
|
102
|
-
- `find` -> `search`
|
|
103
|
-
- `go` -> `navigate`
|
|
104
|
-
|
|
105
|
-
**Discovery Features**
|
|
106
|
-
- `--find <query>` - Fuzzy search for commands
|
|
107
|
-
- `--about <topic>` - Learn about a topic
|
|
108
|
-
|
|
109
|
-
**Help System**
|
|
110
|
-
- `--help` - Basic help with common commands
|
|
111
|
-
- `--help-full` - Complete command reference
|
|
112
|
-
- `--help-topic <topic>` - Topic-specific guide
|
|
113
|
-
- Command-level help with examples
|
|
114
|
-
|
|
115
|
-
**Other**
|
|
116
|
-
- `--version` - Show version
|
|
117
|
-
- `click 100 200` - Positional coordinates for click
|
|
118
|
-
- `click --selector ".btn" --index 2` - Click nth element matching selector
|
|
119
|
-
|
|
120
|
-
### Changed
|
|
121
|
-
- Primary argument support for commands:
|
|
122
|
-
- `wait.element <selector>` (was `--selector`)
|
|
123
|
-
- `wait.url <pattern>` (was `--pattern`)
|
|
124
|
-
- `click <ref>` with e-prefix detection (e.g., `click e5`)
|
|
125
|
-
- Help output includes usage examples for all commands
|
|
126
|
-
- `dialog.dismiss --all` for repeatedly dismissing dialogs
|
|
127
|
-
- Fullpage screenshot delay increased to 300ms for lazy-loaded content
|
|
128
|
-
- Error messages standardized to terse format for AI consumption
|
|
129
|
-
|
|
130
|
-
### Fixed
|
|
131
|
-
- `--limit 0` now correctly returns empty results (was defaulting to max)
|
|
132
|
-
- Screenshot always hides extension UI (was conditional on `--clean` flag)
|
|
133
|
-
|
|
134
|
-
## [1.x] - Previous Releases
|
|
135
|
-
|
|
136
|
-
See git history for changes before v2.0.0.
|
package/native/README.md
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
# Surf Native Host
|
|
2
|
-
|
|
3
|
-
Native messaging host that bridges CLI commands to the Chrome extension via Unix socket.
|
|
4
|
-
|
|
5
|
-
## Architecture
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
CLI (surf) → Unix Socket (/tmp/surf.sock) → Native Host → Chrome Extension → CDP
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Files
|
|
12
|
-
|
|
13
|
-
| File | Purpose |
|
|
14
|
-
|------|---------|
|
|
15
|
-
| `host.cjs` | Main native host with socket server and tool handling |
|
|
16
|
-
| `cli.cjs` | CLI tool for browser automation |
|
|
17
|
-
| `chatgpt-client.cjs` | ChatGPT browser automation client |
|
|
18
|
-
| `protocol.cjs` | Chrome native messaging protocol helpers |
|
|
19
|
-
| `host-wrapper.py` | Python wrapper for native host execution |
|
|
20
|
-
| `host.sh` | Shell script to start the host |
|
|
21
|
-
|
|
22
|
-
## Setup
|
|
23
|
-
|
|
24
|
-
1. Install the native host manifest:
|
|
25
|
-
```bash
|
|
26
|
-
npm run install:native <extension-id>
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Or manually:
|
|
30
|
-
```bash
|
|
31
|
-
mkdir -p ~/Library/Application\ Support/Google/Chrome/NativeMessagingHosts
|
|
32
|
-
cat > ~/Library/Application\ Support/Google/Chrome/NativeMessagingHosts/com.anthropic.pi_chrome.json << EOF
|
|
33
|
-
{
|
|
34
|
-
"name": "com.anthropic.pi_chrome",
|
|
35
|
-
"description": "Surf CLI Native Host",
|
|
36
|
-
"path": "$PWD/host-wrapper.py",
|
|
37
|
-
"type": "stdio",
|
|
38
|
-
"allowed_origins": ["chrome-extension://YOUR_EXTENSION_ID/"]
|
|
39
|
-
}
|
|
40
|
-
EOF
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
2. Start the native host:
|
|
44
|
-
```bash
|
|
45
|
-
node host.cjs
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
The host creates a Unix socket at `/tmp/surf.sock`.
|
|
49
|
-
|
|
50
|
-
## CLI Reference
|
|
51
|
-
|
|
52
|
-
See the main [README](../README.md) for full CLI documentation.
|
|
53
|
-
|
|
54
|
-
### Quick Reference
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
surf go "https://example.com" # Navigate
|
|
58
|
-
surf read # Get accessibility tree
|
|
59
|
-
surf click e5 # Click element
|
|
60
|
-
surf type "hello" --submit # Type and submit
|
|
61
|
-
surf snap # Screenshot to /tmp
|
|
62
|
-
surf chatgpt "explain this" # Query ChatGPT
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Global Options
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
--tab-id <id> # Target specific tab
|
|
69
|
-
--json # Output raw JSON
|
|
70
|
-
--soft-fail # Warn instead of error on restricted pages
|
|
71
|
-
--no-screenshot # Skip auto-screenshot after actions
|
|
72
|
-
--full # Full resolution screenshots
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Protocol
|
|
76
|
-
|
|
77
|
-
### Tool Request
|
|
78
|
-
|
|
79
|
-
```json
|
|
80
|
-
{
|
|
81
|
-
"type": "tool_request",
|
|
82
|
-
"method": "execute_tool",
|
|
83
|
-
"params": {
|
|
84
|
-
"tool": "TOOL_NAME",
|
|
85
|
-
"args": { ... },
|
|
86
|
-
"tabId": 123
|
|
87
|
-
},
|
|
88
|
-
"id": "unique-request-id"
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### Tool Response (Success)
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"type": "tool_response",
|
|
97
|
-
"id": "unique-request-id",
|
|
98
|
-
"result": {
|
|
99
|
-
"content": [
|
|
100
|
-
{ "type": "text", "text": "Result message" }
|
|
101
|
-
]
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Tool Response (With Image)
|
|
107
|
-
|
|
108
|
-
```json
|
|
109
|
-
{
|
|
110
|
-
"type": "tool_response",
|
|
111
|
-
"id": "unique-request-id",
|
|
112
|
-
"result": {
|
|
113
|
-
"content": [
|
|
114
|
-
{ "type": "text", "text": "Screenshot captured" },
|
|
115
|
-
{ "type": "image", "data": "base64...", "mimeType": "image/png" }
|
|
116
|
-
]
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Tool Response (Error)
|
|
122
|
-
|
|
123
|
-
```json
|
|
124
|
-
{
|
|
125
|
-
"type": "tool_response",
|
|
126
|
-
"id": "unique-request-id",
|
|
127
|
-
"error": {
|
|
128
|
-
"content": [{ "type": "text", "text": "Error message" }]
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
## Troubleshooting
|
|
134
|
-
|
|
135
|
-
| Issue | Solution |
|
|
136
|
-
|-------|----------|
|
|
137
|
-
| Socket not found | Ensure `node host.cjs` is running |
|
|
138
|
-
| No response | Check extension is loaded in Chrome |
|
|
139
|
-
| "Content script not loaded" | Navigate to a page first |
|
|
140
|
-
| "Cannot control this page" | Page is restricted (chrome://, extensions) - use `--soft-fail` |
|
|
141
|
-
| Slow first operation | Normal - CDP debugger attachment takes ~100-500ms |
|