@vibemastery/zurf 0.2.2 → 0.2.3
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 +111 -16
- package/dist/commands/browse/index.d.ts +15 -0
- package/dist/commands/browse/index.js +109 -0
- package/dist/commands/config/which.d.ts +1 -0
- package/dist/commands/fetch/index.d.ts +1 -0
- package/dist/commands/fetch/index.js +14 -15
- package/dist/commands/init/index.d.ts +3 -0
- package/dist/commands/init/index.js +26 -5
- package/dist/commands/search/index.d.ts +1 -0
- package/dist/lib/browse-output.d.ts +12 -0
- package/dist/lib/browse-output.js +10 -0
- package/dist/lib/browserbase-session.d.ts +7 -0
- package/dist/lib/browserbase-session.js +39 -0
- package/dist/lib/cli-errors.d.ts +1 -0
- package/dist/lib/cli-errors.js +9 -0
- package/dist/lib/config.d.ts +26 -0
- package/dist/lib/config.js +77 -6
- package/dist/lib/fetch-output.d.ts +2 -1
- package/dist/lib/fetch-output.js +3 -2
- package/dist/lib/flags.d.ts +3 -0
- package/dist/lib/flags.js +7 -0
- package/dist/lib/html-to-markdown.d.ts +1 -0
- package/dist/lib/html-to-markdown.js +21 -0
- package/oclif.manifest.json +148 -55
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,23 +1,124 @@
|
|
|
1
1
|
zurf
|
|
2
2
|
=================
|
|
3
3
|
|
|
4
|
-
A lightweight CLI for searching and fetching web pages, powered by Browserbase.
|
|
4
|
+
A lightweight CLI for searching, browsing, and fetching web pages, powered by Browserbase.
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
[](https://oclif.io)
|
|
8
8
|
[](https://npmjs.org/package/@vibemastery/zurf)
|
|
9
9
|
[](https://npmjs.org/package/@vibemastery/zurf)
|
|
10
10
|
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```sh-session
|
|
14
|
+
$ npm install -g @vibemastery/zurf
|
|
15
|
+
$ zurf init --global # save your Browserbase API key
|
|
16
|
+
$ zurf --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
### `zurf search <query>`
|
|
22
|
+
|
|
23
|
+
Search the web via Browserbase (Exa-powered). Returns a list of matching URLs with titles and snippets.
|
|
24
|
+
|
|
25
|
+
```sh-session
|
|
26
|
+
$ zurf search "browserbase documentation"
|
|
27
|
+
$ zurf search "laravel inertia" --num-results 5 --json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Flag | Description |
|
|
31
|
+
|------|-------------|
|
|
32
|
+
| `-n, --num-results` | Number of results, 1-25 (default: 10) |
|
|
33
|
+
| `--json` | Print machine-readable JSON to stdout |
|
|
34
|
+
|
|
35
|
+
### `zurf browse <url>`
|
|
36
|
+
|
|
37
|
+
Open a URL in a real cloud Chromium browser via Browserbase, wait for JavaScript to fully render, then return the page content as **markdown** (default) or raw HTML.
|
|
38
|
+
|
|
39
|
+
Best for JavaScript-heavy pages (SPAs, dashboards, pages behind client-side rendering).
|
|
40
|
+
|
|
41
|
+
```sh-session
|
|
42
|
+
$ zurf browse https://example.com # markdown output
|
|
43
|
+
$ zurf browse https://example.com --html # raw HTML output
|
|
44
|
+
$ zurf browse https://example.com -o page.md # save full content to file
|
|
45
|
+
$ zurf browse https://example.com --json # JSON with content + metadata
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
| Flag | Description |
|
|
49
|
+
|------|-------------|
|
|
50
|
+
| `--html` | Output raw HTML instead of markdown |
|
|
51
|
+
| `-o, --output` | Write full content to a file |
|
|
52
|
+
| `--json` | Print machine-readable JSON to stdout |
|
|
53
|
+
|
|
54
|
+
### `zurf fetch <url>`
|
|
55
|
+
|
|
56
|
+
Fetch a URL via Browserbase without launching a full browser session. Returns the content as **markdown** (default) or raw HTML. Fast and lightweight, but only works for static pages (no JavaScript rendering). 1 MB max.
|
|
57
|
+
|
|
58
|
+
```sh-session
|
|
59
|
+
$ zurf fetch https://example.com # markdown output
|
|
60
|
+
$ zurf fetch https://example.com --html # raw HTML output
|
|
61
|
+
$ zurf fetch https://example.com -o page.md # save full content to file
|
|
62
|
+
$ zurf fetch https://example.com --proxies # route through Browserbase proxies
|
|
63
|
+
$ zurf fetch https://example.com --json # JSON with content + metadata
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Flag | Description |
|
|
67
|
+
|------|-------------|
|
|
68
|
+
| `--html` | Output raw HTML instead of markdown |
|
|
69
|
+
| `-o, --output` | Write full content to a file |
|
|
70
|
+
| `--proxies` | Route through Browserbase proxies |
|
|
71
|
+
| `--allow-redirects` | Follow HTTP redirects |
|
|
72
|
+
| `--allow-insecure-ssl` | Disable TLS certificate verification |
|
|
73
|
+
| `--json` | Print machine-readable JSON to stdout |
|
|
74
|
+
|
|
75
|
+
### `zurf init`
|
|
76
|
+
|
|
77
|
+
Save your Browserbase API key and optional Project ID to a config file.
|
|
78
|
+
|
|
79
|
+
```sh-session
|
|
80
|
+
$ zurf init --global # save to global config
|
|
81
|
+
$ zurf init --local # save to project .zurf/config.json
|
|
82
|
+
$ zurf init --local --gitignore # also append .zurf/ to .gitignore
|
|
83
|
+
$ zurf init --global --project-id <project-id> # save project ID too
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### `zurf config which`
|
|
87
|
+
|
|
88
|
+
Show where your API key and Project ID would be loaded from (nothing secret is printed).
|
|
89
|
+
|
|
90
|
+
```sh-session
|
|
91
|
+
$ zurf config which
|
|
92
|
+
$ zurf config which --json
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Output format
|
|
96
|
+
|
|
97
|
+
`zurf browse` and `zurf fetch` return **markdown** by default — smaller and more useful for LLM agents. Pass `--html` (or set `ZURF_HTML=true`) to get raw HTML instead.
|
|
98
|
+
|
|
99
|
+
You can also set the default in `.zurf/config.json` or the global config:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{ "format": "html" }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Format resolution (highest precedence first):
|
|
106
|
+
|
|
107
|
+
1. `--html` flag
|
|
108
|
+
2. `ZURF_HTML` environment variable (`true` or `1`)
|
|
109
|
+
3. Local `.zurf/config.json` `format` field
|
|
110
|
+
4. Global config `format` field
|
|
111
|
+
5. Default: `markdown`
|
|
112
|
+
|
|
11
113
|
## Configuration
|
|
12
114
|
|
|
13
|
-
API key resolution
|
|
115
|
+
API key resolution (highest precedence first):
|
|
14
116
|
|
|
15
|
-
1.
|
|
16
|
-
2.
|
|
17
|
-
3.
|
|
18
|
-
4. Global file: `$XDG_CONFIG_HOME/zurf/config.json` if `XDG_CONFIG_HOME` is set, otherwise `~/.config/zurf/config.json` (on Windows, `%APPDATA%\zurf\config.json`)
|
|
117
|
+
1. Environment variable `BROWSERBASE_API_KEY`
|
|
118
|
+
2. Nearest `.zurf/config.json` when walking up from the current working directory
|
|
119
|
+
3. Global config: `$XDG_CONFIG_HOME/zurf/config.json` (or `~/.config/zurf/config.json`)
|
|
19
120
|
|
|
20
|
-
Save a key interactively
|
|
121
|
+
Save a key interactively:
|
|
21
122
|
|
|
22
123
|
```sh-session
|
|
23
124
|
$ zurf init --global
|
|
@@ -28,20 +129,14 @@ For project-local storage, add `.zurf/` to `.gitignore` so the key is never comm
|
|
|
28
129
|
|
|
29
130
|
**Security note:** Keys in `config.json` are stored as plaintext with file mode `0o600`. For shared machines or stricter setups, prefer `BROWSERBASE_API_KEY` from your environment or a secrets manager instead of `init`.
|
|
30
131
|
|
|
31
|
-
See where a key would be loaded from (nothing secret is printed): `zurf config which`.
|
|
32
|
-
|
|
33
132
|
## Claude Code and agents
|
|
34
133
|
|
|
35
|
-
Install `zurf` on your `PATH` and allow the agent to run shell commands. Use `--json` when you want
|
|
134
|
+
Install `zurf` on your `PATH` and allow the agent to run shell commands. Use `--json` when you want structured output:
|
|
36
135
|
|
|
37
136
|
```sh-session
|
|
38
137
|
$ zurf search "browserbase fetch api" --json
|
|
138
|
+
$ zurf browse https://example.com --json
|
|
39
139
|
$ zurf fetch https://example.com --json
|
|
40
140
|
```
|
|
41
141
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
```sh-session
|
|
45
|
-
$ npm install -g @vibemastery/zurf
|
|
46
|
-
$ zurf --help
|
|
47
|
-
```
|
|
142
|
+
Content is returned as markdown by default, which keeps token counts low. Pass `--html` if the agent needs the raw DOM.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ZurfBrowserbaseCommand } from '../../lib/zurf-browserbase-command.js';
|
|
2
|
+
export default class Browse extends ZurfBrowserbaseCommand {
|
|
3
|
+
static args: {
|
|
4
|
+
url: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
5
|
+
};
|
|
6
|
+
static description: string;
|
|
7
|
+
static examples: string[];
|
|
8
|
+
static flags: {
|
|
9
|
+
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
html: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
12
|
+
};
|
|
13
|
+
static summary: string;
|
|
14
|
+
run(): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { HUMAN_BODY_PREVIEW_CHARS, humanBrowseMetaLines, truncateNote, } from '../../lib/browse-output.js';
|
|
5
|
+
import { withBrowserbaseSession } from '../../lib/browserbase-session.js';
|
|
6
|
+
import { cliError, errorCode } from '../../lib/cli-errors.js';
|
|
7
|
+
import { resolveFormat, resolveProjectId } from '../../lib/config.js';
|
|
8
|
+
import { zurfBaseFlags } from '../../lib/flags.js';
|
|
9
|
+
import { htmlToMarkdown } from '../../lib/html-to-markdown.js';
|
|
10
|
+
import { printJson } from '../../lib/json-output.js';
|
|
11
|
+
import { ZurfBrowserbaseCommand } from '../../lib/zurf-browserbase-command.js';
|
|
12
|
+
export default class Browse extends ZurfBrowserbaseCommand {
|
|
13
|
+
static args = {
|
|
14
|
+
url: Args.string({
|
|
15
|
+
description: 'URL to browse',
|
|
16
|
+
required: true,
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
static description = `Browse a URL in a cloud browser and return the rendered content as markdown (default) or raw HTML.
|
|
20
|
+
Uses a real Chromium browser via Browserbase, so JavaScript-heavy pages are fully rendered.
|
|
21
|
+
Requires authentication and a Project ID. Run \`zurf init --global\` before first use.`;
|
|
22
|
+
static examples = [
|
|
23
|
+
'<%= config.bin %> <%= command.id %> https://example.com',
|
|
24
|
+
'<%= config.bin %> <%= command.id %> https://example.com --html',
|
|
25
|
+
'<%= config.bin %> <%= command.id %> https://example.com --json',
|
|
26
|
+
'<%= config.bin %> <%= command.id %> https://example.com -o page.md',
|
|
27
|
+
];
|
|
28
|
+
static flags = {
|
|
29
|
+
...zurfBaseFlags,
|
|
30
|
+
output: Flags.string({
|
|
31
|
+
char: 'o',
|
|
32
|
+
description: 'Write rendered content to this file (full content); otherwise human mode prints a truncated preview to stdout',
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
static summary = 'Browse a URL in a cloud browser and return rendered content as markdown';
|
|
36
|
+
async run() {
|
|
37
|
+
const { args, flags } = await this.parse(Browse);
|
|
38
|
+
const url = args.url.trim();
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = new URL(url);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
cliError({ command: this, exitCode: 2, json: flags.json, message: `Invalid URL: ${url}` });
|
|
45
|
+
}
|
|
46
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
47
|
+
cliError({
|
|
48
|
+
command: this,
|
|
49
|
+
exitCode: 2,
|
|
50
|
+
json: flags.json,
|
|
51
|
+
message: `Only http and https URLs are supported: ${url}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (!parsed.hostname) {
|
|
55
|
+
cliError({ command: this, exitCode: 2, json: flags.json, message: `Invalid URL: ${url}` });
|
|
56
|
+
}
|
|
57
|
+
await this.runWithBrowserbase(flags, `Browsing ${url}`, async (client) => {
|
|
58
|
+
const resolution = resolveProjectId({ globalConfigDir: this.config.configDir });
|
|
59
|
+
if (resolution.source === 'none') {
|
|
60
|
+
throw new Error('No Browserbase Project ID found. Set BROWSERBASE_PROJECT_ID, run `zurf init --global` with --project-id, or add projectId to your .zurf/config.json.');
|
|
61
|
+
}
|
|
62
|
+
const { projectId } = resolution;
|
|
63
|
+
const result = await withBrowserbaseSession({
|
|
64
|
+
client,
|
|
65
|
+
projectId,
|
|
66
|
+
async work(page) {
|
|
67
|
+
const response = await page.goto(url, { timeout: 30_000, waitUntil: 'networkidle' });
|
|
68
|
+
const statusCode = response?.status() ?? null;
|
|
69
|
+
const content = await page.content();
|
|
70
|
+
return { content, statusCode };
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const format = resolveFormat({ flagHtml: flags.html, globalConfigDir: this.config.configDir });
|
|
74
|
+
const content = format === 'markdown' ? await htmlToMarkdown(result.content) : result.content;
|
|
75
|
+
if (flags.json) {
|
|
76
|
+
const payload = { content, format, statusCode: result.statusCode, url };
|
|
77
|
+
printJson(payload);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.logToStderr(humanBrowseMetaLines({ statusCode: result.statusCode, url }).join('\n'));
|
|
81
|
+
this.logToStderr('');
|
|
82
|
+
if (flags.output) {
|
|
83
|
+
try {
|
|
84
|
+
await fs.writeFile(flags.output, content, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (errorCode(error) === 'ENOENT') {
|
|
88
|
+
cliError({
|
|
89
|
+
command: this,
|
|
90
|
+
exitCode: 1,
|
|
91
|
+
json: flags.json,
|
|
92
|
+
message: `Directory does not exist for output file: ${path.dirname(path.resolve(flags.output))}`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
this.logToStderr(`Wrote rendered content to ${flags.output}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (content.length <= HUMAN_BODY_PREVIEW_CHARS) {
|
|
101
|
+
this.log(content);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.log(content.slice(0, HUMAN_BODY_PREVIEW_CHARS));
|
|
105
|
+
this.log('');
|
|
106
|
+
this.logToStderr(truncateNote(content.length));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -3,6 +3,7 @@ export default class ConfigWhich extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
html: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
7
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
8
|
};
|
|
8
9
|
static summary: string;
|
|
@@ -10,6 +10,7 @@ export default class Fetch extends ZurfBrowserbaseCommand {
|
|
|
10
10
|
'allow-redirects': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
proxies: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
html: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
15
|
};
|
|
15
16
|
static summary: string;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { cliError } from '../../lib/cli-errors.js';
|
|
4
|
+
import { cliError, errorCode } from '../../lib/cli-errors.js';
|
|
5
|
+
import { resolveFormat } from '../../lib/config.js';
|
|
5
6
|
import { buildFetchJsonPayload, HUMAN_BODY_PREVIEW_CHARS, humanFetchMetaLines, truncateNote, } from '../../lib/fetch-output.js';
|
|
6
7
|
import { zurfBaseFlags } from '../../lib/flags.js';
|
|
8
|
+
import { htmlToMarkdown } from '../../lib/html-to-markdown.js';
|
|
7
9
|
import { printJson } from '../../lib/json-output.js';
|
|
8
10
|
import { ZurfBrowserbaseCommand } from '../../lib/zurf-browserbase-command.js';
|
|
9
11
|
export default class Fetch extends ZurfBrowserbaseCommand {
|
|
@@ -13,12 +15,13 @@ export default class Fetch extends ZurfBrowserbaseCommand {
|
|
|
13
15
|
required: true,
|
|
14
16
|
}),
|
|
15
17
|
};
|
|
16
|
-
static description = `Fetch a URL via Browserbase (no browser session; static HTML, 1 MB max).
|
|
18
|
+
static description = `Fetch a URL via Browserbase and return content as markdown (default) or raw HTML (no browser session; static HTML, 1 MB max).
|
|
17
19
|
Requires authentication. Run \`zurf init --global\` or use a project key before first use.`;
|
|
18
20
|
static examples = [
|
|
19
21
|
'<%= config.bin %> <%= command.id %> https://example.com',
|
|
22
|
+
'<%= config.bin %> <%= command.id %> https://example.com --html',
|
|
20
23
|
'<%= config.bin %> <%= command.id %> https://example.com --json',
|
|
21
|
-
'<%= config.bin %> <%= command.id %> https://example.com -o page.
|
|
24
|
+
'<%= config.bin %> <%= command.id %> https://example.com -o page.md --proxies',
|
|
22
25
|
];
|
|
23
26
|
static flags = {
|
|
24
27
|
...zurfBaseFlags,
|
|
@@ -39,7 +42,7 @@ Requires authentication. Run \`zurf init --global\` or use a project key before
|
|
|
39
42
|
description: 'Route through Browserbase proxies (helps with some blocked sites)',
|
|
40
43
|
}),
|
|
41
44
|
};
|
|
42
|
-
static summary = 'Fetch a URL via Browserbase';
|
|
45
|
+
static summary = 'Fetch a URL via Browserbase and return content as markdown';
|
|
43
46
|
async run() {
|
|
44
47
|
const { args, flags } = await this.parse(Fetch);
|
|
45
48
|
const url = args.url.trim();
|
|
@@ -68,24 +71,21 @@ Requires authentication. Run \`zurf init --global\` or use a project key before
|
|
|
68
71
|
proxies: flags.proxies,
|
|
69
72
|
url,
|
|
70
73
|
});
|
|
74
|
+
const format = resolveFormat({ flagHtml: flags.html, globalConfigDir: this.config.configDir });
|
|
75
|
+
const content = format === 'markdown' ? await htmlToMarkdown(response.content) : response.content;
|
|
76
|
+
const converted = { ...response, content };
|
|
71
77
|
if (flags.json) {
|
|
72
|
-
printJson(buildFetchJsonPayload(
|
|
78
|
+
printJson(buildFetchJsonPayload(converted, format));
|
|
73
79
|
return;
|
|
74
80
|
}
|
|
75
81
|
this.logToStderr(humanFetchMetaLines(response).join('\n'));
|
|
76
82
|
this.logToStderr('');
|
|
77
83
|
if (flags.output) {
|
|
78
84
|
try {
|
|
79
|
-
await fs.writeFile(flags.output,
|
|
85
|
+
await fs.writeFile(flags.output, content, 'utf8');
|
|
80
86
|
}
|
|
81
87
|
catch (error) {
|
|
82
|
-
|
|
83
|
-
typeof error === 'object' &&
|
|
84
|
-
'code' in error &&
|
|
85
|
-
typeof error.code === 'string'
|
|
86
|
-
? error.code
|
|
87
|
-
: undefined;
|
|
88
|
-
if (code === 'ENOENT') {
|
|
88
|
+
if (errorCode(error) === 'ENOENT') {
|
|
89
89
|
cliError({
|
|
90
90
|
command: this,
|
|
91
91
|
exitCode: 1,
|
|
@@ -95,10 +95,9 @@ Requires authentication. Run \`zurf init --global\` or use a project key before
|
|
|
95
95
|
}
|
|
96
96
|
throw error;
|
|
97
97
|
}
|
|
98
|
-
this.logToStderr(`Wrote
|
|
98
|
+
this.logToStderr(`Wrote content to ${flags.output}`);
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
|
-
const { content } = response;
|
|
102
101
|
if (content.length <= HUMAN_BODY_PREVIEW_CHARS) {
|
|
103
102
|
this.log(content);
|
|
104
103
|
return;
|
|
@@ -7,9 +7,12 @@ export default class Init extends Command {
|
|
|
7
7
|
gitignore: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
8
|
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
html: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
12
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
13
|
};
|
|
12
14
|
static summary: string;
|
|
13
15
|
run(): Promise<void>;
|
|
14
16
|
private readApiKeyForInit;
|
|
17
|
+
private readProjectIdForInit;
|
|
15
18
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
3
|
import { cliError, errorMessage } from '../../lib/cli-errors.js';
|
|
4
|
-
import { globalConfigFilePath, localConfigPathForCwd,
|
|
4
|
+
import { globalConfigFilePath, localConfigPathForCwd, writeConfig } from '../../lib/config.js';
|
|
5
5
|
import { zurfBaseFlags } from '../../lib/flags.js';
|
|
6
6
|
import { dotGitignoreMentionsZurf, ensureZurfGitignoreEntry, gitignorePathForCwd, } from '../../lib/gitignore-zurf.js';
|
|
7
7
|
import { promptLine, readStdinIfPiped } from '../../lib/init-input.js';
|
|
8
8
|
import { printJson } from '../../lib/json-output.js';
|
|
9
9
|
export default class Init extends Command {
|
|
10
|
-
static description = `Save your Browserbase API key to global or project config.
|
|
10
|
+
static description = `Save your Browserbase API key and optional Project ID to global or project config.
|
|
11
11
|
Global path follows oclif config (same as \`zurf config which\`).`;
|
|
12
12
|
static examples = [
|
|
13
13
|
'<%= config.bin %> <%= command.id %> --global',
|
|
14
14
|
'<%= config.bin %> <%= command.id %> --local',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> --global --api-key KEY --project-id PROJ_ID',
|
|
15
16
|
'printenv BROWSERBASE_API_KEY | <%= config.bin %> <%= command.id %> --global',
|
|
16
17
|
];
|
|
17
18
|
static flags = {
|
|
@@ -30,16 +31,23 @@ Global path follows oclif config (same as \`zurf config which\`).`;
|
|
|
30
31
|
description: 'Store API key in ./.zurf/config.json for this directory',
|
|
31
32
|
exactlyOne: ['global', 'local'],
|
|
32
33
|
}),
|
|
34
|
+
'project-id': Flags.string({
|
|
35
|
+
description: 'Browserbase Project ID (optional; needed for browse, screenshot, pdf commands)',
|
|
36
|
+
}),
|
|
33
37
|
};
|
|
34
|
-
static summary = 'Configure Browserbase API key storage';
|
|
38
|
+
static summary = 'Configure Browserbase API key and Project ID storage';
|
|
35
39
|
async run() {
|
|
36
40
|
const { flags } = await this.parse(Init);
|
|
37
41
|
const apiKey = await this.readApiKeyForInit(flags);
|
|
38
42
|
const targetPath = flags.global
|
|
39
43
|
? globalConfigFilePath(this.config.configDir)
|
|
40
44
|
: localConfigPathForCwd();
|
|
45
|
+
const projectId = await this.readProjectIdForInit(flags);
|
|
46
|
+
const configUpdate = { apiKey: apiKey.trim() };
|
|
47
|
+
if (projectId)
|
|
48
|
+
configUpdate.projectId = projectId;
|
|
41
49
|
try {
|
|
42
|
-
await
|
|
50
|
+
await writeConfig(targetPath, configUpdate);
|
|
43
51
|
}
|
|
44
52
|
catch (error) {
|
|
45
53
|
cliError({ command: this, exitCode: 1, json: flags.json, message: errorMessage(error) });
|
|
@@ -53,10 +61,16 @@ Global path follows oclif config (same as \`zurf config which\`).`;
|
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
63
|
if (flags.json) {
|
|
56
|
-
|
|
64
|
+
const payload = { ok: true, path: targetPath, scope: flags.global ? 'global' : 'local' };
|
|
65
|
+
if (projectId)
|
|
66
|
+
payload.projectId = true;
|
|
67
|
+
printJson(payload);
|
|
57
68
|
}
|
|
58
69
|
else {
|
|
59
70
|
this.log(`Saved API key to ${targetPath}`);
|
|
71
|
+
if (projectId) {
|
|
72
|
+
this.log(`Saved Project ID to ${targetPath}`);
|
|
73
|
+
}
|
|
60
74
|
if (flags.local) {
|
|
61
75
|
let showTip = true;
|
|
62
76
|
try {
|
|
@@ -92,4 +106,11 @@ Global path follows oclif config (same as \`zurf config which\`).`;
|
|
|
92
106
|
}
|
|
93
107
|
return apiKey;
|
|
94
108
|
}
|
|
109
|
+
async readProjectIdForInit(flags) {
|
|
110
|
+
let projectId = flags['project-id']?.trim();
|
|
111
|
+
if (!projectId && !flags.json && process.stdin.isTTY) {
|
|
112
|
+
projectId = await promptLine('Browserbase Project ID (optional, press Enter to skip): ');
|
|
113
|
+
}
|
|
114
|
+
return projectId || undefined;
|
|
115
|
+
}
|
|
95
116
|
}
|
|
@@ -7,6 +7,7 @@ export default class Search extends ZurfBrowserbaseCommand {
|
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
9
|
'num-results': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
html: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
};
|
|
12
13
|
static summary: string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface BrowseJsonPayload {
|
|
2
|
+
content: string;
|
|
3
|
+
format: 'html' | 'markdown';
|
|
4
|
+
statusCode: null | number;
|
|
5
|
+
url: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function humanBrowseMetaLines(options: {
|
|
8
|
+
statusCode: null | number;
|
|
9
|
+
url: string;
|
|
10
|
+
}): string[];
|
|
11
|
+
export declare const HUMAN_BODY_PREVIEW_CHARS = 8000;
|
|
12
|
+
export declare function truncateNote(totalChars: number): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function humanBrowseMetaLines(options) {
|
|
2
|
+
return [
|
|
3
|
+
`url: ${options.url}`,
|
|
4
|
+
`statusCode: ${options.statusCode ?? 'unknown'}`,
|
|
5
|
+
];
|
|
6
|
+
}
|
|
7
|
+
export const HUMAN_BODY_PREVIEW_CHARS = 8000;
|
|
8
|
+
export function truncateNote(totalChars) {
|
|
9
|
+
return `… truncated (${totalChars} chars). Use --output FILE to save the full content.`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export async function withBrowserbaseSession(options) {
|
|
2
|
+
const { chromium } = await import('playwright-core');
|
|
3
|
+
const { client, projectId, work } = options;
|
|
4
|
+
const session = await client.sessions.create({ projectId });
|
|
5
|
+
const browser = await chromium.connectOverCDP(session.connectUrl);
|
|
6
|
+
let cleanedUp = false;
|
|
7
|
+
const cleanup = async () => {
|
|
8
|
+
if (cleanedUp)
|
|
9
|
+
return;
|
|
10
|
+
cleanedUp = true;
|
|
11
|
+
try {
|
|
12
|
+
const pages = browser.contexts()[0]?.pages() ?? [];
|
|
13
|
+
await Promise.all(pages.map((p) => p.close().catch(() => { })));
|
|
14
|
+
await browser.close().catch(() => { });
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
await client.sessions.update(session.id, { status: 'REQUEST_RELEASE' }).catch(() => { });
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const onSignal = () => {
|
|
21
|
+
// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
|
|
22
|
+
cleanup().then(() => process.exit(1), () => process.exit(1));
|
|
23
|
+
};
|
|
24
|
+
process.on('SIGINT', onSignal);
|
|
25
|
+
process.on('SIGTERM', onSignal);
|
|
26
|
+
try {
|
|
27
|
+
const defaultContext = browser.contexts()[0];
|
|
28
|
+
if (!defaultContext) {
|
|
29
|
+
throw new Error('No browser context available from Browserbase session');
|
|
30
|
+
}
|
|
31
|
+
const page = defaultContext.pages()[0] ?? (await defaultContext.newPage());
|
|
32
|
+
return await work(page);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
process.removeListener('SIGINT', onSignal);
|
|
36
|
+
process.removeListener('SIGTERM', onSignal);
|
|
37
|
+
await cleanup();
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/lib/cli-errors.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare class CliJsonExitContractError extends Error {
|
|
|
14
14
|
constructor();
|
|
15
15
|
}
|
|
16
16
|
export declare function errorMessage(err: unknown): string;
|
|
17
|
+
export declare function errorCode(err: unknown): string | undefined;
|
|
17
18
|
export declare function errorStatus(err: unknown): number | undefined;
|
|
18
19
|
/** Print JSON or human error and exit; does not return. */
|
|
19
20
|
export declare function cliError(options: CliErrorOptions): never;
|
package/dist/lib/cli-errors.js
CHANGED
|
@@ -15,6 +15,15 @@ export function errorMessage(err) {
|
|
|
15
15
|
}
|
|
16
16
|
return String(err);
|
|
17
17
|
}
|
|
18
|
+
export function errorCode(err) {
|
|
19
|
+
if (err !== null &&
|
|
20
|
+
typeof err === 'object' &&
|
|
21
|
+
'code' in err &&
|
|
22
|
+
typeof err.code === 'string') {
|
|
23
|
+
return err.code;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
18
27
|
export function errorStatus(err) {
|
|
19
28
|
if (err !== null &&
|
|
20
29
|
typeof err === 'object' &&
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -20,15 +20,41 @@ export type ActiveApiKey = Extract<ResolvedApiKey, {
|
|
|
20
20
|
}>;
|
|
21
21
|
export interface ConfigFileShape {
|
|
22
22
|
apiKey?: string;
|
|
23
|
+
format?: 'html' | 'markdown';
|
|
24
|
+
projectId?: string;
|
|
23
25
|
}
|
|
26
|
+
export type ResolvedProjectId = {
|
|
27
|
+
path: string;
|
|
28
|
+
projectId: string;
|
|
29
|
+
source: 'global';
|
|
30
|
+
} | {
|
|
31
|
+
path: string;
|
|
32
|
+
projectId: string;
|
|
33
|
+
source: 'local';
|
|
34
|
+
} | {
|
|
35
|
+
projectId: string;
|
|
36
|
+
source: 'env';
|
|
37
|
+
} | {
|
|
38
|
+
source: 'none';
|
|
39
|
+
};
|
|
24
40
|
/**
|
|
25
41
|
* Path to global `config.json` under oclif's `this.config.configDir` (same rules as @oclif/core `Config.dir('config')` for `dirname` zurf).
|
|
26
42
|
*/
|
|
27
43
|
export declare function globalConfigFilePath(oclifConfigDir: string): string;
|
|
28
44
|
export declare function localConfigPathForCwd(cwd?: string): string;
|
|
29
45
|
export declare function findLocalConfigPath(startDir?: string): string | undefined;
|
|
46
|
+
export declare function resolveFormat(options: {
|
|
47
|
+
cwd?: string;
|
|
48
|
+
flagHtml: boolean;
|
|
49
|
+
globalConfigDir: string;
|
|
50
|
+
}): 'html' | 'markdown';
|
|
30
51
|
export declare function resolveApiKey(options: {
|
|
31
52
|
cwd?: string;
|
|
32
53
|
globalConfigDir: string;
|
|
33
54
|
}): ResolvedApiKey;
|
|
55
|
+
export declare function resolveProjectId(options: {
|
|
56
|
+
cwd?: string;
|
|
57
|
+
globalConfigDir: string;
|
|
58
|
+
}): ResolvedProjectId;
|
|
34
59
|
export declare function writeApiKeyConfig(targetPath: string, apiKey: string): Promise<void>;
|
|
60
|
+
export declare function writeConfig(targetPath: string, fields: Partial<ConfigFileShape>): Promise<void>;
|
package/dist/lib/config.js
CHANGED
|
@@ -26,17 +26,57 @@ export function findLocalConfigPath(startDir = process.cwd()) {
|
|
|
26
26
|
}
|
|
27
27
|
return undefined;
|
|
28
28
|
}
|
|
29
|
-
function
|
|
29
|
+
function readConfigFile(filePath) {
|
|
30
30
|
try {
|
|
31
31
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
32
|
-
|
|
33
|
-
const key = typeof parsed.apiKey === 'string' ? parsed.apiKey.trim() : '';
|
|
34
|
-
return key.length > 0 ? key : undefined;
|
|
32
|
+
return JSON.parse(raw);
|
|
35
33
|
}
|
|
36
34
|
catch {
|
|
37
35
|
return undefined;
|
|
38
36
|
}
|
|
39
37
|
}
|
|
38
|
+
function readApiKeyFromFile(filePath) {
|
|
39
|
+
const parsed = readConfigFile(filePath);
|
|
40
|
+
if (!parsed)
|
|
41
|
+
return undefined;
|
|
42
|
+
const key = typeof parsed.apiKey === 'string' ? parsed.apiKey.trim() : '';
|
|
43
|
+
return key.length > 0 ? key : undefined;
|
|
44
|
+
}
|
|
45
|
+
function readProjectIdFromFile(filePath) {
|
|
46
|
+
const parsed = readConfigFile(filePath);
|
|
47
|
+
if (!parsed)
|
|
48
|
+
return undefined;
|
|
49
|
+
const id = typeof parsed.projectId === 'string' ? parsed.projectId.trim() : '';
|
|
50
|
+
return id.length > 0 ? id : undefined;
|
|
51
|
+
}
|
|
52
|
+
function readFormatFromFile(filePath) {
|
|
53
|
+
const parsed = readConfigFile(filePath);
|
|
54
|
+
if (!parsed)
|
|
55
|
+
return undefined;
|
|
56
|
+
const fmt = parsed.format;
|
|
57
|
+
if (fmt === 'html' || fmt === 'markdown')
|
|
58
|
+
return fmt;
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
export function resolveFormat(options) {
|
|
62
|
+
if (options.flagHtml)
|
|
63
|
+
return 'html';
|
|
64
|
+
const envVal = process.env.ZURF_HTML?.trim().toLowerCase();
|
|
65
|
+
if (envVal === 'true' || envVal === '1')
|
|
66
|
+
return 'html';
|
|
67
|
+
const cwd = options.cwd ?? process.cwd();
|
|
68
|
+
const localPath = findLocalConfigPath(cwd);
|
|
69
|
+
if (localPath) {
|
|
70
|
+
const fmt = readFormatFromFile(localPath);
|
|
71
|
+
if (fmt)
|
|
72
|
+
return fmt;
|
|
73
|
+
}
|
|
74
|
+
const gPath = globalConfigFilePath(options.globalConfigDir);
|
|
75
|
+
const globalFmt = readFormatFromFile(gPath);
|
|
76
|
+
if (globalFmt)
|
|
77
|
+
return globalFmt;
|
|
78
|
+
return 'markdown';
|
|
79
|
+
}
|
|
40
80
|
export function resolveApiKey(options) {
|
|
41
81
|
const cwd = options.cwd ?? process.cwd();
|
|
42
82
|
const envKey = process.env.BROWSERBASE_API_KEY?.trim();
|
|
@@ -57,10 +97,41 @@ export function resolveApiKey(options) {
|
|
|
57
97
|
}
|
|
58
98
|
return { source: 'none' };
|
|
59
99
|
}
|
|
100
|
+
export function resolveProjectId(options) {
|
|
101
|
+
const cwd = options.cwd ?? process.cwd();
|
|
102
|
+
const envId = process.env.BROWSERBASE_PROJECT_ID?.trim();
|
|
103
|
+
if (envId) {
|
|
104
|
+
return { projectId: envId, source: 'env' };
|
|
105
|
+
}
|
|
106
|
+
const localPath = findLocalConfigPath(cwd);
|
|
107
|
+
if (localPath) {
|
|
108
|
+
const id = readProjectIdFromFile(localPath);
|
|
109
|
+
if (id) {
|
|
110
|
+
return { path: localPath, projectId: id, source: 'local' };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const gPath = globalConfigFilePath(options.globalConfigDir);
|
|
114
|
+
const globalId = readProjectIdFromFile(gPath);
|
|
115
|
+
if (globalId) {
|
|
116
|
+
return { path: gPath, projectId: globalId, source: 'global' };
|
|
117
|
+
}
|
|
118
|
+
return { source: 'none' };
|
|
119
|
+
}
|
|
60
120
|
export async function writeApiKeyConfig(targetPath, apiKey) {
|
|
121
|
+
await writeConfig(targetPath, { apiKey: apiKey.trim() });
|
|
122
|
+
}
|
|
123
|
+
export async function writeConfig(targetPath, fields) {
|
|
61
124
|
const dir = path.dirname(targetPath);
|
|
62
125
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
63
|
-
|
|
64
|
-
|
|
126
|
+
let existing = {};
|
|
127
|
+
try {
|
|
128
|
+
const raw = await fs.promises.readFile(targetPath, 'utf8');
|
|
129
|
+
existing = JSON.parse(raw);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// file doesn't exist yet — start fresh
|
|
133
|
+
}
|
|
134
|
+
const merged = { ...existing, ...fields };
|
|
135
|
+
const body = `${JSON.stringify(merged, null, 2)}\n`;
|
|
65
136
|
await fs.promises.writeFile(targetPath, body, { encoding: 'utf8', mode: 0o600 });
|
|
66
137
|
}
|
|
@@ -6,10 +6,11 @@ export type FetchResponseForDisplay = {
|
|
|
6
6
|
id: string;
|
|
7
7
|
statusCode: number;
|
|
8
8
|
};
|
|
9
|
-
export declare function buildFetchJsonPayload(response: FetchResponseForDisplay): {
|
|
9
|
+
export declare function buildFetchJsonPayload(response: FetchResponseForDisplay, format: 'html' | 'markdown'): {
|
|
10
10
|
content: string;
|
|
11
11
|
contentType: string;
|
|
12
12
|
encoding: string;
|
|
13
|
+
format: 'html' | 'markdown';
|
|
13
14
|
headers: Record<string, string>;
|
|
14
15
|
id: string;
|
|
15
16
|
statusCode: number;
|
package/dist/lib/fetch-output.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export function buildFetchJsonPayload(response) {
|
|
1
|
+
export function buildFetchJsonPayload(response, format) {
|
|
2
2
|
return {
|
|
3
3
|
content: response.content,
|
|
4
4
|
contentType: response.contentType,
|
|
5
5
|
encoding: response.encoding,
|
|
6
|
+
format,
|
|
6
7
|
headers: response.headers,
|
|
7
8
|
id: response.id,
|
|
8
9
|
statusCode: response.statusCode,
|
|
@@ -19,5 +20,5 @@ export function humanFetchMetaLines(response) {
|
|
|
19
20
|
/** ~8k chars keeps terminal scrollback usable while showing most HTML pages in preview. */
|
|
20
21
|
export const HUMAN_BODY_PREVIEW_CHARS = 8000;
|
|
21
22
|
export function truncateNote(totalChars) {
|
|
22
|
-
return `… truncated (${totalChars} chars). Use --output FILE to save the full
|
|
23
|
+
return `… truncated (${totalChars} chars). Use --output FILE to save the full content (within the 1 MB Fetch limit).`;
|
|
23
24
|
}
|
package/dist/lib/flags.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** Machine-readable output; kept separate from oclif `enableJsonFlag` so error payloads match zurf's stdout JSON contract. */
|
|
2
2
|
export declare const zurfJsonFlag: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
3
|
+
/** Output raw HTML instead of the default markdown conversion. */
|
|
4
|
+
export declare const zurfHtmlFlag: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
3
5
|
/** Shared flags for commands that support JSON output (no `--api-key` — use BROWSERBASE_API_KEY or config files). */
|
|
4
6
|
export declare const zurfBaseFlags: {
|
|
7
|
+
readonly html: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
5
8
|
readonly json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
9
|
};
|
package/dist/lib/flags.js
CHANGED
|
@@ -4,7 +4,14 @@ export const zurfJsonFlag = Flags.boolean({
|
|
|
4
4
|
description: 'Print machine-readable JSON to stdout',
|
|
5
5
|
env: 'ZURF_JSON',
|
|
6
6
|
});
|
|
7
|
+
/** Output raw HTML instead of the default markdown conversion. */
|
|
8
|
+
export const zurfHtmlFlag = Flags.boolean({
|
|
9
|
+
default: false,
|
|
10
|
+
description: 'Output raw HTML instead of markdown',
|
|
11
|
+
env: 'ZURF_HTML',
|
|
12
|
+
});
|
|
7
13
|
/** Shared flags for commands that support JSON output (no `--api-key` — use BROWSERBASE_API_KEY or config files). */
|
|
8
14
|
export const zurfBaseFlags = {
|
|
15
|
+
html: zurfHtmlFlag,
|
|
9
16
|
json: zurfJsonFlag,
|
|
10
17
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function htmlToMarkdown(html: string): Promise<string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
let cached;
|
|
2
|
+
async function getService() {
|
|
3
|
+
if (cached)
|
|
4
|
+
return cached;
|
|
5
|
+
const { default: Turndown } = await import('turndown');
|
|
6
|
+
const service = new Turndown({
|
|
7
|
+
codeBlockStyle: 'fenced',
|
|
8
|
+
headingStyle: 'atx',
|
|
9
|
+
});
|
|
10
|
+
service.remove(['script', 'style', 'iframe', 'noscript']);
|
|
11
|
+
service.addRule('remove-svg', {
|
|
12
|
+
filter: (node) => node.nodeName.toLowerCase() === 'svg',
|
|
13
|
+
replacement: () => '',
|
|
14
|
+
});
|
|
15
|
+
cached = service;
|
|
16
|
+
return service;
|
|
17
|
+
}
|
|
18
|
+
export async function htmlToMarkdown(html) {
|
|
19
|
+
const service = await getService();
|
|
20
|
+
return service.turndown(html);
|
|
21
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -1,115 +1,120 @@
|
|
|
1
1
|
{
|
|
2
2
|
"commands": {
|
|
3
|
-
"
|
|
3
|
+
"browse": {
|
|
4
4
|
"aliases": [],
|
|
5
|
-
"args": {
|
|
6
|
-
|
|
5
|
+
"args": {
|
|
6
|
+
"url": {
|
|
7
|
+
"description": "URL to browse",
|
|
8
|
+
"name": "url",
|
|
9
|
+
"required": true
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"description": "Browse a URL in a cloud browser and return the rendered content as markdown (default) or raw HTML.\nUses a real Chromium browser via Browserbase, so JavaScript-heavy pages are fully rendered.\nRequires authentication and a Project ID. Run `zurf init --global` before first use.",
|
|
7
13
|
"examples": [
|
|
8
|
-
"<%= config.bin %>
|
|
9
|
-
"<%= config.bin %>
|
|
14
|
+
"<%= config.bin %> <%= command.id %> https://example.com",
|
|
15
|
+
"<%= config.bin %> <%= command.id %> https://example.com --html",
|
|
16
|
+
"<%= config.bin %> <%= command.id %> https://example.com --json",
|
|
17
|
+
"<%= config.bin %> <%= command.id %> https://example.com -o page.md"
|
|
10
18
|
],
|
|
11
19
|
"flags": {
|
|
20
|
+
"html": {
|
|
21
|
+
"description": "Output raw HTML instead of markdown",
|
|
22
|
+
"env": "ZURF_HTML",
|
|
23
|
+
"name": "html",
|
|
24
|
+
"allowNo": false,
|
|
25
|
+
"type": "boolean"
|
|
26
|
+
},
|
|
12
27
|
"json": {
|
|
13
28
|
"description": "Print machine-readable JSON to stdout",
|
|
14
29
|
"env": "ZURF_JSON",
|
|
15
30
|
"name": "json",
|
|
16
31
|
"allowNo": false,
|
|
17
32
|
"type": "boolean"
|
|
33
|
+
},
|
|
34
|
+
"output": {
|
|
35
|
+
"char": "o",
|
|
36
|
+
"description": "Write rendered content to this file (full content); otherwise human mode prints a truncated preview to stdout",
|
|
37
|
+
"name": "output",
|
|
38
|
+
"hasDynamicHelp": false,
|
|
39
|
+
"multiple": false,
|
|
40
|
+
"type": "option"
|
|
18
41
|
}
|
|
19
42
|
},
|
|
20
43
|
"hasDynamicHelp": false,
|
|
21
44
|
"hiddenAliases": [],
|
|
22
|
-
"id": "
|
|
45
|
+
"id": "browse",
|
|
23
46
|
"pluginAlias": "@vibemastery/zurf",
|
|
24
47
|
"pluginName": "@vibemastery/zurf",
|
|
25
48
|
"pluginType": "core",
|
|
26
49
|
"strict": true,
|
|
27
|
-
"summary": "
|
|
28
|
-
"enableJsonFlag": false,
|
|
50
|
+
"summary": "Browse a URL in a cloud browser and return rendered content as markdown",
|
|
29
51
|
"isESM": true,
|
|
30
52
|
"relativePath": [
|
|
31
53
|
"dist",
|
|
32
54
|
"commands",
|
|
33
|
-
"
|
|
34
|
-
"
|
|
55
|
+
"browse",
|
|
56
|
+
"index.js"
|
|
35
57
|
]
|
|
36
58
|
},
|
|
37
|
-
"
|
|
59
|
+
"config:which": {
|
|
38
60
|
"aliases": [],
|
|
39
|
-
"args": {
|
|
40
|
-
|
|
41
|
-
"description": "URL to fetch",
|
|
42
|
-
"name": "url",
|
|
43
|
-
"required": true
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"description": "Fetch a URL via Browserbase (no browser session; static HTML, 1 MB max).\nRequires authentication. Run `zurf init --global` or use a project key before first use.",
|
|
61
|
+
"args": {},
|
|
62
|
+
"description": "Show where the Browserbase API key would be loaded from (no secret printed).\nResolution order: BROWSERBASE_API_KEY, then project .zurf/config.json (walk-up), then global config in the CLI config directory.",
|
|
47
63
|
"examples": [
|
|
48
|
-
"<%= config.bin %>
|
|
49
|
-
"<%= config.bin %>
|
|
50
|
-
"<%= config.bin %> <%= command.id %> https://example.com -o page.html --proxies"
|
|
64
|
+
"<%= config.bin %> config which",
|
|
65
|
+
"<%= config.bin %> config which --json"
|
|
51
66
|
],
|
|
52
67
|
"flags": {
|
|
68
|
+
"html": {
|
|
69
|
+
"description": "Output raw HTML instead of markdown",
|
|
70
|
+
"env": "ZURF_HTML",
|
|
71
|
+
"name": "html",
|
|
72
|
+
"allowNo": false,
|
|
73
|
+
"type": "boolean"
|
|
74
|
+
},
|
|
53
75
|
"json": {
|
|
54
76
|
"description": "Print machine-readable JSON to stdout",
|
|
55
77
|
"env": "ZURF_JSON",
|
|
56
78
|
"name": "json",
|
|
57
79
|
"allowNo": false,
|
|
58
80
|
"type": "boolean"
|
|
59
|
-
},
|
|
60
|
-
"allow-insecure-ssl": {
|
|
61
|
-
"description": "Disable TLS certificate verification (use only if you trust the target)",
|
|
62
|
-
"name": "allow-insecure-ssl",
|
|
63
|
-
"allowNo": false,
|
|
64
|
-
"type": "boolean"
|
|
65
|
-
},
|
|
66
|
-
"allow-redirects": {
|
|
67
|
-
"description": "Follow HTTP redirects",
|
|
68
|
-
"name": "allow-redirects",
|
|
69
|
-
"allowNo": false,
|
|
70
|
-
"type": "boolean"
|
|
71
|
-
},
|
|
72
|
-
"output": {
|
|
73
|
-
"char": "o",
|
|
74
|
-
"description": "Write response body to this file (full content); otherwise human mode prints a truncated preview to stdout",
|
|
75
|
-
"name": "output",
|
|
76
|
-
"hasDynamicHelp": false,
|
|
77
|
-
"multiple": false,
|
|
78
|
-
"type": "option"
|
|
79
|
-
},
|
|
80
|
-
"proxies": {
|
|
81
|
-
"description": "Route through Browserbase proxies (helps with some blocked sites)",
|
|
82
|
-
"name": "proxies",
|
|
83
|
-
"allowNo": false,
|
|
84
|
-
"type": "boolean"
|
|
85
81
|
}
|
|
86
82
|
},
|
|
87
83
|
"hasDynamicHelp": false,
|
|
88
84
|
"hiddenAliases": [],
|
|
89
|
-
"id": "
|
|
85
|
+
"id": "config:which",
|
|
90
86
|
"pluginAlias": "@vibemastery/zurf",
|
|
91
87
|
"pluginName": "@vibemastery/zurf",
|
|
92
88
|
"pluginType": "core",
|
|
93
89
|
"strict": true,
|
|
94
|
-
"summary": "
|
|
90
|
+
"summary": "Show where the API key is loaded from",
|
|
91
|
+
"enableJsonFlag": false,
|
|
95
92
|
"isESM": true,
|
|
96
93
|
"relativePath": [
|
|
97
94
|
"dist",
|
|
98
95
|
"commands",
|
|
99
|
-
"
|
|
100
|
-
"
|
|
96
|
+
"config",
|
|
97
|
+
"which.js"
|
|
101
98
|
]
|
|
102
99
|
},
|
|
103
100
|
"init": {
|
|
104
101
|
"aliases": [],
|
|
105
102
|
"args": {},
|
|
106
|
-
"description": "Save your Browserbase API key to global or project config.\nGlobal path follows oclif config (same as `zurf config which`).",
|
|
103
|
+
"description": "Save your Browserbase API key and optional Project ID to global or project config.\nGlobal path follows oclif config (same as `zurf config which`).",
|
|
107
104
|
"examples": [
|
|
108
105
|
"<%= config.bin %> <%= command.id %> --global",
|
|
109
106
|
"<%= config.bin %> <%= command.id %> --local",
|
|
107
|
+
"<%= config.bin %> <%= command.id %> --global --api-key KEY --project-id PROJ_ID",
|
|
110
108
|
"printenv BROWSERBASE_API_KEY | <%= config.bin %> <%= command.id %> --global"
|
|
111
109
|
],
|
|
112
110
|
"flags": {
|
|
111
|
+
"html": {
|
|
112
|
+
"description": "Output raw HTML instead of markdown",
|
|
113
|
+
"env": "ZURF_HTML",
|
|
114
|
+
"name": "html",
|
|
115
|
+
"allowNo": false,
|
|
116
|
+
"type": "boolean"
|
|
117
|
+
},
|
|
113
118
|
"json": {
|
|
114
119
|
"description": "Print machine-readable JSON to stdout",
|
|
115
120
|
"env": "ZURF_JSON",
|
|
@@ -141,6 +146,13 @@
|
|
|
141
146
|
"name": "local",
|
|
142
147
|
"allowNo": false,
|
|
143
148
|
"type": "boolean"
|
|
149
|
+
},
|
|
150
|
+
"project-id": {
|
|
151
|
+
"description": "Browserbase Project ID (optional; needed for browse, screenshot, pdf commands)",
|
|
152
|
+
"name": "project-id",
|
|
153
|
+
"hasDynamicHelp": false,
|
|
154
|
+
"multiple": false,
|
|
155
|
+
"type": "option"
|
|
144
156
|
}
|
|
145
157
|
},
|
|
146
158
|
"hasDynamicHelp": false,
|
|
@@ -150,7 +162,7 @@
|
|
|
150
162
|
"pluginName": "@vibemastery/zurf",
|
|
151
163
|
"pluginType": "core",
|
|
152
164
|
"strict": true,
|
|
153
|
-
"summary": "Configure Browserbase API key storage",
|
|
165
|
+
"summary": "Configure Browserbase API key and Project ID storage",
|
|
154
166
|
"enableJsonFlag": false,
|
|
155
167
|
"isESM": true,
|
|
156
168
|
"relativePath": [
|
|
@@ -160,6 +172,80 @@
|
|
|
160
172
|
"index.js"
|
|
161
173
|
]
|
|
162
174
|
},
|
|
175
|
+
"fetch": {
|
|
176
|
+
"aliases": [],
|
|
177
|
+
"args": {
|
|
178
|
+
"url": {
|
|
179
|
+
"description": "URL to fetch",
|
|
180
|
+
"name": "url",
|
|
181
|
+
"required": true
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
"description": "Fetch a URL via Browserbase and return content as markdown (default) or raw HTML (no browser session; static HTML, 1 MB max).\nRequires authentication. Run `zurf init --global` or use a project key before first use.",
|
|
185
|
+
"examples": [
|
|
186
|
+
"<%= config.bin %> <%= command.id %> https://example.com",
|
|
187
|
+
"<%= config.bin %> <%= command.id %> https://example.com --html",
|
|
188
|
+
"<%= config.bin %> <%= command.id %> https://example.com --json",
|
|
189
|
+
"<%= config.bin %> <%= command.id %> https://example.com -o page.md --proxies"
|
|
190
|
+
],
|
|
191
|
+
"flags": {
|
|
192
|
+
"html": {
|
|
193
|
+
"description": "Output raw HTML instead of markdown",
|
|
194
|
+
"env": "ZURF_HTML",
|
|
195
|
+
"name": "html",
|
|
196
|
+
"allowNo": false,
|
|
197
|
+
"type": "boolean"
|
|
198
|
+
},
|
|
199
|
+
"json": {
|
|
200
|
+
"description": "Print machine-readable JSON to stdout",
|
|
201
|
+
"env": "ZURF_JSON",
|
|
202
|
+
"name": "json",
|
|
203
|
+
"allowNo": false,
|
|
204
|
+
"type": "boolean"
|
|
205
|
+
},
|
|
206
|
+
"allow-insecure-ssl": {
|
|
207
|
+
"description": "Disable TLS certificate verification (use only if you trust the target)",
|
|
208
|
+
"name": "allow-insecure-ssl",
|
|
209
|
+
"allowNo": false,
|
|
210
|
+
"type": "boolean"
|
|
211
|
+
},
|
|
212
|
+
"allow-redirects": {
|
|
213
|
+
"description": "Follow HTTP redirects",
|
|
214
|
+
"name": "allow-redirects",
|
|
215
|
+
"allowNo": false,
|
|
216
|
+
"type": "boolean"
|
|
217
|
+
},
|
|
218
|
+
"output": {
|
|
219
|
+
"char": "o",
|
|
220
|
+
"description": "Write response body to this file (full content); otherwise human mode prints a truncated preview to stdout",
|
|
221
|
+
"name": "output",
|
|
222
|
+
"hasDynamicHelp": false,
|
|
223
|
+
"multiple": false,
|
|
224
|
+
"type": "option"
|
|
225
|
+
},
|
|
226
|
+
"proxies": {
|
|
227
|
+
"description": "Route through Browserbase proxies (helps with some blocked sites)",
|
|
228
|
+
"name": "proxies",
|
|
229
|
+
"allowNo": false,
|
|
230
|
+
"type": "boolean"
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
"hasDynamicHelp": false,
|
|
234
|
+
"hiddenAliases": [],
|
|
235
|
+
"id": "fetch",
|
|
236
|
+
"pluginAlias": "@vibemastery/zurf",
|
|
237
|
+
"pluginName": "@vibemastery/zurf",
|
|
238
|
+
"pluginType": "core",
|
|
239
|
+
"strict": true,
|
|
240
|
+
"summary": "Fetch a URL via Browserbase and return content as markdown",
|
|
241
|
+
"isESM": true,
|
|
242
|
+
"relativePath": [
|
|
243
|
+
"dist",
|
|
244
|
+
"commands",
|
|
245
|
+
"fetch",
|
|
246
|
+
"index.js"
|
|
247
|
+
]
|
|
248
|
+
},
|
|
163
249
|
"search": {
|
|
164
250
|
"aliases": [],
|
|
165
251
|
"args": {
|
|
@@ -175,6 +261,13 @@
|
|
|
175
261
|
"<%= config.bin %> <%= command.id %> \"laravel inertia\" --num-results 5 --json"
|
|
176
262
|
],
|
|
177
263
|
"flags": {
|
|
264
|
+
"html": {
|
|
265
|
+
"description": "Output raw HTML instead of markdown",
|
|
266
|
+
"env": "ZURF_HTML",
|
|
267
|
+
"name": "html",
|
|
268
|
+
"allowNo": false,
|
|
269
|
+
"type": "boolean"
|
|
270
|
+
},
|
|
178
271
|
"json": {
|
|
179
272
|
"description": "Print machine-readable JSON to stdout",
|
|
180
273
|
"env": "ZURF_JSON",
|
|
@@ -209,5 +302,5 @@
|
|
|
209
302
|
]
|
|
210
303
|
}
|
|
211
304
|
},
|
|
212
|
-
"version": "0.2.
|
|
305
|
+
"version": "0.2.3"
|
|
213
306
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibemastery/zurf",
|
|
3
3
|
"description": "A lightweight CLI for searching and fetching web pages, powered by Browserbase.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"author": "Luis Güette",
|
|
6
6
|
"bin": {
|
|
7
7
|
"zurf": "./bin/run.js"
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"@oclif/plugin-help": "^6",
|
|
15
15
|
"@oclif/plugin-not-found": "^3.2.77",
|
|
16
16
|
"@oclif/plugin-plugins": "^5",
|
|
17
|
-
"@oclif/plugin-warn-if-update-available": "^3.1.57"
|
|
17
|
+
"@oclif/plugin-warn-if-update-available": "^3.1.57",
|
|
18
|
+
"playwright-core": "^1.58.2",
|
|
19
|
+
"turndown": "^7.2.2"
|
|
18
20
|
},
|
|
19
21
|
"devDependencies": {
|
|
20
22
|
"@eslint/compat": "^1",
|
|
@@ -23,6 +25,7 @@
|
|
|
23
25
|
"@types/chai": "^4",
|
|
24
26
|
"@types/mocha": "^10",
|
|
25
27
|
"@types/node": "^18",
|
|
28
|
+
"@types/turndown": "^5.0.6",
|
|
26
29
|
"chai": "^4",
|
|
27
30
|
"eslint": "^9",
|
|
28
31
|
"eslint-config-oclif": "^6",
|