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
@@ -1,12 +1,21 @@
1
1
  /**
2
- * Agents storage — ~/.amalgm/agents.json + built-in defaults.
2
+ * Agents storage — SQLite-backed local agent registry + conversation logs.
3
3
  *
4
- * Built-ins are compile-time (claude_code, codex, opencode, pi, amp, cursor). Custom agents
5
- * persist to agents.json with id prefix "custom-".
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 { AGENTS_FILE, AGENT_CONVOS_DIR, STORAGE_DIR } = require('../config');
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 BUILTIN_AGENTS = [
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-thinking-xhigh',
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-moonshotai-kimi-k2.5',
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 UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
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
- function normalizeConversationId(value) {
85
- const clean = String(value || '').trim();
86
- return UUID_RE.test(clean) ? clean : null;
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 conversationLogPath(sessionId) {
90
- const conversationId = normalizeConversationId(sessionId);
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 ensureAgentsDirs() {
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
- if (!readJson(AGENTS_FILE, null)) writeJsonAtomic(AGENTS_FILE, { version: 1, agents: [] });
432
+ seedBuiltinAgents();
433
+ migrateLegacyAgentsJsonIfNeeded();
434
+ if (!readJson(AGENTS_FILE, null)) writeAgentsJson(readCustomAgentsFromDb());
99
435
  }
100
436
 
101
437
  function loadAgents() {
102
- return readJson(AGENTS_FILE, { version: 1, agents: [] });
438
+ ensureAgentRegistry();
439
+ const agents = readCustomAgentsFromDb();
440
+ writeAgentsJson(agents);
441
+ return { version: 1, agents };
103
442
  }
104
443
 
105
- function saveAgents(data) {
106
- writeJsonAtomic(AGENTS_FILE, data);
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
- const data = loadAgents();
111
- return [...BUILTIN_AGENTS, ...data.agents];
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
- const builtin = BUILTIN_AGENTS.find((a) => a.id === agentId);
116
- if (builtin) return builtin;
117
- const data = loadAgents();
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,