amalgm 0.0.1 → 0.0.33

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 (34) hide show
  1. package/README.md +4 -0
  2. package/lib/cli.js +133 -1
  3. package/lib/supervisor.js +10 -1
  4. package/package.json +2 -1
  5. package/runtime/lib/chatInput.js +9 -0
  6. package/runtime/lib/local/amalgmStore.js +10 -2
  7. package/runtime/scripts/amalgm-mcp/agents/rest.js +60 -81
  8. package/runtime/scripts/amalgm-mcp/agents/store.js +589 -58
  9. package/runtime/scripts/amalgm-mcp/agents/talk.js +10 -4
  10. package/runtime/scripts/amalgm-mcp/agents/tools.js +12 -1
  11. package/runtime/scripts/amalgm-mcp/artifacts/store.js +19 -3
  12. package/runtime/scripts/amalgm-mcp/config.js +2 -0
  13. package/runtime/scripts/amalgm-mcp/events/store.js +16 -1
  14. package/runtime/scripts/amalgm-mcp/lib/prefs.js +85 -37
  15. package/runtime/scripts/amalgm-mcp/local/rest.js +7 -0
  16. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +83 -0
  17. package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
  18. package/runtime/scripts/amalgm-mcp/server/http.js +42 -0
  19. package/runtime/scripts/amalgm-mcp/server/mcp.js +28 -17
  20. package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
  21. package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
  22. package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
  23. package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
  24. package/runtime/scripts/amalgm-mcp/tasks/store.js +16 -1
  25. package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
  26. package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
  27. package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
  28. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
  29. package/runtime/scripts/amalgm-mcp/workspace/rest.js +116 -8
  30. package/runtime/scripts/chat-core/adapters/claude.js +2 -0
  31. package/runtime/scripts/chat-core/contract.js +2 -0
  32. package/runtime/scripts/chat-core/engine.js +77 -19
  33. package/runtime/scripts/credential-adapter.js +4 -2
  34. package/runtime/scripts/local-gateway.js +2 -0
@@ -24,6 +24,7 @@ const {
24
24
  } = require('../lib/supabase');
25
25
  const {
26
26
  hydrateModelPreferences,
27
+ getPreferredAuthMethod,
27
28
  getSelectedModel,
28
29
  resolveModelSelection,
29
30
  DEFAULT_SELECTED_MODELS,
@@ -548,16 +549,20 @@ async function handleTalkToAgent(args, context = {}) {
548
549
  // Chat-server agent ID derives from the agent's base harness.
549
550
  const agentIdMap = { claude_code: 'claude', codex: 'codex', opencode: 'opencode', pi: 'pi', amp: 'amp', cursor: 'cursor' };
550
551
  const chatAgentId = agentIdMap[agent.baseHarnessId] || 'claude';
552
+ const persistedAuthMethod = credentialAdapter.VALID_HARNESS_IDS.includes(agent.baseHarnessId)
553
+ ? credentialAdapter.getPersistedAuthMode(agent.baseHarnessId)
554
+ : 'amalgm';
551
555
  const defaultAuthMethod = coerceAuthMethodForHarness(
552
556
  agent.baseHarnessId,
553
557
  agent.authMethod
554
- || (credentialAdapter.VALID_HARNESS_IDS.includes(agent.baseHarnessId)
555
- ? credentialAdapter.getPersistedAuthMode(agent.baseHarnessId)
556
- : 'amalgm'),
558
+ || getPreferredAuthMethod(agent.baseHarnessId, persistedAuthMethod),
557
559
  );
558
560
  const agentFiles = normalizeStringList(agent.files);
559
561
  const agentSkills = normalizeStringList(agent.skills);
560
- const agentMcpAppIds = normalizeStringList(agent.mcp?.appIds ?? agent.mcpAppIds);
562
+ const agentTools = agent.tools || { mode: 'all', selectedToolIds: [] };
563
+ const agentMcpAppIds = agentTools.mode === 'selected'
564
+ ? normalizeStringList(agentTools.selectedToolIds)
565
+ : normalizeStringList(agent.mcp?.appIds ?? agent.mcpAppIds);
561
566
  const baseAgentSystemPrompt = buildAgentInstructions(
562
567
  agent.systemPrompt,
563
568
  agentFiles,
@@ -734,6 +739,7 @@ async function handleTalkToAgent(args, context = {}) {
734
739
  files: agentFiles,
735
740
  skills: agentSkills,
736
741
  mcpAppIds: agentMcpAppIds,
742
+ tools: agentTools,
737
743
  ...(parentOrigin || {}),
738
744
  },
739
745
  });
@@ -39,7 +39,7 @@ module.exports = [
39
39
  : a.baseModelId ||
40
40
  getSelectedModel(a.baseHarnessId) ||
41
41
  DEFAULT_SELECTED_MODELS[a.baseHarnessId];
42
- return `- ${a.name} (${a.id}) ${tag}\n ${a.description || 'No description'}\n harness: ${a.baseHarnessId} | model: ${model}`;
42
+ return `- ${a.name} (${a.id}) ${tag}\n ${a.description || 'No description'}\n adapter: ${a.adapter || a.baseHarnessId} | harness: ${a.baseHarnessId} | model: ${model}\n location: ${a.locationName || a.location?.name || 'this computer'} | status: ${a.status || 'unknown'}`;
43
43
  })
