@vibemastery/zurf 0.2.3 → 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,7 +1,7 @@
1
1
  zurf
2
2
  =================
3
3
 
4
- A lightweight CLI for searching, browsing, 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)
@@ -12,12 +12,33 @@ A lightweight CLI for searching, browsing, and fetching web pages, powered by Br
12
12
 
13
13
  ```sh-session
14
14
  $ npm install -g @vibemastery/zurf
15
- $ zurf init --global # save your Browserbase API key
15
+ $ zurf setup # configure API keys (Browserbase, Perplexity)
16
16
  $ zurf --help
17
17
  ```
18
18
 
19
19
  ## Commands
20
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
+
21
42
  ### `zurf search <query>`
22
43
 
23
44
  Search the web via Browserbase (Exa-powered). Returns a list of matching URLs with titles and snippets.
@@ -72,20 +93,19 @@ $ zurf fetch https://example.com --json # JSON with content + metadata
72
93
  | `--allow-insecure-ssl` | Disable TLS certificate verification |
73
94
  | `--json` | Print machine-readable JSON to stdout |
74
95
 
75
- ### `zurf init`
96
+ ### `zurf setup`
76
97
 
77
- Save your Browserbase API key and optional Project ID to a config file.
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.
78
99
 
79
100
  ```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
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
84
104
  ```
85
105
 
86
106
  ### `zurf config which`
87
107
 
88
- Show where your API key and Project ID would be loaded from (nothing secret is printed).
108
+ Show where your API keys would be loaded from (nothing secret is printed). Shows resolution for both Browserbase and Perplexity.
89
109
 
90
110
  ```sh-session
91
111
  $ zurf config which
@@ -112,22 +132,53 @@ Format resolution (highest precedence first):
112
132
 
113
133
  ## Configuration
114
134
 
115
- API key resolution (highest precedence first):
135
+ ### Config file structure (v0.3.0+)
116
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:**
117
159
  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`)
160
+ 2. Nearest `.zurf/config.json` `providers.browserbase.apiKey`
161
+ 3. Global config `providers.browserbase.apiKey`
162
+
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`
120
167
 
121
- Save a key interactively:
168
+ Save keys interactively:
122
169
 
123
170
  ```sh-session
124
- $ zurf init --global
125
- $ zurf init --local
171
+ $ zurf setup
126
172
  ```
127
173
 
128
- 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.
175
+
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.
177
+
178
+ ### Migration from v0.2.x
129
179
 
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`.
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.
131
182
 
132
183
  ## Claude Code and agents
133
184
 
@@ -137,6 +188,7 @@ Install `zurf` on your `PATH` and allow the agent to run shell commands. Use `--
137
188
  $ zurf search "browserbase fetch api" --json
138
189
  $ zurf browse https://example.com --json
139
190
  $ zurf fetch https://example.com --json
191
+ $ zurf ask "What is Browserbase?" --json
140
192
  ```
141
193
 
142
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
+ }
@@ -18,7 +18,7 @@ export default class Browse extends ZurfBrowserbaseCommand {
18
18
  };
19
19
  static description = `Browse a URL in a cloud browser and return the rendered content as markdown (default) or raw HTML.
20
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.`;
21
+ Requires authentication and a Project ID. Run \`zurf setup\` before first use.`;
22
22
  static examples = [
23
23
  '<%= config.bin %> <%= command.id %> https://example.com',
24
24
  '<%= config.bin %> <%= command.id %> https://example.com --html',
@@ -57,7 +57,7 @@ Requires authentication and a Project ID. Run \`zurf init --global\` before firs
57
57
  await this.runWithBrowserbase(flags, `Browsing ${url}`, async (client) => {
58
58
  const resolution = resolveProjectId({ globalConfigDir: this.config.configDir });
59
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.');
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
61
  }
62
62
  const { projectId } = resolution;
63
63
  const result = await withBrowserbaseSession({
@@ -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
  }
@@ -16,7 +16,7 @@ export default class Fetch extends ZurfBrowserbaseCommand {
16
16
  }),
17
17
  };
18
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).
19
- Requires authentication. Run \`zurf init --global\` or use a project key before first use.`;
19
+ Requires authentication. Run \`zurf setup\` or use a project key before first use.`;
20
20
  static examples = [
21
21
  '<%= config.bin %> <%= command.id %> https://example.com',
22
22
  '<%= config.bin %> <%= command.id %> https://example.com --html',
@@ -12,7 +12,7 @@ export default class Search extends ZurfBrowserbaseCommand {
12
12
  }),
13
13
  };
