amalgm 0.1.51 → 0.1.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/lib/tunnel-events.js +48 -23
  2. package/package.json +2 -2
  3. package/runtime/lib/harnesses.js +12 -4
  4. package/runtime/scripts/amalgm-mcp/agents/store.js +5 -5
  5. package/runtime/scripts/amalgm-mcp/{artifacts → apps}/advertise.js +39 -24
  6. package/runtime/scripts/amalgm-mcp/apps/rest.js +144 -0
  7. package/runtime/scripts/amalgm-mcp/apps/store.js +171 -0
  8. package/runtime/scripts/amalgm-mcp/apps/supervisor.js +439 -0
  9. package/runtime/scripts/amalgm-mcp/apps/tools.js +176 -0
  10. package/runtime/scripts/amalgm-mcp/automations/cell-references.js +237 -0
  11. package/runtime/scripts/amalgm-mcp/automations/context.js +41 -0
  12. package/runtime/scripts/amalgm-mcp/automations/rest.js +148 -0
  13. package/runtime/scripts/amalgm-mcp/automations/runner.js +613 -0
  14. package/runtime/scripts/amalgm-mcp/automations/scheduler.js +90 -0
  15. package/runtime/scripts/amalgm-mcp/automations/store.js +1125 -0
  16. package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +177 -0
  17. package/runtime/scripts/amalgm-mcp/automations/tools.js +418 -0
  18. package/runtime/scripts/amalgm-mcp/automations/validator.js +225 -0
  19. package/runtime/scripts/amalgm-mcp/browser/agent-browser.js +547 -0
  20. package/runtime/scripts/amalgm-mcp/browser/electron-bridge.js +222 -0
  21. package/runtime/scripts/amalgm-mcp/browser/page.js +13 -631
  22. package/runtime/scripts/amalgm-mcp/browser/tools.js +9 -7
  23. package/runtime/scripts/amalgm-mcp/config.js +33 -48
  24. package/runtime/scripts/amalgm-mcp/deps.js +1 -31
  25. package/runtime/scripts/amalgm-mcp/events/ingress.js +50 -42
  26. package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +169 -0
  27. package/runtime/scripts/amalgm-mcp/events/matcher.js +45 -14
  28. package/runtime/scripts/amalgm-mcp/events/store.js +106 -57
  29. package/runtime/scripts/amalgm-mcp/index.js +12 -14
  30. package/runtime/scripts/amalgm-mcp/lib/prefs.js +229 -65
  31. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +13 -27
  32. package/runtime/scripts/amalgm-mcp/server/core-tools.js +2 -3
  33. package/runtime/scripts/amalgm-mcp/server/http.js +106 -56
  34. package/runtime/scripts/amalgm-mcp/slack/inbound.js +1 -1
  35. package/runtime/scripts/amalgm-mcp/state/db.js +119 -0
  36. package/runtime/scripts/amalgm-mcp/state/snapshot.js +16 -3
  37. package/runtime/scripts/amalgm-mcp/tasks/executor.js +1 -1
  38. package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +348 -0
  39. package/runtime/scripts/amalgm-mcp/tests/events-matcher.test.js +23 -0
  40. package/runtime/scripts/amalgm-mcp/tests/workflows-store-runner.test.js +67 -0
  41. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +16 -3
  42. package/runtime/scripts/amalgm-mcp/workflows/compiler.js +222 -0
  43. package/runtime/scripts/amalgm-mcp/workflows/runner.js +593 -0
  44. package/runtime/scripts/amalgm-mcp/workflows/store.js +237 -0
  45. package/runtime/scripts/chat-core/adapters/claude.js +2 -1
  46. package/runtime/scripts/chat-core/auth.js +82 -12
  47. package/runtime/scripts/chat-core/contract.js +5 -1
  48. package/runtime/scripts/chat-core/engine.js +103 -62
  49. package/runtime/scripts/chat-core/event-schema.js +8 -0
  50. package/runtime/scripts/chat-core/events.js +5 -0
  51. package/runtime/scripts/chat-core/normalizers/codex.js +13 -1
  52. package/runtime/scripts/chat-core/parts.js +21 -6
  53. package/runtime/scripts/chat-core/sse.js +3 -0
  54. package/runtime/scripts/chat-core/tests/auth.test.js +84 -6
  55. package/runtime/scripts/chat-core/tests/engine.test.js +312 -0
  56. package/runtime/scripts/chat-core/tests/native-config.test.js +23 -0
  57. package/runtime/scripts/chat-core/tool-shape.js +4 -4
  58. package/runtime/scripts/chat-core/tooling/active-memory.js +5 -4
  59. package/runtime/scripts/chat-core/tooling/native-binaries.js +34 -9
  60. package/runtime/scripts/chat-core/tooling/native-config.js +34 -3
  61. package/runtime/scripts/local-gateway.js +34 -27
  62. package/runtime/scripts/platform-context.txt +76 -94
  63. package/runtime/scripts/amalgm-mcp/artifacts/rest.js +0 -103
  64. package/runtime/scripts/amalgm-mcp/artifacts/store.js +0 -157
  65. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +0 -439
  66. package/runtime/scripts/amalgm-mcp/artifacts/tools.js +0 -176
  67. package/runtime/scripts/amalgm-mcp/events/executor.js +0 -258
  68. package/runtime/scripts/amalgm-mcp/events/rest.js +0 -214
  69. package/runtime/scripts/amalgm-mcp/events/tools.js +0 -323
  70. package/runtime/scripts/amalgm-mcp/tasks/rest.js +0 -110
  71. package/runtime/scripts/amalgm-mcp/tasks/tools.js +0 -416
