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,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack inbound handoff.
|
|
3
|
+
*
|
|
4
|
+
* The api-proxy verifies Slack, resolves/creates the Supabase session, then
|
|
5
|
+
* forwards a clean Slack payload here. This local route runs the agent through
|
|
6
|
+
* the same chat-server path as email/tasks/events, then asks the proxy to send
|
|
7
|
+
* the Slack reply with the bot token that lives outside the local computer.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
AMALGM_USER_ID,
|
|
14
|
+
DEFAULT_CWD,
|
|
15
|
+
PROXY_BASE_URL,
|
|
16
|
+
PROXY_TOKEN,
|
|
17
|
+
} = require('../config');
|
|
18
|
+
const { runThroughChatServer } = require('../lib/chat-runner');
|
|
19
|
+
const { ensureFreshProxyToken, readProxyToken } = require('../../proxy-token-store');
|
|
20
|
+
const {
|
|
21
|
+
hydrateModelPreferences,
|
|
22
|
+
getSelectedModel,
|
|
23
|
+
resolveModelSelection,
|
|
24
|
+
DEFAULT_SELECTED_MODELS,
|
|
25
|
+
} = require('../lib/prefs');
|
|
26
|
+
const { buildLocalMcpServerConfigs } = require('../lib/mcp-resolver');
|
|
27
|
+
const {
|
|
28
|
+
chatInputToLegacyFields,
|
|
29
|
+
normalizeChatInput,
|
|
30
|
+
} = require('../../../lib/chatInput');
|
|
31
|
+
const credentialAdapter = require('../../credential-adapter');
|
|
32
|
+
|
|
33
|
+
function harnessToAgent(harness) {
|
|
34
|
+
const map = { claude_code: 'claude', codex: 'codex', opencode: 'opencode', pi: 'pi', amp: 'amp', cursor: 'cursor' };
|
|
35
|
+
return map[harness] || 'claude';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function asString(value) {
|
|
39
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isDefaultSessionTitle(title) {
|
|
43
|
+
return ['', 'New Chat', 'New session', 'New code session'].includes(String(title || '').trim());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function resolveBaseHarness(session) {
|
|
47
|
+
const metadata = session?.metadata || {};
|
|
48
|
+
return (
|
|
49
|
+
asString(metadata.baseHarnessId) ||
|
|
50
|
+
asString(metadata.harness) ||
|
|
51
|
+
asString(session?.harness) ||
|
|
52
|
+
'claude_code'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveDefaultAuthMethod(baseHarnessId) {
|
|
57
|
+
if (credentialAdapter.VALID_HARNESS_IDS.includes(baseHarnessId)) {
|
|
58
|
+
return credentialAdapter.getPersistedAuthMode(baseHarnessId);
|
|
59
|
+
}
|
|
60
|
+
return 'amalgm';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function sendSlackReply({ sessionId, channel, threadTs, message }) {
|
|
64
|
+
const replyUrl = PROXY_BASE_URL
|
|
65
|
+
? `${PROXY_BASE_URL.replace(/\/anthropic\/?$/, '')}/slack/reply`
|
|
66
|
+
: null;
|
|
67
|
+
const proxyToken = await ensureFreshProxyToken({ logger: console }) || readProxyToken() || PROXY_TOKEN;
|
|
68
|
+
if (!replyUrl || !proxyToken) {
|
|
69
|
+
throw new Error('Slack reply proxy is not configured.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const res = await fetch(replyUrl, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `Bearer ${proxyToken}`,
|
|
76
|
+
'Content-Type': 'application/json',
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
sessionId,
|
|
80
|
+
channel,
|
|
81
|
+
threadTs,
|
|
82
|
+
text: message,
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const text = await res.text().catch(() => '');
|
|
88
|
+
throw new Error(`Slack reply proxy failed: ${res.status} ${text.slice(0, 200)}`);
|
|
89
|
+
}
|
|
90
|
+
return await res.json().catch(() => ({}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function handleSlackInbound(body, sendJson) {
|
|
94
|
+
const session = body?.session;
|
|
95
|
+
const inbound = body?.inbound || {};
|
|
96
|
+
const sessionId = asString(session?.id);
|
|
97
|
+
const prompt = asString(inbound.text);
|
|
98
|
+
const channel = asString(inbound.channelId);
|
|
99
|
+
|
|
100
|
+
if (!sessionId) return sendJson(400, { error: 'session.id is required' });
|
|
101
|
+
if (!prompt) return sendJson(400, { error: 'inbound.text is required' });
|
|
102
|
+
if (!channel) return sendJson(400, { error: 'inbound.channelId is required' });
|
|
103
|
+
|
|
104
|
+
await hydrateModelPreferences();
|
|
105
|
+
|
|
106
|
+
const metadata = session.metadata || {};
|
|
107
|
+
const baseHarnessId = resolveBaseHarness(session);
|
|
108
|
+
const fallbackModelId =
|
|
109
|
+
asString(metadata.baseModelId) ||
|
|
110
|
+
getSelectedModel(baseHarnessId) ||
|
|
111
|
+
DEFAULT_SELECTED_MODELS[baseHarnessId] ||
|
|
112
|
+
null;
|
|
113
|
+
const defaultAuthMethod = asString(metadata.authMethod) || resolveDefaultAuthMethod(baseHarnessId);
|
|
114
|
+
const cwd = asString(metadata.cwd) || DEFAULT_CWD;
|
|
115
|
+
const computerId = asString(metadata.computerId);
|
|
116
|
+
const slackSystemPrompt = [
|
|
117
|
+
asString(metadata.systemPrompt),
|
|
118
|
+
'You are replying in Slack. Keep responses clear, direct, and Slack-friendly. Use concise Markdown/mrkdwn when helpful.',
|
|
119
|
+
].filter(Boolean).join('\n\n');
|
|
120
|
+
|
|
121
|
+
const normalizedChatInput = normalizeChatInput({
|
|
122
|
+
parts: [{ type: 'text', text: prompt }],
|
|
123
|
+
agent: {
|
|
124
|
+
harness: baseHarnessId,
|
|
125
|
+
model: fallbackModelId,
|
|
126
|
+
authMethod: defaultAuthMethod,
|
|
127
|
+
customAgentId: asString(metadata.agentId) || null,
|
|
128
|
+
systemPrompt: slackSystemPrompt || null,
|
|
129
|
+
},
|
|
130
|
+
tools: {
|
|
131
|
+
mcpAppIds: Array.isArray(metadata.mcpAppIds) ? metadata.mcpAppIds : [],
|
|
132
|
+
},
|
|
133
|
+
execution: {
|
|
134
|
+
cwd,
|
|
135
|
+
computerId,
|
|
136
|
+
},
|
|
137
|
+
}, {
|
|
138
|
+
prompt,
|
|
139
|
+
harness: baseHarnessId,
|
|
140
|
+
modelId: fallbackModelId,
|
|
141
|
+
authMethod: defaultAuthMethod,
|
|
142
|
+
cwd,
|
|
143
|
+
computerId,
|
|
144
|
+
systemPrompt: slackSystemPrompt || undefined,
|
|
145
|
+
mcpAppIds: Array.isArray(metadata.mcpAppIds) ? metadata.mcpAppIds : [],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const legacyChatFields = chatInputToLegacyFields(normalizedChatInput, {
|
|
149
|
+
prompt,
|
|
150
|
+
harness: baseHarnessId,
|
|
151
|
+
modelId: fallbackModelId,
|
|
152
|
+
authMethod: defaultAuthMethod,
|
|
153
|
+
cwd,
|
|
154
|
+
computerId,
|
|
155
|
+
systemPrompt: slackSystemPrompt || undefined,
|
|
156
|
+
});
|
|
157
|
+
const { modelId: uiModelId, cliModel, reasoningEffort } = resolveModelSelection(
|
|
158
|
+
baseHarnessId,
|
|
159
|
+
legacyChatFields.modelId || fallbackModelId,
|
|
160
|
+
);
|
|
161
|
+
const mcpServers = await buildLocalMcpServerConfigs(legacyChatFields.mcpAppIds || []);
|
|
162
|
+
const userMessageId = crypto.randomUUID();
|
|
163
|
+
const assistantMessageId = crypto.randomUUID();
|
|
164
|
+
|
|
165
|
+
const { outputText } = await runThroughChatServer({
|
|
166
|
+
chatInput: normalizedChatInput,
|
|
167
|
+
prompt: legacyChatFields.prompt,
|
|
168
|
+
userId: AMALGM_USER_ID || session.user_id,
|
|
169
|
+
codeSessionId: sessionId,
|
|
170
|
+
assistantMessageId,
|
|
171
|
+
userMessageId,
|
|
172
|
+
userParts: legacyChatFields.userParts,
|
|
173
|
+
agentId: harnessToAgent(baseHarnessId),
|
|
174
|
+
modelId: uiModelId,
|
|
175
|
+
cliModel,
|
|
176
|
+
...(reasoningEffort ? { reasoningEffort } : {}),
|
|
177
|
+
cwd: legacyChatFields.cwd || DEFAULT_CWD,
|
|
178
|
+
authMethod: legacyChatFields.authMethod || defaultAuthMethod,
|
|
179
|
+
mcpServers,
|
|
180
|
+
...(legacyChatFields.systemPrompt ? { systemPrompt: legacyChatFields.systemPrompt } : {}),
|
|
181
|
+
...(!isDefaultSessionTitle(session.title) ? { resumeSessionId: sessionId } : {}),
|
|
182
|
+
origin: 'slack',
|
|
183
|
+
originId: inbound.originId || inbound.providerEventId || inbound.ts || null,
|
|
184
|
+
originName: 'Slack',
|
|
185
|
+
originHarnessId: session.harness || baseHarnessId,
|
|
186
|
+
originBaseHarnessId: baseHarnessId,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const responseText = outputText.trim() || 'Done.';
|
|
190
|
+
await sendSlackReply({
|
|
191
|
+
sessionId,
|
|
192
|
+
channel,
|
|
193
|
+
threadTs: asString(inbound.replyThreadTs),
|
|
194
|
+
message: responseText,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return sendJson(200, { ok: true, sessionId, responseLength: responseText.length });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { handleSlackInbound };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Local Live Store database.
|
|
5
|
+
*
|
|
6
|
+
* SQLite owns the monotonic event sequence. Existing resource stores can keep
|
|
7
|
+
* their current files while they migrate table-by-table; the realtime contract
|
|
8
|
+
* is already backed by this WAL database.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { ensureDir } = require('../lib/storage');
|
|
13
|
+
const { LOCAL_DB_FILE, STORAGE_DIR } = require('../config');
|
|
14
|
+
|
|
15
|
+
let db = null;
|
|
16
|
+
|
|
17
|
+
function getBetterSqlite3() {
|
|
18
|
+
try {
|
|
19
|
+
return require('better-sqlite3');
|
|
20
|
+
} catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
throw new Error(`better-sqlite3 is required for the Local Live Store: ${message}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function openLocalDb() {
|
|
27
|
+
if (db) return db;
|
|
28
|
+
|
|
29
|
+
ensureDir(path.dirname(LOCAL_DB_FILE || STORAGE_DIR));
|
|
30
|
+
const Database = getBetterSqlite3();
|
|
31
|
+
db = new Database(LOCAL_DB_FILE);
|
|
32
|
+
db.pragma('journal_mode = WAL');
|
|
33
|
+
db.pragma('synchronous = NORMAL');
|
|
34
|
+
db.pragma('foreign_keys = ON');
|
|
35
|
+
db.pragma('busy_timeout = 5000');
|
|
36
|
+
migrate(db);
|
|
37
|
+
return db;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function migrate(database = openLocalDb()) {
|
|
41
|
+
database.exec(`
|
|
42
|
+
CREATE TABLE IF NOT EXISTS event_log (
|
|
43
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
45
|
+
resource TEXT NOT NULL,
|
|
46
|
+
op TEXT NOT NULL,
|
|
47
|
+
id TEXT,
|
|
48
|
+
value_json TEXT,
|
|
49
|
+
patch_json TEXT,
|
|
50
|
+
client_mutation_id TEXT,
|
|
51
|
+
source TEXT,
|
|
52
|
+
resource_version INTEGER
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE INDEX IF NOT EXISTS event_log_resource_seq_idx
|
|
56
|
+
ON event_log(resource, seq);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS tools (
|
|
59
|
+
id TEXT PRIMARY KEY,
|
|
60
|
+
name TEXT NOT NULL,
|
|
61
|
+
type TEXT NOT NULL,
|
|
62
|
+
owner TEXT NOT NULL,
|
|
63
|
+
origin TEXT NOT NULL,
|
|
64
|
+
status TEXT NOT NULL,
|
|
65
|
+
updated_at TEXT NOT NULL,
|
|
66
|
+
tool_json TEXT NOT NULL
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX IF NOT EXISTS tools_status_idx
|
|
70
|
+
ON tools(status);
|
|
71
|
+
|
|
72
|
+
CREATE TABLE IF NOT EXISTS tool_actions (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
tool_id TEXT NOT NULL,
|
|
75
|
+
name TEXT NOT NULL,
|
|
76
|
+
status TEXT NOT NULL,
|
|
77
|
+
updated_at TEXT NOT NULL,
|
|
78
|
+
action_json TEXT NOT NULL,
|
|
79
|
+
FOREIGN KEY(tool_id) REFERENCES tools(id) ON DELETE CASCADE
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE INDEX IF NOT EXISTS tool_actions_tool_id_idx
|
|
83
|
+
ON tool_actions(tool_id);
|
|
84
|
+
|
|
85
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
86
|
+
id TEXT PRIMARY KEY,
|
|
87
|
+
name TEXT NOT NULL,
|
|
88
|
+
adapter TEXT NOT NULL,
|
|
89
|
+
base_harness_id TEXT NOT NULL,
|
|
90
|
+
location TEXT NOT NULL,
|
|
91
|
+
owner_computer_id TEXT,
|
|
92
|
+
builtin INTEGER NOT NULL DEFAULT 0,
|
|
93
|
+
deletable INTEGER NOT NULL DEFAULT 1,
|
|
94
|
+
status TEXT NOT NULL,
|
|
95
|
+
updated_at TEXT NOT NULL,
|
|
96
|
+
agent_json TEXT NOT NULL
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE INDEX IF NOT EXISTS agents_base_harness_id_idx
|
|
100
|
+
ON agents(base_harness_id);
|
|
101
|
+
|
|
102
|
+
CREATE INDEX IF NOT EXISTS agents_owner_computer_id_idx
|
|
103
|
+
ON agents(owner_computer_id);
|
|
104
|
+
|
|
105
|
+
CREATE INDEX IF NOT EXISTS agents_builtin_idx
|
|
106
|
+
ON agents(builtin);
|
|
107
|
+
`);
|
|
108
|
+
migrateLegacyToolboxTables(database);
|
|
109
|
+
removeInheritedHarnessTools(database);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function tableExists(database, tableName) {
|
|
113
|
+
const row = database.prepare(
|
|
114
|
+
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?"
|
|
115
|
+
).get(tableName);
|
|
116
|
+
return Boolean(row);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function migrateLegacyToolboxTables(database) {
|
|
120
|
+
if (tableExists(database, 'toolbox_tools')) {
|
|
121
|
+
database.prepare(`
|
|
122
|
+
INSERT OR IGNORE INTO tools (
|
|
123
|
+
id,
|
|
124
|
+
name,
|
|
125
|
+
type,
|
|
126
|
+
owner,
|
|
127
|
+
origin,
|
|
128
|
+
status,
|
|
129
|
+
updated_at,
|
|
130
|
+
tool_json
|
|
131
|
+
)
|
|
132
|
+
SELECT
|
|
133
|
+
id,
|
|
134
|
+
name,
|
|
135
|
+
type,
|
|
136
|
+
owner,
|
|
137
|
+
origin,
|
|
138
|
+
status,
|
|
139
|
+
updated_at,
|
|
140
|
+
tool_json
|
|
141
|
+
FROM toolbox_tools
|
|
142
|
+
`).run();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (tableExists(database, 'toolbox_actions')) {
|
|
146
|
+
database.prepare(`
|
|
147
|
+
INSERT OR IGNORE INTO tool_actions (
|
|
148
|
+
id,
|
|
149
|
+
tool_id,
|
|
150
|
+
name,
|
|
151
|
+
status,
|
|
152
|
+
updated_at,
|
|
153
|
+
action_json
|
|
154
|
+
)
|
|
155
|
+
SELECT
|
|
156
|
+
legacy.id,
|
|
157
|
+
legacy.tool_id,
|
|
158
|
+
legacy.name,
|
|
159
|
+
legacy.status,
|
|
160
|
+
legacy.updated_at,
|
|
161
|
+
legacy.action_json
|
|
162
|
+
FROM toolbox_actions legacy
|
|
163
|
+
INNER JOIN tools ON tools.id = legacy.tool_id
|
|
164
|
+
`).run();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function removeInheritedHarnessTools(database) {
|
|
169
|
+
database.prepare(`
|
|
170
|
+
DELETE FROM tool_actions
|
|
171
|
+
WHERE tool_id IN (
|
|
172
|
+
SELECT id FROM tools
|
|
173
|
+
WHERE owner IN ('codex')
|
|
174
|
+
AND origin = 'catalog'
|
|
175
|
+
)
|
|
176
|
+
`).run();
|
|
177
|
+
|
|
178
|
+
database.prepare(`
|
|
179
|
+
DELETE FROM tools
|
|
180
|
+
WHERE owner IN ('codex')
|
|
181
|
+
AND origin = 'catalog'
|
|
182
|
+
`).run();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function closeLocalDb() {
|
|
186
|
+
if (!db) return;
|
|
187
|
+
db.close();
|
|
188
|
+
db = null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
closeLocalDb,
|
|
193
|
+
openLocalDb,
|
|
194
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
const { openLocalDb } = require('./db');
|
|
5
|
+
|
|
6
|
+
const emitter = new EventEmitter();
|
|
7
|
+
emitter.setMaxListeners(200);
|
|
8
|
+
|
|
9
|
+
function safeJsonStringify(value) {
|
|
10
|
+
if (value === undefined) return null;
|
|
11
|
+
return JSON.stringify(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function safeJsonParse(value) {
|
|
15
|
+
if (typeof value !== 'string' || !value) return undefined;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(value);
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeEventRow(row) {
|
|
24
|
+
if (!row) return null;
|
|
25
|
+
const event = {
|
|
26
|
+
seq: Number(row.seq),
|
|
27
|
+
ts: row.ts,
|
|
28
|
+
resource: row.resource,
|
|
29
|
+
op: row.op,
|
|
30
|
+
};
|
|
31
|
+
if (row.id != null) event.id = row.id;
|
|
32
|
+
if (row.client_mutation_id != null) event.clientMutationId = row.client_mutation_id;
|
|
33
|
+
if (row.source != null) event.source = row.source;
|
|
34
|
+
if (row.resource_version != null) event.version = Number(row.resource_version);
|
|
35
|
+
const value = safeJsonParse(row.value_json);
|
|
36
|
+
const patch = safeJsonParse(row.patch_json);
|
|
37
|
+
if (value !== undefined) event.value = value;
|
|
38
|
+
if (patch !== undefined) event.patch = patch;
|
|
39
|
+
return event;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function insertStateEvent(database, input) {
|
|
43
|
+
if (!input || typeof input.resource !== 'string' || typeof input.op !== 'string') {
|
|
44
|
+
throw new Error('state event requires resource and op');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const info = database.prepare(`
|
|
48
|
+
INSERT INTO event_log (
|
|
49
|
+
resource,
|
|
50
|
+
op,
|
|
51
|
+
id,
|
|
52
|
+
value_json,
|
|
53
|
+
patch_json,
|
|
54
|
+
client_mutation_id,
|
|
55
|
+
source,
|
|
56
|
+
resource_version
|
|
57
|
+
)
|
|
58
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
59
|
+
`).run(
|
|
60
|
+
input.resource,
|
|
61
|
+
input.op,
|
|
62
|
+
input.id ?? null,
|
|
63
|
+
safeJsonStringify(input.value),
|
|
64
|
+
safeJsonStringify(input.patch),
|
|
65
|
+
input.clientMutationId ?? input.client_mutation_id ?? null,
|
|
66
|
+
input.source ?? null,
|
|
67
|
+
input.version ?? input.resourceVersion ?? null,
|
|
68
|
+
);
|
|
69
|
+
return normalizeEventRow(
|
|
70
|
+
database.prepare('SELECT * FROM event_log WHERE seq = ?').get(info.lastInsertRowid),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function publishStateEvent(event) {
|
|
75
|
+
if (event) emitter.emit('event', event);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function currentSeq() {
|
|
79
|
+
const row = openLocalDb()
|
|
80
|
+
.prepare('SELECT COALESCE(MAX(seq), 0) AS seq FROM event_log')
|
|
81
|
+
.get();
|
|
82
|
+
return Number(row?.seq || 0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function appendStateEvent(input) {
|
|
86
|
+
const db = openLocalDb();
|
|
87
|
+
const event = db.transaction(() => insertStateEvent(db, input))();
|
|
88
|
+
publishStateEvent(event);
|
|
89
|
+
return event;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function listEventsAfter(afterSeq, options = {}) {
|
|
93
|
+
const after = Number.isFinite(Number(afterSeq)) ? Number(afterSeq) : 0;
|
|
94
|
+
const limit = Math.max(1, Math.min(Number(options.limit) || 1000, 5000));
|
|
95
|
+
const rows = openLocalDb()
|
|
96
|
+
.prepare('SELECT * FROM event_log WHERE seq > ? ORDER BY seq ASC LIMIT ?')
|
|
97
|
+
.all(after, limit);
|
|
98
|
+
return rows.map(normalizeEventRow).filter(Boolean);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function subscribeStateEvents(callback) {
|
|
102
|
+
emitter.on('event', callback);
|
|
103
|
+
return () => emitter.off('event', callback);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
appendStateEvent,
|
|
108
|
+
currentSeq,
|
|
109
|
+
insertStateEvent,
|
|
110
|
+
listEventsAfter,
|
|
111
|
+
publishStateEvent,
|
|
112
|
+
subscribeStateEvents,
|
|
113
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { buildSnapshot } = require('./snapshot');
|
|
4
|
+
const { listEventsAfter, subscribeStateEvents } = require('./events');
|
|
5
|
+
|
|
6
|
+
function parsePositiveInt(value, fallback) {
|
|
7
|
+
const parsed = Number(value);
|
|
8
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sendSseEvent(res, event) {
|
|
12
|
+
res.write(`id: ${event.seq}\n`);
|
|
13
|
+
res.write('event: state\n');
|
|
14
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function handleSnapshot(query, sendJson) {
|
|
18
|
+
sendJson(200, buildSnapshot(query.resources));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function handleEvents(query, sendJson) {
|
|
22
|
+
const after = parsePositiveInt(query.after, 0);
|
|
23
|
+
const limit = parsePositiveInt(query.limit, 1000);
|
|
24
|
+
const events = listEventsAfter(after, { limit });
|
|
25
|
+
sendJson(200, {
|
|
26
|
+
events,
|
|
27
|
+
seq: events.length > 0 ? events[events.length - 1].seq : after,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function handleStream(req, res, query) {
|
|
32
|
+
const after = parsePositiveInt(query.after, 0);
|
|
33
|
+
|
|
34
|
+
res.writeHead(200, {
|
|
35
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
36
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
37
|
+
Connection: 'keep-alive',
|
|
38
|
+
'X-Accel-Buffering': 'no',
|
|
39
|
+
});
|
|
40
|
+
res.write(': local-live-store connected\n\n');
|
|
41
|
+
|
|
42
|
+
const unsubscribe = subscribeStateEvents((event) => {
|
|
43
|
+
if (event.seq > after) sendSseEvent(res, event);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
for (const event of listEventsAfter(after, { limit: 5000 })) {
|
|
47
|
+
sendSseEvent(res, event);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const heartbeat = setInterval(() => {
|
|
51
|
+
res.write(': heartbeat\n\n');
|
|
52
|
+
}, 25_000);
|
|
53
|
+
|
|
54
|
+
req.on('close', () => {
|
|
55
|
+
clearInterval(heartbeat);
|
|
56
|
+
unsubscribe();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
handleEvents,
|
|
62
|
+
handleSnapshot,
|
|
63
|
+
handleStream,
|
|
64
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { currentSeq } = require('./events');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_RESOURCES = [
|
|
6
|
+
'tasks',
|
|
7
|
+
'event_triggers',
|
|
8
|
+
'agents',
|
|
9
|
+
'artifacts',
|
|
10
|
+
'toolbox',
|
|
11
|
+
'tools',
|
|
12
|
+
'tool_actions',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function normalizeResources(resources) {
|
|
16
|
+
if (!resources) return DEFAULT_RESOURCES;
|
|
17
|
+
const values = Array.isArray(resources)
|
|
18
|
+
? resources
|
|
19
|
+
: String(resources).split(',');
|
|
20
|
+
const clean = values
|
|
21
|
+
.map((value) => String(value || '').trim())
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
return clean.length > 0 ? Array.from(new Set(clean)) : DEFAULT_RESOURCES;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readResource(resource, cache) {
|
|
27
|
+
switch (resource) {
|
|
28
|
+
case 'tasks':
|
|
29
|
+
return require('../tasks/store').loadTasks().tasks;
|
|
30
|
+
case 'event_triggers':
|
|
31
|
+
return require('../events/store').loadEventTriggers().triggers;
|
|
32
|
+
case 'agents':
|
|
33
|
+
return require('../agents/store').getAllAgentsWithBuiltins();
|
|
34
|
+
case 'artifacts':
|
|
35
|
+
return require('../artifacts/store').loadArtifacts().artifacts;
|
|
36
|
+
case 'toolbox': {
|
|
37
|
+
cache.toolbox ||= require('../toolbox/store').readToolbox();
|
|
38
|
+
return cache.toolbox;
|
|
39
|
+
}
|
|
40
|
+
case 'tools': {
|
|
41
|
+
cache.toolbox ||= require('../toolbox/store').readToolbox();
|
|
42
|
+
return cache.toolbox.tools;
|
|
43
|
+
}
|
|
44
|
+
case 'tool_actions': {
|
|
45
|
+
cache.toolbox ||= require('../toolbox/store').readToolbox();
|
|
46
|
+
return cache.toolbox.toolActions;
|
|
47
|
+
}
|
|
48
|
+
default:
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildSnapshot(resourcesInput) {
|
|
54
|
+
const resources = normalizeResources(resourcesInput);
|
|
55
|
+
const beforeSeq = currentSeq();
|
|
56
|
+
const cache = {};
|
|
57
|
+
const data = {};
|
|
58
|
+
|
|
59
|
+
for (const resource of resources) {
|
|
60
|
+
const value = readResource(resource, cache);
|
|
61
|
+
if (value !== undefined) data[resource] = value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const afterSeq = currentSeq();
|
|
65
|
+
return {
|
|
66
|
+
seq: afterSeq,
|
|
67
|
+
stable: beforeSeq === afterSeq,
|
|
68
|
+
resources: data,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = {
|
|
73
|
+
DEFAULT_RESOURCES,
|
|
74
|
+
buildSnapshot,
|
|
75
|
+
normalizeResources,
|
|
76
|
+
};
|