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,269 @@
1
+ 'use strict';
2
+
3
+ const { textResult } = require('../lib/tool-result');
4
+ const {
5
+ deleteTool,
6
+ deleteToolAction,
7
+ getTool,
8
+ getToolAction,
9
+ readToolbox,
10
+ updateTool,
11
+ updateToolAction,
12
+ upsertTool,
13
+ upsertToolAction,
14
+ } = require('./store');
15
+
16
+ function compactTool(tool, actionCount) {
17
+ return {
18
+ id: tool.id,
19
+ name: tool.name,
20
+ type: tool.type,
21
+ owner: tool.owner,
22
+ origin: tool.origin,
23
+ status: tool.status,
24
+ discovery: tool.discovery,
25
+ actionCount,
26
+ source: tool.source,
27
+ guide: tool.guide,
28
+ display: tool.display,
29
+ policy: tool.policy,
30
+ };
31
+ }
32
+
33
+ function listPayload(args = {}) {
34
+ const toolbox = readToolbox();
35
+ const actionsByTool = new Map();
36
+ for (const action of Object.values(toolbox.toolActions || {})) {
37
+ const existing = actionsByTool.get(action.toolId) || [];
38
+ existing.push(action);
39
+ actionsByTool.set(action.toolId, existing);
40
+ }
41
+
42
+ const tools = Object.values(toolbox.tools || {})
43
+ .filter((tool) => !args.type || tool.type === args.type)
44
+ .filter((tool) => !args.owner || tool.owner === args.owner)
45
+ .filter((tool) => !args.origin || tool.origin === args.origin)
46
+ .map((tool) => compactTool(tool, actionsByTool.get(tool.id)?.length || 0));
47
+
48
+ const payload = {
49
+ version: toolbox.version,
50
+ tools,
51
+ ...(args.include_actions
52
+ ? { toolActions: toolbox.toolActions }
53
+ : {}),
54
+ };
55
+ return payload;
56
+ }
57
+
58
+ module.exports = [
59
+ {
60
+ name: 'toolbox_tools_list',
61
+ description: 'List registered Amalgm Toolbox connectors/tools. Use this to see persistent user tools available across sessions.',
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ type: { type: 'string', enum: ['mcp', 'api', 'cli'], description: 'Optional tool type filter.' },
66
+ owner: { type: 'string', description: 'Optional owner filter, e.g. amalgm, claude, cursor, codex, or an agent harness id.' },
67
+ origin: { type: 'string', enum: ['system', 'user', 'catalog'], description: 'Optional origin filter.' },
68
+ include_actions: { type: 'boolean', description: 'Include full tool action records. Defaults false.' },
69
+ },
70
+ },
71
+ async handler(args) {
72
+ return textResult(JSON.stringify(listPayload(args || {}), null, 2));
73
+ },
74
+ },
75
+ {
76
+ name: 'toolbox_tool_get',
77
+ description: 'Read one registered Toolbox connector/tool by id, optionally including its actions.',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ id: { type: 'string', description: 'Tool id to read.' },
82
+ include_actions: { type: 'boolean', description: 'Include actions for this tool. Defaults true.' },
83
+ },
84
+ required: ['id'],
85
+ },
86
+ async handler(args) {
87
+ const payload = getTool(args?.id, { includeActions: args?.include_actions !== false });
88
+ return textResult(JSON.stringify(payload, null, 2));
89
+ },
90
+ },
91
+ {
92
+ name: 'toolbox_tool_register',
93
+ description: [
94
+ 'Register or update a persistent Toolbox connector/tool. This is how agents save custom tools for future sessions.',
95
+ 'For CLI tools, provide type="cli" and source.command. A default <tool>.run action is created automatically unless actions are provided.',
96
+ 'Optional skillPath/guidePath attaches usage instructions to the tool without becoming a separate callable action.',
97
+ ].join('\n'),
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ id: { type: 'string', description: 'Stable tool id, e.g. acme.executor.' },
102
+ name: { type: 'string', description: 'Human-readable connector/tool name.' },
103
+ type: { type: 'string', enum: ['mcp', 'api', 'cli'] },
104
+ owner: { type: 'string', description: 'Tool owner. Defaults to amalgm. Use a harness or agent id for externally owned tools.' },
105
+ origin: { type: 'string', enum: ['user', 'catalog'], description: 'Defaults to user.' },
106
+ status: { type: 'string', enum: ['enabled', 'disabled', 'error'] },
107
+ source: {
108
+ type: 'object',
109
+ description: 'Type-specific source config. CLI: {command,args,cwd,inputMode,outputMode}. MCP: {transport,url|command}. API: {baseUrl,specUrl,specPath,endpoints}.',
110
+ additionalProperties: true,
111
+ },
112
+ command: { type: 'string', description: 'CLI shortcut for source.command.' },
113
+ args: { type: 'array', items: { type: 'string' }, description: 'CLI/MCP stdio args shortcut.' },
114
+ cwd: { type: 'string', description: 'Working directory shortcut.' },
115
+ inputMode: { type: 'string', enum: ['json-stdin', 'argv', 'terminal', 'none'] },
116
+ outputMode: { type: 'string', enum: ['json', 'text'] },
117
+ skillPath: { type: 'string', description: 'Optional local SKILL.md path with usage guidance.' },
118
+ guidePath: { type: 'string', description: 'Optional local guide path with usage guidance.' },
119
+ display: { type: 'object', additionalProperties: true },
120
+ policy: { type: 'object', additionalProperties: true },
121
+ actions: {
122
+ type: 'array',
123
+ description: 'Optional static action records to create/update with the tool.',
124
+ items: { type: 'object', additionalProperties: true },
125
+ },
126
+ },
127
+ required: ['id', 'name', 'type'],
128
+ },
129
+ async handler(args) {
130
+ const saved = upsertTool(args || {});
131
+ return textResult(JSON.stringify({ ok: true, ...saved }, null, 2));
132
+ },
133
+ },
134
+ {
135
+ name: 'toolbox_tool_update',
136
+ description: [
137
+ 'Partially update an existing Toolbox connector/tool without replacing unspecified fields.',
138
+ 'Use this for modifications like status, display metadata, policy, guide/skill path, owner, or source patches.',
139
+ 'System tools cannot be modified.',
140
+ ].join('\n'),
141
+ inputSchema: {
142
+ type: 'object',
143
+ properties: {
144
+ id: { type: 'string', description: 'Tool id to update.' },
145
+ name: { type: 'string', description: 'Optional new human-readable name.' },
146
+ type: { type: 'string', enum: ['mcp', 'api', 'cli'], description: 'Optional type update.' },
147
+ owner: { type: 'string', description: 'Optional owner update.' },
148
+ origin: { type: 'string', enum: ['user', 'catalog'], description: 'Optional origin update.' },
149
+ status: { type: 'string', enum: ['enabled', 'disabled', 'error'] },
150
+ source: {
151
+ type: 'object',
152
+ description: 'Partial source patch. CLI: {command,args,cwd,inputMode,outputMode}. MCP: {transport,url|command}. API: {baseUrl,specUrl,specPath,endpoints}.',
153
+ additionalProperties: true,
154
+ },
155
+ command: { type: 'string', description: 'CLI shortcut for source.command.' },
156
+ args: { type: 'array', items: { type: 'string' }, description: 'CLI/MCP stdio args shortcut.' },
157
+ cwd: { type: 'string', description: 'Working directory shortcut.' },
158
+ inputMode: { type: 'string', enum: ['json-stdin', 'argv', 'terminal', 'none'] },
159
+ outputMode: { type: 'string', enum: ['json', 'text'] },
160
+ discovery: { type: 'object', additionalProperties: true },
161
+ skillPath: { type: 'string', description: 'Optional local SKILL.md path with usage guidance.' },
162
+ guidePath: { type: 'string', description: 'Optional local guide path with usage guidance.' },
163
+ guide: { type: 'object', additionalProperties: true },
164
+ display: { type: 'object', additionalProperties: true },
165
+ policy: { type: 'object', additionalProperties: true },
166
+ },
167
+ required: ['id'],
168
+ },
169
+ async handler(args) {
170
+ const saved = updateTool(args || {});
171
+ return textResult(JSON.stringify({ ok: true, ...saved }, null, 2));
172
+ },
173
+ },
174
+ {
175
+ name: 'toolbox_action_get',
176
+ description: 'Read one registered Toolbox action by id.',
177
+ inputSchema: {
178
+ type: 'object',
179
+ properties: {
180
+ id: { type: 'string', description: 'Tool action id to read.' },
181
+ },
182
+ required: ['id'],
183
+ },
184
+ async handler(args) {
185
+ const payload = getToolAction(args?.id);
186
+ return textResult(JSON.stringify(payload, null, 2));
187
+ },
188
+ },
189
+ {
190
+ name: 'toolbox_action_register',
191
+ description: 'Register or update a persistent action under an existing Toolbox tool. Use this for richer CLI/API schemas beyond the default run action.',
192
+ inputSchema: {
193
+ type: 'object',
194
+ properties: {
195
+ id: { type: 'string', description: 'Stable action id, e.g. acme.executor.run_job.' },
196
+ toolId: { type: 'string', description: 'Parent tool id.' },
197
+ name: { type: 'string', description: 'Action name, e.g. run_job.' },
198
+ displayName: { type: 'string' },
199
+ description: { type: 'string' },
200
+ inputSchema: { type: 'object', additionalProperties: true },
201
+ outputSchema: { type: 'object', additionalProperties: true },
202
+ status: { type: 'string', enum: ['enabled', 'disabled', 'error'] },
203
+ sourceAction: { type: 'object', additionalProperties: true },
204
+ skillPath: { type: 'string', description: 'Optional local SKILL.md path with usage guidance.' },
205
+ guidePath: { type: 'string', description: 'Optional local guide path with usage guidance.' },
206
+ policy: { type: 'object', additionalProperties: true },
207
+ },
208
+ required: ['toolId', 'name'],
209
+ },
210
+ async handler(args) {
211
+ const action = upsertToolAction(args || {});
212
+ return textResult(JSON.stringify({ ok: true, action }, null, 2));
213
+ },
214
+ },
215
+ {
216
+ name: 'toolbox_action_update',
217
+ description: 'Partially update an existing Toolbox action without replacing unspecified fields. System tool actions cannot be modified.',
218
+ inputSchema: {
219
+ type: 'object',
220
+ properties: {
221
+ id: { type: 'string', description: 'Tool action id to update.' },
222
+ name: { type: 'string', description: 'Optional action name update.' },
223
+ displayName: { type: 'string' },
224
+ description: { type: 'string' },
225
+ inputSchema: { type: 'object', additionalProperties: true },
226
+ outputSchema: { type: 'object', additionalProperties: true },
227
+ status: { type: 'string', enum: ['enabled', 'disabled', 'error'] },
228
+ sourceAction: { type: 'object', additionalProperties: true },
229
+ skillPath: { type: 'string', description: 'Optional local SKILL.md path with usage guidance.' },
230
+ guidePath: { type: 'string', description: 'Optional local guide path with usage guidance.' },
231
+ guide: { type: 'object', additionalProperties: true },
232
+ policy: { type: 'object', additionalProperties: true },
233
+ },
234
+ required: ['id'],
235
+ },
236
+ async handler(args) {
237
+ const action = updateToolAction(args || {});
238
+ return textResult(JSON.stringify({ ok: true, action }, null, 2));
239
+ },
240
+ },
241
+ {
242
+ name: 'toolbox_tool_delete',
243
+ description: 'Delete a user/catalog Toolbox tool and its actions. System tools cannot be deleted.',
244
+ inputSchema: {
245
+ type: 'object',
246
+ properties: {
247
+ id: { type: 'string', description: 'Tool id to delete.' },
248
+ },
249
+ required: ['id'],
250
+ },
251
+ async handler(args) {
252
+ return textResult(JSON.stringify(deleteTool(args?.id), null, 2));
253
+ },
254
+ },
255
+ {
256
+ name: 'toolbox_action_delete',
257
+ description: 'Delete a user/catalog Toolbox action. System tool actions cannot be deleted.',
258
+ inputSchema: {
259
+ type: 'object',
260
+ properties: {
261
+ id: { type: 'string', description: 'Tool action id to delete.' },
262
+ },
263
+ required: ['id'],
264
+ },
265
+ async handler(args) {
266
+ return textResult(JSON.stringify(deleteToolAction(args?.id), null, 2));
267
+ },
268
+ },
269
+ ];
@@ -0,0 +1,105 @@
1
+ /**
2
+ * /user-api-keys 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 path = require('path');
8
+ const { AMALGM_DIR } = require('../config');
9
+ const { getEnvKeyNameForHarness } = require('../../../lib/harnesses.js');
10
+ const { deleteCredential, listCredentials, upsertCredential } = require('../../chat-core/credentials/store');
11
+
12
+ const CREDENTIALS_PATH = path.join(AMALGM_DIR, '.amalgm-credentials');
13
+
14
+ function shellQuote(value) {
15
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
16
+ }
17
+
18
+ function decodeShellValue(rawValue) {
19
+ const value = String(rawValue ?? '').trim();
20
+ if (value.startsWith("'") && value.endsWith("'")) {
21
+ return value.slice(1, -1).replace(/'\\''/g, "'");
22
+ }
23
+ if (value.startsWith('"') && value.endsWith('"')) {
24
+ return value
25
+ .slice(1, -1)
26
+ .replace(/\\"/g, '"')
27
+ .replace(/\\\$/g, '$')
28
+ .replace(/\\\\/g, '\\');
29
+ }
30
+ return value;
31
+ }
32
+
33
+ function readCredentialExports() {
34
+ const values = {};
35
+ try {
36
+ const raw = fs.readFileSync(CREDENTIALS_PATH, 'utf-8');
37
+ for (const line of raw.split(/\r?\n/)) {
38
+ const match = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)\s*$/);
39
+ if (!match) continue;
40
+ values[match[1]] = decodeShellValue(match[2]);
41
+ }
42
+ } catch {
43
+ // No existing credential file.
44
+ }
45
+ return values;
46
+ }
47
+
48
+ function writeCredentialExports(values) {
49
+ const keys = Object.keys(values).sort();
50
+ const content = keys
51
+ .map((key) => `export ${key}=${shellQuote(values[key])}`)
52
+ .join('\n') + '\n';
53
+ fs.mkdirSync(path.dirname(CREDENTIALS_PATH), { recursive: true });
54
+ fs.writeFileSync(CREDENTIALS_PATH, content, { mode: 0o600 });
55
+ }
56
+
57
+ async function handleSave(body, sendJson) {
58
+ try {
59
+ const { harnessId, apiKey } = body || {};
60
+
61
+ if (!harnessId || !apiKey) {
62
+ return sendJson(400, { error: 'harnessId and apiKey are required' });
63
+ }
64
+
65
+ const envKeyName = getEnvKeyNameForHarness(harnessId);
66
+ const values = readCredentialExports();
67
+ values[envKeyName] = apiKey;
68
+ writeCredentialExports(values);
69
+ const baseUrl = harnessId === 'claude_code'
70
+ ? 'https://api.anthropic.com'
71
+ : harnessId === 'opencode'
72
+ ? 'https://ai-gateway.vercel.sh/v1'
73
+ : 'https://api.openai.com/v1';
74
+ upsertCredential(AMALGM_DIR, {
75
+ name: harnessId === 'claude_code' ? 'Anthropic' : harnessId === 'opencode' ? 'Vercel AI Gateway' : 'OpenAI',
76
+ baseUrl,
77
+ key: apiKey,
78
+ });
79
+
80
+ sendJson(200, { keyHint: `...${String(apiKey).slice(-4)}` });
81
+ } catch (error) {
82
+ sendJson(500, {
83
+ error: error instanceof Error ? error.message : 'Failed to save API key',
84
+ });
85
+ }
86
+ }
87
+
88
+ async function handleDelete(sendJson) {
89
+ try {
90
+ fs.rmSync(CREDENTIALS_PATH, { force: true });
91
+ for (const credential of listCredentials(AMALGM_DIR)) {
92
+ deleteCredential(AMALGM_DIR, credential.id);
93
+ }
94
+ sendJson(200, { ok: true });
95
+ } catch (error) {
96
+ sendJson(500, {
97
+ error: error instanceof Error ? error.message : 'Failed to remove API key',
98
+ });
99
+ }
100
+ }
101
+
102
+ module.exports = {
103
+ handleDelete,
104
+ handleSave,
105
+ };