@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/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
|
+
}
|