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,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool result helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function textResult(text) {
|
|
6
|
+
return { content: [{ type: 'text', text }] };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function errorResult(text) {
|
|
10
|
+
return { content: [{ type: 'text', text }], isError: true };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function clipText(value, max = 80) {
|
|
14
|
+
if (typeof value !== 'string') return '';
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
if (!trimmed) return '';
|
|
17
|
+
return trimmed.length > max ? `${trimmed.slice(0, max - 1)}...` : trimmed;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function quoteIfPresent(value, max = 60) {
|
|
21
|
+
const clipped = clipText(value, max);
|
|
22
|
+
return clipped ? ` "${clipped}"` : '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function humanizeToolName(toolName) {
|
|
26
|
+
return String(toolName || '')
|
|
27
|
+
.replace(/^amalgm[_-]/i, '')
|
|
28
|
+
.replace(/^mcp[_-]/i, '')
|
|
29
|
+
.replace(/[_-]+/g, ' ')
|
|
30
|
+
.trim();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildActionDescriptor(toolName, args = {}) {
|
|
34
|
+
switch (toolName) {
|
|
35
|
+
case 'scheduled_tasks_create':
|
|
36
|
+
return `Create scheduled task${quoteIfPresent(args.name)}`;
|
|
37
|
+
case 'scheduled_tasks_list':
|
|
38
|
+
return args.enabled_only ? 'List enabled scheduled tasks' : 'List scheduled tasks';
|
|
39
|
+
case 'scheduled_tasks_get':
|
|
40
|
+
return `Get scheduled task${quoteIfPresent(args.task_id)}`;
|
|
41
|
+
case 'scheduled_tasks_update':
|
|
42
|
+
return `Update scheduled task${quoteIfPresent(args.task_id || args.name)}`;
|
|
43
|
+
case 'scheduled_tasks_delete':
|
|
44
|
+
return `Delete scheduled task${quoteIfPresent(args.task_id)}`;
|
|
45
|
+
case 'scheduled_tasks_run_now':
|
|
46
|
+
return `Run scheduled task now${quoteIfPresent(args.task_id)}`;
|
|
47
|
+
case 'scheduled_tasks_cancel':
|
|
48
|
+
return `Cancel scheduled task${quoteIfPresent(args.task_id)}`;
|
|
49
|
+
case 'scheduled_tasks_history':
|
|
50
|
+
return `Get scheduled task history${quoteIfPresent(args.task_id)}`;
|
|
51
|
+
case 'event_triggers_create':
|
|
52
|
+
return `Create event trigger${quoteIfPresent(args.name)}`;
|
|
53
|
+
case 'event_triggers_list':
|
|
54
|
+
return args.enabled_only ? 'List enabled event triggers' : 'List event triggers';
|
|
55
|
+
case 'event_triggers_get':
|
|
56
|
+
return `Get event trigger${quoteIfPresent(args.trigger_id)}`;
|
|
57
|
+
case 'event_triggers_update':
|
|
58
|
+
return `Update event trigger${quoteIfPresent(args.trigger_id || args.name)}`;
|
|
59
|
+
case 'event_triggers_delete':
|
|
60
|
+
return `Delete event trigger${quoteIfPresent(args.trigger_id)}`;
|
|
61
|
+
case 'notify_user':
|
|
62
|
+
return `Notify user${quoteIfPresent(args.subject || args.level || 'info')}`;
|
|
63
|
+
case 'agents_list':
|
|
64
|
+
return 'List agents';
|
|
65
|
+
case 'agents_get':
|
|
66
|
+
return `Get agent${quoteIfPresent(args.agent_id)}`;
|
|
67
|
+
case 'talk_to_agent':
|
|
68
|
+
if (args.response_to_caller) return 'Deliver peer response';
|
|
69
|
+
return `Talk to agent${quoteIfPresent(args.agent || args.agent_id)}`;
|
|
70
|
+
case 'browser_navigate':
|
|
71
|
+
return args.url ? `Navigate browser to ${clipText(args.url, 90)}` : 'Navigate browser';
|
|
72
|
+
case 'browser_tabs_list':
|
|
73
|
+
return 'List browser tabs';
|
|
74
|
+
case 'browser_tabs_selected':
|
|
75
|
+
return 'Get selected browser tab';
|
|
76
|
+
case 'browser_tabs_new':
|
|
77
|
+
return args.url ? `Open browser tab ${clipText(args.url, 90)}` : 'Open browser tab';
|
|
78
|
+
case 'browser_tabs_get':
|
|
79
|
+
return 'Get browser tab';
|
|
80
|
+
case 'browser_tabs_select':
|
|
81
|
+
return 'Select browser tab';
|
|
82
|
+
case 'browser_tabs_close':
|
|
83
|
+
return 'Close browser tab';
|
|
84
|
+
case 'browser_state':
|
|
85
|
+
return 'Get browser state';
|
|
86
|
+
case 'browser_back':
|
|
87
|
+
return 'Go back in browser';
|
|
88
|
+
case 'browser_forward':
|
|
89
|
+
return 'Go forward in browser';
|
|
90
|
+
case 'browser_reload':
|
|
91
|
+
return 'Reload browser';
|
|
92
|
+
case 'browser_screenshot':
|
|
93
|
+
return args.full_page ? 'Take full-page browser screenshot' : 'Take browser screenshot';
|
|
94
|
+
case 'browser_snapshot':
|
|
95
|
+
return 'Capture browser snapshot';
|
|
96
|
+
case 'browser_click':
|
|
97
|
+
if (args.text) return `Click browser text${quoteIfPresent(args.text)}`;
|
|
98
|
+
if (args.selector) return `Click browser selector${quoteIfPresent(args.selector)}`;
|
|
99
|
+
return 'Click in browser';
|
|
100
|
+
case 'browser_type':
|
|
101
|
+
if (args.selector && args.text) {
|
|
102
|
+
return `Type ${clipText(args.text, 40)} into ${clipText(args.selector, 40)}`;
|
|
103
|
+
}
|
|
104
|
+
if (args.selector) return `Type into ${clipText(args.selector, 60)}`;
|
|
105
|
+
return args.text ? `Type ${clipText(args.text, 40)} in browser` : 'Type in browser';
|
|
106
|
+
case 'browser_press':
|
|
107
|
+
return args.key ? `Press ${clipText(args.key, 30)} in browser` : 'Press key in browser';
|
|
108
|
+
case 'browser_get_text':
|
|
109
|
+
return args.selector
|
|
110
|
+
? `Get browser text from ${clipText(args.selector, 60)}`
|
|
111
|
+
: 'Get browser page text';
|
|
112
|
+
case 'browser_locator_count':
|
|
113
|
+
return 'Count browser locator';
|
|
114
|
+
case 'browser_locator_text':
|
|
115
|
+
return 'Read browser locator text';
|
|
116
|
+
case 'browser_evaluate':
|
|
117
|
+
return 'Evaluate browser script';
|
|
118
|
+
case 'browser_wait_for_load_state':
|
|
119
|
+
return 'Wait for browser load state';
|
|
120
|
+
case 'browser_wait_for_url':
|
|
121
|
+
return args.url ? `Wait for browser URL ${clipText(args.url, 80)}` : 'Wait for browser URL';
|
|
122
|
+
case 'browser_wait_for_selector':
|
|
123
|
+
return args.selector ? `Wait for browser selector ${clipText(args.selector, 60)}` : 'Wait for browser selector';
|
|
124
|
+
case 'browser_cua_click':
|
|
125
|
+
return 'Click browser coordinates';
|
|
126
|
+
case 'browser_cua_double_click':
|
|
127
|
+
return 'Double-click browser coordinates';
|
|
128
|
+
case 'browser_cua_move':
|
|
129
|
+
return 'Move browser cursor';
|
|
130
|
+
case 'browser_cua_scroll':
|
|
131
|
+
return 'Scroll browser';
|
|
132
|
+
case 'browser_cua_type':
|
|
133
|
+
return args.text ? `Type ${clipText(args.text, 40)} in browser` : 'Type in browser';
|
|
134
|
+
case 'browser_cua_keypress':
|
|
135
|
+
return 'Press browser key';
|
|
136
|
+
case 'browser_cua_drag':
|
|
137
|
+
return 'Drag in browser';
|
|
138
|
+
case 'browser_cua_screenshot':
|
|
139
|
+
return 'Take visible browser screenshot';
|
|
140
|
+
case 'browser_clipboard_read_text':
|
|
141
|
+
return 'Read browser clipboard';
|
|
142
|
+
case 'browser_clipboard_write_text':
|
|
143
|
+
return 'Write browser clipboard';
|
|
144
|
+
case 'browser_start_video':
|
|
145
|
+
return 'Start browser video recording';
|
|
146
|
+
case 'browser_stop_video':
|
|
147
|
+
return 'Stop browser video recording';
|
|
148
|
+
case 'browser_close':
|
|
149
|
+
return 'Close browser';
|
|
150
|
+
default: {
|
|
151
|
+
const humanized = humanizeToolName(toolName);
|
|
152
|
+
return humanized ? humanized.charAt(0).toUpperCase() + humanized.slice(1) : 'Tool call';
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function attachActionDescriptor(result, toolName, args = {}) {
|
|
158
|
+
if (!result || typeof result !== 'object') return result;
|
|
159
|
+
|
|
160
|
+
const existingDescriptor =
|
|
161
|
+
(typeof result.actionDescriptor === 'string' && result.actionDescriptor.trim())
|
|
162
|
+
|| (typeof result.metadata?.actionDescriptor === 'string' && result.metadata.actionDescriptor.trim());
|
|
163
|
+
|
|
164
|
+
if (existingDescriptor) return result;
|
|
165
|
+
|
|
166
|
+
const actionDescriptor = buildActionDescriptor(toolName, args);
|
|
167
|
+
if (!actionDescriptor) return result;
|
|
168
|
+
|
|
169
|
+
const metadata =
|
|
170
|
+
result.metadata && typeof result.metadata === 'object'
|
|
171
|
+
? { ...result.metadata, actionDescriptor: result.metadata.actionDescriptor || actionDescriptor }
|
|
172
|
+
: { actionDescriptor };
|
|
173
|
+
|
|
174
|
+
return { ...result, actionDescriptor, metadata };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = { textResult, errorResult, buildActionDescriptor, attachActionDescriptor };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /local/* 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 {
|
|
7
|
+
discoverAgents,
|
|
8
|
+
resolveAgentEnv,
|
|
9
|
+
} = require('../../../lib/local/credentialResolver.js');
|
|
10
|
+
const {
|
|
11
|
+
ensureAmalgmDir,
|
|
12
|
+
getHomeDir,
|
|
13
|
+
writeResource,
|
|
14
|
+
} = require('../../../lib/local/amalgmStore.js');
|
|
15
|
+
|
|
16
|
+
function cacheDiscovery(agents) {
|
|
17
|
+
writeResource('agents/discovery.json', {
|
|
18
|
+
version: 1,
|
|
19
|
+
lastScan: new Date().toISOString(),
|
|
20
|
+
discovered: agents,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function handleInit(sendJson) {
|
|
25
|
+
try {
|
|
26
|
+
const { created, path: amalgmPath } = ensureAmalgmDir();
|
|
27
|
+
const agents = discoverAgents();
|
|
28
|
+
cacheDiscovery(agents);
|
|
29
|
+
|
|
30
|
+
sendJson(200, {
|
|
31
|
+
ok: true,
|
|
32
|
+
created,
|
|
33
|
+
amalgmPath,
|
|
34
|
+
homedir: getHomeDir(),
|
|
35
|
+
agents,
|
|
36
|
+
});
|
|
37
|
+
} catch (error) {
|
|
38
|
+
sendJson(500, {
|
|
39
|
+
error: error instanceof Error ? error.message : 'Failed to initialize computer environment',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function handleCredentials(sendJson) {
|
|
45
|
+
try {
|
|
46
|
+
const agents = discoverAgents();
|
|
47
|
+
cacheDiscovery(agents);
|
|
48
|
+
sendJson(200, {
|
|
49
|
+
agents,
|
|
50
|
+
homedir: getHomeDir(),
|
|
51
|
+
});
|
|
52
|
+
} catch (error) {
|
|
53
|
+
sendJson(500, {
|
|
54
|
+
error: error instanceof Error ? error.message : 'Failed to discover credentials',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function handleResolve(body, sendJson) {
|
|
60
|
+
try {
|
|
61
|
+
const { agentId, authMode, proxyToken, proxyBaseUrl } = body || {};
|
|
62
|
+
|
|
63
|
+
if (!agentId || !authMode) {
|
|
64
|
+
return sendJson(400, { error: 'agentId and authMode are required' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const env = resolveAgentEnv(agentId, authMode, proxyToken, proxyBaseUrl);
|
|
68
|
+
sendJson(200, { env });
|
|
69
|
+
} catch (error) {
|
|
70
|
+
sendJson(500, {
|
|
71
|
+
error: error instanceof Error ? error.message : 'Failed to resolve credentials',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
handleCredentials,
|
|
78
|
+
handleInit,
|
|
79
|
+
handleResolve,
|
|
80
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /mcp-connections 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 fs = require('fs');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { AMALGM_DIR } = require('../config');
|
|
10
|
+
|
|
11
|
+
const CONNECTIONS_FILE = path.join(AMALGM_DIR, 'mcp-connections.json');
|
|
12
|
+
const CLAUDE_FILE = path.join(os.homedir(), '.claude.json');
|
|
13
|
+
|
|
14
|
+
function emptyFile() {
|
|
15
|
+
return { version: 1, connections: {} };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ensureParentDir(filePath) {
|
|
19
|
+
const dir = path.dirname(filePath);
|
|
20
|
+
if (!fs.existsSync(dir)) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readConnectionsFile() {
|
|
26
|
+
try {
|
|
27
|
+
const raw = fs.readFileSync(CONNECTIONS_FILE, 'utf8');
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
if (parsed && typeof parsed === 'object' && parsed.connections && typeof parsed.connections === 'object') {
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
return emptyFile();
|
|
33
|
+
} catch {
|
|
34
|
+
return emptyFile();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function writeConnectionsFile(data) {
|
|
39
|
+
ensureParentDir(CONNECTIONS_FILE);
|
|
40
|
+
fs.writeFileSync(CONNECTIONS_FILE, JSON.stringify(data, null, 2));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sanitizeConnections(connections) {
|
|
44
|
+
const sanitized = {};
|
|
45
|
+
|
|
46
|
+
for (const [appId, conn] of Object.entries(connections || {})) {
|
|
47
|
+
sanitized[appId] = {
|
|
48
|
+
enabled: !!conn.enabled,
|
|
49
|
+
authType: conn.authType,
|
|
50
|
+
instanceUrl: conn.instanceUrl,
|
|
51
|
+
config: conn.config,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (conn.authType === 'oauth') {
|
|
55
|
+
sanitized[appId].hasToken = !!conn.accessToken;
|
|
56
|
+
} else if (conn.authType === 'api_key') {
|
|
57
|
+
sanitized[appId].keyHint = conn.apiKey ? `...${String(conn.apiKey).slice(-4)}` : undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return sanitized;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readNativeMcps() {
|
|
65
|
+
try {
|
|
66
|
+
const raw = fs.readFileSync(CLAUDE_FILE, 'utf8');
|
|
67
|
+
const parsed = JSON.parse(raw);
|
|
68
|
+
return Array.isArray(parsed?.claudeAiMcpEverConnected) ? parsed.claudeAiMcpEverConnected : [];
|
|
69
|
+
} catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function handleList(sendJson) {
|
|
75
|
+
const file = readConnectionsFile();
|
|
76
|
+
sendJson(200, {
|
|
77
|
+
connections: sanitizeConnections(file.connections),
|
|
78
|
+
nativeMcps: readNativeMcps(),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleUpsert(body, sendJson) {
|
|
83
|
+
const {
|
|
84
|
+
appId,
|
|
85
|
+
authType,
|
|
86
|
+
apiKey,
|
|
87
|
+
accessToken,
|
|
88
|
+
refreshToken,
|
|
89
|
+
expiresAt,
|
|
90
|
+
clientId,
|
|
91
|
+
clientSecret,
|
|
92
|
+
instanceUrl,
|
|
93
|
+
config,
|
|
94
|
+
enabled,
|
|
95
|
+
} = body || {};
|
|
96
|
+
|
|
97
|
+
if (!appId || !authType) {
|
|
98
|
+
return sendJson(400, { error: 'appId and authType are required' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (authType === 'api_key' && !apiKey) {
|
|
102
|
+
return sendJson(400, { error: 'apiKey is required for api_key auth' });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const file = readConnectionsFile();
|
|
106
|
+
const existing = file.connections?.[appId] || {};
|
|
107
|
+
|
|
108
|
+
file.connections[appId] = {
|
|
109
|
+
...existing,
|
|
110
|
+
enabled: enabled ?? true,
|
|
111
|
+
authType,
|
|
112
|
+
...(apiKey ? { apiKey: String(apiKey).trim() } : {}),
|
|
113
|
+
...(accessToken ? { accessToken } : {}),
|
|
114
|
+
...(refreshToken ? { refreshToken } : {}),
|
|
115
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
116
|
+
...(clientId ? { clientId } : {}),
|
|
117
|
+
...(clientSecret ? { clientSecret } : {}),
|
|
118
|
+
...(instanceUrl ? { instanceUrl } : {}),
|
|
119
|
+
...(config ? { config } : {}),
|
|
120
|
+
updatedAt: new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
writeConnectionsFile(file);
|
|
124
|
+
|
|
125
|
+
sendJson(200, {
|
|
126
|
+
ok: true,
|
|
127
|
+
keyHint: apiKey ? `...${String(apiKey).trim().slice(-4)}` : undefined,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function handleDelete(body, sendJson) {
|
|
132
|
+
const { appId } = body || {};
|
|
133
|
+
|
|
134
|
+
if (!appId) {
|
|
135
|
+
return sendJson(400, { error: 'appId is required' });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const file = readConnectionsFile();
|
|
139
|
+
if (file.connections && appId in file.connections) {
|
|
140
|
+
delete file.connections[appId];
|
|
141
|
+
writeConnectionsFile(file);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
sendJson(200, { ok: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
handleDelete,
|
|
149
|
+
handleList,
|
|
150
|
+
handleUpsert,
|
|
151
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notify_user — email notification via the Fly.io proxy /notify endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Proxy handles email lookup + Resend. When the tool is called from an Amalgm
|
|
5
|
+
* session, the proxy also sets a signed Reply-To address so user replies land
|
|
6
|
+
* back in the same session.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { PROXY_BASE_URL, PROXY_TOKEN } = require('../config');
|
|
10
|
+
const { ensureFreshProxyToken, readProxyToken } = require('../../proxy-token-store');
|
|
11
|
+
const { formatNotificationEmail } = require('../lib/email-md');
|
|
12
|
+
const { textResult, errorResult } = require('../lib/tool-result');
|
|
13
|
+
|
|
14
|
+
async function notifyUserViaProxy({ subject: customSubject, message, link, sessionId }) {
|
|
15
|
+
const notifyUrl = PROXY_BASE_URL
|
|
16
|
+
? `${PROXY_BASE_URL.replace(/\/anthropic\/?$/, '')}/notify`
|
|
17
|
+
: null;
|
|
18
|
+
if (!notifyUrl) {
|
|
19
|
+
console.warn('[AmalgmMCP:Notify] No proxy configured, cannot send notification');
|
|
20
|
+
return { ok: false, error: 'Notification proxy is not configured.' };
|
|
21
|
+
}
|
|
22
|
+
const proxyToken = await ensureFreshProxyToken({ logger: console }) || readProxyToken() || PROXY_TOKEN;
|
|
23
|
+
if (!proxyToken) {
|
|
24
|
+
console.warn('[AmalgmMCP:Notify] No proxy token configured, cannot send notification');
|
|
25
|
+
return { ok: false, error: 'Notification proxy token is not configured.' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const subject =
|
|
29
|
+
(customSubject && customSubject.trim()) || message.slice(0, 80).replace(/\n/g, ' ');
|
|
30
|
+
const html = formatNotificationEmail(message, link || null);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const res = await fetch(notifyUrl, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${proxyToken}`,
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
subject,
|
|
41
|
+
html,
|
|
42
|
+
text: message,
|
|
43
|
+
...(sessionId ? { sessionId } : {}),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
const text = await res.text().catch(() => '');
|
|
48
|
+
const error = `Notification proxy failed: ${res.status} ${text.slice(0, 200)}`.trim();
|
|
49
|
+
console.error(
|
|
50
|
+
`[AmalgmMCP:Notify] ${error}`,
|
|
51
|
+
);
|
|
52
|
+
return { ok: false, error };
|
|
53
|
+
}
|
|
54
|
+
const data = await res.json();
|
|
55
|
+
console.log(`[AmalgmMCP:Notify] Notification sent to ${data.email}`);
|
|
56
|
+
return { ok: true, data };
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.error('[AmalgmMCP:Notify] Error:', err.message);
|
|
59
|
+
return { ok: false, error: `Notification proxy error: ${err.message}` };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = [
|
|
64
|
+
{
|
|
65
|
+
name: 'notify_user',
|
|
66
|
+
description:
|
|
67
|
+
'Send a notification to the user via email (from emails@amalgm.ai). Use this to keep the user informed about important results, completions, or errors — especially during scheduled tasks, event triggers, and long-running operations where the user is likely not actively watching.\n\nBest practices:\n- Always call notify_user at the end of a scheduled task or event-triggered run.\n- For errors, include enough context that the user can act without opening the session.\n- Keep it scannable: lead with what happened, then details.\n\nFormatting: The message is rendered as full markdown. You can use headings (##), **bold**, *italic*, `inline code`, ```code blocks```, [links](url), lists (- or 1.), blockquotes (>), and --- horizontal rules. Write it like a concise status update — no greeting, no sign-off, just the information.',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
subject: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description:
|
|
74
|
+
'Short email subject line (max ~80 chars). If omitted, auto-generated from the message body.',
|
|
75
|
+
},
|
|
76
|
+
message: {
|
|
77
|
+
type: 'string',
|
|
78
|
+
description: 'The email body in markdown. Supports full markdown.',
|
|
79
|
+
},
|
|
80
|
+
level: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
enum: ['info', 'warning', 'error', 'success'],
|
|
83
|
+
description: 'Severity level. Defaults to info.',
|
|
84
|
+
},
|
|
85
|
+
link: { type: 'string', description: 'Optional URL for details.' },
|
|
86
|
+
},
|
|
87
|
+
required: ['message'],
|
|
88
|
+
},
|
|
89
|
+
async handler({ subject, message, level, link }, context = {}) {
|
|
90
|
+
if (!message) return errorResult('message is required');
|
|
91
|
+
const finalLevel = ['info', 'warning', 'error', 'success'].includes(level) ? level : 'info';
|
|
92
|
+
const result = await notifyUserViaProxy({
|
|
93
|
+
subject: subject || null,
|
|
94
|
+
message,
|
|
95
|
+
level: finalLevel,
|
|
96
|
+
link: link || null,
|
|
97
|
+
sessionId: context.callerSessionId || null,
|
|
98
|
+
});
|
|
99
|
+
if (!result.ok) {
|
|
100
|
+
return errorResult(result.error);
|
|
101
|
+
}
|
|
102
|
+
return textResult(
|
|
103
|
+
`Notification sent to ${result.data.email}${result.data.replyTo ? ` (replies route to this session)` : ''}.\n\nMessage: ${message}\nLevel: ${finalLevel}${link ? `\nLink: ${link}` : ''}`,
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
];
|