@vibemastery/zurf 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,47 +1,194 @@
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 (Browserbase) and asking AI-powered questions with web citations (Perplexity Sonar).
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 setup # configure API keys (Browserbase, Perplexity)
16
+ $ zurf --help
17
+ ```
18
+
19
+ ## Commands
20
+
21
+ ### `zurf ask <question>`
22
+
23
+ Ask a question and get an AI-powered answer with web citations via Perplexity Sonar. Use `--depth deep` for more thorough research (sonar-pro).
24
+
25
+ ```sh-session
26
+ $ zurf ask "What is Browserbase?"
27
+ $ zurf ask "latest tech news" --recency day
28
+ $ zurf ask "search reddit for best CLI tools" --domains reddit.com
29
+ $ zurf ask "explain quantum computing" --depth deep
30
+ $ zurf ask "What is Node.js?" --json
31
+ $ zurf ask "What is oclif?" --no-citations
32
+ ```
33
+
34
+ | Flag | Description |
35
+ |------|-------------|
36
+ | `--depth <quick\|deep>` | Search depth: quick (sonar) or deep (sonar-pro). Default: quick |
37
+ | `--recency <hour\|day\|week\|month\|year>` | Filter sources by recency |
38
+ | `--domains <list>` | Restrict search to these domains (comma-separated) |
39
+ | `--no-citations` | Hide the sources list after the answer |
40
+ | `--json` | Print machine-readable JSON to stdout |
41
+
42
+ ### `zurf search <query>`
43
+
44
+ Search the web via Browserbase (Exa-powered). Returns a list of matching URLs with titles and snippets.
45
+
46
+ ```sh-session
47
+ $ zurf search "browserbase documentation"
48
+ $ zurf search "laravel inertia" --num-results 5 --json
49
+ ```
50
+
51
+ | Flag | Description |
52
+ |------|-------------|
53
+ | `-n, --num-results` | Number of results, 1-25 (default: 10) |
54
+ | `--json` | Print machine-readable JSON to stdout |
55
+
56
+ ### `zurf browse <url>`
57
+
58
+ 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.
59
+
60
+ Best for JavaScript-heavy pages (SPAs, dashboards, pages behind client-side rendering).
61
+
62
+ ```sh-session
63
+ $ zurf browse https://example.com # markdown output
64
+ $ zurf browse https://example.com --html # raw HTML output
65
+ $ zurf browse https://example.com -o page.md # save full content to file
66
+ $ zurf browse https://example.com --json # JSON with content + metadata
67
+ ```
68
+
69
+ | Flag | Description |
70
+ |------|-------------|
71
+ | `--html` | Output raw HTML instead of markdown |
72
+ | `-o, --output` | Write full content to a file |
73
+ | `--json` | Print machine-readable JSON to stdout |
74
+
75
+ ### `zurf fetch <url>`
76
+
77
+ 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.
78
+
79
+ ```sh-session
80
+ $ zurf fetch https://example.com # markdown output
81
+ $ zurf fetch https://example.com --html # raw HTML output
82
+ $ zurf fetch https://example.com -o page.md # save full content to file
83
+ $ zurf fetch https://example.com --proxies # route through Browserbase proxies
84
+ $ zurf fetch https://example.com --json # JSON with content + metadata
85
+ ```
86
+
87
+ | Flag | Description |
88
+ |------|-------------|
89
+ | `--html` | Output raw HTML instead of markdown |
90
+ | `-o, --output` | Write full content to a file |
91
+ | `--proxies` | Route through Browserbase proxies |
92
+ | `--allow-redirects` | Follow HTTP redirects |
93
+ | `--allow-insecure-ssl` | Disable TLS certificate verification |
94
+ | `--json` | Print machine-readable JSON to stdout |
95
+
96
+ ### `zurf setup`
97
+
98
+ Interactive wizard to configure API keys for all providers (Browserbase, Perplexity). Stores keys in global or local config. Re-run to update or add providers.
99
+
100
+ ```sh-session
101
+ $ zurf setup # interactive wizard
102
+ $ zurf setup --global # skip scope prompt, save to global config
103
+ $ zurf setup --local # skip scope prompt, save to project .zurf/config.json
104
+ ```
105
+
106
+ ### `zurf config which`
107
+
108
+ Show where your API keys would be loaded from (nothing secret is printed). Shows resolution for both Browserbase and Perplexity.
109
+
110
+ ```sh-session
111
+ $ zurf config which
112
+ $ zurf config which --json
113
+ ```
114
+
115
+ ## Output format
116
+
117
+ `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.
118
+
119
+ You can also set the default in `.zurf/config.json` or the global config:
120
+
121
+ ```json
122
+ { "format": "html" }
123
+ ```
124
+
125
+ Format resolution (highest precedence first):
126
+
127
+ 1. `--html` flag
128
+ 2. `ZURF_HTML` environment variable (`true` or `1`)
129
+ 3. Local `.zurf/config.json` `format` field
130
+ 4. Global config `format` field
131
+ 5. Default: `markdown`
132
+
11
133
  ## Configuration
