openbot 0.2.2 → 0.2.5

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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/agent-creator.js +58 -19
  3. package/dist/agents/os-agent.js +1 -4
  4. package/dist/agents/planner-agent.js +32 -0
  5. package/dist/agents/topic-agent.js +1 -1
  6. package/dist/architecture/contracts.js +1 -0
  7. package/dist/architecture/execution-engine.js +151 -0
  8. package/dist/architecture/intent-classifier.js +26 -0
  9. package/dist/architecture/planner.js +106 -0
  10. package/dist/automation-worker.js +121 -0
  11. package/dist/automations.js +52 -0
  12. package/dist/cli.js +54 -141
  13. package/dist/config.js +20 -0
  14. package/dist/core/agents.js +41 -0
  15. package/dist/core/delegation.js +124 -0
  16. package/dist/core/manager.js +73 -0
  17. package/dist/core/plugins.js +77 -0
  18. package/dist/core/router.js +40 -0
  19. package/dist/installers.js +170 -0
  20. package/dist/marketplace.js +80 -0
  21. package/dist/open-bot.js +34 -157
  22. package/dist/orchestrator.js +247 -51
  23. package/dist/plugins/approval/index.js +107 -3
  24. package/dist/plugins/brain/index.js +17 -86
  25. package/dist/plugins/brain/memory.js +1 -1
  26. package/dist/plugins/brain/prompt.js +8 -13
  27. package/dist/plugins/brain/types.js +0 -15
  28. package/dist/plugins/file-system/index.js +8 -8
  29. package/dist/plugins/llm/context-shaping.js +177 -0
  30. package/dist/plugins/llm/index.js +223 -49
  31. package/dist/plugins/memory/index.js +220 -0
  32. package/dist/plugins/memory/memory.js +122 -0
  33. package/dist/plugins/memory/prompt.js +55 -0
  34. package/dist/plugins/memory/types.js +45 -0
  35. package/dist/plugins/shell/index.js +3 -3
  36. package/dist/plugins/skills/index.js +9 -9
  37. package/dist/registry/index.js +1 -4
  38. package/dist/registry/plugin-loader.js +339 -56
  39. package/dist/registry/plugin-registry.js +21 -4
  40. package/dist/registry/ts-agent-loader.js +4 -4
  41. package/dist/registry/yaml-agent-loader.js +78 -20
  42. package/dist/runtime/execution-trace.js +41 -0
  43. package/dist/runtime/intent-routing.js +26 -0
  44. package/dist/runtime/openbot-runtime.js +354 -0
  45. package/dist/server.js +549 -31
  46. package/dist/ui/widgets/approval-card.js +22 -2
  47. package/dist/ui/widgets/delegation.js +29 -0
  48. package/dist/version.js +62 -0
  49. package/package.json +8 -7
package/dist/server.js CHANGED
@@ -1,28 +1,166 @@
1
1
  import "dotenv/config";
2
2
  import express from "express";
3
3
  import cors from "cors";
4
+ import chokidar from "chokidar";
4
5
  import { generateId } from "melony";
5
6
  import { createOpenBot } from "./open-bot.js";
6
7
  import { loadConfig, saveConfig, isConfigured, resolvePath, DEFAULT_BASE_DIR } from "./config.js";
7
8
  import { loadSession, saveSession, logEvent, loadEvents, listSessions } from "./session.js";
8
- import { listYamlAgents } from "./registry/index.js";
9
+ import { listPlugins } from "./registry/index.js";
9
10
  import { exec } from "node:child_process";
10
11
  import os from "node:os";
11
12
  import path from "node:path";
12
13
  import fs from "node:fs/promises";
13
14
  import { randomUUID } from "node:crypto";
15
+ import matter from "gray-matter";
14
16
  import { fetchProviderModels, getModelCatalog } from "./model-catalog.js";
15
17
  import { DEFAULT_MODEL_BY_PROVIDER, DEFAULT_MODEL_ID } from "./model-defaults.js";
