@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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser.cash API Client
|
|
3
|
+
*
|
|
4
|
+
* Handles creating, managing, and stopping browser sessions via the Browser.cash API.
|
|
5
|
+
*/
|
|
6
|
+
const BROWSER_CASH_API_URL = process.env.BROWSER_CASH_API_URL || 'https://api.browser.cash';
|
|
7
|
+
/**
|
|
8
|
+
* Create a new browser session
|
|
9
|
+
*/
|
|
10
|
+
export async function createBrowserSession(apiKey, options = {}) {
|
|
11
|
+
const response = await fetch(`${BROWSER_CASH_API_URL}/v1/browser/session`, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify(options),
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
const error = await response.json();
|
|
21
|
+
throw new Error(`Browser.cash API error: ${error.error || response.statusText}`);
|
|
22
|
+
}
|
|
23
|
+
return response.json();
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Get an existing browser session
|
|
27
|
+
*/
|
|
28
|
+
export async function getBrowserSession(apiKey, sessionId) {
|
|
29
|
+
const response = await fetch(`${BROWSER_CASH_API_URL}/v1/browser/session?sessionId=${encodeURIComponent(sessionId)}`, {
|
|
30
|
+
method: 'GET',
|
|
31
|
+
headers: {
|
|
32
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const error = await response.json();
|
|
37
|
+
throw new Error(`Browser.cash API error: ${error.error || response.statusText}`);
|
|
38
|
+
}
|
|
39
|
+
return response.json();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Stop a browser session
|
|
43
|
+
*/
|
|
44
|
+
export async function stopBrowserSession(apiKey, sessionId) {
|
|
45
|
+
const response = await fetch(`${BROWSER_CASH_API_URL}/v1/browser/session?sessionId=${encodeURIComponent(sessionId)}`, {
|
|
46
|
+
method: 'DELETE',
|
|
47
|
+
headers: {
|
|
48
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const error = await response.json();
|
|
53
|
+
throw new Error(`Browser.cash API error: ${error.error || response.statusText}`);
|
|
54
|
+
}
|
|
55
|
+
return response.json();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Wait for a session to become active and return its CDP URL
|
|
59
|
+
*/
|
|
60
|
+
export async function waitForSessionReady(apiKey, sessionId, timeoutMs = 30000, pollIntervalMs = 1000) {
|
|
61
|
+
const startTime = Date.now();
|
|
62
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
63
|
+
const session = await getBrowserSession(apiKey, sessionId);
|
|
64
|
+
if (session.status === 'active' && session.cdpUrl) {
|
|
65
|
+
return session.cdpUrl;
|
|
66
|
+
}
|
|
67
|
+
if (session.status === 'error') {
|
|
68
|
+
throw new Error('Browser session failed to start');
|
|
69
|
+
}
|
|
70
|
+
if (session.status === 'completed') {
|
|
71
|
+
throw new Error('Browser session already completed');
|
|
72
|
+
}
|
|
73
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
74
|
+
}
|
|
75
|
+
throw new Error(`Timeout waiting for browser session to become ready`);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Create a browser session and wait for it to be ready
|
|
79
|
+
*/
|
|
80
|
+
export async function createAndWaitForSession(apiKey, options = {}, timeoutMs = 30000) {
|
|
81
|
+
const session = await createBrowserSession(apiKey, options);
|
|
82
|
+
// If session is already active with CDP URL, return immediately
|
|
83
|
+
if (session.status === 'active' && session.cdpUrl) {
|
|
84
|
+
return { session, cdpUrl: session.cdpUrl };
|
|
85
|
+
}
|
|
86
|
+
// Otherwise wait for it to be ready
|
|
87
|
+
const cdpUrl = await waitForSessionReady(apiKey, session.sessionId, timeoutMs);
|
|
88
|
+
const updatedSession = await getBrowserSession(apiKey, session.sessionId);
|
|
89
|
+
return { session: updatedSession, cdpUrl };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Helper to manage a browser session lifecycle for script execution
|
|
93
|
+
*/
|
|
94
|
+
export class BrowserSessionManager {
|
|
95
|
+
apiKey;
|
|
96
|
+
session = null;
|
|
97
|
+
cdpUrl = null;
|
|
98
|
+
constructor(apiKey) {
|
|
99
|
+
this.apiKey = apiKey;
|
|
100
|
+
}
|
|
101
|
+
async start(options = {}) {
|
|
102
|
+
const result = await createAndWaitForSession(this.apiKey, options);
|
|
103
|
+
this.session = result.session;
|
|
104
|
+
this.cdpUrl = result.cdpUrl;
|
|
105
|
+
return this.cdpUrl;
|
|
106
|
+
}
|
|
107
|
+
async stop() {
|
|
108
|
+
if (this.session) {
|
|
109
|
+
try {
|
|
110
|
+
await stopBrowserSession(this.apiKey, this.session.sessionId);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Ignore errors when stopping - session may already be stopped
|
|
114
|
+
}
|
|
115
|
+
this.session = null;
|
|
116
|
+
this.cdpUrl = null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
getSessionId() {
|
|
120
|
+
return this.session?.sessionId || null;
|
|
121
|
+
}
|
|
122
|
+
getCdpUrl() {
|
|
123
|
+
return this.cdpUrl;
|
|
124
|
+
}
|
|
125
|
+
isActive() {
|
|
126
|
+
return this.session?.status === 'active' && this.cdpUrl !== null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { getSystemPrompt } from './prompts/system-prompt.js';
|
|
5
|
+
/**
|
|
6
|
+
* Generate a unique session ID
|
|
7
|
+
*/
|
|
8
|
+
function generateSessionId() {
|
|
9
|
+
const timestamp = Date.now().toString(36);
|
|
10
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
11
|
+
return `session-${timestamp}-${random}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Run Claude Code CLI to generate a browser automation script.
|
|
15
|
+
*
|
|
16
|
+
* Claude will:
|
|
17
|
+
* 1. Use snapshot commands to understand the page (for its own understanding)
|
|
18
|
+
* 2. Output a complete bash script in a code block
|
|
19
|
+
*
|
|
20
|
+
* We extract the script from Claude's output, not by capturing individual commands.
|
|
21
|
+
*/
|
|
22
|
+
export async function runClaudeForScriptGeneration(taskPrompt, config) {
|
|
23
|
+
const sessionId = generateSessionId();
|
|
24
|
+
// Ensure sessions directory exists
|
|
25
|
+
if (!fs.existsSync(config.sessionsDir)) {
|
|
26
|
+
fs.mkdirSync(config.sessionsDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
// Get the system prompt and combine with task
|
|
29
|
+
const systemPrompt = getSystemPrompt(config.cdpUrl);
|
|
30
|
+
const fullPrompt = `${systemPrompt}
|
|
31
|
+
|
|
32
|
+
## Your Task
|
|
33
|
+
|
|
34
|
+
${taskPrompt}`;
|
|
35
|
+
// Write full prompt to a file
|
|
36
|
+
const promptFile = path.join(config.sessionsDir, `${sessionId}-prompt.txt`);
|
|
37
|
+
fs.writeFileSync(promptFile, fullPrompt);
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
let output = '';
|
|
40
|
+
let errorOutput = '';
|
|
41
|
+
const env = {
|
|
42
|
+
...process.env,
|
|
43
|
+
CDP_URL: config.cdpUrl,
|
|
44
|
+
};
|
|
45
|
+
console.log(`\n[claude-gen] Starting Claude Code...`);
|
|
46
|
+
console.log(`[claude-gen] Session ID: ${sessionId}`);
|
|
47
|
+
// Use stream-json for verbose output, allow Bash for snapshots
|
|
48
|
+
const shellCmd = `cat "${promptFile}" | claude -p --model ${config.model} --max-turns ${config.maxTurns} --allowedTools "Bash" --output-format stream-json --verbose`;
|
|
49
|
+
const claude = spawn('bash', ['-c', shellCmd], {
|
|
50
|
+
env,
|
|
51
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
52
|
+
detached: false,
|
|
53
|
+
});
|
|
54
|
+
claude.stdout?.on('data', (data) => {
|
|
55
|
+
const text = data.toString();
|
|
56
|
+
output += text;
|
|
57
|
+
process.stdout.write(text);
|
|
58
|
+
});
|
|
59
|
+
claude.stderr?.on('data', (data) => {
|
|
60
|
+
const text = data.toString();
|
|
61
|
+
errorOutput += text;
|
|
62
|
+
process.stderr.write(text);
|
|
63
|
+
});
|
|
64
|
+
claude.on('close', (code) => {
|
|
65
|
+
// Clean up temp files
|
|
66
|
+
cleanup(promptFile);
|
|
67
|
+
// Extract the script from Claude's output
|
|
68
|
+
const script = extractScriptFromOutput(output);
|
|
69
|
+
if (script) {
|
|
70
|
+
console.log(`\n[claude-gen] Successfully extracted script from Claude's output`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.log(`\n[claude-gen] WARNING: Could not extract script from Claude's output`);
|
|
74
|
+
}
|
|
75
|
+
resolve({
|
|
76
|
+
success: code === 0 && script !== null,
|
|
77
|
+
sessionId,
|
|
78
|
+
output,
|
|
79
|
+
script,
|
|
80
|
+
error: code !== 0 ? (errorOutput || `Exit code: ${code}`) : undefined,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
claude.on('error', (err) => {
|
|
84
|
+
cleanup(promptFile);
|
|
85
|
+
resolve({
|
|
86
|
+
success: false,
|
|
87
|
+
sessionId,
|
|
88
|
+
output,
|
|
89
|
+
script: null,
|
|
90
|
+
error: err.message,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function cleanup(...files) {
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
try {
|
|
98
|
+
if (fs.existsSync(file)) {
|
|
99
|
+
fs.unlinkSync(file);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Ignore cleanup errors
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Extract a bash script from Claude's output.
|
|
109
|
+
* Looks for code blocks containing agent-browser commands.
|
|
110
|
+
*/
|
|
111
|
+
function extractScriptFromOutput(output) {
|
|
112
|
+
// First, try to extract text content from stream-json format
|
|
113
|
+
const textContent = extractTextFromStreamJson(output);
|
|
114
|
+
// Look for bash code blocks in the extracted text
|
|
115
|
+
// Use new RegExp to avoid backtick escaping issues
|
|
116
|
+
const codeBlockPatterns = [
|
|
117
|
+
new RegExp('```bash\\n([\\s\\S]*?)```', 'g'),
|
|
118
|
+
new RegExp('```sh\\n([\\s\\S]*?)```', 'g'),
|
|
119
|
+
new RegExp('```shell\\n([\\s\\S]*?)```', 'g'),
|
|
120
|
+
new RegExp('```\\n(#!/bin/bash[\\s\\S]*?)```', 'g'),
|
|
121
|
+
];
|
|
122
|
+
let bestScript = null;
|
|
123
|
+
let bestScore = 0;
|
|
124
|
+
for (const pattern of codeBlockPatterns) {
|
|
125
|
+
let match;
|
|
126
|
+
while ((match = pattern.exec(textContent)) !== null) {
|
|
127
|
+
const script = match[1].trim();
|
|
128
|
+
const score = scoreScript(script);
|
|
129
|
+
if (score > bestScore) {
|
|
130
|
+
bestScore = score;
|
|
131
|
+
bestScript = script;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (bestScript) {
|
|
136
|
+
// Ensure it has shebang
|
|
137
|
+
if (!bestScript.startsWith('#!/')) {
|
|
138
|
+
bestScript = '#!/bin/bash\nset -e\n\nCDP="${CDP_URL:?Required}"\n\n' + bestScript;
|
|
139
|
+
}
|
|
140
|
+
return bestScript;
|
|
141
|
+
}
|
|
142
|
+
// Fallback: look for agent-browser commands and construct a script
|
|
143
|
+
const commands = extractAgentBrowserCommands(textContent);
|
|
144
|
+
if (commands.length > 0) {
|
|
145
|
+
// Filter out snapshot commands
|
|
146
|
+
const replayableCommands = commands.filter(cmd => !cmd.includes('snapshot'));
|
|
147
|
+
if (replayableCommands.length > 0) {
|
|
148
|
+
return constructScriptFromCommands(replayableCommands);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Extract text content from stream-json format output
|
|
155
|
+
*/
|
|
156
|
+
function extractTextFromStreamJson(output) {
|
|
157
|
+
const lines = output.split('\n');
|
|
158
|
+
const textParts = [];
|
|
159
|
+
for (const line of lines) {
|
|
160
|
+
if (!line.trim().startsWith('{'))
|
|
161
|
+
continue;
|
|
162
|
+
try {
|
|
163
|
+
const json = JSON.parse(line);
|
|
164
|
+
// Extract text from assistant messages
|
|
165
|
+
if (json.type === 'assistant' && json.message?.content) {
|
|
166
|
+
for (const block of json.message.content) {
|
|
167
|
+
if (block.type === 'text') {
|
|
168
|
+
textParts.push(block.text);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Also check for result messages
|
|
173
|
+
if (json.type === 'result' && json.result) {
|
|
174
|
+
textParts.push(json.result);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Not valid JSON, might be raw text
|
|
179
|
+
textParts.push(line);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return textParts.join('\n');
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Score a script based on how complete it looks
|
|
186
|
+
*/
|
|
187
|
+
function scoreScript(script) {
|
|
188
|
+
let score = 0;
|
|
189
|
+
// Must have agent-browser commands
|
|
190
|
+
if (!script.includes('agent-browser'))
|
|
191
|
+
return 0;
|
|
192
|
+
// Bonus for having shebang
|
|
193
|
+
if (script.includes('#!/bin/bash'))
|
|
194
|
+
score += 10;
|
|
195
|
+
// Bonus for having CDP variable
|
|
196
|
+
if (script.includes('CDP=') || script.includes('$CDP'))
|
|
197
|
+
score += 10;
|
|
198
|
+
// Bonus for having open command
|
|
199
|
+
if (script.includes('open "http'))
|
|
200
|
+
score += 20;
|
|
201
|
+
// Bonus for having eval command (data extraction)
|
|
202
|
+
if (script.includes('eval "'))
|
|
203
|
+
score += 20;
|
|
204
|
+
// Bonus for having FINAL RESULTS section
|
|
205
|
+
if (script.includes('FINAL RESULTS') || script.includes('echo "$'))
|
|
206
|
+
score += 10;
|
|
207
|
+
// Penalty for snapshot commands (shouldn't be in final script)
|
|
208
|
+
const snapshotCount = (script.match(/snapshot/g) || []).length;
|
|
209
|
+
score -= snapshotCount * 5;
|
|
210
|
+
// Penalty for @eN refs (shouldn't be in final script)
|
|
211
|
+
const refCount = (script.match(/@e\d+/g) || []).length;
|
|
212
|
+
score -= refCount * 10;
|
|
213
|
+
// Count useful commands
|
|
214
|
+
const openCount = (script.match(/\bopen\s+"/g) || []).length;
|
|
215
|
+
const evalCount = (script.match(/\beval\s+"/g) || []).length;
|
|
216
|
+
score += openCount * 5 + evalCount * 10;
|
|
217
|
+
return score;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Extract agent-browser commands from text
|
|
221
|
+
*/
|
|
222
|
+
function extractAgentBrowserCommands(text) {
|
|
223
|
+
const commands = [];
|
|
224
|
+
const lines = text.split('\n');
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
const trimmed = line.trim();
|
|
227
|
+
// Skip comment lines
|
|
228
|
+
if (trimmed.startsWith('#'))
|
|
229
|
+
continue;
|
|
230
|
+
// Look for agent-browser commands
|
|
231
|
+
if (trimmed.includes('agent-browser') && trimmed.includes('--cdp')) {
|
|
232
|
+
// Extract the command portion
|
|
233
|
+
const match = trimmed.match(/(agent-browser\s+--cdp\s+[^\n]+)/);
|
|
234
|
+
if (match && !commands.includes(match[1])) {
|
|
235
|
+
commands.push(match[1]);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return commands;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Construct a script from extracted commands (fallback)
|
|
243
|
+
*/
|
|
244
|
+
function constructScriptFromCommands(commands) {
|
|
245
|
+
const lines = [
|
|
246
|
+
'#!/bin/bash',
|
|
247
|
+
'set -e',
|
|
248
|
+
'',
|
|
249
|
+
'# Generated by claude-gen (fallback mode)',
|
|
250
|
+
'CDP="${CDP_URL:?Required: CDP_URL}"',
|
|
251
|
+
'',
|
|
252
|
+
];
|
|
253
|
+
for (const cmd of commands) {
|
|
254
|
+
// Normalize CDP variable
|
|
255
|
+
let normalized = cmd
|
|
256
|
+
.replace(/"\$CDP_URL"/g, '"$CDP"')
|
|
257
|
+
.replace(/\$CDP_URL/g, '$CDP');
|
|
258
|
+
lines.push(normalized);
|
|
259
|
+
// Add sleep after navigation
|
|
260
|
+
if (cmd.includes(' open ')) {
|
|
261
|
+
lines.push('sleep 2');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
lines.push('');
|
|
265
|
+
lines.push('echo "Script completed"');
|
|
266
|
+
return lines.join('\n');
|
|
267
|
+
}
|
|
268
|
+
// Keep the old function for backwards compatibility, but have it use the new one
|
|
269
|
+
export async function runClaudeWithOutputParsing(taskPrompt, config) {
|
|
270
|
+
const result = await runClaudeForScriptGeneration(taskPrompt, config);
|
|
271
|
+
// Extract commands from script for backwards compatibility
|
|
272
|
+
const commands = [];
|
|
273
|
+
if (result.script) {
|
|
274
|
+
const lines = result.script.split('\n');
|
|
275
|
+
for (const line of lines) {
|
|
276
|
+
if (line.includes('agent-browser') && !line.trim().startsWith('#')) {
|
|
277
|
+
commands.push(line.trim());
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
...result,
|
|
283
|
+
commands,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Chase CLI Installer
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx chase-install # Install skill + show MCP setup
|
|
7
|
+
* npx chase-install --skill # Install skill only
|
|
8
|
+
* npx chase-install --mcp # Show MCP setup only
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import * as https from 'https';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
const SKILL_URL = 'https://raw.githubusercontent.com/alexander-spring/chase/main/skill/SKILL.md';
|
|
15
|
+
const MCP_URL = 'https://chase-api-gth2quoxyq-uc.a.run.app/mcp';
|
|
16
|
+
function downloadFile(url, dest) {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const file = fs.createWriteStream(dest);
|
|
19
|
+
https.get(url, (response) => {
|
|
20
|
+
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
21
|
+
// Follow redirect
|
|
22
|
+
https.get(response.headers.location, (res) => {
|
|
23
|
+
res.pipe(file);
|
|
24
|
+
file.on('finish', () => { file.close(); resolve(); });
|
|
25
|
+
}).on('error', reject);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
response.pipe(file);
|
|
29
|
+
file.on('finish', () => { file.close(); resolve(); });
|
|
30
|
+
}
|
|
31
|
+
}).on('error', reject);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function installSkill() {
|
|
35
|
+
const skillDir = path.join(os.homedir(), '.claude', 'skills', 'chase');
|
|
36
|
+
const skillPath = path.join(skillDir, 'SKILL.md');
|
|
37
|
+
console.log('Installing chase skill...');
|
|
38
|
+
// Create directory
|
|
39
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
40
|
+
// Download skill
|
|
41
|
+
await downloadFile(SKILL_URL, skillPath);
|
|
42
|
+
console.log(`✓ Skill installed to ${skillPath}`);
|
|
43
|
+
}
|
|
44
|
+
function showMcpSetup() {
|
|
45
|
+
console.log(`
|
|
46
|
+
MCP Server Setup
|
|
47
|
+
================
|
|
48
|
+
|
|
49
|
+
Option 1: Hosted HTTP (Recommended)
|
|
50
|
+
-----------------------------------
|
|
51
|
+
claude mcp add --transport http chase ${MCP_URL} -H "x-api-key: YOUR_API_KEY"
|
|
52
|
+
|
|
53
|
+
Option 2: Local stdio
|
|
54
|
+
---------------------
|
|
55
|
+
git clone https://github.com/alexander-spring/chase.git
|
|
56
|
+
cd chase/mcp-server && npm install && npm run build
|
|
57
|
+
claude mcp add chase node ./dist/index.js -e BROWSER_CASH_API_KEY=YOUR_KEY
|
|
58
|
+
|
|
59
|
+
Claude Desktop Config
|
|
60
|
+
---------------------
|
|
61
|
+
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
|
|
62
|
+
|
|
63
|
+
{
|
|
64
|
+
"mcpServers": {
|
|
65
|
+
"chase": {
|
|
66
|
+
"transport": "http",
|
|
67
|
+
"url": "${MCP_URL}",
|
|
68
|
+
"headers": {
|
|
69
|
+
"x-api-key": "YOUR_API_KEY"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
`);
|
|
75
|
+
}
|
|
76
|
+
async function main() {
|
|
77
|
+
const args = process.argv.slice(2);
|
|
78
|
+
const skillOnly = args.includes('--skill');
|
|
79
|
+
const mcpOnly = args.includes('--mcp');
|
|
80
|
+
console.log(`
|
|
81
|
+
╔═══════════════════════════════════════════╗
|
|
82
|
+
║ Chase Browser Automation ║
|
|
83
|
+
╚═══════════════════════════════════════════╝
|
|
84
|
+
`);
|
|
85
|
+
if (!mcpOnly) {
|
|
86
|
+
try {
|
|
87
|
+
await installSkill();
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.error('Failed to install skill:', err instanceof Error ? err.message : err);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!skillOnly) {
|
|
94
|
+
showMcpSetup();
|
|
95
|
+
}
|
|
96
|
+
console.log(`
|
|
97
|
+
Get your API key at: https://browser.cash
|
|
98
|
+
|
|
99
|
+
Quick Start:
|
|
100
|
+
export BROWSER_CASH_API_KEY="your-key"
|
|
101
|
+
# Then ask Claude to extract data from any website!
|
|
102
|
+
`);
|
|
103
|
+
}
|
|
104
|
+
main().catch(console.error);
|