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