amalgm 0.1.51 → 0.1.53
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/tunnel-events.js +48 -23
- package/package.json +2 -2
- package/runtime/lib/harnesses.js +12 -4
- package/runtime/scripts/amalgm-mcp/agents/store.js +5 -5
- package/runtime/scripts/amalgm-mcp/{artifacts → apps}/advertise.js +39 -24
- package/runtime/scripts/amalgm-mcp/apps/rest.js +144 -0
- package/runtime/scripts/amalgm-mcp/apps/store.js +171 -0
- package/runtime/scripts/amalgm-mcp/apps/supervisor.js +439 -0
- package/runtime/scripts/amalgm-mcp/apps/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/automations/cell-references.js +237 -0
- package/runtime/scripts/amalgm-mcp/automations/context.js +41 -0
- package/runtime/scripts/amalgm-mcp/automations/rest.js +148 -0
- package/runtime/scripts/amalgm-mcp/automations/runner.js +613 -0
- package/runtime/scripts/amalgm-mcp/automations/scheduler.js +90 -0
- package/runtime/scripts/amalgm-mcp/automations/store.js +1125 -0
- package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +177 -0
- package/runtime/scripts/amalgm-mcp/automations/tools.js +418 -0
- package/runtime/scripts/amalgm-mcp/automations/validator.js +225 -0
- package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +547 -0
- package/runtime/scripts/amalgm-mcp/browser/electron-bridge.js +222 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +13 -631
- package/runtime/scripts/amalgm-mcp/browser/tools.js +9 -7
- package/runtime/scripts/amalgm-mcp/config.js +33 -48
- package/runtime/scripts/amalgm-mcp/deps.js +1 -31
- package/runtime/scripts/amalgm-mcp/events/ingress.js +50 -42
- package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +169 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +45 -14
- package/runtime/scripts/amalgm-mcp/events/store.js +106 -57
- package/runtime/scripts/amalgm-mcp/index.js +12 -14
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +229 -65
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +13 -27
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +2 -3
- package/runtime/scripts/amalgm-mcp/server/http.js +106 -56
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +1 -1
- package/runtime/scripts/amalgm-mcp/state/db.js +119 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +16 -3
- package/runtime/scripts/amalgm-mcp/tasks/executor.js +1 -1
- package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +348 -0
- package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +23 -0
- package/runtime/scripts/amalgm-mcp/tests/workflows-store-runner.test.js +67 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +16 -3
- package/runtime/scripts/amalgm-mcp/workflows/compiler.js +222 -0
- package/runtime/scripts/amalgm-mcp/workflows/runner.js +593 -0
- package/runtime/scripts/amalgm-mcp/workflows/store.js +237 -0
- package/runtime/scripts/chat-core/adapters/claude.js +2 -1
- package/runtime/scripts/chat-core/auth.js +82 -12
- package/runtime/scripts/chat-core/contract.js +5 -1
- package/runtime/scripts/chat-core/engine.js +103 -62
- package/runtime/scripts/chat-core/event-schema.js +8 -0
- package/runtime/scripts/chat-core/events.js +5 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +13 -1
- package/runtime/scripts/chat-core/parts.js +21 -6
- package/runtime/scripts/chat-core/sse.js +3 -0
- package/runtime/scripts/chat-core/tests/auth.test.js +84 -6
- package/runtime/scripts/chat-core/tests/engine.test.js +312 -0
- package/runtime/scripts/chat-core/tests/native-config.test.js +23 -0
- package/runtime/scripts/chat-core/tool-shape.js +4 -4
- package/runtime/scripts/chat-core/tooling/active-memory.js +5 -4
- package/runtime/scripts/chat-core/tooling/native-binaries.js +34 -9
- package/runtime/scripts/chat-core/tooling/native-config.js +34 -3
- package/runtime/scripts/local-gateway.js +34 -27
- package/runtime/scripts/platform-context.txt +76 -94
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +0 -103
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +0 -157
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +0 -439
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +0 -176
- package/runtime/scripts/amalgm-mcp/events/executor.js +0 -258
- package/runtime/scripts/amalgm-mcp/events/rest.js +0 -214
- package/runtime/scripts/amalgm-mcp/events/tools.js +0 -323
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +0 -110
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +0 -416
package/lib/tunnel-events.js
CHANGED
|
@@ -122,36 +122,55 @@ function sanitizePreviewWsHeaders(headers = {}) {
|
|
|
122
122
|
return out;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
function
|
|
126
|
-
const
|
|
125
|
+
function readAppsFile() {
|
|
126
|
+
const appsFile = path.join(AMALGM_DIR, 'apps.json');
|
|
127
|
+
const legacyArtifactsFile = path.join(AMALGM_DIR, 'artifacts.json');
|
|
127
128
|
let parsed = null;
|
|
128
129
|
try {
|
|
129
|
-
parsed = JSON.parse(fs.readFileSync(
|
|
130
|
+
parsed = JSON.parse(fs.readFileSync(appsFile, 'utf8'));
|
|
130
131
|
} catch {
|
|
131
|
-
|
|
132
|
+
try {
|
|
133
|
+
parsed = JSON.parse(fs.readFileSync(legacyArtifactsFile, 'utf8'));
|
|
134
|
+
} catch {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
132
137
|
}
|
|
133
138
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
return Array.isArray(parsed?.apps)
|
|
140
|
+
? parsed.apps
|
|
141
|
+
: (Array.isArray(parsed?.artifacts) ? parsed.artifacts : []);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readAppRoutes() {
|
|
145
|
+
const apps = readAppsFile();
|
|
146
|
+
return apps
|
|
147
|
+
.filter((app) => {
|
|
148
|
+
const connected = app.dnsConnected !== false && app.connected !== false;
|
|
149
|
+
const desiredRunning = (app.desiredState || 'running') === 'running';
|
|
150
|
+
const active = app.status !== 'stopped' && app.status !== 'error';
|
|
151
|
+
return connected && desiredRunning && active && app.port && (app.appRef || app.app_ref || app.artifactRef || app.artifact_ref);
|
|
141
152
|
})
|
|
142
|
-
.map((
|
|
143
|
-
|
|
144
|
-
port: Number(
|
|
145
|
-
name:
|
|
153
|
+
.map((app) => ({
|
|
154
|
+
app_ref: app.appRef || app.app_ref || app.artifactRef || app.artifact_ref,
|
|
155
|
+
port: Number(app.port),
|
|
156
|
+
name: app.name,
|
|
146
157
|
}))
|
|
147
158
|
.filter((route) =>
|
|
148
|
-
/^[a-z0-9]{8,24}$/.test(route.
|
|
159
|
+
/^[a-z0-9]{8,24}$/.test(route.app_ref)
|
|
149
160
|
&& Number.isInteger(route.port)
|
|
150
161
|
&& route.port > 0
|
|
151
162
|
&& route.port <= 65535,
|
|
152
163
|
);
|
|
153
164
|
}
|
|
154
165
|
|
|
166
|
+
function legacyArtifactRoutes(routes) {
|
|
167
|
+
return routes.map((route) => ({
|
|
168
|
+
artifact_ref: route.app_ref,
|
|
169
|
+
port: route.port,
|
|
170
|
+
name: route.name,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
|
|
155
174
|
function readRuntimePorts() {
|
|
156
175
|
try {
|
|
157
176
|
const parsed = JSON.parse(fs.readFileSync(RUNTIME_STATE_FILE, 'utf8'));
|
|
@@ -175,7 +194,7 @@ function allowedTargetPorts() {
|
|
|
175
194
|
for (const port of [runtimePorts.gateway, runtimePorts.mcp, runtimePorts.chat]) {
|
|
176
195
|
if (Number.isInteger(port) && port > 0 && port <= 65535) ports.add(port);
|
|
177
196
|
}
|
|
178
|
-
for (const route of
|
|
197
|
+
for (const route of readAppRoutes()) ports.add(route.port);
|
|
179
198
|
return ports;
|
|
180
199
|
}
|
|
181
200
|
|
|
@@ -252,11 +271,13 @@ function createEventTunnel({ record, foreground = false }) {
|
|
|
252
271
|
|
|
253
272
|
if (ws.readyState !== WebSocket.OPEN) return;
|
|
254
273
|
|
|
274
|
+
const appRoutes = readAppRoutes();
|
|
255
275
|
send({
|
|
256
276
|
type: 'hello',
|
|
257
277
|
app_version: require('../package.json').version,
|
|
258
278
|
runtime_gateway_port: runtimeGatewayPort(),
|
|
259
|
-
|
|
279
|
+
app_routes: appRoutes,
|
|
280
|
+
artifact_routes: legacyArtifactRoutes(appRoutes),
|
|
260
281
|
});
|
|
261
282
|
|
|
262
283
|
try {
|
|
@@ -287,11 +308,13 @@ function createEventTunnel({ record, foreground = false }) {
|
|
|
287
308
|
return out;
|
|
288
309
|
}
|
|
289
310
|
|
|
290
|
-
function
|
|
311
|
+
function advertiseApps() {
|
|
312
|
+
const appRoutes = readAppRoutes();
|
|
291
313
|
send({
|
|
292
|
-
type: '
|
|
314
|
+
type: 'app_routes',
|
|
293
315
|
runtime_gateway_port: runtimeGatewayPort(),
|
|
294
|
-
|
|
316
|
+
app_routes: appRoutes,
|
|
317
|
+
artifact_routes: legacyArtifactRoutes(appRoutes),
|
|
295
318
|
});
|
|
296
319
|
}
|
|
297
320
|
|
|
@@ -436,14 +459,16 @@ function createEventTunnel({ record, foreground = false }) {
|
|
|
436
459
|
lastGatewayFrameAt = Date.now();
|
|
437
460
|
log(`connected ${record.computer_id}`);
|
|
438
461
|
void postPresence(record, true);
|
|
462
|
+
const appRoutes = readAppRoutes();
|
|
439
463
|
send({
|
|
440
464
|
type: 'hello',
|
|
441
465
|
app_version: require('../package.json').version,
|
|
442
466
|
runtime_gateway_port: runtimeGatewayPort(),
|
|
443
|
-
|
|
467
|
+
app_routes: appRoutes,
|
|
468
|
+
artifact_routes: legacyArtifactRoutes(appRoutes),
|
|
444
469
|
});
|
|
445
470
|
if (!routeTimer) {
|
|
446
|
-
routeTimer = setInterval(
|
|
471
|
+
routeTimer = setInterval(advertiseApps, 30000);
|
|
447
472
|
if (typeof routeTimer.unref === 'function') routeTimer.unref();
|
|
448
473
|
}
|
|
449
474
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amalgm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.53",
|
|
4
4
|
"description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
30
30
|
"@openai/codex": "^0.128.0",
|
|
31
31
|
"@opencode-ai/sdk": "^1.14.29",
|
|
32
|
+
"agent-browser": "^0.27.0",
|
|
32
33
|
"ai": "^6.0.116",
|
|
33
34
|
"better-sqlite3": "^12.10.0",
|
|
34
35
|
"cron-parser": "^5.5.0",
|
|
35
36
|
"opencode-ai": "^1.14.35",
|
|
36
|
-
"playwright-core": "^1.59.1",
|
|
37
37
|
"tsx": "^4.21.0",
|
|
38
38
|
"ws": "^8.18.3",
|
|
39
39
|
"zod": "^4.1.8"
|
package/runtime/lib/harnesses.js
CHANGED
|
@@ -56,8 +56,8 @@ function buildGatewayCliModel(gatewayId, harness) {
|
|
|
56
56
|
// lib/modelNaming.ts
|
|
57
57
|
var DEFAULT_MODEL_IDS = {
|
|
58
58
|
claude_code: "anthropic/claude-opus-4.7",
|
|
59
|
-
codex: "openai/gpt-5.
|
|
60
|
-
opencode: "opencode-
|
|
59
|
+
codex: "openai/gpt-5.5",
|
|
60
|
+
opencode: "opencode-deepseek-deepseek-v4-pro",
|
|
61
61
|
pi: "pi-anthropic-claude-sonnet-4.6",
|
|
62
62
|
amp: "amp-default",
|
|
63
63
|
cursor: "cursor/composer-2-fast"
|
|
@@ -492,6 +492,14 @@ var OPENCODE_MODELS = [
|
|
|
492
492
|
providerGroup: "xai"
|
|
493
493
|
},
|
|
494
494
|
// ── DeepSeek ──
|
|
495
|
+
{
|
|
496
|
+
id: DEFAULT_MODEL_IDS.opencode,
|
|
497
|
+
name: "DeepSeek V4 Pro",
|
|
498
|
+
description: "DeepSeek flagship model",
|
|
499
|
+
cliModel: buildGatewayCliModel("deepseek/deepseek-v4-pro", "opencode"),
|
|
500
|
+
provider: "opencode",
|
|
501
|
+
providerGroup: "deepseek"
|
|
502
|
+
},
|
|
495
503
|
{
|
|
496
504
|
id: "opencode-deepseek-deepseek-v3.2",
|
|
497
505
|
name: "DeepSeek V3.2",
|
|
@@ -765,7 +773,7 @@ var HARNESSES = {
|
|
|
765
773
|
description: "Open-source coding agent",
|
|
766
774
|
logoKey: "opencode",
|
|
767
775
|
models: OPENCODE_MODELS,
|
|
768
|
-
defaultModelId:
|
|
776
|
+
defaultModelId: DEFAULT_MODEL_IDS.opencode,
|
|
769
777
|
supportedAuthMethods: ["amalgm", "byok"],
|
|
770
778
|
dynamicModels: true
|
|
771
779
|
},
|
|
@@ -916,7 +924,7 @@ function resolveHarnessModel(harnessId, modelId) {
|
|
|
916
924
|
return {
|
|
917
925
|
modelId: normalizedModelId,
|
|
918
926
|
cliModel: model?.cliModel ?? adaptedModel,
|
|
919
|
-
reasoningEffort: selection?.reasoningEffort ?? model?.reasoningEffort
|
|
927
|
+
reasoningEffort: selection?.reasoningEffort ?? model?.reasoningEffort ?? (harnessId === "codex" ? "xhigh" : void 0)
|
|
920
928
|
};
|
|
921
929
|
}
|
|
922
930
|
function getAuthMethodsForHarness(harnessId) {
|
|
@@ -50,7 +50,7 @@ const BUILTIN_AGENT_BLUEPRINTS = [
|
|
|
50
50
|
description: 'OpenAI Codex agent — code generation and analysis',
|
|
51
51
|
adapter: 'codex',
|
|
52
52
|
baseHarnessId: 'codex',
|
|
53
|
-
baseModelId: 'openai/gpt-5.
|
|
53
|
+
baseModelId: 'openai/gpt-5.5',
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
id: 'opencode',
|
|
@@ -58,7 +58,7 @@ const BUILTIN_AGENT_BLUEPRINTS = [
|
|
|
58
58
|
description: 'Open-source agent — multi-provider coding assistant',
|
|
59
59
|
adapter: 'opencode',
|
|
60
60
|
baseHarnessId: 'opencode',
|
|
61
|
-
baseModelId: 'opencode-
|
|
61
|
+
baseModelId: 'opencode-deepseek-deepseek-v4-pro',
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
id: 'pi',
|
|
@@ -85,7 +85,7 @@ const BUILTIN_AGENTS = BUILTIN_AGENT_BLUEPRINTS.map((agent) => ({
|
|
|
85
85
|
status: DEFAULT_AGENT_STATUS,
|
|
86
86
|
}));
|
|
87
87
|
|
|
88
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{12}$/i;
|
|
88
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
89
89
|
|
|
90
90
|
function nowIso() {
|
|
91
91
|
return new Date().toISOString();
|
|
@@ -406,7 +406,7 @@ function seedBuiltinAgents(options = {}) {
|
|
|
406
406
|
description: existing?.description ?? blueprint.description,
|
|
407
407
|
adapter: existing?.adapter || blueprint.adapter,
|
|
408
408
|
baseHarnessId: existing?.baseHarnessId || blueprint.baseHarnessId,
|
|
409
|
-
baseModelId:
|
|
409
|
+
baseModelId: blueprint.baseModelId,
|
|
410
410
|
systemPrompt: existing?.systemPrompt || '',
|
|
411
411
|
files: existing?.files || [],
|
|
412
412
|
skills: existing?.skills || [],
|
|
@@ -423,7 +423,7 @@ function seedBuiltinAgents(options = {}) {
|
|
|
423
423
|
authDetails: existing?.authDetails,
|
|
424
424
|
configDir: existing?.configDir,
|
|
425
425
|
createdAt: existing?.createdAt,
|
|
426
|
-
authMethod:
|
|
426
|
+
authMethod: persistedAuthMethod(existing?.baseHarnessId || blueprint.baseHarnessId),
|
|
427
427
|
}, existing);
|
|
428
428
|
if (existing && agentsEqualForWrite(existing, next)) continue;
|
|
429
429
|
upsertAgentRow(db, next);
|
|
@@ -3,7 +3,7 @@ const {
|
|
|
3
3
|
COMPUTER_RECORD_FILE,
|
|
4
4
|
GATEWAY_BASE_URL,
|
|
5
5
|
GATEWAY_INTERNAL_SECRET,
|
|
6
|
-
|
|
6
|
+
APP_ROUTE_SYNC_INTERVAL_MS,
|
|
7
7
|
} = require('../config');
|
|
8
8
|
const { readJson } = require('../lib/storage');
|
|
9
9
|
const { getConnectedRoutes } = require('./store');
|
|
@@ -31,19 +31,34 @@ function loadComputerRecord() {
|
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
async function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
body: JSON.stringify({ routes }),
|
|
44
|
-
},
|
|
34
|
+
async function postAppRoutes(computerId, routes, authHeaders) {
|
|
35
|
+
const body = JSON.stringify({ routes });
|
|
36
|
+
const headers = {
|
|
37
|
+
'content-type': 'application/json',
|
|
38
|
+
...authHeaders,
|
|
39
|
+
};
|
|
40
|
+
let response = await fetch(
|
|
41
|
+
`${GATEWAY_BASE_URL}/internal/computers/${encodeURIComponent(computerId)}/app-routes`,
|
|
42
|
+
{ method: 'POST', headers, body },
|
|
45
43
|
);
|
|
46
44
|
|
|
45
|
+
if (response.status === 404) {
|
|
46
|
+
response = await fetch(
|
|
47
|
+
`${GATEWAY_BASE_URL}/internal/computers/${encodeURIComponent(computerId)}/artifact-routes`,
|
|
48
|
+
{
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers,
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
routes: routes.map((route) => ({
|
|
53
|
+
artifact_ref: route.app_ref,
|
|
54
|
+
port: route.port,
|
|
55
|
+
name: route.name,
|
|
56
|
+
})),
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
47
62
|
if (response.ok) {
|
|
48
63
|
lastFailure = '';
|
|
49
64
|
return response.json().catch(() => ({ ok: true, routesCount: routes.length }));
|
|
@@ -53,7 +68,7 @@ async function postArtifactRoutes(computerId, routes, authHeaders) {
|
|
|
53
68
|
throw new Error(`Gateway route sync failed (${response.status}): ${text.slice(0, 300)}`);
|
|
54
69
|
}
|
|
55
70
|
|
|
56
|
-
function
|
|
71
|
+
function syncAppRoutesToGateway(reason = 'manual', options = {}) {
|
|
57
72
|
const { throwOnError = false } = options;
|
|
58
73
|
|
|
59
74
|
syncQueue = syncQueue
|
|
@@ -67,7 +82,7 @@ function syncArtifactRoutesToGateway(reason = 'manual', options = {}) {
|
|
|
67
82
|
if (!computerId) {
|
|
68
83
|
warnOnce(
|
|
69
84
|
'missing-computer',
|
|
70
|
-
`[AmalgmMCP] Skipping
|
|
85
|
+
`[AmalgmMCP] Skipping app route sync: missing computer_id in ${COMPUTER_RECORD_FILE}.`,
|
|
71
86
|
);
|
|
72
87
|
return { ok: false, skipped: 'missing-computer' };
|
|
73
88
|
}
|
|
@@ -82,7 +97,7 @@ function syncArtifactRoutesToGateway(reason = 'manual', options = {}) {
|
|
|
82
97
|
if (!authHeaders) {
|
|
83
98
|
warnOnce(
|
|
84
99
|
'missing-auth',
|
|
85
|
-
'[AmalgmMCP] Skipping
|
|
100
|
+
'[AmalgmMCP] Skipping app route sync: neither tunnel_token nor AMALGM_GATEWAY_INTERNAL_SECRET is configured.',
|
|
86
101
|
);
|
|
87
102
|
return { ok: false, skipped: 'missing-auth' };
|
|
88
103
|
}
|
|
@@ -90,17 +105,17 @@ function syncArtifactRoutesToGateway(reason = 'manual', options = {}) {
|
|
|
90
105
|
const routes = getConnectedRoutes();
|
|
91
106
|
|
|
92
107
|
try {
|
|
93
|
-
const payload = await
|
|
108
|
+
const payload = await postAppRoutes(computerId, routes, authHeaders);
|
|
94
109
|
if (reason !== 'interval') {
|
|
95
110
|
console.log(
|
|
96
|
-
`[AmalgmMCP] Synced
|
|
111
|
+
`[AmalgmMCP] Synced app routes (${reason}): computer=${computerId} count=${routes.length}`,
|
|
97
112
|
);
|
|
98
113
|
}
|
|
99
114
|
return { ok: true, computerId, routes, payload };
|
|
100
115
|
} catch (err) {
|
|
101
116
|
const message = err instanceof Error ? err.message : String(err);
|
|
102
117
|
if (message !== lastFailure || reason !== 'interval') {
|
|
103
|
-
console.warn(`[AmalgmMCP]
|
|
118
|
+
console.warn(`[AmalgmMCP] App route sync failed (${reason}): ${message}`);
|
|
104
119
|
}
|
|
105
120
|
lastFailure = message;
|
|
106
121
|
if (throwOnError) throw err;
|
|
@@ -111,13 +126,13 @@ function syncArtifactRoutesToGateway(reason = 'manual', options = {}) {
|
|
|
111
126
|
return syncQueue;
|
|
112
127
|
}
|
|
113
128
|
|
|
114
|
-
function
|
|
129
|
+
function startAppRouteSyncLoop() {
|
|
115
130
|
if (routeSyncTimer) return routeSyncTimer;
|
|
116
131
|
|
|
117
|
-
void
|
|
132
|
+
void syncAppRoutesToGateway('boot');
|
|
118
133
|
routeSyncTimer = setInterval(() => {
|
|
119
|
-
void
|
|
120
|
-
},
|
|
134
|
+
void syncAppRoutesToGateway('interval');
|
|
135
|
+
}, APP_ROUTE_SYNC_INTERVAL_MS);
|
|
121
136
|
|
|
122
137
|
if (typeof routeSyncTimer.unref === 'function') {
|
|
123
138
|
routeSyncTimer.unref();
|
|
@@ -127,6 +142,6 @@ function startArtifactRouteSyncLoop() {
|
|
|
127
142
|
}
|
|
128
143
|
|
|
129
144
|
module.exports = {
|
|
130
|
-
|
|
131
|
-
|
|
145
|
+
startAppRouteSyncLoop,
|
|
146
|
+
syncAppRoutesToGateway,
|
|
132
147
|
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /apps/* REST routes for the Next.js API and local tunnel runtime.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { getConnectedRoutes, loadApps } = require('./store');
|
|
6
|
+
const {
|
|
7
|
+
deleteApp,
|
|
8
|
+
connectDns,
|
|
9
|
+
disconnectDns,
|
|
10
|
+
redeployApp,
|
|
11
|
+
registerApp,
|
|
12
|
+
startApp,
|
|
13
|
+
stopApp,
|
|
14
|
+
} = require('./supervisor');
|
|
15
|
+
|
|
16
|
+
function appIdFromBody(body) {
|
|
17
|
+
return body?.app_id || body?.appId || body?.artifact_id || body?.artifactId;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function legacyApp(app) {
|
|
21
|
+
if (!app) return app;
|
|
22
|
+
return {
|
|
23
|
+
...app,
|
|
24
|
+
kind: app.kind === 'app' ? 'artifact' : app.kind,
|
|
25
|
+
artifactRef: app.appRef,
|
|
26
|
+
artifact_ref: app.app_ref,
|
|
27
|
+
artifactUrl: app.appUrl || app.publicUrl,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function appPayload(app, options = {}) {
|
|
32
|
+
return options.legacy ? { artifact: legacyApp(app) } : { app };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function handleList(sendJson, options = {}) {
|
|
36
|
+
const data = loadApps();
|
|
37
|
+
if (options.legacy) {
|
|
38
|
+
return sendJson(200, {
|
|
39
|
+
version: data.version,
|
|
40
|
+
artifacts: data.apps.map(legacyApp),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
sendJson(200, data);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function handleRoutes(sendJson, options = {}) {
|
|
47
|
+
const routes = getConnectedRoutes();
|
|
48
|
+
sendJson(200, {
|
|
49
|
+
routes: options.legacy
|
|
50
|
+
? routes.map((route) => ({
|
|
51
|
+
artifact_ref: route.app_ref,
|
|
52
|
+
port: route.port,
|
|
53
|
+
name: route.name,
|
|
54
|
+
}))
|
|
55
|
+
: routes,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function handleRegister(body, sendJson, options = {}) {
|
|
60
|
+
try {
|
|
61
|
+
const app = await registerApp(body || {});
|
|
62
|
+
sendJson(200, appPayload(app, options));
|
|
63
|
+
} catch (err) {
|
|
64
|
+
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function handleRedeploy(body, sendJson, options = {}) {
|
|
69
|
+
try {
|
|
70
|
+
const appId = appIdFromBody(body);
|
|
71
|
+
if (!appId) return sendJson(400, { error: 'app_id is required' });
|
|
72
|
+
const app = await redeployApp(appId, body);
|
|
73
|
+
sendJson(200, appPayload(app, options));
|
|
74
|
+
} catch (err) {
|
|
75
|
+
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function handleStart(body, sendJson, options = {}) {
|
|
80
|
+
try {
|
|
81
|
+
const appId = appIdFromBody(body);
|
|
82
|
+
if (!appId) return sendJson(400, { error: 'app_id is required' });
|
|
83
|
+
const app = await startApp(appId);
|
|
84
|
+
sendJson(200, appPayload(app, options));
|
|
85
|
+
} catch (err) {
|
|
86
|
+
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function handleStop(body, sendJson, options = {}) {
|
|
91
|
+
try {
|
|
92
|
+
const appId = appIdFromBody(body);
|
|
93
|
+
if (!appId) return sendJson(400, { error: 'app_id is required' });
|
|
94
|
+
const app = await stopApp(appId);
|
|
95
|
+
sendJson(200, appPayload(app, options));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function handleDelete(body, sendJson, options = {}) {
|
|
102
|
+
try {
|
|
103
|
+
const appId = appIdFromBody(body);
|
|
104
|
+
if (!appId) return sendJson(400, { error: 'app_id is required' });
|
|
105
|
+
const app = await deleteApp(appId);
|
|
106
|
+
sendJson(200, appPayload(app, options));
|
|
107
|
+
} catch (err) {
|
|
108
|
+
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function handleConnectDns(body, sendJson, options = {}) {
|
|
113
|
+
try {
|
|
114
|
+
const appId = appIdFromBody(body);
|
|
115
|
+
if (!appId) return sendJson(400, { error: 'app_id is required' });
|
|
116
|
+
const app = await connectDns(appId);
|
|
117
|
+
sendJson(200, appPayload(app, options));
|
|
118
|
+
} catch (err) {
|
|
119
|
+
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function handleDisconnectDns(body, sendJson, options = {}) {
|
|
124
|
+
try {
|
|
125
|
+
const appId = appIdFromBody(body);
|
|
126
|
+
if (!appId) return sendJson(400, { error: 'app_id is required' });
|
|
127
|
+
const app = await disconnectDns(appId);
|
|
128
|
+
sendJson(200, appPayload(app, options));
|
|
129
|
+
} catch (err) {
|
|
130
|
+
sendJson(400, { error: err instanceof Error ? err.message : String(err) });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = {
|
|
135
|
+
handleConnectDns,
|
|
136
|
+
handleDelete,
|
|
137
|
+
handleDisconnectDns,
|
|
138
|
+
handleList,
|
|
139
|
+
handleRedeploy,
|
|
140
|
+
handleRegister,
|
|
141
|
+
handleRoutes,
|
|
142
|
+
handleStart,
|
|
143
|
+
handleStop,
|
|
144
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App storage — ~/.amalgm/apps.json
|
|
3
|
+
*
|
|
4
|
+
* This is intentionally local-first. The laptop volume is the source of truth;
|
|
5
|
+
* the public gateway only gets ephemeral route advertisements while the
|
|
6
|
+
* computer is connected.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const {
|
|
11
|
+
APPS_DIR,
|
|
12
|
+
APPS_FILE,
|
|
13
|
+
APPS_DOMAIN,
|
|
14
|
+
APP_PORT_MIN,
|
|
15
|
+
APP_PORT_MAX,
|
|
16
|
+
LEGACY_ARTIFACTS_FILE,
|
|
17
|
+
} = require('../config');
|
|
18
|
+
const { ensureDir, readJson, writeJsonAtomic } = require('../lib/storage');
|
|
19
|
+
const { appendStateEvent } = require('../state/events');
|
|
20
|
+
|
|
21
|
+
function ensureAppsDirs() {
|
|
22
|
+
ensureDir(APPS_DIR);
|
|
23
|
+
if (!readJson(APPS_FILE, null)) {
|
|
24
|
+
const legacy = readJson(LEGACY_ARTIFACTS_FILE, null);
|
|
25
|
+
const legacyApps = Array.isArray(legacy?.artifacts) ? legacy.artifacts : [];
|
|
26
|
+
writeJsonAtomic(APPS_FILE, { version: 1, apps: legacyApps.map(normalizeApp) });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function publicUrlForRef(appRef) {
|
|
31
|
+
return `https://${appRef}.${APPS_DOMAIN}/`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function genAppRef() {
|
|
35
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
36
|
+
const bytes = crypto.randomBytes(12);
|
|
37
|
+
let out = '';
|
|
38
|
+
for (let i = 0; i < 12; i += 1) {
|
|
39
|
+
out += chars[bytes[i] % chars.length];
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeApp(app) {
|
|
45
|
+
const appRef = app.appRef || app.app_ref || app.artifactRef || app.artifact_ref || genAppRef();
|
|
46
|
+
const savedUrl = app.appUrl || app.publicUrl || '';
|
|
47
|
+
const publicUrl = savedUrl && !String(savedUrl).includes('.artifacts.')
|
|
48
|
+
? savedUrl
|
|
49
|
+
: publicUrlForRef(appRef);
|
|
50
|
+
const dnsConnected =
|
|
51
|
+
app.dnsConnected !== undefined
|
|
52
|
+
? app.dnsConnected !== false
|
|
53
|
+
: app.connected !== false;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...app,
|
|
57
|
+
kind: 'app',
|
|
58
|
+
appRef,
|
|
59
|
+
app_ref: appRef,
|
|
60
|
+
appUrl: publicUrl,
|
|
61
|
+
publicUrl,
|
|
62
|
+
dnsConnected,
|
|
63
|
+
autostart: app.autostart !== false,
|
|
64
|
+
keepAlive: app.keepAlive !== false,
|
|
65
|
+
desiredState: app.desiredState || (app.status === 'stopped' ? 'stopped' : 'running'),
|
|
66
|
+
status: app.status || 'stopped',
|
|
67
|
+
pid: app.pid || null,
|
|
68
|
+
error: app.error || null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadApps() {
|
|
73
|
+
let data = readJson(APPS_FILE, null);
|
|
74
|
+
if (!data) {
|
|
75
|
+
const legacy = readJson(LEGACY_ARTIFACTS_FILE, null);
|
|
76
|
+
data = Array.isArray(legacy?.artifacts)
|
|
77
|
+
? { version: 1, apps: legacy.artifacts }
|
|
78
|
+
: { version: 1, apps: [] };
|
|
79
|
+
}
|
|
80
|
+
const apps = Array.isArray(data.apps) ? data.apps.map(normalizeApp) : [];
|
|
81
|
+
return { version: 1, apps };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function publishAppsChange(data, source = 'apps') {
|
|
85
|
+
try {
|
|
86
|
+
appendStateEvent({
|
|
87
|
+
resource: 'apps',
|
|
88
|
+
op: 'replace',
|
|
89
|
+
value: Array.isArray(data?.apps) ? data.apps.map(normalizeApp) : [],
|
|
90
|
+
source,
|
|
91
|
+
});
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.warn('[Apps] Local Live Store publish failed:', error.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function saveApps(data, options = {}) {
|
|
98
|
+
ensureDir(APPS_DIR);
|
|
99
|
+
const normalizedData = {
|
|
100
|
+
version: 1,
|
|
101
|
+
apps: Array.isArray(data.apps) ? data.apps.map(normalizeApp) : [],
|
|
102
|
+
};
|
|
103
|
+
writeJsonAtomic(APPS_FILE, normalizedData);
|
|
104
|
+
publishAppsChange(normalizedData, options.source || 'apps:save');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function updateAppMeta(appId, updates) {
|
|
108
|
+
const data = loadApps();
|
|
109
|
+
const app = data.apps.find((item) => item.id === appId);
|
|
110
|
+
if (!app) return null;
|
|
111
|
+
Object.assign(app, updates, { updatedAt: new Date().toISOString() });
|
|
112
|
+
saveApps(data);
|
|
113
|
+
return normalizeApp(app);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getApp(appId) {
|
|
117
|
+
return loadApps().apps.find((app) => app.id === appId) || null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function allocatePort(preferredPort) {
|
|
121
|
+
if (preferredPort !== undefined && preferredPort !== null) {
|
|
122
|
+
const parsed = Number(preferredPort);
|
|
123
|
+
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) return parsed;
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const used = new Set(loadApps().apps.map((app) => Number(app.port)));
|
|
128
|
+
for (let port = APP_PORT_MIN; port <= APP_PORT_MAX; port += 1) {
|
|
129
|
+
if (!used.has(port)) return port;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function allocateUniqueAppRef() {
|
|
135
|
+
const used = new Set(loadApps().apps.map((app) => app.appRef));
|
|
136
|
+
for (let attempt = 0; attempt < 12; attempt += 1) {
|
|
137
|
+
const candidate = genAppRef();
|
|
138
|
+
if (!used.has(candidate)) return candidate;
|
|
139
|
+
}
|
|
140
|
+
throw new Error('Could not allocate app DNS ref');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getConnectedRoutes() {
|
|
144
|
+
return loadApps()
|
|
145
|
+
.apps
|
|
146
|
+
.filter((app) =>
|
|
147
|
+
app.dnsConnected
|
|
148
|
+
&& app.desiredState === 'running'
|
|
149
|
+
&& app.status !== 'stopped'
|
|
150
|
+
&& app.status !== 'error'
|
|
151
|
+
&& app.port
|
|
152
|
+
&& app.appRef,
|
|
153
|
+
)
|
|
154
|
+
.map((app) => ({
|
|
155
|
+
app_ref: app.appRef,
|
|
156
|
+
port: app.port,
|
|
157
|
+
name: app.name,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
allocatePort,
|
|
163
|
+
allocateUniqueAppRef,
|
|
164
|
+
ensureAppsDirs,
|
|
165
|
+
getApp,
|
|
166
|
+
getConnectedRoutes,
|
|
167
|
+
loadApps,
|
|
168
|
+
publicUrlForRef,
|
|
169
|
+
saveApps,
|
|
170
|
+
updateAppMeta,
|
|
171
|
+
};
|