@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 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
  [![oclif](https://img.shields.io/badge/cli-oclif-brightgreen.svg)](https://oclif.io)
8
8
  [![Version](https://img.shields.io/npm/v/@vibemastery/zurf.svg)](https://npmjs.org/package/@vibemastery/zurf)
9
9
  [![Downloads/week](https://img.shields.io/npm/dw/@vibemastery/zurf.svg)](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 for `zurf search` and `zurf fetch` (highest precedence first):
115
+ API key resolution (highest precedence first):
14
116
 
15
- 1. `--api-key` / `-k` on the command
16
- 2. Environment variable `BROWSERBASE_API_KEY`
17
- 3. Nearest `.zurf/config.json` when walking up from the current working directory
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 or with `--api-key`:
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 a single JSON object on stdout, for example:
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
- ## Installation
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.html --proxies',
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(response));
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, response.content, 'utf8');
85
+ await fs.writeFile(flags.output, content, 'utf8');
80
86
  }
81
87
  catch (error) {
82
- const code = error !== null &&
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 body to ${flags.output}`);
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, writeApiKeyConfig } from '../../lib/config.js';
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 writeApiKeyConfig(targetPath, apiKey);
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
- printJson({ ok: true, path: targetPath, scope: flags.global ? 'global' : 'local' });
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,7 @@
1
+ import type { Browserbase } from '@browserbasehq/sdk';
2
+ import type { Page } from 'playwright-core';
3
+ export declare function withBrowserbaseSession<T>(options: {
4
+ client: Browserbase;
5
+ projectId: string;
6
+ work: (page: Page) => Promise<T>;
7
+ }): Promise<T>;
@@ -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
+ }
@@ -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;
@@ -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' &&
@@ -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>;
@@ -26,17 +26,57 @@ export function findLocalConfigPath(startDir = process.cwd()) {
26
26
  }
27
27
  return undefined;
28
28
  }
29
- function readApiKeyFromFile(filePath) {
29
+ function readConfigFile(filePath) {
30
30
  try {
31
31
  const raw = fs.readFileSync(filePath, 'utf8');
32
- const parsed = JSON.parse(raw);
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
- const payload = { apiKey: apiKey.trim() };
64
- const body = `${JSON.stringify(payload, null, 2)}\n`;
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;
@@ -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 body (within the 1 MB Fetch limit).`;
23
+ return `… truncated (${totalChars} chars). Use --output FILE to save the full content (within the 1 MB Fetch limit).`;
23
24
  }
@@ -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
+ }
@@ -1,115 +1,120 @@
1
1
  {
2
2
  "commands": {
3
- "config:which": {
3
+ "browse": {
4
4
  "aliases": [],
5
- "args": {},
6
- "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.",
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 %> config which",
9
- "<%= config.bin %> config which --json"
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": "config:which",
45
+ "id": "browse",
23
46
  "pluginAlias": "@vibemastery/zurf",
24
47
  "pluginName": "@vibemastery/zurf",
25
48
  "pluginType": "core",
26
49
  "strict": true,
27
- "summary": "Show where the API key is loaded from",
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
- "config",
34
- "which.js"
55
+ "browse",
56
+ "index.js"
35
57
  ]
36
58
  },
37
- "fetch": {
59
+ "config:which": {
38
60
  "aliases": [],
39
- "args": {
40
- "url": {
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 %> <%= command.id %> https://example.com",
49
- "<%= config.bin %> <%= command.id %> https://example.com --json",
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": "fetch",
85
+ "id": "config:which",
90
86
  "pluginAlias": "@vibemastery/zurf",
91
87
  "pluginName": "@vibemastery/zurf",
92
88
  "pluginType": "core",
93
89
  "strict": true,
94
- "summary": "Fetch a URL via Browserbase",
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
- "fetch",
100
- "index.js"
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.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.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",