@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/.claude/settings.local.json +14 -0
- package/.dockerignore +34 -0
- package/README.md +256 -0
- package/api-1 (3).json +831 -0
- package/dist/browser-cash.js +128 -0
- package/dist/claude-runner.js +285 -0
- package/dist/cli-install.js +104 -0
- package/dist/cli.js +503 -0
- package/dist/codegen/bash-generator.js +104 -0
- package/dist/config.js +112 -0
- package/dist/errors/error-classifier.js +351 -0
- package/dist/hooks/capture-hook.js +57 -0
- package/dist/index.js +180 -0
- package/dist/iterative-tester.js +407 -0
- package/dist/logger/command-log.js +38 -0
- package/dist/prompts/agentic-prompt.js +78 -0
- package/dist/prompts/fix-prompt.js +477 -0
- package/dist/prompts/helpers.js +214 -0
- package/dist/prompts/system-prompt.js +282 -0
- package/dist/script-runner.js +429 -0
- package/dist/server.js +1934 -0
- package/dist/types/iteration-history.js +139 -0
- package/openapi.json +1131 -0
- package/package.json +44 -0
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
|
+
}
|