amalgm 0.0.0 → 0.0.32
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 +41 -1
- package/bin/amalgm.js +8 -2
- package/lib/auth-store.js +223 -0
- package/lib/cli.js +1132 -0
- package/lib/paths.js +30 -0
- package/lib/supervisor.js +476 -0
- package/lib/tunnel-chat.js +328 -0
- package/lib/tunnel-events.js +499 -0
- package/package.json +30 -3
- package/runtime/README.md +4 -0
- package/runtime/lib/chatInput.js +315 -0
- package/runtime/lib/harnesses.js +988 -0
- package/runtime/lib/local/amalgmStore.js +136 -0
- package/runtime/lib/local/credentialResolver.js +425 -0
- package/runtime/lib/mcpApps/registry.js +619 -0
- package/runtime/package.json +5 -0
- package/runtime/scripts/amalgm-mcp/agents/rest.js +144 -0
- package/runtime/scripts/amalgm-mcp/agents/store.js +684 -0
- package/runtime/scripts/amalgm-mcp/agents/talk.js +1162 -0
- package/runtime/scripts/amalgm-mcp/agents/tools.js +221 -0
- package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
- package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
- package/runtime/scripts/amalgm-mcp/artifacts/store.js +157 -0
- package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
- package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
- package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
- package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
- package/runtime/scripts/amalgm-mcp/config.js +140 -0
- package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
- package/runtime/scripts/amalgm-mcp/deps.js +40 -0
- package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
- package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
- package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
- package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
- package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
- package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
- package/runtime/scripts/amalgm-mcp/events/store.js +113 -0
- package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
- package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
- package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
- package/runtime/scripts/amalgm-mcp/index.js +100 -0
- package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
- package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
- package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
- package/runtime/scripts/amalgm-mcp/lib/prefs.js +441 -0
- package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
- package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
- package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
- package/runtime/scripts/amalgm-mcp/local/rest.js +87 -0
- package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +234 -0
- package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
- package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
- package/runtime/scripts/amalgm-mcp/server/http.js +377 -0
- package/runtime/scripts/amalgm-mcp/server/mcp.js +127 -0
- package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
- 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/executor.js +225 -0
- package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
- package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
- package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
- package/runtime/scripts/amalgm-mcp/tasks/store.js +154 -0
- package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
- 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/user-api-keys/rest.js +105 -0
- package/runtime/scripts/amalgm-mcp/workspace/rest.js +497 -0
- package/runtime/scripts/chat-core/adapters/claude.js +165 -0
- package/runtime/scripts/chat-core/adapters/codex.js +313 -0
- package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
- package/runtime/scripts/chat-core/auth.js +177 -0
- package/runtime/scripts/chat-core/contract.js +328 -0
- package/runtime/scripts/chat-core/credentials/store.js +212 -0
- package/runtime/scripts/chat-core/egress.js +87 -0
- package/runtime/scripts/chat-core/engine.js +253 -0
- package/runtime/scripts/chat-core/event-schema.js +231 -0
- package/runtime/scripts/chat-core/events.js +190 -0
- package/runtime/scripts/chat-core/index.js +11 -0
- package/runtime/scripts/chat-core/input.js +50 -0
- package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
- package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
- package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
- package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
- package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
- package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
- package/runtime/scripts/chat-core/parts.js +253 -0
- package/runtime/scripts/chat-core/recorder.js +65 -0
- package/runtime/scripts/chat-core/runtime.js +86 -0
- package/runtime/scripts/chat-core/server.js +163 -0
- package/runtime/scripts/chat-core/sse.js +196 -0
- package/runtime/scripts/chat-core/stores.js +100 -0
- package/runtime/scripts/chat-core/tool-display.js +149 -0
- package/runtime/scripts/chat-core/tool-shape.js +143 -0
- package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
- package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
- package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
- package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
- package/runtime/scripts/chat-core/usage.js +343 -0
- package/runtime/scripts/chat-server/config.js +110 -0
- package/runtime/scripts/chat-server/db.js +529 -0
- package/runtime/scripts/chat-server/index.js +33 -0
- package/runtime/scripts/chat-server/model-catalog.js +327 -0
- package/runtime/scripts/chat-server.js +75 -0
- package/runtime/scripts/credential-adapter.js +131 -0
- package/runtime/scripts/fs-watcher.js +888 -0
- package/runtime/scripts/local-gateway.js +854 -0
- package/runtime/scripts/platform-context.txt +246 -0
- package/runtime/scripts/port-monitor.js +175 -0
- package/runtime/scripts/proxy-token-store.js +162 -0
- package/runtime/scripts/runtime-auth.js +163 -0
- package/runtime/scripts/test-claude-code-models.js +87 -0
- package/runtime/tsconfig.json +15 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents storage — SQLite-backed local agent registry + conversation logs.
|
|
3
|
+
*
|
|
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.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const {
|
|
14
|
+
AGENTS_FILE,
|
|
15
|
+
AGENT_CONVOS_DIR,
|
|
16
|
+
STORAGE_DIR,
|
|
17
|
+
COMPUTER_RECORD_FILE,
|
|
18
|
+
} = require('../config');
|
|
19
|
+
const {
|
|
20
|
+
ensureDir,
|
|
21
|
+
readJson,
|
|
22
|
+
writeJsonAtomic,
|
|
23
|
+
appendJsonl,
|
|
24
|
+
tailJsonl,
|
|
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 || '';
|
|
33
|
+
|
|
34
|
+
const BUILTIN_AGENT_BLUEPRINTS = [
|
|
35
|
+
{
|
|
36
|
+
id: 'claude_code',
|
|
37
|
+
name: 'Claude Code',
|
|
38
|
+
description:
|
|
39
|
+
'Anthropic Claude agent — general-purpose coding, research, and reasoning',
|
|
40
|
+
adapter: 'claude_code',
|
|
41
|
+
baseHarnessId: 'claude_code',
|
|
42
|
+
baseModelId: 'anthropic/claude-opus-4.7',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'codex',
|
|
46
|
+
name: 'Codex',
|
|
47
|
+
description: 'OpenAI Codex agent — code generation and analysis',
|
|
48
|
+
adapter: 'codex',
|
|
49
|
+
baseHarnessId: 'codex',
|
|
50
|
+
baseModelId: 'openai/gpt-5.4',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: 'opencode',
|
|
54
|
+
name: 'OpenCode',
|
|
55
|
+
description: 'Open-source agent — multi-provider coding assistant',
|
|
56
|
+
adapter: 'opencode',
|
|
57
|
+
baseHarnessId: 'opencode',
|
|
58
|
+
baseModelId: 'opencode-anthropic-claude-sonnet-4-6',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'pi',
|
|
62
|
+
name: 'Pi',
|
|
63
|
+
description: 'Pi agent — multi-provider coding assistant',
|
|
64
|
+
adapter: 'pi',
|
|
65
|
+
baseHarnessId: 'pi',
|
|
66
|
+
baseModelId: 'pi-anthropic-claude-sonnet-4.6',
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
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
|
+
}));
|
|
84
|
+
|
|
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();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isObject(value) {
|
|
92
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
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() {
|
|
430
|
+
ensureDir(STORAGE_DIR);
|
|
431
|
+
ensureDir(AGENT_CONVOS_DIR);
|
|
432
|
+
seedBuiltinAgents();
|
|
433
|
+
migrateLegacyAgentsJsonIfNeeded();
|
|
434
|
+
if (!readJson(AGENTS_FILE, null)) writeAgentsJson(readCustomAgentsFromDb());
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function loadAgents() {
|
|
438
|
+
ensureAgentRegistry();
|
|
439
|
+
const agents = readCustomAgentsFromDb();
|
|
440
|
+
writeAgentsJson(agents);
|
|
441
|
+
return { version: 1, agents };
|
|
442
|
+
}
|
|
443
|
+
|
|
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 };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function getAllAgentsWithBuiltins() {
|
|
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();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function resolveAgent(agentId) {
|
|
627
|
+
ensureAgentRegistry();
|
|
628
|
+
const db = openLocalDb();
|
|
629
|
+
return readAgentRow(db, agentId);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function normalizeAgentLookup(value) {
|
|
633
|
+
return String(value || '').trim().toLowerCase().replace(/[\s_-]+/g, '');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function resolveAgentByNameOrId(value) {
|
|
637
|
+
const clean = normalizeAgentLookup(value);
|
|
638
|
+
if (!clean) return null;
|
|
639
|
+
return getAllAgentsWithBuiltins().find((agent) => (
|
|
640
|
+
normalizeAgentLookup(agent.id) === clean ||
|
|
641
|
+
normalizeAgentLookup(agent.name) === clean
|
|
642
|
+
)) || null;
|
|
643
|
+
}
|
|
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
|
+
|
|
660
|
+
function appendAgentConvoLog(sessionId, entry) {
|
|
661
|
+
appendJsonl(conversationLogPath(sessionId), entry);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function readAgentConvoLog(sessionId, limit = 50) {
|
|
665
|
+
return tailJsonl(conversationLogPath(sessionId), limit);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
module.exports = {
|
|
669
|
+
BUILTIN_AGENTS,
|
|
670
|
+
BUILTIN_AGENT_IDS,
|
|
671
|
+
ensureAgentsDirs,
|
|
672
|
+
loadAgents,
|
|
673
|
+
saveAgents,
|
|
674
|
+
getAllAgentsWithBuiltins,
|
|
675
|
+
createAgent,
|
|
676
|
+
updateAgent,
|
|
677
|
+
deleteAgent,
|
|
678
|
+
syncDiscoveredAgents,
|
|
679
|
+
resolveAgent,
|
|
680
|
+
resolveAgentByNameOrId,
|
|
681
|
+
normalizeConversationId,
|
|
682
|
+
appendAgentConvoLog,
|
|
683
|
+
readAgentConvoLog,
|
|
684
|
+
};
|