amalgm 0.0.0 → 0.0.1

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 (107) hide show
  1. package/README.md +37 -1
  2. package/bin/amalgm.js +8 -2
  3. package/lib/auth-store.js +223 -0
  4. package/lib/cli.js +1000 -0
  5. package/lib/paths.js +30 -0
  6. package/lib/supervisor.js +467 -0
  7. package/lib/tunnel-chat.js +328 -0
  8. package/lib/tunnel-events.js +499 -0
  9. package/package.json +29 -3
  10. package/runtime/README.md +4 -0
  11. package/runtime/lib/chatInput.js +306 -0
  12. package/runtime/lib/harnesses.js +988 -0
  13. package/runtime/lib/local/amalgmStore.js +128 -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 +165 -0
  18. package/runtime/scripts/amalgm-mcp/agents/store.js +153 -0
  19. package/runtime/scripts/amalgm-mcp/agents/talk.js +1156 -0
  20. package/runtime/scripts/amalgm-mcp/agents/tools.js +210 -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 +141 -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 +138 -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 +98 -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 +393 -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 +80 -0
  50. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +151 -0
  51. package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
  52. package/runtime/scripts/amalgm-mcp/server/http.js +335 -0
  53. package/runtime/scripts/amalgm-mcp/server/mcp.js +116 -0
  54. package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
  55. package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
  56. package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
  57. package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
  58. package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
  59. package/runtime/scripts/amalgm-mcp/tasks/store.js +139 -0
  60. package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
  61. package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/rest.js +389 -0
  63. package/runtime/scripts/chat-core/adapters/claude.js +163 -0
  64. package/runtime/scripts/chat-core/adapters/codex.js +313 -0
  65. package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
  66. package/runtime/scripts/chat-core/auth.js +177 -0
  67. package/runtime/scripts/chat-core/contract.js +326 -0
  68. package/runtime/scripts/chat-core/credentials/store.js +212 -0
  69. package/runtime/scripts/chat-core/egress.js +87 -0
  70. package/runtime/scripts/chat-core/engine.js +195 -0
  71. package/runtime/scripts/chat-core/event-schema.js +231 -0
  72. package/runtime/scripts/chat-core/events.js +190 -0
  73. package/runtime/scripts/chat-core/index.js +11 -0
  74. package/runtime/scripts/chat-core/input.js +50 -0
  75. package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
  76. package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
  77. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
  78. package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
  79. package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
  80. package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
  81. package/runtime/scripts/chat-core/parts.js +253 -0
  82. package/runtime/scripts/chat-core/recorder.js +65 -0
  83. package/runtime/scripts/chat-core/runtime.js +86 -0
  84. package/runtime/scripts/chat-core/server.js +163 -0
  85. package/runtime/scripts/chat-core/sse.js +196 -0
  86. package/runtime/scripts/chat-core/stores.js +100 -0
  87. package/runtime/scripts/chat-core/tool-display.js +149 -0
  88. package/runtime/scripts/chat-core/tool-shape.js +143 -0
  89. package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
  90. package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
  91. package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
  92. package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
  93. package/runtime/scripts/chat-core/usage.js +343 -0
  94. package/runtime/scripts/chat-server/config.js +110 -0
  95. package/runtime/scripts/chat-server/db.js +529 -0
  96. package/runtime/scripts/chat-server/index.js +33 -0
  97. package/runtime/scripts/chat-server/model-catalog.js +327 -0
  98. package/runtime/scripts/chat-server.js +75 -0
  99. package/runtime/scripts/credential-adapter.js +129 -0
  100. package/runtime/scripts/fs-watcher.js +888 -0
  101. package/runtime/scripts/local-gateway.js +852 -0
  102. package/runtime/scripts/platform-context.txt +246 -0
  103. package/runtime/scripts/port-monitor.js +175 -0
  104. package/runtime/scripts/proxy-token-store.js +162 -0
  105. package/runtime/scripts/runtime-auth.js +163 -0
  106. package/runtime/scripts/test-claude-code-models.js +87 -0
  107. package/runtime/tsconfig.json +15 -0
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Minimal artifact supervisor.
3
+ *
4
+ * Runs user-provided commands. It does not scaffold, wrap, or impose an app
5
+ * framework. The artifact process is expected to bind to PORT / {port}.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { spawn } = require('child_process');
11
+ const { DEFAULT_CWD } = require('../config');
12
+ const { syncArtifactRoutesToGateway } = require('./advertise');
13
+ const {
14
+ allocatePort,
15
+ allocateUniqueArtifactRef,
16
+ getArtifact,
17
+ loadArtifacts,
18
+ publicUrlForRef,
19
+ saveArtifacts,
20
+ updateArtifactMeta,
21
+ } = require('./store');
22
+
23
+ const running = new Map();
24
+ const stopping = new Set();
25
+ const restartTimers = new Map();
26
+
27
+ const ARTIFACT_ENV_ALLOWLIST = [
28
+ 'PATH',
29
+ 'HOME',
30
+ 'USER',
31
+ 'LOGNAME',
32
+ 'SHELL',
33
+ 'TMPDIR',
34
+ 'TMP',
35
+ 'TEMP',
36
+ 'LANG',
37
+ 'LC_ALL',
38
+ 'LC_CTYPE',
39
+ 'TERM',
40
+ 'NODE_ENV',
41
+ 'COREPACK_HOME',
42
+ 'PNPM_HOME',
43
+ 'YARN_CACHE_FOLDER',
44
+ 'BUN_INSTALL',
45
+ 'PLAYWRIGHT_BROWSERS_PATH',
46
+ ];
47
+
48
+ function isProcessAlive(pid) {
49
+ if (!Number.isInteger(pid) || pid <= 0) return false;
50
+ try {
51
+ process.kill(pid, 0);
52
+ return true;
53
+ } catch {
54
+ return false;
55
+ }
56
+ }
57
+
58
+ async function syncRoutesBestEffort(reason) {
59
+ try {
60
+ await syncArtifactRoutesToGateway(reason);
61
+ } catch (err) {
62
+ const message = err instanceof Error ? err.message : String(err);
63
+ console.warn(`[AmalgmMCP] Artifact route sync failed (${reason}): ${message}`);
64
+ }
65
+ }
66
+
67
+ function replacePort(command, port) {
68
+ return String(command || '').replace(/\{port\}/g, String(port));
69
+ }
70
+
71
+ function buildEnv(artifact) {
72
+ const env = {};
73
+ for (const key of ARTIFACT_ENV_ALLOWLIST) {
74
+ if (process.env[key]) env[key] = process.env[key];
75
+ }
76
+
77
+ return {
78
+ ...env,
79
+ PORT: String(artifact.port),
80
+ AMALGM_ARTIFACT_ID: artifact.id,
81
+ AMALGM_ARTIFACT_REF: artifact.artifactRef,
82
+ AMALGM_ARTIFACT_URL: artifact.publicUrl,
83
+ };
84
+ }
85
+
86
+ function runCommand(command, cwd, env, timeoutMs = 10 * 60 * 1000) {
87
+ return new Promise((resolve, reject) => {
88
+ const child = spawn(command, {
89
+ cwd,
90
+ env,
91
+ shell: true,
92
+ stdio: ['ignore', 'pipe', 'pipe'],
93
+ windowsHide: true,
94
+ });
95
+ let output = '';
96
+ const timer = setTimeout(() => {
97
+ try { child.kill('SIGTERM'); } catch {}
98
+ reject(new Error(`Command timed out after ${Math.round(timeoutMs / 1000)}s`));
99
+ }, timeoutMs);
100
+
101
+ const append = (chunk) => {
102
+ output += chunk.toString();
103
+ if (output.length > 12_000) output = output.slice(-12_000);
104
+ };
105
+ child.stdout?.on('data', append);
106
+ child.stderr?.on('data', append);
107
+ child.on('error', (err) => {
108
+ clearTimeout(timer);
109
+ reject(err);
110
+ });
111
+ child.on('exit', (code) => {
112
+ clearTimeout(timer);
113
+ if (code === 0) {
114
+ resolve(output);
115
+ } else {
116
+ reject(new Error(output.trim() || `Command exited with code ${code}`));
117
+ }
118
+ });
119
+ });
120
+ }
121
+
122
+ async function runBuild(artifact) {
123
+ if (!artifact.buildCommand) return;
124
+ updateArtifactMeta(artifact.id, { status: 'building', error: null });
125
+ const command = replacePort(artifact.buildCommand, artifact.port);
126
+ await runCommand(command, artifact.cwd, buildEnv(artifact));
127
+ }
128
+
129
+ function startArtifactProcess(artifact) {
130
+ if (!artifact.startCommand) throw new Error('startCommand is required');
131
+ if (!fs.existsSync(artifact.cwd)) throw new Error(`Artifact cwd does not exist: ${artifact.cwd}`);
132
+
133
+ const command = replacePort(artifact.startCommand, artifact.port);
134
+ const child = spawn(command, {
135
+ cwd: artifact.cwd,
136
+ env: buildEnv(artifact),
137
+ shell: true,
138
+ detached: process.platform !== 'win32',
139
+ stdio: ['ignore', 'pipe', 'pipe'],
140
+ windowsHide: true,
141
+ });
142
+
143
+ running.set(artifact.id, child);
144
+ child.unref();
145
+ child.stdout?.on('data', (chunk) => {
146
+ console.log(`[Artifact:${artifact.id}] ${chunk.toString().trim()}`);
147
+ });
148
+ child.stderr?.on('data', (chunk) => {
149
+ console.error(`[Artifact:${artifact.id}] ${chunk.toString().trim()}`);
150
+ });
151
+ child.on('exit', (code) => {
152
+ const activeChild = running.get(artifact.id);
153
+ if (activeChild && activeChild !== child) return;
154
+ running.delete(artifact.id);
155
+ const wasStopping = stopping.has(artifact.id);
156
+ stopping.delete(artifact.id);
157
+ if (wasStopping) return;
158
+
159
+ const current = getArtifact(artifact.id);
160
+ if (!current) return;
161
+
162
+ if (current.desiredState === 'running' && current.keepAlive !== false) {
163
+ updateArtifactMeta(artifact.id, {
164
+ status: 'restarting',
165
+ pid: null,
166
+ error: code === 0 ? null : `Process exited with code ${code}`,
167
+ });
168
+ void syncRoutesBestEffort(`exit:${artifact.id}`);
169
+ const timer = setTimeout(() => {
170
+ restartTimers.delete(artifact.id);
171
+ startArtifact(artifact.id).catch((err) => {
172
+ updateArtifactMeta(artifact.id, { status: 'error', error: err.message, pid: null });
173
+ });
174
+ }, 2000);
175
+ restartTimers.set(artifact.id, timer);
176
+ return;
177
+ }
178
+
179
+ updateArtifactMeta(artifact.id, {
180
+ status: code === 0 ? 'stopped' : 'error',
181
+ pid: null,
182
+ error: code === 0 ? null : `Process exited with code ${code}`,
183
+ });
184
+ void syncRoutesBestEffort(`exit:${artifact.id}`);
185
+ });
186
+
187
+ return child;
188
+ }
189
+
190
+ async function startArtifact(artifactId) {
191
+ const artifact = getArtifact(artifactId);
192
+ if (!artifact) throw new Error(`Artifact not found: ${artifactId}`);
193
+ const existingTimer = restartTimers.get(artifactId);
194
+ if (existingTimer) clearTimeout(existingTimer);
195
+ restartTimers.delete(artifactId);
196
+
197
+ if (running.has(artifactId)) {
198
+ const current = updateArtifactMeta(artifact.id, {
199
+ desiredState: 'running',
200
+ status: 'running',
201
+ error: null,
202
+ });
203
+ await syncRoutesBestEffort(`start:${artifactId}`);
204
+ return current;
205
+ }
206
+
207
+ if (artifact.pid && isProcessAlive(artifact.pid)) {
208
+ const current = updateArtifactMeta(artifact.id, {
209
+ desiredState: 'running',
210
+ status: 'running',
211
+ error: null,
212
+ });
213
+ await syncRoutesBestEffort(`start:${artifactId}`);
214
+ return current;
215
+ }
216
+
217
+ if (artifact.pid && !isProcessAlive(artifact.pid)) {
218
+ updateArtifactMeta(artifact.id, { pid: null });
219
+ }
220
+
221
+ const child = startArtifactProcess(artifact);
222
+ const current = updateArtifactMeta(artifact.id, {
223
+ desiredState: 'running',
224
+ status: 'running',
225
+ pid: child.pid,
226
+ error: null,
227
+ lastStartedAt: new Date().toISOString(),
228
+ });
229
+ await syncRoutesBestEffort(`start:${artifactId}`);
230
+ return current;
231
+ }
232
+
233
+ async function stopArtifact(artifactId, options = {}) {
234
+ const artifact = getArtifact(artifactId);
235
+ if (!artifact) throw new Error(`Artifact not found: ${artifactId}`);
236
+
237
+ const timer = restartTimers.get(artifactId);
238
+ if (timer) clearTimeout(timer);
239
+ restartTimers.delete(artifactId);
240
+ stopping.add(artifactId);
241
+
242
+ const child = running.get(artifactId);
243
+ if (child?.pid) {
244
+ try { process.kill(-child.pid, 'SIGTERM'); } catch {}
245
+ try { child.kill('SIGTERM'); } catch {}
246
+ } else if (artifact.pid) {
247
+ try { process.kill(artifact.pid, 'SIGTERM'); } catch {}
248
+ }
249
+ running.delete(artifactId);
250
+
251
+ setTimeout(() => stopping.delete(artifactId), 1000);
252
+ const current = updateArtifactMeta(artifactId, {
253
+ desiredState: options.desiredState || 'stopped',
254
+ status: 'stopped',
255
+ pid: null,
256
+ });
257
+ await syncRoutesBestEffort(`stop:${artifactId}`);
258
+ return current;
259
+ }
260
+
261
+ async function deleteArtifact(artifactId) {
262
+ const artifact = getArtifact(artifactId);
263
+ if (!artifact) throw new Error(`Artifact not found: ${artifactId}`);
264
+
265
+ const timer = restartTimers.get(artifactId);
266
+ if (timer) clearTimeout(timer);
267
+ restartTimers.delete(artifactId);
268
+
269
+ await stopArtifact(artifactId, { desiredState: 'stopped' }).catch(() => {});
270
+
271
+ const data = loadArtifacts();
272
+ data.artifacts = data.artifacts.filter((item) => item.id !== artifactId);
273
+ saveArtifacts(data);
274
+
275
+ stopping.delete(artifactId);
276
+ running.delete(artifactId);
277
+ await syncRoutesBestEffort(`delete:${artifactId}`);
278
+
279
+ return artifact;
280
+ }
281
+
282
+ async function registerArtifact(input) {
283
+ const name = String(input.name || '').trim() || 'Artifact';
284
+ const cwd = path.resolve(input.cwd || input.root_dir || DEFAULT_CWD);
285
+ const startCommand = String(input.start_command || input.startCommand || '').trim();
286
+ if (!startCommand) throw new Error('start_command is required');
287
+ if (!fs.existsSync(cwd)) throw new Error(`cwd does not exist: ${cwd}`);
288
+
289
+ const port = allocatePort(input.port);
290
+ if (!port) throw new Error('No valid artifact port available');
291
+
292
+ const artifactRef = allocateUniqueArtifactRef();
293
+ const now = new Date().toISOString();
294
+ const artifact = {
295
+ id: input.id || cryptoRandomId(),
296
+ kind: 'artifact',
297
+ name,
298
+ description: input.description || '',
299
+ cwd,
300
+ port,
301
+ buildCommand: input.build_command || input.buildCommand || null,
302
+ startCommand,
303
+ artifactRef,
304
+ artifact_ref: artifactRef,
305
+ publicUrl: publicUrlForRef(artifactRef),
306
+ dnsConnected: input.connect_dns !== false,
307
+ autostart: input.autostart !== false,
308
+ keepAlive: input.keep_alive !== false,
309
+ desiredState: 'running',
310
+ status: 'registered',
311
+ pid: null,
312
+ createdAt: now,
313
+ updatedAt: now,
314
+ lastStartedAt: null,
315
+ lastDeployedAt: null,
316
+ error: null,
317
+ };
318
+
319
+ const data = loadArtifacts();
320
+ data.artifacts.push(artifact);
321
+ saveArtifacts(data);
322
+
323
+ if (artifact.buildCommand) await runBuild(artifact);
324
+ await startArtifact(artifact.id);
325
+ return updateArtifactMeta(artifact.id, { lastDeployedAt: new Date().toISOString() });
326
+ }
327
+
328
+ async function redeployArtifact(artifactId, updates = {}) {
329
+ let artifact = getArtifact(artifactId);
330
+ if (!artifact) throw new Error(`Artifact not found: ${artifactId}`);
331
+
332
+ const patch = {};
333
+ if (updates.cwd || updates.root_dir) patch.cwd = path.resolve(updates.cwd || updates.root_dir);
334
+ if (updates.port !== undefined) {
335
+ const parsed = Number(updates.port);
336
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
337
+ throw new Error('port must be between 1 and 65535');
338
+ }
339
+ patch.port = parsed;
340
+ }
341
+ if (updates.build_command !== undefined) patch.buildCommand = updates.build_command || null;
342
+ if (updates.start_command !== undefined) patch.startCommand = updates.start_command;
343
+ if (updates.keep_alive !== undefined) patch.keepAlive = updates.keep_alive !== false;
344
+ if (updates.autostart !== undefined) patch.autostart = updates.autostart !== false;
345
+ if (Object.keys(patch).length > 0) artifact = updateArtifactMeta(artifactId, patch);
346
+
347
+ if (!artifact.startCommand) throw new Error('startCommand is required');
348
+ if (!fs.existsSync(artifact.cwd)) throw new Error(`cwd does not exist: ${artifact.cwd}`);
349
+
350
+ if (artifact.buildCommand) await runBuild(artifact);
351
+ await stopArtifact(artifactId, { desiredState: 'running' });
352
+ await startArtifact(artifactId);
353
+ return updateArtifactMeta(artifactId, { lastDeployedAt: new Date().toISOString() });
354
+ }
355
+
356
+ function cryptoRandomId() {
357
+ return `artifact-${Math.random().toString(36).slice(2, 8)}${Date.now().toString(36).slice(-4)}`;
358
+ }
359
+
360
+ async function connectDns(artifactId) {
361
+ const artifact = getArtifact(artifactId);
362
+ if (!artifact) throw new Error(`Artifact not found: ${artifactId}`);
363
+ const current = updateArtifactMeta(artifactId, { dnsConnected: true });
364
+ await syncRoutesBestEffort(`connect-dns:${artifactId}`);
365
+ return current;
366
+ }
367
+
368
+ async function disconnectDns(artifactId) {
369
+ const artifact = getArtifact(artifactId);
370
+ if (!artifact) throw new Error(`Artifact not found: ${artifactId}`);
371
+ const current = updateArtifactMeta(artifactId, { dnsConnected: false });
372
+ await syncRoutesBestEffort(`disconnect-dns:${artifactId}`);
373
+ return current;
374
+ }
375
+
376
+ async function startRegisteredArtifacts() {
377
+ const data = loadArtifacts();
378
+ await Promise.allSettled(data.artifacts.map(async (artifact) => {
379
+ if (artifact.pid && !isProcessAlive(artifact.pid)) {
380
+ updateArtifactMeta(artifact.id, { pid: null });
381
+ }
382
+
383
+ if (artifact.autostart !== false && artifact.desiredState === 'running') {
384
+ await startArtifact(artifact.id).catch((err) => {
385
+ updateArtifactMeta(artifact.id, { status: 'error', error: err.message, pid: null });
386
+ });
387
+ }
388
+ }));
389
+ await syncRoutesBestEffort('boot');
390
+ }
391
+
392
+ module.exports = {
393
+ deleteArtifact,
394
+ connectDns,
395
+ disconnectDns,
396
+ redeployArtifact,
397
+ registerArtifact,
398
+ runBuild,
399
+ startArtifact,
400
+ startRegisteredArtifacts,
401
+ stopArtifact,
402
+ };
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Minimal artifact MCP tools.
3
+ *
4
+ * An artifact is a user-hosted production service. We do not scaffold or wrap
5
+ * it; we register the command, keep it alive locally, and expose an artifact
6
+ * DNS route while this computer is connected.
7
+ */
8
+
9
+ const { textResult, errorResult } = require('../lib/tool-result');
10
+ const { getConnectedRoutes, loadArtifacts } = require('./store');
11
+ const {
12
+ connectDns,
13
+ disconnectDns,
14
+ redeployArtifact,
15
+ registerArtifact,
16
+ startArtifact,
17
+ stopArtifact,
18
+ } = require('./supervisor');
19
+
20
+ function artifactSummary(artifact) {
21
+ return [
22
+ `${artifact.name} (${artifact.id})`,
23
+ `status: ${artifact.status}`,
24
+ `cwd: ${artifact.cwd}`,
25
+ `port: ${artifact.port}`,
26
+ `dns: ${artifact.dnsConnected ? artifact.publicUrl : 'disconnected'}`,
27
+ `autostart: ${artifact.autostart !== false}`,
28
+ `keepAlive: ${artifact.keepAlive !== false}`,
29
+ artifact.error ? `error: ${artifact.error}` : null,
30
+ ].filter(Boolean).join('\n');
31
+ }
32
+
33
+ module.exports = [
34
+ {
35
+ name: 'artifacts_register',
36
+ description:
37
+ 'Register a user-hosted artifact from an existing project on this computer. Artifacts should be production services: prefer passing a build_command such as "npm run build" and a production start_command such as "npm run start -- --host 0.0.0.0 --port {port}" or any command that binds to PORT/{port}. The MCP does not scaffold or enforce a framework; it stores the record in ~/.amalgm, starts it, keeps it alive while the computer is on, and connects it to artifact DNS by default.',
38
+ inputSchema: {
39
+ type: 'object',
40
+ properties: {
41
+ name: { type: 'string', description: 'Human-readable artifact name' },
42
+ cwd: { type: 'string', description: 'Project directory on this computer' },
43
+ port: { type: 'number', description: 'Port the artifact should bind to. If omitted, one is allocated.' },
44
+ build_command: { type: 'string', description: 'Optional production build command, run before start/redeploy' },
45
+ start_command: { type: 'string', description: 'Required production start command. Use {port} or PORT.' },
46
+ description: { type: 'string' },
47
+ connect_dns: { type: 'boolean', description: 'Whether to expose through artifact DNS immediately. Defaults true.' },
48
+ autostart: { type: 'boolean', description: 'Start on Amalgm boot. Defaults true.' },
49
+ keep_alive: { type: 'boolean', description: 'Restart if the process exits. Defaults true.' },
50
+ },
51
+ required: ['start_command'],
52
+ },
53
+ async handler(args) {
54
+ try {
55
+ const artifact = await registerArtifact(args || {});
56
+ return textResult(`Artifact registered.\n\n${artifactSummary(artifact)}`);
57
+ } catch (err) {
58
+ return errorResult(err.message);
59
+ }
60
+ },
61
+ },
62
+ {
63
+ name: 'artifacts_list',
64
+ description: 'List locally registered artifacts from ~/.amalgm/artifacts.json.',
65
+ inputSchema: { type: 'object', properties: {} },
66
+ async handler() {
67
+ const artifacts = loadArtifacts().artifacts;
68
+ if (artifacts.length === 0) return textResult('No artifacts registered.');
69
+ return textResult(artifacts.map(artifactSummary).join('\n\n'));
70
+ },
71
+ },
72
+ {
73
+ name: 'artifacts_routes',
74
+ description: 'List artifact DNS routes currently advertised by this computer.',
75
+ inputSchema: { type: 'object', properties: {} },
76
+ async handler() {
77
+ const routes = getConnectedRoutes();
78
+ return textResult(JSON.stringify({ routes }, null, 2));
79
+ },
80
+ },
81
+ {
82
+ name: 'artifacts_redeploy',
83
+ description:
84
+ 'Redeploy an artifact: optionally update cwd/build/start/port settings, run its build command if configured, then restart the production process. DNS connection state is preserved.',
85
+ inputSchema: {
86
+ type: 'object',
87
+ properties: {
88
+ artifact_id: { type: 'string' },
89
+ cwd: { type: 'string' },
90
+ port: { type: 'number' },
91
+ build_command: { type: 'string' },
92
+ start_command: { type: 'string' },
93
+ autostart: { type: 'boolean' },
94
+ keep_alive: { type: 'boolean' },
95
+ },
96
+ required: ['artifact_id'],
97
+ },
98
+ async handler(args) {
99
+ try {
100
+ const artifact = await redeployArtifact(args.artifact_id, args || {});
101
+ return textResult(`Artifact redeployed.\n\n${artifactSummary(artifact)}`);
102
+ } catch (err) {
103
+ return errorResult(err.message);
104
+ }
105
+ },
106
+ },
107
+ {
108
+ name: 'artifacts_disconnect_dns',
109
+ description:
110
+ 'Disconnect an artifact from artifact DNS without stopping the local process. The artifact remains registered locally and can be reconnected later.',
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: { artifact_id: { type: 'string' } },
114
+ required: ['artifact_id'],
115
+ },
116
+ async handler({ artifact_id }) {
117
+ try {
118
+ const artifact = await disconnectDns(artifact_id);
119
+ return textResult(`Artifact DNS disconnected.\n\n${artifactSummary(artifact)}`);
120
+ } catch (err) {
121
+ return errorResult(err.message);
122
+ }
123
+ },
124
+ },
125
+ {
126
+ name: 'artifacts_connect_dns',
127
+ description: 'Reconnect a registered artifact to artifact DNS.',
128
+ inputSchema: {
129
+ type: 'object',
130
+ properties: { artifact_id: { type: 'string' } },
131
+ required: ['artifact_id'],
132
+ },
133
+ async handler({ artifact_id }) {
134
+ try {
135
+ const artifact = await connectDns(artifact_id);
136
+ return textResult(`Artifact DNS connected.\n\n${artifactSummary(artifact)}`);
137
+ } catch (err) {
138
+ return errorResult(err.message);
139
+ }
140
+ },
141
+ },
142
+ {
143
+ name: 'artifacts_start',
144
+ description: 'Start a stopped registered artifact using its production start command.',
145
+ inputSchema: {
146
+ type: 'object',
147
+ properties: { artifact_id: { type: 'string' } },
148
+ required: ['artifact_id'],
149
+ },
150
+ async handler({ artifact_id }) {
151
+ try {
152
+ const artifact = await startArtifact(artifact_id);
153
+ return textResult(`Artifact started.\n\n${artifactSummary(artifact)}`);
154
+ } catch (err) {
155
+ return errorResult(err.message);
156
+ }
157
+ },
158
+ },
159
+ {
160
+ name: 'artifacts_stop',
161
+ description: 'Stop a registered artifact and mark it as intentionally stopped.',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: { artifact_id: { type: 'string' } },
165
+ required: ['artifact_id'],
166
+ },
167
+ async handler({ artifact_id }) {
168
+ try {
169
+ const artifact = await stopArtifact(artifact_id);
170
+ return textResult(`Artifact stopped.\n\n${artifactSummary(artifact)}`);
171
+ } catch (err) {
172
+ return errorResult(err.message);
173
+ }
174
+ },
175
+ },
176
+ ];