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,933 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Toolbox store — local capability database.
5
+ *
6
+ * The durable model has two core tables:
7
+ * tools → registered connector/provider records
8
+ * tool_actions → LLM-callable actions materialized from each tool
9
+ *
10
+ * Secrets intentionally do not live here. Tool records may point at secret refs
11
+ * once auth wiring is added.
12
+ */
13
+
14
+ const path = require('path');
15
+ const { AMALGM_DIR, PORT } = require('../config');
16
+ const { ensureDir, readJson, writeJsonAtomic } = require('../lib/storage');
17
+ const { openLocalDb } = require('../state/db');
18
+ const { insertStateEvent, publishStateEvent } = require('../state/events');
19
+
20
+ const TOOLBOX_VERSION = 1;
21
+ const TOOLBOX_DIR = path.join(AMALGM_DIR, 'toolbox');
22
+ const TOOLBOX_FILE = path.join(TOOLBOX_DIR, 'toolbox.json');
23
+
24
+ const TOOL_TYPES = new Set(['mcp', 'api', 'cli']);
25
+ const TOOL_ORIGINS = new Set(['system', 'user', 'catalog']);
26
+ const RECORD_STATUSES = new Set(['enabled', 'disabled', 'error']);
27
+ const DISCOVERY_MODES = new Set(['dynamic', 'static', 'single']);
28
+ const DEFAULT_TOOL_OWNER = 'amalgm';
29
+ const MODEL_UNSUPPORTED_TOP_LEVEL_SCHEMA_KEYS = ['anyOf', 'oneOf', 'allOf', 'not', 'enum'];
30
+
31
+ function nowIso() {
32
+ return new Date().toISOString();
33
+ }
34
+
35
+ function isObject(value) {
36
+ return !!value && typeof value === 'object' && !Array.isArray(value);
37
+ }
38
+
39
+ function hasOwn(value, key) {
40
+ return Object.prototype.hasOwnProperty.call(value || {}, key);
41
+ }
42
+
43
+ function hasAnyOwn(value, keys) {
44
+ return keys.some((key) => hasOwn(value, key));
45
+ }
46
+
47
+ function nonEmptyString(value) {
48
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
49
+ }
50
+
51
+ function sanitizeId(value, fallback = 'tool') {
52
+ return String(value || fallback)
53
+ .trim()
54
+ .toLowerCase()
55
+ .replace(/[^a-z0-9_.-]+/g, '.')
56
+ .replace(/\.+/g, '.')
57
+ .replace(/^[._-]+|[._-]+$/g, '')
58
+ || fallback;
59
+ }
60
+
61
+ function sanitizeActionName(value, fallback = 'run') {
62
+ return String(value || fallback)
63
+ .trim()
64
+ .replace(/[^A-Za-z0-9_.-]+/g, '_')
65
+ .replace(/_+/g, '_')
66
+ .replace(/^[._-]+|[._-]+$/g, '')
67
+ || fallback;
68
+ }
69
+
70
+ function emptyToolbox() {
71
+ return {
72
+ version: TOOLBOX_VERSION,
73
+ tools: {},
74
+ toolActions: {},
75
+ };
76
+ }
77
+
78
+ function normalizeToolbox(raw) {
79
+ const parsed = isObject(raw) ? raw : {};
80
+ const tools = {};
81
+ if (isObject(parsed.tools)) {
82
+ for (const [toolId, tool] of Object.entries(parsed.tools)) {
83
+ if (!isObject(tool)) continue;
84
+ tools[toolId] = {
85
+ ...tool,
86
+ owner: normalizeOwner(tool.owner || tool.toolOwner || tool.ownership || tool.policy?.owner),
87
+ };
88
+ }
89
+ }
90
+
91
+ return {
92
+ version: TOOLBOX_VERSION,
93
+ tools,
94
+ toolActions: normalizeToolActions(parsed.toolActions),
95
+ };
96
+ }
97
+
98
+ function safeJsonParse(value, fallback = null) {
99
+ if (typeof value !== 'string' || !value) return fallback;
100
+ try {
101
+ return JSON.parse(value);
102
+ } catch {
103
+ return fallback;
104
+ }
105
+ }
106
+
107
+ function anyObjectSchema(description) {
108
+ return {
109
+ type: 'object',
110
+ description,
111
+ additionalProperties: true,
112
+ };
113
+ }
114
+
115
+ function summarizeSchemaAlternatives(schema) {
116
+ const alternatives = [];
117
+ for (const key of ['anyOf', 'oneOf', 'allOf']) {
118
+ const entries = Array.isArray(schema?.[key]) ? schema[key] : [];
119
+ for (const entry of entries) {
120
+ if (!isObject(entry) || !Array.isArray(entry.required)) continue;
121
+ const required = entry.required
122
+ .map((value) => nonEmptyString(value))
123
+ .filter(Boolean);
124
+ if (required.length > 0) alternatives.push(required.join(', '));
125
+ }
126
+ }
127
+ return alternatives;
128
+ }
129
+
130
+ function normalizeInputSchema(inputSchema, description) {
131
+ if (!isObject(inputSchema)) return anyObjectSchema(description);
132
+
133
+ const alternatives = summarizeSchemaAlternatives(inputSchema);
134
+ const schema = { ...inputSchema };
135
+ for (const key of MODEL_UNSUPPORTED_TOP_LEVEL_SCHEMA_KEYS) {
136
+ delete schema[key];
137
+ }
138
+
139
+ if (schema.type !== 'object') {
140
+ return anyObjectSchema(schema.description || description);
141
+ }
142
+
143
+ if (!isObject(schema.properties)) schema.properties = {};
144
+ if (alternatives.length > 0) {
145
+ const hint = `Provide fields matching one of these alternatives: ${alternatives.join('; ')}.`;
146
+ schema.description = [nonEmptyString(schema.description), hint].filter(Boolean).join(' ');
147
+ }
148
+ return schema;
149
+ }
150
+
151
+ function normalizeToolActions(rawToolActions) {
152
+ const actions = {};
153
+ if (!isObject(rawToolActions)) return actions;
154
+ for (const [actionId, action] of Object.entries(rawToolActions)) {
155
+ if (!isObject(action)) continue;
156
+ actions[actionId] = {
157
+ ...action,
158
+ inputSchema: normalizeInputSchema(action.inputSchema, `Input for ${action.name || actionId}.`),
159
+ };
160
+ }
161
+ return actions;
162
+ }
163
+
164
+ function isInheritedHarnessTool(tool) {
165
+ return tool?.owner === 'codex' && tool?.origin === 'catalog';
166
+ }
167
+
168
+ function dropInheritedHarnessTools(toolbox) {
169
+ for (const [toolId, tool] of Object.entries(toolbox.tools || {})) {
170
+ if (!isInheritedHarnessTool(tool)) continue;
171
+ delete toolbox.tools[toolId];
172
+ for (const [actionId, action] of Object.entries(toolbox.toolActions || {})) {
173
+ if (action?.toolId === toolId) delete toolbox.toolActions[actionId];
174
+ }
175
+ }
176
+ return toolbox;
177
+ }
178
+
179
+ function cliRunInputSchema(source) {
180
+ const inputMode = nonEmptyString(source.inputMode) || 'json-stdin';
181
+ if (inputMode === 'argv') {
182
+ return {
183
+ type: 'object',
184
+ properties: {
185
+ args: {
186
+ type: 'array',
187
+ items: { type: 'string' },
188
+ description: 'Arguments to append when running the command.',
189
+ },
190
+ },
191
+ required: ['args'],
192
+ additionalProperties: false,
193
+ };
194
+ }
195
+ if (inputMode === 'terminal') {
196
+ return {
197
+ type: 'object',
198
+ properties: {
199
+ command: {
200
+ type: 'string',
201
+ description: 'Terminal command or subcommand for this registered CLI surface.',
202
+ },
203
+ },
204
+ required: ['command'],
205
+ additionalProperties: false,
206
+ };
207
+ }
208
+ return anyObjectSchema('JSON payload passed to the CLI command on stdin.');
209
+ }
210
+
211
+ function normalizeGuide(input) {
212
+ if (!input) return undefined;
213
+ if (typeof input === 'string') {
214
+ const pathValue = nonEmptyString(input);
215
+ return pathValue ? { kind: 'skill', path: pathValue, appliesTo: 'tool' } : undefined;
216
+ }
217
+ if (!isObject(input)) return undefined;
218
+
219
+ const pathValue = nonEmptyString(input.path || input.skillPath || input.guidePath);
220
+ const text = nonEmptyString(input.text || input.instructions);
221
+ const kind = nonEmptyString(input.kind) || (pathValue ? 'skill' : 'text');
222
+ if (kind === 'skill' && pathValue) {
223
+ return {
224
+ kind: 'skill',
225
+ path: pathValue,
226
+ appliesTo: nonEmptyString(input.appliesTo) || 'tool',
227
+ };
228
+ }
229
+ if (text) {
230
+ return {
231
+ kind: kind === 'markdown' ? 'markdown' : 'text',
232
+ text,
233
+ appliesTo: nonEmptyString(input.appliesTo) || 'tool',
234
+ };
235
+ }
236
+ return undefined;
237
+ }
238
+
239
+ function normalizePolicy(policy) {
240
+ return isObject(policy) ? { ...policy } : undefined;
241
+ }
242
+
243
+ function normalizeOwner(owner) {
244
+ const value = nonEmptyString(owner);
245
+ return value || DEFAULT_TOOL_OWNER;
246
+ }
247
+
248
+ function normalizeDisplay(display) {
249
+ if (!isObject(display)) return undefined;
250
+ const out = {};
251
+ for (const key of ['icon', 'description', 'docsUrl']) {
252
+ const value = nonEmptyString(display[key]);
253
+ if (value) out[key] = value;
254
+ }
255
+ return Object.keys(out).length ? out : undefined;
256
+ }
257
+
258
+ function normalizeSource(type, source = {}) {
259
+ const raw = isObject(source) ? source : {};
260
+ if (type === 'cli') {
261
+ const command = nonEmptyString(raw.command);
262
+ if (!command) throw new Error('CLI tools require source.command');
263
+ return {
264
+ command,
265
+ ...(Array.isArray(raw.args) ? { args: raw.args.map(String) } : {}),
266
+ ...(nonEmptyString(raw.cwd) ? { cwd: raw.cwd.trim() } : {}),
267
+ inputMode: ['json-stdin', 'argv', 'terminal', 'none'].includes(raw.inputMode)
268
+ ? raw.inputMode
269
+ : 'json-stdin',
270
+ outputMode: ['json', 'text'].includes(raw.outputMode) ? raw.outputMode : 'text',
271
+ };
272
+ }
273
+
274
+ if (type === 'mcp') {
275
+ const transport = ['http', 'sse', 'stdio', 'codex-app'].includes(raw.transport)
276
+ ? raw.transport
277
+ : 'http';
278
+ const url = nonEmptyString(raw.url);
279
+ const command = nonEmptyString(raw.command);
280
+ const connectorId = nonEmptyString(raw.connectorId);
281
+ const serverName = nonEmptyString(raw.serverName);
282
+ const toolNamespace = nonEmptyString(raw.toolNamespace);
283
+ if (transport === 'codex-app' && !connectorId && !toolNamespace) {
284
+ throw new Error('Codex app MCP tools require source.connectorId or source.toolNamespace');
285
+ }
286
+ if (transport === 'stdio' && !command) throw new Error('stdio MCP tools require source.command');
287
+ if (transport !== 'stdio' && transport !== 'codex-app' && !url) throw new Error('remote MCP tools require source.url');
288
+ return {
289
+ transport,
290
+ ...(url ? { url } : {}),
291
+ ...(command ? { command } : {}),
292
+ ...(connectorId ? { connectorId } : {}),
293
+ ...(serverName ? { serverName } : {}),
294
+ ...(toolNamespace ? { toolNamespace } : {}),
295
+ ...(Array.isArray(raw.args) ? { args: raw.args.map(String) } : {}),
296
+ ...(nonEmptyString(raw.cwd) ? { cwd: raw.cwd.trim() } : {}),
297
+ };
298
+ }
299
+
300
+ if (type === 'api') {
301
+ const endpoints = Array.isArray(raw.endpoints) ? raw.endpoints : undefined;
302
+ const baseUrl = nonEmptyString(raw.baseUrl);
303
+ const specUrl = nonEmptyString(raw.specUrl);
304
+ const specPath = nonEmptyString(raw.specPath);
305
+ if (!baseUrl && !specUrl && !specPath && !endpoints) {
306
+ throw new Error('API tools require source.baseUrl, source.specUrl, source.specPath, or source.endpoints');
307
+ }
308
+ return {
309
+ ...(baseUrl ? { baseUrl } : {}),
310
+ ...(specUrl ? { specUrl } : {}),
311
+ ...(specPath ? { specPath } : {}),
312
+ ...(endpoints ? { endpoints } : {}),
313
+ };
314
+ }
315
+
316
+ throw new Error(`Unsupported tool type: ${type}`);
317
+ }
318
+
319
+ function defaultDiscoveryMode(type, hasActions) {
320
+ if (hasActions) return 'static';
321
+ if (type === 'cli') return 'single';
322
+ return 'dynamic';
323
+ }
324
+
325
+ function normalizeDiscovery(type, discovery, hasActions) {
326
+ const raw = isObject(discovery) ? discovery : {};
327
+ const mode = DISCOVERY_MODES.has(raw.mode)
328
+ ? raw.mode
329
+ : defaultDiscoveryMode(type, hasActions);
330
+ return {
331
+ mode,
332
+ ...(nonEmptyString(raw.lastDiscoveredAt) ? { lastDiscoveredAt: raw.lastDiscoveredAt.trim() } : {}),
333
+ };
334
+ }
335
+
336
+ function normalizeTool(input, existing) {
337
+ if (!isObject(input)) throw new Error('tool must be an object');
338
+ const type = TOOL_TYPES.has(input.type) ? input.type : null;
339
+ if (!type) throw new Error('tool.type must be mcp, api, or cli');
340
+
341
+ const id = sanitizeId(input.id || input.name);
342
+ const name = nonEmptyString(input.name) || id;
343
+ const createdAt = existing?.createdAt || nowIso();
344
+ const guide = normalizeGuide(
345
+ input.guide || input.skill || input.skillPath || input.guidePath,
346
+ );
347
+ const display = normalizeDisplay(input.display);
348
+ const policy = normalizePolicy(input.policy);
349
+
350
+ return {
351
+ id,
352
+ name,
353
+ type,
354
+ owner: normalizeOwner(input.owner || input.toolOwner || input.ownership || input.policy?.owner),
355
+ origin: TOOL_ORIGINS.has(input.origin) ? input.origin : 'user',
356
+ status: RECORD_STATUSES.has(input.status) ? input.status : 'enabled',
357
+ source: normalizeSource(type, isObject(input.source) ? input.source : input),
358
+ discovery: normalizeDiscovery(type, input.discovery, Array.isArray(input.actions) && input.actions.length > 0),
359
+ ...(display ? { display } : {}),
360
+ ...(policy ? { policy } : {}),
361
+ ...(guide ? { guide } : {}),
362
+ createdAt,
363
+ updatedAt: nowIso(),
364
+ };
365
+ }
366
+
367
+ function normalizeAction(input, tool, existing) {
368
+ if (!isObject(input)) throw new Error('tool action must be an object');
369
+ const name = sanitizeActionName(input.name || input.actionName || 'run');
370
+ const id = sanitizeId(input.id || `${tool.id}.${name}`);
371
+ const createdAt = existing?.createdAt || nowIso();
372
+ const guide = normalizeGuide(
373
+ input.guide || input.skill || input.skillPath || input.guidePath,
374
+ );
375
+ const policy = normalizePolicy(input.policy);
376
+
377
+ return {
378
+ id,
379
+ toolId: tool.id,
380
+ name,
381
+ ...(nonEmptyString(input.displayName) ? { displayName: input.displayName.trim() } : {}),
382
+ ...(nonEmptyString(input.description) ? { description: input.description.trim() } : {}),
383
+ inputSchema: normalizeInputSchema(
384
+ input.inputSchema,
385
+ `Input for ${tool.name} ${name}.`,
386
+ ),
387
+ ...(isObject(input.outputSchema) ? { outputSchema: input.outputSchema } : {}),
388
+ status: RECORD_STATUSES.has(input.status) ? input.status : 'enabled',
389
+ ...(isObject(input.sourceAction) ? { sourceAction: input.sourceAction } : {}),
390
+ ...(policy ? { policy } : {}),
391
+ ...(guide ? { guide } : {}),
392
+ createdAt,
393
+ updatedAt: nowIso(),
394
+ };
395
+ }
396
+
397
+ function defaultCliAction(tool) {
398
+ return normalizeAction({
399
+ id: `${tool.id}.run`,
400
+ name: 'run',
401
+ displayName: 'Run',
402
+ description: `Run ${tool.name} using its registered CLI command.`,
403
+ inputSchema: cliRunInputSchema(tool.source),
404
+ sourceAction: { cliSubcommand: null },
405
+ }, tool);
406
+ }
407
+
408
+ function actionFromMcpTool(tool, mcpTool) {
409
+ return normalizeAction({
410
+ id: `${tool.id}.${mcpTool.name}`,
411
+ name: mcpTool.name,
412
+ displayName: mcpTool.name,
413
+ description: mcpTool.description,
414
+ inputSchema: isObject(mcpTool.inputSchema) ? mcpTool.inputSchema : { type: 'object', properties: {} },
415
+ sourceAction: { mcpToolName: mcpTool.name },
416
+ }, tool);
417
+ }
418
+
419
+ function systemAmalgmTool() {
420
+ return {
421
+ id: 'amalgm',
422
+ name: 'amalgm',
423
+ type: 'mcp',
424
+ owner: DEFAULT_TOOL_OWNER,
425
+ origin: 'system',
426
+ status: 'enabled',
427
+ source: {
428
+ transport: 'http',
429
+ url: `http://127.0.0.1:${PORT}/mcp`,
430
+ },
431
+ discovery: {
432
+ mode: 'dynamic',
433
+ },
434
+ display: {
435
+ icon: '/assets/images/amalgm_logo.svg',
436
+ description: 'Built-in Amalgm tools for tasks, events, agents, artifacts, browser, and notifications.',
437
+ },
438
+ policy: {
439
+ system: true,
440
+ },
441
+ createdAt: nowIso(),
442
+ updatedAt: nowIso(),
443
+ };
444
+ }
445
+
446
+ function seedSystemTools(toolbox) {
447
+ let changed = false;
448
+ const existing = toolbox.tools.amalgm;
449
+ const systemTool = {
450
+ ...systemAmalgmTool(),
451
+ createdAt: existing?.createdAt || nowIso(),
452
+ updatedAt: existing?.updatedAt || nowIso(),
453
+ };
454
+
455
+ if (!existing || JSON.stringify({ ...existing, updatedAt: undefined }) !== JSON.stringify({ ...systemTool, updatedAt: undefined })) {
456
+ toolbox.tools.amalgm = { ...existing, ...systemTool };
457
+ changed = true;
458
+ }
459
+
460
+ let toolsList = [];
461
+ try {
462
+ toolsList = require('../server/core-tools').CORE_TOOLS || [];
463
+ } catch {
464
+ toolsList = [];
465
+ }
466
+
467
+ for (const mcpTool of toolsList) {
468
+ if (!mcpTool?.name) continue;
469
+ const action = actionFromMcpTool(toolbox.tools.amalgm, mcpTool);
470
+ const existingAction = toolbox.toolActions[action.id];
471
+ if (!existingAction || JSON.stringify({
472
+ ...existingAction,
473
+ createdAt: undefined,
474
+ updatedAt: undefined,
475
+ }) !== JSON.stringify({
476
+ ...action,
477
+ createdAt: undefined,
478
+ updatedAt: undefined,
479
+ })) {
480
+ toolbox.toolActions[action.id] = {
481
+ ...existingAction,
482
+ ...action,
483
+ createdAt: existingAction?.createdAt || action.createdAt,
484
+ };
485
+ changed = true;
486
+ }
487
+ }
488
+
489
+ return changed;
490
+ }
491
+
492
+ function readToolboxFromDb() {
493
+ const db = openLocalDb();
494
+ const tools = {};
495
+ const toolActions = {};
496
+
497
+ for (const row of db.prepare('SELECT tool_json FROM tools ORDER BY updated_at DESC, id ASC').all()) {
498
+ const tool = safeJsonParse(row.tool_json);
499
+ if (isObject(tool) && tool.id) tools[tool.id] = tool;
500
+ }
501
+
502
+ for (const row of db.prepare('SELECT action_json FROM tool_actions ORDER BY updated_at DESC, id ASC').all()) {
503
+ const action = safeJsonParse(row.action_json);
504
+ if (isObject(action) && action.id) toolActions[action.id] = action;
505
+ }
506
+
507
+ return normalizeToolbox({ tools, toolActions });
508
+ }
509
+
510
+ function toolboxRowCount(db = openLocalDb()) {
511
+ const row = db.prepare('SELECT COUNT(*) AS count FROM tools').get();
512
+ return Number(row?.count || 0);
513
+ }
514
+
515
+ function upsertToolRow(db, tool) {
516
+ db.prepare(`
517
+ INSERT INTO tools (
518
+ id,
519
+ name,
520
+ type,
521
+ owner,
522
+ origin,
523
+ status,
524
+ updated_at,
525
+ tool_json
526
+ )
527
+ VALUES (@id, @name, @type, @owner, @origin, @status, @updated_at, @tool_json)
528
+ ON CONFLICT(id) DO UPDATE SET
529
+ name = excluded.name,
530
+ type = excluded.type,
531
+ owner = excluded.owner,
532
+ origin = excluded.origin,
533
+ status = excluded.status,
534
+ updated_at = excluded.updated_at,
535
+ tool_json = excluded.tool_json
536
+ `).run({
537
+ id: tool.id,
538
+ name: tool.name,
539
+ type: tool.type,
540
+ owner: tool.owner || DEFAULT_TOOL_OWNER,
541
+ origin: tool.origin || 'user',
542
+ status: tool.status || 'enabled',
543
+ updated_at: tool.updatedAt || nowIso(),
544
+ tool_json: JSON.stringify(tool),
545
+ });
546
+ }
547
+
548
+ function upsertToolActionRow(db, action) {
549
+ db.prepare(`
550
+ INSERT INTO tool_actions (
551
+ id,
552
+ tool_id,
553
+ name,
554
+ status,
555
+ updated_at,
556
+ action_json
557
+ )
558
+ VALUES (@id, @tool_id, @name, @status, @updated_at, @action_json)
559
+ ON CONFLICT(id) DO UPDATE SET
560
+ tool_id = excluded.tool_id,
561
+ name = excluded.name,
562
+ status = excluded.status,
563
+ updated_at = excluded.updated_at,
564
+ action_json = excluded.action_json
565
+ `).run({
566
+ id: action.id,
567
+ tool_id: action.toolId,
568
+ name: action.name,
569
+ status: action.status || 'enabled',
570
+ updated_at: action.updatedAt || nowIso(),
571
+ action_json: JSON.stringify(action),
572
+ });
573
+ }
574
+
575
+ function writeToolboxToDb(toolbox, options = {}) {
576
+ const normalized = normalizeToolbox(toolbox);
577
+ const db = openLocalDb();
578
+ db.transaction(() => {
579
+ if (options.replace) {
580
+ db.prepare('DELETE FROM tool_actions').run();
581
+ db.prepare('DELETE FROM tools').run();
582
+ }
583
+
584
+ for (const tool of Object.values(normalized.tools)) {
585
+ upsertToolRow(db, tool);
586
+ }
587
+
588
+ for (const action of Object.values(normalized.toolActions)) {
589
+ if (!normalized.tools[action.toolId]) continue;
590
+ upsertToolActionRow(db, action);
591
+ }
592
+ })();
593
+ return normalized;
594
+ }
595
+
596
+ function exportToolboxJson(toolbox) {
597
+ ensureDir(TOOLBOX_DIR);
598
+ writeJsonAtomic(TOOLBOX_FILE, normalizeToolbox(toolbox));
599
+ }
600
+
601
+ function migrateLegacyToolboxFileIfNeeded() {
602
+ const db = openLocalDb();
603
+ if (toolboxRowCount(db) > 0) return;
604
+
605
+ const legacyToolbox = dropInheritedHarnessTools(normalizeToolbox(readJson(TOOLBOX_FILE, emptyToolbox())));
606
+ if (Object.keys(legacyToolbox.tools).length === 0 && Object.keys(legacyToolbox.toolActions).length === 0) return;
607
+ writeToolboxToDb(legacyToolbox, { replace: true });
608
+ }
609
+
610
+ function publishCommittedEvents(events) {
611
+ for (const event of events) publishStateEvent(event);
612
+ }
613
+
614
+ function insertToolEvent(db, op, tool, options = {}) {
615
+ return insertStateEvent(db, {
616
+ resource: 'tools',
617
+ op,
618
+ id: tool.id,
619
+ value: tool,
620
+ clientMutationId: options.clientMutationId,
621
+ source: options.source || 'toolbox',
622
+ });
623
+ }
624
+
625
+ function insertToolActionEvent(db, op, action, options = {}) {
626
+ return insertStateEvent(db, {
627
+ resource: 'tool_actions',
628
+ op,
629
+ id: action.id,
630
+ value: action,
631
+ clientMutationId: options.clientMutationId,
632
+ source: options.source || 'toolbox',
633
+ });
634
+ }
635
+
636
+ function readToolbox() {
637
+ migrateLegacyToolboxFileIfNeeded();
638
+ const toolbox = readToolboxFromDb();
639
+ const systemChanged = seedSystemTools(toolbox);
640
+ if (systemChanged) writeToolbox(toolbox);
641
+ return toolbox;
642
+ }
643
+
644
+ function writeToolbox(toolbox) {
645
+ const normalized = writeToolboxToDb(toolbox, { replace: true });
646
+ exportToolboxJson(normalized);
647
+ return normalized;
648
+ }
649
+
650
+ function toolActionsForTool(toolbox, toolId) {
651
+ return Object.values(toolbox.toolActions || {})
652
+ .filter((action) => action.toolId === toolId);
653
+ }
654
+
655
+ function getTool(id, options = {}) {
656
+ const idValue = nonEmptyString(id);
657
+ if (!idValue) throw new Error('id is required');
658
+ const toolbox = readToolbox();
659
+ const toolId = sanitizeId(idValue);
660
+ const tool = toolbox.tools[toolId];
661
+ if (!tool) throw new Error(`Unknown tool: ${toolId}`);
662
+ return {
663
+ tool,
664
+ ...(options.includeActions === false ? {} : { actions: toolActionsForTool(toolbox, toolId) }),
665
+ };
666
+ }
667
+
668
+ function getToolAction(id) {
669
+ const idValue = nonEmptyString(id);
670
+ if (!idValue) throw new Error('id is required');
671
+ const toolbox = readToolbox();
672
+ const actionId = sanitizeId(idValue);
673
+ const action = toolbox.toolActions[actionId];
674
+ if (!action) throw new Error(`Unknown tool action: ${actionId}`);
675
+ return { action };
676
+ }
677
+
678
+ function upsertTool(input) {
679
+ const toolbox = readToolbox();
680
+ const existing = input?.id ? toolbox.tools[sanitizeId(input.id)] : null;
681
+ const existingActionIds = new Set(Object.keys(toolbox.toolActions));
682
+ const tool = normalizeTool(input, existing);
683
+ toolbox.tools[tool.id] = tool;
684
+
685
+ const actions = [];
686
+ if (Array.isArray(input.actions) && input.actions.length > 0) {
687
+ for (const actionInput of input.actions) {
688
+ const action = normalizeAction(actionInput, tool, actionInput?.id ? toolbox.toolActions[sanitizeId(actionInput.id)] : null);
689
+ toolbox.toolActions[action.id] = action;
690
+ actions.push(action);
691
+ }
692
+ } else {
693
+ const existingToolActions = Object.values(toolbox.toolActions)
694
+ .filter((action) => action.toolId === tool.id);
695
+ const defaultActionId = `${tool.id}.run`;
696
+ if (tool.type === 'cli' && (existingToolActions.length === 0 || toolbox.toolActions[defaultActionId])) {
697
+ const defaultAction = defaultCliAction(tool);
698
+ const action = {
699
+ ...defaultAction,
700
+ ...(toolbox.toolActions[defaultActionId] || {}),
701
+ inputSchema: defaultAction.inputSchema,
702
+ sourceAction: defaultAction.sourceAction,
703
+ updatedAt: defaultAction.updatedAt,
704
+ createdAt: toolbox.toolActions[defaultActionId]?.createdAt || nowIso(),
705
+ };
706
+ toolbox.toolActions[action.id] = action;
707
+ actions.push(action);
708
+ }
709
+ }
710
+
711
+ const db = openLocalDb();
712
+ const events = db.transaction(() => {
713
+ const inserted = [];
714
+ upsertToolRow(db, tool);
715
+ inserted.push(insertToolEvent(db, existing ? 'update' : 'insert', tool, {
716
+ clientMutationId: input.clientMutationId,
717
+ source: 'toolbox:upsert-tool',
718
+ }));
719
+ for (const action of actions) {
720
+ upsertToolActionRow(db, action);
721
+ inserted.push(insertToolActionEvent(db, existingActionIds.has(action.id) ? 'update' : 'insert', action, {
722
+ clientMutationId: input.clientMutationId,
723
+ source: 'toolbox:upsert-tool',
724
+ }));
725
+ }
726
+ return inserted;
727
+ })();
728
+ publishCommittedEvents(events);
729
+ return { tool, actions };
730
+ }
731
+
732
+ function sourcePatchFromToolInput(input) {
733
+ const patch = isObject(input?.source) ? { ...input.source } : {};
734
+ for (const key of [
735
+ 'command',
736
+ 'args',
737
+ 'cwd',
738
+ 'inputMode',
739
+ 'outputMode',
740
+ 'transport',
741
+ 'url',
742
+ 'connectorId',
743
+ 'serverName',
744
+ 'toolNamespace',
745
+ 'baseUrl',
746
+ 'specUrl',
747
+ 'specPath',
748
+ 'endpoints',
749
+ ]) {
750
+ if (hasOwn(input, key)) patch[key] = input[key];
751
+ }
752
+ return Object.keys(patch).length ? patch : null;
753
+ }
754
+
755
+ function updateTool(input) {
756
+ if (!isObject(input)) throw new Error('tool update must be an object');
757
+ const idValue = nonEmptyString(input.id || input.toolId);
758
+ if (!idValue) throw new Error('id is required');
759
+
760
+ const toolbox = readToolbox();
761
+ const toolId = sanitizeId(idValue);
762
+ const existing = toolbox.tools[toolId];
763
+ if (!existing) throw new Error(`Unknown tool: ${toolId}`);
764
+ if (existing.origin === 'system') throw new Error('System tools cannot be modified');
765
+
766
+ const sourcePatch = sourcePatchFromToolInput(input);
767
+ const guideKeys = ['guide', 'skill', 'skillPath', 'guidePath'];
768
+ const nextInput = {
769
+ ...existing,
770
+ ...input,
771
+ id: existing.id,
772
+ name: hasOwn(input, 'name') ? input.name : existing.name,
773
+ type: hasOwn(input, 'type') ? input.type : existing.type,
774
+ owner: hasAnyOwn(input, ['owner', 'toolOwner', 'ownership'])
775
+ ? (input.owner || input.toolOwner || input.ownership)
776
+ : existing.owner,
777
+ origin: hasOwn(input, 'origin') ? input.origin : existing.origin,
778
+ status: hasOwn(input, 'status') ? input.status : existing.status,
779
+ source: sourcePatch ? { ...(isObject(existing.source) ? existing.source : {}), ...sourcePatch } : existing.source,
780
+ discovery: hasOwn(input, 'discovery') && isObject(input.discovery)
781
+ ? { ...(isObject(existing.discovery) ? existing.discovery : {}), ...input.discovery }
782
+ : existing.discovery,
783
+ display: hasOwn(input, 'display')
784
+ ? (input.display === null ? undefined : { ...(isObject(existing.display) ? existing.display : {}), ...(isObject(input.display) ? input.display : {}) })
785
+ : existing.display,
786
+ policy: hasOwn(input, 'policy')
787
+ ? (input.policy === null ? undefined : { ...(isObject(existing.policy) ? existing.policy : {}), ...(isObject(input.policy) ? input.policy : {}) })
788
+ : existing.policy,
789
+ guide: hasAnyOwn(input, guideKeys)
790
+ ? (input.guide || input.skill || input.skillPath || input.guidePath)
791
+ : existing.guide,
792
+ };
793
+
794
+ const tool = normalizeTool(nextInput, existing);
795
+ toolbox.tools[tool.id] = tool;
796
+ const db = openLocalDb();
797
+ const events = db.transaction(() => {
798
+ upsertToolRow(db, tool);
799
+ return [insertToolEvent(db, 'update', tool, {
800
+ clientMutationId: input.clientMutationId,
801
+ source: 'toolbox:update-tool',
802
+ })];
803
+ })();
804
+ publishCommittedEvents(events);
805
+ return { tool, actions: toolActionsForTool(toolbox, tool.id) };
806
+ }
807
+
808
+ function upsertToolAction(input) {
809
+ const toolbox = readToolbox();
810
+ const toolId = sanitizeId(input?.toolId || input?.providerId || input?.tool);
811
+ const tool = toolbox.tools[toolId];
812
+ if (!tool) throw new Error(`Unknown tool: ${toolId}`);
813
+ const existing = input?.id ? toolbox.toolActions[sanitizeId(input.id)] : null;
814
+ const action = normalizeAction(input, tool, existing);
815
+ toolbox.toolActions[action.id] = action;
816
+ const db = openLocalDb();
817
+ const events = db.transaction(() => {
818
+ upsertToolActionRow(db, action);
819
+ return [insertToolActionEvent(db, existing ? 'update' : 'insert', action, {
820
+ clientMutationId: input.clientMutationId,
821
+ source: 'toolbox:upsert-action',
822
+ })];
823
+ })();
824
+ publishCommittedEvents(events);
825
+ return action;
826
+ }
827
+
828
+ function updateToolAction(input) {
829
+ if (!isObject(input)) throw new Error('tool action update must be an object');
830
+ const idValue = nonEmptyString(input.id || input.actionId);
831
+ if (!idValue) throw new Error('id is required');
832
+
833
+ const toolbox = readToolbox();
834
+ const actionId = sanitizeId(idValue);
835
+ const existing = toolbox.toolActions[actionId];
836
+ if (!existing) throw new Error(`Unknown tool action: ${actionId}`);
837
+ const tool = toolbox.tools[existing.toolId];
838
+ if (!tool) throw new Error(`Unknown tool: ${existing.toolId}`);
839
+ if (tool.origin === 'system') throw new Error('System tool actions cannot be modified');
840
+
841
+ const guideKeys = ['guide', 'skill', 'skillPath', 'guidePath'];
842
+ const nextInput = {
843
+ ...existing,
844
+ ...input,
845
+ id: existing.id,
846
+ toolId: existing.toolId,
847
+ name: hasOwn(input, 'name') ? input.name : existing.name,
848
+ displayName: hasOwn(input, 'displayName') ? input.displayName : existing.displayName,
849
+ description: hasOwn(input, 'description') ? input.description : existing.description,
850
+ inputSchema: hasOwn(input, 'inputSchema') ? input.inputSchema : existing.inputSchema,
851
+ outputSchema: hasOwn(input, 'outputSchema') ? input.outputSchema : existing.outputSchema,
852
+ status: hasOwn(input, 'status') ? input.status : existing.status,
853
+ sourceAction: hasOwn(input, 'sourceAction')
854
+ ? (input.sourceAction === null ? undefined : { ...(isObject(existing.sourceAction) ? existing.sourceAction : {}), ...(isObject(input.sourceAction) ? input.sourceAction : {}) })
855
+ : existing.sourceAction,
856
+ policy: hasOwn(input, 'policy')
857
+ ? (input.policy === null ? undefined : { ...(isObject(existing.policy) ? existing.policy : {}), ...(isObject(input.policy) ? input.policy : {}) })
858
+ : existing.policy,
859
+ guide: hasAnyOwn(input, guideKeys)
860
+ ? (input.guide || input.skill || input.skillPath || input.guidePath)
861
+ : existing.guide,
862
+ };
863
+
864
+ const action = normalizeAction(nextInput, tool, existing);
865
+ toolbox.toolActions[action.id] = action;
866
+ const db = openLocalDb();
867
+ const events = db.transaction(() => {
868
+ upsertToolActionRow(db, action);
869
+ return [insertToolActionEvent(db, 'update', action, {
870
+ clientMutationId: input.clientMutationId,
871
+ source: 'toolbox:update-action',
872
+ })];
873
+ })();
874
+ publishCommittedEvents(events);
875
+ return action;
876
+ }
877
+
878
+ function deleteTool(id, options = {}) {
879
+ const toolbox = readToolbox();
880
+ const toolId = sanitizeId(id);
881
+ const tool = toolbox.tools[toolId];
882
+ if (!tool) return { ok: true, deleted: false };
883
+ if (tool.origin === 'system') throw new Error('System tools cannot be deleted');
884
+ const actions = Object.values(toolbox.toolActions).filter((action) => action.toolId === toolId);
885
+ const db = openLocalDb();
886
+ const events = db.transaction(() => {
887
+ const inserted = actions.map((action) => insertToolActionEvent(db, 'delete', action, {
888
+ clientMutationId: options.clientMutationId,
889
+ source: 'toolbox:delete-tool',
890
+ }));
891
+ db.prepare('DELETE FROM tool_actions WHERE tool_id = ?').run(toolId);
892
+ db.prepare('DELETE FROM tools WHERE id = ?').run(toolId);
893
+ inserted.push(insertToolEvent(db, 'delete', tool, {
894
+ clientMutationId: options.clientMutationId,
895
+ source: 'toolbox:delete-tool',
896
+ }));
897
+ return inserted;
898
+ })();
899
+ publishCommittedEvents(events);
900
+ return { ok: true, deleted: true };
901
+ }
902
+
903
+ function deleteToolAction(id, options = {}) {
904
+ const toolbox = readToolbox();
905
+ const actionId = sanitizeId(id);
906
+ const action = toolbox.toolActions[actionId];
907
+ if (!action) return { ok: true, deleted: false };
908
+ const tool = toolbox.tools[action.toolId];
909
+ if (tool?.origin === 'system') throw new Error('System tool actions cannot be deleted');
910
+ const db = openLocalDb();
911
+ const events = db.transaction(() => {
912
+ db.prepare('DELETE FROM tool_actions WHERE id = ?').run(actionId);
913
+ return [insertToolActionEvent(db, 'delete', action, {
914
+ clientMutationId: options.clientMutationId,
915
+ source: 'toolbox:delete-action',
916
+ })];
917
+ })();
918
+ publishCommittedEvents(events);
919
+ return { ok: true, deleted: true };
920
+ }
921
+
922
+ module.exports = {
923
+ TOOLBOX_FILE,
924
+ deleteTool,
925
+ deleteToolAction,
926
+ getTool,
927
+ getToolAction,
928
+ readToolbox,
929
+ updateTool,
930
+ updateToolAction,
931
+ upsertTool,
932
+ upsertToolAction,
933
+ };