@browsercash/chase 1.0.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/dist/cli.js ADDED
@@ -0,0 +1,503 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Chase CLI
4
+ *
5
+ * A command-line interface for browser automation using the chase API.
6
+ *
7
+ * Usage:
8
+ * chase automate "Go to example.com and get the title"
9
+ * chase generate "Scrape products from amazon.com"
10
+ * chase scripts
11
+ * chase run <script-id>
12
+ * chase tasks
13
+ * chase task <task-id>
14
+ */
15
+ import * as https from 'https';
16
+ const API_BASE = 'https://chase-api-gth2quoxyq-uc.a.run.app';
17
+ function getApiKey() {
18
+ const key = process.env.BROWSER_CASH_API_KEY;
19
+ if (!key) {
20
+ console.error('Error: BROWSER_CASH_API_KEY environment variable is required');
21
+ console.error('');
22
+ console.error('Set it with:');
23
+ console.error(' export BROWSER_CASH_API_KEY="your-api-key"');
24
+ console.error('');
25
+ console.error('Get an API key at: https://browser.cash');
26
+ process.exit(1);
27
+ }
28
+ return key;
29
+ }
30
+ function parseArgs() {
31
+ const rawArgs = process.argv.slice(2);
32
+ const flags = {};
33
+ const positional = [];
34
+ for (let i = 0; i < rawArgs.length; i++) {
35
+ const arg = rawArgs[i];
36
+ if (arg.startsWith('--')) {
37
+ const key = arg.slice(2);
38
+ const next = rawArgs[i + 1];
39
+ if (next && !next.startsWith('--')) {
40
+ flags[key] = next;
41
+ i++;
42
+ }
43
+ else {
44
+ flags[key] = true;
45
+ }
46
+ }
47
+ else {
48
+ positional.push(arg);
49
+ }
50
+ }
51
+ return {
52
+ command: positional[0] || 'help',
53
+ args: positional.slice(1),
54
+ flags,
55
+ };
56
+ }
57
+ async function streamRequest(endpoint, body, onEvent) {
58
+ return new Promise((resolve, reject) => {
59
+ const url = new URL(endpoint, API_BASE);
60
+ const options = {
61
+ hostname: url.hostname,
62
+ port: 443,
63
+ path: url.pathname,
64
+ method: 'POST',
65
+ headers: {
66
+ 'Content-Type': 'application/json',
67
+ },
68
+ };
69
+ const req = https.request(options, (res) => {
70
+ let buffer = '';
71
+ res.on('data', (chunk) => {
72
+ buffer += chunk.toString();
73
+ const lines = buffer.split('\n');
74
+ buffer = lines.pop() || '';
75
+ for (const line of lines) {
76
+ if (line.startsWith('data: ')) {
77
+ try {
78
+ const event = JSON.parse(line.slice(6));
79
+ onEvent(event.type, event.data);
80
+ }
81
+ catch {
82
+ // Ignore parse errors
83
+ }
84
+ }
85
+ }
86
+ });
87
+ res.on('end', () => {
88
+ if (buffer.startsWith('data: ')) {
89
+ try {
90
+ const event = JSON.parse(buffer.slice(6));
91
+ onEvent(event.type, event.data);
92
+ }
93
+ catch {
94
+ // Ignore
95
+ }
96
+ }
97
+ resolve();
98
+ });
99
+ res.on('error', reject);
100
+ });
101
+ req.on('error', reject);
102
+ req.write(JSON.stringify(body));
103
+ req.end();
104
+ });
105
+ }
106
+ async function apiGet(endpoint, apiKey) {
107
+ return new Promise((resolve, reject) => {
108
+ const url = new URL(endpoint, API_BASE);
109
+ const options = {
110
+ hostname: url.hostname,
111
+ port: 443,
112
+ path: url.pathname,
113
+ method: 'GET',
114
+ headers: {
115
+ 'x-api-key': apiKey,
116
+ },
117
+ };
118
+ const req = https.request(options, (res) => {
119
+ let data = '';
120
+ res.on('data', (chunk) => (data += chunk.toString()));
121
+ res.on('end', () => {
122
+ try {
123
+ resolve(JSON.parse(data));
124
+ }
125
+ catch {
126
+ reject(new Error(`Invalid JSON response: ${data}`));
127
+ }
128
+ });
129
+ res.on('error', reject);
130
+ });
131
+ req.on('error', reject);
132
+ req.end();
133
+ });
134
+ }
135
+ async function commandAutomate(task, flags) {
136
+ const apiKey = getApiKey();
137
+ console.log('');
138
+ console.log('╔═══════════════════════════════════════════════════════════╗');
139
+ console.log('║ Chase Browser Automation ║');
140
+ console.log('╚═══════════════════════════════════════════════════════════╝');
141
+ console.log('');
142
+ console.log(`Task: ${task}`);
143
+ console.log('');
144
+ const body = {
145
+ task,
146
+ browserCashApiKey: apiKey,
147
+ };
148
+ if (flags.country) {
149
+ body.browserOptions = { ...(body.browserOptions || {}), country: flags.country };
150
+ }
151
+ if (flags.adblock) {
152
+ body.browserOptions = { ...(body.browserOptions || {}), adblock: true };
153
+ }
154
+ if (flags.captcha) {
155
+ body.browserOptions = { ...(body.browserOptions || {}), captchaSolver: true };
156
+ }
157
+ let taskId = null;
158
+ let result = null;
159
+ await streamRequest('/automate/stream', body, (type, data) => {
160
+ const d = data;
161
+ switch (type) {
162
+ case 'start':
163
+ taskId = d.taskId;
164
+ console.log(`Task ID: ${taskId}`);
165
+ console.log('Status: Running...');
166
+ break;
167
+ case 'log':
168
+ if (!flags.quiet) {
169
+ console.log(` ${d.message}`);
170
+ }
171
+ break;
172
+ case 'complete':
173
+ result = d;
174
+ break;
175
+ case 'error':
176
+ console.error(`Error: ${d.message}`);
177
+ break;
178
+ }
179
+ });
180
+ console.log('');
181
+ if (result) {
182
+ const r = result;
183
+ if (r.success) {
184
+ console.log('Status: Complete ✓');
185
+ console.log('');
186
+ console.log('Result:');
187
+ console.log(JSON.stringify(r.result, null, 2));
188
+ if (r.summary) {
189
+ console.log('');
190
+ console.log(`Summary: ${r.summary}`);
191
+ }
192
+ }
193
+ else {
194
+ console.log('Status: Failed ✗');
195
+ console.log(`Error: ${r.error}`);
196
+ }
197
+ }
198
+ console.log('');
199
+ }
200
+ async function commandGenerate(task, flags) {
201
+ const apiKey = getApiKey();
202
+ console.log('');
203
+ console.log('╔═══════════════════════════════════════════════════════════╗');
204
+ console.log('║ Chase Script Generator ║');
205
+ console.log('╚═══════════════════════════════════════════════════════════╝');
206
+ console.log('');
207
+ console.log(`Task: ${task}`);
208
+ console.log('');
209
+ const body = {
210
+ task,
211
+ browserCashApiKey: apiKey,
212
+ skipTest: flags['skip-test'] === true,
213
+ };
214
+ if (flags.country) {
215
+ body.browserOptions = { ...(body.browserOptions || {}), country: flags.country };
216
+ }
217
+ let taskId = null;
218
+ let result = null;
219
+ await streamRequest('/generate/stream', body, (type, data) => {
220
+ const d = data;
221
+ switch (type) {
222
+ case 'start':
223
+ taskId = d.taskId;
224
+ console.log(`Task ID: ${taskId}`);
225
+ console.log('Status: Generating...');
226
+ break;
227
+ case 'log':
228
+ if (!flags.quiet) {
229
+ console.log(` ${d.message}`);
230
+ }
231
+ break;
232
+ case 'iteration_result':
233
+ console.log(` Iteration ${d.iteration}: ${d.success ? 'passed' : 'failed'}`);
234
+ break;
235
+ case 'script_saved':
236
+ console.log(` Script saved: ${d.scriptId}`);
237
+ break;
238
+ case 'complete':
239
+ result = d;
240
+ break;
241
+ case 'error':
242
+ console.error(`Error: ${d.message}`);
243
+ break;
244
+ }
245
+ });
246
+ console.log('');
247
+ if (result) {
248
+ const r = result;
249
+ if (r.success) {
250
+ console.log('Status: Complete ✓');
251
+ console.log(`Script ID: ${r.scriptId}`);
252
+ console.log(`Iterations: ${r.iterations}`);
253
+ if (!flags.quiet) {
254
+ console.log('');
255
+ console.log('Script Preview:');
256
+ console.log('─'.repeat(60));
257
+ const lines = r.script.split('\n').slice(0, 20);
258
+ lines.forEach((line) => console.log(line));
259
+ if (r.script.split('\n').length > 20) {
260
+ console.log('... (truncated)');
261
+ }
262
+ console.log('─'.repeat(60));
263
+ }
264
+ console.log('');
265
+ console.log(`Run with: chase run ${r.scriptId}`);
266
+ }
267
+ else {
268
+ console.log('Status: Failed ✗');
269
+ console.log(`Error: ${r.error || 'Script generation failed'}`);
270
+ }
271
+ }
272
+ console.log('');
273
+ }
274
+ async function commandScripts() {
275
+ const apiKey = getApiKey();
276
+ console.log('');
277
+ console.log('Your Scripts:');
278
+ console.log('─'.repeat(60));
279
+ const response = (await apiGet('/scripts', apiKey));
280
+ if (!response.scripts || response.scripts.length === 0) {
281
+ console.log(' No scripts found.');
282
+ console.log('');
283
+ console.log(' Generate one with:');
284
+ console.log(' chase generate "Your task here"');
285
+ }
286
+ else {
287
+ for (const script of response.scripts) {
288
+ console.log('');
289
+ console.log(` ID: ${script.id}`);
290
+ console.log(` Task: ${script.task}`);
291
+ console.log(` Created: ${script.createdAt}`);
292
+ console.log(` Status: ${script.success ? '✓ Passed' : '✗ Failed'} (${script.iterations} iterations)`);
293
+ }
294
+ }
295
+ console.log('');
296
+ }
297
+ async function commandRun(scriptId, flags) {
298
+ const apiKey = getApiKey();
299
+ console.log('');
300
+ console.log('╔═══════════════════════════════════════════════════════════╗');
301
+ console.log('║ Chase Script Runner ║');
302
+ console.log('╚═══════════════════════════════════════════════════════════╝');
303
+ console.log('');
304
+ console.log(`Script ID: ${scriptId}`);
305
+ console.log('');
306
+ const body = {
307
+ browserCashApiKey: apiKey,
308
+ };
309
+ if (flags.country) {
310
+ body.browserOptions = { ...(body.browserOptions || {}), country: flags.country };
311
+ }
312
+ let result = null;
313
+ let output = '';
314
+ await streamRequest(`/scripts/${scriptId}/run`, body, (type, data) => {
315
+ const d = data;
316
+ switch (type) {
317
+ case 'start':
318
+ console.log(`Task ID: ${d.taskId}`);
319
+ console.log('Status: Running...');
320
+ break;
321
+ case 'output':
322
+ output += d.text;
323
+ if (!flags.quiet) {
324
+ process.stdout.write(d.text);
325
+ }
326
+ break;
327
+ case 'complete':
328
+ result = d;
329
+ break;
330
+ case 'error':
331
+ console.error(`Error: ${d.message}`);
332
+ break;
333
+ }
334
+ });
335
+ console.log('');
336
+ if (result) {
337
+ const r = result;
338
+ if (r.success) {
339
+ console.log('Status: Complete ✓');
340
+ if (flags.quiet && output) {
341
+ console.log('Output:');
342
+ console.log(output);
343
+ }
344
+ }
345
+ else {
346
+ console.log('Status: Failed ✗');
347
+ console.log(`Exit code: ${r.exitCode}`);
348
+ }
349
+ }
350
+ console.log('');
351
+ }
352
+ async function commandTasks() {
353
+ const apiKey = getApiKey();
354
+ console.log('');
355
+ console.log('Recent Tasks:');
356
+ console.log('─'.repeat(60));
357
+ const response = (await apiGet('/tasks', apiKey));
358
+ if (!response.tasks || response.tasks.length === 0) {
359
+ console.log(' No tasks found.');
360
+ }
361
+ else {
362
+ for (const task of response.tasks) {
363
+ console.log('');
364
+ console.log(` ID: ${task.taskId}`);
365
+ console.log(` Type: ${task.type}`);
366
+ console.log(` Status: ${task.status}`);
367
+ console.log(` Task: ${task.task?.substring(0, 50)}${task.task?.length > 50 ? '...' : ''}`);
368
+ console.log(` Created: ${task.createdAt}`);
369
+ }
370
+ }
371
+ console.log('');
372
+ }
373
+ async function commandTask(taskId) {
374
+ const apiKey = getApiKey();
375
+ console.log('');
376
+ console.log('Task Details:');
377
+ console.log('─'.repeat(60));
378
+ const task = (await apiGet(`/tasks/${taskId}`, apiKey));
379
+ if (task.error) {
380
+ console.log(` Error: ${task.error}`);
381
+ }
382
+ else {
383
+ console.log(` ID: ${task.taskId}`);
384
+ console.log(` Type: ${task.type}`);
385
+ console.log(` Status: ${task.status}`);
386
+ console.log(` Task: ${task.task}`);
387
+ console.log(` Created: ${task.createdAt}`);
388
+ console.log(` Updated: ${task.updatedAt}`);
389
+ if (task.status === 'completed') {
390
+ if (task.result) {
391
+ console.log('');
392
+ console.log(' Result:');
393
+ console.log(JSON.stringify(task.result, null, 2).split('\n').map((l) => ' ' + l).join('\n'));
394
+ }
395
+ if (task.script) {
396
+ console.log('');
397
+ console.log(` Script ID: ${task.scriptId}`);
398
+ }
399
+ }
400
+ else if (task.status === 'error') {
401
+ console.log(` Error: ${task.error}`);
402
+ }
403
+ }
404
+ console.log('');
405
+ }
406
+ function printHelp() {
407
+ console.log(`
408
+ ╔═══════════════════════════════════════════════════════════╗
409
+ ║ Chase: AI Browser Automation ║
410
+ ╚═══════════════════════════════════════════════════════════╝
411
+
412
+ USAGE:
413
+ chase <command> [options]
414
+
415
+ COMMANDS:
416
+ automate <task> Perform a one-off browser automation task
417
+ generate <task> Generate a reusable automation script
418
+ scripts List your saved scripts
419
+ run <script-id> Run a saved script
420
+ tasks List your recent tasks
421
+ task <task-id> Get details of a specific task
422
+ help Show this help message
423
+
424
+ EXAMPLES:
425
+ chase automate "Go to example.com and get the page title"
426
+ chase automate "Extract the top 10 stories from Hacker News"
427
+ chase generate "Scrape product prices from amazon.com/dp/B09V3KXJPB"
428
+ chase scripts
429
+ chase run script-abc123
430
+ chase task task-xyz789
431
+
432
+ OPTIONS:
433
+ --country <code> Use a browser from specific country (e.g., US, DE, JP)
434
+ --adblock Enable ad-blocking
435
+ --captcha Enable CAPTCHA solving
436
+ --quiet Reduce output verbosity
437
+ --skip-test Skip script testing (generate only)
438
+ --help Show this help message
439
+
440
+ ENVIRONMENT:
441
+ BROWSER_CASH_API_KEY Your Browser.cash API key (required)
442
+
443
+ Get your API key at: https://browser.cash
444
+ `);
445
+ }
446
+ async function main() {
447
+ const { command, args, flags } = parseArgs();
448
+ if (flags.help || command === 'help') {
449
+ printHelp();
450
+ process.exit(0);
451
+ }
452
+ try {
453
+ switch (command) {
454
+ case 'automate':
455
+ if (!args[0]) {
456
+ console.error('Error: Task description required');
457
+ console.error('Usage: chase automate "Your task here"');
458
+ process.exit(1);
459
+ }
460
+ await commandAutomate(args.join(' '), flags);
461
+ break;
462
+ case 'generate':
463
+ if (!args[0]) {
464
+ console.error('Error: Task description required');
465
+ console.error('Usage: chase generate "Your task here"');
466
+ process.exit(1);
467
+ }
468
+ await commandGenerate(args.join(' '), flags);
469
+ break;
470
+ case 'scripts':
471
+ await commandScripts();
472
+ break;
473
+ case 'run':
474
+ if (!args[0]) {
475
+ console.error('Error: Script ID required');
476
+ console.error('Usage: chase run <script-id>');
477
+ process.exit(1);
478
+ }
479
+ await commandRun(args[0], flags);
480
+ break;
481
+ case 'tasks':
482
+ await commandTasks();
483
+ break;
484
+ case 'task':
485
+ if (!args[0]) {
486
+ console.error('Error: Task ID required');
487
+ console.error('Usage: chase task <task-id>');
488
+ process.exit(1);
489
+ }
490
+ await commandTask(args[0]);
491
+ break;
492
+ default:
493
+ console.error(`Unknown command: ${command}`);
494
+ console.error('Run "chase help" for usage information');
495
+ process.exit(1);
496
+ }
497
+ }
498
+ catch (error) {
499
+ console.error('Error:', error instanceof Error ? error.message : error);
500
+ process.exit(1);
501
+ }
502
+ }
503
+ main();
@@ -0,0 +1,104 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ /**
4
+ * Write a script to file
5
+ */
6
+ export function writeScript(scriptContent, options) {
7
+ const { outputDir, filename } = options;
8
+ // Generate filename if not provided
9
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
10
+ const scriptFilename = filename || `script-${timestamp}.sh`;
11
+ const outputPath = path.join(outputDir, scriptFilename);
12
+ // Ensure output directory exists
13
+ if (!fs.existsSync(outputDir)) {
14
+ fs.mkdirSync(outputDir, { recursive: true });
15
+ }
16
+ // Write script file
17
+ fs.writeFileSync(outputPath, scriptContent);
18
+ fs.chmodSync(outputPath, '755');
19
+ return outputPath;
20
+ }
21
+ /**
22
+ * Generate a standalone bash script from captured commands (legacy function)
23
+ * Now simplified to just process the script for variable normalization
24
+ */
25
+ export function generateBashScript(commands, options) {
26
+ const { cdpUrl, outputDir, filename } = options;
27
+ // Generate filename if not provided
28
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
29
+ const scriptFilename = filename || `script-${timestamp}.sh`;
30
+ const outputPath = path.join(outputDir, scriptFilename);
31
+ // Filter and process commands
32
+ const processedCommands = [];
33
+ for (const cmd of commands) {
34
+ // Skip snapshot commands
35
+ if (cmd.includes('snapshot'))
36
+ continue;
37
+ // Skip screenshot commands
38
+ if (cmd.includes('screenshot'))
39
+ continue;
40
+ // Skip commands with ephemeral refs
41
+ if (/@e\d+/.test(cmd)) {
42
+ processedCommands.push(`# SKIP: Uses ephemeral ref`);
43
+ processedCommands.push(`# ${cmd}`);
44
+ continue;
45
+ }
46
+ let processed = cmd;
47
+ // Normalize CDP variable references
48
+ processed = processed.replace(/"\$CDP_URL"/g, '"$CDP"');
49
+ processed = processed.replace(/'\$CDP_URL'/g, '"$CDP"');
50
+ processed = processed.replace(/\$CDP_URL/g, '$CDP');
51
+ processed = processed.replace(cdpUrl, '$CDP');
52
+ processed = processed.replace(`"${cdpUrl}"`, '"$CDP"');
53
+ // Ensure proper --cdp quoting
54
+ processed = processed.replace(/--cdp\s+\$CDP(?!\w)/g, '--cdp "$CDP"');
55
+ processedCommands.push(processed);
56
+ // Add sleep after navigation
57
+ if (cmd.includes(' open ')) {
58
+ processedCommands.push('sleep 2');
59
+ }
60
+ }
61
+ // Build script content
62
+ const scriptLines = [
63
+ '#!/bin/bash',
64
+ 'set -e',
65
+ '',
66
+ '# Generated by claude-gen',
67
+ `# Created: ${new Date().toISOString()}`,
68
+ '',
69
+ 'CDP="${CDP_URL:?Required: CDP_URL}"',
70
+ '',
71
+ ...processedCommands,
72
+ '',
73
+ 'echo ""',
74
+ 'echo "============================================"',
75
+ 'echo "FINAL RESULTS"',
76
+ 'echo "============================================"',
77
+ 'echo "Script completed"',
78
+ ];
79
+ const scriptContent = scriptLines.join('\n');
80
+ // Ensure output directory exists
81
+ if (!fs.existsSync(outputDir)) {
82
+ fs.mkdirSync(outputDir, { recursive: true });
83
+ }
84
+ // Write script file
85
+ fs.writeFileSync(outputPath, scriptContent);
86
+ fs.chmodSync(outputPath, '755');
87
+ return outputPath;
88
+ }
89
+ /**
90
+ * Normalize a script's CDP variable references
91
+ */
92
+ export function normalizeScriptCdp(script, cdpUrl) {
93
+ let normalized = script;
94
+ // Replace hardcoded CDP URL
95
+ normalized = normalized.replace(new RegExp(escapeRegex(cdpUrl), 'g'), '$CDP');
96
+ // Normalize variable references
97
+ normalized = normalized.replace(/"\$CDP_URL"/g, '"$CDP"');
98
+ normalized = normalized.replace(/'\$CDP_URL'/g, '"$CDP"');
99
+ normalized = normalized.replace(/\$CDP_URL/g, '$CDP');
100
+ return normalized;
101
+ }
102
+ function escapeRegex(string) {
103
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
104
+ }