@vibemastery/zurf 0.1.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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +541 -0
  3. package/bin/dev.cmd +3 -0
  4. package/bin/dev.js +28 -0
  5. package/bin/run.cmd +3 -0
  6. package/bin/run.js +17 -0
  7. package/dist/commands/config/which.d.ts +10 -0
  8. package/dist/commands/config/which.js +61 -0
  9. package/dist/commands/fetch/index.d.ts +17 -0
  10. package/dist/commands/fetch/index.js +111 -0
  11. package/dist/commands/init/index.d.ts +15 -0
  12. package/dist/commands/init/index.js +95 -0
  13. package/dist/commands/search/index.d.ts +14 -0
  14. package/dist/commands/search/index.js +59 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +1 -0
  17. package/dist/lib/browserbase-client.d.ts +12 -0
  18. package/dist/lib/browserbase-client.js +16 -0
  19. package/dist/lib/browserbase-command.d.ts +8 -0
  20. package/dist/lib/browserbase-command.js +16 -0
  21. package/dist/lib/cli-errors.d.ts +19 -0
  22. package/dist/lib/cli-errors.js +36 -0
  23. package/dist/lib/config.d.ts +34 -0
  24. package/dist/lib/config.js +66 -0
  25. package/dist/lib/fetch-output.d.ts +20 -0
  26. package/dist/lib/fetch-output.js +23 -0
  27. package/dist/lib/flags.d.ts +6 -0
  28. package/dist/lib/flags.js +10 -0
  29. package/dist/lib/gitignore-zurf.d.ts +5 -0
  30. package/dist/lib/gitignore-zurf.js +45 -0
  31. package/dist/lib/init-input.d.ts +2 -0
  32. package/dist/lib/init-input.js +26 -0
  33. package/dist/lib/json-output.d.ts +3 -0
  34. package/dist/lib/json-output.js +11 -0
  35. package/dist/lib/search-output.d.ts +16 -0
  36. package/dist/lib/search-output.js +20 -0
  37. package/dist/lib/zurf-browserbase-command.d.ts +8 -0
  38. package/dist/lib/zurf-browserbase-command.js +36 -0
  39. package/oclif.manifest.json +213 -0
  40. package/package.json +90 -0
