@vibemastery/zurf 0.1.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -485
- 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/fetch/index.d.ts +1 -0
- package/dist/commands/fetch/index.js +14 -15
- package/dist/commands/init/index.d.ts +3 -0
- package/dist/commands/init/index.js +26 -5
- package/dist/commands/search/index.d.ts +1 -0
- package/dist/lib/browse-output.d.ts +12 -0
- package/dist/lib/browse-output.js +10 -0
- 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 +26 -0
- package/dist/lib/config.js +77 -6
- 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/oclif.manifest.json +157 -64
- package/package.json +7 -5
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { HUMAN_BODY_PREVIEW_CHARS, humanBrowseMetaLines, truncateNote, } from '../../lib/browse-output.js';
|
|
5
|
+
import { withBrowserbaseSession } from '../../lib/browserbase-session.js';
|
|
6
|
+
import { cliError, errorCode } from '../../lib/cli-errors.js';
|
|
7
|
+
import { resolveFormat, resolveProjectId } from '../../lib/config.js';
|
|
8
|
+
import { zurfBaseFlags } from '../../lib/flags.js';
|
|
9
|
+
import { htmlToMarkdown } from '../../lib/html-to-markdown.js';
|
|
10
|
+
import { printJson } from '../../lib/json-output.js';
|
|
11
|
+
import { ZurfBrowserbaseCommand } from '../../lib/zurf-browserbase-command.js';
|
|
12
|
+
export default class Browse extends ZurfBrowserbaseCommand {
|
|
13
|
+
static args = {
|
|
14
|
+
url: Args.string({
|
|
15
|
+
description: 'URL to browse',
|
|
16
|
+
required: true,
|
|
17
|
+
}),
|
|
18
|
+
};
|
|
19
|
+
static description = `Browse a URL in a cloud browser and return the rendered content as markdown (default) or raw HTML.
|
|
20
|
+
Uses a real Chromium browser via Browserbase, so JavaScript-heavy pages are fully rendered.
|
|
21
|
+
Requires authentication and a Project ID. Run \`zurf init --global\` before first use.`;
|
|
22
|
+
static examples = [
|
|
23
|
+
'<%= config.bin %> <%= command.id %> https://example.com',
|
|
24
|
+
'<%= config.bin %> <%= command.id %> https://example.com --html',
|
|
25
|
+
'<%= config.bin %> <%= command.id %> https://example.com --json',
|
|
26
|
+
'<%= config.bin %> <%= command.id %> https://example.com -o page.md',
|
|
27
|
+
];
|
|
28
|
+
static flags = {
|
|
29
|
+
...zurfBaseFlags,
|
|
30
|
+
output: Flags.string({
|
|
31
|
+
char: 'o',
|
|
32
|
+
description: 'Write rendered content to this file (full content); otherwise human mode prints a truncated preview to stdout',
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
static summary = 'Browse a URL in a cloud browser and return rendered content as markdown';
|
|
36
|
+
async run() {
|
|
37
|
+
const { args, flags } = await this.parse(Browse);
|
|
38
|
+
const url = args.url.trim();
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = new URL(url);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
cliError({ command: this, exitCode: 2, json: flags.json, message: `Invalid URL: ${url}` });
|
|
45
|
+
}
|
|
46
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
47
|
+
cliError({
|
|
48
|
+
command: this,
|
|
49
|
+
exitCode: 2,
|
|
50
|
+
json: flags.json,
|
|
51
|
+
message: `Only http and https URLs are supported: ${url}`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
if (!parsed.hostname) {
|
|
55
|
+
cliError({ command: this, exitCode: 2, json: flags.json, message: `Invalid URL: ${url}` });
|
|
56
|
+
}
|
|
57
|
+
await this.runWithBrowserbase(flags, `Browsing ${url}`, async (client) => {
|
|
58
|
+
const resolution = resolveProjectId({ globalConfigDir: this.config.configDir });
|
|
59
|
+
if (resolution.source === 'none') {
|
|
60
|
+
throw new Error('No Browserbase Project ID found. Set BROWSERBASE_PROJECT_ID, run `zurf init --global` with --project-id, or add projectId to your .zurf/config.json.');
|
|
61
|
+
}
|
|
62
|
+
const { projectId } = resolution;
|
|
63
|
+
const result = await withBrowserbaseSession({
|
|
64
|
+
client,
|
|
65
|
+
projectId,
|
|
66
|
+
async work(page) {
|
|
67
|
+
const response = await page.goto(url, { timeout: 30_000, waitUntil: 'networkidle' });
|
|
68
|
+
const statusCode = response?.status() ?? null;
|
|
69
|
+
const content = await page.content();
|
|
70
|
+
return { content, statusCode };
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const format = resolveFormat({ flagHtml: flags.html, globalConfigDir: this.config.configDir });
|
|
74
|
+
const content = format === 'markdown' ? await htmlToMarkdown(result.content) : result.content;
|
|
75
|
+
if (flags.json) {
|
|
76
|
+
const payload = { content, format, statusCode: result.statusCode, url };
|
|
77
|
+
printJson(payload);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.logToStderr(humanBrowseMetaLines({ statusCode: result.statusCode, url }).join('\n'));
|
|
81
|
+
this.logToStderr('');
|
|
82
|
+
if (flags.output) {
|
|
83
|
+
try {
|
|
84
|
+
await fs.writeFile(flags.output, content, 'utf8');
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (errorCode(error) === 'ENOENT') {
|
|
88
|
+
cliError({
|
|
89
|
+
command: this,
|
|
90
|
+
exitCode: 1,
|
|
91
|
+
json: flags.json,
|
|
92
|
+
message: `Directory does not exist for output file: ${path.dirname(path.resolve(flags.output))}`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
this.logToStderr(`Wrote rendered content to ${flags.output}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (content.length <= HUMAN_BODY_PREVIEW_CHARS) {
|
|
101
|
+
this.log(content);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
this.log(content.slice(0, HUMAN_BODY_PREVIEW_CHARS));
|
|
105
|
+
this.log('');
|
|
106
|
+
this.logToStderr(truncateNote(content.length));
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -3,6 +3,7 @@ export default class ConfigWhich extends Command {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
html: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
7
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
7
8
|
};
|
|
8
9
|
static summary: string;
|
|
@@ -10,6 +10,7 @@ export default class Fetch extends ZurfBrowserbaseCommand {
|
|
|
10
10
|
'allow-redirects': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
output: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
proxies: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
|
+
html: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
15
|
};
|
|
15
16
|
static summary: string;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { cliError } from '../../lib/cli-errors.js';
|
|
4
|
+
import { cliError, errorCode } from '../../lib/cli-errors.js';
|
|
5
|
+
import { resolveFormat } from '../../lib/config.js';
|
|
5
6
|
import { buildFetchJsonPayload, HUMAN_BODY_PREVIEW_CHARS, humanFetchMetaLines, truncateNote, } from '../../lib/fetch-output.js';
|
|
6
7
|
import { zurfBaseFlags } from '../../lib/flags.js';
|
|
8
|
+
import { htmlToMarkdown } from '../../lib/html-to-markdown.js';
|
|
7
9
|
import { printJson } from '../../lib/json-output.js';
|
|
8
10
|
import { ZurfBrowserbaseCommand } from '../../lib/zurf-browserbase-command.js';
|
|
9
11
|
export default class Fetch extends ZurfBrowserbaseCommand {
|
|
@@ -13,12 +15,13 @@ export default class Fetch extends ZurfBrowserbaseCommand {
|
|
|
13
15
|
required: true,
|
|
14
16
|
}),
|
|
15
17
|
};
|
|
16
|
-
static description = `Fetch a URL via Browserbase (no browser session; static HTML, 1 MB max).
|
|
18
|
+
static description = `Fetch a URL via Browserbase and return content as markdown (default) or raw HTML (no browser session; static HTML, 1 MB max).
|
|
17
19
|
Requires authentication. Run \`zurf init --global\` or use a project key before first use.`;
|
|
18
20
|
static examples = [
|
|
19
21
|
'<%= config.bin %> <%= command.id %> https://example.com',
|
|
22
|
+
'<%= config.bin %> <%= command.id %> https://example.com --html',
|
|
20
23
|
'<%= config.bin %> <%= command.id %> https://example.com --json',
|
|
21
|
-
'<%= config.bin %> <%= command.id %> https://example.com -o page.
|
|
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,9 +7,12 @@ export default class Init extends Command {
|
|
|
7
7
|
gitignore: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
8
8
|
global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
9
|
local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
'project-id': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
html: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
12
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
13
|
};
|
|
12
14
|
static summary: string;
|
|
13
15
|
run(): Promise<void>;
|
|
14
16
|
private readApiKeyForInit;
|
|
17
|
+
private readProjectIdForInit;
|
|
15
18
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs/promises';
|
|
3
3
|
import { cliError, errorMessage } from '../../lib/cli-errors.js';
|
|
4
|
-
import { globalConfigFilePath, localConfigPathForCwd,
|
|
4
|
+
import { globalConfigFilePath, localConfigPathForCwd, writeConfig } from '../../lib/config.js';
|
|
5
5
|
import { zurfBaseFlags } from '../../lib/flags.js';
|
|
6
6
|
import { dotGitignoreMentionsZurf, ensureZurfGitignoreEntry, gitignorePathForCwd, } from '../../lib/gitignore-zurf.js';
|
|
7
7
|
import { promptLine, readStdinIfPiped } from '../../lib/init-input.js';
|
|
8
8
|
import { printJson } from '../../lib/json-output.js';
|
|
9
9
|
export default class Init extends Command {
|
|
10
|
-
static description = `Save your Browserbase API key to global or project config.
|
|
10
|
+
static description = `Save your Browserbase API key and optional Project ID to global or project config.
|
|
11
11
|
Global path follows oclif config (same as \`zurf config which\`).`;
|
|
12
12
|
static examples = [
|
|
13
13
|
'<%= config.bin %> <%= command.id %> --global',
|
|
14
14
|
'<%= config.bin %> <%= command.id %> --local',
|
|
15
|
+
'<%= config.bin %> <%= command.id %> --global --api-key KEY --project-id PROJ_ID',
|
|
15
16
|
'printenv BROWSERBASE_API_KEY | <%= config.bin %> <%= command.id %> --global',
|
|
16
17
|
];
|
|
17
18
|
static flags = {
|
|
@@ -30,16 +31,23 @@ Global path follows oclif config (same as \`zurf config which\`).`;
|
|
|
30
31
|
description: 'Store API key in ./.zurf/config.json for this directory',
|
|
31
32
|
exactlyOne: ['global', 'local'],
|
|
32
33
|
}),
|
|
34
|
+
'project-id': Flags.string({
|
|
35
|
+
description: 'Browserbase Project ID (optional; needed for browse, screenshot, pdf commands)',
|
|
36
|
+
}),
|
|
33
37
|
};
|
|
34
|
-
static summary = 'Configure Browserbase API key storage';
|
|
38
|
+
static summary = 'Configure Browserbase API key and Project ID storage';
|
|
35
39
|
async run() {
|
|
36
40
|
const { flags } = await this.parse(Init);
|
|
37
41
|
const apiKey = await this.readApiKeyForInit(flags);
|
|
38
42
|
const targetPath = flags.global
|
|
39
43
|
? globalConfigFilePath(this.config.configDir)
|
|
40
44
|
: localConfigPathForCwd();
|
|
45
|
+
const projectId = await this.readProjectIdForInit(flags);
|
|
46
|
+
const configUpdate = { apiKey: apiKey.trim() };
|
|
47
|
+
if (projectId)
|
|
48
|
+
configUpdate.projectId = projectId;
|
|
41
49
|
try {
|
|
42
|
-
await
|
|
50
|
+
await writeConfig(targetPath, configUpdate);
|
|
43
51
|
}
|
|
44
52
|
catch (error) {
|
|
45
53
|
cliError({ command: this, exitCode: 1, json: flags.json, message: errorMessage(error) });
|
|
@@ -53,10 +61,16 @@ Global path follows oclif config (same as \`zurf config which\`).`;
|
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
63
|
if (flags.json) {
|
|
56
|
-
|
|
64
|
+
const payload = { ok: true, path: targetPath, scope: flags.global ? 'global' : 'local' };
|
|
65
|
+
if (projectId)
|
|
66
|
+
payload.projectId = true;
|
|
67
|
+
printJson(payload);
|
|
57
68
|
}
|
|
58
69
|
else {
|
|
59
70
|
this.log(`Saved API key to ${targetPath}`);
|
|
71
|
+
if (projectId) {
|
|
72
|
+
this.log(`Saved Project ID to ${targetPath}`);
|
|
73
|
+
}
|
|
60
74
|
if (flags.local) {
|
|
61
75
|
let showTip = true;
|
|
62
76
|
try {
|
|
@@ -92,4 +106,11 @@ Global path follows oclif config (same as \`zurf config which\`).`;
|
|
|
92
106
|
}
|
|
93
107
|
return apiKey;
|
|
94
108
|
}
|
|
109
|
+
async readProjectIdForInit(flags) {
|
|
110
|
+
let projectId = flags['project-id']?.trim();
|
|
111
|
+
if (!projectId && !flags.json && process.stdin.isTTY) {
|
|
112
|
+
projectId = await promptLine('Browserbase Project ID (optional, press Enter to skip): ');
|
|
113
|
+
}
|
|
114
|
+
return projectId || undefined;
|
|
115
|
+
}
|
|
95
116
|
}
|
|
@@ -7,6 +7,7 @@ export default class Search extends ZurfBrowserbaseCommand {
|
|
|
7
7
|
static examples: string[];
|
|
8
8
|
static flags: {
|
|
9
9
|
'num-results': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
html: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
10
11
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
12
|
};
|
|
12
13
|
static summary: string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface BrowseJsonPayload {
|
|
2
|
+
content: string;
|
|
3
|
+
format: 'html' | 'markdown';
|
|
4
|
+
statusCode: null | number;
|
|
5
|
+
url: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function humanBrowseMetaLines(options: {
|
|
8
|
+
statusCode: null | number;
|
|
9
|
+
url: string;
|
|
10
|
+
}): string[];
|
|
11
|
+
export declare const HUMAN_BODY_PREVIEW_CHARS = 8000;
|
|
12
|
+
export declare function truncateNote(totalChars: number): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function humanBrowseMetaLines(options) {
|
|
2
|
+
return [
|
|
3
|
+
`url: ${options.url}`,
|
|
4
|
+
`statusCode: ${options.statusCode ?? 'unknown'}`,
|
|
5
|
+
];
|
|
6
|
+
}
|
|
7
|
+
export const HUMAN_BODY_PREVIEW_CHARS = 8000;
|
|
8
|
+
export function truncateNote(totalChars) {
|
|
9
|
+
return `… truncated (${totalChars} chars). Use --output FILE to save the full content.`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,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
|
@@ -20,15 +20,41 @@ export type ActiveApiKey = Extract<ResolvedApiKey, {
|
|
|
20
20
|
}>;
|
|
21
21
|
export interface ConfigFileShape {
|
|
22
22
|
apiKey?: string;
|
|
23
|
+
format?: 'html' | 'markdown';
|
|
24
|
+
projectId?: string;
|
|
23
25
|
}
|
|
26
|
+
export type ResolvedProjectId = {
|
|
27
|
+
path: string;
|
|
28
|
+
projectId: string;
|
|
29
|
+
source: 'global';
|
|
30
|
+
} | {
|
|
31
|
+
path: string;
|
|
32
|
+
projectId: string;
|
|
33
|
+
source: 'local';
|
|
34
|
+
} | {
|
|
35
|
+
projectId: string;
|
|
36
|
+
source: 'env';
|
|
37
|
+
} | {
|
|
38
|
+
source: 'none';
|
|
39
|
+
};
|
|
24
40
|
/**
|
|
25
41
|
* Path to global `config.json` under oclif's `this.config.configDir` (same rules as @oclif/core `Config.dir('config')` for `dirname` zurf).
|
|
26
42
|
*/
|
|
27
43
|
export declare function globalConfigFilePath(oclifConfigDir: string): string;
|
|
28
44
|
export declare function localConfigPathForCwd(cwd?: string): string;
|
|
29
45
|
export declare function findLocalConfigPath(startDir?: string): string | undefined;
|
|
46
|
+
export declare function resolveFormat(options: {
|
|
47
|
+
cwd?: string;
|
|
48
|
+
flagHtml: boolean;
|
|
49
|
+
globalConfigDir: string;
|
|
50
|
+
}): 'html' | 'markdown';
|
|
30
51
|
export declare function resolveApiKey(options: {
|
|
31
52
|
cwd?: string;
|
|
32
53
|
globalConfigDir: string;
|
|
33
54
|
}): ResolvedApiKey;
|
|
55
|
+
export declare function resolveProjectId(options: {
|
|
56
|
+
cwd?: string;
|
|
57
|
+
globalConfigDir: string;
|
|
58
|
+
}): ResolvedProjectId;
|
|
34
59
|
export declare function writeApiKeyConfig(targetPath: string, apiKey: string): Promise<void>;
|
|
60
|
+
export declare function writeConfig(targetPath: string, fields: Partial<ConfigFileShape>): Promise<void>;
|
package/dist/lib/config.js
CHANGED
|
@@ -26,17 +26,57 @@ export function findLocalConfigPath(startDir = process.cwd()) {
|
|
|
26
26
|
}
|
|
27
27
|
return undefined;
|
|
28
28
|
}
|
|
29
|
-
function
|
|
29
|
+
function readConfigFile(filePath) {
|
|
30
30
|
try {
|
|
31
31
|
const raw = fs.readFileSync(filePath, 'utf8');
|
|
32
|
-
|
|
33
|
-
const key = typeof parsed.apiKey === 'string' ? parsed.apiKey.trim() : '';
|
|
34
|
-
return key.length > 0 ? key : undefined;
|
|
32
|
+
return JSON.parse(raw);
|
|
35
33
|
}
|
|
36
34
|
catch {
|
|
37
35
|
return undefined;
|
|
38
36
|
}
|
|
39
37
|
}
|
|
38
|
+
function readApiKeyFromFile(filePath) {
|
|
39
|
+
const parsed = readConfigFile(filePath);
|
|
40
|
+
if (!parsed)
|
|
41
|
+
return undefined;
|
|
42
|
+
const key = typeof parsed.apiKey === 'string' ? parsed.apiKey.trim() : '';
|
|
43
|
+
return key.length > 0 ? key : undefined;
|
|
44
|
+
}
|
|
45
|
+
function readProjectIdFromFile(filePath) {
|
|
46
|
+
const parsed = readConfigFile(filePath);
|
|
47
|
+
if (!parsed)
|
|
48
|
+
return undefined;
|
|
49
|
+
const id = typeof parsed.projectId === 'string' ? parsed.projectId.trim() : '';
|
|
50
|
+
return id.length > 0 ? id : undefined;
|
|
51
|
+
}
|
|
52
|
+
function readFormatFromFile(filePath) {
|
|
53
|
+
const parsed = readConfigFile(filePath);
|
|
54
|
+
if (!parsed)
|
|
55
|
+
return undefined;
|
|
56
|
+
const fmt = parsed.format;
|
|
57
|
+
if (fmt === 'html' || fmt === 'markdown')
|
|
58
|
+
return fmt;
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
export function resolveFormat(options) {
|
|
62
|
+
if (options.flagHtml)
|
|
63
|
+
return 'html';
|
|
64
|
+
const envVal = process.env.ZURF_HTML?.trim().toLowerCase();
|
|
65
|
+
if (envVal === 'true' || envVal === '1')
|
|
66
|
+
return 'html';
|
|
67
|
+
const cwd = options.cwd ?? process.cwd();
|
|
68
|
+
const localPath = findLocalConfigPath(cwd);
|
|
69
|
+
if (localPath) {
|
|
70
|
+
const fmt = readFormatFromFile(localPath);
|
|
71
|
+
if (fmt)
|
|
72
|
+
return fmt;
|
|
73
|
+
}
|
|
74
|
+
const gPath = globalConfigFilePath(options.globalConfigDir);
|
|
75
|
+
const globalFmt = readFormatFromFile(gPath);
|
|
76
|
+
if (globalFmt)
|
|
77
|
+
return globalFmt;
|
|
78
|
+
return 'markdown';
|
|
79
|
+
}
|
|
40
80
|
export function resolveApiKey(options) {
|
|
41
81
|
const cwd = options.cwd ?? process.cwd();
|
|
42
82
|
const envKey = process.env.BROWSERBASE_API_KEY?.trim();
|
|
@@ -57,10 +97,41 @@ export function resolveApiKey(options) {
|
|
|
57
97
|
}
|
|
58
98
|
return { source: 'none' };
|
|
59
99
|
}
|
|
100
|
+
export function resolveProjectId(options) {
|
|
101
|
+
const cwd = options.cwd ?? process.cwd();
|
|
102
|
+
const envId = process.env.BROWSERBASE_PROJECT_ID?.trim();
|
|
103
|
+
if (envId) {
|
|
104
|
+
return { projectId: envId, source: 'env' };
|
|
105
|
+
}
|
|
106
|
+
const localPath = findLocalConfigPath(cwd);
|
|
107
|
+
if (localPath) {
|
|
108
|
+
const id = readProjectIdFromFile(localPath);
|
|
109
|
+
if (id) {
|
|
110
|
+
return { path: localPath, projectId: id, source: 'local' };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const gPath = globalConfigFilePath(options.globalConfigDir);
|
|
114
|
+
const globalId = readProjectIdFromFile(gPath);
|
|
115
|
+
if (globalId) {
|
|
116
|
+
return { path: gPath, projectId: globalId, source: 'global' };
|
|
117
|
+
}
|
|
118
|
+
return { source: 'none' };
|
|
119
|
+
}
|
|
60
120
|
export async function writeApiKeyConfig(targetPath, apiKey) {
|
|
121
|
+
await writeConfig(targetPath, { apiKey: apiKey.trim() });
|
|
122
|
+
}
|
|
123
|
+
export async function writeConfig(targetPath, fields) {
|
|
61
124
|
const dir = path.dirname(targetPath);
|
|
62
125
|
await fs.promises.mkdir(dir, { recursive: true });
|
|
63
|
-
|
|
64
|
-
|
|
126
|
+
let existing = {};
|
|
127
|
+
try {
|
|
128
|
+
const raw = await fs.promises.readFile(targetPath, 'utf8');
|
|
129
|
+
existing = JSON.parse(raw);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// file doesn't exist yet — start fresh
|
|
133
|
+
}
|
|
134
|
+
const merged = { ...existing, ...fields };
|
|
135
|
+
const body = `${JSON.stringify(merged, null, 2)}\n`;
|
|
65
136
|
await fs.promises.writeFile(targetPath, body, { encoding: 'utf8', mode: 0o600 });
|
|
66
137
|
}
|
|
@@ -6,10 +6,11 @@ export type FetchResponseForDisplay = {
|
|
|
6
6
|
id: string;
|
|
7
7
|
statusCode: number;
|
|
8
8
|
};
|
|
9
|
-
export declare function buildFetchJsonPayload(response: FetchResponseForDisplay): {
|
|
9
|
+
export declare function buildFetchJsonPayload(response: FetchResponseForDisplay, format: 'html' | 'markdown'): {
|
|
10
10
|
content: string;
|
|
11
11
|
contentType: string;
|
|
12
12
|
encoding: string;
|
|
13
|
+
format: 'html' | 'markdown';
|
|
13
14
|
headers: Record<string, string>;
|
|
14
15
|
id: string;
|
|
15
16
|
statusCode: number;
|
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
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function htmlToMarkdown(html: string): Promise<string>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
let cached;
|
|
2
|
+
async function getService() {
|
|
3
|
+
if (cached)
|
|
4
|
+
return cached;
|
|
5
|
+
const { default: Turndown } = await import('turndown');
|
|
6
|
+
const service = new Turndown({
|
|
7
|
+
codeBlockStyle: 'fenced',
|
|
8
|
+
headingStyle: 'atx',
|
|
9
|
+
});
|
|
10
|
+
service.remove(['script', 'style', 'iframe', 'noscript']);
|
|
11
|
+
service.addRule('remove-svg', {
|
|
12
|
+
filter: (node) => node.nodeName.toLowerCase() === 'svg',
|
|
13
|
+
replacement: () => '',
|
|
14
|
+
});
|
|
15
|
+
cached = service;
|
|
16
|
+
return service;
|
|
17
|
+
}
|
|
18
|
+
export async function htmlToMarkdown(html) {
|
|
19
|
+
const service = await getService();
|
|
20
|
+
return service.turndown(html);
|
|
21
|
+
}
|