amalgm 0.0.0 → 0.0.32

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 (116) hide show
  1. package/README.md +41 -1
  2. package/bin/amalgm.js +8 -2
  3. package/lib/auth-store.js +223 -0
  4. package/lib/cli.js +1132 -0
  5. package/lib/paths.js +30 -0
  6. package/lib/supervisor.js +476 -0
  7. package/lib/tunnel-chat.js +328 -0
  8. package/lib/tunnel-events.js +499 -0
  9. package/package.json +30 -3
  10. package/runtime/README.md +4 -0
  11. package/runtime/lib/chatInput.js +315 -0
  12. package/runtime/lib/harnesses.js +988 -0
  13. package/runtime/lib/local/amalgmStore.js +136 -0
  14. package/runtime/lib/local/credentialResolver.js +425 -0
  15. package/runtime/lib/mcpApps/registry.js +619 -0
  16. package/runtime/package.json +5 -0
  17. package/runtime/scripts/amalgm-mcp/agents/rest.js +144 -0
  18. package/runtime/scripts/amalgm-mcp/agents/store.js +684 -0
  19. package/runtime/scripts/amalgm-mcp/agents/talk.js +1162 -0
  20. package/runtime/scripts/amalgm-mcp/agents/tools.js +221 -0
  21. package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
  22. package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
  23. package/runtime/scripts/amalgm-mcp/artifacts/store.js +157 -0
  24. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
  25. package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
  26. package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
  27. package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
  28. package/runtime/scripts/amalgm-mcp/config.js +140 -0
  29. package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
  30. package/runtime/scripts/amalgm-mcp/deps.js +40 -0
  31. package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
  32. package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
  33. package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
  34. package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
  35. package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
  36. package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
  37. package/runtime/scripts/amalgm-mcp/events/store.js +113 -0
  38. package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
  39. package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
  40. package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
  41. package/runtime/scripts/amalgm-mcp/index.js +100 -0
  42. package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
  43. package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
  44. package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
  45. package/runtime/scripts/amalgm-mcp/lib/prefs.js +441 -0
  46. package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
  47. package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
  48. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
  49. package/runtime/scripts/amalgm-mcp/local/rest.js +87 -0
  50. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +234 -0
  51. package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
  52. package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
  53. package/runtime/scripts/amalgm-mcp/server/http.js +377 -0
  54. package/runtime/scripts/amalgm-mcp/server/mcp.js +127 -0
  55. package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
  56. package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
  57. package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
  58. package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
  59. package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
  60. package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
  61. package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
  62. package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
  63. package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
  64. package/runtime/scripts/amalgm-mcp/tasks/store.js +154 -0
  65. package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
  66. package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
  67. package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
  68. package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
  69. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
  70. package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
  71. package/runtime/scripts/amalgm-mcp/workspace/rest.js +497 -0
  72. package/runtime/scripts/chat-core/adapters/claude.js +165 -0
  73. package/runtime/scripts/chat-core/adapters/codex.js +313 -0
  74. package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
  75. package/runtime/scripts/chat-core/auth.js +177 -0
  76. package/runtime/scripts/chat-core/contract.js +328 -0
  77. package/runtime/scripts/chat-core/credentials/store.js +212 -0
  78. package/runtime/scripts/chat-core/egress.js +87 -0
  79. package/runtime/scripts/chat-core/engine.js +253 -0
  80. package/runtime/scripts/chat-core/event-schema.js +231 -0
  81. package/runtime/scripts/chat-core/events.js +190 -0
  82. package/runtime/scripts/chat-core/index.js +11 -0
  83. package/runtime/scripts/chat-core/input.js +50 -0
  84. package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
  85. package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
  86. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
  87. package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
  88. package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
  89. package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
  90. package/runtime/scripts/chat-core/parts.js +253 -0
  91. package/runtime/scripts/chat-core/recorder.js +65 -0
  92. package/runtime/scripts/chat-core/runtime.js +86 -0
  93. package/runtime/scripts/chat-core/server.js +163 -0
  94. package/runtime/scripts/chat-core/sse.js +196 -0
  95. package/runtime/scripts/chat-core/stores.js +100 -0
  96. package/runtime/scripts/chat-core/tool-display.js +149 -0
  97. package/runtime/scripts/chat-core/tool-shape.js +143 -0
  98. package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
  99. package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
  100. package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
  101. package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
  102. package/runtime/scripts/chat-core/usage.js +343 -0
  103. package/runtime/scripts/chat-server/config.js +110 -0
  104. package/runtime/scripts/chat-server/db.js +529 -0
  105. package/runtime/scripts/chat-server/index.js +33 -0
  106. package/runtime/scripts/chat-server/model-catalog.js +327 -0
  107. package/runtime/scripts/chat-server.js +75 -0
  108. package/runtime/scripts/credential-adapter.js +131 -0
  109. package/runtime/scripts/fs-watcher.js +888 -0
  110. package/runtime/scripts/local-gateway.js +854 -0
  111. package/runtime/scripts/platform-context.txt +246 -0
  112. package/runtime/scripts/port-monitor.js +175 -0
  113. package/runtime/scripts/proxy-token-store.js +162 -0
  114. package/runtime/scripts/runtime-auth.js +163 -0
  115. package/runtime/scripts/test-claude-code-models.js +87 -0
  116. package/runtime/tsconfig.json +15 -0