@@ -1,176 +0,0 @@
1
- /**
2
- * Minimal artifact MCP tools.
3
- *
4
- * An artifact is a user-hosted production service. We do not scaffold or wrap
5
- * it; we register the command, keep it alive locally, and expose an artifact
6
- * DNS route while this computer is connected.
7
- */
8
-
9
- const { textResult, errorResult } = require('../lib/tool-result');
10
- const { getConnectedRoutes, loadArtifacts } = require('./store');
11
- const {
12
- connectDns,
13
- disconnectDns,
14
- redeployArtifact,
15
- registerArtifact,
16
- startArtifact,
17
- stopArtifact,
18
- } = require('./supervisor');
19
-
20
- function artifactSummary(artifact) {
21
- return [
22
- `${artifact.name} (${artifact.id})`,
23
- `status: ${artifact.status}`,
24
- `cwd: ${artifact.cwd}`,
25
- `port: ${artifact.port}`,
26
- `dns: ${artifact.dnsConnected ? artifact.publicUrl : 'disconnected'}`,
27
- `autostart: ${artifact.autostart !== false}`,
28
- `keepAlive: ${artifact.keepAlive !== false}`,
29
- artifact.error ? `error: ${artifact.error}` : null,
30
- ].filter(Boolean).join('\n');
31
- }
32
-
33
- module.exports = [
34
- {
35
- name: 'artifacts_register',
36
- description:
37
- 'Register a user-hosted artifact from an existing project on this computer. Artifacts should be production services: prefer passing a build_command such as "npm run build" and a production start_command such as "npm run start -- --host 0.0.0.0 --port {port}" or any command that binds to PORT/{port}. The MCP does not scaffold or enforce a framework; it stores the record in ~/.amalgm, starts it, keeps it alive while the computer is on, and connects it to artifact DNS by default.',
38
- inputSchema: {
39
- type: 'object',
40
- properties: {
41
- name: { type: 'string', description: 'Human-readable artifact name' },
42
- cwd: { type: 'string', description: 'Project directory on this computer' },
43
- port: { type: 'number', description: 'Port the artifact should bind to. If omitted, one is allocated.' },
44
- build_command: { type: 'string', description: 'Optional production build command, run before start/redeploy' },
45
- start_command: { type: 'string', description: 'Required production start command. Use {port} or PORT.' },
46
- description: { type: 'string' },
47
- connect_dns: { type: 'boolean', description: 'Whether to expose through artifact DNS immediately. Defaults true.' },
48
- autostart: { type: 'boolean', description: 'Start on Amalgm boot. Defaults true.' },
49
- keep_alive: { type: 'boolean', description: 'Restart if the process exits. Defaults true.' },
50
- },
51
- required: ['start_command'],
52
- },
53
- async handler(args) {
54
- try {
55
- const artifact = await registerArtifact(args || {});
56
- return textResult(`Artifact registered.\n\n${artifactSummary(artifact)}`);
57
- } catch (err) {
58
- return errorResult(err.message);
59
- }
60
- },
61
- },
62
- {
63
- name: 'artifacts_list',
64
- description: 'List locally registered artifacts from ~/.amalgm/artifacts.json.',
65
- inputSchema: { type: 'object', properties: {} },
66
- async handler() {
67
- const artifacts = loadArtifacts().artifacts;
68
- if (artifacts.length === 0) return textResult('No artifacts registered.');
69
- return textResult(artifacts.map(artifactSummary).join('\n\n'));
70
- },
71
- },
72
- {
73
- name: 'artifacts_routes',
74
- description: 'List artifact DNS routes currently advertised by this computer.',
75
- inputSchema: { type: 'object', properties: {} },
76
- async handler() {
77
- const routes = getConnectedRoutes();
78
- return textResult(JSON.stringify({ routes }, null, 2));
79
- },
80
- },
81
- {
82
- name: 'artifacts_redeploy',
83
- description:
84
- 'Redeploy an artifact: optionally update cwd/build/start/port settings, run its build command if configured, then restart the production process. DNS connection state is preserved.',
85
- inputSchema: {
86
- type: 'object',
87
- properties: {
88
- artifact_id: { type: 'string' },
89
- cwd: { type: 'string' },
90
- port: { type: 'number' },
91
- build_command: { type: 'string' },
92
- start_command: { type: 'string' },
93
- autostart: { type: 'boolean' },
94
- keep_alive: { type: 'boolean' },
95
- },
96
- required: ['artifact_id'],
97
- },
98
- async handler(args) {
99
- try {
100
- const artifact = await redeployArtifact(args.artifact_id, args || {});
101
- return textResult(`Artifact redeployed.\n\n${artifactSummary(artifact)}`);
102
- } catch (err) {
103
- return errorResult(err.message);
104
- }
105
- },
106
- },
107
- {
108
- name: 'artifacts_disconnect_dns',
109
- description:
110
- 'Disconnect an artifact from artifact DNS without stopping the local process. The artifact remains registered locally and can be reconnected later.',
111
- inputSchema: {
112
- type: 'object',
113
- properties: { artifact_id: { type: 'string' } },
114
- required: ['artifact_id'],
115
- },
116
- async handler({ artifact_id }) {
117
- try {
118
- const artifact = await disconnectDns(artifact_id);
119
- return textResult(`Artifact DNS disconnected.\n\n${artifactSummary(artifact)}`);
120
- } catch (err) {
121
- return errorResult(err.message);
122
- }
123
- },
124
- },
125
- {
126
- name: 'artifacts_connect_dns',
127
- description: 'Reconnect a registered artifact to artifact DNS.',
128
- inputSchema: {
129
- type: 'object',
130
- properties: { artifact_id: { type: 'string' } },
131
- required: ['artifact_id'],
132
- },
133
- async handler({ artifact_id }) {
134
- try {
135
- const artifact = await connectDns(artifact_id);
136
- return textResult(`Artifact DNS connected.\n\n${artifactSummary(artifact)}`);
137
- } catch (err) {
138
- return errorResult(err.message);
139
- }
140
- },
141
- },
142
- {
143
- name: 'artifacts_start',
144
- description: 'Start a stopped registered artifact using its production start command.',
145
- inputSchema: {
146
- type: 'object',
147
- properties: { artifact_id: { type: 'string' } },
148
- required: ['artifact_id'],
149
- },
150
- async handler({ artifact_id }) {
151
- try {
152
- const artifact = await startArtifact(artifact_id);
153
- return textResult(`Artifact started.\n\n${artifactSummary(artifact)}`);
154
- } catch (err) {
155
- return errorResult(err.message);
156
- }
157
- },
158
- },
159
- {
160
- name: 'artifacts_stop',
161
- description: 'Stop a registered artifact and mark it as intentionally stopped.',
162
- inputSchema: {
163
- type: 'object',
164
- properties: { artifact_id: { type: 'string' } },
165
- required: ['artifact_id'],
166
- },
167
- async handler({ artifact_id }) {
168
- try {
169
- const artifact = await stopArtifact(artifact_id);
170
- return textResult(`Artifact stopped.\n\n${artifactSummary(artifact)}`);
171
- } catch (err) {
172
- return errorResult(err.message);
173
- }
174
- },
175
- },
176
- ];
@@ -1,258 +0,0 @@
1
- /**
2
- * Event-fired agent execution.
3
- *
4
- * Used for both webhook-triggered event runs and (legacy) artifact-fired
5
- * events. Mirrors tasks/executor.js but without persisted run logs — each
6
- * event run gets its own one-shot Supabase session (if configured).
7
- */
8
-
9
- const crypto = require('crypto');
10
- const os = require('os');
11
-
12
- const { AMALGM_COMPUTER_ID, AMALGM_USER_ID, DEFAULT_CWD } = require('../config');
13
- const { runThroughChatServer } = require('../lib/chat-runner');
14
- const { hasSupabase, supabaseInsert, supabasePatch } = require('../lib/supabase');
15
- const { recordEventRun } = require('./store');
16
- const {
17
- chatInputToLegacyFields,
18
- normalizeChatInput,
19
- withChatInputText,
20
- } = require('../../../lib/chatInput');
21
- const { DEFAULT_SELECTED_MODELS, resolveModelSelection } = require('../lib/prefs');
22
- const credentialAdapter = require('../../credential-adapter');
23
- const { buildLocalMcpServerConfigs } = require('../lib/mcp-resolver');
24
-
25
- function harnessToAgent(harness) {
26
- const map = { claude_code: 'claude', codex: 'codex', opencode: 'opencode', pi: 'pi', amp: 'amp', cursor: 'cursor' };
27
- return map[harness] || 'claude';
28
- }
29
-
30
- function normalizeEffort(value) {
31
- if (typeof value !== 'string') return null;
32
- const normalized = value.trim().toLowerCase().replace(/_/g, '-');
33
- if (normalized === 'low') return 'low';
34
- if (normalized === 'medium') return 'medium';
35
- if (normalized === 'high') return 'high';
36
- if (normalized === 'xhigh' || normalized === 'extra-high' || normalized === 'extra high') return 'xhigh';
37
- if (normalized === 'max') return 'max';
38
- return null;
39
- }
40
-
41
- function sanitizeRunModelSettings(settings, harness) {
42
- if (!settings || typeof settings !== 'object') return {};
43
- const effort = normalizeEffort(settings.effort || settings.reasoningEffort || settings.reasoning);
44
- const allowedEfforts =
45
- harness === 'claude_code'
46
- ? new Set(['low', 'medium', 'high', 'xhigh', 'max'])
47
- : harness === 'codex'
48
- ? new Set(['low', 'medium', 'high', 'xhigh'])
49
- : new Set();
50
- return {
51
- ...(effort && allowedEfforts.has(effort) ? { effort } : {}),
52
- ...(harness === 'claude_code' && settings.fastMode === true ? { fastMode: true } : {}),
53
- };
54
- }
55
-
56
- function modelIdForSettings(modelId, harness, modelSettings) {
57
- if (harness !== 'codex' || !modelSettings.effort || typeof modelId !== 'string') return modelId;
58
- return modelId
59
- .replace(/(?::thinking-|-thinking-)(low|medium|high|xhigh)$/i, '')
60
- .replace(/\/(low|medium|high|xhigh)$/i, '');
61
- }
62
-
63
- function resolveEventRequest(eventDef, projectPath) {
64
- const harness =
65
- (eventDef.chatInput && eventDef.chatInput.agent && eventDef.chatInput.agent.harness)
66
- || eventDef.harness
67
- || 'claude_code';
68
- const model =
69
- (eventDef.chatInput && eventDef.chatInput.agent && eventDef.chatInput.agent.model)
70
- || eventDef.model
71
- || DEFAULT_SELECTED_MODELS[harness]
72
- || null;
73
- const authMethod =
74
- (eventDef.chatInput && eventDef.chatInput.agent && eventDef.chatInput.agent.authMethod)
75
- || eventDef.authMethod
76
- || (credentialAdapter.VALID_HARNESS_IDS.includes(harness)
77
- ? credentialAdapter.getPersistedAuthMode(harness)
78
- : 'amalgm');
79
- const templateChatInput = normalizeChatInput(eventDef.chatInput, {
80
- prompt:
81
- eventDef.agent_prompt
82
- || `An event "${eventDef.name}" was triggered. Payload: {payload}`,
83
- harness,
84
- modelId: model,
85
- authMethod,
86
- cwd: projectPath,
87
- projectPath,
88
- });
89
-
90
- return {
91
- harness,
92
- templateChatInput,
93
- authMethod,
94
- model,
95
- modelSettings: sanitizeRunModelSettings(
96
- eventDef.modelSettings || (eventDef.chatInput && eventDef.chatInput.modelSettings),
97
- harness,
98
- ),
99
- };
100
- }
101
-
102
- /**
103
- * Execute an event-triggered agent run.
104
- *
105
- * @param {{ id: string, name: string }} artifactOrTrigger - source metadata
106
- * @param {{ name: string, agent_prompt: string }} eventDef - trigger/event definition
107
- * @param {any} payload - event payload (injected into prompt via {payload})
108
- * @param {{ triggerId?: string, projectPath?: string }} [opts]
109
- */
110
- async function executeArtifactEvent(artifactOrTrigger, eventDef, payload, opts = {}) {
111
- const { triggerId, projectPath } = opts;
112
- const codeSessionId = crypto.randomUUID();
113
- const userMessageId = crypto.randomUUID();
114
- const assistantMessageId = crypto.randomUUID();
115
- const startedAt = new Date().toISOString();
116
- const { harness, templateChatInput, authMethod, model, modelSettings } = resolveEventRequest(eventDef, projectPath);
117
- const payloadText = JSON.stringify(payload);
118
- const chatInput = withChatInputText(templateChatInput, (text) => text.replaceAll('{payload}', payloadText));
119
- const legacy = chatInputToLegacyFields(chatInput, {
120
- prompt: eventDef.agent_prompt,
121
- harness,
122
- modelId: model,
123
- authMethod,
124
- cwd: projectPath,
125
- projectPath,
126
- });
127
- const cwd = legacy.cwd || DEFAULT_CWD;
128
- const runTriggerId = triggerId || artifactOrTrigger.id;
129
- const modelSelection = resolveModelSelection(
130
- harness,
131
- modelIdForSettings(legacy.modelId || model, harness, modelSettings),
132
- );
133
- const mcpServers = await buildLocalMcpServerConfigs(legacy.mcpAppIds || []);
134
- const reasoningEffort = modelSettings.effort || modelSelection.reasoningEffort;
135
-
136
- console.log(
137
- `[AmalgmMCP:Event] Starting agent run for ${artifactOrTrigger.id}:${eventDef.name} (session: ${codeSessionId}, cwd: ${cwd})`,
138
- );
139
-
140
- recordEventRun(runTriggerId, {
141
- runId: codeSessionId,
142
- sessionId: codeSessionId,
143
- triggerName: eventDef.name,
144
- startedAt,
145
- status: 'running',
146
- harness,
147
- projectPath: projectPath || null,
148
- prompt: legacy.prompt,
149
- }, { source: 'event_runs:start' });
150
-
151
- if (hasSupabase()) {
152
- try {
153
- const metadata = {
154
- artifactId: artifactOrTrigger.id,
155
- artifactName: artifactOrTrigger.name,
156
- event: eventDef.name,
157
- automated: true,
158
- originName: eventDef.name,
159
- };
160
- if (triggerId) metadata.triggerId = triggerId;
161
- await supabaseInsert('sessions', {
162
- id: codeSessionId,
163
- user_id: AMALGM_USER_ID,
164
- computer_id: AMALGM_COMPUTER_ID,
165
- container_id: os.hostname() || 'artifact-event',
166
- harness,
167
- title: 'New Chat',
168
- origin: 'event',
169
- origin_id: triggerId || artifactOrTrigger.id,
170
- project_path: projectPath || null,
171
- status: 'in-progress',
172
- metadata,
173
- });
174
- } catch (err) {
175
- console.error('[AmalgmMCP:Event] DB session creation failed:', err.message);
176
- }
177
- }
178
-
179
- try {
180
- const { outputText } = await runThroughChatServer({
181
- chatInput,
182
- prompt: legacy.prompt,
183
- userId: AMALGM_USER_ID,
184
- computerId: AMALGM_COMPUTER_ID,
185
- codeSessionId,
186
- assistantMessageId,
187
- userMessageId,
188
- userParts: legacy.userParts,
189
- agentId: harnessToAgent(harness),
190
- modelId: modelSelection.modelId || legacy.modelId || null,
191
- cliModel: modelSelection.cliModel || null,
192
- ...(reasoningEffort ? { reasoningEffort } : {}),
193
- ...(modelSettings.fastMode ? { fastMode: true } : {}),
194
- cwd,
195
- authMethod: legacy.authMethod || authMethod,
196
- mcpServers,
197
- origin: {
198
- type: 'event',
199
- id: triggerId || artifactOrTrigger.id,
200
- name: eventDef.name,
201
- projectPath: projectPath || null,
202
- },
203
- automated: true,
204
- });
205
-
206
- const durationMs = Date.now() - new Date(startedAt).getTime();
207
- console.log(
208
- `[AmalgmMCP:Event] ${artifactOrTrigger.id}:${eventDef.name} completed in ${durationMs}ms (output: ${outputText.length} chars)`,
209
- );
210
-
211
- recordEventRun(runTriggerId, {
212
- runId: codeSessionId,
213
- sessionId: codeSessionId,
214
- triggerName: eventDef.name,
215
- finishedAt: new Date().toISOString(),
216
- status: 'completed',
217
- durationMs,
218
- outputLength: outputText.length,
219
- }, { source: 'event_runs:complete' });
220
-
221
- if (hasSupabase()) {
222
- supabasePatch('sessions', 'id', codeSessionId, {
223
- last_message_at: new Date().toISOString(),
224
- status: 'complete',
225
- new_messages: true,
226
- }).catch(() => {});
227
- }
228
- } catch (err) {
229
- console.error(
230
- `[AmalgmMCP:Event] ${artifactOrTrigger.id}:${eventDef.name} failed:`,
231
- err.message,
232
- );
233
- recordEventRun(runTriggerId, {
234
- runId: codeSessionId,
235
- sessionId: codeSessionId,
236
- triggerName: eventDef.name,
237
- finishedAt: new Date().toISOString(),
238
- status: 'failed',
239
- error: err.message,
240
- }, { source: 'event_runs:failed' });
241
- if (hasSupabase()) {
242
- supabasePatch('sessions', 'id', codeSessionId, {
243
- last_message_at: new Date().toISOString(),
244
- status: 'error',
245
- new_messages: true,
246
- metadata: {
247
- artifactId: artifactOrTrigger.id,
248
- event: eventDef.name,
249
- automated: true,
250
- originName: eventDef.name,
251
- error: err.message,
252
- },
253
- }).catch(() => {});
254
- }
255
- }
256
- }
257
-
258
- module.exports = { executeArtifactEvent };
@@ -1,214 +0,0 @@
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
- const activeMemory = require('../../chat-core/tooling/active-memory');
21
-
22
- function resolveEventHarness(harness, chatInput) {
23
- return (
24
- (chatInput && chatInput.agent && typeof chatInput.agent.harness === 'string' && chatInput.agent.harness)
25
- || (typeof harness === 'string' && harness)
26
- || 'claude_code'
27
- );
28
- }
29
-
30
- function resolveEventModel(harness, model) {
31
- if (typeof model === 'string' && model) return model;
32
- return getSelectedModel(harness) || DEFAULT_SELECTED_MODELS[harness] || null;
33
- }
34
-
35
- function resolveEventAuthMethod(harness, authMethod) {
36
- if (typeof authMethod === 'string' && authMethod) return authMethod;
37
- if (credentialAdapter.VALID_HARNESS_IDS.includes(harness)) {
38
- return credentialAdapter.getPersistedAuthMode(harness);
39
- }
40
- return 'amalgm';
41
- }
42
-
43
- function buildEventChatInput(fields, existing = null) {
44
- if (fields.chatInput) return fields.chatInput;
45
-
46
- const existingNonTextParts =
47
- existing && existing.chatInput && Array.isArray(existing.chatInput.parts)
48
- ? existing.chatInput.parts.filter((part) => part.type !== 'text')
49
- : [];
50
-
51
- return {
52
- ...(existing && existing.chatInput ? existing.chatInput : {}),
53
- parts: [
54
- ...existingNonTextParts,
55
- ...((typeof fields.agent_prompt === 'string' && fields.agent_prompt)
56
- ? [{ type: 'text', text: fields.agent_prompt }]
57
- : []),
58
- ],
59
- agent: {
60
- ...((existing && existing.chatInput && existing.chatInput.agent) || {}),
61
- ...(fields.harness ? { harness: fields.harness } : {}),
62
- ...(fields.model ? { model: fields.model } : {}),
63
- ...(fields.authMethod ? { authMethod: fields.authMethod } : {}),
64
- },
65
- tools: (existing && existing.chatInput && existing.chatInput.tools) || { mcpAppIds: [] },
66
- execution: {
67
- ...((existing && existing.chatInput && existing.chatInput.execution) || {}),
68
- ...(fields.projectPath !== undefined ? { cwd: fields.projectPath || null } : {}),
69
- },
70
- };
71
- }
72
-
73
- function normalizeEventTriggerRecord(triggerLike, existing = null) {
74
- const rawChatInput = buildEventChatInput(triggerLike, existing);
75
- const harness = resolveEventHarness(triggerLike.harness, rawChatInput);
76
- const model = resolveEventModel(harness, triggerLike.model);
77
- const authMethod = resolveEventAuthMethod(harness, triggerLike.authMethod);
78
- const chatInput = normalizeChatInput(rawChatInput, {
79
- prompt: triggerLike.agent_prompt,
80
- harness,
81
- modelId: model,
82
- authMethod,
83
- cwd: triggerLike.projectPath,
84
- projectPath: triggerLike.projectPath,
85
- });
86
- const legacy = chatInputToLegacyFields(chatInput, {
87
- prompt: triggerLike.agent_prompt,
88
- harness,
89
- modelId: model,
90
- authMethod,
91
- cwd: triggerLike.projectPath,
92
- projectPath: triggerLike.projectPath,
93
- });
94
-
95
- return {
96
- ...triggerLike,
97
- chatInput,
98
- agent_prompt: getChatInputText(chatInput, triggerLike.agent_prompt) || legacy.prompt,
99
- harness: legacy.harness || harness,
100
- model: legacy.modelId || model,
101
- authMethod: legacy.authMethod || authMethod,
102
- projectPath: legacy.cwd || triggerLike.projectPath || null,
103
- };
104
- }
105
-
106
- async function handleList(req, sendJson) {
107
- const data = loadEventTriggers();
108
- sendJson(200, { triggers: data.triggers });
109
- }
110
-
111
- async function handleCreate(body, sendJson) {
112
- const { name, description, source, event, agent_prompt, projectPath, harness, model, modelSettings, authMethod, chatInput } = body;
113
- if (!name || !source || !event || (!agent_prompt && !chatInput)) {
114
- return sendJson(400, { error: 'name, source, event, and agent_prompt or chatInput are required' });
115
- }
116
- await hydrateModelPreferences();
117
- const data = loadEventTriggers();
118
- const trigger = normalizeEventTriggerRecord({
119
- id: crypto.randomUUID(),
120
- name,
121
- description: description || '',
122
- source,
123
- event,
124
- agent_prompt: agent_prompt || null,
125
- enabled: true,
126
- webhookUrl: getWebhookUrl(),
127
- secret: crypto.randomUUID().replace(/-/g, ''),
128
- projectPath: projectPath || null,
129
- harness: harness || null,
130
- model: model || null,
131
- modelSettings: modelSettings || null,
132
- authMethod: authMethod || null,
133
- chatInput: chatInput || null,
134
- createdAt: new Date().toISOString(),
135
- lastFiredAt: null,
136
- });
137
- data.triggers.push(trigger);
138
- saveEventTriggers(data);
139
- activeMemory.ensureConstructMemory({
140
- type: 'event',
141
- id: trigger.id,
142
- name: trigger.name,
143
- projectPath: trigger.projectPath,
144
- }, { source: 'event:create' });
145
- console.log(`[AmalgmMCP:Events] Created event trigger: ${name} (${source}:${event})`);
146
- sendJson(200, { ok: true, trigger });
147
- }
148
-
149
- async function handleUpdate(body, sendJson) {
150
- const { trigger_id, ...updates } = body;
151
- if (!trigger_id) return sendJson(400, { error: 'trigger_id is required' });
152
- await hydrateModelPreferences();
153
- const data = loadEventTriggers();
154
- const trigger = data.triggers.find((t) => t.id === trigger_id);
155
- if (!trigger) return sendJson(404, { error: 'Trigger not found' });
156
- Object.assign(trigger, updates, { updatedAt: new Date().toISOString() });
157
- if (
158
- updates.chatInput === undefined
159
- && (
160
- updates.agent_prompt !== undefined
161
- || updates.harness !== undefined
162
- || updates.model !== undefined
163
- || updates.authMethod !== undefined
164
- || updates.projectPath !== undefined
165
- )
166
- ) {
167
- const existingNonTextParts = Array.isArray(trigger.chatInput?.parts)
168
- ? trigger.chatInput.parts.filter((part) => part.type !== 'text')
169
- : [];
170
- trigger.chatInput = {
171
- ...(trigger.chatInput || {}),
172
- parts: [
173
- ...existingNonTextParts,
174
- ...((typeof trigger.agent_prompt === 'string' && trigger.agent_prompt)
175
- ? [{ type: 'text', text: trigger.agent_prompt }]
176
- : []),
177
- ],
178
- agent: {
179
- ...(trigger.chatInput?.agent || {}),
180
- ...(trigger.harness ? { harness: trigger.harness } : {}),
181
- ...(trigger.model ? { model: trigger.model } : {}),
182
- ...(trigger.authMethod ? { authMethod: trigger.authMethod } : {}),
183
- },
184
- tools: trigger.chatInput?.tools || { mcpAppIds: [] },
185
- execution: {
186
- ...(trigger.chatInput?.execution || {}),
187
- ...(updates.projectPath !== undefined ? { cwd: trigger.projectPath || null } : {}),
188
- },
189
- };
190
- }
191
- Object.assign(trigger, normalizeEventTriggerRecord(trigger, trigger));
192
- saveEventTriggers(data);
193
- activeMemory.ensureConstructMemory({
194
- type: 'event',
195
- id: trigger.id,
196
- name: trigger.name,
197
- projectPath: trigger.projectPath,
198
- }, { source: 'event:update' });
199
- console.log(`[AmalgmMCP:Events] Updated trigger: ${trigger_id}`);
200
- sendJson(200, { ok: true, trigger });
201
- }
202
-
203
- async function handleDelete(body, sendJson) {
204
- const { trigger_id } = body;
205
- if (!trigger_id) return sendJson(400, { error: 'trigger_id is required' });
206
- const data = loadEventTriggers();
207
- const idx = data.triggers.findIndex((t) => t.id === trigger_id);
208
- if (idx === -1) return sendJson(404, { error: 'Trigger not found' });
209
- data.triggers.splice(idx, 1);
210
- saveEventTriggers(data);
211
- sendJson(200, { ok: true });
212
- }
213
-
214
- module.exports = { handleList, handleCreate, handleUpdate, handleDelete };