12
134
 
13
- API key resolution for `zurf search` and `zurf fetch` (highest precedence first):
135
+ ### Config file structure (v0.3.0+)
136
+
137
+ ```json
138
+ {
139
+ "providers": {
140
+ "browserbase": {
141
+ "apiKey": "bb_...",
142
+ "projectId": "proj_..."
143
+ },
144
+ "perplexity": {
145
+ "apiKey": "pplx-..."
146
+ }
147
+ },
148
+ "format": "markdown"
149
+ }
150
+ ```
151
+
152
+ Old flat configs (v0.2.x) are auto-migrated when read — no manual action needed.
153
+
154
+ ### API key resolution
155
+
156
+ Each provider resolves its key independently (highest precedence first):
157
+
158
+ **Browserbase:**
159
+ 1. Environment variable `BROWSERBASE_API_KEY`
160
+ 2. Nearest `.zurf/config.json` → `providers.browserbase.apiKey`
161
+ 3. Global config → `providers.browserbase.apiKey`
14
162
 
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`)
163
+ **Perplexity:**
164
+ 1. Environment variable `PERPLEXITY_API_KEY`
165
+ 2. Nearest `.zurf/config.json` `providers.perplexity.apiKey`
166
+ 3. Global config `providers.perplexity.apiKey`
19
167
 
20
- Save a key interactively or with `--api-key`:
168
+ Save keys interactively:
21
169
 
22
170
  ```sh-session
23
- $ zurf init --global
24
- $ zurf init --local
171
+ $ zurf setup
25
172
  ```
26
173
 
27
- For project-local storage, add `.zurf/` to `.gitignore` so the key is never committed. You can run `zurf init --local --gitignore` to append a `.zurf/` entry automatically.
174
+ For project-local storage, add `.zurf/` to `.gitignore` so keys are never committed. `zurf setup --local` will offer to do this automatically.
28
175
 
29
- **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`.
176
+ **Security note:** Keys in `config.json` are stored as plaintext with file mode `0o600`. For shared machines or stricter setups, prefer environment variables from a secrets manager.
30
177
 
31
- See where a key would be loaded from (nothing secret is printed): `zurf config which`.
178
+ ### Migration from v0.2.x
179
+
180
+ - Config files auto-migrate from the old flat shape (`{ "apiKey": "..." }`) to the new nested shape. No manual changes needed.
181
+ - `zurf init` has been replaced by `zurf setup`. The setup wizard supports multiple providers.
32
182
 
33
183
  ## Claude Code and agents
34
184
 
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:
185
+ Install `zurf` on your `PATH` and allow the agent to run shell commands. Use `--json` when you want structured output:
36
186
 
37
187
  ```sh-session
38
188
  $ zurf search "browserbase fetch api" --json
189
+ $ zurf browse https://example.com --json
39
190
  $ zurf fetch https://example.com --json
191
+ $ zurf ask "What is Browserbase?" --json
40
192
  ```
41
193
 
