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
@@ -0,0 +1,199 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Session recorder: capture any browser session to a local WebM file.
5
+ *
6
+ * Implementation is a direct CDP Page.startScreencast consumer piped into
7
+ * ffmpeg (ffmpeg-static when bundled, PATH ffmpeg otherwise). This is
8
+ * deliberately NOT agent-browser's `record` command:
9
+ * - `record` needs ffmpeg on the daemon's PATH at daemon-spawn time, and
10
+ * silently "starts" even when ffmpeg is missing (fails only on stop).
11
+ * - `record` calls Target.createBrowserContext, which Electron forbids — so
12
+ * it can never capture the visible in-tab webview.
13
+ * A raw screencast works against every CDP target we care about: launched
14
+ * chromium AND the desktop app's splitscreen webview.
15
+ *
16
+ * Storage: recordings belong to the Amalgm project the work is for —
17
+ * <project>/.amalgm/recordings/ when a project path is known, otherwise
18
+ * ~/.amalgm/browser-sessions/<session>/.
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { spawn } = require('child_process');
24
+
25
+ const cli = require('./cli');
26
+ const engine = require('./engine');
27
+
28
+ const active = new Map(); // session -> { ws, ffmpeg, file, frames, startedAt }
29
+
30
+ function ffmpegBinary() {
31
+ try {
32
+ const resolved = require('ffmpeg-static');
33
+ if (resolved && fs.existsSync(resolved)) return resolved;
34
+ } catch {}
35
+ return 'ffmpeg';
36
+ }
37
+
38
+ function resolveProjectPath(args = {}, context = {}) {
39
+ const candidate = args.projectPath
40
+ || context?.sessionMetadata?.projectPath
41
+ || context?.sessionMetadata?.cwd
42
+ || process.env.AMALGM_PROJECT_PATH
43
+ || null;
44
+ if (!candidate) return null;
45
+ try {
46
+ return fs.statSync(candidate).isDirectory() ? candidate : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function recordingDir(args = {}, context = {}, session) {
53
+ const project = resolveProjectPath(args, context);
54
+ if (project) return cli.ensureDir(path.join(project, '.amalgm', 'recordings'));
55
+ return cli.ensureDir(path.join(cli.amalgmDir(), 'browser-sessions', session));
56
+ }
57
+
58
+ function recordingFile(args, context, session) {
59
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
60
+ const name = cli.safeId(args.name || session, session);
61
+ return path.join(recordingDir(args, context, session), `${name}-${stamp}.webm`);
62
+ }
63
+
64
+ /**
65
+ * Page.startScreencast only works on a page-level CDP connection. Launched
66
+ * sessions report a browser-level ws URL, so resolve the active page target
67
+ * through the /json/list endpoint on the same debugger port.
68
+ */
69
+ async function pageWsUrl(wsUrl) {
70
+ if (!/\/devtools\/browser\//.test(wsUrl)) return wsUrl;
71
+ const { hostname, port } = new URL(wsUrl);
72
+ const list = await new Promise((resolve, reject) => {
73
+ const http = require('http');
74
+ const req = http.get({ hostname, port, path: '/json/list', timeout: 5_000 }, (res) => {
75
+ let raw = '';
76
+ res.on('data', (chunk) => { raw += chunk.toString(); });
77
+ res.on('end', () => {
78
+ try { resolve(JSON.parse(raw)); } catch (err) { reject(err); }
79
+ });
80
+ });
81
+ req.on('error', reject);
82
+ req.on('timeout', () => req.destroy(new Error('Timed out listing CDP targets')));
83
+ });
84
+ const page = list.find((t) => t.type === 'webview' && t.webSocketDebuggerUrl)
85
+ || list.find((t) => t.type === 'page' && t.webSocketDebuggerUrl && !t.url.startsWith('devtools://'));
86
+ if (!page) throw new Error('No recordable page target found on the CDP endpoint.');
87
+ return page.webSocketDebuggerUrl;
88
+ }
89
+
90
+ async function start(args = {}, context = {}) {
91
+ const session = engine.sessionFor(args, context);
92
+ if (active.has(session)) {
93
+ throw new Error(`Recording already active for session "${session}". Call record_stop first.`);
94
+ }
95
+
96
+ const rawUrl = await engine.cdpUrlForSession(session);
97
+ if (!rawUrl) throw new Error('Could not resolve a CDP endpoint for this browser session.');
98
+ const wsUrl = await pageWsUrl(rawUrl);
99
+
100
+ const WebSocket = require('ws');
101
+ const file = recordingFile(args, context, session);
102
+ const fps = Math.min(Math.max(Number(args.fps) || 10, 1), 30);
103
+
104
+ const ffmpeg = spawn(ffmpegBinary(), [
105
+ '-y', '-f', 'image2pipe', '-c:v', 'mjpeg', '-framerate', String(fps), '-i', 'pipe:0',
106
+ '-vf', 'pad=ceil(iw/2)*2:ceil(ih/2)*2', '-c:v', 'libvpx', '-pix_fmt', 'yuv420p', file,
107
+ ], { stdio: ['pipe', 'ignore', 'pipe'] });
108
+ let ffmpegErr = '';
109
+ ffmpeg.stderr.on('data', (chunk) => { ffmpegErr = (ffmpegErr + chunk.toString()).slice(-2000); });
110
+
111
+ const ws = new WebSocket(wsUrl, { perMessageDeflate: false });
112
+ const recording = { ws, ffmpeg, file, frames: 0, startedAt: Date.now(), ffmpegErr: () => ffmpegErr };
113
+ active.set(session, recording);
114
+
115
+ await new Promise((resolve, reject) => {
116
+ const timer = setTimeout(() => reject(new Error('Timed out connecting to browser CDP endpoint')), 10_000);
117
+ ws.on('open', () => { clearTimeout(timer); resolve(); });
118
+ ws.on('error', (err) => { clearTimeout(timer); reject(err); });
119
+ }).catch((err) => {
120
+ active.delete(session);
121
+ try { ffmpeg.kill('SIGKILL'); } catch {}
122
+ throw err;
123
+ });
124
+
125
+ let id = 0;
126
+ const send = (method, params = {}) => ws.send(JSON.stringify({ id: ++id, method, params }));
127
+ ws.on('message', (raw) => {
128
+ let msg;
129
+ try { msg = JSON.parse(raw); } catch { return; }
130
+ if (msg.method === 'Page.screencastFrame') {
131
+ recording.frames += 1;
132
+ try { ffmpeg.stdin.write(Buffer.from(msg.params.data, 'base64')); } catch {}
133
+ send('Page.screencastFrameAck', { sessionId: msg.params.sessionId });
134
+ }
135
+ });
136
+ send('Page.enable');
137
+ send('Page.startScreencast', { format: 'jpeg', quality: 70, everyNthFrame: 1 });
138
+
139
+ return { session, path: file, fps, mimeType: 'video/webm', backend: engine.mode() };
140
+ }
141
+
142
+ async function stop(args = {}, context = {}) {
143
+ const session = engine.sessionFor(args, context);
144
+ const recording = active.get(session);
145
+ if (!recording) throw new Error(`No active recording for session "${session}".`);
146
+ active.delete(session);
147
+
148
+ try {
149
+ recording.ws.send(JSON.stringify({ id: 9_999, method: 'Page.stopScreencast' }));
150
+ } catch {}
151
+ try { recording.ws.close(); } catch {}
152
+
153
+ const exitCode = await new Promise((resolve) => {
154
+ recording.ffmpeg.on('close', resolve);
155
+ try { recording.ffmpeg.stdin.end(); } catch { resolve(-1); }
156
+ });
157
+
158
+ const durationMs = Date.now() - recording.startedAt;
159
+ if (recording.frames === 0 || exitCode !== 0) {
160
+ try { fs.rmSync(recording.file, { force: true }); } catch {}
161
+ throw new Error(recording.frames === 0
162
+ ? 'No frames captured — the page may not have repainted while recording. (Screencast only emits on visual change.)'
163
+ : `ffmpeg failed (exit ${exitCode}): ${recording.ffmpegErr()}`);
164
+ }
165
+
166
+ const bytes = fs.existsSync(recording.file) ? fs.statSync(recording.file).size : 0;
167
+ return {
168
+ session,
169
+ path: recording.file,
170
+ relativePath: path.relative(cli.amalgmDir(), recording.file),
171
+ bytes,
172
+ frames: recording.frames,
173
+ durationMs,
174
+ mimeType: 'video/webm',
175
+ };
176
+ }
177
+
178
+ function list(args = {}, context = {}) {
179
+ const session = engine.sessionFor(args, context);
180
+ const dir = recordingDir(args, context, session);
181
+ let entries = [];
182
+ try {
183
+ entries = fs.readdirSync(dir)
184
+ .filter((name) => name.endsWith('.webm'))
185
+ .map((name) => {
186
+ const file = path.join(dir, name);
187
+ const stat = fs.statSync(file);
188
+ return { name, path: file, bytes: stat.size, modifiedAt: stat.mtime.toISOString() };
189
+ })
190
+ .sort((a, b) => b.modifiedAt.localeCompare(a.modifiedAt));
191
+ } catch {}
192
+ return { dir, recordings: entries, activeSession: active.has(session) ? session : null };
193
+ }
194
+
195
+ function isActive(session) {
196
+ return active.has(session);
197
+ }
198
+
199
+ module.exports = { isActive, list, recordingDir, resolveProjectPath, start, stop };
@@ -1,6 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const { loadStateBrowser, navigateBrowser, saveStateBrowser } = require('./page');
3
+ const engine = require('./engine');
4
+
5
+ const loadStateBrowser = (args, ctx) => engine.loadState(args, ctx);
6
+ const saveStateBrowser = (args, ctx) => engine.saveState(args, ctx);
7
+ const navigateBrowser = (args, ctx) => engine.open(args, ctx);
4
8
  const {
5
9
  assertBrowserLoginSessionToken,
6
10
  cancelBrowserLoginSession,