Skip to content

Commit 9570b98

Browse files
committed
feat: pipe separator for inline workflows (v2.4.1)
- Pipe | is now the primary separator: surf do 'go "url" | click e5 | snap' - Much cleaner than escaped newlines - Newlines still supported for files and heredocs
1 parent b1a8c49 commit 9570b98

File tree

8 files changed

+51
-29
lines changed

8 files changed

+51
-29
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
# Changelog
22

3+
## [2.4.1] - 2026-01-22
4+
5+
### Changed
6+
- **Workflow syntax** - Pipe separator `|` is now the primary way to chain commands inline:
7+
`surf do 'go "https://example.com" | click e5 | screenshot'`
8+
Newlines still supported for file-based workflows and heredocs.
9+
310
## [2.4.0] - 2026-01-22
411

512
### Added
613
- **Workflow execution** - New `surf do` command to execute multi-step browser workflows as a single operation. Reduces token overhead and improves reliability for common automation sequences.
7-
- Inline workflows: `surf do 'go "https://example.com"\nclick e5\nscreenshot'`
14+
- Inline workflows: `surf do 'go "url" | click e5 | screenshot'`
815
- File-based workflows: `surf do --file workflow.json`
916
- Dry run validation: `surf do '...' --dry-run`
1017
- Smart auto-waits after navigation, clicks, and form submissions

README.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -378,18 +378,17 @@ Auto-cleanup: 24 hours TTL, 200MB max.
378378
Execute multi-step browser automation as a single command:
379379
380380
```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'
381+
# Inline workflow (pipe-separated)
382+
surf do 'go "https://example.com" | click e5 | screenshot'
387383
388-
# From JSON file (same format as --script)
389-
surf do --file login-workflow.json
384+
# Multi-step login flow
385+
surf do 'go "https://example.com/login" | type "user@example.com" --selector "#email" | type "pass" --selector "#password" | click --selector "button[type=submit]"'
386+
387+
# From JSON file
388+
surf do --file workflow.json
390389
391390
# Validate without executing
392-
surf do 'go "url"\nclick e5\nscreenshot' --dry-run
391+
surf do 'go "url" | click e5 | screenshot' --dry-run
393392
```
394393
395394
**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.

native/cli.cjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -817,9 +817,9 @@ const TOOLS = {
817817
"dry-run": "Parse and validate without executing"
818818
},
819819
examples: [
820-
{ cmd: 'do \'go "https://example.com"\\nclick e5\\nscreenshot\'', desc: "Inline workflow" },
820+
{ cmd: 'do \'go "https://example.com" | click e5 | screenshot\'', desc: "Inline workflow" },
821821
{ cmd: 'do -f login.json', desc: "From JSON file" },
822-
{ cmd: 'do \'go "url"\\nclick e5\' --dry-run', desc: "Validate without running" },
822+
{ cmd: 'do \'go "url" | click e5\' --dry-run', desc: "Validate without running" },
823823
]
824824
},
825825
}

native/do-parser.cjs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,17 +206,23 @@ function parseCommandLine(line) {
206206
}
207207

208208
/**
209-
* Parse a multi-line workflow string into step array
210-
* @param {string} input - Multi-line workflow string
209+
* Parse a workflow string into step array
210+
* Supports pipe-separated (inline) or newline-separated (file) commands
211+
* @param {string} input - Workflow string
211212
* @returns {Array<{ cmd: string, args: object }>}
212213
*/
213214
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');
215+
// Determine separator: use pipe if present, otherwise newlines
216+
// Pipe is preferred for inline: 'go "url" | click e5 | screenshot'
217+
// Newlines for files or heredocs
218+
const hasPipe = input.includes('|');
219+
const separator = hasPipe ? '|' : '\n';
220+
221+
// Also handle literal \n for backwards compatibility
222+
const normalized = hasPipe ? input : input.replace(/\\n/g, '\n');
217223

218224
return normalized
219-
.split('\n')
225+
.split(separator)
220226
.map(line => line.trim())
221227
.filter(line => line && !line.startsWith('#'))
222228
.map(line => parseCommandLine(line))

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "surf-cli",
3-
"version": "2.4.0",
3+
"version": "2.4.1",
44
"description": "CLI for AI agents to control Chrome. Zero config, agent-agnostic, battle-tested.",
55
"keywords": [
66
"chrome",

skills/surf/SKILL.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -307,18 +307,17 @@ surf smoke --urls "..." --screenshot /tmp/smoke
307307
Execute multi-step browser automation as a single command with smart auto-waits:
308308

309309
```bash
310-
# Inline workflow (newline-separated commands)
311-
surf do 'go "https://example.com/login"
312-
type "user@example.com" --selector "input[name=email]"
313-
type "password123" --selector "input[name=password]"
314-
click --selector "button[type=submit]"
315-
screenshot --output /tmp/after-login.png'
310+
# Inline workflow (pipe-separated)
311+
surf do 'go "https://example.com" | click e5 | screenshot'
312+
313+
# Multi-step login flow
314+
surf do 'go "https://example.com/login" | type "user@example.com" --selector "#email" | type "pass" --selector "#password" | click --selector "button[type=submit]"'
316315

317316
# From JSON file
318-
surf do --file login-workflow.json
317+
surf do --file workflow.json
319318

320319
# Validate without executing
321-
surf do 'go "url"\nclick e5' --dry-run
320+
surf do 'go "url" | click e5' --dry-run
322321
```
323322

324323
**Why use `do`?** Instead of 6-8 separate CLI calls with LLM orchestration between each, a workflow executes deterministically. Faster, cheaper, and more reliable.

test/unit/do-parser.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ describe("parseDoCommands", () => {
1212
expect(steps[0].args).toEqual({});
1313
});
1414

15+
it("parses pipe-separated commands", () => {
16+
const input = 'go "https://example.com" | click e5 | screenshot';
17+
const steps = parser.parseDoCommands(input);
18+
expect(steps).toHaveLength(3);
19+
expect(steps[0].cmd).toBe("navigate");
20+
expect(steps[0].args.url).toBe("https://example.com");
21+
expect(steps[1].cmd).toBe("click");
22+
expect(steps[1].args.ref).toBe("e5");
23+
expect(steps[2].cmd).toBe("screenshot");
24+
});
25+
1526
it("parses newline-separated commands", () => {
1627
const input = 'go "https://example.com"\nclick e5';
1728
const steps = parser.parseDoCommands(input);

0 commit comments

Comments
 (0)