@bsbofmusic/agent-browser-mcp-opencode 0.1.1 → 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/src/ensure.js ADDED
@@ -0,0 +1,425 @@
1
+ import { execSync, exec } from 'child_process';
2
+ import os from 'os';
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import semver from 'semver';
7
+ import { runFullDetection } from './detect.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
12
+
13
+ const ALWAYS_LATEST = process.env.ALWAYS_LATEST !== '0';
14
+ const UPDATE_STRATEGY = process.env.UPDATE_STRATEGY || 'simple';
15
+ const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
16
+ const LOCK_FILE = path.join(os.tmpdir(), 'agent-browser-mcp.lock');
17
+
18
+ const MCP_VERSION = '1.0.0';
19
+ const MIN_NODE_VERSION = '18.0.0';
20
+
21
+ function log(level, message, data = null) {
22
+ const levels = { error: 0, warn: 1, info: 2, debug: 3 };
23
+ if (levels[level] <= levels[LOG_LEVEL]) {
24
+ const timestamp = new Date().toISOString();
25
+ console.error(`[${timestamp}] [${level.toUpperCase()}] ${message}`, data ? JSON.stringify(data) : '');
26
+ }
27
+ }
28
+
29
+ function execPromise(command, options = {}) {
30
+ return new Promise((resolve, reject) => {
31
+ const defaultOptions = {
32
+ encoding: 'utf8',
33
+ timeout: 300000,
34
+ ...options
35
+ };
36
+ exec(command, defaultOptions, (error, stdout, stderr) => {
37
+ if (error) {
38
+ reject({ error, stdout, stderr });
39
+ } else {
40
+ resolve({ stdout, stderr });
41
+ }
42
+ });
43
+ });
44
+ }
45
+
46
+ async function acquireLock() {
47
+ const maxWait = 30000;
48
+ const start = Date.now();
49
+
50
+ while (true) {
51
+ try {
52
+ if (!fs.existsSync(LOCK_FILE)) {
53
+ fs.writeFileSync(LOCK_FILE, `${process.pid}:${Date.now()}`);
54
+ log('debug', 'Lock acquired', { pid: process.pid });
55
+ return true;
56
+ }
57
+
58
+ const lockContent = fs.readFileSync(LOCK_FILE, 'utf8');
59
+ const [lockPid, lockTime] = lockContent.split(':');
60
+
61
+ if (Date.now() - parseInt(lockTime) > maxWait) {
62
+ log('warn', 'Stale lock detected, removing', { lockPid, lockTime });
63
+ fs.unlinkSync(LOCK_FILE);
64
+ continue;
65
+ }
66
+
67
+ try {
68
+ process.kill(lockPid, 0);
69
+ } catch (e) {
70
+ fs.unlinkSync(LOCK_FILE);
71
+ log('debug', 'Stale lock removed');
72
+ continue;
73
+ }
74
+
75
+ await new Promise(r => setTimeout(r, 1000));
76
+ } catch (e) {
77
+ await new Promise(r => setTimeout(r, 1000));
78
+ }
79
+
80
+ if (Date.now() - start > maxWait) {
81
+ throw new Error('Failed to acquire lock after 30 seconds');
82
+ }
83
+ }
84
+ }
85
+
86
+ function releaseLock() {
87
+ try {
88
+ if (fs.existsSync(LOCK_FILE)) {
89
+ fs.unlinkSync(LOCK_FILE);
90
+ log('debug', 'Lock released');
91
+ }
92
+ } catch (e) {
93
+ log('warn', 'Failed to release lock', { error: e.message });
94
+ }
95
+ }
96
+
97
+ async function checkNodeVersion() {
98
+ const currentVersion = process.version;
99
+ if (!semver.gte(currentVersion, MIN_NODE_VERSION)) {
100
+ return { ok: false, error: `Node.js version ${currentVersion} is below minimum required ${MIN_NODE_VERSION}` };
101
+ }
102
+ return { ok: true, version: currentVersion };
103
+ }
104
+
105
+ async function checkNpmAvailable() {
106
+ try {
107
+ execSync('npm --version', { stdio: 'ignore' });
108
+ return { ok: true };
109
+ } catch (e) {
110
+ return { ok: false, error: 'npm is not available' };
111
+ }
112
+ }
113
+
114
+ async function installAgentBrowser() {
115
+ const logs = [];
116
+
117
+ log('info', 'Installing agent-browser globally...');
118
+ logs.push('Step 1: Install agent-browser via npm');
119
+
120
+ try {
121
+ execSync('npm install -g agent-browser', {
122
+ encoding: 'utf8',
123
+ stdio: 'inherit',
124
+ timeout: 300000
125
+ });
126
+ logs.push('agent-browser installed successfully');
127
+ } catch (e) {
128
+ const errorMsg = `Failed to install agent-browser: ${e.message}`;
129
+ log('error', errorMsg);
130
+ logs.push(errorMsg);
131
+ return { ok: false, logs, error: errorMsg };
132
+ }
133
+
134
+ return { ok: true, logs };
135
+ }
136
+
137
+ async function installChromium() {
138
+ const logs = [];
139
+
140
+ log('info', 'Installing Chromium browser...');
141
+ logs.push('Step 2: Install Chromium browser');
142
+
143
+ try {
144
+ execSync('npx agent-browser install', {
145
+ encoding: 'utf8',
146
+ stdio: 'inherit',
147
+ timeout: 300000
148
+ });
149
+ logs.push('Chromium installed successfully');
150
+ } catch (e) {
151
+ try {
152
+ log('warn', 'npx install failed, trying direct agent-browser install');
153
+ execSync('agent-browser install', {
154
+ encoding: 'utf8',
155
+ stdio: 'inherit',
156
+ timeout: 300000
157
+ });
158
+ logs.push('Chromium installed via agent-browser install');
159
+ } catch (e2) {
160
+ const errorMsg = `Failed to install Chromium: ${e2.message}`;
161
+ log('error', errorMsg);
162
+ logs.push(errorMsg);
163
+ return { ok: false, logs, error: errorMsg };
164
+ }
165
+ }
166
+
167
+ return { ok: true, logs };
168
+ }
169
+
170
+ async function verifyInstallation() {
171
+ const logs = [];
172
+
173
+ logs.push('Step 3: Verify installation');
174
+
175
+ try {
176
+ const version = execSync('agent-browser --version', { encoding: 'utf8' }).trim();
177
+ logs.push(`agent-browser version: ${version}`);
178
+ } catch (e) {
179
+ const errorMsg = 'agent-browser not found in PATH after installation';
180
+ log('error', errorMsg);
181
+ logs.push(errorMsg);
182
+ return { ok: false, logs, error: errorMsg };
183
+ }
184
+
185
+ try {
186
+ execSync('agent-browser --help', { encoding: 'utf8', stdio: 'ignore' });
187
+ logs.push('Chromium is ready');
188
+ } catch (e) {
189
+ const errorMsg = 'Chromium may not be fully installed';
190
+ log('warn', errorMsg);
191
+ logs.push(errorMsg);
192
+ }
193
+
194
+ return { ok: true, logs };
195
+ }
196
+
197
+ async function getAgentBrowserVersion() {
198
+ try {
199
+ const version = execSync('agent-browser --version', { encoding: 'utf8' }).trim();
200
+ return { installed: true, version };
201
+ } catch (e) {
202
+ return { installed: false, version: null };
203
+ }
204
+ }
205
+
206
+ async function checkForUpdates() {
207
+ const current = await getAgentBrowserVersion();
208
+
209
+ if (!current.installed) {
210
+ return { hasUpdate: null, currentVersion: null, latestVersion: null };
211
+ }
212
+
213
+ try {
214
+ const npmVersion = execSync('npm view agent-browser version', { encoding: 'utf8' }).trim();
215
+ const hasUpdate = semver.lt(current.version, npmVersion);
216
+
217
+ return {
218
+ hasUpdate,
219
+ currentVersion: current.version,
220
+ latestVersion: npmVersion,
221
+ };
222
+ } catch (e) {
223
+ return { hasUpdate: null, currentVersion: current.version, latestVersion: null };
224
+ }
225
+ }
226
+
227
+ async function upgradeAgentBrowser() {
228
+ const logs = [];
229
+
230
+ log('info', 'Upgrading agent-browser to latest...');
231
+ logs.push('Upgrading agent-browser to latest version');
232
+
233
+ try {
234
+ execSync('npm install -g agent-browser@latest', {
235
+ encoding: 'utf8',
236
+ stdio: 'inherit',
237
+ timeout: 300000
238
+ });
239
+ logs.push('Upgrade completed');
240
+
241
+ const version = execSync('agent-browser --version', { encoding: 'utf8' }).trim();
242
+ logs.push(`New version: ${version}`);
243
+
244
+ return { ok: true, logs, version };
245
+ } catch (e) {
246
+ const errorMsg = `Upgrade failed: ${e.message}`;
247
+ log('error', errorMsg);
248
+ logs.push(errorMsg);
249
+ return { ok: false, logs, error: errorMsg };
250
+ }
251
+ }
252
+
253
+ export async function ensureLatest() {
254
+ if (!ALWAYS_LATEST) {
255
+ log('info', 'always-latest disabled, skipping update check');
256
+ return { ok: true, skipped: true };
257
+ }
258
+
259
+ log('info', 'Running ensureLatest...');
260
+
261
+ const updateCheck = await checkForUpdates();
262
+
263
+ if (updateCheck.hasUpdate === null) {
264
+ log('warn', 'Could not check for updates');
265
+ return { ok: true, skipped: true, reason: 'update_check_failed' };
266
+ }
267
+
268
+ if (!updateCheck.hasUpdate) {
269
+ log('info', 'Already at latest version', updateCheck);
270
+ return {
271
+ ok: true,
272
+ skipped: true,
273
+ currentVersion: updateCheck.currentVersion,
274
+ latestVersion: updateCheck.latestVersion,
275
+ message: 'Already at latest version'
276
+ };
277
+ }
278
+
279
+ log('info', 'Update available', updateCheck);
280
+
281
+ const upgradeResult = await upgradeAgentBrowser();
282
+
283
+ return {
284
+ ok: upgradeResult.ok,
285
+ skipped: false,
286
+ currentVersion: updateCheck.currentVersion,
287
+ latestVersion: updateCheck.latestVersion,
288
+ logs: upgradeResult.logs,
289
+ error: upgradeResult.error,
290
+ };
291
+ }
292
+
293
+ export async function bootstrapEnsure() {
294
+ log('info', 'Starting bootstrapEnsure...');
295
+
296
+ await acquireLock();
297
+
298
+ try {
299
+ const logs = [];
300
+ const startTime = Date.now();
301
+
302
+ logs.push(`[${new Date().toISOString()}] Starting bootstrapEnsure`);
303
+
304
+ const nodeCheck = await checkNodeVersion();
305
+ if (!nodeCheck.ok) {
306
+ return { ok: false, logs: nodeCheck.error, nextSteps: ['Upgrade Node.js to >= 18.0.0'] };
307
+ }
308
+ logs.push(`Node.js version: ${nodeCheck.version}`);
309
+
310
+ const npmCheck = await checkNpmAvailable();
311
+ if (!npmCheck.ok) {
312
+ return {
313
+ ok: false,
314
+ logs,
315
+ error: npmCheck.error,
316
+ nextSteps: ['Install Node.js with npm', 'Add npm to PATH']
317
+ };
318
+ }
319
+ logs.push('npm is available');
320
+
321
+ const detection = await runFullDetection();
322
+ logs.push(`Detection: agent-browser installed=${detection.agentBrowser.installed}`);
323
+
324
+ if (!detection.agentBrowser.installed) {
325
+ const installResult = await installAgentBrowser();
326
+ logs.push(...installResult.logs);
327
+
328
+ if (!installResult.ok) {
329
+ return {
330
+ ok: false,
331
+ logs,
332
+ error: installResult.error,
333
+ nextSteps: ['Check npm permissions', 'Try: npm install -g agent-browser --unsafe-perm']
334
+ };
335
+ }
336
+ }
337
+
338
+ if (!detection.agentBrowser.chromiumInstalled) {
339
+ const chromiumResult = await installChromium();
340
+ logs.push(...chromiumResult.logs);
341
+
342
+ if (!chromiumResult.ok) {
343
+ return {
344
+ ok: false,
345
+ logs,
346
+ error: chromiumResult.error,
347
+ nextSteps: ['Manually run: npx agent-browser install', 'Check system dependencies']
348
+ };
349
+ }
350
+ }
351
+
352
+ const verifyResult = await verifyInstallation();
353
+ logs.push(...verifyResult.logs);
354
+
355
+ if (!verifyResult.ok) {
356
+ return {
357
+ ok: false,
358
+ logs,
359
+ error: verifyResult.error,
360
+ nextSteps: ['Run: agent-browser install', 'Check PATH configuration']
361
+ };
362
+ }
363
+
364
+ const elapsed = Date.now() - startTime;
365
+ logs.push(`[${new Date().toISOString()}] bootstrapEnsure completed in ${elapsed}ms`);
366
+
367
+ return {
368
+ ok: true,
369
+ logs,
370
+ duration: elapsed,
371
+ };
372
+ } finally {
373
+ releaseLock();
374
+ }
375
+ }
376
+
377
+ export async function fullEnsure() {
378
+ await acquireLock();
379
+
380
+ try {
381
+ const logs = [];
382
+ const startTime = Date.now();
383
+
384
+ logs.push(`[${new Date().toISOString()}] Starting fullEnsure (manual trigger)`);
385
+
386
+ const reinstall = await installAgentBrowser();
387
+ if (!reinstall.ok) {
388
+ return { ok: false, logs: reinstall.logs, error: reinstall.error };
389
+ }
390
+ logs.push(...reinstall.logs);
391
+
392
+ const chromium = await installChromium();
393
+ if (!chromium.ok) {
394
+ return { ok: false, logs: chromium.logs, error: chromium.error };
395
+ }
396
+ logs.push(...chromium.logs);
397
+
398
+ const verify = await verifyInstallation();
399
+ logs.push(...verify.logs);
400
+
401
+ const updateCheck = await checkForUpdates();
402
+ logs.push(`Update check: current=${updateCheck.currentVersion}, latest=${updateCheck.latestVersion}`);
403
+
404
+ const elapsed = Date.now() - startTime;
405
+ logs.push(`[${new Date().toISOString()}] fullEnsure completed in ${elapsed}ms`);
406
+
407
+ return {
408
+ ok: verify.ok && chromium.ok,
409
+ logs,
410
+ duration: elapsed,
411
+ version: updateCheck.currentVersion,
412
+ latestVersion: updateCheck.latestVersion,
413
+ };
414
+ } finally {
415
+ releaseLock();
416
+ }
417
+ }
418
+
419
+ export default {
420
+ bootstrapEnsure,
421
+ fullEnsure,
422
+ ensureLatest,
423
+ checkForUpdates,
424
+ getAgentBrowserVersion,
425
+ };
@@ -0,0 +1,77 @@
1
+ import { execSync } from 'child_process';
2
+
3
+ export const clickTool = {
4
+ name: 'browser_click',
5
+ description: 'Click an element. Use ref from snapshot (e.g., @e1) or CSS selector (e.g., #button).',
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ selector: {
10
+ type: 'string',
11
+ description: 'Element selector (e.g., @e1, #submit, button.primary)',
12
+ },
13
+ newTab: {
14
+ type: 'boolean',
15
+ description: 'Open link in new tab',
16
+ default: false,
17
+ },
18
+ },
19
+ required: ['selector'],
20
+ },
21
+ };
22
+
23
+ export async function clickToolHandler(args) {
24
+ const { selector, newTab = false } = args;
25
+ const logs = [];
26
+ const startTime = Date.now();
27
+
28
+ logs.push(`[${new Date().toISOString()}] browser_click: ${selector}`);
29
+
30
+ try {
31
+ let command = `click "${selector}"`;
32
+ if (newTab) command += ' --new-tab';
33
+
34
+ const stdout = execSync(`agent-browser ${command} --json`, {
35
+ encoding: 'utf8',
36
+ timeout: 30000,
37
+ });
38
+
39
+ const elapsed = Date.now() - startTime;
40
+ logs.push(`[${new Date().toISOString()}] Completed in ${elapsed}ms`);
41
+
42
+ let parsed;
43
+ try {
44
+ parsed = JSON.parse(stdout);
45
+ } catch {
46
+ parsed = { success: true };
47
+ }
48
+
49
+ return {
50
+ ok: true,
51
+ logs,
52
+ stdout,
53
+ output: parsed,
54
+ selector,
55
+ duration: elapsed,
56
+ };
57
+ } catch (error) {
58
+ const elapsed = Date.now() - startTime;
59
+ logs.push(`[${new Date().toISOString()}] Error: ${error.message}`);
60
+
61
+ const nextSteps = [
62
+ 'Run browser_snapshot first to get element refs',
63
+ 'Verify selector is correct',
64
+ 'Wait for element: browser_wait',
65
+ ];
66
+
67
+ return {
68
+ ok: false,
69
+ logs,
70
+ stderr: error.stderr || '',
71
+ error: error.message,
72
+ selector,
73
+ duration: elapsed,
74
+ nextSteps,
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,45 @@
1
+ import { execSync } from 'child_process';
2
+
3
+ export const closeTool = {
4
+ name: 'browser_close',
5
+ description: 'Close the browser. Ends the current browser session.',
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {},
9
+ },
10
+ };
11
+
12
+ export async function closeToolHandler(args = {}) {
13
+ const logs = [];
14
+ const startTime = Date.now();
15
+
16
+ logs.push(`[${new Date().toISOString()}] browser_close called`);
17
+
18
+ try {
19
+ const stdout = execSync('agent-browser close --json', {
20
+ encoding: 'utf8',
21
+ timeout: 10000,
22
+ });
23
+
24
+ const elapsed = Date.now() - startTime;
25
+ logs.push(`[${new Date().toISOString()}] Completed in ${elapsed}ms`);
26
+
27
+ return {
28
+ ok: true,
29
+ logs,
30
+ stdout,
31
+ duration: elapsed,
32
+ };
33
+ } catch (error) {
34
+ const elapsed = Date.now() - startTime;
35
+ logs.push(`[${new Date().toISOString()}] Error: ${error.message}`);
36
+
37
+ return {
38
+ ok: false,
39
+ logs,
40
+ stderr: error.stderr || '',
41
+ error: error.message,
42
+ duration: elapsed,
43
+ };
44
+ }
45
+ }
@@ -0,0 +1,77 @@
1
+ import { execSync } from 'child_process';
2
+
3
+ export const fillTool = {
4
+ name: 'browser_fill',
5
+ description: 'Clear and fill an input field. Use ref from snapshot (e.g., @e1) or CSS selector.',
6
+ inputSchema: {
7
+ type: 'object',
8
+ properties: {
9
+ selector: {
10
+ type: 'string',
11
+ description: 'Element selector (e.g., @e1, #email, input[name="email"])',
12
+ },
13
+ value: {
14
+ type: 'string',
15
+ description: 'Value to fill in the input field',
16
+ },
17
+ },
18
+ required: ['selector', 'value'],
19
+ },
20
+ };
21
+
22
+ export async function fillToolHandler(args) {
23
+ const { selector, value } = args;
24
+ const logs = [];
25
+ const startTime = Date.now();
26
+
27
+ logs.push(`[${new Date().toISOString()}] browser_fill: ${selector} = "${value}"`);
28
+
29
+ try {
30
+ const command = `fill "${selector}" "${value}"`;
31
+
32
+ const stdout = execSync(`agent-browser ${command} --json`, {
33
+ encoding: 'utf8',
34
+ timeout: 30000,
35
+ });
36
+
37
+ const elapsed = Date.now() - startTime;
38
+ logs.push(`[${new Date().toISOString()}] Completed in ${elapsed}ms`);
39
+
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(stdout);
43
+ } catch {
44
+ parsed = { success: true };
45
+ }
46
+
47
+ return {
48
+ ok: true,
49
+ logs,
50
+ stdout,
51
+ output: parsed,
52
+ selector,
53
+ value,
54
+ duration: elapsed,
55
+ };
56
+ } catch (error) {
57
+ const elapsed = Date.now() - startTime;
58
+ logs.push(`[${new Date().toISOString()}] Error: ${error.message}`);
59
+
60
+ const nextSteps = [
61
+ 'Run browser_snapshot first to get element refs',
62
+ 'Verify selector targets an input element',
63
+ 'Wait for element: browser_wait',
64
+ ];
65
+
66
+ return {
67
+ ok: false,
68
+ logs,
69
+ stderr: error.stderr || '',
70
+ error: error.message,
71
+ selector,
72
+ value,
73
+ duration: elapsed,
74
+ nextSteps,
75
+ };
76
+ }
77
+ }