amalgm 0.1.75 → 0.1.76

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 (45) hide show
  1. package/package.json +1 -1
  2. package/runtime/scripts/amalgm-mcp/agent-config/importers/claude.js +52 -0
  3. package/runtime/scripts/amalgm-mcp/agent-config/importers/codex.js +36 -0
  4. package/runtime/scripts/amalgm-mcp/agent-config/importers/common.js +115 -0
  5. package/runtime/scripts/amalgm-mcp/agent-config/importers/index.js +14 -0
  6. package/runtime/scripts/amalgm-mcp/agent-config/importers/opencode.js +59 -0
  7. package/runtime/scripts/amalgm-mcp/agent-config/renderers/hooks.js +55 -0
  8. package/runtime/scripts/amalgm-mcp/agent-config/renderers/instructions.js +54 -0
  9. package/runtime/scripts/amalgm-mcp/agent-config/rest.js +168 -0
  10. package/runtime/scripts/amalgm-mcp/agent-config/schema.js +248 -0
  11. package/runtime/scripts/amalgm-mcp/agent-config/store.js +123 -0
  12. package/runtime/scripts/amalgm-mcp/agents/hooks.js +2 -0
  13. package/runtime/scripts/amalgm-mcp/agents/rest.js +45 -2
  14. package/runtime/scripts/amalgm-mcp/agents/talk.js +20 -4
  15. package/runtime/scripts/amalgm-mcp/automations/tool-actions.js +52 -9
  16. package/runtime/scripts/amalgm-mcp/automations/tools.js +3 -3
  17. package/runtime/scripts/amalgm-mcp/events/internal-workflows.js +3 -3
  18. package/runtime/scripts/amalgm-mcp/lib/chat-payloads.js +101 -0
  19. package/runtime/scripts/amalgm-mcp/server/core-tools.js +10 -10
  20. package/runtime/scripts/amalgm-mcp/server/local-service-router.js +2 -0
  21. package/runtime/scripts/amalgm-mcp/server/routes/agents.js +13 -0
  22. package/runtime/scripts/amalgm-mcp/server/routes/chat-payloads.js +37 -0
  23. package/runtime/scripts/amalgm-mcp/state/db.js +11 -0
  24. package/runtime/scripts/amalgm-mcp/state/snapshot.js +13 -3
  25. package/runtime/scripts/amalgm-mcp/tests/agent-config.test.js +80 -0
  26. package/runtime/scripts/amalgm-mcp/tests/automations-store-runner.test.js +17 -5
  27. package/runtime/scripts/amalgm-mcp/tests/chat-payloads.test.js +71 -0
  28. package/runtime/scripts/amalgm-mcp/tests/core-tools.test.js +9 -9
  29. package/runtime/scripts/amalgm-mcp/tests/mcp-surface.test.js +29 -0
  30. package/runtime/scripts/amalgm-mcp/tests/system-catalog.test.js +21 -16
  31. package/runtime/scripts/amalgm-mcp/tests/toolbox-service.test.js +9 -7
  32. package/runtime/scripts/amalgm-mcp/toolbox/selection.js +21 -1
  33. package/runtime/scripts/amalgm-mcp/toolbox/system-catalog.js +16 -1
  34. package/runtime/scripts/amalgm-mcp/workflows/compiler.js +1 -1
  35. package/runtime/scripts/amalgm-mcp/workflows/runner.js +8 -2
  36. package/runtime/scripts/chat-core/adapters/claude.js +2 -2
  37. package/runtime/scripts/chat-core/adapters/codex.js +82 -12
  38. package/runtime/scripts/chat-core/auth.js +13 -4
  39. package/runtime/scripts/chat-core/contract.js +18 -0
  40. package/runtime/scripts/chat-core/tests/auth.test.js +27 -2
  41. package/runtime/scripts/chat-core/tests/native-config.test.js +65 -12
  42. package/runtime/scripts/chat-core/tooling/native-config.js +73 -0
  43. package/runtime/scripts/chat-core/tooling/runtime-home.js +2 -2
  44. package/runtime/scripts/chat-core/tooling/system-prompt.js +3 -1
  45. package/runtime/scripts/local-gateway.js +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.75",
