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.
- package/README.md +37 -1
- package/bin/amalgm.js +8 -2
- package/lib/auth-store.js +223 -0
- package/lib/cli.js +1000 -0
- package/lib/paths.js +30 -0
- package/lib/supervisor.js +467 -0
- package/lib/tunnel-chat.js +328 -0
- package/lib/tunnel-events.js +499 -0
- package/package.json +29 -3
- package/runtime/README.md +4 -0
- package/runtime/lib/chatInput.js +306 -0
- package/runtime/lib/harnesses.js +988 -0
- package/runtime/lib/local/amalgmStore.js +128 -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 +165 -0
- package/runtime/scripts/amalgm-mcp/agents/store.js +153 -0
- package/runtime/scripts/amalgm-mcp/agents/talk.js +1156 -0
- package/runtime/scripts/amalgm-mcp/agents/tools.js +210 -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 +141 -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 +138 -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 +98 -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 +393 -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 +80 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +151 -0
- package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +335 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +116 -0
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -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 +139 -0
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
- package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +389 -0
- package/runtime/scripts/chat-core/adapters/claude.js +163 -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 +326 -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 +195 -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 +129 -0
- package/runtime/scripts/fs-watcher.js +888 -0
- package/runtime/scripts/local-gateway.js +852 -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,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 };
|