openbot 0.2.11 → 0.2.13

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 (141) hide show
  1. package/.prettierrc +8 -0
  2. package/AGENTS.md +68 -0
  3. package/CONTRIBUTING.md +74 -0
  4. package/LICENSE +21 -0
  5. package/README.md +117 -14
  6. package/dist/agents/system.js +106 -0
  7. package/dist/app/cli.js +27 -0
  8. package/dist/app/config.js +64 -0
  9. package/dist/app/server.js +237 -0
  10. package/dist/app/utils.js +35 -0
  11. package/dist/harness/agent-harness.js +45 -0
  12. package/dist/harness/mcp.js +61 -0
  13. package/dist/harness/orchestrator.js +273 -0
  14. package/dist/harness/process.js +7 -0
  15. package/dist/plugins/ai-sdk.js +141 -0
  16. package/dist/plugins/delegation.js +52 -0
  17. package/dist/plugins/mcp.js +140 -0
  18. package/dist/plugins/storage.js +502 -0
  19. package/dist/plugins/ui.js +47 -0
  20. package/dist/registry/plugins.js +73 -0
  21. package/dist/services/storage.js +724 -0
  22. package/docs/README.md +7 -0
  23. package/docs/agents.md +83 -0
  24. package/docs/architecture.md +34 -0
  25. package/docs/plugins.md +77 -0
  26. package/logo-black.png +0 -0
  27. package/{dist/assets/logo.js → logo-black.svg} +24 -24
  28. package/{dist/ui/sidebar.js → logo-white.svg} +23 -88
  29. package/package.json +10 -9
  30. package/src/agents/system.ts +112 -0
  31. package/src/app/cli.ts +38 -0
  32. package/src/app/config.ts +104 -0
  33. package/src/app/server.ts +284 -0
  34. package/src/app/types.ts +476 -0
  35. package/src/app/utils.ts +43 -0
  36. package/src/assets/icon.svg +1 -0
  37. package/src/harness/agent-harness.ts +58 -0
  38. package/src/harness/mcp.ts +78 -0
  39. package/src/harness/orchestrator.ts +342 -0
  40. package/src/harness/process.ts +9 -0
  41. package/src/harness/types.ts +34 -0
  42. package/src/plugins/ai-sdk.ts +197 -0
  43. package/src/plugins/delegation.ts +60 -0
  44. package/src/plugins/mcp.ts +154 -0
  45. package/src/plugins/storage.ts +725 -0
  46. package/src/plugins/ui.ts +57 -0
  47. package/src/registry/plugins.ts +85 -0
  48. package/src/services/storage.ts +957 -0
  49. package/tsconfig.json +18 -0
  50. package/dist/agents/agent-creator.js +0 -74
  51. package/dist/agents/browser-agent.js +0 -31
  52. package/dist/agents/os-agent.js +0 -32
  53. package/dist/agents/planner-agent.js +0 -32
  54. package/dist/agents/topic-agent.js +0 -46
  55. package/dist/architecture/execution-engine.js +0 -151
  56. package/dist/architecture/intent-classifier.js +0 -26
  57. package/dist/architecture/planner.js +0 -106
  58. package/dist/automation-worker.js +0 -121
  59. package/dist/automations.js +0 -52
  60. package/dist/cli.js +0 -275
  61. package/dist/config.js +0 -53
  62. package/dist/core/agents.js +0 -41
  63. package/dist/core/delegation.js +0 -230
  64. package/dist/core/manager.js +0 -96
  65. package/dist/core/plugins.js +0 -74
  66. package/dist/core/router.js +0 -191
  67. package/dist/handlers/init.js +0 -29
  68. package/dist/handlers/session-change.js +0 -21
  69. package/dist/handlers/settings.js +0 -47
  70. package/dist/handlers/tab-change.js +0 -14
  71. package/dist/installers.js +0 -156
  72. package/dist/marketplace.js +0 -80
  73. package/dist/model-catalog.js +0 -132
  74. package/dist/model-defaults.js +0 -25
  75. package/dist/models.js +0 -47
  76. package/dist/open-bot.js +0 -51
  77. package/dist/orchestrator/direct-invocation.js +0 -13
  78. package/dist/orchestrator/events.js +0 -36
  79. package/dist/orchestrator/state.js +0 -54
  80. package/dist/orchestrator.js +0 -422
  81. package/dist/plugins/agent/index.js +0 -81
  82. package/dist/plugins/approval/index.js +0 -100
  83. package/dist/plugins/brain/identity.js +0 -77
  84. package/dist/plugins/brain/index.js +0 -204
  85. package/dist/plugins/brain/memory.js +0 -120
  86. package/dist/plugins/brain/prompt.js +0 -46
  87. package/dist/plugins/brain/types.js +0 -45
  88. package/dist/plugins/brain/ui.js +0 -7
  89. package/dist/plugins/browser/index.js +0 -629
  90. package/dist/plugins/browser/ui.js +0 -13
  91. package/dist/plugins/file-system/index.js +0 -171
  92. package/dist/plugins/file-system/ui.js +0 -6
  93. package/dist/plugins/llm/context-budget.js +0 -139
  94. package/dist/plugins/llm/context-shaping.js +0 -177
  95. package/dist/plugins/llm/index.js +0 -380
  96. package/dist/plugins/memory/index.js +0 -220
  97. package/dist/plugins/memory/memory.js +0 -122
  98. package/dist/plugins/memory/prompt.js +0 -55
  99. package/dist/plugins/memory/types.js +0 -45
  100. package/dist/plugins/meta-agent/index.js +0 -570
  101. package/dist/plugins/meta-agent/ui.js +0 -11
  102. package/dist/plugins/shell/index.js +0 -100
  103. package/dist/plugins/shell/ui.js +0 -6
  104. package/dist/plugins/skills/index.js +0 -286
  105. package/dist/plugins/skills/types.js +0 -50
  106. package/dist/plugins/skills/ui.js +0 -12
  107. package/dist/registry/agent-registry.js +0 -35
  108. package/dist/registry/index.js +0 -2
  109. package/dist/registry/plugin-loader.js +0 -499
  110. package/dist/registry/plugin-registry.js +0 -44
  111. package/dist/registry/ts-agent-loader.js +0 -82
  112. package/dist/registry/yaml-agent-loader.js +0 -246
  113. package/dist/runtime/execution-trace.js +0 -41
  114. package/dist/runtime/intent-routing.js +0 -26
  115. package/dist/runtime/openbot-runtime.js +0 -354
  116. package/dist/server.js +0 -890
  117. package/dist/session.js +0 -179
  118. package/dist/ui/block.js +0 -12
  119. package/dist/ui/header.js +0 -52
  120. package/dist/ui/layout.js +0 -26
  121. package/dist/ui/navigation.js +0 -15
  122. package/dist/ui/settings.js +0 -106
  123. package/dist/ui/skills.js +0 -7
  124. package/dist/ui/thread.js +0 -16
  125. package/dist/ui/widgets/action-list.js +0 -2
  126. package/dist/ui/widgets/approval-card.js +0 -9
  127. package/dist/ui/widgets/code-snippet.js +0 -2
  128. package/dist/ui/widgets/data-block.js +0 -2
  129. package/dist/ui/widgets/data-table.js +0 -2
  130. package/dist/ui/widgets/delegation.js +0 -29
  131. package/dist/ui/widgets/empty-state.js +0 -2
  132. package/dist/ui/widgets/index.js +0 -23
  133. package/dist/ui/widgets/inquiry.js +0 -7
  134. package/dist/ui/widgets/key-value.js +0 -2
  135. package/dist/ui/widgets/progress-step.js +0 -2
  136. package/dist/ui/widgets/resource-card.js +0 -2
  137. package/dist/ui/widgets/status.js +0 -2
  138. package/dist/ui/widgets/todo-list.js +0 -2
  139. package/dist/version.js +0 -62
  140. /package/dist/{types.js → app/types.js} +0 -0
  141. /package/dist/{architecture/contracts.js → harness/types.js} +0 -0
