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
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 defaultCwd = process.env.AMALGM_DEFAULT_CWD || os.homedir();
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.1",
3
+ "version": "0.0.33",
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",
@@ -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
- loadAgents,
8
- saveAgents,
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 data = loadAgents();
65
- if (data.agents.some((a) => a.name.toLowerCase() === name.trim().toLowerCase())) {
66
- return sendJson(400, { error: `An agent named "${name.trim()}" already exists` });
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 data = loadAgents();
95
- const agent = data.agents.find((a) => a.id === agent_id);
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.name &&
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
- const allowedFields = [
108
- 'name',
109
- 'description',
110
- 'baseHarnessId',
111
- 'baseModelId',
112
- 'systemPrompt',
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
- const data = loadAgents();
157
- const idx = data.agents.findIndex((a) => a.id === agent_id);
158
- if (idx === -1) return sendJson(404, { error: `Agent not found: ${agent_id}` });
159
- const deleted = data.agents.splice(idx, 1)[0];
160
- saveAgents(data);
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
  }