@amalgm/agents 0.1.1 → 0.1.3

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.
@@ -0,0 +1,56 @@
1
+ import crypto from 'node:crypto';
2
+ export const BUNDLE_KIND = 'amalgm.bundle';
3
+ export const BUNDLE_SCHEMA_VERSION = 2;
4
+ export function clean(value) {
5
+ return typeof value === 'string' ? value.trim() : '';
6
+ }
7
+ export function object(value) {
8
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
9
+ }
10
+ export function clone(value) {
11
+ return JSON.parse(JSON.stringify(value));
12
+ }
13
+ export function uniqueStrings(value) {
14
+ if (!Array.isArray(value))
15
+ return [];
16
+ return [...new Set(value.map(clean).filter(Boolean))];
17
+ }
18
+ export function addUnique(target, value) {
19
+ if (value && !target.includes(value))
20
+ target.push(value);
21
+ }
22
+ export function bundleId() {
23
+ return `agent-bundle-${crypto.randomUUID()}`;
24
+ }
25
+ export function digest(value) {
26
+ return crypto.createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(value))
27
+ .digest('hex').slice(0, 16);
28
+ }
29
+ export function emptyRequirements() {
30
+ return { runtimes: [], auth: [], secrets: [], bindings: [], systemTools: [],
31
+ systemActions: [], missingTools: [] };
32
+ }
33
+ export function mergeRequirements(target, incoming = {}) {
34
+ for (const key of Object.keys(target)) {
35
+ for (const value of incoming[key] || [])
36
+ addUnique(target[key], value);
37
+ }
38
+ return target;
39
+ }
40
+ const SECRET = /(api[_-]?key|authorization|bearer|cookie|password|passwd|secret|token|credential|private[_-]?key)/i;
41
+ export function scrubSecrets(value, path, requires) {
42
+ if (Array.isArray(value))
43
+ return value.map((item, index) => scrubSecrets(item, `${path}[${index}]`, requires));
44
+ const record = object(value);
45
+ if (!record)
46
+ return value;
47
+ const output = {};
48
+ for (const [key, child] of Object.entries(record)) {
49
+ const childPath = path ? `${path}.${key}` : key;
50
+ if (SECRET.test(key))
51
+ addUnique(requires.secrets, childPath);
52
+ else
53
+ output[key] = scrubSecrets(child, childPath, requires);
54
+ }
55
+ return output;
56
+ }
@@ -0,0 +1,2 @@
1
+ import type { AgentBundle } from './types.js';
2
+ export declare function validateAgentBundle(input: unknown): AgentBundle;
@@ -0,0 +1,92 @@
1
+ import { actionEntryId, agentEntryId, appEntryId, automationEntryId, skillFingerprint, toolEntryId } from './graph-records.js';
2
+ import { buildBundleGraph } from './graph.js';
3
+ import { BUNDLE_KIND, BUNDLE_SCHEMA_VERSION, clean, object } from './util.js';
4
+ function unique(label, records, identify) {
5
+ const seen = new Set();
6
+ for (const record of records) {
7
+ const id = identify(record);
8
+ if (!id)
9
+ throw new Error(`${label} entries must include ids`);
10
+ if (seen.has(id))
11
+ throw new Error(`Bundle contains duplicate ${label} id: ${id}`);
12
+ seen.add(id);
13
+ }
14
+ }
15
+ function assertClosed(bundle) {
16
+ const elements = new Set();
17
+ for (const element of bundle.elements) {
18
+ if (!clean(element.id) || !clean(element.type))
19
+ throw new Error('bundle graph elements must include id and type');
20
+ if (object(element)?.data !== undefined)
21
+ throw new Error(`bundle graph element must not copy data: ${element.id}`);
22
+ if (elements.has(element.id))
23
+ throw new Error(`bundle graph contains duplicate element id: ${element.id}`);
24
+ elements.add(element.id);
25
+ }
26
+ const outgoing = new Map();
27
+ const edgeKeys = new Set();
28
+ for (const edge of bundle.edges) {
29
+ if (!elements.has(edge.from) || !elements.has(edge.to))
30
+ throw new Error('bundle graph edge references missing element');
31
+ const key = `${edge.from}::${edge.kind}::${edge.to}`;
32
+ if (edgeKeys.has(key))
33
+ throw new Error(`bundle graph contains duplicate edge: ${key}`);
34
+ edgeKeys.add(key);
35
+ outgoing.set(edge.from, [...(outgoing.get(edge.from) || []), edge.to]);
36
+ }
37
+ const reachable = new Set();
38
+ const pending = [...bundle.headIds];
39
+ while (pending.length) {
40
+ const id = pending.pop();
41
+ if (reachable.has(id))
42
+ continue;
43
+ reachable.add(id);
44
+ pending.push(...(outgoing.get(id) || []));
45
+ }
46
+ const unreachable = [...elements].filter((id) => !reachable.has(id));
47
+ if (unreachable.length)
48
+ throw new Error(`bundle graph contains unreachable elements: ${unreachable.join(', ')}`);
49
+ }
50
+ // Engine reference: amalgm-mcp/agent-bundles/install.js validateBundle.
51
+ export function validateAgentBundle(input) {
52
+ const raw = object(input);
53
+ if (!raw)
54
+ throw new Error('bundle must be an object');
55
+ if (raw.kind !== BUNDLE_KIND)
56
+ throw new Error(`Unsupported bundle kind: ${clean(raw.kind) || 'unknown'}`);
57
+ if (raw.schemaVersion !== BUNDLE_SCHEMA_VERSION) {
58
+ throw new Error(`Unsupported bundle schema version: ${String(raw.schemaVersion || 'unknown')}`);
59
+ }
60
+ const bundle = input;
61
+ const arrays = ['agents', 'automations', 'apps', 'tools', 'toolActions', 'skills', 'heads'];
62
+ if (arrays.some((key) => !Array.isArray(bundle[key])))
63
+ throw new Error('bundle canonical records must be arrays');
64
+ if (!bundle.heads.length)
65
+ throw new Error('bundle must include at least one head');
66
+ unique('agent', bundle.agents, agentEntryId);
67
+ unique('automation', bundle.automations, automationEntryId);
68
+ unique('app', bundle.apps, appEntryId);
69
+ unique('tool', bundle.tools, toolEntryId);
70
+ unique('tool action', bundle.toolActions, actionEntryId);
71
+ unique('skill', bundle.skills, skillFingerprint);
72
+ unique('head', bundle.heads, (value) => {
73
+ const head = object(value);
74
+ return ['agent', 'automation', 'app', 'tool'].includes(clean(head?.type))
75
+ ? `${clean(head?.type)}:${clean(head?.id)}` : '';
76
+ });
77
+ if (bundle.requires?.missingTools?.length) {
78
+ throw new Error(`Bundle is missing tools: ${bundle.requires.missingTools.join(', ')}`);
79
+ }
80
+ const expected = buildBundleGraph(bundle);
81
+ const expectedRoot = bundle.heads.find((head) => head.type === 'agent')?.id || null;
82
+ if (bundle.rootAgentId !== expectedRoot) {
83
+ throw new Error('bundle rootAgentId does not match its canonical records and connections');
84
+ }
85
+ for (const key of ['headIds', 'elements', 'edges']) {
86
+ if (JSON.stringify(bundle[key]) !== JSON.stringify(expected[key])) {
87
+ throw new Error(`bundle ${key} does not match its canonical records and connections`);
88
+ }
89
+ }
90
+ assertClosed(bundle);
91
+ return bundle;
92
+ }
@@ -0,0 +1,14 @@
1
+ import type { Agents } from '../agents.js';
2
+ import type { AgentRecord } from '../types.js';
3
+ import type { AgentConfig } from './types.js';
4
+ export declare function importAgentConfig(agents: Agents, agentId: string, options?: {
5
+ homeDir?: string;
6
+ replace?: boolean;
7
+ }): {
8
+ config: AgentConfig;
9
+ importedAgents: {
10
+ created: boolean;
11
+ agent: AgentRecord;
12
+ }[];
13
+ sources: string[];
14
+ };
@@ -0,0 +1,59 @@
1
+ import { AgentError } from '../errors.js';
2
+ import { importNativeConfig } from './native.js';
3
+ import { normalizeAgentConfig, stableConfigId } from './schema.js';
4
+ import { configFromAgent, updateAgentConfig } from './store.js';
5
+ function mergeById(existing, incoming) {
6
+ return [...new Map([...existing, ...incoming].map((item) => [item.id, item])).values()];
7
+ }
8
+ function mergeSubagents(existing, incoming) {
9
+ return [...new Map([...existing, ...incoming].map((item) => [item.agentId, item])).values()];
10
+ }
11
+ function importedId(parent, name) {
12
+ return stableConfigId('imported-agent', `${parent.id}-${name || 'subagent'}`);
13
+ }
14
+ function ensureImportedAgent(agents, parent, native) {
15
+ const id = importedId(parent, native.name);
16
+ const existing = agents.getAgent(id) || agents.listAgents().find((item) => item.definition.name === native.name);
17
+ if (existing) {
18
+ updateAgentConfig(agents, existing.id, {
19
+ instructions: native.instructions || existing.definition.instructions,
20
+ });
21
+ return { agent: agents.getAgent(existing.id), created: false };
22
+ }
23
+ const agent = agents.createAgent({
24
+ id, name: native.name, description: native.description,
25
+ driver: { ...parent.definition.driver, id: native.driverId }, model: parent.definition.model,
26
+ authRef: parent.definition.authRef, instructions: native.instructions,
27
+ resources: { files: [], skills: [], subagents: [] },
28
+ toolbox: { toolIds: [], actionIds: [] }, workspace: parent.definition.workspace,
29
+ metadata: { importedFrom: native.source },
30
+ });
31
+ updateAgentConfig(agents, agent.id, { instructions: native.instructions });
32
+ return { agent: agents.getAgent(agent.id), created: true };
33
+ }
34
+ function mergedConfig(existing, imported, subagents, replace) {
35
+ if (replace)
36
+ return normalizeAgentConfig({ ...existing, ...imported, subagents }, existing);
37
+ return normalizeAgentConfig({
38
+ ...existing,
39
+ instructions: existing.instructions || imported.instructions || '',
40
+ skills: mergeById(existing.skills, imported.skills || []),
41
+ hooks: mergeById(existing.hooks, imported.hooks || []),
42
+ subagents: mergeSubagents(existing.subagents, subagents),
43
+ }, existing);
44
+ }
45
+ // Engine reference: amalgm-mcp/agent-config/rest.js handleImportNative.
46
+ export function importAgentConfig(agents, agentId, options = {}) {
47
+ const parent = agents.getAgent(agentId);
48
+ if (!parent)
49
+ throw new AgentError('not_found', `Agent not found: ${agentId}`);
50
+ const imported = importNativeConfig(parent.definition.driver.id, options.homeDir);
51
+ const importedAgents = imported.agents.map((native) => ensureImportedAgent(agents, parent, native));
52
+ const references = importedAgents.map(({ agent }, index) => ({
53
+ id: stableConfigId('subagent', agent.id), agentId: agent.id,
54
+ name: agent.definition.name, description: agent.definition.description, enabled: true,
55
+ source: imported.agents[index]?.source || { kind: 'native-import' },
56
+ }));
57
+ const config = mergedConfig(configFromAgent(parent), imported.config, references, options.replace === true);
58
+ return { config: updateAgentConfig(agents, agentId, config).config, importedAgents, sources: imported.sources };
59
+ }
@@ -0,0 +1,2 @@
1
+ import type { NativeConfigImport } from './types.js';
2
+ export declare function importNativeConfig(driverId: string, homeDir?: string): NativeConfigImport;
@@ -0,0 +1,179 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { parseSkillFrontmatter } from '../skills/scanner.js';
5
+ import { stableConfigId } from './schema.js';
6
+ function readText(file, maxBytes = 128 * 1024) {
7
+ try {
8
+ const stat = fs.statSync(file);
9
+ return stat.isFile() && stat.size <= maxBytes ? fs.readFileSync(file, 'utf8') : '';
10
+ }
11
+ catch {
12
+ return '';
13
+ }
14
+ }
15
+ function readJson(file) {
16
+ try {
17
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'));
18
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ function directories(root) {
25
+ try {
26
+ return fs.readdirSync(root, { withFileTypes: true })
27
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
28
+ .map((entry) => path.join(root, entry.name));
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ function markdownFiles(root) {
35
+ try {
36
+ return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
37
+ const file = path.join(root, entry.name);
38
+ if (entry.isDirectory() && !entry.name.startsWith('.'))
39
+ return markdownFiles(file);
40
+ return entry.isFile() && entry.name.toLowerCase().endsWith('.md') ? [file] : [];
41
+ });
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ }
47
+ function skillsFrom(root, provider) {
48
+ return directories(root).flatMap((directory) => {
49
+ const file = path.join(directory, 'SKILL.md');
50
+ const content = readText(file);
51
+ if (!content)
52
+ return [];
53
+ const parsed = parseSkillFrontmatter(content);
54
+ const name = parsed.name || path.basename(directory);
55
+ return [{
56
+ id: stableConfigId('skill', `${provider}-${name}`), name,
57
+ description: parsed.description || '', content, enabled: true,
58
+ source: { kind: 'native-import', provider, path: file },
59
+ }];
60
+ });
61
+ }
62
+ function agentsFrom(root, provider) {
63
+ return markdownFiles(root).flatMap((file) => {
64
+ const content = readText(file);
65
+ if (!content)
66
+ return [];
67
+ const parsed = parseSkillFrontmatter(content);
68
+ const end = content.startsWith('---\n') ? content.indexOf('\n---', 4) : -1;
69
+ const body = end >= 0 ? content.slice(end + 4).replace(/^\r?\n/, '').trim() : content.trim();
70
+ return [{
71
+ name: parsed.name || path.basename(file, path.extname(file)),
72
+ description: parsed.description || '', driverId: provider, instructions: body,
73
+ source: { kind: 'native-import', provider, path: file },
74
+ }];
75
+ });
76
+ }
77
+ function appendHook(hooks, provider, sourcePath, event, value, matcher = '') {
78
+ const command = typeof value.command === 'string' ? value.command.trim() : '';
79
+ if (!command)
80
+ return;
81
+ const timeout = Number(value.timeout);
82
+ const phases = {
83
+ UserPromptSubmit: 'userSubmit', UserSubmit: 'userSubmit', Stop: 'responseComplete',
84
+ ResponseComplete: 'responseComplete',
85
+ };
86
+ hooks.push({
87
+ id: `${provider}:${sourcePath}:${event}:${hooks.length}`,
88
+ name: command.split(/\s+/).slice(0, 3).join(' '), enabled: true,
89
+ event, phase: phases[event] || event, matcher, type: 'command', command,
90
+ ...(Number.isFinite(timeout) && timeout > 0 ? { timeout } : {}),
91
+ ...(typeof value.statusMessage === 'string' && value.statusMessage.trim()
92
+ ? { statusMessage: value.statusMessage.trim() } : {}),
93
+ source: { kind: 'native-import', provider, path: sourcePath },
94
+ });
95
+ }
96
+ function extractHooks(value, hooks, provider, sourcePath, event, matcher = '') {
97
+ if (typeof value === 'string') {
98
+ appendHook(hooks, provider, sourcePath, event, { command: value }, matcher);
99
+ return;
100
+ }
101
+ if (Array.isArray(value)) {
102
+ value.forEach((item) => extractHooks(item, hooks, provider, sourcePath, event, matcher));
103
+ return;
104
+ }
105
+ if (!value || typeof value !== 'object')
106
+ return;
107
+ const object = value;
108
+ const nextEvent = typeof object.event === 'string' ? object.event : event;
109
+ const nextMatcher = typeof object.matcher === 'string' ? object.matcher : matcher;
110
+ appendHook(hooks, provider, sourcePath, nextEvent, object, nextMatcher);
111
+ if (Array.isArray(object.hooks)) {
112
+ extractHooks(object.hooks, hooks, provider, sourcePath, nextEvent, nextMatcher);
113
+ }
114
+ }
115
+ function hooksFrom(files, provider) {
116
+ const hooks = [];
117
+ for (const file of files) {
118
+ const data = readJson(file);
119
+ const raw = data?.hooks ?? data;
120
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
121
+ continue;
122
+ for (const [event, value] of Object.entries(raw)) {
123
+ extractHooks(value, hooks, provider, file, event);
124
+ }
125
+ }
126
+ return hooks;
127
+ }
128
+ // Engine references: agent-config/importers/* and agents/hooks.js. Only
129
+ // instructions, skills, command hooks, and agent markdown cross this boundary.
130
+ export function importNativeConfig(driverId, homeDir = os.homedir()) {
131
+ if (driverId === 'codex') {
132
+ const root = path.join(homeDir, '.codex');
133
+ const instructions = readText(path.join(root, 'AGENTS.md'));
134
+ const hooksFile = path.join(root, 'hooks.json');
135
+ const skills = skillsFrom(path.join(root, 'skills'), driverId);
136
+ const hooks = hooksFrom([hooksFile], driverId);
137
+ return {
138
+ config: { ...(instructions ? { instructions } : {}), ...(skills.length ? { skills } : {}),
139
+ ...(hooks.length ? { hooks } : {}) },
140
+ agents: [], sources: [path.join(root, 'AGENTS.md'), path.join(root, 'skills'), hooksFile],
141
+ };
142
+ }
143
+ if (driverId === 'claude_code') {
144
+ const root = path.join(homeDir, '.claude');
145
+ const settings = ['settings.json', 'settings.local.json'].map((name) => path.join(root, name));
146
+ const instructions = readText(path.join(root, 'CLAUDE.md')) || readText(path.join(homeDir, 'CLAUDE.md'));
147
+ const configRoot = path.join(homeDir, '.config', 'claude');
148
+ const skills = [...skillsFrom(path.join(root, 'skills'), driverId),
149
+ ...skillsFrom(path.join(configRoot, 'skills'), driverId)];
150
+ const hooks = hooksFrom([...settings, path.join(homeDir, '.claude.json')], driverId);
151
+ const agents = [...agentsFrom(path.join(root, 'agents'), driverId),
152
+ ...agentsFrom(path.join(configRoot, 'agents'), driverId)];
153
+ return {
154
+ config: { ...(instructions ? { instructions } : {}), ...(skills.length ? { skills } : {}),
155
+ ...(hooks.length ? { hooks } : {}) },
156
+ agents,
157
+ sources: [path.join(root, 'CLAUDE.md'), path.join(root, 'skills'), path.join(root, 'agents'), ...settings],
158
+ };
159
+ }
160
+ if (driverId === 'opencode') {
161
+ const config = path.join(homeDir, '.config', 'opencode');
162
+ const dot = path.join(homeDir, '.opencode');
163
+ const data = readJson(path.join(config, 'opencode.json')) || readJson(path.join(config, 'config.json'))
164
+ || readJson(path.join(dot, 'opencode.json')) || {};
165
+ const instructions = ['instructions', 'systemPrompt', 'prompt']
166
+ .map((key) => data[key]).find((value) => typeof value === 'string');
167
+ const instructionText = instructions || readText(path.join(dot, 'AGENTS.md'));
168
+ const skills = [...skillsFrom(path.join(config, 'skills'), driverId),
169
+ ...skillsFrom(path.join(dot, 'skills'), driverId)];
170
+ return {
171
+ config: { ...(instructionText ? { instructions: instructionText } : {}),
172
+ ...(skills.length ? { skills } : {}) },
173
+ agents: [...agentsFrom(path.join(config, 'agents'), driverId), ...agentsFrom(path.join(dot, 'agents'), driverId)],
174
+ sources: [path.join(config, 'opencode.json'), path.join(config, 'skills'), path.join(config, 'agents'),
175
+ path.join(dot, 'AGENTS.md'), path.join(dot, 'skills'), path.join(dot, 'agents')],
176
+ };
177
+ }
178
+ return { config: {}, agents: [], sources: [] };
179
+ }
@@ -0,0 +1,3 @@
1
+ import type { AgentConfig, AgentConfigInput } from './types.js';
2
+ export declare function stableConfigId(prefix: string, seed: string): string;
3
+ export declare function normalizeAgentConfig(input?: AgentConfigInput, fallback?: AgentConfigInput): AgentConfig;
@@ -0,0 +1,135 @@
1
+ import crypto from 'node:crypto';
2
+ import { isObject } from '../json.js';
3
+ function text(value) {
4
+ return typeof value === 'string' ? value.trim() : '';
5
+ }
6
+ function clone(value, fallback) {
7
+ return isObject(value) ? JSON.parse(JSON.stringify(value)) : fallback;
8
+ }
9
+ function uniqueStrings(value) {
10
+ if (!Array.isArray(value))
11
+ return [];
12
+ return [...new Set(value.map(text).filter(Boolean))];
13
+ }
14
+ export function stableConfigId(prefix, seed) {
15
+ const clean = text(seed).toLowerCase().replace(/[^a-z0-9._-]+/g, '-')
16
+ .replace(/^-+|-+$/g, '').slice(0, 80);
17
+ return clean ? `${prefix}-${clean}` : `${prefix}-${crypto.randomUUID()}`;
18
+ }
19
+ function skill(value, index) {
20
+ if (typeof value === 'string') {
21
+ const name = text(value);
22
+ return name ? {
23
+ id: stableConfigId('skill', name), name, description: '', content: '', enabled: true,
24
+ source: { kind: 'amalgm' },
25
+ } : null;
26
+ }
27
+ if (!isObject(value))
28
+ return null;
29
+ const content = typeof value.content === 'string' ? value.content : '';
30
+ const name = text(value.name || value.id) || `Skill ${index + 1}`;
31
+ if (!name && !content)
32
+ return null;
33
+ return {
34
+ id: text(value.id) || stableConfigId('skill', name || content.slice(0, 80)),
35
+ name, description: typeof value.description === 'string' ? value.description : '', content,
36
+ enabled: value.enabled !== false, source: clone(value.source, { kind: 'amalgm' }),
37
+ };
38
+ }
39
+ function skills(value) {
40
+ if (!Array.isArray(value))
41
+ return [];
42
+ const byId = new Map();
43
+ value.forEach((item, index) => {
44
+ const normalized = skill(item, index);
45
+ if (normalized)
46
+ byId.set(normalized.id, normalized);
47
+ });
48
+ return [...byId.values()];
49
+ }
50
+ function hook(value, index) {
51
+ if (!isObject(value))
52
+ return null;
53
+ const command = text(value.command);
54
+ const type = text(value.type) || (command ? 'command' : 'native');
55
+ if (type === 'command' && !command)
56
+ return null;
57
+ const event = text(value.event || value.phase) || 'UserPromptSubmit';
58
+ const name = text(value.name || value.label) || command.split(/\s+/).slice(0, 3).join(' ') || event;
59
+ const timeout = Number(value.timeout);
60
+ return {
61
+ id: text(value.id) || stableConfigId('hook', `${event}-${name}-${index}`),
62
+ name, enabled: value.enabled !== false, event, phase: text(value.phase) || event,
63
+ matcher: typeof value.matcher === 'string' ? value.matcher : '', type,
64
+ ...(command ? { command } : {}),
65
+ ...(Number.isFinite(timeout) && timeout > 0 ? { timeout } : {}),
66
+ ...(text(value.statusMessage) ? { statusMessage: text(value.statusMessage) } : {}),
67
+ source: clone(value.source, {
68
+ kind: value.source === 'native' ? 'native-import' : 'amalgm',
69
+ ...(text(value.harnessId) ? { harnessId: text(value.harnessId) } : {}),
70
+ ...(text(value.sourcePath) ? { path: text(value.sourcePath) } : {}),
71
+ }),
72
+ };
73
+ }
74
+ function hooks(value) {
75
+ if (!Array.isArray(value))
76
+ return [];
77
+ const byId = new Map();
78
+ value.forEach((item, index) => {
79
+ const normalized = hook(item, index);
80
+ if (normalized)
81
+ byId.set(normalized.id, normalized);
82
+ });
83
+ return [...byId.values()];
84
+ }
85
+ function subagent(value, index) {
86
+ const raw = typeof value === 'string' ? { agentId: value } : value;
87
+ if (!isObject(raw))
88
+ return null;
89
+ const agentId = text(raw.agentId || raw.agent_id || raw.id);
90
+ if (!agentId)
91
+ return null;
92
+ return {
93
+ id: text(raw.id) || stableConfigId('subagent', `${agentId}-${index}`), agentId,
94
+ name: text(raw.name), description: typeof raw.description === 'string' ? raw.description : '',
95
+ enabled: raw.enabled !== false, source: clone(raw.source, { kind: 'amalgm' }),
96
+ };
97
+ }
98
+ function subagents(value) {
99
+ if (!Array.isArray(value))
100
+ return [];
101
+ const byAgent = new Map();
102
+ value.forEach((item, index) => {
103
+ const normalized = subagent(item, index);
104
+ if (normalized)
105
+ byAgent.set(normalized.agentId, normalized);
106
+ });
107
+ return [...byAgent.values()];
108
+ }
109
+ function loadout(value, fallback) {
110
+ const raw = isObject(value) ? value : {};
111
+ const prior = isObject(fallback) ? fallback : {};
112
+ const aliases = (source) => source.toolIds || source.selectedToolIds
113
+ || source.selected || source.tools;
114
+ const explicit = ['toolIds', 'selectedToolIds', 'selected', 'tools']
115
+ .some((key) => Object.hasOwn(raw, key));
116
+ return { toolIds: uniqueStrings(explicit ? aliases(raw) : aliases(prior)) };
117
+ }
118
+ // Engine reference: amalgm-mcp/agent-config/schema.js.
119
+ export function normalizeAgentConfig(input = {}, fallback = {}) {
120
+ const raw = isObject(input) ? input : {};
121
+ const prior = isObject(fallback) ? fallback : {};
122
+ const agentId = text(raw.agentId || raw.agent_id || prior.agentId);
123
+ const instructions = raw.instructions ?? raw.systemPrompt;
124
+ const instructionText = isObject(instructions) && typeof instructions.text === 'string'
125
+ ? instructions.text : instructions;
126
+ return {
127
+ version: 1, ...(agentId ? { agentId } : {}), runtimeHomeLayout: 'per-agent',
128
+ instructions: typeof instructionText === 'string'
129
+ ? instructionText : typeof prior.instructions === 'string' ? prior.instructions : '',
130
+ skills: skills(raw.skills ?? prior.skills ?? []),
131
+ loadout: loadout(raw.loadout ?? raw.toolset ?? raw.tools, prior.loadout ?? prior.tools),
132
+ hooks: hooks(raw.hooks ?? prior.hooks ?? []),
133
+ subagents: subagents(raw.subagents ?? prior.subagents ?? []),
134
+ };
135
+ }
@@ -0,0 +1,9 @@
1
+ import type { Agents } from '../agents.js';
2
+ import type { AgentRecord } from '../types.js';
3
+ import type { AgentConfig, AgentConfigInput } from './types.js';
4
+ export declare function configFromAgent(agent: AgentRecord): AgentConfig;
5
+ export declare function listAgentConfigs(agents: Agents): AgentConfig[];
6
+ export declare function updateAgentConfig(agents: Agents, agentId: string, patch: AgentConfigInput): {
7
+ agent: AgentRecord;
8
+ config: AgentConfig;
9
+ };
@@ -0,0 +1,56 @@
1
+ import { AgentError } from '../errors.js';
2
+ import { isObject } from '../json.js';
3
+ import { normalizeAgentConfig } from './schema.js';
4
+ const METADATA_KEY = 'agentConfig';
5
+ function configDocument(agent) {
6
+ const value = agent.definition.metadata[METADATA_KEY];
7
+ return isObject(value) ? value : {};
8
+ }
9
+ function same(left, right) {
10
+ return left.length === right.length && left.every((value, index) => value === right[index]);
11
+ }
12
+ // Engine reference: amalgm-mcp/agent-config/store.js. The immutable Agent
13
+ // revision is the store here; no second agent_configs database is introduced.
14
+ export function configFromAgent(agent) {
15
+ const stored = normalizeAgentConfig(configDocument(agent), {
16
+ agentId: agent.id,
17
+ instructions: agent.definition.instructions,
18
+ skills: agent.definition.resources.skills,
19
+ loadout: { toolIds: agent.definition.toolbox.toolIds },
20
+ subagents: agent.definition.resources.subagents,
21
+ });
22
+ const skillRefs = stored.skills.filter((item) => item.enabled).map((item) => item.name || item.id);
23
+ const subagentRefs = stored.subagents.filter((item) => item.enabled).map((item) => item.agentId);
24
+ return normalizeAgentConfig({
25
+ ...stored,
26
+ agentId: agent.id,
27
+ instructions: stored.instructions.trim() === agent.definition.instructions
28
+ ? stored.instructions : agent.definition.instructions,
29
+ skills: same(skillRefs, agent.definition.resources.skills)
30
+ ? stored.skills : agent.definition.resources.skills,
31
+ loadout: { toolIds: agent.definition.toolbox.toolIds },
32
+ subagents: same(subagentRefs, agent.definition.resources.subagents)
33
+ ? stored.subagents : agent.definition.resources.subagents,
34
+ }, stored);
35
+ }
36
+ export function listAgentConfigs(agents) {
37
+ return agents.listAgents().map(configFromAgent);
38
+ }
39
+ export function updateAgentConfig(agents, agentId, patch) {
40
+ const current = agents.getAgent(agentId);
41
+ if (!current)
42
+ throw new AgentError('not_found', `Agent not found: ${agentId}`);
43
+ const existing = configFromAgent(current);
44
+ const config = normalizeAgentConfig({ ...existing, ...patch, agentId }, existing);
45
+ const metadata = { ...current.definition.metadata, [METADATA_KEY]: config };
46
+ const projected = {
47
+ instructions: config.instructions,
48
+ resources: {
49
+ skills: config.skills.filter((item) => item.enabled).map((item) => item.name || item.id),
50
+ subagents: config.subagents.filter((item) => item.enabled).map((item) => item.agentId),
51
+ },
52
+ toolbox: { toolIds: config.loadout.toolIds },
53
+ metadata,
54
+ };
55
+ return { agent: agents.updateAgent(agentId, projected), config };
56
+ }
@@ -0,0 +1,55 @@
1
+ import type { JsonObject } from '../types.js';
2
+ export interface ConfigSkill {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ content: string;
7
+ enabled: boolean;
8
+ source: JsonObject;
9
+ }
10
+ export interface ConfigHook {
11
+ id: string;
12
+ name: string;
13
+ enabled: boolean;
14
+ event: string;
15
+ phase: string;
16
+ matcher: string;
17
+ type: string;
18
+ command?: string;
19
+ timeout?: number;
20
+ statusMessage?: string;
21
+ source: JsonObject;
22
+ }
23
+ export interface ConfigSubagent {
24
+ id: string;
25
+ agentId: string;
26
+ name: string;
27
+ description: string;
28
+ enabled: boolean;
29
+ source: JsonObject;
30
+ }
31
+ export interface AgentConfig {
32
+ version: 1;
33
+ agentId?: string;
34
+ runtimeHomeLayout: 'per-agent';
35
+ instructions: string;
36
+ skills: ConfigSkill[];
37
+ loadout: {
38
+ toolIds: string[];
39
+ };
40
+ hooks: ConfigHook[];
41
+ subagents: ConfigSubagent[];
42
+ }
43
+ export interface NativeAgent {
44
+ name: string;
45
+ description: string;
46
+ driverId: string;
47
+ instructions: string;
48
+ source: JsonObject;
49
+ }
50
+ export interface NativeConfigImport {
51
+ config: Partial<AgentConfig>;
52
+ agents: NativeAgent[];
53
+ sources: string[];
54
+ }
55
+ export type AgentConfigInput = AgentConfig | Record<string, unknown>;