14
14
  static description = `Search the web via Browserbase (Exa-powered).
15
- Requires authentication. Run \`zurf init --global\` or use a project key before first use.`;
15
+ Requires authentication. Run \`zurf setup\` or use a project key before first use.`;
16
16
  static examples = [
17
17
  '<%= config.bin %> <%= command.id %> "browserbase documentation"',
18
18
  '<%= config.bin %> <%= command.id %> "laravel inertia" --num-results 5 --json',
@@ -0,0 +1,14 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Setup extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ static flags: {
6
+ global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
7
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
8
+ local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ };
10
+ static summary: string;
11
+ run(): Promise<void>;
12
+ private collectProviderKeys;
13
+ private suggestGitignore;
14
+ }
@@ -0,0 +1,123 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import * as fs from 'node:fs/promises';
3
+ import { cliError, errorMessage } from '../../lib/cli-errors.js';
4
+ import { globalConfigFilePath, localConfigPathForCwd, readConfigFile, writeConfig, } from '../../lib/config.js';
5
+ import { zurfJsonFlag } from '../../lib/flags.js';
6
+ import { dotGitignoreMentionsZurf, ensureZurfGitignoreEntry, gitignorePathForCwd, } from '../../lib/gitignore-zurf.js';
7
+ import { printJson } from '../../lib/json-output.js';
8
+ import { promptApiKey, promptProjectId, selectProviders, selectScope, } from '../../lib/setup-prompts.js';
9
+ async function promptBrowserbase() {
10
+ const apiKey = await promptApiKey('Browserbase');
11
+ const projectId = await promptProjectId();
12
+ const browserbase = { apiKey: apiKey.trim() };
13
+ if (projectId.trim()) {
14
+ browserbase.projectId = projectId.trim();
15
+ }
16
+ return { configured: 'Browserbase', providers: { browserbase } };
17
+ }
18
+ async function promptPerplexity() {
19
+ const apiKey = await promptApiKey('Perplexity');
20
+ return { configured: 'Perplexity', providers: { perplexity: { apiKey: apiKey.trim() } } };
21
+ }
22
+ function resolveScope(flags) {
23
+ if (flags.global)
24
+ return 'global';
25
+ if (flags.local)
26
+ return 'local';
27
+ return undefined;
28
+ }
29
+ export default class Setup extends Command {
30
+ static description = `Interactive setup wizard for configuring API keys for all providers (Browserbase, Perplexity).
31
+ Stores keys in global or local config. Re-run to update or add providers.`;
32
+ static examples = [
33
+ '<%= config.bin %> <%= command.id %>',
34
+ '<%= config.bin %> <%= command.id %> --global',
35
+ '<%= config.bin %> <%= command.id %> --local',
36
+ ];
37
+ static flags = {
38
+ global: Flags.boolean({
39
+ description: 'Store config in user config directory (skip scope prompt)',
40
+ }),
41
+ json: zurfJsonFlag,
42
+ local: Flags.boolean({
43
+ description: 'Store config in .zurf/config.json in the current directory (skip scope prompt)',
44
+ }),
45
+ };
46
+ static summary = 'Configure API keys for Browserbase and Perplexity';
47
+ async run() {
48
+ const { flags } = await this.parse(Setup);
49
+ if (flags.global && flags.local) {
50
+ cliError({ command: this, exitCode: 2, json: flags.json, message: 'Cannot use both --global and --local.' });
51
+ }
52
+ const scope = resolveScope(flags) ?? (process.stdin.isTTY
53
+ ? await selectScope()
54
+ : cliError({
55
+ command: this,
56
+ exitCode: 1,
57
+ json: flags.json,
58
+ message: 'Non-interactive environment detected. Use --global or --local flag, or set API keys via environment variables (BROWSERBASE_API_KEY, PERPLEXITY_API_KEY).',
59
+ }));
60
+ const targetPath = scope === 'global'
61
+ ? globalConfigFilePath(this.config.configDir)
62
+ : localConfigPathForCwd();
63
+ const existing = readConfigFile(targetPath);
64
+ const bbConfigured = Boolean(existing?.providers?.browserbase?.apiKey);
65
+ const pplxConfigured = Boolean(existing?.providers?.perplexity?.apiKey);
66
+ const selectedProviders = process.stdin.isTTY
67
+ ? await selectProviders([
68
+ { configured: bbConfigured, name: 'Browserbase', value: 'browserbase' },
69
+ { configured: pplxConfigured, name: 'Perplexity', value: 'perplexity' },
70
+ ])
71
+ : ['browserbase', 'perplexity'];
72
+ const { configUpdate, configured } = await this.collectProviderKeys(selectedProviders);
73
+ try {
74
+ await writeConfig(targetPath, configUpdate);
75
+ }
76
+ catch (error) {
77
+ cliError({ command: this, exitCode: 1, json: flags.json, message: errorMessage(error) });
78
+ }
79
+ if (flags.json) {
80
+ printJson({ configured, ok: true, path: targetPath, scope });
81
+ return;
82
+ }
83
+ for (const name of configured) {
84
+ this.log(`Configured ${name} in ${targetPath}`);
85
+ }
86
+ if (scope === 'local') {
87
+ await this.suggestGitignore();
88
+ }
89
+ }
90
+ async collectProviderKeys(selectedProviders) {
91
+ const configUpdate = { providers: {} };
92
+ const configured = [];
93
+ for (const provider of selectedProviders) {
94
+ // eslint-disable-next-line no-await-in-loop -- sequential prompts, must run in order
95
+ const result = provider === 'browserbase' ? await promptBrowserbase() : await promptPerplexity();
96
+ Object.assign(configUpdate.providers, result.providers);
97
+ configured.push(result.configured);
98
+ }
99
+ return { configUpdate, configured };
100
+ }
101
+ async suggestGitignore() {
102
+ let showTip = true;
103
+ try {
104
+ const gi = await fs.readFile(gitignorePathForCwd(), 'utf8');
105
+ if (dotGitignoreMentionsZurf(gi)) {
106
+ showTip = false;
107
+ }
108
+ }
109
+ catch {
110
+ // no .gitignore yet
111
+ }
112
+ if (showTip) {
113
+ this.log('Tip: add .zurf/ to .gitignore so keys are not committed.');
114
+ try {
115
+ await ensureZurfGitignoreEntry(gitignorePathForCwd());
116
+ this.log('Added .zurf/ to .gitignore.');
117
+ }
118
+ catch {
119
+ // best effort
120
+ }
121
+ }
122
+ }
123
+ }
@@ -10,7 +10,7 @@ export async function createBrowserbaseClient(options) {
10
10
  }