@@ -0,0 +1,724 @@
1
+ import { DEFAULT_AGENTS_DIR, DEFAULT_BASE_DIR, DEFAULT_CHANNELS_DIR, DEFAULT_PLUGINS_DIR, loadConfig, resolvePath, VARIABLES_FILE, } from '../app/config.js';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import matter from 'gray-matter';
6
+ import { getSystemAgentDetails } from '../agents/system.js';
7
+ import { pathToFileURL } from 'node:url';
8
+ const mapNameToPlugin = (name, description, kind = 'tool', image) => ({
9
+ id: name,
10
+ name,
11
+ description,
12
+ kind,
13
+ image,
14
+ createdAt: new Date(),
15
+ updatedAt: new Date(),
16
+ });
17
+ const resolveBaseDir = () => {
18
+ const config = loadConfig();
19
+ return resolvePath(config.baseDir || DEFAULT_BASE_DIR);
20
+ };
21
+ const ENTITY_SVG_CANDIDATE_NAMES = ['avatar.svg', 'icon.svg', 'image.svg', 'logo.svg'];
22
+ const toSvgDataUrl = (svg) => `data:image/svg+xml;base64,${Buffer.from(svg, 'utf-8').toString('base64')}`;
23
+ const tryReadSvgDataUrl = async (filePath) => {
24
+ try {
25
+ const svg = await fs.readFile(filePath, 'utf-8');
26
+ const trimmed = svg.trim();
27
+ if (!trimmed.startsWith('<svg'))
28
+ return null;
29
+ return toSvgDataUrl(trimmed);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ };
35
+ /**
36
+ * Auto-discovers an entity SVG avatar and returns it as a data URL.
37
+ *
38
+ * Search order:
39
+ * 1) <entity>/assets/avatar.svg|icon.svg|image.svg|logo.svg
40
+ * 2) <entity>/avatar.svg|icon.svg|image.svg|logo.svg
41
+ * 3) first *.svg in <entity>/assets
42
+ * 4) first *.svg in <entity>
43
+ */
44
+ const resolveEntityImageDataUrl = async (entityDir) => {
45
+ const preferredDirs = [path.join(entityDir, 'assets'), entityDir];
46
+ for (const dir of preferredDirs) {
47
+ for (const fileName of ENTITY_SVG_CANDIDATE_NAMES) {
48
+ const dataUrl = await tryReadSvgDataUrl(path.join(dir, fileName));
49
+ if (dataUrl)
50
+ return dataUrl;
51
+ }
52
+ }
53
+ for (const dir of preferredDirs) {
54
+ try {
55
+ const entries = await fs.readdir(dir, { withFileTypes: true });
56
+ const firstSvg = entries.find((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.svg'));
57
+ if (!firstSvg)
58
+ continue;
59
+ const dataUrl = await tryReadSvgDataUrl(path.join(dir, firstSvg.name));
60
+ if (dataUrl)
61
+ return dataUrl;
62
+ }
63
+ catch {
64
+ // ignore missing/unreadable folders
65
+ }
66
+ }
67
+ return undefined;
68
+ };
69
+ const getConversationDir = (channelId, threadId) => {
70
+ const base = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR + '/' + channelId);
71
+ return threadId ? `${base}/threads/${threadId}` : base;
72
+ };
73
+ const getLastReadFilePath = () => path.join(resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR), '_meta', 'last-read.json');
74
+ const THREAD_TITLE_MAX_LENGTH = 80;
75
+ const buildThreadTitleFromEvent = (event) => {
76
+ let rawContent = '';
77
+ if (event.type === 'user:input' && typeof event.data?.content === 'string') {
78
+ rawContent = event.data.content;
79
+ }
80
+ else if (event.type === 'agent:invoke' &&
81
+ event.data?.role === 'user' &&
82
+ typeof event.data.content === 'string') {
83
+ rawContent = event.data.content;
84
+ }
85
+ const normalized = rawContent.replace(/\s+/g, ' ').trim();
86
+ if (!normalized)
87
+ return undefined;
88
+ if (normalized.length <= THREAD_TITLE_MAX_LENGTH) {
89
+ return normalized;
90
+ }
91
+ return `${normalized.slice(0, THREAD_TITLE_MAX_LENGTH).trimEnd()}...`;
92
+ };
93
+ const readJsonFile = async (filePath, fallback) => {
94
+ try {
95
+ return JSON.parse(await fs.readFile(filePath, 'utf-8'));
96
+ }
97
+ catch (e) {
98
+ if (e?.code === 'ENOENT')
99
+ return fallback;
100
+ throw e;
101
+ }
102
+ };
103
+ const toVariablesRecord = (raw) => {
104
+ if (!raw || typeof raw !== 'object') {
105
+ return {};
106
+ }
107
+ // Current format: { version: number, variables: StoredVariable[] }
108
+ if ('variables' in raw && Array.isArray(raw.variables)) {
109
+ const entries = raw.variables
110
+ .filter((variable) => typeof variable?.key === 'string')
111
+ .map((variable) => [variable.key, String(variable.value ?? '')]);
112
+ return Object.fromEntries(entries);
113
+ }
114
+ // Legacy format: { [key: string]: string }
115
+ return Object.fromEntries(Object.entries(raw).map(([key, value]) => [
116
+ key,
117
+ String(value ?? ''),
118
+ ]));
119
+ };
120
+ const listBuiltInPlugins = async () => {
121
+ return [
122
+ mapNameToPlugin('storage', 'Built-in storage plugin'),
123
+ mapNameToPlugin('ai-sdk', 'Built-in AI SDK plugin', 'runtime'),
124
+ mapNameToPlugin('delegation', 'Built-in delegation plugin'),
125
+ ];
126
+ };
127
+ const listPluginsFromDisk = async () => {
128
+ const pluginsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_PLUGINS_DIR);
129
+ try {
130
+ await fs.access(pluginsDir);
131
+ }
132
+ catch {
133
+ await fs.mkdir(pluginsDir, { recursive: true });
134
+ }
135
+ const plugins = (await fs.readdir(pluginsDir, { withFileTypes: true }))
136
+ .filter((entry) => !entry.name.startsWith('.') && (entry.isDirectory() || entry.isSymbolicLink()))
137
+ .map(async (entry) => {
138
+ // get dist/index module and find inside module.plugin.description
139
+ const module = await import(pathToFileURL(`${pluginsDir}/${entry.name}/dist/index.js`).href);
140
+ const pluginDir = path.join(pluginsDir, entry.name);
141
+ const image = await resolveEntityImageDataUrl(pluginDir);
142
+ return mapNameToPlugin(module.plugin.name || entry.name, module.plugin.description || '', module.plugin.kind || 'tool', image);
143
+ });
144
+ return Promise.all(plugins);
145
+ };
146
+ export const storageService = {
147
+ getLastReadByChannel: async () => {
148
+ return readJsonFile(getLastReadFilePath(), {});
149
+ },
150
+ setLastReadForChannel: async ({ channelId, lastReadEventId, }) => {
151
+ const p = getLastReadFilePath();
152
+ await fs.mkdir(path.dirname(p), { recursive: true });
153
+ const map = await readJsonFile(p, {});
154
+ map[channelId] = lastReadEventId;
155
+ await fs.writeFile(p, JSON.stringify(map, null, 2), 'utf-8');
156
+ },
157
+ getChannels: async () => {
158
+ const channelsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR);
159
+ try {
160
+ await fs.access(channelsDir);
161
+ }
162
+ catch {
163
+ await fs.mkdir(channelsDir, { recursive: true });
164
+ }
165
+ const channelNames = (await fs.readdir(channelsDir)).filter((name) => !name.startsWith('.') && name !== '_meta');
166
+ const lastReadByChannel = await storageService.getLastReadByChannel();
167
+ const channels = await Promise.all(channelNames.map(async (name) => {
168
+ const channelDir = getConversationDir(name);
169
+ const statePath = path.join(channelDir, 'state.json');
170
+ let cwd;
171
+ try {
172
+ const stateContent = await fs.readFile(statePath, 'utf-8');
173
+ const state = JSON.parse(stateContent);
174
+ cwd = typeof state.cwd === 'string' ? state.cwd : undefined;
175
+ }
176
+ catch {
177
+ // Ignore if state.json is missing or invalid
178
+ }
179
+ const channel = {
180
+ id: name,
181
+ name: name,
182
+ description: '',
183
+ cwd,
184
+ createdAt: new Date(),
185
+ updatedAt: new Date(),
186
+ };
187
+ const rid = lastReadByChannel[name];
188
+ try {
189
+ const events = await storageService.getEvents({ channelId: name });
190
+ const latestId = events[events.length - 1]?.id;
191
+ channel.hasUnseenMessages = !!(latestId && latestId !== rid);
192
+ }
193
+ catch {
194
+ channel.hasUnseenMessages = false;
195
+ }
196
+ try {
197
+ // Fetch up to 5 most recent threads for the sidebar
198
+ const allThreads = await storageService.getThreads({ channelId: name });
199
+ channel.recentThreads = allThreads
200
+ .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
201
+ .slice(0, 5);
202
+ }
203
+ catch {
204
+ channel.recentThreads = [];
205
+ }
206
+ return channel;
207
+ }));
208
+ return channels;
209
+ },
210
+ createChannel: async ({ channelId, spec, initialState, cwd, }) => {
211
+ const normalizedChannelId = channelId.trim();
212
+ if (!normalizedChannelId) {
213
+ throw new Error('channelId is required');
214
+ }
215
+ const channelDir = getConversationDir(normalizedChannelId);
216
+ const specPath = `${channelDir}/SPEC.md`;
217
+ const statePath = `${channelDir}/state.json`;
218
+ try {
219
+ await fs.access(channelDir);
220
+ throw new Error(`Channel "${normalizedChannelId}" already exists`);
221
+ }
222
+ catch (error) {
223
+ if (error.code !== 'ENOENT') {
224
+ throw error;
225
+ }
226
+ }
227
+ const finalState = {
228
+ ...(initialState || {}),
229
+ };
230
+ if (cwd) {
231
+ finalState.cwd = cwd;
232
+ }
233
+ await fs.mkdir(channelDir, { recursive: true });
234
+ await fs.writeFile(specPath, spec?.trim() ||
235
+ `# ${normalizedChannelId}\n\nDefine the goals and rules for this channel here.\n`);
236
+ await fs.writeFile(statePath, JSON.stringify(finalState, null, 2));
237
+ },
238
+ createThread: async ({ channelId, threadId, threadTitle, spec, initialState, }) => {
239
+ const normalizedChannelId = channelId.trim();
240
+ const normalizedThreadId = threadId.trim();
241
+ if (!normalizedChannelId) {
242
+ throw new Error('channelId is required');
243
+ }
244
+ if (!normalizedThreadId) {
245
+ throw new Error('threadId is required');
246
+ }
247
+ const threadDir = getConversationDir(normalizedChannelId, normalizedThreadId);
248
+ const specPath = `${threadDir}/SPEC.md`;
249
+ const statePath = `${threadDir}/state.json`;
250
+ try {
251
+ await fs.access(threadDir);
252
+ throw new Error(`Thread "${normalizedThreadId}" already exists in channel "${normalizedChannelId}"`);
253
+ }
254
+ catch (error) {
255
+ if (error.code !== 'ENOENT') {
256
+ throw error;
257
+ }
258
+ }
259
+ const baseState = { ...(initialState || {}) };
260
+ if (threadTitle?.trim()) {
261
+ baseState.generatedName = threadTitle.trim();
262
+ }
263
+ await fs.mkdir(threadDir, { recursive: true });
264
+ await fs.writeFile(specPath, spec?.trim() ||
265
+ `# ${normalizedThreadId}\n\nDefine the goals and plan for this thread here.\n`);
266
+ await fs.writeFile(statePath, JSON.stringify(baseState, null, 2));
267
+ },
268
+ getThreads: async ({ channelId }) => {
269
+ const threadsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR + '/' + channelId + '/threads');
270
+ try {
271
+ await fs.access(threadsDir);
272
+ }
273
+ catch {
274
+ return [];
275
+ }
276
+ const threadNames = (await fs.readdir(threadsDir)).filter((name) => !name.startsWith('.'));
277
+ const threads = await Promise.all(threadNames.map(async (name) => {
278
+ const threadPath = path.join(threadsDir, name);
279
+ const stats = await fs.stat(threadPath);
280
+ const threadStatePath = path.join(threadPath, 'state.json');
281
+ let threadDisplayName = name;
282
+ try {
283
+ const threadStateRaw = await fs.readFile(threadStatePath, 'utf-8');
284
+ const threadState = JSON.parse(threadStateRaw);
285
+ const generatedName = typeof threadState.generatedName === 'string' ? threadState.generatedName.trim() : '';
286
+ if (generatedName) {
287
+ threadDisplayName = generatedName;
288
+ }
289
+ }
290
+ catch (error) {
291
+ if (error.code !== 'ENOENT') {
292
+ console.error(`Failed to read thread state for channel ${channelId} thread ${name}`, error);
293
+ }
294
+ }
295
+ return {
296
+ id: name,
297
+ name: threadDisplayName,
298
+ channelId,
299
+ createdAt: stats.birthtime,
300
+ updatedAt: stats.mtime,
301
+ };
302
+ }));
303
+ return threads;
304
+ },
305
+ getThreadDetails: async ({ channelId, threadId, }) => {
306
+ const threadDir = getConversationDir(channelId, threadId);
307
+ const specPath = `${threadDir}/SPEC.md`;
308
+ const statePath = `${threadDir}/state.json`;
309
+ let spec = '';
310
+ try {
311
+ spec = await fs.readFile(specPath, 'utf-8');
312
+ }
313
+ catch (error) {
314
+ if (error.code !== 'ENOENT') {
315
+ console.error(`Failed to read thread spec for channel ${channelId} thread ${threadId}`, error);
316
+ }
317
+ }
318
+ let state = {};
319
+ try {
320
+ const stateContent = await fs.readFile(statePath, 'utf-8');
321
+ state = JSON.parse(stateContent);
322
+ }
323
+ catch (error) {
324
+ if (error.code !== 'ENOENT') {
325
+ console.error(`Failed to read thread state for channel ${channelId} thread ${threadId}`, error);
326
+ }
327
+ }
328
+ const generatedName = typeof state.generatedName === 'string'
329
+ ? state.generatedName.trim()
330
+ : '';
331
+ return {
332
+ id: threadId,
333
+ name: generatedName || threadId,
334
+ channelId,
335
+ spec,
336
+ state,
337
+ };
338
+ },
339
+ getChannelDetails: async ({ channelId }) => {
340
+ const channelDir = getConversationDir(channelId);
341
+ const specPath = `${channelDir}/SPEC.md`;
342
+ const statePath = `${channelDir}/state.json`;
343
+ let spec = '';
344
+ try {
345
+ spec = await fs.readFile(specPath, 'utf-8');
346
+ }
347
+ catch (error) {
348
+ if (error.code !== 'ENOENT') {
349
+ console.error(`Failed to read spec file for channel ${channelId}`, error);
350
+ }
351
+ }
352
+ let state = {};
353
+ try {
354
+ const stateContent = await fs.readFile(statePath, 'utf-8');
355
+ state = JSON.parse(stateContent);
356
+ }
357
+ catch (error) {
358
+ if (error.code !== 'ENOENT') {
359
+ console.error(`Failed to read state file for channel ${channelId}`, error);
360
+ }
361
+ }
362
+ const details = {
363
+ id: channelId,
364
+ name: channelId,
365
+ spec,
366
+ state,
367
+ cwd: typeof state.cwd === 'string' ? state.cwd : undefined,
368
+ };
369
+ details.threads = await storageService.getThreads({ channelId });
370
+ return details;
371
+ },
372
+ patchChannelState: async ({ channelId, state: patch, }) => {
373
+ const channelDir = getConversationDir(channelId);
374
+ const statePath = `${channelDir}/state.json`;
375
+ try {
376
+ // 1. Fetch current details to get the existing state
377
+ const currentDetails = await storageService.getChannelDetails({ channelId });
378
+ const currentState = currentDetails.state || {};
379
+ // 2. Perform a shallow merge (patch)
380
+ const newState = {
381
+ ...currentState,
382
+ ...patch,
383
+ };
384
+ // 3. Write back the merged state
385
+ await fs.mkdir(channelDir, { recursive: true });
386
+ await fs.writeFile(statePath, JSON.stringify(newState, null, 2));
387
+ }
388
+ catch (error) {
389
+ console.error(`Failed to patch channel state for channel ${channelId}`, error);
390
+ throw error;
391
+ }
392
+ },
393
+ patchThreadState: async ({ channelId, threadId, state: patch, }) => {
394
+ const threadDir = getConversationDir(channelId, threadId);
395
+ const statePath = `${threadDir}/state.json`;
396
+ try {
397
+ // 1. Fetch current details to get the existing state
398
+ const currentDetails = await storageService.getThreadDetails({ channelId, threadId });
399
+ const currentState = currentDetails.state || {};
400
+ // 2. Perform a shallow merge (patch)
401
+ const newState = {
402
+ ...currentState,
403
+ ...patch,
404
+ };
405
+ // 3. Write back the merged state
406
+ await fs.mkdir(threadDir, { recursive: true });
407
+ await fs.writeFile(statePath, JSON.stringify(newState, null, 2));
408
+ }
409
+ catch (error) {
410
+ console.error(`Failed to patch thread state for channel ${channelId} thread ${threadId}`, error);
411
+ throw error;
412
+ }
413
+ },
414
+ patchChannelSpec: async ({ channelId, spec, }) => {
415
+ const channelDir = getConversationDir(channelId);
416
+ const specPath = `${channelDir}/SPEC.md`;
417
+ try {
418
+ await fs.mkdir(channelDir, { recursive: true });
419
+ await fs.writeFile(specPath, spec);
420
+ }
421
+ catch (error) {
422
+ console.error(`Failed to patch channel spec for channel ${channelId}`, error);
423
+ throw error;
424
+ }
425
+ },
426
+ patchThreadSpec: async ({ channelId, threadId, spec, }) => {
427
+ const threadDir = getConversationDir(channelId, threadId);
428
+ const specPath = `${threadDir}/SPEC.md`;
429
+ try {
430
+ await fs.mkdir(threadDir, { recursive: true });
431
+ await fs.writeFile(specPath, spec);
432
+ }
433
+ catch (error) {
434
+ console.error(`Failed to patch thread spec for channel ${channelId} thread ${threadId}`, error);
435
+ throw error;
436
+ }
437
+ },
438
+ getAgents: async () => {
439
+ const agentsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_AGENTS_DIR);
440
+ try {
441
+ await fs.access(agentsDir);
442
+ }
443
+ catch {
444
+ await fs.mkdir(agentsDir, { recursive: true });
445
+ }
446
+ const agentIds = (await fs.readdir(agentsDir)).filter((name) => !name.startsWith('.'));
447
+ const agents = await Promise.all(agentIds.map(async (id) => {
448
+ try {
449
+ const details = await storageService.getAgentDetails({ agentId: id });
450
+ return {
451
+ id,
452
+ name: details.name || id,
453
+ description: details.description || '',
454
+ image: details.image,
455
+ runtime: details.runtime,
456
+ createdAt: details.createdAt,
457
+ updatedAt: details.updatedAt,
458
+ };
459
+ }
460
+ catch {
461
+ return {
462
+ id,
463
+ name: id,
464
+ description: '',
465
+ createdAt: new Date(),
466
+ updatedAt: new Date(),
467
+ };
468
+ }
469
+ }));
470
+ const system = await storageService.getAgentDetails({ agentId: 'system' });
471
+ const builtInSystemAgent = {
472
+ id: system.id,
473
+ name: system.name,
474
+ description: system.description || '',
475
+ image: system.image,
476
+ runtime: system.runtime,
477
+ createdAt: system.createdAt,
478
+ updatedAt: system.updatedAt,
479
+ };
480
+ const deduped = new Map();
481
+ deduped.set(builtInSystemAgent.id, builtInSystemAgent);
482
+ for (const agent of agents) {
483
+ if (!deduped.has(agent.id))
484
+ deduped.set(agent.id, agent);
485
+ }
486
+ return Array.from(deduped.values());
487
+ },
488
+ getPlugins: async () => {
489
+ const [builtInPlugins, diskPlugins] = await Promise.all([
490
+ listBuiltInPlugins(),
491
+ listPluginsFromDisk(),
492
+ ]);
493
+ const merged = [...builtInPlugins, ...diskPlugins];
494
+ const deduped = new Map();
495
+ for (const plugin of merged) {
496
+ if (!deduped.has(plugin.id)) {
497
+ deduped.set(plugin.id, plugin);
498
+ }
499
+ }
500
+ return Array.from(deduped.values());
501
+ },
502
+ getAgentDetails: async ({ agentId }) => {
503
+ const agentDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_AGENTS_DIR + '/' + agentId);
504
+ const agentMdPath = `${agentDir}/AGENT.md`;
505
+ let diskDetails;
506
+ try {
507
+ await fs.access(agentMdPath);
508
+ const agentMd = await fs.readFile(agentMdPath, 'utf-8');
509
+ const { data, content: instructions } = matter(agentMd);
510
+ const discoveredImage = await resolveEntityImageDataUrl(agentDir);
511
+ diskDetails = {
512
+ id: agentId,
513
+ name: data.name || agentId,
514
+ instructions: instructions.trim(),
515
+ runtime: data.runtime,
516
+ plugins: data.plugins || [],
517
+ description: data.description || '',
518
+ image: discoveredImage || undefined,
519
+ createdAt: new Date(),
520
+ updatedAt: new Date(),
521
+ };
522
+ }
523
+ catch (error) {
524
+ if (agentId !== 'system') {
525
+ const err = new Error(`Agent "${agentId}" does not exist.`);
526
+ err.code = 'AGENT_NOT_FOUND';
527
+ throw err;
528
+ }
529
+ }
530
+ if (agentId === 'system') {
531
+ return getSystemAgentDetails(diskDetails);
532
+ }
533
+ if (!diskDetails) {
534
+ const error = new Error(`Agent "${agentId}" does not exist.`);
535
+ error.code = 'AGENT_NOT_FOUND';
536
+ throw error;
537
+ }
538
+ return diskDetails;
539
+ },
540
+ getEvents: async ({ channelId, threadId, }) => {
541
+ try {
542
+ const threadDir = getConversationDir(channelId, threadId);
543
+ const eventsPath = `${threadDir}/events.jsonl`;
544
+ const eventsData = await fs.readFile(eventsPath);
545
+ const events = eventsData
546
+ .toString()
547
+ .split(/\r?\n/)
548
+ .map((line) => line.trim())
549
+ .filter(Boolean)
550
+ .map((line) => {
551
+ const event = JSON.parse(line);
552
+ if (!event.id) {
553
+ event.id = crypto.randomUUID();
554
+ }
555
+ return event;
556
+ });
557
+ // If we are at the channel level (no threadId), check which events have threads
558
+ if (!threadId) {
559
+ const threadsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR + '/' + channelId + '/threads');
560
+ try {
561
+ const threadDirs = await fs.readdir(threadsDir);
562
+ const threadSet = new Set(threadDirs);
563
+ return events.map((event) => {
564
+ // Check if this event has a threadId associated with it
565
+ // The frontend provides the threadId, and it matches the directory name on disk
566
+ const threadId = event.id;
567
+ // If an explicit threadId exists and has a directory, use it
568
+ if (threadId && threadSet.has(threadId)) {
569
+ return {
570
+ ...event,
571
+ meta: {
572
+ ...event?.meta,
573
+ hasThread: true,
574
+ },
575
+ };
576
+ }
577
+ return event;
578
+ });
579
+ }
580
+ catch {
581
+ // No threads folder or other error, just return events as is
582
+ return events;
583
+ }
584
+ }
585
+ return events;
586
+ }
587
+ catch (error) {
588
+ if (error.code !== 'ENOENT') {
589
+ console.error(`Failed to get events for channel ${channelId} thread ${threadId}`, error);
590
+ }
591
+ return [];
592
+ }
593
+ },
594
+ storeEvent: async ({ channelId, threadId, event, }) => {
595
+ try {
596
+ const threadDir = getConversationDir(channelId, threadId);
597
+ if (threadId) {
598
+ try {
599
+ await fs.access(threadDir);
600
+ }
601
+ catch (error) {
602
+ if (error.code === 'ENOENT') {
603
+ const threadTitle = buildThreadTitleFromEvent(event);
604
+ await storageService.createThread({
605
+ channelId,
606
+ threadId,
607
+ threadTitle,
608
+ });
609
+ }
610
+ else {
611
+ throw error;
612
+ }
613
+ }
614
+ }
615
+ else {
616
+ await fs.mkdir(threadDir, { recursive: true });
617
+ }
618
+ // Ensure the event has a unique ID
619
+ if (!event.id) {
620
+ event.id = crypto.randomUUID();
621
+ }
622
+ await fs.appendFile(`${threadDir}/events.jsonl`, `${JSON.stringify(event)}\n`);
623
+ }
624
+ catch (error) {
625
+ console.error(`Failed to store event for channel ${channelId} thread ${threadId}`, error);
626
+ throw error;
627
+ }
628
+ },
629
+ getVariables: async () => {
630
+ const variablesFilePath = resolvePath(resolveBaseDir() + '/' + VARIABLES_FILE);
631
+ const raw = await readJsonFile(variablesFilePath, {});
632
+ if (raw && typeof raw === 'object' && 'variables' in raw && Array.isArray(raw.variables)) {
633
+ const entries = raw.variables
634
+ .filter((v) => typeof v?.key === 'string')
635
+ .map((v) => [v.key, { value: String(v.value ?? ''), secret: !!v.secret }]);
636
+ return Object.fromEntries(entries);
637
+ }
638
+ // Legacy or simple format
639
+ return toVariablesRecord(raw);
640
+ },
641
+ listFiles: async ({ channelId, path: subPath = '', }) => {
642
+ const details = await storageService.getChannelDetails({ channelId });
643
+ const baseCwd = details.cwd;
644
+ if (!baseCwd) {
645
+ throw new Error('Channel has no CWD configured');
646
+ }
647
+ const resolvedBase = path.resolve(baseCwd);
648
+ const targetDir = path.resolve(resolvedBase, subPath);
649
+ // Security check: ensure target is within baseCwd
650
+ if (!targetDir.startsWith(resolvedBase)) {
651
+ throw new Error('Access denied: directory escape');
652
+ }
653
+ const entries = await fs.readdir(targetDir, { withFileTypes: true });
654
+ return entries
655
+ .filter((e) => !e.name.startsWith('.')) // Hide hidden files by default for MVP
656
+ .map((e) => ({
657
+ name: e.name,
658
+ isDirectory: e.isDirectory(),
659
+ }));
660
+ },
661
+ readFile: async ({ channelId, path: filePath }) => {
662
+ const details = await storageService.getChannelDetails({ channelId });
663
+ const baseCwd = details.cwd;
664
+ if (!baseCwd) {
665
+ throw new Error('Channel has no CWD configured');
666
+ }
667
+ const resolvedBase = path.resolve(baseCwd);
668
+ const targetFile = path.resolve(resolvedBase, filePath);
669
+ // Security check: ensure target is within baseCwd
670
+ if (!targetFile.startsWith(resolvedBase)) {
671
+ throw new Error('Access denied: directory escape');
672
+ }
673
+ return fs.readFile(targetFile, 'utf-8');
674
+ },
675
+ /**
676
+ * Hydrates the full OpenBot state from disk/storage before a run.
677
+ */
678
+ getOpenBotState: async (options) => {
679
+ const { runId, agentId, channelId, threadId, event } = options;
680
+ let agentDetails;
681
+ try {
682
+ agentDetails = await storageService.getAgentDetails({ agentId });
683
+ }
684
+ catch (error) {
685
+ console.warn(`[storage] Failed to load agent details for agent: ${agentId}`, error);
686
+ throw error;
687
+ }
688
+ let channelDetails;
689
+ if (channelId && channelId !== 'general') {
690
+ try {
691
+ channelDetails = await storageService.getChannelDetails({ channelId });
692
+ }
693
+ catch (error) {
694
+ console.warn(`[storage] Failed to load channel details for channel ${channelId}`, error);
695
+ }
696
+ }
697
+ let threadDetails;
698
+ if (channelId && threadId) {
699
+ try {
700
+ threadDetails = await storageService.getThreadDetails({ channelId, threadId });
701
+ }
702
+ catch (error) {
703
+ console.warn(`[storage] Failed to load thread details for channel ${channelId} thread: ${threadId}`, error);
704
+ }
705
+ }
706
+ return {
707
+ runId,
708
+ agentId,
709
+ channelId,
710
+ threadId,
711
+ triggerEvent: event,
712
+ agentDetails: {
713
+ id: agentDetails.id,
714
+ name: agentDetails.name,
715
+ description: agentDetails.description || '',
716
+ instructions: agentDetails.instructions || '',
717
+ runtime: agentDetails.runtime,
718
+ plugins: agentDetails.plugins,
719
+ },
720
+ channelDetails: channelDetails,
721
+ threadDetails: threadDetails,
722
+ };
723
+ },
724
+ };