openbot 0.2.3 → 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 +489 -40
  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 +7 -7
package/dist/server.js CHANGED
@@ -6,39 +6,44 @@ import { generateId } from "melony";
6
6
  import { createOpenBot } from "./open-bot.js";
7
7
  import { loadConfig, saveConfig, isConfigured, resolvePath, DEFAULT_BASE_DIR } from "./config.js";
8
8
  import { loadSession, saveSession, logEvent, loadEvents, listSessions } from "./session.js";
9
- import { listYamlAgents } from "./registry/index.js";
9
+ import { listPlugins } from "./registry/index.js";
10
10
  import { exec } from "node:child_process";
11
11
  import os from "node:os";
12
12
  import path from "node:path";
13
13
  import fs from "node:fs/promises";
14
14
  import { randomUUID } from "node:crypto";
15
+ import matter from "gray-matter";
15
16
  import { fetchProviderModels, getModelCatalog } from "./model-catalog.js";
16
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";
17
22
  export async function startServer(options = {}) {
18
23
  const config = loadConfig();
19
24
  const PORT = Number(options.port ?? config.port ?? process.env.PORT ?? 4001);
20
25
  const app = express();
21
- const createOrchestrator = () => createOpenBot({
26
+ const createRuntime = () => createOpenBot({
22
27
  openaiApiKey: options.openaiApiKey,
23
28
  anthropicApiKey: options.anthropicApiKey,
24
29
  });
25
- let orchestrator = await createOrchestrator();
30
+ let runtime = await createRuntime();
26
31
  let reloadTimer = null;
27
32
  let reloadInProgress = false;
28
33
  let queuedReload = false;
29
- const reloadOrchestrator = async () => {
34
+ const reloadRuntime = async () => {
30
35
  if (reloadInProgress) {
31
36
  queuedReload = true;
32
37
  return;
33
38
  }
34
39
  reloadInProgress = true;
35
40
  try {
36
- const nextOrchestrator = await createOrchestrator();
37
- orchestrator = nextOrchestrator;
38
- console.log("[hot-reload] Orchestrator reloaded from ~/.openbot changes");
41
+ const nextRuntime = await createRuntime();
42
+ runtime = nextRuntime;
43
+ console.log("[hot-reload] Runtime reloaded from ~/.openbot changes");
39
44
  }
40
45
  catch (error) {
41
- console.error("[hot-reload] Reload failed; keeping previous orchestrator", error);
46
+ console.error("[hot-reload] Reload failed; keeping previous runtime", error);
42
47
  }
43
48
  finally {
44
49
  reloadInProgress = false;
@@ -53,12 +58,13 @@ export async function startServer(options = {}) {
53
58
  clearTimeout(reloadTimer);
54
59
  reloadTimer = setTimeout(() => {
55
60
  reloadTimer = null;
56
- void reloadOrchestrator();
61
+ void reloadRuntime();
57
62
  }, 800);
58
63
  };
59
64
  const openBotDir = path.join(os.homedir(), ".openbot");
60
65
  const watcher = chokidar.watch([
61
66
  path.join(openBotDir, "config.json"),
67
+ path.join(openBotDir, "AGENT.md"),
62
68
  path.join(openBotDir, "agents", "**", "*"),
63
69
  path.join(openBotDir, "plugins", "**", "*"),
64
70
  ], {
@@ -84,14 +90,77 @@ export async function startServer(options = {}) {
84
90
  }
85
91
  await watcher.close();
86
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
+ };
87
134
  process.once("SIGINT", () => {
88
- void cleanupWatcher().finally(() => process.exit(0));
135
+ void cleanupBackground().finally(() => process.exit(0));
89
136
  });
90
137
  process.once("SIGTERM", () => {
91
- void cleanupWatcher().finally(() => process.exit(0));
138
+ void cleanupBackground().finally(() => process.exit(0));
92
139
  });
93
140
  app.use(cors());
94
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
+ };
95
164
  const getUploadsDir = () => {
96
165
  const cfg = loadConfig();
97
166
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
@@ -125,6 +194,16 @@ export async function startServer(options = {}) {
125
194
  res.json([]);
126
195
  }
127
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
+ });
128
207
  app.post("/api/models/preview", async (req, res) => {
129
208
  const { provider, apiKey } = req.body;
130
209
  if (provider !== "openai" && provider !== "anthropic") {
@@ -152,6 +231,7 @@ export async function startServer(options = {}) {
152
231
  sessions: "GET /api/sessions",
153
232
  agents: "GET /api/agents",
154
233
  prompts: "GET /api/prompts",
234
+ version: "GET /api/version",
155
235
  },
156
236
  });
157
237
  });
@@ -164,6 +244,83 @@ export async function startServer(options = {}) {
164
244
  { label: "What is the weather in Tokyo?", icon: "sun" },
165
245
  ]);
166
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
+ });
167
324
  app.post("/api/uploads/image", async (req, res) => {
168
325
  const { name, mimeType, dataBase64 } = req.body;
169
326
  if (!mimeType || !allowedMimeTypes.has(mimeType)) {
@@ -229,6 +386,8 @@ export async function startServer(options = {}) {
229
386
  const cfg = loadConfig();
230
387
  res.json({
231
388
  configured: isConfigured(),
389
+ name: cfg.name || "OpenBot",
390
+ description: cfg.description || "The main orchestrator and system settings",
232
391
  model: cfg.model || DEFAULT_MODEL_ID,
233
392
  defaultModelId: DEFAULT_MODEL_ID,
234
393
  defaultModels: DEFAULT_MODEL_BY_PROVIDER,
@@ -237,10 +396,16 @@ export async function startServer(options = {}) {
237
396
  });
238
397
  });
239
398
  app.post("/api/config", async (req, res) => {
240
- const { openai_api_key, anthropic_api_key, model } = req.body;
399
+ const { openai_api_key, anthropic_api_key, model, name, description, image } = req.body;
241
400
  const updates = {};
401
+ if (name)
402
+ updates.name = name.trim();
403
+ if (description)
404
+ updates.description = description.trim();
242
405
  if (model)
243
406
  updates.model = model.trim();
407
+ if (image !== undefined)
408
+ updates.image = image.trim();
244
409
  if (openai_api_key && openai_api_key !== "••••••••••••••••")
245
410
  updates.openaiApiKey = openai_api_key.trim();
246
411
  if (anthropic_api_key && anthropic_api_key !== "••••••••••••••••")
@@ -263,53 +428,334 @@ export async function startServer(options = {}) {
263
428
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
264
429
  const resolvedBaseDir = resolvePath(baseDir);
265
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
+ ];
266
444
  try {
267
- const agents = await listYamlAgents(agentsDir);
268
- 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
+ }));
269
456
  }
270
457
  catch {
271
- res.json([]);
458
+ // ignore
272
459
  }
460
+ res.json(agents);
273
461
  });
274
- app.get("/api/agents/:name/yaml", async (req, res) => {
275
- 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;
276
549
  const cfg = loadConfig();
277
550
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
278
551
  const resolvedBaseDir = resolvePath(baseDir);
279
- 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
+ }
280
564
  try {
281
- const content = await fs.readFile(yamlPath, "utf-8");
282
- res.send(content);
565
+ const content = await fs.readFile(mdPath, "utf-8");
566
+ const { content: body } = matter(content);
567
+ res.send(body.trim());
283
568
  }
284
569
  catch {
285
- res.status(404).send("Agent not found or has no agent.yaml");
570
+ res.status(404).send("");
286
571
  }
287
572
  });
