amalgm 0.1.88 → 0.1.89

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.
Files changed (45) hide show
  1. package/lib/cli.js +16 -6
  2. package/lib/service.js +19 -0
  3. package/lib/supervisor.js +51 -0
  4. package/package.json +1 -1
  5. package/runtime/scripts/amalgm-mcp/apps/supervisor.js +0 -25
  6. package/runtime/scripts/amalgm-mcp/browser/auth-tools.js +179 -0
  7. package/runtime/scripts/amalgm-mcp/browser/cli.js +217 -0
  8. package/runtime/scripts/amalgm-mcp/browser/cua-tools.js +147 -0
  9. package/runtime/scripts/amalgm-mcp/browser/engine.js +454 -0
  10. package/runtime/scripts/amalgm-mcp/browser/recorder-tools.js +82 -0
  11. package/runtime/scripts/amalgm-mcp/browser/recorder.js +199 -0
  12. package/runtime/scripts/amalgm-mcp/browser/rest.js +5 -1
  13. package/runtime/scripts/amalgm-mcp/browser/tools.js +104 -731
  14. package/runtime/scripts/amalgm-mcp/fs/rest.js +5 -6
  15. package/runtime/scripts/amalgm-mcp/index.js +2 -0
  16. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +32 -66
  17. package/runtime/scripts/amalgm-mcp/project-context/paths.js +266 -0
  18. package/runtime/scripts/amalgm-mcp/project-context/prompt.js +232 -0
  19. package/runtime/scripts/amalgm-mcp/project-context/rest.js +100 -0
  20. package/runtime/scripts/amalgm-mcp/project-context/store.js +671 -0
  21. package/runtime/scripts/amalgm-mcp/project-context/tools.js +146 -0
  22. package/runtime/scripts/amalgm-mcp/server/core-tools.js +35 -2
  23. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  24. package/runtime/scripts/amalgm-mcp/server/routes/project-context.js +33 -0
  25. package/runtime/scripts/amalgm-mcp/state/db.js +11 -0
  26. package/runtime/scripts/amalgm-mcp/state/snapshot.js +3 -3
  27. package/runtime/scripts/amalgm-mcp/tests/core-tools.test.js +37 -4
  28. package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +328 -0
  29. package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +9 -9
  30. package/runtime/scripts/amalgm-mcp/tests/toolbox-service.test.js +2 -2
  31. package/runtime/scripts/amalgm-mcp/toolbox/loadout-context.js +1 -0
  32. package/runtime/scripts/amalgm-mcp/toolbox/store.js +0 -15
  33. package/runtime/scripts/amalgm-mcp/toolbox/system-catalog.js +2 -0
  34. package/runtime/scripts/amalgm-mcp/workspace/rest.js +11 -7
  35. package/runtime/scripts/chat-core/adapters/opencode.js +1 -1
  36. package/runtime/scripts/chat-core/engine.js +16 -2
  37. package/runtime/scripts/chat-core/input.js +19 -1
  38. package/runtime/scripts/chat-core/tool-shape.js +6 -4
  39. package/runtime/scripts/chat-core/tooling/system-instructions.js +51 -0
  40. package/runtime/scripts/chat-core/tooling/system-prompt.js +34 -8
  41. package/runtime/scripts/local-gateway.js +1 -0
  42. package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +0 -569
  43. package/runtime/scripts/amalgm-mcp/browser/page.js +0 -25
  44. package/runtime/scripts/chat-core/tooling/active-memory.js +0 -574
  45. package/runtime/scripts/chat-core/tooling/passive-memory.js +0 -576
@@ -1,409 +1,92 @@
1
+ 'use strict';
2
+
1
3
  /**
2
- * Browser MCP tools. In the desktop app they drive Amalgm's visible
3
- * Electron browser surface; elsewhere they fall back to agent-browser.
4
+ * Browser tools the thin surface.
5
+ *
6
+ * Nine verbs, built on the agent-browser CLI (vercel-labs/agent-browser).
7
+ * The loop is: open → snapshot (refs) → click/fill (@ref) → re-snapshot.
8
+ * Everything else agent-browser can do (scroll, hover, cookies, network
9
+ * mocking, traces, diffs, React inspection, ...) is reachable through
10
+ * browser_cli without widening this schema.
11
+ *
12
+ * In the Amalgm desktop app these drive the visible in-tab splitscreen
13
+ * browser; elsewhere they drive a standalone chromium.
4
14
  */
5
15
 
6
16
  const { textResult, errorResult } = require('../lib/tool-result');
7
- const {
8
- clipboardBrowser,
9
- closeBrowser,
10
- clickBrowser,
11
- cuaBrowser,
12
- evaluateBrowser,
13
- getTextBrowser,
14
- locatorCountBrowser,
15
- locatorTextBrowser,
16
- loadStateBrowser,
17
- navigateBrowser,
18
- pressBrowser,
19
- screenshotBrowser,
20
- saveStateBrowser,
21
- snapshotBrowser,
22
- startVideoBrowser,
23
- stateBrowser,
24
- stopVideoBrowser,
25
- tabNavigationBrowser,
26
- tabsGetBrowser,
27
- tabsListBrowser,
28
- tabsNewBrowser,
29
- tabsSelectBrowser,
30
- tabsSelectedBrowser,
31
- typeBrowser,
32
- waitForLoadStateBrowser,
33
- waitForSelectorBrowser,
34
- waitForURLBrowser,
35
- } = require('./page');
36
- const {
37
- createBrowserLoginSession,
38
- listBrowserAuthBundles,
39
- listBrowserLoginSessions,
40
- listBrowserProfiles,
41
- markBrowserAuthBundleImported,
42
- readEncryptedStateBundle,
43
- safeId,
44
- touchBrowserProfile,
45
- writeEncryptedStateBundle,
46
- } = require('./store');
17
+ const engine = require('./engine');
47
18
 
