openbot 0.2.14 → 0.3.1
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/dist/agents/openbot/index.js +76 -0
- package/dist/agents/openbot/middleware/approval.js +132 -0
- package/dist/agents/openbot/runtime.js +289 -0
- package/dist/agents/openbot/system-prompt.js +32 -0
- package/dist/agents/openbot/tools/delegation.js +78 -0
- package/dist/agents/openbot/tools/mcp.js +99 -0
- package/dist/agents/openbot/tools/shell.js +91 -0
- package/dist/agents/openbot/tools/storage.js +75 -0
- package/dist/agents/openbot/tools/ui.js +176 -0
- package/dist/agents/system.js +20 -93
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +4 -1
- package/dist/app/server.js +15 -8
- package/dist/bus/agent-package.js +1 -0
- package/dist/bus/plugin.js +1 -0
- package/dist/bus/services.js +711 -0
- package/dist/bus/types.js +1 -0
- package/dist/harness/context.js +250 -0
- package/dist/harness/event-normalizer.js +59 -0
- package/dist/harness/orchestrator.js +27 -227
- package/dist/harness/process.js +25 -3
- package/dist/harness/queue-processor.js +227 -0
- package/dist/harness/runtime-factory.js +103 -0
- package/dist/plugins/ai-sdk/index.js +37 -0
- package/dist/plugins/ai-sdk/runtime.js +402 -0
- package/dist/plugins/ai-sdk/system-prompt.js +3 -0
- package/dist/plugins/ai-sdk.js +277 -87
- package/dist/plugins/approval/index.js +159 -0
- package/dist/plugins/approval.js +163 -0
- package/dist/plugins/delegation/index.js +79 -0
- package/dist/plugins/delegation.js +67 -11
- package/dist/plugins/mcp/index.js +108 -0
- package/dist/plugins/memory/index.js +71 -0
- package/dist/plugins/shell/index.js +99 -0
- package/dist/plugins/shell.js +123 -0
- package/dist/plugins/storage-tools/index.js +85 -0
- package/dist/plugins/storage.js +240 -5
- package/dist/plugins/ui/index.js +184 -0
- package/dist/plugins/ui.js +185 -21
- package/dist/registry/agents.js +138 -0
- package/dist/registry/plugins.js +93 -50
- package/dist/services/agent-packages.js +103 -0
- package/dist/services/memory.js +152 -0
- package/dist/services/plugins.js +98 -0
- package/dist/services/storage.js +366 -94
- package/docs/agents.md +52 -65
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +70 -58
- package/docs/templates/AGENT.example.md +57 -0
- package/package.json +8 -7
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +14 -4
- package/src/app/server.ts +23 -10
- package/src/app/types.ts +445 -16
- package/src/assets/icon.svg +4 -1
- package/src/bus/plugin.ts +67 -0
- package/src/bus/services.ts +786 -0
- package/src/bus/types.ts +160 -0
- package/src/harness/context.ts +293 -0
- package/src/harness/event-normalizer.ts +82 -0
- package/src/harness/orchestrator.ts +35 -273
- package/src/harness/process.ts +28 -4
- package/src/harness/queue-processor.ts +309 -0
- package/src/harness/runtime-factory.ts +125 -0
- package/src/plugins/ai-sdk/index.ts +44 -0
- package/src/plugins/ai-sdk/runtime.ts +484 -0
- package/src/plugins/ai-sdk/system-prompt.ts +4 -0
- package/src/plugins/approval/index.ts +228 -0
- package/src/plugins/delegation/index.ts +94 -0
- package/src/plugins/mcp/index.ts +128 -0
- package/src/plugins/memory/index.ts +85 -0
- package/src/plugins/shell/index.ts +123 -0
- package/src/plugins/storage-tools/index.ts +101 -0
- package/src/plugins/ui/index.ts +227 -0
- package/src/registry/plugins.ts +108 -55
- package/src/services/memory.ts +213 -0
- package/src/services/plugins.ts +133 -0
- package/src/services/storage.ts +472 -137
- package/src/agents/system.ts +0 -112
- package/src/plugins/ai-sdk.ts +0 -197
- package/src/plugins/delegation.ts +0 -60
- package/src/plugins/mcp.ts +0 -154
- package/src/plugins/storage.ts +0 -725
- package/src/plugins/ui.ts +0 -57
package/src/services/storage.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_PLUGINS_DIR,
|
|
2
3
|
DEFAULT_AGENTS_DIR,
|
|
3
4
|
DEFAULT_BASE_DIR,
|
|
4
5
|
DEFAULT_CHANNELS_DIR,
|
|
5
|
-
DEFAULT_PLUGINS_DIR,
|
|
6
6
|
loadConfig,
|
|
7
7
|
resolvePath,
|
|
8
8
|
StoredVariable,
|
|
@@ -17,30 +17,19 @@ import {
|
|
|
17
17
|
AgentDetails,
|
|
18
18
|
Channel,
|
|
19
19
|
ChannelDetails,
|
|
20
|
-
|
|
21
|
-
PluginKind,
|
|
20
|
+
PluginDescriptor,
|
|
22
21
|
Thread,
|
|
23
22
|
ThreadDetails,
|
|
24
|
-
} from '../
|
|
25
|
-
import {
|
|
23
|
+
} from '../bus/types.js';
|
|
24
|
+
import type { PluginRef } from '../bus/plugin.js';
|
|
25
|
+
import { aiSdkPlugin } from '../plugins/ai-sdk/index.js';
|
|
26
|
+
import { AI_SDK_SYSTEM_PROMPT } from '../plugins/ai-sdk/system-prompt.js';
|
|
27
|
+
import { listBuiltInPlugins, parsePluginModule } from '../registry/plugins.js';
|
|
26
28
|
import { OpenBotEvent, OpenBotState } from '../app/types.js';
|
|
29
|
+
import { processService } from '../harness/process.js';
|
|
30
|
+
import { memoryService } from './memory.js';
|
|
27
31
|
import { pathToFileURL } from 'node:url';
|
|
28
32
|
|
|
29
|
-
const mapNameToPlugin = (
|
|
30
|
-
name: string,
|
|
31
|
-
description: string,
|
|
32
|
-
kind: PluginKind = 'tool',
|
|
33
|
-
image?: string,
|
|
34
|
-
): Plugin => ({
|
|
35
|
-
id: name,
|
|
36
|
-
name,
|
|
37
|
-
description,
|
|
38
|
-
kind,
|
|
39
|
-
image,
|
|
40
|
-
createdAt: new Date(),
|
|
41
|
-
updatedAt: new Date(),
|
|
42
|
-
});
|
|
43
|
-
|
|
44
33
|
const resolveBaseDir = () => {
|
|
45
34
|
const config = loadConfig();
|
|
46
35
|
return resolvePath(config.baseDir || DEFAULT_BASE_DIR);
|
|
@@ -62,15 +51,6 @@ const tryReadSvgDataUrl = async (filePath: string): Promise<string | null> => {
|
|
|
62
51
|
}
|
|
63
52
|
};
|
|
64
53
|
|
|
65
|
-
/**
|
|
66
|
-
* Auto-discovers an entity SVG avatar and returns it as a data URL.
|
|
67
|
-
*
|
|
68
|
-
* Search order:
|
|
69
|
-
* 1) <entity>/assets/avatar.svg|icon.svg|image.svg|logo.svg
|
|
70
|
-
* 2) <entity>/avatar.svg|icon.svg|image.svg|logo.svg
|
|
71
|
-
* 3) first *.svg in <entity>/assets
|
|
72
|
-
* 4) first *.svg in <entity>
|
|
73
|
-
*/
|
|
74
54
|
const resolveEntityImageDataUrl = async (entityDir: string): Promise<string | undefined> => {
|
|
75
55
|
const preferredDirs = [path.join(entityDir, 'assets'), entityDir];
|
|
76
56
|
|
|
@@ -91,7 +71,7 @@ const resolveEntityImageDataUrl = async (entityDir: string): Promise<string | un
|
|
|
91
71
|
const dataUrl = await tryReadSvgDataUrl(path.join(dir, firstSvg.name));
|
|
92
72
|
if (dataUrl) return dataUrl;
|
|
93
73
|
} catch {
|
|
94
|
-
// ignore
|
|
74
|
+
// ignore
|
|
95
75
|
}
|
|
96
76
|
}
|
|
97
77
|
|
|
@@ -103,6 +83,70 @@ const getConversationDir = (channelId: string, threadId?: string) => {
|
|
|
103
83
|
return threadId ? `${base}/threads/${threadId}` : base;
|
|
104
84
|
};
|
|
105
85
|
|
|
86
|
+
/** Built-in orchestrator agent id. Not creatable as a normal disk agent. */
|
|
87
|
+
const SYSTEM_AGENT_ID = 'system';
|
|
88
|
+
|
|
89
|
+
const SYSTEM_DEFAULT_PLUGINS: PluginRef[] = [
|
|
90
|
+
{ id: 'ai-sdk', config: { model: 'openai/gpt-5.4-nano' } },
|
|
91
|
+
{ id: 'storage-tools' },
|
|
92
|
+
// { id: 'mcp' },
|
|
93
|
+
{ id: 'shell' },
|
|
94
|
+
{ id: 'delegation' },
|
|
95
|
+
// { id: 'ui' },
|
|
96
|
+
{ id: 'approval' },
|
|
97
|
+
{ id: 'memory' },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
function getSystemAgentDetails(overrides?: Partial<AgentDetails>): AgentDetails {
|
|
101
|
+
const defaults: AgentDetails = {
|
|
102
|
+
id: SYSTEM_AGENT_ID,
|
|
103
|
+
name: 'OpenBot',
|
|
104
|
+
image: undefined,
|
|
105
|
+
description:
|
|
106
|
+
'First-party orchestration agent for OpenBot. Coordinates other agents via handoff and delegation.',
|
|
107
|
+
instructions: AI_SDK_SYSTEM_PROMPT,
|
|
108
|
+
plugins: SYSTEM_DEFAULT_PLUGINS.map((ref) => ref.id),
|
|
109
|
+
pluginRefs: SYSTEM_DEFAULT_PLUGINS,
|
|
110
|
+
createdAt: new Date(),
|
|
111
|
+
updatedAt: new Date(),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (!overrides) return defaults;
|
|
115
|
+
|
|
116
|
+
const refs = overrides.pluginRefs && overrides.pluginRefs.length > 0
|
|
117
|
+
? overrides.pluginRefs
|
|
118
|
+
: defaults.pluginRefs;
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
...defaults,
|
|
122
|
+
...overrides,
|
|
123
|
+
id: SYSTEM_AGENT_ID,
|
|
124
|
+
image: overrides.image || defaults.image,
|
|
125
|
+
plugins: refs.map((ref) => ref.id),
|
|
126
|
+
pluginRefs: refs,
|
|
127
|
+
updatedAt: new Date(),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Suppress unused warning until system agent customization re-uses aiSdkPlugin metadata.
|
|
132
|
+
void aiSdkPlugin;
|
|
133
|
+
|
|
134
|
+
const RESERVED_DISK_AGENT_IDS = new Set([SYSTEM_AGENT_ID]);
|
|
135
|
+
|
|
136
|
+
const assertValidDiskAgentId = (agentId: string): void => {
|
|
137
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
138
|
+
throw new Error('agentId is required');
|
|
139
|
+
}
|
|
140
|
+
if (RESERVED_DISK_AGENT_IDS.has(agentId)) {
|
|
141
|
+
throw new Error(`Agent id "${agentId}" is reserved`);
|
|
142
|
+
}
|
|
143
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
|
|
144
|
+
throw new Error('agentId must contain only letters, digits, underscores, and hyphens');
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const getAgentsRootDir = () => path.join(resolveBaseDir(), DEFAULT_AGENTS_DIR);
|
|
149
|
+
|
|
106
150
|
const getLastReadFilePath = () =>
|
|
107
151
|
path.join(resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR), '_meta', 'last-read.json');
|
|
108
152
|
|
|
@@ -145,7 +189,6 @@ const toVariablesRecord = (raw: unknown): Record<string, string> => {
|
|
|
145
189
|
return {};
|
|
146
190
|
}
|
|
147
191
|
|
|
148
|
-
// Current format: { version: number, variables: StoredVariable[] }
|
|
149
192
|
if ('variables' in raw && Array.isArray((raw as { variables?: unknown }).variables)) {
|
|
150
193
|
const entries = (raw as { variables: StoredVariable[] }).variables
|
|
151
194
|
.filter((variable) => typeof variable?.key === 'string')
|
|
@@ -153,7 +196,6 @@ const toVariablesRecord = (raw: unknown): Record<string, string> => {
|
|
|
153
196
|
return Object.fromEntries(entries);
|
|
154
197
|
}
|
|
155
198
|
|
|
156
|
-
// Legacy format: { [key: string]: string }
|
|
157
199
|
return Object.fromEntries(
|
|
158
200
|
Object.entries(raw as Record<string, unknown>).map(([key, value]) => [
|
|
159
201
|
key,
|
|
@@ -162,15 +204,59 @@ const toVariablesRecord = (raw: unknown): Record<string, string> => {
|
|
|
162
204
|
);
|
|
163
205
|
};
|
|
164
206
|
|
|
165
|
-
const
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
207
|
+
const listBuiltInPluginDescriptors = async (): Promise<PluginDescriptor[]> => {
|
|
208
|
+
return listBuiltInPlugins().map((plugin) => ({
|
|
209
|
+
id: plugin.id,
|
|
210
|
+
name: plugin.name,
|
|
211
|
+
description: plugin.description,
|
|
212
|
+
builtIn: true,
|
|
213
|
+
image: plugin.image,
|
|
214
|
+
defaultInstructions: plugin.defaultInstructions,
|
|
215
|
+
configSchema: plugin.configSchema,
|
|
216
|
+
createdAt: new Date(),
|
|
217
|
+
updatedAt: new Date(),
|
|
218
|
+
}));
|
|
171
219
|
};
|
|
172
220
|
|
|
173
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Walk `plugins/` and yield candidate plugin ids (npm names). Includes scoped
|
|
223
|
+
* packages by recursing one level into directories starting with `@`.
|
|
224
|
+
*/
|
|
225
|
+
const listInstalledPluginIds = async (pluginsDir: string): Promise<string[]> => {
|
|
226
|
+
const out: string[] = [];
|
|
227
|
+
let topEntries;
|
|
228
|
+
try {
|
|
229
|
+
topEntries = await fs.readdir(pluginsDir, { withFileTypes: true });
|
|
230
|
+
} catch {
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const entry of topEntries) {
|
|
235
|
+
if (entry.name.startsWith('.')) continue;
|
|
236
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
237
|
+
|
|
238
|
+
if (entry.name.startsWith('@')) {
|
|
239
|
+
try {
|
|
240
|
+
const inner = await fs.readdir(path.join(pluginsDir, entry.name), { withFileTypes: true });
|
|
241
|
+
for (const sub of inner) {
|
|
242
|
+
if (sub.name.startsWith('.')) continue;
|
|
243
|
+
if (sub.isDirectory() || sub.isSymbolicLink()) {
|
|
244
|
+
out.push(`${entry.name}/${sub.name}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
// ignore
|
|
249
|
+
}
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
out.push(entry.name);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return out;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const listPluginsFromDisk = async (): Promise<PluginDescriptor[]> => {
|
|
174
260
|
const pluginsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_PLUGINS_DIR);
|
|
175
261
|
try {
|
|
176
262
|
await fs.access(pluginsDir);
|
|
@@ -178,26 +264,64 @@ const listPluginsFromDisk = async (): Promise<Plugin[]> => {
|
|
|
178
264
|
await fs.mkdir(pluginsDir, { recursive: true });
|
|
179
265
|
}
|
|
180
266
|
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
267
|
+
const ids = await listInstalledPluginIds(pluginsDir);
|
|
268
|
+
|
|
269
|
+
const descriptors = await Promise.all(
|
|
270
|
+
ids.map(async (id): Promise<PluginDescriptor | null> => {
|
|
271
|
+
try {
|
|
272
|
+
const pluginDir = path.join(pluginsDir, id);
|
|
273
|
+
const distPath = path.join(pluginDir, 'dist', 'index.js');
|
|
274
|
+
const module = await import(pathToFileURL(distPath).href);
|
|
275
|
+
const parsed = parsePluginModule(module as Record<string, unknown>);
|
|
276
|
+
if (!parsed) return null;
|
|
277
|
+
const image = await resolveEntityImageDataUrl(pluginDir);
|
|
278
|
+
return {
|
|
279
|
+
id,
|
|
280
|
+
name: parsed.name || id,
|
|
281
|
+
description: parsed.description || '',
|
|
282
|
+
builtIn: false,
|
|
283
|
+
image: parsed.image || image,
|
|
284
|
+
defaultInstructions: parsed.defaultInstructions,
|
|
285
|
+
configSchema: parsed.configSchema,
|
|
286
|
+
createdAt: new Date(),
|
|
287
|
+
updatedAt: new Date(),
|
|
288
|
+
};
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.warn(`[storage] Failed to load plugin ${id}:`, error);
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return descriptors.filter((d): d is PluginDescriptor => d !== null);
|
|
297
|
+
};
|
|
197
298
|
|
|
198
|
-
|
|
299
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
300
|
+
!!value && typeof value === 'object' && !Array.isArray(value);
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Parse the `plugins:` array from AGENT.md frontmatter. Each entry must have an
|
|
304
|
+
* `id`; `config` is optional. Strings are accepted as a shorthand for `{ id }`.
|
|
305
|
+
*/
|
|
306
|
+
const parsePluginRefs = (raw: unknown): PluginRef[] => {
|
|
307
|
+
if (!Array.isArray(raw)) return [];
|
|
308
|
+
const refs: PluginRef[] = [];
|
|
309
|
+
for (const entry of raw) {
|
|
310
|
+
if (typeof entry === 'string' && entry.trim()) {
|
|
311
|
+
refs.push({ id: entry.trim() });
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (isRecord(entry) && typeof entry.id === 'string' && entry.id.trim()) {
|
|
315
|
+
const config = isRecord(entry.config) ? (entry.config as Record<string, unknown>) : undefined;
|
|
316
|
+
refs.push({ id: entry.id.trim(), ...(config ? { config } : {}) });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return refs;
|
|
199
320
|
};
|
|
200
321
|
|
|
322
|
+
const serializePluginRefs = (refs: PluginRef[]): unknown[] =>
|
|
323
|
+
refs.map((ref) => (ref.config ? { id: ref.id, config: ref.config } : { id: ref.id }));
|
|
324
|
+
|
|
201
325
|
export const storageService = {
|
|
202
326
|
getLastReadByChannel: async (): Promise<Record<string, string>> => {
|
|
203
327
|
return readJsonFile(getLastReadFilePath(), {});
|
|
@@ -241,7 +365,7 @@ export const storageService = {
|
|
|
241
365
|
const state = JSON.parse(stateContent);
|
|
242
366
|
cwd = typeof state.cwd === 'string' ? state.cwd : undefined;
|
|
243
367
|
} catch {
|
|
244
|
-
//
|
|
368
|
+
// ignore
|
|
245
369
|
}
|
|
246
370
|
|
|
247
371
|
const channel: Channel = {
|
|
@@ -262,7 +386,6 @@ export const storageService = {
|
|
|
262
386
|
}
|
|
263
387
|
|
|
264
388
|
try {
|
|
265
|
-
// Fetch up to 5 most recent threads for the sidebar
|
|
266
389
|
const allThreads = await storageService.getThreads({ channelId: name });
|
|
267
390
|
channel.recentThreads = allThreads
|
|
268
391
|
.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
|
|
@@ -300,8 +423,9 @@ export const storageService = {
|
|
|
300
423
|
try {
|
|
301
424
|
await fs.access(channelDir);
|
|
302
425
|
throw new Error(`Channel "${normalizedChannelId}" already exists`);
|
|
303
|
-
} catch (error:
|
|
304
|
-
|
|
426
|
+
} catch (error: unknown) {
|
|
427
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
428
|
+
if (code !== 'ENOENT') {
|
|
305
429
|
throw error;
|
|
306
430
|
}
|
|
307
431
|
}
|
|
@@ -311,7 +435,7 @@ export const storageService = {
|
|
|
311
435
|
};
|
|
312
436
|
|
|
313
437
|
if (cwd) {
|
|
314
|
-
finalState.cwd = cwd;
|
|
438
|
+
(finalState as Record<string, unknown>).cwd = cwd;
|
|
315
439
|
}
|
|
316
440
|
|
|
317
441
|
await fs.mkdir(channelDir, { recursive: true });
|
|
@@ -338,12 +462,8 @@ export const storageService = {
|
|
|
338
462
|
const normalizedChannelId = channelId.trim();
|
|
339
463
|
const normalizedThreadId = threadId.trim();
|
|
340
464
|
|
|
341
|
-
if (!normalizedChannelId)
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
if (!normalizedThreadId) {
|
|
345
|
-
throw new Error('threadId is required');
|
|
346
|
-
}
|
|
465
|
+
if (!normalizedChannelId) throw new Error('channelId is required');
|
|
466
|
+
if (!normalizedThreadId) throw new Error('threadId is required');
|
|
347
467
|
|
|
348
468
|
const threadDir = getConversationDir(normalizedChannelId, normalizedThreadId);
|
|
349
469
|
const specPath = `${threadDir}/SPEC.md`;
|
|
@@ -354,13 +474,14 @@ export const storageService = {
|
|
|
354
474
|
throw new Error(
|
|
355
475
|
`Thread "${normalizedThreadId}" already exists in channel "${normalizedChannelId}"`,
|
|
356
476
|
);
|
|
357
|
-
} catch (error:
|
|
358
|
-
|
|
477
|
+
} catch (error: unknown) {
|
|
478
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
479
|
+
if (code !== 'ENOENT') {
|
|
359
480
|
throw error;
|
|
360
481
|
}
|
|
361
482
|
}
|
|
362
483
|
|
|
363
|
-
const baseState = { ...(initialState || {}) };
|
|
484
|
+
const baseState: Record<string, unknown> = { ...(initialState || {}) };
|
|
364
485
|
if (threadTitle?.trim()) {
|
|
365
486
|
baseState.generatedName = threadTitle.trim();
|
|
366
487
|
}
|
|
@@ -400,8 +521,8 @@ export const storageService = {
|
|
|
400
521
|
if (generatedName) {
|
|
401
522
|
threadDisplayName = generatedName;
|
|
402
523
|
}
|
|
403
|
-
} catch (error:
|
|
404
|
-
if (error.code !== 'ENOENT') {
|
|
524
|
+
} catch (error: unknown) {
|
|
525
|
+
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
|
|
405
526
|
console.error(
|
|
406
527
|
`Failed to read thread state for channel ${channelId} thread ${name}`,
|
|
407
528
|
error,
|
|
@@ -435,8 +556,8 @@ export const storageService = {
|
|
|
435
556
|
let spec = '';
|
|
436
557
|
try {
|
|
437
558
|
spec = await fs.readFile(specPath, 'utf-8');
|
|
438
|
-
} catch (error:
|
|
439
|
-
if (error.code !== 'ENOENT') {
|
|
559
|
+
} catch (error: unknown) {
|
|
560
|
+
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
|
|
440
561
|
console.error(
|
|
441
562
|
`Failed to read thread spec for channel ${channelId} thread ${threadId}`,
|
|
442
563
|
error,
|
|
@@ -444,12 +565,12 @@ export const storageService = {
|
|
|
444
565
|
}
|
|
445
566
|
}
|
|
446
567
|
|
|
447
|
-
let state = {};
|
|
568
|
+
let state: unknown = {};
|
|
448
569
|
try {
|
|
449
570
|
const stateContent = await fs.readFile(statePath, 'utf-8');
|
|
450
571
|
state = JSON.parse(stateContent);
|
|
451
|
-
} catch (error:
|
|
452
|
-
if (error.code !== 'ENOENT') {
|
|
572
|
+
} catch (error: unknown) {
|
|
573
|
+
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
|
|
453
574
|
console.error(
|
|
454
575
|
`Failed to read thread state for channel ${channelId} thread ${threadId}`,
|
|
455
576
|
error,
|
|
@@ -458,8 +579,8 @@ export const storageService = {
|
|
|
458
579
|
}
|
|
459
580
|
|
|
460
581
|
const generatedName =
|
|
461
|
-
|
|
462
|
-
?
|
|
582
|
+
isRecord(state) && typeof state.generatedName === 'string'
|
|
583
|
+
? state.generatedName.trim()
|
|
463
584
|
: '';
|
|
464
585
|
|
|
465
586
|
return {
|
|
@@ -478,28 +599,30 @@ export const storageService = {
|
|
|
478
599
|
let spec = '';
|
|
479
600
|
try {
|
|
480
601
|
spec = await fs.readFile(specPath, 'utf-8');
|
|
481
|
-
} catch (error:
|
|
482
|
-
if (error.code !== 'ENOENT') {
|
|
602
|
+
} catch (error: unknown) {
|
|
603
|
+
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
|
|
483
604
|
console.error(`Failed to read spec file for channel ${channelId}`, error);
|
|
484
605
|
}
|
|
485
606
|
}
|
|
486
607
|
|
|
487
|
-
let state = {};
|
|
608
|
+
let state: unknown = {};
|
|
488
609
|
try {
|
|
489
610
|
const stateContent = await fs.readFile(statePath, 'utf-8');
|
|
490
611
|
state = JSON.parse(stateContent);
|
|
491
|
-
} catch (error:
|
|
492
|
-
if (error.code !== 'ENOENT') {
|
|
612
|
+
} catch (error: unknown) {
|
|
613
|
+
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
|
|
493
614
|
console.error(`Failed to read state file for channel ${channelId}`, error);
|
|
494
615
|
}
|
|
495
616
|
}
|
|
496
617
|
|
|
618
|
+
const cwd = isRecord(state) && typeof state.cwd === 'string' ? state.cwd : undefined;
|
|
619
|
+
|
|
497
620
|
const details: ChannelDetails = {
|
|
498
621
|
id: channelId,
|
|
499
622
|
name: channelId,
|
|
500
623
|
spec,
|
|
501
624
|
state,
|
|
502
|
-
cwd
|
|
625
|
+
cwd,
|
|
503
626
|
};
|
|
504
627
|
|
|
505
628
|
details.threads = await storageService.getThreads({ channelId });
|
|
@@ -517,17 +640,14 @@ export const storageService = {
|
|
|
517
640
|
const statePath = `${channelDir}/state.json`;
|
|
518
641
|
|
|
519
642
|
try {
|
|
520
|
-
// 1. Fetch current details to get the existing state
|
|
521
643
|
const currentDetails = await storageService.getChannelDetails({ channelId });
|
|
522
644
|
const currentState = (currentDetails.state as Record<string, unknown>) || {};
|
|
523
645
|
|
|
524
|
-
// 2. Perform a shallow merge (patch)
|
|
525
646
|
const newState = {
|
|
526
647
|
...currentState,
|
|
527
648
|
...(patch as Record<string, unknown>),
|
|
528
649
|
};
|
|
529
650
|
|
|
530
|
-
// 3. Write back the merged state
|
|
531
651
|
await fs.mkdir(channelDir, { recursive: true });
|
|
532
652
|
await fs.writeFile(statePath, JSON.stringify(newState, null, 2));
|
|
533
653
|
} catch (error) {
|
|
@@ -548,17 +668,14 @@ export const storageService = {
|
|
|
548
668
|
const statePath = `${threadDir}/state.json`;
|
|
549
669
|
|
|
550
670
|
try {
|
|
551
|
-
// 1. Fetch current details to get the existing state
|
|
552
671
|
const currentDetails = await storageService.getThreadDetails({ channelId, threadId });
|
|
553
672
|
const currentState = (currentDetails.state as Record<string, unknown>) || {};
|
|
554
673
|
|
|
555
|
-
// 2. Perform a shallow merge (patch)
|
|
556
674
|
const newState = {
|
|
557
675
|
...currentState,
|
|
558
676
|
...(patch as Record<string, unknown>),
|
|
559
677
|
};
|
|
560
678
|
|
|
561
|
-
// 3. Write back the merged state
|
|
562
679
|
await fs.mkdir(threadDir, { recursive: true });
|
|
563
680
|
await fs.writeFile(statePath, JSON.stringify(newState, null, 2));
|
|
564
681
|
} catch (error) {
|
|
@@ -629,29 +746,30 @@ export const storageService = {
|
|
|
629
746
|
name: details.name || id,
|
|
630
747
|
description: details.description || '',
|
|
631
748
|
image: details.image,
|
|
632
|
-
|
|
749
|
+
plugins: details.plugins,
|
|
633
750
|
createdAt: details.createdAt,
|
|
634
751
|
updatedAt: details.updatedAt,
|
|
635
|
-
};
|
|
752
|
+
} satisfies Agent;
|
|
636
753
|
} catch {
|
|
637
754
|
return {
|
|
638
755
|
id,
|
|
639
756
|
name: id,
|
|
640
757
|
description: '',
|
|
758
|
+
plugins: [],
|
|
641
759
|
createdAt: new Date(),
|
|
642
760
|
updatedAt: new Date(),
|
|
643
|
-
};
|
|
761
|
+
} satisfies Agent;
|
|
644
762
|
}
|
|
645
763
|
}),
|
|
646
764
|
);
|
|
647
765
|
|
|
648
|
-
const system = await storageService.getAgentDetails({ agentId:
|
|
766
|
+
const system = await storageService.getAgentDetails({ agentId: SYSTEM_AGENT_ID });
|
|
649
767
|
const builtInSystemAgent: Agent = {
|
|
650
768
|
id: system.id,
|
|
651
769
|
name: system.name,
|
|
652
770
|
description: system.description || '',
|
|
653
771
|
image: system.image,
|
|
654
|
-
|
|
772
|
+
plugins: system.plugins,
|
|
655
773
|
createdAt: system.createdAt,
|
|
656
774
|
updatedAt: system.updatedAt,
|
|
657
775
|
};
|
|
@@ -664,20 +782,19 @@ export const storageService = {
|
|
|
664
782
|
|
|
665
783
|
return Array.from(deduped.values());
|
|
666
784
|
},
|
|
667
|
-
getPlugins: async (): Promise<
|
|
668
|
-
const [
|
|
669
|
-
|
|
785
|
+
getPlugins: async (): Promise<PluginDescriptor[]> => {
|
|
786
|
+
const [builtIn, fromDisk] = await Promise.all([
|
|
787
|
+
listBuiltInPluginDescriptors(),
|
|
670
788
|
listPluginsFromDisk(),
|
|
671
789
|
]);
|
|
672
790
|
|
|
673
|
-
const merged = [...
|
|
674
|
-
const deduped = new Map<string,
|
|
791
|
+
const merged = [...builtIn, ...fromDisk];
|
|
792
|
+
const deduped = new Map<string, PluginDescriptor>();
|
|
675
793
|
for (const plugin of merged) {
|
|
676
794
|
if (!deduped.has(plugin.id)) {
|
|
677
795
|
deduped.set(plugin.id, plugin);
|
|
678
796
|
}
|
|
679
797
|
}
|
|
680
|
-
|
|
681
798
|
return Array.from(deduped.values());
|
|
682
799
|
},
|
|
683
800
|
getAgentDetails: async ({ agentId }: { agentId: string }): Promise<AgentDetails> => {
|
|
@@ -691,27 +808,32 @@ export const storageService = {
|
|
|
691
808
|
const agentMd = await fs.readFile(agentMdPath, 'utf-8');
|
|
692
809
|
const { data, content: instructions } = matter(agentMd);
|
|
693
810
|
const discoveredImage = await resolveEntityImageDataUrl(agentDir);
|
|
811
|
+
const stats = await fs.stat(agentMdPath);
|
|
812
|
+
|
|
813
|
+
const pluginRefs = parsePluginRefs(data.plugins);
|
|
694
814
|
|
|
695
815
|
diskDetails = {
|
|
696
816
|
id: agentId,
|
|
697
|
-
name: data.name
|
|
817
|
+
name: typeof data.name === 'string' ? data.name : agentId,
|
|
698
818
|
instructions: instructions.trim(),
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
description: data.description
|
|
819
|
+
plugins: pluginRefs.map((ref) => ref.id),
|
|
820
|
+
pluginRefs,
|
|
821
|
+
description: typeof data.description === 'string' ? data.description : '',
|
|
702
822
|
image: discoveredImage || undefined,
|
|
703
|
-
createdAt:
|
|
704
|
-
updatedAt:
|
|
823
|
+
createdAt: stats.birthtime,
|
|
824
|
+
updatedAt: stats.mtime,
|
|
705
825
|
};
|
|
706
826
|
} catch (error) {
|
|
707
|
-
if (agentId !==
|
|
827
|
+
if (agentId !== SYSTEM_AGENT_ID) {
|
|
708
828
|
const err = new Error(`Agent "${agentId}" does not exist.`);
|
|
709
829
|
(err as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
|
|
710
830
|
throw err;
|
|
711
831
|
}
|
|
832
|
+
// swallow: system agent has on-disk overrides optional
|
|
833
|
+
void error;
|
|
712
834
|
}
|
|
713
835
|
|
|
714
|
-
if (agentId ===
|
|
836
|
+
if (agentId === SYSTEM_AGENT_ID) {
|
|
715
837
|
return getSystemAgentDetails(diskDetails);
|
|
716
838
|
}
|
|
717
839
|
|
|
@@ -723,6 +845,132 @@ export const storageService = {
|
|
|
723
845
|
|
|
724
846
|
return diskDetails as AgentDetails;
|
|
725
847
|
},
|
|
848
|
+
createAgent: async ({
|
|
849
|
+
agentId,
|
|
850
|
+
name,
|
|
851
|
+
description = '',
|
|
852
|
+
instructions,
|
|
853
|
+
plugins,
|
|
854
|
+
}: {
|
|
855
|
+
agentId: string;
|
|
856
|
+
name: string;
|
|
857
|
+
description?: string;
|
|
858
|
+
instructions: string;
|
|
859
|
+
plugins: PluginRef[];
|
|
860
|
+
}): Promise<void> => {
|
|
861
|
+
assertValidDiskAgentId(agentId);
|
|
862
|
+
const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
|
|
863
|
+
const agentMdPath = path.join(agentDir, 'AGENT.md');
|
|
864
|
+
|
|
865
|
+
try {
|
|
866
|
+
await fs.access(agentMdPath);
|
|
867
|
+
throw new Error(`Agent "${agentId}" already exists`);
|
|
868
|
+
} catch (error: unknown) {
|
|
869
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
870
|
+
if (code === 'ENOENT') {
|
|
871
|
+
// proceed
|
|
872
|
+
} else if (error instanceof Error && error.message.includes('already exists')) {
|
|
873
|
+
throw error;
|
|
874
|
+
} else {
|
|
875
|
+
throw error;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
await fs.mkdir(agentDir, { recursive: true });
|
|
880
|
+
|
|
881
|
+
const data: Record<string, unknown> = {
|
|
882
|
+
name,
|
|
883
|
+
description,
|
|
884
|
+
plugins: serializePluginRefs(plugins),
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
const body = matter.stringify(`${instructions.trim()}\n`, data);
|
|
888
|
+
await fs.writeFile(agentMdPath, body, 'utf-8');
|
|
889
|
+
},
|
|
890
|
+
updateAgent: async ({
|
|
891
|
+
agentId,
|
|
892
|
+
name,
|
|
893
|
+
description,
|
|
894
|
+
instructions,
|
|
895
|
+
plugins,
|
|
896
|
+
}: {
|
|
897
|
+
agentId: string;
|
|
898
|
+
name?: string;
|
|
899
|
+
description?: string;
|
|
900
|
+
instructions?: string;
|
|
901
|
+
plugins?: PluginRef[];
|
|
902
|
+
}): Promise<void> => {
|
|
903
|
+
assertValidDiskAgentId(agentId);
|
|
904
|
+
const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
|
|
905
|
+
const agentMdPath = path.join(agentDir, 'AGENT.md');
|
|
906
|
+
|
|
907
|
+
let raw: string;
|
|
908
|
+
try {
|
|
909
|
+
raw = await fs.readFile(agentMdPath, 'utf-8');
|
|
910
|
+
} catch (error: unknown) {
|
|
911
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
912
|
+
const err = new Error(`Agent "${agentId}" does not exist.`);
|
|
913
|
+
(err as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
|
|
914
|
+
throw err;
|
|
915
|
+
}
|
|
916
|
+
throw error;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const parsed = matter(raw);
|
|
920
|
+
const nextData: Record<string, unknown> = { ...parsed.data };
|
|
921
|
+
if (name !== undefined) nextData.name = name;
|
|
922
|
+
if (description !== undefined) nextData.description = description;
|
|
923
|
+
if (plugins !== undefined) nextData.plugins = serializePluginRefs(plugins);
|
|
924
|
+
|
|
925
|
+
const nextContent = instructions !== undefined ? instructions : parsed.content;
|
|
926
|
+
const body = matter.stringify(`${String(nextContent).trim()}\n`, nextData);
|
|
927
|
+
await fs.writeFile(agentMdPath, body, 'utf-8');
|
|
928
|
+
},
|
|
929
|
+
deleteAgent: async ({ agentId }: { agentId: string }): Promise<void> => {
|
|
930
|
+
assertValidDiskAgentId(agentId);
|
|
931
|
+
const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
|
|
932
|
+
const agentMdPath = path.join(agentDir, 'AGENT.md');
|
|
933
|
+
const packageJsonPath = path.join(agentDir, 'package.json');
|
|
934
|
+
|
|
935
|
+
try {
|
|
936
|
+
await fs.access(agentDir);
|
|
937
|
+
} catch (error: unknown) {
|
|
938
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
939
|
+
const err = new Error(`Agent "${agentId}" does not exist.`);
|
|
940
|
+
(err as Error & { code?: string }).code = 'AGENT_NOT_FOUND';
|
|
941
|
+
throw err;
|
|
942
|
+
}
|
|
943
|
+
throw error;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
let hasPackage = false;
|
|
947
|
+
let hasAgentMd = false;
|
|
948
|
+
try {
|
|
949
|
+
await fs.access(packageJsonPath);
|
|
950
|
+
hasPackage = true;
|
|
951
|
+
} catch {
|
|
952
|
+
// ignore
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
await fs.access(agentMdPath);
|
|
956
|
+
hasAgentMd = true;
|
|
957
|
+
} catch {
|
|
958
|
+
// ignore
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if (hasPackage && !hasAgentMd) {
|
|
962
|
+
throw new Error(
|
|
963
|
+
`Cannot delete TypeScript agent package "${agentId}" through this action; remove the folder manually.`,
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
if (!hasAgentMd) {
|
|
967
|
+
throw new Error(
|
|
968
|
+
`Agent "${agentId}" has no AGENT.md and cannot be deleted through this action.`,
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
await fs.rm(agentDir, { recursive: true, force: true });
|
|
973
|
+
},
|
|
726
974
|
getEvents: async ({
|
|
727
975
|
channelId,
|
|
728
976
|
threadId,
|
|
@@ -748,7 +996,6 @@ export const storageService = {
|
|
|
748
996
|
return event;
|
|
749
997
|
});
|
|
750
998
|
|
|
751
|
-
// If we are at the channel level (no threadId), check which events have threads
|
|
752
999
|
if (!threadId) {
|
|
753
1000
|
const threadsDir = resolvePath(
|
|
754
1001
|
resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR + '/' + channelId + '/threads',
|
|
@@ -758,32 +1005,26 @@ export const storageService = {
|
|
|
758
1005
|
const threadSet = new Set(threadDirs);
|
|
759
1006
|
|
|
760
1007
|
return events.map((event) => {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const threadId = event.id;
|
|
764
|
-
|
|
765
|
-
// If an explicit threadId exists and has a directory, use it
|
|
766
|
-
if (threadId && threadSet.has(threadId)) {
|
|
1008
|
+
const eventThreadId = event.id;
|
|
1009
|
+
if (eventThreadId && threadSet.has(eventThreadId)) {
|
|
767
1010
|
return {
|
|
768
1011
|
...event,
|
|
769
1012
|
meta: {
|
|
770
|
-
...(event
|
|
1013
|
+
...(event.meta || {}),
|
|
771
1014
|
hasThread: true,
|
|
772
1015
|
},
|
|
773
1016
|
};
|
|
774
1017
|
}
|
|
775
|
-
|
|
776
1018
|
return event;
|
|
777
1019
|
});
|
|
778
1020
|
} catch {
|
|
779
|
-
// No threads folder or other error, just return events as is
|
|
780
1021
|
return events;
|
|
781
1022
|
}
|
|
782
1023
|
}
|
|
783
1024
|
|
|
784
1025
|
return events;
|
|
785
|
-
} catch (error:
|
|
786
|
-
if (error.code !== 'ENOENT') {
|
|
1026
|
+
} catch (error: unknown) {
|
|
1027
|
+
if ((error as NodeJS.ErrnoException)?.code !== 'ENOENT') {
|
|
787
1028
|
console.error(`Failed to get events for channel ${channelId} thread ${threadId}`, error);
|
|
788
1029
|
}
|
|
789
1030
|
return [];
|
|
@@ -803,8 +1044,8 @@ export const storageService = {
|
|
|
803
1044
|
if (threadId) {
|
|
804
1045
|
try {
|
|
805
1046
|
await fs.access(threadDir);
|
|
806
|
-
} catch (error:
|
|
807
|
-
if (error.code === 'ENOENT') {
|
|
1047
|
+
} catch (error: unknown) {
|
|
1048
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
808
1049
|
const threadTitle = buildThreadTitleFromEvent(event);
|
|
809
1050
|
await storageService.createThread({
|
|
810
1051
|
channelId,
|
|
@@ -819,7 +1060,6 @@ export const storageService = {
|
|
|
819
1060
|
await fs.mkdir(threadDir, { recursive: true });
|
|
820
1061
|
}
|
|
821
1062
|
|
|
822
|
-
// Ensure the event has a unique ID
|
|
823
1063
|
if (!event.id) {
|
|
824
1064
|
event.id = crypto.randomUUID();
|
|
825
1065
|
}
|
|
@@ -832,19 +1072,102 @@ export const storageService = {
|
|
|
832
1072
|
},
|
|
833
1073
|
getVariables: async (): Promise<Record<string, string | { value: string; secret: boolean }>> => {
|
|
834
1074
|
const variablesFilePath = resolvePath(resolveBaseDir() + '/' + VARIABLES_FILE);
|
|
835
|
-
const raw = await readJsonFile<
|
|
836
|
-
|
|
837
|
-
if (
|
|
838
|
-
|
|
1075
|
+
const raw = await readJsonFile<unknown>(variablesFilePath, {});
|
|
1076
|
+
|
|
1077
|
+
if (
|
|
1078
|
+
raw &&
|
|
1079
|
+
typeof raw === 'object' &&
|
|
1080
|
+
'variables' in raw &&
|
|
1081
|
+
Array.isArray((raw as { variables: unknown }).variables)
|
|
1082
|
+
) {
|
|
1083
|
+
const entries = ((raw as { variables: StoredVariable[] }).variables)
|
|
839
1084
|
.filter((v) => typeof v?.key === 'string')
|
|
840
1085
|
.map((v) => [v.key, { value: String(v.value ?? ''), secret: !!v.secret }] as const);
|
|
841
1086
|
return Object.fromEntries(entries);
|
|
842
1087
|
}
|
|
843
1088
|
|
|
844
|
-
// Legacy or simple format
|
|
845
1089
|
return toVariablesRecord(raw);
|
|
846
1090
|
},
|
|
847
1091
|
|
|
1092
|
+
createVariable: async ({
|
|
1093
|
+
key,
|
|
1094
|
+
value,
|
|
1095
|
+
secret = false,
|
|
1096
|
+
}: {
|
|
1097
|
+
key: string;
|
|
1098
|
+
value: string;
|
|
1099
|
+
secret?: boolean;
|
|
1100
|
+
}): Promise<void> => {
|
|
1101
|
+
const variablesFilePath = resolvePath(resolveBaseDir() + '/' + VARIABLES_FILE);
|
|
1102
|
+
const raw = await readJsonFile<unknown>(variablesFilePath, { version: 1, variables: [] });
|
|
1103
|
+
|
|
1104
|
+
let variables: StoredVariable[] = [];
|
|
1105
|
+
if (
|
|
1106
|
+
raw &&
|
|
1107
|
+
typeof raw === 'object' &&
|
|
1108
|
+
'variables' in raw &&
|
|
1109
|
+
Array.isArray((raw as { variables: unknown }).variables)
|
|
1110
|
+
) {
|
|
1111
|
+
variables = (raw as { variables: StoredVariable[] }).variables;
|
|
1112
|
+
} else {
|
|
1113
|
+
variables = Object.entries(toVariablesRecord(raw)).map(([k, v]) => ({
|
|
1114
|
+
key: k,
|
|
1115
|
+
value: v,
|
|
1116
|
+
secret: false,
|
|
1117
|
+
}));
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const existingIndex = variables.findIndex((v) => v.key === key);
|
|
1121
|
+
if (existingIndex !== -1) {
|
|
1122
|
+
variables[existingIndex] = { key, value, secret };
|
|
1123
|
+
} else {
|
|
1124
|
+
variables.push({ key, value, secret });
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
await fs.mkdir(path.dirname(variablesFilePath), { recursive: true });
|
|
1128
|
+
await fs.writeFile(
|
|
1129
|
+
variablesFilePath,
|
|
1130
|
+
JSON.stringify({ version: 1, variables }, null, 2),
|
|
1131
|
+
'utf-8',
|
|
1132
|
+
);
|
|
1133
|
+
processService.syncWorkspaceVariablesToProcessEnv();
|
|
1134
|
+
},
|
|
1135
|
+
|
|
1136
|
+
deleteVariable: async ({ key }: { key: string }): Promise<void> => {
|
|
1137
|
+
const variablesFilePath = resolvePath(resolveBaseDir() + '/' + VARIABLES_FILE);
|
|
1138
|
+
const raw = await readJsonFile<unknown>(variablesFilePath, { version: 1, variables: [] });
|
|
1139
|
+
|
|
1140
|
+
let variables: StoredVariable[] = [];
|
|
1141
|
+
if (
|
|
1142
|
+
raw &&
|
|
1143
|
+
typeof raw === 'object' &&
|
|
1144
|
+
'variables' in raw &&
|
|
1145
|
+
Array.isArray((raw as { variables: unknown }).variables)
|
|
1146
|
+
) {
|
|
1147
|
+
variables = (raw as { variables: StoredVariable[] }).variables;
|
|
1148
|
+
} else {
|
|
1149
|
+
variables = Object.entries(toVariablesRecord(raw)).map(([k, v]) => ({
|
|
1150
|
+
key: k,
|
|
1151
|
+
value: v,
|
|
1152
|
+
secret: false,
|
|
1153
|
+
}));
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const newVariables = variables.filter((v) => v.key !== key);
|
|
1157
|
+
|
|
1158
|
+
if (newVariables.length === variables.length) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
await fs.mkdir(path.dirname(variablesFilePath), { recursive: true });
|
|
1163
|
+
await fs.writeFile(
|
|
1164
|
+
variablesFilePath,
|
|
1165
|
+
JSON.stringify({ version: 1, variables: newVariables }, null, 2),
|
|
1166
|
+
'utf-8',
|
|
1167
|
+
);
|
|
1168
|
+
processService.syncWorkspaceVariablesToProcessEnv();
|
|
1169
|
+
},
|
|
1170
|
+
|
|
848
1171
|
listFiles: async ({
|
|
849
1172
|
channelId,
|
|
850
1173
|
path: subPath = '',
|
|
@@ -862,21 +1185,26 @@ export const storageService = {
|
|
|
862
1185
|
const resolvedBase = path.resolve(baseCwd);
|
|
863
1186
|
const targetDir = path.resolve(resolvedBase, subPath);
|
|
864
1187
|
|
|
865
|
-
// Security check: ensure target is within baseCwd
|
|
866
1188
|
if (!targetDir.startsWith(resolvedBase)) {
|
|
867
1189
|
throw new Error('Access denied: directory escape');
|
|
868
1190
|
}
|
|
869
1191
|
|
|
870
1192
|
const entries = await fs.readdir(targetDir, { withFileTypes: true });
|
|
871
1193
|
return entries
|
|
872
|
-
.filter((e) => !e.name.startsWith('.'))
|
|
1194
|
+
.filter((e) => !e.name.startsWith('.'))
|
|
873
1195
|
.map((e) => ({
|
|
874
1196
|
name: e.name,
|
|
875
1197
|
isDirectory: e.isDirectory(),
|
|
876
1198
|
}));
|
|
877
1199
|
},
|
|
878
1200
|
|
|
879
|
-
readFile: async ({
|
|
1201
|
+
readFile: async ({
|
|
1202
|
+
channelId,
|
|
1203
|
+
path: filePath,
|
|
1204
|
+
}: {
|
|
1205
|
+
channelId: string;
|
|
1206
|
+
path: string;
|
|
1207
|
+
}): Promise<string> => {
|
|
880
1208
|
const details = await storageService.getChannelDetails({ channelId });
|
|
881
1209
|
const baseCwd = details.cwd;
|
|
882
1210
|
|
|
@@ -887,7 +1215,6 @@ export const storageService = {
|
|
|
887
1215
|
const resolvedBase = path.resolve(baseCwd);
|
|
888
1216
|
const targetFile = path.resolve(resolvedBase, filePath);
|
|
889
1217
|
|
|
890
|
-
// Security check: ensure target is within baseCwd
|
|
891
1218
|
if (!targetFile.startsWith(resolvedBase)) {
|
|
892
1219
|
throw new Error('Access denied: directory escape');
|
|
893
1220
|
}
|
|
@@ -895,6 +1222,11 @@ export const storageService = {
|
|
|
895
1222
|
return fs.readFile(targetFile, 'utf-8');
|
|
896
1223
|
},
|
|
897
1224
|
|
|
1225
|
+
appendMemory: memoryService.appendMemory,
|
|
1226
|
+
listMemories: memoryService.listMemories,
|
|
1227
|
+
deleteMemory: memoryService.deleteMemory,
|
|
1228
|
+
updateMemory: memoryService.updateMemory,
|
|
1229
|
+
|
|
898
1230
|
/**
|
|
899
1231
|
* Hydrates the full OpenBot state from disk/storage before a run.
|
|
900
1232
|
*/
|
|
@@ -916,7 +1248,7 @@ export const storageService = {
|
|
|
916
1248
|
}
|
|
917
1249
|
|
|
918
1250
|
let channelDetails;
|
|
919
|
-
if (channelId
|
|
1251
|
+
if (channelId) {
|
|
920
1252
|
try {
|
|
921
1253
|
channelDetails = await storageService.getChannelDetails({ channelId });
|
|
922
1254
|
} catch (error) {
|
|
@@ -946,10 +1278,13 @@ export const storageService = {
|
|
|
946
1278
|
id: agentDetails.id,
|
|
947
1279
|
name: agentDetails.name,
|
|
948
1280
|
description: agentDetails.description || '',
|
|
1281
|
+
image: agentDetails.image,
|
|
949
1282
|
instructions: agentDetails.instructions || '',
|
|
950
|
-
runtime: agentDetails.runtime,
|
|
951
1283
|
plugins: agentDetails.plugins,
|
|
952
|
-
|
|
1284
|
+
pluginRefs: agentDetails.pluginRefs,
|
|
1285
|
+
createdAt: agentDetails.createdAt,
|
|
1286
|
+
updatedAt: agentDetails.updatedAt,
|
|
1287
|
+
} satisfies AgentDetails,
|
|
953
1288
|
channelDetails: channelDetails as ChannelDetails,
|
|
954
1289
|
threadDetails: threadDetails as ThreadDetails,
|
|
955
1290
|
};
|