openbot 0.2.3 → 0.2.6

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 +116 -146
  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 +156 -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 +361 -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 +513 -41
  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 +4 -1
package/dist/server.js CHANGED
@@ -6,39 +6,45 @@ 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
+ import { readAgentConfig } from "./registry/plugin-loader.js";
10
11
  import { exec } from "node:child_process";
11
12
  import os from "node:os";
12
13
  import path from "node:path";
13
14
  import fs from "node:fs/promises";
14
15
  import { randomUUID } from "node:crypto";
16
+ import matter from "gray-matter";
15
17
  import { fetchProviderModels, getModelCatalog } from "./model-catalog.js";
16
18
  import { DEFAULT_MODEL_BY_PROVIDER, DEFAULT_MODEL_ID } from "./model-defaults.js";
19
+ import { listAutomations, saveAutomations } from "./automations.js";
20
+ import { startAutomationWorker } from "./automation-worker.js";
21
+ import { getMarketplaceRegistry, installMarketplaceAgent, installMarketplacePlugin } from "./marketplace.js";
22
+ import { getVersionStatus } from "./version.js";
17
23
  export async function startServer(options = {}) {
18
24
  const config = loadConfig();
19
25
  const PORT = Number(options.port ?? config.port ?? process.env.PORT ?? 4001);
20
26
  const app = express();
21
- const createOrchestrator = () => createOpenBot({
27
+ const createRuntime = () => createOpenBot({
22
28
  openaiApiKey: options.openaiApiKey,
23
29
  anthropicApiKey: options.anthropicApiKey,
24
30
  });
25
- let orchestrator = await createOrchestrator();
31
+ let runtime = await createRuntime();
26
32
  let reloadTimer = null;
27
33
  let reloadInProgress = false;
28
34
  let queuedReload = false;
29
- const reloadOrchestrator = async () => {
35
+ const reloadRuntime = async () => {
30
36
  if (reloadInProgress) {
31
37
  queuedReload = true;
32
38
  return;
33
39
  }
34
40
  reloadInProgress = true;
35
41
  try {
36
- const nextOrchestrator = await createOrchestrator();
37
- orchestrator = nextOrchestrator;
38
- console.log("[hot-reload] Orchestrator reloaded from ~/.openbot changes");
42
+ const nextRuntime = await createRuntime();
43
+ runtime = nextRuntime;
44
+ console.log("[hot-reload] Runtime reloaded from ~/.openbot changes");
39
45
  }
40
46
  catch (error) {
41
- console.error("[hot-reload] Reload failed; keeping previous orchestrator", error);
47
+ console.error("[hot-reload] Reload failed; keeping previous runtime", error);
42
48
  }
43
49
  finally {
44
50
  reloadInProgress = false;
@@ -53,12 +59,13 @@ export async function startServer(options = {}) {
53
59
  clearTimeout(reloadTimer);
54
60
  reloadTimer = setTimeout(() => {
55
61
  reloadTimer = null;
56
- void reloadOrchestrator();
62
+ void reloadRuntime();
57
63
  }, 800);
58
64
  };
59
65
  const openBotDir = path.join(os.homedir(), ".openbot");
60
66
  const watcher = chokidar.watch([
61
67
  path.join(openBotDir, "config.json"),
68
+ path.join(openBotDir, "AGENT.md"),
62
69
  path.join(openBotDir, "agents", "**", "*"),
63
70
  path.join(openBotDir, "plugins", "**", "*"),
64
71
  ], {
@@ -84,14 +91,77 @@ export async function startServer(options = {}) {
84
91
  }
85
92
  await watcher.close();
86
93
  };
94
+ const runAutomation = async (automation, scheduledAt) => {
95
+ const sessionId = `automation_${automation.id}`;
96
+ const runId = `run_auto_${generateId()}`;
97
+ const state = (await loadSession(sessionId)) ?? {};
98
+ state.sessionId = sessionId;
99
+ if (!state.cwd)
100
+ state.cwd = process.cwd();
101
+ if (!state.workspaceRoot)
102
+ state.workspaceRoot = process.cwd();
103
+ if (!state.title)
104
+ state.title = `Automation: ${automation.name}`;
105
+ const content = automation.targetType === "agent" && automation.agentName
106
+ ? `/${automation.agentName} ${automation.prompt}`
107
+ : automation.prompt;
108
+ const iterator = runtime.run({
109
+ type: "agent:input",
110
+ data: { content },
111
+ }, { runId, state });
112
+ try {
113
+ console.log(`[automations] Running "${automation.name}" (${automation.id}) at ${scheduledAt.toISOString()}`);
114
+ for await (const chunk of iterator) {
115
+ await logEvent(sessionId, runId, chunk);
116
+ }
117
+ console.log(`[automations] Completed "${automation.name}" (${automation.id})`);
118
+ }
119
+ catch (error) {
120
+ console.error(`[automations] Run failed for "${automation.name}" (${automation.id})`, error);
121
+ throw error;
122
+ }
123
+ finally {
124
+ await saveSession(sessionId, state);
125
+ }
126
+ };
127
+ const stopAutomationWorker = startAutomationWorker({
128
+ listAutomations,
129
+ runAutomation,
130
+ });
131
+ const cleanupBackground = async () => {
132
+ stopAutomationWorker();
133
+ await cleanupWatcher();
134
+ };
87
135
  process.once("SIGINT", () => {
88
- void cleanupWatcher().finally(() => process.exit(0));
136
+ void cleanupBackground().finally(() => process.exit(0));
89
137
  });
90
138
  process.once("SIGTERM", () => {
91
- void cleanupWatcher().finally(() => process.exit(0));
139
+ void cleanupBackground().finally(() => process.exit(0));
92
140
  });
93
141
  app.use(cors());
94
142
  app.use(express.json({ limit: "20mb" }));
143
+ const fileExists = async (targetPath) => fs.access(targetPath).then(() => true).catch(() => false);
144
+ const toTitleCaseFromSlug = (value) => value
145
+ .split(/[-_]+/)
146
+ .filter(Boolean)
147
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
148
+ .join(" ") || "Agent";
149
+ const resolveAgentFolder = async (agentIdOrName, resolvedBaseDir) => {
150
+ const agentsDir = path.join(resolvedBaseDir, "agents");
151
+ const directFolder = path.join(agentsDir, agentIdOrName);
152
+ if (await fileExists(path.join(directFolder, "AGENT.md"))) {
153
+ return directFolder;
154
+ }
155
+ try {
156
+ const allPlugins = await listPlugins(agentsDir);
157
+ const match = allPlugins.find((plugin) => plugin.type === "agent"
158
+ && (path.basename(plugin.folder) === agentIdOrName || plugin.name === agentIdOrName));
159
+ return match?.folder ?? null;
160
+ }
161
+ catch {
162
+ return null;
163
+ }
164
+ };
95
165
  const getUploadsDir = () => {
96
166
  const cfg = loadConfig();
97
167
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
@@ -125,6 +195,16 @@ export async function startServer(options = {}) {
125
195
  res.json([]);
126
196
  }
127
197
  });
198
+ app.get("/api/version", async (_req, res) => {
199
+ try {
200
+ const status = await getVersionStatus();
201
+ res.json(status);
202
+ }
203
+ catch (err) {
204
+ console.error("Failed to check version:", err);
205
+ res.status(500).json({ error: "Failed to check version" });
206
+ }
207
+ });
128
208
  app.post("/api/models/preview", async (req, res) => {
129
209
  const { provider, apiKey } = req.body;
130
210
  if (provider !== "openai" && provider !== "anthropic") {
@@ -152,6 +232,7 @@ export async function startServer(options = {}) {
152
232
  sessions: "GET /api/sessions",
153
233
  agents: "GET /api/agents",
154
234
  prompts: "GET /api/prompts",
235
+ version: "GET /api/version",
155
236
  },
156
237
  });
157
238
  });
@@ -164,6 +245,83 @@ export async function startServer(options = {}) {
164
245
  { label: "What is the weather in Tokyo?", icon: "sun" },
165
246
  ]);
166
247
  });
248
+ app.get("/api/automations", async (_req, res) => {
249
+ const items = await listAutomations();
250
+ res.json(items);
251
+ });
252
+ app.post("/api/automations", async (req, res) => {
253
+ const { name, prompt, cron, targetType, agentName } = req.body;
254
+ const normalizedTargetType = targetType === "agent" ? "agent" : "orchestrator";
255
+ const normalizedAgentName = typeof agentName === "string" ? agentName.trim() : "";
256
+ if (typeof name !== "string" ||
257
+ typeof prompt !== "string" ||
258
+ typeof cron !== "string" ||
259
+ !name.trim() ||
260
+ !prompt.trim() ||
261
+ !cron.trim() ||
262
+ (normalizedTargetType === "agent" && !normalizedAgentName)) {
263
+ return res.status(400).json({ error: "Invalid automation payload" });
264
+ }
265
+ const now = new Date().toISOString();
266
+ const next = {
267
+ id: `auto_${randomUUID()}`,
268
+ name: name.trim(),
269
+ prompt: prompt.trim(),
270
+ cron: cron.trim(),
271
+ targetType: normalizedTargetType,
272
+ agentName: normalizedTargetType === "agent" ? normalizedAgentName : undefined,
273
+ enabled: true,
274
+ createdAt: now,
275
+ updatedAt: now,
276
+ };
277
+ const current = await listAutomations();
278
+ await saveAutomations([next, ...current]);
279
+ res.status(201).json(next);
280
+ });
281
+ app.put("/api/automations/:id", async (req, res) => {
282
+ const { id } = req.params;
283
+ const { name, prompt, cron, enabled, targetType, agentName } = req.body;
284
+ const current = await listAutomations();
285
+ const index = current.findIndex((item) => item.id === id);
286
+ if (index < 0) {
287
+ return res.status(404).json({ error: "Automation not found" });
288
+ }
289
+ const existing = current[index];
290
+ const nextTargetType = targetType === "agent"
291
+ ? "agent"
292
+ : targetType === "orchestrator"
293
+ ? "orchestrator"
294
+ : existing.targetType;
295
+ const nextAgentName = typeof agentName === "string"
296
+ ? agentName.trim()
297
+ : (existing.agentName ?? "");
298
+ if (nextTargetType === "agent" && !nextAgentName) {
299
+ return res.status(400).json({ error: "agentName is required when targetType is agent" });
300
+ }
301
+ const updated = {
302
+ ...existing,
303
+ name: typeof name === "string" ? name.trim() || existing.name : existing.name,
304
+ prompt: typeof prompt === "string" ? prompt.trim() || existing.prompt : existing.prompt,
305
+ cron: typeof cron === "string" ? cron.trim() || existing.cron : existing.cron,
306
+ targetType: nextTargetType,
307
+ agentName: nextTargetType === "agent" ? nextAgentName : undefined,
308
+ enabled: typeof enabled === "boolean" ? enabled : existing.enabled,
309
+ updatedAt: new Date().toISOString(),
310
+ };
311
+ current[index] = updated;
312
+ await saveAutomations(current);
313
+ res.json(updated);
314
+ });
315
+ app.delete("/api/automations/:id", async (req, res) => {
316
+ const { id } = req.params;
317
+ const current = await listAutomations();
318
+ const next = current.filter((item) => item.id !== id);
319
+ if (next.length === current.length) {
320
+ return res.status(404).json({ error: "Automation not found" });
321
+ }
322
+ await saveAutomations(next);
323
+ res.json({ success: true });
324
+ });
167
325
  app.post("/api/uploads/image", async (req, res) => {
168
326
  const { name, mimeType, dataBase64 } = req.body;
169
327
  if (!mimeType || !allowedMimeTypes.has(mimeType)) {
@@ -229,6 +387,8 @@ export async function startServer(options = {}) {
229
387
  const cfg = loadConfig();
230
388
  res.json({
231
389
  configured: isConfigured(),
390
+ name: cfg.name || "OpenBot",
391
+ description: cfg.description || "The main orchestrator and system settings",
232
392
  model: cfg.model || DEFAULT_MODEL_ID,
233
393
  defaultModelId: DEFAULT_MODEL_ID,
234
394
  defaultModels: DEFAULT_MODEL_BY_PROVIDER,
@@ -237,10 +397,16 @@ export async function startServer(options = {}) {
237
397
  });
238
398
  });
239
399
  app.post("/api/config", async (req, res) => {
240
- const { openai_api_key, anthropic_api_key, model } = req.body;
400
+ const { openai_api_key, anthropic_api_key, model, name, description, image } = req.body;
241
401
  const updates = {};
402
+ if (name)
403
+ updates.name = name.trim();
404
+ if (description)
405
+ updates.description = description.trim();
242
406
  if (model)
243
407
  updates.model = model.trim();
408
+ if (image !== undefined)
409
+ updates.image = image.trim();
244
410
  if (openai_api_key && openai_api_key !== "••••••••••••••••")
245
411
  updates.openaiApiKey = openai_api_key.trim();
246
412
  if (anthropic_api_key && anthropic_api_key !== "••••••••••••••••")
@@ -263,53 +429,334 @@ export async function startServer(options = {}) {
263
429
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
264
430
  const resolvedBaseDir = resolvePath(baseDir);
265
431
  const agentsDir = path.join(resolvedBaseDir, "agents");
432
+ const defaultName = cfg.name || "OpenBot";
433
+ const defaultDescription = cfg.description || "The main orchestrator and system settings";
434
+ const agents = [
435
+ {
436
+ id: "default",
437
+ name: defaultName,
438
+ description: defaultDescription,
439
+ folder: resolvedBaseDir,
440
+ isDefault: true,
441
+ hasAgentMd: true,
442
+ image: cfg.image,
443
+ },
444
+ ];
266
445
  try {
267
- const agents = await listYamlAgents(agentsDir);
268
- res.json(agents);
446
+ const allPlugins = await listPlugins(agentsDir);
447
+ const agentPlugins = allPlugins.filter(p => p.type === "agent");
448
+ agents.push(...agentPlugins.map((plugin) => {
449
+ const id = path.basename(plugin.folder);
450
+ const hasUnnamedDisplayName = /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(plugin.name);
451
+ return {
452
+ ...plugin,
453
+ id,
454
+ name: hasUnnamedDisplayName ? toTitleCaseFromSlug(id) : plugin.name,
455
+ };
456
+ }));
269
457
  }
270
458
  catch {
271
- res.json([]);
459
+ // ignore
272
460
  }
461
+ res.json(agents);
273
462
  });
274
- app.get("/api/agents/:name/yaml", async (req, res) => {
275
- const { name } = req.params;
463
+ app.get("/api/plugins", async (_req, res) => {
464
+ const cfg = loadConfig();
465
+ const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
466
+ const resolvedBaseDir = resolvePath(baseDir);
467
+ const pluginsDir = path.join(resolvedBaseDir, "plugins");
468
+ try {
469
+ const allPlugins = await listPlugins(pluginsDir);
470
+ const toolPlugins = allPlugins.filter((plugin) => plugin.type === "tool");
471
+ res.json(toolPlugins.map((plugin) => {
472
+ const id = path.basename(plugin.folder);
473
+ const hasUnnamedDisplayName = /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(plugin.name);
474
+ return {
475
+ ...plugin,
476
+ id,
477
+ name: hasUnnamedDisplayName ? toTitleCaseFromSlug(id) : plugin.name,
478
+ };
479
+ }));
480
+ }
481
+ catch (error) {
482
+ console.error(error);
483
+ res.status(500).json({ error: "Failed to list plugins" });
484
+ }
485
+ });
486
+ app.get("/api/registry/plugins", async (_req, res) => {
487
+ try {
488
+ const tools = runtime.registry.getTools();
489
+ res.json(tools.map((t) => ({
490
+ name: t.name,
491
+ description: t.description,
492
+ isBuiltIn: !!t.isBuiltIn,
493
+ })));
494
+ }
495
+ catch (error) {
496
+ console.error(error);
497
+ res.status(500).json({ error: "Failed to list registry plugins" });
498
+ }
499
+ });
500
+ app.get("/api/marketplace/agents", async (_req, res) => {
501
+ try {
502
+ const registry = await getMarketplaceRegistry();
503
+ res.json(registry.agents);
504
+ }
505
+ catch (error) {
506
+ console.error(error);
507
+ res.status(500).json({ error: "Failed to load marketplace agents" });
508
+ }
509
+ });
510
+ app.get("/api/marketplace/plugins", async (_req, res) => {
511
+ try {
512
+ const registry = await getMarketplaceRegistry();
513
+ res.json(registry.plugins);
514
+ }
515
+ catch (error) {
516
+ console.error(error);
517
+ res.status(500).json({ error: "Failed to load marketplace plugins" });
518
+ }
519
+ });
520
+ app.post("/api/marketplace/install-agent", async (req, res) => {
521
+ const { id } = req.body;
522
+ if (typeof id !== "string" || !id.trim()) {
523
+ return res.status(400).json({ error: "Marketplace agent id is required" });
524
+ }
525
+ try {
526
+ const result = await installMarketplaceAgent(id.trim());
527
+ res.json({ success: true, installedName: result.installedName, item: result.agent });
528
+ }
529
+ catch (error) {
530
+ console.error(error);
531
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to install agent" });
532
+ }
533
+ });
534
+ app.post("/api/marketplace/install-plugin", async (req, res) => {
535
+ const { id } = req.body;
536
+ if (typeof id !== "string" || !id.trim()) {
537
+ return res.status(400).json({ error: "Marketplace plugin id is required" });
538
+ }
539
+ try {
540
+ const result = await installMarketplacePlugin(id.trim());
541
+ res.json({ success: true, installedName: result.installedName, item: result.plugin });
542
+ }
543
+ catch (error) {
544
+ console.error(error);
545
+ res.status(500).json({ error: error instanceof Error ? error.message : "Failed to install plugin" });
546
+ }
547
+ });
548
+ app.get("/api/agents/:agentId/md", async (req, res) => {
549
+ const { agentId } = req.params;
276
550
  const cfg = loadConfig();
277
551
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
278
552
  const resolvedBaseDir = resolvePath(baseDir);
279
- const yamlPath = path.join(resolvedBaseDir, "agents", name, "agent.yaml");
553
+ const defaultName = cfg.name || "OpenBot";
554
+ let mdPath;
555
+ if (agentId === defaultName || agentId === "default") {
556
+ mdPath = path.join(resolvedBaseDir, "AGENT.md");
557
+ }
558
+ else {
559
+ const pluginFolder = await resolveAgentFolder(agentId, resolvedBaseDir);
560
+ if (!pluginFolder) {
561
+ return res.status(404).send("");
562
+ }
563
+ mdPath = path.join(pluginFolder, "AGENT.md");
564
+ }
280
565
  try {
281
- const content = await fs.readFile(yamlPath, "utf-8");
282
- res.send(content);
566
+ const content = await fs.readFile(mdPath, "utf-8");
567
+ const { content: body } = matter(content);
568
+ res.send(body.trim());
283
569
  }
284
570
  catch {
285
- res.status(404).send("Agent not found or has no agent.yaml");
571
+ res.status(404).send("");
286
572
  }
287
573
  });
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" });
574
+ app.put("/api/agents/:agentId/md", async (req, res) => {
575
+ const { agentId } = req.params;
576
+ const { md } = req.body;
577
+ const cfg = loadConfig();
578
+ const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
579
+ const resolvedBaseDir = resolvePath(baseDir);
580
+ const defaultName = cfg.name || "OpenBot";
581
+ let mdPath;
582
+ let pluginDir;
583
+ if (agentId === defaultName || agentId === "default") {
584
+ pluginDir = resolvedBaseDir;
585
+ mdPath = path.join(resolvedBaseDir, "AGENT.md");
586
+ }
587
+ else {
588
+ const pluginFolder = await resolveAgentFolder(agentId, resolvedBaseDir);
589
+ if (!pluginFolder) {
590
+ return res.status(404).json({ error: "Agent not found" });
591
+ }
592
+ pluginDir = pluginFolder;
593
+ mdPath = path.join(pluginDir, "AGENT.md");
594
+ }
595
+ try {
596
+ await fs.mkdir(pluginDir, { recursive: true });
597
+ let frontmatter = {};
598
+ try {
599
+ const currentContent = await fs.readFile(mdPath, "utf-8");
600
+ const parsed = matter(currentContent);
601
+ frontmatter = parsed.data || {};
602
+ }
603
+ catch {
604
+ // No current AGENT.md, starting with empty frontmatter or defaults
605
+ }
606
+ const consolidated = matter.stringify(md, frontmatter);
607
+ await fs.writeFile(mdPath, consolidated, "utf-8");
608
+ res.json({ success: true });
609
+ }
610
+ catch (err) {
611
+ console.error(err);
612
+ res.status(500).json({ error: "Failed to write AGENT.md" });
613
+ }
614
+ });
615
+ app.get("/api/agents/:agentId/config", async (req, res) => {
616
+ const { agentId } = req.params;
617
+ const cfg = loadConfig();
618
+ const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
619
+ const resolvedBaseDir = resolvePath(baseDir);
620
+ const defaultName = cfg.name || "OpenBot";
621
+ let mdPath;
622
+ if (agentId === defaultName || agentId === "default") {
623
+ mdPath = path.join(resolvedBaseDir, "AGENT.md");
624
+ }
625
+ else {
626
+ const pluginFolder = await resolveAgentFolder(agentId, resolvedBaseDir);
627
+ if (!pluginFolder) {
628
+ return res.status(404).json({ error: "Agent not found or invalid format" });
629
+ }
630
+ mdPath = path.join(pluginFolder, "AGENT.md");
631
+ }
632
+ try {
633
+ const content = await fs.readFile(mdPath, "utf-8");
634
+ const { data: parsed, content: body } = matter(content);
635
+ if (!parsed || typeof parsed !== "object") {
636
+ return res.status(400).json({ error: "Invalid AGENT.md frontmatter" });
637
+ }
638
+ res.json({
639
+ name: typeof parsed.name === "string" ? parsed.name : (agentId === defaultName || agentId === "default" ? defaultName : ""),
640
+ description: typeof parsed.description === "string" ? parsed.description : (agentId === defaultName || agentId === "default" ? cfg.description || "" : ""),
641
+ model: typeof parsed.model === "string" ? parsed.model : (agentId === defaultName || agentId === "default" ? cfg.model : undefined),
642
+ plugins: Array.isArray(parsed.plugins) ? parsed.plugins : [],
643
+ subscribe: Array.isArray(parsed.subscribe)
644
+ ? parsed.subscribe.filter((item) => typeof item === "string")
645
+ : [],
646
+ });
647
+ }
648
+ catch {
649
+ if (agentId === defaultName || agentId === "default") {
650
+ // Fallback for default agent if AGENT.md is missing or unreadable
651
+ return res.json({
652
+ name: defaultName,
653
+ description: cfg.description || "",
654
+ model: cfg.model,
655
+ plugins: [],
656
+ systemPrompt: "",
657
+ subscribe: [],
658
+ });
659
+ }
660
+ res.status(404).json({ error: "Agent not found or invalid format" });
661
+ }
662
+ });
663
+ app.put("/api/agents/:agentId/config", async (req, res) => {
664
+ const { agentId } = req.params;
665
+ const body = req.body;
666
+ if (typeof body.name !== "string" ||
667
+ typeof body.description !== "string" ||
668
+ !Array.isArray(body.plugins)) {
669
+ return res.status(400).json({ error: "Invalid agent config payload" });
670
+ }
671
+ const normalizedPlugins = [];
672
+ for (const plugin of body.plugins) {
673
+ if (typeof plugin === "string") {
674
+ const normalized = plugin.trim();
675
+ if (normalized)
676
+ normalizedPlugins.push(normalized);
677
+ continue;
678
+ }
679
+ if (!plugin || typeof plugin !== "object" || typeof plugin.name !== "string") {
680
+ continue;
681
+ }
682
+ const normalizedName = plugin.name.trim();
683
+ if (!normalizedName)
684
+ continue;
685
+ if (typeof plugin.config === "undefined") {
686
+ normalizedPlugins.push({ name: normalizedName });
687
+ }
688
+ else {
689
+ normalizedPlugins.push({ name: normalizedName, config: plugin.config });
690
+ }
691
+ }
692
+ const normalizedName = body.name.trim();
693
+ const normalizedDescription = body.description.trim();
694
+ if (!normalizedName || !normalizedDescription) {
695
+ return res.status(400).json({ error: "name and description are required" });
293
696
  }
294
697
  const cfg = loadConfig();
295
698
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
296
699
  const resolvedBaseDir = resolvePath(baseDir);
297
- const agentDir = path.join(resolvedBaseDir, "agents", name);
298
- const yamlPath = path.join(agentDir, "agent.yaml");
700
+ const defaultName = cfg.name || "OpenBot";
701
+ let pluginDir;
702
+ let mdPath;
703
+ if (agentId === defaultName || agentId === "default") {
704
+ pluginDir = resolvedBaseDir;
705
+ mdPath = path.join(resolvedBaseDir, "AGENT.md");
706
+ }
707
+ else {
708
+ const pluginFolder = await resolveAgentFolder(agentId, resolvedBaseDir);
709
+ if (!pluginFolder) {
710
+ return res.status(404).json({ error: "Agent not found" });
711
+ }
712
+ pluginDir = pluginFolder;
713
+ mdPath = path.join(pluginDir, "AGENT.md");
714
+ }
715
+ // Read current content to preserve the body (instructions)
716
+ let currentBody = "";
717
+ try {
718
+ const currentContent = await fs.readFile(mdPath, "utf-8");
719
+ const parsed = matter(currentContent);
720
+ currentBody = parsed.content;
721
+ }
722
+ catch {
723
+ // No current AGENT.md, starting with empty body or defaults
724
+ }
725
+ // Prepare frontmatter
726
+ const frontmatter = {
727
+ name: normalizedName,
728
+ description: normalizedDescription,
729
+ plugins: normalizedPlugins,
730
+ };
731
+ if (typeof body.model === "string" && body.model.trim()) {
732
+ frontmatter.model = body.model.trim();
733
+ }
734
+ if (Array.isArray(body.subscribe) && body.subscribe.length > 0) {
735
+ const normalizedSubscribe = body.subscribe
736
+ .filter((item) => typeof item === "string")
737
+ .map((item) => item.trim())
738
+ .filter(Boolean);
739
+ if (normalizedSubscribe.length > 0) {
740
+ frontmatter.subscribe = normalizedSubscribe;
741
+ }
742
+ }
299
743
  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.
744
+ await fs.mkdir(pluginDir, { recursive: true });
745
+ const consolidated = matter.stringify(currentBody, frontmatter);
746
+ await fs.writeFile(mdPath, consolidated, "utf-8");
747
+ if (agentId === defaultName || agentId === "default") {
748
+ // For the default agent, sync changes back to config.json
749
+ saveConfig({
750
+ name: normalizedName,
751
+ description: normalizedDescription,
752
+ model: (typeof body.model === "string" && body.model.trim()) ? body.model.trim() : undefined,
753
+ });
754
+ }
308
755
  res.json({ success: true });
309
756
  }
310
757
  catch (err) {
311
758
  console.error(err);
312
- res.status(500).json({ error: "Failed to write agent.yaml" });
759
+ res.status(500).json({ error: "Failed to write AGENT.md" });
313
760
  }
314
761
  });
315
762
  app.post("/api/actions/open-folder", async (req, res) => {
@@ -332,10 +779,33 @@ export async function startServer(options = {}) {
332
779
  const cfg = loadConfig();
333
780
  const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
334
781
  const resolvedBaseDir = resolvePath(baseDir);
782
+ const defaultName = cfg.name || "OpenBot";
783
+ // 1. Resolve agent folder
784
+ let agentFolder = null;
785
+ if (name === defaultName || name === "default") {
786
+ agentFolder = resolvedBaseDir;
787
+ }
788
+ else {
789
+ agentFolder = await resolveAgentFolder(name, resolvedBaseDir);
790
+ }
791
+ // 2. Check for remote image in AGENT.md if folder exists
792
+ if (agentFolder) {
793
+ try {
794
+ const { image } = await readAgentConfig(agentFolder);
795
+ if (image && (image.startsWith("http://") || image.startsWith("https://"))) {
796
+ return res.redirect(image);
797
+ }
798
+ }
799
+ catch {
800
+ // ignore
801
+ }
802
+ }
335
803
  const extensions = [".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif"];
336
804
  const fileNames = ["avatar", "icon", "image", "logo"];
337
805
  const searchDirs = [
338
- path.join(resolvedBaseDir, "agents", name, "assets"),
806
+ (name === defaultName || name === "default")
807
+ ? path.join(resolvedBaseDir, "assets")
808
+ : (agentFolder ? path.join(agentFolder, "assets") : path.join(resolvedBaseDir, "agents", name, "assets")),
339
809
  path.join(process.cwd(), "server", "src", "agents", name, "assets"),
340
810
  path.join(process.cwd(), "server", "src", "assets", "agents", name),
341
811
  path.join(process.cwd(), "server", "src", "agents", "assets"),
@@ -344,7 +814,8 @@ export async function startServer(options = {}) {
344
814
  for (const dir of searchDirs) {
345
815
  for (const fileName of fileNames) {
346
816
  for (const ext of extensions) {
347
- const baseName = (dir.endsWith("assets") && !dir.includes(name)) ? name : fileName;
817
+ const isAgentSpecificDir = dir.includes(name) || (agentFolder && dir.includes(agentFolder));
818
+ const baseName = (dir.endsWith("assets") && !isAgentSpecificDir) ? name : fileName;
348
819
  const p = path.join(dir, `${baseName}${ext}`);
349
820
  try {
350
821
  await fs.access(p);
@@ -382,12 +853,12 @@ export async function startServer(options = {}) {
382
853
  state.cwd = process.cwd();
383
854
  if (!state.workspaceRoot)
384
855
  state.workspaceRoot = process.cwd();
385
- const iterator = orchestrator.run(body.event, {
856
+ const iterator = runtime.run(body.event, {
386
857
  runId,
387
858
  state,
388
859
  });
389
860
  const stopStreaming = () => {
390
- iterator.return?.({ done: true, value: undefined });
861
+ void iterator.return?.();
391
862
  };
392
863
  res.on("close", stopStreaming);
393
864
  try {
@@ -419,6 +890,7 @@ export async function startServer(options = {}) {
419
890
  console.log(`OpenBot server listening at http://localhost:${PORT}`);
420
891
  console.log(` - Chat endpoint: POST /api/chat`);
421
892
  console.log(` - REST endpoints: /api/config, /api/sessions, /api/agents`);
893
+ console.log(`\nšŸš€ TIP: Use 'openbot up' to run both the server and web dashboard together.`);
422
894
  if (options.openaiApiKey)
423
895
  console.log(" - Using OpenAI API Key from CLI");
424
896
  if (options.anthropicApiKey)