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.
Files changed (116) hide show
  1. package/README.md +41 -1
  2. package/bin/amalgm.js +8 -2
  3. package/lib/auth-store.js +223 -0
  4. package/lib/cli.js +1132 -0
  5. package/lib/paths.js +30 -0
  6. package/lib/supervisor.js +476 -0
  7. package/lib/tunnel-chat.js +328 -0
  8. package/lib/tunnel-events.js +499 -0
  9. package/package.json +30 -3
  10. package/runtime/README.md +4 -0
  11. package/runtime/lib/chatInput.js +315 -0
  12. package/runtime/lib/harnesses.js +988 -0
  13. package/runtime/lib/local/amalgmStore.js +136 -0
  14. package/runtime/lib/local/credentialResolver.js +425 -0
  15. package/runtime/lib/mcpApps/registry.js +619 -0
  16. package/runtime/package.json +5 -0
  17. package/runtime/scripts/amalgm-mcp/agents/rest.js +144 -0
  18. package/runtime/scripts/amalgm-mcp/agents/store.js +684 -0
  19. package/runtime/scripts/amalgm-mcp/agents/talk.js +1162 -0
  20. package/runtime/scripts/amalgm-mcp/agents/tools.js +221 -0
  21. package/runtime/scripts/amalgm-mcp/artifacts/advertise.js +132 -0
  22. package/runtime/scripts/amalgm-mcp/artifacts/rest.js +103 -0
  23. package/runtime/scripts/amalgm-mcp/artifacts/store.js +157 -0
  24. package/runtime/scripts/amalgm-mcp/artifacts/supervisor.js +402 -0
  25. package/runtime/scripts/amalgm-mcp/artifacts/tools.js +176 -0
  26. package/runtime/scripts/amalgm-mcp/browser/page.js +637 -0
  27. package/runtime/scripts/amalgm-mcp/browser/tools.js +688 -0
  28. package/runtime/scripts/amalgm-mcp/config.js +140 -0
  29. package/runtime/scripts/amalgm-mcp/credentials/rest.js +45 -0
  30. package/runtime/scripts/amalgm-mcp/deps.js +40 -0
  31. package/runtime/scripts/amalgm-mcp/email/inbound.js +215 -0
  32. package/runtime/scripts/amalgm-mcp/events/executor.js +179 -0
  33. package/runtime/scripts/amalgm-mcp/events/ingress.js +113 -0
  34. package/runtime/scripts/amalgm-mcp/events/matcher.js +125 -0
  35. package/runtime/scripts/amalgm-mcp/events/rest.js +200 -0
  36. package/runtime/scripts/amalgm-mcp/events/ring-buffer.js +19 -0
  37. package/runtime/scripts/amalgm-mcp/events/store.js +113 -0
  38. package/runtime/scripts/amalgm-mcp/events/tools.js +306 -0
  39. package/runtime/scripts/amalgm-mcp/events/webhook-url.js +28 -0
  40. package/runtime/scripts/amalgm-mcp/fs/rest.js +293 -0
  41. package/runtime/scripts/amalgm-mcp/index.js +100 -0
  42. package/runtime/scripts/amalgm-mcp/lib/chat-runner.js +167 -0
  43. package/runtime/scripts/amalgm-mcp/lib/email-md.js +288 -0
  44. package/runtime/scripts/amalgm-mcp/lib/mcp-resolver.js +63 -0
  45. package/runtime/scripts/amalgm-mcp/lib/prefs.js +441 -0
  46. package/runtime/scripts/amalgm-mcp/lib/storage.js +92 -0
  47. package/runtime/scripts/amalgm-mcp/lib/supabase.js +118 -0
  48. package/runtime/scripts/amalgm-mcp/lib/tool-result.js +177 -0
  49. package/runtime/scripts/amalgm-mcp/local/rest.js +87 -0
  50. package/runtime/scripts/amalgm-mcp/mcp-connections/rest.js +234 -0
  51. package/runtime/scripts/amalgm-mcp/notify/index.js +107 -0
  52. package/runtime/scripts/amalgm-mcp/server/core-tools.js +20 -0
  53. package/runtime/scripts/amalgm-mcp/server/http.js +377 -0
  54. package/runtime/scripts/amalgm-mcp/server/mcp.js +127 -0
  55. package/runtime/scripts/amalgm-mcp/slack/inbound.js +200 -0
  56. package/runtime/scripts/amalgm-mcp/state/db.js +194 -0
  57. package/runtime/scripts/amalgm-mcp/state/events.js +113 -0
  58. package/runtime/scripts/amalgm-mcp/state/rest.js +64 -0
  59. package/runtime/scripts/amalgm-mcp/state/snapshot.js +76 -0
  60. package/runtime/scripts/amalgm-mcp/tasks/executor.js +225 -0
  61. package/runtime/scripts/amalgm-mcp/tasks/rest.js +110 -0
  62. package/runtime/scripts/amalgm-mcp/tasks/schedule-normalization.js +85 -0
  63. package/runtime/scripts/amalgm-mcp/tasks/scheduler.js +105 -0
  64. package/runtime/scripts/amalgm-mcp/tasks/store.js +154 -0
  65. package/runtime/scripts/amalgm-mcp/tasks/tools.js +391 -0
  66. package/runtime/scripts/amalgm-mcp/toolbox/rest.js +75 -0
  67. package/runtime/scripts/amalgm-mcp/toolbox/runner.js +257 -0
  68. package/runtime/scripts/amalgm-mcp/toolbox/store.js +933 -0
  69. package/runtime/scripts/amalgm-mcp/toolbox/tools.js +269 -0
  70. package/runtime/scripts/amalgm-mcp/user-api-keys/rest.js +105 -0
  71. package/runtime/scripts/amalgm-mcp/workspace/rest.js +497 -0
  72. package/runtime/scripts/chat-core/adapters/claude.js +165 -0
  73. package/runtime/scripts/chat-core/adapters/codex.js +313 -0
  74. package/runtime/scripts/chat-core/adapters/opencode.js +412 -0
  75. package/runtime/scripts/chat-core/auth.js +177 -0
  76. package/runtime/scripts/chat-core/contract.js +328 -0
  77. package/runtime/scripts/chat-core/credentials/store.js +212 -0
  78. package/runtime/scripts/chat-core/egress.js +87 -0
  79. package/runtime/scripts/chat-core/engine.js +253 -0
  80. package/runtime/scripts/chat-core/event-schema.js +231 -0
  81. package/runtime/scripts/chat-core/events.js +190 -0
  82. package/runtime/scripts/chat-core/index.js +11 -0
  83. package/runtime/scripts/chat-core/input.js +50 -0
  84. package/runtime/scripts/chat-core/normalizers/claude.js +450 -0
  85. package/runtime/scripts/chat-core/normalizers/codex.js +380 -0
  86. package/runtime/scripts/chat-core/normalizers/normalizer_spec.md +259 -0
  87. package/runtime/scripts/chat-core/normalizers/opencode.js +552 -0
  88. package/runtime/scripts/chat-core/normalizers/tool_contract.md +123 -0
  89. package/runtime/scripts/chat-core/normalizers/usage_contract.md +304 -0
  90. package/runtime/scripts/chat-core/parts.js +253 -0
  91. package/runtime/scripts/chat-core/recorder.js +65 -0
  92. package/runtime/scripts/chat-core/runtime.js +86 -0
  93. package/runtime/scripts/chat-core/server.js +163 -0
  94. package/runtime/scripts/chat-core/sse.js +196 -0
  95. package/runtime/scripts/chat-core/stores.js +100 -0
  96. package/runtime/scripts/chat-core/tool-display.js +149 -0
  97. package/runtime/scripts/chat-core/tool-shape.js +143 -0
  98. package/runtime/scripts/chat-core/tooling/mcp-bundle.js +161 -0
  99. package/runtime/scripts/chat-core/tooling/mcp-relay.js +97 -0
  100. package/runtime/scripts/chat-core/tooling/native-binaries.js +608 -0
  101. package/runtime/scripts/chat-core/tooling/system-prompt.js +20 -0
  102. package/runtime/scripts/chat-core/usage.js +343 -0
  103. package/runtime/scripts/chat-server/config.js +110 -0
  104. package/runtime/scripts/chat-server/db.js +529 -0
  105. package/runtime/scripts/chat-server/index.js +33 -0
  106. package/runtime/scripts/chat-server/model-catalog.js +327 -0
  107. package/runtime/scripts/chat-server.js +75 -0
  108. package/runtime/scripts/credential-adapter.js +131 -0
  109. package/runtime/scripts/fs-watcher.js +888 -0
  110. package/runtime/scripts/local-gateway.js +854 -0
  111. package/runtime/scripts/platform-context.txt +246 -0
  112. package/runtime/scripts/port-monitor.js +175 -0
  113. package/runtime/scripts/proxy-token-store.js +162 -0
  114. package/runtime/scripts/runtime-auth.js +163 -0
  115. package/runtime/scripts/test-claude-code-models.js +87 -0
  116. 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
+ };