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.
Files changed (107) hide show
  1. package/README.md +37 -1
  2. package/bin/amalgm.js +8 -2
  3. package/lib/auth-store.js +223 -0
  4. package/lib/cli.js +1000 -0
  5. package/lib/paths.js +30 -0
  6. package/lib/supervisor.js +467 -0
  7. package/lib/tunnel-chat.js +328 -0
  8. package/lib/tunnel-events.js +499 -0
  9. package/package.json +29 -3
  10. package/runtime/README.md +4 -0
  11. package/runtime/lib/chatInput.js +306 -0
  12. package/runtime/lib/harnesses.js +988 -0
  13. package/runtime/lib/local/amalgmStore.js +128 -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 +165 -0
  18. package/runtime/scripts/amalgm-mcp/agents/store.js +153 -0
  19. package/runtime/scripts/amalgm-mcp/agents/talk.js +1156 -0
  20. package/runtime/scripts/amalgm-mcp/agents/tools.js +210 -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 +141 -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 +138 -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 +98 -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 +393 -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 +80 -0
  50. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +151 -0
  51. package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
  52. package/runtime/scripts/amalgm-mcp/server/http.js +335 -0
  53. package/runtime/scripts/amalgm-mcp/server/mcp.js +116 -0
  54. package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
  55. package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
  56. package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
  57. package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
  58. package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
  59. package/runtime/scripts/amalgm-mcp/tasks/store.js +139 -0
  60. package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
  61. package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
  62. package/runtime/scripts/amalgm-mcp/workspace/rest.js +389 -0
  63. package/runtime/scripts/chat-core/adapters/claude.js +163 -0
  64. package/runtime/scripts/chat-core/adapters/codex.js +313 -0
  65. package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
  66. package/runtime/scripts/chat-core/auth.js +177 -0
  67. package/runtime/scripts/chat-core/contract.js +326 -0
  68. package/runtime/scripts/chat-core/credentials/store.js +212 -0
  69. package/runtime/scripts/chat-core/egress.js +87 -0
  70. package/runtime/scripts/chat-core/engine.js +195 -0
  71. package/runtime/scripts/chat-core/event-schema.js +231 -0
  72. package/runtime/scripts/chat-core/events.js +190 -0
  73. package/runtime/scripts/chat-core/index.js +11 -0
  74. package/runtime/scripts/chat-core/input.js +50 -0
  75. package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
  76. package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
  77. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
  78. package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
  79. package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
  80. package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
  81. package/runtime/scripts/chat-core/parts.js +253 -0
  82. package/runtime/scripts/chat-core/recorder.js +65 -0
  83. package/runtime/scripts/chat-core/runtime.js +86 -0
  84. package/runtime/scripts/chat-core/server.js +163 -0
  85. package/runtime/scripts/chat-core/sse.js +196 -0
  86. package/runtime/scripts/chat-core/stores.js +100 -0
  87. package/runtime/scripts/chat-core/tool-display.js +149 -0
  88. package/runtime/scripts/chat-core/tool-shape.js +143 -0
  89. package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
  90. package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
  91. package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
  92. package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
  93. package/runtime/scripts/chat-core/usage.js +343 -0
  94. package/runtime/scripts/chat-server/config.js +110 -0
  95. package/runtime/scripts/chat-server/db.js +529 -0
  96. package/runtime/scripts/chat-server/index.js +33 -0
  97. package/runtime/scripts/chat-server/model-catalog.js +327 -0
  98. package/runtime/scripts/chat-server.js +75 -0
  99. package/runtime/scripts/credential-adapter.js +129 -0
  100. package/runtime/scripts/fs-watcher.js +888 -0
  101. package/runtime/scripts/local-gateway.js +852 -0
  102. package/runtime/scripts/platform-context.txt +246 -0
  103. package/runtime/scripts/port-monitor.js +175 -0
  104. package/runtime/scripts/proxy-token-store.js +162 -0
  105. package/runtime/scripts/runtime-auth.js +163 -0
  106. package/runtime/scripts/test-claude-code-models.js +87 -0
  107. package/runtime/tsconfig.json +15 -0