48
- const tabTargetProperties = {
49
- tabId: { type: 'string', description: 'Explicit browser tab id returned by browser_tabs_* or browser_navigate' },
50
- browserSessionId: { type: 'string', description: 'Stable browser automation session id' },
51
- browserProfileId: { type: 'string', description: 'Durable browser profile id; aliases browserSessionId for login persistence' },
19
+ const sessionProperty = {
20
+ session: {
21
+ type: 'string',
22
+ description: 'Browser session/profile id. Defaults to your chat session omit it unless you need a second browser.',
23
+ },
52
24
  };
53
25
 
54
- const locatorProperties = {
55
- ...tabTargetProperties,
56
- ref: { type: 'string', description: 'Snapshot ref such as @e1' },
57
- selector: { type: 'string', description: 'CSS selector for the target element' },
58
- text: { type: 'string', description: 'Visible text to match' },
59
- role: { type: 'string', description: 'ARIA role, e.g. button, link, textbox' },
60
- name: { type: 'string', description: 'Accessible name or visible name used with role/text matching' },
61
- label: { type: 'string', description: 'Form label text' },
62
- placeholder: { type: 'string', description: 'Input placeholder text' },
63
- testId: { type: 'string', description: 'data-testid, data-test-id, or data-test value' },
64
- exact: { type: 'boolean', description: 'Require exact text/name match' },
65
- strict: { type: 'boolean', description: 'Require the locator to resolve to exactly one visible element' },
66
- timeoutMs: { type: 'number', description: 'Maximum wait time in milliseconds' },
26
+ const targetProperty = {
27
+ target: {
28
+ type: 'string',
29
+ description: 'Element target: a snapshot ref like "@e12" (preferred — run browser_snapshot first), a CSS selector, or "text=Visible label".',
30
+ },
67
31
  };
68
32
 
69
- function jsonText(value) {
70
- return textResult(JSON.stringify(value, null, 2));
71
- }
72
-
73
- function targetLine(result) {
74
- const bits = [];
75
- if (result?.tabId) bits.push(`Tab: ${result.tabId}`);
76
- if (result?.browserSessionId) bits.push(`Session: ${result.browserSessionId}`);
77
- return bits.length ? `\n${bits.join('\n')}` : '';
78
- }
79
-
80
- function requireFiniteCoordinate(value, name) {
81
- const number = Number(value);
82
- if (!Number.isFinite(number)) throw new Error(`${name} must be a finite number`);
83
- return number;
84
- }
85
-
86
- function authDomainsFrom(inputDomains, fallbackUrl) {
87
- if (Array.isArray(inputDomains) && inputDomains.length > 0) return inputDomains;
88
- try {
89
- const hostname = new URL(fallbackUrl).hostname;
90
- return hostname ? [hostname] : [];
91
- } catch {
92
- return [];
93
- }
33
+ function line(result, extra = '') {
34
+ const bits = [extra, result?.url ? `URL: ${result.url}` : '', result?.session ? `Session: ${result.session}` : '']
35
+ .filter(Boolean);
36
+ return bits.join('\n');
94
37
  }
95
38
 
96
39
  module.exports = [
97
40
  {
98
- name: 'browser_profiles_list',
99
- description: 'List durable Amalgm browser profiles on this computer.',
100
- inputSchema: { type: 'object', properties: {} },
101
- async handler() {
102
- try {
103
- return jsonText({ profiles: listBrowserProfiles() });
104
- } catch (err) {
105
- return errorResult(`List browser profiles failed: ${err.message}`);
106
- }
107
- },
108
- },
109
- {
110
- name: 'browser_auth_bundles_list',
111
- description: 'List encrypted browser auth bundles available on this computer. Raw cookies/storage are never returned.',
112
- inputSchema: { type: 'object', properties: {} },
113
- async handler() {
114
- try {
115
- return jsonText({ bundles: listBrowserAuthBundles() });
116
- } catch (err) {
117
- return errorResult(`List browser auth bundles failed: ${err.message}`);
118
- }
119
- },
120
- },
121
- {
122
- name: 'browser_login_sessions_list',
123
- description: 'List temporary browser login links on this computer. Raw tokens and cookies are never returned.',
124
- inputSchema: { type: 'object', properties: {} },
125
- async handler() {
126
- try {
127
- return jsonText({ sessions: listBrowserLoginSessions() });
128
- } catch (err) {
129
- return errorResult(`List browser login sessions failed: ${err.message}`);
130
- }
131
- },
132
- },
133
- {
134
- name: 'browser_auth_link_create',
135
- description: 'Create a short-lived browser login link for the user. Use this when an agent needs the user to log into a site before QA or browser work.',
136
- inputSchema: {
137
- type: 'object',
138
- properties: {
139
- targetUrl: { type: 'string', description: 'The site or app URL the user should log into.' },
140
- browserProfileId: { type: 'string', description: 'Durable browser profile id that will receive the login.' },
141
- browserSessionId: { type: 'string', description: 'Alias for browserProfileId.' },
142
- name: { type: 'string', description: 'Human-readable login/bundle name.' },
143
- domains: { type: 'array', items: { type: 'string' }, description: 'Domains to save after login, e.g. app.example.com.' },
144
- transport: { type: 'string', description: 'Optional live-view transport: electron-splitview, novnc, or auto.' },
145
- liveUrl: { type: 'string', description: 'Optional noVNC/live-view URL for web/mobile login.' },
146
- appBaseUrl: { type: 'string', description: 'Optional Amalgm app base URL used to build an absolute link.' },
147
- ttlMs: { type: 'number', description: 'Optional link lifetime in milliseconds. Defaults to 15 minutes, max 1 hour.' },
148
- },
149
- required: ['targetUrl'],
150
- },
151
- async handler(args, ctx) {
152
- try {
153
- const profileId = safeId(
154
- args.browserProfileId
155
- || args.browserSessionId
156
- || ctx?.callerSessionId
157
- || `login-${Date.now().toString(36)}`,
158
- );
159
- const result = createBrowserLoginSession({
160
- ...args,
161
- browserProfileId: profileId,
162
- profileId,
163
- }, { source: 'browser-auth-link-tool' });
164
- return jsonText({
165
- session: result.session,
166
- authLink: result.authLink,
167
- authLinkPath: result.authLinkPath,
168
- message: `Open this temporary login link: ${result.authLink}`,
169
- });
170
- } catch (err) {
171
- return errorResult(`Create browser auth link failed: ${err.message}`);
172
- }
173
- },
174
- },
175
- {
176
- name: 'browser_auth_save',
177
- description: 'Save the current browser profile cookies/storage into an encrypted auth bundle for later agent use.',
178
- inputSchema: {
179
- type: 'object',
180
- properties: {
181
- browserSessionId: { type: 'string', description: 'Browser session/profile id to save from' },
182
- browserProfileId: { type: 'string', description: 'Durable browser profile id to save from' },
183
- name: { type: 'string', description: 'Human-readable bundle name' },
184
- domains: { type: 'array', items: { type: 'string' }, description: 'Optional domains to include, e.g. example.com' },
185
- },
186
- },
187
- async handler(args, ctx) {
188
- try {
189
- const profileId = safeId(args.browserProfileId || args.browserSessionId || ctx?.callerSessionId || 'default');
190
- const saved = await saveStateBrowser({ ...args, browserProfileId: profileId, browserSessionId: profileId }, ctx);
191
- const bundle = writeEncryptedStateBundle({
192
- name: args.name || saved.title || profileId,
193
- profileId,
194
- domains: authDomainsFrom(args.domains, saved.url),
195
- state: saved.state || {},
196
- });
197
- touchBrowserProfile({
198
- id: profileId,
199
- name: args.name || profileId,
200
- source: saved.backend || 'browser-auth-save',
201
- }, { source: 'browser-auth-save' });
202
- return jsonText({ bundle });
203
- } catch (err) {
204
- return errorResult(`Save browser auth bundle failed: ${err.message}`);
205
- }
206
- },
207
- },
208
- {
209
- name: 'browser_auth_load',
210
- description: 'Load an encrypted browser auth bundle into a durable browser profile so an agent can start past login.',
211
- inputSchema: {
212
- type: 'object',
213
- properties: {
214
- id: { type: 'string', description: 'Auth bundle id' },
215
- browserSessionId: { type: 'string', description: 'Target browser session/profile id' },
216
- browserProfileId: { type: 'string', description: 'Target durable browser profile id' },
217
- },
218
- required: ['id'],
219
- },
220
- async handler(args, ctx) {
221
- try {
222
- const { bundle, payload } = readEncryptedStateBundle(args.id);
223
- const profileId = safeId(args.browserProfileId || args.browserSessionId || bundle.profileId);
224
- const loaded = await loadStateBrowser({
225
- ...args,
226
- browserProfileId: profileId,
227
- browserSessionId: profileId,
228
- state: payload.state,
229
- }, ctx);
230
- const updated = markBrowserAuthBundleImported(args.id, profileId);
231
- touchBrowserProfile({
232
- id: profileId,
233
- name: profileId,
234
- source: loaded.backend || 'browser-auth-load',
235
- }, { source: 'browser-auth-load' });
236
- return jsonText({ bundle: updated, loaded });
237
- } catch (err) {
238
- return errorResult(`Load browser auth bundle failed: ${err.message}`);
239
- }
240
- },
241
- },
242
- {
243
- name: 'browser_tabs_list',
244
- description: 'List visible Amalgm browser-use tabs/surfaces.',
245
- inputSchema: { type: 'object', properties: {} },
246
- async handler(_args, ctx) {
247
- try {
248
- return jsonText(await tabsListBrowser(ctx));
249
- } catch (err) {
250
- return errorResult(`List browser tabs failed: ${err.message}`);
251
- }
252
- },
253
- },
254
- {
255
- name: 'browser_tabs_selected',
256
- description: 'Return the most recently used browser-use tab.',
257
- inputSchema: { type: 'object', properties: {} },
258
- async handler(_args, ctx) {
259
- try {
260
- return jsonText(await tabsSelectedBrowser(ctx));
261
- } catch (err) {
262
- return errorResult(`Get selected browser tab failed: ${err.message}`);
263
- }
264
- },
265
- },
266
- {
267
- name: 'browser_tabs_new',
268
- description: 'Create a new visible browser-use tab/surface, optionally navigating it to a URL.',
269
- inputSchema: {
270
- type: 'object',
271
- properties: {
272
- url: { type: 'string', description: 'Optional URL to open' },
273
- browserSessionId: { type: 'string', description: 'Optional session id; generated if omitted' },
274
- },
275
- },
276
- async handler(args, ctx) {
277
- try {
278
- return jsonText(await tabsNewBrowser(args, ctx));
279
- } catch (err) {
280
- return errorResult(`Create browser tab failed: ${err.message}`);
281
- }
282
- },
283
- },
284
- {
285
- name: 'browser_tabs_get',
286
- description: 'Get state for a browser tab by tabId or browserSessionId.',
287
- inputSchema: { type: 'object', properties: tabTargetProperties },
288
- async handler(args, ctx) {
289
- try {
290
- return jsonText(await tabsGetBrowser(args, ctx));
291
- } catch (err) {
292
- return errorResult(`Get browser tab failed: ${err.message}`);
293
- }
294
- },
295
- },
296
- {
297
- name: 'browser_tabs_select',
298
- description: 'Select/focus a browser-use tab by tabId or browserSessionId.',
299
- inputSchema: { type: 'object', properties: tabTargetProperties },
300
- async handler(args, ctx) {
301
- try {
302
- return jsonText(await tabsSelectBrowser(args, ctx));
303
- } catch (err) {
304
- return errorResult(`Select browser tab failed: ${err.message}`);
305
- }
306
- },
307
- },
308
- {
309
- name: 'browser_tabs_close',
310
- description: 'Close a browser-use tab by tabId or browserSessionId.',
311
- inputSchema: { type: 'object', properties: tabTargetProperties },
312
- async handler(args, ctx) {
313
- try {
314
- await closeBrowser(args, ctx);
315
- return textResult('Browser tab closed.');
316
- } catch (err) {
317
- return errorResult(`Close browser tab failed: ${err.message}`);
318
- }
319
- },
320
- },
321
- {
322
- name: 'browser_state',
323
- description: 'Return URL/title/loading state for the current or specified browser tab.',
324
- inputSchema: { type: 'object', properties: tabTargetProperties },
325
- async handler(args, ctx) {
326
- try {
327
- return jsonText(await stateBrowser(args, ctx));
328
- } catch (err) {
329
- return errorResult(`Browser state failed: ${err.message}`);
330
- }
331
- },
332
- },
333
- {
334
- name: 'browser_navigate',
335
- description: 'Navigate a visible browser tab to a URL. Returns tabId and browserSessionId for follow-up calls.',
41
+ name: 'browser_open',
42
+ description: 'Open a URL in the browser. In the Amalgm desktop app this appears in the in-tab splitscreen surface. Follow with browser_snapshot to see interactive elements.',
336
43
  inputSchema: {
337
44
  type: 'object',
338
- properties: {
339
- ...tabTargetProperties,
340
- url: { type: 'string', description: 'The URL to navigate to' },
341
- },
45
+ properties: { url: { type: 'string', description: 'URL to open' }, ...sessionProperty },
342
46
  required: ['url'],
343
47
  },
344
48
  async handler(args, ctx) {
345
49
  if (!args.url) return errorResult('url is required');
346
50
  try {
347
- const result = await navigateBrowser(args, ctx);
348
- return textResult(`Navigated to ${result.url || args.url}\nStatus: ${result.status}\nTitle: ${result.title || ''}${targetLine(result)}`);
51
+ const result = await engine.open(args, ctx);
52
+ return textResult(line(result, `Opened: ${result.title || args.url}`)
53
+ + '\n\nNext: browser_snapshot lists interactive elements as @refs.');
349
54
  } catch (err) {
350
- return errorResult(`Navigation failed: ${err.message}`);
55
+ return errorResult(`Open failed: ${err.message}`);
351
56
  }
352
57
  },
353
58
  },
354
59
  {
355
- name: 'browser_back',
356
- description: 'Navigate the browser tab back in history.',
357
- inputSchema: { type: 'object', properties: tabTargetProperties },
358
- async handler(args, ctx) {
359
- try {
360
- return jsonText(await tabNavigationBrowser('back', args, ctx));
361
- } catch (err) {
362
- return errorResult(`Browser back failed: ${err.message}`);
363
- }
364
- },
365
- },
366
- {
367
- name: 'browser_forward',
368
- description: 'Navigate the browser tab forward in history.',
369
- inputSchema: { type: 'object', properties: tabTargetProperties },
370
- async handler(args, ctx) {
371
- try {
372
- return jsonText(await tabNavigationBrowser('forward', args, ctx));
373
- } catch (err) {
374
- return errorResult(`Browser forward failed: ${err.message}`);
375
- }
376
- },
377
- },
378
- {
379
- name: 'browser_reload',
380
- description: 'Reload the browser tab.',
381
- inputSchema: { type: 'object', properties: tabTargetProperties },
60
+ name: 'browser_snapshot',
61
+ description: 'Accessibility snapshot of the current page with stable @refs for every interactive element. This is how you see the page; use the refs with browser_click and browser_fill.',
62
+ inputSchema: { type: 'object', properties: sessionProperty },
382
63
  async handler(args, ctx) {
383
64
  try {
384
- return jsonText(await tabNavigationBrowser('reload', args, ctx));
65
+ const result = await engine.snapshot(args, ctx);
66
+ const header = `Page: ${result.title || ''}\nURL: ${result.url || ''}\n\n`;
67
+ return textResult(header + (result.snapshotText || '(empty page)').slice(0, 12_000));
385
68
  } catch (err) {
386
- return errorResult(`Browser reload failed: ${err.message}`);
69
+ return errorResult(`Snapshot failed: ${err.message}`);
387
70
  }
388
71
  },
389
72
  },
390
73
  {
391
74
  name: 'browser_screenshot',
392
- description: 'Take a screenshot of the current or specified browser tab. Returns a base64-encoded PNG.',
75
+ description: 'Screenshot the current page (PNG). Use browser_snapshot for interaction targets; use this when you need to see pixels.',
393
76
  inputSchema: {
394
77
  type: 'object',
395
78
  properties: {
396
- ...tabTargetProperties,
397
- full_page: { type: 'boolean', description: 'Capture the full scrollable page' },
79
+ fullPage: { type: 'boolean', description: 'Capture the full scrollable page instead of the viewport' },
80
+ ...sessionProperty,
398
81
  },
399
82
  },
400
83
  async handler(args, ctx) {
401
84
  try {
402
- const capture = await screenshotBrowser({ ...args, fullPage: args.full_page || false }, ctx);
85
+ const capture = await engine.screenshot(args, ctx);
403
86
  return {
404
87
  content: [
405
88
  { type: 'image', data: capture.base64, mimeType: 'image/png' },
406
- { type: 'text', text: `Screenshot captured (${capture.bytes} bytes, ${capture.mode})${targetLine(capture)}` },
89
+ { type: 'text', text: `Screenshot (${capture.bytes} bytes, ${capture.mode || 'viewport'})` },
407
90
  ],
408
91
  };
409
92
  } catch (err) {
@@ -411,449 +94,139 @@ module.exports = [
411
94
  }
412
95
  },
413
96
  },
414
- {
415
- name: 'browser_snapshot',
416
- description: 'Get a token-efficient DOM/text snapshot of the current or specified browser tab.',
417
- inputSchema: { type: 'object', properties: tabTargetProperties },
418
- async handler(args, ctx) {
419
- try {
420
- const snapshot = await snapshotBrowser(args, ctx);
421
- let result = `Page: ${snapshot.title}\nURL: ${snapshot.url}${targetLine(snapshot)}\n\n`;
422
- result += (snapshot.snapshotText || '(empty)').slice(0, 10_000);
423
- return textResult(result);
424
- } catch (err) {
425
- return errorResult(`Snapshot failed: ${err.message}`);
426
- }
427
- },
428
- },
429
97
  {
430
98
  name: 'browser_click',
431
- description: 'Click an element by selector, text, role/name, label, placeholder, or testId.',
432
- inputSchema: { type: 'object', properties: locatorProperties },
99
+ description: 'Click an element. Target with a snapshot @ref, CSS selector, or "text=Label".',
100
+ inputSchema: {
101
+ type: 'object',
102
+ properties: { ...targetProperty, ...sessionProperty },
103
+ required: ['target'],
104
+ },
433
105
  async handler(args, ctx) {
434
- if (!args.ref && !args.selector && !args.text && !args.role && !args.label && !args.placeholder && !args.testId && !args.name) {
435
- return errorResult('A ref, selector, text, role, label, placeholder, testId, or name is required');
436
- }
106
+ if (!args.target) return errorResult('target is required (e.g. "@e3", "#submit", "text=Sign in")');
437
107
  try {
438
- const result = await clickBrowser(args, ctx);
439
- return textResult(`${result.description}${targetLine(result)}`);
108
+ const result = await engine.click(args, ctx);
109
+ return textResult(`Clicked ${args.target}\nSession: ${result.session}\n\nRe-run browser_snapshot if the page changed.`);
440
110
  } catch (err) {
441
111
  return errorResult(`Click failed: ${err.message}`);
442
112
  }
443
113
  },
444
114
  },
445
115
  {
446
- name: 'browser_type',
447
- description: 'Type text into a focused element or a target matched by selector/role/label/placeholder/testId.',
116
+ name: 'browser_fill',
117
+ description: 'Clear a field and type into it. Optionally press Enter to submit.',
448
118
  inputSchema: {
449
119
  type: 'object',
450
120
  properties: {
451
- ...locatorProperties,
452
- text: { type: 'string', description: 'The text to type' },
453
- clear: { type: 'boolean', description: 'Clear the field before typing (default: true)' },
121
+ ...targetProperty,
122
+ text: { type: 'string', description: 'Text to enter' },
123
+ submit: { type: 'boolean', description: 'Press Enter after typing' },
124
+ ...sessionProperty,
454
125
  },
455
- required: ['text'],
126
+ required: ['target', 'text'],
456
127
  },
457
128
  async handler(args, ctx) {
458
- if (typeof args.text !== 'string') return errorResult('text is required');
129
+ if (!args.target || typeof args.text !== 'string') return errorResult('target and text are required');
459
130
  try {
460
- const result = await typeBrowser(args, ctx);
461
- return textResult(`${result.description}${targetLine(result)}`);
131
+ const result = await engine.fill(args, ctx);
132
+ return textResult(`Filled ${args.target}${result.submitted ? ' and pressed Enter' : ''}\nSession: ${result.session}`);
462
133
  } catch (err) {
463
- return errorResult(`Type failed: ${err.message}`);
134
+ return errorResult(`Fill failed: ${err.message}`);
464
135
  }
465
136
  },
466
137
  },
467
138
  {
468
139
  name: 'browser_press',
469
- description: 'Press a key on a target element or the focused element.',
140
+ description: 'Press a key or combination at the focused element, e.g. "Enter", "Escape", "Tab", "Control+a".',
470
141
  inputSchema: {
471
142
  type: 'object',
472
- properties: {
473
- ...locatorProperties,
474
- key: { type: 'string', description: 'Key to press, e.g. Enter, Escape, A' },
475
- },
143
+ properties: { key: { type: 'string', description: 'Key or combination' }, ...sessionProperty },
476
144
  required: ['key'],
477
145
  },
478
146
  async handler(args, ctx) {
147
+ if (!args.key) return errorResult('key is required');
479
148
  try {
480
- return jsonText(await pressBrowser(args, ctx));
149
+ const result = await engine.press(args, ctx);
150
+ return textResult(`Pressed ${args.key}\nSession: ${result.session}`);
481
151
  } catch (err) {
482
152
  return errorResult(`Press failed: ${err.message}`);
483
153
  }
484
154
  },
485
155
  },
486
156
  {
487
- name: 'browser_get_text',
488
- description: 'Extract text content from the current page or a CSS selector.',
489
- inputSchema: {
490
- type: 'object',
491
- properties: {
492
- ...tabTargetProperties,
493
- ref: { type: 'string', description: 'Snapshot ref such as @e1' },
494
- selector: { type: 'string', description: 'CSS selector to extract (omit for full page)' },
495
- },
496
- },
497
- async handler(args, ctx) {
498
- try {
499
- let text = (await getTextBrowser(args, ctx)).text;
500
- if (text && text.length > 15_000) {
501
- text = `${text.slice(0, 15_000)}\n\n... (truncated, use a selector for specific content)`;
502
- }
503
- return textResult(text || '(empty)');
504
- } catch (err) {
505
- return errorResult(`Get text failed: ${err.message}`);
506
- }
507
- },
508
- },
509
- {
510
- name: 'browser_locator_count',
511
- description: 'Count visible elements matching a locator.',
512
- inputSchema: { type: 'object', properties: locatorProperties },
513
- async handler(args, ctx) {
514
- try {
515
- return jsonText(await locatorCountBrowser(args, ctx));
516
- } catch (err) {
517
- return errorResult(`Locator count failed: ${err.message}`);
518
- }
519
- },
520
- },
521
- {
522
- name: 'browser_locator_text',
523
- description: 'Return visible text for the first element matching a locator, plus match count.',
524
- inputSchema: { type: 'object', properties: locatorProperties },
525
- async handler(args, ctx) {
526
- try {
527
- return jsonText(await locatorTextBrowser(args, ctx));
528
- } catch (err) {
529
- return errorResult(`Locator text failed: ${err.message}`);
530
- }
531
- },
532
- },
533
- {
534
- name: 'browser_evaluate',
535
- description: 'Execute JavaScript in the browser page context.',
157
+ name: 'browser_eval',
158
+ description: 'Run JavaScript in the page and return the result. The general-purpose escape hatch for reading or mutating page state.',
536
159
  inputSchema: {
537
160
  type: 'object',
538
- properties: {
539
- ...tabTargetProperties,
540
- script: { type: 'string' },
541
- },
161
+ properties: { script: { type: 'string', description: 'JavaScript to evaluate' }, ...sessionProperty },
542
162
  required: ['script'],
543
163
  },
544
164
  async handler(args, ctx) {
545
165
  if (!args.script) return errorResult('script is required');
546
166
  try {
547
- const { result } = await evaluateBrowser(args, ctx);
548
- const output = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result);
549
- return textResult(output);
550
- } catch (err) {
551
- return errorResult(`Evaluate failed: ${err.message}`);
552
- }
553
- },
554
- },
555
- {
556
- name: 'browser_wait_for_load_state',
557
- description: 'Wait for the browser tab to reach load, domcontentloaded, or networkidle.',
558
- inputSchema: {
559
- type: 'object',
560
- properties: {
561
- ...tabTargetProperties,
562
- state: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle'] },
563
- timeoutMs: { type: 'number' },
564
- },
565
- },
566
- async handler(args, ctx) {
567
- try {
568
- return jsonText(await waitForLoadStateBrowser(args, ctx));
569
- } catch (err) {
570
- return errorResult(`Wait for load state failed: ${err.message}`);
571
- }
572
- },
573
- },
574
- {
575
- name: 'browser_wait_for_url',
576
- description: 'Wait for the browser tab URL to match a string. Supports * wildcards in Electron mode.',
577
- inputSchema: {
578
- type: 'object',
579
- properties: {
580
- ...tabTargetProperties,
581
- url: { type: 'string' },
582
- waitUntil: { type: 'string', enum: ['commit', 'load', 'domcontentloaded', 'networkidle'] },
583
- timeoutMs: { type: 'number' },
584
- },
585
- required: ['url'],
586
- },
587
- async handler(args, ctx) {
588
- try {
589
- return jsonText(await waitForURLBrowser(args, ctx));
590
- } catch (err) {
591
- return errorResult(`Wait for URL failed: ${err.message}`);
592
- }
593
- },
594
- },
595
- {
596
- name: 'browser_wait_for_selector',
597
- description: 'Wait for a CSS selector to become attached, detached, visible, or hidden.',
598
- inputSchema: {
599
- type: 'object',
600
- properties: {
601
- ...tabTargetProperties,
602
- selector: { type: 'string' },
603
- state: { type: 'string', enum: ['attached', 'detached', 'visible', 'hidden'] },
604
- timeoutMs: { type: 'number' },
605
- },
606
- required: ['selector'],
607
- },
608
- async handler(args, ctx) {
609
- try {
610
- return jsonText(await waitForSelectorBrowser(args, ctx));
611
- } catch (err) {
612
- return errorResult(`Wait for selector failed: ${err.message}`);
613
- }
614
- },
615
- },
616
- {
617
- name: 'browser_cua_click',
618
- description: 'Click viewport coordinates in the visible browser tab.',
619
- inputSchema: {
620
- type: 'object',
621
- properties: {
622
- ...tabTargetProperties,
623
- x: { type: 'number' },
624
- y: { type: 'number' },
625
- button: { type: 'number', description: '1 left, 2 middle, 3 right' },
626
- keypress: { type: 'array', items: { type: 'string' } },
627
- },
628
- required: ['x', 'y'],
629
- },
630
- async handler(args, ctx) {
631
- try {
632
- return jsonText(await cuaBrowser('cua_click', { ...args, x: requireFiniteCoordinate(args.x, 'x'), y: requireFiniteCoordinate(args.y, 'y') }, ctx));
633
- } catch (err) {
634
- return errorResult(`CUA click failed: ${err.message}`);
635
- }
636
- },
637
- },
638
- {
639
- name: 'browser_cua_double_click',
640
- description: 'Double-click viewport coordinates in the visible browser tab.',
641
- inputSchema: {
642
- type: 'object',
643
- properties: { ...tabTargetProperties, x: { type: 'number' }, y: { type: 'number' }, keypress: { type: 'array', items: { type: 'string' } } },
644
- required: ['x', 'y'],
645
- },
646
- async handler(args, ctx) {
647
- try {
648
- return jsonText(await cuaBrowser('cua_double_click', { ...args, x: requireFiniteCoordinate(args.x, 'x'), y: requireFiniteCoordinate(args.y, 'y') }, ctx));
649
- } catch (err) {
650
- return errorResult(`CUA double-click failed: ${err.message}`);
651
- }
652
- },
653
- },
654
- {
655
- name: 'browser_cua_move',
656
- description: 'Move the browser mouse to viewport coordinates.',
657
- inputSchema: {
658
- type: 'object',
659
- properties: { ...tabTargetProperties, x: { type: 'number' }, y: { type: 'number' }, keys: { type: 'array', items: { type: 'string' } } },
660
- required: ['x', 'y'],
661
- },
662
- async handler(args, ctx) {
663
- try {
664
- return jsonText(await cuaBrowser('cua_move', { ...args, x: requireFiniteCoordinate(args.x, 'x'), y: requireFiniteCoordinate(args.y, 'y') }, ctx));
167
+ const { result } = await engine.evaluate(args, ctx);
168
+ const output = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
169
+ return textResult(output.slice(0, 15_000) || '(no result)');
665
170
  } catch (err) {
666
- return errorResult(`CUA move failed: ${err.message}`);
171
+ return errorResult(`Eval failed: ${err.message}`);
667
172
  }
668
173
  },
669
174
  },
670
175
  {
671
- name: 'browser_cua_scroll',
672
- description: 'Scroll the visible browser tab from viewport coordinates.',
176
+ name: 'browser_wait',
177
+ description: 'Wait for a CSS selector to appear, a URL substring/wildcard to match, or a fixed number of milliseconds.',
673
178
  inputSchema: {
674
179
  type: 'object',
675
180
  properties: {
676
- ...tabTargetProperties,
677
- x: { type: 'number' },
678
- y: { type: 'number' },
679
- scrollX: { type: 'number' },
680
- scrollY: { type: 'number' },
681
- keypress: { type: 'array', items: { type: 'string' } },
181
+ selector: { type: 'string', description: 'CSS selector to wait for' },
182
+ url: { type: 'string', description: 'URL substring or *-wildcard to wait for' },
183
+ ms: { type: 'number', description: 'Plain delay in milliseconds (max 60000)' },
184
+ timeoutMs: { type: 'number', description: 'Timeout (default 30000)' },
185
+ ...sessionProperty,
682
186
  },
683
- required: ['x', 'y', 'scrollX', 'scrollY'],
684
- },
685
- async handler(args, ctx) {
686
- try {
687
- return jsonText(await cuaBrowser('cua_scroll', {
688
- ...args,
689
- x: requireFiniteCoordinate(args.x, 'x'),
690
- y: requireFiniteCoordinate(args.y, 'y'),
691
- scrollX: Number(args.scrollX || 0),
692
- scrollY: Number(args.scrollY || 0),
693
- }, ctx));
694
- } catch (err) {
695
- return errorResult(`CUA scroll failed: ${err.message}`);
696
- }
697
- },
698
- },
699
- {
700
- name: 'browser_cua_type',
701
- description: 'Type text at the currently focused browser element.',
702
- inputSchema: {
703
- type: 'object',
704
- properties: { ...tabTargetProperties, text: { type: 'string' } },
705
- required: ['text'],
706
- },
707
- async handler(args, ctx) {
708
- try {
709
- return jsonText(await cuaBrowser('cua_type', args, ctx));
710
- } catch (err) {
711
- return errorResult(`CUA type failed: ${err.message}`);
712
- }
713
- },
714
- },
715
- {
716
- name: 'browser_cua_keypress',
717
- description: 'Press a key combination in the visible browser tab.',
718
- inputSchema: {
719
- type: 'object',
720
- properties: { ...tabTargetProperties, keys: { type: 'array', items: { type: 'string' } } },
721
- required: ['keys'],
722
187
  },
723
188
  async handler(args, ctx) {
724
189
  try {
725
- return jsonText(await cuaBrowser('cua_keypress', args, ctx));
190
+ const result = await engine.wait(args, ctx);
191
+ return textResult(`Done: ${result.matched || result.waited}\nSession: ${result.session}`);
726
192
  } catch (err) {
727
- return errorResult(`CUA keypress failed: ${err.message}`);
193
+ return errorResult(`Wait failed: ${err.message}`);
728
194
  }
729
195
  },
730
196
  },
731
197
  {
732
- name: 'browser_cua_drag',
733
- description: 'Drag through a list of viewport points.',
198
+ name: 'browser_cli',
199
+ description: 'Run any agent-browser CLI command against this session — the full surface beyond the core verbs: ["scroll","down"], ["hover","@e3"], ["back"], ["tab"], ["get","html","@e1"], ["cookies","get"], ["network","requests"], ["find","role","button","click","--name","Submit"], and more. Run ["--help"] for the command list.',
734
200
  inputSchema: {
735
201
  type: 'object',
736
202
  properties: {
737
- ...tabTargetProperties,
738
- path: { type: 'array', items: { type: 'object', properties: { x: { type: 'number' }, y: { type: 'number' } }, required: ['x', 'y'] } },
739
- keys: { type: 'array', items: { type: 'string' } },
203
+ args: { type: 'array', items: { type: 'string' }, description: 'agent-browser argv, e.g. ["scroll", "down", "400"]' },
204
+ timeoutMs: { type: 'number', description: 'Timeout (default 60000)' },
205
+ ...sessionProperty,
740
206
  },
741
- required: ['path'],
742
- },
743
- async handler(args, ctx) {
744
- try {
745
- return jsonText(await cuaBrowser('cua_drag', args, ctx));
746
- } catch (err) {
747
- return errorResult(`CUA drag failed: ${err.message}`);
748
- }
207
+ required: ['args'],
749
208
  },
750
- },
751
- {
752
- name: 'browser_cua_screenshot',
753
- description: 'Capture the visible browser tab viewport.',
754
- inputSchema: { type: 'object', properties: tabTargetProperties },
755
209
  async handler(args, ctx) {
756
210
  try {
757
- const capture = await screenshotBrowser({ ...args, fullPage: false }, ctx);
758
- return {
759
- content: [
760
- { type: 'image', data: capture.base64, mimeType: 'image/png' },
761
- { type: 'text', text: `Visible browser screenshot captured (${capture.bytes} bytes)${targetLine(capture)}` },
762
- ],
763
- };
211
+ const { result } = await engine.passthrough(args, ctx);
212
+ const output = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result ?? '');
213
+ return textResult(output.slice(0, 15_000) || 'ok');
764
214
  } catch (err) {
765
- return errorResult(`CUA screenshot failed: ${err.message}`);
215
+ return errorResult(`browser_cli failed: ${err.message}`);
766
216
  }
767
217
  },
768
218
  },
769
219
  {
770
- name: 'browser_clipboard_read_text',
771
- description: 'Read plain text from the browser page clipboard API.',
772
- inputSchema: { type: 'object', properties: tabTargetProperties },
773
- async handler(args, ctx) {
774
- try {
775
- return jsonText(await clipboardBrowser('clipboard_read_text', args, ctx));
776
- } catch (err) {
777
- return errorResult(`Clipboard read failed: ${err.message}`);
778
- }
779
- },
780
- },
781
- {
782
- name: 'browser_clipboard_write_text',
783
- description: 'Write plain text through the browser page clipboard API.',
784
- inputSchema: {
785
- type: 'object',
786
- properties: { ...tabTargetProperties, text: { type: 'string' } },
787
- required: ['text'],
788
- },
789
- async handler(args, ctx) {
790
- try {
791
- return jsonText(await clipboardBrowser('clipboard_write_text', args, ctx));
792
- } catch (err) {
793
- return errorResult(`Clipboard write failed: ${err.message}`);
794
- }
795
- },
796
- },
797
- {
798
- name: 'browser_start_video',
799
- description: 'Start recording the browser session to a local WebM video. Saved when browser_stop_video is called.',
800
- inputSchema: {
801
- type: 'object',
802
- properties: {
803
- ...tabTargetProperties,
804
- fps: { type: 'number', description: 'Recording frames per second (default: 12, max: 30)' },
805
- session_id: { type: 'string', description: 'Deprecated alias for recording_id. Optional recording folder name under ~/.amalgm/browser-sessions' },
806
- recording_id: { type: 'string', description: 'Optional recording folder name under ~/.amalgm/browser-sessions' },
807
- },
808
- },
220
+ name: 'browser_close',
221
+ description: 'Close the browser session. A fresh one launches on the next browser call.',
222
+ inputSchema: { type: 'object', properties: sessionProperty },
809
223
  async handler(args, ctx) {
810
224
  try {
811
- const result = await startVideoBrowser({
812
- ...args,
813
- fps: args.fps,
814
- sessionId: args.recording_id || args.session_id,
815
- }, ctx);
816
- return textResult(`Browser video recording started (${result.fps || 'default'} fps).${targetLine(result)}`);
225
+ await engine.close(args, ctx);
226
+ return textResult('Browser closed.');
817
227
  } catch (err) {
818
- return errorResult(`Start video failed: ${err.message}`);
228
+ return errorResult(`Close failed: ${err.message}`);
819
229
  }
820
230
  },
821
231
  },
822
- {
823
- name: 'browser_stop_video',
824
- description: 'Stop browser video recording and save it under ~/.amalgm/browser-sessions/.',
825
- inputSchema: { type: 'object', properties: tabTargetProperties },
826
- async handler(args, ctx) {
827
- try {
828
- const result = await stopVideoBrowser(args, ctx);
829
- if (!result.path) return textResult(`Browser video recording stopped.${targetLine(result)}`);
830
- const urlLine = result.url ? `\nOpen: ${result.url}` : '';
831
- return {
832
- content: [
833
- { type: 'text', text: `Browser video saved: ${result.path}\nBytes: ${result.bytes || 0}${urlLine}${targetLine(result)}` },
834
- ],
835
- metadata: {
836
- browserVideo: result.browserVideo || {
837
- path: result.path,
838
- relativePath: result.relativePath,
839
- url: result.url,
840
- bytes: result.bytes,
841
- mimeType: result.mimeType || 'video/webm',
842
- },
843
- },
844
- };
845
- } catch (err) {
846
- return errorResult(`Stop video failed: ${err.message}`);
847
- }
848
- },
849
- },
850
- {
851
- name: 'browser_close',
852
- description: 'Close the current or specified browser tab/surface.',
853
- inputSchema: { type: 'object', properties: tabTargetProperties },
854
- async handler(args, ctx) {
855
- await closeBrowser(args, ctx);
856
- return textResult('Browser closed. A new instance will launch on the next browser_* call.');
857
- },
858
- },
859
232
  ];