@@ -0,0 +1,111 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import * as fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { cliError } from '../../lib/cli-errors.js';
5
+ import { buildFetchJsonPayload, HUMAN_BODY_PREVIEW_CHARS, humanFetchMetaLines, truncateNote, } from '../../lib/fetch-output.js';
6
+ import { zurfBaseFlags } from '../../lib/flags.js';
7
+ import { printJson } from '../../lib/json-output.js';
8
+ import { ZurfBrowserbaseCommand } from '../../lib/zurf-browserbase-command.js';
9
+ export default class Fetch extends ZurfBrowserbaseCommand {
10
+ static args = {
11
+ url: Args.string({
12
+ description: 'URL to fetch',
13
+ required: true,
14
+ }),
15
+ };
16
+ static description = `Fetch a URL via Browserbase (no browser session; static HTML, 1 MB max).
17
+ Requires authentication. Run \`zurf init --global\` or use a project key before first use.`;
18
+ static examples = [
19
+ '<%= config.bin %> <%= command.id %> https://example.com',
20
+ '<%= config.bin %> <%= command.id %> https://example.com --json',
21
+ '<%= config.bin %> <%= command.id %> https://example.com -o page.html --proxies',
22
+ ];
23
+ static flags = {
24
+ ...zurfBaseFlags,
25
+ 'allow-insecure-ssl': Flags.boolean({
26
+ default: false,
27
+ description: 'Disable TLS certificate verification (use only if you trust the target)',
28
+ }),
29
+ 'allow-redirects': Flags.boolean({
30
+ default: false,
31
+ description: 'Follow HTTP redirects',
32
+ }),
33
+ output: Flags.string({
34
+ char: 'o',
35
+ description: 'Write response body to this file (full content); otherwise human mode prints a truncated preview to stdout',
36
+ }),
37
+ proxies: Flags.boolean({
38
+ default: false,
39
+ description: 'Route through Browserbase proxies (helps with some blocked sites)',
40
+ }),
41
+ };
42
+ static summary = 'Fetch a URL via Browserbase';
43
+ async run() {
44
+ const { args, flags } = await this.parse(Fetch);
45
+ const url = args.url.trim();
46
+ let parsed;
47
+ try {
48
+ parsed = new URL(url);
49
+ }
50
+ catch {
51
+ cliError({ command: this, exitCode: 2, json: flags.json, message: `Invalid URL: ${url}` });
52
+ }
53
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
54
+ cliError({
55
+ command: this,
56
+ exitCode: 2,
57
+ json: flags.json,
58
+ message: `Only http and https URLs are supported: ${url}`,
59
+ });
60
+ }
61
+ if (!parsed.hostname) {
62
+ cliError({ command: this, exitCode: 2, json: flags.json, message: `Invalid URL: ${url}` });
63
+ }
64
+ await this.runWithBrowserbase(flags, 'Fetching URL', async (client) => {
65
+ const response = await client.fetchAPI.create({
66
+ allowInsecureSsl: flags['allow-insecure-ssl'],
67
+ allowRedirects: flags['allow-redirects'],
68
+ proxies: flags.proxies,
69
+ url,
70
+ });
71
+ if (flags.json) {
72
+ printJson(buildFetchJsonPayload(response));
73
+ return;
74
+ }
75
+ this.logToStderr(humanFetchMetaLines(response).join('\n'));
76
+ this.logToStderr('');
77
+ if (flags.output) {
78
+ try {
79
+ await fs.writeFile(flags.output, response.content, 'utf8');
80
+ }
81
+ catch (error) {
82
+ const code = error !== null &&
83
+ typeof error === 'object' &&
84
+ 'code' in error &&
85
+ typeof error.code === 'string'
86
+ ? error.code
87
+ : undefined;
88
+ if (code === 'ENOENT') {
89
+ cliError({
90
+ command: this,
91
+ exitCode: 1,
92
+ json: flags.json,
93
+ message: `Directory does not exist for output file: ${path.dirname(path.resolve(flags.output))}`,
94
+ });
95
+ }
96
+ throw error;
97
+ }
98
+ this.logToStderr(`Wrote body to ${flags.output}`);
99
+ return;
100
+ }
101
+ const { content } = response;
102
+ if (content.length <= HUMAN_BODY_PREVIEW_CHARS) {
103
+ this.log(content);
104
+ return;
105
+ }
106
+ this.log(content.slice(0, HUMAN_BODY_PREVIEW_CHARS));
107
+ this.log('');
108
+ this.logToStderr(truncateNote(content.length));
109
+ });
110
+ }
111
+ }
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Init extends Command {
3
+ static description: string;
4
+ static examples: string[];
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
+ global: import("@oclif/core/interfaces").BooleanFlag<boolean>;
9
+ local: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ static summary: string;
13
+ run(): Promise<void>;
14
+ private readApiKeyForInit;
15
+ }
@@ -0,0 +1,95 @@
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, writeApiKeyConfig } from '../../lib/config.js';
5
+ import { zurfBaseFlags } from '../../lib/flags.js';
6
+ import { dotGitignoreMentionsZurf, ensureZurfGitignoreEntry, gitignorePathForCwd, } from '../../lib/gitignore-zurf.js';
7
+ import { promptLine, readStdinIfPiped } from '../../lib/init-input.js';
8
+ import { printJson } from '../../lib/json-output.js';
9
+ export default class Init extends Command {
10
+ static description = `Save your Browserbase API key to global or project config.
11
+ Global path follows oclif config (same as \`zurf config which\`).`;
12
+ static examples = [
13
+ '<%= config.bin %> <%= command.id %> --global',
14
+ '<%= config.bin %> <%= command.id %> --local',
15
+ 'printenv BROWSERBASE_API_KEY | <%= config.bin %> <%= command.id %> --global',
16
+ ];
17
+ static flags = {
18
+ ...zurfBaseFlags,
19
+ 'api-key': Flags.string({
20
+ 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.',
21
+ }),
22
+ gitignore: Flags.boolean({
23
+ description: 'Append .zurf/ to ./.gitignore if that entry is missing',
24
+ }),
25
+ global: Flags.boolean({
26
+ description: 'Store API key in user config (oclif config dir for this CLI)',
27
+ exactlyOne: ['global', 'local'],
28
+ }),
29
+ local: Flags.boolean({
30
+ description: 'Store API key in ./.zurf/config.json for this directory',
31
+ exactlyOne: ['global', 'local'],
32
+ }),
33
+ };
34
+ static summary = 'Configure Browserbase API key storage';
35
+ async run() {
36
+ const { flags } = await this.parse(Init);
37
+ const apiKey = await this.readApiKeyForInit(flags);
38
+ const targetPath = flags.global
39
+ ? globalConfigFilePath(this.config.configDir)
40
+ : localConfigPathForCwd();
41
+ try {
42
+ await writeApiKeyConfig(targetPath, apiKey);
43
+ }
44
+ catch (error) {
45
+ cliError({ command: this, exitCode: 1, json: flags.json, message: errorMessage(error) });
46
+ }
47
+ if (flags.gitignore) {
48
+ try {
49
+ await ensureZurfGitignoreEntry(gitignorePathForCwd());
50
+ }
51
+ catch (error) {
52
+ cliError({ command: this, exitCode: 1, json: flags.json, message: errorMessage(error) });
53
+ }
54
+ }
55
+ if (flags.json) {
56
+ printJson({ ok: true, path: targetPath, scope: flags.global ? 'global' : 'local' });
57
+ }
58
+ else {
59
+ this.log(`Saved API key to ${targetPath}`);
60
+ if (flags.local) {
61
+ let showTip = true;
62
+ try {
63
+ const gi = await fs.readFile(gitignorePathForCwd(), 'utf8');
64
+ if (dotGitignoreMentionsZurf(gi)) {
65
+ showTip = false;
66
+ }
67
+ }
68
+ catch {
69
+ // no .gitignore yet
70
+ }
71
+ if (showTip) {
72
+ this.log('Tip: add .zurf/ to .gitignore so the key is not committed (or run with --gitignore).');
73
+ }
74
+ }
75
+ }
76
+ }
77
+ async readApiKeyForInit(flags) {
78
+ let apiKey = flags['api-key']?.trim();
79
+ if (!apiKey) {
80
+ apiKey = await readStdinIfPiped();
81
+ }
82
+ if (!apiKey && process.stdin.isTTY) {
83
+ apiKey = await promptLine('Browserbase API key: ');
84
+ }
85
+ if (!apiKey) {
86
+ cliError({
87
+ command: this,
88
+ exitCode: 2,
89
+ json: flags.json,
90
+ message: 'No API key provided. Pipe stdin, use --api-key, or run interactively in a TTY.',
91
+ });
92
+ }
93
+ return apiKey;
94
+ }
95
+ }
@@ -0,0 +1,14 @@
1
+ import { ZurfBrowserbaseCommand } from '../../lib/zurf-browserbase-command.js';
2
+ export default class Search extends ZurfBrowserbaseCommand {
3
+ static args: {
4
+ query: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ 'num-results': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
10
+ json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
11
+ };
12
+ static summary: string;
13
+ run(): Promise<void>;
14
+ }
@@ -0,0 +1,59 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { cliError } from '../../lib/cli-errors.js';
3
+ import { zurfBaseFlags } from '../../lib/flags.js';
4
+ import { printJson } from '../../lib/json-output.js';
5
+ import { buildSearchJsonPayload, linesForHumanSearch } from '../../lib/search-output.js';
6
+ import { ZurfBrowserbaseCommand } from '../../lib/zurf-browserbase-command.js';
7
+ export default class Search extends ZurfBrowserbaseCommand {
8
+ static args = {
9
+ query: Args.string({
10
+ description: 'Search query, max 200 characters (quote for multiple words)',
11
+ required: true,
12
+ }),
13
+ };
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.`;
16
+ static examples = [
17
+ '<%= config.bin %> <%= command.id %> "browserbase documentation"',
18
+ '<%= config.bin %> <%= command.id %> "laravel inertia" --num-results 5 --json',
19
+ ];
20
+ static flags = {
21
+ ...zurfBaseFlags,
22
+ 'num-results': Flags.integer({
23
+ char: 'n',
24
+ default: 10,
25
+ description: 'Number of results (1–25)',
26
+ max: 25,
27
+ min: 1,
28
+ }),
29
+ };
30
+ static summary = 'Search the web via Browserbase';
31
+ async run() {
32
+ const { args, flags } = await this.parse(Search);
33
+ const query = args.query.trim();
34
+ if (query.length === 0) {
35
+ cliError({ command: this, exitCode: 2, json: flags.json, message: 'Query must not be empty.' });
36
+ }
37
+ if (query.length > 200) {
38
+ cliError({
39
+ command: this,
40
+ exitCode: 2,
41
+ json: flags.json,
42
+ message: 'Query must be at most 200 characters.',
43
+ });
44
+ }
45
+ await this.runWithBrowserbase(flags, 'Searching the web', async (client) => {
46
+ const response = await client.search.web({
47
+ numResults: flags['num-results'],
48
+ query,
49
+ });
50
+ if (flags.json) {
51
+ printJson(buildSearchJsonPayload(response));
52
+ return;
53
+ }
54
+ for (const line of linesForHumanSearch(response)) {
55
+ this.log(line);
56
+ }
57
+ });
58
+ }
59
+ }
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { run } from '@oclif/core';
@@ -0,0 +1,12 @@
1
+ import type { Browserbase } from '@browserbasehq/sdk';
2
+ import { type ActiveApiKey } from './config.js';
3
+ export declare function createBrowserbaseClient(options: {
4
+ cwd?: string;
5
+ globalConfigDir: string;
6
+ }): Promise<{
7
+ client: Browserbase;
8
+ resolution: ActiveApiKey;
9
+ }>;
10
+ export declare class MissingApiKeyError extends Error {
11
+ constructor();
12
+ }
@@ -0,0 +1,16 @@
1
+ import { resolveApiKey } from './config.js';
2
+ export async function createBrowserbaseClient(options) {
3
+ const { Browserbase } = await import('@browserbasehq/sdk');
4
+ const resolution = resolveApiKey({ cwd: options.cwd, globalConfigDir: options.globalConfigDir });
5
+ if (resolution.source === 'none') {
6
+ throw new MissingApiKeyError();
7
+ }
8
+ const client = new Browserbase({ apiKey: resolution.apiKey });
9
+ return { client, resolution };
10
+ }
11
+ export class MissingApiKeyError extends Error {
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`.');
14
+ this.name = 'MissingApiKeyError';
15
+ }
16
+ }
@@ -0,0 +1,8 @@
1
+ import type { Command } from '@oclif/core';
2
+ import { createBrowserbaseClient } from './browserbase-client.js';
3
+ export declare function getBrowserbaseClientOrExit(command: Command, flags: {
4
+ json: boolean;
5
+ }, options: {
6
+ cwd?: string;
7
+ globalConfigDir: string;
8
+ }): Promise<Awaited<ReturnType<typeof createBrowserbaseClient>>>;
@@ -0,0 +1,16 @@
1
+ import { createBrowserbaseClient, MissingApiKeyError } from './browserbase-client.js';
2
+ import { cliError } from './cli-errors.js';
3
+ export async function getBrowserbaseClientOrExit(command, flags, options) {
4
+ try {
5
+ return await createBrowserbaseClient({
6
+ cwd: options.cwd,
7
+ globalConfigDir: options.globalConfigDir,
8
+ });
9
+ }
10
+ catch (error) {
11
+ if (error instanceof MissingApiKeyError) {
12
+ cliError({ command, exitCode: 2, json: flags.json, message: error.message });
13
+ }
14
+ throw error;
15
+ }
16
+ }
@@ -0,0 +1,19 @@
1
+ import type { Command } from '@oclif/core';
2
+ export type CliErrorOptions = {
3
+ command: Command;
4
+ exitCode?: number;
5
+ json: boolean;
6
+ message: string;
7
+ statusCode?: number | string;
8
+ };
9
+ /**
10
+ * Thrown only if `command.exit` returns without throwing (e.g. test double). JSON is already on stdout.
11
+ * Caught in `bin/run.js` / `bin/dev.js` so this does not surface as an unhandled rejection.
12
+ */
13
+ export declare class CliJsonExitContractError extends Error {
14
+ constructor();
15
+ }
16
+ export declare function errorMessage(err: unknown): string;
17
+ export declare function errorStatus(err: unknown): number | undefined;
18
+ /** Print JSON or human error and exit; does not return. */
19
+ export declare function cliError(options: CliErrorOptions): never;
@@ -0,0 +1,36 @@
1
+ import { printErrorJson } from './json-output.js';
2
+ /**
3
+ * Thrown only if `command.exit` returns without throwing (e.g. test double). JSON is already on stdout.
4
+ * Caught in `bin/run.js` / `bin/dev.js` so this does not surface as an unhandled rejection.
5
+ */
6
+ export class CliJsonExitContractError extends Error {
7
+ constructor() {
8
+ super('zurf internal error: command.exit() returned after JSON error output; fix the test double or oclif version.');
9
+ this.name = 'CliJsonExitContractError';
10
+ }
11
+ }
12
+ export function errorMessage(err) {
13
+ if (err instanceof Error) {
14
+ return err.message;
15
+ }
16
+ return String(err);
17
+ }
18
+ export function errorStatus(err) {
19
+ if (err !== null &&
20
+ typeof err === 'object' &&
21
+ 'status' in err &&
22
+ typeof err.status === 'number') {
23
+ return err.status;
24
+ }
25
+ return undefined;
26
+ }
27
+ /** Print JSON or human error and exit; does not return. */
28
+ export function cliError(options) {
29
+ const { command, exitCode = 1, json, message, statusCode } = options;
30
+ if (json) {
31
+ printErrorJson(message, statusCode);
32
+ command.exit(exitCode);
33
+ throw new CliJsonExitContractError();
34
+ }
35
+ return command.error(message, { exit: exitCode });
36
+ }
@@ -0,0 +1,34 @@
1
+ export declare const ZURF_DIR_NAME = ".zurf";
2
+ export declare const CONFIG_FILENAME = "config.json";
3
+ export type ResolvedApiKey = {
4
+ apiKey: string;
5
+ path: string;
6
+ source: 'global';
7
+ } | {
8
+ apiKey: string;
9
+ path: string;
10
+ source: 'local';
11
+ } | {
12
+ apiKey: string;
13
+ source: 'env';
14
+ } | {
15
+ source: 'none';
16
+ };
17
+ /** Resolved non-empty API key (excludes `none`). */
18
+ export type ActiveApiKey = Extract<ResolvedApiKey, {
19
+ apiKey: string;
20
+ }>;
21
+ export interface ConfigFileShape {
22
+ apiKey?: string;
23
+ }
24
+ /**
25
+ * Path to global `config.json` under oclif's `this.config.configDir` (same rules as @oclif/core `Config.dir('config')` for `dirname` zurf).
26
+ */
27
+ export declare function globalConfigFilePath(oclifConfigDir: string): string;
28
+ export declare function localConfigPathForCwd(cwd?: string): string;
29
+ export declare function findLocalConfigPath(startDir?: string): string | undefined;
30
+ export declare function resolveApiKey(options: {
31
+ cwd?: string;
32
+ globalConfigDir: string;
33
+ }): ResolvedApiKey;
34
+ export declare function writeApiKeyConfig(targetPath: string, apiKey: string): Promise<void>;
@@ -0,0 +1,66 @@
1
+ import * as fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export const ZURF_DIR_NAME = '.zurf';
4
+ export const CONFIG_FILENAME = 'config.json';
5
+ /**
6
+ * Path to global `config.json` under oclif's `this.config.configDir` (same rules as @oclif/core `Config.dir('config')` for `dirname` zurf).
7
+ */
8
+ export function globalConfigFilePath(oclifConfigDir) {
9
+ return path.join(oclifConfigDir, CONFIG_FILENAME);
10
+ }
11
+ export function localConfigPathForCwd(cwd = process.cwd()) {
12
+ return path.join(path.resolve(cwd), ZURF_DIR_NAME, CONFIG_FILENAME);
13
+ }
14
+ export function findLocalConfigPath(startDir = process.cwd()) {
15
+ let dir = path.resolve(startDir);
16
+ const { root } = path.parse(dir);
17
+ while (true) {
18
+ const candidate = path.join(dir, ZURF_DIR_NAME, CONFIG_FILENAME);
19
+ if (fs.existsSync(candidate)) {
20
+ return candidate;
21
+ }
22
+ if (dir === root) {
23
+ break;
24
+ }
25
+ dir = path.dirname(dir);
26
+ }
27
+ return undefined;
28
+ }
29
+ function readApiKeyFromFile(filePath) {
30
+ try {
31
+ const raw = fs.readFileSync(filePath, 'utf8');
32
+ const parsed = JSON.parse(raw);
33
+ const key = typeof parsed.apiKey === 'string' ? parsed.apiKey.trim() : '';
34
+ return key.length > 0 ? key : undefined;
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ export function resolveApiKey(options) {
41
+ const cwd = options.cwd ?? process.cwd();
42
+ const envKey = process.env.BROWSERBASE_API_KEY?.trim();
43
+ if (envKey) {
44
+ return { apiKey: envKey, source: 'env' };
45
+ }
46
+ const localPath = findLocalConfigPath(cwd);
47
+ if (localPath) {
48
+ const key = readApiKeyFromFile(localPath);
49
+ if (key) {
50
+ return { apiKey: key, path: localPath, source: 'local' };
51
+ }
52
+ }
53
+ const gPath = globalConfigFilePath(options.globalConfigDir);
54
+ const globalKey = readApiKeyFromFile(gPath);
55
+ if (globalKey) {
56
+ return { apiKey: globalKey, path: gPath, source: 'global' };
57
+ }
58
+ return { source: 'none' };
59
+ }
60
+ export async function writeApiKeyConfig(targetPath, apiKey) {
61
+ const dir = path.dirname(targetPath);
62
+ await fs.promises.mkdir(dir, { recursive: true });
63
+ const payload = { apiKey: apiKey.trim() };
64
+ const body = `${JSON.stringify(payload, null, 2)}\n`;
65
+ await fs.promises.writeFile(targetPath, body, { encoding: 'utf8', mode: 0o600 });
66
+ }
@@ -0,0 +1,20 @@
1
+ export type FetchResponseForDisplay = {
2
+ content: string;
3
+ contentType: string;
4
+ encoding: string;
5
+ headers: Record<string, string>;
6
+ id: string;
7
+ statusCode: number;
8
+ };
9
+ export declare function buildFetchJsonPayload(response: FetchResponseForDisplay): {
10
+ content: string;
11
+ contentType: string;
12
+ encoding: string;
13
+ headers: Record<string, string>;
14
+ id: string;
15
+ statusCode: number;
16
+ };
17
+ export declare function humanFetchMetaLines(response: FetchResponseForDisplay): string[];
18
+ /** ~8k chars keeps terminal scrollback usable while showing most HTML pages in preview. */
19
+ export declare const HUMAN_BODY_PREVIEW_CHARS = 8000;
20
+ export declare function truncateNote(totalChars: number): string;
@@ -0,0 +1,23 @@
1
+ export function buildFetchJsonPayload(response) {
2
+ return {
3
+ content: response.content,
4
+ contentType: response.contentType,
5
+ encoding: response.encoding,
6
+ headers: response.headers,
7
+ id: response.id,
8
+ statusCode: response.statusCode,
9
+ };
10
+ }
11
+ export function humanFetchMetaLines(response) {
12
+ return [
13
+ `id: ${response.id}`,
14
+ `statusCode: ${response.statusCode}`,
15
+ `contentType: ${response.contentType}`,
16
+ `encoding: ${response.encoding}`,
17
+ ];
18
+ }
19
+ /** ~8k chars keeps terminal scrollback usable while showing most HTML pages in preview. */
20
+ export const HUMAN_BODY_PREVIEW_CHARS = 8000;
21
+ export function truncateNote(totalChars) {
22
+ return `… truncated (${totalChars} chars). Use --output FILE to save the full body (within the 1 MB Fetch limit).`;
23
+ }
@@ -0,0 +1,6 @@
1
+ /** Machine-readable output; kept separate from oclif `enableJsonFlag` so error payloads match zurf's stdout JSON contract. */
2
+ export declare const zurfJsonFlag: import("@oclif/core/interfaces").BooleanFlag<boolean>;
3
+ /** Shared flags for commands that support JSON output (no `--api-key` — use BROWSERBASE_API_KEY or config files). */
4
+ export declare const zurfBaseFlags: {
5
+ readonly json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
+ };
@@ -0,0 +1,10 @@
1
+ import { Flags } from '@oclif/core';
2
+ /** Machine-readable output; kept separate from oclif `enableJsonFlag` so error payloads match zurf's stdout JSON contract. */
3
+ export const zurfJsonFlag = Flags.boolean({
4
+ description: 'Print machine-readable JSON to stdout',
5
+ env: 'ZURF_JSON',
6
+ });
7
+ /** Shared flags for commands that support JSON output (no `--api-key` — use BROWSERBASE_API_KEY or config files). */
8
+ export const zurfBaseFlags = {
9
+ json: zurfJsonFlag,
10
+ };
@@ -0,0 +1,5 @@
1
+ /** True if `.gitignore` content already mentions ignoring zurf's local config dir. */
2
+ export declare function dotGitignoreMentionsZurf(content: string): boolean;
3
+ /** Append a `.zurf/` block to `gitignorePath` when missing. No-op if already present. */
4
+ export declare function ensureZurfGitignoreEntry(gitignorePath: string): Promise<void>;
5
+ export declare function gitignorePathForCwd(cwd?: string): string;
@@ -0,0 +1,45 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ /** True if `.gitignore` content already mentions ignoring zurf's local config dir. */
4
+ export function dotGitignoreMentionsZurf(content) {
5
+ for (const line of content.split(/\r?\n/)) {
6
+ const t = line.trim();
7
+ if (t === '' || t.startsWith('#')) {
8
+ continue;
9
+ }
10
+ if (t === '.zurf' || t === '.zurf/') {
11
+ return true;
12
+ }
13
+ if (t.endsWith('/.zurf') || t.endsWith('/.zurf/')) {
14
+ return true;
15
+ }
16
+ }
17
+ return false;
18
+ }
19
+ const BLOCK = '\n# zurf local API key\n.zurf/\n';
20
+ /** Append a `.zurf/` block to `gitignorePath` when missing. No-op if already present. */
21
+ export async function ensureZurfGitignoreEntry(gitignorePath) {
22
+ let existing = '';
23
+ try {
24
+ existing = await fs.readFile(gitignorePath, 'utf8');
25
+ }
26
+ catch (error) {
27
+ const code = error !== null &&
28
+ typeof error === 'object' &&
29
+ 'code' in error &&
30
+ typeof error.code === 'string'
31
+ ? error.code
32
+ : undefined;
33
+ if (code !== 'ENOENT') {
34
+ throw error;
35
+ }
36
+ }
37
+ if (dotGitignoreMentionsZurf(existing)) {
38
+ return;
39
+ }
40
+ const prefix = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
41
+ await fs.writeFile(gitignorePath, `${existing}${prefix}${BLOCK}`, 'utf8');
42
+ }
43
+ export function gitignorePathForCwd(cwd = process.cwd()) {
44
+ return path.join(path.resolve(cwd), '.gitignore');
45
+ }
@@ -0,0 +1,2 @@
1
+ export declare function readStdinIfPiped(): Promise<string | undefined>;
2
+ export declare function promptLine(question: string): Promise<string>;