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,335 @@
1
+ /**
2
+ * HTTP server — routes + lifecycle.
3
+ *
4
+ * Route table:
5
+ * GET /healthz → stats snapshot
6
+ * * /mcp → MCP streamable-HTTP (delegated to SDK transport)
7
+ * POST /events → webhook ingress (signature match + agent run)
8
+ * GET /events → recent events (in-memory ring buffer)
9
+ * GET /tasks/list → list tasks
10
+ * POST /tasks/create → create task
11
+ * POST /tasks/update → update task
12
+ * POST /tasks/delete → delete task
13
+ * POST /tasks/run-now → run task immediately
14
+ * GET /event-triggers/list → list triggers
15
+ * POST /event-triggers/{create,update,delete}
16
+ * GET /workspace/projects → list projects
17
+ * POST /github/clone → clone repository onto the computer
18
+ * GET /workspace/branches → list git branches
19
+ * POST /workspace/checkout → checkout git branch
20
+ * GET /workspace/diff → git diff summary + patch
21
+ * GET /workspace/worktree → list worktrees
22
+ * POST /workspace/worktree → create worktree
23
+ * DELETE /workspace/worktree → remove worktree
24
+ * GET /fs/list → list files
25
+ * GET /fs/read → read file
26
+ * POST /fs/{write,delete,mkdir,rename}
27
+ * GET /mcp-connections → list sanitized MCP connections
28
+ * POST /mcp-connections → save/update MCP connection
29
+ * DELETE /mcp-connections → remove MCP connection
30
+ * POST /local/init → init local computer state
31
+ * GET /local/credentials → discover local agent auth
32
+ * POST /local/credentials/resolve → resolve spawn env for agent auth
33
+ * POST /email/inbound → run a reply turn for inbound email
34
+ * POST /slack/inbound → run a reply turn for inbound Slack
35
+ * GET /credentials → list local Amalgm credential profiles
36
+ * POST /credentials → save local Amalgm credential profile
37
+ * DELETE /credentials → remove local Amalgm credential profile
38
+ * POST /user-api-keys → legacy save BYOK credential file
39
+ * DELETE /user-api-keys → legacy remove BYOK credential file
40
+ * GET /artifacts/list → list artifacts
41
+ * GET /artifacts/routes → artifact DNS routes for tunnel adverts
42
+ * POST /artifacts/{register,redeploy,start,stop,connect-dns,disconnect-dns}
43
+ * GET /agents/list → list agents
44
+ * POST /agents/{get,create,update,delete}
45
+ */
46
+
47
+ const http = require('http');
48
+ const { PORT, STORAGE_DIR } = require('../config');
49
+ const { hasSupabase } = require('../lib/supabase');
50
+ const { authorizeRuntimeHttp } = require('../../runtime-auth');
51
+
52
+ const { loadTasks } = require('../tasks/store');
53
+ const { loadAgents } = require('../agents/store');
54
+ const { loadArtifacts } = require('../artifacts/store');
55
+ const { activeAgentConversations } = require('../agents/talk');
56
+ const { startScheduler, stopScheduler } = require('../tasks/scheduler');
57
+ const { createTransport } = require('./mcp');
58
+ const { handleEventsPost } = require('../events/ingress');
59
+ const eventsRing = require('../events/ring-buffer');
60
+ const eventsRest = require('../events/rest');
61
+ const agentsRest = require('../agents/rest');
62
+ const artifactsRest = require('../artifacts/rest');
63
+ const emailInbound = require('../email/inbound');
64
+ const slackInbound = require('../slack/inbound');
65
+ const fsRest = require('../fs/rest');
66
+ const localRest = require('../local/rest');
67
+ const mcpConnectionsRest = require('../mcp-connections/rest');
68
+ const tasksRest = require('../tasks/rest');
69
+ const userApiKeysRest = require('../user-api-keys/rest');
70
+ const credentialsRest = require('../credentials/rest');
71
+ const workspaceRest = require('../workspace/rest');
72
+
73
+ function createServer() {
74
+ return http.createServer(async (req, res) => {
75
+ if (!authorizeRuntimeHttp(req, res, { exposeHeaders: 'Mcp-Session-Id' })) return;
76
+
77
+ async function readJsonBody() {
78
+ const chunks = [];
79
+ for await (const chunk of req) chunks.push(chunk);
80
+ return JSON.parse(Buffer.concat(chunks).toString());
81
+ }
82
+ function sendJson(statusCode, data) {
83
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
84
+ res.end(JSON.stringify(data));
85
+ }
86
+ function getQuery() {
87
+ return Object.fromEntries(new URL(req.url, 'http://127.0.0.1').searchParams.entries());
88
+ }
89
+
90
+ try {
91
+ // ── MCP ─────────────────────────────────────────────────────────────────
92
+ // Delegate anything under /mcp directly to the SDK transport. A fresh
93
+ // transport + Server pair is created per request: the stateless
94
+ // StreamableHTTPServerTransport is single-shot in this mode, so reusing
95
+ // one across requests would fail on the second call with HTTP 500.
96
+ if (req.url === '/mcp' || req.url === '/mcp/' || req.url.startsWith('/mcp?')) {
97
+ const transport = await createTransport();
98
+ await transport.handleRequest(req, res);
99
+ return;
100
+ }
101
+
102
+ // ── Health ──────────────────────────────────────────────────────────────
103
+ if (req.url === '/healthz') {
104
+ const tasks = loadTasks();
105
+ const agents = loadAgents();
106
+ return sendJson(200, {
107
+ status: 'ok',
108
+ tasks: tasks.tasks.length,
109
+ enabledTasks: tasks.tasks.filter((t) => t.enabled).length,
110
+ artifacts: loadArtifacts().artifacts.length,
111
+ customAgents: agents.agents.length,
112
+ activeAgentConversations: activeAgentConversations.size,
113
+ });
114
+ }
115
+
116
+ // ── /agents (REST, not MCP) ─────────────────────────────────────────────
117
+ if (req.url === '/agents/list' && req.method === 'GET') {
118
+ return agentsRest.handleList(sendJson);
119
+ }
120
+ if (req.url === '/agents/get' && req.method === 'POST') {
121
+ return agentsRest.handleGet(await readJsonBody(), sendJson);
122
+ }
123
+ if (req.url === '/agents/create' && req.method === 'POST') {
124
+ return agentsRest.handleCreate(await readJsonBody(), sendJson);
125
+ }
126
+ if (req.url === '/agents/update' && req.method === 'POST') {
127
+ return agentsRest.handleUpdate(await readJsonBody(), sendJson);
128
+ }
129
+ if (req.url === '/agents/delete' && req.method === 'POST') {
130
+ return agentsRest.handleDelete(await readJsonBody(), sendJson);
131
+ }
132
+
133
+ // ── /events — webhook ingress ───────────────────────────────────────────
134
+ if (req.url === '/events' && req.method === 'POST') {
135
+ return handleEventsPost(req, sendJson);
136
+ }
137
+ if (req.url === '/events' && req.method === 'GET') {
138
+ return sendJson(200, { events: eventsRing.recent(50) });
139
+ }
140
+
141
+ // ── /tasks REST ─────────────────────────────────────────────────────────
142
+ if (req.url === '/tasks/list' && req.method === 'GET') {
143
+ return tasksRest.handleList(sendJson);
144
+ }
145
+ if (req.url === '/tasks/create' && req.method === 'POST') {
146
+ return tasksRest.handleCreate(await readJsonBody(), sendJson);
147
+ }
148
+ if (req.url === '/tasks/update' && req.method === 'POST') {
149
+ return tasksRest.handleUpdate(await readJsonBody(), sendJson);
150
+ }
151
+ if (req.url === '/tasks/delete' && req.method === 'POST') {
152
+ return tasksRest.handleDelete(await readJsonBody(), sendJson);
153
+ }
154
+ if (req.url === '/tasks/run-now' && req.method === 'POST') {
155
+ return tasksRest.handleRunNow(await readJsonBody(), sendJson);
156
+ }
157
+
158
+ // ── /event-triggers REST ────────────────────────────────────────────────
159
+ if (req.url === '/event-triggers/list' && req.method === 'GET') {
160
+ return eventsRest.handleList(req, sendJson);
161
+ }
162
+ if (req.url === '/event-triggers/create' && req.method === 'POST') {
163
+ return eventsRest.handleCreate(await readJsonBody(), sendJson);
164
+ }
165
+ if (req.url === '/event-triggers/update' && req.method === 'POST') {
166
+ return eventsRest.handleUpdate(await readJsonBody(), sendJson);
167
+ }
168
+ if (req.url === '/event-triggers/delete' && req.method === 'POST') {
169
+ return eventsRest.handleDelete(await readJsonBody(), sendJson);
170
+ }
171
+
172
+ // ── /workspace REST ─────────────────────────────────────────────────────
173
+ if (req.url === '/workspace/projects' && req.method === 'GET') {
174
+ return workspaceRest.handleProjects(sendJson);
175
+ }
176
+ if (req.url === '/github/clone' && req.method === 'POST') {
177
+ return workspaceRest.handleClone(await readJsonBody(), sendJson);
178
+ }
179
+ if (req.url.startsWith('/workspace/branches') && req.method === 'GET') {
180
+ return workspaceRest.handleBranches(getQuery(), sendJson);
181
+ }
182
+ if (req.url === '/workspace/checkout' && req.method === 'POST') {
183
+ return workspaceRest.handleCheckout(await readJsonBody(), sendJson);
184
+ }
185
+ if (req.url.startsWith('/workspace/diff') && req.method === 'GET') {
186
+ return workspaceRest.handleDiff(getQuery(), sendJson);
187
+ }
188
+ if (req.url.startsWith('/workspace/worktree') && req.method === 'GET') {
189
+ return workspaceRest.handleWorktreeList(getQuery(), sendJson);
190
+ }
191
+ if (req.url === '/workspace/worktree' && req.method === 'POST') {
192
+ return workspaceRest.handleWorktreeCreate(await readJsonBody(), sendJson);
193
+ }
194
+ if (req.url === '/workspace/worktree' && req.method === 'DELETE') {
195
+ return workspaceRest.handleWorktreeDelete(await readJsonBody(), sendJson);
196
+ }
197
+
198
+ // ── /fs REST ────────────────────────────────────────────────────────────
199
+ if (req.url.startsWith('/fs/list') && req.method === 'GET') {
200
+ return fsRest.handleList(getQuery(), sendJson);
201
+ }
202
+ if (req.url.startsWith('/fs/read') && req.method === 'GET') {
203
+ return fsRest.handleRead(getQuery(), sendJson);
204
+ }
205
+ if (req.url === '/fs/write' && req.method === 'POST') {
206
+ return fsRest.handleWrite(await readJsonBody(), sendJson);
207
+ }
208
+ if (req.url === '/fs/delete' && req.method === 'POST') {
209
+ return fsRest.handleDelete(await readJsonBody(), sendJson);
210
+ }
211
+ if (req.url === '/fs/mkdir' && req.method === 'POST') {
212
+ return fsRest.handleMkdir(await readJsonBody(), sendJson);
213
+ }
214
+ if (req.url === '/fs/rename' && req.method === 'POST') {
215
+ return fsRest.handleRename(await readJsonBody(), sendJson);
216
+ }
217
+
218
+ // ── /mcp-connections REST ──────────────────────────────────────────────
219
+ if (req.url === '/mcp-connections' && req.method === 'GET') {
220
+ return mcpConnectionsRest.handleList(sendJson);
221
+ }
222
+ if (req.url === '/mcp-connections' && req.method === 'POST') {
223
+ return mcpConnectionsRest.handleUpsert(await readJsonBody(), sendJson);
224
+ }
225
+ if (req.url === '/mcp-connections' && req.method === 'DELETE') {
226
+ return mcpConnectionsRest.handleDelete(await readJsonBody(), sendJson);
227
+ }
228
+
229
+ // ── /local REST ────────────────────────────────────────────────────────
230
+ if (req.url === '/local/init' && req.method === 'POST') {
231
+ return localRest.handleInit(sendJson);
232
+ }
233
+ if (req.url === '/local/credentials' && req.method === 'GET') {
234
+ return localRest.handleCredentials(sendJson);
235
+ }
236
+ if (req.url === '/local/credentials/resolve' && req.method === 'POST') {
237
+ return localRest.handleResolve(await readJsonBody(), sendJson);
238
+ }
239
+
240
+ // ── /credentials REST ─────────────────────────────────────────────────
241
+ if (req.url === '/credentials' && req.method === 'GET') {
242
+ return credentialsRest.handleList(sendJson);
243
+ }
244
+ if (req.url === '/credentials' && req.method === 'POST') {
245
+ return credentialsRest.handleSave(await readJsonBody(), sendJson);
246
+ }
247
+ if (req.url === '/credentials' && req.method === 'DELETE') {
248
+ return credentialsRest.handleDelete(await readJsonBody(), sendJson);
249
+ }
250
+
251
+ // ── /email REST ────────────────────────────────────────────────────────
252
+ if (req.url === '/email/inbound' && req.method === 'POST') {
253
+ return emailInbound.handleEmailInbound(await readJsonBody(), sendJson);
254
+ }
255
+ if (req.url === '/slack/inbound' && req.method === 'POST') {
256
+ return slackInbound.handleSlackInbound(await readJsonBody(), sendJson);
257
+ }
258
+
259
+ // ── /user-api-keys REST ───────────────────────────────────────────────
260
+ if (req.url === '/user-api-keys' && req.method === 'POST') {
261
+ return userApiKeysRest.handleSave(await readJsonBody(), sendJson);
262
+ }
263
+ if (req.url === '/user-api-keys' && req.method === 'DELETE') {
264
+ return userApiKeysRest.handleDelete(sendJson);
265
+ }
266
+
267
+ // ── /artifacts REST ────────────────────────────────────────────────────
268
+ if (req.url === '/artifacts/list' && req.method === 'GET') {
269
+ return artifactsRest.handleList(sendJson);
270
+ }
271
+ if (req.url === '/artifacts/routes' && req.method === 'GET') {
272
+ return artifactsRest.handleRoutes(sendJson);
273
+ }
274
+ if (req.url === '/artifacts/register' && req.method === 'POST') {
275
+ return artifactsRest.handleRegister(await readJsonBody(), sendJson);
276
+ }
277
+ if (req.url === '/artifacts/redeploy' && req.method === 'POST') {
278
+ return artifactsRest.handleRedeploy(await readJsonBody(), sendJson);
279
+ }
280
+ if (req.url === '/artifacts/start' && req.method === 'POST') {
281
+ return artifactsRest.handleStart(await readJsonBody(), sendJson);
282
+ }
283
+ if (req.url === '/artifacts/stop' && req.method === 'POST') {
284
+ return artifactsRest.handleStop(await readJsonBody(), sendJson);
285
+ }
286
+ if (req.url === '/artifacts/delete' && req.method === 'POST') {
287
+ return artifactsRest.handleDelete(await readJsonBody(), sendJson);
288
+ }
289
+ if (req.url === '/artifacts/connect-dns' && req.method === 'POST') {
290
+ return artifactsRest.handleConnectDns(await readJsonBody(), sendJson);
291
+ }
292
+ if (req.url === '/artifacts/disconnect-dns' && req.method === 'POST') {
293
+ return artifactsRest.handleDisconnectDns(await readJsonBody(), sendJson);
294
+ }
295
+
296
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
297
+ res.end('Not found');
298
+ } catch (err) {
299
+ console.error('[AmalgmMCP] Unhandled error:', err);
300
+ if (!res.headersSent) sendJson(500, { error: 'Internal server error' });
301
+ }
302
+ });
303
+ }
304
+
305
+ async function listen() {
306
+ const httpServer = createServer();
307
+
308
+ httpServer.listen(PORT, '127.0.0.1', async () => {
309
+ console.log(`[AmalgmMCP] Listening on 127.0.0.1:${PORT}`);
310
+ console.log(`[AmalgmMCP] Storage: ${STORAGE_DIR}`);
311
+ console.log(
312
+ `[AmalgmMCP] Supabase: ${hasSupabase() ? 'connected' : 'not configured (runs are local-only)'}`,
313
+ );
314
+ // Hydrate user model prefs non-blocking (requires Supabase).
315
+ require('../lib/prefs').hydrateModelPreferences().catch(() => {});
316
+ startScheduler();
317
+ });
318
+
319
+ // Graceful shutdown. Running tasks get aborted via SIGTERM → process exit;
320
+ // we don't try to individually abort them here since chat-server owns its own
321
+ // runtime teardown paths.
322
+ function shutdown() {
323
+ console.log('[AmalgmMCP] Shutting down...');
324
+ stopScheduler();
325
+ httpServer.close(() => process.exit(0));
326
+ setTimeout(() => process.exit(0), 5000);
327
+ }
328
+
329
+ process.on('SIGTERM', shutdown);
330
+ process.on('SIGINT', shutdown);
331
+
332
+ return httpServer;
333
+ }
334
+
335
+ module.exports = { createServer, listen };
@@ -0,0 +1,116 @@
1
+ /**
2
+ * MCP server — built on @modelcontextprotocol/sdk.
3
+ *
4
+ * The SDK handles JSON-RPC framing, protocol-version negotiation, batching,
5
+ * and streamable-HTTP transport. We just register the two tool handlers.
6
+ *
7
+ * Each domain module (tasks, events, notify, agents, browser) exports a flat
8
+ * array of { name, description, inputSchema, handler }. We concatenate them
9
+ * into a single TOOLS list, look the tool up by name on each call, and pass
10
+ * the result through unchanged.
11
+ */
12
+
13
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
14
+ const {
15
+ StreamableHTTPServerTransport,
16
+ } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
17
+ const {
18
+ ListToolsRequestSchema,
19
+ CallToolRequestSchema,
20
+ } = require('@modelcontextprotocol/sdk/types.js');
21
+
22
+ const { AMALGM_USER_ID } = require('../config');
23
+ const { hasSupabase, supabaseSelect } = require('../lib/supabase');
24
+ const { attachActionDescriptor } = require('../lib/tool-result');
25
+
26
+ const TOOLS = [
27
+ ...require('../tasks/tools'),
28
+ ...require('../events/tools'),
29
+ ...require('../notify'),
30
+ ...require('../agents/tools'),
31
+ ...require('../artifacts/tools'),
32
+ ...require('../browser/tools'),
33
+ ];
34
+
35
+ /**
36
+ * Look up the caller's session to tag DB rows with origin metadata. Reads
37
+ * x-amalgm-session-id from the transport's passed-through HTTP headers.
38
+ */
39
+ async function buildRequestContext(extra) {
40
+ const headers = extra?.requestInfo?.headers || {};
41
+ const h = headers['x-amalgm-session-id'];
42
+ const callerSessionId = Array.isArray(h) ? h[0] : h || null;
43
+ const ctx = { callerSessionId };
44
+
45
+ if (callerSessionId && hasSupabase()) {
46
+ const rows = await supabaseSelect(
47
+ 'sessions',
48
+ `id=eq.${callerSessionId}&user_id=eq.${AMALGM_USER_ID}&select=id,title,harness,metadata`,
49
+ );
50
+ const parent = Array.isArray(rows) ? rows[0] : null;
51
+ if (parent) {
52
+ ctx.originName = parent.title || undefined;
53
+ ctx.originHarnessId = parent.harness || undefined;
54
+ ctx.originBaseHarnessId = parent.metadata?.baseHarnessId || undefined;
55
+ }
56
+ }
57
+ return ctx;
58
+ }
59
+
60
+ function createMcpServer() {
61
+ const server = new Server(
62
+ { name: 'amalgm', version: '1.0.0' },
63
+ { capabilities: { tools: {} } },
64
+ );
65
+
66
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
67
+ tools: TOOLS.map(({ name, description, inputSchema }) => ({
68
+ name,
69
+ description,
70
+ inputSchema,
71
+ })),
72
+ }));
73
+
74
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
75
+ const toolName = request.params?.name;
76
+ const toolArgs = request.params?.arguments || {};
77
+ const tool = TOOLS.find((t) => t.name === toolName);
78
+ if (!tool) {
79
+ return attachActionDescriptor({
80
+ content: [{ type: 'text', text: `Unknown tool: ${toolName}` }],
81
+ isError: true,
82
+ }, toolName || 'unknown', toolArgs);
83
+ }
84
+ const ctx = await buildRequestContext(extra);
85
+ try {
86
+ const result = await tool.handler(toolArgs, ctx);
87
+ return attachActionDescriptor(result, toolName, toolArgs);
88
+ } catch (err) {
89
+ return attachActionDescriptor({
90
+ content: [{ type: 'text', text: `Error: ${err.message}` }],
91
+ isError: true,
92
+ }, toolName, toolArgs);
93
+ }
94
+ });
95
+
96
+ return server;
97
+ }
98
+
99
+ /**
100
+ * One stateless streamable-HTTP transport + Server pair. The transport is
101
+ * reusable across requests because we don't use session IDs or bidirectional
102
+ * notifications — every tools/list and tools/call is self-contained.
103
+ */
104
+ async function createTransport() {
105
+ const server = createMcpServer();
106
+ const transport = new StreamableHTTPServerTransport({
107
+ sessionIdGenerator: undefined,
108
+ // Return plain JSON responses instead of opening an SSE stream. We don't
109
+ // emit progress notifications, so there's nothing to stream.
110
+ enableJsonResponse: true,
111
+ });
112
+ await server.connect(transport);
113
+ return transport;
114
+ }
115
+
116
+ module.exports = { createTransport, TOOLS };
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Slack inbound handoff.
3
+ *
4
+ * The api-proxy verifies Slack, resolves/creates the Supabase session, then
5
+ * forwards a clean Slack payload here. This local route runs the agent through
6
+ * the same chat-server path as email/tasks/events, then asks the proxy to send
7
+ * the Slack reply with the bot token that lives outside the local computer.
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+
12
+ const {
13
+ AMALGM_USER_ID,
14
+ DEFAULT_CWD,
15
+ PROXY_BASE_URL,
16
+ PROXY_TOKEN,
17
+ } = require('../config');
18
+ const { runThroughChatServer } = require('../lib/chat-runner');
19
+ const { ensureFreshProxyToken, readProxyToken } = require('../../proxy-token-store');
20
+ const {
21
+ hydrateModelPreferences,
22
+ getSelectedModel,
23
+ resolveModelSelection,
24
+ DEFAULT_SELECTED_MODELS,
25
+ } = require('../lib/prefs');
26
+ const { buildLocalMcpServerConfigs } = require('../lib/mcp-resolver');
27
+ const {
28
+ chatInputToLegacyFields,
29
+ normalizeChatInput,
30
+ } = require('../../../lib/chatInput');
31
+ const credentialAdapter = require('../../credential-adapter');
32
+
33
+ function harnessToAgent(harness) {
34
+ const map = { claude_code: 'claude', codex: 'codex', opencode: 'opencode', pi: 'pi', amp: 'amp', cursor: 'cursor' };
35
+ return map[harness] || 'claude';
36
+ }
37
+
38
+ function asString(value) {
39
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
40
+ }
41
+
42
+ function isDefaultSessionTitle(title) {
43
+ return ['', 'New Chat', 'New session', 'New code session'].includes(String(title || '').trim());
44
+ }
45
+
46
+ function resolveBaseHarness(session) {
47
+ const metadata = session?.metadata || {};
48
+ return (
49
+ asString(metadata.baseHarnessId) ||
50
+ asString(metadata.harness) ||
51
+ asString(session?.harness) ||
52
+ 'claude_code'
53
+ );
54
+ }
55
+
56
+ function resolveDefaultAuthMethod(baseHarnessId) {
57
+ if (credentialAdapter.VALID_HARNESS_IDS.includes(baseHarnessId)) {
58
+ return credentialAdapter.getPersistedAuthMode(baseHarnessId);
59
+ }
60
+ return 'amalgm';
61
+ }
62
+
63
+ async function sendSlackReply({ sessionId, channel, threadTs, message }) {
64
+ const replyUrl = PROXY_BASE_URL
65
+ ? `${PROXY_BASE_URL.replace(/\/anthropic\/?$/, '')}/slack/reply`
66
+ : null;
67
+ const proxyToken = await ensureFreshProxyToken({ logger: console }) || readProxyToken() || PROXY_TOKEN;
68
+ if (!replyUrl || !proxyToken) {
69
+ throw new Error('Slack reply proxy is not configured.');
70
+ }
71
+
72
+ const res = await fetch(replyUrl, {
73
+ method: 'POST',
74
+ headers: {
75
+ Authorization: `Bearer ${proxyToken}`,
76
+ 'Content-Type': 'application/json',
77
+ },
78
+ body: JSON.stringify({
79
+ sessionId,
80
+ channel,
81
+ threadTs,
82
+ text: message,
83
+ }),
84
+ });
85
+
86
+ if (!res.ok) {
87
+ const text = await res.text().catch(() => '');
88
+ throw new Error(`Slack reply proxy failed: ${res.status} ${text.slice(0, 200)}`);
89
+ }
90
+ return await res.json().catch(() => ({}));
91
+ }
92
+
93
+ async function handleSlackInbound(body, sendJson) {
94
+ const session = body?.session;
95
+ const inbound = body?.inbound || {};
96
+ const sessionId = asString(session?.id);
97
+ const prompt = asString(inbound.text);
98
+ const channel = asString(inbound.channelId);
99
+
100
+ if (!sessionId) return sendJson(400, { error: 'session.id is required' });
101
+ if (!prompt) return sendJson(400, { error: 'inbound.text is required' });
102
+ if (!channel) return sendJson(400, { error: 'inbound.channelId is required' });
103
+
104
+ await hydrateModelPreferences();
105
+
106
+ const metadata = session.metadata || {};
107
+ const baseHarnessId = resolveBaseHarness(session);
108
+ const fallbackModelId =
109
+ asString(metadata.baseModelId) ||
110
+ getSelectedModel(baseHarnessId) ||
111
+ DEFAULT_SELECTED_MODELS[baseHarnessId] ||
112
+ null;
113
+ const defaultAuthMethod = asString(metadata.authMethod) || resolveDefaultAuthMethod(baseHarnessId);
114
+ const cwd = asString(metadata.cwd) || DEFAULT_CWD;
115
+ const computerId = asString(metadata.computerId);
116
+ const slackSystemPrompt = [
117
+ asString(metadata.systemPrompt),
118
+ 'You are replying in Slack. Keep responses clear, direct, and Slack-friendly. Use concise Markdown/mrkdwn when helpful.',
119
+ ].filter(Boolean).join('\n\n');
120
+
121
+ const normalizedChatInput = normalizeChatInput({
122
+ parts: [{ type: 'text', text: prompt }],
123
+ agent: {
124
+ harness: baseHarnessId,
125
+ model: fallbackModelId,
126
+ authMethod: defaultAuthMethod,
127
+ customAgentId: asString(metadata.agentId) || null,
128
+ systemPrompt: slackSystemPrompt || null,
129
+ },
130
+ tools: {
131
+ mcpAppIds: Array.isArray(metadata.mcpAppIds) ? metadata.mcpAppIds : [],
132
+ },
133
+ execution: {
134
+ cwd,
135
+ computerId,
136
+ },
137
+ }, {
138
+ prompt,
139
+ harness: baseHarnessId,
140
+ modelId: fallbackModelId,
141
+ authMethod: defaultAuthMethod,
142
+ cwd,
143
+ computerId,
144
+ systemPrompt: slackSystemPrompt || undefined,
145
+ mcpAppIds: Array.isArray(metadata.mcpAppIds) ? metadata.mcpAppIds : [],
146
+ });
147
+
148
+ const legacyChatFields = chatInputToLegacyFields(normalizedChatInput, {
149
+ prompt,
150
+ harness: baseHarnessId,
151
+ modelId: fallbackModelId,
152
+ authMethod: defaultAuthMethod,
153
+ cwd,
154
+ computerId,
155
+ systemPrompt: slackSystemPrompt || undefined,
156
+ });
157
+ const { modelId: uiModelId, cliModel, reasoningEffort } = resolveModelSelection(
158
+ baseHarnessId,
159
+ legacyChatFields.modelId || fallbackModelId,
160
+ );
161
+ const mcpServers = await buildLocalMcpServerConfigs(legacyChatFields.mcpAppIds || []);
162
+ const userMessageId = crypto.randomUUID();
163
+ const assistantMessageId = crypto.randomUUID();
164
+
165
+ const { outputText } = await runThroughChatServer({
166
+ chatInput: normalizedChatInput,
167
+ prompt: legacyChatFields.prompt,
168
+ userId: AMALGM_USER_ID || session.user_id,
169
+ codeSessionId: sessionId,
170
+ assistantMessageId,
171
+ userMessageId,
172
+ userParts: legacyChatFields.userParts,
173
+ agentId: harnessToAgent(baseHarnessId),
174
+ modelId: uiModelId,
175
+ cliModel,
176
+ ...(reasoningEffort ? { reasoningEffort } : {}),
177
+ cwd: legacyChatFields.cwd || DEFAULT_CWD,
178
+ authMethod: legacyChatFields.authMethod || defaultAuthMethod,
179
+ mcpServers,
180
+ ...(legacyChatFields.systemPrompt ? { systemPrompt: legacyChatFields.systemPrompt } : {}),
181
+ ...(!isDefaultSessionTitle(session.title) ? { resumeSessionId: sessionId } : {}),
182
+ origin: 'slack',
183
+ originId: inbound.originId || inbound.providerEventId || inbound.ts || null,
184
+ originName: 'Slack',
185
+ originHarnessId: session.harness || baseHarnessId,
186
+ originBaseHarnessId: baseHarnessId,
187
+ });
188
+
189
+ const responseText = outputText.trim() || 'Done.';
190
+ await sendSlackReply({
191
+ sessionId,
192
+ channel,
193
+ threadTs: asString(inbound.replyThreadTs),
194
+ message: responseText,
195
+ });
196
+
197
+ return sendJson(200, { ok: true, sessionId, responseLength: responseText.length });
198
+ }
199
+
200
+ module.exports = { handleSlackInbound };