42
- ## Installation
43
-
44
- ```sh-session
45
- $ npm install -g @vibemastery/zurf
46
- $ zurf --help
47
- ```
194
+ Content is returned as markdown by default, which keeps token counts low. Pass `--html` if the agent needs the raw DOM.
@@ -0,0 +1,17 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Ask extends Command {
3
+ static args: {
4
+ question: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ citations: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ depth: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ domains: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
13
+ recency: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
+ };
15
+ static summary: string;
16
+ run(): Promise<void>;
17
+ }
@@ -0,0 +1,102 @@
1
+ import { Args, Command, Flags, ux } from '@oclif/core';
2
+ import { cliError, errorMessage } from '../../lib/cli-errors.js';
3
+ import { zurfJsonFlag } from '../../lib/flags.js';
4
+ import { printJson } from '../../lib/json-output.js';
5
+ import { createPerplexityClient, MissingPerplexityKeyError, } from '../../lib/perplexity-client.js';
6
+ export default class Ask extends Command {
7
+ static args = {
8
+ question: Args.string({
9
+ description: 'The question to ask Perplexity',
10
+ required: true,
11
+ }),
12
+ };
13
+ static description = `Ask a question and get an AI-powered answer with web citations via Perplexity Sonar.
14
+ Returns an answer with inline citations and a sources list. Use --depth deep for more thorough research.`;
15
+ static examples = [
16
+ '<%= config.bin %> <%= command.id %> "What is Browserbase?"',
17
+ '<%= config.bin %> <%= command.id %> "latest tech news" --recency day',
18
+ '<%= config.bin %> <%= command.id %> "search reddit for best CLI tools" --domains reddit.com',
19
+ '<%= config.bin %> <%= command.id %> "explain quantum computing" --depth deep',
20
+ '<%= config.bin %> <%= command.id %> "What is Node.js?" --json',
21
+ '<%= config.bin %> <%= command.id %> "What is oclif?" --no-citations',
22
+ ];
23
+ static flags = {
24
+ citations: Flags.boolean({
25
+ allowNo: true,
26
+ default: true,
27
+ description: 'Show sources list after the answer (use --no-citations to hide)',
28
+ }),
29
+ depth: Flags.string({
30
+ default: 'quick',
31
+ description: 'Search depth: quick (sonar) or deep (sonar-pro)',
32
+ options: ['quick', 'deep'],
33
+ }),
34
+ domains: Flags.string({
35
+ description: 'Restrict search to these domains (comma-separated)',
36
+ }),
37
+ json: zurfJsonFlag,
38
+ recency: Flags.string({
39
+ description: 'Filter sources by recency',
40
+ options: ['hour', 'day', 'week', 'month', 'year'],
41
+ }),
42
+ };
43
+ static summary = 'Ask a question via Perplexity Sonar';
44
+ async run() {
45
+ const { args, flags } = await this.parse(Ask);
46
+ const question = args.question.trim();
47
+ if (question.length === 0) {
48
+ cliError({ command: this, exitCode: 2, json: flags.json, message: 'Question must not be empty.' });
49
+ }
50
+ let client;
51
+ try {
52
+ ;
53
+ ({ client } = createPerplexityClient({ globalConfigDir: this.config.configDir }));
54
+ }
55
+ catch (error) {
56
+ if (error instanceof MissingPerplexityKeyError) {
57
+ cliError({ command: this, exitCode: 1, json: flags.json, message: error.message });
58
+ }
59
+ throw error;
60
+ }
61
+ const domains = flags.domains?.split(',').map((d) => d.trim()).filter(Boolean);
62
+ const doWork = async () => {
63
+ const result = await client.ask({
64
+ depth: flags.depth,
65
+ domains,
66
+ question,
67
+ recency: flags.recency,
68
+ });
69
+ if (flags.json) {
70
+ printJson({ answer: result.answer, citations: result.citations, model: result.model, query: question });
71
+ return;
72
+ }
73
+ this.log(result.answer);
74
+ if (flags.citations && result.citations.length > 0) {
75
+ this.log('');
76
+ this.log('Sources:');
77
+ for (const [i, url] of result.citations.entries()) {
78
+ this.log(`[${i + 1}] ${url}`);
79
+ }
80
+ }
81
+ };
82
+ if (flags.json) {
83
+ try {
84
+ await doWork();
85
+ }
86
+ catch (error) {
87
+ cliError({ command: this, exitCode: 1, json: flags.json, message: errorMessage(error) });
88
+ }
89
+ return;
90
+ }
91
+ ux.action.start('Asking Perplexity');
92
+ try {
93
+ await doWork();
94
+ }
95
+ catch (error) {
96
+ cliError({ command: this, exitCode: 1, json: flags.json, message: errorMessage(error) });
97
+ }
98
+ finally {
99
+ ux.action.stop();
100
+ }
101
+ }
102
+ }
@@ -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 setup\` 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 setup` 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;
@@ -1,61 +1,68 @@
1
1
  import { Command } from '@oclif/core';
2
- import { globalConfigFilePath, resolveApiKey } from '../../lib/config.js';
2
+ import { globalConfigFilePath, resolveApiKey, resolvePerplexityApiKey } from '../../lib/config.js';
3
3
  import { zurfBaseFlags } from '../../lib/flags.js';
4
4
  import { printJson } from '../../lib/json-output.js';
