@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/CHANGELOG.md +29 -0
- package/README.md +237 -13
- package/THIRD_PARTY_NOTICES.md +291 -0
- package/index.js +186 -0
- package/package.json +33 -18
- package/src/detect.js +286 -0
- package/src/doctor.js +387 -0
- package/src/ensure.js +425 -0
- package/src/tools/actions/click.js +77 -0
- package/src/tools/actions/close.js +45 -0
- package/src/tools/actions/fill.js +77 -0
- package/src/tools/actions/find.js +107 -0
- package/src/tools/actions/getText.js +70 -0
- package/src/tools/actions/open.js +96 -0
- package/src/tools/actions/screenshot.js +81 -0
- package/src/tools/actions/snapshot.js +94 -0
- package/src/tools/actions/tab.js +91 -0
- package/src/tools/actions/wait.js +85 -0
- package/src/tools/doctor.js +42 -0
- package/src/tools/ensure.js +46 -0
- package/src/tools/exec.js +120 -0
- package/src/tools/help.js +76 -0
- package/src/tools/index.js +50 -0
- package/src/tools/version.js +83 -0
- package/dist/bootstrap.js +0 -72
- package/dist/index.js +0 -121
- package/src/bootstrap.ts +0 -75
- package/src/index.ts +0 -162
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export const findTool = {
|
|
4
|
+
name: 'browser_find',
|
|
5
|
+
description: 'Find element by semantic locator (role, text, label, placeholder, alt, title, testid). Then optionally perform an action.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
type: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'Locator type: role, text, label, placeholder, alt, title, testid, first, last, nth',
|
|
12
|
+
enum: ['role', 'text', 'label', 'placeholder', 'alt', 'title', 'testid', 'first', 'last', 'nth'],
|
|
13
|
+
},
|
|
14
|
+
value: {
|
|
15
|
+
type: 'string',
|
|
16
|
+
description: 'Value to search for (text content, role name, label, etc.)',
|
|
17
|
+
},
|
|
18
|
+
action: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Action to perform: click, fill, type, hover, focus, check, uncheck, text',
|
|
21
|
+
enum: ['click', 'fill', 'type', 'hover', 'focus', 'check', 'uncheck', 'text'],
|
|
22
|
+
},
|
|
23
|
+
actionValue: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Value for fill/type actions',
|
|
26
|
+
},
|
|
27
|
+
exact: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
description: 'Require exact text match',
|
|
30
|
+
default: false,
|
|
31
|
+
},
|
|
32
|
+
name: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: 'Filter by accessible name (for role locator)',
|
|
35
|
+
},
|
|
36
|
+
nth: {
|
|
37
|
+
type: 'number',
|
|
38
|
+
description: 'For nth locator: index number',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
required: ['type', 'value'],
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export async function findToolHandler(args) {
|
|
46
|
+
const { type, value, action, actionValue, exact = false, name, nth } = args;
|
|
47
|
+
const logs = [];
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
|
|
50
|
+
logs.push(`[${new Date().toISOString()}] browser_find: type=${type}, value=${value}`);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
let command = `find ${type} "${value}"`;
|
|
54
|
+
|
|
55
|
+
if (exact) command += ' --exact';
|
|
56
|
+
if (name) command += ` --name "${name}"`;
|
|
57
|
+
if (nth !== undefined) command += ` ${nth}`;
|
|
58
|
+
|
|
59
|
+
if (action) {
|
|
60
|
+
command += ` ${action}`;
|
|
61
|
+
if (actionValue) command += ` "${actionValue}"`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const stdout = execSync(`agent-browser ${command} --json`, {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
timeout: 30000,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const elapsed = Date.now() - startTime;
|
|
70
|
+
logs.push(`[${new Date().toISOString()}] Completed in ${elapsed}ms`);
|
|
71
|
+
|
|
72
|
+
let parsed;
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(stdout);
|
|
75
|
+
} catch {
|
|
76
|
+
parsed = { success: true, raw: stdout };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
logs,
|
|
82
|
+
stdout,
|
|
83
|
+
output: parsed,
|
|
84
|
+
locator: { type, value, action },
|
|
85
|
+
duration: elapsed,
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const elapsed = Date.now() - startTime;
|
|
89
|
+
logs.push(`[${new Date().toISOString()}] Error: ${error.message}`);
|
|
90
|
+
|
|
91
|
+
const nextSteps = [
|
|
92
|
+
'Run browser_snapshot to see available elements',
|
|
93
|
+
'Verify locator type and value are correct',
|
|
94
|
+
'Try different locator: role, text, label',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
logs,
|
|
100
|
+
stderr: error.stderr || '',
|
|
101
|
+
error: error.message,
|
|
102
|
+
locator: { type, value, action },
|
|
103
|
+
duration: elapsed,
|
|
104
|
+
nextSteps,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export const getTextTool = {
|
|
4
|
+
name: 'browser_get_text',
|
|
5
|
+
description: 'Get text content from an element. Use ref from snapshot or CSS selector.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
selector: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'Element selector (e.g., @e1, #title, h1)',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
required: ['selector'],
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export async function getTextToolHandler(args) {
|
|
19
|
+
const { selector } = args;
|
|
20
|
+
const logs = [];
|
|
21
|
+
const startTime = Date.now();
|
|
22
|
+
|
|
23
|
+
logs.push(`[${new Date().toISOString()}] browser_get_text: ${selector}`);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const command = `get text "${selector}"`;
|
|
27
|
+
|
|
28
|
+
const stdout = execSync(`agent-browser ${command} --json`, {
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
timeout: 10000,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const elapsed = Date.now() - startTime;
|
|
34
|
+
logs.push(`[${new Date().toISOString()}] Completed in ${elapsed}ms`);
|
|
35
|
+
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(stdout);
|
|
39
|
+
} catch {
|
|
40
|
+
parsed = { text: stdout.trim() };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
logs,
|
|
46
|
+
stdout,
|
|
47
|
+
output: parsed,
|
|
48
|
+
selector,
|
|
49
|
+
duration: elapsed,
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const elapsed = Date.now() - startTime;
|
|
53
|
+
logs.push(`[${new Date().toISOString()}] Error: ${error.message}`);
|
|
54
|
+
|
|
55
|
+
const nextSteps = [
|
|
56
|
+
'Run browser_snapshot first to get element refs',
|
|
57
|
+
'Verify selector is correct',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
logs,
|
|
63
|
+
stderr: error.stderr || '',
|
|
64
|
+
error: error.message,
|
|
65
|
+
selector,
|
|
66
|
+
duration: elapsed,
|
|
67
|
+
nextSteps,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export const openTool = {
|
|
4
|
+
name: 'browser_open',
|
|
5
|
+
description: 'Navigate to a URL. Opens a browser and navigates to the specified URL.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
url: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'The URL to navigate to (e.g., https://example.com)',
|
|
12
|
+
},
|
|
13
|
+
timeout: {
|
|
14
|
+
type: 'number',
|
|
15
|
+
description: 'Timeout in milliseconds',
|
|
16
|
+
default: 30000,
|
|
17
|
+
},
|
|
18
|
+
newTab: {
|
|
19
|
+
type: 'boolean',
|
|
20
|
+
description: 'Open in a new tab instead of current tab',
|
|
21
|
+
default: false,
|
|
22
|
+
},
|
|
23
|
+
session: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Session name for isolated browser state',
|
|
26
|
+
},
|
|
27
|
+
profile: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Profile directory for persistent browser state',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ['url'],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export async function openToolHandler(args) {
|
|
37
|
+
const { url, timeout = 30000, newTab = false, session, profile } = args;
|
|
38
|
+
const logs = [];
|
|
39
|
+
const startTime = Date.now();
|
|
40
|
+
|
|
41
|
+
logs.push(`[${new Date().toISOString()}] browser_open: ${url}`);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
let command = `open ${newTab ? '--new-tab ' : ''}"${url}"`;
|
|
45
|
+
|
|
46
|
+
if (session) {
|
|
47
|
+
command = `open ${newTab ? '--new-tab ' : ''}--session ${session} "${url}"`;
|
|
48
|
+
} else if (profile) {
|
|
49
|
+
command = `open ${newTab ? '--new-tab ' : ''}--profile "${profile}" "${url}"`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const stdout = execSync(`agent-browser ${command} --json`, {
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
timeout,
|
|
55
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const elapsed = Date.now() - startTime;
|
|
59
|
+
logs.push(`[${new Date().toISOString()}] Completed in ${elapsed}ms`);
|
|
60
|
+
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(stdout);
|
|
64
|
+
} catch {
|
|
65
|
+
parsed = { success: true, raw: stdout };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
logs,
|
|
71
|
+
stdout,
|
|
72
|
+
output: parsed,
|
|
73
|
+
url,
|
|
74
|
+
duration: elapsed,
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
const elapsed = Date.now() - startTime;
|
|
78
|
+
logs.push(`[${new Date().toISOString()}] Error: ${error.message}`);
|
|
79
|
+
|
|
80
|
+
const nextSteps = [
|
|
81
|
+
'Check if URL is valid and accessible',
|
|
82
|
+
'Run: browser_ensure to install browser',
|
|
83
|
+
'Run: browser_doctor to diagnose issues',
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
logs,
|
|
89
|
+
stderr: error.stderr || '',
|
|
90
|
+
error: error.message,
|
|
91
|
+
url,
|
|
92
|
+
duration: elapsed,
|
|
93
|
+
nextSteps,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
|
|
6
|
+
export const screenshotTool = {
|
|
7
|
+
name: 'browser_screenshot',
|
|
8
|
+
description: 'Take a screenshot of the current page. Optionally saves to a file and can include annotated labels.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
path: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description: 'Path to save screenshot (optional, defaults to temp file)',
|
|
15
|
+
},
|
|
16
|
+
fullPage: {
|
|
17
|
+
type: 'boolean',
|
|
18
|
+
description: 'Capture full scrollable page',
|
|
19
|
+
default: false,
|
|
20
|
+
},
|
|
21
|
+
annotate: {
|
|
22
|
+
type: 'boolean',
|
|
23
|
+
description: 'Add numbered labels to interactive elements',
|
|
24
|
+
default: false,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export async function screenshotToolHandler(args) {
|
|
31
|
+
const { path: savePath, fullPage = false, annotate = false } = args;
|
|
32
|
+
const logs = [];
|
|
33
|
+
const startTime = Date.now();
|
|
34
|
+
|
|
35
|
+
logs.push(`[${new Date().toISOString()}] browser_screenshot called`);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
let command = 'screenshot';
|
|
39
|
+
if (savePath) command += ` "${savePath}"`;
|
|
40
|
+
if (fullPage) command += ' --full';
|
|
41
|
+
if (annotate) command += ' --annotate';
|
|
42
|
+
|
|
43
|
+
const stdout = execSync(`agent-browser ${command}`, {
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
timeout: 30000,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const elapsed = Date.now() - startTime;
|
|
49
|
+
logs.push(`[${new Date().toISOString()}] Completed in ${elapsed}ms`);
|
|
50
|
+
|
|
51
|
+
const outputPath = savePath || stdout.trim().split('\n').find(l => l.includes('saved to'));
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
logs,
|
|
56
|
+
stdout,
|
|
57
|
+
outputPath: outputPath || null,
|
|
58
|
+
fullPage,
|
|
59
|
+
annotate,
|
|
60
|
+
duration: elapsed,
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const elapsed = Date.now() - startTime;
|
|
64
|
+
logs.push(`[${new Date().toISOString()}] Error: ${error.message}`);
|
|
65
|
+
|
|
66
|
+
const nextSteps = [
|
|
67
|
+
'Run browser_open first to navigate to a page',
|
|
68
|
+
'Check if browser is open: browser_tab',
|
|
69
|
+
'Verify screenshot path is writable',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
logs,
|
|
75
|
+
stderr: error.stderr || '',
|
|
76
|
+
error: error.message,
|
|
77
|
+
duration: elapsed,
|
|
78
|
+
nextSteps,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export const snapshotTool = {
|
|
4
|
+
name: 'browser_snapshot',
|
|
5
|
+
description: 'Get the accessibility tree of the current page with element refs. This is the recommended way for AI agents to interact with pages - use refs from snapshot to click, fill, etc.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
interactive: {
|
|
10
|
+
type: 'boolean',
|
|
11
|
+
description: 'Only show interactive elements (buttons, inputs, links)',
|
|
12
|
+
default: false,
|
|
13
|
+
},
|
|
14
|
+
compact: {
|
|
15
|
+
type: 'boolean',
|
|
16
|
+
description: 'Remove empty structural elements',
|
|
17
|
+
default: false,
|
|
18
|
+
},
|
|
19
|
+
depth: {
|
|
20
|
+
type: 'number',
|
|
21
|
+
description: 'Limit tree depth',
|
|
22
|
+
},
|
|
23
|
+
selector: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Scope to CSS selector',
|
|
26
|
+
},
|
|
27
|
+
cursor: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
description: 'Include cursor-interactive elements',
|
|
30
|
+
default: false,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export async function snapshotToolHandler(args) {
|
|
37
|
+
const { interactive = false, compact = false, depth, selector, cursor = false } = args;
|
|
38
|
+
const logs = [];
|
|
39
|
+
const startTime = Date.now();
|
|
40
|
+
|
|
41
|
+
logs.push(`[${new Date().toISOString()}] browser_snapshot called`);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
let command = 'snapshot --json';
|
|
45
|
+
|
|
46
|
+
if (interactive) command += ' -i';
|
|
47
|
+
if (compact) command += ' -c';
|
|
48
|
+
if (cursor) command += ' -C';
|
|
49
|
+
if (depth) command += ` -d ${depth}`;
|
|
50
|
+
if (selector) command += ` -s "${selector}"`;
|
|
51
|
+
|
|
52
|
+
const stdout = execSync(`agent-browser ${command}`, {
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
timeout: 30000,
|
|
55
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const elapsed = Date.now() - startTime;
|
|
59
|
+
logs.push(`[${new Date().toISOString()}] Completed in ${elapsed}ms`);
|
|
60
|
+
|
|
61
|
+
let parsed;
|
|
62
|
+
try {
|
|
63
|
+
parsed = JSON.parse(stdout);
|
|
64
|
+
} catch {
|
|
65
|
+
parsed = { success: true, raw: stdout };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
logs,
|
|
71
|
+
stdout,
|
|
72
|
+
output: parsed,
|
|
73
|
+
duration: elapsed,
|
|
74
|
+
};
|
|
75
|
+
} catch (error) {
|
|
76
|
+
const elapsed = Date.now() - startTime;
|
|
77
|
+
logs.push(`[${new Date().toISOString()}] Error: ${error.message}`);
|
|
78
|
+
|
|
79
|
+
const nextSteps = [
|
|
80
|
+
'Run browser_open first to navigate to a page',
|
|
81
|
+
'Check if browser is open: browser_tab',
|
|
82
|
+
'Run: browser_doctor to diagnose issues',
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
logs,
|
|
88
|
+
stderr: error.stderr || '',
|
|
89
|
+
error: error.message,
|
|
90
|
+
duration: elapsed,
|
|
91
|
+
nextSteps,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export const tabTool = {
|
|
4
|
+
name: 'browser_tab',
|
|
5
|
+
description: 'Manage browser tabs: list, create new, switch, or close tabs.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
action: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'Tab action: list, new, close, or number to switch to',
|
|
12
|
+
enum: ['list', 'new', 'close'],
|
|
13
|
+
},
|
|
14
|
+
index: {
|
|
15
|
+
type: 'number',
|
|
16
|
+
description: 'Tab index to switch to (0-based)',
|
|
17
|
+
},
|
|
18
|
+
url: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'URL to open in new tab (for action=new)',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export async function tabToolHandler(args) {
|
|
27
|
+
const { action, index, url } = args;
|
|
28
|
+
const logs = [];
|
|
29
|
+
const startTime = Date.now();
|
|
30
|
+
|
|
31
|
+
logs.push(`[${new Date().toISOString()}] browser_tab called: action=${action}, index=${index}`);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
let command = 'tab';
|
|
35
|
+
|
|
36
|
+
if (action === 'list' || action === undefined) {
|
|
37
|
+
command = 'tab';
|
|
38
|
+
} else if (action === 'new') {
|
|
39
|
+
command = 'tab new';
|
|
40
|
+
if (url) command += ` "${url}"`;
|
|
41
|
+
} else if (action === 'close') {
|
|
42
|
+
command = 'tab close';
|
|
43
|
+
} else if (index !== undefined) {
|
|
44
|
+
command = `tab ${index}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const stdout = execSync(`agent-browser ${command} --json`, {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
timeout: 10000,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const elapsed = Date.now() - startTime;
|
|
53
|
+
logs.push(`[${new Date().toISOString()}] Completed in ${elapsed}ms`);
|
|
54
|
+
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(stdout);
|
|
58
|
+
} catch {
|
|
59
|
+
parsed = { raw: stdout };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
ok: true,
|
|
64
|
+
logs,
|
|
65
|
+
stdout,
|
|
66
|
+
output: parsed,
|
|
67
|
+
action,
|
|
68
|
+
index,
|
|
69
|
+
duration: elapsed,
|
|
70
|
+
};
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const elapsed = Date.now() - startTime;
|
|
73
|
+
logs.push(`[${new Date().toISOString()}] Error: ${error.message}`);
|
|
74
|
+
|
|
75
|
+
const nextSteps = [
|
|
76
|
+
'Run browser_open first to open a browser',
|
|
77
|
+
'Verify tab index is valid',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
logs,
|
|
83
|
+
stderr: error.stderr || '',
|
|
84
|
+
error: error.message,
|
|
85
|
+
action,
|
|
86
|
+
index,
|
|
87
|
+
duration: elapsed,
|
|
88
|
+
nextSteps,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export const waitTool = {
|
|
4
|
+
name: 'browser_wait',
|
|
5
|
+
description: 'Wait for element, text, URL, or specified time. Useful for waiting for dynamic content to load.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
target: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'What to wait for: selector, --text, --url, or milliseconds',
|
|
12
|
+
},
|
|
13
|
+
text: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Text to wait for (use with --text flag)',
|
|
16
|
+
},
|
|
17
|
+
url: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'URL pattern to wait for (use with --url flag)',
|
|
20
|
+
},
|
|
21
|
+
loadState: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'Page load state: load, domcontentloaded, networkidle',
|
|
24
|
+
enum: ['load', 'domcontentloaded', 'networkidle'],
|
|
25
|
+
},
|
|
26
|
+
timeout: {
|
|
27
|
+
type: 'number',
|
|
28
|
+
description: 'Timeout in milliseconds (default: 30000)',
|
|
29
|
+
default: 30000,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export async function waitToolHandler(args) {
|
|
36
|
+
const { target, text, url, loadState, timeout = 30000 } = args;
|
|
37
|
+
const logs = [];
|
|
38
|
+
const startTime = Date.now();
|
|
39
|
+
|
|
40
|
+
logs.push(`[${new Date().toISOString()}] browser_wait called`);
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
let command = 'wait';
|
|
44
|
+
|
|
45
|
+
if (target && !target.startsWith('--')) {
|
|
46
|
+
command += ` "${target}"`;
|
|
47
|
+
}
|
|
48
|
+
if (text) command += ` --text "${text}"`;
|
|
49
|
+
if (url) command += ` --url "${url}"`;
|
|
50
|
+
if (loadState) command += ` --${loadState}`;
|
|
51
|
+
|
|
52
|
+
const stdout = execSync(`agent-browser ${command} --json`, {
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
timeout,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const elapsed = Date.now() - startTime;
|
|
58
|
+
logs.push(`[${new Date().toISOString()}] Completed in ${elapsed}ms`);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
logs,
|
|
63
|
+
stdout,
|
|
64
|
+
duration: elapsed,
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const elapsed = Date.now() - startTime;
|
|
68
|
+
logs.push(`[${new Date().toISOString()}] Error: ${error.message}`);
|
|
69
|
+
|
|
70
|
+
const nextSteps = [
|
|
71
|
+
'Verify the target element/text/URL exists',
|
|
72
|
+
'Increase timeout value',
|
|
73
|
+
'Check page for dynamic content issues',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
logs,
|
|
79
|
+
stderr: error.stderr || '',
|
|
80
|
+
error: error.message,
|
|
81
|
+
duration: elapsed,
|
|
82
|
+
nextSteps,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { runDoctor } from '../doctor.js';
|
|
2
|
+
|
|
3
|
+
export const doctorTool = {
|
|
4
|
+
name: 'browser_doctor',
|
|
5
|
+
description: 'Diagnose environment issues and provide actionable nextSteps. Checks network, permissions, dependencies, system libraries, executable, browser, and authentication. Returns categorized issues with fix recommendations.',
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {},
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function doctorToolHandler(args = {}) {
|
|
13
|
+
const logs = [];
|
|
14
|
+
|
|
15
|
+
logs.push(`[${new Date().toISOString()}] browser_doctor called`);
|
|
16
|
+
|
|
17
|
+
const result = await runDoctor();
|
|
18
|
+
|
|
19
|
+
const output = {
|
|
20
|
+
ok: result.ok,
|
|
21
|
+
summary: result.summary,
|
|
22
|
+
issues: result.issues,
|
|
23
|
+
version: result.versionInfo?.version,
|
|
24
|
+
latestVersion: result.versionInfo?.latestVersion,
|
|
25
|
+
hasUpdate: result.updateInfo?.hasUpdate,
|
|
26
|
+
nextSteps: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (!result.ok) {
|
|
30
|
+
for (const issue of result.issues) {
|
|
31
|
+
output.nextSteps.push(...issue.nextSteps);
|
|
32
|
+
}
|
|
33
|
+
output.nextSteps = [...new Set(output.nextSteps)];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
ok: result.ok,
|
|
38
|
+
logs,
|
|
39
|
+
stdout: JSON.stringify(output, null, 2),
|
|
40
|
+
...output,
|
|
41
|
+
};
|
|
42
|
+
}
|