amalgm 0.1.89 → 0.1.90

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.
@@ -8,6 +8,10 @@
8
8
  * circular dependency on the MCP server.
9
9
  */
10
10
 
11
+ function withCapability(tools, capability) {
12
+ return tools.map((tool) => ({ ...tool, capability }));
13
+ }
14
+
11
15
  function group(id, name, description, tools, extra = {}) {
12
16
  return {
13
17
  id,
@@ -58,37 +62,21 @@ const CORE_TOOL_GROUPS = [
58
62
  'Register, run, and route local-first Amalgm apps.',
59
63
  require('../apps/tools'),
60
64
  ),
61
- // The Browser family: one product primitive ("browser") plus optional
62
- // capabilities that stay separately grantable but present as Browser modes,
63
- // not peer products. `family`/`capability` flow into the toolbox catalog so
64
- // UIs can nest them under Browser.
65
+ // Browser is ONE toolbox entry. Users enable/disable it per agent as a
66
+ // single tool; fine-grained control stays available because loadouts can
67
+ // hold individual action ids (`browser.cua_click`). Each action carries a
68
+ // `capability` tag so UIs can section the card (core / computer-use /
69
+ // recording / auth) without splitting the entry.
65
70
  group(
66
71
  'browser',
67
72
  'Browser',
68
- "Drive Amalgm's browser: open pages, snapshot interactive elements, click, fill, and run any agent-browser CLI command.",
69
- require('../browser/tools'),
70
- { family: 'browser', capability: 'core' },
71
- ),
72
- group(
73
- 'browser_cua',
74
- 'Browser Computer Use',
75
- 'Browser capability: coordinate/pixel control for vision-model loops — screenshot, click and type at x/y.',
76
- require('../browser/cua-tools'),
77
- { family: 'browser', capability: 'computer-use' },
78
- ),
79
- group(
80
- 'browser_recording',
81
- 'Browser Recording',
82
- 'Browser capability: record sessions to WebM videos stored with the Amalgm project they belong to.',
83
- require('../browser/recorder-tools'),
84
- { family: 'browser', capability: 'recording' },
85
- ),
86
- group(
87
- 'browser_auth',
88
- 'Browser Auth',
89
- 'Browser infrastructure: durable profiles, encrypted auth bundles, and temporary login links.',
90
- require('../browser/auth-tools'),
91
- { family: 'browser', capability: 'auth' },
73
+ "Drive Amalgm's browser: open pages, snapshot interactive elements as @refs, click, fill, record sessions to project videos, manage durable login profiles, and run any agent-browser CLI command. Visible in the desktop split view; headless on servers.",
74
+ [
75
+ ...withCapability(require('../browser/tools'), 'core'),
76
+ ...withCapability(require('../browser/cua-tools'), 'computer-use'),
77
+ ...withCapability(require('../browser/recorder-tools'), 'recording'),
78
+ ...withCapability(require('../browser/auth-tools'), 'auth'),
79
+ ],
92
80
  ),
93
81
  ];
94
82
 
@@ -75,6 +75,10 @@ function readResource(resource, cache) {
75
75
  return require('../browser/store').listBrowserAuthBundles();
76
76
  case 'browser_login_sessions':
77
77
  return require('../browser/store').listBrowserLoginSessions();
78
+ case 'browser_surfaces':
79
+ // Surface-open requests are commands, not state: delivered via the
80
+ // event stream only, so snapshots always start empty.
81
+ return [];
78
82
  default:
79
83
  if (String(resource).startsWith('files:')) {
80
84
  return require('../fs/rest')._private.buildFilesResource(resource);
@@ -0,0 +1,133 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const test = require('node:test');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+
9
+ const engine = require('../browser/engine');
10
+
11
+ const ROUTING_ENV = [
12
+ 'AMALGM_BROWSER_BACKEND',
13
+ 'AMALGM_BROWSER_CDP_URL',
14
+ 'AMALGM_RUNTIME_STATE_DIR',
15
+ 'AMALGM_RUNTIME_LABEL',
16
+ 'AMALGM_BRANCH',
17
+ 'AMALGM_DIR',
18
+ ];
19
+
20
+ function withEnv(overrides, fn) {
21
+ const saved = {};
22
+ for (const key of ROUTING_ENV) {
23
+ saved[key] = process.env[key];
24
+ delete process.env[key];
25
+ }
26
+ for (const [key, value] of Object.entries(overrides)) {
27
+ if (value !== undefined) process.env[key] = value;
28
+ }
29
+ try {
30
+ return fn();
31
+ } finally {
32
+ for (const key of ROUTING_ENV) {
33
+ if (saved[key] === undefined) delete process.env[key];
34
+ else process.env[key] = saved[key];
35
+ }
36
+ }
37
+ }
38
+
39
+ function tempDir(name) {
40
+ return fs.mkdtempSync(path.join(os.tmpdir(), `amalgm-${name}-`));
41
+ }
42
+
43
+ function writeAdvertisement(dir, data) {
44
+ fs.mkdirSync(dir, { recursive: true });
45
+ fs.writeFileSync(
46
+ path.join(dir, 'electron-browser-bridge.json'),
47
+ `${JSON.stringify(data, null, 2)}\n`,
48
+ );
49
+ }
50
+
51
+ test('entrypoint routing: no advertisement means headless cli', () => {
52
+ withEnv({ AMALGM_DIR: tempDir('empty') }, () => {
53
+ assert.equal(engine.mode(), 'cli');
54
+ assert.equal(engine.cdpEndpoint(), null);
55
+ });
56
+ });
57
+
58
+ test('entrypoint routing: AMALGM_BROWSER_BACKEND forces a backend', () => {
59
+ withEnv({ AMALGM_BROWSER_BACKEND: 'cli', AMALGM_BROWSER_CDP_URL: 'ws://127.0.0.1:9242/x' }, () => {
60
+ assert.equal(engine.mode(), 'cli');
61
+ });
62
+ withEnv({ AMALGM_BROWSER_BACKEND: 'attached' }, () => {
63
+ assert.equal(engine.mode(), 'attached');
64
+ });
65
+ // Old override value keeps meaning "the visible desktop browser".
66
+ withEnv({ AMALGM_BROWSER_BACKEND: 'electron' }, () => {
67
+ assert.equal(engine.mode(), 'attached');
68
+ });
69
+ });
70
+
71
+ test('entrypoint routing: explicit AMALGM_BROWSER_CDP_URL attaches', () => {
72
+ withEnv({ AMALGM_BROWSER_CDP_URL: 'ws://127.0.0.1:9999/devtools/browser/abc' }, () => {
73
+ assert.equal(engine.mode(), 'attached');
74
+ // --cdp takes a port; URLs are reduced to their port.
75
+ assert.equal(engine.cdpEndpoint(), '9999');
76
+ });
77
+ });
78
+
79
+ test('entrypoint routing: fresh advertisement in the runtime state dir attaches', () => {
80
+ const stateDir = tempDir('state');
81
+ writeAdvertisement(stateDir, {
82
+ cdpUrl: 'ws://127.0.0.1:9242/devtools/browser/live',
83
+ cdpPort: 9242,
84
+ pid: process.pid,
85
+ });
86
+ withEnv({ AMALGM_RUNTIME_STATE_DIR: stateDir }, () => {
87
+ assert.equal(engine.mode(), 'attached');
88
+ assert.equal(engine.cdpEndpoint(), '9242');
89
+ });
90
+ });
91
+
92
+ test('entrypoint routing: stale advertisement (dead pid) means headless', () => {
93
+ const stateDir = tempDir('stale');
94
+ writeAdvertisement(stateDir, {
95
+ cdpUrl: 'ws://127.0.0.1:9242/devtools/browser/dead',
96
+ pid: 2 ** 30, // not a real pid
97
+ });
98
+ withEnv({ AMALGM_RUNTIME_STATE_DIR: stateDir, AMALGM_DIR: tempDir('empty2') }, () => {
99
+ assert.equal(engine.mode(), 'cli');
100
+ });
101
+ });
102
+
103
+ test('entrypoint routing: AMALGM_DIR + label derives the state dir path', () => {
104
+ const amalgmDir = tempDir('scoped');
105
+ writeAdvertisement(path.join(amalgmDir, 'runtimes', 'main'), {
106
+ cdpUrl: 'ws://127.0.0.1:9242/devtools/browser/scoped',
107
+ pid: process.pid,
108
+ });
109
+ withEnv({ AMALGM_DIR: amalgmDir, AMALGM_RUNTIME_LABEL: 'main' }, () => {
110
+ assert.equal(engine.mode(), 'attached');
111
+ assert.equal(engine.cdpEndpoint(), '9242');
112
+ });
113
+ // A different label does not see it (branch isolation).
114
+ withEnv({ AMALGM_DIR: amalgmDir, AMALGM_RUNTIME_LABEL: 'preview' }, () => {
115
+ assert.equal(engine.mode(), 'cli');
116
+ });
117
+ });
118
+
119
+ test('entrypoint routing: candidate order is state dir, derived label dir, AMALGM_DIR, home', () => {
120
+ withEnv({
121
+ AMALGM_RUNTIME_STATE_DIR: '/tmp/a-state',
122
+ AMALGM_DIR: '/tmp/a-dir',
123
+ AMALGM_RUNTIME_LABEL: 'main',
124
+ }, () => {
125
+ const candidates = engine.bridgeFileCandidates();
126
+ assert.deepEqual(candidates.slice(0, 3), [
127
+ '/tmp/a-state/electron-browser-bridge.json',
128
+ '/tmp/a-dir/runtimes/main/electron-browser-bridge.json',
129
+ '/tmp/a-dir/electron-browser-bridge.json',
130
+ ]);
131
+ assert.equal(candidates[3], path.join(os.homedir(), '.amalgm', 'electron-browser-bridge.json'));
132
+ });
133
+ });
@@ -13,9 +13,6 @@ test('core tools are grouped into first-party toolbox-style defaults', () => {
13
13
  'agents',
14
14
  'apps',
15
15
  'browser',
16
- 'browser_cua',
17
- 'browser_recording',
18
- 'browser_auth',
19
16
  ]);
20
17
  assert.equal(CORE_TOOL_GROUPS.every((group) => group.origin === 'system'), true);
21
18
  assert.equal(CORE_TOOL_GROUPS.every((group) => group.owner === 'amalgm'), true);
@@ -34,16 +31,22 @@ test('core tool grouping preserves handler definitions for MCP surface wrapping'
34
31
  test('core tool actions carry stable toolbox ids', () => {
35
32
  const browserOpen = CORE_TOOLS.find((tool) => tool.name === 'browser_open');
36
33
  const notifyUser = CORE_TOOLS.find((tool) => tool.name === 'notify_user');
34
+ const cuaClick = CORE_TOOLS.find((tool) => tool.name === 'cua_click');
35
+ const recordStart = CORE_TOOLS.find((tool) => tool.name === 'record_start');
37
36
 
38
37
  assert.equal(browserOpen.toolboxToolId, 'browser');
39
38
  assert.equal(browserOpen.toolboxActionId, 'browser.browser_open');
39
+ assert.equal(cuaClick.toolboxToolId, 'browser');
40
+ assert.equal(cuaClick.toolboxActionId, 'browser.cua_click');
41
+ assert.equal(recordStart.toolboxActionId, 'browser.record_start');
40
42
  assert.equal(notifyUser.toolboxToolId, 'notifications');
41
43
  assert.equal(notifyUser.toolboxActionId, 'notifications.notify_user');
42
44
  });
43
45
 
44
- test('browser surface stays thin and cua/recorder are separate groups', () => {
46
+ test('browser is a single toolbox entry holding every browser action', () => {
45
47
  const browser = CORE_TOOL_GROUPS.find((group) => group.id === 'browser');
46
48
  assert.deepEqual(browser.tools.map((tool) => tool.name), [
49
+ // core verbs
47
50
  'browser_open',
48
51
  'browser_snapshot',
49
52
  'browser_screenshot',
@@ -54,17 +57,69 @@ test('browser surface stays thin and cua/recorder are separate groups', () => {
54
57
  'browser_wait',
55
58
  'browser_cli',
56
59
  'browser_close',
60
+ // computer use
61
+ 'cua_screenshot',
62
+ 'cua_click',
63
+ 'cua_double_click',
64
+ 'cua_move',
65
+ 'cua_scroll',
66
+ 'cua_type',
67
+ 'cua_keypress',
68
+ 'cua_drag',
69
+ // recording
70
+ 'record_start',
71
+ 'record_stop',
72
+ 'record_list',
73
+ // auth / profiles
74
+ 'browser_profiles_list',
75
+ 'browser_auth_bundles_list',
76
+ 'browser_login_sessions_list',
77
+ 'browser_auth_link_create',
78
+ 'browser_auth_save',
79
+ 'browser_auth_load',
57
80
  ]);
58
81
 
59
- const cua = CORE_TOOL_GROUPS.find((group) => group.id === 'browser_cua');
60
- assert.equal(cua.tools.every((tool) => tool.name.startsWith('cua_')), true);
82
+ // No other browser-ish groups exist.
83
+ assert.deepEqual(
84
+ CORE_TOOL_GROUPS.filter((group) => group.id.startsWith('browser')).map((group) => group.id),
85
+ ['browser'],
86
+ );
87
+ });
88
+
89
+ test('browser actions carry capability tags so UIs can section one card', () => {
90
+ const browser = CORE_TOOL_GROUPS.find((group) => group.id === 'browser');
91
+ const byCapability = (capability) => browser.tools
92
+ .filter((tool) => tool.capability === capability)
93
+ .map((tool) => tool.name);
61
94
 
62
- const recorder = CORE_TOOL_GROUPS.find((group) => group.id === 'browser_recording');
63
- assert.deepEqual(recorder.tools.map((tool) => tool.name), ['record_start', 'record_stop', 'record_list']);
95
+ assert.equal(byCapability('core').length, 10);
96
+ assert.equal(byCapability('computer-use').every((name) => name.startsWith('cua_')), true);
97
+ assert.deepEqual(byCapability('recording'), ['record_start', 'record_stop', 'record_list']);
98
+ assert.equal(byCapability('auth').length, 6);
99
+ assert.equal(browser.tools.every((tool) => Boolean(tool.capability)), true);
64
100
  });
65
101
 
66
- test('browser capability groups carry family metadata for nested presentation', () => {
67
- const family = CORE_TOOL_GROUPS.filter((group) => group.family === 'browser');
68
- assert.deepEqual(family.map((group) => group.id), ['browser', 'browser_cua', 'browser_recording', 'browser_auth']);
69
- assert.deepEqual(family.map((group) => group.capability), ['core', 'computer-use', 'recording', 'auth']);
102
+ test('legacy capability-group loadouts still grant the collapsed actions', () => {
103
+ const { loadoutContainsToolOrAction } = require('../toolbox/loadout-context');
104
+
105
+ // Old group id grants exactly that group's actions.
106
+ const cuaLoadout = new Set(['browser_cua']);
107
+ assert.equal(loadoutContainsToolOrAction(cuaLoadout, 'browser', 'browser.cua_click'), true);
108
+ assert.equal(loadoutContainsToolOrAction(cuaLoadout, 'browser', 'browser.record_start'), false);
109
+ assert.equal(loadoutContainsToolOrAction(cuaLoadout, 'browser', 'browser.browser_open'), false);
110
+
111
+ // Old fine-grained action ids map 1:1.
112
+ const actionLoadout = new Set(['browser_recording.record_start']);
113
+ assert.equal(loadoutContainsToolOrAction(actionLoadout, 'browser', 'browser.record_start'), true);
114
+ assert.equal(loadoutContainsToolOrAction(actionLoadout, 'browser', 'browser.record_stop'), false);
115
+
116
+ // New single entry grants everything.
117
+ const browserLoadout = new Set(['browser']);
118
+ assert.equal(loadoutContainsToolOrAction(browserLoadout, 'browser', 'browser.browser_open'), true);
119
+ assert.equal(loadoutContainsToolOrAction(browserLoadout, 'browser', 'browser.cua_click'), true);
120
+ assert.equal(loadoutContainsToolOrAction(browserLoadout, 'browser', 'browser.browser_auth_save'), true);
121
+
122
+ // Legacy amalgm.browser aggregate keeps granting browser actions.
123
+ const legacyAggregate = new Set(['amalgm.browser']);
124
+ assert.equal(loadoutContainsToolOrAction(legacyAggregate, 'browser', 'browser.browser_open'), true);
70
125
  });
@@ -11,6 +11,35 @@ const FIRST_PARTY_TOOL_IDS = new Set([
11
11
  'browser',
12
12
  ]);
13
13
 
14
+ // Browser used to ship as four toolbox entries; it is one (`browser`) now.
15
+ // Loadouts saved against the old capability groups keep working: the old
16
+ // group id grants exactly the actions that group contained, and old action
17
+ // ids map 1:1 onto `browser.<action>`. Frozen sets — these never grow.
18
+ const LEGACY_BROWSER_GROUP_ACTIONS = {
19
+ browser_cua: new Set([
20
+ 'cua_screenshot', 'cua_click', 'cua_double_click', 'cua_move',
21
+ 'cua_scroll', 'cua_type', 'cua_keypress', 'cua_drag',
22
+ ]),
23
+ browser_recording: new Set(['record_start', 'record_stop', 'record_list']),
24
+ browser_auth: new Set([
25
+ 'browser_profiles_list', 'browser_auth_bundles_list',
26
+ 'browser_login_sessions_list', 'browser_auth_link_create',
27
+ 'browser_auth_save', 'browser_auth_load',
28
+ ]),
29
+ };
30
+
31
+ function legacyBrowserLoadoutHas(loadoutToolIds, actionId) {
32
+ if (!actionId.startsWith('browser.')) return false;
33
+ const action = actionId.slice('browser.'.length);
34
+ for (const [legacyId, actions] of Object.entries(LEGACY_BROWSER_GROUP_ACTIONS)) {
35
+ if (!actions.has(action)) continue;
36
+ if (loadoutToolIds.has(legacyId)) return true;
37
+ if (loadoutToolIds.has(`${legacyId}.${action}`)) return true;
38
+ if (loadoutToolIds.has(`amalgm.${legacyId}`)) return true;
39
+ }
40
+ return false;
41
+ }
42
+
14
43
  function agentConfigIdFromContext(context) {
15
44
  const metadata = context?.sessionMetadata || {};
16
45
  return (
@@ -43,6 +72,7 @@ function loadoutHas(loadoutToolIds, id) {
43
72
  if (FIRST_PARTY_TOOL_IDS.has(value) && loadoutToolIds.has(`amalgm.${value}`)) return true;
44
73
  if (value === 'notifications.notify_user' && loadoutToolIds.has('amalgm.notify_user')) return true;
45
74
  if (value === 'agents.talk_to_agent' && loadoutToolIds.has('amalgm.talk_to_agent')) return true;
75
+ if (legacyBrowserLoadoutHas(loadoutToolIds, value)) return true;
46
76
  return false;
47
77
  }
48
78
 
@@ -18,6 +18,11 @@ const LEGACY_FIRST_PARTY_TOOL_IDS = new Set([
18
18
  'amalgm.agents',
19
19
  'amalgm.apps',
20
20
  'amalgm.browser',
21
+ // Former Browser capability groups, collapsed into the single `browser`
22
+ // entry. Loadout compat lives in toolbox/loadout-context.js.
23
+ 'browser_cua',
24
+ 'browser_recording',
25
+ 'browser_auth',
21
26
  ]);
22
27
 
23
28
  function coreToolGroups() {
@@ -40,8 +45,6 @@ function systemToolForGroup(group) {
40
45
  display: {
41
46
  icon: '/assets/images/amalgm_logo.svg',
42
47
  description: group.description,
43
- ...(group.family ? { family: group.family } : {}),
44
- ...(group.capability ? { capability: group.capability } : {}),
45
48
  },
46
49
  policy: {
47
50
  system: true,
@@ -60,6 +63,7 @@ function systemActionForTool(tool, group) {
60
63
  displayName: tool.toolboxActionName,
61
64
  description: tool.description || '',
62
65
  inputSchema: tool.inputSchema || { type: 'object', properties: {} },
66
+ ...(tool.capability ? { capability: tool.capability } : {}),
63
67
  status: 'enabled',
64
68
  sourceAction: {
65
69
  handlerName: tool.name,
@@ -1,281 +0,0 @@
1
- const http = require('http');
2
- const https = require('https');
3
- const fs = require('fs');
4
- const os = require('os');
5
- const path = require('path');
6
-
7
- const DEFAULT_SESSION = `mcp-${process.pid}-default`;
8
-
9
- function amalgmDir() {
10
- return process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
11
- }
12
-
13
- function bridgeConfigFile() {
14
- return path.join(amalgmDir(), 'electron-browser-bridge.json');
15
- }
16
-
17
- function isLoopbackUrl(value) {
18
- try {
19
- const url = new URL(value);
20
- return url.protocol === 'http:' && (url.hostname === '127.0.0.1' || url.hostname === 'localhost');
21
- } catch {
22
- return false;
23
- }
24
- }
25
-
26
- function pidIsRunning(pid) {
27
- if (!Number.isInteger(pid) || pid <= 0) return false;
28
- try {
29
- process.kill(pid, 0);
30
- return true;
31
- } catch (err) {
32
- return err && err.code === 'EPERM';
33
- }
34
- }
35
-
36
- function normalizeConfig(data) {
37
- const baseUrl = typeof data?.baseUrl === 'string' ? data.baseUrl : '';
38
- const token = typeof data?.token === 'string' ? data.token : '';
39
- if (!baseUrl || !token || !isLoopbackUrl(baseUrl)) return null;
40
- if (typeof data?.pid === 'number' && !pidIsRunning(data.pid)) return null;
41
- return { baseUrl, token };
42
- }
43
-
44
- function fileConfig() {
45
- try {
46
- return normalizeConfig(JSON.parse(fs.readFileSync(bridgeConfigFile(), 'utf8')));
47
- } catch {
48
- return null;
49
- }
50
- }
51
-
52
- function config() {
53
- const baseUrl = process.env.AMALGM_ELECTRON_BROWSER_URL;
54
- const token = process.env.AMALGM_ELECTRON_BROWSER_TOKEN;
55
- return normalizeConfig({ baseUrl, token }) || fileConfig();
56
- }
57
-
58
- function isConfigured() {
59
- return Boolean(config());
60
- }
61
-
62
- function requestJson(url, { method = 'GET', headers = {}, body } = {}) {
63
- return new Promise((resolve, reject) => {
64
- const target = new URL(url);
65
- const transport = target.protocol === 'https:' ? https : http;
66
- const payload = body ? JSON.stringify(body) : null;
67
- const req = transport.request(target, {
68
- method,
69
- headers: {
70
- Accept: 'application/json',
71
- ...(payload ? {
72
- 'Content-Type': 'application/json',
73
- 'Content-Length': String(Buffer.byteLength(payload)),
74
- } : {}),
75
- ...headers,
76
- },
77
- }, (res) => {
78
- let raw = '';
79
- res.on('data', (chunk) => { raw += chunk.toString(); });
80
- res.on('end', () => {
81
- let parsed = {};
82
- if (raw.trim()) {
83
- try {
84
- parsed = JSON.parse(raw);
85
- } catch (err) {
86
- reject(new Error(`Browser bridge returned invalid JSON: ${err.message}`));
87
- return;
88
- }
89
- }
90
-
91
- if (res.statusCode && res.statusCode >= 400) {
92
- const error = new Error(parsed?.error || `Browser bridge request failed with status ${res.statusCode}`);
93
- error.statusCode = res.statusCode;
94
- reject(error);
95
- return;
96
- }
97
-
98
- resolve(parsed);
99
- });
100
- });
101
-
102
- req.on('error', reject);
103
- req.setTimeout(120_000, () => {
104
- req.destroy(new Error('Browser bridge request timed out'));
105
- });
106
- if (payload) req.write(payload);
107
- req.end();
108
- });
109
- }
110
-
111
- function withContext(body = {}, context = {}) {
112
- const next = {
113
- ...body,
114
- ...(context?.callerSessionId ? { callerSessionId: context.callerSessionId } : {}),
115
- };
116
- if (!next.browserSessionId && next.browserProfileId) {
117
- next.browserSessionId = next.browserProfileId;
118
- }
119
- if (!next.tabId && !next.sessionId && !next.browserSessionId) {
120
- next.browserSessionId = context?.callerSessionId || DEFAULT_SESSION;
121
- }
122
- return next;
123
- }
124
-
125
- async function call(action, body = {}, context) {
126
- const bridge = config();
127
- if (!bridge) throw new Error('Electron browser bridge is not configured');
128
- return requestJson(`${bridge.baseUrl}/browser/${action}`, {
129
- method: 'POST',
130
- headers: { 'x-amalgm-browser-token': bridge.token },
131
- body: withContext(body, context),
132
- });
133
- }
134
-
135
- async function navigateBrowser(args = {}, context) {
136
- return call('navigate', args, context);
137
- }
138
-
139
- async function screenshotBrowser(args = {}, context) {
140
- return call('screenshot', args, context);
141
- }
142
-
143
- async function snapshotBrowser(args = {}, context) {
144
- return call('snapshot', args, context);
145
- }
146
-
147
- async function clickBrowser(args = {}, context) {
148
- return call('click', args, context);
149
- }
150
-
151
- async function typeBrowser(args = {}, context) {
152
- return call('type', args, context);
153
- }
154
-
155
- async function pressBrowser(args = {}, context) {
156
- return call('press', args, context);
157
- }
158
-
159
- async function getTextBrowser(args = {}, context) {
160
- return call('get_text', args, context);
161
- }
162
-
163
- async function evaluateBrowser(args = {}, context) {
164
- return call('evaluate', args, context);
165
- }
166
-
167
- async function stateBrowser(args = {}, context) {
168
- return call('state', args, context);
169
- }
170
-
171
- async function saveStateBrowser(args = {}, context) {
172
- return call('state_save', args, context);
173
- }
174
-
175
- async function loadStateBrowser(args = {}, context) {
176
- return call('state_load', args, context);
177
- }
178
-
179
- async function locatorCountBrowser(args = {}, context) {
180
- return call('locator_count', args, context);
181
- }
182
-
183
- async function locatorTextBrowser(args = {}, context) {
184
- return call('locator_text', args, context);
185
- }
186
-
187
- async function waitForLoadStateBrowser(args = {}, context) {
188
- return call('wait_for_load_state', args, context);
189
- }
190
-
191
- async function waitForURLBrowser(args = {}, context) {
192
- return call('wait_for_url', args, context);
193
- }
194
-
195
- async function waitForSelectorBrowser(args = {}, context) {
196
- return call('wait_for_selector', args, context);
197
- }
198
-
199
- async function tabNavigationBrowser(action, args = {}, context) {
200
- return call(action, args, context);
201
- }
202
-
203
- async function tabsListBrowser(context) {
204
- return call('tabs_list', {}, context);
205
- }
206
-
207
- async function tabsSelectedBrowser(context) {
208
- return call('tabs_selected', {}, context);
209
- }
210
-
211
- async function tabsNewBrowser(args = {}, context) {
212
- const sessionId = args.browserSessionId || args.sessionId || `browser-${Date.now().toString(36)}`;
213
- return call('tabs_new', {
214
- ...args,
215
- browserSessionId: sessionId,
216
- sessionId,
217
- newTab: true,
218
- }, context);
219
- }
220
-
221
- async function tabsGetBrowser(args = {}, context) {
222
- return call('tabs_get', args, context);
223
- }
224
-
225
- async function tabsSelectBrowser(args = {}, context) {
226
- return call('tabs_select', args, context);
227
- }
228
-
229
- async function cuaBrowser(action, args = {}, context) {
230
- return call(action, args, context);
231
- }
232
-
233
- async function clipboardBrowser(action, args = {}, context) {
234
- return call(action, args, context);
235
- }
236
-
237
- async function startVideoBrowser(args = {}, context) {
238
- return call('start_video', {
239
- ...args,
240
- recordingSessionId: args.sessionId || args.recording_id || args.session_id,
241
- }, context);
242
- }
243
-
244
- async function stopVideoBrowser(args = {}, context) {
245
- return call('stop_video', args, context);
246
- }
247
-
248
- async function closeBrowser(args = {}, context) {
249
- return call('close', args, context);
250
- }
251
-
252
- module.exports = {
253
- clipboardBrowser,
254
- closeBrowser,
255
- clickBrowser,
256
- cuaBrowser,
257
- evaluateBrowser,
258
- getTextBrowser,
259
- isConfigured,
260
- locatorCountBrowser,
261
- locatorTextBrowser,
262
- loadStateBrowser,
263
- navigateBrowser,
264
- pressBrowser,
265
- screenshotBrowser,
266
- saveStateBrowser,
267
- snapshotBrowser,
268
- startVideoBrowser,
269
- stateBrowser,
270
- stopVideoBrowser,
271
- tabNavigationBrowser,
272
- tabsGetBrowser,
273
- tabsListBrowser,
274
- tabsNewBrowser,
275
- tabsSelectBrowser,
276
- tabsSelectedBrowser,
277
- typeBrowser,
278
- waitForLoadStateBrowser,
279
- waitForSelectorBrowser,
280
- waitForURLBrowser,
281
- };