11
11
  export class MissingApiKeyError extends Error {
12
12
  constructor() {
13
- super('No Browserbase API key found. Set BROWSERBASE_API_KEY, run `zurf init --global` or `zurf init --local`, or add a project `.zurf/config.json`.');
13
+ super('No Browserbase API key found. Set BROWSERBASE_API_KEY, run `zurf setup`, or add a project `.zurf/config.json`.');
14
14
  this.name = 'MissingApiKeyError';
15
15
  }
16
16
  }
@@ -18,11 +18,25 @@ export type ResolvedApiKey = {
18
18
  export type ActiveApiKey = Extract<ResolvedApiKey, {
19
19
  apiKey: string;
20
20
  }>;
21
- export interface ConfigFileShape {
21
+ /** Old flat config shape (v0.2.x and earlier). */
22
+ export interface LegacyConfigFileShape {
22
23
  apiKey?: string;
23
24
  format?: 'html' | 'markdown';
24
25
  projectId?: string;
25
26
  }
27
+ /** Current config shape (v0.3.0+). */
28
+ export interface ConfigFileShape {
29
+ format?: 'html' | 'markdown';
30
+ providers?: {
31
+ browserbase?: {
32
+ apiKey?: string;
33
+ projectId?: string;
34
+ };
35
+ perplexity?: {
36
+ apiKey?: string;
37
+ };
38
+ };
39
+ }
26
40
  export type ResolvedProjectId = {
27
41
  path: string;
28
42
  projectId: string;
@@ -37,12 +51,15 @@ export type ResolvedProjectId = {
37
51
  } | {
38
52
  source: 'none';
39
53
  };
54
+ /** Alias — structurally identical to ResolvedApiKey; kept for semantic clarity at call sites. */
55
+ export type ResolvedPerplexityApiKey = ResolvedApiKey;
40
56
  /**
41
57
  * Path to global `config.json` under oclif's `this.config.configDir` (same rules as @oclif/core `Config.dir('config')` for `dirname` zurf).
42
58
  */
43
59
  export declare function globalConfigFilePath(oclifConfigDir: string): string;
44
60
  export declare function localConfigPathForCwd(cwd?: string): string;
45
61
  export declare function findLocalConfigPath(startDir?: string): string | undefined;
62
+ export declare function readConfigFile(filePath: string): ConfigFileShape | undefined;
46
63
  export declare function resolveFormat(options: {
47
64
  cwd?: string;
48
65
  flagHtml: boolean;
@@ -56,5 +73,9 @@ export declare function resolveProjectId(options: {
56
73
  cwd?: string;
57
74
  globalConfigDir: string;
58
75
  }): ResolvedProjectId;
76
+ export declare function resolvePerplexityApiKey(options: {
77
+ cwd?: string;
78
+ globalConfigDir: string;
79
+ }): ResolvedPerplexityApiKey;
59
80
  export declare function writeApiKeyConfig(targetPath: string, apiKey: string): Promise<void>;
60
81
  export declare function writeConfig(targetPath: string, fields: Partial<ConfigFileShape>): Promise<void>;