44
44
  .join('\n\n');
45
45
  return textResult(`Available agents:\n\n${summary}`);
@@ -61,6 +61,7 @@ module.exports = [
61
61
  id: agent.id,
62
62
  name: agent.name,
63
63
  description: agent.description,
64
+ adapter: agent.adapter || agent.baseHarnessId,
64
65
  baseHarnessId: agent.baseHarnessId,
65
66
  baseModelId: agent.builtin
66
67
  ? getSelectedModel(agent.baseHarnessId) || agent.baseModelId
@@ -72,7 +73,17 @@ module.exports = [
72
73
  inheritAll: agent.mcp?.inheritAll ?? true,
73
74
  customServerCount: agent.mcp?.customServers?.length ?? 0,
74
75
  },
76
+ tools: agent.tools || { mode: 'all', selectedToolIds: [] },
77
+ location: agent.location || null,
78
+ locationName: agent.locationName || null,
79
+ ownerComputerId: agent.ownerComputerId || null,
80
+ status: agent.status || 'unknown',
81
+ installStatus: agent.installStatus || 'unknown',
82
+ authStatus: agent.authStatus || 'unknown',
83
+ capabilities: agent.capabilities || {},
75
84
  builtin: !!agent.builtin,
85
+ deletable: agent.deletable !== false,
86
+ editable: agent.editable !== false,
76
87
  };
77
88
  return textResult(JSON.stringify(safe, null, 2));
78
89
  },
@@ -15,6 +15,7 @@ const {
15
15
  ARTIFACT_PORT_MAX,
16
16
  } = require('../config');
17
17
  const { ensureDir, readJson, writeJsonAtomic } = require('../lib/storage');
18
+ const { appendStateEvent } = require('../state/events');
18
19
 
