surf-cli 2.3.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 +112 -1
- package/native/cli.cjs +161 -2
- package/native/do-executor.cjs +273 -0
- package/native/do-parser.cjs +232 -0
- package/package.json +1 -1
- package/native/CHANGELOG.md +0 -136
- package/native/README.md +0 -141
package/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
<p>
|
|
2
|
+
<img src="surf-banner.png" alt="surf" width="1100">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# Surf
|
|
2
6
|
|
|
3
|
-
The CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested
|
|
7
|
+
**The CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.**
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/surf-cli)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[]()
|
|
4
12
|
|
|
5
13
|
```bash
|
|
6
14
|
surf go "https://example.com"
|
|
@@ -365,6 +373,55 @@ surf network.stats # Capture statistics
|
|
|
365
373
|
Storage location: `/tmp/surf/` (override with `--network-path` or `SURF_NETWORK_PATH` env).
|
|
366
374
|
Auto-cleanup: 24 hours TTL, 200MB max.
|
|
367
375
|
|
|
376
|
+
### Workflows
|
|
377
|
+
|
|
378
|
+
Execute multi-step browser automation as a single command:
|
|
379
|
+
|
|
380
|
+
```bash
|
|
381
|
+
# Inline workflow (newline-separated commands)
|
|
382
|
+
surf do 'go "https://example.com/login"
|
|
383
|
+
type "user@example.com" --selector "input[name=email]"
|
|
384
|
+
type "password123" --selector "input[name=password]"
|
|
385
|
+
click --selector "button[type=submit]"
|
|
386
|
+
screenshot --output /tmp/after-login.png'
|
|
387
|
+
|
|
388
|
+
# From JSON file (same format as --script)
|
|
389
|
+
surf do --file login-workflow.json
|
|
390
|
+
|
|
391
|
+
# Validate without executing
|
|
392
|
+
surf do 'go "url"\nclick e5\nscreenshot' --dry-run
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Why workflows?** Instead of 6-8 separate CLI calls with LLM orchestration between each step, a workflow executes deterministically with smart auto-waits. Faster, cheaper, and more reliable.
|
|
396
|
+
|
|
397
|
+
**Options:**
|
|
398
|
+
- `--file`, `-f` - Load workflow from JSON file
|
|
399
|
+
- `--dry-run` - Parse and validate without executing
|
|
400
|
+
- `--on-error stop|continue` - Error handling (default: stop)
|
|
401
|
+
- `--step-delay <ms>` - Delay between steps (default: 100, use 0 to disable)
|
|
402
|
+
- `--no-auto-wait` - Disable automatic waits between steps
|
|
403
|
+
- `--json` - Output structured JSON result
|
|
404
|
+
|
|
405
|
+
**Auto-waits:** Commands that trigger page changes automatically wait for completion:
|
|
406
|
+
- Navigation (`go`, `back`, `forward`) → waits for page load
|
|
407
|
+
- Clicks, key presses, form fills → waits for DOM stability
|
|
408
|
+
- Tab switches → waits for tab to load
|
|
409
|
+
|
|
410
|
+
**JSON file format:**
|
|
411
|
+
```json
|
|
412
|
+
{
|
|
413
|
+
"name": "Login Flow",
|
|
414
|
+
"steps": [
|
|
415
|
+
{ "tool": "navigate", "args": { "url": "https://example.com/login" } },
|
|
416
|
+
{ "tool": "type", "args": { "text": "user@example.com", "selector": "input[name=email]" } },
|
|
417
|
+
{ "tool": "click", "args": { "selector": "button[type=submit]" } },
|
|
418
|
+
{ "tool": "screenshot", "args": {} }
|
|
419
|
+
]
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**Supported commands:** All surf commands work in workflows. Use aliases (`go`, `snap`, `read`) or full names (`navigate`, `screenshot`, `page.read`).
|
|
424
|
+
|
|
368
425
|
## Global Options
|
|
369
426
|
|
|
370
427
|
```bash
|
|
@@ -385,10 +442,50 @@ For programmatic integration, send JSON to `/tmp/surf.sock`:
|
|
|
385
442
|
echo '{"type":"tool_request","method":"execute_tool","params":{"tool":"tab.list","args":{}},"id":"1"}' | nc -U /tmp/surf.sock
|
|
386
443
|
```
|
|
387
444
|
|
|
445
|
+
### Protocol Reference
|
|
446
|
+
|
|
447
|
+
**Request:**
|
|
448
|
+
```json
|
|
449
|
+
{
|
|
450
|
+
"type": "tool_request",
|
|
451
|
+
"method": "execute_tool",
|
|
452
|
+
"params": {
|
|
453
|
+
"tool": "click",
|
|
454
|
+
"args": { "ref": "e5" }
|
|
455
|
+
},
|
|
456
|
+
"id": "unique-request-id",
|
|
457
|
+
"tabId": 123,
|
|
458
|
+
"windowId": 456
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**Success Response:**
|
|
463
|
+
```json
|
|
464
|
+
{
|
|
465
|
+
"type": "tool_response",
|
|
466
|
+
"id": "unique-request-id",
|
|
467
|
+
"result": {
|
|
468
|
+
"content": [{ "type": "text", "text": "Result message" }]
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**Error Response:**
|
|
474
|
+
```json
|
|
475
|
+
{
|
|
476
|
+
"type": "tool_response",
|
|
477
|
+
"id": "unique-request-id",
|
|
478
|
+
"error": {
|
|
479
|
+
"content": [{ "type": "text", "text": "Error message" }]
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
```
|
|
483
|
+
|
|
388
484
|
## Command Groups
|
|
389
485
|
|
|
390
486
|
| Group | Commands |
|
|
391
487
|
|-------|----------|
|
|
488
|
+
| `workflow` | `do` |
|
|
392
489
|
| `window.*` | `new`, `list`, `focus`, `close`, `resize` |
|
|
393
490
|
| `tab.*` | `list`, `new`, `switch`, `close`, `name`, `unname`, `named`, `group`, `ungroup`, `groups`, `reload` |
|
|
394
491
|
| `scroll.*` | `top`, `bottom`, `to`, `info` |
|
|
@@ -449,6 +546,20 @@ surf install <extension-id> --browser chromium
|
|
|
449
546
|
- Screenshot resize uses ImageMagick instead of macOS `sips`
|
|
450
547
|
- Headless servers need Xvfb + VNC for initial login setup
|
|
451
548
|
|
|
549
|
+
## AI Agent Integration
|
|
550
|
+
|
|
551
|
+
Surf includes a skill file for AI coding agents like [Pi](https://github.com/badlogic/pi-mono):
|
|
552
|
+
|
|
553
|
+
```bash
|
|
554
|
+
# Symlink for auto-updates
|
|
555
|
+
ln -s "$(pwd)/skills/surf" ~/.pi/agent/skills/surf
|
|
556
|
+
|
|
557
|
+
# Or copy
|
|
558
|
+
cp -r skills/surf ~/.pi/agent/skills/
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
See [`skills/README.md`](skills/README.md) for details.
|
|
562
|
+
|
|
452
563
|
## Development
|
|
453
564
|
|
|
454
565
|
```bash
|
package/native/cli.cjs
CHANGED
|
@@ -5,6 +5,8 @@ const { execSync } = require("child_process");
|
|
|
5
5
|
const { loadConfig, getConfigPath, createStarterConfig } = require("./config.cjs");
|
|
6
6
|
const networkFormatters = require("./formatters/network.cjs");
|
|
7
7
|
const networkStore = require("./network-store.cjs");
|
|
8
|
+
const { parseDoCommands } = require("./do-parser.cjs");
|
|
9
|
+
const { executeDoSteps } = require("./do-executor.cjs");
|
|
8
10
|
|
|
9
11
|
const SOCKET_PATH = "/tmp/surf.sock";
|
|
10
12
|
|
|
@@ -801,6 +803,27 @@ const TOOLS = {
|
|
|
801
803
|
},
|
|
802
804
|
}
|
|
803
805
|
},
|
|
806
|
+
workflow: {
|
|
807
|
+
desc: "Workflow execution",
|
|
808
|
+
commands: {
|
|
809
|
+
"do": {
|
|
810
|
+
desc: "Execute multiple commands as a single workflow",
|
|
811
|
+
args: ["commands"],
|
|
812
|
+
opts: {
|
|
813
|
+
file: "Load workflow from JSON file",
|
|
814
|
+
"on-error": "stop (default) | continue",
|
|
815
|
+
"no-auto-wait": "Disable automatic waits between steps",
|
|
816
|
+
"step-delay": "Delay between steps in ms (default: 100)",
|
|
817
|
+
"dry-run": "Parse and validate without executing"
|
|
818
|
+
},
|
|
819
|
+
examples: [
|
|
820
|
+
{ cmd: 'do \'go "https://example.com"\\nclick e5\\nscreenshot\'', desc: "Inline workflow" },
|
|
821
|
+
{ cmd: 'do -f login.json', desc: "From JSON file" },
|
|
822
|
+
{ cmd: 'do \'go "url"\\nclick e5\' --dry-run', desc: "Validate without running" },
|
|
823
|
+
]
|
|
824
|
+
},
|
|
825
|
+
}
|
|
826
|
+
},
|
|
804
827
|
zoom: {
|
|
805
828
|
desc: "Zoom control",
|
|
806
829
|
commands: {
|
|
@@ -1725,7 +1748,139 @@ if (args.includes("--script")) {
|
|
|
1725
1748
|
return;
|
|
1726
1749
|
}
|
|
1727
1750
|
|
|
1728
|
-
|
|
1751
|
+
// Handle `surf do` workflow command
|
|
1752
|
+
// Must be parsed before general parseArgs since it uses its own arg handling
|
|
1753
|
+
if (args[0] === "do") {
|
|
1754
|
+
const doArgs = args.slice(1);
|
|
1755
|
+
let commandsInput = null;
|
|
1756
|
+
let fileInput = null;
|
|
1757
|
+
let dryRun = false;
|
|
1758
|
+
let onError = "stop";
|
|
1759
|
+
let noAutoWait = false;
|
|
1760
|
+
let stepDelay = 100;
|
|
1761
|
+
let wantJson = false;
|
|
1762
|
+
let tabId = undefined;
|
|
1763
|
+
let windowId = undefined;
|
|
1764
|
+
|
|
1765
|
+
// Parse do-specific arguments
|
|
1766
|
+
for (let i = 0; i < doArgs.length; i++) {
|
|
1767
|
+
const arg = doArgs[i];
|
|
1768
|
+
if (arg === "--file" || arg === "-f") {
|
|
1769
|
+
fileInput = doArgs[i + 1];
|
|
1770
|
+
i++;
|
|
1771
|
+
} else if (arg === "--dry-run") {
|
|
1772
|
+
dryRun = true;
|
|
1773
|
+
} else if (arg === "--on-error") {
|
|
1774
|
+
onError = doArgs[i + 1] || "stop";
|
|
1775
|
+
i++;
|
|
1776
|
+
} else if (arg === "--no-auto-wait") {
|
|
1777
|
+
noAutoWait = true;
|
|
1778
|
+
} else if (arg === "--step-delay") {
|
|
1779
|
+
const parsed = parseInt(doArgs[i + 1], 10);
|
|
1780
|
+
stepDelay = isNaN(parsed) ? 100 : parsed;
|
|
1781
|
+
i++;
|
|
1782
|
+
} else if (arg === "--json") {
|
|
1783
|
+
wantJson = true;
|
|
1784
|
+
} else if (arg === "--tab-id") {
|
|
1785
|
+
tabId = parseInt(doArgs[i + 1], 10);
|
|
1786
|
+
i++;
|
|
1787
|
+
} else if (arg === "--window-id") {
|
|
1788
|
+
windowId = parseInt(doArgs[i + 1], 10);
|
|
1789
|
+
i++;
|
|
1790
|
+
} else if (!arg.startsWith("-")) {
|
|
1791
|
+
commandsInput = arg;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
if (!commandsInput && !fileInput) {
|
|
1796
|
+
console.error("Error: commands string or --file required");
|
|
1797
|
+
console.error('Usage: surf do \'go "url"\\nclick e5\'');
|
|
1798
|
+
console.error(" surf do --file workflow.json");
|
|
1799
|
+
process.exit(1);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
let steps;
|
|
1803
|
+
try {
|
|
1804
|
+
if (fileInput) {
|
|
1805
|
+
if (!fs.existsSync(fileInput)) {
|
|
1806
|
+
console.error(`Error: File not found: ${fileInput}`);
|
|
1807
|
+
process.exit(1);
|
|
1808
|
+
}
|
|
1809
|
+
const content = fs.readFileSync(fileInput, "utf8");
|
|
1810
|
+
// JSON file format (same as --script)
|
|
1811
|
+
const script = JSON.parse(content);
|
|
1812
|
+
if (!script.steps || !Array.isArray(script.steps)) {
|
|
1813
|
+
throw new Error("JSON must have a 'steps' array");
|
|
1814
|
+
}
|
|
1815
|
+
// Convert --script format { tool, args } to do format { cmd, args }
|
|
1816
|
+
steps = script.steps.map(s => ({ cmd: s.tool, args: s.args || {} }));
|
|
1817
|
+
} else {
|
|
1818
|
+
// Inline string parsing
|
|
1819
|
+
steps = parseDoCommands(commandsInput);
|
|
1820
|
+
}
|
|
1821
|
+
} catch (e) {
|
|
1822
|
+
console.error(`Error: Failed to parse workflow: ${e.message}`);
|
|
1823
|
+
process.exit(1);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
if (steps.length === 0) {
|
|
1827
|
+
console.error("Error: No commands found in workflow");
|
|
1828
|
+
process.exit(1);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// Validate with --dry-run
|
|
1832
|
+
if (dryRun) {
|
|
1833
|
+
console.log(`Would execute ${steps.length} steps:`);
|
|
1834
|
+
steps.forEach((s, i) => {
|
|
1835
|
+
const argStr = Object.entries(s.args || {})
|
|
1836
|
+
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
1837
|
+
.join(" ");
|
|
1838
|
+
console.log(` ${i + 1}. ${s.cmd} ${argStr}`);
|
|
1839
|
+
});
|
|
1840
|
+
process.exit(0);
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if (!wantJson) {
|
|
1844
|
+
console.log(`Running workflow (${steps.length} steps)...\n`);
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
const runWorkflow = async () => {
|
|
1848
|
+
const result = await executeDoSteps(steps, {
|
|
1849
|
+
onError,
|
|
1850
|
+
autoWait: !noAutoWait,
|
|
1851
|
+
stepDelay,
|
|
1852
|
+
quiet: wantJson,
|
|
1853
|
+
context: {
|
|
1854
|
+
tabId,
|
|
1855
|
+
windowId,
|
|
1856
|
+
},
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
// Print summary
|
|
1860
|
+
if (wantJson) {
|
|
1861
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1862
|
+
process.exit(result.status === "completed" ? 0 : 1);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
console.log("");
|
|
1866
|
+
if (result.status === "completed") {
|
|
1867
|
+
console.log(`Completed: ${result.completedSteps}/${result.totalSteps} steps (${result.totalMs}ms)`);
|
|
1868
|
+
process.exit(0);
|
|
1869
|
+
} else if (result.status === "partial") {
|
|
1870
|
+
console.log(`Partial: ${result.completedSteps}/${result.totalSteps} steps completed, ${result.failed} failed`);
|
|
1871
|
+
process.exit(1);
|
|
1872
|
+
} else {
|
|
1873
|
+
console.error(`Failed: ${result.completedSteps}/${result.totalSteps} steps completed`);
|
|
1874
|
+
if (result.error) console.error(`Error: ${result.error}`);
|
|
1875
|
+
process.exit(1);
|
|
1876
|
+
}
|
|
1877
|
+
};
|
|
1878
|
+
|
|
1879
|
+
runWorkflow();
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl", "no-save", "no-auto-wait"];
|
|
1729
1884
|
|
|
1730
1885
|
const AUTO_SCREENSHOT_TOOLS = ["click", "type", "key", "smart_type", "form.fill", "form_input", "drag", "hover", "scroll", "scroll.top", "scroll.bottom", "scroll.to", "dialog.accept", "dialog.dismiss", "js", "eval"];
|
|
1731
1886
|
|
|
@@ -1755,8 +1910,12 @@ const parseArgs = (rawArgs) => {
|
|
|
1755
1910
|
result.options.v = true;
|
|
1756
1911
|
} else if (arg === "-vv") {
|
|
1757
1912
|
result.options.vv = true;
|
|
1913
|
+
} else if (arg === "-f" && rawArgs[i + 1] && !rawArgs[i + 1].startsWith("-")) {
|
|
1914
|
+
// -f takes a file path argument (for surf do -f <file>)
|
|
1915
|
+
result.options.file = rawArgs[i + 1];
|
|
1916
|
+
i++;
|
|
1758
1917
|
} else if (arg.startsWith("-") && arg.length === 2) {
|
|
1759
|
-
// Short flag like -n
|
|
1918
|
+
// Short flag like -n
|
|
1760
1919
|
result.options[arg.slice(1)] = true;
|
|
1761
1920
|
} else {
|
|
1762
1921
|
result.positional.push(arg);
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executor for surf `do` workflow commands
|
|
3
|
+
*
|
|
4
|
+
* Executes steps sequentially with auto-waits and streaming progress output.
|
|
5
|
+
* Follows the same socket communication pattern as --script mode in cli.cjs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const net = require("net");
|
|
9
|
+
|
|
10
|
+
const SOCKET_PATH = "/tmp/surf.sock";
|
|
11
|
+
|
|
12
|
+
// Commands that trigger auto-wait after execution
|
|
13
|
+
// Note: 'type' is intentionally excluded - typing doesn't trigger navigation or DOM changes
|
|
14
|
+
const AUTO_WAIT_COMMANDS = [
|
|
15
|
+
'go', 'navigate', 'click', 'key', 'form.fill', 'submit',
|
|
16
|
+
'tab.switch', 'tab.new', 'back', 'forward'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Auto-wait strategies per command type
|
|
20
|
+
const AUTO_WAIT_MAP = {
|
|
21
|
+
'navigate': 'wait.load',
|
|
22
|
+
'go': 'wait.load',
|
|
23
|
+
'click': 'wait.dom',
|
|
24
|
+
'key': 'wait.dom',
|
|
25
|
+
'form.fill': 'wait.dom',
|
|
26
|
+
'submit': 'wait.load', // Form submission typically triggers navigation
|
|
27
|
+
'tab.switch': 'wait.load',
|
|
28
|
+
'tab.new': 'wait.load',
|
|
29
|
+
'back': 'wait.load',
|
|
30
|
+
'forward': 'wait.load',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if a command should trigger an auto-wait
|
|
35
|
+
* @param {string} cmd - Command name
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
function shouldAutoWait(cmd) {
|
|
39
|
+
return AUTO_WAIT_COMMANDS.some(c => cmd === c || cmd.startsWith(c + '.'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get the appropriate auto-wait command for a given command
|
|
44
|
+
* @param {string} cmd - Command name
|
|
45
|
+
* @returns {string|null} - Wait command to execute, or null
|
|
46
|
+
*/
|
|
47
|
+
function getAutoWaitCommand(cmd) {
|
|
48
|
+
// Check exact match first
|
|
49
|
+
if (AUTO_WAIT_MAP[cmd] !== undefined) return AUTO_WAIT_MAP[cmd];
|
|
50
|
+
|
|
51
|
+
// Check prefix match
|
|
52
|
+
for (const [prefix, waitCmd] of Object.entries(AUTO_WAIT_MAP)) {
|
|
53
|
+
if (cmd.startsWith(prefix + '.')) return waitCmd;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Send a single tool request over socket
|
|
61
|
+
* @param {string} toolName - Tool/command name
|
|
62
|
+
* @param {object} toolArgs - Tool arguments
|
|
63
|
+
* @param {object} context - Execution context (tabId, windowId)
|
|
64
|
+
* @returns {Promise<object>} - Response from host
|
|
65
|
+
*/
|
|
66
|
+
function sendDoRequest(toolName, toolArgs, context = {}) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const sock = net.createConnection(SOCKET_PATH, () => {
|
|
69
|
+
const req = {
|
|
70
|
+
type: "tool_request",
|
|
71
|
+
method: "execute_tool",
|
|
72
|
+
params: { tool: toolName, args: toolArgs },
|
|
73
|
+
id: "do-" + Date.now() + "-" + Math.random(),
|
|
74
|
+
};
|
|
75
|
+
if (context.tabId) req.tabId = context.tabId;
|
|
76
|
+
if (context.windowId) req.windowId = context.windowId;
|
|
77
|
+
sock.write(JSON.stringify(req) + "\n");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let buf = "";
|
|
81
|
+
sock.on("data", (d) => {
|
|
82
|
+
buf += d.toString();
|
|
83
|
+
const lines = buf.split("\n");
|
|
84
|
+
buf = lines.pop();
|
|
85
|
+
for (const line of lines) {
|
|
86
|
+
if (!line.trim()) continue;
|
|
87
|
+
try {
|
|
88
|
+
const resp = JSON.parse(line);
|
|
89
|
+
sock.end();
|
|
90
|
+
resolve(resp);
|
|
91
|
+
} catch {
|
|
92
|
+
sock.end();
|
|
93
|
+
reject(new Error("Invalid JSON response"));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
sock.on("error", (e) => {
|
|
99
|
+
if (e.code === "ENOENT") {
|
|
100
|
+
reject(new Error("Socket not found. Is Chrome running with the extension?"));
|
|
101
|
+
} else if (e.code === "ECONNREFUSED") {
|
|
102
|
+
reject(new Error("Connection refused. Native host not running."));
|
|
103
|
+
} else {
|
|
104
|
+
reject(e);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const timeoutId = setTimeout(() => {
|
|
109
|
+
sock.destroy();
|
|
110
|
+
reject(new Error("Request timeout"));
|
|
111
|
+
}, 30000);
|
|
112
|
+
|
|
113
|
+
sock.on("close", () => clearTimeout(timeoutId));
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Substitute variables in arguments using %{varname} syntax
|
|
119
|
+
* @param {object} args - Arguments object
|
|
120
|
+
* @param {object} vars - Variables map
|
|
121
|
+
* @returns {object} - Arguments with variables substituted
|
|
122
|
+
*/
|
|
123
|
+
function substituteVars(args, vars) {
|
|
124
|
+
if (!args || typeof args !== 'object') return args;
|
|
125
|
+
|
|
126
|
+
const result = {};
|
|
127
|
+
for (const [key, val] of Object.entries(args)) {
|
|
128
|
+
if (typeof val === 'string') {
|
|
129
|
+
result[key] = val.replace(/%\{(\w+)\}/g, (_, name) => vars[name] ?? `%{${name}}`);
|
|
130
|
+
} else {
|
|
131
|
+
result[key] = val;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Execute all workflow steps sequentially
|
|
139
|
+
* @param {Array<{ cmd: string, args: object }>} steps - Steps to execute
|
|
140
|
+
* @param {object} options - Execution options
|
|
141
|
+
* @returns {Promise<object>} - Execution result
|
|
142
|
+
*/
|
|
143
|
+
async function executeDoSteps(steps, options = {}) {
|
|
144
|
+
const {
|
|
145
|
+
onError = 'stop',
|
|
146
|
+
autoWait = true,
|
|
147
|
+
stepDelay = 100,
|
|
148
|
+
context = {},
|
|
149
|
+
quiet = false, // For --json mode, suppress streaming output
|
|
150
|
+
} = options;
|
|
151
|
+
|
|
152
|
+
const results = [];
|
|
153
|
+
const vars = context.vars || {};
|
|
154
|
+
const total = steps.length;
|
|
155
|
+
let failed = 0;
|
|
156
|
+
const startTotal = Date.now();
|
|
157
|
+
|
|
158
|
+
for (let i = 0; i < total; i++) {
|
|
159
|
+
const step = steps[i];
|
|
160
|
+
const startTime = Date.now();
|
|
161
|
+
const stepNum = `[${i + 1}/${total}]`;
|
|
162
|
+
|
|
163
|
+
// Build description (matches --script output format)
|
|
164
|
+
const argSummary = Object.entries(step.args || {})
|
|
165
|
+
.map(([k, v]) => typeof v === "string" && v.length > 40
|
|
166
|
+
? `${k}="${v.slice(0, 37)}..."`
|
|
167
|
+
: `${k}=${JSON.stringify(v)}`)
|
|
168
|
+
.join(" ");
|
|
169
|
+
const desc = argSummary ? `${step.cmd} ${argSummary}` : step.cmd;
|
|
170
|
+
|
|
171
|
+
// Print step prefix (streaming output)
|
|
172
|
+
if (!quiet) {
|
|
173
|
+
process.stdout.write(`${stepNum} ${desc} ... `);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
// Substitute variables in args
|
|
178
|
+
const resolvedArgs = substituteVars(step.args, vars);
|
|
179
|
+
|
|
180
|
+
const resp = await sendDoRequest(step.cmd, resolvedArgs, context);
|
|
181
|
+
const ms = Date.now() - startTime;
|
|
182
|
+
|
|
183
|
+
if (resp.error) {
|
|
184
|
+
const errText = resp.error.content?.[0]?.text || JSON.stringify(resp.error);
|
|
185
|
+
|
|
186
|
+
if (!quiet) {
|
|
187
|
+
console.log(`FAIL`);
|
|
188
|
+
console.log(` Error: ${errText}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
results.push({ step: i + 1, cmd: step.cmd, status: 'error', error: errText, ms });
|
|
192
|
+
failed++;
|
|
193
|
+
|
|
194
|
+
if (onError === 'stop') {
|
|
195
|
+
return {
|
|
196
|
+
status: 'failed',
|
|
197
|
+
completedSteps: i,
|
|
198
|
+
totalSteps: total,
|
|
199
|
+
results,
|
|
200
|
+
error: errText,
|
|
201
|
+
totalMs: Date.now() - startTotal
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
if (!quiet) {
|
|
206
|
+
console.log(`OK (${ms}ms)`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
results.push({ step: i + 1, cmd: step.cmd, status: 'ok', ms });
|
|
210
|
+
|
|
211
|
+
// Command-specific auto-wait
|
|
212
|
+
if (autoWait) {
|
|
213
|
+
const waitCmd = getAutoWaitCommand(step.cmd);
|
|
214
|
+
if (waitCmd) {
|
|
215
|
+
const waitArgs = waitCmd === 'wait.load'
|
|
216
|
+
? { timeout: 10000 }
|
|
217
|
+
: { stable: 100, timeout: 5000 };
|
|
218
|
+
try {
|
|
219
|
+
await sendDoRequest(waitCmd, waitArgs, context);
|
|
220
|
+
} catch {
|
|
221
|
+
// Ignore auto-wait failures silently
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Fixed delay between steps
|
|
228
|
+
if (stepDelay > 0 && i < total - 1) {
|
|
229
|
+
await new Promise(r => setTimeout(r, stepDelay));
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const ms = Date.now() - startTime;
|
|
233
|
+
|
|
234
|
+
if (!quiet) {
|
|
235
|
+
console.log(`FAIL`);
|
|
236
|
+
console.log(` Error: ${err.message}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
results.push({ step: i + 1, cmd: step.cmd, status: 'error', error: err.message, ms });
|
|
240
|
+
failed++;
|
|
241
|
+
|
|
242
|
+
if (onError === 'stop') {
|
|
243
|
+
return {
|
|
244
|
+
status: 'failed',
|
|
245
|
+
completedSteps: i,
|
|
246
|
+
totalSteps: total,
|
|
247
|
+
results,
|
|
248
|
+
error: err.message,
|
|
249
|
+
totalMs: Date.now() - startTotal
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
status: failed > 0 ? 'partial' : 'completed',
|
|
257
|
+
completedSteps: total - failed,
|
|
258
|
+
totalSteps: total,
|
|
259
|
+
results,
|
|
260
|
+
failed,
|
|
261
|
+
totalMs: Date.now() - startTotal
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
module.exports = {
|
|
266
|
+
executeDoSteps,
|
|
267
|
+
sendDoRequest,
|
|
268
|
+
shouldAutoWait,
|
|
269
|
+
getAutoWaitCommand,
|
|
270
|
+
substituteVars,
|
|
271
|
+
AUTO_WAIT_COMMANDS,
|
|
272
|
+
AUTO_WAIT_MAP
|
|
273
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for surf `do` workflow commands
|
|
3
|
+
*
|
|
4
|
+
* Parses newline-separated commands into structured step arrays:
|
|
5
|
+
*
|
|
6
|
+
* Input:
|
|
7
|
+
* 'go "https://example.com"
|
|
8
|
+
* click e5
|
|
9
|
+
* screenshot'
|
|
10
|
+
*
|
|
11
|
+
* Output:
|
|
12
|
+
* [
|
|
13
|
+
* { cmd: 'navigate', args: { url: 'https://example.com' } },
|
|
14
|
+
* { cmd: 'click', args: { ref: 'e5' } },
|
|
15
|
+
* { cmd: 'screenshot', args: {} }
|
|
16
|
+
* ]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Aliases mapping (matches cli.cjs)
|
|
20
|
+
const ALIASES = {
|
|
21
|
+
snap: "screenshot",
|
|
22
|
+
read: "page.read",
|
|
23
|
+
find: "search",
|
|
24
|
+
go: "navigate",
|
|
25
|
+
net: "network",
|
|
26
|
+
"network.dump": "network.get",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Primary argument mapping for positional args (matches cli.cjs)
|
|
30
|
+
const PRIMARY_ARG_MAP = {
|
|
31
|
+
ai: "query",
|
|
32
|
+
gemini: "query",
|
|
33
|
+
chatgpt: "query",
|
|
34
|
+
perplexity: "query",
|
|
35
|
+
grok: "query",
|
|
36
|
+
navigate: "url",
|
|
37
|
+
go: "url",
|
|
38
|
+
js: "code",
|
|
39
|
+
javascript_tool: "code",
|
|
40
|
+
key: "key",
|
|
41
|
+
wait: "duration",
|
|
42
|
+
health: "url",
|
|
43
|
+
new_tab: "url",
|
|
44
|
+
"tab.new": "url",
|
|
45
|
+
switch_tab: "tab_id",
|
|
46
|
+
"tab.switch": "id",
|
|
47
|
+
close_tab: "tab_id",
|
|
48
|
+
"tab.close": "id",
|
|
49
|
+
"tab.name": "name",
|
|
50
|
+
"tab.unname": "name",
|
|
51
|
+
scroll_to_position: "position",
|
|
52
|
+
type: "text",
|
|
53
|
+
smart_type: "text",
|
|
54
|
+
"emulate.network": "preset",
|
|
55
|
+
"emulate.cpu": "rate",
|
|
56
|
+
search: "term",
|
|
57
|
+
find: "term",
|
|
58
|
+
"wait.element": "selector",
|
|
59
|
+
"wait.url": "pattern",
|
|
60
|
+
zoom: "level",
|
|
61
|
+
"history.search": "query",
|
|
62
|
+
"network.get": "id",
|
|
63
|
+
"network.body": "id",
|
|
64
|
+
"network.curl": "id",
|
|
65
|
+
"network.path": "id",
|
|
66
|
+
"window.new": "url",
|
|
67
|
+
"window.focus": "id",
|
|
68
|
+
"window.close": "id",
|
|
69
|
+
"locate.role": "role",
|
|
70
|
+
"locate.text": "text",
|
|
71
|
+
"locate.label": "label",
|
|
72
|
+
"emulate.device": "device",
|
|
73
|
+
"frame.js": "code",
|
|
74
|
+
"element.styles": "selector",
|
|
75
|
+
"select": "selector",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Tokenize a command line, respecting single and double quotes
|
|
80
|
+
* @param {string} line - Single line to tokenize
|
|
81
|
+
* @returns {string[]} - Array of tokens
|
|
82
|
+
*/
|
|
83
|
+
function tokenize(line) {
|
|
84
|
+
const tokens = [];
|
|
85
|
+
let current = '';
|
|
86
|
+
let inQuote = null;
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < line.length; i++) {
|
|
89
|
+
const ch = line[i];
|
|
90
|
+
|
|
91
|
+
if (inQuote) {
|
|
92
|
+
if (ch === inQuote) {
|
|
93
|
+
// End of quoted string
|
|
94
|
+
inQuote = null;
|
|
95
|
+
} else {
|
|
96
|
+
current += ch;
|
|
97
|
+
}
|
|
98
|
+
} else if (ch === '"' || ch === "'") {
|
|
99
|
+
// Start of quoted string
|
|
100
|
+
inQuote = ch;
|
|
101
|
+
} else if (ch === ' ' || ch === '\t') {
|
|
102
|
+
// Whitespace separator
|
|
103
|
+
if (current) {
|
|
104
|
+
tokens.push(current);
|
|
105
|
+
current = '';
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
current += ch;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Don't forget last token
|
|
113
|
+
if (current) {
|
|
114
|
+
tokens.push(current);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return tokens;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse a single command line into a step object
|
|
122
|
+
* @param {string} line - Single command line
|
|
123
|
+
* @returns {{ cmd: string, args: object } | null}
|
|
124
|
+
*/
|
|
125
|
+
function parseCommandLine(line) {
|
|
126
|
+
const tokens = tokenize(line);
|
|
127
|
+
if (tokens.length === 0) return null;
|
|
128
|
+
|
|
129
|
+
// Get command and apply alias
|
|
130
|
+
let cmd = tokens[0];
|
|
131
|
+
cmd = ALIASES[cmd] || cmd;
|
|
132
|
+
|
|
133
|
+
const args = {};
|
|
134
|
+
let i = 1;
|
|
135
|
+
|
|
136
|
+
// Handle first positional argument based on command type
|
|
137
|
+
if (i < tokens.length && !tokens[i].startsWith('--')) {
|
|
138
|
+
const firstArg = tokens[i];
|
|
139
|
+
|
|
140
|
+
// Special handling for click command
|
|
141
|
+
if (cmd === 'click') {
|
|
142
|
+
if (/^e\d+$/.test(firstArg)) {
|
|
143
|
+
// Element reference: e5 -> ref
|
|
144
|
+
args.ref = firstArg;
|
|
145
|
+
i++;
|
|
146
|
+
} else if (/^\d+$/.test(firstArg) && tokens[i + 1] && /^\d+$/.test(tokens[i + 1])) {
|
|
147
|
+
// Coordinates: 100 200 -> x, y
|
|
148
|
+
args.x = parseInt(firstArg, 10);
|
|
149
|
+
args.y = parseInt(tokens[i + 1], 10);
|
|
150
|
+
i += 2;
|
|
151
|
+
}
|
|
152
|
+
} else if (cmd === 'select') {
|
|
153
|
+
// Select takes selector + one or more values: select e5 "US" or select e5 "opt1" "opt2"
|
|
154
|
+
args.selector = firstArg;
|
|
155
|
+
i++;
|
|
156
|
+
// Collect remaining positional args as values
|
|
157
|
+
const values = [];
|
|
158
|
+
while (i < tokens.length && !tokens[i].startsWith('--')) {
|
|
159
|
+
values.push(tokens[i]);
|
|
160
|
+
i++;
|
|
161
|
+
}
|
|
162
|
+
// Host expects 'values' (always), matching CLI behavior
|
|
163
|
+
if (values.length === 1) {
|
|
164
|
+
args.values = values[0]; // Single value as string (host will wrap in array)
|
|
165
|
+
} else if (values.length > 1) {
|
|
166
|
+
args.values = values; // Multiple values as array
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
// Use PRIMARY_ARG_MAP for other commands
|
|
170
|
+
const primaryKey = PRIMARY_ARG_MAP[cmd];
|
|
171
|
+
if (primaryKey) {
|
|
172
|
+
args[primaryKey] = firstArg;
|
|
173
|
+
i++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Parse --flag value pairs
|
|
179
|
+
while (i < tokens.length) {
|
|
180
|
+
const token = tokens[i];
|
|
181
|
+
if (token.startsWith('--')) {
|
|
182
|
+
const key = token.slice(2);
|
|
183
|
+
const next = tokens[i + 1];
|
|
184
|
+
if (next && !next.startsWith('--')) {
|
|
185
|
+
// Flag with value
|
|
186
|
+
let val = next;
|
|
187
|
+
// Type coercion
|
|
188
|
+
if (val === "true") val = true;
|
|
189
|
+
else if (val === "false") val = false;
|
|
190
|
+
else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
|
|
191
|
+
else if (/^-?\d+\.\d+$/.test(val)) val = parseFloat(val);
|
|
192
|
+
args[key] = val;
|
|
193
|
+
i += 2;
|
|
194
|
+
} else {
|
|
195
|
+
// Boolean flag
|
|
196
|
+
args[key] = true;
|
|
197
|
+
i++;
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
// Skip unrecognized positional (shouldn't happen normally)
|
|
201
|
+
i++;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { cmd, args };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parse a multi-line workflow string into step array
|
|
210
|
+
* @param {string} input - Multi-line workflow string
|
|
211
|
+
* @returns {Array<{ cmd: string, args: object }>}
|
|
212
|
+
*/
|
|
213
|
+
function parseDoCommands(input) {
|
|
214
|
+
// Replace literal \n (backslash + n) with actual newlines
|
|
215
|
+
// This handles bash single-quoted strings like 'go "url"\nclick e5'
|
|
216
|
+
const normalized = input.replace(/\\n/g, '\n');
|
|
217
|
+
|
|
218
|
+
return normalized
|
|
219
|
+
.split('\n')
|
|
220
|
+
.map(line => line.trim())
|
|
221
|
+
.filter(line => line && !line.startsWith('#'))
|
|
222
|
+
.map(line => parseCommandLine(line))
|
|
223
|
+
.filter(step => step !== null);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
parseDoCommands,
|
|
228
|
+
parseCommandLine,
|
|
229
|
+
tokenize,
|
|
230
|
+
ALIASES,
|
|
231
|
+
PRIMARY_ARG_MAP
|
|
232
|
+
};
|
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 |
|