amalgm 0.0.1 → 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.
- 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
package/README.md
CHANGED
|
@@ -9,6 +9,9 @@ amalgm doctor
|
|
|
9
9
|
amalgm start
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
To move an existing install to the newest published build, run `amalgm update --tag canary`.
|
|
13
|
+
Set `AMALGM_AUTO_UPDATE=1` when running `amalgm start` to let the CLI update itself before launching if its current dist-tag has moved.
|
|
14
|
+
|
|
12
15
|
`amalgm login` opens a browser approval page, then registers the machine and stores its local tunnel/computer record in `~/.amalgm/computer.json`.
|
|
13
16
|
|
|
14
17
|
If you start from the Amalgm web app or are setting up a remote/headless machine, create a setup code from the web app and run:
|
|
@@ -34,6 +37,7 @@ Useful commands:
|
|
|
34
37
|
|
|
35
38
|
```sh
|
|
36
39
|
amalgm status
|
|
40
|
+
amalgm update --tag canary
|
|
37
41
|
amalgm logs
|
|
38
42
|
amalgm logs chat-server
|
|
39
43
|
amalgm stop
|
package/lib/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ const net = require('net');
|
|
|
7
7
|
const os = require('os');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const readline = require('readline');
|
|
10
|
-
const { spawn } = require('child_process');
|
|
10
|
+
const { spawn, spawnSync } = require('child_process');
|
|
11
11
|
|
|
12
12
|
const {
|
|
13
13
|
AMALGM_DIR,
|
|
@@ -79,11 +79,13 @@ function usage() {
|
|
|
79
79
|
' amalgm stop',
|
|
80
80
|
' amalgm status',
|
|
81
81
|
' amalgm doctor',
|
|
82
|
+
' amalgm update [--tag latest|canary]',
|
|
82
83
|
' amalgm logs [service]',
|
|
83
84
|
' amalgm logout',
|
|
84
85
|
'',
|
|
85
86
|
'Environment:',
|
|
86
87
|
' AMALGM_APP_URL Web app base URL for login/register',
|
|
88
|
+
' AMALGM_AUTO_UPDATE Set to 1 to update before start when a newer dist-tag exists',
|
|
87
89
|
' AMALGM_DIR Runtime state dir (default ~/.amalgm)',
|
|
88
90
|
' AMALGM_PROXY_TOKEN Optional short-lived proxy token override',
|
|
89
91
|
].join('\n');
|
|
@@ -178,12 +180,14 @@ const SAFE_DAEMON_ENV_KEYS = [
|
|
|
178
180
|
'AMALGM_NATIVE_NODE_MODULES',
|
|
179
181
|
'AMALGM_SKIP_NATIVE_INSTALL',
|
|
180
182
|
'CHAT_SERVER_PORT',
|
|
183
|
+
'ELECTRON_RUN_AS_NODE',
|
|
181
184
|
'FS_WATCHER_PORT',
|
|
182
185
|
'FS_WATCHER_ROOT',
|
|
183
186
|
'HOME',
|
|
184
187
|
'LANG',
|
|
185
188
|
'LC_ALL',
|
|
186
189
|
'LOGNAME',
|
|
190
|
+
'NODE_PATH',
|
|
187
191
|
'PATH',
|
|
188
192
|
'PORT_MONITOR_PORT',
|
|
189
193
|
'SHELL',
|
|
@@ -367,6 +371,91 @@ function commandExists(command) {
|
|
|
367
371
|
});
|
|
368
372
|
}
|
|
369
373
|
|
|
374
|
+
function parseSemver(value) {
|
|
375
|
+
const [core, prerelease = ''] = String(value || '').trim().replace(/^v/, '').split('-', 2);
|
|
376
|
+
const parts = core.split('.').map((part) => Number.parseInt(part, 10));
|
|
377
|
+
return {
|
|
378
|
+
major: Number.isFinite(parts[0]) ? parts[0] : 0,
|
|
379
|
+
minor: Number.isFinite(parts[1]) ? parts[1] : 0,
|
|
380
|
+
patch: Number.isFinite(parts[2]) ? parts[2] : 0,
|
|
381
|
+
prerelease: prerelease ? prerelease.split('.') : [],
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function comparePrerelease(left, right) {
|
|
386
|
+
if (left.length === 0 && right.length === 0) return 0;
|
|
387
|
+
if (left.length === 0) return 1;
|
|
388
|
+
if (right.length === 0) return -1;
|
|
389
|
+
|
|
390
|
+
const length = Math.max(left.length, right.length);
|
|
391
|
+
for (let i = 0; i < length; i += 1) {
|
|
392
|
+
const a = left[i];
|
|
393
|
+
const b = right[i];
|
|
394
|
+
if (a === undefined) return -1;
|
|
395
|
+
if (b === undefined) return 1;
|
|
396
|
+
const aNum = /^\d+$/.test(a) ? Number.parseInt(a, 10) : null;
|
|
397
|
+
const bNum = /^\d+$/.test(b) ? Number.parseInt(b, 10) : null;
|
|
398
|
+
if (aNum !== null && bNum !== null && aNum !== bNum) return aNum > bNum ? 1 : -1;
|
|
399
|
+
if (aNum !== null && bNum === null) return -1;
|
|
400
|
+
if (aNum === null && bNum !== null) return 1;
|
|
401
|
+
if (a !== b) return a > b ? 1 : -1;
|
|
402
|
+
}
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function compareVersions(left, right) {
|
|
407
|
+
const a = parseSemver(left);
|
|
408
|
+
const b = parseSemver(right);
|
|
409
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
410
|
+
if (a[key] !== b[key]) return a[key] > b[key] ? 1 : -1;
|
|
411
|
+
}
|
|
412
|
+
return comparePrerelease(a.prerelease, b.prerelease);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function npmCommand() {
|
|
416
|
+
return process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function npmInstallTag(options = {}) {
|
|
420
|
+
const explicit = options.tag || options.channel;
|
|
421
|
+
if (explicit && explicit !== true) return String(explicit).replace(/^@/, '');
|
|
422
|
+
return PACKAGE_VERSION.includes('-canary.') ? 'canary' : 'latest';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function npmPackageVersionForTag(tag) {
|
|
426
|
+
const result = spawnSync(npmCommand(), ['view', `amalgm@${tag}`, 'version', '--json'], {
|
|
427
|
+
encoding: 'utf8',
|
|
428
|
+
timeout: 15000,
|
|
429
|
+
env: {
|
|
430
|
+
...process.env,
|
|
431
|
+
npm_config_audit: 'false',
|
|
432
|
+
npm_config_fund: 'false',
|
|
433
|
+
},
|
|
434
|
+
});
|
|
435
|
+
if (result.status !== 0) {
|
|
436
|
+
const detail = String(result.stderr || result.stdout || '').trim();
|
|
437
|
+
throw new Error(`Could not check npm dist-tag "${tag}". ${detail}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const raw = String(result.stdout || '').trim();
|
|
441
|
+
try {
|
|
442
|
+
return JSON.parse(raw);
|
|
443
|
+
} catch {
|
|
444
|
+
return raw.replace(/^"|"$/g, '');
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function installNpmPackageTag(tag) {
|
|
449
|
+
return spawnSync(npmCommand(), ['install', '-g', `amalgm@${tag}`], {
|
|
450
|
+
stdio: 'inherit',
|
|
451
|
+
env: {
|
|
452
|
+
...process.env,
|
|
453
|
+
npm_config_audit: 'false',
|
|
454
|
+
npm_config_fund: 'false',
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
370
459
|
function openBrowser(url) {
|
|
371
460
|
const command =
|
|
372
461
|
process.platform === 'darwin'
|
|
@@ -671,8 +760,50 @@ function ensureAgentRuntimeCommands(options = {}) {
|
|
|
671
760
|
}
|
|
672
761
|
}
|
|
673
762
|
|
|
763
|
+
async function update(options = {}) {
|
|
764
|
+
const tag = npmInstallTag(options);
|
|
765
|
+
const targetVersion = npmPackageVersionForTag(tag);
|
|
766
|
+
if (!targetVersion) throw new Error(`npm did not return a version for amalgm@${tag}`);
|
|
767
|
+
|
|
768
|
+
if (compareVersions(targetVersion, PACKAGE_VERSION) <= 0) {
|
|
769
|
+
console.log(`Amalgm is already current for ${tag}: ${PACKAGE_VERSION}`);
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
console.log(`Updating Amalgm from ${PACKAGE_VERSION} to ${targetVersion} (${tag}).`);
|
|
774
|
+
const result = installNpmPackageTag(tag);
|
|
775
|
+
if (result.status !== 0) {
|
|
776
|
+
throw new Error(`npm install -g amalgm@${tag} failed with status ${result.status ?? result.signal ?? 'unknown'}`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
console.log(`Amalgm updated to ${targetVersion}.`);
|
|
780
|
+
const pid = readPid();
|
|
781
|
+
if (isPidRunning(pid)) {
|
|
782
|
+
console.log('A previous Amalgm daemon is still running. Run `amalgm stop && amalgm start` to restart it with the new package.');
|
|
783
|
+
}
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async function updateBeforeStartIfRequested(options = {}) {
|
|
788
|
+
const requested = options['auto-update'] || process.env.AMALGM_AUTO_UPDATE === '1';
|
|
789
|
+
if (!requested) return false;
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
return await update(options);
|
|
793
|
+
} catch (error) {
|
|
794
|
+
console.warn(`Auto-update skipped: ${error.message}`);
|
|
795
|
+
return false;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
674
799
|
async function start(options) {
|
|
675
800
|
ensureBaseDirs();
|
|
801
|
+
|
|
802
|
+
if (await updateBeforeStartIfRequested(options)) {
|
|
803
|
+
console.log('Run `amalgm start` again to launch the updated package.');
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
676
807
|
assertRuntimePresent();
|
|
677
808
|
|
|
678
809
|
let record = loadComputerRecord({ migrate: true });
|
|
@@ -988,6 +1119,7 @@ async function main(argv) {
|
|
|
988
1119
|
if (command === 'stop') return stop();
|
|
989
1120
|
if (command === 'status') return status();
|
|
990
1121
|
if (command === 'doctor') return doctor();
|
|
1122
|
+
if (command === 'update') return update(options);
|
|
991
1123
|
if (command === 'logs') return logs(options, positionals);
|
|
992
1124
|
if (command === 'logout') return logout();
|
|
993
1125
|
|
package/lib/supervisor.js
CHANGED
|
@@ -87,7 +87,9 @@ const SAFE_PROCESS_ENV_KEYS = [
|
|
|
87
87
|
'LOGNAME',
|
|
88
88
|
'AMALGM_NATIVE_NODE_MODULES',
|
|
89
89
|
'AMALGM_SKIP_NATIVE_INSTALL',
|
|
90
|
+
'ELECTRON_RUN_AS_NODE',
|
|
90
91
|
'PATH',
|
|
92
|
+
'NODE_PATH',
|
|
91
93
|
'SHELL',
|
|
92
94
|
'SSH_AUTH_SOCK',
|
|
93
95
|
'TERM',
|
|
@@ -134,7 +136,12 @@ function ensureRuntimeToken() {
|
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
function baseRuntimeEnv(record, ports) {
|
|
137
|
-
const
|
|
139
|
+
const workspaceRoot =
|
|
140
|
+
process.env.AMALGM_WORKSPACES_DIR ||
|
|
141
|
+
process.env.AMALGM_PROJECTS_DIR ||
|
|
142
|
+
path.join(AMALGM_DIR, 'workspaces');
|
|
143
|
+
ensureDir(workspaceRoot);
|
|
144
|
+
const defaultCwd = process.env.AMALGM_DEFAULT_CWD || workspaceRoot;
|
|
138
145
|
const proxyToken = proxyTokenFromRecord(record);
|
|
139
146
|
const env = {
|
|
140
147
|
...safeBaseProcessEnv(),
|
|
@@ -146,6 +153,8 @@ function baseRuntimeEnv(record, ports) {
|
|
|
146
153
|
AMALGM_CREATE_AUTH_WATCH_DIRS: process.env.AMALGM_CREATE_AUTH_WATCH_DIRS || 'false',
|
|
147
154
|
AMALGM_DIR,
|
|
148
155
|
AMALGM_RUNTIME_DIR: RUNTIME_DIR,
|
|
156
|
+
AMALGM_WORKSPACES_DIR: workspaceRoot,
|
|
157
|
+
AMALGM_PROJECTS_DIR: workspaceRoot,
|
|
149
158
|
AMALGM_DEFAULT_CWD: defaultCwd,
|
|
150
159
|
AMALGM_COMPUTER_ID: record?.computer_id || process.env.AMALGM_COMPUTER_ID || '',
|
|
151
160
|
AMALGM_CONTAINER_ID: record?.computer_id || process.env.AMALGM_CONTAINER_ID || 'local-computer',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amalgm",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.32",
|
|
4
4
|
"description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"@openai/codex": "^0.128.0",
|
|
31
31
|
"@opencode-ai/sdk": "^1.14.29",
|
|
32
32
|
"ai": "^6.0.116",
|
|
33
|
+
"better-sqlite3": "^12.10.0",
|
|
33
34
|
"cron-parser": "^5.5.0",
|
|
34
35
|
"opencode-ai": "^1.14.35",
|
|
35
36
|
"playwright-core": "^1.59.1",
|
package/runtime/lib/chatInput.js
CHANGED
|
@@ -58,6 +58,8 @@ const ChatInputSchema = z.object({
|
|
|
58
58
|
}).default({}),
|
|
59
59
|
tools: z.object({
|
|
60
60
|
mcpAppIds: z.array(z.string()).default([]),
|
|
61
|
+
mode: z.enum(['all', 'selected']).default('all'),
|
|
62
|
+
toolIds: z.array(z.string()).default([]),
|
|
61
63
|
}).default({}),
|
|
62
64
|
execution: z.object({
|
|
63
65
|
cwd: z.string().nullable().optional(),
|
|
@@ -222,6 +224,13 @@ function normalizeChatInput(chatInput, legacy = {}) {
|
|
|
222
224
|
},
|
|
223
225
|
tools: {
|
|
224
226
|
mcpAppIds: uniqueStrings(rawTools.mcpAppIds || legacy.mcpAppIds),
|
|
227
|
+
mode: rawTools.mode === 'selected' || legacy.toolMode === 'selected' ? 'selected' : 'all',
|
|
228
|
+
toolIds: uniqueStrings(
|
|
229
|
+
rawTools.toolIds
|
|
230
|
+
|| rawTools.selectedToolIds
|
|
231
|
+
|| legacy.toolIds
|
|
232
|
+
|| legacy.selectedToolIds,
|
|
233
|
+
),
|
|
225
234
|
},
|
|
226
235
|
execution: {
|
|
227
236
|
cwd: resolvedCwd,
|
|
@@ -32,6 +32,7 @@ var amalgmStore_exports = {};
|
|
|
32
32
|
__export(amalgmStore_exports, {
|
|
33
33
|
ensureAmalgmDir: () => ensureAmalgmDir,
|
|
34
34
|
getAmalgmDir: () => getAmalgmDir,
|
|
35
|
+
getAmalgmWorkspaceRoot: () => getAmalgmWorkspaceRoot,
|
|
35
36
|
getHomeDir: () => getHomeDir,
|
|
36
37
|
readResource: () => readResource,
|
|
37
38
|
writeResource: () => writeResource
|
|
@@ -40,7 +41,8 @@ module.exports = __toCommonJS(amalgmStore_exports);
|
|
|
40
41
|
var import_fs = __toESM(require("fs"));
|
|
41
42
|
var import_path = __toESM(require("path"));
|
|
42
43
|
var import_os = __toESM(require("os"));
|
|
43
|
-
var AMALGM_DIR = import_path.default.join(import_os.default.homedir(), ".amalgm");
|
|
44
|
+
var AMALGM_DIR = process.env.AMALGM_DIR || import_path.default.join(import_os.default.homedir(), ".amalgm");
|
|
45
|
+
var AMALGM_WORKSPACE_ROOT = process.env.AMALGM_WORKSPACES_DIR || process.env.AMALGM_PROJECTS_DIR || import_path.default.join(AMALGM_DIR, "workspaces");
|
|
44
46
|
var DIRECTORIES = [
|
|
45
47
|
"",
|
|
46
48
|
// ~/.amalgm/
|
|
@@ -58,8 +60,10 @@ var DIRECTORIES = [
|
|
|
58
60
|
// ~/.amalgm/conversations/
|
|
59
61
|
"conversations/active",
|
|
60
62
|
// ~/.amalgm/conversations/active/
|
|
61
|
-
"conversations/saved"
|
|
63
|
+
"conversations/saved",
|
|
62
64
|
// ~/.amalgm/conversations/saved/
|
|
65
|
+
"workspaces"
|
|
66
|
+
// ~/.amalgm/workspaces/
|
|
63
67
|
];
|
|
64
68
|
var DEFAULT_FILES = {
|
|
65
69
|
"config.json": {
|
|
@@ -115,6 +119,9 @@ function writeResource(resourcePath, data) {
|
|
|
115
119
|
function getAmalgmDir() {
|
|
116
120
|
return AMALGM_DIR;
|
|
117
121
|
}
|
|
122
|
+
function getAmalgmWorkspaceRoot() {
|
|
123
|
+
return AMALGM_WORKSPACE_ROOT;
|
|
124
|
+
}
|
|
118
125
|
function getHomeDir() {
|
|
119
126
|
return import_os.default.homedir();
|
|
120
127
|
}
|
|
@@ -122,6 +129,7 @@ function getHomeDir() {
|
|
|
122
129
|
0 && (module.exports = {
|
|
123
130
|
ensureAmalgmDir,
|
|
124
131
|
getAmalgmDir,
|
|
132
|
+
getAmalgmWorkspaceRoot,
|
|
125
133
|
getHomeDir,
|
|
126
134
|
readResource,
|
|
127
135
|
writeResource
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* /agents/* REST routes (UI-only, not MCP tools).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
const crypto = require('crypto');
|
|
6
5
|
const {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
createAgent,
|
|
7
|
+
deleteAgent,
|
|
9
8
|
getAllAgentsWithBuiltins,
|
|
10
9
|
resolveAgent,
|
|
10
|
+
updateAgent,
|
|
11
11
|
} = require('./store');
|
|
12
12
|
const credentialAdapter = require('../../credential-adapter');
|
|
13
13
|
|
|
@@ -33,6 +33,22 @@ function normalizeMcpConfig(mcp, mcpAppIds, nativeMcps) {
|
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function normalizeToolConfig(tools, legacyMcp) {
|
|
37
|
+
const raw = tools && typeof tools === 'object' && !Array.isArray(tools) ? tools : null;
|
|
38
|
+
const selectedToolIds = normalizeStringList(
|
|
39
|
+
raw?.selectedToolIds
|
|
40
|
+
?? raw?.selected
|
|
41
|
+
?? [
|
|
42
|
+
...normalizeStringList(legacyMcp?.appIds),
|
|
43
|
+
...normalizeStringList(legacyMcp?.nativeMcps),
|
|
44
|
+
],
|
|
45
|
+
);
|
|
46
|
+
return {
|
|
47
|
+
mode: raw?.mode === 'selected' || (!raw && legacyMcp?.inheritAll === false) ? 'selected' : 'all',
|
|
48
|
+
selectedToolIds,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
async function handleList(sendJson) {
|
|
37
53
|
sendJson(200, { agents: getAllAgentsWithBuiltins() });
|
|
38
54
|
}
|
|
@@ -56,34 +72,36 @@ async function handleCreate(body, sendJson) {
|
|
|
56
72
|
mcpAppIds,
|
|
57
73
|
nativeMcps,
|
|
58
74
|
mcp,
|
|
75
|
+
tools,
|
|
59
76
|
authMethod,
|
|
60
77
|
} = body;
|
|
61
78
|
if (!name || !name.trim()) return sendJson(400, { error: 'name is required' });
|
|
62
79
|
if (!baseHarnessId) return sendJson(400, { error: 'baseHarnessId is required' });
|
|
63
80
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
const mcpConfig = normalizeMcpConfig(mcp, mcpAppIds, nativeMcps);
|
|
82
|
+
const toolConfig = normalizeToolConfig(tools, mcpConfig);
|
|
83
|
+
let agent;
|
|
84
|
+
try {
|
|
85
|
+
agent = createAgent({
|
|
86
|
+
name: name.trim(),
|
|
87
|
+
description: description || '',
|
|
88
|
+
baseHarnessId,
|
|
89
|
+
baseModelId: baseModelId || '',
|
|
90
|
+
systemPrompt: systemPrompt || '',
|
|
91
|
+
files: normalizeStringList(files),
|
|
92
|
+
skills: normalizeStringList(skills),
|
|
93
|
+
mcpAppIds: normalizeStringList(mcpConfig.appIds),
|
|
94
|
+
nativeMcps: normalizeStringList(mcpConfig.nativeMcps),
|
|
95
|
+
tools: toolConfig,
|
|
96
|
+
mcp: {
|
|
97
|
+
...mcpConfig,
|
|
98
|
+
inheritAll: toolConfig.mode !== 'selected',
|
|
99
|
+
},
|
|
100
|
+
authMethod: coerceAuthMethodForHarness(baseHarnessId, authMethod),
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return sendJson(400, { error: error.message || 'Failed to create agent' });
|
|
67
104
|
}
|
|
68
|
-
|
|
69
|
-
const agent = {
|
|
70
|
-
id: `custom-${crypto.randomUUID()}`,
|
|
71
|
-
name: name.trim(),
|
|
72
|
-
description: description || '',
|
|
73
|
-
baseHarnessId,
|
|
74
|
-
baseModelId: baseModelId || '',
|
|
75
|
-
systemPrompt: systemPrompt || '',
|
|
76
|
-
files: normalizeStringList(files),
|
|
77
|
-
skills: normalizeStringList(skills),
|
|
78
|
-
mcpAppIds: normalizeStringList(mcpAppIds),
|
|
79
|
-
nativeMcps: normalizeStringList(nativeMcps),
|
|
80
|
-
mcp: normalizeMcpConfig(mcp, mcpAppIds, nativeMcps),
|
|
81
|
-
authMethod: coerceAuthMethodForHarness(baseHarnessId, authMethod),
|
|
82
|
-
createdAt: new Date().toISOString(),
|
|
83
|
-
updatedAt: new Date().toISOString(),
|
|
84
|
-
};
|
|
85
|
-
data.agents.push(agent);
|
|
86
|
-
saveAgents(data);
|
|
87
105
|
console.log(`[AmalgmMCP] Created agent: ${agent.id} (${agent.name})`);
|
|
88
106
|
sendJson(200, { ok: true, agent });
|
|
89
107
|
}
|
|
@@ -91,61 +109,20 @@ async function handleCreate(body, sendJson) {
|
|
|
91
109
|
async function handleUpdate(body, sendJson) {
|
|
92
110
|
const { agent_id, ...updates } = body;
|
|
93
111
|
if (!agent_id) return sendJson(400, { error: 'agent_id is required' });
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
if (!agent) return sendJson(404, { error: `Agent not found: ${agent_id}` });
|
|
112
|
+
const existing = resolveAgent(agent_id);
|
|
113
|
+
if (!existing) return sendJson(404, { error: `Agent not found: ${agent_id}` });
|
|
97
114
|
|
|
98
|
-
if (
|
|
99
|
-
updates.
|
|
100
|
-
data.agents.some(
|
|
101
|
-
(a) => a.id !== agent_id && a.name.toLowerCase() === updates.name.trim().toLowerCase(),
|
|
102
|
-
)
|
|
103
|
-
) {
|
|
104
|
-
return sendJson(400, { error: `An agent named "${updates.name.trim()}" already exists` });
|
|
115
|
+
if (updates.authMethod !== undefined) {
|
|
116
|
+
updates.authMethod = coerceAuthMethodForHarness(existing.baseHarnessId, updates.authMethod);
|
|
105
117
|
}
|
|
106
118
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
'
|
|
112
|
-
'
|
|
113
|
-
'files',
|
|
114
|
-
'skills',
|
|
115
|
-
'mcpAppIds',
|
|
116
|
-
'nativeMcps',
|
|
117
|
-
'mcp',
|
|
118
|
-
'authMethod',
|
|
119
|
-
];
|
|
120
|
-
for (const field of allowedFields) {
|
|
121
|
-
if (updates[field] === undefined) continue;
|
|
122
|
-
|
|
123
|
-
if (field === 'files' || field === 'skills' || field === 'mcpAppIds' || field === 'nativeMcps') {
|
|
124
|
-
agent[field] = normalizeStringList(updates[field]);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (field === 'mcp') {
|
|
129
|
-
agent.mcp = normalizeMcpConfig(
|
|
130
|
-
updates.mcp,
|
|
131
|
-
updates.mcpAppIds ?? agent.mcpAppIds,
|
|
132
|
-
updates.nativeMcps ?? agent.nativeMcps,
|
|
133
|
-
);
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (field === 'authMethod') {
|
|
138
|
-
agent.authMethod = coerceAuthMethodForHarness(agent.baseHarnessId, updates.authMethod);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
agent[field] = updates[field];
|
|
119
|
+
let agent;
|
|
120
|
+
try {
|
|
121
|
+
agent = updateAgent(agent_id, updates);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const message = error.message || 'Failed to update agent';
|
|
124
|
+
return sendJson(message.includes('not found') ? 404 : 400, { error: message });
|
|
143
125
|
}
|
|
144
|
-
agent.authMethod = coerceAuthMethodForHarness(agent.baseHarnessId, agent.authMethod);
|
|
145
|
-
agent.mcpAppIds = normalizeStringList(agent.mcp?.appIds ?? agent.mcpAppIds);
|
|
146
|
-
agent.nativeMcps = normalizeStringList(agent.mcp?.nativeMcps ?? agent.nativeMcps);
|
|
147
|
-
agent.updatedAt = new Date().toISOString();
|
|
148
|
-
saveAgents(data);
|
|
149
126
|
console.log(`[AmalgmMCP] Updated agent: ${agent.id} (${agent.name})`);
|
|
150
127
|
sendJson(200, { ok: true, agent });
|
|
151
128
|
}
|
|
@@ -153,11 +130,13 @@ async function handleUpdate(body, sendJson) {
|
|
|
153
130
|
async function handleDelete(body, sendJson) {
|
|
154
131
|
const { agent_id } = body;
|
|
155
132
|
if (!agent_id) return sendJson(400, { error: 'agent_id is required' });
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
133
|
+
let deleted;
|
|
134
|
+
try {
|
|
135
|
+
deleted = deleteAgent(agent_id);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const message = error.message || 'Failed to delete agent';
|
|
138
|
+
return sendJson(message.includes('not found') ? 404 : 400, { error: message });
|
|
139
|
+
}
|
|
161
140
|
console.log(`[AmalgmMCP] Deleted agent: ${agent_id} (${deleted.name})`);
|
|
162
141
|
sendJson(200, { ok: true, deleted });
|
|
163
142
|
}
|