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.
- package/.prettierrc +8 -0
- package/AGENTS.md +68 -0
- package/CONTRIBUTING.md +74 -0
- package/LICENSE +21 -0
- package/README.md +117 -14
- package/dist/agents/system.js +106 -0
- package/dist/app/cli.js +27 -0
- package/dist/app/config.js +64 -0
- package/dist/app/server.js +237 -0
- package/dist/app/utils.js +35 -0
- package/dist/harness/agent-harness.js +45 -0
- package/dist/harness/mcp.js +61 -0
- package/dist/harness/orchestrator.js +273 -0
- package/dist/harness/process.js +7 -0
- package/dist/plugins/ai-sdk.js +141 -0
- package/dist/plugins/delegation.js +52 -0
- package/dist/plugins/mcp.js +140 -0
- package/dist/plugins/storage.js +502 -0
- package/dist/plugins/ui.js +47 -0
- package/dist/registry/plugins.js +73 -0
- package/dist/services/storage.js +724 -0
- package/docs/README.md +7 -0
- package/docs/agents.md +83 -0
- package/docs/architecture.md +34 -0
- package/docs/plugins.md +77 -0
- package/logo-black.png +0 -0
- package/{dist/assets/logo.js → logo-black.svg} +24 -24
- package/{dist/ui/sidebar.js → logo-white.svg} +23 -88
- package/package.json +10 -9
- package/src/agents/system.ts +112 -0
- package/src/app/cli.ts +38 -0
- package/src/app/config.ts +104 -0
- package/src/app/server.ts +284 -0
- package/src/app/types.ts +476 -0
- package/src/app/utils.ts +43 -0
- package/src/assets/icon.svg +1 -0
- package/src/harness/agent-harness.ts +58 -0
- package/src/harness/mcp.ts +78 -0
- package/src/harness/orchestrator.ts +342 -0
- package/src/harness/process.ts +9 -0
- package/src/harness/types.ts +34 -0
- package/src/plugins/ai-sdk.ts +197 -0
- package/src/plugins/delegation.ts +60 -0
- package/src/plugins/mcp.ts +154 -0
- package/src/plugins/storage.ts +725 -0
- package/src/plugins/ui.ts +57 -0
- package/src/registry/plugins.ts +85 -0
- package/src/services/storage.ts +957 -0
- package/tsconfig.json +18 -0
- package/dist/agents/agent-creator.js +0 -74
- package/dist/agents/browser-agent.js +0 -31
- package/dist/agents/os-agent.js +0 -32
- package/dist/agents/planner-agent.js +0 -32
- package/dist/agents/topic-agent.js +0 -46
- package/dist/architecture/execution-engine.js +0 -151
- package/dist/architecture/intent-classifier.js +0 -26
- package/dist/architecture/planner.js +0 -106
- package/dist/automation-worker.js +0 -121
- package/dist/automations.js +0 -52
- package/dist/cli.js +0 -275
- package/dist/config.js +0 -53
- package/dist/core/agents.js +0 -41
- package/dist/core/delegation.js +0 -230
- package/dist/core/manager.js +0 -96
- package/dist/core/plugins.js +0 -74
- package/dist/core/router.js +0 -191
- package/dist/handlers/init.js +0 -29
- package/dist/handlers/session-change.js +0 -21
- package/dist/handlers/settings.js +0 -47
- package/dist/handlers/tab-change.js +0 -14
- package/dist/installers.js +0 -156
- package/dist/marketplace.js +0 -80
- package/dist/model-catalog.js +0 -132
- package/dist/model-defaults.js +0 -25
- package/dist/models.js +0 -47
- package/dist/open-bot.js +0 -51
- package/dist/orchestrator/direct-invocation.js +0 -13
- package/dist/orchestrator/events.js +0 -36
- package/dist/orchestrator/state.js +0 -54
- package/dist/orchestrator.js +0 -422
- package/dist/plugins/agent/index.js +0 -81
- package/dist/plugins/approval/index.js +0 -100
- package/dist/plugins/brain/identity.js +0 -77
- package/dist/plugins/brain/index.js +0 -204
- package/dist/plugins/brain/memory.js +0 -120
- package/dist/plugins/brain/prompt.js +0 -46
- package/dist/plugins/brain/types.js +0 -45
- package/dist/plugins/brain/ui.js +0 -7
- package/dist/plugins/browser/index.js +0 -629
- package/dist/plugins/browser/ui.js +0 -13
- package/dist/plugins/file-system/index.js +0 -171
- package/dist/plugins/file-system/ui.js +0 -6
- package/dist/plugins/llm/context-budget.js +0 -139
- package/dist/plugins/llm/context-shaping.js +0 -177
- package/dist/plugins/llm/index.js +0 -380
- package/dist/plugins/memory/index.js +0 -220
- package/dist/plugins/memory/memory.js +0 -122
- package/dist/plugins/memory/prompt.js +0 -55
- package/dist/plugins/memory/types.js +0 -45
- package/dist/plugins/meta-agent/index.js +0 -570
- package/dist/plugins/meta-agent/ui.js +0 -11
- package/dist/plugins/shell/index.js +0 -100
- package/dist/plugins/shell/ui.js +0 -6
- package/dist/plugins/skills/index.js +0 -286
- package/dist/plugins/skills/types.js +0 -50
- package/dist/plugins/skills/ui.js +0 -12
- package/dist/registry/agent-registry.js +0 -35
- package/dist/registry/index.js +0 -2
- package/dist/registry/plugin-loader.js +0 -499
- package/dist/registry/plugin-registry.js +0 -44
- package/dist/registry/ts-agent-loader.js +0 -82
- package/dist/registry/yaml-agent-loader.js +0 -246
- package/dist/runtime/execution-trace.js +0 -41
- package/dist/runtime/intent-routing.js +0 -26
- package/dist/runtime/openbot-runtime.js +0 -354
- package/dist/server.js +0 -890
- package/dist/session.js +0 -179
- package/dist/ui/block.js +0 -12
- package/dist/ui/header.js +0 -52
- package/dist/ui/layout.js +0 -26
- package/dist/ui/navigation.js +0 -15
- package/dist/ui/settings.js +0 -106
- package/dist/ui/skills.js +0 -7
- package/dist/ui/thread.js +0 -16
- package/dist/ui/widgets/action-list.js +0 -2
- package/dist/ui/widgets/approval-card.js +0 -9
- package/dist/ui/widgets/code-snippet.js +0 -2
- package/dist/ui/widgets/data-block.js +0 -2
- package/dist/ui/widgets/data-table.js +0 -2
- package/dist/ui/widgets/delegation.js +0 -29
- package/dist/ui/widgets/empty-state.js +0 -2
- package/dist/ui/widgets/index.js +0 -23
- package/dist/ui/widgets/inquiry.js +0 -7
- package/dist/ui/widgets/key-value.js +0 -2
- package/dist/ui/widgets/progress-step.js +0 -2
- package/dist/ui/widgets/resource-card.js +0 -2
- package/dist/ui/widgets/status.js +0 -2
- package/dist/ui/widgets/todo-list.js +0 -2
- package/dist/version.js +0 -62
- /package/dist/{types.js → app/types.js} +0 -0
- /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
|
+
};
|