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.
- package/lib/cli.js +16 -6
- package/lib/service.js +19 -0
- package/lib/supervisor.js +51 -0
- package/package.json +1 -1
- package/runtime/scripts/amalgm-mcp/apps/supervisor.js +0 -25
- package/runtime/scripts/amalgm-mcp/browser/auth-tools.js +179 -0
- package/runtime/scripts/amalgm-mcp/browser/cli.js +217 -0
- package/runtime/scripts/amalgm-mcp/browser/cua-tools.js +147 -0
- package/runtime/scripts/amalgm-mcp/browser/engine.js +454 -0
- package/runtime/scripts/amalgm-mcp/browser/recorder-tools.js +82 -0
- package/runtime/scripts/amalgm-mcp/browser/recorder.js +199 -0
- package/runtime/scripts/amalgm-mcp/browser/rest.js +5 -1
- package/runtime/scripts/amalgm-mcp/browser/tools.js +104 -731
- package/runtime/scripts/amalgm-mcp/fs/rest.js +5 -6
- package/runtime/scripts/amalgm-mcp/index.js +2 -0
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +32 -66
- package/runtime/scripts/amalgm-mcp/project-context/paths.js +266 -0
- package/runtime/scripts/amalgm-mcp/project-context/prompt.js +232 -0
- package/runtime/scripts/amalgm-mcp/project-context/rest.js +100 -0
- package/runtime/scripts/amalgm-mcp/project-context/store.js +671 -0
- package/runtime/scripts/amalgm-mcp/project-context/tools.js +146 -0
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +35 -2
- package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
- package/runtime/scripts/amalgm-mcp/server/routes/project-context.js +33 -0
- package/runtime/scripts/amalgm-mcp/state/db.js +11 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +3 -3
- package/runtime/scripts/amalgm-mcp/tests/core-tools.test.js +37 -4
- package/runtime/scripts/amalgm-mcp/tests/project-context.test.js +328 -0
- package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +9 -9
- package/runtime/scripts/amalgm-mcp/tests/toolbox-service.test.js +2 -2
- package/runtime/scripts/amalgm-mcp/toolbox/loadout-context.js +1 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +0 -15
- package/runtime/scripts/amalgm-mcp/toolbox/system-catalog.js +2 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +11 -7
- package/runtime/scripts/chat-core/adapters/opencode.js +1 -1
- package/runtime/scripts/chat-core/engine.js +16 -2
- package/runtime/scripts/chat-core/input.js +19 -1
- package/runtime/scripts/chat-core/tool-shape.js +6 -4
- package/runtime/scripts/chat-core/tooling/system-instructions.js +51 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +34 -8
- package/runtime/scripts/local-gateway.js +1 -0
- package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +0 -569
- package/runtime/scripts/amalgm-mcp/browser/page.js +0 -25
- package/runtime/scripts/chat-core/tooling/active-memory.js +0 -574
- package/runtime/scripts/chat-core/tooling/passive-memory.js +0 -576
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { PLATFORM_CONTEXT } = require('../../chat-server/config');
|
|
4
|
-
const { activeMemoryContextBlock, instructionsContextBlock } = require('./active-memory');
|
|
5
|
-
const { passiveMemoryContextBlock } = require('./passive-memory');
|
|
6
4
|
const { renderAgentInstructions } = require('../../amalgm-mcp/agent-config/renderers/instructions');
|
|
7
5
|
|
|
8
6
|
function trimBlock(value) {
|
|
9
7
|
return String(value || '').trim();
|
|
10
8
|
}
|
|
11
9
|
|
|
10
|
+
// Fresh provider threads carry the full project context in the system prompt;
|
|
11
|
+
// resumed threads receive per-turn <project_context_update> blocks instead
|
|
12
|
+
// (see input.js). The block gates itself on contract.providerSessionId, and a
|
|
13
|
+
// failure here must never break prompt composition.
|
|
14
|
+
function projectContextBlock(contract) {
|
|
15
|
+
try {
|
|
16
|
+
return require('../../amalgm-mcp/project-context/prompt').projectContextPromptBlock(contract);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
const message = error instanceof Error ? error.message.split('\n')[0] : String(error);
|
|
19
|
+
console.warn('[chat-core] project context block failed:', message);
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function systemInstructionsBlockSafe() {
|
|
25
|
+
try {
|
|
26
|
+
return require('./system-instructions').systemInstructionsBlock();
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const message = error instanceof Error ? error.message.split('\n')[0] : String(error);
|
|
29
|
+
console.warn('[chat-core] system instructions block failed:', message);
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Definitive memory model: platform context, system instructions
|
|
36
|
+
* (machine-wide, user-authored), project context (instructions + project
|
|
37
|
+
* state + recent change log, maintained through the memories tool), and the
|
|
38
|
+
* agent's own instructions. The legacy active/passive memory blocks are gone.
|
|
39
|
+
*/
|
|
12
40
|
function composeSystemPrompt(contract) {
|
|
13
41
|
const parts = [];
|
|
14
42
|
const platform = trimBlock(PLATFORM_CONTEXT);
|
|
@@ -16,13 +44,11 @@ function composeSystemPrompt(contract) {
|
|
|
16
44
|
trimBlock(contract?.compiledAgentInstructions)
|
|
17
45
|
|| trimBlock(renderAgentInstructions(contract?.agentConfig, { includeAgentToAgentPrompt: false }));
|
|
18
46
|
const custom = compiledAgentInstructions || trimBlock(contract?.systemPrompt);
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const passive = contract?.providerSessionId ? '' : trimBlock(passiveMemoryContextBlock());
|
|
47
|
+
const systemInstructions = contract?.providerSessionId ? '' : trimBlock(systemInstructionsBlockSafe());
|
|
48
|
+
const projectContext = trimBlock(projectContextBlock(contract));
|
|
22
49
|
if (platform) parts.push(platform);
|
|
23
|
-
if (
|
|
24
|
-
if (
|
|
25
|
-
if (passive) parts.push(passive);
|
|
50
|
+
if (systemInstructions) parts.push(systemInstructions);
|
|
51
|
+
if (projectContext) parts.push(projectContext);
|
|
26
52
|
if (custom) parts.push(custom);
|
|
27
53
|
return parts.join('\n\n');
|
|
28
54
|
}
|
|
@@ -1,569 +0,0 @@
|
|
|
1
|
-
const crypto = require('crypto');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const os = require('os');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const { spawn } = require('child_process');
|
|
6
|
-
|
|
7
|
-
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
8
|
-
const DEFAULT_SESSION = 'default';
|
|
9
|
-
const activeRecordings = new Map();
|
|
10
|
-
const knownSessions = new Map();
|
|
11
|
-
|
|
12
|
-
function amalgmDir() {
|
|
13
|
-
return process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function ensureDir(dir) {
|
|
17
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
18
|
-
try { fs.chmodSync(dir, 0o700); } catch {}
|
|
19
|
-
return dir;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function safeId(value, fallback = DEFAULT_SESSION) {
|
|
23
|
-
const raw = String(value || fallback).replace(/^agent-browser:/, '');
|
|
24
|
-
return raw.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 96) || fallback;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function browserRoot() {
|
|
28
|
-
return ensureDir(path.join(amalgmDir(), 'browser'));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function profileDir(session) {
|
|
32
|
-
return ensureDir(path.join(browserRoot(), 'profiles', session));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function screenshotDir() {
|
|
36
|
-
return ensureDir(path.join(browserRoot(), 'screenshots'));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function recordingDir(session) {
|
|
40
|
-
return ensureDir(path.join(amalgmDir(), 'browser-sessions', session));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function browserSession(args = {}, context = {}) {
|
|
44
|
-
return safeId(
|
|
45
|
-
args.browserProfileId
|
|
46
|
-
|| args.profileId
|
|
47
|
-
|| args.browserSessionId
|
|
48
|
-
|| args.sessionId
|
|
49
|
-
|| args.tabId
|
|
50
|
-
|| context?.callerSessionId
|
|
51
|
-
|| DEFAULT_SESSION,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function rememberBrowserProfile(session, input = {}) {
|
|
56
|
-
try {
|
|
57
|
-
require('./profiles').touchBrowserProfile({
|
|
58
|
-
id: session,
|
|
59
|
-
name: input.name || input.profileName || input.browserProfileName || session,
|
|
60
|
-
source: 'agent-browser',
|
|
61
|
-
profilePath: profileDir(session),
|
|
62
|
-
}, { source: 'agent-browser' });
|
|
63
|
-
} catch (error) {
|
|
64
|
-
if (process.env.AMALGM_DEBUG_BROWSER_PROFILE_STORE) {
|
|
65
|
-
console.warn('[agent-browser] Failed to record browser profile:', error.message);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function tabInfo(session, extra = {}) {
|
|
71
|
-
rememberBrowserProfile(session, extra);
|
|
72
|
-
const info = {
|
|
73
|
-
tabId: `agent-browser:${session}`,
|
|
74
|
-
browserSessionId: session,
|
|
75
|
-
browserProfileId: session,
|
|
76
|
-
backend: 'agent-browser',
|
|
77
|
-
...extra,
|
|
78
|
-
};
|
|
79
|
-
knownSessions.set(session, { ...knownSessions.get(session), ...info, lastActiveAt: Date.now() });
|
|
80
|
-
return info;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function agentBrowserCommand() {
|
|
84
|
-
if (process.env.AMALGM_AGENT_BROWSER_BIN) {
|
|
85
|
-
return { command: process.env.AMALGM_AGENT_BROWSER_BIN, prefix: [] };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
return {
|
|
90
|
-
command: process.execPath,
|
|
91
|
-
prefix: [require.resolve('agent-browser/bin/agent-browser.js')],
|
|
92
|
-
};
|
|
93
|
-
} catch {
|
|
94
|
-
return { command: 'agent-browser', prefix: [] };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function wantsHeaded() {
|
|
99
|
-
const explicit = process.env.AMALGM_BROWSER_HEADED || process.env.AGENT_BROWSER_HEADED;
|
|
100
|
-
if (explicit) return !/^(0|false|no)$/i.test(explicit);
|
|
101
|
-
if (/^(1|true|yes)$/i.test(process.env.AMALGM_BROWSER_HEADLESS || '')) return false;
|
|
102
|
-
if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function baseArgs(session, json = true) {
|
|
107
|
-
const args = [
|
|
108
|
-
'--session', session,
|
|
109
|
-
'--profile', profileDir(session),
|
|
110
|
-
'--screenshot-dir', screenshotDir(),
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
if (json) args.push('--json');
|
|
114
|
-
if (wantsHeaded()) args.push('--headed');
|
|
115
|
-
if (process.env.AMALGM_BROWSER_EXECUTABLE_PATH) {
|
|
116
|
-
args.push('--executable-path', process.env.AMALGM_BROWSER_EXECUTABLE_PATH);
|
|
117
|
-
}
|
|
118
|
-
if (process.env.AMALGM_BROWSER_PROVIDER) {
|
|
119
|
-
args.push('--provider', process.env.AMALGM_BROWSER_PROVIDER);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return args;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function parseJson(stdout) {
|
|
126
|
-
const text = String(stdout || '').trim();
|
|
127
|
-
if (!text) return {};
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
return JSON.parse(text);
|
|
131
|
-
} catch {
|
|
132
|
-
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
133
|
-
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
134
|
-
if (!/^[{[]/.test(lines[i])) continue;
|
|
135
|
-
try {
|
|
136
|
-
return JSON.parse(lines.slice(i).join('\n'));
|
|
137
|
-
} catch {
|
|
138
|
-
// Keep looking for a JSON suffix.
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return { text };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function asText(value) {
|
|
147
|
-
if (value == null) return '';
|
|
148
|
-
if (typeof value === 'string') return value;
|
|
149
|
-
if (value.success === true && value.data !== undefined) return asText(value.data);
|
|
150
|
-
if (typeof value.snapshot === 'string') return value.snapshot;
|
|
151
|
-
if (typeof value.text === 'string') return value.text;
|
|
152
|
-
if (typeof value.value === 'string') return value.value;
|
|
153
|
-
if (typeof value.result === 'string') return value.result;
|
|
154
|
-
if (typeof value.output === 'string') return value.output;
|
|
155
|
-
if (typeof value.url === 'string') return value.url;
|
|
156
|
-
if (typeof value.title === 'string') return value.title;
|
|
157
|
-
return JSON.stringify(value, null, 2);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function runAgentBrowser(commandArgs, options = {}) {
|
|
161
|
-
const session = safeId(options.session);
|
|
162
|
-
const json = options.json !== false;
|
|
163
|
-
const timeoutMs = options.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
164
|
-
const bin = agentBrowserCommand();
|
|
165
|
-
const args = [...bin.prefix, ...baseArgs(session, json), ...commandArgs.map(String)];
|
|
166
|
-
|
|
167
|
-
return new Promise((resolve, reject) => {
|
|
168
|
-
const child = spawn(bin.command, args, {
|
|
169
|
-
cwd: process.cwd(),
|
|
170
|
-
env: {
|
|
171
|
-
...process.env,
|
|
172
|
-
AGENT_BROWSER_SESSION: session,
|
|
173
|
-
AGENT_BROWSER_PROFILE: profileDir(session),
|
|
174
|
-
},
|
|
175
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
let stdout = '';
|
|
179
|
-
let stderr = '';
|
|
180
|
-
const timer = setTimeout(() => {
|
|
181
|
-
child.kill('SIGTERM');
|
|
182
|
-
reject(new Error(`agent-browser timed out running: ${commandArgs.join(' ')}`));
|
|
183
|
-
}, timeoutMs);
|
|
184
|
-
|
|
185
|
-
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
186
|
-
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
187
|
-
child.on('error', (err) => {
|
|
188
|
-
clearTimeout(timer);
|
|
189
|
-
reject(err.code === 'ENOENT'
|
|
190
|
-
? new Error('agent-browser is not installed. Reinstall Amalgm or run `npm install agent-browser`.')
|
|
191
|
-
: err);
|
|
192
|
-
});
|
|
193
|
-
child.on('close', (code) => {
|
|
194
|
-
clearTimeout(timer);
|
|
195
|
-
if (code !== 0) {
|
|
196
|
-
reject(new Error((stderr || stdout || `agent-browser exited with code ${code}`).trim()));
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
const result = json ? parseJson(stdout) : { text: stdout.trim() };
|
|
200
|
-
if (result && result.success === false) {
|
|
201
|
-
reject(new Error(result.error || result.message || 'agent-browser command failed'));
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
resolve(result);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function directTarget(args = {}) {
|
|
210
|
-
if (args.ref) return String(args.ref);
|
|
211
|
-
if (args.selector) return String(args.selector);
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function findCommand(args = {}, action, value) {
|
|
216
|
-
const command = ['find'];
|
|
217
|
-
if (args.text || args.name) command.push('text', args.text || args.name);
|
|
218
|
-
else if (args.label) command.push('label', args.label);
|
|
219
|
-
else if (args.placeholder) command.push('placeholder', args.placeholder);
|
|
220
|
-
else if (args.testId) command.push('testid', args.testId);
|
|
221
|
-
else if (args.role) command.push('role', args.role);
|
|
222
|
-
else throw new Error('A selector, ref, text, role, label, placeholder, or testId target is required');
|
|
223
|
-
|
|
224
|
-
command.push(action);
|
|
225
|
-
if (value !== undefined) command.push(value);
|
|
226
|
-
if (args.role && args.name) command.push('--name', args.name);
|
|
227
|
-
return command;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async function readTextCommand(command, session) {
|
|
231
|
-
return asText(await runAgentBrowser(command, { session, timeoutMs: 30_000 }));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function stateBrowser(args = {}, context) {
|
|
235
|
-
const session = browserSession(args, context);
|
|
236
|
-
let url = '';
|
|
237
|
-
let title = '';
|
|
238
|
-
|
|
239
|
-
try { url = await readTextCommand(['get', 'url'], session); } catch {}
|
|
240
|
-
try { title = await readTextCommand(['get', 'title'], session); } catch {}
|
|
241
|
-
|
|
242
|
-
return tabInfo(session, { title, url, loading: false, visible: wantsHeaded() });
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function saveStateBrowser(args = {}, context) {
|
|
246
|
-
const session = browserSession(args, context);
|
|
247
|
-
const file = args.path || path.join(os.tmpdir(), `amalgm-browser-state-${session}-${Date.now()}-${crypto.randomUUID()}.json`);
|
|
248
|
-
await runAgentBrowser(['state', 'save', file], { session });
|
|
249
|
-
const state = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
250
|
-
if (!args.path) {
|
|
251
|
-
try { fs.rmSync(file, { force: true }); } catch {}
|
|
252
|
-
}
|
|
253
|
-
return tabInfo(session, {
|
|
254
|
-
state,
|
|
255
|
-
path: args.path ? file : undefined,
|
|
256
|
-
storageKinds: ['cookies', 'localStorage', 'sessionStorage'],
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
async function loadStateBrowser(args = {}, context) {
|
|
261
|
-
const session = browserSession(args, context);
|
|
262
|
-
let file = args.path;
|
|
263
|
-
let temporary = false;
|
|
264
|
-
if (!file) {
|
|
265
|
-
if (!args.state || typeof args.state !== 'object') throw new Error('state or path is required');
|
|
266
|
-
file = path.join(os.tmpdir(), `amalgm-browser-state-load-${session}-${Date.now()}-${crypto.randomUUID()}.json`);
|
|
267
|
-
fs.writeFileSync(file, `${JSON.stringify(args.state, null, 2)}\n`, { mode: 0o600 });
|
|
268
|
-
temporary = true;
|
|
269
|
-
}
|
|
270
|
-
await runAgentBrowser(['state', 'load', file], { session });
|
|
271
|
-
if (temporary) {
|
|
272
|
-
try { fs.rmSync(file, { force: true }); } catch {}
|
|
273
|
-
}
|
|
274
|
-
return tabInfo(session, {
|
|
275
|
-
loaded: true,
|
|
276
|
-
storageKinds: ['cookies', 'localStorage', 'sessionStorage'],
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
async function navigateBrowser(args = {}, context) {
|
|
281
|
-
const session = browserSession(args, context);
|
|
282
|
-
await runAgentBrowser(['open', args.url], { session });
|
|
283
|
-
const state = await stateBrowser(args, context);
|
|
284
|
-
return { ...state, status: 'ok' };
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
async function screenshotBrowser(args = {}, context) {
|
|
288
|
-
const session = browserSession(args, context);
|
|
289
|
-
const file = path.join(screenshotDir(), `${session}-${Date.now()}-${crypto.randomUUID()}.png`);
|
|
290
|
-
await runAgentBrowser(['screenshot', ...(args.fullPage ? ['--full'] : []), file], { session });
|
|
291
|
-
const buffer = fs.readFileSync(file);
|
|
292
|
-
return {
|
|
293
|
-
...tabInfo(session),
|
|
294
|
-
base64: buffer.toString('base64'),
|
|
295
|
-
bytes: buffer.length,
|
|
296
|
-
mode: args.fullPage ? 'full page' : 'viewport',
|
|
297
|
-
path: file,
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
async function snapshotBrowser(args = {}, context) {
|
|
302
|
-
const session = browserSession(args, context);
|
|
303
|
-
const snapshot = await runAgentBrowser(['snapshot', '-i'], { session, timeoutMs: 30_000 });
|
|
304
|
-
const state = await stateBrowser(args, context);
|
|
305
|
-
return { ...state, snapshotText: asText(snapshot) };
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
async function clickBrowser(args = {}, context) {
|
|
309
|
-
const session = browserSession(args, context);
|
|
310
|
-
const target = directTarget(args);
|
|
311
|
-
await runAgentBrowser(target ? ['click', target] : findCommand(args, 'click'), { session });
|
|
312
|
-
return tabInfo(session, { description: `Clicked ${target || args.text || args.name || args.label || args.placeholder || args.testId || args.role}` });
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async function typeBrowser(args = {}, context) {
|
|
316
|
-
const session = browserSession(args, context);
|
|
317
|
-
const target = directTarget(args);
|
|
318
|
-
const action = args.clear === false ? 'type' : 'fill';
|
|
319
|
-
await runAgentBrowser(target ? [action, target, args.text || ''] : findCommand(args, action, args.text || ''), { session });
|
|
320
|
-
return tabInfo(session, { description: `Typed into ${target || args.label || args.placeholder || args.role || 'target'}` });
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
async function pressBrowser(args = {}, context) {
|
|
324
|
-
const session = browserSession(args, context);
|
|
325
|
-
const target = directTarget(args);
|
|
326
|
-
if (target) await runAgentBrowser(['focus', target], { session });
|
|
327
|
-
await runAgentBrowser(['press', args.key], { session });
|
|
328
|
-
return tabInfo(session, { pressed: args.key });
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async function getTextBrowser(args = {}, context) {
|
|
332
|
-
const session = browserSession(args, context);
|
|
333
|
-
const target = directTarget(args) || 'body';
|
|
334
|
-
return { ...tabInfo(session), text: await readTextCommand(['get', 'text', target], session) };
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
async function evaluateBrowser(args = {}, context) {
|
|
338
|
-
const session = browserSession(args, context);
|
|
339
|
-
return { ...tabInfo(session), result: await runAgentBrowser(['eval', args.script || ''], { session }) };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async function locatorCountBrowser(args = {}, context) {
|
|
343
|
-
const session = browserSession(args, context);
|
|
344
|
-
const target = directTarget(args);
|
|
345
|
-
if (!target) throw new Error('Count currently requires selector or ref. Use browser_snapshot for refs.');
|
|
346
|
-
const count = Number(await readTextCommand(['get', 'count', target], session));
|
|
347
|
-
return { ...tabInfo(session), count: Number.isFinite(count) ? count : 0 };
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async function locatorTextBrowser(args = {}, context) {
|
|
351
|
-
const session = browserSession(args, context);
|
|
352
|
-
const target = directTarget(args);
|
|
353
|
-
if (!target) throw new Error('Text lookup currently requires selector or ref. Use browser_snapshot for refs.');
|
|
354
|
-
return { ...tabInfo(session), text: await readTextCommand(['get', 'text', target], session) };
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function sleep(ms) {
|
|
358
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function globToRegExp(pattern) {
|
|
362
|
-
const escaped = String(pattern)
|
|
363
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
364
|
-
.replace(/\*\*/g, '.*')
|
|
365
|
-
.replace(/\*/g, '[^/]*');
|
|
366
|
-
return new RegExp(`^${escaped}$`);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function urlMatches(actual, expected) {
|
|
370
|
-
if (!expected) return true;
|
|
371
|
-
const target = String(expected);
|
|
372
|
-
if (actual === target) return true;
|
|
373
|
-
if (target.includes('*')) return globToRegExp(target).test(actual);
|
|
374
|
-
return actual.includes(target);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
async function pollUntil(description, timeoutMs, check) {
|
|
378
|
-
const deadline = Date.now() + timeoutMs;
|
|
379
|
-
let lastError;
|
|
380
|
-
|
|
381
|
-
while (Date.now() < deadline) {
|
|
382
|
-
try {
|
|
383
|
-
if (await check()) return;
|
|
384
|
-
} catch (err) {
|
|
385
|
-
lastError = err;
|
|
386
|
-
}
|
|
387
|
-
await sleep(250);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
throw new Error(`${description} timed out${lastError?.message ? `: ${lastError.message}` : ''}`);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
async function waitForLoadStateBrowser(args = {}, context) {
|
|
394
|
-
const session = browserSession(args, context);
|
|
395
|
-
const state = args.state || 'load';
|
|
396
|
-
const timeoutMs = args.timeoutMs || 30_000;
|
|
397
|
-
|
|
398
|
-
if (state === 'networkidle') {
|
|
399
|
-
await runAgentBrowser(['wait', Math.min(timeoutMs, 1000)], { session, timeoutMs });
|
|
400
|
-
return stateBrowser(args, context);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
await pollUntil(`wait for ${state}`, timeoutMs, async () => {
|
|
404
|
-
const readyState = await readTextCommand(['eval', 'document.readyState'], session);
|
|
405
|
-
return state === 'domcontentloaded'
|
|
406
|
-
? readyState !== 'loading'
|
|
407
|
-
: readyState === 'complete';
|
|
408
|
-
});
|
|
409
|
-
return stateBrowser(args, context);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
async function waitForURLBrowser(args = {}, context) {
|
|
413
|
-
const session = browserSession(args, context);
|
|
414
|
-
const expectedUrl = args.url;
|
|
415
|
-
await pollUntil(`wait for URL ${expectedUrl}`, args.timeoutMs || 30_000, async () => {
|
|
416
|
-
const currentUrl = await readTextCommand(['get', 'url'], session);
|
|
417
|
-
return urlMatches(currentUrl, expectedUrl);
|
|
418
|
-
});
|
|
419
|
-
return stateBrowser(args, context);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
async function waitForSelectorBrowser(args = {}, context) {
|
|
423
|
-
const session = browserSession(args, context);
|
|
424
|
-
await runAgentBrowser(['wait', args.selector], { session, timeoutMs: args.timeoutMs || 30_000 });
|
|
425
|
-
return { ...tabInfo(session), selector: args.selector, matched: true };
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
async function tabNavigationBrowser(action, args = {}, context) {
|
|
429
|
-
const session = browserSession(args, context);
|
|
430
|
-
await runAgentBrowser([action], { session });
|
|
431
|
-
return stateBrowser(args, context);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
async function tabsListBrowser() {
|
|
435
|
-
return {
|
|
436
|
-
tabs: Array.from(knownSessions.values()).sort((a, b) => (b.lastActiveAt || 0) - (a.lastActiveAt || 0)),
|
|
437
|
-
backend: 'agent-browser',
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
async function tabsSelectedBrowser(context) {
|
|
442
|
-
return stateBrowser({}, context);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
async function tabsNewBrowser(args = {}, context) {
|
|
446
|
-
const session = safeId(args.browserSessionId || args.sessionId || `browser-${Date.now().toString(36)}`);
|
|
447
|
-
if (args.url) await navigateBrowser({ ...args, browserSessionId: session }, context);
|
|
448
|
-
return stateBrowser({ ...args, browserSessionId: session }, context);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
async function tabsGetBrowser(args = {}, context) {
|
|
452
|
-
return stateBrowser(args, context);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
async function tabsSelectBrowser(args = {}, context) {
|
|
456
|
-
return stateBrowser(args, context);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async function cuaBrowser(action, args = {}, context) {
|
|
460
|
-
const session = browserSession(args, context);
|
|
461
|
-
const x = Math.round(Number(args.x || 0));
|
|
462
|
-
const y = Math.round(Number(args.y || 0));
|
|
463
|
-
if (action === 'cua_move') await runAgentBrowser(['mouse', 'move', x, y], { session });
|
|
464
|
-
if (action === 'cua_click') {
|
|
465
|
-
await runAgentBrowser(['mouse', 'move', x, y], { session });
|
|
466
|
-
await runAgentBrowser(['mouse', 'down', args.button || 1], { session });
|
|
467
|
-
await runAgentBrowser(['mouse', 'up', args.button || 1], { session });
|
|
468
|
-
}
|
|
469
|
-
if (action === 'cua_double_click') {
|
|
470
|
-
await runAgentBrowser(['mouse', 'move', x, y], { session });
|
|
471
|
-
await runAgentBrowser(['mouse', 'down', 1], { session });
|
|
472
|
-
await runAgentBrowser(['mouse', 'up', 1], { session });
|
|
473
|
-
await runAgentBrowser(['mouse', 'down', 1], { session });
|
|
474
|
-
await runAgentBrowser(['mouse', 'up', 1], { session });
|
|
475
|
-
}
|
|
476
|
-
if (action === 'cua_scroll') await runAgentBrowser(['mouse', 'wheel', args.scrollY || 0, args.scrollX || 0], { session });
|
|
477
|
-
if (action === 'cua_type') await runAgentBrowser(['keyboard', 'type', args.text || ''], { session });
|
|
478
|
-
if (action === 'cua_keypress') await runAgentBrowser(['press', (args.keys || []).join('+')], { session });
|
|
479
|
-
if (action === 'cua_drag') await dragBrowser(args, session);
|
|
480
|
-
return tabInfo(session, { ok: true });
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
async function dragBrowser(args, session) {
|
|
484
|
-
const pathPoints = Array.isArray(args.path) ? args.path : [];
|
|
485
|
-
if (pathPoints.length < 2) throw new Error('drag path must contain at least two points');
|
|
486
|
-
await runAgentBrowser(['mouse', 'move', pathPoints[0].x, pathPoints[0].y], { session });
|
|
487
|
-
await runAgentBrowser(['mouse', 'down', 1], { session });
|
|
488
|
-
for (const point of pathPoints.slice(1)) {
|
|
489
|
-
await runAgentBrowser(['mouse', 'move', point.x, point.y], { session });
|
|
490
|
-
}
|
|
491
|
-
await runAgentBrowser(['mouse', 'up', 1], { session });
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
async function clipboardBrowser(action, args = {}, context) {
|
|
495
|
-
const session = browserSession(args, context);
|
|
496
|
-
if (action === 'clipboard_read_text') {
|
|
497
|
-
return { ...tabInfo(session), text: await readTextCommand(['clipboard', 'read'], session) };
|
|
498
|
-
}
|
|
499
|
-
await runAgentBrowser(['clipboard', 'write', args.text || ''], { session });
|
|
500
|
-
return tabInfo(session, { written: true });
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
async function startVideoBrowser(args = {}, context) {
|
|
504
|
-
const session = browserSession(args, context);
|
|
505
|
-
const id = safeId(args.sessionId || args.recording_id || session, session);
|
|
506
|
-
const file = path.join(recordingDir(id), `${new Date().toISOString().replace(/[:.]/g, '-')}.webm`);
|
|
507
|
-
await runAgentBrowser(['record', 'start', file], { session });
|
|
508
|
-
activeRecordings.set(session, file);
|
|
509
|
-
return tabInfo(session, { path: file, mimeType: 'video/webm' });
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
async function stopVideoBrowser(args = {}, context) {
|
|
513
|
-
const session = browserSession(args, context);
|
|
514
|
-
await runAgentBrowser(['record', 'stop'], { session });
|
|
515
|
-
const file = activeRecordings.get(session);
|
|
516
|
-
activeRecordings.delete(session);
|
|
517
|
-
if (!file) return tabInfo(session, { stopped: true });
|
|
518
|
-
const bytes = fs.existsSync(file) ? fs.statSync(file).size : 0;
|
|
519
|
-
return tabInfo(session, {
|
|
520
|
-
path: file,
|
|
521
|
-
relativePath: path.relative(amalgmDir(), file),
|
|
522
|
-
url: `/api/browser-sessions/video?file=${encodeURIComponent(path.relative(amalgmDir(), file))}`,
|
|
523
|
-
bytes,
|
|
524
|
-
mimeType: 'video/webm',
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
async function closeBrowser(args = {}, context) {
|
|
529
|
-
const session = browserSession(args, context);
|
|
530
|
-
await runAgentBrowser(['close'], { session });
|
|
531
|
-
knownSessions.delete(session);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
module.exports = {
|
|
535
|
-
clipboardBrowser,
|
|
536
|
-
closeBrowser,
|
|
537
|
-
clickBrowser,
|
|
538
|
-
cuaBrowser,
|
|
539
|
-
evaluateBrowser,
|
|
540
|
-
getTextBrowser,
|
|
541
|
-
locatorCountBrowser,
|
|
542
|
-
locatorTextBrowser,
|
|
543
|
-
loadStateBrowser,
|
|
544
|
-
navigateBrowser,
|
|
545
|
-
pressBrowser,
|
|
546
|
-
screenshotBrowser,
|
|
547
|
-
saveStateBrowser,
|
|
548
|
-
snapshotBrowser,
|
|
549
|
-
startVideoBrowser,
|
|
550
|
-
stateBrowser,
|
|
551
|
-
stopVideoBrowser,
|
|
552
|
-
tabNavigationBrowser,
|
|
553
|
-
tabsGetBrowser,
|
|
554
|
-
tabsListBrowser,
|
|
555
|
-
tabsNewBrowser,
|
|
556
|
-
tabsSelectBrowser,
|
|
557
|
-
tabsSelectedBrowser,
|
|
558
|
-
typeBrowser,
|
|
559
|
-
waitForLoadStateBrowser,
|
|
560
|
-
waitForSelectorBrowser,
|
|
561
|
-
waitForURLBrowser,
|
|
562
|
-
_private: {
|
|
563
|
-
browserRoot,
|
|
564
|
-
browserSession,
|
|
565
|
-
profileDir,
|
|
566
|
-
runAgentBrowser,
|
|
567
|
-
safeId,
|
|
568
|
-
},
|
|
569
|
-
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
const electron = require('./electron-bridge');
|
|
2
|
-
const agentBrowser = require('./agent-browser');
|
|
3
|
-
|
|
4
|
-
function isPackagedDesktopRuntime() {
|
|
5
|
-
const execPath = process.execPath || '';
|
|
6
|
-
return process.env.ELECTRON_RUN_AS_NODE === '1' && execPath.includes('.app/Contents/MacOS/');
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function backend() {
|
|
10
|
-
const requested = (process.env.AMALGM_BROWSER_BACKEND || '').toLowerCase();
|
|
11
|
-
if (requested === 'agent-browser' || requested === 'external') return agentBrowser;
|
|
12
|
-
if (requested === 'electron') return electron;
|
|
13
|
-
|
|
14
|
-
// In the desktop app this bridge drives the visible in-tab Electron webview.
|
|
15
|
-
// Outside Electron, keep the agent-browser CLI fallback for headless/remote use.
|
|
16
|
-
if (electron.isConfigured() || isPackagedDesktopRuntime()) return electron;
|
|
17
|
-
return agentBrowser;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const exported = {};
|
|
21
|
-
for (const key of Object.keys(agentBrowser)) {
|
|
22
|
-
exported[key] = (...args) => backend()[key](...args);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
module.exports = exported;
|