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.
- package/README.md +4 -0
- package/lib/cli.js +133 -1
- package/lib/supervisor.js +10 -1
- package/package.json +2 -1
- package/runtime/lib/chatInput.js +9 -0
- package/runtime/lib/local/amalgmStore.js +10 -2
- package/runtime/scripts/amalgm-mcp/agents/rest.js +60 -81
- package/runtime/scripts/amalgm-mcp/agents/store.js +589 -58
- package/runtime/scripts/amalgm-mcp/agents/talk.js +10 -4
- package/runtime/scripts/amalgm-mcp/agents/tools.js +12 -1
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +19 -3
- package/runtime/scripts/amalgm-mcp/config.js +2 -0
- package/runtime/scripts/amalgm-mcp/events/store.js +16 -1
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +85 -37
- package/runtime/scripts/amalgm-mcp/local/rest.js +7 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +83 -0
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +42 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +28 -17
- package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
- package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
- package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
- package/runtime/scripts/amalgm-mcp/tasks/store.js +16 -1
- package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
- package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +116 -8
- package/runtime/scripts/chat-core/adapters/claude.js +2 -0
- package/runtime/scripts/chat-core/contract.js +2 -0
- package/runtime/scripts/chat-core/engine.js +77 -19
- package/runtime/scripts/credential-adapter.js +4 -2
- 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
|
-
|| (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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);
|