amalgm 0.0.1 → 0.0.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/lib/cli.js +133 -1
- package/lib/supervisor.js +10 -1
- package/package.json +2 -1
- package/runtime/lib/chatInput.js +9 -0
- package/runtime/lib/local/amalgmStore.js +10 -2
- package/runtime/scripts/amalgm-mcp/agents/rest.js +60 -81
- package/runtime/scripts/amalgm-mcp/agents/store.js +589 -58
- package/runtime/scripts/amalgm-mcp/agents/talk.js +10 -4
- package/runtime/scripts/amalgm-mcp/agents/tools.js +12 -1
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +19 -3
- package/runtime/scripts/amalgm-mcp/config.js +2 -0
- package/runtime/scripts/amalgm-mcp/events/store.js +16 -1
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +85 -37
- package/runtime/scripts/amalgm-mcp/local/rest.js +7 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +83 -0
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +42 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +28 -17
- package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
- package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
- package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
- package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
- package/runtime/scripts/amalgm-mcp/tasks/store.js +16 -1
- package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
- package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
- package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
- package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +116 -8
- package/runtime/scripts/chat-core/adapters/claude.js +2 -0
- package/runtime/scripts/chat-core/contract.js +2 -0
- package/runtime/scripts/chat-core/engine.js +77 -19
- package/runtime/scripts/credential-adapter.js +4 -2
- package/runtime/scripts/local-gateway.js +2 -0
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agents storage —
|
|
2
|
+
* Agents storage — SQLite-backed local agent registry + conversation logs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The registry is the Local Live Store source of truth for agent capabilities.
|
|
5
|
+
* Built-in harnesses are seeded as non-deletable rows. Custom agents are rows
|
|
6
|
+
* in the same table, so discovery, snapshots, and agent-to-agent lookup use one
|
|
7
|
+
* shape.
|
|
6
8
|
*/
|
|
7
9
|
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const os = require('os');
|
|
8
12
|
const path = require('path');
|
|
9
|
-
const {
|
|
13
|
+
const {
|
|
14
|
+
AGENTS_FILE,
|
|
15
|
+
AGENT_CONVOS_DIR,
|
|
16
|
+
STORAGE_DIR,
|
|
17
|
+
COMPUTER_RECORD_FILE,
|
|
18
|
+
} = require('../config');
|
|
10
19
|
const {
|
|
11
20
|
ensureDir,
|
|
12
21
|
readJson,
|
|
@@ -14,108 +23,610 @@ const {
|
|
|
14
23
|
appendJsonl,
|
|
15
24
|
tailJsonl,
|
|
16
25
|
} = require('../lib/storage');
|
|
26
|
+
const { openLocalDb } = require('../state/db');
|
|
27
|
+
const { insertStateEvent, publishStateEvent } = require('../state/events');
|
|
28
|
+
|
|
29
|
+
const DEFAULT_AGENT_STATUS = 'unknown';
|
|
30
|
+
const CUSTOM_AGENT_STATUS = 'ready';
|
|
31
|
+
const BUILTIN_AGENT_IDS = ['claude_code', 'codex', 'opencode', 'pi'];
|
|
32
|
+
const AMALGM_COMPUTER_ID = process.env.AMALGM_COMPUTER_ID || '';
|
|
17
33
|
|
|
18
|
-
const
|
|
34
|
+
const BUILTIN_AGENT_BLUEPRINTS = [
|
|
19
35
|
{
|
|
20
36
|
id: 'claude_code',
|
|
21
37
|
name: 'Claude Code',
|
|
22
38
|
description:
|
|
23
39
|
'Anthropic Claude agent — general-purpose coding, research, and reasoning',
|
|
40
|
+
adapter: 'claude_code',
|
|
24
41
|
baseHarnessId: 'claude_code',
|
|
25
42
|
baseModelId: 'anthropic/claude-opus-4.7',
|
|
26
|
-
systemPrompt: '',
|
|
27
|
-
mcp: { inheritAll: true, customServers: [] },
|
|
28
|
-
builtin: true,
|
|
29
43
|
},
|
|
30
44
|
{
|
|
31
45
|
id: 'codex',
|
|
32
46
|
name: 'Codex',
|
|
33
47
|
description: 'OpenAI Codex agent — code generation and analysis',
|
|
48
|
+
adapter: 'codex',
|
|
34
49
|
baseHarnessId: 'codex',
|
|
35
|
-
baseModelId: 'openai/gpt-5.4
|
|
36
|
-
systemPrompt: '',
|
|
37
|
-
mcp: { inheritAll: true, customServers: [] },
|
|
38
|
-
builtin: true,
|
|
50
|
+
baseModelId: 'openai/gpt-5.4',
|
|
39
51
|
},
|
|
40
52
|
{
|
|
41
53
|
id: 'opencode',
|
|
42
54
|
name: 'OpenCode',
|
|
43
55
|
description: 'Open-source agent — multi-provider coding assistant',
|
|
56
|
+
adapter: 'opencode',
|
|
44
57
|
baseHarnessId: 'opencode',
|
|
45
|
-
baseModelId: 'opencode-
|
|
46
|
-
systemPrompt: '',
|
|
47
|
-
mcp: { inheritAll: true, customServers: [] },
|
|
48
|
-
builtin: true,
|
|
58
|
+
baseModelId: 'opencode-anthropic-claude-sonnet-4-6',
|
|
49
59
|
},
|
|
50
60
|
{
|
|
51
61
|
id: 'pi',
|
|
52
62
|
name: 'Pi',
|
|
53
63
|
description: 'Pi agent — multi-provider coding assistant',
|
|
64
|
+
adapter: 'pi',
|
|
54
65
|
baseHarnessId: 'pi',
|
|
55
66
|
baseModelId: 'pi-anthropic-claude-sonnet-4.6',
|
|
56
|
-
systemPrompt: '',
|
|
57
|
-
mcp: { inheritAll: true, customServers: [] },
|
|
58
|
-
builtin: true,
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: 'amp',
|
|
62
|
-
name: 'Amp',
|
|
63
|
-
description: 'Sourcegraph Amp agent — terminal-auth coding assistant',
|
|
64
|
-
baseHarnessId: 'amp',
|
|
65
|
-
baseModelId: 'amp-default',
|
|
66
|
-
systemPrompt: '',
|
|
67
|
-
mcp: { inheritAll: true, customServers: [] },
|
|
68
|
-
builtin: true,
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
id: 'cursor',
|
|
72
|
-
name: 'Cursor',
|
|
73
|
-
description: 'Cursor Agent CLI — account-auth coding assistant',
|
|
74
|
-
baseHarnessId: 'cursor',
|
|
75
|
-
baseModelId: 'cursor/composer-2-fast',
|
|
76
|
-
systemPrompt: '',
|
|
77
|
-
mcp: { inheritAll: true, customServers: [] },
|
|
78
|
-
builtin: true,
|
|
79
67
|
},
|
|
80
68
|
];
|
|
81
69
|
|
|
82
|
-
const
|
|
70
|
+
const BUILTIN_AGENTS = BUILTIN_AGENT_BLUEPRINTS.map((agent) => ({
|
|
71
|
+
...agent,
|
|
72
|
+
systemPrompt: '',
|
|
73
|
+
files: [],
|
|
74
|
+
skills: [],
|
|
75
|
+
mcpAppIds: [],
|
|
76
|
+
nativeMcps: [],
|
|
77
|
+
tools: { mode: 'all', selectedToolIds: [] },
|
|
78
|
+
mcp: { inheritAll: true, customServers: [], appIds: [], nativeMcps: [] },
|
|
79
|
+
builtin: true,
|
|
80
|
+
deletable: false,
|
|
81
|
+
editable: false,
|
|
82
|
+
status: DEFAULT_AGENT_STATUS,
|
|
83
|
+
}));
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{12}$/i;
|
|
86
|
+
|
|
87
|
+
function nowIso() {
|
|
88
|
+
return new Date().toISOString();
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
if (!conversationId) throw new Error('conversation_id must be a UUID');
|
|
92
|
-
return path.join(AGENT_CONVOS_DIR, `${conversationId}.jsonl`);
|
|
91
|
+
function isObject(value) {
|
|
92
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
function
|
|
95
|
+
function safeJsonParse(value, fallback = null) {
|
|
96
|
+
if (typeof value !== 'string' || !value) return fallback;
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(value);
|
|
99
|
+
} catch {
|
|
100
|
+
return fallback;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function nonEmptyString(value) {
|
|
105
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function normalizeStringList(values) {
|
|
109
|
+
if (!Array.isArray(values)) return [];
|
|
110
|
+
return [...new Set(values
|
|
111
|
+
.filter((value) => typeof value === 'string')
|
|
112
|
+
.map((value) => value.trim())
|
|
113
|
+
.filter(Boolean))];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readComputerRecord() {
|
|
117
|
+
return readJson(COMPUTER_RECORD_FILE, {}) || {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function computerId() {
|
|
121
|
+
const record = readComputerRecord();
|
|
122
|
+
return nonEmptyString(AMALGM_COMPUTER_ID)
|
|
123
|
+
|| nonEmptyString(record.computer_id)
|
|
124
|
+
|| nonEmptyString(process.env.AMALGM_CONTAINER_ID)
|
|
125
|
+
|| '';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function computerName() {
|
|
129
|
+
const record = readComputerRecord();
|
|
130
|
+
return nonEmptyString(record.name)
|
|
131
|
+
|| nonEmptyString(process.env.AMALGM_COMPUTER_NAME)
|
|
132
|
+
|| os.hostname()
|
|
133
|
+
|| 'This computer';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeLocation(inputLocation, fallback = {}) {
|
|
137
|
+
const id = nonEmptyString(fallback.ownerComputerId) || computerId();
|
|
138
|
+
const name = nonEmptyString(fallback.locationName) || computerName();
|
|
139
|
+
const raw = isObject(inputLocation) ? inputLocation : {};
|
|
140
|
+
const type = nonEmptyString(raw.type) || 'computer';
|
|
141
|
+
return {
|
|
142
|
+
type,
|
|
143
|
+
...(nonEmptyString(raw.computerId) || id ? { computerId: nonEmptyString(raw.computerId) || id } : {}),
|
|
144
|
+
name: nonEmptyString(raw.name) || nonEmptyString(raw.computerName) || name,
|
|
145
|
+
...(nonEmptyString(raw.endpoint) ? { endpoint: raw.endpoint.trim() } : {}),
|
|
146
|
+
...(nonEmptyString(raw.localUrl) ? { localUrl: raw.localUrl.trim() } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function locationLabel(location) {
|
|
151
|
+
if (isObject(location)) {
|
|
152
|
+
return nonEmptyString(location.name)
|
|
153
|
+
|| nonEmptyString(location.computerName)
|
|
154
|
+
|| nonEmptyString(location.endpoint)
|
|
155
|
+
|| nonEmptyString(location.localUrl)
|
|
156
|
+
|| 'This computer';
|
|
157
|
+
}
|
|
158
|
+
return nonEmptyString(location) || 'This computer';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function defaultCapabilities(agent) {
|
|
162
|
+
return {
|
|
163
|
+
streaming: true,
|
|
164
|
+
resume: true,
|
|
165
|
+
cancel: true,
|
|
166
|
+
tools: agent?.tools?.mode === 'selected' ? 'selected' : 'all',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function normalizeMcpConfig(mcp, mcpAppIds, nativeMcps) {
|
|
171
|
+
return {
|
|
172
|
+
inheritAll: mcp?.inheritAll !== false,
|
|
173
|
+
customServers: Array.isArray(mcp?.customServers) ? mcp.customServers : [],
|
|
174
|
+
appIds: normalizeStringList(mcpAppIds !== undefined ? mcpAppIds : mcp?.appIds),
|
|
175
|
+
nativeMcps: normalizeStringList(nativeMcps !== undefined ? nativeMcps : mcp?.nativeMcps),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeToolConfig(tools, legacyMcp) {
|
|
180
|
+
const raw = isObject(tools) ? tools : null;
|
|
181
|
+
const selectedToolIds = normalizeStringList(
|
|
182
|
+
raw?.selectedToolIds
|
|
183
|
+
?? raw?.selected
|
|
184
|
+
?? [
|
|
185
|
+
...normalizeStringList(legacyMcp?.appIds),
|
|
186
|
+
...normalizeStringList(legacyMcp?.nativeMcps),
|
|
187
|
+
],
|
|
188
|
+
);
|
|
189
|
+
return {
|
|
190
|
+
mode: raw?.mode === 'selected' || (!raw && legacyMcp?.inheritAll === false) ? 'selected' : 'all',
|
|
191
|
+
selectedToolIds,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function normalizeAgent(input, existing) {
|
|
196
|
+
if (!isObject(input)) throw new Error('agent must be an object');
|
|
197
|
+
const id = nonEmptyString(input.id) || `custom-${crypto.randomUUID()}`;
|
|
198
|
+
const builtin = input.builtin === true || BUILTIN_AGENT_IDS.includes(id);
|
|
199
|
+
const baseHarnessId = nonEmptyString(input.baseHarnessId || input.base_harness_id) || id;
|
|
200
|
+
const adapter = nonEmptyString(input.adapter) || baseHarnessId;
|
|
201
|
+
const mcpConfig = normalizeMcpConfig(input.mcp, input.mcpAppIds, input.nativeMcps);
|
|
202
|
+
const toolConfig = normalizeToolConfig(input.tools, mcpConfig);
|
|
203
|
+
const ownerComputerId = nonEmptyString(input.ownerComputerId || input.owner_computer_id)
|
|
204
|
+
|| nonEmptyString(existing?.ownerComputerId)
|
|
205
|
+
|| computerId();
|
|
206
|
+
const location = normalizeLocation(input.location, {
|
|
207
|
+
ownerComputerId,
|
|
208
|
+
locationName: input.locationName || existing?.locationName,
|
|
209
|
+
});
|
|
210
|
+
const createdAt = input.createdAt || existing?.createdAt || nowIso();
|
|
211
|
+
const updatedAt = input.updatedAt || nowIso();
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
id,
|
|
215
|
+
name: nonEmptyString(input.name) || id,
|
|
216
|
+
description: input.description || '',
|
|
217
|
+
adapter,
|
|
218
|
+
baseHarnessId,
|
|
219
|
+
baseModelId: input.baseModelId || existing?.baseModelId || '',
|
|
220
|
+
systemPrompt: input.systemPrompt || '',
|
|
221
|
+
files: normalizeStringList(input.files),
|
|
222
|
+
skills: normalizeStringList(input.skills),
|
|
223
|
+
mcpAppIds: normalizeStringList(mcpConfig.appIds),
|
|
224
|
+
nativeMcps: normalizeStringList(mcpConfig.nativeMcps),
|
|
225
|
+
tools: toolConfig,
|
|
226
|
+
mcp: {
|
|
227
|
+
...mcpConfig,
|
|
228
|
+
inheritAll: toolConfig.mode !== 'selected',
|
|
229
|
+
},
|
|
230
|
+
authMethod: input.authMethod || existing?.authMethod || 'amalgm',
|
|
231
|
+
location,
|
|
232
|
+
locationName: locationLabel(location),
|
|
233
|
+
ownerComputerId,
|
|
234
|
+
status: input.status || existing?.status || (builtin ? DEFAULT_AGENT_STATUS : CUSTOM_AGENT_STATUS),
|
|
235
|
+
installStatus: input.installStatus || existing?.installStatus || (builtin ? 'unknown' : 'external'),
|
|
236
|
+
authStatus: input.authStatus || existing?.authStatus || 'unknown',
|
|
237
|
+
capabilities: isObject(input.capabilities)
|
|
238
|
+
? { ...defaultCapabilities({ tools: toolConfig }), ...input.capabilities }
|
|
239
|
+
: defaultCapabilities({ tools: toolConfig }),
|
|
240
|
+
builtin,
|
|
241
|
+
deletable: input.deletable === false || builtin ? false : true,
|
|
242
|
+
editable: input.editable === false || builtin ? false : true,
|
|
243
|
+
createdAt,
|
|
244
|
+
updatedAt,
|
|
245
|
+
...(input.configDir ? { configDir: input.configDir } : existing?.configDir ? { configDir: existing.configDir } : {}),
|
|
246
|
+
...(input.authDetails ? { authDetails: input.authDetails } : existing?.authDetails ? { authDetails: existing.authDetails } : {}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function semanticAgent(agent) {
|
|
251
|
+
const { updatedAt, ...rest } = agent || {};
|
|
252
|
+
return rest;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function agentsEqualForWrite(left, right) {
|
|
256
|
+
return JSON.stringify(semanticAgent(left)) === JSON.stringify(semanticAgent(right));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function rowToAgent(row) {
|
|
260
|
+
if (!row) return null;
|
|
261
|
+
const parsed = safeJsonParse(row.agent_json, null);
|
|
262
|
+
if (!isObject(parsed)) return null;
|
|
263
|
+
return normalizeAgent({
|
|
264
|
+
...parsed,
|
|
265
|
+
builtin: row.builtin === 1 || parsed.builtin === true,
|
|
266
|
+
deletable: row.deletable === 1 && parsed.deletable !== false,
|
|
267
|
+
}, null);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function readAgentRow(db, id) {
|
|
271
|
+
return rowToAgent(db.prepare('SELECT * FROM agents WHERE id = ?').get(id));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function upsertAgentRow(db, agent) {
|
|
275
|
+
db.prepare(`
|
|
276
|
+
INSERT INTO agents (
|
|
277
|
+
id,
|
|
278
|
+
name,
|
|
279
|
+
adapter,
|
|
280
|
+
base_harness_id,
|
|
281
|
+
location,
|
|
282
|
+
owner_computer_id,
|
|
283
|
+
builtin,
|
|
284
|
+
deletable,
|
|
285
|
+
status,
|
|
286
|
+
updated_at,
|
|
287
|
+
agent_json
|
|
288
|
+
)
|
|
289
|
+
VALUES (
|
|
290
|
+
@id,
|
|
291
|
+
@name,
|
|
292
|
+
@adapter,
|
|
293
|
+
@base_harness_id,
|
|
294
|
+
@location,
|
|
295
|
+
@owner_computer_id,
|
|
296
|
+
@builtin,
|
|
297
|
+
@deletable,
|
|
298
|
+
@status,
|
|
299
|
+
@updated_at,
|
|
300
|
+
@agent_json
|
|
301
|
+
)
|
|
302
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
303
|
+
name = excluded.name,
|
|
304
|
+
adapter = excluded.adapter,
|
|
305
|
+
base_harness_id = excluded.base_harness_id,
|
|
306
|
+
location = excluded.location,
|
|
307
|
+
owner_computer_id = excluded.owner_computer_id,
|
|
308
|
+
builtin = excluded.builtin,
|
|
309
|
+
deletable = excluded.deletable,
|
|
310
|
+
status = excluded.status,
|
|
311
|
+
updated_at = excluded.updated_at,
|
|
312
|
+
agent_json = excluded.agent_json
|
|
313
|
+
`).run({
|
|
314
|
+
id: agent.id,
|
|
315
|
+
name: agent.name,
|
|
316
|
+
adapter: agent.adapter,
|
|
317
|
+
base_harness_id: agent.baseHarnessId,
|
|
318
|
+
location: agent.locationName || locationLabel(agent.location),
|
|
319
|
+
owner_computer_id: agent.ownerComputerId || null,
|
|
320
|
+
builtin: agent.builtin ? 1 : 0,
|
|
321
|
+
deletable: agent.deletable === false ? 0 : 1,
|
|
322
|
+
status: agent.status || DEFAULT_AGENT_STATUS,
|
|
323
|
+
updated_at: agent.updatedAt || nowIso(),
|
|
324
|
+
agent_json: JSON.stringify(agent),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function insertAgentEvent(db, op, agent, options = {}) {
|
|
329
|
+
return insertStateEvent(db, {
|
|
330
|
+
resource: 'agents',
|
|
331
|
+
op,
|
|
332
|
+
id: agent.id,
|
|
333
|
+
value: op === 'delete' ? undefined : agent,
|
|
334
|
+
clientMutationId: options.clientMutationId,
|
|
335
|
+
source: options.source || 'agents',
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function publishEvents(events) {
|
|
340
|
+
for (const event of events) publishStateEvent(event);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function writeAgentsJson(customAgents) {
|
|
344
|
+
ensureDir(STORAGE_DIR);
|
|
345
|
+
writeJsonAtomic(AGENTS_FILE, {
|
|
346
|
+
version: 1,
|
|
347
|
+
agents: customAgents,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function readCustomAgentsFromDb(db = openLocalDb()) {
|
|
352
|
+
return db.prepare(`
|
|
353
|
+
SELECT * FROM agents
|
|
354
|
+
WHERE builtin = 0
|
|
355
|
+
ORDER BY updated_at DESC, id ASC
|
|
356
|
+
`).all()
|
|
357
|
+
.map(rowToAgent)
|
|
358
|
+
.filter(Boolean);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function readAllAgentsFromDb(db = openLocalDb()) {
|
|
362
|
+
return db.prepare('SELECT * FROM agents ORDER BY builtin DESC, updated_at DESC, id ASC')
|
|
363
|
+
.all()
|
|
364
|
+
.map(rowToAgent)
|
|
365
|
+
.filter(Boolean);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function customAgentRowCount(db = openLocalDb()) {
|
|
369
|
+
const row = db.prepare('SELECT COUNT(*) AS count FROM agents WHERE builtin = 0').get();
|
|
370
|
+
return Number(row?.count || 0);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function seedBuiltinAgents(options = {}) {
|
|
374
|
+
const db = openLocalDb();
|
|
375
|
+
const events = db.transaction(() => {
|
|
376
|
+
const inserted = [];
|
|
377
|
+
for (const blueprint of BUILTIN_AGENT_BLUEPRINTS) {
|
|
378
|
+
const existing = readAgentRow(db, blueprint.id);
|
|
379
|
+
const next = normalizeAgent({
|
|
380
|
+
...blueprint,
|
|
381
|
+
systemPrompt: '',
|
|
382
|
+
files: [],
|
|
383
|
+
skills: [],
|
|
384
|
+
mcpAppIds: [],
|
|
385
|
+
nativeMcps: [],
|
|
386
|
+
tools: { mode: 'all', selectedToolIds: [] },
|
|
387
|
+
mcp: { inheritAll: true, customServers: [], appIds: [], nativeMcps: [] },
|
|
388
|
+
builtin: true,
|
|
389
|
+
deletable: false,
|
|
390
|
+
editable: false,
|
|
391
|
+
status: existing?.status || DEFAULT_AGENT_STATUS,
|
|
392
|
+
installStatus: existing?.installStatus || 'unknown',
|
|
393
|
+
authStatus: existing?.authStatus || 'unknown',
|
|
394
|
+
authDetails: existing?.authDetails,
|
|
395
|
+
configDir: existing?.configDir,
|
|
396
|
+
createdAt: existing?.createdAt,
|
|
397
|
+
authMethod: existing?.authMethod || 'amalgm',
|
|
398
|
+
}, existing);
|
|
399
|
+
if (existing && agentsEqualForWrite(existing, next)) continue;
|
|
400
|
+
upsertAgentRow(db, next);
|
|
401
|
+
if (options.emit) {
|
|
402
|
+
inserted.push(insertAgentEvent(db, existing ? 'update' : 'insert', next, {
|
|
403
|
+
source: options.source || 'agents:seed-defaults',
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return inserted;
|
|
408
|
+
})();
|
|
409
|
+
publishEvents(events);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function migrateLegacyAgentsJsonIfNeeded() {
|
|
413
|
+
const db = openLocalDb();
|
|
414
|
+
if (customAgentRowCount(db) > 0) return;
|
|
415
|
+
|
|
416
|
+
const legacy = readJson(AGENTS_FILE, { version: 1, agents: [] });
|
|
417
|
+
const legacyAgents = Array.isArray(legacy?.agents) ? legacy.agents : [];
|
|
418
|
+
if (legacyAgents.length === 0) return;
|
|
419
|
+
|
|
420
|
+
db.transaction(() => {
|
|
421
|
+
for (const agent of legacyAgents) {
|
|
422
|
+
const normalized = normalizeAgent(agent, null);
|
|
423
|
+
if (normalized.builtin) continue;
|
|
424
|
+
upsertAgentRow(db, normalized);
|
|
425
|
+
}
|
|
426
|
+
})();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function ensureAgentRegistry() {
|
|
96
430
|
ensureDir(STORAGE_DIR);
|
|
97
431
|
ensureDir(AGENT_CONVOS_DIR);
|
|
98
|
-
|
|
432
|
+
seedBuiltinAgents();
|
|
433
|
+
migrateLegacyAgentsJsonIfNeeded();
|
|
434
|
+
if (!readJson(AGENTS_FILE, null)) writeAgentsJson(readCustomAgentsFromDb());
|
|
99
435
|
}
|
|
100
436
|
|
|
101
437
|
function loadAgents() {
|
|
102
|
-
|
|
438
|
+
ensureAgentRegistry();
|
|
439
|
+
const agents = readCustomAgentsFromDb();
|
|
440
|
+
writeAgentsJson(agents);
|
|
441
|
+
return { version: 1, agents };
|
|
103
442
|
}
|
|
104
443
|
|
|
105
|
-
function saveAgents(data) {
|
|
106
|
-
|
|
444
|
+
function saveAgents(data, options = {}) {
|
|
445
|
+
ensureAgentRegistry();
|
|
446
|
+
const inputAgents = Array.isArray(data?.agents) ? data.agents : [];
|
|
447
|
+
const db = openLocalDb();
|
|
448
|
+
const events = db.transaction(() => {
|
|
449
|
+
const inserted = [];
|
|
450
|
+
const keepIds = new Set();
|
|
451
|
+
const existing = new Map(readCustomAgentsFromDb(db).map((agent) => [agent.id, agent]));
|
|
452
|
+
for (const input of inputAgents) {
|
|
453
|
+
const normalized = normalizeAgent(input, input?.id ? existing.get(input.id) : null);
|
|
454
|
+
if (normalized.builtin) continue;
|
|
455
|
+
keepIds.add(normalized.id);
|
|
456
|
+
const previous = existing.get(normalized.id);
|
|
457
|
+
if (!previous || !agentsEqualForWrite(previous, normalized)) {
|
|
458
|
+
upsertAgentRow(db, normalized);
|
|
459
|
+
inserted.push(insertAgentEvent(db, previous ? 'update' : 'insert', normalized, {
|
|
460
|
+
source: options.source || 'agents:save',
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
for (const previous of existing.values()) {
|
|
465
|
+
if (keepIds.has(previous.id)) continue;
|
|
466
|
+
db.prepare('DELETE FROM agents WHERE id = ? AND builtin = 0').run(previous.id);
|
|
467
|
+
inserted.push(insertAgentEvent(db, 'delete', previous, {
|
|
468
|
+
source: options.source || 'agents:save',
|
|
469
|
+
}));
|
|
470
|
+
}
|
|
471
|
+
return inserted;
|
|
472
|
+
})();
|
|
473
|
+
const agents = readCustomAgentsFromDb(db);
|
|
474
|
+
writeAgentsJson(agents);
|
|
475
|
+
publishEvents(events);
|
|
476
|
+
return { version: 1, agents };
|
|
107
477
|
}
|
|
108
478
|
|
|
109
479
|
function getAllAgentsWithBuiltins() {
|
|
110
|
-
|
|
111
|
-
|
|
480
|
+
ensureAgentRegistry();
|
|
481
|
+
const agents = readAllAgentsFromDb();
|
|
482
|
+
const builtinOrder = new Map(BUILTIN_AGENT_IDS.map((id, index) => [id, index]));
|
|
483
|
+
return [
|
|
484
|
+
...agents
|
|
485
|
+
.filter((agent) => agent.builtin)
|
|
486
|
+
.sort((a, b) => (builtinOrder.get(a.id) ?? 999) - (builtinOrder.get(b.id) ?? 999)),
|
|
487
|
+
...agents.filter((agent) => !agent.builtin),
|
|
488
|
+
];
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function createAgent(input, options = {}) {
|
|
492
|
+
ensureAgentRegistry();
|
|
493
|
+
const db = openLocalDb();
|
|
494
|
+
const agent = normalizeAgent({
|
|
495
|
+
...input,
|
|
496
|
+
id: input?.id || `custom-${crypto.randomUUID()}`,
|
|
497
|
+
builtin: false,
|
|
498
|
+
deletable: true,
|
|
499
|
+
editable: true,
|
|
500
|
+
status: input?.status || CUSTOM_AGENT_STATUS,
|
|
501
|
+
}, null);
|
|
502
|
+
if (!agent.name || !agent.name.trim()) throw new Error('name is required');
|
|
503
|
+
if (!agent.baseHarnessId) throw new Error('baseHarnessId is required');
|
|
504
|
+
|
|
505
|
+
const duplicate = getAllAgentsWithBuiltins()
|
|
506
|
+
.find((candidate) => candidate.id !== agent.id && candidate.name.toLowerCase() === agent.name.toLowerCase());
|
|
507
|
+
if (duplicate) throw new Error(`An agent named "${agent.name}" already exists`);
|
|
508
|
+
|
|
509
|
+
const event = db.transaction(() => {
|
|
510
|
+
upsertAgentRow(db, agent);
|
|
511
|
+
return insertAgentEvent(db, 'insert', agent, {
|
|
512
|
+
clientMutationId: input?.clientMutationId,
|
|
513
|
+
source: options.source || 'agents:create',
|
|
514
|
+
});
|
|
515
|
+
})();
|
|
516
|
+
writeAgentsJson(readCustomAgentsFromDb(db));
|
|
517
|
+
publishStateEvent(event);
|
|
518
|
+
return agent;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function updateAgent(agentId, updates, options = {}) {
|
|
522
|
+
ensureAgentRegistry();
|
|
523
|
+
const db = openLocalDb();
|
|
524
|
+
const existing = readAgentRow(db, agentId);
|
|
525
|
+
if (!existing) throw new Error(`Agent not found: ${agentId}`);
|
|
526
|
+
if (existing.builtin && options.allowBuiltinUpdate !== true) {
|
|
527
|
+
throw new Error(`Built-in agent "${existing.name}" cannot be edited`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const requestedName = nonEmptyString(updates?.name);
|
|
531
|
+
if (requestedName) {
|
|
532
|
+
const duplicate = getAllAgentsWithBuiltins()
|
|
533
|
+
.find((candidate) => candidate.id !== agentId && candidate.name.toLowerCase() === requestedName.toLowerCase());
|
|
534
|
+
if (duplicate) throw new Error(`An agent named "${requestedName}" already exists`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const hasOwn = (key) => Object.prototype.hasOwnProperty.call(updates || {}, key);
|
|
538
|
+
const merged = {
|
|
539
|
+
...existing,
|
|
540
|
+
...updates,
|
|
541
|
+
id: existing.id,
|
|
542
|
+
builtin: existing.builtin,
|
|
543
|
+
deletable: existing.deletable,
|
|
544
|
+
editable: existing.editable,
|
|
545
|
+
updatedAt: nowIso(),
|
|
546
|
+
};
|
|
547
|
+
if (!hasOwn('tools') && (hasOwn('mcp') || hasOwn('mcpAppIds') || hasOwn('nativeMcps'))) {
|
|
548
|
+
delete merged.tools;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const next = normalizeAgent(merged, existing);
|
|
552
|
+
|
|
553
|
+
if (agentsEqualForWrite(existing, next)) return existing;
|
|
554
|
+
|
|
555
|
+
const event = db.transaction(() => {
|
|
556
|
+
upsertAgentRow(db, next);
|
|
557
|
+
return insertAgentEvent(db, 'update', next, {
|
|
558
|
+
clientMutationId: updates?.clientMutationId,
|
|
559
|
+
source: options.source || 'agents:update',
|
|
560
|
+
});
|
|
561
|
+
})();
|
|
562
|
+
writeAgentsJson(readCustomAgentsFromDb(db));
|
|
563
|
+
publishStateEvent(event);
|
|
564
|
+
return next;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function deleteAgent(agentId, options = {}) {
|
|
568
|
+
ensureAgentRegistry();
|
|
569
|
+
const db = openLocalDb();
|
|
570
|
+
const existing = readAgentRow(db, agentId);
|
|
571
|
+
if (!existing) throw new Error(`Agent not found: ${agentId}`);
|
|
572
|
+
if (existing.builtin || existing.deletable === false) {
|
|
573
|
+
throw new Error(`Built-in agent "${existing.name}" cannot be deleted`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const event = db.transaction(() => {
|
|
577
|
+
db.prepare('DELETE FROM agents WHERE id = ? AND builtin = 0').run(agentId);
|
|
578
|
+
return insertAgentEvent(db, 'delete', existing, {
|
|
579
|
+
source: options.source || 'agents:delete',
|
|
580
|
+
});
|
|
581
|
+
})();
|
|
582
|
+
writeAgentsJson(readCustomAgentsFromDb(db));
|
|
583
|
+
publishStateEvent(event);
|
|
584
|
+
return existing;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function syncDiscoveredAgents(discoveredAgents = [], options = {}) {
|
|
588
|
+
ensureAgentRegistry();
|
|
589
|
+
const discoveredById = new Map(
|
|
590
|
+
(Array.isArray(discoveredAgents) ? discoveredAgents : [])
|
|
591
|
+
.filter((agent) => agent && typeof agent.id === 'string')
|
|
592
|
+
.map((agent) => [agent.id, agent]),
|
|
593
|
+
);
|
|
594
|
+
const db = openLocalDb();
|
|
595
|
+
const events = db.transaction(() => {
|
|
596
|
+
const inserted = [];
|
|
597
|
+
for (const blueprint of BUILTIN_AGENT_BLUEPRINTS) {
|
|
598
|
+
const existing = readAgentRow(db, blueprint.id);
|
|
599
|
+
if (!existing) continue;
|
|
600
|
+
const discovered = discoveredById.get(blueprint.id);
|
|
601
|
+
const next = normalizeAgent({
|
|
602
|
+
...existing,
|
|
603
|
+
status: discovered
|
|
604
|
+
? discovered.hasAuth ? 'ready' : 'auth_needed'
|
|
605
|
+
: 'install_needed',
|
|
606
|
+
installStatus: discovered ? 'installed' : 'missing',
|
|
607
|
+
authStatus: discovered
|
|
608
|
+
? discovered.hasAuth ? 'ready' : 'auth_needed'
|
|
609
|
+
: 'unknown',
|
|
610
|
+
configDir: discovered?.configDir || existing.configDir,
|
|
611
|
+
authDetails: discovered?.authDetails || existing.authDetails,
|
|
612
|
+
updatedAt: nowIso(),
|
|
613
|
+
}, existing);
|
|
614
|
+
if (agentsEqualForWrite(existing, next)) continue;
|
|
615
|
+
upsertAgentRow(db, next);
|
|
616
|
+
inserted.push(insertAgentEvent(db, 'update', next, {
|
|
617
|
+
source: options.source || 'agents:discovery',
|
|
618
|
+
}));
|
|
619
|
+
}
|
|
620
|
+
return inserted;
|
|
621
|
+
})();
|
|
622
|
+
publishEvents(events);
|
|
623
|
+
return getAllAgentsWithBuiltins();
|
|
112
624
|
}
|
|
113
625
|
|
|
114
626
|
function resolveAgent(agentId) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return data.agents.find((a) => a.id === agentId) || null;
|
|
627
|
+
ensureAgentRegistry();
|
|
628
|
+
const db = openLocalDb();
|
|
629
|
+
return readAgentRow(db, agentId);
|
|
119
630
|
}
|
|
120
631
|
|
|
121
632
|
function normalizeAgentLookup(value) {
|
|
@@ -131,6 +642,21 @@ function resolveAgentByNameOrId(value) {
|
|
|
131
642
|
)) || null;
|
|
132
643
|
}
|
|
133
644
|
|
|
645
|
+
function normalizeConversationId(value) {
|
|
646
|
+
const clean = String(value || '').trim();
|
|
647
|
+
return UUID_RE.test(clean) ? clean : null;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function conversationLogPath(sessionId) {
|
|
651
|
+
const conversationId = normalizeConversationId(sessionId);
|
|
652
|
+
if (!conversationId) throw new Error('conversation_id must be a UUID');
|
|
653
|
+
return path.join(AGENT_CONVOS_DIR, `${conversationId}.jsonl`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function ensureAgentsDirs() {
|
|
657
|
+
ensureAgentRegistry();
|
|
658
|
+
}
|
|
659
|
+
|
|
134
660
|
function appendAgentConvoLog(sessionId, entry) {
|
|
135
661
|
appendJsonl(conversationLogPath(sessionId), entry);
|
|
136
662
|
}
|
|
@@ -141,10 +667,15 @@ function readAgentConvoLog(sessionId, limit = 50) {
|
|
|
141
667
|
|
|
142
668
|
module.exports = {
|
|
143
669
|
BUILTIN_AGENTS,
|
|
670
|
+
BUILTIN_AGENT_IDS,
|
|
144
671
|
ensureAgentsDirs,
|
|
145
672
|
loadAgents,
|
|
146
673
|
saveAgents,
|
|
147
674
|
getAllAgentsWithBuiltins,
|
|
675
|
+
createAgent,
|
|
676
|
+
updateAgent,
|
|
677
|
+
deleteAgent,
|
|
678
|
+
syncDiscoveredAgents,
|
|
148
679
|
resolveAgent,
|
|
149
680
|
resolveAgentByNameOrId,
|
|
150
681
|
normalizeConversationId,
|