288
- app.put("/api/agents/:name/yaml", async (req, res) => {
289
- const { name } = req.params;
290
- const { yaml } = req.body;
291
- if (!yaml || typeof yaml !== "string") {
292
- 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");
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;
616
+ const cfg = loadConfig();
617
+ const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
618
+ const resolvedBaseDir = resolvePath(baseDir);
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
+ }
631
+ try {
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" });
293
695
  }
294
696
  const cfg = loadConfig();
295
697
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
296
698
  const resolvedBaseDir = resolvePath(baseDir);
297
- const agentDir = path.join(resolvedBaseDir, "agents", name);
298
- const yamlPath = path.join(agentDir, "agent.yaml");
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 = "";
299
716
  try {
300
- await fs.mkdir(agentDir, { recursive: true });
301
- await fs.writeFile(yamlPath, yaml, "utf-8");
302
- // Optionally, hot-reload openBotAgent if needed here.
303
- // But OpenBot runtime loads agents at startup or dynamically per request?
304
- // createOpenBot builds the Melony App. Since createOpenBot is called at startup:
305
- // openBotAgent = await createOpenBot(...) happens once.
306
- // Restarting server is required unless we hot-reload. We can just leave it as is
307
- // and instruct the user to restart, or implement a simple hot reload. Let's stick to simple file write first.
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
+ }
308
754
  res.json({ success: true });
309
755
  }
310
756
  catch (err) {
311
757
  console.error(err);
312
- res.status(500).json({ error: "Failed to write agent.yaml" });
758
+ res.status(500).json({ error: "Failed to write AGENT.md" });
313
759
  }
314
760
  });
315
761
  app.post("/api/actions/open-folder", async (req, res) => {
@@ -332,10 +778,13 @@ export async function startServer(options = {}) {
332
778
  const cfg = loadConfig();
333
779
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
334
780
  const resolvedBaseDir = resolvePath(baseDir);
781
+ const defaultName = cfg.name || "OpenBot";
335
782
  const extensions = [".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif"];
336
783
  const fileNames = ["avatar", "icon", "image", "logo"];
337
784
  const searchDirs = [
338
- path.join(resolvedBaseDir, "agents", name, "assets"),
785
+ (name === defaultName || name === "default")
786
+ ? path.join(resolvedBaseDir, "assets")
787
+ : path.join(resolvedBaseDir, "agents", name, "assets"),
339
788
  path.join(process.cwd(), "server", "src", "agents", name, "assets"),
340
789
  path.join(process.cwd(), "server", "src", "assets", "agents", name),
341
790
  path.join(process.cwd(), "server", "src", "agents", "assets"),
@@ -382,12 +831,12 @@ export async function startServer(options = {}) {
382
831
  state.cwd = process.cwd();
383
832
  if (!state.workspaceRoot)
384
833
  state.workspaceRoot = process.cwd();
385
- const iterator = orchestrator.run(body.event, {
834
+ const iterator = runtime.run(body.event, {
386
835
  runId,
387
836
  state,
388
837
  });
389
838
  const stopStreaming = () => {
390
- iterator.return?.({ done: true, value: undefined });
839
+ void iterator.return?.();
391
840
  };
392
841
  res.on("close", stopStreaming);
393
842
  try {