amalgm 0.1.51 → 0.1.53
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 +547 -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-binaries.js +34 -9
- 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
|
@@ -1,637 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* In desktop local mode, browser_* tools talk to Electron's visible browser
|
|
5
|
-
* bridge so users can watch the session in a real tab. Outside Electron, we
|
|
6
|
-
* fall back to a private Playwright Chromium instance.
|
|
7
|
-
*/
|
|
1
|
+
const electron = require('./electron-bridge');
|
|
2
|
+
const agentBrowser = require('./agent-browser');
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
4
|
+
function backend() {
|
|
5
|
+
const requested = (process.env.AMALGM_BROWSER_BACKEND || '').toLowerCase();
|
|
6
|
+
if (requested === 'agent-browser' || requested === 'external') return agentBrowser;
|
|
7
|
+
if (requested === 'electron') return electron;
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
let _browserContext = null;
|
|
17
|
-
let _browserPage = null;
|
|
18
|
-
let _selectedPlaywrightTabId = 'playwright-default';
|
|
19
|
-
const _playwrightTabs = new Map();
|
|
20
|
-
const _defaultElectronSessionId = `mcp-${process.pid}-default`;
|
|
21
|
-
|
|
22
|
-
function newBrowserSessionId(prefix = 'browser') {
|
|
23
|
-
return `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function formatAccessibilityTree(node, depth = 0) {
|
|
27
|
-
if (!node) return '';
|
|
28
|
-
const indent = ' '.repeat(depth);
|
|
29
|
-
let line = `${indent}${node.role || 'unknown'}`;
|
|
30
|
-
if (node.name) line += `: "${node.name}"`;
|
|
31
|
-
if (node.value) line += ` = "${node.value}"`;
|
|
32
|
-
let result = line + '\n';
|
|
33
|
-
if (node.children) {
|
|
34
|
-
for (const child of node.children) result += formatAccessibilityTree(child, depth + 1);
|
|
35
|
-
}
|
|
36
|
-
return result;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function getElectronBridgeConfig() {
|
|
40
|
-
const baseUrl = process.env.AMALGM_ELECTRON_BROWSER_URL;
|
|
41
|
-
const token = process.env.AMALGM_ELECTRON_BROWSER_TOKEN;
|
|
42
|
-
if (!baseUrl || !token) return null;
|
|
43
|
-
return { baseUrl, token };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function isBridgeTransportError(err) {
|
|
47
|
-
return Boolean(
|
|
48
|
-
err && (
|
|
49
|
-
err.code === 'ECONNREFUSED'
|
|
50
|
-
|| err.code === 'ECONNRESET'
|
|
51
|
-
|| err.code === 'ENOTFOUND'
|
|
52
|
-
|| err.code === 'ETIMEDOUT'
|
|
53
|
-
|| err.bridgeTransport === true
|
|
54
|
-
),
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function requestJson(url, { method = 'GET', headers = {}, body } = {}) {
|
|
59
|
-
return new Promise((resolve, reject) => {
|
|
60
|
-
const target = new URL(url);
|
|
61
|
-
const transport = target.protocol === 'https:' ? https : http;
|
|
62
|
-
const payload = body ? JSON.stringify(body) : null;
|
|
63
|
-
const req = transport.request(target, {
|
|
64
|
-
method,
|
|
65
|
-
headers: {
|
|
66
|
-
Accept: 'application/json',
|
|
67
|
-
...(payload ? { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(payload)) } : {}),
|
|
68
|
-
...headers,
|
|
69
|
-
},
|
|
70
|
-
}, (res) => {
|
|
71
|
-
let raw = '';
|
|
72
|
-
res.on('data', (chunk) => {
|
|
73
|
-
raw += chunk.toString();
|
|
74
|
-
});
|
|
75
|
-
res.on('end', () => {
|
|
76
|
-
let parsed = {};
|
|
77
|
-
if (raw) {
|
|
78
|
-
try {
|
|
79
|
-
parsed = JSON.parse(raw);
|
|
80
|
-
} catch (parseErr) {
|
|
81
|
-
const err = new Error(`Browser bridge returned invalid JSON: ${parseErr.message}`);
|
|
82
|
-
err.bridgeTransport = true;
|
|
83
|
-
reject(err);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (res.statusCode && res.statusCode >= 400) {
|
|
89
|
-
const message = parsed && typeof parsed.error === 'string'
|
|
90
|
-
? parsed.error
|
|
91
|
-
: `Browser bridge request failed with status ${res.statusCode}`;
|
|
92
|
-
const err = new Error(message);
|
|
93
|
-
err.statusCode = res.statusCode;
|
|
94
|
-
reject(err);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
resolve(parsed);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
req.on('error', (err) => {
|
|
103
|
-
err.bridgeTransport = true;
|
|
104
|
-
reject(err);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
req.setTimeout(120_000, () => {
|
|
108
|
-
const err = new Error('Browser bridge request timed out');
|
|
109
|
-
err.code = 'ETIMEDOUT';
|
|
110
|
-
err.bridgeTransport = true;
|
|
111
|
-
req.destroy(err);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
if (payload) req.write(payload);
|
|
115
|
-
req.end();
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function callElectronBridge(action, body = {}) {
|
|
120
|
-
const bridge = getElectronBridgeConfig();
|
|
121
|
-
if (!bridge) throw new Error('Electron browser bridge is not configured');
|
|
122
|
-
|
|
123
|
-
try {
|
|
124
|
-
return await requestJson(`${bridge.baseUrl}/browser/${action}`, {
|
|
125
|
-
method: 'POST',
|
|
126
|
-
headers: { 'x-amalgm-browser-token': bridge.token },
|
|
127
|
-
body,
|
|
128
|
-
});
|
|
129
|
-
} catch (err) {
|
|
130
|
-
if (isBridgeTransportError(err)) {
|
|
131
|
-
_browserBackend = null;
|
|
132
|
-
}
|
|
133
|
-
throw err;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function withRequestContext(body = {}, context = {}) {
|
|
138
|
-
const next = {
|
|
139
|
-
...body,
|
|
140
|
-
...(context?.callerSessionId ? { callerSessionId: context.callerSessionId } : {}),
|
|
141
|
-
};
|
|
142
|
-
if (!next.tabId && !next.sessionId && !next.browserSessionId) {
|
|
143
|
-
next.browserSessionId = context?.callerSessionId || _defaultElectronSessionId;
|
|
144
|
-
}
|
|
145
|
-
return next;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async function probeElectronBridge() {
|
|
149
|
-
const bridge = getElectronBridgeConfig();
|
|
150
|
-
if (!bridge) return false;
|
|
151
|
-
|
|
152
|
-
_lastElectronBridgeProbeAt = Date.now();
|
|
153
|
-
try {
|
|
154
|
-
await requestJson(`${bridge.baseUrl}/healthz`, {
|
|
155
|
-
headers: { 'x-amalgm-browser-token': bridge.token },
|
|
156
|
-
});
|
|
157
|
-
console.log('[AmalgmMCP] Using Electron browser bridge');
|
|
158
|
-
return true;
|
|
159
|
-
} catch (err) {
|
|
160
|
-
if (isBridgeTransportError(err)) {
|
|
161
|
-
console.warn(`[AmalgmMCP] Electron browser bridge unavailable, falling back to Playwright: ${err.message}`);
|
|
162
|
-
return false;
|
|
163
|
-
}
|
|
164
|
-
throw err;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function resolveBrowserBackend() {
|
|
169
|
-
if (_browserBackend === 'electron') return _browserBackend;
|
|
170
|
-
if (_browserBackend === 'playwright') {
|
|
171
|
-
const hasElectronBridgeConfig = Boolean(getElectronBridgeConfig());
|
|
172
|
-
const shouldRetryElectron = hasElectronBridgeConfig && Date.now() - _lastElectronBridgeProbeAt > 1_000;
|
|
173
|
-
if (!shouldRetryElectron) return _browserBackend;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (await probeElectronBridge()) {
|
|
177
|
-
_browserBackend = 'electron';
|
|
178
|
-
return _browserBackend;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
_browserBackend = 'playwright';
|
|
182
|
-
return _browserBackend;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
async function getPlaywrightPage() {
|
|
186
|
-
if (_browserPage && !_browserPage.isClosed()) {
|
|
187
|
-
_playwrightTabs.set(_selectedPlaywrightTabId, _browserPage);
|
|
188
|
-
return _browserPage;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const pw = loadPlaywright();
|
|
192
|
-
if (!_browser || !_browser.isConnected()) {
|
|
193
|
-
console.log('[AmalgmMCP] Launching headless Chromium...');
|
|
194
|
-
_browser = await pw.chromium.launch({
|
|
195
|
-
headless: true,
|
|
196
|
-
args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
_browserContext = await _browser.newContext({ viewport: { width: 1280, height: 720 } });
|
|
201
|
-
_browserPage = await _browserContext.newPage();
|
|
202
|
-
_playwrightTabs.set(_selectedPlaywrightTabId, _browserPage);
|
|
203
|
-
return _browserPage;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function navigateBrowser(urlOrArgs, context) {
|
|
207
|
-
const args = typeof urlOrArgs === 'object' && urlOrArgs
|
|
208
|
-
? urlOrArgs
|
|
209
|
-
: { url: urlOrArgs };
|
|
210
|
-
const url = args.url;
|
|
211
|
-
const backend = await resolveBrowserBackend();
|
|
212
|
-
if (backend === 'electron') {
|
|
213
|
-
return callElectronBridge('navigate', withRequestContext(args, context));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const page = await getPlaywrightPage();
|
|
217
|
-
const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
218
|
-
const status = response ? response.status() : 'unknown';
|
|
219
|
-
const title = await page.title();
|
|
220
|
-
return { title, url: page.url(), status };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
async function screenshotBrowser(fullPageOrArgs = false, context) {
|
|
224
|
-
const args = typeof fullPageOrArgs === 'object' && fullPageOrArgs
|
|
225
|
-
? fullPageOrArgs
|
|
226
|
-
: { fullPage: Boolean(fullPageOrArgs) };
|
|
227
|
-
const fullPage = Boolean(args.fullPage);
|
|
228
|
-
const backend = await resolveBrowserBackend();
|
|
229
|
-
if (backend === 'electron') {
|
|
230
|
-
return callElectronBridge('screenshot', withRequestContext(args, context));
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const page = await getPlaywrightPage();
|
|
234
|
-
const buffer = await page.screenshot({ fullPage, type: 'png' });
|
|
235
|
-
return {
|
|
236
|
-
base64: buffer.toString('base64'),
|
|
237
|
-
bytes: buffer.length,
|
|
238
|
-
mode: fullPage ? 'full page' : 'viewport',
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async function snapshotBrowser(argsOrContext, maybeContext) {
|
|
243
|
-
const hasArgs = maybeContext !== undefined;
|
|
244
|
-
const args = hasArgs ? (argsOrContext || {}) : {};
|
|
245
|
-
const context = hasArgs ? maybeContext : argsOrContext;
|
|
246
|
-
const backend = await resolveBrowserBackend();
|
|
247
|
-
if (backend === 'electron') {
|
|
248
|
-
return callElectronBridge('snapshot', withRequestContext(args, context));
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const page = await getPlaywrightPage();
|
|
252
|
-
const snapshot = await page.accessibility.snapshot();
|
|
253
|
-
const title = await page.title();
|
|
254
|
-
const url = page.url();
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
title,
|
|
258
|
-
url,
|
|
259
|
-
snapshotText: snapshot
|
|
260
|
-
? formatAccessibilityTree(snapshot, 0)
|
|
261
|
-
: await page.evaluate(() => document.body.innerText),
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function clickBrowser(args = {}, context) {
|
|
266
|
-
const { selector, text } = args;
|
|
267
|
-
const backend = await resolveBrowserBackend();
|
|
268
|
-
if (backend === 'electron') {
|
|
269
|
-
return callElectronBridge('click', withRequestContext(args, context));
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const page = await getPlaywrightPage();
|
|
273
|
-
if (text) {
|
|
274
|
-
await page.getByText(text, { exact: false }).first().click({ timeout: 5_000 });
|
|
275
|
-
return { description: `Clicked element with text: "${text}"` };
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
await page.click(selector, { timeout: 5_000 });
|
|
279
|
-
return { description: `Clicked element: ${selector}` };
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
async function typeBrowser(args = {}, context) {
|
|
283
|
-
const { selector, text, clear = true } = args;
|
|
284
|
-
const backend = await resolveBrowserBackend();
|
|
285
|
-
if (backend === 'electron') {
|
|
286
|
-
return callElectronBridge('type', withRequestContext(args, context));
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const page = await getPlaywrightPage();
|
|
290
|
-
if (selector) {
|
|
291
|
-
if (clear) await page.fill(selector, text);
|
|
292
|
-
else await page.type(selector, text);
|
|
293
|
-
return { description: `Typed into ${selector}: "${text}"` };
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
await page.keyboard.type(text);
|
|
297
|
-
return { description: `Typed: "${text}"` };
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
async function getTextBrowser(selectorOrArgs, context) {
|
|
301
|
-
const args = typeof selectorOrArgs === 'object' && selectorOrArgs
|
|
302
|
-
? selectorOrArgs
|
|
303
|
-
: { selector: selectorOrArgs };
|
|
304
|
-
const { selector } = args;
|
|
305
|
-
const backend = await resolveBrowserBackend();
|
|
306
|
-
if (backend === 'electron') {
|
|
307
|
-
return callElectronBridge('get_text', withRequestContext(args, context));
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
const page = await getPlaywrightPage();
|
|
311
|
-
let text;
|
|
312
|
-
if (selector) text = await page.textContent(selector);
|
|
313
|
-
else text = await page.evaluate(() => document.body.innerText);
|
|
314
|
-
return { text: text || '' };
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async function evaluateBrowser(scriptOrArgs, context) {
|
|
318
|
-
const args = typeof scriptOrArgs === 'object' && scriptOrArgs
|
|
319
|
-
? scriptOrArgs
|
|
320
|
-
: { script: scriptOrArgs };
|
|
321
|
-
const { script } = args;
|
|
322
|
-
const backend = await resolveBrowserBackend();
|
|
323
|
-
if (backend === 'electron') {
|
|
324
|
-
return callElectronBridge('evaluate', withRequestContext(args, context));
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const page = await getPlaywrightPage();
|
|
328
|
-
const result = await page.evaluate(script);
|
|
329
|
-
return { result };
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async function stateBrowser(args = {}, context) {
|
|
333
|
-
const backend = await resolveBrowserBackend();
|
|
334
|
-
if (backend === 'electron') {
|
|
335
|
-
return callElectronBridge('state', withRequestContext(args, context));
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const page = await getPlaywrightPage();
|
|
339
|
-
return {
|
|
340
|
-
tabId: _selectedPlaywrightTabId,
|
|
341
|
-
browserSessionId: _selectedPlaywrightTabId,
|
|
342
|
-
title: await page.title(),
|
|
343
|
-
url: page.url(),
|
|
344
|
-
loading: false,
|
|
345
|
-
backend: 'playwright',
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async function tabsListBrowser(context) {
|
|
350
|
-
const backend = await resolveBrowserBackend();
|
|
351
|
-
if (backend === 'electron') {
|
|
352
|
-
return callElectronBridge('tabs_list', withRequestContext({}, context));
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
await getPlaywrightPage();
|
|
356
|
-
const tabs = [];
|
|
357
|
-
for (const [tabId, page] of _playwrightTabs.entries()) {
|
|
358
|
-
if (!page || page.isClosed()) continue;
|
|
359
|
-
tabs.push({
|
|
360
|
-
tabId,
|
|
361
|
-
browserSessionId: tabId,
|
|
362
|
-
title: await page.title(),
|
|
363
|
-
url: page.url(),
|
|
364
|
-
selected: tabId === _selectedPlaywrightTabId,
|
|
365
|
-
backend: 'playwright',
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
return { tabs, selectedTabId: _selectedPlaywrightTabId };
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async function tabsSelectedBrowser(context) {
|
|
372
|
-
const backend = await resolveBrowserBackend();
|
|
373
|
-
if (backend === 'electron') {
|
|
374
|
-
return callElectronBridge('tabs_selected', withRequestContext({}, context));
|
|
375
|
-
}
|
|
376
|
-
return stateBrowser({}, context);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
async function tabsNewBrowser({ url, browserSessionId, sessionId } = {}, context) {
|
|
380
|
-
const nextSessionId = browserSessionId || sessionId || newBrowserSessionId('browser-tab');
|
|
381
|
-
const backend = await resolveBrowserBackend();
|
|
382
|
-
if (backend === 'electron') {
|
|
383
|
-
return callElectronBridge('tabs_new', withRequestContext({
|
|
384
|
-
browserSessionId: nextSessionId,
|
|
385
|
-
sessionId: nextSessionId,
|
|
386
|
-
url,
|
|
387
|
-
newTab: true,
|
|
388
|
-
}, context));
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const pw = loadPlaywright();
|
|
392
|
-
if (!_browser || !_browser.isConnected()) {
|
|
393
|
-
_browser = await pw.chromium.launch({
|
|
394
|
-
headless: true,
|
|
395
|
-
args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
const contextInstance = await _browser.newContext({ viewport: { width: 1280, height: 720 } });
|
|
399
|
-
const page = await contextInstance.newPage();
|
|
400
|
-
_browserContext = contextInstance;
|
|
401
|
-
_browserPage = page;
|
|
402
|
-
_selectedPlaywrightTabId = nextSessionId;
|
|
403
|
-
_playwrightTabs.set(nextSessionId, page);
|
|
404
|
-
if (url) await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
405
|
-
return stateBrowser({}, context);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
async function tabsGetBrowser({ tabId, browserSessionId, sessionId } = {}, context) {
|
|
409
|
-
const backend = await resolveBrowserBackend();
|
|
410
|
-
const targetSessionId = browserSessionId || sessionId;
|
|
411
|
-
if (backend === 'electron') {
|
|
412
|
-
return callElectronBridge('tabs_get', withRequestContext({
|
|
413
|
-
...(tabId ? { tabId } : {}),
|
|
414
|
-
...(targetSessionId ? { browserSessionId: targetSessionId } : {}),
|
|
415
|
-
}, context));
|
|
416
|
-
}
|
|
417
|
-
const targetTabId = tabId || targetSessionId;
|
|
418
|
-
if (targetTabId && _playwrightTabs.has(targetTabId)) {
|
|
419
|
-
_selectedPlaywrightTabId = targetTabId;
|
|
420
|
-
_browserPage = _playwrightTabs.get(targetTabId);
|
|
421
|
-
}
|
|
422
|
-
return stateBrowser({}, context);
|
|
9
|
+
// In the desktop app this bridge drives the visible in-tab Electron webview.
|
|
10
|
+
// Outside Electron, keep the agent-browser CLI fallback for headless/remote use.
|
|
11
|
+
return electron.isConfigured() ? electron : agentBrowser;
|
|
423
12
|
}
|
|
424
13
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
return callElectronBridge('tabs_select', withRequestContext(args, context));
|
|
429
|
-
}
|
|
430
|
-
return tabsGetBrowser(args, context);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
async function tabNavigationBrowser(action, args = {}, context) {
|
|
434
|
-
const backend = await resolveBrowserBackend();
|
|
435
|
-
if (backend === 'electron') {
|
|
436
|
-
return callElectronBridge(action, withRequestContext(args, context));
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
const page = await getPlaywrightPage();
|
|
440
|
-
if (action === 'back') await page.goBack({ waitUntil: 'domcontentloaded', timeout: 30_000 }).catch(() => null);
|
|
441
|
-
if (action === 'forward') await page.goForward({ waitUntil: 'domcontentloaded', timeout: 30_000 }).catch(() => null);
|
|
442
|
-
if (action === 'reload') await page.reload({ waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
443
|
-
return stateBrowser({}, context);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
async function waitForLoadStateBrowser(args = {}, context) {
|
|
447
|
-
const backend = await resolveBrowserBackend();
|
|
448
|
-
if (backend === 'electron') {
|
|
449
|
-
return callElectronBridge('wait_for_load_state', withRequestContext(args, context));
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const page = await getPlaywrightPage();
|
|
453
|
-
await page.waitForLoadState(args.state || 'load', { timeout: args.timeoutMs || 10_000 });
|
|
454
|
-
return stateBrowser({}, context);
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
async function waitForURLBrowser(args = {}, context) {
|
|
458
|
-
const backend = await resolveBrowserBackend();
|
|
459
|
-
if (backend === 'electron') {
|
|
460
|
-
return callElectronBridge('wait_for_url', withRequestContext(args, context));
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const page = await getPlaywrightPage();
|
|
464
|
-
await page.waitForURL(args.url, { timeout: args.timeoutMs || 10_000, waitUntil: args.waitUntil || 'load' });
|
|
465
|
-
return stateBrowser({}, context);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
async function waitForSelectorBrowser(args = {}, context) {
|
|
469
|
-
const backend = await resolveBrowserBackend();
|
|
470
|
-
if (backend === 'electron') {
|
|
471
|
-
return callElectronBridge('wait_for_selector', withRequestContext(args, context));
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const page = await getPlaywrightPage();
|
|
475
|
-
await page.locator(args.selector).waitFor({ state: args.state || 'visible', timeout: args.timeoutMs || 10_000 });
|
|
476
|
-
return { selector: args.selector, state: args.state || 'visible', matched: true };
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
async function locatorCountBrowser(args = {}, context) {
|
|
480
|
-
const backend = await resolveBrowserBackend();
|
|
481
|
-
if (backend === 'electron') {
|
|
482
|
-
return callElectronBridge('locator_count', withRequestContext(args, context));
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const page = await getPlaywrightPage();
|
|
486
|
-
const locator = args.selector
|
|
487
|
-
? page.locator(args.selector)
|
|
488
|
-
: args.text
|
|
489
|
-
? page.getByText(args.text, { exact: args.exact === true })
|
|
490
|
-
: args.role
|
|
491
|
-
? page.getByRole(args.role, { name: args.name, exact: args.exact === true })
|
|
492
|
-
: args.label
|
|
493
|
-
? page.getByLabel(args.label, { exact: args.exact === true })
|
|
494
|
-
: args.placeholder
|
|
495
|
-
? page.getByPlaceholder(args.placeholder, { exact: args.exact === true })
|
|
496
|
-
: args.testId
|
|
497
|
-
? page.getByTestId(args.testId)
|
|
498
|
-
: null;
|
|
499
|
-
if (!locator) throw new Error('A selector, text, role, label, placeholder, or testId target is required');
|
|
500
|
-
return { count: await locator.count() };
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
async function locatorTextBrowser(args = {}, context) {
|
|
504
|
-
const backend = await resolveBrowserBackend();
|
|
505
|
-
if (backend === 'electron') {
|
|
506
|
-
return callElectronBridge('locator_text', withRequestContext(args, context));
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const page = await getPlaywrightPage();
|
|
510
|
-
const locator = args.selector ? page.locator(args.selector) : page.getByText(args.text || args.name || '', { exact: args.exact === true });
|
|
511
|
-
return { text: await locator.first().innerText({ timeout: args.timeoutMs || 5_000 }), count: await locator.count() };
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
async function pressBrowser(args = {}, context) {
|
|
515
|
-
const backend = await resolveBrowserBackend();
|
|
516
|
-
if (backend === 'electron') {
|
|
517
|
-
return callElectronBridge('press', withRequestContext(args, context));
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const page = await getPlaywrightPage();
|
|
521
|
-
if (args.selector) await page.locator(args.selector).press(args.key, { timeout: args.timeoutMs || 5_000 });
|
|
522
|
-
else await page.keyboard.press(args.key);
|
|
523
|
-
return { pressed: args.key };
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
async function cuaBrowser(action, args = {}, context) {
|
|
527
|
-
const backend = await resolveBrowserBackend();
|
|
528
|
-
if (backend === 'electron') {
|
|
529
|
-
return callElectronBridge(action, withRequestContext(args, context));
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const page = await getPlaywrightPage();
|
|
533
|
-
if (action === 'cua_click') await page.mouse.click(args.x, args.y, { button: args.button === 3 ? 'right' : args.button === 2 ? 'middle' : 'left' });
|
|
534
|
-
else if (action === 'cua_double_click') await page.mouse.dblclick(args.x, args.y);
|
|
535
|
-
else if (action === 'cua_move') await page.mouse.move(args.x, args.y);
|
|
536
|
-
else if (action === 'cua_scroll') await page.mouse.wheel(args.scrollX || 0, args.scrollY || 0);
|
|
537
|
-
else if (action === 'cua_type') await page.keyboard.type(args.text || '');
|
|
538
|
-
else if (action === 'cua_keypress') await page.keyboard.press((args.keys || []).join('+'));
|
|
539
|
-
else if (action === 'cua_drag') {
|
|
540
|
-
const path = Array.isArray(args.path) ? args.path : [];
|
|
541
|
-
if (path.length < 2) throw new Error('drag path must contain at least two points');
|
|
542
|
-
await page.mouse.move(path[0].x, path[0].y);
|
|
543
|
-
await page.mouse.down();
|
|
544
|
-
for (const point of path.slice(1)) await page.mouse.move(point.x, point.y);
|
|
545
|
-
await page.mouse.up();
|
|
546
|
-
}
|
|
547
|
-
return { ok: true };
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
async function clipboardBrowser(action, args = {}, context) {
|
|
551
|
-
const backend = await resolveBrowserBackend();
|
|
552
|
-
if (backend === 'electron') {
|
|
553
|
-
return callElectronBridge(action, withRequestContext(args, context));
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const page = await getPlaywrightPage();
|
|
557
|
-
if (action === 'clipboard_read_text') {
|
|
558
|
-
return { text: await page.evaluate(() => navigator.clipboard.readText()) };
|
|
559
|
-
}
|
|
560
|
-
await page.evaluate((text) => navigator.clipboard.writeText(text), args.text || '');
|
|
561
|
-
return { written: true };
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
async function closePlaywrightBrowser() {
|
|
565
|
-
if (_browserContext) {
|
|
566
|
-
await _browserContext.close().catch(() => {});
|
|
567
|
-
_browserContext = null;
|
|
568
|
-
_browserPage = null;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
if (_browser && _browser.isConnected()) {
|
|
572
|
-
await _browser.close().catch(() => {});
|
|
573
|
-
_browser = null;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
async function startVideoBrowser({ fps, sessionId, browserSessionId, tabId } = {}, context) {
|
|
578
|
-
const backend = await resolveBrowserBackend();
|
|
579
|
-
if (backend === 'electron') {
|
|
580
|
-
return callElectronBridge('start_video', withRequestContext({ fps, recordingSessionId: sessionId, browserSessionId, tabId }, context));
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
throw new Error('Video recording is available in the Amalgm desktop app only. Open the desktop app to use the visible browser tab and local recording.');
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
async function stopVideoBrowser(argsOrContext, maybeContext) {
|
|
587
|
-
const hasArgs = maybeContext !== undefined;
|
|
588
|
-
const args = hasArgs ? (argsOrContext || {}) : {};
|
|
589
|
-
const context = hasArgs ? maybeContext : argsOrContext;
|
|
590
|
-
const backend = await resolveBrowserBackend();
|
|
591
|
-
if (backend === 'electron') {
|
|
592
|
-
return callElectronBridge('stop_video', withRequestContext(args, context));
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
throw new Error('Video recording is available in the Amalgm desktop app only. Open the desktop app to use the visible browser tab and local recording.');
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
async function closeBrowser(argsOrContext, maybeContext) {
|
|
599
|
-
const hasArgs = maybeContext !== undefined;
|
|
600
|
-
const args = hasArgs ? (argsOrContext || {}) : {};
|
|
601
|
-
const context = hasArgs ? maybeContext : argsOrContext;
|
|
602
|
-
const backend = await resolveBrowserBackend();
|
|
603
|
-
if (backend === 'electron') {
|
|
604
|
-
await callElectronBridge('close', withRequestContext(args, context));
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
await closePlaywrightBrowser();
|
|
14
|
+
const exported = {};
|
|
15
|
+
for (const key of Object.keys(agentBrowser)) {
|
|
16
|
+
exported[key] = (...args) => backend()[key](...args);
|
|
609
17
|
}
|
|
610
18
|
|
|
611
|
-
module.exports =
|
|
612
|
-
clipboardBrowser,
|
|
613
|
-
closeBrowser,
|
|
614
|
-
clickBrowser,
|
|
615
|
-
cuaBrowser,
|
|
616
|
-
evaluateBrowser,
|
|
617
|
-
getTextBrowser,
|
|
618
|
-
locatorCountBrowser,
|
|
619
|
-
locatorTextBrowser,
|
|
620
|
-
navigateBrowser,
|
|
621
|
-
pressBrowser,
|
|
622
|
-
screenshotBrowser,
|
|
623
|
-
snapshotBrowser,
|
|
624
|
-
startVideoBrowser,
|
|
625
|
-
stateBrowser,
|
|
626
|
-
stopVideoBrowser,
|
|
627
|
-
tabNavigationBrowser,
|
|
628
|
-
tabsGetBrowser,
|
|
629
|
-
tabsListBrowser,
|
|
630
|
-
tabsNewBrowser,
|
|
631
|
-
tabsSelectBrowser,
|
|
632
|
-
tabsSelectedBrowser,
|
|
633
|
-
typeBrowser,
|
|
634
|
-
waitForLoadStateBrowser,
|
|
635
|
-
waitForSelectorBrowser,
|
|
636
|
-
waitForURLBrowser,
|
|
637
|
-
};
|
|
19
|
+
module.exports = exported;
|