package/lib/paths.js ADDED
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const os = require('os');
4
+ const path = require('path');
5
+
6
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
7
+ const RUNTIME_DIR = path.join(PACKAGE_ROOT, 'runtime');
8
+ const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
9
+ const DEVICE_FILE = path.join(AMALGM_DIR, 'device.json');
10
+ const COMPUTER_FILE = path.join(AMALGM_DIR, 'computer.json');
11
+ const AUTH_FILE = path.join(AMALGM_DIR, 'auth.json');
12
+ const RUNTIME_TOKEN_FILE = path.join(AMALGM_DIR, 'runtime-token.json');
13
+ const PID_FILE = path.join(AMALGM_DIR, 'amalgm.pid');
14
+ const RUNTIME_STATE_FILE = path.join(AMALGM_DIR, 'runtime-state.json');
15
+ const LOG_DIR = path.join(AMALGM_DIR, 'logs');
16
+ const DEFAULT_APP_URL = process.env.AMALGM_APP_URL || 'https://amalgm.ai';
17
+
18
+ module.exports = {
19
+ AMALGM_DIR,
20
+ AUTH_FILE,
21
+ COMPUTER_FILE,
22
+ DEFAULT_APP_URL,
23
+ DEVICE_FILE,
24
+ LOG_DIR,
25
+ PACKAGE_ROOT,
26
+ PID_FILE,
27
+ RUNTIME_DIR,
28
+ RUNTIME_STATE_FILE,
29
+ RUNTIME_TOKEN_FILE,
30
+ };
@@ -0,0 +1,476 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { spawn } = require('child_process');
8
+ const net = require('net');
9
+
10
+ const {
11
+ AMALGM_DIR,
12
+ LOG_DIR,
13
+ PID_FILE,
14
+ RUNTIME_DIR,
15
+ RUNTIME_STATE_FILE,
16
+ RUNTIME_TOKEN_FILE,
17
+ } = require('./paths');
18
+ const {
19
+ loadComputerRecord,
20
+ proxyTokenFromRecord,
21
+ } = require('./auth-store');
22
+ const { createChatTunnel } = require('./tunnel-chat');
23
+ const { createEventTunnel } = require('./tunnel-events');
24
+ const {
25
+ ensureAgentCommandShims,
26
+ ensureNativeBinaries,
27
+ } = require('../runtime/scripts/chat-core/tooling/native-binaries');
28
+ const PACKAGE_VERSION = require('../package.json').version;
29
+
30
+ function ensureDir(dir, mode = 0o700) {
31
+ fs.mkdirSync(dir, { recursive: true, mode });
32
+ try {
33
+ fs.chmodSync(dir, mode);
34
+ } catch {
35
+ // Best effort.
36
+ }
37
+ }
38
+
39
+ function readJson(file, fallback = null) {
40
+ try {
41
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
42
+ } catch {
43
+ return fallback;
44
+ }
45
+ }
46
+
47
+ function writeJsonSecret(file, data) {
48
+ ensureDir(path.dirname(file));
49
+ const temp = `${file}.${process.pid}.tmp`;
50
+ fs.writeFileSync(temp, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600 });
51
+ fs.renameSync(temp, file);
52
+ try {
53
+ fs.chmodSync(file, 0o600);
54
+ } catch {
55
+ // Best effort.
56
+ }
57
+ }
58
+
59
+ function runtimeEntry(relativePath) {
60
+ return path.join(RUNTIME_DIR, relativePath);
61
+ }
62
+
63
+ function assertRuntimePresent() {
64
+ const required = [
65
+ runtimeEntry('scripts/amalgm-mcp/index.js'),
66
+ runtimeEntry('scripts/chat-server.js'),
67
+ runtimeEntry('scripts/fs-watcher.js'),
68
+ runtimeEntry('scripts/local-gateway.js'),
69
+ runtimeEntry('scripts/port-monitor.js'),
70
+ ];
71
+ const missing = required.filter((file) => !fs.existsSync(file));
72
+ if (missing.length > 0) {
73
+ throw new Error(
74
+ [
75
+ 'The Amalgm runtime has not been synced into this package.',
76
+ 'Run `npm run npm:sync` from amalgm-engine, or reinstall the published package.',
77
+ `Missing: ${missing[0]}`,
78
+ ].join('\n'),
79
+ );
80
+ }
81
+ }
82
+
83
+ const SAFE_PROCESS_ENV_KEYS = [
84
+ 'HOME',
85
+ 'LANG',
86
+ 'LC_ALL',
87
+ 'LOGNAME',
88
+ 'AMALGM_NATIVE_NODE_MODULES',
89
+ 'AMALGM_SKIP_NATIVE_INSTALL',
90
+ 'ELECTRON_RUN_AS_NODE',
91
+ 'PATH',
92
+ 'NODE_PATH',
93
+ 'SHELL',
94
+ 'SSH_AUTH_SOCK',
95
+ 'TERM',
96
+ 'TMP',
97
+ 'TMPDIR',
98
+ 'TEMP',
99
+ 'USER',
100
+ 'XDG_CACHE_HOME',
101
+ 'XDG_CONFIG_HOME',
102
+ 'XDG_DATA_HOME',
103
+ ];
104
+
105
+ function safeBaseProcessEnv() {
106
+ const env = {};
107
+ for (const key of SAFE_PROCESS_ENV_KEYS) {
108
+ if (process.env[key]) env[key] = process.env[key];
109
+ }
110
+ return env;
111
+ }
112
+
113
+ function ensureRuntimeToken() {
114
+ const fromEnv = String(process.env.AMALGM_RUNTIME_TOKEN || '').trim();
115
+ if (fromEnv) {
116
+ writeJsonSecret(RUNTIME_TOKEN_FILE, {
117
+ token: fromEnv,
118
+ source: 'env',
119
+ updated_at: new Date().toISOString(),
120
+ });
121
+ return fromEnv;
122
+ }
123
+
124
+ const existing = readJson(RUNTIME_TOKEN_FILE, null);
125
+ if (typeof existing?.token === 'string' && existing.token.length >= 32) {
126
+ return existing.token;
127
+ }
128
+
129
+ const token = crypto.randomBytes(32).toString('base64url');
130
+ writeJsonSecret(RUNTIME_TOKEN_FILE, {
131
+ token,
132
+ source: 'generated',
133
+ created_at: new Date().toISOString(),
134
+ });
135
+ return token;
136
+ }
137
+
138
+ function baseRuntimeEnv(record, ports) {
139
+ const workspaceRoot =
140
+ process.env.AMALGM_WORKSPACES_DIR ||
141
+ process.env.AMALGM_PROJECTS_DIR ||
142
+ path.join(AMALGM_DIR, 'workspaces');
143
+ ensureDir(workspaceRoot);
144
+ const defaultCwd = process.env.AMALGM_DEFAULT_CWD || workspaceRoot;
145
+ const proxyToken = proxyTokenFromRecord(record);
146
+ const env = {
147
+ ...safeBaseProcessEnv(),
148
+ AMALGM_RUNTIME_SOURCE: 'npm',
149
+ AMALGM_RUNTIME_TOKEN: record?.runtime_token || process.env.AMALGM_RUNTIME_TOKEN || '',
150
+ AMALGM_LOCAL_MODE: 'true',
151
+ AMALGM_BIND_HOST: process.env.AMALGM_BIND_HOST || '127.0.0.1',
152
+ AMALGM_AUTH_BACKUP_ENABLED: process.env.AMALGM_AUTH_BACKUP_ENABLED || 'false',
153
+ AMALGM_CREATE_AUTH_WATCH_DIRS: process.env.AMALGM_CREATE_AUTH_WATCH_DIRS || 'false',
154
+ AMALGM_DIR,
155
+ AMALGM_RUNTIME_DIR: RUNTIME_DIR,
156
+ AMALGM_WORKSPACES_DIR: workspaceRoot,
157
+ AMALGM_PROJECTS_DIR: workspaceRoot,
158
+ AMALGM_DEFAULT_CWD: defaultCwd,
159
+ AMALGM_COMPUTER_ID: record?.computer_id || process.env.AMALGM_COMPUTER_ID || '',
160
+ AMALGM_CONTAINER_ID: record?.computer_id || process.env.AMALGM_CONTAINER_ID || 'local-computer',
161
+ AMALGM_DEVICE_ID: record?.device_id || process.env.AMALGM_DEVICE_ID || '',
162
+ AMALGM_EVENTS_PUBLIC_URL: record?.webhook_url || process.env.AMALGM_EVENTS_PUBLIC_URL || '',
163
+ AMALGM_USER_ID: record?.user_id || process.env.AMALGM_USER_ID || '',
164
+ AMALGM_GATEWAY_PORT: String(ports.gateway),
165
+ CHAT_SERVER_PORT: String(ports.chatServer),
166
+ AMALGM_MCP_PORT: String(ports.amalgmMcp),
167
+ PORT_MONITOR_PORT: String(ports.portMonitor),
168
+ FS_WATCHER_PORT: String(ports.fsWatcher),
169
+ FS_WATCHER_ROOT: process.env.FS_WATCHER_ROOT || defaultCwd,
170
+ AMALGM_RUNTIME_VERSION: require('../package.json').version,
171
+ };
172
+
173
+ if (proxyToken) {
174
+ env.AMALGM_PROXY_TOKEN = proxyToken;
175
+ }
176
+ if (record?.app_url) {
177
+ env.AMALGM_APP_URL = record.app_url;
178
+ }
179
+ return env;
180
+ }
181
+
182
+ function serviceSpecs(record, ports) {
183
+ const env = baseRuntimeEnv(record, ports);
184
+ return [
185
+ {
186
+ name: 'port-monitor',
187
+ command: process.execPath,
188
+ args: [runtimeEntry('scripts/port-monitor.js')],
189
+ env: { ...env, PORT: process.env.PORT || '0' },
190
+ },
191
+ {
192
+ name: 'fs-watcher',
193
+ command: process.execPath,
194
+ args: [runtimeEntry('scripts/fs-watcher.js')],
195
+ env,
196
+ },
197
+ {
198
+ name: 'chat-server',
199
+ command: process.execPath,
200
+ args: [runtimeEntry('scripts/chat-server.js')],
201
+ env,
202
+ },
203
+ {
204
+ name: 'amalgm-mcp',
205
+ command: process.execPath,
206
+ args: [runtimeEntry('scripts/amalgm-mcp/index.js')],
207
+ env,
208
+ },
209
+ {
210
+ name: 'local-gateway',
211
+ command: process.execPath,
212
+ args: [runtimeEntry('scripts/local-gateway.js')],
213
+ env,
214
+ },
215
+ ];
216
+ }
217
+
218
+ function isPortFree(port, host = '127.0.0.1') {
219
+ return new Promise((resolve) => {
220
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
221
+ resolve(false);
222
+ return;
223
+ }
224
+
225
+ const server = net.createServer();
226
+ server.unref();
227
+ server.on('error', () => resolve(false));
228
+ server.listen({ host, port }, () => {
229
+ server.close(() => resolve(true));
230
+ });
231
+ });
232
+ }
233
+
234
+ function randomFreePort(host = '127.0.0.1') {
235
+ return new Promise((resolve, reject) => {
236
+ const server = net.createServer();
237
+ server.unref();
238
+ server.on('error', reject);
239
+ server.listen({ host, port: 0 }, () => {
240
+ const address = server.address();
241
+ const port = typeof address === 'object' && address ? address.port : 0;
242
+ server.close(() => resolve(port));
243
+ });
244
+ });
245
+ }
246
+
247
+ async function pickPort(name, envName, preferred, used) {
248
+ const explicit = String(process.env[envName] || '').trim();
249
+ if (explicit) {
250
+ const port = Number(explicit);
251
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
252
+ throw new Error(`${envName} must be a valid TCP port`);
253
+ }
254
+ if (used.has(port)) throw new Error(`${envName} duplicates another Amalgm runtime port: ${port}`);
255
+ if (!(await isPortFree(port))) {
256
+ throw new Error(`${name} port ${port} is already in use`);
257
+ }
258
+ used.add(port);
259
+ return port;
260
+ }
261
+
262
+ if (!used.has(preferred) && (await isPortFree(preferred))) {
263
+ used.add(preferred);
264
+ return preferred;
265
+ }
266
+
267
+ for (let attempt = 0; attempt < 50; attempt += 1) {
268
+ const port = await randomFreePort();
269
+ if (!used.has(port)) {
270
+ used.add(port);
271
+ return port;
272
+ }
273
+ }
274
+
275
+ throw new Error(`Could not allocate a free port for ${name}`);
276
+ }
277
+
278
+ async function resolveRuntimePorts() {
279
+ const used = new Set();
280
+ return {
281
+ gateway: await pickPort('local-gateway', 'AMALGM_GATEWAY_PORT', 28781, used),
282
+ portMonitor: await pickPort('port-monitor', 'PORT_MONITOR_PORT', 8081, used),
283
+ fsWatcher: await pickPort('fs-watcher', 'FS_WATCHER_PORT', 8082, used),
284
+ amalgmMcp: await pickPort('amalgm-mcp', 'AMALGM_MCP_PORT', 8083, used),
285
+ chatServer: await pickPort('chat-server', 'CHAT_SERVER_PORT', 8084, used),
286
+ };
287
+ }
288
+
289
+ function openServiceLog(name) {
290
+ ensureDir(LOG_DIR);
291
+ const logPath = path.join(LOG_DIR, `${name}.log`);
292
+ return {
293
+ logPath,
294
+ stream: fs.createWriteStream(logPath, { flags: 'a' }),
295
+ };
296
+ }
297
+
298
+ function writePrefixed(stream, name, chunk, consoleStream) {
299
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk);
300
+ const lines = text.split(/(?<=\n)/);
301
+ for (const line of lines) {
302
+ if (!line) continue;
303
+ const prefixed = `[${new Date().toISOString()}] [${name}] ${line}`;
304
+ stream.write(prefixed);
305
+ if (consoleStream) consoleStream.write(prefixed);
306
+ }
307
+ }
308
+
309
+ function createManagedProcess(spec, options) {
310
+ const { stream } = openServiceLog(spec.name);
311
+ let child = null;
312
+ let stopped = false;
313
+ let restartTimer = null;
314
+
315
+ const launch = () => {
316
+ if (stopped) return;
317
+ child = spawn(spec.command, spec.args, {
318
+ cwd: RUNTIME_DIR,
319
+ env: spec.env,
320
+ stdio: ['ignore', 'pipe', 'pipe'],
321
+ windowsHide: true,
322
+ });
323
+
324
+ writePrefixed(
325
+ stream,
326
+ spec.name,
327
+ `started pid=${child.pid} command=${[spec.command, ...spec.args].join(' ')}\n`,
328
+ options.foreground ? process.stdout : null,
329
+ );
330
+
331
+ child.stdout.on('data', (chunk) => {
332
+ writePrefixed(stream, spec.name, chunk, options.foreground ? process.stdout : null);
333
+ });
334
+ child.stderr.on('data', (chunk) => {
335
+ writePrefixed(stream, spec.name, chunk, options.foreground ? process.stderr : null);
336
+ });
337
+ child.on('exit', (code, signal) => {
338
+ writePrefixed(
339
+ stream,
340
+ spec.name,
341
+ `exited code=${code ?? ''} signal=${signal ?? ''}\n`,
342
+ options.foreground ? process.stderr : null,
343
+ );
344
+ child = null;
345
+ if (!stopped) {
346
+ restartTimer = setTimeout(launch, 2000);
347
+ }
348
+ });
349
+ };
350
+
351
+ launch();
352
+
353
+ return {
354
+ stop() {
355
+ stopped = true;
356
+ if (restartTimer) clearTimeout(restartTimer);
357
+ if (child && !child.killed) {
358
+ try {
359
+ child.kill('SIGTERM');
360
+ } catch {
361
+ // noop
362
+ }
363
+ }
364
+ stream.end();
365
+ },
366
+ };
367
+ }
368
+
369
+ async function startSupervisor(options = {}) {
370
+ assertRuntimePresent();
371
+ ensureDir(AMALGM_DIR, 0o700);
372
+ ensureDir(LOG_DIR, 0o700);
373
+
374
+ const nativeResult = ensureNativeBinaries({
375
+ logger: console,
376
+ quiet: !options.foreground,
377
+ });
378
+ if (!nativeResult.ok) {
379
+ console.warn(`Could not install all agent binaries: ${nativeResult.error?.message || 'unknown error'}`);
380
+ const missing = nativeResult.after.filter((item) => !item.ok).map((item) => item.name);
381
+ if (missing.length > 0) console.warn(`Missing agents: ${missing.join(', ')}`);
382
+ } else if (nativeResult.installed && options.foreground) {
383
+ console.log('Agent binaries are ready.');
384
+ }
385
+ const shimResult = ensureAgentCommandShims({
386
+ logger: console,
387
+ quiet: !options.foreground,
388
+ });
389
+ if (!shimResult.ok) {
390
+ console.warn(`Could not expose all agent commands: ${shimResult.error?.message || 'unknown error'}`);
391
+ }
392
+
393
+ writeJsonSecret(PID_FILE, { pid: process.pid, started_at: new Date().toISOString() });
394
+
395
+ const runtimeToken = ensureRuntimeToken();
396
+ const storedRecord = loadComputerRecord({ migrate: true });
397
+ const record = {
398
+ ...(storedRecord || {}),
399
+ runtime_token: runtimeToken,
400
+ };
401
+ writeJsonSecret(RUNTIME_STATE_FILE, {
402
+ pid: process.pid,
403
+ supervisor_pid: process.pid,
404
+ source: 'npm',
405
+ package_version: PACKAGE_VERSION,
406
+ runtime_dir: RUNTIME_DIR,
407
+ computer_id: record?.computer_id || null,
408
+ device_id: record?.device_id || null,
409
+ event_ref: record?.event_ref || null,
410
+ local_only: !!options.localOnly,
411
+ started_at: new Date().toISOString(),
412
+ });
413
+ if (!storedRecord?.computer_id && options.foreground) {
414
+ console.warn('No registered computer found. Run `amalgm login` to enable event/chat tunnels.');
415
+ }
416
+
417
+ const ports = await resolveRuntimePorts();
418
+ Object.assign(process.env, {
419
+ AMALGM_GATEWAY_PORT: String(ports.gateway),
420
+ AMALGM_MCP_PORT: String(ports.amalgmMcp),
421
+ CHAT_SERVER_PORT: String(ports.chatServer),
422
+ FS_WATCHER_PORT: String(ports.fsWatcher),
423
+ PORT_MONITOR_PORT: String(ports.portMonitor),
424
+ });
425
+ const managed = serviceSpecs(record, ports).map((spec) => createManagedProcess(spec, options));
426
+ const tunnels = [];
427
+
428
+ if (!options.localOnly && record?.tunnel_url && record?.tunnel_token) {
429
+ const eventTunnel = createEventTunnel({ record, foreground: options.foreground });
430
+ eventTunnel.start();
431
+ tunnels.push(eventTunnel);
432
+ }
433
+ if (!options.localOnly && record?.tunnel_token) {
434
+ const chatTunnel = createChatTunnel({ record, foreground: options.foreground });
435
+ chatTunnel.start();
436
+ tunnels.push(chatTunnel);
437
+ }
438
+
439
+ if (options.foreground) {
440
+ console.log('Amalgm local runtime is running.');
441
+ console.log(`Gateway: http://127.0.0.1:${ports.gateway}`);
442
+ if (record?.computer_id) {
443
+ console.log(`Computer: ${record.computer_id}`);
444
+ if (record.webhook_url) console.log(`Events: ${record.webhook_url}`);
445
+ }
446
+ }
447
+
448
+ await new Promise((resolve) => {
449
+ let cleaned = false;
450
+ const cleanup = () => {
451
+ if (cleaned) return;
452
+ cleaned = true;
453
+ for (const tunnel of tunnels) tunnel.stop();
454
+ for (const proc of managed) proc.stop();
455
+ try {
456
+ const pidRecord = readJson(PID_FILE, null);
457
+ if (pidRecord?.pid === process.pid) fs.unlinkSync(PID_FILE);
458
+ } catch {
459
+ // noop
460
+ }
461
+ try {
462
+ const stateRecord = readJson(RUNTIME_STATE_FILE, null);
463
+ if (stateRecord?.pid === process.pid) fs.unlinkSync(RUNTIME_STATE_FILE);
464
+ } catch {
465
+ // noop
466
+ }
467
+ resolve();
468
+ };
469
+
470
+ process.once('SIGINT', cleanup);
471
+ process.once('SIGTERM', cleanup);
472
+ process.once('SIGHUP', cleanup);
473
+ });
474
+ }
475
+
476
+ module.exports = { startSupervisor };