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.
- package/README.md +41 -1
- package/bin/amalgm.js +8 -2
- package/lib/auth-store.js +223 -0
- package/lib/cli.js +1132 -0
- package/lib/paths.js +30 -0
- package/lib/supervisor.js +476 -0
- package/lib/tunnel-chat.js +328 -0
- package/lib/tunnel-events.js +499 -0
- package/package.json +30 -3
- package/runtime/README.md +4 -0
- package/runtime/lib/chatInput.js +315 -0
- package/runtime/lib/harnesses.js +988 -0
- package/runtime/lib/local/amalgmStore.js +136 -0
- package/runtime/lib/local/credentialResolver.js +425 -0
- package/runtime/lib/mcpApps/registry.js +619 -0
- package/runtime/package.json +5 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +144 -0
- package/runtime/scripts/amalgm-mcp/agents/store.js +684 -0
- package/runtime/scripts/amalgm-mcp/agents/talk.js +1162 -0
- package/runtime/scripts/amalgm-mcp/agents/tools.js +221 -0
- package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +157 -0
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
- package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
- package/runtime/scripts/amalgm-mcp/config.js +140 -0
- package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
- package/runtime/scripts/amalgm-mcp/deps.js +40 -0
- package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
- package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
- package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
- package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
- package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
- package/runtime/scripts/amalgm-mcp/events/store.js +113 -0
- package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
- package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
- package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
- package/runtime/scripts/amalgm-mcp/index.js +100 -0
- package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
- package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
- package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +441 -0
- package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
- package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
- package/runtime/scripts/amalgm-mcp/local/rest.js +87 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +234 -0
- package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +377 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +127 -0
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
- package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
- package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
- package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
- package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
- package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
- package/runtime/scripts/amalgm-mcp/tasks/store.js +154 -0
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
- package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
- package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
- package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +497 -0
- package/runtime/scripts/chat-core/adapters/claude.js +165 -0
- package/runtime/scripts/chat-core/adapters/codex.js +313 -0
- package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
- package/runtime/scripts/chat-core/auth.js +177 -0
- package/runtime/scripts/chat-core/contract.js +328 -0
- package/runtime/scripts/chat-core/credentials/store.js +212 -0
- package/runtime/scripts/chat-core/egress.js +87 -0
- package/runtime/scripts/chat-core/engine.js +253 -0
- package/runtime/scripts/chat-core/event-schema.js +231 -0
- package/runtime/scripts/chat-core/events.js +190 -0
- package/runtime/scripts/chat-core/index.js +11 -0
- package/runtime/scripts/chat-core/input.js +50 -0
- package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
- package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
- package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
- package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
- package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
- package/runtime/scripts/chat-core/parts.js +253 -0
- package/runtime/scripts/chat-core/recorder.js +65 -0
- package/runtime/scripts/chat-core/runtime.js +86 -0
- package/runtime/scripts/chat-core/server.js +163 -0
- package/runtime/scripts/chat-core/sse.js +196 -0
- package/runtime/scripts/chat-core/stores.js +100 -0
- package/runtime/scripts/chat-core/tool-display.js +149 -0
- package/runtime/scripts/chat-core/tool-shape.js +143 -0
- package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
- package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
- package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
- package/runtime/scripts/chat-core/usage.js +343 -0
- package/runtime/scripts/chat-server/config.js +110 -0
- package/runtime/scripts/chat-server/db.js +529 -0
- package/runtime/scripts/chat-server/index.js +33 -0
- package/runtime/scripts/chat-server/model-catalog.js +327 -0
- package/runtime/scripts/chat-server.js +75 -0
- package/runtime/scripts/credential-adapter.js +131 -0
- package/runtime/scripts/fs-watcher.js +888 -0
- package/runtime/scripts/local-gateway.js +854 -0
- package/runtime/scripts/platform-context.txt +246 -0
- package/runtime/scripts/port-monitor.js +175 -0
- package/runtime/scripts/proxy-token-store.js +162 -0
- package/runtime/scripts/runtime-auth.js +163 -0
- package/runtime/scripts/test-claude-code-models.js +87 -0
- package/runtime/tsconfig.json +15 -0
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { WebSocket } = require('ws');
|
|
8
|
+
|
|
9
|
+
const { AMALGM_DIR, RUNTIME_STATE_FILE } = require('./paths');
|
|
10
|
+
|
|
11
|
+
const DEFAULT_TARGET_PORT = 8083;
|
|
12
|
+
const CONNECT_TIMEOUT_MS = 20_000;
|
|
13
|
+
const WATCHDOG_INTERVAL_MS = 15_000;
|
|
14
|
+
const HEARTBEAT_TIMEOUT_MS = 75_000;
|
|
15
|
+
const CORE_TARGET_PORTS = new Set([8083, 8084]);
|
|
16
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
17
|
+
'connection',
|
|
18
|
+
'keep-alive',
|
|
19
|
+
'proxy-authenticate',
|
|
20
|
+
'proxy-authorization',
|
|
21
|
+
'te',
|
|
22
|
+
'trailer',
|
|
23
|
+
'trailers',
|
|
24
|
+
'transfer-encoding',
|
|
25
|
+
'upgrade',
|
|
26
|
+
'host',
|
|
27
|
+
'content-length',
|
|
28
|
+
]);
|
|
29
|
+
const WS_HANDSHAKE_HEADERS = new Set([
|
|
30
|
+
'connection',
|
|
31
|
+
'upgrade',
|
|
32
|
+
'host',
|
|
33
|
+
'sec-websocket-key',
|
|
34
|
+
'sec-websocket-version',
|
|
35
|
+
'sec-websocket-extensions',
|
|
36
|
+
'sec-websocket-accept',
|
|
37
|
+
'sec-websocket-protocol',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
function tunnelUrl(rawUrl) {
|
|
41
|
+
const url = new URL(rawUrl);
|
|
42
|
+
url.searchParams.delete('token');
|
|
43
|
+
return url.toString();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function tunnelHeaders(token) {
|
|
47
|
+
return {
|
|
48
|
+
authorization: `Bearer ${token}`,
|
|
49
|
+
'user-agent': `amalgm-npm/${require('../package.json').version} (${os.platform()}; ${os.arch()})`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function postPresence(record, online) {
|
|
54
|
+
if (!record?.app_url || !record?.tunnel_token || !record?.computer_id) return;
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(`${record.app_url}/api/computers/presence`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
authorization: `Bearer ${record.tunnel_token}`,
|
|
60
|
+
'content-type': 'application/json',
|
|
61
|
+
},
|
|
62
|
+
body: JSON.stringify({
|
|
63
|
+
computer_id: record.computer_id,
|
|
64
|
+
device_id: record.device_id || '',
|
|
65
|
+
online,
|
|
66
|
+
}),
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const text = await res.text().catch(() => '');
|
|
70
|
+
throw new Error(`${res.status} ${text || res.statusText}`);
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
const action = online ? 'online' : 'offline';
|
|
74
|
+
console.warn(`[event-tunnel] could not mark ${action}: ${error.message || error}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeHeaders(headers = {}) {
|
|
79
|
+
const out = {};
|
|
80
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
81
|
+
const lower = key.toLowerCase();
|
|
82
|
+
if (HOP_BY_HOP_HEADERS.has(lower)) continue;
|
|
83
|
+
if (Array.isArray(value)) out[key] = value.join(', ');
|
|
84
|
+
else if (typeof value === 'string') out[key] = value;
|
|
85
|
+
else if (value != null) out[key] = String(value);
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function responseHeaders(headers = {}) {
|
|
91
|
+
const out = {};
|
|
92
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
93
|
+
if (Array.isArray(value)) out[key] = value.join(', ');
|
|
94
|
+
else if (typeof value === 'string') out[key] = value;
|
|
95
|
+
else if (value != null) out[key] = String(value);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function bufferFromMessage(data) {
|
|
101
|
+
if (Buffer.isBuffer(data)) return data;
|
|
102
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
103
|
+
if (Array.isArray(data)) return Buffer.concat(data.map(bufferFromMessage));
|
|
104
|
+
return Buffer.from(String(data));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseProtocols(header) {
|
|
108
|
+
if (!header) return [];
|
|
109
|
+
return String(header).split(',').map((value) => value.trim()).filter(Boolean);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function sanitizePreviewWsHeaders(headers = {}) {
|
|
113
|
+
const out = {};
|
|
114
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
115
|
+
if (WS_HANDSHAKE_HEADERS.has(key.toLowerCase())) continue;
|
|
116
|
+
out[key] = value;
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readArtifactRoutes() {
|
|
122
|
+
const file = path.join(AMALGM_DIR, 'artifacts.json');
|
|
123
|
+
let parsed = null;
|
|
124
|
+
try {
|
|
125
|
+
parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const artifacts = Array.isArray(parsed?.artifacts) ? parsed.artifacts : [];
|
|
131
|
+
return artifacts
|
|
132
|
+
.filter((artifact) => {
|
|
133
|
+
const connected = artifact.dnsConnected !== false && artifact.connected !== false;
|
|
134
|
+
const desiredRunning = (artifact.desiredState || 'running') === 'running';
|
|
135
|
+
const active = artifact.status !== 'stopped' && artifact.status !== 'error';
|
|
136
|
+
return connected && desiredRunning && active && artifact.port && (artifact.artifactRef || artifact.artifact_ref);
|
|
137
|
+
})
|
|
138
|
+
.map((artifact) => ({
|
|
139
|
+
artifact_ref: artifact.artifactRef || artifact.artifact_ref,
|
|
140
|
+
port: Number(artifact.port),
|
|
141
|
+
name: artifact.name,
|
|
142
|
+
}))
|
|
143
|
+
.filter((route) =>
|
|
144
|
+
/^[a-z0-9]{8,24}$/.test(route.artifact_ref)
|
|
145
|
+
&& Number.isInteger(route.port)
|
|
146
|
+
&& route.port > 0
|
|
147
|
+
&& route.port <= 65535,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readRuntimePorts() {
|
|
152
|
+
const envPort = (name) => {
|
|
153
|
+
const port = Number(process.env[name]);
|
|
154
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : undefined;
|
|
155
|
+
};
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(fs.readFileSync(RUNTIME_STATE_FILE, 'utf8'));
|
|
158
|
+
return {
|
|
159
|
+
gateway: Number(parsed?.gateway_port || parsed?.ports?.gateway) || envPort('AMALGM_GATEWAY_PORT'),
|
|
160
|
+
mcp: Number(parsed?.ports?.amalgm_mcp) || envPort('AMALGM_MCP_PORT'),
|
|
161
|
+
chat: Number(parsed?.ports?.chat_server) || envPort('CHAT_SERVER_PORT'),
|
|
162
|
+
};
|
|
163
|
+
} catch {
|
|
164
|
+
return {
|
|
165
|
+
gateway: envPort('AMALGM_GATEWAY_PORT'),
|
|
166
|
+
mcp: envPort('AMALGM_MCP_PORT'),
|
|
167
|
+
chat: envPort('CHAT_SERVER_PORT'),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function allowedTargetPorts() {
|
|
173
|
+
const ports = new Set(CORE_TARGET_PORTS);
|
|
174
|
+
const runtimePorts = readRuntimePorts();
|
|
175
|
+
for (const port of [runtimePorts.gateway, runtimePorts.mcp, runtimePorts.chat]) {
|
|
176
|
+
if (Number.isInteger(port) && port > 0 && port <= 65535) ports.add(port);
|
|
177
|
+
}
|
|
178
|
+
for (const route of readArtifactRoutes()) ports.add(route.port);
|
|
179
|
+
return ports;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function defaultTargetPort() {
|
|
183
|
+
const port = readRuntimePorts().mcp;
|
|
184
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : DEFAULT_TARGET_PORT;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function runtimeGatewayPort() {
|
|
188
|
+
const port = readRuntimePorts().gateway;
|
|
189
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function resolveTargetPort(frame, fallbackPort = defaultTargetPort()) {
|
|
193
|
+
const candidate = frame.target_port == null ? fallbackPort : Number(frame.target_port);
|
|
194
|
+
if (!Number.isInteger(candidate) || candidate <= 0 || candidate > 65535) return null;
|
|
195
|
+
return allowedTargetPorts().has(candidate) ? candidate : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function createEventTunnel({ record, foreground = false }) {
|
|
199
|
+
let stopped = false;
|
|
200
|
+
let ws = null;
|
|
201
|
+
let reconnectTimer = null;
|
|
202
|
+
let routeTimer = null;
|
|
203
|
+
let watchdogTimer = null;
|
|
204
|
+
let backoffMs = 1000;
|
|
205
|
+
let connectStartedAt = 0;
|
|
206
|
+
let lastGatewayFrameAt = 0;
|
|
207
|
+
const upstreamSockets = new Map();
|
|
208
|
+
|
|
209
|
+
function log(message) {
|
|
210
|
+
const line = `[event-tunnel] ${message}`;
|
|
211
|
+
if (foreground) console.log(line);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function warn(message) {
|
|
215
|
+
const line = `[event-tunnel] ${message}`;
|
|
216
|
+
if (foreground) console.warn(line);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function send(frame) {
|
|
220
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return false;
|
|
221
|
+
ws.send(JSON.stringify(frame));
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function terminateSocket(reason) {
|
|
226
|
+
if (!ws) return;
|
|
227
|
+
warn(`${reason}; reconnecting`);
|
|
228
|
+
try {
|
|
229
|
+
ws.terminate();
|
|
230
|
+
} catch {
|
|
231
|
+
try {
|
|
232
|
+
ws.close();
|
|
233
|
+
} catch {
|
|
234
|
+
// noop
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function startWatchdog() {
|
|
240
|
+
if (watchdogTimer) return;
|
|
241
|
+
|
|
242
|
+
watchdogTimer = setInterval(() => {
|
|
243
|
+
if (stopped || !ws) return;
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
|
|
246
|
+
if (ws.readyState === WebSocket.CONNECTING) {
|
|
247
|
+
if (connectStartedAt && now - connectStartedAt > CONNECT_TIMEOUT_MS) {
|
|
248
|
+
terminateSocket('connect timeout');
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (ws.readyState !== WebSocket.OPEN) return;
|
|
254
|
+
|
|
255
|
+
send({
|
|
256
|
+
type: 'hello',
|
|
257
|
+
app_version: require('../package.json').version,
|
|
258
|
+
runtime_gateway_port: runtimeGatewayPort(),
|
|
259
|
+
artifact_routes: readArtifactRoutes(),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
ws.ping();
|
|
264
|
+
} catch {
|
|
265
|
+
terminateSocket('ping failed');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (lastGatewayFrameAt && now - lastGatewayFrameAt > HEARTBEAT_TIMEOUT_MS) {
|
|
270
|
+
terminateSocket('heartbeat timeout');
|
|
271
|
+
}
|
|
272
|
+
}, WATCHDOG_INTERVAL_MS);
|
|
273
|
+
|
|
274
|
+
if (typeof watchdogTimer.unref === 'function') watchdogTimer.unref();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function stopWatchdog() {
|
|
278
|
+
if (!watchdogTimer) return;
|
|
279
|
+
clearInterval(watchdogTimer);
|
|
280
|
+
watchdogTimer = null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function localHeaders(headers) {
|
|
284
|
+
const out = normalizeHeaders(headers);
|
|
285
|
+
const runtimeToken = record?.runtime_token || process.env.AMALGM_RUNTIME_TOKEN || '';
|
|
286
|
+
if (runtimeToken) out['x-amalgm-runtime-token'] = runtimeToken;
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function advertiseArtifacts() {
|
|
291
|
+
send({
|
|
292
|
+
type: 'artifact_routes',
|
|
293
|
+
runtime_gateway_port: runtimeGatewayPort(),
|
|
294
|
+
artifact_routes: readArtifactRoutes(),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function scheduleReconnect() {
|
|
299
|
+
if (stopped || reconnectTimer) return;
|
|
300
|
+
const delay = backoffMs;
|
|
301
|
+
backoffMs = Math.min(backoffMs * 1.5, 15000);
|
|
302
|
+
reconnectTimer = setTimeout(() => {
|
|
303
|
+
reconnectTimer = null;
|
|
304
|
+
connect();
|
|
305
|
+
}, delay);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function handleRequest(frame) {
|
|
309
|
+
const targetPort = resolveTargetPort(frame);
|
|
310
|
+
if (!targetPort) {
|
|
311
|
+
send({
|
|
312
|
+
type: 'res',
|
|
313
|
+
req_id: frame.req_id,
|
|
314
|
+
status: 403,
|
|
315
|
+
headers: { 'content-type': 'application/json' },
|
|
316
|
+
body_b64: Buffer.from(JSON.stringify({ error: 'Target port is not allowed' })).toString('base64'),
|
|
317
|
+
});
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const body = frame.body_b64 ? Buffer.from(frame.body_b64, 'base64') : Buffer.alloc(0);
|
|
321
|
+
const req = http.request(
|
|
322
|
+
{
|
|
323
|
+
hostname: '127.0.0.1',
|
|
324
|
+
port: targetPort,
|
|
325
|
+
method: frame.method || 'GET',
|
|
326
|
+
path: frame.path || '/',
|
|
327
|
+
headers: localHeaders(frame.headers),
|
|
328
|
+
},
|
|
329
|
+
(res) => {
|
|
330
|
+
const chunks = [];
|
|
331
|
+
res.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
|
332
|
+
res.on('end', () => {
|
|
333
|
+
send({
|
|
334
|
+
type: 'res',
|
|
335
|
+
req_id: frame.req_id,
|
|
336
|
+
status: res.statusCode || 502,
|
|
337
|
+
headers: responseHeaders(res.headers),
|
|
338
|
+
body_b64: Buffer.concat(chunks).toString('base64'),
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
req.on('error', (error) => {
|
|
345
|
+
send({
|
|
346
|
+
type: 'res',
|
|
347
|
+
req_id: frame.req_id,
|
|
348
|
+
status: 502,
|
|
349
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
350
|
+
body_b64: Buffer.from(error.message || 'Local request failed').toString('base64'),
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
req.end(body);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function openPreviewSocket(frame) {
|
|
358
|
+
const targetPort = resolveTargetPort(frame);
|
|
359
|
+
if (!targetPort) {
|
|
360
|
+
send({ type: 'ws_error', conn_id: frame.conn_id, message: 'Target port is not allowed' });
|
|
361
|
+
send({ type: 'ws_close', conn_id: frame.conn_id, code: 1008, reason: 'Target port is not allowed' });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const localUrl = `ws://127.0.0.1:${targetPort}${frame.path || '/'}`;
|
|
365
|
+
const protocols = parseProtocols(frame.headers?.['sec-websocket-protocol']);
|
|
366
|
+
const upstreamHeaders = localHeaders(sanitizePreviewWsHeaders(frame.headers));
|
|
367
|
+
const upstream = protocols.length > 0
|
|
368
|
+
? new WebSocket(localUrl, protocols, { headers: upstreamHeaders })
|
|
369
|
+
: new WebSocket(localUrl, { headers: upstreamHeaders });
|
|
370
|
+
upstreamSockets.set(frame.conn_id, upstream);
|
|
371
|
+
|
|
372
|
+
upstream.on('open', () => {
|
|
373
|
+
send({ type: 'ws_opened', conn_id: frame.conn_id, protocol: upstream.protocol || undefined });
|
|
374
|
+
});
|
|
375
|
+
upstream.on('message', (data, binary) => {
|
|
376
|
+
send({
|
|
377
|
+
type: 'ws_data',
|
|
378
|
+
conn_id: frame.conn_id,
|
|
379
|
+
data_b64: bufferFromMessage(data).toString('base64'),
|
|
380
|
+
binary: !!binary,
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
upstream.on('close', (code, reason) => {
|
|
384
|
+
upstreamSockets.delete(frame.conn_id);
|
|
385
|
+
send({
|
|
386
|
+
type: 'ws_close',
|
|
387
|
+
conn_id: frame.conn_id,
|
|
388
|
+
code,
|
|
389
|
+
reason: reason ? reason.toString() : '',
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
upstream.on('error', (error) => {
|
|
393
|
+
send({ type: 'ws_error', conn_id: frame.conn_id, message: error.message });
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function handleFrame(frame) {
|
|
398
|
+
if (!frame || typeof frame.type !== 'string') return;
|
|
399
|
+
if (frame.type === 'ping') {
|
|
400
|
+
send({ type: 'pong' });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (frame.type === 'req') {
|
|
404
|
+
handleRequest(frame);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (frame.type === 'ws_open') {
|
|
408
|
+
openPreviewSocket(frame);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (frame.type === 'ws_data') {
|
|
412
|
+
const upstream = upstreamSockets.get(frame.conn_id);
|
|
413
|
+
if (upstream && upstream.readyState === WebSocket.OPEN) {
|
|
414
|
+
upstream.send(Buffer.from(frame.data_b64 || '', 'base64'), { binary: frame.binary !== false });
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (frame.type === 'ws_close') {
|
|
419
|
+
const upstream = upstreamSockets.get(frame.conn_id);
|
|
420
|
+
if (upstream) upstream.close(frame.code || 1000, frame.reason || undefined);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function connect() {
|
|
425
|
+
if (stopped) return;
|
|
426
|
+
const url = tunnelUrl(record.tunnel_url);
|
|
427
|
+
ws = new WebSocket(url, {
|
|
428
|
+
headers: tunnelHeaders(record.tunnel_token),
|
|
429
|
+
});
|
|
430
|
+
connectStartedAt = Date.now();
|
|
431
|
+
startWatchdog();
|
|
432
|
+
|
|
433
|
+
ws.on('open', () => {
|
|
434
|
+
backoffMs = 1000;
|
|
435
|
+
connectStartedAt = 0;
|
|
436
|
+
lastGatewayFrameAt = Date.now();
|
|
437
|
+
log(`connected ${record.computer_id}`);
|
|
438
|
+
void postPresence(record, true);
|
|
439
|
+
send({
|
|
440
|
+
type: 'hello',
|
|
441
|
+
app_version: require('../package.json').version,
|
|
442
|
+
runtime_gateway_port: runtimeGatewayPort(),
|
|
443
|
+
artifact_routes: readArtifactRoutes(),
|
|
444
|
+
});
|
|
445
|
+
if (!routeTimer) {
|
|
446
|
+
routeTimer = setInterval(advertiseArtifacts, 30000);
|
|
447
|
+
if (typeof routeTimer.unref === 'function') routeTimer.unref();
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
ws.on('pong', () => {
|
|
451
|
+
lastGatewayFrameAt = Date.now();
|
|
452
|
+
});
|
|
453
|
+
ws.on('message', (data) => {
|
|
454
|
+
lastGatewayFrameAt = Date.now();
|
|
455
|
+
try {
|
|
456
|
+
handleFrame(JSON.parse(data.toString()));
|
|
457
|
+
} catch {
|
|
458
|
+
// Ignore malformed gateway frames.
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
ws.on('close', (code, reason) => {
|
|
462
|
+
connectStartedAt = 0;
|
|
463
|
+
lastGatewayFrameAt = 0;
|
|
464
|
+
warn(`closed code=${code} reason=${reason ? reason.toString() : ''}`);
|
|
465
|
+
scheduleReconnect();
|
|
466
|
+
});
|
|
467
|
+
ws.on('error', (error) => {
|
|
468
|
+
warn(error.message);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
start: connect,
|
|
474
|
+
stop() {
|
|
475
|
+
stopped = true;
|
|
476
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
477
|
+
if (routeTimer) clearInterval(routeTimer);
|
|
478
|
+
stopWatchdog();
|
|
479
|
+
for (const upstream of upstreamSockets.values()) {
|
|
480
|
+
try {
|
|
481
|
+
upstream.close();
|
|
482
|
+
} catch {
|
|
483
|
+
// noop
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
upstreamSockets.clear();
|
|
487
|
+
void postPresence(record, false);
|
|
488
|
+
if (ws) {
|
|
489
|
+
try {
|
|
490
|
+
ws.close();
|
|
491
|
+
} catch {
|
|
492
|
+
// noop
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
module.exports = { createEventTunnel };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amalgm",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.32",
|
|
4
|
+
"description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
7
7
|
"bin": {
|
|
@@ -9,6 +9,33 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin",
|
|
12
|
+
"lib",
|
|
13
|
+
"runtime",
|
|
12
14
|
"README.md"
|
|
13
|
-
]
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"sync-runtime": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
18
|
+
"prepack": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
19
|
+
"pack:dry": "npm pack --dry-run",
|
|
20
|
+
"check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/fs-watcher.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@ai-sdk/gateway": "^3.0.107",
|
|
27
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.122",
|
|
28
|
+
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
30
|
+
"@openai/codex": "^0.128.0",
|
|
31
|
+
"@opencode-ai/sdk": "^1.14.29",
|
|
32
|
+
"ai": "^6.0.116",
|
|
33
|
+
"better-sqlite3": "^12.10.0",
|
|
34
|
+
"cron-parser": "^5.5.0",
|
|
35
|
+
"opencode-ai": "^1.14.35",
|
|
36
|
+
"playwright-core": "^1.59.1",
|
|
37
|
+
"tsx": "^4.21.0",
|
|
38
|
+
"ws": "^8.18.3",
|
|
39
|
+
"zod": "^4.1.8"
|
|
40
|
+
}
|
|
14
41
|
}
|