amalgm 0.0.0 → 0.0.32

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 (116) hide show
  1. package/README.md +41 -1
  2. package/bin/amalgm.js +8 -2
  3. package/lib/auth-store.js +223 -0
  4. package/lib/cli.js +1132 -0
  5. package/lib/paths.js +30 -0
  6. package/lib/supervisor.js +476 -0
  7. package/lib/tunnel-chat.js +328 -0
  8. package/lib/tunnel-events.js +499 -0
  9. package/package.json +30 -3
  10. package/runtime/README.md +4 -0
  11. package/runtime/lib/chatInput.js +315 -0
  12. package/runtime/lib/harnesses.js +988 -0
  13. package/runtime/lib/local/amalgmStore.js +136 -0
  14. package/runtime/lib/local/credentialResolver.js +425 -0
  15. package/runtime/lib/mcpApps/registry.js +619 -0
  16. package/runtime/package.json +5 -0
  17. package/runtime/scripts/amalgm-mcp/agents/rest.js +144 -0
  18. package/runtime/scripts/amalgm-mcp/agents/store.js +684 -0
  19. package/runtime/scripts/amalgm-mcp/agents/talk.js +1162 -0
  20. package/runtime/scripts/amalgm-mcp/agents/tools.js +221 -0
  21. package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
  22. package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
  23. package/runtime/scripts/amalgm-mcp/artifacts/store.js +157 -0
  24. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
  25. package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
  26. package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
  27. package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
  28. package/runtime/scripts/amalgm-mcp/config.js +140 -0
  29. package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
  30. package/runtime/scripts/amalgm-mcp/deps.js +40 -0
  31. package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
  32. package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
  33. package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
  34. package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
  35. package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
  36. package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
  37. package/runtime/scripts/amalgm-mcp/events/store.js +113 -0
  38. package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
  39. package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
  40. package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
  41. package/runtime/scripts/amalgm-mcp/index.js +100 -0
  42. package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
  43. package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
  44. package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
  45. package/runtime/scripts/amalgm-mcp/lib/prefs.js +441 -0
  46. package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
  47. package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
  48. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
  49. package/runtime/scripts/amalgm-mcp/local/rest.js +87 -0
  50. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +234 -0
  51. package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
  52. package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
  53. package/runtime/scripts/amalgm-mcp/server/http.js +377 -0
  54. package/runtime/scripts/amalgm-mcp/server/mcp.js +127 -0
  55. package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
  56. package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
  57. package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
  58. package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
  59. package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
  60. package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
  61. package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
  62. package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
  63. package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
  64. package/runtime/scripts/amalgm-mcp/tasks/store.js +154 -0
  65. package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
  66. package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
  67. package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
  68. package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
  69. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
  70. package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
  71. package/runtime/scripts/amalgm-mcp/workspace/rest.js +497 -0
  72. package/runtime/scripts/chat-core/adapters/claude.js +165 -0
  73. package/runtime/scripts/chat-core/adapters/codex.js +313 -0
  74. package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
  75. package/runtime/scripts/chat-core/auth.js +177 -0
  76. package/runtime/scripts/chat-core/contract.js +328 -0
  77. package/runtime/scripts/chat-core/credentials/store.js +212 -0
  78. package/runtime/scripts/chat-core/egress.js +87 -0
  79. package/runtime/scripts/chat-core/engine.js +253 -0
  80. package/runtime/scripts/chat-core/event-schema.js +231 -0
  81. package/runtime/scripts/chat-core/events.js +190 -0
  82. package/runtime/scripts/chat-core/index.js +11 -0
  83. package/runtime/scripts/chat-core/input.js +50 -0
  84. package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
  85. package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
  86. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
  87. package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
  88. package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
  89. package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
  90. package/runtime/scripts/chat-core/parts.js +253 -0
  91. package/runtime/scripts/chat-core/recorder.js +65 -0
  92. package/runtime/scripts/chat-core/runtime.js +86 -0
  93. package/runtime/scripts/chat-core/server.js +163 -0
  94. package/runtime/scripts/chat-core/sse.js +196 -0
  95. package/runtime/scripts/chat-core/stores.js +100 -0
  96. package/runtime/scripts/chat-core/tool-display.js +149 -0
  97. package/runtime/scripts/chat-core/tool-shape.js +143 -0
  98. package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
  99. package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
  100. package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
  101. package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
  102. package/runtime/scripts/chat-core/usage.js +343 -0
  103. package/runtime/scripts/chat-server/config.js +110 -0
  104. package/runtime/scripts/chat-server/db.js +529 -0
  105. package/runtime/scripts/chat-server/index.js +33 -0
  106. package/runtime/scripts/chat-server/model-catalog.js +327 -0
  107. package/runtime/scripts/chat-server.js +75 -0
  108. package/runtime/scripts/credential-adapter.js +131 -0
  109. package/runtime/scripts/fs-watcher.js +888 -0
  110. package/runtime/scripts/local-gateway.js +854 -0
  111. package/runtime/scripts/platform-context.txt +246 -0
  112. package/runtime/scripts/port-monitor.js +175 -0
  113. package/runtime/scripts/proxy-token-store.js +162 -0
  114. package/runtime/scripts/runtime-auth.js +163 -0
  115. package/runtime/scripts/test-claude-code-models.js +87 -0
  116. package/runtime/tsconfig.json +15 -0
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Config — env vars, ports, paths.
3
+ *
4
+ * All storage is rooted at AMALGM_DIR (default: ~/.amalgm). The orchestrator
5
+ * (electron/main.ts) sets AMALGM_DIR; we never hardcode /workspace or /persist.
6
+ * Same pattern as scripts/credential-adapter.js.
7
+ */
8
+
9
+ const path = require('path');
10
+ const os = require('os');
11
+ const fs = require('fs');
12
+ const {
13
+ DEFAULT_PROXY_BASE_URL,
14
+ proxyBaseUrl,
15
+ readProxyToken,
16
+ } = require('../proxy-token-store');
17
+
18
+ const PORT = parseInt(process.env.AMALGM_MCP_PORT || '8083', 10);
19
+ const MCP_PROTOCOL_VERSION = '2024-11-05';
20
+
21
+ const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
22
+
23
+ // Storage file/dir layout under AMALGM_DIR.
24
+ const STORAGE_DIR = AMALGM_DIR; // tasks.json etc. live directly under ~/.amalgm
25
+ const LOCAL_DB_FILE = path.join(STORAGE_DIR, 'amalgm.db');
26
+ const TASKS_FILE = path.join(STORAGE_DIR, 'tasks.json');
27
+ const AGENTS_FILE = path.join(STORAGE_DIR, 'agents.json');
28
+ const ARTIFACTS_DIR = path.join(STORAGE_DIR, 'artifacts');
29
+ const ARTIFACTS_FILE = path.join(STORAGE_DIR, 'artifacts.json');
30
+ const COMPUTER_RECORD_FILE = path.join(STORAGE_DIR, 'computer.json');
31
+ const COMPUTER_AUTH_FILE = path.join(STORAGE_DIR, 'auth.json');
32
+ const TASK_RUNS_DIR = path.join(STORAGE_DIR, 'task-runs');
33
+ const EVENT_TRIGGERS_FILE = path.join(STORAGE_DIR, 'event-triggers.json');
34
+ const AGENT_CONVOS_DIR = path.join(STORAGE_DIR, 'agent-convos');
35
+
36
+ function readJson(file) {
37
+ try {
38
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ function cleanString(value) {
45
+ return typeof value === 'string' && value.trim() ? value.trim() : '';
46
+ }
47
+
48
+ // Chat server (local Next.js/Electron) — same process tree; no cloud hop.
49
+ const CHAT_SERVER_URL = `http://localhost:${process.env.CHAT_SERVER_PORT || 8084}`;
50
+
51
+ const SCHEDULER_INTERVAL_MS = 30_000;
52
+
53
+ // Default working directory for automated runs (was /workspace in the container).
54
+ const DEFAULT_CWD = process.env.AMALGM_DEFAULT_CWD || os.homedir();
55
+
56
+ // ── Supabase / proxy ────────────────────────────────────────────────────────
57
+ // Sensitive provider keys live on the Fly.io api-proxy. When running locally
58
+ // without signing in, none of this is configured and hasSupabase() returns false.
59
+
60
+ const savedComputerRecord = readJson(COMPUTER_RECORD_FILE) || {};
61
+ const savedComputerAuth = readJson(COMPUTER_AUTH_FILE) || {};
62
+ const AMALGM_USER_ID = cleanString(process.env.AMALGM_USER_ID)
63
+ || cleanString(savedComputerRecord.user_id)
64
+ || cleanString(savedComputerAuth.user_id);
65
+ const AMALGM_COMPUTER_ID = cleanString(process.env.AMALGM_COMPUTER_ID)
66
+ || cleanString(savedComputerRecord.computer_id)
67
+ || cleanString(savedComputerAuth.computer_id)
68
+ || cleanString(process.env.AMALGM_CONTAINER_ID);
69
+
70
+ const PROXY_TOKEN = readProxyToken()
71
+ || (process.env.AMALGM_RUNTIME_SOURCE === 'npm' ? '' : process.env.ANTHROPIC_API_KEY || '');
72
+ const PROXY_BASE_URL = proxyBaseUrl() || (PROXY_TOKEN ? DEFAULT_PROXY_BASE_URL : '');
73
+ const SUPABASE_PROXY_URL = PROXY_BASE_URL ? `${PROXY_BASE_URL}/supabase` : '';
74
+
75
+ const SUPABASE_URL_DIRECT =
76
+ process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || '';
77
+ const SUPABASE_KEY_DIRECT = process.env.SUPABASE_SERVICE_ROLE_KEY || '';
78
+
79
+ const SUPABASE_URL = SUPABASE_PROXY_URL || SUPABASE_URL_DIRECT;
80
+ const SUPABASE_AUTH_HEADERS = SUPABASE_PROXY_URL
81
+ ? { Authorization: `Bearer ${PROXY_TOKEN}` }
82
+ : { apikey: SUPABASE_KEY_DIRECT, Authorization: `Bearer ${SUPABASE_KEY_DIRECT}` };
83
+
84
+ // ── Webhook public URL ──────────────────────────────────────────────────────
85
+ // In local mode we don't have a public ingress yet. If the orchestrator plumbs
86
+ // one in (e.g. via a tunnel to amalgm-events-proxy), it sets this env var.
87
+ // Otherwise event-trigger records get a localhost URL as a placeholder.
88
+ const EVENTS_PUBLIC_URL =
89
+ process.env.AMALGM_EVENTS_PUBLIC_URL || `http://localhost:${PORT}/events`;
90
+
91
+ const ARTIFACTS_DOMAIN = process.env.AMALGM_ARTIFACTS_DOMAIN || 'artifacts.amalgm.ai';
92
+ const ARTIFACT_PORT_MIN = parseInt(process.env.AMALGM_ARTIFACT_PORT_MIN || '9100', 10);
93
+ const ARTIFACT_PORT_MAX = parseInt(process.env.AMALGM_ARTIFACT_PORT_MAX || '9199', 10);
94
+ const GATEWAY_BASE_URL = (
95
+ process.env.AMALGM_GATEWAY_INTERNAL_URL
96
+ || process.env.AMALGM_GATEWAY_URL
97
+ || 'https://wire.events.amalgm.ai'
98
+ ).replace(/\/$/, '');
99
+ const GATEWAY_INTERNAL_SECRET =
100
+ process.env.AMALGM_GATEWAY_INTERNAL_SECRET
101
+ || process.env.PROXY_ADMIN_SECRET
102
+ || process.env.ADMIN_SECRET
103
+ || '';
104
+ const ARTIFACT_ROUTE_SYNC_INTERVAL_MS = parseInt(
105
+ process.env.AMALGM_ARTIFACT_ROUTE_SYNC_INTERVAL_MS || '30000',
106
+ 10,
107
+ );
108
+
109
+ module.exports = {
110
+ PORT,
111
+ MCP_PROTOCOL_VERSION,
112
+ AMALGM_DIR,
113
+ STORAGE_DIR,
114
+ LOCAL_DB_FILE,
115
+ TASKS_FILE,
116
+ AGENTS_FILE,
117
+ ARTIFACTS_DIR,
118
+ ARTIFACTS_FILE,
119
+ COMPUTER_RECORD_FILE,
120
+ COMPUTER_AUTH_FILE,
121
+ TASK_RUNS_DIR,
122
+ EVENT_TRIGGERS_FILE,
123
+ AGENT_CONVOS_DIR,
124
+ CHAT_SERVER_URL,
125
+ SCHEDULER_INTERVAL_MS,
126
+ DEFAULT_CWD,
127
+ AMALGM_USER_ID,
128
+ AMALGM_COMPUTER_ID,
129
+ PROXY_TOKEN,
130
+ PROXY_BASE_URL,
131
+ SUPABASE_URL,
132
+ SUPABASE_AUTH_HEADERS,
133
+ EVENTS_PUBLIC_URL,
134
+ ARTIFACTS_DOMAIN,
135
+ ARTIFACT_PORT_MIN,
136
+ ARTIFACT_PORT_MAX,
137
+ GATEWAY_BASE_URL,
138
+ GATEWAY_INTERNAL_SECRET,
139
+ ARTIFACT_ROUTE_SYNC_INTERVAL_MS,
140
+ };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const { AMALGM_DIR } = require('../config');
4
+ const {
5
+ deleteCredential,
6
+ listCredentials,
7
+ upsertCredential,
8
+ } = require('../../chat-core/credentials/store');
9
+
10
+ async function handleList(sendJson) {
11
+ try {
12
+ sendJson(200, { credentials: listCredentials(AMALGM_DIR) });
13
+ } catch (error) {
14
+ sendJson(500, { error: error instanceof Error ? error.message : 'Failed to list credentials' });
15
+ }
16
+ }
17
+
18
+ async function handleSave(body, sendJson) {
19
+ try {
20
+ const saved = upsertCredential(AMALGM_DIR, {
21
+ id: body?.id,
22
+ name: body?.name,
23
+ key: body?.key,
24
+ });
25
+ sendJson(200, { credential: saved });
26
+ } catch (error) {
27
+ sendJson(400, { error: error instanceof Error ? error.message : 'Failed to save credential' });
28
+ }
29
+ }
30
+
31
+ async function handleDelete(body, sendJson) {
32
+ try {
33
+ const id = String(body?.id || '').trim();
34
+ if (!id) return sendJson(400, { error: 'id is required' });
35
+ sendJson(200, deleteCredential(AMALGM_DIR, id));
36
+ } catch (error) {
37
+ sendJson(500, { error: error instanceof Error ? error.message : 'Failed to delete credential' });
38
+ }
39
+ }
40
+
41
+ module.exports = {
42
+ handleDelete,
43
+ handleList,
44
+ handleSave,
45
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Deps — resolves optional native deps (cron-parser, playwright).
3
+ *
4
+ * The container-era code looked in /opt/amalgm-mcp-dep/ first and then the
5
+ * global require path. Locally we only need the global require path — these
6
+ * are npm deps of the main repo.
7
+ *
8
+ * Playwright is lazy: we don't require() it until a browser_* tool is called.
9
+ * This keeps boot fast and lets the MCP server run on machines that haven't
10
+ * run `npx playwright install chromium` yet.
11
+ */
12
+
13
+ function loadCronParser() {
14
+ try {
15
+ return require('cron-parser');
16
+ } catch (err) {
17
+ console.error('[AmalgmMCP] cron-parser not installed. Run: npm install cron-parser');
18
+ throw err;
19
+ }
20
+ }
21
+
22
+ let _playwrightMod = null;
23
+
24
+ function loadPlaywright() {
25
+ if (_playwrightMod) return _playwrightMod;
26
+ for (const mod of ['playwright', 'playwright-core']) {
27
+ try {
28
+ _playwrightMod = require(mod);
29
+ console.log(`[AmalgmMCP] Loaded Playwright from: ${mod}`);
30
+ return _playwrightMod;
31
+ } catch {
32
+ // Try next candidate.
33
+ }
34
+ }
35
+ throw new Error(
36
+ 'Playwright is not installed. Run: npm install playwright && npx playwright install chromium',
37
+ );
38
+ }
39
+
40
+ module.exports = { loadCronParser, loadPlaywright };
@@ -0,0 +1,215 @@
1
+ /**
2
+ * Email inbound handoff.
3
+ *
4
+ * The api-proxy verifies the provider ingress, resolves/creates the Supabase
5
+ * session, then forwards the clean inbound email payload to this local route.
6
+ * From here email follows the same local chat-server execution path as user
7
+ * input, tasks, events, and agent-to-agent conversations. The proxy is only
8
+ * used again to send the email reply.
9
+ */
10
+
11
+ const crypto = require('crypto');
12
+
13
+ const {
14
+ AMALGM_USER_ID,
15
+ DEFAULT_CWD,
16
+ PROXY_BASE_URL,
17
+ PROXY_TOKEN,
18
+ } = require('../config');
19
+ const { runThroughChatServer } = require('../lib/chat-runner');
20
+ const { formatNotificationEmail } = require('../lib/email-md');
21
+ const { ensureFreshProxyToken, readProxyToken } = require('../../proxy-token-store');
22
+ const {
23
+ hydrateModelPreferences,
24
+ getSelectedModel,
25
+ resolveModelSelection,
26
+ DEFAULT_SELECTED_MODELS,
27
+ } = require('../lib/prefs');
28
+ const { buildLocalMcpServerConfigs } = require('../lib/mcp-resolver');
29
+ const {
30
+ chatInputToLegacyFields,
31
+ normalizeChatInput,
32
+ } = require('../../../lib/chatInput');
33
+ const credentialAdapter = require('../../credential-adapter');
34
+
35
+ function harnessToAgent(harness) {
36
+ const map = { claude_code: 'claude', codex: 'codex', opencode: 'opencode', pi: 'pi', amp: 'amp', cursor: 'cursor' };
37
+ return map[harness] || 'claude';
38
+ }
39
+
40
+ function asString(value) {
41
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
42
+ }
43
+
44
+ function normalizeSubject(subject) {
45
+ const trimmed = asString(subject) || 'Amalgm reply';
46
+ return /^re:/i.test(trimmed) ? trimmed : `Re: ${trimmed}`;
47
+ }
48
+
49
+ function isDefaultSessionTitle(title) {
50
+ return ['', 'New Chat', 'New session', 'New code session'].includes(String(title || '').trim());
51
+ }
52
+
53
+ function resolveBaseHarness(session) {
54
+ const metadata = session?.metadata || {};
55
+ return (
56
+ asString(metadata.baseHarnessId) ||
57
+ asString(metadata.harness) ||
58
+ asString(session?.harness) ||
59
+ 'claude_code'
60
+ );
61
+ }
62
+
63
+ function resolveDefaultAuthMethod(baseHarnessId) {
64
+ if (credentialAdapter.VALID_HARNESS_IDS.includes(baseHarnessId)) {
65
+ return credentialAdapter.getPersistedAuthMode(baseHarnessId);
66
+ }
67
+ return 'amalgm';
68
+ }
69
+
70
+ async function sendEmailReply({ sessionId, to, subject, message, inbound }) {
71
+ const notifyUrl = PROXY_BASE_URL
72
+ ? `${PROXY_BASE_URL.replace(/\/anthropic\/?$/, '')}/email/reply`
73
+ : null;
74
+ const proxyToken = await ensureFreshProxyToken({ logger: console }) || readProxyToken() || PROXY_TOKEN;
75
+ if (!notifyUrl || !proxyToken) {
76
+ throw new Error('Email reply proxy is not configured.');
77
+ }
78
+
79
+ const html = formatNotificationEmail(message, null);
80
+ const res = await fetch(notifyUrl, {
81
+ method: 'POST',
82
+ headers: {
83
+ Authorization: `Bearer ${proxyToken}`,
84
+ 'Content-Type': 'application/json',
85
+ },
86
+ body: JSON.stringify({
87
+ sessionId,
88
+ to,
89
+ subject: normalizeSubject(subject),
90
+ html,
91
+ text: message,
92
+ inReplyTo: inbound.externalMessageId || inbound.inReplyTo || null,
93
+ references: Array.isArray(inbound.references) ? inbound.references : [],
94
+ }),
95
+ });
96
+
97
+ if (!res.ok) {
98
+ const text = await res.text().catch(() => '');
99
+ throw new Error(`Email reply proxy failed: ${res.status} ${text.slice(0, 200)}`);
100
+ }
101
+ return await res.json().catch(() => ({}));
102
+ }
103
+
104
+ async function handleEmailInbound(body, sendJson) {
105
+ const session = body?.session;
106
+ const inbound = body?.inbound || {};
107
+ const sessionId = asString(session?.id);
108
+ const prompt = asString(inbound.text);
109
+ const from = asString(inbound.from);
110
+
111
+ if (!sessionId) return sendJson(400, { error: 'session.id is required' });
112
+ if (!prompt) return sendJson(400, { error: 'inbound.text is required' });
113
+ if (!from) return sendJson(400, { error: 'inbound.from is required' });
114
+
115
+ await hydrateModelPreferences();
116
+
117
+ const metadata = session.metadata || {};
118
+ const baseHarnessId = resolveBaseHarness(session);
119
+ const fallbackModelId =
120
+ asString(metadata.baseModelId) ||
121
+ getSelectedModel(baseHarnessId) ||
122
+ DEFAULT_SELECTED_MODELS[baseHarnessId] ||
123
+ null;
124
+ const defaultAuthMethod = asString(metadata.authMethod) || resolveDefaultAuthMethod(baseHarnessId);
125
+ const cwd = asString(metadata.cwd) || DEFAULT_CWD;
126
+ const computerId = asString(metadata.computerId);
127
+
128
+ const normalizedChatInput = normalizeChatInput({
129
+ parts: [{ type: 'text', text: prompt }],
130
+ agent: {
131
+ harness: baseHarnessId,
132
+ model: fallbackModelId,
133
+ authMethod: defaultAuthMethod,
134
+ customAgentId: asString(metadata.agentId) || null,
135
+ systemPrompt: asString(metadata.systemPrompt),
136
+ },
137
+ tools: {
138
+ mcpAppIds: Array.isArray(metadata.mcpAppIds) ? metadata.mcpAppIds : [],
139
+ },
140
+ execution: {
141
+ cwd,
142
+ computerId,
143
+ },
144
+ }, {
145
+ prompt,
146
+ harness: baseHarnessId,
147
+ modelId: fallbackModelId,
148
+ authMethod: defaultAuthMethod,
149
+ cwd,
150
+ computerId,
151
+ systemPrompt: asString(metadata.systemPrompt),
152
+ mcpAppIds: Array.isArray(metadata.mcpAppIds) ? metadata.mcpAppIds : [],
153
+ });
154
+
155
+ const legacyChatFields = chatInputToLegacyFields(normalizedChatInput, {
156
+ prompt,
157
+ harness: baseHarnessId,
158
+ modelId: fallbackModelId,
159
+ authMethod: defaultAuthMethod,
160
+ cwd,
161
+ computerId,
162
+ systemPrompt: asString(metadata.systemPrompt),
163
+ });
164
+ const { modelId: uiModelId, cliModel, reasoningEffort } = resolveModelSelection(
165
+ baseHarnessId,
166
+ legacyChatFields.modelId || fallbackModelId,
167
+ );
168
+ const mcpServers = await buildLocalMcpServerConfigs(legacyChatFields.mcpAppIds || []);
169
+ const userMessageId = crypto.randomUUID();
170
+ const assistantMessageId = crypto.randomUUID();
171
+
172
+ const emailContext = [
173
+ `Inbound email from ${from}.`,
174
+ inbound.subject ? `Subject: ${inbound.subject}` : '',
175
+ 'Reply in a concise email-friendly style unless the user asks for detail.',
176
+ ].filter(Boolean).join('\n');
177
+
178
+ const { outputText } = await runThroughChatServer({
179
+ chatInput: normalizedChatInput,
180
+ prompt: legacyChatFields.prompt,
181
+ userId: AMALGM_USER_ID || session.user_id,
182
+ codeSessionId: sessionId,
183
+ assistantMessageId,
184
+ userMessageId,
185
+ userParts: legacyChatFields.userParts,
186
+ agentId: harnessToAgent(baseHarnessId),
187
+ modelId: uiModelId,
188
+ cliModel,
189
+ ...(reasoningEffort ? { reasoningEffort } : {}),
190
+ cwd: legacyChatFields.cwd || DEFAULT_CWD,
191
+ authMethod: legacyChatFields.authMethod || defaultAuthMethod,
192
+ mcpServers,
193
+ ...(legacyChatFields.systemPrompt ? { systemPrompt: legacyChatFields.systemPrompt } : {}),
194
+ ...(!isDefaultSessionTitle(session.title) ? { resumeSessionId: sessionId } : {}),
195
+ origin: 'email',
196
+ originId: inbound.providerMessageId || inbound.externalMessageId || inbound.providerEventId || null,
197
+ originName: 'Email',
198
+ originHarnessId: session.harness || baseHarnessId,
199
+ originBaseHarnessId: baseHarnessId,
200
+ emailContext,
201
+ });
202
+
203
+ const responseText = outputText.trim() || 'Done.';
204
+ await sendEmailReply({
205
+ sessionId,
206
+ to: from,
207
+ subject: inbound.subject || session.title || 'Amalgm reply',
208
+ message: responseText,
209
+ inbound,
210
+ });
211
+
212
+ return sendJson(200, { ok: true, sessionId, responseLength: responseText.length });
213
+ }
214
+
215
+ module.exports = { handleEmailInbound };
@@ -0,0 +1,179 @@
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 {
16
+ chatInputToLegacyFields,
17
+ normalizeChatInput,
18
+ withChatInputText,
19
+ } = require('../../../lib/chatInput');
20
+ const { DEFAULT_SELECTED_MODELS, resolveModelSelection } = require('../lib/prefs');
21
+ const credentialAdapter = require('../../credential-adapter');
22
+ const { buildLocalMcpServerConfigs } = require('../lib/mcp-resolver');
23
+
24
+ function harnessToAgent(harness) {
25
+ const map = { claude_code: 'claude', codex: 'codex', opencode: 'opencode', pi: 'pi', amp: 'amp', cursor: 'cursor' };
26
+ return map[harness] || 'claude';
27
+ }
28
+
29
+ function resolveEventRequest(eventDef, projectPath) {
30
+ const harness =
31
+ (eventDef.chatInput && eventDef.chatInput.agent && eventDef.chatInput.agent.harness)
32
+ || eventDef.harness
33
+ || 'claude_code';
34
+ const model =
35
+ (eventDef.chatInput && eventDef.chatInput.agent && eventDef.chatInput.agent.model)
36
+ || eventDef.model
37
+ || DEFAULT_SELECTED_MODELS[harness]
38
+ || null;
39
+ const authMethod =
40
+ (eventDef.chatInput && eventDef.chatInput.agent && eventDef.chatInput.agent.authMethod)
41
+ || eventDef.authMethod
42
+ || (credentialAdapter.VALID_HARNESS_IDS.includes(harness)
43
+ ? credentialAdapter.getPersistedAuthMode(harness)
44
+ : 'amalgm');
45
+ const templateChatInput = normalizeChatInput(eventDef.chatInput, {
46
+ prompt:
47
+ eventDef.agent_prompt
48
+ || `An event "${eventDef.name}" was triggered. Payload: {payload}`,
49
+ harness,
50
+ modelId: model,
51
+ authMethod,
52
+ cwd: projectPath,
53
+ projectPath,
54
+ });
55
+
56
+ return {
57
+ harness,
58
+ templateChatInput,
59
+ authMethod,
60
+ model,
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Execute an event-triggered agent run.
66
+ *
67
+ * @param {{ id: string, name: string }} artifactOrTrigger - source metadata
68
+ * @param {{ name: string, agent_prompt: string }} eventDef - trigger/event definition
69
+ * @param {any} payload - event payload (injected into prompt via {payload})
70
+ * @param {{ triggerId?: string, projectPath?: string }} [opts]
71
+ */
72
+ async function executeArtifactEvent(artifactOrTrigger, eventDef, payload, opts = {}) {
73
+ const { triggerId, projectPath } = opts;
74
+ const codeSessionId = crypto.randomUUID();
75
+ const userMessageId = crypto.randomUUID();
76
+ const assistantMessageId = crypto.randomUUID();
77
+ const startedAt = new Date().toISOString();
78
+ const { harness, templateChatInput, authMethod, model } = resolveEventRequest(eventDef, projectPath);
79
+ const payloadText = JSON.stringify(payload);
80
+ const chatInput = withChatInputText(templateChatInput, (text) => text.replaceAll('{payload}', payloadText));
81
+ const legacy = chatInputToLegacyFields(chatInput, {
82
+ prompt: eventDef.agent_prompt,
83
+ harness,
84
+ modelId: model,
85
+ authMethod,
86
+ cwd: projectPath,
87
+ projectPath,
88
+ });
89
+ const cwd = legacy.cwd || DEFAULT_CWD;
90
+ const modelSelection = resolveModelSelection(harness, legacy.modelId || model);
91
+ const mcpServers = await buildLocalMcpServerConfigs(legacy.mcpAppIds || []);
92
+
93
+ console.log(
94
+ `[AmalgmMCP:Event] Starting agent run for ${artifactOrTrigger.id}:${eventDef.name} (session: ${codeSessionId}, cwd: ${cwd})`,
95
+ );
96
+
97
+ if (hasSupabase()) {
98
+ try {
99
+ const metadata = {
100
+ artifactId: artifactOrTrigger.id,
101
+ artifactName: artifactOrTrigger.name,
102
+ event: eventDef.name,
103
+ automated: true,
104
+ originName: eventDef.name,
105
+ };
106
+ if (triggerId) metadata.triggerId = triggerId;
107
+ await supabaseInsert('sessions', {
108
+ id: codeSessionId,
109
+ user_id: AMALGM_USER_ID,
110
+ computer_id: AMALGM_COMPUTER_ID,
111
+ container_id: os.hostname() || 'artifact-event',
112
+ harness,
113
+ title: 'New Chat',
114
+ origin: 'event',
115
+ origin_id: triggerId || artifactOrTrigger.id,
116
+ project_path: projectPath || null,
117
+ status: 'in-progress',
118
+ metadata,
119
+ });
120
+ } catch (err) {
121
+ console.error('[AmalgmMCP:Event] DB session creation failed:', err.message);
122
+ }
123
+ }
124
+
125
+ try {
126
+ const { outputText } = await runThroughChatServer({
127
+ chatInput,
128
+ prompt: legacy.prompt,
129
+ userId: AMALGM_USER_ID,
130
+ computerId: AMALGM_COMPUTER_ID,
131
+ codeSessionId,
132
+ assistantMessageId,
133
+ userMessageId,
134
+ userParts: legacy.userParts,
135
+ agentId: harnessToAgent(harness),
136
+ modelId: modelSelection.modelId || legacy.modelId || null,
137
+ cliModel: modelSelection.cliModel || null,
138
+ ...(modelSelection.reasoningEffort ? { reasoningEffort: modelSelection.reasoningEffort } : {}),
139
+ cwd,
140
+ authMethod: legacy.authMethod || authMethod,
141
+ mcpServers,
142
+ automated: true,
143
+ });
144
+
145
+ const durationMs = Date.now() - new Date(startedAt).getTime();
146
+ console.log(
147
+ `[AmalgmMCP:Event] ${artifactOrTrigger.id}:${eventDef.name} completed in ${durationMs}ms (output: ${outputText.length} chars)`,
148
+ );
149
+
150
+ if (hasSupabase()) {
151
+ supabasePatch('sessions', 'id', codeSessionId, {
152
+ last_message_at: new Date().toISOString(),
153
+ status: 'completed',
154
+ new_messages: true,
155
+ }).catch(() => {});
156
+ }
157
+ } catch (err) {
158
+ console.error(
159
+ `[AmalgmMCP:Event] ${artifactOrTrigger.id}:${eventDef.name} failed:`,
160
+ err.message,
161
+ );
162
+ if (hasSupabase()) {
163
+ supabasePatch('sessions', 'id', codeSessionId, {
164
+ last_message_at: new Date().toISOString(),
165
+ status: 'error',
166
+ new_messages: true,
167
+ metadata: {
168
+ artifactId: artifactOrTrigger.id,
169
+ event: eventDef.name,
170
+ automated: true,
171
+ originName: eventDef.name,
172
+ error: err.message,
173
+ },
174
+ }).catch(() => {});
175
+ }
176
+ }
177
+ }
178
+
179
+ module.exports = { executeArtifactEvent };