18
+ import { listAutomations, saveAutomations } from "./automations.js";
19
+ import { startAutomationWorker } from "./automation-worker.js";
20
+ import { getMarketplaceRegistry, installMarketplaceAgent, installMarketplacePlugin } from "./marketplace.js";
21
+ import { getVersionStatus } from "./version.js";
16
22
  export async function startServer(options = {}) {
17
23
  const config = loadConfig();
18
24
  const PORT = Number(options.port ?? config.port ?? process.env.PORT ?? 4001);
19
25
  const app = express();
20
- const orchestrator = await createOpenBot({
26
+ const createRuntime = () => createOpenBot({
21
27
  openaiApiKey: options.openaiApiKey,
22
28
  anthropicApiKey: options.anthropicApiKey,
23
29
  });
30
+ let runtime = await createRuntime();
31
+ let reloadTimer = null;
32
+ let reloadInProgress = false;
33
+ let queuedReload = false;
34
+ const reloadRuntime = async () => {
35
+ if (reloadInProgress) {
36
+ queuedReload = true;
37
+ return;
38
+ }
39
+ reloadInProgress = true;
40
+ try {
41
+ const nextRuntime = await createRuntime();
42
+ runtime = nextRuntime;
43
+ console.log("[hot-reload] Runtime reloaded from ~/.openbot changes");
44
+ }
45
+ catch (error) {
46
+ console.error("[hot-reload] Reload failed; keeping previous runtime", error);
47
+ }
48
+ finally {
49
+ reloadInProgress = false;
50
+ if (queuedReload) {
51
+ queuedReload = false;
52
+ scheduleReload();
53
+ }
54
+ }
55
+ };
56
+ const scheduleReload = () => {
57
+ if (reloadTimer)
58
+ clearTimeout(reloadTimer);
59
+ reloadTimer = setTimeout(() => {
60
+ reloadTimer = null;
61
+ void reloadRuntime();
62
+ }, 800);
63
+ };
64
+ const openBotDir = path.join(os.homedir(), ".openbot");
65
+ const watcher = chokidar.watch([
66
+ path.join(openBotDir, "config.json"),
67
+ path.join(openBotDir, "AGENT.md"),
68
+ path.join(openBotDir, "agents", "**", "*"),
69
+ path.join(openBotDir, "plugins", "**", "*"),
70
+ ], {
71
+ ignoreInitial: true,
72
+ awaitWriteFinish: {
73
+ stabilityThreshold: 300,
74
+ pollInterval: 100,
75
+ },
76
+ });
77
+ watcher
78
+ .on("add", scheduleReload)
79
+ .on("change", scheduleReload)
80
+ .on("unlink", scheduleReload)
81
+ .on("addDir", scheduleReload)
82
+ .on("unlinkDir", scheduleReload)
83
+ .on("error", (error) => {
84
+ console.error("[hot-reload] Watcher error", error);
85
+ });
86
+ const cleanupWatcher = async () => {
87
+ if (reloadTimer) {
88
+ clearTimeout(reloadTimer);
89
+ reloadTimer = null;
90
+ }
91
+ await watcher.close();
92
+ };
93
+ const runAutomation = async (automation, scheduledAt) => {
94
+ const sessionId = `automation_${automation.id}`;
95
+ const runId = `run_auto_${generateId()}`;
96
+ const state = (await loadSession(sessionId)) ?? {};
97
+ state.sessionId = sessionId;
98
+ if (!state.cwd)
99
+ state.cwd = process.cwd();
100
+ if (!state.workspaceRoot)
101
+ state.workspaceRoot = process.cwd();
102
+ if (!state.title)
103
+ state.title = `Automation: ${automation.name}`;
104
+ const content = automation.targetType === "agent" && automation.agentName
105
+ ? `/${automation.agentName} ${automation.prompt}`
106
+ : automation.prompt;
107
+ const iterator = runtime.run({
108
+ type: "agent:input",
109
+ data: { content },
110
+ }, { runId, state });
111
+ try {
112
+ console.log(`[automations] Running "${automation.name}" (${automation.id}) at ${scheduledAt.toISOString()}`);
113
+ for await (const chunk of iterator) {
114
+ await logEvent(sessionId, runId, chunk);
115
+ }
116
+ console.log(`[automations] Completed "${automation.name}" (${automation.id})`);
117
+ }
118
+ catch (error) {
119
+ console.error(`[automations] Run failed for "${automation.name}" (${automation.id})`, error);
120
+ throw error;
121
+ }
122
+ finally {
123
+ await saveSession(sessionId, state);
124
+ }
125
+ };
126
+ const stopAutomationWorker = startAutomationWorker({
127
+ listAutomations,
128
+ runAutomation,
129
+ });
130
+ const cleanupBackground = async () => {
131
+ stopAutomationWorker();
132
+ await cleanupWatcher();
133
+ };
134
+ process.once("SIGINT", () => {
135
+ void cleanupBackground().finally(() => process.exit(0));
136
+ });
137
+ process.once("SIGTERM", () => {
138
+ void cleanupBackground().finally(() => process.exit(0));
139
+ });
24
140
  app.use(cors());
25
141
  app.use(express.json({ limit: "20mb" }));
142
+ const fileExists = async (targetPath) => fs.access(targetPath).then(() => true).catch(() => false);
143
+ const toTitleCaseFromSlug = (value) => value
144
+ .split(/[-_]+/)
145
+ .filter(Boolean)
146
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
147
+ .join(" ") || "Agent";
148
+ const resolveAgentFolder = async (agentIdOrName, resolvedBaseDir) => {
149
+ const agentsDir = path.join(resolvedBaseDir, "agents");
150
+ const directFolder = path.join(agentsDir, agentIdOrName);
151
+ if (await fileExists(path.join(directFolder, "AGENT.md"))) {
152
+ return directFolder;
153
+ }
154
+ try {
155
+ const allPlugins = await listPlugins(agentsDir);
156
+ const match = allPlugins.find((plugin) => plugin.type === "agent"
157
+ && (path.basename(plugin.folder) === agentIdOrName || plugin.name === agentIdOrName));
158
+ return match?.folder ?? null;
159
+ }
160
+ catch {
161
+ return null;
162
+ }
163
+ };
26
164
  const getUploadsDir = () => {
27
165
  const cfg = loadConfig();
28
166
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
@@ -56,6 +194,16 @@ export async function startServer(options = {}) {
56
194
  res.json([]);
57
195
  }
58
196
  });
197
+ app.get("/api/version", async (_req, res) => {
198
+ try {
199
+ const status = await getVersionStatus();
200
+ res.json(status);
201
+ }
202
+ catch (err) {
203
+ console.error("Failed to check version:", err);
204
+ res.status(500).json({ error: "Failed to check version" });
205
+ }
206
+ });
59
207
  app.post("/api/models/preview", async (req, res) => {
60
208
  const { provider, apiKey } = req.body;
61
209
  if (provider !== "openai" && provider !== "anthropic") {
@@ -83,6 +231,7 @@ export async function startServer(options = {}) {
83
231
  sessions: "GET /api/sessions",
84
232
  agents: "GET /api/agents",
85
233
  prompts: "GET /api/prompts",
234
+ version: "GET /api/version",
86
235
  },
87
236
  });
88
237
  });
@@ -95,6 +244,83 @@ export async function startServer(options = {}) {
95
244
  { label: "What is the weather in Tokyo?", icon: "sun" },
96
245
  ]);