5
+ function resolvedSource(resolved) {
6
+ switch (resolved.source) {
7
+ case 'env': {
8
+ return { source: 'env' };
9
+ }
10
+ case 'global':
11
+ case 'local': {
12
+ return { path: resolved.path, source: resolved.source };
13
+ }
14
+ default: {
15
+ return { source: 'none' };
16
+ }
17
+ }
18
+ }
19
+ function humanSourceLine(label, resolved, envVarName) {
20
+ switch (resolved.source) {
21
+ case 'env': {
22
+ return `${label}: environment variable ${envVarName}`;
23
+ }
24
+ case 'global': {
25
+ return `${label}: global file ${resolved.path}`;
26
+ }
27
+ case 'local': {
28
+ return `${label}: local file ${resolved.path}`;
29
+ }
30
+ default: {
31
+ return `${label}: not configured`;
32
+ }
33
+ }
34
+ }
5
35
  export default class ConfigWhich extends Command {
6
- static description = `Show where the Browserbase API key would be loaded from (no secret printed).
7
- Resolution order: BROWSERBASE_API_KEY, then project .zurf/config.json (walk-up), then global config in the CLI config directory.`;
36
+ static description = `Show where API keys would be loaded from (no secrets printed).
37
+ Resolution order: env var project .zurf/config.json (walk-up) global config.`;
8
38
  static examples = ['<%= config.bin %> config which', '<%= config.bin %> config which --json'];
9
39
  static flags = {
10
40
  ...zurfBaseFlags,
11
41
  };
12
- static summary = 'Show where the API key is loaded from';
42
+ static summary = 'Show where API keys are loaded from';
13
43
  async run() {
14
44
  const { flags } = await this.parse(ConfigWhich);
15
- const resolved = resolveApiKey({ globalConfigDir: this.config.configDir });
45
+ const bbResolved = resolveApiKey({ globalConfigDir: this.config.configDir });
46
+ const pplxResolved = resolvePerplexityApiKey({ globalConfigDir: this.config.configDir });
16
47
  if (flags.json) {
17
- switch (resolved.source) {
18
- case 'env': {
19
- printJson({ envVar: 'BROWSERBASE_API_KEY', source: 'env' });
20
- break;
21
- }
22
- case 'global': {
23
- printJson({ path: resolved.path, source: 'global' });
24
- break;
25
- }
26
- case 'local': {
27
- printJson({ path: resolved.path, source: 'local' });
28
- break;
29
- }
30
- case 'none': {
31
- printJson({
32
- globalConfigPath: globalConfigFilePath(this.config.configDir),
33
- hint: `Run \`${this.config.bin} init --global\` or \`${this.config.bin} init --local\`, or set BROWSERBASE_API_KEY.`,
34
- source: 'none',
35
- });
36
- this.exit(1);
37
- break;
38
- }
48
+ const payload = {
49
+ browserbase: resolvedSource(bbResolved),
50
+ perplexity: resolvedSource(pplxResolved),
51
+ };
52
+ if (bbResolved.source === 'none' && pplxResolved.source === 'none') {
53
+ payload.globalConfigPath = globalConfigFilePath(this.config.configDir);
54
+ payload.hint = `Run \`${this.config.bin} setup\` or set BROWSERBASE_API_KEY / PERPLEXITY_API_KEY.`;
55
+ }
56
+ printJson(payload);
57
+ if (bbResolved.source === 'none' && pplxResolved.source === 'none') {
58
+ this.exit(1);
39
59
  }
40
60
  return;
41
61
  }
42
- switch (resolved.source) {
43
- case 'env': {
44
- this.log('API key source: environment variable BROWSERBASE_API_KEY');
45
- break;
46
- }
47
- case 'global': {
48
- this.log(`API key source: global file ${resolved.path}`);
49
- break;
50
- }
51
- case 'local': {
52
- this.log(`API key source: local file ${resolved.path}`);
53
- break;
54
- }
55
- case 'none': {
56
- this.error(`No API key configured. Set BROWSERBASE_API_KEY or run \`${this.config.bin} init --global\` / \`--local\`.`);
57
- break;
58
- }
62
+ this.log(humanSourceLine('Browserbase API key', bbResolved, 'BROWSERBASE_API_KEY'));
63
+ this.log(humanSourceLine('Perplexity API key', pplxResolved, 'PERPLEXITY_API_KEY'));
64
+ if (bbResolved.source === 'none' && pplxResolved.source === 'none') {
65
+ this.error(`No API keys configured. Run \`${this.config.bin} setup\` or set environment variables.`);
59
66
  }
60
67
  }
61
68
  }
@@ -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;