@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.
@@ -26,29 +26,56 @@ export function findLocalConfigPath(startDir = process.cwd()) {
26
26
  }
27
27
  return undefined;
28
28
  }
29
- function readConfigFile(filePath) {
29
+ /**
30
+ * Detect whether a parsed config object uses the old flat shape (has `apiKey` at root, no `providers` key).
31
+ */
32
+ function isLegacyConfig(raw) {
33
+ if (raw === null || typeof raw !== 'object')
34
+ return false;
35
+ const obj = raw;
36
+ return ('apiKey' in obj || 'projectId' in obj) && !('providers' in obj);
37
+ }
38
+ /**
39
+ * Migrate a legacy flat config to the new nested shape.
40
+ */
41
+ function migrateLegacyConfig(legacy) {
42
+ const config = {};
43
+ if (legacy.apiKey || legacy.projectId) {
44
+ config.providers = {
45
+ browserbase: {},
46
+ };
47
+ if (legacy.apiKey)
48
+ config.providers.browserbase.apiKey = legacy.apiKey;
49
+ if (legacy.projectId)
50
+ config.providers.browserbase.projectId = legacy.projectId;
51
+ }
52
+ if (legacy.format)
53
+ config.format = legacy.format;
54
+ return config;
55
+ }
56
+ export function readConfigFile(filePath) {
30
57
  try {
31
58
  const raw = fs.readFileSync(filePath, 'utf8');
32
- return JSON.parse(raw);
59
+ const parsed = JSON.parse(raw);
60
+ if (isLegacyConfig(parsed)) {
61
+ return migrateLegacyConfig(parsed);
62
+ }
63
+ return parsed;
33
64
  }
34
65
  catch {
35
66
  return undefined;
36
67
  }
37
68
  }
38
- function readApiKeyFromFile(filePath) {
69
+ function readStringField(filePath, getter) {
39
70
  const parsed = readConfigFile(filePath);
40
71
  if (!parsed)
41
72
  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;
73
+ const val = getter(parsed)?.trim() ?? '';
74
+ return val.length > 0 ? val : undefined;
51
75
  }
76
+ const readBrowserbaseApiKeyFromFile = (f) => readStringField(f, (c) => c.providers?.browserbase?.apiKey);
77
+ const readBrowserbaseProjectIdFromFile = (f) => readStringField(f, (c) => c.providers?.browserbase?.projectId);
78
+ const readPerplexityApiKeyFromFile = (f) => readStringField(f, (c) => c.providers?.perplexity?.apiKey);
52
79
  function readFormatFromFile(filePath) {
53
80
  const parsed = readConfigFile(filePath);
54
81
  if (!parsed)
@@ -85,13 +112,13 @@ export function resolveApiKey(options) {
85
112
  }
86
113
  const localPath = findLocalConfigPath(cwd);
87
114
  if (localPath) {
88
- const key = readApiKeyFromFile(localPath);
115
+ const key = readBrowserbaseApiKeyFromFile(localPath);
89
116
  if (key) {
90
117
  return { apiKey: key, path: localPath, source: 'local' };
91
118
  }
92
119
  }
93
120
  const gPath = globalConfigFilePath(options.globalConfigDir);
94
- const globalKey = readApiKeyFromFile(gPath);
121
+ const globalKey = readBrowserbaseApiKeyFromFile(gPath);
95
122
  if (globalKey) {
96
123
  return { apiKey: globalKey, path: gPath, source: 'global' };
97
124
  }
@@ -105,20 +132,40 @@ export function resolveProjectId(options) {
105
132
  }
106
133
  const localPath = findLocalConfigPath(cwd);
107
134
  if (localPath) {
108
- const id = readProjectIdFromFile(localPath);
135
+ const id = readBrowserbaseProjectIdFromFile(localPath);
109
136
  if (id) {
110
137
  return { path: localPath, projectId: id, source: 'local' };
111
138
  }
112
139
  }
113
140
  const gPath = globalConfigFilePath(options.globalConfigDir);
114
- const globalId = readProjectIdFromFile(gPath);
141
+ const globalId = readBrowserbaseProjectIdFromFile(gPath);
115
142
  if (globalId) {
116
143
  return { path: gPath, projectId: globalId, source: 'global' };
117
144
  }
118
145
  return { source: 'none' };
119
146
  }
147
+ export function resolvePerplexityApiKey(options) {
148
+ const cwd = options.cwd ?? process.cwd();
149
+ const envKey = process.env.PERPLEXITY_API_KEY?.trim();
150
+ if (envKey) {
151
+ return { apiKey: envKey, source: 'env' };
152
+ }
153
+ const localPath = findLocalConfigPath(cwd);
154
+ if (localPath) {
155
+ const key = readPerplexityApiKeyFromFile(localPath);
156
+ if (key) {
157
+ return { apiKey: key, path: localPath, source: 'local' };
158
+ }
159
+ }
160
+ const gPath = globalConfigFilePath(options.globalConfigDir);
161
+ const globalKey = readPerplexityApiKeyFromFile(gPath);
162
+ if (globalKey) {
163
+ return { apiKey: globalKey, path: gPath, source: 'global' };
164
+ }
165
+ return { source: 'none' };
166
+ }
120
167
  export async function writeApiKeyConfig(targetPath, apiKey) {
121
- await writeConfig(targetPath, { apiKey: apiKey.trim() });
168
+ await writeConfig(targetPath, { providers: { browserbase: { apiKey: apiKey.trim() } } });
122
169
  }
123
170
  export async function writeConfig(targetPath, fields) {
124
171
  const dir = path.dirname(targetPath);
@@ -126,12 +173,37 @@ export async function writeConfig(targetPath, fields) {
126
173
  let existing = {};
127
174
  try {
128
175
  const raw = await fs.promises.readFile(targetPath, 'utf8');
129
- existing = JSON.parse(raw);
176
+ const parsed = JSON.parse(raw);
177
+ existing = isLegacyConfig(parsed) ? migrateLegacyConfig(parsed) : parsed;
130
178
  }
131
179
  catch {
132
180
  // file doesn't exist yet — start fresh
133
181
  }
134
- const merged = { ...existing, ...fields };
182
+ const merged = {
183
+ ...existing,
184
+ ...fields,
185
+ providers: {
186
+ ...existing.providers,
187
+ ...fields.providers,
188
+ browserbase: {
189
+ ...existing.providers?.browserbase,
190
+ ...fields.providers?.browserbase,
191
+ },
192
+ perplexity: {
193
+ ...existing.providers?.perplexity,
194
+ ...fields.providers?.perplexity,
195
+ },
196
+ },
197
+ };
198
+ if (merged.providers?.browserbase && Object.keys(merged.providers.browserbase).length === 0) {
199
+ delete merged.providers.browserbase;
200
+ }
201
+ if (merged.providers?.perplexity && Object.keys(merged.providers.perplexity).length === 0) {
202
+ delete merged.providers.perplexity;
203
+ }
204
+ if (merged.providers && Object.keys(merged.providers).length === 0) {
205
+ delete merged.providers;
206
+ }
135
207
  const body = `${JSON.stringify(merged, null, 2)}\n`;
136
208
  await fs.promises.writeFile(targetPath, body, { encoding: 'utf8', mode: 0o600 });
137
209
  }
@@ -0,0 +1,33 @@
1
+ import { resolvePerplexityApiKey } from './config.js';
2
+ export type PerplexityDepth = 'deep' | 'quick';
3
+ export type PerplexityRecency = 'day' | 'hour' | 'month' | 'week' | 'year';
4
+ export interface PerplexityAskOptions {
5
+ depth?: PerplexityDepth;
6
+ domains?: string[];
7
+ question: string;
8
+ recency?: PerplexityRecency;
9
+ }
10
+ export interface PerplexityAskResult {
11
+ answer: string;
12
+ citations: string[];
13
+ model: string;
14
+ }
15
+ export declare class PerplexityClient {
16
+ private readonly apiKey;
17
+ constructor(apiKey: string);
18
+ ask(options: PerplexityAskOptions): Promise<PerplexityAskResult>;
19
+ }
20
+ export declare class MissingPerplexityKeyError extends Error {
21
+ code: string;
22
+ suggestions: string[];
23
+ constructor();
24
+ }
25
+ export declare function createPerplexityClient(options: {
26
+ cwd?: string;
27
+ globalConfigDir: string;
28
+ }): {
29
+ client: PerplexityClient;
30
+ resolution: Extract<ReturnType<typeof resolvePerplexityApiKey>, {
31
+ apiKey: string;
32
+ }>;
33
+ };
@@ -0,0 +1,59 @@
1
+ import { resolvePerplexityApiKey } from './config.js';
2
+ function buildRequestBody(options) {
3
+ const model = options.depth === 'deep' ? 'sonar-pro' : 'sonar';
4
+ const body = {
5
+ messages: [{ content: options.question, role: 'user' }],
6
+ model,
7
+ };
8
+ if (options.recency) {
9
+ body.search_recency_filter = options.recency; // eslint-disable-line camelcase
10
+ }
11
+ if (options.domains && options.domains.length > 0) {
12
+ body.search_domain_filter = options.domains; // eslint-disable-line camelcase
13
+ }
14
+ return body;
15
+ }
16
+ export class PerplexityClient {
17
+ apiKey;
18
+ constructor(apiKey) {
19
+ this.apiKey = apiKey;
20
+ }
21
+ async ask(options) {
22
+ const body = buildRequestBody(options);
23
+ const response = await fetch('https://api.perplexity.ai/chat/completions', {
24
+ body: JSON.stringify(body),
25
+ headers: {
26
+ 'Authorization': `Bearer ${this.apiKey}`,
27
+ 'Content-Type': 'application/json',
28
+ },
29
+ method: 'POST',
30
+ });
31
+ if (!response.ok) {
32
+ const text = await response.text().catch(() => '');
33
+ throw new Error(`Perplexity API error (${response.status}): ${text || response.statusText}`);
34
+ }
35
+ const data = (await response.json());
36
+ const answer = data.choices?.[0]?.message?.content ?? '';
37
+ const citations = data.citations ?? [];
38
+ return { answer, citations, model: data.model };
39
+ }
40
+ }
41
+ export class MissingPerplexityKeyError extends Error {
42
+ code = 'MISSING_PERPLEXITY_KEY';
43
+ suggestions = [
44
+ 'Run `zurf setup` to configure your Perplexity API key',
45
+ 'Or set the PERPLEXITY_API_KEY environment variable',
46
+ ];
47
+ constructor() {
48
+ super('No Perplexity API key found.');
49
+ this.name = 'MissingPerplexityKeyError';
50
+ }
51
+ }
52
+ export function createPerplexityClient(options) {
53
+ const resolution = resolvePerplexityApiKey({ cwd: options.cwd, globalConfigDir: options.globalConfigDir });
54
+ if (resolution.source === 'none') {
55
+ throw new MissingPerplexityKeyError();
56
+ }
57
+ const client = new PerplexityClient(resolution.apiKey);
58
+ return { client, resolution };
59
+ }
@@ -0,0 +1,10 @@
1
+ export type ConfigScope = 'global' | 'local';
2
+ export interface ProviderChoice {
3
+ configured: boolean;
4
+ name: string;
5
+ value: string;
6
+ }
7
+ export declare function selectScope(): Promise<ConfigScope>;
8
+ export declare function selectProviders(providers: ProviderChoice[]): Promise<string[]>;
9
+ export declare function promptApiKey(label: string): Promise<string>;
10
+ export declare function promptProjectId(): Promise<string>;
@@ -0,0 +1,34 @@
1
+ import { checkbox, input, password, select } from '@inquirer/prompts';
2
+ export async function selectScope() {
3
+ return select({
4
+ choices: [
5
+ { name: 'Global (user-wide, recommended)', value: 'global' },
6
+ { name: 'Local (this project only)', value: 'local' },
7
+ ],
8
+ message: 'Where should zurf store your config?',
9
+ });
10
+ }
11
+ export async function selectProviders(providers) {
12
+ const choices = providers.map((p) => ({
13
+ checked: p.configured,
14
+ name: p.configured ? `${p.name} [configured]` : p.name,
15
+ value: p.value,
16
+ }));
17
+ return checkbox({
18
+ choices,
19
+ message: 'Which providers do you want to configure?',
20
+ required: true,
21
+ });
22
+ }
23
+ export async function promptApiKey(label) {
24
+ return password({
25
+ mask: '*',
26
+ message: `${label} API key:`,
27
+ validate: (value) => (value.trim().length > 0 ? true : 'API key cannot be empty'),
28
+ });
29
+ }
30
+ export async function promptProjectId() {
31
+ return input({
32
+ message: 'Browserbase Project ID (optional, press Enter to skip):',
33
+ });
34
+ }
@@ -1,29 +1,49 @@
1
1
  {
2
2
  "commands": {
3
- "browse": {
3
+ "ask": {
4
4
  "aliases": [],
5
5
  "args": {
6
- "url": {
7
- "description": "URL to browse",
8
- "name": "url",
6
+ "question": {
7
+ "description": "The question to ask Perplexity",
8
+ "name": "question",
9
9
  "required": true
10
10
  }
11
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.",
12
+ "description": "Ask a question and get an AI-powered answer with web citations via Perplexity Sonar.\nReturns an answer with inline citations and a sources list. Use --depth deep for more thorough research.",
13
13
  "examples": [
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"
14
+ "<%= config.bin %> <%= command.id %> \"What is Browserbase?\"",
15
+ "<%= config.bin %> <%= command.id %> \"latest tech news\" --recency day",
16
+ "<%= config.bin %> <%= command.id %> \"search reddit for best CLI tools\" --domains reddit.com",
17
+ "<%= config.bin %> <%= command.id %> \"explain quantum computing\" --depth deep",
18
+ "<%= config.bin %> <%= command.id %> \"What is Node.js?\" --json",
19
+ "<%= config.bin %> <%= command.id %> \"What is oclif?\" --no-citations"
18
20
  ],
19
21
  "flags": {
20
- "html": {
21
- "description": "Output raw HTML instead of markdown",
22
- "env": "ZURF_HTML",
23
- "name": "html",
24
- "allowNo": false,
22
+ "citations": {
23
+ "description": "Show sources list after the answer (use --no-citations to hide)",
24
+ "name": "citations",
25
+ "allowNo": true,
25
26
  "type": "boolean"
26
27
  },
28
+ "depth": {
29
+ "description": "Search depth: quick (sonar) or deep (sonar-pro)",
30
+ "name": "depth",
31
+ "default": "quick",
32
+ "hasDynamicHelp": false,
33
+ "multiple": false,
34
+ "options": [
35
+ "quick",
36
+ "deep"
37
+ ],
38
+ "type": "option"
39
+ },
40
+ "domains": {
41
+ "description": "Restrict search to these domains (comma-separated)",
42
+ "name": "domains",
43
+ "hasDynamicHelp": false,
44
+ "multiple": false,
45
+ "type": "option"
46
+ },
27
47
  "json": {
28
48
  "description": "Print machine-readable JSON to stdout",
29
49
  "env": "ZURF_JSON",
@@ -31,81 +51,53 @@
31
51
  "allowNo": false,
32
52
  "type": "boolean"
33
53
  },
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",
54
+ "recency": {
55
+ "description": "Filter sources by recency",
56
+ "name": "recency",
38
57
  "hasDynamicHelp": false,
39
58
  "multiple": false,
59
+ "options": [
60
+ "hour",
61
+ "day",
62
+ "week",
63
+ "month",
64
+ "year"
65
+ ],
40
66
  "type": "option"
41
67
  }
42
68
  },
43
69
  "hasDynamicHelp": false,
44
70
  "hiddenAliases": [],
45
- "id": "browse",
71
+ "id": "ask",
46
72
  "pluginAlias": "@vibemastery/zurf",
47
73
  "pluginName": "@vibemastery/zurf",
48
74
  "pluginType": "core",
49
75
  "strict": true,
50
- "summary": "Browse a URL in a cloud browser and return rendered content as markdown",
76
+ "summary": "Ask a question via Perplexity Sonar",
77
+ "enableJsonFlag": false,
51
78
  "isESM": true,
52
79
  "relativePath": [
53
80
  "dist",
54
81
  "commands",
55
- "browse",
82
+ "ask",
56
83
  "index.js"
57
84
  ]
58
85
  },
59
- "config:which": {
86
+ "browse": {
60
87
  "aliases": [],
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.",
63
- "examples": [
64
- "<%= config.bin %> config which",
65
- "<%= config.bin %> config which --json"
66
- ],
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
- },
75
- "json": {
76
- "description": "Print machine-readable JSON to stdout",
77
- "env": "ZURF_JSON",
78
- "name": "json",
79
- "allowNo": false,
80
- "type": "boolean"
88
+ "args": {
89
+ "url": {
90
+ "description": "URL to browse",
91
+ "name": "url",
92
+ "required": true
81
93
  }
82
94
  },
83
- "hasDynamicHelp": false,
84
- "hiddenAliases": [],
85
- "id": "config:which",
86
- "pluginAlias": "@vibemastery/zurf",
87
- "pluginName": "@vibemastery/zurf",
88
- "pluginType": "core",
89
- "strict": true,
90
- "summary": "Show where the API key is loaded from",
91
- "enableJsonFlag": false,
92
- "isESM": true,
93
- "relativePath": [
94
- "dist",
95
- "commands",
96
- "config",
97
- "which.js"
98
- ]
99
- },
100
- "init": {
101
- "aliases": [],
102
- "args": {},
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`).",
95
+ "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 setup` before first use.",
104
96
  "examples": [
105
- "<%= config.bin %> <%= command.id %> --global",
106
- "<%= config.bin %> <%= command.id %> --local",
107
- "<%= config.bin %> <%= command.id %> --global --api-key KEY --project-id PROJ_ID",
108
- "printenv BROWSERBASE_API_KEY | <%= config.bin %> <%= command.id %> --global"
97
+ "<%= config.bin %> <%= command.id %> https://example.com",
98
+ "<%= config.bin %> <%= command.id %> https://example.com --html",
99
+ "<%= config.bin %> <%= command.id %> https://example.com --json",
100
+ "<%= config.bin %> <%= command.id %> https://example.com -o page.md"
109
101
  ],
110
102
  "flags": {
111
103
  "html": {
@@ -122,34 +114,10 @@
122
114
  "allowNo": false,
123
115
  "type": "boolean"
124
116
  },
125
- "api-key": {
126
- "description": "API key for non-interactive use. Prefer piping stdin or using a TTY prompt — values on the command line are visible in shell history and process listings.",
127
- "name": "api-key",
128
- "hasDynamicHelp": false,
129
- "multiple": false,
130
- "type": "option"
131
- },
132
- "gitignore": {
133
- "description": "Append .zurf/ to ./.gitignore if that entry is missing",
134
- "name": "gitignore",
135
- "allowNo": false,
136
- "type": "boolean"
137
- },
138
- "global": {
139
- "description": "Store API key in user config (oclif config dir for this CLI)",
140
- "name": "global",
141
- "allowNo": false,
142
- "type": "boolean"
143
- },
144
- "local": {
145
- "description": "Store API key in ./.zurf/config.json for this directory",
146
- "name": "local",
147
- "allowNo": false,
148
- "type": "boolean"
149
- },
150
- "project-id": {
151
- "description": "Browserbase Project ID (optional; needed for browse, screenshot, pdf commands)",
152
- "name": "project-id",
117
+ "output": {
118
+ "char": "o",
119
+ "description": "Write rendered content to this file (full content); otherwise human mode prints a truncated preview to stdout",
120
+ "name": "output",
153
121
  "hasDynamicHelp": false,
154
122
  "multiple": false,
155
123
  "type": "option"
@@ -157,18 +125,17 @@
157
125
  },
158
126
  "hasDynamicHelp": false,
159
127
  "hiddenAliases": [],
160
- "id": "init",
128
+ "id": "browse",
161
129
  "pluginAlias": "@vibemastery/zurf",
162
130
  "pluginName": "@vibemastery/zurf",
163
131
  "pluginType": "core",
164
132
  "strict": true,
165
- "summary": "Configure Browserbase API key and Project ID storage",
166
- "enableJsonFlag": false,
133
+ "summary": "Browse a URL in a cloud browser and return rendered content as markdown",
167
134
  "isESM": true,
168
135
  "relativePath": [
169
136
  "dist",
170
137
  "commands",
171
- "init",
138
+ "browse",
172
139
  "index.js"
173
140
  ]
174
141
  },
@@ -181,7 +148,7 @@
181
148
  "required": true
182
149
  }
183
150
  },
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.",
151
+ "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 setup` or use a project key before first use.",
185
152
  "examples": [
186
153
  "<%= config.bin %> <%= command.id %> https://example.com",
187
154
  "<%= config.bin %> <%= command.id %> https://example.com --html",
@@ -246,6 +213,47 @@
246
213
  "index.js"
247
214
  ]
248
215
  },
216
+ "config:which": {
217
+ "aliases": [],
218
+ "args": {},
219
+ "description": "Show where API keys would be loaded from (no secrets printed).\nResolution order: env var → project .zurf/config.json (walk-up) → global config.",
220
+ "examples": [
221
+ "<%= config.bin %> config which",
222
+ "<%= config.bin %> config which --json"
223
+ ],
224
+ "flags": {
225
+ "html": {
226
+ "description": "Output raw HTML instead of markdown",
227
+ "env": "ZURF_HTML",
228
+ "name": "html",
229
+ "allowNo": false,
230
+ "type": "boolean"
231
+ },
232
+ "json": {
233
+ "description": "Print machine-readable JSON to stdout",
234
+ "env": "ZURF_JSON",
235
+ "name": "json",
236
+ "allowNo": false,
237
+ "type": "boolean"
238
+ }
239
+ },
240
+ "hasDynamicHelp": false,
241
+ "hiddenAliases": [],
242
+ "id": "config:which",
243
+ "pluginAlias": "@vibemastery/zurf",
244
+ "pluginName": "@vibemastery/zurf",
245
+ "pluginType": "core",
246
+ "strict": true,
247
+ "summary": "Show where API keys are loaded from",
248
+ "enableJsonFlag": false,
249
+ "isESM": true,
250
+ "relativePath": [
251
+ "dist",
252
+ "commands",
253
+ "config",
254
+ "which.js"
255
+ ]
256
+ },
249
257
  "search": {
250
258
  "aliases": [],
251
259
  "args": {
@@ -255,7 +263,7 @@
255
263
  "required": true
256
264
  }
257
265
  },
258
- "description": "Search the web via Browserbase (Exa-powered).\nRequires authentication. Run `zurf init --global` or use a project key before first use.",
266
+ "description": "Search the web via Browserbase (Exa-powered).\nRequires authentication. Run `zurf setup` or use a project key before first use.",
259
267
  "examples": [
260
268
  "<%= config.bin %> <%= command.id %> \"browserbase documentation\"",
261
269
  "<%= config.bin %> <%= command.id %> \"laravel inertia\" --num-results 5 --json"
@@ -300,7 +308,54 @@
300
308
  "search",
301
309
  "index.js"
302
310
  ]
311
+ },
312
+ "setup": {
313
+ "aliases": [],
314
+ "args": {},
315
+ "description": "Interactive setup wizard for configuring API keys for all providers (Browserbase, Perplexity).\nStores keys in global or local config. Re-run to update or add providers.",
316
+ "examples": [
317
+ "<%= config.bin %> <%= command.id %>",
318
+ "<%= config.bin %> <%= command.id %> --global",
319
+ "<%= config.bin %> <%= command.id %> --local"
320
+ ],
321
+ "flags": {
322
+ "global": {
323
+ "description": "Store config in user config directory (skip scope prompt)",
324
+ "name": "global",
325
+ "allowNo": false,
326
+ "type": "boolean"
327
+ },
328
+ "json": {
329
+ "description": "Print machine-readable JSON to stdout",
330
+ "env": "ZURF_JSON",
331
+ "name": "json",
332
+ "allowNo": false,
333
+ "type": "boolean"
334
+ },
335
+ "local": {
336
+ "description": "Store config in .zurf/config.json in the current directory (skip scope prompt)",
337
+ "name": "local",
338
+ "allowNo": false,
339
+ "type": "boolean"
340
+ }
341
+ },
342
+ "hasDynamicHelp": false,
343
+ "hiddenAliases": [],
344
+ "id": "setup",
345
+ "pluginAlias": "@vibemastery/zurf",
346
+ "pluginName": "@vibemastery/zurf",
347
+ "pluginType": "core",
348
+ "strict": true,
349
+ "summary": "Configure API keys for Browserbase and Perplexity",
350
+ "enableJsonFlag": false,
351
+ "isESM": true,
352
+ "relativePath": [
353
+ "dist",
354
+ "commands",
355
+ "setup",
356
+ "index.js"
357
+ ]
303
358
  }
304
359
  },
305
- "version": "0.2.3"
360
+ "version": "0.3.0"
306
361
  }