@@ -0,0 +1,100 @@
1
+ /**
2
+ * amalgm-mcp — local-first MCP server.
3
+ *
4
+ * Entry point. Spawned by electron/main.ts (or runnable standalone with
5
+ * `node scripts/amalgm-mcp/index.js`). Stores all state under AMALGM_DIR
6
+ * (default: ~/.amalgm/) — same pattern as scripts/credential-adapter.js.
7
+ *
8
+ * Ports:
9
+ * 8083 this server
10
+ * 8084 chat-server (we delegate task/event/agent runs here)
11
+ *
12
+ * Modules:
13
+ * config.js env + paths
14
+ * deps.js cron-parser + playwright loaders
15
+ * lib/ generic utilities (storage, chat-runner,
16
+ * supabase, prefs, email-md, tool-result)
17
+ * tasks/ scheduled tasks (8 MCP tools + poll loop)
18
+ * events/ event triggers (5 MCP tools + /events ingress)
19
+ * agents/ agents CRUD + talk_to_agent
20
+ * browser/ visible Electron browser + Playwright fallback tools
21
+ * notify/ notify_user (email via proxy)
22
+ * server/ MCP JSON-RPC dispatch + HTTP route table
23
+ */
24
+
25
+ async function ensureProxyToken() {
26
+ if (process.env.AMALGM_PROXY_TOKEN) return;
27
+ const { DEFAULT_PROXY_BASE_URL, ensureFreshProxyToken } = require('../proxy-token-store');
28
+ const savedToken = await ensureFreshProxyToken({ logger: console });
29
+ if (savedToken) {
30
+ process.env.AMALGM_PROXY_TOKEN = savedToken;
31
+ if (!process.env.AMALGM_PROXY_URL) process.env.AMALGM_PROXY_URL = DEFAULT_PROXY_BASE_URL;
32
+ return;
33
+ }
34
+ if (
35
+ process.env.AMALGM_RUNTIME_SOURCE === 'npm'
36
+ || process.env.AMALGM_LOCAL_MODE === 'true'
37
+ || process.env.AMALGM_ALLOW_ADMIN_TOKEN_MINT !== 'true'
38
+ ) {
39
+ return;
40
+ }
41
+
42
+ const adminSecret = process.env.PROXY_ADMIN_SECRET || process.env.ADMIN_SECRET;
43
+ if (!adminSecret) return;
44
+
45
+ const proxyUrl = process.env.AMALGM_PROXY_URL || 'https://amalgm-api-proxy-v2.fly.dev';
46
+
47
+ try {
48
+ const res = await fetch(`${proxyUrl}/internal/token`, {
49
+ method: 'POST',
50
+ headers: {
51
+ 'Content-Type': 'application/json',
52
+ 'x-admin-secret': adminSecret,
53
+ },
54
+ body: JSON.stringify({
55
+ userId: process.env.AMALGM_USER_ID || 'local',
56
+ containerId: process.env.AMALGM_CONTAINER_ID || 'standalone',
57
+ }),
58
+ });
59
+
60
+ if (!res.ok) {
61
+ const text = await res.text().catch(() => '');
62
+ console.warn(`[AmalgmMCP] Failed to mint proxy token: ${res.status} ${text.slice(0, 100)}`);
63
+ return;
64
+ }
65
+
66
+ const { token } = await res.json();
67
+ process.env.AMALGM_PROXY_TOKEN = token;
68
+ if (!process.env.AMALGM_PROXY_URL) process.env.AMALGM_PROXY_URL = proxyUrl;
69
+ console.log('[AmalgmMCP] Minted HMAC proxy token (standalone fallback)');
70
+ } catch (err) {
71
+ console.warn(`[AmalgmMCP] Error minting proxy token: ${err.message}`);
72
+ }
73
+ }
74
+
75
+ async function boot() {
76
+ await ensureProxyToken();
77
+
78
+ const { ensureTasksDirs } = require('./tasks/store');
79
+ const { ensureTriggersDirs } = require('./events/store');
80
+ const { ensureAgentsDirs } = require('./agents/store');
81
+ const { ensureArtifactsDirs } = require('./artifacts/store');
82
+ const { startArtifactRouteSyncLoop } = require('./artifacts/advertise');
83
+ const { startRegisteredArtifacts } = require('./artifacts/supervisor');
84
+ const { listen } = require('./server/http');
85
+
86
+ // Make sure every storage dir/file exists before we accept traffic.
87
+ ensureTasksDirs();
88
+ ensureTriggersDirs();
89
+ ensureAgentsDirs();
90
+ ensureArtifactsDirs();
91
+
92
+ await listen();
93
+ await startRegisteredArtifacts();
94
+ startArtifactRouteSyncLoop();
95
+ }
96
+
97
+ boot().catch((err) => {
98
+ console.error('[AmalgmMCP] Boot failed:', err);
99
+ process.exit(1);
100
+ });
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Chat-server runner — generic "execute a prompt via the local chat-server".
3
+ *
4
+ * Used by:
5
+ * - tasks/executor.js (scheduled task runs)
6
+ * - events/executor.js (webhook → agent runs)
7
+ * - agents/talk.js (talk_to_agent tool)
8
+ * - email/inbound.js (email replies/new email → agent runs)
9
+ *
10
+ * POSTs to chat-server (:8084) and consumes the SSE stream, accumulating
11
+ * text from agent_message_chunk events and returning the final output.
12
+ */
13
+
14
+ const { CHAT_SERVER_URL } = require('../config');
15
+ const { runtimeAuthHeaders } = require('../../runtime-auth');
16
+
17
+ /**
18
+ * Run a prompt through the chat-server and return the assembled output text.
19
+ *
20
+ * @param {object} payload - body sent to POST /chat (prompt, codeSessionId, agentId, cwd, ...)
21
+ * @param {AbortSignal} [signal]
22
+ * @param {{ onEvent?: (event: object, metrics: object) => void }} [options]
23
+ * @returns {Promise<{ outputText: string, stopReason: string | null, usage: object | null, metrics: object }>}
24
+ */
25
+ async function runThroughChatServer(payload, signal, options = {}) {
26
+ const startedAt = Date.now();
27
+ const res = await fetch(`${CHAT_SERVER_URL}/chat`, {
28
+ method: 'POST',
29
+ headers: runtimeAuthHeaders({ 'Content-Type': 'application/json' }),
30
+ body: JSON.stringify(payload),
31
+ ...(signal ? { signal } : {}),
32
+ });
33
+ if (!res.ok) {
34
+ const errText = await res.text().catch(() => '');
35
+ throw new Error(`chat-server returned ${res.status}: ${errText.slice(0, 200)}`);
36
+ }
37
+
38
+ let outputText = '';
39
+ let stopReason = null;
40
+ let usage = null;
41
+ let sawComplete = false;
42
+ let sawError = false;
43
+ const toolCalls = new Map();
44
+ const metrics = {
45
+ textChunks: 0,
46
+ reasoningChunks: 0,
47
+ toolEvents: 0,
48
+ usageEvents: 0,
49
+ };
50
+
51
+ const notifyEvent = (event) => {
52
+ if (typeof options.onEvent !== 'function') return;
53
+ try {
54
+ options.onEvent(event, {
55
+ ...metrics,
56
+ sawComplete,
57
+ sawError,
58
+ outputText,
59
+ outputTextLength: outputText.length,
60
+ wallTimeMs: Date.now() - startedAt,
61
+ toolCalls: [...toolCalls.values()],
62
+ });
63
+ } catch {
64
+ // Observability hooks must not interrupt the agent run.
65
+ }
66
+ };
67
+
68
+ const handleEvent = (event) => {
69
+ if (event._type === 'update' && event.sessionUpdate === 'agent_message_chunk') {
70
+ if (event.content?.type === 'text' && event.content.text) {
71
+ outputText += event.content.text;
72
+ metrics.textChunks += 1;
73
+ }
74
+ notifyEvent(event);
75
+ return;
76
+ }
77
+ if (event._type === 'update' && event.sessionUpdate === 'agent_thought_chunk') {
78
+ metrics.reasoningChunks += 1;
79
+ notifyEvent(event);
80
+ return;
81
+ }
82
+ if (event._type === 'update' && (event.sessionUpdate === 'tool_call' || event.sessionUpdate === 'tool_call_update')) {
83
+ metrics.toolEvents += 1;
84
+ const id = event.toolCallId || `tool-${metrics.toolEvents}`;
85
+ const current = toolCalls.get(id) || { id };
86
+ toolCalls.set(id, {
87
+ ...current,
88
+ title: event.title || current.title,
89
+ kind: event.kind || current.kind,
90
+ status: event.status || current.status,
91
+ });
92
+ notifyEvent(event);
93
+ return;
94
+ }
95
+ if (event._type === 'update' && event.sessionUpdate === 'usage_update') {
96
+ usage = event.usage || usage;
97
+ metrics.usageEvents += 1;
98
+ notifyEvent(event);
99
+ return;
100
+ }
101
+ if (event._type === 'complete') {
102
+ stopReason = event.stopReason || null;
103
+ usage = event.usage || usage;
104
+ sawComplete = true;
105
+ notifyEvent(event);
106
+ return;
107
+ }
108
+ if (event._type === 'error') {
109
+ sawError = true;
110
+ notifyEvent(event);
111
+ throw new Error(event.message || 'chat-server error');
112
+ }
113
+ notifyEvent(event);
114
+ };
115
+
116
+ const handleLine = (line) => {
117
+ if (!line.startsWith('data: ')) return;
118
+ const data = line.slice(6).trim();
119
+ if (!data) return;
120
+ const event = JSON.parse(data);
121
+ handleEvent(event);
122
+ };
123
+
124
+ const reader = res.body.getReader();
125
+ const decoder = new TextDecoder();
126
+ let buffer = '';
127
+
128
+ while (true) {
129
+ const { done, value } = await reader.read();
130
+ if (done) break;
131
+ buffer += decoder.decode(value, { stream: true });
132
+ const lines = buffer.split('\n');
133
+ buffer = lines.pop() || '';
134
+
135
+ for (const line of lines) {
136
+ try {
137
+ handleLine(line);
138
+ } catch (e) {
139
+ // Swallow JSON parse errors on partial lines; re-throw everything else.
140
+ if (!(e instanceof SyntaxError)) throw e;
141
+ }
142
+ }
143
+ }
144
+
145
+ if (buffer.trim()) {
146
+ try {
147
+ handleLine(buffer);
148
+ } catch (e) {
149
+ if (!(e instanceof SyntaxError)) throw e;
150
+ }
151
+ }
152
+
153
+ return {
154
+ outputText,
155
+ stopReason,
156
+ usage,
157
+ metrics: {
158
+ ...metrics,
159
+ sawComplete,
160
+ sawError,
161
+ wallTimeMs: Date.now() - startedAt,
162
+ toolCalls: [...toolCalls.values()],
163
+ },
164
+ };
165
+ }
166
+
167
+ module.exports = { runThroughChatServer };
@@ -0,0 +1,288 @@
1
+ /**
2
+ * Email markdown → inline-styled HTML.
3
+ *
4
+ * Line-based parser: processes each line sequentially, accumulates contiguous
5
+ * blocks of the same type (list items, table rows, paragraph text, etc.) and
6
+ * flushes when the type changes. Headings, tables, and lists are detected
7
+ * even without blank-line separators.
8
+ *
9
+ * Output is inline-styled HTML suitable for email clients that strip <style>
10
+ * tags. Colors match the amalgm dark theme.
11
+ */
12
+
13
+ function escapeHtmlAttr(value) {
14
+ return String(value)
15
+ .replace(/&/g, '&amp;')
16
+ .replace(/</g, '&lt;')
17
+ .replace(/>/g, '&gt;')
18
+ .replace(/"/g, '&quot;');
19
+ }
20
+
21
+ function escapeHtmlText(value) {
22
+ return String(value)
23
+ .replace(/&/g, '&amp;')
24
+ .replace(/</g, '&lt;')
25
+ .replace(/>/g, '&gt;');
26
+ }
27
+
28
+ // Inline citation chip for amalgm:// references (files, folders, skills, agents).
29
+ // Non-clickable in email — local resources can't be opened from a mail client —
30
+ // but the chip preserves visual continuity with the in-app render.
31
+ const AMALGM_KIND_ICONS = {
32
+ file: '\u{1F4C4}', // 📄
33
+ folder: '\u{1F4C1}', // 📁
34
+ skill: '\u{2728}', // ✨
35
+ agent: '@',
36
+ };
37
+
38
+ function renderAmalgmCitation(label, kindAndPath, description) {
39
+ const slashIdx = kindAndPath.indexOf('/');
40
+ const kind = slashIdx === -1 ? kindAndPath : kindAndPath.slice(0, slashIdx);
41
+ const icon = AMALGM_KIND_ICONS[kind] || '\u{1F4CE}'; // 📎 fallback
42
+ const titleAttr = description ? ` title="${escapeHtmlAttr(description)}"` : '';
43
+ const safeLabel = escapeHtmlText(label);
44
+ return `<span style="display:inline-block;background:#0c0a09;border:1px solid #1c1917;border-radius:9999px;padding:1px 8px;font-size:12px;color:#ffffff;line-height:1.5;white-space:nowrap"${titleAttr}>${icon}&nbsp;${safeLabel}</span>`;
45
+ }
46
+
47
+ /**
48
+ * Inline format: bold, italic, code, links, images, amalgm:// citation chips.
49
+ */
50
+ function inlineFormat(text) {
51
+ return (
52
+ text
53
+ // Images: ![alt](url)
54
+ .replace(
55
+ /!\[([^\]]*)\]\(([^)]+)\)/g,
56
+ '<img src="$2" alt="$1" style="max-width:100%;height:auto;border-radius:8px;margin:8px 0">',
57
+ )
58
+ // amalgm:// citations (run before generic link regex so the optional
59
+ // title attribute doesn't get swallowed into the href). Format:
60
+ // [label](amalgm://kind/encoded-path)
61
+ // [label](amalgm://kind/encoded-path "description")
62
+ .replace(
63
+ /\[([^\]]+)\]\(amalgm:\/\/([^\s)"]+)(?:\s+"([^"]*)")?\)/g,
64
+ (_, label, kindAndPath, description) =>
65
+ renderAmalgmCitation(label, kindAndPath, description),
66
+ )
67
+ // Links: [text](url)
68
+ .replace(
69
+ /\[([^\]]+)\]\(([^)]+)\)/g,
70
+ '<a href="$2" style="color:#ffffff;text-decoration:underline;text-underline-offset:2px" target="_blank">$1</a>',
71
+ )
72
+ // Bold + italic: ***text***
73
+ .replace(
74
+ /\*{3}(.+?)\*{3}/g,
75
+ '<strong style="color:#ffffff;font-weight:600"><em>$1</em></strong>',
76
+ )
77
+ // Bold: **text**
78
+ .replace(/\*{2}(.+?)\*{2}/g, '<strong style="color:#ffffff;font-weight:600">$1</strong>')
79
+ // Italic: *text* / _text_
80
+ .replace(/(?<!\w)\*(.+?)\*(?!\w)/g, '<em>$1</em>')
81
+ .replace(/(?<!\w)_(.+?)_(?!\w)/g, '<em>$1</em>')
82
+ // Inline code: `text`
83
+ .replace(
84
+ /`([^`]+)`/g,
85
+ '<code style="background:#1c1917;padding:2px 6px;border-radius:4px;font-size:13px;font-family:\'SF Mono\',SFMono-Regular,Menlo,Consolas,monospace;color:#a8a29e">$1</code>',
86
+ )
87
+ );
88
+ }
89
+
90
+ function markdownToEmailHtml(md) {
91
+ let text = md.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
92
+ // Normalize literal \n sequences (LLMs sometimes send \\n in JSON).
93
+ text = text.replace(/\\n/g, '\n');
94
+
95
+ // Extract fenced code blocks first so line parser doesn't touch them.
96
+ const codeBlocks = [];
97
+ text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, _lang, code) => {
98
+ const escaped = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
99
+ const placeholder = `%%CODEBLOCK_${codeBlocks.length}%%`;
100
+ codeBlocks.push(
101
+ `<pre style="background:#0a0a0a;border:1px solid #1c1917;border-radius:8px;padding:12px 16px;font-size:13px;font-family:'SF Mono',SFMono-Regular,Menlo,Consolas,monospace;color:#f5f5f4;overflow-x:auto;margin:16px 0;line-height:1.5"><code>${escaped.trimEnd()}</code></pre>`,
102
+ );
103
+ return placeholder;
104
+ });
105
+
106
+ const lines = text.split('\n');
107
+ const out = [];
108
+
109
+ let paraLines = [];
110
+ let listItems = [];
111
+ let listType = '';
112
+ let tableRows = [];
113
+ let quoteLines = [];
114
+
115
+ function flushPara() {
116
+ if (!paraLines.length) return;
117
+ const content = inlineFormat(paraLines.join('\n')).replace(/\n/g, '<br>');
118
+ out.push(`<p style="margin:12px 0;line-height:1.7;color:#ffffff">${content}</p>`);
119
+ paraLines = [];
120
+ }
121
+ function flushList() {
122
+ if (!listItems.length) return;
123
+ const tag = listType;
124
+ const items = listItems
125
+ .map((t) => `<li style="margin:4px 0;color:#ffffff">${inlineFormat(t)}</li>`)
126
+ .join('');
127
+ out.push(
128
+ `<${tag} style="margin:12px 0;padding-left:24px;color:#ffffff">${items}</${tag}>`,
129
+ );
130
+ listItems = [];
131
+ listType = '';
132
+ }
133
+ function flushTable() {
134
+ if (tableRows.length < 2) {
135
+ paraLines.push(...tableRows.map((r) => r._raw));
136
+ tableRows = [];
137
+ return;
138
+ }
139
+ const headerCells = tableRows[0].cells;
140
+ const bodyRows = tableRows.slice(1);
141
+ const hdr = headerCells
142
+ .map(
143
+ (c) =>
144
+ `<th style="background:#292524;padding:8px 12px;text-align:left;font-size:13px;font-weight:600;color:#ffffff;border:1px solid #44403c">${inlineFormat(c)}</th>`,
145
+ )
146
+ .join('');
147
+ const body = bodyRows
148
+ .map(
149
+ (r) =>
150
+ '<tr>' +
151
+ r.cells
152
+ .map(
153
+ (c) =>
154
+ `<td style="padding:8px 12px;font-size:13px;color:#d6d3d1;border:1px solid #44403c">${inlineFormat(c)}</td>`,
155
+ )
156
+ .join('') +
157
+ '</tr>',
158
+ )
159
+ .join('');
160
+ out.push(
161
+ `<table style="width:100%;border-collapse:collapse;margin:16px 0;background:#1c1917;border-radius:8px;overflow:hidden"><thead><tr>${hdr}</tr></thead><tbody>${body}</tbody></table>`,
162
+ );
163
+ tableRows = [];
164
+ }
165
+ function flushQuote() {
166
+ if (!quoteLines.length) return;
167
+ const content = inlineFormat(quoteLines.join('\n')).replace(/\n/g, '<br>');
168
+ out.push(
169
+ `<blockquote style="margin:16px 0;padding:8px 16px;border-left:3px solid #44403c;color:#d6d3d1;font-style:italic">${content}</blockquote>`,
170
+ );
171
+ quoteLines = [];
172
+ }
173
+ function flushAll() {
174
+ flushPara();
175
+ flushList();
176
+ flushTable();
177
+ flushQuote();
178
+ }
179
+
180
+ function parseTableRow(line) {
181
+ const t = line.trim();
182
+ if (!t.startsWith('|') || !t.endsWith('|')) return null;
183
+ const cells = t.slice(1, -1).split('|').map((c) => c.trim());
184
+ if (cells.every((c) => /^[-:]+$/.test(c))) return 'separator';
185
+ return cells;
186
+ }
187
+
188
+ for (const line of lines) {
189
+ const trimmed = line.trim();
190
+
191
+ if (/^%%CODEBLOCK_\d+%%$/.test(trimmed)) {
192
+ flushAll();
193
+ const idx = parseInt(trimmed.match(/\d+/)[0], 10);
194
+ out.push(codeBlocks[idx]);
195
+ continue;
196
+ }
197
+ if (!trimmed) {
198
+ flushAll();
199
+ continue;
200
+ }
201
+ if (/^(-{3,}|_{3,}|\*{3,})$/.test(trimmed)) {
202
+ flushAll();
203
+ out.push('<hr style="border:none;border-top:1px solid #292524;margin:24px 0">');
204
+ continue;
205
+ }
206
+ const headingMatch = trimmed.match(/^(#{1,6})\s+(.+)$/);
207
+ if (headingMatch) {
208
+ flushAll();
209
+ const level = headingMatch[1].length;
210
+ const sizes = { 1: '22px', 2: '18px', 3: '16px', 4: '15px', 5: '14px', 6: '13px' };
211
+ out.push(
212
+ `<h${level} style="margin:20px 0 8px;font-size:${sizes[level]};font-weight:600;color:#ffffff;line-height:1.3">${inlineFormat(headingMatch[2])}</h${level}>`,
213
+ );
214
+ continue;
215
+ }
216
+ if (/^>\s?/.test(trimmed)) {
217
+ flushPara();
218
+ flushList();
219
+ flushTable();
220
+ quoteLines.push(trimmed.replace(/^>\s?/, ''));
221
+ continue;
222
+ } else if (quoteLines.length) {
223
+ flushQuote();
224
+ }
225
+
226
+ const tableResult = parseTableRow(trimmed);
227
+ if (tableResult === 'separator') continue;
228
+ if (tableResult) {
229
+ flushPara();
230
+ flushList();
231
+ flushQuote();
232
+ tableRows.push({ cells: tableResult, _raw: trimmed });
233
+ continue;
234
+ } else if (tableRows.length) {
235
+ flushTable();
236
+ }
237
+
238
+ const ulMatch = trimmed.match(/^\s*[-*]\s+(.*)/);
239
+ if (ulMatch) {
240
+ if (listType === 'ol') flushList();
241
+ flushPara();
242
+ flushTable();
243
+ flushQuote();
244
+ listType = 'ul';
245
+ listItems.push(ulMatch[1]);
246
+ continue;
247
+ }
248
+ const olMatch = trimmed.match(/^\s*\d+\.\s+(.*)/);
249
+ if (olMatch) {
250
+ if (listType === 'ul') flushList();
251
+ flushPara();
252
+ flushTable();
253
+ flushQuote();
254
+ listType = 'ol';
255
+ listItems.push(olMatch[1]);
256
+ continue;
257
+ }
258
+ if (listItems.length) flushList();
259
+
260
+ paraLines.push(trimmed);
261
+ }
262
+
263
+ flushAll();
264
+ return out.join('\n');
265
+ }
266
+
267
+ /**
268
+ * Wrap the converted markdown body in a full notification email shell.
269
+ */
270
+ function formatNotificationEmail(message, link) {
271
+ const htmlMessage = markdownToEmailHtml(message);
272
+ const linkHtml = link
273
+ ? `<p style="margin:24px 0 0"><a href="${link}" style="color:#ffffff;text-decoration:underline;font-size:14px">View details &rarr;</a></p>`
274
+ : '';
275
+ return `<!DOCTYPE html>
276
+ <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
277
+ <body style="margin:0;padding:0;background:#000000;font-family:'SF Pro Text',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',sans-serif;-webkit-font-smoothing:antialiased">
278
+ <table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#000000"><tr><td align="center" style="padding:0">
279
+ <table role="presentation" width="520" cellpadding="0" cellspacing="0" style="max-width:520px;width:100%"><tr><td style="padding:40px 28px 48px">
280
+ <div style="font-size:15px;color:#ffffff">${htmlMessage}</div>
281
+ ${linkHtml}
282
+ <p style="margin:48px 0 0;font-size:12px;color:#ffffff">amalgm.ai</p>
283
+ </td></tr></table>
284
+ </td></tr></table>
285
+ </body></html>`;
286
+ }
287
+
288
+ module.exports = { markdownToEmailHtml, formatNotificationEmail };
@@ -0,0 +1,63 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+
5
+ const { buildSessionConfig, getMcpApp } = require('../../../lib/mcpApps/registry.js');
6
+
7
+ const CONNECTIONS_FILE = path.join(os.homedir(), '.amalgm', 'mcp-connections.json');
8
+
9
+ function readConnectionsFile() {
10
+ try {
11
+ const raw = fs.readFileSync(CONNECTIONS_FILE, 'utf8');
12
+ const parsed = JSON.parse(raw);
13
+ return parsed && typeof parsed === 'object' ? parsed : { connections: {} };
14
+ } catch {
15
+ return { connections: {} };
16
+ }
17
+ }
18
+
19
+ async function buildLocalMcpServerConfigs(appIds) {
20
+ if (!Array.isArray(appIds) || appIds.length === 0) return [];
21
+
22
+ const file = readConnectionsFile();
23
+ const configs = [];
24
+
25
+ for (const appId of [...new Set(appIds)]) {
26
+ const app = getMcpApp(appId);
27
+ if (!app) continue;
28
+
29
+ const conn = file.connections?.[appId];
30
+
31
+ if (app.authType === 'api_key') {
32
+ if (!conn?.apiKey) continue;
33
+ configs.push(
34
+ buildSessionConfig(
35
+ app,
36
+ { apiKey: conn.apiKey },
37
+ { instanceUrl: conn.instanceUrl, config: conn.config },
38
+ ),
39
+ );
40
+ continue;
41
+ }
42
+
43
+ if (app.authType === 'oauth') {
44
+ if (!conn?.accessToken) continue;
45
+ configs.push(
46
+ buildSessionConfig(
47
+ app,
48
+ { accessToken: conn.accessToken },
49
+ { instanceUrl: conn.instanceUrl, config: conn.config },
50
+ ),
51
+ );
52
+ continue;
53
+ }
54
+
55
+ configs.push(buildSessionConfig(app));
56
+ }
57
+
58
+ return configs;
59
+ }
60
+
61
+ module.exports = {
62
+ buildLocalMcpServerConfigs,
63
+ };