19
20
  function ensureArtifactsDirs() {
20
21
  ensureDir(ARTIFACTS_DIR);
@@ -66,12 +67,27 @@ function loadArtifacts() {
66
67
  return { version: 1, artifacts };
67
68
  }
68
69
 
69
- function saveArtifacts(data) {
70
+ function publishArtifactsChange(data, source = 'artifacts') {
71
+ try {
72
+ appendStateEvent({
73
+ resource: 'artifacts',
74
+ op: 'replace',
75
+ value: Array.isArray(data?.artifacts) ? data.artifacts.map(normalizeArtifact) : [],
76
+ source,
77
+ });
78
+ } catch (error) {
79
+ console.warn('[Artifacts] Local Live Store publish failed:', error.message);
80
+ }
81
+ }
82
+
83
+ function saveArtifacts(data, options = {}) {
70
84
  ensureDir(ARTIFACTS_DIR);
71
- writeJsonAtomic(ARTIFACTS_FILE, {
85
+ const normalizedData = {
72
86
  version: 1,
73
87
  artifacts: Array.isArray(data.artifacts) ? data.artifacts.map(normalizeArtifact) : [],
74
- });
88
+ };
89
+ writeJsonAtomic(ARTIFACTS_FILE, normalizedData);
90
+ publishArtifactsChange(normalizedData, options.source || 'artifacts:save');
75
91
  }
76
92
 
77
93
  function updateArtifactMeta(artifactId, updates) {
@@ -22,6 +22,7 @@ const AMALGM_DIR = process.env.AMALGM_DIR || path.join(os.homedir(), '.amalgm');
22
22
 
23
23
  // Storage file/dir layout under AMALGM_DIR.
24
24
  const STORAGE_DIR = AMALGM_DIR; // tasks.json etc. live directly under ~/.amalgm
25
+ const LOCAL_DB_FILE = path.join(STORAGE_DIR, 'amalgm.db');
25
26
  const TASKS_FILE = path.join(STORAGE_DIR, 'tasks.json');
26
27
  const AGENTS_FILE = path.join(STORAGE_DIR, 'agents.json');
27
28
  const ARTIFACTS_DIR = path.join(STORAGE_DIR, 'artifacts');
@@ -110,6 +111,7 @@ module.exports = {
110
111
  MCP_PROTOCOL_VERSION,
111
112
  AMALGM_DIR,
112
113
  STORAGE_DIR,
114
+ LOCAL_DB_FILE,
113
115
  TASKS_FILE,
114
116
  AGENTS_FILE,
115
117
  ARTIFACTS_DIR,
@@ -11,6 +11,7 @@ const {
11
11
  } = require('../../../lib/chatInput');
12
12
  const { DEFAULT_SELECTED_MODELS, getSelectedModel } = require('../lib/prefs');
13
13
  const credentialAdapter = require('../../credential-adapter');
14
+ const { appendStateEvent } = require('../state/events');
14
15
 
15
16
  function normalizeStoredTrigger(trigger) {
16
17
  const harness =
@@ -91,8 +92,22 @@ function loadEventTriggers() {
91
92
  return migrated.data;
92
93
  }
93
94
 
94
- function saveEventTriggers(data) {
95
+ function publishEventTriggersChange(data, source = 'event-triggers') {
96
+ try {
97
+ appendStateEvent({
98
+ resource: 'event_triggers',
99
+ op: 'replace',
100
+ value: Array.isArray(data?.triggers) ? data.triggers : [],
101
+ source,
102
+ });
103
+ } catch (error) {
104
+ console.warn('[Events] Local Live Store publish failed:', error.message);
105
+ }
106
+ }
107
+
108
+ function saveEventTriggers(data, options = {}) {
95
109
  writeJsonAtomic(EVENT_TRIGGERS_FILE, data);
110
+ publishEventTriggersChange(data, options.source || 'event-triggers:save');
96
111
  }
97
112
 
98
113
  module.exports = { ensureTriggersDirs, loadEventTriggers, saveEventTriggers };
@@ -17,6 +17,24 @@ const DEFAULT_SELECTED_MODELS = {
17
17
  cursor: 'cursor/composer-2-fast',
18
18
  };
19
19
 
20
+ const DEFAULT_AUTH_METHODS = {
21
+ claude_code: 'amalgm',
22
+ codex: 'amalgm',
23
+ opencode: 'amalgm',
24
+ pi: 'amalgm',
25
+ amp: 'provider_auth',
26
+ cursor: 'provider_auth',
27
+ };
28
+
29
+ const SUPPORTED_AUTH_BY_HARNESS = {
30
+ claude_code: ['amalgm', 'byok', 'provider_auth'],
31
+ codex: ['amalgm', 'byok', 'provider_auth'],
32
+ opencode: ['amalgm', 'byok'],
33
+ pi: ['amalgm', 'byok', 'provider_auth'],
34
+ amp: ['provider_auth'],
35
+ cursor: ['provider_auth'],
36
+ };
37
+
20
38
  const GATEWAY_PROVIDER_PREFIXES = [
21
39
  'prime-intellect', 'arcee-ai',
22
40
  'alibaba', 'amazon', 'anthropic', 'bytedance', 'cohere', 'deepseek',
@@ -107,9 +125,18 @@ const CURSOR_MODEL_ALIASES = {
107
125
  };
108
126
 
109
127
  const _selectedModels = { ...DEFAULT_SELECTED_MODELS };
128
+ const _authMethods = {};
110
129
  let _pinnedHarness = null;
111
130
  let _pinnedModel = null;
112
131
  let _prefsHydrated = false;
132
+ let _prefsHydratePromise = null;
133
+
134
+ function coerceAuthMethodForHarness(harnessId, authMethod) {
135
+ const supported = SUPPORTED_AUTH_BY_HARNESS[harnessId] || ['amalgm'];
136
+ return supported.includes(authMethod)
137
+ ? authMethod
138
+ : DEFAULT_AUTH_METHODS[harnessId] || supported[0] || 'amalgm';
139
+ }
113
140
 
114
141
  function cleanModelId(value) {
115
142
  return String(value || '').trim().replace(/^vercel\//i, '').toLowerCase();
@@ -319,47 +346,60 @@ function resolveCliModel(harnessId, modelId) {
319
346
  }
320
347
 
321
348
  async function hydrateModelPreferences() {
349
+ if (_prefsHydratePromise) return _prefsHydratePromise;
322
350
  if (_prefsHydrated || !hasSupabase()) return;
323
- _prefsHydrated = true;
324
- try {
325
- const rows = await supabaseSelect(
326
- 'user_preferences',
327
- `user_id=eq.${AMALGM_USER_ID}&select=preferences`,
328
- );
329
- if (!rows) return;
330
- const preferences = rows?.[0]?.preferences;
331
- const recentPrefs = preferences?.recent_prefs;
332
- if (recentPrefs && typeof recentPrefs === 'object') {
333
- for (const [h, m] of Object.entries(recentPrefs)) {
334
- if (m) {
335
- const normalized = normalizeModelSelection(h, m).modelId || m;
336
- if (_selectedModels[h] !== normalized) _selectedModels[h] = normalized;
351
+ _prefsHydratePromise = (async () => {
352
+ _prefsHydrated = true;
353
+ try {
354
+ const rows = await supabaseSelect(
355
+ 'user_preferences',
356
+ `user_id=eq.${AMALGM_USER_ID}&select=preferences`,
357
+ );
358
+ if (!rows) return;
359
+ const preferences = rows?.[0]?.preferences;
360
+ const recentPrefs = preferences?.recent_prefs;
361
+ if (recentPrefs && typeof recentPrefs === 'object') {
362
+ for (const [h, m] of Object.entries(recentPrefs)) {
363
+ if (m) {
364
+ const normalized = normalizeModelSelection(h, m).modelId || m;
365
+ if (_selectedModels[h] !== normalized) _selectedModels[h] = normalized;
366
+ }
337
367
  }
338
368
  }
369
+ const usagePrefs = preferences?.usage;
370
+ if (usagePrefs && typeof usagePrefs === 'object') {
371
+ for (const [h, method] of Object.entries(usagePrefs)) {
372
+ if (method) _authMethods[h] = coerceAuthMethodForHarness(h, method);
373
+ }
374
+ }
375
+ _pinnedHarness = preferences?.autopilot?.harness || null;
376
+ const rawPinnedModel = preferences?.autopilot?.model || null;
377
+ if (rawPinnedModel) {
378
+ const harnessForPinned =
379
+ _pinnedHarness ||
380
+ inferHarnessForModel(rawPinnedModel, preferences?.last_used?.harness || null);
381
+ _pinnedModel = harnessForPinned
382
+ ? normalizeModelSelection(harnessForPinned, rawPinnedModel).modelId || rawPinnedModel
383
+ : rawPinnedModel;
384
+ } else {
385
+ _pinnedModel = null;
386
+ }
387
+ console.log(
388
+ '[AmalgmMCP] Hydrated user prefs:',
389
+ JSON.stringify({
390
+ selectedModels: _selectedModels,
391
+ authMethods: _authMethods,
392
+ pinnedHarness: _pinnedHarness,
393
+ pinnedModel: _pinnedModel,
394
+ }),
395
+ );
396
+ } catch (err) {
397
+ console.warn('[AmalgmMCP] Failed to hydrate user prefs:', err.message);
398
+ } finally {
399
+ _prefsHydratePromise = null;
339
400
  }
340
- _pinnedHarness = preferences?.autopilot?.harness || null;
341
- const rawPinnedModel = preferences?.autopilot?.model || null;
342
- if (rawPinnedModel) {
343
- const harnessForPinned =
344
- _pinnedHarness ||
345
- inferHarnessForModel(rawPinnedModel, preferences?.last_used?.harness || null);
346
- _pinnedModel = harnessForPinned
347
- ? normalizeModelSelection(harnessForPinned, rawPinnedModel).modelId || rawPinnedModel
348
- : rawPinnedModel;
349
- } else {
350
- _pinnedModel = null;
351
- }
352
- console.log(
353
- '[AmalgmMCP] Hydrated user prefs:',
354
- JSON.stringify({
355
- selectedModels: _selectedModels,
356
- pinnedHarness: _pinnedHarness,
357
- pinnedModel: _pinnedModel,
358
- }),
359
- );
360
- } catch (err) {
361
- console.warn('[AmalgmMCP] Failed to hydrate user prefs:', err.message);
362
- }
401
+ })();
402
+ return _prefsHydratePromise;
363
403
  }
364
404
 
365
405
  function getSelectedModel(harnessId) {
@@ -367,6 +407,13 @@ function getSelectedModel(harnessId) {
367
407
  return _selectedModels[harnessId] || DEFAULT_SELECTED_MODELS[harnessId] || null;
368
408
  }
369
409
 
410
+ function getPreferredAuthMethod(harnessId, fallbackAuthMethod) {
411
+ if (Object.prototype.hasOwnProperty.call(_authMethods, harnessId)) {
412
+ return coerceAuthMethodForHarness(harnessId, _authMethods[harnessId]);
413
+ }
414
+ return coerceAuthMethodForHarness(harnessId, fallbackAuthMethod);
415
+ }
416
+
370
417
  function resolveModelSelection(harnessId, uiModelId) {
371
418
  const selectedModelId =
372
419
  uiModelId ||
@@ -389,5 +436,6 @@ module.exports = {
389
436
  DEFAULT_SELECTED_MODELS,
390
437
  hydrateModelPreferences,
391
438
  getSelectedModel,
439
+ getPreferredAuthMethod,
392
440
  resolveModelSelection,
393
441
  };
@@ -9,9 +9,11 @@ const {
9
9
  } = require('../../../lib/local/credentialResolver.js');
10
10
  const {
11
11
  ensureAmalgmDir,
12
+ getAmalgmWorkspaceRoot,
12
13
  getHomeDir,
13
14
  writeResource,
14
15
  } = require('../../../lib/local/amalgmStore.js');
16
+ const { syncDiscoveredAgents } = require('../agents/store');
15
17
 
16
18
  function cacheDiscovery(agents) {
17
19
  writeResource('agents/discovery.json', {
@@ -26,12 +28,14 @@ async function handleInit(sendJson) {
26
28
  const { created, path: amalgmPath } = ensureAmalgmDir();
27
29
  const agents = discoverAgents();
28
30
  cacheDiscovery(agents);
31
+ syncDiscoveredAgents(agents);
29
32
 
30
33
  sendJson(200, {
31
34
  ok: true,
32
35
  created,
33
36
  amalgmPath,
34
37
  homedir: getHomeDir(),
38
+ workspaceRoot: getAmalgmWorkspaceRoot(),
35
39
  agents,
36
40
  });
37
41
  } catch (error) {
@@ -43,11 +47,14 @@ async function handleInit(sendJson) {
43
47
 
44
48
  async function handleCredentials(sendJson) {
45
49
  try {
50
+ ensureAmalgmDir();
46
51
  const agents = discoverAgents();
47
52
  cacheDiscovery(agents);
53
+ syncDiscoveredAgents(agents);
48
54
  sendJson(200, {
49
55
  agents,
50
56
  homedir: getHomeDir(),
57
+ workspaceRoot: getAmalgmWorkspaceRoot(),
51
58
  });
52
59
  } catch (error) {
53
60
  sendJson(500, {
@@ -7,9 +7,40 @@ const fs = require('fs');
7
7
  const os = require('os');
8
8
  const path = require('path');
9
9
  const { AMALGM_DIR } = require('../config');
10
+ const { deleteTool, upsertTool } = require('../toolbox/store');
10
11
 
11
12
  const CONNECTIONS_FILE = path.join(AMALGM_DIR, 'mcp-connections.json');
12
13
  const CLAUDE_FILE = path.join(os.homedir(), '.claude.json');
14
+ let mcpRegistry = null;
15
+ let mcpRegistryLoadAttempted = false;
16
+ let warnedRegistryLoadFailure = false;
17
+
18
+ function getMcpRegistry() {
19
+ if (mcpRegistry) return mcpRegistry;
20
+ if (mcpRegistryLoadAttempted) return null;
21
+ mcpRegistryLoadAttempted = true;
22
+
23
+ const candidates = [
24
+ path.join(__dirname, '../../../lib/mcpApps/registry.ts'),
25
+ path.join(__dirname, '../../../../web/lib/mcpApps/registry.ts'),
26
+ ];
27
+
28
+ for (const candidate of candidates) {
29
+ if (!fs.existsSync(candidate)) continue;
30
+ try {
31
+ require('tsx/cjs');
32
+ mcpRegistry = require(candidate);
33
+ return mcpRegistry;
34
+ } catch (err) {
35
+ if (!warnedRegistryLoadFailure) {
36
+ warnedRegistryLoadFailure = true;
37
+ console.warn('[mcp-connections] MCP registry metadata unavailable:', err?.message || String(err));
38
+ }
39
+ }
40
+ }
41
+
42
+ return null;
43
+ }
13
44
 
14
45
  function emptyFile() {
15
46
  return { version: 1, connections: {} };
@@ -47,6 +78,7 @@ function sanitizeConnections(connections) {
47
78
  sanitized[appId] = {
48
79
  enabled: !!conn.enabled,
49
80
  authType: conn.authType,
81
+ owner: conn.owner || 'amalgm',
50
82
  instanceUrl: conn.instanceUrl,
51
83
  config: conn.config,
52
84
  };
@@ -71,6 +103,53 @@ function readNativeMcps() {
71
103
  }
72
104
  }
73
105
 
106
+ function mirrorConnectionToToolbox(appId, conn) {
107
+ const registry = getMcpRegistry();
108
+ const app = registry?.getMcpApp?.(appId);
109
+ const config = app
110
+ ? registry.buildSessionConfig(
111
+ app,
112
+ undefined,
113
+ { instanceUrl: conn?.instanceUrl, config: conn?.config },
114
+ )
115
+ : {
116
+ type: conn?.transport || conn?.config?.transport || 'http',
117
+ url: conn?.instanceUrl || conn?.config?.url,
118
+ };
119
+
120
+ upsertTool({
121
+ id: app?.id || appId,
122
+ name: app?.name || appId,
123
+ type: 'mcp',
124
+ owner: conn?.owner || 'amalgm',
125
+ origin: 'catalog',
126
+ status: conn?.enabled === false ? 'disabled' : 'enabled',
127
+ source: {
128
+ transport: config.type,
129
+ url: config.url,
130
+ },
131
+ discovery: { mode: 'dynamic' },
132
+ display: {
133
+ icon: app?.iconUrl,
134
+ description: app?.description,
135
+ docsUrl: app?.docsUrl,
136
+ },
137
+ policy: {
138
+ connector: true,
139
+ authType: app?.authType || conn?.authType,
140
+ },
141
+ });
142
+ }
143
+
144
+ function removeConnectionFromToolbox(appId) {
145
+ try {
146
+ deleteTool(appId);
147
+ } catch {
148
+ // The toolbox mirror is best-effort; MCP connection removal should still
149
+ // succeed even if a future system connector cannot be deleted.
150
+ }
151
+ }
152
+
74
153
  async function handleList(sendJson) {
75
154
  const file = readConnectionsFile();
76
155
  sendJson(200, {
@@ -92,6 +171,7 @@ async function handleUpsert(body, sendJson) {
92
171
  instanceUrl,
93
172
  config,
94
173
  enabled,
174
+ owner,
95
175
  } = body || {};
96
176
 
97
177
  if (!appId || !authType) {
@@ -109,6 +189,7 @@ async function handleUpsert(body, sendJson) {
109
189
  ...existing,
110
190
  enabled: enabled ?? true,
111
191
  authType,
192
+ owner: owner || existing.owner || 'amalgm',
112
193
  ...(apiKey ? { apiKey: String(apiKey).trim() } : {}),
113
194
  ...(accessToken ? { accessToken } : {}),
114
195
  ...(refreshToken ? { refreshToken } : {}),
@@ -121,6 +202,7 @@ async function handleUpsert(body, sendJson) {
121
202
  };
122
203
 
123
204
  writeConnectionsFile(file);
205
+ mirrorConnectionToToolbox(appId, file.connections[appId]);
124
206
 
125
207
  sendJson(200, {
126
208
  ok: true,
@@ -140,6 +222,7 @@ async function handleDelete(body, sendJson) {
140
222
  delete file.connections[appId];
141
223
  writeConnectionsFile(file);
142
224
  }
225
+ removeConnectionFromToolbox(appId);
143
226
 
144
227
  sendJson(200, { ok: true });
145
228
  }
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Built-in Amalgm MCP tools.
5
+ *
6
+ * This module intentionally excludes toolbox management/dynamic toolbox actions
7
+ * so the toolbox store can seed the built-in Amalgm tool catalog without a
8
+ * circular dependency on the MCP server.
9
+ */
10
+
11
+ const CORE_TOOLS = [
12
+ ...require('../tasks/tools'),
13
+ ...require('../events/tools'),
14
+ ...require('../notify'),
15
+ ...require('../agents/tools'),
16
+ ...require('../artifacts/tools'),
17
+ ...require('../browser/tools'),
18
+ ];
19
+
20
+ module.exports = { CORE_TOOLS };
@@ -14,6 +14,7 @@
14
14
  * GET /event-triggers/list → list triggers
15
15
  * POST /event-triggers/{create,update,delete}
16
16
  * GET /workspace/projects → list projects
17
+ * POST /workspace/import → copy a local folder into workspaces
17
18
  * POST /github/clone → clone repository onto the computer
18
19
  * GET /workspace/branches → list git branches
19
20
  * POST /workspace/checkout → checkout git branch
@@ -27,6 +28,14 @@
27
28
  * GET /mcp-connections → list sanitized MCP connections
28
29
  * POST /mcp-connections → save/update MCP connection
29
30
  * DELETE /mcp-connections → remove MCP connection
31
+ * GET /toolbox → list local ToolboxDB tools/actions
32
+ * POST /toolbox/tools → save/update a toolbox tool
33
+ * DELETE /toolbox/tools → remove a toolbox tool
34
+ * POST /toolbox/actions → save/update a toolbox action
35
+ * DELETE /toolbox/actions → remove a toolbox action
36
+ * GET /state/snapshot → current Local Live Store snapshot
37
+ * GET /state/events → replay Local Live Store events
38
+ * GET /state/stream → SSE Local Live Store stream
30
39
  * POST /local/init → init local computer state
31
40
  * GET /local/credentials → discover local agent auth
32
41
  * POST /local/credentials/resolve → resolve spawn env for agent auth
@@ -65,6 +74,8 @@ const slackInbound = require('../slack/inbound');
65
74
  const fsRest = require('../fs/rest');
66
75
  const localRest = require('../local/rest');
67
76
  const mcpConnectionsRest = require('../mcp-connections/rest');
77
+ const toolboxRest = require('../toolbox/rest');
78
+ const stateRest = require('../state/rest');
68
79
  const tasksRest = require('../tasks/rest');
69
80
  const userApiKeysRest = require('../user-api-keys/rest');
70
81
  const credentialsRest = require('../credentials/rest');
@@ -113,6 +124,17 @@ function createServer() {
113
124
  });
114
125
  }
115
126
 
127
+ // ── /state — Local Live Store snapshot/replay/stream ─────────────────
128
+ if (req.url.startsWith('/state/snapshot') && req.method === 'GET') {
129
+ return stateRest.handleSnapshot(getQuery(), sendJson);
130
+ }
131
+ if (req.url.startsWith('/state/events') && req.method === 'GET') {
132
+ return stateRest.handleEvents(getQuery(), sendJson);
133
+ }
134
+ if (req.url.startsWith('/state/stream') && req.method === 'GET') {
135
+ return stateRest.handleStream(req, res, getQuery());
136
+ }
137
+
116
138
  // ── /agents (REST, not MCP) ─────────────────────────────────────────────
117
139
  if (req.url === '/agents/list' && req.method === 'GET') {
118
140
  return agentsRest.handleList(sendJson);
@@ -173,6 +195,9 @@ function createServer() {
173
195
  if (req.url === '/workspace/projects' && req.method === 'GET') {
174
196
  return workspaceRest.handleProjects(sendJson);
175
197
  }
198
+ if (req.url === '/workspace/import' && req.method === 'POST') {
199
+ return workspaceRest.handleImport(await readJsonBody(), sendJson);
200
+ }
176
201
  if (req.url === '/github/clone' && req.method === 'POST') {
177
202
  return workspaceRest.handleClone(await readJsonBody(), sendJson);
178
203
  }
@@ -226,6 +251,23 @@ function createServer() {
226
251
  return mcpConnectionsRest.handleDelete(await readJsonBody(), sendJson);
227
252
  }
228
253
 
254
+ // ── /toolbox REST ─────────────────────────────────────────────────────
255
+ if (req.url === '/toolbox' && req.method === 'GET') {
256
+ return toolboxRest.handleList(sendJson);
257
+ }
258
+ if (req.url === '/toolbox/tools' && req.method === 'POST') {
259
+ return toolboxRest.handleUpsertTool(await readJsonBody(), sendJson);
260
+ }
261
+ if (req.url === '/toolbox/tools' && req.method === 'DELETE') {
262
+ return toolboxRest.handleDeleteTool(await readJsonBody(), sendJson);
263
+ }
264
+ if (req.url === '/toolbox/actions' && req.method === 'POST') {
265
+ return toolboxRest.handleUpsertAction(await readJsonBody(), sendJson);
266
+ }
267
+ if (req.url === '/toolbox/actions' && req.method === 'DELETE') {
268
+ return toolboxRest.handleDeleteAction(await readJsonBody(), sendJson);
269
+ }
270
+
229
271
  // ── /local REST ────────────────────────────────────────────────────────
230
272
  if (req.url === '/local/init' && req.method === 'POST') {
231
273
  return localRest.handleInit(sendJson);