@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 CHANGED
@@ -17,7 +17,7 @@ AI-powered browser automation - extract data from any website using natural lang
17
17
 
18
18
  ```bash
19
19
  # Install globally
20
- npm install -g chase-browser
20
+ npm install -g @browsercash/chase
21
21
 
22
22
  # Set your API key
23
23
  export BROWSER_CASH_API_KEY="your-key"
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
- const API_BASE = 'https://chase-api-gth2quoxyq-uc.a.run.app';
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 req = https.request(options, (res) => {
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 req = https.request(options, (res) => {
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 || '15', 10),
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
- const bucket = storage.bucket(BUCKET_NAME);
128
- const file = bucket.file(`tasks/${task.taskId}.json`);
129
- await file.save(JSON.stringify(task, null, 2), {
130
- contentType: 'application/json',
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
- sendEvent('log', { message: 'Successfully extracted result from Claude output', level: 'info' });
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
- sendEvent('log', { message: 'Could not extract structured result from Claude output', level: 'warn' });
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
- // Fallback: try to find any JSON that looks like extracted data
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browsercash/chase",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "AI-powered browser automation - extract data from any website with natural language",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",