openbot 0.3.5 → 0.4.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.
Files changed (98) hide show
  1. package/README.md +15 -16
  2. package/dist/app/agent-ids.js +4 -0
  3. package/dist/app/cli.js +1 -1
  4. package/dist/app/config.js +0 -19
  5. package/dist/app/server.js +8 -14
  6. package/dist/assets/icon.svg +9 -3
  7. package/dist/bus/services.js +78 -132
  8. package/dist/harness/agent-invoke-run.js +44 -0
  9. package/dist/harness/agent-turn.js +99 -0
  10. package/dist/harness/channel-participants.js +40 -0
  11. package/dist/harness/constants.js +2 -0
  12. package/dist/harness/context-meter.js +97 -0
  13. package/dist/harness/context.js +98 -45
  14. package/dist/harness/dispatch.js +144 -0
  15. package/dist/harness/dispatcher.js +45 -156
  16. package/dist/harness/history.js +177 -0
  17. package/dist/harness/index.js +91 -0
  18. package/dist/harness/orchestration.js +88 -0
  19. package/dist/harness/participants.js +22 -0
  20. package/dist/harness/run-harness.js +154 -0
  21. package/dist/harness/run.js +98 -0
  22. package/dist/harness/runtime-factory.js +0 -34
  23. package/dist/harness/runtime.js +57 -0
  24. package/dist/harness/todo-dispatch.js +51 -0
  25. package/dist/harness/todos.js +5 -0
  26. package/dist/harness/turn.js +79 -0
  27. package/dist/plugins/approval/index.js +105 -149
  28. package/dist/plugins/delegation/index.js +119 -32
  29. package/dist/plugins/memory/index.js +103 -14
  30. package/dist/plugins/memory/service.js +152 -0
  31. package/dist/plugins/openbot/context.js +80 -0
  32. package/dist/plugins/openbot/history.js +98 -0
  33. package/dist/plugins/openbot/index.js +31 -0
  34. package/dist/plugins/openbot/runtime.js +317 -0
  35. package/dist/plugins/openbot/system-prompt.js +5 -0
  36. package/dist/plugins/plugin-manager/index.js +105 -0
  37. package/dist/plugins/storage/index.js +573 -0
  38. package/dist/plugins/storage/service.js +1159 -0
  39. package/dist/plugins/storage-tools/index.js +2 -2
  40. package/dist/plugins/thread-namer/index.js +72 -0
  41. package/dist/plugins/thread-naming/generate-title.js +44 -0
  42. package/dist/plugins/thread-naming/index.js +103 -0
  43. package/dist/plugins/threads/index.js +114 -0
  44. package/dist/plugins/todo/index.js +24 -25
  45. package/dist/plugins/ui/index.js +2 -32
  46. package/dist/registry/plugins.js +3 -9
  47. package/dist/services/plugins/domain.js +1 -0
  48. package/dist/services/plugins/plugin-cache.js +9 -0
  49. package/dist/services/plugins/registry.js +110 -0
  50. package/dist/services/plugins/service.js +177 -0
  51. package/dist/services/plugins/types.js +1 -0
  52. package/dist/services/process.js +29 -0
  53. package/dist/services/storage.js +41 -15
  54. package/dist/services/thread-naming.js +81 -0
  55. package/docs/agents.md +16 -10
  56. package/docs/architecture.md +2 -2
  57. package/docs/plugins.md +6 -15
  58. package/docs/templates/AGENT.example.md +7 -13
  59. package/package.json +1 -2
  60. package/src/app/agent-ids.ts +5 -0
  61. package/src/app/cli.ts +1 -1
  62. package/src/app/config.ts +1 -31
  63. package/src/app/server.ts +8 -16
  64. package/src/app/types.ts +70 -190
  65. package/src/assets/icon.svg +9 -3
  66. package/src/harness/index.ts +145 -0
  67. package/src/plugins/approval/index.ts +91 -189
  68. package/src/plugins/delegation/index.ts +136 -39
  69. package/src/plugins/memory/index.ts +112 -15
  70. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  71. package/src/plugins/openbot/context.ts +91 -0
  72. package/src/plugins/openbot/history.ts +107 -0
  73. package/src/plugins/openbot/index.ts +37 -0
  74. package/src/plugins/openbot/runtime.ts +384 -0
  75. package/src/plugins/openbot/system-prompt.ts +7 -0
  76. package/src/plugins/plugin-manager/index.ts +122 -0
  77. package/src/plugins/shell/index.ts +1 -1
  78. package/src/plugins/storage/index.ts +633 -0
  79. package/src/{services/storage.ts → plugins/storage/service.ts} +257 -72
  80. package/src/{bus/types.ts → services/plugins/domain.ts} +20 -7
  81. package/src/services/plugins/plugin-cache.ts +13 -0
  82. package/src/{registry/plugins.ts → services/plugins/registry.ts} +25 -27
  83. package/src/services/{plugins.ts → plugins/service.ts} +96 -2
  84. package/src/{bus/plugin.ts → services/plugins/types.ts} +3 -3
  85. package/src/bus/services.ts +0 -908
  86. package/src/harness/context.ts +0 -356
  87. package/src/harness/dispatcher.ts +0 -379
  88. package/src/harness/mcp.ts +0 -78
  89. package/src/harness/runtime-factory.ts +0 -129
  90. package/src/harness/todo-advance.ts +0 -128
  91. package/src/plugins/ai-sdk/index.ts +0 -41
  92. package/src/plugins/ai-sdk/runtime.ts +0 -468
  93. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  94. package/src/plugins/mcp/index.ts +0 -128
  95. package/src/plugins/storage-tools/index.ts +0 -90
  96. package/src/plugins/todo/index.ts +0 -64
  97. package/src/plugins/ui/index.ts +0 -227
  98. /package/src/{harness → services}/process.ts +0 -0