97
246
  });
247
+ app.get("/api/automations", async (_req, res) => {
248
+ const items = await listAutomations();
249
+ res.json(items);
250
+ });
251
+ app.post("/api/automations", async (req, res) => {
252
+ const { name, prompt, cron, targetType, agentName } = req.body;
253
+ const normalizedTargetType = targetType === "agent" ? "agent" : "orchestrator";
254
+ const normalizedAgentName = typeof agentName === "string" ? agentName.trim() : "";
255
+ if (typeof name !== "string" ||
256
+ typeof prompt !== "string" ||
257
+ typeof cron !== "string" ||
258
+ !name.trim() ||
259
+ !prompt.trim() ||
260
+ !cron.trim() ||
261
+ (normalizedTargetType === "agent" && !normalizedAgentName)) {
262
+ return res.status(400).json({ error: "Invalid automation payload" });
263
+ }
264
+ const now = new Date().toISOString();
265
+ const next = {
266
+ id: `auto_${randomUUID()}`,
267
+ name: name.trim(),
268
+ prompt: prompt.trim(),
269
+ cron: cron.trim(),
270
+ targetType: normalizedTargetType,
271
+ agentName: normalizedTargetType === "agent" ? normalizedAgentName : undefined,
272
+ enabled: true,
273
+ createdAt: now,
274
+ updatedAt: now,
275
+ };
276
+ const current = await listAutomations();
277
+ await saveAutomations([next, ...current]);
278
+ res.status(201).json(next);
279
+ });
280
+ app.put("/api/automations/:id", async (req, res) => {
281
+ const { id } = req.params;
282
+ const { name, prompt, cron, enabled, targetType, agentName } = req.body;
283
+ const current = await listAutomations();
284
+ const index = current.findIndex((item) => item.id === id);
285
+ if (index < 0) {
286
+ return res.status(404).json({ error: "Automation not found" });
287
+ }
288
+ const existing = current[index];
289
+ const nextTargetType = targetType === "agent"
290
+ ? "agent"
291
+ : targetType === "orchestrator"
292
+ ? "orchestrator"
293
+ : existing.targetType;
294
+ const nextAgentName = typeof agentName === "string"
295
+ ? agentName.trim()
296
+ : (existing.agentName ?? "");
297
+ if (nextTargetType === "agent" && !nextAgentName) {
298
+ return res.status(400).json({ error: "agentName is required when targetType is agent" });
299
+ }
300
+ const updated = {
301
+ ...existing,
302
+ name: typeof name === "string" ? name.trim() || existing.name : existing.name,
303
+ prompt: typeof prompt === "string" ? prompt.trim() || existing.prompt : existing.prompt,
304
+ cron: typeof cron === "string" ? cron.trim() || existing.cron : existing.cron,
305
+ targetType: nextTargetType,
306
+ agentName: nextTargetType === "agent" ? nextAgentName : undefined,
307
+ enabled: typeof enabled === "boolean" ? enabled : existing.enabled,
308
+ updatedAt: new Date().toISOString(),
309
+ };
310
+ current[index] = updated;
311
+ await saveAutomations(current);
312
+ res.json(updated);
313
+ });
314
+ app.delete("/api/automations/:id", async (req, res) => {
315
+ const { id } = req.params;
316
+ const current = await listAutomations();
317
+ const next = current.filter((item) => item.id !== id);
318
+ if (next.length === current.length) {
319
+ return res.status(404).json({ error: "Automation not found" });
320
+ }
321
+ await saveAutomations(next);
322
+ res.json({ success: true });
323
+ });
98
324
  app.post("/api/uploads/image", async (req, res) => {
99
325
  const { name, mimeType, dataBase64 } = req.body;
100
326
  if (!mimeType || !allowedMimeTypes.has(mimeType)) {
@@ -160,6 +386,8 @@ export async function startServer(options = {}) {
160
386
  const cfg = loadConfig();
161
387
  res.json({
162
388
  configured: isConfigured(),
389
+ name: cfg.name || "OpenBot",
390
+ description: cfg.description || "The main orchestrator and system settings",
163
391
  model: cfg.model || DEFAULT_MODEL_ID,
164
392
  defaultModelId: DEFAULT_MODEL_ID,
165
393
  defaultModels: DEFAULT_MODEL_BY_PROVIDER,
@@ -168,10 +396,16 @@ export async function startServer(options = {}) {
168
396
  });
169
397
  });
170
398
  app.post("/api/config", async (req, res) => {
171
- const { openai_api_key, anthropic_api_key, model } = req.body;
399
+ const { openai_api_key, anthropic_api_key, model, name, description, image } = req.body;
172
400
  const updates = {};
401
+ if (name)
402
+ updates.name = name.trim();
403
+ if (description)
404
+ updates.description = description.trim();
173
405
  if (model)
174
406
  updates.model = model.trim();
407
+ if (image !== undefined)
408
+ updates.image = image.trim();
175
409
  if (openai_api_key && openai_api_key !== "••••••••••••••••")
176
410
  updates.openaiApiKey = openai_api_key.trim();
177
411
  if (anthropic_api_key && anthropic_api_key !== "••••••••••••••••")
@@ -194,53 +428,334 @@ export async function startServer(options = {}) {
194
428
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
195
429
  const resolvedBaseDir = resolvePath(baseDir);
196
430
  const agentsDir = path.join(resolvedBaseDir, "agents");
431
+ const defaultName = cfg.name || "OpenBot";
432
+ const defaultDescription = cfg.description || "The main orchestrator and system settings";
433
+ const agents = [
434
+ {
435
+ id: "default",
436
+ name: defaultName,
437
+ description: defaultDescription,
438
+ folder: resolvedBaseDir,
439
+ isDefault: true,
440
+ hasAgentMd: true,
441
+ image: cfg.image,
442
+ },
443
+ ];
197
444
  try {
198
- const agents = await listYamlAgents(agentsDir);
199
- res.json(agents);
445
+ const allPlugins = await listPlugins(agentsDir);
446
+ const agentPlugins = allPlugins.filter(p => p.type === "agent");
447
+ agents.push(...agentPlugins.map((plugin) => {
448
+ const id = path.basename(plugin.folder);
449
+ const hasUnnamedDisplayName = /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(plugin.name);
450
+ return {
451
+ ...plugin,
452
+ id,
453
+ name: hasUnnamedDisplayName ? toTitleCaseFromSlug(id) : plugin.name,
454
+ };
455
+ }));
200
456
  }
201
457
  catch {
202
- res.json([]);
458
+ // ignore
203
459
  }
460
+ res.json(agents);
204
461
  });
205
- app.get("/api/agents/:name/yaml", async (req, res) => {
206
- const { name } = req.params;
462
+ app.get("/api/plugins", async (_req, res) => {
463
+ const cfg = loadConfig();
464
+ const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
465
+ const resolvedBaseDir = resolvePath(baseDir);
466
+ const pluginsDir = path.join(resolvedBaseDir, "plugins");
467
+ try {
468
+ const allPlugins = await listPlugins(pluginsDir);
469
+ const toolPlugins = allPlugins.filter((plugin) => plugin.type === "tool");
470
+ res.json(toolPlugins.map((plugin) => {
471
+ const id = path.basename(plugin.folder);
472
+ const hasUnnamedDisplayName = /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(plugin.name);
473
+ return {
474
+ ...plugin,
475
+ id,
476
+ name: hasUnnamedDisplayName ? toTitleCaseFromSlug(id) : plugin.name,
477
+ };
478
+ }));
479
+ }
480
+ catch (error) {
481
+ console.error(error);
482
+ res.status(500).json({ error: "Failed to list plugins" });
483
+ }
484
+ });
485
+ app.get("/api/registry/plugins", async (_req, res) => {
486
+ try {
487
+ const tools = runtime.registry.getTools();
488
+ res.json(tools.map((t) => ({
489
+ name: t.name,
490
+ description: t.description,
491
+ isBuiltIn: !!t.isBuiltIn,
492
+ })));
493
+ }
494
+ catch (error) {
495
+ console.error(error);
496
+ res.status(500).json({ error: "Failed to list registry plugins" });
497
+ }
498
+ });
499
+ app.get("/api/marketplace/agents", async (_req, res) => {
500
+ try {
501
+ const registry = await getMarketplaceRegistry();
502
+ res.json(registry.agents);
503
+ }
504
+ catch (error) {
505
+ console.error(error);
506
+ res.status(500).json({ error: "Failed to load marketplace agents" });
507
+ }
508
+ });
509
+ app.get("/api/marketplace/plugins", async (_req, res) => {
510
+ try {
511
+ const registry = await getMarketplaceRegistry();
512
+ res.json(registry.plugins);
513
+ }
514
+ catch (error) {
515
+ console.error(error);
516
+ res.status(500).json({ error: "Failed to load marketplace plugins" });
517
+ }
518
+ });
519
+ app.post("/api/marketplace/install-agent", async (req, res) => {
520
+ const { id } = req.body;
521
+ if (typeof id !== "string" || !id.trim()) {
522
+ return res.status(400).json({ error: "Marketplace agent id is required" });
523
+ }
524
+ try {
525
+ const result = await installMarketplaceAgent(id.trim());
526
+ res.json({ success: true, installedName: result.installedName, item: result.agent });
527
+ }
528
+ catch (error) {
529
+ console.error(error);
530
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to install agent" });
531
+ }
532
+ });
533
+ app.post("/api/marketplace/install-plugin", async (req, res) => {
534
+ const { id } = req.body;
535
+ if (typeof id !== "string" || !id.trim()) {
536
+ return res.status(400).json({ error: "Marketplace plugin id is required" });
537
+ }
538
+ try {
539
+ const result = await installMarketplacePlugin(id.trim());
540
+ res.json({ success: true, installedName: result.installedName, item: result.plugin });
541
+ }
542
+ catch (error) {
543
+ console.error(error);
544
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to install plugin" });
545
+ }
546
+ });
547
+ app.get("/api/agents/:agentId/md", async (req, res) => {
548
+ const { agentId } = req.params;
207
549
  const cfg = loadConfig();
208
550
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
209
551
  const resolvedBaseDir = resolvePath(baseDir);
210
- const yamlPath = path.join(resolvedBaseDir, "agents", name, "agent.yaml");
552
+ const defaultName = cfg.name || "OpenBot";
553
+ let mdPath;
554
+ if (agentId === defaultName || agentId === "default") {
555
+ mdPath = path.join(resolvedBaseDir, "AGENT.md");
556
+ }
557
+ else {
558
+ const pluginFolder = await resolveAgentFolder(agentId, resolvedBaseDir);
559
+ if (!pluginFolder) {
560
+ return res.status(404).send("");
561
+ }
562
+ mdPath = path.join(pluginFolder, "AGENT.md");
563
+ }
211
564
  try {
212
- const content = await fs.readFile(yamlPath, "utf-8");
213
- res.send(content);
565
+ const content = await fs.readFile(mdPath, "utf-8");
566
+ const { content: body } = matter(content);
567
+ res.send(body.trim());
214
568
  }
215
569
  catch {
216
- res.status(404).send("Agent not found or has no agent.yaml");
570
+ res.status(404).send("");
217
571
  }
218
572
  });
219
- app.put("/api/agents/:name/yaml", async (req, res) => {
220
- const { name } = req.params;
221
- const { yaml } = req.body;
222
- if (!yaml || typeof yaml !== "string") {
223
- return res.status(400).json({ error: "YAML content is required" });
573
+ app.put("/api/agents/:agentId/md", async (req, res) => {
574
+ const { agentId } = req.params;
575
+ const { md } = req.body;
576
+ const cfg = loadConfig();
577
+ const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
578
+ const resolvedBaseDir = resolvePath(baseDir);
579
+ const defaultName = cfg.name || "OpenBot";
580
+ let mdPath;
581
+ let pluginDir;
582
+ if (agentId === defaultName || agentId === "default") {
583
+ pluginDir = resolvedBaseDir;
584
+ mdPath = path.join(resolvedBaseDir, "AGENT.md");
224
585
  }
586
+ else {
587
+ const pluginFolder = await resolveAgentFolder(agentId, resolvedBaseDir);
588
+ if (!pluginFolder) {
589
+ return res.status(404).json({ error: "Agent not found" });
590
+ }
591
+ pluginDir = pluginFolder;
592
+ mdPath = path.join(pluginDir, "AGENT.md");
593
+ }
594
+ try {
595
+ await fs.mkdir(pluginDir, { recursive: true });
596
+ let frontmatter = {};
597
+ try {
598
+ const currentContent = await fs.readFile(mdPath, "utf-8");
599
+ const parsed = matter(currentContent);
600
+ frontmatter = parsed.data || {};
601
+ }
602
+ catch {
603
+ // No current AGENT.md, starting with empty frontmatter or defaults
604
+ }
605
+ const consolidated = matter.stringify(md, frontmatter);
606
+ await fs.writeFile(mdPath, consolidated, "utf-8");
607
+ res.json({ success: true });
608
+ }
609
+ catch (err) {
610
+ console.error(err);
611
+ res.status(500).json({ error: "Failed to write AGENT.md" });
612
+ }
613
+ });
614
+ app.get("/api/agents/:agentId/config", async (req, res) => {
615
+ const { agentId } = req.params;
225
616
  const cfg = loadConfig();
226
617
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
227
618
  const resolvedBaseDir = resolvePath(baseDir);
228
- const agentDir = path.join(resolvedBaseDir, "agents", name);
229
- const yamlPath = path.join(agentDir, "agent.yaml");
619
+ const defaultName = cfg.name || "OpenBot";
620
+ let mdPath;
621
+ if (agentId === defaultName || agentId === "default") {
622
+ mdPath = path.join(resolvedBaseDir, "AGENT.md");
623
+ }
624
+ else {
625
+ const pluginFolder = await resolveAgentFolder(agentId, resolvedBaseDir);
626
+ if (!pluginFolder) {
627
+ return res.status(404).json({ error: "Agent not found or invalid format" });
628
+ }
629
+ mdPath = path.join(pluginFolder, "AGENT.md");
630
+ }
230
631
  try {
231
- await fs.mkdir(agentDir, { recursive: true });
232
- await fs.writeFile(yamlPath, yaml, "utf-8");
233
- // Optionally, hot-reload openBotAgent if needed here.
234
- // But OpenBot runtime loads agents at startup or dynamically per request?
235
- // createOpenBot builds the Melony App. Since createOpenBot is called at startup:
236
- // openBotAgent = await createOpenBot(...) happens once.
237
- // Restarting server is required unless we hot-reload. We can just leave it as is
238
- // and instruct the user to restart, or implement a simple hot reload. Let's stick to simple file write first.
632
+ const content = await fs.readFile(mdPath, "utf-8");
633
+ const { data: parsed, content: body } = matter(content);
634
+ if (!parsed || typeof parsed !== "object") {
635
+ return res.status(400).json({ error: "Invalid AGENT.md frontmatter" });
636
+ }
637
+ res.json({
638
+ name: typeof parsed.name === "string" ? parsed.name : (agentId === defaultName || agentId === "default" ? defaultName : ""),
639
+ description: typeof parsed.description === "string" ? parsed.description : (agentId === defaultName || agentId === "default" ? cfg.description || "" : ""),
640
+ model: typeof parsed.model === "string" ? parsed.model : (agentId === defaultName || agentId === "default" ? cfg.model : undefined),
641
+ plugins: Array.isArray(parsed.plugins) ? parsed.plugins : [],
642
+ subscribe: Array.isArray(parsed.subscribe)
643
+ ? parsed.subscribe.filter((item) => typeof item === "string")
644
+ : [],
645
+ });
646
+ }
647
+ catch {
648
+ if (agentId === defaultName || agentId === "default") {
649
+ // Fallback for default agent if AGENT.md is missing or unreadable
650
+ return res.json({
651
+ name: defaultName,
652
+ description: cfg.description || "",
653
+ model: cfg.model,
654
+ plugins: [],
655
+ systemPrompt: "",
656
+ subscribe: [],
657
+ });
658
+ }
659
+ res.status(404).json({ error: "Agent not found or invalid format" });
660
+ }
661
+ });
662
+ app.put("/api/agents/:agentId/config", async (req, res) => {
663
+ const { agentId } = req.params;
664
+ const body = req.body;
665
+ if (typeof body.name !== "string" ||
666
+ typeof body.description !== "string" ||
667
+ !Array.isArray(body.plugins)) {
668
+ return res.status(400).json({ error: "Invalid agent config payload" });
669
+ }
670
+ const normalizedPlugins = [];
671
+ for (const plugin of body.plugins) {
672
+ if (typeof plugin === "string") {
673
+ const normalized = plugin.trim();
674
+ if (normalized)
675
+ normalizedPlugins.push(normalized);
676
+ continue;
677
+ }
678
+ if (!plugin || typeof plugin !== "object" || typeof plugin.name !== "string") {
679
+ continue;
680
+ }
681
+ const normalizedName = plugin.name.trim();
682
+ if (!normalizedName)
683
+ continue;
684
+ if (typeof plugin.config === "undefined") {
685
+ normalizedPlugins.push({ name: normalizedName });
686
+ }
687
+ else {
688
+ normalizedPlugins.push({ name: normalizedName, config: plugin.config });
689
+ }
690
+ }
691
+ const normalizedName = body.name.trim();
692
+ const normalizedDescription = body.description.trim();
693
+ if (!normalizedName || !normalizedDescription) {
694
+ return res.status(400).json({ error: "name and description are required" });
695
+ }
696
+ const cfg = loadConfig();
697
+ const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
698
+ const resolvedBaseDir = resolvePath(baseDir);
699
+ const defaultName = cfg.name || "OpenBot";
700
+ let pluginDir;
701
+ let mdPath;
702
+ if (agentId === defaultName || agentId === "default") {
703
+ pluginDir = resolvedBaseDir;
704
+ mdPath = path.join(resolvedBaseDir, "AGENT.md");
705
+ }
706
+ else {
707
+ const pluginFolder = await resolveAgentFolder(agentId, resolvedBaseDir);
708
+ if (!pluginFolder) {
709
+ return res.status(404).json({ error: "Agent not found" });
710
+ }
711
+ pluginDir = pluginFolder;
712
+ mdPath = path.join(pluginDir, "AGENT.md");
713
+ }
714
+ // Read current content to preserve the body (instructions)
715
+ let currentBody = "";
716
+ try {
717
+ const currentContent = await fs.readFile(mdPath, "utf-8");
718
+ const parsed = matter(currentContent);
719
+ currentBody = parsed.content;
720
+ }
721
+ catch {
722
+ // No current AGENT.md, starting with empty body or defaults
723
+ }
724
+ // Prepare frontmatter
725
+ const frontmatter = {
726
+ name: normalizedName,
727
+ description: normalizedDescription,
728
+ plugins: normalizedPlugins,
729
+ };
730
+ if (typeof body.model === "string" && body.model.trim()) {
731
+ frontmatter.model = body.model.trim();
732
+ }
733
+ if (Array.isArray(body.subscribe) && body.subscribe.length > 0) {
734
+ const normalizedSubscribe = body.subscribe
735
+ .filter((item) => typeof item === "string")
736
+ .map((item) => item.trim())
737
+ .filter(Boolean);
738
+ if (normalizedSubscribe.length > 0) {
739
+ frontmatter.subscribe = normalizedSubscribe;
740
+ }
741
+ }
742
+ try {
743
+ await fs.mkdir(pluginDir, { recursive: true });
744
+ const consolidated = matter.stringify(currentBody, frontmatter);
745
+ await fs.writeFile(mdPath, consolidated, "utf-8");
746
+ if (agentId === defaultName || agentId === "default") {
747
+ // For the default agent, sync changes back to config.json
748
+ saveConfig({
749
+ name: normalizedName,
750
+ description: normalizedDescription,
751
+ model: (typeof body.model === "string" && body.model.trim()) ? body.model.trim() : undefined,
752
+ });
753
+ }
239
754
  res.json({ success: true });
240
755
  }
241
756
  catch (err) {
242
757
  console.error(err);
243
- res.status(500).json({ error: "Failed to write agent.yaml" });
758
+ res.status(500).json({ error: "Failed to write AGENT.md" });
244
759
  }
245
760
  });
246
761
  app.post("/api/actions/open-folder", async (req, res) => {
@@ -263,10 +778,13 @@ export async function startServer(options = {}) {
263
778
  const cfg = loadConfig();
264
779
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
265
780
  const resolvedBaseDir = resolvePath(baseDir);
781
+ const defaultName = cfg.name || "OpenBot";
266
782
  const extensions = [".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif"];
267
783
  const fileNames = ["avatar", "icon", "image", "logo"];
268
784
  const searchDirs = [
269
- path.join(resolvedBaseDir, "agents", name, "assets"),
785
+ (name === defaultName || name === "default")
786
+ ? path.join(resolvedBaseDir, "assets")
787
+ : path.join(resolvedBaseDir, "agents", name, "assets"),
270
788
  path.join(process.cwd(), "server", "src", "agents", name, "assets"),
271
789
  path.join(process.cwd(), "server", "src", "assets", "agents", name),
272
790
  path.join(process.cwd(), "server", "src", "agents", "assets"),
@@ -313,12 +831,12 @@ export async function startServer(options = {}) {
313
831
  state.cwd = process.cwd();
314
832
  if (!state.workspaceRoot)
315
833
  state.workspaceRoot = process.cwd();
316
- const iterator = orchestrator.run(body.event, {
834
+ const iterator = runtime.run(body.event, {
317
835
  runId,
318
836
  state,
319
837
  });
320
838
  const stopStreaming = () => {
321
- iterator.return?.({ done: true, value: undefined });
839
+ void iterator.return?.();
322
840
  };
323
841
  res.on("close", stopStreaming);
324
842
  try {