amalgm 0.1.51 → 0.1.52
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/lib/tunnel-events.js +48 -23
- package/package.json +2 -2
- package/runtime/lib/harnesses.js +12 -4
- package/runtime/scripts/amalgm-mcp/agents/store.js +5 -5
- package/runtime/scripts/amalgm-mcp/{artifacts → apps}/advertise.js +39 -24
- package/runtime/scripts/amalgm-mcp/apps/rest.js +144 -0
- package/runtime/scripts/amalgm-mcp/apps/store.js +171 -0
- package/runtime/scripts/amalgm-mcp/apps/supervisor.js +439 -0
- package/runtime/scripts/amalgm-mcp/apps/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/automations/cell-references.js +237 -0
- package/runtime/scripts/amalgm-mcp/automations/context.js +41 -0
- package/runtime/scripts/amalgm-mcp/automations/rest.js +148 -0
- package/runtime/scripts/amalgm-mcp/automations/runner.js +613 -0
- package/runtime/scripts/amalgm-mcp/automations/scheduler.js +90 -0
- package/runtime/scripts/amalgm-mcp/automations/store.js +1125 -0
- package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +177 -0
- package/runtime/scripts/amalgm-mcp/automations/tools.js +418 -0
- package/runtime/scripts/amalgm-mcp/automations/validator.js +225 -0
- package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +505 -0
- package/runtime/scripts/amalgm-mcp/browser/electron-bridge.js +222 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +13 -631
- package/runtime/scripts/amalgm-mcp/browser/tools.js +9 -7
- package/runtime/scripts/amalgm-mcp/config.js +33 -48
- package/runtime/scripts/amalgm-mcp/deps.js +1 -31
- package/runtime/scripts/amalgm-mcp/events/ingress.js +50 -42
- package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +169 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +45 -14
- package/runtime/scripts/amalgm-mcp/events/store.js +106 -57
- package/runtime/scripts/amalgm-mcp/index.js +12 -14
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +229 -65
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +13 -27
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +2 -3
- package/runtime/scripts/amalgm-mcp/server/http.js +106 -56
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +1 -1
- package/runtime/scripts/amalgm-mcp/state/db.js +119 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +16 -3
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +348 -0
- package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +23 -0
- package/runtime/scripts/amalgm-mcp/tests/workflows-store-runner.test.js +67 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +16 -3
- package/runtime/scripts/amalgm-mcp/workflows/compiler.js +222 -0
- package/runtime/scripts/amalgm-mcp/workflows/runner.js +593 -0
- package/runtime/scripts/amalgm-mcp/workflows/store.js +237 -0
- package/runtime/scripts/chat-core/adapters/claude.js +2 -1
- package/runtime/scripts/chat-core/auth.js +82 -12
- package/runtime/scripts/chat-core/contract.js +5 -1
- package/runtime/scripts/chat-core/engine.js +103 -62
- package/runtime/scripts/chat-core/event-schema.js +8 -0
- package/runtime/scripts/chat-core/events.js +5 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +13 -1
- package/runtime/scripts/chat-core/parts.js +21 -6
- package/runtime/scripts/chat-core/sse.js +3 -0
- package/runtime/scripts/chat-core/tests/auth.test.js +84 -6
- package/runtime/scripts/chat-core/tests/engine.test.js +312 -0
- package/runtime/scripts/chat-core/tests/native-config.test.js +23 -0
- package/runtime/scripts/chat-core/tool-shape.js +4 -4
- package/runtime/scripts/chat-core/tooling/active-memory.js +5 -4
- package/runtime/scripts/chat-core/tooling/native-config.js +34 -3
- package/runtime/scripts/local-gateway.js +34 -27
- package/runtime/scripts/platform-context.txt +76 -94
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +0 -103
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +0 -157
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +0 -439
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +0 -176
- package/runtime/scripts/amalgm-mcp/events/executor.js +0 -258
- package/runtime/scripts/amalgm-mcp/events/rest.js +0 -214
- package/runtime/scripts/amalgm-mcp/events/tools.js +0 -323
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +0 -110
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +0 -416
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
8
|
+
const DEFAULT_SESSION = 'default';
|
|
9
|
+
const activeRecordings = new Map();
|
|
10
|
+
const knownSessions = new Map();
|
|
11
|
+
|
|
12
|
+
function amalgmDir() {
|
|
13
|
+
return process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ensureDir(dir) {
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function safeId(value, fallback = DEFAULT_SESSION) {
|
|
22
|
+
const raw = String(value || fallback).replace(/^agent-browser:/, '');
|
|
23
|
+
return raw.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 96) || fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function browserRoot() {
|
|
27
|
+
return ensureDir(path.join(amalgmDir(), 'browser'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function profileDir(session) {
|
|
31
|
+
return ensureDir(path.join(browserRoot(), 'profiles', session));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function screenshotDir() {
|
|
35
|
+
return ensureDir(path.join(browserRoot(), 'screenshots'));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function recordingDir(session) {
|
|
39
|
+
return ensureDir(path.join(amalgmDir(), 'browser-sessions', session));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function browserSession(args = {}, context = {}) {
|
|
43
|
+
return safeId(
|
|
44
|
+
args.browserSessionId
|
|
45
|
+
|| args.sessionId
|
|
46
|
+
|| args.tabId
|
|
47
|
+
|| context?.callerSessionId
|
|
48
|
+
|| DEFAULT_SESSION,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tabInfo(session, extra = {}) {
|
|
53
|
+
const info = {
|
|
54
|
+
tabId: `agent-browser:${session}`,
|
|
55
|
+
browserSessionId: session,
|
|
56
|
+
backend: 'agent-browser',
|
|
57
|
+
...extra,
|
|
58
|
+
};
|
|
59
|
+
knownSessions.set(session, { ...knownSessions.get(session), ...info, lastActiveAt: Date.now() });
|
|
60
|
+
return info;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function agentBrowserCommand() {
|
|
64
|
+
if (process.env.AMALGM_AGENT_BROWSER_BIN) {
|
|
65
|
+
return { command: process.env.AMALGM_AGENT_BROWSER_BIN, prefix: [] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
return {
|
|
70
|
+
command: process.execPath,
|
|
71
|
+
prefix: [require.resolve('agent-browser/bin/agent-browser.js')],
|
|
72
|
+
};
|
|
73
|
+
} catch {
|
|
74
|
+
return { command: 'agent-browser', prefix: [] };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function wantsHeaded() {
|
|
79
|
+
const explicit = process.env.AMALGM_BROWSER_HEADED || process.env.AGENT_BROWSER_HEADED;
|
|
80
|
+
if (explicit) return !/^(0|false|no)$/i.test(explicit);
|
|
81
|
+
if (/^(1|true|yes)$/i.test(process.env.AMALGM_BROWSER_HEADLESS || '')) return false;
|
|
82
|
+
if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function baseArgs(session, json = true) {
|
|
87
|
+
const args = [
|
|
88
|
+
'--session', session,
|
|
89
|
+
'--profile', profileDir(session),
|
|
90
|
+
'--screenshot-dir', screenshotDir(),
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
if (json) args.push('--json');
|
|
94
|
+
if (wantsHeaded()) args.push('--headed');
|
|
95
|
+
if (process.env.AMALGM_BROWSER_EXECUTABLE_PATH) {
|
|
96
|
+
args.push('--executable-path', process.env.AMALGM_BROWSER_EXECUTABLE_PATH);
|
|
97
|
+
}
|
|
98
|
+
if (process.env.AMALGM_BROWSER_PROVIDER) {
|
|
99
|
+
args.push('--provider', process.env.AMALGM_BROWSER_PROVIDER);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return args;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseJson(stdout) {
|
|
106
|
+
const text = String(stdout || '').trim();
|
|
107
|
+
if (!text) return {};
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(text);
|
|
111
|
+
} catch {
|
|
112
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
113
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
114
|
+
if (!/^[{[]/.test(lines[i])) continue;
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(lines.slice(i).join('\n'));
|
|
117
|
+
} catch {
|
|
118
|
+
// Keep looking for a JSON suffix.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { text };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function asText(value) {
|
|
127
|
+
if (value == null) return '';
|
|
128
|
+
if (typeof value === 'string') return value;
|
|
129
|
+
if (value.success === true && value.data !== undefined) return asText(value.data);
|
|
130
|
+
if (typeof value.snapshot === 'string') return value.snapshot;
|
|
131
|
+
if (typeof value.text === 'string') return value.text;
|
|
132
|
+
if (typeof value.value === 'string') return value.value;
|
|
133
|
+
if (typeof value.result === 'string') return value.result;
|
|
134
|
+
if (typeof value.output === 'string') return value.output;
|
|
135
|
+
if (typeof value.url === 'string') return value.url;
|
|
136
|
+
if (typeof value.title === 'string') return value.title;
|
|
137
|
+
return JSON.stringify(value, null, 2);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function runAgentBrowser(commandArgs, options = {}) {
|
|
141
|
+
const session = safeId(options.session);
|
|
142
|
+
const json = options.json !== false;
|
|
143
|
+
const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
144
|
+
const bin = agentBrowserCommand();
|
|
145
|
+
const args = [...bin.prefix, ...baseArgs(session, json), ...commandArgs.map(String)];
|
|
146
|
+
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
const child = spawn(bin.command, args, {
|
|
149
|
+
cwd: process.cwd(),
|
|
150
|
+
env: {
|
|
151
|
+
...process.env,
|
|
152
|
+
AGENT_BROWSER_SESSION: session,
|
|
153
|
+
AGENT_BROWSER_PROFILE: profileDir(session),
|
|
154
|
+
},
|
|
155
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
let stdout = '';
|
|
159
|
+
let stderr = '';
|
|
160
|
+
const timer = setTimeout(() => {
|
|
161
|
+
child.kill('SIGTERM');
|
|
162
|
+
reject(new Error(`agent-browser timed out running: ${commandArgs.join(' ')}`));
|
|
163
|
+
}, timeoutMs);
|
|
164
|
+
|
|
165
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
166
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
167
|
+
child.on('error', (err) => {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
reject(err.code === 'ENOENT'
|
|
170
|
+
? new Error('agent-browser is not installed. Reinstall Amalgm or run `npm install agent-browser`.')
|
|
171
|
+
: err);
|
|
172
|
+
});
|
|
173
|
+
child.on('close', (code) => {
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
if (code !== 0) {
|
|
176
|
+
reject(new Error((stderr || stdout || `agent-browser exited with code ${code}`).trim()));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const result = json ? parseJson(stdout) : { text: stdout.trim() };
|
|
180
|
+
if (result && result.success === false) {
|
|
181
|
+
reject(new Error(result.error || result.message || 'agent-browser command failed'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
resolve(result);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function directTarget(args = {}) {
|
|
190
|
+
if (args.ref) return String(args.ref);
|
|
191
|
+
if (args.selector) return String(args.selector);
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function findCommand(args = {}, action, value) {
|
|
196
|
+
const command = ['find'];
|
|
197
|
+
if (args.text || args.name) command.push('text', args.text || args.name);
|
|
198
|
+
else if (args.label) command.push('label', args.label);
|
|
199
|
+
else if (args.placeholder) command.push('placeholder', args.placeholder);
|
|
200
|
+
else if (args.testId) command.push('testid', args.testId);
|
|
201
|
+
else if (args.role) command.push('role', args.role);
|
|
202
|
+
else throw new Error('A selector, ref, text, role, label, placeholder, or testId target is required');
|
|
203
|
+
|
|
204
|
+
command.push(action);
|
|
205
|
+
if (value !== undefined) command.push(value);
|
|
206
|
+
if (args.role && args.name) command.push('--name', args.name);
|
|
207
|
+
return command;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function readTextCommand(command, session) {
|
|
211
|
+
return asText(await runAgentBrowser(command, { session, timeoutMs: 30_000 }));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function stateBrowser(args = {}, context) {
|
|
215
|
+
const session = browserSession(args, context);
|
|
216
|
+
let url = '';
|
|
217
|
+
let title = '';
|
|
218
|
+
|
|
219
|
+
try { url = await readTextCommand(['get', 'url'], session); } catch {}
|
|
220
|
+
try { title = await readTextCommand(['get', 'title'], session); } catch {}
|
|
221
|
+
|
|
222
|
+
return tabInfo(session, { title, url, loading: false, visible: wantsHeaded() });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function navigateBrowser(args = {}, context) {
|
|
226
|
+
const session = browserSession(args, context);
|
|
227
|
+
await runAgentBrowser(['open', args.url], { session });
|
|
228
|
+
const state = await stateBrowser(args, context);
|
|
229
|
+
return { ...state, status: 'ok' };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function screenshotBrowser(args = {}, context) {
|
|
233
|
+
const session = browserSession(args, context);
|
|
234
|
+
const file = path.join(screenshotDir(), `${session}-${Date.now()}-${crypto.randomUUID()}.png`);
|
|
235
|
+
await runAgentBrowser(['screenshot', ...(args.fullPage ? ['--full'] : []), file], { session });
|
|
236
|
+
const buffer = fs.readFileSync(file);
|
|
237
|
+
return {
|
|
238
|
+
...tabInfo(session),
|
|
239
|
+
base64: buffer.toString('base64'),
|
|
240
|
+
bytes: buffer.length,
|
|
241
|
+
mode: args.fullPage ? 'full page' : 'viewport',
|
|
242
|
+
path: file,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function snapshotBrowser(args = {}, context) {
|
|
247
|
+
const session = browserSession(args, context);
|
|
248
|
+
const snapshot = await runAgentBrowser(['snapshot', '-i'], { session, timeoutMs: 30_000 });
|
|
249
|
+
const state = await stateBrowser(args, context);
|
|
250
|
+
return { ...state, snapshotText: asText(snapshot) };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function clickBrowser(args = {}, context) {
|
|
254
|
+
const session = browserSession(args, context);
|
|
255
|
+
const target = directTarget(args);
|
|
256
|
+
await runAgentBrowser(target ? ['click', target] : findCommand(args, 'click'), { session });
|
|
257
|
+
return tabInfo(session, { description: `Clicked ${target || args.text || args.name || args.label || args.placeholder || args.testId || args.role}` });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function typeBrowser(args = {}, context) {
|
|
261
|
+
const session = browserSession(args, context);
|
|
262
|
+
const target = directTarget(args);
|
|
263
|
+
const action = args.clear === false ? 'type' : 'fill';
|
|
264
|
+
await runAgentBrowser(target ? [action, target, args.text || ''] : findCommand(args, action, args.text || ''), { session });
|
|
265
|
+
return tabInfo(session, { description: `Typed into ${target || args.label || args.placeholder || args.role || 'target'}` });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function pressBrowser(args = {}, context) {
|
|
269
|
+
const session = browserSession(args, context);
|
|
270
|
+
const target = directTarget(args);
|
|
271
|
+
if (target) await runAgentBrowser(['focus', target], { session });
|
|
272
|
+
await runAgentBrowser(['press', args.key], { session });
|
|
273
|
+
return tabInfo(session, { pressed: args.key });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function getTextBrowser(args = {}, context) {
|
|
277
|
+
const session = browserSession(args, context);
|
|
278
|
+
const target = directTarget(args) || 'body';
|
|
279
|
+
return { ...tabInfo(session), text: await readTextCommand(['get', 'text', target], session) };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function evaluateBrowser(args = {}, context) {
|
|
283
|
+
const session = browserSession(args, context);
|
|
284
|
+
return { ...tabInfo(session), result: await runAgentBrowser(['eval', args.script || ''], { session }) };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function locatorCountBrowser(args = {}, context) {
|
|
288
|
+
const session = browserSession(args, context);
|
|
289
|
+
const target = directTarget(args);
|
|
290
|
+
if (!target) throw new Error('Count currently requires selector or ref. Use browser_snapshot for refs.');
|
|
291
|
+
const count = Number(await readTextCommand(['get', 'count', target], session));
|
|
292
|
+
return { ...tabInfo(session), count: Number.isFinite(count) ? count : 0 };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function locatorTextBrowser(args = {}, context) {
|
|
296
|
+
const session = browserSession(args, context);
|
|
297
|
+
const target = directTarget(args);
|
|
298
|
+
if (!target) throw new Error('Text lookup currently requires selector or ref. Use browser_snapshot for refs.');
|
|
299
|
+
return { ...tabInfo(session), text: await readTextCommand(['get', 'text', target], session) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function sleep(ms) {
|
|
303
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function globToRegExp(pattern) {
|
|
307
|
+
const escaped = String(pattern)
|
|
308
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
309
|
+
.replace(/\*\*/g, '.*')
|
|
310
|
+
.replace(/\*/g, '[^/]*');
|
|
311
|
+
return new RegExp(`^${escaped}$`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function urlMatches(actual, expected) {
|
|
315
|
+
if (!expected) return true;
|
|
316
|
+
const target = String(expected);
|
|
317
|
+
if (actual === target) return true;
|
|
318
|
+
if (target.includes('*')) return globToRegExp(target).test(actual);
|
|
319
|
+
return actual.includes(target);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function pollUntil(description, timeoutMs, check) {
|
|
323
|
+
const deadline = Date.now() + timeoutMs;
|
|
324
|
+
let lastError;
|
|
325
|
+
|
|
326
|
+
while (Date.now() < deadline) {
|
|
327
|
+
try {
|
|
328
|
+
if (await check()) return;
|
|
329
|
+
} catch (err) {
|
|
330
|
+
lastError = err;
|
|
331
|
+
}
|
|
332
|
+
await sleep(250);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
throw new Error(`${description} timed out${lastError?.message ? `: ${lastError.message}` : ''}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function waitForLoadStateBrowser(args = {}, context) {
|
|
339
|
+
const session = browserSession(args, context);
|
|
340
|
+
const state = args.state || 'load';
|
|
341
|
+
const timeoutMs = args.timeoutMs || 30_000;
|
|
342
|
+
|
|
343
|
+
if (state === 'networkidle') {
|
|
344
|
+
await runAgentBrowser(['wait', Math.min(timeoutMs, 1000)], { session, timeoutMs });
|
|
345
|
+
return stateBrowser(args, context);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await pollUntil(`wait for ${state}`, timeoutMs, async () => {
|
|
349
|
+
const readyState = await readTextCommand(['eval', 'document.readyState'], session);
|
|
350
|
+
return state === 'domcontentloaded'
|
|
351
|
+
? readyState !== 'loading'
|
|
352
|
+
: readyState === 'complete';
|
|
353
|
+
});
|
|
354
|
+
return stateBrowser(args, context);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function waitForURLBrowser(args = {}, context) {
|
|
358
|
+
const session = browserSession(args, context);
|
|
359
|
+
const expectedUrl = args.url;
|
|
360
|
+
await pollUntil(`wait for URL ${expectedUrl}`, args.timeoutMs || 30_000, async () => {
|
|
361
|
+
const currentUrl = await readTextCommand(['get', 'url'], session);
|
|
362
|
+
return urlMatches(currentUrl, expectedUrl);
|
|
363
|
+
});
|
|
364
|
+
return stateBrowser(args, context);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function waitForSelectorBrowser(args = {}, context) {
|
|
368
|
+
const session = browserSession(args, context);
|
|
369
|
+
await runAgentBrowser(['wait', args.selector], { session, timeoutMs: args.timeoutMs || 30_000 });
|
|
370
|
+
return { ...tabInfo(session), selector: args.selector, matched: true };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function tabNavigationBrowser(action, args = {}, context) {
|
|
374
|
+
const session = browserSession(args, context);
|
|
375
|
+
await runAgentBrowser([action], { session });
|
|
376
|
+
return stateBrowser(args, context);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function tabsListBrowser() {
|
|
380
|
+
return {
|
|
381
|
+
tabs: Array.from(knownSessions.values()).sort((a, b) => (b.lastActiveAt || 0) - (a.lastActiveAt || 0)),
|
|
382
|
+
backend: 'agent-browser',
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function tabsSelectedBrowser(context) {
|
|
387
|
+
return stateBrowser({}, context);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function tabsNewBrowser(args = {}, context) {
|
|
391
|
+
const session = safeId(args.browserSessionId || args.sessionId || `browser-${Date.now().toString(36)}`);
|
|
392
|
+
if (args.url) await navigateBrowser({ ...args, browserSessionId: session }, context);
|
|
393
|
+
return stateBrowser({ ...args, browserSessionId: session }, context);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function tabsGetBrowser(args = {}, context) {
|
|
397
|
+
return stateBrowser(args, context);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function tabsSelectBrowser(args = {}, context) {
|
|
401
|
+
return stateBrowser(args, context);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function cuaBrowser(action, args = {}, context) {
|
|
405
|
+
const session = browserSession(args, context);
|
|
406
|
+
const x = Math.round(Number(args.x || 0));
|
|
407
|
+
const y = Math.round(Number(args.y || 0));
|
|
408
|
+
if (action === 'cua_move') await runAgentBrowser(['mouse', 'move', x, y], { session });
|
|
409
|
+
if (action === 'cua_click') {
|
|
410
|
+
await runAgentBrowser(['mouse', 'move', x, y], { session });
|
|
411
|
+
await runAgentBrowser(['mouse', 'down', args.button || 1], { session });
|
|
412
|
+
await runAgentBrowser(['mouse', 'up', args.button || 1], { session });
|
|
413
|
+
}
|
|
414
|
+
if (action === 'cua_double_click') {
|
|
415
|
+
await runAgentBrowser(['mouse', 'move', x, y], { session });
|
|
416
|
+
await runAgentBrowser(['mouse', 'down', 1], { session });
|
|
417
|
+
await runAgentBrowser(['mouse', 'up', 1], { session });
|
|
418
|
+
await runAgentBrowser(['mouse', 'down', 1], { session });
|
|
419
|
+
await runAgentBrowser(['mouse', 'up', 1], { session });
|
|
420
|
+
}
|
|
421
|
+
if (action === 'cua_scroll') await runAgentBrowser(['mouse', 'wheel', args.scrollY || 0, args.scrollX || 0], { session });
|
|
422
|
+
if (action === 'cua_type') await runAgentBrowser(['keyboard', 'type', args.text || ''], { session });
|
|
423
|
+
if (action === 'cua_keypress') await runAgentBrowser(['press', (args.keys || []).join('+')], { session });
|
|
424
|
+
if (action === 'cua_drag') await dragBrowser(args, session);
|
|
425
|
+
return tabInfo(session, { ok: true });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function dragBrowser(args, session) {
|
|
429
|
+
const pathPoints = Array.isArray(args.path) ? args.path : [];
|
|
430
|
+
if (pathPoints.length < 2) throw new Error('drag path must contain at least two points');
|
|
431
|
+
await runAgentBrowser(['mouse', 'move', pathPoints[0].x, pathPoints[0].y], { session });
|
|
432
|
+
await runAgentBrowser(['mouse', 'down', 1], { session });
|
|
433
|
+
for (const point of pathPoints.slice(1)) {
|
|
434
|
+
await runAgentBrowser(['mouse', 'move', point.x, point.y], { session });
|
|
435
|
+
}
|
|
436
|
+
await runAgentBrowser(['mouse', 'up', 1], { session });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function clipboardBrowser(action, args = {}, context) {
|
|
440
|
+
const session = browserSession(args, context);
|
|
441
|
+
if (action === 'clipboard_read_text') {
|
|
442
|
+
return { ...tabInfo(session), text: await readTextCommand(['clipboard', 'read'], session) };
|
|
443
|
+
}
|
|
444
|
+
await runAgentBrowser(['clipboard', 'write', args.text || ''], { session });
|
|
445
|
+
return tabInfo(session, { written: true });
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function startVideoBrowser(args = {}, context) {
|
|
449
|
+
const session = browserSession(args, context);
|
|
450
|
+
const id = safeId(args.sessionId || args.recording_id || session, session);
|
|
451
|
+
const file = path.join(recordingDir(id), `${new Date().toISOString().replace(/[:.]/g, '-')}.webm`);
|
|
452
|
+
await runAgentBrowser(['record', 'start', file], { session });
|
|
453
|
+
activeRecordings.set(session, file);
|
|
454
|
+
return tabInfo(session, { path: file, mimeType: 'video/webm' });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function stopVideoBrowser(args = {}, context) {
|
|
458
|
+
const session = browserSession(args, context);
|
|
459
|
+
await runAgentBrowser(['record', 'stop'], { session });
|
|
460
|
+
const file = activeRecordings.get(session);
|
|
461
|
+
activeRecordings.delete(session);
|
|
462
|
+
if (!file) return tabInfo(session, { stopped: true });
|
|
463
|
+
const bytes = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
464
|
+
return tabInfo(session, {
|
|
465
|
+
path: file,
|
|
466
|
+
relativePath: path.relative(amalgmDir(), file),
|
|
467
|
+
url: `/api/browser-sessions/video?file=${encodeURIComponent(path.relative(amalgmDir(), file))}`,
|
|
468
|
+
bytes,
|
|
469
|
+
mimeType: 'video/webm',
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function closeBrowser(args = {}, context) {
|
|
474
|
+
const session = browserSession(args, context);
|
|
475
|
+
await runAgentBrowser(['close'], { session });
|
|
476
|
+
knownSessions.delete(session);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
module.exports = {
|
|
480
|
+
clipboardBrowser,
|
|
481
|
+
closeBrowser,
|
|
482
|
+
clickBrowser,
|
|
483
|
+
cuaBrowser,
|
|
484
|
+
evaluateBrowser,
|
|
485
|
+
getTextBrowser,
|
|
486
|
+
locatorCountBrowser,
|
|
487
|
+
locatorTextBrowser,
|
|
488
|
+
navigateBrowser,
|
|
489
|
+
pressBrowser,
|
|
490
|
+
screenshotBrowser,
|
|
491
|
+
snapshotBrowser,
|
|
492
|
+
startVideoBrowser,
|
|
493
|
+
stateBrowser,
|
|
494
|
+
stopVideoBrowser,
|
|
495
|
+
tabNavigationBrowser,
|
|
496
|
+
tabsGetBrowser,
|
|
497
|
+
tabsListBrowser,
|
|
498
|
+
tabsNewBrowser,
|
|
499
|
+
tabsSelectBrowser,
|
|
500
|
+
tabsSelectedBrowser,
|
|
501
|
+
typeBrowser,
|
|
502
|
+
waitForLoadStateBrowser,
|
|
503
|
+
waitForSelectorBrowser,
|
|
504
|
+
waitForURLBrowser,
|
|
505
|
+
};
|