@browsercash/chase 1.0.0 → 1.2.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', () => {
@@ -154,6 +159,9 @@ async function commandAutomate(task, flags) {
154
159
  if (flags.captcha) {
155
160
  body.browserOptions = { ...(body.browserOptions || {}), captchaSolver: true };
156
161
  }
162
+ if (flags['max-turns']) {
163
+ body.maxTurns = parseInt(flags['max-turns'], 10);
164
+ }
157
165
  let taskId = null;
158
166
  let result = null;
159
167
  await streamRequest('/automate/stream', body, (type, data) => {
@@ -433,6 +441,7 @@ OPTIONS:
433
441
  --country <code> Use a browser from specific country (e.g., US, DE, JP)
434
442
  --adblock Enable ad-blocking
435
443
  --captcha Enable CAPTCHA solving
444
+ --max-turns <n> Max Claude iterations (default: 30, use 50+ for complex tasks)
436
445
  --quiet Reduce output verbosity
437
446
  --skip-test Skip script testing (generate only)
438
447
  --help Show this help message
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
@@ -789,11 +796,12 @@ server.post('/automate/stream', {
789
796
  captchaSolver: { type: 'boolean' },
790
797
  },
791
798
  },
799
+ maxTurns: { type: 'integer', minimum: 1, maximum: 100 },
792
800
  },
793
801
  },
794
802
  },
