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,391 @@
1
+ /**
2
+ * Scheduled-tasks MCP tools.
3
+ *
4
+ * 8 tools: create, list, get, update, delete, run_now, cancel, history.
5
+ * Each entry is self-contained: name, description, inputSchema, handler.
6
+ * Storage lives in ./store.js, execution in ./executor.js.
7
+ */
8
+
9
+ const crypto = require('crypto');
10
+ const { textResult, errorResult } = require('../lib/tool-result');
11
+ const { loadTasks, saveTasks, readRunLog } = require('./store');
12
+ const { validateCronExpr } = require('./scheduler');
13
+ const { normalizeTaskSchedule } = require('./schedule-normalization');
14
+ const { executeTask, isRunning, getRunning, abortRunning } = require('./executor');
15
+ const {
16
+ chatInputToLegacyFields,
17
+ getChatInputText,
18
+ normalizeChatInput,
19
+ } = require('../../../lib/chatInput');
20
+ const {
21
+ DEFAULT_SELECTED_MODELS,
22
+ getSelectedModel,
23
+ hydrateModelPreferences,
24
+ } = require('../lib/prefs');
25
+ const credentialAdapter = require('../../credential-adapter');
26
+
27
+ function resolveTaskHarness(harness, chatInput) {
28
+ const candidate =
29
+ (chatInput && chatInput.agent && typeof chatInput.agent.harness === 'string' && chatInput.agent.harness)
30
+ || (typeof harness === 'string' && harness)
31
+ || 'claude_code';
32
+ return candidate;
33
+ }
34
+
35
+ function resolveTaskModel(harness, model) {
36
+ if (typeof model === 'string' && model) return model;
37
+ return getSelectedModel(harness) || DEFAULT_SELECTED_MODELS[harness] || null;
38
+ }
39
+
40
+ function resolveTaskAuthMethod(harness, authMethod) {
41
+ if (typeof authMethod === 'string' && authMethod) return authMethod;
42
+ if (credentialAdapter.VALID_HARNESS_IDS.includes(harness)) {
43
+ return credentialAdapter.getPersistedAuthMode(harness);
44
+ }
45
+ return 'amalgm';
46
+ }
47
+
48
+ function normalizeTaskRecord(taskLike) {
49
+ const harness = resolveTaskHarness(taskLike.harness, taskLike.chatInput);
50
+ const model = resolveTaskModel(harness, taskLike.model);
51
+ const authMethod = resolveTaskAuthMethod(harness, taskLike.authMethod);
52
+ const chatInput = normalizeChatInput(taskLike.chatInput, {
53
+ prompt: taskLike.prompt,
54
+ harness,
55
+ modelId: model,
56
+ authMethod,
57
+ projectPath: taskLike.projectPath,
58
+ cwd: taskLike.projectPath,
59
+ });
60
+ const legacy = chatInputToLegacyFields(chatInput, {
61
+ prompt: taskLike.prompt,
62
+ harness,
63
+ modelId: model,
64
+ authMethod,
65
+ projectPath: taskLike.projectPath,
66
+ cwd: taskLike.projectPath,
67
+ });
68
+
69
+ return {
70
+ ...taskLike,
71
+ chatInput,
72
+ prompt: getChatInputText(chatInput, taskLike.prompt) || legacy.prompt,
73
+ harness: legacy.harness || harness,
74
+ model: legacy.modelId || model,
75
+ authMethod: legacy.authMethod || authMethod,
76
+ projectPath: legacy.cwd || taskLike.projectPath || null,
77
+ };
78
+ }
79
+
80
+ module.exports = [
81
+ {
82
+ name: 'scheduled_tasks_create',
83
+ description:
84
+ 'Create a new scheduled task that runs a prompt on a schedule. Supports cron (recurring), once (one-shot), and interval (fixed ms) schedules.',
85
+ inputSchema: {
86
+ type: 'object',
87
+ properties: {
88
+ name: { type: 'string', description: 'Human-readable task name' },
89
+ schedule: {
90
+ type: 'object',
91
+ description: 'Schedule configuration',
92
+ properties: {
93
+ kind: {
94
+ type: 'string',
95
+ enum: ['cron', 'once', 'interval'],
96
+ description: 'cron=recurring, once=one-shot, interval=fixed ms',
97
+ },
98
+ expr: { type: 'string', description: 'Cron expression (for kind=cron), e.g. "0 9 * * *"' },
99
+ tz: { type: 'string', description: 'IANA timezone (defaults to the local user timezone)' },
100
+ at: { type: 'string', description: 'ISO datetime (for kind=once)' },
101
+ ms: { type: 'number', description: 'Interval in ms (for kind=interval, min 60000)' },
102
+ },
103
+ required: ['kind'],
104
+ },
105
+ description: { type: 'string', description: 'Optional markdown description shown in the UI' },
106
+ prompt: { type: 'string', description: 'The prompt to execute when the task runs' },
107
+ enabled: { type: 'boolean', description: 'Whether the task is active (default: true)' },
108
+ endsAt: { type: 'string', description: 'ISO datetime when the task auto-disables (null = never)' },
109
+ maxConcurrentRuns: { type: 'number', description: 'Max parallel executions (default: 1)' },
110
+ harness: { type: 'string', description: 'Agent harness (claude_code, codex, opencode)' },
111
+ model: { type: 'string', description: 'Model ID to use for execution' },
112
+ authMethod: { type: 'string', description: 'Auth method for this task run' },
113
+ projectPath: { type: 'string', description: 'Working directory path' },
114
+ chatInput: { type: 'object', description: 'Shared chat input shape for the task run' },
115
+ },
116
+ required: ['name', 'schedule'],
117
+ },
118
+ async handler({ name, description, schedule, prompt, enabled, endsAt, maxConcurrentRuns, harness, model, authMethod, projectPath, chatInput }) {
119
+ const normalizedScheduleResult = normalizeTaskSchedule(schedule);
120
+ if (normalizedScheduleResult.error) {
121
+ return errorResult(normalizedScheduleResult.error);
122
+ }
123
+ const normalizedSchedule = normalizedScheduleResult.schedule;
124
+
125
+ if (normalizedSchedule.kind === 'cron') {
126
+ const err = validateCronExpr(normalizedSchedule.expr, normalizedSchedule.tz);
127
+ if (err) return errorResult(`Invalid cron expression: ${err}`);
128
+ }
129
+ if (!prompt && !chatInput) {
130
+ return errorResult('prompt or chatInput is required');
131
+ }
132
+
133
+ await hydrateModelPreferences();
134
+
135
+ const task = normalizeTaskRecord({
136
+ id: crypto.randomUUID(),
137
+ name,
138
+ description: description || '',
139
+ enabled: enabled !== false,
140
+ schedule: normalizedSchedule,
141
+ prompt: prompt || null,
142
+ endsAt: endsAt || null,
143
+ maxConcurrentRuns: maxConcurrentRuns || 1,
144
+ harness: harness || null,
145
+ model: model || null,
146
+ authMethod: authMethod || null,
147
+ projectPath: projectPath || null,
148
+ chatInput: chatInput || null,
149
+ createdAt: new Date().toISOString(),
150
+ lastRunAt: null,
151
+ lastStatus: null,
152
+ });
153
+ const data = loadTasks();
154
+ data.tasks.push(task);
155
+ saveTasks(data);
156
+ console.log(`[AmalgmMCP] Created task: ${task.id} (${task.name})`);
157
+ return textResult(`Task created successfully.\n\n${JSON.stringify(task, null, 2)}`);
158
+ },
159
+ },
160
+ {
161
+ name: 'scheduled_tasks_list',
162
+ description: 'List all scheduled tasks with their current status',
163
+ inputSchema: {
164
+ type: 'object',
165
+ properties: {
166
+ enabled_only: { type: 'boolean', description: 'If true, only return enabled tasks' },
167
+ },
168
+ },
169
+ async handler({ enabled_only }) {
170
+ const data = loadTasks();
171
+ let tasks = data.tasks.map(normalizeTaskRecord);
172
+ if (enabled_only) tasks = tasks.filter((t) => t.enabled);
173
+ if (tasks.length === 0) return textResult('No scheduled tasks found.');
174
+ const summary = tasks
175
+ .map((t) => {
176
+ const running = isRunning(t.id) ? ' [RUNNING]' : '';
177
+ const sched =
178
+ t.schedule.kind === 'cron'
179
+ ? `cron: ${t.schedule.expr} (${t.schedule.tz || 'UTC'})`
180
+ : t.schedule.kind === 'once'
181
+ ? `once: ${t.schedule.at}`
182
+ : `every ${t.schedule.ms}ms`;
183
+ return `- ${t.name} (${t.id})\n ${t.enabled ? 'enabled' : 'disabled'} | ${sched} | last: ${t.lastStatus || 'never'}${running}`;
184
+ })
185
+ .join('\n\n');
186
+ return textResult(summary);
187
+ },
188
+ },
189
+ {
190
+ name: 'scheduled_tasks_get',
191
+ description: 'Get detailed information about a specific task including recent run history',
192
+ inputSchema: {
193
+ type: 'object',
194
+ properties: { task_id: { type: 'string', description: 'The task ID' } },
195
+ required: ['task_id'],
196
+ },
197
+ async handler({ task_id }) {
198
+ const data = loadTasks();
199
+ const task = data.tasks.find((t) => t.id === task_id);
200
+ if (!task) return errorResult(`Task not found: ${task_id}`);
201
+ const normalizedTask = normalizeTaskRecord(task);
202
+ return textResult(
203
+ JSON.stringify(
204
+ { ...normalizedTask, isRunning: isRunning(task_id), recentRuns: readRunLog(task_id, 10) },
205
+ null,
206
+ 2,
207
+ ),
208
+ );
209
+ },
210
+ },
211
+ {
212
+ name: 'scheduled_tasks_update',
213
+ description: 'Update properties of an existing scheduled task',
214
+ inputSchema: {
215
+ type: 'object',
216
+ properties: {
217
+ task_id: { type: 'string' },
218
+ name: { type: 'string' },
219
+ schedule: {
220
+ type: 'object',
221
+ properties: {
222
+ kind: { type: 'string', enum: ['cron', 'once', 'interval'] },
223
+ expr: { type: 'string' },
224
+ tz: { type: 'string' },
225
+ at: { type: 'string' },
226
+ ms: { type: 'number' },
227
+ },
228
+ },
229
+ description: { type: 'string' },
230
+ prompt: { type: 'string' },
231
+ enabled: { type: 'boolean' },
232
+ endsAt: { type: 'string' },
233
+ maxConcurrentRuns: { type: 'number' },
234
+ harness: { type: 'string' },
235
+ model: { type: 'string' },
236
+ authMethod: { type: 'string' },
237
+ projectPath: { type: 'string' },
238
+ chatInput: { type: 'object' },
239
+ },
240
+ required: ['task_id'],
241
+ },
242
+ async handler({ task_id, name, description, schedule, prompt, enabled, endsAt, maxConcurrentRuns, harness, model, authMethod, projectPath, chatInput }) {
243
+ const data = loadTasks();
244
+ const task = data.tasks.find((t) => t.id === task_id);
245
+ if (!task) return errorResult(`Task not found: ${task_id}`);
246
+
247
+ await hydrateModelPreferences();
248
+
249
+ if (schedule) {
250
+ const normalizedScheduleResult = normalizeTaskSchedule(schedule);
251
+ if (normalizedScheduleResult.error) {
252
+ return errorResult(normalizedScheduleResult.error);
253
+ }
254
+
255
+ const normalizedSchedule = normalizedScheduleResult.schedule;
256
+ if (normalizedSchedule.kind === 'cron') {
257
+ const err = validateCronExpr(normalizedSchedule.expr, normalizedSchedule.tz);
258
+ if (err) return errorResult(`Invalid cron expression: ${err}`);
259
+ }
260
+ task.schedule = normalizedSchedule;
261
+ }
262
+ if (name !== undefined) task.name = name;
263
+ if (description !== undefined) task.description = description || '';
264
+ if (prompt !== undefined) task.prompt = prompt;
265
+ if (enabled !== undefined) task.enabled = enabled;
266
+ if (endsAt !== undefined) task.endsAt = endsAt || null;
267
+ if (maxConcurrentRuns !== undefined) task.maxConcurrentRuns = maxConcurrentRuns;
268
+ if (harness !== undefined) task.harness = harness || null;
269
+ if (model !== undefined) task.model = model || null;
270
+ if (authMethod !== undefined) task.authMethod = authMethod || null;
271
+ if (projectPath !== undefined) task.projectPath = projectPath || null;
272
+ if (chatInput !== undefined) task.chatInput = chatInput || null;
273
+ if (
274
+ chatInput === undefined
275
+ && (
276
+ prompt !== undefined
277
+ || harness !== undefined
278
+ || model !== undefined
279
+ || authMethod !== undefined
280
+ || projectPath !== undefined
281
+ )
282
+ ) {
283
+ const existingNonTextParts = Array.isArray(task.chatInput?.parts)
284
+ ? task.chatInput.parts.filter((part) => part.type !== 'text')
285
+ : [];
286
+ task.chatInput = {
287
+ ...(task.chatInput || {}),
288
+ parts: [
289
+ ...existingNonTextParts,
290
+ ...((typeof task.prompt === 'string' && task.prompt)
291
+ ? [{ type: 'text', text: task.prompt }]
292
+ : []),
293
+ ],
294
+ agent: {
295
+ ...(task.chatInput?.agent || {}),
296
+ ...(task.harness ? { harness: task.harness } : {}),
297
+ ...(task.model ? { model: task.model } : {}),
298
+ ...(task.authMethod ? { authMethod: task.authMethod } : {}),
299
+ },
300
+ tools: task.chatInput?.tools || { mcpAppIds: [] },
301
+ execution: {
302
+ ...(task.chatInput?.execution || {}),
303
+ ...(projectPath !== undefined ? { cwd: task.projectPath || null } : {}),
304
+ },
305
+ };
306
+ }
307
+
308
+ const normalizedTask = normalizeTaskRecord(task);
309
+ Object.assign(task, normalizedTask);
310
+
311
+ saveTasks(data);
312
+ console.log(`[AmalgmMCP] Updated task: ${task.id} (${task.name})`);
313
+ return textResult(`Task updated.\n\n${JSON.stringify(task, null, 2)}`);
314
+ },
315
+ },
316
+ {
317
+ name: 'scheduled_tasks_delete',
318
+ description: 'Delete a scheduled task permanently',
319
+ inputSchema: {
320
+ type: 'object',
321
+ properties: { task_id: { type: 'string' } },
322
+ required: ['task_id'],
323
+ },
324
+ async handler({ task_id }) {
325
+ const data = loadTasks();
326
+ const idx = data.tasks.findIndex((t) => t.id === task_id);
327
+ if (idx === -1) return errorResult(`Task not found: ${task_id}`);
328
+ abortRunning(task_id);
329
+ const deleted = data.tasks.splice(idx, 1)[0];
330
+ saveTasks(data);
331
+ console.log(`[AmalgmMCP] Deleted task: ${task_id} (${deleted.name})`);
332
+ return textResult(`Task "${deleted.name}" deleted.`);
333
+ },
334
+ },
335
+ {
336
+ name: 'scheduled_tasks_run_now',
337
+ description: 'Trigger immediate execution of a scheduled task, bypassing its schedule',
338
+ inputSchema: {
339
+ type: 'object',
340
+ properties: { task_id: { type: 'string' } },
341
+ required: ['task_id'],
342
+ },
343
+ async handler({ task_id }) {
344
+ const data = loadTasks();
345
+ const task = data.tasks.find((t) => t.id === task_id);
346
+ if (!task) return errorResult(`Task not found: ${task_id}`);
347
+ if (isRunning(task_id)) return textResult(`Task "${task.name}" is already running.`);
348
+ executeTask(task).catch((err) =>
349
+ console.error(`[AmalgmMCP:RunNow] ${task_id} failed:`, err.message),
350
+ );
351
+ return textResult(`Task "${task.name}" triggered. Running in background.`);
352
+ },
353
+ },
354
+ {
355
+ name: 'scheduled_tasks_cancel',
356
+ description: 'Cancel a currently running task execution',
357
+ inputSchema: {
358
+ type: 'object',
359
+ properties: { task_id: { type: 'string' } },
360
+ required: ['task_id'],
361
+ },
362
+ async handler({ task_id }) {
363
+ const running = getRunning(task_id);
364
+ if (!running) return textResult(`No running execution for task: ${task_id}`);
365
+ abortRunning(task_id);
366
+ return textResult(`Cancelled execution of task ${task_id} (run: ${running.runId}).`);
367
+ },
368
+ },
369
+ {
370
+ name: 'scheduled_tasks_history',
371
+ description: 'Get execution history for a scheduled task',
372
+ inputSchema: {
373
+ type: 'object',
374
+ properties: {
375
+ task_id: { type: 'string' },
376
+ limit: { type: 'number', description: 'Max runs to return (default: 20)' },
377
+ },
378
+ required: ['task_id'],
379
+ },
380
+ async handler({ task_id, limit }) {
381
+ const data = loadTasks();
382
+ const task = data.tasks.find((t) => t.id === task_id);
383
+ if (!task) return errorResult(`Task not found: ${task_id}`);
384
+ const runs = readRunLog(task_id, limit || 20);
385
+ if (runs.length === 0) return textResult(`No execution history for "${task.name}".`);
386
+ return textResult(
387
+ `Execution history for "${task.name}":\n\n${JSON.stringify(runs, null, 2)}`,
388
+ );
389
+ },
390
+ },
391
+ ];
@@ -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
+ };