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,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-triggers MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* 5 tools: create, list, get, update, delete. Create returns the webhook URL
|
|
5
|
+
* and secret that the user needs to configure in the external service.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const crypto = require('crypto');
|
|
9
|
+
const { textResult, errorResult } = require('../lib/tool-result');
|
|
10
|
+
const { loadEventTriggers, saveEventTriggers } = require('./store');
|
|
11
|
+
const { getWebhookUrl } = require('./webhook-url');
|
|
12
|
+
const {
|
|
13
|
+
chatInputToLegacyFields,
|
|
14
|
+
getChatInputText,
|
|
15
|
+
normalizeChatInput,
|
|
16
|
+
} = require('../../../lib/chatInput');
|
|
17
|
+
const {
|
|
18
|
+
DEFAULT_SELECTED_MODELS,
|
|
19
|
+
getSelectedModel,
|
|
20
|
+
hydrateModelPreferences,
|
|
21
|
+
} = require('../lib/prefs');
|
|
22
|
+
const credentialAdapter = require('../../credential-adapter');
|
|
23
|
+
|
|
24
|
+
function resolveEventHarness(harness, chatInput) {
|
|
25
|
+
return (
|
|
26
|
+
(chatInput && chatInput.agent && typeof chatInput.agent.harness === 'string' && chatInput.agent.harness)
|
|
27
|
+
|| (typeof harness === 'string' && harness)
|
|
28
|
+
|| 'claude_code'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveEventModel(harness, model) {
|
|
33
|
+
if (typeof model === 'string' && model) return model;
|
|
34
|
+
return getSelectedModel(harness) || DEFAULT_SELECTED_MODELS[harness] || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolveEventAuthMethod(harness, authMethod) {
|
|
38
|
+
if (typeof authMethod === 'string' && authMethod) return authMethod;
|
|
39
|
+
if (credentialAdapter.VALID_HARNESS_IDS.includes(harness)) {
|
|
40
|
+
return credentialAdapter.getPersistedAuthMode(harness);
|
|
41
|
+
}
|
|
42
|
+
return 'amalgm';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildEventChatInput(fields, existing = null) {
|
|
46
|
+
if (fields.chatInput) return fields.chatInput;
|
|
47
|
+
|
|
48
|
+
const existingNonTextParts =
|
|
49
|
+
existing && existing.chatInput && Array.isArray(existing.chatInput.parts)
|
|
50
|
+
? existing.chatInput.parts.filter((part) => part.type !== 'text')
|
|
51
|
+
: [];
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...(existing && existing.chatInput ? existing.chatInput : {}),
|
|
55
|
+
parts: [
|
|
56
|
+
...existingNonTextParts,
|
|
57
|
+
...((typeof fields.agent_prompt === 'string' && fields.agent_prompt)
|
|
58
|
+
? [{ type: 'text', text: fields.agent_prompt }]
|
|
59
|
+
: []),
|
|
60
|
+
],
|
|
61
|
+
agent: {
|
|
62
|
+
...((existing && existing.chatInput && existing.chatInput.agent) || {}),
|
|
63
|
+
...(fields.harness ? { harness: fields.harness } : {}),
|
|
64
|
+
...(fields.model ? { model: fields.model } : {}),
|
|
65
|
+
...(fields.authMethod ? { authMethod: fields.authMethod } : {}),
|
|
66
|
+
},
|
|
67
|
+
tools: (existing && existing.chatInput && existing.chatInput.tools) || { mcpAppIds: [] },
|
|
68
|
+
execution: {
|
|
69
|
+
...((existing && existing.chatInput && existing.chatInput.execution) || {}),
|
|
70
|
+
...(fields.projectPath !== undefined ? { cwd: fields.projectPath || null } : {}),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeEventTriggerRecord(triggerLike, existing = null) {
|
|
76
|
+
const rawChatInput = buildEventChatInput(triggerLike, existing);
|
|
77
|
+
const harness = resolveEventHarness(triggerLike.harness, rawChatInput);
|
|
78
|
+
const model = resolveEventModel(harness, triggerLike.model);
|
|
79
|
+
const authMethod = resolveEventAuthMethod(harness, triggerLike.authMethod);
|
|
80
|
+
const chatInput = normalizeChatInput(rawChatInput, {
|
|
81
|
+
prompt: triggerLike.agent_prompt,
|
|
82
|
+
harness,
|
|
83
|
+
modelId: model,
|
|
84
|
+
authMethod,
|
|
85
|
+
cwd: triggerLike.projectPath,
|
|
86
|
+
projectPath: triggerLike.projectPath,
|
|
87
|
+
});
|
|
88
|
+
const legacy = chatInputToLegacyFields(chatInput, {
|
|
89
|
+
prompt: triggerLike.agent_prompt,
|
|
90
|
+
harness,
|
|
91
|
+
modelId: model,
|
|
92
|
+
authMethod,
|
|
93
|
+
cwd: triggerLike.projectPath,
|
|
94
|
+
projectPath: triggerLike.projectPath,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...triggerLike,
|
|
99
|
+
chatInput,
|
|
100
|
+
agent_prompt: getChatInputText(chatInput, triggerLike.agent_prompt) || legacy.prompt,
|
|
101
|
+
harness: legacy.harness || harness,
|
|
102
|
+
model: legacy.modelId || model,
|
|
103
|
+
authMethod: legacy.authMethod || authMethod,
|
|
104
|
+
projectPath: legacy.cwd || triggerLike.projectPath || null,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = [
|
|
109
|
+
{
|
|
110
|
+
name: 'event_triggers_create',
|
|
111
|
+
description:
|
|
112
|
+
'Create a new event trigger that fires an agent run when a signed event is received (e.g. GitHub push, a first-party artifact event). Returns a webhook URL and secret to configure in the sender.',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
name: {
|
|
117
|
+
type: 'string',
|
|
118
|
+
description: 'Human-readable trigger name (e.g. "PR merged", "Payment received")',
|
|
119
|
+
},
|
|
120
|
+
description: { type: 'string', description: 'Optional markdown description shown in the UI.' },
|
|
121
|
+
source: {
|
|
122
|
+
type: 'string',
|
|
123
|
+
description: 'Source label (e.g. "github", "stripe", "*" for any).',
|
|
124
|
+
},
|
|
125
|
+
event: { type: 'string', description: 'Event label (e.g. "push", "*" for any).' },
|
|
126
|
+
agent_prompt: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description: 'Prompt template for the agent run. Use {payload} to inject the body.',
|
|
129
|
+
},
|
|
130
|
+
projectPath: { type: 'string', description: 'Working directory path for the agent run.' },
|
|
131
|
+
harness: { type: 'string', description: 'Agent harness for the event run.' },
|
|
132
|
+
model: { type: 'string', description: 'Model ID for the event run.' },
|
|
133
|
+
authMethod: { type: 'string', description: 'Auth method for the event run.' },
|
|
134
|
+
chatInput: { type: 'object', description: 'Shared chat input shape for the event run.' },
|
|
135
|
+
},
|
|
136
|
+
required: ['name', 'source', 'event'],
|
|
137
|
+
},
|
|
138
|
+
async handler({ name, description, source, event, agent_prompt, projectPath, harness, model, authMethod, chatInput }) {
|
|
139
|
+
if (!name || (!agent_prompt && !chatInput)) return errorResult('name and agent_prompt or chatInput are required');
|
|
140
|
+
await hydrateModelPreferences();
|
|
141
|
+
const data = loadEventTriggers();
|
|
142
|
+
const trigger = normalizeEventTriggerRecord({
|
|
143
|
+
id: crypto.randomUUID(),
|
|
144
|
+
name,
|
|
145
|
+
description: description || '',
|
|
146
|
+
source: source || '*',
|
|
147
|
+
event: event || '*',
|
|
148
|
+
agent_prompt: agent_prompt || null,
|
|
149
|
+
enabled: true,
|
|
150
|
+
webhookUrl: getWebhookUrl(),
|
|
151
|
+
secret: crypto.randomUUID().replace(/-/g, ''),
|
|
152
|
+
projectPath: projectPath || null,
|
|
153
|
+
harness: harness || null,
|
|
154
|
+
model: model || null,
|
|
155
|
+
authMethod: authMethod || null,
|
|
156
|
+
chatInput: chatInput || null,
|
|
157
|
+
createdAt: new Date().toISOString(),
|
|
158
|
+
lastFiredAt: null,
|
|
159
|
+
});
|
|
160
|
+
data.triggers.push(trigger);
|
|
161
|
+
saveEventTriggers(data);
|
|
162
|
+
console.log(`[AmalgmMCP:Events] Created trigger via MCP: ${name} (${source}:${event})`);
|
|
163
|
+
return textResult(
|
|
164
|
+
`Event trigger created.\n\nWebhook URL: ${trigger.webhookUrl}\nSecret: ${trigger.secret}\n\nConfigure the sender to either sign each request with this secret or send it directly in a secret header, then POST JSON to the webhook URL.\n\n${JSON.stringify(trigger, null, 2)}`,
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'event_triggers_list',
|
|
170
|
+
description: 'List all event triggers with their webhook URLs, secrets, and status',
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
enabled_only: { type: 'boolean', description: 'If true, only return enabled triggers' },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
async handler({ enabled_only }) {
|
|
178
|
+
const data = loadEventTriggers();
|
|
179
|
+
let triggers = data.triggers.map((trigger) => normalizeEventTriggerRecord(trigger));
|
|
180
|
+
if (enabled_only) triggers = triggers.filter((t) => t.enabled);
|
|
181
|
+
if (triggers.length === 0) return textResult('No event triggers found.');
|
|
182
|
+
const summary = triggers
|
|
183
|
+
.map(
|
|
184
|
+
(t) =>
|
|
185
|
+
`- ${t.name} (${t.id})\n ${t.enabled ? 'enabled' : 'disabled'} | source: ${t.source || '*'} | event: ${t.event || '*'}\n URL: ${t.webhookUrl}\n project: ${t.projectPath || '(default)'}\n last fired: ${t.lastFiredAt || 'never'}`,
|
|
186
|
+
)
|
|
187
|
+
.join('\n\n');
|
|
188
|
+
return textResult(`Event triggers:\n\n${summary}`);
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'event_triggers_get',
|
|
193
|
+
description: 'Get detailed information about a specific event trigger',
|
|
194
|
+
inputSchema: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: { trigger_id: { type: 'string' } },
|
|
197
|
+
required: ['trigger_id'],
|
|
198
|
+
},
|
|
199
|
+
async handler({ trigger_id }) {
|
|
200
|
+
const data = loadEventTriggers();
|
|
201
|
+
const trigger = data.triggers.find((t) => t.id === trigger_id);
|
|
202
|
+
if (!trigger) return errorResult(`Trigger not found: ${trigger_id}`);
|
|
203
|
+
return textResult(JSON.stringify(normalizeEventTriggerRecord(trigger), null, 2));
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'event_triggers_update',
|
|
208
|
+
description: 'Update properties of an existing event trigger',
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
trigger_id: { type: 'string' },
|
|
213
|
+
name: { type: 'string' },
|
|
214
|
+
description: { type: 'string' },
|
|
215
|
+
source: { type: 'string' },
|
|
216
|
+
event: { type: 'string' },
|
|
217
|
+
agent_prompt: { type: 'string' },
|
|
218
|
+
enabled: { type: 'boolean' },
|
|
219
|
+
projectPath: { type: 'string' },
|
|
220
|
+
harness: { type: 'string' },
|
|
221
|
+
model: { type: 'string' },
|
|
222
|
+
authMethod: { type: 'string' },
|
|
223
|
+
chatInput: { type: 'object' },
|
|
224
|
+
},
|
|
225
|
+
required: ['trigger_id'],
|
|
226
|
+
},
|
|
227
|
+
async handler({ trigger_id, name, description, source, event, agent_prompt, enabled, projectPath, harness, model, authMethod, chatInput }) {
|
|
228
|
+
const data = loadEventTriggers();
|
|
229
|
+
const trigger = data.triggers.find((t) => t.id === trigger_id);
|
|
230
|
+
if (!trigger) return errorResult(`Trigger not found: ${trigger_id}`);
|
|
231
|
+
|
|
232
|
+
await hydrateModelPreferences();
|
|
233
|
+
|
|
234
|
+
if (name !== undefined) trigger.name = name;
|
|
235
|
+
if (description !== undefined) trigger.description = description || '';
|
|
236
|
+
if (source !== undefined) trigger.source = source;
|
|
237
|
+
if (event !== undefined) trigger.event = event;
|
|
238
|
+
if (agent_prompt !== undefined) trigger.agent_prompt = agent_prompt;
|
|
239
|
+
if (enabled !== undefined) trigger.enabled = enabled;
|
|
240
|
+
if (projectPath !== undefined) trigger.projectPath = projectPath || null;
|
|
241
|
+
if (harness !== undefined) trigger.harness = harness || null;
|
|
242
|
+
if (model !== undefined) trigger.model = model || null;
|
|
243
|
+
if (authMethod !== undefined) trigger.authMethod = authMethod || null;
|
|
244
|
+
if (chatInput !== undefined) trigger.chatInput = chatInput || null;
|
|
245
|
+
trigger.updatedAt = new Date().toISOString();
|
|
246
|
+
if (
|
|
247
|
+
chatInput === undefined
|
|
248
|
+
&& (
|
|
249
|
+
agent_prompt !== undefined
|
|
250
|
+
|| harness !== undefined
|
|
251
|
+
|| model !== undefined
|
|
252
|
+
|| authMethod !== undefined
|
|
253
|
+
|| projectPath !== undefined
|
|
254
|
+
)
|
|
255
|
+
) {
|
|
256
|
+
const existingNonTextParts = Array.isArray(trigger.chatInput?.parts)
|
|
257
|
+
? trigger.chatInput.parts.filter((part) => part.type !== 'text')
|
|
258
|
+
: [];
|
|
259
|
+
trigger.chatInput = {
|
|
260
|
+
...(trigger.chatInput || {}),
|
|
261
|
+
parts: [
|
|
262
|
+
...existingNonTextParts,
|
|
263
|
+
...((typeof trigger.agent_prompt === 'string' && trigger.agent_prompt)
|
|
264
|
+
? [{ type: 'text', text: trigger.agent_prompt }]
|
|
265
|
+
: []),
|
|
266
|
+
],
|
|
267
|
+
agent: {
|
|
268
|
+
...(trigger.chatInput?.agent || {}),
|
|
269
|
+
...(trigger.harness ? { harness: trigger.harness } : {}),
|
|
270
|
+
...(trigger.model ? { model: trigger.model } : {}),
|
|
271
|
+
...(trigger.authMethod ? { authMethod: trigger.authMethod } : {}),
|
|
272
|
+
},
|
|
273
|
+
tools: trigger.chatInput?.tools || { mcpAppIds: [] },
|
|
274
|
+
execution: {
|
|
275
|
+
...(trigger.chatInput?.execution || {}),
|
|
276
|
+
...(projectPath !== undefined ? { cwd: trigger.projectPath || null } : {}),
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
Object.assign(trigger, normalizeEventTriggerRecord(trigger, trigger));
|
|
282
|
+
|
|
283
|
+
saveEventTriggers(data);
|
|
284
|
+
console.log(`[AmalgmMCP:Events] Updated trigger via MCP: ${trigger_id}`);
|
|
285
|
+
return textResult(`Trigger updated.\n\n${JSON.stringify(trigger, null, 2)}`);
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'event_triggers_delete',
|
|
290
|
+
description: 'Delete an event trigger permanently. The webhook URL will stop working.',
|
|
291
|
+
inputSchema: {
|
|
292
|
+
type: 'object',
|
|
293
|
+
properties: { trigger_id: { type: 'string' } },
|
|
294
|
+
required: ['trigger_id'],
|
|
295
|
+
},
|
|
296
|
+
async handler({ trigger_id }) {
|
|
297
|
+
const data = loadEventTriggers();
|
|
298
|
+
const idx = data.triggers.findIndex((t) => t.id === trigger_id);
|
|
299
|
+
if (idx === -1) return errorResult(`Trigger not found: ${trigger_id}`);
|
|
300
|
+
const deleted = data.triggers.splice(idx, 1)[0];
|
|
301
|
+
saveEventTriggers(data);
|
|
302
|
+
console.log(`[AmalgmMCP:Events] Deleted trigger via MCP: ${trigger_id} (${deleted.name})`);
|
|
303
|
+
return textResult(`Trigger "${deleted.name}" deleted.`);
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook URL resolver — local-first.
|
|
3
|
+
*
|
|
4
|
+
* In the container era, this fetched a stable `event_ref` from Supabase and
|
|
5
|
+
* generated `https://{ref}.events.amalgm.ai/events`.
|
|
6
|
+
*
|
|
7
|
+
* Locally, we don't have a public ingress yet. The orchestrator can provide
|
|
8
|
+
* one via AMALGM_EVENTS_PUBLIC_URL (e.g. a tunnel registered with the
|
|
9
|
+
* amalgm-events-proxy); otherwise we return a localhost URL as a placeholder
|
|
10
|
+
* so the trigger record is still well-formed.
|
|
11
|
+
*
|
|
12
|
+
* The public URL is resolved at trigger-create time — existing triggers keep
|
|
13
|
+
* whatever URL they were created with until updated/recreated.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { EVENTS_PUBLIC_URL } = require('../config');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Return the canonical webhook URL for this machine.
|
|
20
|
+
*
|
|
21
|
+
* TODO(events-url): discuss with user — cloudflared tunnel? events-proxy
|
|
22
|
+
* WebSocket tunnel? user-managed port-forward? For now, env var wins.
|
|
23
|
+
*/
|
|
24
|
+
function getWebhookUrl() {
|
|
25
|
+
return EVENTS_PUBLIC_URL;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { getWebhookUrl };
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /fs/* REST routes for the Next.js API to call.
|
|
3
|
+
* Not part of the MCP tool surface; this is the Amalgm computer filesystem API.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const TEXT_EXTENSIONS = new Set([
|
|
11
|
+
'txt', 'md', 'json', 'js', 'ts', 'tsx', 'jsx', 'py', 'html', 'css',
|
|
12
|
+
'xml', 'yaml', 'yml', 'sh', 'bash', 'env', 'gitignore', 'dockerfile',
|
|
13
|
+
'svg', 'csv', 'sql', 'toml', 'rs', 'go', 'rb', 'java', 'kt', 'swift',
|
|
14
|
+
'c', 'cpp', 'cs', 'php', 'scss', 'log', 'ini', 'cfg', 'conf',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const IS_LOCAL_MACHINE = process.env.AMALGM_LOCAL_MODE === 'true';
|
|
18
|
+
const CONTAINER_ROOT = process.env.AMALGM_WORKSPACE_ROOT || '/workspace';
|
|
19
|
+
const MAX_READ_BYTES = Number.parseInt(process.env.AMALGM_FS_MAX_READ_BYTES || '', 10) || 25 * 1024 * 1024;
|
|
20
|
+
|
|
21
|
+
function expandHome(targetPath) {
|
|
22
|
+
if (targetPath === '~') return os.homedir();
|
|
23
|
+
if (typeof targetPath === 'string' && targetPath.startsWith('~/')) {
|
|
24
|
+
return path.join(os.homedir(), targetPath.slice(2));
|
|
25
|
+
}
|
|
26
|
+
return targetPath;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function splitConfiguredRoots(value) {
|
|
30
|
+
if (!value) return [];
|
|
31
|
+
return value
|
|
32
|
+
.split(path.delimiter)
|
|
33
|
+
.flatMap((part) => part.split(','))
|
|
34
|
+
.map((part) => part.trim())
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function configuredRootPaths() {
|
|
39
|
+
const roots = splitConfiguredRoots(process.env.AMALGM_FS_ROOTS);
|
|
40
|
+
|
|
41
|
+
if (IS_LOCAL_MACHINE) {
|
|
42
|
+
roots.push(os.homedir());
|
|
43
|
+
} else {
|
|
44
|
+
roots.push(CONTAINER_ROOT);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return Array.from(new Set(
|
|
48
|
+
roots.map((root) => path.resolve(expandHome(root))),
|
|
49
|
+
));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function existingAllowedRoots() {
|
|
53
|
+
const roots = [];
|
|
54
|
+
for (const root of configuredRootPaths()) {
|
|
55
|
+
try {
|
|
56
|
+
const real = await fs.promises.realpath(root);
|
|
57
|
+
roots.push(real);
|
|
58
|
+
} catch {
|
|
59
|
+
// Ignore configured roots that are not mounted on this computer.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Array.from(new Set(roots));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isWithin(root, target) {
|
|
66
|
+
const relative = path.relative(root, target);
|
|
67
|
+
return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function requestPathOrDefault(targetPath) {
|
|
71
|
+
if (typeof targetPath === 'string' && targetPath.trim()) return targetPath;
|
|
72
|
+
return IS_LOCAL_MACHINE ? os.homedir() : CONTAINER_ROOT;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function nearestExistingParent(targetPath) {
|
|
76
|
+
let current = targetPath;
|
|
77
|
+
while (current && current !== path.dirname(current)) {
|
|
78
|
+
try {
|
|
79
|
+
const stats = await fs.promises.lstat(current);
|
|
80
|
+
return stats.isDirectory() || stats.isSymbolicLink() ? current : path.dirname(current);
|
|
81
|
+
} catch {
|
|
82
|
+
current = path.dirname(current);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return current || path.parse(targetPath).root;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function assertContained(realPath) {
|
|
89
|
+
const roots = await existingAllowedRoots();
|
|
90
|
+
if (roots.length === 0) {
|
|
91
|
+
const rootHint = IS_LOCAL_MACHINE ? os.homedir() : CONTAINER_ROOT;
|
|
92
|
+
throw Object.assign(new Error(`No accessible filesystem roots configured; expected ${rootHint}`), {
|
|
93
|
+
statusCode: 500,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!roots.some((root) => isWithin(root, realPath))) {
|
|
98
|
+
throw Object.assign(new Error('Filesystem path is outside the approved Amalgm computer roots'), {
|
|
99
|
+
statusCode: 403,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function resolveSafePath(targetPath, options = {}) {
|
|
105
|
+
const { forCreate = false } = options;
|
|
106
|
+
const requested = requestPathOrDefault(targetPath);
|
|
107
|
+
|
|
108
|
+
if (typeof requested !== 'string' || requested.length === 0) {
|
|
109
|
+
throw Object.assign(new Error('path is required'), { statusCode: 400 });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const expanded = expandHome(requested);
|
|
113
|
+
if (!path.isAbsolute(expanded)) {
|
|
114
|
+
throw Object.assign(new Error('Path must be absolute or use ~'), { statusCode: 400 });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const normalized = path.resolve(expanded);
|
|
118
|
+
|
|
119
|
+
if (!forCreate) {
|
|
120
|
+
try {
|
|
121
|
+
const real = await fs.promises.realpath(normalized);
|
|
122
|
+
await assertContained(real);
|
|
123
|
+
return normalized;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error && error.code === 'ENOENT') {
|
|
126
|
+
throw Object.assign(new Error('Filesystem path not found'), { statusCode: 404 });
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const real = await fs.promises.realpath(normalized);
|
|
134
|
+
await assertContained(real);
|
|
135
|
+
return normalized;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (!error || error.code !== 'ENOENT') throw error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const existingParent = await nearestExistingParent(path.dirname(normalized));
|
|
141
|
+
const realParent = await fs.promises.realpath(existingParent);
|
|
142
|
+
await assertContained(realParent);
|
|
143
|
+
return normalized;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function statusForError(error, fallbackStatus = 500) {
|
|
147
|
+
if (typeof error?.statusCode === 'number') return error.statusCode;
|
|
148
|
+
if (error?.code === 'ENOENT') return 404;
|
|
149
|
+
if (error?.code === 'EACCES' || error?.code === 'EPERM') return 403;
|
|
150
|
+
return fallbackStatus;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function classifyFile(targetPath) {
|
|
154
|
+
const ext = path.extname(targetPath).slice(1).toLowerCase();
|
|
155
|
+
return {
|
|
156
|
+
ext,
|
|
157
|
+
isText: TEXT_EXTENSIONS.has(ext),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function handleList(query, sendJson) {
|
|
162
|
+
try {
|
|
163
|
+
const targetPath = await resolveSafePath(query.path);
|
|
164
|
+
const entries = await fs.promises.readdir(targetPath, { withFileTypes: true });
|
|
165
|
+
const files = await Promise.all(
|
|
166
|
+
entries.map(async (entry) => {
|
|
167
|
+
const fullPath = path.join(targetPath, entry.name);
|
|
168
|
+
const stats = await fs.promises.lstat(fullPath);
|
|
169
|
+
return {
|
|
170
|
+
name: entry.name,
|
|
171
|
+
isDir: stats.isDirectory(),
|
|
172
|
+
size: stats.size,
|
|
173
|
+
modTime: stats.mtime.toISOString(),
|
|
174
|
+
};
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
files.sort((a, b) => {
|
|
179
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
180
|
+
return a.name.localeCompare(b.name);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
sendJson(200, { files, path: targetPath });
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const message = error instanceof Error ? error.message : 'Failed to list files';
|
|
186
|
+
sendJson(statusForError(error), { error: message });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function handleRead(query, sendJson) {
|
|
191
|
+
try {
|
|
192
|
+
const targetPath = await resolveSafePath(query.path);
|
|
193
|
+
const stats = await fs.promises.stat(targetPath);
|
|
194
|
+
if (stats.isDirectory()) {
|
|
195
|
+
return sendJson(400, { error: 'Cannot read a directory' });
|
|
196
|
+
}
|
|
197
|
+
if (stats.size > MAX_READ_BYTES) {
|
|
198
|
+
return sendJson(413, { error: 'File is too large to read through the filesystem API' });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const content = await fs.promises.readFile(targetPath);
|
|
202
|
+
const { isText } = classifyFile(targetPath);
|
|
203
|
+
|
|
204
|
+
if (isText) {
|
|
205
|
+
sendJson(200, {
|
|
206
|
+
kind: 'text',
|
|
207
|
+
path: targetPath,
|
|
208
|
+
content: content.toString('utf-8'),
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
sendJson(200, {
|
|
214
|
+
kind: 'binary',
|
|
215
|
+
path: targetPath,
|
|
216
|
+
contentBase64: content.toString('base64'),
|
|
217
|
+
filename: path.basename(targetPath),
|
|
218
|
+
});
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const message = error instanceof Error ? error.message : 'Failed to read file';
|
|
221
|
+
sendJson(statusForError(error), { error: message });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function handleWrite(body, sendJson) {
|
|
226
|
+
try {
|
|
227
|
+
const targetPath = await resolveSafePath(body.path, { forCreate: true });
|
|
228
|
+
if (body.content === undefined) {
|
|
229
|
+
return sendJson(400, { error: 'content is required' });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const encoding = body.encoding === 'base64' ? 'base64' : 'utf8';
|
|
233
|
+
const data = encoding === 'base64'
|
|
234
|
+
? Buffer.from(String(body.content), 'base64')
|
|
235
|
+
: Buffer.from(String(body.content), 'utf-8');
|
|
236
|
+
|
|
237
|
+
await fs.promises.mkdir(path.dirname(targetPath), { recursive: true });
|
|
238
|
+
await fs.promises.writeFile(targetPath, data);
|
|
239
|
+
|
|
240
|
+
sendJson(200, { success: true, path: targetPath });
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const message = error instanceof Error ? error.message : 'Failed to write file';
|
|
243
|
+
sendJson(statusForError(error), { error: message });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function handleDelete(body, sendJson) {
|
|
248
|
+
try {
|
|
249
|
+
const targetPath = await resolveSafePath(body.path);
|
|
250
|
+
await fs.promises.rm(targetPath, { recursive: true, force: true });
|
|
251
|
+
sendJson(200, { success: true, path: targetPath });
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const message = error instanceof Error ? error.message : 'Failed to delete';
|
|
254
|
+
sendJson(statusForError(error), { error: message });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function handleMkdir(body, sendJson) {
|
|
259
|
+
try {
|
|
260
|
+
const targetPath = await resolveSafePath(body.path, { forCreate: true });
|
|
261
|
+
await fs.promises.mkdir(targetPath, { recursive: true, mode: 0o755 });
|
|
262
|
+
sendJson(200, { success: true, path: targetPath });
|
|
263
|
+
} catch (error) {
|
|
264
|
+
const message = error instanceof Error ? error.message : 'Failed to create folder';
|
|
265
|
+
sendJson(statusForError(error), { error: message });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function handleRename(body, sendJson) {
|
|
270
|
+
try {
|
|
271
|
+
const oldPath = await resolveSafePath(body.oldPath);
|
|
272
|
+
const newPath = await resolveSafePath(body.newPath, { forCreate: true });
|
|
273
|
+
await fs.promises.mkdir(path.dirname(newPath), { recursive: true });
|
|
274
|
+
await fs.promises.rename(oldPath, newPath);
|
|
275
|
+
sendJson(200, { success: true, oldPath, newPath });
|
|
276
|
+
} catch (error) {
|
|
277
|
+
const message = error instanceof Error ? error.message : 'Failed to rename';
|
|
278
|
+
sendJson(statusForError(error), { error: message });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
module.exports = {
|
|
283
|
+
handleDelete,
|
|
284
|
+
handleList,
|
|
285
|
+
handleMkdir,
|
|
286
|
+
handleRead,
|
|
287
|
+
handleRename,
|
|
288
|
+
handleWrite,
|
|
289
|
+
_private: {
|
|
290
|
+
configuredRootPaths,
|
|
291
|
+
resolveSafePath,
|
|
292
|
+
},
|
|
293
|
+
};
|