795
803
  }, async (request, reply) => {
796
- const { task, browserCashApiKey, cdpUrl, browserOptions } = request.body;
804
+ const { task, browserCashApiKey, cdpUrl, browserOptions, maxTurns } = request.body;
797
805
  // Validate that either browserCashApiKey or cdpUrl is provided
798
806
  if (!browserCashApiKey && !cdpUrl) {
799
807
  reply.raw.writeHead(400, { 'Content-Type': 'application/json' });
@@ -879,6 +887,10 @@ server.post('/automate/stream', {
879
887
  cdpUrl: effectiveCdpUrl,
880
888
  taskDescription: task,
881
889
  });
890
+ // Override maxTurns if provided in request
891
+ if (maxTurns) {
892
+ config.maxTurns = maxTurns;
893
+ }
882
894
  sendEvent('start', {
883
895
  taskId,
884
896
  task,
@@ -1405,10 +1417,23 @@ async function runAgenticAutomation(taskPrompt, config, sendEvent) {
1405
1417
  catch {
1406
1418
  // Ignore cleanup errors
1407
1419
  }
1420
+ // Check if we likely hit max turns limit
1421
+ const assistantTurns = output.split('\n').filter((l) => l.includes('"type":"assistant"')).length;
1422
+ if (assistantTurns >= config.maxTurns - 1) {
1423
+ sendEvent('log', {
1424
+ message: `Task may have hit max turns limit (${config.maxTurns}). Consider increasing MAX_TURNS env var or --max-turns flag.`,
1425
+ level: 'warn',
1426
+ });
1427
+ }
1408
1428
  // Extract the JSON result from Claude's output
1409
1429
  const result = extractAgenticResult(output);
1410
1430
  if (result) {
1411
- sendEvent('log', { message: 'Successfully extracted result from Claude output', level: 'info' });
1431
+ if (result.success) {
1432
+ sendEvent('log', { message: 'Successfully extracted result from Claude output', level: 'info' });
1433
+ }
1434
+ else {
1435
+ sendEvent('log', { message: 'Task completed with failure status', level: 'warn' });
1436
+ }
1412
1437
  resolve({
1413
1438
  success: result.success,
1414
1439
  result: result.data,
@@ -1417,11 +1442,17 @@ async function runAgenticAutomation(taskPrompt, config, sendEvent) {
1417
1442
  });
1418
1443
  }
1419
1444
  else {
1420
- sendEvent('log', { message: 'Could not extract structured result from Claude output', level: 'warn' });
1445
+ // No result could be extracted - provide context for debugging
1446
+ const textContent = extractTextFromStreamJson(output);
1447
+ const lastOutput = textContent.slice(-500);
1448
+ sendEvent('log', {
1449
+ message: `Could not extract structured result from Claude output. Last output: ${lastOutput.substring(0, 200)}...`,
1450
+ level: 'warn',
1451
+ });
1421
1452
  resolve({
1422
1453
  success: false,
1423
1454
  result: null,
1424
- error: code !== 0 ? `Exit code: ${code}` : 'Failed to extract result from output',
1455
+ error: code !== 0 ? `Exit code: ${code}` : 'Failed to extract result from output. Claude may not have produced JSON in expected format.',
1425
1456
  });
1426
1457
  }
1427
1458
  });
@@ -1445,10 +1476,11 @@ async function runAgenticAutomation(taskPrompt, config, sendEvent) {
1445
1476
  /**
1446
1477
  * Extract the final JSON result from agentic automation output.
1447
1478
  * Looks for the structured JSON output format in Claude's response.
1479
+ * Uses multiple strategies to be resilient to format variations.
1448
1480
  */
1449
1481
  function extractAgenticResult(output) {
1450
1482
  const textContent = extractTextFromStreamJson(output);
1451
- // Look for JSON code blocks with our expected format
1483
+ // Strategy 1: Look for JSON code blocks with our expected format (```json ... ```)
1452
1484
  const jsonBlockPattern = /```json\s*([\s\S]*?)```/g;
1453
1485
  let lastValidResult = null;
1454
1486
  let match;
@@ -1471,22 +1503,29 @@ function extractAgenticResult(output) {
1471
1503
  // Not valid JSON, continue looking
1472
1504
  }
1473
1505
  }
1474
- // If we found a structured result, return it
1475
1506
  if (lastValidResult) {
1476
1507
  return lastValidResult;
1477
1508
  }
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
1509
+ // Strategy 2: Look for any code blocks with JSON data (``` ... ```)
1480
1510
  const anyJsonPattern = /```(?:json)?\s*([\s\S]*?)```/g;
1481
1511
  while ((match = anyJsonPattern.exec(textContent)) !== null) {
1482
1512
  try {
1483
1513
  const jsonStr = match[1].trim();
1484
- // Skip if it doesn't look like JSON
1485
1514
  if (!jsonStr.startsWith('{') && !jsonStr.startsWith('['))
1486
1515
  continue;
1487
1516
  const parsed = JSON.parse(jsonStr);
1488
- // If it's an array or object with data, wrap it in our format
1489
1517
  if (Array.isArray(parsed) || (typeof parsed === 'object' && parsed !== null)) {
1518
+ // Check if it has success field (format we expect)
1519
+ if ('success' in parsed) {
1520
+ return {
1521
+ success: parsed.success === true,
1522
+ data: parsed.data,
1523
+ summary: parsed.summary,
1524
+ error: parsed.error,
1525
+ attempted: parsed.attempted,
1526
+ };
1527
+ }
1528
+ // Otherwise wrap it
1490
1529
  return {
1491
1530
  success: true,
1492
1531
  data: parsed,
@@ -1498,6 +1537,93 @@ function extractAgenticResult(output) {
1498
1537
  // Not valid JSON, continue looking
1499
1538
  }
1500
1539
  }
1540
+ // Strategy 3: Look for unfenced JSON objects with success field anywhere in text
1541
+ // This handles cases where Claude outputs JSON without code fences
1542
+ const unfencedPattern = /\{\s*"success"\s*:\s*(true|false)[\s\S]*?\}(?=\s*$|\s*\n\s*\n|\s*[^,\]}])/g;
1543
+ while ((match = unfencedPattern.exec(textContent)) !== null) {
1544
+ try {
1545
+ // Try to find the complete JSON object by balancing braces
1546
+ const startIdx = match.index;
1547
+ let depth = 0;
1548
+ let endIdx = startIdx;
1549
+ for (let i = startIdx; i < textContent.length; i++) {
1550
+ if (textContent[i] === '{')
1551
+ depth++;
1552
+ else if (textContent[i] === '}') {
1553
+ depth--;
1554
+ if (depth === 0) {
1555
+ endIdx = i + 1;
1556
+ break;
1557
+ }
1558
+ }
1559
+ }
1560
+ const jsonStr = textContent.substring(startIdx, endIdx);
1561
+ const parsed = JSON.parse(jsonStr);
1562
+ if (typeof parsed === 'object' && parsed !== null && 'success' in parsed) {
1563
+ return {
1564
+ success: parsed.success === true,
1565
+ data: parsed.data,
1566
+ summary: parsed.summary,
1567
+ error: parsed.error,
1568
+ attempted: parsed.attempted,
1569
+ };
1570
+ }
1571
+ }
1572
+ catch {
1573
+ // Continue looking
1574
+ }
1575
+ }
1576
+ // Strategy 4: Look for any JSON array/object in the last portion of output
1577
+ // Sometimes Claude puts the result at the end without code fences
1578
+ const lastChunk = textContent.slice(-3000);
1579
+ const jsonMatches = lastChunk.match(/(\[[\s\S]*\]|\{[\s\S]*\})/g);
1580
+ if (jsonMatches) {
1581
+ // Try from last to first (most likely to be the result)
1582
+ for (let i = jsonMatches.length - 1; i >= 0; i--) {
1583
+ try {
1584
+ const parsed = JSON.parse(jsonMatches[i]);
1585
+ if (Array.isArray(parsed) && parsed.length > 0) {
1586
+ return {
1587
+ success: true,
1588
+ data: parsed,
1589
+ summary: `Extracted ${parsed.length} items from output`,
1590
+ };
1591
+ }
1592
+ if (typeof parsed === 'object' && parsed !== null) {
1593
+ if ('success' in parsed) {
1594
+ return {
1595
+ success: parsed.success === true,
1596
+ data: parsed.data,
1597
+ summary: parsed.summary,
1598
+ error: parsed.error,
1599
+ attempted: parsed.attempted,
1600
+ };
1601
+ }
1602
+ if (Object.keys(parsed).length > 0) {
1603
+ return {
1604
+ success: true,
1605
+ data: parsed,
1606
+ summary: 'Extracted data from output',
1607
+ };
1608
+ }
1609
+ }
1610
+ }
1611
+ catch {
1612
+ // Not valid JSON
1613
+ }
1614
+ }
1615
+ }
1616
+ // Strategy 5: If we have substantial text content but no JSON, return it as a failure with context
1617
+ // This helps users understand what happened
1618
+ if (textContent.trim().length > 50) {
1619
+ return {
1620
+ success: false,
1621
+ error: 'Could not parse structured JSON result from output',
1622
+ data: null,
1623
+ summary: 'Task completed but output was not in expected format',
1624
+ rawOutput: textContent.slice(-2000), // Last 2000 chars for debugging
1625
+ };
1626
+ }
1501
1627
  return null;
1502
1628
  }
1503
1629
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browsercash/chase",
3
- "version": "1.0.0",
3
+ "version": "1.2.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",