3
+ "version": "0.1.76",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { collectClaudeHooks } = require('../../agents/hooks');
5
+ const {
6
+ markdownAgentsFromDir,
7
+ nativeHome,
8
+ readText,
9
+ skillsFromDir,
10
+ } = require('./common');
11
+
12
+ function importClaudeConfig() {
13
+ const home = nativeHome();
14
+ const claudeHome = path.join(home, '.claude');
15
+ const hooks = collectClaudeHooks(home).hooks.map((hook) => ({
16
+ ...hook,
17
+ source: {
18
+ kind: 'native-import',
19
+ provider: 'claude_code',
20
+ path: hook.sourcePath,
21
+ },
22
+ }));
23
+ const instructions =
24
+ readText(path.join(claudeHome, 'CLAUDE.md'))
25
+ || readText(path.join(home, 'CLAUDE.md'));
26
+ const skills = [
27
+ ...skillsFromDir(path.join(claudeHome, 'skills'), 'claude_code'),
28
+ ...skillsFromDir(path.join(home, '.config', 'claude', 'skills'), 'claude_code'),
29
+ ];
30
+ const agents = [
31
+ ...markdownAgentsFromDir(path.join(claudeHome, 'agents'), 'claude_code', 'claude_code'),
32
+ ...markdownAgentsFromDir(path.join(home, '.config', 'claude', 'agents'), 'claude_code', 'claude_code'),
33
+ ];
34
+ return {
35
+ config: {
36
+ ...(instructions ? { instructions } : {}),
37
+ ...(skills.length ? { skills } : {}),
38
+ ...(hooks.length ? { hooks } : {}),
39
+ },
40
+ agents,
41
+ sources: [
42
+ path.join(claudeHome, 'CLAUDE.md'),
43
+ path.join(claudeHome, 'skills'),
44
+ path.join(claudeHome, 'agents'),
45
+ path.join(claudeHome, 'settings.json'),
46
+ path.join(claudeHome, 'settings.local.json'),
47
+ path.join(home, '.claude.json'),
48
+ ],
49
+ };
50
+ }
51
+
52
+ module.exports = { importClaudeConfig };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { collectCodexHooks } = require('../../agents/hooks');
5
+ const { nativeHome, readText, skillsFromDir } = require('./common');
6
+
7
+ function importCodexConfig() {
8
+ const home = nativeHome();
9
+ const codexHome = path.join(home, '.codex');
10
+ const hooks = collectCodexHooks(home).hooks.map((hook) => ({
11
+ ...hook,
12
+ source: {
13
+ kind: 'native-import',
14
+ provider: 'codex',
15
+ path: hook.sourcePath,
16
+ },
17
+ }));
18
+ const instructions = readText(path.join(codexHome, 'AGENTS.md'));
19
+ const skills = skillsFromDir(path.join(codexHome, 'skills'), 'codex')
20
+ .filter((skill) => !/\/\.system\//.test(skill.source.path));
21
+ return {
22
+ config: {
23
+ ...(instructions ? { instructions } : {}),
24
+ ...(skills.length ? { skills } : {}),
25
+ ...(hooks.length ? { hooks } : {}),
26
+ },
27
+ agents: [],
28
+ sources: [
29
+ path.join(codexHome, 'AGENTS.md'),
30
+ path.join(codexHome, 'skills'),
31
+ path.join(codexHome, 'hooks.json'),
32
+ ],
33
+ };
34
+ }
35
+
36
+ module.exports = { importCodexConfig };
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { stableId } = require('../schema');
7
+
8
+ function nativeHome() {
9
+ return process.env.AMALGM_NATIVE_HOME || os.homedir();
10
+ }
11
+
12
+ function readText(file, maxBytes = 128 * 1024) {
13
+ try {
14
+ const stat = fs.statSync(file);
15
+ if (!stat.isFile() || stat.size > maxBytes) return '';
16
+ return fs.readFileSync(file, 'utf8');
17
+ } catch {
18
+ return '';
19
+ }
20
+ }
21
+
22
+ function readJson(file) {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ function listDirs(root) {
31
+ try {
32
+ return fs.readdirSync(root, { withFileTypes: true })
33
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
34
+ .map((entry) => path.join(root, entry.name));
35
+ } catch {
36
+ return [];
37
+ }
38
+ }
39
+
40
+ function listMarkdownFiles(root) {
41
+ try {
42
+ return fs.readdirSync(root, { withFileTypes: true })
43
+ .flatMap((entry) => {
44
+ const full = path.join(root, entry.name);
45
+ if (entry.isDirectory() && !entry.name.startsWith('.')) return listMarkdownFiles(full);
46
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) return [full];
47
+ return [];
48
+ });
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
54
+ function parseFrontmatter(markdown) {
55
+ const text = String(markdown || '');
56
+ if (!text.startsWith('---\n')) return { data: {}, body: text };
57
+ const end = text.indexOf('\n---', 4);
58
+ if (end === -1) return { data: {}, body: text };
59
+ const raw = text.slice(4, end);
60
+ const data = {};
61
+ for (const line of raw.split(/\r?\n/)) {
62
+ const match = line.match(/^\s*([A-Za-z0-9_-]+)\s*:\s*(.*)\s*$/);
63
+ if (!match) continue;
64
+ let value = match[2].trim();
65
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
66
+ value = value.slice(1, -1);
67
+ }
68
+ data[match[1]] = value;
69
+ }
70
+ return { data, body: text.slice(end + 4).replace(/^\r?\n/, '') };
71
+ }
72
+
73
+ function skillsFromDir(root, sourceKind) {
74
+ return listDirs(root)
75
+ .map((dir) => {
76
+ const content = readText(path.join(dir, 'SKILL.md'));
77
+ if (!content) return null;
78
+ const name = path.basename(dir);
79
+ return {
80
+ id: stableId('skill', `${sourceKind}-${name}`),
81
+ name,
82
+ description: '',
83
+ content,
84
+ enabled: true,
85
+ source: { kind: 'native-import', provider: sourceKind, path: path.join(dir, 'SKILL.md') },
86
+ };
87
+ })
88
+ .filter(Boolean);
89
+ }
90
+
91
+ function markdownAgentsFromDir(root, sourceKind, harnessId) {
92
+ return listMarkdownFiles(root)
93
+ .map((file) => {
94
+ const content = readText(file);
95
+ if (!content) return null;
96
+ const { data, body } = parseFrontmatter(content);
97
+ const name = data.name || path.basename(file, path.extname(file));
98
+ return {
99
+ name,
100
+ description: data.description || '',
101
+ baseHarnessId: harnessId,
102
+ instructions: body.trim(),
103
+ source: { kind: 'native-import', provider: sourceKind, path: file },
104
+ };
105
+ })
106
+ .filter(Boolean);
107
+ }
108
+
109
+ module.exports = {
110
+ markdownAgentsFromDir,
111
+ nativeHome,
112
+ readJson,
113
+ readText,
114
+ skillsFromDir,
115
+ };
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ const { importClaudeConfig } = require('./claude');
4
+ const { importCodexConfig } = require('./codex');
5
+ const { importOpenCodeConfig } = require('./opencode');
6
+
7
+ function importNativeConfigForHarness(harnessId) {
8
+ if (harnessId === 'codex') return importCodexConfig();
9
+ if (harnessId === 'claude_code') return importClaudeConfig();
10
+ if (harnessId === 'opencode') return importOpenCodeConfig();
11
+ return { config: {}, agents: [], sources: [] };
12
+ }
13
+
14
+ module.exports = { importNativeConfigForHarness };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const {
5
+ markdownAgentsFromDir,
6
+ nativeHome,
7
+ readJson,
8
+ readText,
9
+ skillsFromDir,
10
+ } = require('./common');
11
+
12
+ function stringFromConfig(data, keys) {
13
+ if (!data || typeof data !== 'object') return '';
14
+ for (const key of keys) {
15
+ const value = data[key];
16
+ if (typeof value === 'string' && value.trim()) return value;
17
+ }
18
+ return '';
19
+ }
20
+
21
+ function importOpenCodeConfig() {
22
+ const home = nativeHome();
23
+ const configDir = path.join(home, '.config', 'opencode');
24
+ const dotDir = path.join(home, '.opencode');
25
+ const configJson =
26
+ readJson(path.join(configDir, 'opencode.json'))
27
+ || readJson(path.join(configDir, 'config.json'))
28
+ || readJson(path.join(dotDir, 'opencode.json'))
29
+ || {};
30
+ const instructions =
31
+ stringFromConfig(configJson, ['instructions', 'systemPrompt', 'prompt'])
32
+ || readText(path.join(dotDir, 'AGENTS.md'));
33
+ const skills = [
34
+ ...skillsFromDir(path.join(configDir, 'skills'), 'opencode'),
35
+ ...skillsFromDir(path.join(dotDir, 'skills'), 'opencode'),
36
+ ];
37
+ const agents = [
38
+ ...markdownAgentsFromDir(path.join(configDir, 'agents'), 'opencode', 'opencode'),
39
+ ...markdownAgentsFromDir(path.join(dotDir, 'agents'), 'opencode', 'opencode'),
40
+ ];
41
+ return {
42
+ config: {
43
+ ...(instructions ? { instructions } : {}),
44
+ ...(skills.length ? { skills } : {}),
45
+ },
46
+ agents,
47
+ sources: [
48
+ path.join(configDir, 'opencode.json'),
49
+ path.join(configDir, 'config.json'),
50
+ path.join(configDir, 'skills'),
51
+ path.join(configDir, 'agents'),
52
+ path.join(dotDir, 'AGENTS.md'),
53
+ path.join(dotDir, 'skills'),
54
+ path.join(dotDir, 'agents'),
55
+ ],
56
+ };
57
+ }
58
+
59
+ module.exports = { importOpenCodeConfig };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const { normalizeAgentConfig } = require('../schema');
4
+
5
+ function enabledCommandHooks(config) {
6
+ return normalizeAgentConfig(config).hooks
7
+ .filter((hook) => hook.enabled !== false && hook.type === 'command' && hook.command);
8
+ }
9
+
10
+ function hookEvent(hook, fallback = 'UserPromptSubmit') {
11
+ return hook.event || hook.phase || fallback;
12
+ }
13
+
14
+ function toNativeHookEntry(hook) {
15
+ return {
16
+ ...(hook.matcher ? { matcher: hook.matcher } : {}),
17
+ hooks: [{
18
+ type: 'command',
19
+ command: hook.command,
20
+ ...(hook.timeout ? { timeout: hook.timeout } : {}),
21
+ ...(hook.statusMessage ? { statusMessage: hook.statusMessage } : {}),
22
+ }],
23
+ };
24
+ }
25
+
26
+ function toHookMap(config) {
27
+ const out = {};
28
+ for (const hook of enabledCommandHooks(config)) {
29
+ const event = hookEvent(hook);
30
+ out[event] ||= [];
31
+ out[event].push(toNativeHookEntry(hook));
32
+ }
33
+ return out;
34
+ }
35
+
36
+ function toClaudeSettings(config) {
37
+ const hooks = toHookMap(config);
38
+ return Object.keys(hooks).length > 0 ? { hooks } : {};
39
+ }
40
+
41
+ function toCodexHooksJson(config) {
42
+ return JSON.stringify({ hooks: toHookMap(config) }, null, 2) + '\n';
43
+ }
44
+
45
+ function hasHooks(config) {
46
+ return enabledCommandHooks(config).length > 0;
47
+ }
48
+
49
+ module.exports = {
50
+ enabledCommandHooks,
51
+ hasHooks,
52
+ toClaudeSettings,
53
+ toCodexHooksJson,
54
+ toHookMap,
55
+ };
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ const { normalizeAgentConfig } = require('../schema');
4
+
5
+ const AGENT_TO_AGENT_PROMPT = [
6
+ 'You are an Amalgm agent. Other first-class Amalgm agents may be available as peers.',
7
+ 'When a task is better handled by a configured peer, use the Amalgm agent-to-agent tool instead of inventing a hidden subagent.',
8
+ ].join(' ');
9
+
10
+ function renderSkills(skills) {
11
+ const enabled = (Array.isArray(skills) ? skills : []).filter((skill) => skill.enabled !== false);
12
+ if (enabled.length === 0) return '';
13
+ const lines = ['Skills available to this agent:'];
14
+ for (const skill of enabled) {
15
+ if (skill.content) {
16
+ lines.push(`- ${skill.name || skill.id}: ${skill.description || 'see skill content below'}`);
17
+ lines.push('');
18
+ lines.push(`<skill id="${skill.id}" name="${skill.name || skill.id}">`);
19
+ lines.push(skill.content.trim());
20
+ lines.push('</skill>');
21
+ } else {
22
+ lines.push(`- ${skill.name || skill.id}${skill.description ? `: ${skill.description}` : ''}`);
23
+ }
24
+ }
25
+ return `<agent-skills>\n${lines.join('\n')}\n</agent-skills>`;
26
+ }
27
+
28
+ function renderSubagents(subagents) {
29
+ const enabled = (Array.isArray(subagents) ? subagents : []).filter((subagent) => subagent.enabled !== false);
30
+ if (enabled.length === 0) return '';
31
+ const lines = [
32
+ 'Peer agents available through the Amalgm agent-to-agent tool:',
33
+ ...enabled.map((subagent) => (
34
+ `- ${subagent.name || subagent.agentId} (${subagent.agentId})${subagent.description ? `: ${subagent.description}` : ''}`
35
+ )),
36
+ ];
37
+ return `<agent-peers>\n${lines.join('\n')}\n</agent-peers>`;
38
+ }
39
+
40
+ function renderAgentInstructions(config, options = {}) {
41
+ const normalized = normalizeAgentConfig(config);
42
+ const chunks = [
43
+ normalized.instructions,
44
+ options.includeAgentToAgentPrompt === false ? '' : AGENT_TO_AGENT_PROMPT,
45
+ renderSkills(normalized.skills),
46
+ renderSubagents(normalized.subagents),
47
+ ];
48
+ return chunks.map((chunk) => String(chunk || '').trim()).filter(Boolean).join('\n\n');
49
+ }
50
+
51
+ module.exports = {
52
+ AGENT_TO_AGENT_PROMPT,
53
+ renderAgentInstructions,
54
+ };
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ createAgent,
5
+ resolveAgent,
6
+ resolveAgentByNameOrId,
7
+ updateAgent,
8
+ } = require('../agents/store');
9
+ const { importNativeConfigForHarness } = require('./importers');
10
+ const {
11
+ getAgentConfig,
12
+ legacyFieldsFromConfig,
13
+ upsertAgentConfig,
14
+ } = require('./store');
15
+ const {
16
+ normalizeAgentConfig,
17
+ stableId,
18
+ } = require('./schema');
19
+
20
+ function mergeById(existing, incoming) {
21
+ const byId = new Map();
22
+ for (const item of Array.isArray(existing) ? existing : []) byId.set(item.id, item);
23
+ for (const item of Array.isArray(incoming) ? incoming : []) byId.set(item.id, item);
24
+ return [...byId.values()];
25
+ }
26
+
27
+ function mergeSubagents(existing, incoming) {
28
+ const byAgentId = new Map();
29
+ for (const item of Array.isArray(existing) ? existing : []) byAgentId.set(item.agentId, item);
30
+ for (const item of Array.isArray(incoming) ? incoming : []) byAgentId.set(item.agentId, item);
31
+ return [...byAgentId.values()];
32
+ }
33
+
34
+ function mergeImportedConfig(existing, imported, subagents, replace) {
35
+ if (replace) {
36
+ return normalizeAgentConfig({
37
+ ...existing,
38
+ ...imported,
39
+ subagents,
40
+ }, existing);
41
+ }
42
+ return normalizeAgentConfig({
43
+ ...existing,
44
+ instructions: existing.instructions || imported.instructions || '',
45
+ skills: mergeById(existing.skills, imported.skills),
46
+ hooks: mergeById(existing.hooks, imported.hooks),
47
+ subagents: mergeSubagents(existing.subagents, subagents),
48
+ }, existing);
49
+ }
50
+
51
+ function importedAgentId(parentAgent, nativeAgent) {
52
+ const seed = `${parentAgent.id}-${nativeAgent.name || 'subagent'}`;
53
+ return stableId('imported-agent', seed);
54
+ }
55
+
56
+ function ensureImportedAgent(parentAgent, nativeAgent) {
57
+ const id = importedAgentId(parentAgent, nativeAgent);
58
+ const existing = resolveAgent(id) || resolveAgentByNameOrId(nativeAgent.name);
59
+ if (existing) {
60
+ const config = upsertAgentConfig(existing.id, {
61
+ instructions: nativeAgent.instructions || existing.systemPrompt || '',
62
+ tools: existing.tools || { mode: 'all', selectedToolIds: [] },
63
+ skills: existing.skills || [],
64
+ }, { source: 'agent-config:import-native-subagent' });
65
+ return { agent: { ...existing, config }, created: false };
66
+ }
67
+ const agent = createAgent({
68
+ id,
69
+ name: nativeAgent.name,
70
+ description: nativeAgent.description || '',
71
+ baseHarnessId: nativeAgent.baseHarnessId || parentAgent.baseHarnessId,
72
+ baseModelId: parentAgent.baseModelId || '',
73
+ modelSettings: parentAgent.modelSettings,
74
+ systemPrompt: nativeAgent.instructions || '',
75
+ skills: [],
76
+ tools: { mode: 'all', selectedToolIds: [] },
77
+ authMethod: parentAgent.authMethod,
78
+ }, { source: 'agent-config:import-native-subagent' });
79
+ const config = upsertAgentConfig(agent.id, {
80
+ runtimeHomeLayout: 'per-agent',
81
+ instructions: nativeAgent.instructions || '',
82
+ skills: [],
83
+ tools: { mode: 'all', selectedToolIds: [] },
84
+ }, { source: 'agent-config:import-native-subagent' });
85
+ return { agent: { ...agent, config }, created: true };
86
+ }
87
+
88
+ function syncLegacyAgentFields(agentId, config) {
89
+ const legacy = legacyFieldsFromConfig(config);
90
+ try {
91
+ updateAgent(agentId, legacy, { source: 'agent-config:sync-legacy-fields' });
92
+ } catch {
93
+ // Config is the source of truth; legacy sync is best-effort for older UI paths.
94
+ }
95
+ }
96
+
97
+ async function handleGet(body, sendJson) {
98
+ const { agent_id } = body || {};
99
+ if (!agent_id) return sendJson(400, { error: 'agent_id is required' });
100
+ const agent = resolveAgent(agent_id);
101
+ if (!agent) return sendJson(404, { error: `Agent not found: ${agent_id}` });
102
+ return sendJson(200, { config: getAgentConfig(agent) });
103
+ }
104
+
105
+ async function handleUpdate(body, sendJson) {
106
+ const { agent_id, config } = body || {};
107
+ if (!agent_id) return sendJson(400, { error: 'agent_id is required' });
108
+ const agent = resolveAgent(agent_id);
109
+ if (!agent) return sendJson(404, { error: `Agent not found: ${agent_id}` });
110
+ let saved;
111
+ try {
112
+ saved = upsertAgentConfig(agent_id, config || {}, { source: 'agent-config:update-rest' });
113
+ syncLegacyAgentFields(agent_id, saved);
114
+ } catch (error) {
115
+ return sendJson(400, { error: error.message || 'Failed to update agent config' });
116
+ }
117
+ return sendJson(200, { ok: true, config: saved });
118
+ }
119
+
120
+ async function handleImportNative(body, sendJson) {
121
+ const { agent_id, replace } = body || {};
122
+ if (!agent_id) return sendJson(400, { error: 'agent_id is required' });
123
+ const agent = resolveAgent(agent_id);
124
+ if (!agent) return sendJson(404, { error: `Agent not found: ${agent_id}` });
125
+
126
+ let imported;
127
+ try {
128
+ imported = importNativeConfigForHarness(agent.baseHarnessId);
129
+ } catch (error) {
130
+ return sendJson(400, { error: error.message || 'Failed to import native config' });
131
+ }
132
+
133
+ const importedAgents = [];
134
+ const subagentRefs = [];
135
+ for (const nativeAgent of imported.agents || []) {
136
+ const result = ensureImportedAgent(agent, nativeAgent);
137
+ importedAgents.push(result);
138
+ subagentRefs.push({
139
+ id: stableId('subagent', result.agent.id),
140
+ agentId: result.agent.id,
141
+ name: result.agent.name || nativeAgent.name || '',
142
+ description: result.agent.description || nativeAgent.description || '',
143
+ enabled: true,
144
+ source: nativeAgent.source || { kind: 'native-import', provider: agent.baseHarnessId },
145
+ });
146
+ }
147
+
148
+ const existing = getAgentConfig(agent);
149
+ const merged = mergeImportedConfig(existing, imported.config || {}, subagentRefs, replace === true);
150
+ const saved = upsertAgentConfig(agent.id, merged, { source: 'agent-config:import-native' });
151
+ syncLegacyAgentFields(agent.id, saved);
152
+
153
+ return sendJson(200, {
154
+ ok: true,
155
+ config: saved,
156
+ importedAgents: importedAgents.map((entry) => ({
157
+ created: entry.created,
158
+ agent: entry.agent,
159
+ })),
160
+ sources: imported.sources || [],
161
+ });
162
+ }
163
+
164
+ module.exports = {
165
+ handleGet,
166
+ handleImportNative,
167
+ handleUpdate,
168
+ };