@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/index.js ADDED
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+ import { loadConfig } from './config.js';
4
+ import { runClaudeForScriptGeneration } from './claude-runner.js';
5
+ import { runIterativeTest } from './iterative-tester.js';
6
+ import { writeScript } from './codegen/bash-generator.js';
7
+ import * as path from 'path';
8
+ async function main() {
9
+ // Parse command line arguments
10
+ const args = process.argv.slice(2);
11
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
12
+ printHelp();
13
+ process.exit(0);
14
+ }
15
+ // Extract options
16
+ const outputIndex = args.findIndex(a => a === '--output' || a === '-o');
17
+ let customOutput;
18
+ if (outputIndex !== -1 && args[outputIndex + 1]) {
19
+ customOutput = args[outputIndex + 1];
20
+ }
21
+ const skipRun = args.includes('--skip-run');
22
+ // Get the task prompt (everything that's not a flag)
23
+ const taskPrompt = args
24
+ .filter((a, i) => {
25
+ if (a.startsWith('--') || a.startsWith('-'))
26
+ return false;
27
+ if (outputIndex !== -1 && i === outputIndex + 1)
28
+ return false;
29
+ return true;
30
+ })
31
+ .join(' ');
32
+ if (!taskPrompt) {
33
+ console.error('Error: Please provide a task description');
34
+ console.error('Usage: npx claude-gen "Your task description"');
35
+ process.exit(1);
36
+ }
37
+ try {
38
+ // Load configuration with task for validation threshold inference
39
+ const config = loadConfig(taskPrompt);
40
+ console.log('');
41
+ console.log('='.repeat(60));
42
+ console.log(' Claude-Gen: Browser Automation Script Generator');
43
+ console.log('='.repeat(60));
44
+ console.log('');
45
+ console.log(`Task: ${taskPrompt}`);
46
+ console.log(`Model: ${config.model}`);
47
+ console.log(`CDP URL: ${config.cdpUrl.substring(0, 50)}...`);
48
+ console.log('');
49
+ // Run Claude Code to generate the script
50
+ console.log('[claude-gen] Running Claude Code to generate script...');
51
+ console.log('-'.repeat(60));
52
+ const result = await runClaudeForScriptGeneration(taskPrompt, config);
53
+ console.log('-'.repeat(60));
54
+ if (!result.success || !result.script) {
55
+ console.error(`\n[claude-gen] Failed to generate script`);
56
+ if (result.error) {
57
+ console.error(`[claude-gen] Error: ${result.error}`);
58
+ }
59
+ if (!result.script) {
60
+ console.error('[claude-gen] Could not extract a valid script from Claude\'s output.');
61
+ console.error('[claude-gen] Make sure Claude outputs a bash script in a code block.');
62
+ }
63
+ process.exit(1);
64
+ }
65
+ console.log(`\n[claude-gen] Successfully generated script`);
66
+ // Show a preview of the script
67
+ const scriptLines = result.script.split('\n');
68
+ const previewLines = scriptLines.slice(0, 15);
69
+ console.log('\n[claude-gen] Script preview:');
70
+ console.log('---');
71
+ previewLines.forEach(line => console.log(` ${line}`));
72
+ if (scriptLines.length > 15) {
73
+ console.log(` ... (${scriptLines.length - 15} more lines)`);
74
+ }
75
+ console.log('---');
76
+ let finalScriptPath;
77
+ let testResult = null;
78
+ if (!skipRun) {
79
+ // Run iterative testing: test script, fix if needed, repeat
80
+ const iterResult = await runIterativeTest(result.script, taskPrompt, config, customOutput);
81
+ finalScriptPath = iterResult.finalScriptPath;
82
+ testResult = {
83
+ success: iterResult.success,
84
+ iterations: iterResult.iterations,
85
+ skippedDueToStaleCdp: iterResult.skippedDueToStaleCdp,
86
+ };
87
+ if (iterResult.success) {
88
+ console.log(`\n[claude-gen] Script passed after ${iterResult.iterations} iteration(s)!`);
89
+ }
90
+ else if (iterResult.skippedDueToStaleCdp) {
91
+ console.log(`\n[claude-gen] Testing skipped - CDP connection unavailable.`);
92
+ console.log(`[claude-gen] Script was generated but needs testing with a fresh CDP_URL.`);
93
+ }
94
+ else {
95
+ console.log(`\n[claude-gen] Script did not pass after ${iterResult.iterations} attempts.`);
96
+ if (iterResult.lastError) {
97
+ console.log(`[claude-gen] Last error: ${iterResult.lastError.substring(0, 300)}...`);
98
+ }
99
+ }
100
+ }
101
+ else {
102
+ // Skip testing, just write the script
103
+ finalScriptPath = writeScript(result.script, {
104
+ cdpUrl: config.cdpUrl,
105
+ outputDir: config.outputDir,
106
+ filename: customOutput,
107
+ });
108
+ console.log(`\n[claude-gen] Generated script (testing skipped): ${finalScriptPath}`);
109
+ }
110
+ // Final output
111
+ console.log('\n' + '='.repeat(60));
112
+ if (testResult?.success) {
113
+ console.log(' Script generated and validated successfully!');
114
+ }
115
+ else if (testResult?.skippedDueToStaleCdp) {
116
+ console.log(' Script generated (testing skipped - CDP unavailable)');
117
+ }
118
+ else if (testResult) {
119
+ console.log(' Script generated (validation failed after ' + testResult.iterations + ' attempts)');
120
+ }
121
+ else {
122
+ console.log(' Script generated successfully!');
123
+ }
124
+ console.log('='.repeat(60));
125
+ console.log('');
126
+ console.log(`Script location: ${path.resolve(finalScriptPath)}`);
127
+ console.log('');
128
+ console.log('To run the script:');
129
+ console.log(` ${finalScriptPath}`);
130
+ console.log('');
131
+ console.log('Or with a custom CDP URL:');
132
+ console.log(` CDP_URL="wss://..." ${finalScriptPath}`);
133
+ console.log('');
134
+ }
135
+ catch (error) {
136
+ console.error('\nFatal error:', error instanceof Error ? error.message : error);
137
+ process.exit(1);
138
+ }
139
+ }
140
+ function printHelp() {
141
+ console.log(`
142
+ Claude-Gen: Browser Automation Script Generator
143
+
144
+ Generate repeatable bash scripts from natural language prompts using
145
+ Claude Code + agent-browser CLI.
146
+
147
+ Usage:
148
+ npx claude-gen "Your task description"
149
+
150
+ Examples:
151
+ npx claude-gen "Go to example.com and get the page title"
152
+ npx claude-gen "Search for 'laptop' on amazon.com and list the first 5 results"
153
+ npx claude-gen "Go to canadagoose.com and extract details of all mens parkas"
154
+
155
+ Options:
156
+ --output, -o <name> Custom output filename (without .sh extension)
157
+ --skip-run Skip automatic script validation
158
+ --help, -h Show this help message
159
+
160
+ Environment Variables:
161
+ CDP_URL WebSocket URL for browser connection (required)
162
+ MODEL Claude model to use (default: claude-opus-4-5-20251101)
163
+ MAX_TURNS Max Claude turns (default: 15)
164
+ OUTPUT_DIR Directory for generated scripts (default: ./generated)
165
+ SESSIONS_DIR Directory for session logs (default: ./sessions)
166
+ MAX_FIX_ITERATIONS Max attempts to fix failing scripts (default: 5)
167
+ FIX_TIMEOUT Timeout for script execution in ms (default: 300000)
168
+ FIX_REQUEST_TIMEOUT Timeout for Claude fix requests in ms (default: 300000)
169
+
170
+ How it works:
171
+ 1. Your prompt is sent to Claude Code with agent-browser instructions
172
+ 2. Claude uses snapshots to understand the page structure
173
+ 3. Claude outputs a complete bash script using replay-safe patterns
174
+ 4. The script is tested and iteratively fixed if needed
175
+ 5. Final script uses only: open, eval, scroll, sleep (no @eN refs)
176
+
177
+ Note: Ensure 'agent-browser' and 'claude' CLIs are installed globally.
178
+ `);
179
+ }
180
+ main();
@@ -0,0 +1,407 @@
1
+ import { spawn } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { writeScript, normalizeScriptCdp } from './codegen/bash-generator.js';
5
+ import { runScript, formatErrorOutput, checkCdpConnectivity } from './script-runner.js';
6
+ import { getFixPrompt, parseFixedScript } from './prompts/fix-prompt.js';
7
+ import { createIterationHistory, addAttemptToHistory, } from './types/iteration-history.js';
8
+ /**
9
+ * Run iterative testing loop: test script, if fails ask Claude to fix, repeat
10
+ */
11
+ export async function runIterativeTest(scriptContent, originalTask, config, customOutput) {
12
+ const maxIterations = config.maxFixIterations;
13
+ console.log(`\n[claude-gen] Starting iterative testing (max ${maxIterations} attempts)...`);
14
+ // Normalize the script's CDP references
15
+ let normalizedScript = normalizeScriptCdp(scriptContent, config.cdpUrl);
16
+ // Check CDP connectivity before starting
17
+ console.log(`[claude-gen] Checking CDP connectivity...`);
18
+ const cdpCheck = await checkCdpConnectivity(config.cdpUrl);
19
+ if (!cdpCheck.connected) {
20
+ console.log(`[claude-gen] WARNING: CDP connection appears stale or unavailable.`);
21
+ console.log(`[claude-gen] Error: ${cdpCheck.error}`);
22
+ console.log(`[claude-gen] Skipping iterative testing - you'll need to test with a fresh CDP_URL.`);
23
+ // Write script anyway but skip testing
24
+ const scriptPath = writeScript(normalizedScript, {
25
+ cdpUrl: config.cdpUrl,
26
+ outputDir: config.outputDir,
27
+ filename: customOutput,
28
+ });
29
+ return {
30
+ success: false,
31
+ iterations: 0,
32
+ finalScriptPath: scriptPath,
33
+ scriptContent: normalizedScript,
34
+ lastError: `CDP connection unavailable: ${cdpCheck.error}`,
35
+ skippedDueToStaleCdp: true,
36
+ };
37
+ }
38
+ console.log(`[claude-gen] CDP connection OK`);
39
+ // Write initial script
40
+ let scriptPath = writeScript(normalizedScript, {
41
+ cdpUrl: config.cdpUrl,
42
+ outputDir: config.outputDir,
43
+ filename: customOutput,
44
+ });
45
+ let currentScript = normalizedScript;
46
+ let lastResult = null;
47
+ // Initialize iteration history for tracking what was tried
48
+ const history = createIterationHistory(originalTask);
49
+ for (let iteration = 1; iteration <= maxIterations; iteration++) {
50
+ console.log(`\n[claude-gen] Iteration ${iteration}/${maxIterations}: Testing script...`);
51
+ // Re-check CDP connectivity before each iteration
52
+ const iterationCdpCheck = await checkCdpConnectivity(config.cdpUrl);
53
+ if (!iterationCdpCheck.connected) {
54
+ console.log(`[claude-gen] CDP connection became unavailable during testing.`);
55
+ console.log(`[claude-gen] Returning current script - test with a fresh CDP_URL.`);
56
+ return {
57
+ success: false,
58
+ iterations: iteration - 1,
59
+ finalScriptPath: scriptPath,
60
+ scriptContent: currentScript,
61
+ lastError: `CDP connection lost: ${iterationCdpCheck.error}`,
62
+ skippedDueToStaleCdp: true,
63
+ };
64
+ }
65
+ // Run the script
66
+ const result = await runScript(scriptPath, config.cdpUrl, config.fixTimeout, originalTask);
67
+ lastResult = result;
68
+ // Validate data quality even if script "succeeded"
69
+ const dataValidation = validateExtractedData(result.stdout || '', originalTask, config);
70
+ if (result.success && dataValidation.valid) {
71
+ console.log(`[claude-gen] Script passed on iteration ${iteration}!`);
72
+ return {
73
+ success: true,
74
+ iterations: iteration,
75
+ finalScriptPath: scriptPath,
76
+ scriptContent: currentScript,
77
+ };
78
+ }
79
+ // Script failed or has data quality issues
80
+ let errorOutput = formatErrorOutput(result);
81
+ // Include validation issues in error output
82
+ if (!dataValidation.valid) {
83
+ const validationError = `[DATA QUALITY ISSUES]\n${dataValidation.issues.join('\n')}\n\n`;
84
+ errorOutput = validationError + errorOutput;
85
+ console.log(`[claude-gen] Data quality issues on iteration ${iteration}:`);
86
+ dataValidation.issues.forEach(issue => console.log(` - ${issue}`));
87
+ }
88
+ else {
89
+ console.log(`[claude-gen] Script failed on iteration ${iteration}`);
90
+ console.log(`[claude-gen] Error: ${errorOutput.substring(0, 300)}...`);
91
+ }
92
+ // If this was the last iteration, don't try to fix
93
+ if (iteration === maxIterations) {
94
+ console.log(`[claude-gen] Max iterations reached. Returning last attempt.`);
95
+ break;
96
+ }
97
+ // Add this attempt to history before asking for fix
98
+ addAttemptToHistory(history, iteration, currentScript, errorOutput);
99
+ // Ask Claude to fix the script with retry for syntax errors
100
+ console.log(`[claude-gen] Asking Claude to fix the script...`);
101
+ const maxSyntaxRetries = 2;
102
+ let fixedScript = null;
103
+ let syntaxRetryError = '';
104
+ for (let syntaxRetry = 0; syntaxRetry <= maxSyntaxRetries; syntaxRetry++) {
105
+ // Include syntax error from previous retry in the error output
106
+ const errorWithSyntax = syntaxRetryError
107
+ ? `[SYNTAX ERROR IN YOUR PREVIOUS FIX]\n${syntaxRetryError}\n\nPlease fix the bash syntax error and try again.\n\n${errorOutput}`
108
+ : errorOutput;
109
+ const attemptedFix = await askClaudeToFix(originalTask, currentScript, errorWithSyntax, result.failedLineNumber, config, history);
110
+ if (!attemptedFix) {
111
+ console.log(`[claude-gen] Could not parse fixed script from Claude's response.`);
112
+ break;
113
+ }
114
+ // Validate bash syntax
115
+ const syntaxError = await validateBashSyntax(attemptedFix);
116
+ if (!syntaxError) {
117
+ // Syntax is valid
118
+ fixedScript = attemptedFix;
119
+ break;
120
+ }
121
+ // Syntax error - retry with error feedback
122
+ syntaxRetryError = syntaxError;
123
+ console.log(`[claude-gen] Fixed script has syntax error (retry ${syntaxRetry + 1}/${maxSyntaxRetries}): ${syntaxError}`);
124
+ if (syntaxRetry < maxSyntaxRetries) {
125
+ console.log(`[claude-gen] Asking Claude to fix the syntax error...`);
126
+ }
127
+ }
128
+ if (!fixedScript) {
129
+ console.log(`[claude-gen] Could not get a syntactically valid fix. Continuing with current script.`);
130
+ continue;
131
+ }
132
+ // Update script content and file
133
+ currentScript = fixedScript;
134
+ // Generate new filename for the fixed version
135
+ const fixedFilename = customOutput
136
+ ? `${customOutput.replace('.sh', '')}-fix${iteration}.sh`
137
+ : `script-fix${iteration}-${Date.now()}.sh`;
138
+ scriptPath = path.join(config.outputDir, fixedFilename);
139
+ fs.writeFileSync(scriptPath, currentScript);
140
+ fs.chmodSync(scriptPath, '755');
141
+ console.log(`[claude-gen] Updated script: ${scriptPath}`);
142
+ }
143
+ // Return last attempt even if failed
144
+ return {
145
+ success: false,
146
+ iterations: maxIterations,
147
+ finalScriptPath: scriptPath,
148
+ scriptContent: currentScript,
149
+ lastError: lastResult ? formatErrorOutput(lastResult) : undefined,
150
+ };
151
+ }
152
+ /**
153
+ * Ask Claude to fix a failing script
154
+ */
155
+ async function askClaudeToFix(originalTask, scriptContent, errorOutput, failedLineNumber, config, history) {
156
+ const fixPrompt = getFixPrompt(originalTask, scriptContent, errorOutput, failedLineNumber, config.cdpUrl, history);
157
+ // Write prompt to temp file
158
+ const promptFile = `/tmp/claude-gen-fix-prompt-${Date.now()}.txt`;
159
+ fs.writeFileSync(promptFile, fixPrompt);
160
+ return new Promise((resolve) => {
161
+ let output = '';
162
+ let resolved = false;
163
+ const cleanup = (promptPath) => {
164
+ try {
165
+ fs.unlinkSync(promptPath);
166
+ }
167
+ catch {
168
+ // Ignore
169
+ }
170
+ };
171
+ const safeResolve = (value) => {
172
+ if (!resolved) {
173
+ resolved = true;
174
+ clearTimeout(timeoutId);
175
+ resolve(value);
176
+ }
177
+ };
178
+ // Call Claude to fix the script - allow Bash so it can inspect the DOM
179
+ const shellCmd = `cat "${promptFile}" | claude -p --model ${config.model} --max-turns 5 --allowedTools "Bash" --output-format stream-json --verbose`;
180
+ const claude = spawn('bash', ['-c', shellCmd], {
181
+ env: { ...process.env, CDP_URL: config.cdpUrl },
182
+ stdio: ['ignore', 'pipe', 'pipe'],
183
+ });
184
+ claude.stdout?.on('data', (data) => {
185
+ output += data.toString();
186
+ });
187
+ claude.stderr?.on('data', (data) => {
188
+ process.stderr.write(`[fix] ${data.toString()}`);
189
+ });
190
+ claude.on('close', (code) => {
191
+ cleanup(promptFile);
192
+ if (code !== 0) {
193
+ console.log(`[claude-gen] Claude fix request failed with code ${code}`);
194
+ safeResolve(null);
195
+ return;
196
+ }
197
+ // Extract text from stream-json format
198
+ const textContent = extractTextFromStreamJson(output);
199
+ // Debug: log if we got no text content
200
+ if (!textContent || textContent.trim().length < 50) {
201
+ console.log(`[claude-gen] Warning: Fix response appears empty or too short (${textContent?.length || 0} chars)`);
202
+ }
203
+ // Parse the fixed script from Claude's response
204
+ const fixedScript = parseFixedScript(textContent);
205
+ // Debug: log if parsing failed
206
+ if (!fixedScript && textContent && textContent.length > 100) {
207
+ console.log(`[claude-gen] Warning: Could not parse script from response. Response preview:`);
208
+ console.log(`[claude-gen] ${textContent.substring(0, 200).replace(/\n/g, '\\n')}...`);
209
+ }
210
+ safeResolve(fixedScript);
211
+ });
212
+ claude.on('error', (err) => {
213
+ cleanup(promptFile);
214
+ console.log(`[claude-gen] Claude fix request error: ${err.message}`);
215
+ safeResolve(null);
216
+ });
217
+ // Timeout for fix request
218
+ const fixRequestTimeout = config.fixRequestTimeout;
219
+ const timeoutId = setTimeout(() => {
220
+ claude.kill();
221
+ cleanup(promptFile);
222
+ console.log(`[claude-gen] Fix request timed out after ${fixRequestTimeout / 1000}s`);
223
+ safeResolve(null);
224
+ }, fixRequestTimeout);
225
+ });
226
+ }
227
+ /**
228
+ * Validate bash script syntax without executing it
229
+ */
230
+ async function validateBashSyntax(script) {
231
+ return new Promise((resolve) => {
232
+ const tempPath = `/tmp/claude-gen-syntax-check-${Date.now()}.sh`;
233
+ fs.writeFileSync(tempPath, script);
234
+ const proc = spawn('bash', ['-n', tempPath], {
235
+ stdio: ['ignore', 'pipe', 'pipe'],
236
+ });
237
+ let stderr = '';
238
+ proc.stderr?.on('data', (data) => {
239
+ stderr += data.toString();
240
+ });
241
+ proc.on('close', (code) => {
242
+ try {
243
+ fs.unlinkSync(tempPath);
244
+ }
245
+ catch {
246
+ // Ignore
247
+ }
248
+ if (code === 0) {
249
+ resolve(null);
250
+ }
251
+ else {
252
+ resolve(stderr.trim() || `Syntax check failed with code ${code}`);
253
+ }
254
+ });
255
+ proc.on('error', (err) => {
256
+ try {
257
+ fs.unlinkSync(tempPath);
258
+ }
259
+ catch {
260
+ // Ignore
261
+ }
262
+ resolve(err.message);
263
+ });
264
+ setTimeout(() => {
265
+ proc.kill();
266
+ try {
267
+ fs.unlinkSync(tempPath);
268
+ }
269
+ catch {
270
+ // Ignore
271
+ }
272
+ resolve('Syntax check timed out');
273
+ }, 5000);
274
+ });
275
+ }
276
+ /**
277
+ * Extract text content from stream-json format output
278
+ */
279
+ function extractTextFromStreamJson(output) {
280
+ const lines = output.split('\n');
281
+ const textParts = [];
282
+ for (const line of lines) {
283
+ if (!line.trim().startsWith('{'))
284
+ continue;
285
+ try {
286
+ const json = JSON.parse(line);
287
+ // Extract text from assistant messages
288
+ if (json.type === 'assistant' && json.message?.content) {
289
+ for (const block of json.message.content) {
290
+ if (block.type === 'text') {
291
+ textParts.push(block.text);
292
+ }
293
+ }
294
+ }
295
+ // Also check for result messages (final output)
296
+ if (json.type === 'result' && json.result) {
297
+ textParts.push(json.result);
298
+ }
299
+ // Handle content_block_delta for streaming responses
300
+ if (json.type === 'content_block_delta' && json.delta?.text) {
301
+ textParts.push(json.delta.text);
302
+ }
303
+ }
304
+ catch {
305
+ // Not valid JSON, might be raw text - only add if it looks like script content
306
+ if (line.includes('#!/bin/bash') || line.includes('agent-browser') || line.includes('```')) {
307
+ textParts.push(line);
308
+ }
309
+ }
310
+ }
311
+ return textParts.join('\n');
312
+ }
313
+ /**
314
+ * Validate extracted data quality
315
+ * Uses configurable thresholds - can be set via config or env vars
316
+ */
317
+ function validateExtractedData(output, taskDescription, config) {
318
+ const issues = [];
319
+ // Get validation thresholds from config or use defaults
320
+ const thresholds = config?.validation ?? {
321
+ minPriceRate: 0.9,
322
+ minRatingRate: 0.8,
323
+ minItemCount: 1,
324
+ requirePrices: true,
325
+ requireRatings: true,
326
+ };
327
+ try {
328
+ // Try to find JSON in output - handle both regular and double-encoded JSON
329
+ let jsonMatch = output.match(/\{[\s\S]*"items"[\s\S]*\}/);
330
+ // If no match, try to handle double-encoded JSON (from agent-browser eval)
331
+ // The output looks like: "{\n \"totalExtracted\": 22,\n \"items\": [..."
332
+ if (!jsonMatch) {
333
+ // Look for escaped JSON pattern
334
+ const escapedMatch = output.match(/"(\{[\s\S]*\\?"items\\?"[\s\S]*\})"/);
335
+ if (escapedMatch) {
336
+ // Parse the outer JSON string to get the inner JSON
337
+ try {
338
+ const innerJson = JSON.parse('"' + escapedMatch[1] + '"');
339
+ jsonMatch = [innerJson];
340
+ }
341
+ catch {
342
+ // If that fails, try simple unescape
343
+ const unescaped = escapedMatch[1]
344
+ .replace(/\\n/g, '\n')
345
+ .replace(/\\"/g, '"')
346
+ .replace(/\\\\/g, '\\');
347
+ jsonMatch = [unescaped];
348
+ }
349
+ }
350
+ }
351
+ if (!jsonMatch)
352
+ return { valid: true, issues: [] }; // Can't validate, assume OK
353
+ const data = JSON.parse(jsonMatch[0]);
354
+ const items = data.items || [];
355
+ // Fail validation if zero items
356
+ if (items.length === 0) {
357
+ issues.push('No items extracted');
358
+ return { valid: false, issues };
359
+ }
360
+ // Check minimum item count
361
+ if (items.length < thresholds.minItemCount) {
362
+ issues.push(`Only ${items.length} items extracted (need ${thresholds.minItemCount}+). Check if your selector targets the main product grid (not ads/carousel).`);
363
+ }
364
+ // Check for placeholder values (N/A, empty, etc.)
365
+ const invalidValues = ['', 'N/A', 'n/a', 'TBD', 'null', 'undefined'];
366
+ // Price validation - only if prices are required
367
+ if (thresholds.requirePrices) {
368
+ const invalidPrices = items.filter((i) => {
369
+ const price = (i.price || '').trim();
370
+ return invalidValues.includes(price) || !price;
371
+ }).length;
372
+ const priceRate = (items.length - invalidPrices) / items.length;
373
+ if (priceRate < thresholds.minPriceRate) {
374
+ issues.push(`Only ${Math.round(priceRate * 100)}% of items have valid prices (need ${Math.round(thresholds.minPriceRate * 100)}%+)`);
375
+ }
376
+ }
377
+ // Rating validation - only if ratings are required
378
+ if (thresholds.requireRatings) {
379
+ const invalidRatings = items.filter((i) => {
380
+ const rating = (i.rating || '').trim();
381
+ return invalidValues.includes(rating) || !rating;
382
+ }).length;
383
+ const ratingRate = (items.length - invalidRatings) / items.length;
384
+ if (ratingRate < thresholds.minRatingRate) {
385
+ issues.push(`Only ${Math.round(ratingRate * 100)}% of items have valid ratings (need ${Math.round(thresholds.minRatingRate * 100)}%+)`);
386
+ }
387
+ }
388
+ return { valid: issues.length === 0, issues };
389
+ }
390
+ catch {
391
+ return { valid: true, issues: [] }; // Can't parse, don't fail validation
392
+ }
393
+ }
394
+ // Legacy function signature for backwards compatibility
395
+ export async function runIterativeTestFromCommands(commands, originalTask, config, customOutput) {
396
+ // Import the legacy generator
397
+ const { generateBashScript } = await import('./codegen/bash-generator.js');
398
+ // Generate script from commands
399
+ const scriptPath = generateBashScript(commands, {
400
+ cdpUrl: config.cdpUrl,
401
+ outputDir: config.outputDir,
402
+ filename: customOutput,
403
+ });
404
+ const scriptContent = fs.readFileSync(scriptPath, 'utf-8');
405
+ // Run the iterative test
406
+ return runIterativeTest(scriptContent, originalTask, config, customOutput);
407
+ }
@@ -0,0 +1,38 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ /**
4
+ * Read captured commands from a session log file
5
+ */
6
+ export function readCommandLog(sessionId, sessionsDir) {
7
+ const sessionFile = path.join(sessionsDir, `${sessionId}.jsonl`);
8
+ if (!fs.existsSync(sessionFile)) {
9
+ return [];
10
+ }
11
+ const content = fs.readFileSync(sessionFile, 'utf-8');
12
+ const lines = content.trim().split('\n').filter(line => line.trim());
13
+ return lines.map(line => {
14
+ try {
15
+ return JSON.parse(line);
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }).filter((entry) => entry !== null);
21
+ }
22
+ /**
23
+ * Get only successful agent-browser commands
24
+ */
25
+ export function getSuccessfulCommands(entries) {
26
+ return entries
27
+ .filter(entry => entry.success && entry.command.includes('agent-browser'))
28
+ .map(entry => entry.command);
29
+ }
30
+ /**
31
+ * Clean up session file after script generation
32
+ */
33
+ export function cleanupSession(sessionId, sessionsDir) {
34
+ const sessionFile = path.join(sessionsDir, `${sessionId}.jsonl`);
35
+ if (fs.existsSync(sessionFile)) {
36
+ fs.unlinkSync(sessionFile);
37
+ }
38
+ }