@browsercash/chase 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +10 -5
- package/dist/config.js +1 -1
- package/dist/prompts/agentic-prompt.js +18 -0
- package/dist/server.js +136 -15
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* chase task <task-id>
|
|
14
14
|
*/
|
|
15
15
|
import * as https from 'https';
|
|
16
|
-
|
|
16
|
+
import * as http from 'http';
|
|
17
|
+
const API_BASE = process.env.CHASE_API_URL || 'https://chase-api-gth2quoxyq-uc.a.run.app';
|
|
17
18
|
function getApiKey() {
|
|
18
19
|
const key = process.env.BROWSER_CASH_API_KEY;
|
|
19
20
|
if (!key) {
|
|
@@ -57,16 +58,18 @@ function parseArgs() {
|
|
|
57
58
|
async function streamRequest(endpoint, body, onEvent) {
|
|
58
59
|
return new Promise((resolve, reject) => {
|
|
59
60
|
const url = new URL(endpoint, API_BASE);
|
|
61
|
+
const isHttps = url.protocol === 'https:';
|
|
60
62
|
const options = {
|
|
61
63
|
hostname: url.hostname,
|
|
62
|
-
port: 443,
|
|
64
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
63
65
|
path: url.pathname,
|
|
64
66
|
method: 'POST',
|
|
65
67
|
headers: {
|
|
66
68
|
'Content-Type': 'application/json',
|
|
67
69
|
},
|
|
68
70
|
};
|
|
69
|
-
const
|
|
71
|
+
const transport = isHttps ? https : http;
|
|
72
|
+
const req = transport.request(options, (res) => {
|
|
70
73
|
let buffer = '';
|
|
71
74
|
res.on('data', (chunk) => {
|
|
72
75
|
buffer += chunk.toString();
|
|
@@ -106,16 +109,18 @@ async function streamRequest(endpoint, body, onEvent) {
|
|
|
106
109
|
async function apiGet(endpoint, apiKey) {
|
|
107
110
|
return new Promise((resolve, reject) => {
|
|
108
111
|
const url = new URL(endpoint, API_BASE);
|
|
112
|
+
const isHttps = url.protocol === 'https:';
|
|
109
113
|
const options = {
|
|
110
114
|
hostname: url.hostname,
|
|
111
|
-
port: 443,
|
|
115
|
+
port: url.port || (isHttps ? 443 : 80),
|
|
112
116
|
path: url.pathname,
|
|
113
117
|
method: 'GET',
|
|
114
118
|
headers: {
|
|
115
119
|
'x-api-key': apiKey,
|
|
116
120
|
},
|
|
117
121
|
};
|
|
118
|
-
const
|
|
122
|
+
const transport = isHttps ? https : http;
|
|
123
|
+
const req = transport.request(options, (res) => {
|
|
119
124
|
let data = '';
|
|
120
125
|
res.on('data', (chunk) => (data += chunk.toString()));
|
|
121
126
|
res.on('end', () => {
|
package/dist/config.js
CHANGED
|
@@ -100,7 +100,7 @@ export function loadConfig(taskDescriptionOrOptions) {
|
|
|
100
100
|
cdpUrl,
|
|
101
101
|
outputDir: process.env.OUTPUT_DIR || './generated',
|
|
102
102
|
sessionsDir: process.env.SESSIONS_DIR || './sessions',
|
|
103
|
-
maxTurns: parseInt(process.env.MAX_TURNS || '
|
|
103
|
+
maxTurns: parseInt(process.env.MAX_TURNS || '30', 10),
|
|
104
104
|
model: process.env.MODEL || 'claude-opus-4-5-20251101',
|
|
105
105
|
maxFixIterations: parseInt(process.env.MAX_FIX_ITERATIONS || '5', 10),
|
|
106
106
|
// Script execution timeout (default 5 minutes for complex multi-page tasks)
|
|
@@ -74,5 +74,23 @@ agent-browser --cdp "$CDP_URL" eval 'JSON.stringify(Array.from(document.querySel
|
|
|
74
74
|
|
|
75
75
|
4. Output final JSON result
|
|
76
76
|
|
|
77
|
+
## CRITICAL: Output Format Requirement
|
|
78
|
+
|
|
79
|
+
Your FINAL message MUST contain a JSON code block. This is REQUIRED for the system to process your results.
|
|
80
|
+
|
|
81
|
+
**For success - use EXACTLY this format:**
|
|
82
|
+
\`\`\`json
|
|
83
|
+
{"success": true, "data": {...}, "summary": "Brief description"}
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
**For failure - use EXACTLY this format:**
|
|
87
|
+
\`\`\`json
|
|
88
|
+
{"success": false, "error": "What went wrong", "attempted": "What was tried"}
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
Do NOT output results as plain text. Always wrap in \`\`\`json code fence.
|
|
92
|
+
Do NOT use comments inside JSON. JSON must be valid and parseable.
|
|
93
|
+
The JSON block should be your LAST output after completing all actions.
|
|
94
|
+
|
|
77
95
|
NOW: Perform the requested task and return the results.`;
|
|
78
96
|
}
|
package/dist/server.js
CHANGED
|
@@ -121,14 +121,21 @@ function generateTaskId() {
|
|
|
121
121
|
return `task-${timestamp}-${random}`;
|
|
122
122
|
}
|
|
123
123
|
/**
|
|
124
|
-
* Save a task record to GCS
|
|
124
|
+
* Save a task record to GCS (gracefully handles missing credentials for local dev)
|
|
125
125
|
*/
|
|
126
126
|
async function saveTask(task) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
try {
|
|
128
|
+
const bucket = storage.bucket(BUCKET_NAME);
|
|
129
|
+
const file = bucket.file(`tasks/${task.taskId}.json`);
|
|
130
|
+
await file.save(JSON.stringify(task, null, 2), {
|
|
131
|
+
contentType: 'application/json',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
// Gracefully handle GCS errors (e.g., missing credentials in local dev)
|
|
136
|
+
// Task will still work, just won't be persisted for later retrieval
|
|
137
|
+
console.warn(`[saveTask] Could not persist task ${task.taskId} to GCS:`, err.message);
|
|
138
|
+
}
|
|
132
139
|
}
|
|
133
140
|
/**
|
|
134
141
|
* Get a task record from GCS, verifying ownership
|
|
@@ -1405,10 +1412,23 @@ async function runAgenticAutomation(taskPrompt, config, sendEvent) {
|
|
|
1405
1412
|
catch {
|
|
1406
1413
|
// Ignore cleanup errors
|
|
1407
1414
|
}
|
|
1415
|
+
// Check if we likely hit max turns limit
|
|
1416
|
+
const assistantTurns = output.split('\n').filter((l) => l.includes('"type":"assistant"')).length;
|
|
1417
|
+
if (assistantTurns >= config.maxTurns - 1) {
|
|
1418
|
+
sendEvent('log', {
|
|
1419
|
+
message: `Task may have hit max turns limit (${config.maxTurns}). Consider increasing MAX_TURNS env var or --max-turns flag.`,
|
|
1420
|
+
level: 'warn',
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1408
1423
|
// Extract the JSON result from Claude's output
|
|
1409
1424
|
const result = extractAgenticResult(output);
|
|
1410
1425
|
if (result) {
|
|
1411
|
-
|
|
1426
|
+
if (result.success) {
|
|
1427
|
+
sendEvent('log', { message: 'Successfully extracted result from Claude output', level: 'info' });
|
|
1428
|
+
}
|
|
1429
|
+
else {
|
|
1430
|
+
sendEvent('log', { message: 'Task completed with failure status', level: 'warn' });
|
|
1431
|
+
}
|
|
1412
1432
|
resolve({
|
|
1413
1433
|
success: result.success,
|
|
1414
1434
|
result: result.data,
|
|
@@ -1417,11 +1437,17 @@ async function runAgenticAutomation(taskPrompt, config, sendEvent) {
|
|
|
1417
1437
|
});
|
|
1418
1438
|
}
|
|
1419
1439
|
else {
|
|
1420
|
-
|
|
1440
|
+
// No result could be extracted - provide context for debugging
|
|
1441
|
+
const textContent = extractTextFromStreamJson(output);
|
|
1442
|
+
const lastOutput = textContent.slice(-500);
|
|
1443
|
+
sendEvent('log', {
|
|
1444
|
+
message: `Could not extract structured result from Claude output. Last output: ${lastOutput.substring(0, 200)}...`,
|
|
1445
|
+
level: 'warn',
|
|
1446
|
+
});
|
|
1421
1447
|
resolve({
|
|
1422
1448
|
success: false,
|
|
1423
1449
|
result: null,
|
|
1424
|
-
error: code !== 0 ? `Exit code: ${code}` : 'Failed to extract result from output',
|
|
1450
|
+
error: code !== 0 ? `Exit code: ${code}` : 'Failed to extract result from output. Claude may not have produced JSON in expected format.',
|
|
1425
1451
|
});
|
|
1426
1452
|
}
|
|
1427
1453
|
});
|
|
@@ -1445,10 +1471,11 @@ async function runAgenticAutomation(taskPrompt, config, sendEvent) {
|
|
|
1445
1471
|
/**
|
|
1446
1472
|
* Extract the final JSON result from agentic automation output.
|
|
1447
1473
|
* Looks for the structured JSON output format in Claude's response.
|
|
1474
|
+
* Uses multiple strategies to be resilient to format variations.
|
|
1448
1475
|
*/
|
|
1449
1476
|
function extractAgenticResult(output) {
|
|
1450
1477
|
const textContent = extractTextFromStreamJson(output);
|
|
1451
|
-
// Look for JSON code blocks with our expected format
|
|
1478
|
+
// Strategy 1: Look for JSON code blocks with our expected format (```json ... ```)
|
|
1452
1479
|
const jsonBlockPattern = /```json\s*([\s\S]*?)```/g;
|
|
1453
1480
|
let lastValidResult = null;
|
|
1454
1481
|
let match;
|
|
@@ -1471,22 +1498,29 @@ function extractAgenticResult(output) {
|
|
|
1471
1498
|
// Not valid JSON, continue looking
|
|
1472
1499
|
}
|
|
1473
1500
|
}
|
|
1474
|
-
// If we found a structured result, return it
|
|
1475
1501
|
if (lastValidResult) {
|
|
1476
1502
|
return lastValidResult;
|
|
1477
1503
|
}
|
|
1478
|
-
//
|
|
1479
|
-
// This handles cases where Claude outputs data directly without our wrapper format
|
|
1504
|
+
// Strategy 2: Look for any code blocks with JSON data (``` ... ```)
|
|
1480
1505
|
const anyJsonPattern = /```(?:json)?\s*([\s\S]*?)```/g;
|
|
1481
1506
|
while ((match = anyJsonPattern.exec(textContent)) !== null) {
|
|
1482
1507
|
try {
|
|
1483
1508
|
const jsonStr = match[1].trim();
|
|
1484
|
-
// Skip if it doesn't look like JSON
|
|
1485
1509
|
if (!jsonStr.startsWith('{') && !jsonStr.startsWith('['))
|
|
1486
1510
|
continue;
|
|
1487
1511
|
const parsed = JSON.parse(jsonStr);
|
|
1488
|
-
// If it's an array or object with data, wrap it in our format
|
|
1489
1512
|
if (Array.isArray(parsed) || (typeof parsed === 'object' && parsed !== null)) {
|
|
1513
|
+
// Check if it has success field (format we expect)
|
|
1514
|
+
if ('success' in parsed) {
|
|
1515
|
+
return {
|
|
1516
|
+
success: parsed.success === true,
|
|
1517
|
+
data: parsed.data,
|
|
1518
|
+
summary: parsed.summary,
|
|
1519
|
+
error: parsed.error,
|
|
1520
|
+
attempted: parsed.attempted,
|
|
1521
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
// Otherwise wrap it
|
|
1490
1524
|
return {
|
|
1491
1525
|
success: true,
|
|
1492
1526
|
data: parsed,
|
|
@@ -1498,6 +1532,93 @@ function extractAgenticResult(output) {
|
|
|
1498
1532
|
// Not valid JSON, continue looking
|
|
1499
1533
|
}
|
|
1500
1534
|
}
|
|
1535
|
+
// Strategy 3: Look for unfenced JSON objects with success field anywhere in text
|
|
1536
|
+
// This handles cases where Claude outputs JSON without code fences
|
|
1537
|
+
const unfencedPattern = /\{\s*"success"\s*:\s*(true|false)[\s\S]*?\}(?=\s*$|\s*\n\s*\n|\s*[^,\]}])/g;
|
|
1538
|
+
while ((match = unfencedPattern.exec(textContent)) !== null) {
|
|
1539
|
+
try {
|
|
1540
|
+
// Try to find the complete JSON object by balancing braces
|
|
1541
|
+
const startIdx = match.index;
|
|
1542
|
+
let depth = 0;
|
|
1543
|
+
let endIdx = startIdx;
|
|
1544
|
+
for (let i = startIdx; i < textContent.length; i++) {
|
|
1545
|
+
if (textContent[i] === '{')
|
|
1546
|
+
depth++;
|
|
1547
|
+
else if (textContent[i] === '}') {
|
|
1548
|
+
depth--;
|
|
1549
|
+
if (depth === 0) {
|
|
1550
|
+
endIdx = i + 1;
|
|
1551
|
+
break;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
const jsonStr = textContent.substring(startIdx, endIdx);
|
|
1556
|
+
const parsed = JSON.parse(jsonStr);
|
|
1557
|
+
if (typeof parsed === 'object' && parsed !== null && 'success' in parsed) {
|
|
1558
|
+
return {
|
|
1559
|
+
success: parsed.success === true,
|
|
1560
|
+
data: parsed.data,
|
|
1561
|
+
summary: parsed.summary,
|
|
1562
|
+
error: parsed.error,
|
|
1563
|
+
attempted: parsed.attempted,
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
catch {
|
|
1568
|
+
// Continue looking
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
// Strategy 4: Look for any JSON array/object in the last portion of output
|
|
1572
|
+
// Sometimes Claude puts the result at the end without code fences
|
|
1573
|
+
const lastChunk = textContent.slice(-3000);
|
|
1574
|
+
const jsonMatches = lastChunk.match(/(\[[\s\S]*\]|\{[\s\S]*\})/g);
|
|
1575
|
+
if (jsonMatches) {
|
|
1576
|
+
// Try from last to first (most likely to be the result)
|
|
1577
|
+
for (let i = jsonMatches.length - 1; i >= 0; i--) {
|
|
1578
|
+
try {
|
|
1579
|
+
const parsed = JSON.parse(jsonMatches[i]);
|
|
1580
|
+
if (Array.isArray(parsed) && parsed.length > 0) {
|
|
1581
|
+
return {
|
|
1582
|
+
success: true,
|
|
1583
|
+
data: parsed,
|
|
1584
|
+
summary: `Extracted ${parsed.length} items from output`,
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
1588
|
+
if ('success' in parsed) {
|
|
1589
|
+
return {
|
|
1590
|
+
success: parsed.success === true,
|
|
1591
|
+
data: parsed.data,
|
|
1592
|
+
summary: parsed.summary,
|
|
1593
|
+
error: parsed.error,
|
|
1594
|
+
attempted: parsed.attempted,
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
if (Object.keys(parsed).length > 0) {
|
|
1598
|
+
return {
|
|
1599
|
+
success: true,
|
|
1600
|
+
data: parsed,
|
|
1601
|
+
summary: 'Extracted data from output',
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
catch {
|
|
1607
|
+
// Not valid JSON
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
// Strategy 5: If we have substantial text content but no JSON, return it as a failure with context
|
|
1612
|
+
// This helps users understand what happened
|
|
1613
|
+
if (textContent.trim().length > 50) {
|
|
1614
|
+
return {
|
|
1615
|
+
success: false,
|
|
1616
|
+
error: 'Could not parse structured JSON result from output',
|
|
1617
|
+
data: null,
|
|
1618
|
+
summary: 'Task completed but output was not in expected format',
|
|
1619
|
+
rawOutput: textContent.slice(-2000), // Last 2000 chars for debugging
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1501
1622
|
return null;
|
|
1502
1623
|
}
|
|
1503
1624
|
/**
|