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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /events — webhook ingress.
|
|
3
|
+
*
|
|
4
|
+
* Accepts raw POST body, requires a verified signature, and on a hit fires an
|
|
5
|
+
* agent run via events/executor.js and returns 200 with
|
|
6
|
+
* `{ ok, event, triggered }`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
extractSignature,
|
|
11
|
+
matchBySignature,
|
|
12
|
+
pickSourceLabel,
|
|
13
|
+
collectPassthroughHeaders,
|
|
14
|
+
} = require('./matcher');
|
|
15
|
+
const { loadEventTriggers, saveEventTriggers } = require('./store');
|
|
16
|
+
const { executeArtifactEvent } = require('./executor');
|
|
17
|
+
const ring = require('./ring-buffer');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read the raw request body as a string.
|
|
21
|
+
*/
|
|
22
|
+
async function readRawBody(req) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
let data = '';
|
|
25
|
+
req.on('data', (chunk) => {
|
|
26
|
+
data += chunk;
|
|
27
|
+
});
|
|
28
|
+
req.on('end', () => resolve(data));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Handle a POST /events request. Assumes CORS + method checking has already
|
|
34
|
+
* happened. Writes the response via `sendJson`.
|
|
35
|
+
*
|
|
36
|
+
* @param req - IncomingMessage
|
|
37
|
+
* @param sendJson - (status, body) => void
|
|
38
|
+
*/
|
|
39
|
+
async function handleEventsPost(req, sendJson) {
|
|
40
|
+
const rawBody = await readRawBody(req);
|
|
41
|
+
const signature = extractSignature(req.headers);
|
|
42
|
+
if (!signature) {
|
|
43
|
+
console.warn('[AmalgmMCP:Events] Rejected unsigned event request');
|
|
44
|
+
return sendJson(401, { error: 'Webhook signature required' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const triggerData = loadEventTriggers();
|
|
48
|
+
const matchedTrigger = matchBySignature(triggerData.triggers, signature, rawBody);
|
|
49
|
+
if (!matchedTrigger) {
|
|
50
|
+
console.warn('[AmalgmMCP:Events] No trigger matched the webhook signature');
|
|
51
|
+
return sendJson(401, { error: 'Invalid webhook signature — no matching trigger' });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const githubEvent = req.headers['x-github-event'];
|
|
55
|
+
// GitHub webhook handshake: acknowledge verified ping events immediately.
|
|
56
|
+
if (githubEvent === 'ping') {
|
|
57
|
+
console.log('[AmalgmMCP:Events] GitHub ping — pong');
|
|
58
|
+
return sendJson(200, { ok: true, message: 'pong' });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const eventHeaders = collectPassthroughHeaders(req.headers);
|
|
62
|
+
|
|
63
|
+
let parsedBody;
|
|
64
|
+
try {
|
|
65
|
+
parsedBody = JSON.parse(rawBody);
|
|
66
|
+
} catch {
|
|
67
|
+
parsedBody = rawBody;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(`[AmalgmMCP:Events] Matched trigger "${matchedTrigger.name}" (${matchedTrigger.id})`);
|
|
71
|
+
|
|
72
|
+
const eventObj = {
|
|
73
|
+
source: pickSourceLabel(req.headers),
|
|
74
|
+
event:
|
|
75
|
+
githubEvent ||
|
|
76
|
+
req.headers['x-linear-event'] ||
|
|
77
|
+
req.headers['x-gitlab-event'] ||
|
|
78
|
+
'webhook',
|
|
79
|
+
payload: parsedBody,
|
|
80
|
+
headers: eventHeaders,
|
|
81
|
+
timestamp: new Date().toISOString(),
|
|
82
|
+
};
|
|
83
|
+
ring.push(eventObj);
|
|
84
|
+
|
|
85
|
+
const eventDef = {
|
|
86
|
+
name: matchedTrigger.name,
|
|
87
|
+
description: matchedTrigger.name,
|
|
88
|
+
agent_prompt: matchedTrigger.agent_prompt,
|
|
89
|
+
harness: matchedTrigger.harness,
|
|
90
|
+
model: matchedTrigger.model,
|
|
91
|
+
authMethod: matchedTrigger.authMethod,
|
|
92
|
+
chatInput: matchedTrigger.chatInput,
|
|
93
|
+
};
|
|
94
|
+
const syntheticArtifact = { id: matchedTrigger.id, name: matchedTrigger.name };
|
|
95
|
+
const fullPayload = { body: parsedBody, headers: eventHeaders };
|
|
96
|
+
|
|
97
|
+
executeArtifactEvent(syntheticArtifact, eventDef, fullPayload, {
|
|
98
|
+
triggerId: matchedTrigger.id,
|
|
99
|
+
projectPath: matchedTrigger.projectPath,
|
|
100
|
+
}).catch((err) => {
|
|
101
|
+
console.error(
|
|
102
|
+
`[AmalgmMCP:Events] Trigger agent run failed for "${matchedTrigger.name}":`,
|
|
103
|
+
err.message,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
matchedTrigger.lastFiredAt = new Date().toISOString();
|
|
108
|
+
saveEventTriggers(triggerData);
|
|
109
|
+
|
|
110
|
+
return sendJson(200, { ok: true, event: eventObj, triggered: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { handleEventsPost };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signed event matching.
|
|
3
|
+
*
|
|
4
|
+
* Every inbound event sender must authenticate with the shared secret attached
|
|
5
|
+
* to its trigger. We support HMAC-SHA256 headers for generic/GitHub-style
|
|
6
|
+
* senders and direct secret comparison for simple custom/GitLab-style senders.
|
|
7
|
+
*
|
|
8
|
+
* Pure logic — no I/O, no side effects.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
|
|
13
|
+
const HMAC_SIGNATURE_HEADERS = [
|
|
14
|
+
'x-webhook-signature',
|
|
15
|
+
'x-hub-signature-256',
|
|
16
|
+
'x-hub-signature',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const TOKEN_HEADERS = [
|
|
20
|
+
'x-amalgm-webhook-secret',
|
|
21
|
+
'x-webhook-secret',
|
|
22
|
+
'x-gitlab-token',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extract the first supported auth header present.
|
|
27
|
+
*/
|
|
28
|
+
function extractSignature(headers) {
|
|
29
|
+
for (const h of HMAC_SIGNATURE_HEADERS) {
|
|
30
|
+
if (headers[h]) return { header: h, value: headers[h] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const authorization = headers.authorization;
|
|
34
|
+
if (typeof authorization === 'string') {
|
|
35
|
+
const match = authorization.match(/^Bearer\s+(.+)$/i);
|
|
36
|
+
if (match) return { header: 'authorization', value: match[1] };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const h of TOKEN_HEADERS) {
|
|
40
|
+
if (headers[h]) return { header: h, value: headers[h] };
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compute the expected `sha256=<hex>` HMAC over `rawBody` using `secret`.
|
|
47
|
+
*/
|
|
48
|
+
function computeSignature(secret, rawBody) {
|
|
49
|
+
return 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return the trigger whose secret verifies the provided auth proof, or null.
|
|
54
|
+
*/
|
|
55
|
+
function matchBySignature(triggers, signature, rawBody) {
|
|
56
|
+
if (!signature || typeof signature.value !== 'string') return null;
|
|
57
|
+
|
|
58
|
+
for (const trigger of triggers) {
|
|
59
|
+
if (!trigger.enabled || !trigger.secret) continue;
|
|
60
|
+
if (
|
|
61
|
+
signature.header === 'authorization'
|
|
62
|
+
|| TOKEN_HEADERS.includes(signature.header)
|
|
63
|
+
) {
|
|
64
|
+
if (timingSafeEqual(signature.value, trigger.secret)) return trigger;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const expected = computeSignature(trigger.secret, rawBody);
|
|
69
|
+
if (timingSafeEqual(signature.value, expected)) return trigger;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Timing-safe string compare that doesn't throw on length mismatch.
|
|
76
|
+
*/
|
|
77
|
+
function timingSafeEqual(a, b) {
|
|
78
|
+
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
79
|
+
const ab = Buffer.from(a);
|
|
80
|
+
const bb = Buffer.from(b);
|
|
81
|
+
if (ab.length !== bb.length) return false;
|
|
82
|
+
return crypto.timingSafeEqual(ab, bb);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Pick a human-readable source label from webhook headers.
|
|
87
|
+
*/
|
|
88
|
+
function pickSourceLabel(headers) {
|
|
89
|
+
if (headers['x-github-event']) return 'github';
|
|
90
|
+
if (headers['stripe-signature']) return 'stripe';
|
|
91
|
+
if (headers['x-linear-event']) return 'linear';
|
|
92
|
+
if (headers['x-gitlab-event']) return 'gitlab';
|
|
93
|
+
return 'external';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Headers worth passing through to the agent run context.
|
|
98
|
+
*/
|
|
99
|
+
const PASSTHROUGH_HEADERS = [
|
|
100
|
+
'x-github-event',
|
|
101
|
+
'x-github-delivery',
|
|
102
|
+
'x-hub-signature-256',
|
|
103
|
+
'x-hub-signature',
|
|
104
|
+
'stripe-signature',
|
|
105
|
+
'x-webhook-signature',
|
|
106
|
+
'content-type',
|
|
107
|
+
'x-linear-event',
|
|
108
|
+
'x-linear-signature',
|
|
109
|
+
'x-gitlab-event',
|
|
110
|
+
'x-gitlab-token',
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
function collectPassthroughHeaders(reqHeaders) {
|
|
114
|
+
const out = {};
|
|
115
|
+
for (const h of PASSTHROUGH_HEADERS) if (reqHeaders[h]) out[h] = reqHeaders[h];
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
extractSignature,
|
|
121
|
+
computeSignature,
|
|
122
|
+
matchBySignature,
|
|
123
|
+
pickSourceLabel,
|
|
124
|
+
collectPassthroughHeaders,
|
|
125
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /event-triggers/* REST routes for the Next.js API to call.
|
|
3
|
+
* Not part of the MCP tool surface — used by the internal UI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const { loadEventTriggers, saveEventTriggers } = require('./store');
|
|
8
|
+
const { getWebhookUrl } = require('./webhook-url');
|
|
9
|
+
const {
|
|
10
|
+
chatInputToLegacyFields,
|
|
11
|
+
getChatInputText,
|
|
12
|
+
normalizeChatInput,
|
|
13
|
+
} = require('../../../lib/chatInput');
|
|
14
|
+
const {
|
|
15
|
+
DEFAULT_SELECTED_MODELS,
|
|
16
|
+
getSelectedModel,
|
|
17
|
+
hydrateModelPreferences,
|
|
18
|
+
} = require('../lib/prefs');
|
|
19
|
+
const credentialAdapter = require('../../credential-adapter');
|
|
20
|
+
|
|
21
|
+
function resolveEventHarness(harness, chatInput) {
|
|
22
|
+
return (
|
|
23
|
+
(chatInput && chatInput.agent && typeof chatInput.agent.harness === 'string' && chatInput.agent.harness)
|
|
24
|
+
|| (typeof harness === 'string' && harness)
|
|
25
|
+
|| 'claude_code'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolveEventModel(harness, model) {
|
|
30
|
+
if (typeof model === 'string' && model) return model;
|
|
31
|
+
return getSelectedModel(harness) || DEFAULT_SELECTED_MODELS[harness] || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveEventAuthMethod(harness, authMethod) {
|
|
35
|
+
if (typeof authMethod === 'string' && authMethod) return authMethod;
|
|
36
|
+
if (credentialAdapter.VALID_HARNESS_IDS.includes(harness)) {
|
|
37
|
+
return credentialAdapter.getPersistedAuthMode(harness);
|
|
38
|
+
}
|
|
39
|
+
return 'amalgm';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildEventChatInput(fields, existing = null) {
|
|
43
|
+
if (fields.chatInput) return fields.chatInput;
|
|
44
|
+
|
|
45
|
+
const existingNonTextParts =
|
|
46
|
+
existing && existing.chatInput && Array.isArray(existing.chatInput.parts)
|
|
47
|
+
? existing.chatInput.parts.filter((part) => part.type !== 'text')
|
|
48
|
+
: [];
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...(existing && existing.chatInput ? existing.chatInput : {}),
|
|
52
|
+
parts: [
|
|
53
|
+
...existingNonTextParts,
|
|
54
|
+
...((typeof fields.agent_prompt === 'string' && fields.agent_prompt)
|
|
55
|
+
? [{ type: 'text', text: fields.agent_prompt }]
|
|
56
|
+
: []),
|
|
57
|
+
],
|
|
58
|
+
agent: {
|
|
59
|
+
...((existing && existing.chatInput && existing.chatInput.agent) || {}),
|
|
60
|
+
...(fields.harness ? { harness: fields.harness } : {}),
|
|
61
|
+
...(fields.model ? { model: fields.model } : {}),
|
|
62
|
+
...(fields.authMethod ? { authMethod: fields.authMethod } : {}),
|
|
63
|
+
},
|
|
64
|
+
tools: (existing && existing.chatInput && existing.chatInput.tools) || { mcpAppIds: [] },
|
|
65
|
+
execution: {
|
|
66
|
+
...((existing && existing.chatInput && existing.chatInput.execution) || {}),
|
|
67
|
+
...(fields.projectPath !== undefined ? { cwd: fields.projectPath || null } : {}),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeEventTriggerRecord(triggerLike, existing = null) {
|
|
73
|
+
const rawChatInput = buildEventChatInput(triggerLike, existing);
|
|
74
|
+
const harness = resolveEventHarness(triggerLike.harness, rawChatInput);
|
|
75
|
+
const model = resolveEventModel(harness, triggerLike.model);
|
|
76
|
+
const authMethod = resolveEventAuthMethod(harness, triggerLike.authMethod);
|
|
77
|
+
const chatInput = normalizeChatInput(rawChatInput, {
|
|
78
|
+
prompt: triggerLike.agent_prompt,
|
|
79
|
+
harness,
|
|
80
|
+
modelId: model,
|
|
81
|
+
authMethod,
|
|
82
|
+
cwd: triggerLike.projectPath,
|
|
83
|
+
projectPath: triggerLike.projectPath,
|
|
84
|
+
});
|
|
85
|
+
const legacy = chatInputToLegacyFields(chatInput, {
|
|
86
|
+
prompt: triggerLike.agent_prompt,
|
|
87
|
+
harness,
|
|
88
|
+
modelId: model,
|
|
89
|
+
authMethod,
|
|
90
|
+
cwd: triggerLike.projectPath,
|
|
91
|
+
projectPath: triggerLike.projectPath,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
...triggerLike,
|
|
96
|
+
chatInput,
|
|
97
|
+
agent_prompt: getChatInputText(chatInput, triggerLike.agent_prompt) || legacy.prompt,
|
|
98
|
+
harness: legacy.harness || harness,
|
|
99
|
+
model: legacy.modelId || model,
|
|
100
|
+
authMethod: legacy.authMethod || authMethod,
|
|
101
|
+
projectPath: legacy.cwd || triggerLike.projectPath || null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function handleList(req, sendJson) {
|
|
106
|
+
const data = loadEventTriggers();
|
|
107
|
+
sendJson(200, { triggers: data.triggers });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function handleCreate(body, sendJson) {
|
|
111
|
+
const { name, description, source, event, agent_prompt, projectPath, harness, model, authMethod, chatInput } = body;
|
|
112
|
+
if (!name || !source || !event || (!agent_prompt && !chatInput)) {
|
|
113
|
+
return sendJson(400, { error: 'name, source, event, and agent_prompt or chatInput are required' });
|
|
114
|
+
}
|
|
115
|
+
await hydrateModelPreferences();
|
|
116
|
+
const data = loadEventTriggers();
|
|
117
|
+
const trigger = normalizeEventTriggerRecord({
|
|
118
|
+
id: crypto.randomUUID(),
|
|
119
|
+
name,
|
|
120
|
+
description: description || '',
|
|
121
|
+
source,
|
|
122
|
+
event,
|
|
123
|
+
agent_prompt: agent_prompt || null,
|
|
124
|
+
enabled: true,
|
|
125
|
+
webhookUrl: getWebhookUrl(),
|
|
126
|
+
secret: crypto.randomUUID().replace(/-/g, ''),
|
|
127
|
+
projectPath: projectPath || null,
|
|
128
|
+
harness: harness || null,
|
|
129
|
+
model: model || null,
|
|
130
|
+
authMethod: authMethod || null,
|
|
131
|
+
chatInput: chatInput || null,
|
|
132
|
+
createdAt: new Date().toISOString(),
|
|
133
|
+
lastFiredAt: null,
|
|
134
|
+
});
|
|
135
|
+
data.triggers.push(trigger);
|
|
136
|
+
saveEventTriggers(data);
|
|
137
|
+
console.log(`[AmalgmMCP:Events] Created event trigger: ${name} (${source}:${event})`);
|
|
138
|
+
sendJson(200, { ok: true, trigger });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function handleUpdate(body, sendJson) {
|
|
142
|
+
const { trigger_id, ...updates } = body;
|
|
143
|
+
if (!trigger_id) return sendJson(400, { error: 'trigger_id is required' });
|
|
144
|
+
await hydrateModelPreferences();
|
|
145
|
+
const data = loadEventTriggers();
|
|
146
|
+
const trigger = data.triggers.find((t) => t.id === trigger_id);
|
|
147
|
+
if (!trigger) return sendJson(404, { error: 'Trigger not found' });
|
|
148
|
+
Object.assign(trigger, updates, { updatedAt: new Date().toISOString() });
|
|
149
|
+
if (
|
|
150
|
+
updates.chatInput === undefined
|
|
151
|
+
&& (
|
|
152
|
+
updates.agent_prompt !== undefined
|
|
153
|
+
|| updates.harness !== undefined
|
|
154
|
+
|| updates.model !== undefined
|
|
155
|
+
|| updates.authMethod !== undefined
|
|
156
|
+
|| updates.projectPath !== undefined
|
|
157
|
+
)
|
|
158
|
+
) {
|
|
159
|
+
const existingNonTextParts = Array.isArray(trigger.chatInput?.parts)
|
|
160
|
+
? trigger.chatInput.parts.filter((part) => part.type !== 'text')
|
|
161
|
+
: [];
|
|
162
|
+
trigger.chatInput = {
|
|
163
|
+
...(trigger.chatInput || {}),
|
|
164
|
+
parts: [
|
|
165
|
+
...existingNonTextParts,
|
|
166
|
+
...((typeof trigger.agent_prompt === 'string' && trigger.agent_prompt)
|
|
167
|
+
? [{ type: 'text', text: trigger.agent_prompt }]
|
|
168
|
+
: []),
|
|
169
|
+
],
|
|
170
|
+
agent: {
|
|
171
|
+
...(trigger.chatInput?.agent || {}),
|
|
172
|
+
...(trigger.harness ? { harness: trigger.harness } : {}),
|
|
173
|
+
...(trigger.model ? { model: trigger.model } : {}),
|
|
174
|
+
...(trigger.authMethod ? { authMethod: trigger.authMethod } : {}),
|
|
175
|
+
},
|
|
176
|
+
tools: trigger.chatInput?.tools || { mcpAppIds: [] },
|
|
177
|
+
execution: {
|
|
178
|
+
...(trigger.chatInput?.execution || {}),
|
|
179
|
+
...(updates.projectPath !== undefined ? { cwd: trigger.projectPath || null } : {}),
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
Object.assign(trigger, normalizeEventTriggerRecord(trigger, trigger));
|
|
184
|
+
saveEventTriggers(data);
|
|
185
|
+
console.log(`[AmalgmMCP:Events] Updated trigger: ${trigger_id}`);
|
|
186
|
+
sendJson(200, { ok: true, trigger });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function handleDelete(body, sendJson) {
|
|
190
|
+
const { trigger_id } = body;
|
|
191
|
+
if (!trigger_id) return sendJson(400, { error: 'trigger_id is required' });
|
|
192
|
+
const data = loadEventTriggers();
|
|
193
|
+
const idx = data.triggers.findIndex((t) => t.id === trigger_id);
|
|
194
|
+
if (idx === -1) return sendJson(404, { error: 'Trigger not found' });
|
|
195
|
+
data.triggers.splice(idx, 1);
|
|
196
|
+
saveEventTriggers(data);
|
|
197
|
+
sendJson(200, { ok: true });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { handleList, handleCreate, handleUpdate, handleDelete };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory event ring buffer (lost on restart, by design).
|
|
3
|
+
*
|
|
4
|
+
* Used for the "recent events" UI. Capped at MAX_EVENTS.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const MAX_EVENTS = 200;
|
|
8
|
+
const buffer = [];
|
|
9
|
+
|
|
10
|
+
function push(event) {
|
|
11
|
+
buffer.push({ ...event, receivedAt: new Date().toISOString() });
|
|
12
|
+
if (buffer.length > MAX_EVENTS) buffer.shift();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function recent(limit = 50) {
|
|
16
|
+
return buffer.slice(-limit);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { push, recent };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event triggers storage — ~/.amalgm/event-triggers.json
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { EVENT_TRIGGERS_FILE, STORAGE_DIR } = require('../config');
|
|
6
|
+
const { ensureDir, readJson, writeJsonAtomic } = require('../lib/storage');
|
|
7
|
+
const {
|
|
8
|
+
chatInputToLegacyFields,
|
|
9
|
+
getChatInputText,
|
|
10
|
+
normalizeChatInput,
|
|
11
|
+
} = require('../../../lib/chatInput');
|
|
12
|
+
const { DEFAULT_SELECTED_MODELS, getSelectedModel } = require('../lib/prefs');
|
|
13
|
+
const credentialAdapter = require('../../credential-adapter');
|
|
14
|
+
|
|
15
|
+
function normalizeStoredTrigger(trigger) {
|
|
16
|
+
const harness =
|
|
17
|
+
(trigger.chatInput && trigger.chatInput.agent && trigger.chatInput.agent.harness)
|
|
18
|
+
|| trigger.harness
|
|
19
|
+
|| 'claude_code';
|
|
20
|
+
const model =
|
|
21
|
+
(trigger.chatInput && trigger.chatInput.agent && trigger.chatInput.agent.model)
|
|
22
|
+
|| trigger.model
|
|
23
|
+
|| getSelectedModel(harness)
|
|
24
|
+
|| DEFAULT_SELECTED_MODELS[harness]
|
|
25
|
+
|| null;
|
|
26
|
+
const authMethod =
|
|
27
|
+
(trigger.chatInput && trigger.chatInput.agent && trigger.chatInput.agent.authMethod)
|
|
28
|
+
|| trigger.authMethod
|
|
29
|
+
|| (credentialAdapter.VALID_HARNESS_IDS.includes(harness)
|
|
30
|
+
? credentialAdapter.getPersistedAuthMode(harness)
|
|
31
|
+
: 'amalgm');
|
|
32
|
+
const chatInput = normalizeChatInput(trigger.chatInput, {
|
|
33
|
+
prompt: trigger.agent_prompt,
|
|
34
|
+
harness,
|
|
35
|
+
modelId: model,
|
|
36
|
+
authMethod,
|
|
37
|
+
cwd: trigger.projectPath,
|
|
38
|
+
projectPath: trigger.projectPath,
|
|
39
|
+
});
|
|
40
|
+
const legacy = chatInputToLegacyFields(chatInput, {
|
|
41
|
+
prompt: trigger.agent_prompt,
|
|
42
|
+
harness,
|
|
43
|
+
modelId: model,
|
|
44
|
+
authMethod,
|
|
45
|
+
cwd: trigger.projectPath,
|
|
46
|
+
projectPath: trigger.projectPath,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...trigger,
|
|
51
|
+
chatInput,
|
|
52
|
+
agent_prompt: getChatInputText(chatInput, trigger.agent_prompt) || legacy.prompt,
|
|
53
|
+
harness: legacy.harness || harness,
|
|
54
|
+
model: legacy.modelId || model,
|
|
55
|
+
authMethod: legacy.authMethod || authMethod,
|
|
56
|
+
projectPath: legacy.cwd || trigger.projectPath || null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function migrateTriggersData(data) {
|
|
61
|
+
let changed = false;
|
|
62
|
+
const triggers = Array.isArray(data.triggers)
|
|
63
|
+
? data.triggers.map((trigger) => {
|
|
64
|
+
const normalizedTrigger = normalizeStoredTrigger(trigger);
|
|
65
|
+
if (JSON.stringify(normalizedTrigger) !== JSON.stringify(trigger)) {
|
|
66
|
+
changed = true;
|
|
67
|
+
}
|
|
68
|
+
return normalizedTrigger;
|
|
69
|
+
})
|
|
70
|
+
: [];
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
changed,
|
|
74
|
+
data: { triggers },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ensureTriggersDirs() {
|
|
79
|
+
ensureDir(STORAGE_DIR);
|
|
80
|
+
if (!readJson(EVENT_TRIGGERS_FILE, null)) {
|
|
81
|
+
writeJsonAtomic(EVENT_TRIGGERS_FILE, { triggers: [] });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function loadEventTriggers() {
|
|
86
|
+
const raw = readJson(EVENT_TRIGGERS_FILE, { triggers: [] });
|
|
87
|
+
const migrated = migrateTriggersData(raw);
|
|
88
|
+
if (migrated.changed) {
|
|
89
|
+
writeJsonAtomic(EVENT_TRIGGERS_FILE, migrated.data);
|
|
90
|
+
}
|
|
91
|
+
return migrated.data;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function saveEventTriggers(data) {
|
|
95
|
+
writeJsonAtomic(EVENT_TRIGGERS_FILE, data);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { ensureTriggersDirs, loadEventTriggers, saveEventTriggers };
|