@@ -0,0 +1,1159 @@
1
+ import { ORCHESTRATOR_AGENT_ID, STATE_AGENT_ID } from '../../app/agent-ids.js';
2
+ import { DEFAULT_PLUGINS_DIR, DEFAULT_AGENTS_DIR, DEFAULT_BASE_DIR, DEFAULT_CHANNELS_DIR, loadConfig, resolvePath, VARIABLES_FILE, } from '../../app/config.js';
3
+ import fs from 'node:fs/promises';
4
+ import { readFileSync } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath, pathToFileURL } from 'node:url';
7
+ import crypto from 'node:crypto';
8
+ import matter from 'gray-matter';
9
+ import { openbotPlugin } from '../openbot/index.js';
10
+ import { OPENBOT_SYSTEM_PROMPT } from '../openbot/system-prompt.js';
11
+ import { listBuiltInPlugins, parsePluginModule } from '../../services/plugins/registry.js';
12
+ import { processService } from '../../services/process.js';
13
+ import { memoryService } from '../memory/service.js';
14
+ const resolveBaseDir = () => {
15
+ const config = loadConfig();
16
+ return resolvePath(config.baseDir || DEFAULT_BASE_DIR);
17
+ };
18
+ const ENTITY_SVG_CANDIDATE_NAMES = ['avatar.svg', 'icon.svg', 'image.svg', 'logo.svg'];
19
+ const toSvgDataUrl = (svg) => `data:image/svg+xml;base64,${Buffer.from(svg, 'utf-8').toString('base64')}`;
20
+ let bundledSystemAgentImage;
21
+ let bundledSystemAgentImageLoaded = false;
22
+ /** OpenBot mark from `src/assets/icon.svg` (also copied to `dist/assets` at build). */
23
+ function getBundledSystemAgentImage() {
24
+ if (bundledSystemAgentImageLoaded)
25
+ return bundledSystemAgentImage;
26
+ bundledSystemAgentImageLoaded = true;
27
+ try {
28
+ const iconPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '../../assets/icon.svg');
29
+ const trimmed = readFileSync(iconPath, 'utf-8').trim();
30
+ if (!trimmed.startsWith('<svg'))
31
+ return undefined;
32
+ bundledSystemAgentImage = toSvgDataUrl(trimmed);
33
+ }
34
+ catch {
35
+ bundledSystemAgentImage = undefined;
36
+ }
37
+ return bundledSystemAgentImage;
38
+ }
39
+ const tryReadSvgDataUrl = async (filePath) => {
40
+ try {
41
+ const svg = await fs.readFile(filePath, 'utf-8');
42
+ const trimmed = svg.trim();
43
+ if (!trimmed.startsWith('<svg'))
44
+ return null;
45
+ return toSvgDataUrl(trimmed);
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ };
51
+ const resolveEntityImageDataUrl = async (entityDir) => {
52
+ const preferredDirs = [path.join(entityDir, 'assets'), entityDir];
53
+ for (const dir of preferredDirs) {
54
+ for (const fileName of ENTITY_SVG_CANDIDATE_NAMES) {
55
+ const dataUrl = await tryReadSvgDataUrl(path.join(dir, fileName));
56
+ if (dataUrl)
57
+ return dataUrl;
58
+ }
59
+ }
60
+ for (const dir of preferredDirs) {
61
+ try {
62
+ const entries = await fs.readdir(dir, { withFileTypes: true });
63
+ const firstSvg = entries.find((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.svg'));
64
+ if (!firstSvg)
65
+ continue;
66
+ const dataUrl = await tryReadSvgDataUrl(path.join(dir, firstSvg.name));
67
+ if (dataUrl)
68
+ return dataUrl;
69
+ }
70
+ catch {
71
+ // ignore
72
+ }
73
+ }
74
+ return undefined;
75
+ };
76
+ const getConversationDir = (channelId, threadId) => {
77
+ const base = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR + '/' + channelId);
78
+ return threadId ? `${base}/threads/${threadId}` : base;
79
+ };
80
+ /** Built-in orchestrator agent id. Not creatable as a normal disk agent. */
81
+ const SYSTEM_AGENT_ID = ORCHESTRATOR_AGENT_ID;
82
+ const SYSTEM_DEFAULT_PLUGINS = [
83
+ { id: 'openbot', config: { model: 'openai/gpt-5.4-mini' } },
84
+ { id: 'shell' },
85
+ { id: 'approval' },
86
+ { id: 'memory' },
87
+ { id: 'delegation' },
88
+ { id: 'storage' },
89
+ ];
90
+ /** No `openbot` / `shell` — storage-side effects and infra plugins only. */
91
+ const STATE_DEFAULT_PLUGINS = [
92
+ { id: 'storage' },
93
+ { id: 'plugin-manager' },
94
+ ];
95
+ const STATE_AGENT_INSTRUCTIONS = 'Built-in infra agent for deterministic state reads. No conversational model is attached; handle storage, approvals, memory, and plugin marketplace events.';
96
+ function getSystemAgentDetails(overrides) {
97
+ const defaults = {
98
+ id: SYSTEM_AGENT_ID,
99
+ name: 'OpenBot',
100
+ image: getBundledSystemAgentImage(),
101
+ description: 'First-party orchestration agent for OpenBot.',
102
+ instructions: OPENBOT_SYSTEM_PROMPT,
103
+ plugins: SYSTEM_DEFAULT_PLUGINS.map((ref) => ref.id),
104
+ pluginRefs: SYSTEM_DEFAULT_PLUGINS,
105
+ createdAt: new Date(),
106
+ updatedAt: new Date(),
107
+ };
108
+ if (!overrides)
109
+ return defaults;
110
+ const refs = overrides.pluginRefs && overrides.pluginRefs.length > 0
111
+ ? overrides.pluginRefs
112
+ : defaults.pluginRefs;
113
+ const diskInstructions = overrides.instructions?.trim();
114
+ const instructions = diskInstructions && diskInstructions.length > 0 ? diskInstructions : defaults.instructions;
115
+ return {
116
+ ...defaults,
117
+ ...overrides,
118
+ id: SYSTEM_AGENT_ID,
119
+ instructions,
120
+ image: overrides.image || defaults.image,
121
+ plugins: refs.map((ref) => ref.id),
122
+ pluginRefs: refs,
123
+ updatedAt: new Date(),
124
+ };
125
+ }
126
+ function getStateAgentDetails(overrides) {
127
+ const defaults = {
128
+ id: STATE_AGENT_ID,
129
+ name: 'State',
130
+ image: getBundledSystemAgentImage(),
131
+ description: 'Infrastructure agent for OpenBot — storage and hooks without an LLM.',
132
+ instructions: STATE_AGENT_INSTRUCTIONS,
133
+ plugins: STATE_DEFAULT_PLUGINS.map((ref) => ref.id),
134
+ pluginRefs: STATE_DEFAULT_PLUGINS,
135
+ hidden: true,
136
+ createdAt: new Date(),
137
+ updatedAt: new Date(),
138
+ };
139
+ if (!overrides)
140
+ return defaults;
141
+ const refs = overrides.pluginRefs && overrides.pluginRefs.length > 0
142
+ ? overrides.pluginRefs
143
+ : defaults.pluginRefs;
144
+ const diskInstructions = overrides.instructions?.trim();
145
+ const instructions = diskInstructions && diskInstructions.length > 0 ? diskInstructions : defaults.instructions;
146
+ return {
147
+ ...defaults,
148
+ ...overrides,
149
+ id: STATE_AGENT_ID,
150
+ instructions,
151
+ image: overrides.image || defaults.image,
152
+ hidden: overrides.hidden !== undefined ? overrides.hidden : defaults.hidden,
153
+ plugins: refs.map((ref) => ref.id),
154
+ pluginRefs: refs,
155
+ updatedAt: new Date(),
156
+ };
157
+ }
158
+ const agentSummaryFromDetails = (details) => ({
159
+ id: details.id,
160
+ name: details.name || details.id,
161
+ description: details.description || '',
162
+ image: details.image,
163
+ plugins: details.plugins,
164
+ hidden: details.hidden,
165
+ createdAt: details.createdAt,
166
+ updatedAt: details.updatedAt,
167
+ });
168
+ // Suppress unused warning until system agent customization re-uses openbotPlugin metadata.
169
+ void openbotPlugin;
170
+ /** Built-in agents may persist optional `agents/<id>/AGENT.md` overlays; read path merges them with defaults. */
171
+ const isBuiltinOverlayAgentId = (agentId) => agentId === SYSTEM_AGENT_ID || agentId === STATE_AGENT_ID;
172
+ const assertAgentIdFormat = (agentId) => {
173
+ if (!agentId || typeof agentId !== 'string') {
174
+ throw new Error('agentId is required');
175
+ }
176
+ if (!/^[a-zA-Z0-9_-]+$/.test(agentId)) {
177
+ throw new Error('agentId must contain only letters, digits, underscores, and hyphens');
178
+ }
179
+ };
180
+ const getAgentsRootDir = () => path.join(resolveBaseDir(), DEFAULT_AGENTS_DIR);
181
+ const getLastReadFilePath = () => path.join(resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR), '_meta', 'last-read.json');
182
+ const THREAD_TITLE_MAX_LENGTH = 80;
183
+ const buildThreadTitleFromEvent = (event) => {
184
+ let rawContent = '';
185
+ if (event.type === 'agent:invoke' &&
186
+ event.data?.role === 'user' &&
187
+ typeof event.data.content === 'string') {
188
+ rawContent = event.data.content;
189
+ }
190
+ const normalized = rawContent.replace(/\s+/g, ' ').trim();
191
+ if (!normalized)
192
+ return undefined;
193
+ if (normalized.length <= THREAD_TITLE_MAX_LENGTH) {
194
+ return normalized;
195
+ }
196
+ return `${normalized.slice(0, THREAD_TITLE_MAX_LENGTH).trimEnd()}...`;
197
+ };
198
+ const readJsonFile = async (filePath, fallback) => {
199
+ try {
200
+ return JSON.parse(await fs.readFile(filePath, 'utf-8'));
201
+ }
202
+ catch (e) {
203
+ if (e?.code === 'ENOENT')
204
+ return fallback;
205
+ throw e;
206
+ }
207
+ };
208
+ const toVariablesRecord = (raw) => {
209
+ if (!raw || typeof raw !== 'object') {
210
+ return {};
211
+ }
212
+ if ('variables' in raw && Array.isArray(raw.variables)) {
213
+ const entries = raw.variables
214
+ .filter((variable) => typeof variable?.key === 'string')
215
+ .map((variable) => [variable.key, String(variable.value ?? '')]);
216
+ return Object.fromEntries(entries);
217
+ }
218
+ return Object.fromEntries(Object.entries(raw).map(([key, value]) => [
219
+ key,
220
+ String(value ?? ''),
221
+ ]));
222
+ };
223
+ const listBuiltInPluginDescriptors = async () => {
224
+ return listBuiltInPlugins().map((plugin) => ({
225
+ id: plugin.id,
226
+ name: plugin.name,
227
+ description: plugin.description,
228
+ builtIn: true,
229
+ image: plugin.image,
230
+ configSchema: plugin.configSchema,
231
+ createdAt: new Date(),
232
+ updatedAt: new Date(),
233
+ }));
234
+ };
235
+ /**
236
+ * Walk `plugins/` and yield candidate plugin ids (npm names). Includes scoped
237
+ * packages by recursing one level into directories starting with `@`.
238
+ */
239
+ const listInstalledPluginIds = async (pluginsDir) => {
240
+ const out = [];
241
+ let topEntries;
242
+ try {
243
+ topEntries = await fs.readdir(pluginsDir, { withFileTypes: true });
244
+ }
245
+ catch {
246
+ return out;
247
+ }
248
+ for (const entry of topEntries) {
249
+ if (entry.name.startsWith('.'))
250
+ continue;
251
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
252
+ continue;
253
+ if (entry.name.startsWith('@')) {
254
+ try {
255
+ const inner = await fs.readdir(path.join(pluginsDir, entry.name), { withFileTypes: true });
256
+ for (const sub of inner) {
257
+ if (sub.name.startsWith('.'))
258
+ continue;
259
+ if (sub.isDirectory() || sub.isSymbolicLink()) {
260
+ out.push(`${entry.name}/${sub.name}`);
261
+ }
262
+ }
263
+ }
264
+ catch {
265
+ // ignore
266
+ }
267
+ continue;
268
+ }
269
+ out.push(entry.name);
270
+ }
271
+ return out;
272
+ };
273
+ const listPluginsFromDisk = async () => {
274
+ const pluginsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_PLUGINS_DIR);
275
+ try {
276
+ await fs.access(pluginsDir);
277
+ }
278
+ catch {
279
+ await fs.mkdir(pluginsDir, { recursive: true });
280
+ }
281
+ const ids = await listInstalledPluginIds(pluginsDir);
282
+ const descriptors = await Promise.all(ids.map(async (id) => {
283
+ try {
284
+ const pluginDir = path.join(pluginsDir, id);
285
+ const distPath = path.join(pluginDir, 'dist', 'index.js');
286
+ const module = await import(pathToFileURL(distPath).href);
287
+ const parsed = parsePluginModule(module);
288
+ if (!parsed)
289
+ return null;
290
+ const image = await resolveEntityImageDataUrl(pluginDir);
291
+ return {
292
+ id,
293
+ name: parsed.name || id,
294
+ description: parsed.description || '',
295
+ builtIn: false,
296
+ image: parsed.image || image,
297
+ configSchema: parsed.configSchema,
298
+ createdAt: new Date(),
299
+ updatedAt: new Date(),
300
+ };
301
+ }
302
+ catch (error) {
303
+ console.warn(`[storage] Failed to load plugin ${id}:`, error);
304
+ return null;
305
+ }
306
+ }));
307
+ return descriptors.filter((d) => d !== null);
308
+ };
309
+ const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
310
+ /** Display-oriented fields persisted in a channel's `state.json`. */
311
+ const readChannelStateFileFields = (parsed) => {
312
+ if (!isRecord(parsed)) {
313
+ return { participants: [] };
314
+ }
315
+ const name = typeof parsed.name === 'string' && parsed.name.trim() ? parsed.name.trim() : undefined;
316
+ const cwd = typeof parsed.cwd === 'string' ? parsed.cwd : undefined;
317
+ const participants = [];
318
+ if (Array.isArray(parsed.participants)) {
319
+ for (const x of parsed.participants) {
320
+ if (typeof x === 'string' && x.trim())
321
+ participants.push(x.trim());
322
+ }
323
+ }
324
+ return { name, cwd, participants };
325
+ };
326
+ /**
327
+ * Parse the `plugins:` array from AGENT.md frontmatter. Each entry must have an
328
+ * `id`; `config` is optional. Strings are accepted as a shorthand for `{ id }`.
329
+ */
330
+ const parseHiddenFlag = (raw) => {
331
+ if (raw === true)
332
+ return true;
333
+ if (raw === false)
334
+ return false;
335
+ return undefined;
336
+ };
337
+ const parsePluginRefs = (raw) => {
338
+ if (!Array.isArray(raw))
339
+ return [];
340
+ const refs = [];
341
+ for (const entry of raw) {
342
+ if (typeof entry === 'string' && entry.trim()) {
343
+ refs.push({ id: entry.trim() });
344
+ continue;
345
+ }
346
+ if (isRecord(entry) && typeof entry.id === 'string' && entry.id.trim()) {
347
+ const config = isRecord(entry.config) ? entry.config : undefined;
348
+ refs.push({ id: entry.id.trim(), ...(config ? { config } : {}) });
349
+ }
350
+ }
351
+ return refs;
352
+ };
353
+ const serializePluginRefs = (refs) => refs.map((ref) => (ref.config ? { id: ref.id, config: ref.config } : { id: ref.id }));
354
+ export const storageService = {
355
+ getLastReadByChannel: async () => {
356
+ return readJsonFile(getLastReadFilePath(), {});
357
+ },
358
+ setLastReadForChannel: async ({ channelId, lastReadEventId, }) => {
359
+ const p = getLastReadFilePath();
360
+ await fs.mkdir(path.dirname(p), { recursive: true });
361
+ const map = await readJsonFile(p, {});
362
+ map[channelId] = lastReadEventId;
363
+ await fs.writeFile(p, JSON.stringify(map, null, 2), 'utf-8');
364
+ },
365
+ getChannels: async () => {
366
+ const channelsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR);
367
+ try {
368
+ await fs.access(channelsDir);
369
+ }
370
+ catch {
371
+ await fs.mkdir(channelsDir, { recursive: true });
372
+ }
373
+ const channelNames = (await fs.readdir(channelsDir)).filter((name) => !name.startsWith('.') && name !== '_meta');
374
+ const lastReadByChannel = await storageService.getLastReadByChannel();
375
+ const channels = await Promise.all(channelNames.map(async (name) => {
376
+ const channelDir = getConversationDir(name);
377
+ const statePath = path.join(channelDir, 'state.json');
378
+ let cwd;
379
+ let displayName = name;
380
+ let participants = [];
381
+ try {
382
+ const stateContent = await fs.readFile(statePath, 'utf-8');
383
+ const parsed = JSON.parse(stateContent);
384
+ const fields = readChannelStateFileFields(parsed);
385
+ cwd = fields.cwd;
386
+ displayName = fields.name ?? name;
387
+ participants = fields.participants;
388
+ }
389
+ catch {
390
+ // ignore
391
+ }
392
+ const channel = {
393
+ id: name,
394
+ name: displayName,
395
+ description: '',
396
+ cwd,
397
+ participants,
398
+ createdAt: new Date(),
399
+ updatedAt: new Date(),
400
+ };
401
+ const rid = lastReadByChannel[name];
402
+ try {
403
+ const events = await storageService.getEvents({ channelId: name });
404
+ const latestId = events[events.length - 1]?.id;
405
+ channel.hasUnseenMessages = !!(latestId && latestId !== rid);
406
+ }
407
+ catch {
408
+ channel.hasUnseenMessages = false;
409
+ }
410
+ try {
411
+ const allThreads = await storageService.getThreads({ channelId: name });
412
+ channel.recentThreads = allThreads
413
+ .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
414
+ .slice(0, 5);
415
+ }
416
+ catch {
417
+ channel.recentThreads = [];
418
+ }
419
+ return channel;
420
+ }));
421
+ return channels;
422
+ },
423
+ createChannel: async ({ channelId, spec, initialState, cwd, }) => {
424
+ const normalizedChannelId = channelId.trim();
425
+ if (!normalizedChannelId) {
426
+ throw new Error('channelId is required');
427
+ }
428
+ const channelDir = getConversationDir(normalizedChannelId);
429
+ const specPath = `${channelDir}/SPEC.md`;
430
+ const statePath = `${channelDir}/state.json`;
431
+ try {
432
+ await fs.access(channelDir);
433
+ throw new Error(`Channel "${normalizedChannelId}" already exists`);
434
+ }
435
+ catch (error) {
436
+ const code = error?.code;
437
+ if (code !== 'ENOENT') {
438
+ throw error;
439
+ }
440
+ }
441
+ const finalState = {
442
+ ...(initialState || {}),
443
+ };
444
+ if (cwd) {
445
+ finalState.cwd = cwd;
446
+ }
447
+ await fs.mkdir(channelDir, { recursive: true });
448
+ await fs.writeFile(specPath, spec?.trim() ||
449
+ `# ${normalizedChannelId}\n\nDefine the goals and rules for this channel here.\n`);
450
+ await fs.writeFile(statePath, JSON.stringify(finalState, null, 2));
451
+ },
452
+ createThread: async ({ channelId, threadId, threadTitle, initialState, }) => {
453
+ const normalizedChannelId = channelId.trim();
454
+ const normalizedThreadId = threadId.trim();
455
+ if (!normalizedChannelId)
456
+ throw new Error('channelId is required');
457
+ if (!normalizedThreadId)
458
+ throw new Error('threadId is required');
459
+ const threadDir = getConversationDir(normalizedChannelId, normalizedThreadId);
460
+ const statePath = `${threadDir}/state.json`;
461
+ try {
462
+ await fs.access(threadDir);
463
+ throw new Error(`Thread "${normalizedThreadId}" already exists in channel "${normalizedChannelId}"`);
464
+ }
465
+ catch (error) {
466
+ const code = error?.code;
467
+ if (code !== 'ENOENT') {
468
+ throw error;
469
+ }
470
+ }
471
+ const baseState = { ...(initialState || {}) };
472
+ if (threadTitle?.trim()) {
473
+ baseState.name = threadTitle.trim();
474
+ }
475
+ await fs.mkdir(threadDir, { recursive: true });
476
+ await fs.writeFile(statePath, JSON.stringify(baseState, null, 2));
477
+ },
478
+ getThreads: async ({ channelId }) => {
479
+ const threadsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR + '/' + channelId + '/threads');
480
+ try {
481
+ await fs.access(threadsDir);
482
+ }
483
+ catch {
484
+ return [];
485
+ }
486
+ const threadNames = (await fs.readdir(threadsDir)).filter((name) => !name.startsWith('.'));
487
+ const threads = await Promise.all(threadNames.map(async (name) => {
488
+ const threadPath = path.join(threadsDir, name);
489
+ const stats = await fs.stat(threadPath);
490
+ const threadStatePath = path.join(threadPath, 'state.json');
491
+ let threadDisplayName = name;
492
+ try {
493
+ const threadStateRaw = await fs.readFile(threadStatePath, 'utf-8');
494
+ const threadState = JSON.parse(threadStateRaw);
495
+ const threadName = typeof threadState.name === 'string' ? threadState.name.trim() : '';
496
+ if (threadName) {
497
+ threadDisplayName = threadName;
498
+ }
499
+ }
500
+ catch (error) {
501
+ if (error?.code !== 'ENOENT') {
502
+ console.error(`Failed to read thread state for channel ${channelId} thread ${name}`, error);
503
+ }
504
+ }
505
+ return {
506
+ id: name,
507
+ name: threadDisplayName,
508
+ channelId,
509
+ createdAt: stats.birthtime,
510
+ updatedAt: stats.mtime,
511
+ };
512
+ }));
513
+ return threads;
514
+ },
515
+ getThreadDetails: async ({ channelId, threadId, }) => {
516
+ const threadDir = getConversationDir(channelId, threadId);
517
+ const statePath = `${threadDir}/state.json`;
518
+ let state = {};
519
+ try {
520
+ const stateContent = await fs.readFile(statePath, 'utf-8');
521
+ state = JSON.parse(stateContent);
522
+ }
523
+ catch (error) {
524
+ if (error?.code !== 'ENOENT') {
525
+ console.error(`Failed to read thread state for channel ${channelId} thread ${threadId}`, error);
526
+ }
527
+ }
528
+ const threadName = isRecord(state) && typeof state.name === 'string'
529
+ ? state.name.trim()
530
+ : '';
531
+ return {
532
+ id: threadId,
533
+ name: threadName || threadId,
534
+ channelId,
535
+ state,
536
+ };
537
+ },
538
+ getChannelDetails: async ({ channelId }) => {
539
+ const channelDir = getConversationDir(channelId);
540
+ const specPath = `${channelDir}/SPEC.md`;
541
+ const statePath = `${channelDir}/state.json`;
542
+ let spec = '';
543
+ try {
544
+ spec = await fs.readFile(specPath, 'utf-8');
545
+ }
546
+ catch (error) {
547
+ if (error?.code !== 'ENOENT') {
548
+ console.error(`Failed to read spec file for channel ${channelId}`, error);
549
+ }
550
+ }
551
+ let state = {};
552
+ try {
553
+ const stateContent = await fs.readFile(statePath, 'utf-8');
554
+ state = JSON.parse(stateContent);
555
+ }
556
+ catch (error) {
557
+ if (error?.code !== 'ENOENT') {
558
+ console.error(`Failed to read state file for channel ${channelId}`, error);
559
+ }
560
+ }
561
+ const diskFields = readChannelStateFileFields(state);
562
+ const cwd = diskFields.cwd;
563
+ const displayName = diskFields.name ?? channelId;
564
+ const details = {
565
+ id: channelId,
566
+ name: displayName,
567
+ spec,
568
+ state,
569
+ cwd,
570
+ participants: diskFields.participants,
571
+ };
572
+ details.threads = await storageService.getThreads({ channelId });
573
+ return details;
574
+ },
575
+ patchChannelState: async ({ channelId, state: patch, }) => {
576
+ const channelDir = getConversationDir(channelId);
577
+ const statePath = `${channelDir}/state.json`;
578
+ try {
579
+ const currentDetails = await storageService.getChannelDetails({ channelId });
580
+ const currentState = currentDetails.state || {};
581
+ const newState = {
582
+ ...currentState,
583
+ ...patch,
584
+ };
585
+ await fs.mkdir(channelDir, { recursive: true });
586
+ await fs.writeFile(statePath, JSON.stringify(newState, null, 2));
587
+ }
588
+ catch (error) {
589
+ console.error(`Failed to patch channel state for channel ${channelId}`, error);
590
+ throw error;
591
+ }
592
+ },
593
+ patchThreadState: async ({ channelId, threadId, state: patch, }) => {
594
+ const threadDir = getConversationDir(channelId, threadId);
595
+ const statePath = `${threadDir}/state.json`;
596
+ try {
597
+ const currentDetails = await storageService.getThreadDetails({ channelId, threadId });
598
+ const currentState = currentDetails.state || {};
599
+ const newState = {
600
+ ...currentState,
601
+ ...patch,
602
+ };
603
+ await fs.mkdir(threadDir, { recursive: true });
604
+ await fs.writeFile(statePath, JSON.stringify(newState, null, 2));
605
+ }
606
+ catch (error) {
607
+ console.error(`Failed to patch thread state for channel ${channelId} thread ${threadId}`, error);
608
+ throw error;
609
+ }
610
+ },
611
+ patchChannelSpec: async ({ channelId, spec, }) => {
612
+ const channelDir = getConversationDir(channelId);
613
+ const specPath = `${channelDir}/SPEC.md`;
614
+ try {
615
+ await fs.mkdir(channelDir, { recursive: true });
616
+ await fs.writeFile(specPath, spec);
617
+ }
618
+ catch (error) {
619
+ console.error(`Failed to patch channel spec for channel ${channelId}`, error);
620
+ throw error;
621
+ }
622
+ },
623
+ getAgents: async () => {
624
+ const agentsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_AGENTS_DIR);
625
+ try {
626
+ await fs.access(agentsDir);
627
+ }
628
+ catch {
629
+ await fs.mkdir(agentsDir, { recursive: true });
630
+ }
631
+ const agentIds = (await fs.readdir(agentsDir)).filter((name) => !name.startsWith('.'));
632
+ const agents = await Promise.all(agentIds.map(async (id) => {
633
+ try {
634
+ const details = await storageService.getAgentDetails({ agentId: id });
635
+ return agentSummaryFromDetails(details);
636
+ }
637
+ catch {
638
+ return {
639
+ id,
640
+ name: id,
641
+ description: '',
642
+ plugins: [],
643
+ createdAt: new Date(),
644
+ updatedAt: new Date(),
645
+ };
646
+ }
647
+ }));
648
+ const system = await storageService.getAgentDetails({ agentId: SYSTEM_AGENT_ID });
649
+ const builtInSystemAgent = agentSummaryFromDetails(system);
650
+ const builtInStateRow = await storageService.getAgentDetails({ agentId: STATE_AGENT_ID });
651
+ const builtInStateAgent = agentSummaryFromDetails(builtInStateRow);
652
+ const deduped = new Map();
653
+ deduped.set(builtInSystemAgent.id, builtInSystemAgent);
654
+ deduped.set(builtInStateAgent.id, builtInStateAgent);
655
+ for (const agent of agents) {
656
+ if (!deduped.has(agent.id))
657
+ deduped.set(agent.id, agent);
658
+ }
659
+ return Array.from(deduped.values()).filter((agent) => !agent.hidden);
660
+ },
661
+ getPlugins: async () => {
662
+ const [builtIn, fromDisk] = await Promise.all([
663
+ listBuiltInPluginDescriptors(),
664
+ listPluginsFromDisk(),
665
+ ]);
666
+ const merged = [...builtIn, ...fromDisk];
667
+ const deduped = new Map();
668
+ for (const plugin of merged) {
669
+ if (!deduped.has(plugin.id)) {
670
+ deduped.set(plugin.id, plugin);
671
+ }
672
+ }
673
+ return Array.from(deduped.values());
674
+ },
675
+ getAgentDetails: async ({ agentId }) => {
676
+ const agentDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_AGENTS_DIR + '/' + agentId);
677
+ const agentMdPath = `${agentDir}/AGENT.md`;
678
+ let diskDetails;
679
+ try {
680
+ await fs.access(agentMdPath);
681
+ const agentMd = await fs.readFile(agentMdPath, 'utf-8');
682
+ const { data, content: instructions } = matter(agentMd);
683
+ const discoveredImage = await resolveEntityImageDataUrl(agentDir);
684
+ const stats = await fs.stat(agentMdPath);
685
+ const pluginRefs = parsePluginRefs(data.plugins);
686
+ const frontmatterImage = typeof data.image === 'string' && data.image.trim() !== ''
687
+ ? data.image.trim()
688
+ : undefined;
689
+ diskDetails = {
690
+ id: agentId,
691
+ name: typeof data.name === 'string' ? data.name : agentId,
692
+ instructions: instructions.trim(),
693
+ plugins: pluginRefs.map((ref) => ref.id),
694
+ pluginRefs,
695
+ description: typeof data.description === 'string' ? data.description : '',
696
+ image: frontmatterImage || discoveredImage || undefined,
697
+ hidden: parseHiddenFlag(data.hidden),
698
+ createdAt: stats.birthtime,
699
+ updatedAt: stats.mtime,
700
+ };
701
+ }
702
+ catch (error) {
703
+ if (agentId !== SYSTEM_AGENT_ID && agentId !== STATE_AGENT_ID) {
704
+ const err = new Error(`Agent "${agentId}" does not exist.`);
705
+ err.code = 'AGENT_NOT_FOUND';
706
+ throw err;
707
+ }
708
+ // swallow: built-in agents have optional `agents/<id>/AGENT.md` overrides
709
+ void error;
710
+ }
711
+ if (agentId === SYSTEM_AGENT_ID) {
712
+ return getSystemAgentDetails(diskDetails);
713
+ }
714
+ if (agentId === STATE_AGENT_ID) {
715
+ return getStateAgentDetails(diskDetails);
716
+ }
717
+ if (!diskDetails) {
718
+ const error = new Error(`Agent "${agentId}" does not exist.`);
719
+ error.code = 'AGENT_NOT_FOUND';
720
+ throw error;
721
+ }
722
+ return diskDetails;
723
+ },
724
+ createAgent: async ({ agentId, name, description = '', image, hidden, instructions, plugins, }) => {
725
+ assertAgentIdFormat(agentId);
726
+ const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
727
+ const agentMdPath = path.join(agentDir, 'AGENT.md');
728
+ try {
729
+ await fs.access(agentMdPath);
730
+ throw new Error(`Agent "${agentId}" already exists`);
731
+ }
732
+ catch (error) {
733
+ const code = error?.code;
734
+ if (code === 'ENOENT') {
735
+ // proceed
736
+ }
737
+ else if (error instanceof Error && error.message.includes('already exists')) {
738
+ throw error;
739
+ }
740
+ else {
741
+ throw error;
742
+ }
743
+ }
744
+ await fs.mkdir(agentDir, { recursive: true });
745
+ const data = {
746
+ name,
747
+ description,
748
+ plugins: serializePluginRefs(plugins),
749
+ };
750
+ if (typeof image === 'string' && image.trim() !== '') {
751
+ data.image = image.trim();
752
+ }
753
+ if (hidden === true) {
754
+ data.hidden = true;
755
+ }
756
+ const body = matter.stringify(`${instructions.trim()}\n`, data);
757
+ await fs.writeFile(agentMdPath, body, 'utf-8');
758
+ },
759
+ updateAgent: async ({ agentId, name, description, image, hidden, instructions, plugins, }) => {
760
+ assertAgentIdFormat(agentId);
761
+ const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
762
+ const agentMdPath = path.join(agentDir, 'AGENT.md');
763
+ let raw;
764
+ try {
765
+ raw = await fs.readFile(agentMdPath, 'utf-8');
766
+ }
767
+ catch (error) {
768
+ if (error?.code === 'ENOENT') {
769
+ if (!isBuiltinOverlayAgentId(agentId)) {
770
+ const err = new Error(`Agent "${agentId}" does not exist.`);
771
+ err.code = 'AGENT_NOT_FOUND';
772
+ throw err;
773
+ }
774
+ raw = '';
775
+ }
776
+ else {
777
+ throw error;
778
+ }
779
+ }
780
+ const parsed = raw === '' ? { data: {}, content: '' } : matter(raw);
781
+ const nextData = { ...parsed.data };
782
+ if (name !== undefined)
783
+ nextData.name = name;
784
+ if (description !== undefined)
785
+ nextData.description = description;
786
+ if (plugins !== undefined)
787
+ nextData.plugins = serializePluginRefs(plugins);
788
+ if (image !== undefined) {
789
+ if (typeof image === 'string' && image.trim() !== '') {
790
+ nextData.image = image.trim();
791
+ }
792
+ else {
793
+ delete nextData.image;
794
+ }
795
+ }
796
+ if (hidden !== undefined) {
797
+ if (hidden) {
798
+ nextData.hidden = true;
799
+ }
800
+ else {
801
+ delete nextData.hidden;
802
+ }
803
+ }
804
+ let nextContent = instructions !== undefined ? instructions : parsed.content;
805
+ // Built-in agents merge disk overlays with code defaults on read; on write, partial
806
+ // updates (e.g. plugins-only) must still persist a complete AGENT.md.
807
+ if (isBuiltinOverlayAgentId(agentId)) {
808
+ const pluginRefs = plugins ??
809
+ (nextData.plugins !== undefined ? parsePluginRefs(nextData.plugins) : undefined);
810
+ const diskOverrides = {};
811
+ if (typeof nextData.name === 'string' && nextData.name.trim() !== '') {
812
+ diskOverrides.name = nextData.name;
813
+ }
814
+ if (typeof nextData.description === 'string') {
815
+ diskOverrides.description = nextData.description;
816
+ }
817
+ const trimmedContent = String(nextContent).trim();
818
+ if (trimmedContent) {
819
+ diskOverrides.instructions = trimmedContent;
820
+ }
821
+ if (pluginRefs && pluginRefs.length > 0) {
822
+ diskOverrides.pluginRefs = pluginRefs;
823
+ }
824
+ const effective = agentId === SYSTEM_AGENT_ID
825
+ ? getSystemAgentDetails(diskOverrides)
826
+ : getStateAgentDetails(diskOverrides);
827
+ if (name === undefined)
828
+ nextData.name = effective.name;
829
+ if (description === undefined)
830
+ nextData.description = effective.description;
831
+ if (instructions === undefined && !String(nextContent).trim()) {
832
+ nextContent = effective.instructions;
833
+ }
834
+ }
835
+ const body = matter.stringify(`${String(nextContent).trim()}\n`, nextData);
836
+ await fs.mkdir(path.dirname(agentMdPath), { recursive: true });
837
+ await fs.writeFile(agentMdPath, body, 'utf-8');
838
+ },
839
+ deleteAgent: async ({ agentId }) => {
840
+ assertAgentIdFormat(agentId);
841
+ const agentDir = resolvePath(path.join(getAgentsRootDir(), agentId));
842
+ const agentMdPath = path.join(agentDir, 'AGENT.md');
843
+ const packageJsonPath = path.join(agentDir, 'package.json');
844
+ if (isBuiltinOverlayAgentId(agentId)) {
845
+ try {
846
+ await fs.access(agentMdPath);
847
+ }
848
+ catch (error) {
849
+ if (error?.code === 'ENOENT') {
850
+ const err = new Error(`Agent "${agentId}" has no AGENT.md on disk; nothing to remove (defaults already apply).`);
851
+ err.code = 'AGENT_NOT_FOUND';
852
+ throw err;
853
+ }
854
+ throw error;
855
+ }
856
+ await fs.unlink(agentMdPath);
857
+ try {
858
+ const remaining = await fs.readdir(agentDir);
859
+ if (remaining.length === 0) {
860
+ await fs.rmdir(agentDir);
861
+ }
862
+ }
863
+ catch {
864
+ // ignore cleanup failures
865
+ }
866
+ return;
867
+ }
868
+ try {
869
+ await fs.access(agentDir);
870
+ }
871
+ catch (error) {
872
+ if (error?.code === 'ENOENT') {
873
+ const err = new Error(`Agent "${agentId}" does not exist.`);
874
+ err.code = 'AGENT_NOT_FOUND';
875
+ throw err;
876
+ }
877
+ throw error;
878
+ }
879
+ let hasPackage = false;
880
+ let hasAgentMd = false;
881
+ try {
882
+ await fs.access(packageJsonPath);
883
+ hasPackage = true;
884
+ }
885
+ catch {
886
+ // ignore
887
+ }
888
+ try {
889
+ await fs.access(agentMdPath);
890
+ hasAgentMd = true;
891
+ }
892
+ catch {
893
+ // ignore
894
+ }
895
+ if (hasPackage && !hasAgentMd) {
896
+ throw new Error(`Cannot delete TypeScript agent package "${agentId}" through this action; remove the folder manually.`);
897
+ }
898
+ if (!hasAgentMd) {
899
+ throw new Error(`Agent "${agentId}" has no AGENT.md and cannot be deleted through this action.`);
900
+ }
901
+ await fs.rm(agentDir, { recursive: true, force: true });
902
+ },
903
+ getEvents: async ({ channelId, threadId, }) => {
904
+ try {
905
+ const threadDir = getConversationDir(channelId, threadId);
906
+ const eventsPath = `${threadDir}/events.jsonl`;
907
+ const eventsData = await fs.readFile(eventsPath);
908
+ const events = eventsData
909
+ .toString()
910
+ .split(/\r?\n/)
911
+ .map((line) => line.trim())
912
+ .filter(Boolean)
913
+ .map((line) => {
914
+ const event = JSON.parse(line);
915
+ if (!event.id) {
916
+ event.id = crypto.randomUUID();
917
+ }
918
+ return event;
919
+ });
920
+ if (!threadId) {
921
+ const threadsDir = resolvePath(resolveBaseDir() + '/' + DEFAULT_CHANNELS_DIR + '/' + channelId + '/threads');
922
+ try {
923
+ const threadDirs = await fs.readdir(threadsDir);
924
+ const threadSet = new Set(threadDirs);
925
+ return events.map((event) => {
926
+ const eventThreadId = event.id;
927
+ if (eventThreadId && threadSet.has(eventThreadId)) {
928
+ return {
929
+ ...event,
930
+ meta: {
931
+ ...(event.meta || {}),
932
+ hasThread: true,
933
+ },
934
+ };
935
+ }
936
+ return event;
937
+ });
938
+ }
939
+ catch {
940
+ return events;
941
+ }
942
+ }
943
+ return events;
944
+ }
945
+ catch (error) {
946
+ if (error?.code !== 'ENOENT') {
947
+ console.error(`Failed to get events for channel ${channelId} thread ${threadId}`, error);
948
+ }
949
+ return [];
950
+ }
951
+ },
952
+ storeEvent: async ({ channelId, threadId, event, }) => {
953
+ try {
954
+ const threadDir = getConversationDir(channelId, threadId);
955
+ if (threadId) {
956
+ let exists = false;
957
+ try {
958
+ await fs.access(threadDir);
959
+ exists = true;
960
+ }
961
+ catch (error) {
962
+ if (error?.code === 'ENOENT') {
963
+ const threadTitle = buildThreadTitleFromEvent(event);
964
+ await storageService.createThread({
965
+ channelId,
966
+ threadId,
967
+ threadTitle,
968
+ });
969
+ }
970
+ else {
971
+ throw error;
972
+ }
973
+ }
974
+ if (exists) {
975
+ // If the thread already exists, check if it has a name.
976
+ // This handles threads created via action:create_thread without a title.
977
+ const threadDetails = await storageService.getThreadDetails({ channelId, threadId });
978
+ const currentState = threadDetails.state || {};
979
+ if (!currentState.name) {
980
+ const threadTitle = buildThreadTitleFromEvent(event);
981
+ if (threadTitle) {
982
+ await storageService.patchThreadState({
983
+ channelId,
984
+ threadId,
985
+ state: { name: threadTitle },
986
+ });
987
+ }
988
+ }
989
+ }
990
+ }
991
+ else {
992
+ await fs.mkdir(threadDir, { recursive: true });
993
+ }
994
+ if (!event.id) {
995
+ event.id = crypto.randomUUID();
996
+ }
997
+ await fs.appendFile(`${threadDir}/events.jsonl`, `${JSON.stringify(event)}\n`);
998
+ }
999
+ catch (error) {
1000
+ console.error(`Failed to store event for channel ${channelId} thread ${threadId}`, error);
1001
+ throw error;
1002
+ }
1003
+ },
1004
+ getVariables: async () => {
1005
+ const variablesFilePath = resolvePath(resolveBaseDir() + '/' + VARIABLES_FILE);
1006
+ const raw = await readJsonFile(variablesFilePath, {});
1007
+ if (raw &&
1008
+ typeof raw === 'object' &&
1009
+ 'variables' in raw &&
1010
+ Array.isArray(raw.variables)) {
1011
+ const entries = (raw.variables)
1012
+ .filter((v) => typeof v?.key === 'string')
1013
+ .map((v) => [v.key, { value: String(v.value ?? ''), secret: !!v.secret }]);
1014
+ return Object.fromEntries(entries);
1015
+ }
1016
+ return toVariablesRecord(raw);
1017
+ },
1018
+ createVariable: async ({ key, value, secret = false, }) => {
1019
+ const variablesFilePath = resolvePath(resolveBaseDir() + '/' + VARIABLES_FILE);
1020
+ const raw = await readJsonFile(variablesFilePath, { version: 1, variables: [] });
1021
+ let variables = [];
1022
+ if (raw &&
1023
+ typeof raw === 'object' &&
1024
+ 'variables' in raw &&
1025
+ Array.isArray(raw.variables)) {
1026
+ variables = raw.variables;
1027
+ }
1028
+ else {
1029
+ variables = Object.entries(toVariablesRecord(raw)).map(([k, v]) => ({
1030
+ key: k,
1031
+ value: v,
1032
+ secret: false,
1033
+ }));
1034
+ }
1035
+ const existingIndex = variables.findIndex((v) => v.key === key);
1036
+ if (existingIndex !== -1) {
1037
+ variables[existingIndex] = { key, value, secret };
1038
+ }
1039
+ else {
1040
+ variables.push({ key, value, secret });
1041
+ }
1042
+ await fs.mkdir(path.dirname(variablesFilePath), { recursive: true });
1043
+ await fs.writeFile(variablesFilePath, JSON.stringify({ version: 1, variables }, null, 2), 'utf-8');
1044
+ processService.syncWorkspaceVariablesToProcessEnv();
1045
+ },
1046
+ deleteVariable: async ({ key }) => {
1047
+ const variablesFilePath = resolvePath(resolveBaseDir() + '/' + VARIABLES_FILE);
1048
+ const raw = await readJsonFile(variablesFilePath, { version: 1, variables: [] });
1049
+ let variables = [];
1050
+ if (raw &&
1051
+ typeof raw === 'object' &&
1052
+ 'variables' in raw &&
1053
+ Array.isArray(raw.variables)) {
1054
+ variables = raw.variables;
1055
+ }
1056
+ else {
1057
+ variables = Object.entries(toVariablesRecord(raw)).map(([k, v]) => ({
1058
+ key: k,
1059
+ value: v,
1060
+ secret: false,
1061
+ }));
1062
+ }
1063
+ const newVariables = variables.filter((v) => v.key !== key);
1064
+ if (newVariables.length === variables.length) {
1065
+ return;
1066
+ }
1067
+ await fs.mkdir(path.dirname(variablesFilePath), { recursive: true });
1068
+ await fs.writeFile(variablesFilePath, JSON.stringify({ version: 1, variables: newVariables }, null, 2), 'utf-8');
1069
+ processService.syncWorkspaceVariablesToProcessEnv();
1070
+ },
1071
+ listFiles: async ({ channelId, path: subPath = '', }) => {
1072
+ const details = await storageService.getChannelDetails({ channelId });
1073
+ const baseCwd = details.cwd;
1074
+ if (!baseCwd) {
1075
+ throw new Error('Channel has no CWD configured');
1076
+ }
1077
+ const resolvedBase = path.resolve(baseCwd);
1078
+ const targetDir = path.resolve(resolvedBase, subPath);
1079
+ if (!targetDir.startsWith(resolvedBase)) {
1080
+ throw new Error('Access denied: directory escape');
1081
+ }
1082
+ const entries = await fs.readdir(targetDir, { withFileTypes: true });
1083
+ return entries
1084
+ .filter((e) => !e.name.startsWith('.'))
1085
+ .map((e) => ({
1086
+ name: e.name,
1087
+ isDirectory: e.isDirectory(),
1088
+ }));
1089
+ },
1090
+ readFile: async ({ channelId, path: filePath, }) => {
1091
+ const details = await storageService.getChannelDetails({ channelId });
1092
+ const baseCwd = details.cwd;
1093
+ if (!baseCwd) {
1094
+ throw new Error('Channel has no CWD configured');
1095
+ }
1096
+ const resolvedBase = path.resolve(baseCwd);
1097
+ const targetFile = path.resolve(resolvedBase, filePath);
1098
+ if (!targetFile.startsWith(resolvedBase)) {
1099
+ throw new Error('Access denied: directory escape');
1100
+ }
1101
+ return fs.readFile(targetFile, 'utf-8');
1102
+ },
1103
+ appendMemory: memoryService.appendMemory,
1104
+ listMemories: memoryService.listMemories,
1105
+ deleteMemory: memoryService.deleteMemory,
1106
+ updateMemory: memoryService.updateMemory,
1107
+ /**
1108
+ * Hydrates the full OpenBot state from disk/storage before a run.
1109
+ */
1110
+ getOpenBotState: async (options) => {
1111
+ const { runId, agentId, channelId, threadId, event } = options;
1112
+ let agentDetails;
1113
+ try {
1114
+ agentDetails = await storageService.getAgentDetails({ agentId });
1115
+ }
1116
+ catch (error) {
1117
+ console.warn(`[storage] Failed to load agent details for agent: ${agentId}`, error);
1118
+ throw error;
1119
+ }
1120
+ let channelDetails;
1121
+ if (channelId) {
1122
+ try {
1123
+ channelDetails = await storageService.getChannelDetails({ channelId });
1124
+ }
1125
+ catch (error) {
1126
+ console.warn(`[storage] Failed to load channel details for channel ${channelId}`, error);
1127
+ }
1128
+ }
1129
+ let threadDetails;
1130
+ if (channelId && threadId) {
1131
+ try {
1132
+ threadDetails = await storageService.getThreadDetails({ channelId, threadId });
1133
+ }
1134
+ catch (error) {
1135
+ console.warn(`[storage] Failed to load thread details for channel ${channelId} thread: ${threadId}`, error);
1136
+ }
1137
+ }
1138
+ return {
1139
+ runId,
1140
+ agentId,
1141
+ channelId,
1142
+ threadId,
1143
+ triggerEvent: event,
1144
+ agentDetails: {
1145
+ id: agentDetails.id,
1146
+ name: agentDetails.name,
1147
+ description: agentDetails.description || '',
1148
+ image: agentDetails.image,
1149
+ instructions: agentDetails.instructions || '',
1150
+ plugins: agentDetails.plugins,
1151
+ pluginRefs: agentDetails.pluginRefs,
1152
+ createdAt: agentDetails.createdAt,
1153
+ updatedAt: agentDetails.updatedAt,
1154
+ },
1155
+ channelDetails: channelDetails,
1156
+ threadDetails: threadDetails,
1157
+ };
1158
+ },
1159
+ };