@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 +166 -19
- package/dist/commands/ask/index.d.ts +17 -0
- package/dist/commands/ask/index.js +102 -0
- package/dist/commands/browse/index.d.ts +15 -0
- package/dist/commands/browse/index.js +109 -0
- package/dist/commands/config/which.d.ts +1 -0
- package/dist/commands/config/which.js +51 -44
- package/dist/commands/fetch/index.d.ts +1 -0
- package/dist/commands/fetch/index.js +15 -16
- package/dist/commands/search/index.d.ts +1 -0
- package/dist/commands/search/index.js +1 -1
- package/dist/commands/{init → setup}/index.d.ts +4 -5
- package/dist/commands/setup/index.js +123 -0
- package/dist/lib/browse-output.d.ts +12 -0
- package/dist/lib/browse-output.js +10 -0
- package/dist/lib/browserbase-client.js +1 -1
- package/dist/lib/browserbase-session.d.ts +7 -0
- package/dist/lib/browserbase-session.js +39 -0
- package/dist/lib/cli-errors.d.ts +1 -0
- package/dist/lib/cli-errors.js +9 -0
- package/dist/lib/config.d.ts +48 -1
- package/dist/lib/config.js +150 -7
- package/dist/lib/fetch-output.d.ts +2 -1
- package/dist/lib/fetch-output.js +3 -2
- package/dist/lib/flags.d.ts +3 -0
- package/dist/lib/flags.js +7 -0
- package/dist/lib/html-to-markdown.d.ts +1 -0
- package/dist/lib/html-to-markdown.js +21 -0
- package/dist/lib/perplexity-client.d.ts +33 -0
- package/dist/lib/perplexity-client.js +59 -0
- package/dist/lib/setup-prompts.d.ts +10 -0
- package/dist/lib/setup-prompts.js +34 -0
- package/oclif.manifest.json +196 -48
- package/package.json +8 -4
- package/dist/commands/init/index.js +0 -95
|
@@ -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).
|
|
17
|
-
Requires authentication. Run \`zurf
|
|
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 setup\` or use a project key before first use.`;
|
|
18
20
|
static examples = [
|
|
19
21
|
'<%= config.bin %> <%= command.id %> https://example.com',
|
|
22
|
+
'<%= config.bin %> <%= command.id %> https://example.com --html',
|
|
20
23
|
'<%= config.bin %> <%= command.id %> https://example.com --json',
|
|
21
|
-
'<%= config.bin %> <%= command.id %> https://example.com -o page.
|
|
24
|
+
'<%= config.bin %> <%= command.id %> https://example.com -o page.md --proxies',
|
|
22
25
|
];
|
|
23
26
|
static flags = {
|
|
24
27
|
...zurfBaseFlags,
|
|
@@ -39,7 +42,7 @@ Requires authentication. Run \`zurf init --global\` or use a project key before
|
|
|
39
42
|
description: 'Route through Browserbase proxies (helps with some blocked sites)',
|
|
40
43
|
}),
|
|
41
44
|
};
|
|
42
|
-
static summary = 'Fetch a URL via Browserbase';
|
|
45
|
+
static summary = 'Fetch a URL via Browserbase and return content as markdown';
|
|
43
46
|
async run() {
|
|
44
47
|
const { args, flags } = await this.parse(Fetch);
|
|
45
48
|
const url = args.url.trim();
|
|
@@ -68,24 +71,21 @@ Requires authentication. Run \`zurf init --global\` or use a project key before
|
|
|
68
71
|
proxies: flags.proxies,
|
|
69
72
|
url,
|
|
70
73
|
});
|
|
74
|
+
const format = resolveFormat({ flagHtml: flags.html, globalConfigDir: this.config.configDir });
|
|
75
|
+
const content = format === 'markdown' ? await htmlToMarkdown(response.content) : response.content;
|
|
76
|
+
const converted = { ...response, content };
|
|
71
77
|
if (flags.json) {
|
|
72
|
-
printJson(buildFetchJsonPayload(
|
|
78
|
+
printJson(buildFetchJsonPayload(converted, format));
|
|
73
79
|
return;
|
|
74
80
|
}
|
|
75
81
|
this.logToStderr(humanFetchMetaLines(response).join('\n'));
|
|
76
82
|
this.logToStderr('');
|
|
77
83
|
if (flags.output) {
|
|
78
84
|
try {
|
|
79
|
-
await fs.writeFile(flags.output,
|
|
85
|
+
await fs.writeFile(flags.output, content, 'utf8');
|
|
80
86
|
}
|
|
81
87
|
catch (error) {
|
|
82
|
-
|
|
83
|
-
typeof error === 'object' &&
|
|
84
|
-
'code' in error &&
|
|
85
|
-
typeof error.code === 'string'
|
|
86
|
-
? error.code
|
|
87
|
-
: undefined;
|
|
88
|
-
if (code === 'ENOENT') {
|
|
88
|
+
if (errorCode(error) === 'ENOENT') {
|
|
89
89
|
cliError({
|
|
90
90
|
command: this,
|
|
91
91
|
exitCode: 1,
|
|
@@ -95,10 +95,9 @@ Requires authentication. Run \`zurf init --global\` or use a project key before
|
|
|
95
95
|
}
|
|
96
96
|
throw error;
|
|
97
97
|
}
|
|
98
|
-
this.logToStderr(`Wrote
|
|
98
|
+
this.logToStderr(`Wrote content to ${flags.output}`);
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
|
-
const { content } = response;
|
|
102
101
|
if (content.length <= HUMAN_BODY_PREVIEW_CHARS) {
|
|
103
102
|
this.log(content);
|
|
104
103
|
return;
|
|
@@ -7,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;
|
|
@@ -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
|
|
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',
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
export default class
|
|
2
|
+
export default class Setup extends Command {
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
-
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
-
gitignore: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
6
|
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
-
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
7
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
9
|
};
|
|
12
10
|
static summary: string;
|
|
13
11
|
run(): Promise<void>;
|
|
14
|
-
private
|
|
12
|
+
private collectProviderKeys;
|
|
13
|
+
private suggestGitignore;
|
|
15
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
|
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
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export async function withBrowserbaseSession(options) {
|
|
2
|
+
const { chromium } = await import('playwright-core');
|
|
3
|
+
const { client, projectId, work } = options;
|
|
4
|
+
const session = await client.sessions.create({ projectId });
|
|
5
|
+
const browser = await chromium.connectOverCDP(session.connectUrl);
|
|
6
|
+
let cleanedUp = false;
|
|
7
|
+
const cleanup = async () => {
|
|
8
|
+
if (cleanedUp)
|
|
9
|
+
return;
|
|
10
|
+
cleanedUp = true;
|
|
11
|
+
try {
|
|
12
|
+
const pages = browser.contexts()[0]?.pages() ?? [];
|
|
13
|
+
await Promise.all(pages.map((p) => p.close().catch(() => { })));
|
|
14
|
+
await browser.close().catch(() => { });
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
await client.sessions.update(session.id, { status: 'REQUEST_RELEASE' }).catch(() => { });
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const onSignal = () => {
|
|
21
|
+
// eslint-disable-next-line n/no-process-exit, unicorn/no-process-exit
|
|
22
|
+
cleanup().then(() => process.exit(1), () => process.exit(1));
|
|
23
|
+
};
|
|
24
|
+
process.on('SIGINT', onSignal);
|
|
25
|
+
process.on('SIGTERM', onSignal);
|
|
26
|
+
try {
|
|
27
|
+
const defaultContext = browser.contexts()[0];
|
|
28
|
+
if (!defaultContext) {
|
|
29
|
+
throw new Error('No browser context available from Browserbase session');
|
|
30
|
+
}
|
|
31
|
+
const page = defaultContext.pages()[0] ?? (await defaultContext.newPage());
|
|
32
|
+
return await work(page);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
process.removeListener('SIGINT', onSignal);
|
|
36
|
+
process.removeListener('SIGTERM', onSignal);
|
|
37
|
+
await cleanup();
|
|
38
|
+
}
|
|
39
|
+
}
|
package/dist/lib/cli-errors.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare class CliJsonExitContractError extends Error {
|
|
|
14
14
|
constructor();
|
|
15
15
|
}
|
|
16
16
|
export declare function errorMessage(err: unknown): string;
|
|
17
|
+
export declare function errorCode(err: unknown): string | undefined;
|
|
17
18
|
export declare function errorStatus(err: unknown): number | undefined;
|
|
18
19
|
/** Print JSON or human error and exit; does not return. */
|
|
19
20
|
export declare function cliError(options: CliErrorOptions): never;
|
package/dist/lib/cli-errors.js
CHANGED
|
@@ -15,6 +15,15 @@ export function errorMessage(err) {
|
|
|
15
15
|
}
|
|
16
16
|
return String(err);
|
|
17
17
|
}
|
|
18
|
+
export function errorCode(err) {
|
|
19
|
+
if (err !== null &&
|
|
20
|
+
typeof err === 'object' &&
|
|
21
|
+
'code' in err &&
|
|
22
|
+
typeof err.code === 'string') {
|
|
23
|
+
return err.code;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
18
27
|
export function errorStatus(err) {
|
|
19
28
|
if (err !== null &&
|
|
20
29
|
typeof err === 'object' &&
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -18,17 +18,64 @@ export type ResolvedApiKey = {
|
|
|
18
18
|
export type ActiveApiKey = Extract<ResolvedApiKey, {
|
|
19
19
|
apiKey: string;
|
|
20
20
|
}>;
|
|
21
|
-
|
|
21
|
+
/** Old flat config shape (v0.2.x and earlier). */
|
|
22
|
+
export interface LegacyConfigFileShape {
|
|
22
23
|
apiKey?: string;
|
|
24
|
+
format?: 'html' | 'markdown';
|
|
25
|
+
projectId?: string;
|
|
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
|
+
};
|
|
23
39
|
}
|
|
40
|
+
export type ResolvedProjectId = {
|
|
41
|
+
path: string;
|
|
42
|
+
projectId: string;
|
|
43
|
+
source: 'global';
|
|
44
|
+
} | {
|
|
45
|
+
path: string;
|
|
46
|
+
projectId: string;
|
|
47
|
+
source: 'local';
|
|
48
|
+
} | {
|
|
49
|
+
projectId: string;
|
|
50
|
+
source: 'env';
|
|
51
|
+
} | {
|
|
52
|
+
source: 'none';
|
|
53
|
+
};
|
|
54
|
+
/** Alias — structurally identical to ResolvedApiKey; kept for semantic clarity at call sites. */
|
|
55
|
+
export type ResolvedPerplexityApiKey = ResolvedApiKey;
|
|
24
56
|
/**
|
|
25
57
|
* Path to global `config.json` under oclif's `this.config.configDir` (same rules as @oclif/core `Config.dir('config')` for `dirname` zurf).
|
|
26
58
|
*/
|
|
27
59
|
export declare function globalConfigFilePath(oclifConfigDir: string): string;
|
|
28
60
|
export declare function localConfigPathForCwd(cwd?: string): string;
|
|
29
61
|
export declare function findLocalConfigPath(startDir?: string): string | undefined;
|
|
62
|
+
export declare function readConfigFile(filePath: string): ConfigFileShape | undefined;
|
|
63
|
+
export declare function resolveFormat(options: {
|
|
64
|
+
cwd?: string;
|
|
65
|
+
flagHtml: boolean;
|
|
66
|
+
globalConfigDir: string;
|
|
67
|
+
}): 'html' | 'markdown';
|
|
30
68
|
export declare function resolveApiKey(options: {
|
|
31
69
|
cwd?: string;
|
|
32
70
|
globalConfigDir: string;
|
|
33
71
|
}): ResolvedApiKey;
|
|
72
|
+
export declare function resolveProjectId(options: {
|
|
73
|
+
cwd?: string;
|
|
74
|
+
globalConfigDir: string;
|
|
75
|
+
}): ResolvedProjectId;
|
|
76
|
+
export declare function resolvePerplexityApiKey(options: {
|
|
77
|
+
cwd?: string;
|
|
78
|
+
globalConfigDir: string;
|
|
79
|
+
}): ResolvedPerplexityApiKey;
|
|
34
80
|
export declare function writeApiKeyConfig(targetPath: string, apiKey: string): Promise<void>;
|
|
81
|
+
export declare function writeConfig(targetPath: string, fields: Partial<ConfigFileShape>): Promise<void>;
|
package/dist/lib/config.js
CHANGED
|
@@ -26,17 +26,84 @@ export function findLocalConfigPath(startDir = process.cwd()) {
|
|
|
26
26
|
}
|
|
27
27
|
return undefined;
|
|
28
28
|
}
|
|
29
|
-
|
|
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
59
|
const parsed = JSON.parse(raw);
|
|
33
|
-
|
|
34
|
-
|
|
60
|
+
if (isLegacyConfig(parsed)) {
|
|
61
|
+
return migrateLegacyConfig(parsed);
|
|
62
|
+
}
|
|
63
|
+
return parsed;
|
|
35
64
|
}
|
|
36
65
|
catch {
|
|
37
66
|
return undefined;
|
|
38
67
|
}
|
|
39
68
|
}
|
|
69
|
+
function readStringField(filePath, getter) {
|
|
70
|
+
const parsed = readConfigFile(filePath);
|
|
71
|
+
if (!parsed)
|
|
72
|
+
return undefined;
|
|
73
|
+
const val = getter(parsed)?.trim() ?? '';
|
|
74
|
+
return val.length > 0 ? val : undefined;
|
|
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);
|
|
79
|
+
function readFormatFromFile(filePath) {
|
|
80
|
+
const parsed = readConfigFile(filePath);
|
|
81
|
+
if (!parsed)
|
|
82
|
+
return undefined;
|
|
83
|
+
const fmt = parsed.format;
|
|
84
|
+
if (fmt === 'html' || fmt === 'markdown')
|
|
85
|
+
return fmt;
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
export function resolveFormat(options) {
|
|
89
|
+
if (options.flagHtml)
|
|
90
|
+
return 'html';
|
|
91
|
+
const envVal = process.env.ZURF_HTML?.trim().toLowerCase();
|
|
92
|
+
if (envVal === 'true' || envVal === '1')
|
|
93
|
+
return 'html';
|
|
94
|
+
const cwd = options.cwd ?? process.cwd();
|
|
95
|
+
const localPath = findLocalConfigPath(cwd);
|
|
96
|
+
if (localPath) {
|
|
97
|
+
const fmt = readFormatFromFile(localPath);
|
|
98
|
+
if (fmt)
|
|
99
|
+
return fmt;
|
|
100
|
+
}
|
|
101
|
+
const gPath = globalConfigFilePath(options.globalConfigDir);
|
|
102
|
+
const globalFmt = readFormatFromFile(gPath);
|
|
103
|
+
if (globalFmt)
|
|
104
|
+
return globalFmt;
|
|
105
|
+
return 'markdown';
|
|
106
|
+
}
|
|
40
107
|
export function resolveApiKey(options) {
|
|
41
108
|
const cwd = options.cwd ?? process.cwd();
|
|
42
109
|
const envKey = process.env.BROWSERBASE_API_KEY?.trim();
|
|
@@ -45,22 +112,98 @@ export function resolveApiKey(options) {
|
|
|
45
112
|
}
|
|
46
113
|
const localPath = findLocalConfigPath(cwd);
|
|
47
114
|
if (localPath) {
|
|
48
|
-
const key =
|
|
115
|
+
const key = readBrowserbaseApiKeyFromFile(localPath);
|
|
116
|
+
if (key) {
|
|
117
|
+
return { apiKey: key, path: localPath, source: 'local' };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const gPath = globalConfigFilePath(options.globalConfigDir);
|
|
121
|
+
const globalKey = readBrowserbaseApiKeyFromFile(gPath);
|
|
122
|
+
if (globalKey) {
|
|
123
|
+
return { apiKey: globalKey, path: gPath, source: 'global' };
|
|
124
|
+
}
|
|
125
|
+
return { source: 'none' };
|
|
126
|
+
}
|
|
127
|
+
export function resolveProjectId(options) {
|
|
128
|
+
const cwd = options.cwd ?? process.cwd();
|
|
129
|
+
const envId = process.env.BROWSERBASE_PROJECT_ID?.trim();
|
|
130
|
+
if (envId) {
|
|
131
|
+
return { projectId: envId, source: 'env' };
|
|
132
|
+
}
|
|
133
|
+
const localPath = findLocalConfigPath(cwd);
|
|
134
|
+
if (localPath) {
|
|
135
|
+
const id = readBrowserbaseProjectIdFromFile(localPath);
|
|
136
|
+
if (id) {
|
|
137
|
+
return { path: localPath, projectId: id, source: 'local' };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const gPath = globalConfigFilePath(options.globalConfigDir);
|
|
141
|
+
const globalId = readBrowserbaseProjectIdFromFile(gPath);
|
|
142
|
+
if (globalId) {
|
|
143
|
+
return { path: gPath, projectId: globalId, source: 'global' };
|
|
144
|
+
}
|
|
145
|
+
return { source: 'none' };
|
|
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);
|
|
49
156
|
if (key) {
|
|
50
157
|
return { apiKey: key, path: localPath, source: 'local' };
|
|
51
158
|
}
|
|
52
159
|
}
|
|
53
160
|
const gPath = globalConfigFilePath(options.globalConfigDir);
|
|
54
|
-
const globalKey =
|
|
161
|
+
const globalKey = readPerplexityApiKeyFromFile(gPath);
|
|
55
162
|
if (globalKey) {
|
|
56
163
|
return { apiKey: globalKey, path: gPath, source: 'global' };
|
|
57
164
|
}
|
|
58
165
|
return { source: 'none' };
|
|
59
166
|
}
|
|
60
167
|
export async function writeApiKeyConfig(targetPath, apiKey) {
|
|
168
|
+
await writeConfig(targetPath, { providers: { browserbase: { apiKey: apiKey.trim() } } });
|
|
169
|
+
}
|
|
170
|
+
export async function writeConfig(targetPath, fields) {
|
|
61
171
|
const dir = path.dirname(targetPath);
|
|
62
172
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
63
|
-
|
|
64
|
-
|
|
173
|
+
let existing = {};
|
|
174
|
+
try {
|
|
175
|
+
const raw = await fs.promises.readFile(targetPath, 'utf8');
|
|
176
|
+
const parsed = JSON.parse(raw);
|
|
177
|
+
existing = isLegacyConfig(parsed) ? migrateLegacyConfig(parsed) : parsed;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// file doesn't exist yet — start fresh
|
|
181
|
+
}
|
|
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
|
+
}
|
|
207
|
+
const body = `${JSON.stringify(merged, null, 2)}\n`;
|
|
65
208
|
await fs.promises.writeFile(targetPath, body, { encoding: 'utf8', mode: 0o600 });
|
|
66
209
|
}
|
|
@@ -6,10 +6,11 @@ export type FetchResponseForDisplay = {
|
|
|
6
6
|
id: string;
|
|
7
7
|
statusCode: number;
|
|
8
8
|
};
|
|
9
|
-
export declare function buildFetchJsonPayload(response: FetchResponseForDisplay): {
|
|
9
|
+
export declare function buildFetchJsonPayload(response: FetchResponseForDisplay, format: 'html' | 'markdown'): {
|
|
10
10
|
content: string;
|
|
11
11
|
contentType: string;
|
|
12
12
|
encoding: string;
|
|
13
|
+
format: 'html' | 'markdown';
|
|
13
14
|
headers: Record<string, string>;
|
|
14
15
|
id: string;
|
|
15
16
|
statusCode: number;
|
package/dist/lib/fetch-output.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export function buildFetchJsonPayload(response) {
|
|
1
|
+
export function buildFetchJsonPayload(response, format) {
|
|
2
2
|
return {
|
|
3
3
|
content: response.content,
|
|
4
4
|
contentType: response.contentType,
|
|
5
5
|
encoding: response.encoding,
|
|
6
|
+
format,
|
|
6
7
|
headers: response.headers,
|
|
7
8
|
id: response.id,
|
|
8
9
|
statusCode: response.statusCode,
|
|
@@ -19,5 +20,5 @@ export function humanFetchMetaLines(response) {
|
|
|
19
20
|
/** ~8k chars keeps terminal scrollback usable while showing most HTML pages in preview. */
|
|
20
21
|
export const HUMAN_BODY_PREVIEW_CHARS = 8000;
|
|
21
22
|
export function truncateNote(totalChars) {
|
|
22
|
-
return `… truncated (${totalChars} chars). Use --output FILE to save the full
|
|
23
|
+
return `… truncated (${totalChars} chars). Use --output FILE to save the full content (within the 1 MB Fetch limit).`;
|
|
23
24
|
}
|
package/dist/lib/flags.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** Machine-readable output; kept separate from oclif `enableJsonFlag` so error payloads match zurf's stdout JSON contract. */
|
|
2
2
|
export declare const zurfJsonFlag: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
3
|
+
/** Output raw HTML instead of the default markdown conversion. */
|
|
4
|
+
export declare const zurfHtmlFlag: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
3
5
|
/** Shared flags for commands that support JSON output (no `--api-key` — use BROWSERBASE_API_KEY or config files). */
|
|
4
6
|
export declare const zurfBaseFlags: {
|
|
7
|
+
readonly html: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
5
8
|
readonly json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
9
|
};
|
package/dist/lib/flags.js
CHANGED
|
@@ -4,7 +4,14 @@ export const zurfJsonFlag = Flags.boolean({
|
|
|
4
4
|
description: 'Print machine-readable JSON to stdout',
|
|
5
5
|
env: 'ZURF_JSON',
|
|
6
6
|
});
|
|
7
|
+
/** Output raw HTML instead of the default markdown conversion. */
|
|
8
|
+
export const zurfHtmlFlag = Flags.boolean({
|
|
9
|
+
default: false,
|
|
10
|
+
description: 'Output raw HTML instead of markdown',
|
|
11
|
+
env: 'ZURF_HTML',
|
|
12
|
+
});
|
|
7
13
|
/** Shared flags for commands that support JSON output (no `--api-key` — use BROWSERBASE_API_KEY or config files). */
|
|
8
14
|
export const zurfBaseFlags = {
|
|
15
|
+
html: zurfHtmlFlag,
|
|
9
16
|
json: zurfJsonFlag,
|
|
10
17
|
};
|