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.
- package/README.md +1 -1
- package/dist/agents/agent-creator.js +58 -19
- package/dist/agents/os-agent.js +1 -4
- package/dist/agents/planner-agent.js +32 -0
- package/dist/agents/topic-agent.js +1 -1
- package/dist/architecture/contracts.js +1 -0
- package/dist/architecture/execution-engine.js +151 -0
- package/dist/architecture/intent-classifier.js +26 -0
- package/dist/architecture/planner.js +106 -0
- package/dist/automation-worker.js +121 -0
- package/dist/automations.js +52 -0
- package/dist/cli.js +116 -146
- package/dist/config.js +20 -0
- package/dist/core/agents.js +41 -0
- package/dist/core/delegation.js +124 -0
- package/dist/core/manager.js +73 -0
- package/dist/core/plugins.js +77 -0
- package/dist/core/router.js +40 -0
- package/dist/installers.js +156 -0
- package/dist/marketplace.js +80 -0
- package/dist/open-bot.js +34 -157
- package/dist/orchestrator.js +247 -51
- package/dist/plugins/approval/index.js +107 -3
- package/dist/plugins/brain/index.js +17 -86
- package/dist/plugins/brain/memory.js +1 -1
- package/dist/plugins/brain/prompt.js +8 -13
- package/dist/plugins/brain/types.js +0 -15
- package/dist/plugins/file-system/index.js +8 -8
- package/dist/plugins/llm/context-shaping.js +177 -0
- package/dist/plugins/llm/index.js +223 -49
- package/dist/plugins/memory/index.js +220 -0
- package/dist/plugins/memory/memory.js +122 -0
- package/dist/plugins/memory/prompt.js +55 -0
- package/dist/plugins/memory/types.js +45 -0
- package/dist/plugins/shell/index.js +3 -3
- package/dist/plugins/skills/index.js +9 -9
- package/dist/registry/index.js +1 -4
- package/dist/registry/plugin-loader.js +361 -56
- package/dist/registry/plugin-registry.js +21 -4
- package/dist/registry/ts-agent-loader.js +4 -4
- package/dist/registry/yaml-agent-loader.js +78 -20
- package/dist/runtime/execution-trace.js +41 -0
- package/dist/runtime/intent-routing.js +26 -0
- package/dist/runtime/openbot-runtime.js +354 -0
- package/dist/server.js +513 -41
- package/dist/ui/widgets/approval-card.js +22 -2
- package/dist/ui/widgets/delegation.js +29 -0
- package/dist/version.js +62 -0
- 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 {
|
|
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
|
|
27
|
+
const createRuntime = () => createOpenBot({
|
|
22
28
|
openaiApiKey: options.openaiApiKey,
|
|
23
29
|
anthropicApiKey: options.anthropicApiKey,
|
|
24
30
|
});
|
|
25
|
-
let
|
|
31
|
+
let runtime = await createRuntime();
|
|
26
32
|
let reloadTimer = null;
|
|
27
33
|
let reloadInProgress = false;
|
|
28
34
|
let queuedReload = false;
|
|
29
|
-
const
|
|
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
|
|
37
|
-
|
|
38
|
-
console.log("[hot-reload]
|
|
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
|
|
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
|
|
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
|
|
136
|
+
void cleanupBackground().finally(() => process.exit(0));
|
|
89
137
|
});
|
|
90
138
|
process.once("SIGTERM", () => {
|
|
91
|
-
void
|
|
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
|
|
268
|
-
|
|
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
|
-
|
|
459
|
+
// ignore
|
|
272
460
|
}
|
|
461
|
+
res.json(agents);
|
|
273
462
|
});
|
|
274
|
-
app.get("/api/
|
|
275
|
-
const
|
|
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
|
|
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(
|
|
282
|
-
|
|
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("
|
|
571
|
+
res.status(404).send("");
|
|
286
572
|
}
|
|
287
573
|
});
|
|
288
|
-
app.put("/api/agents/:
|
|
289
|
-
const {
|
|
290
|
-
const {
|
|
291
|
-
|
|
292
|
-
|
|
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
|
|
298
|
-
|
|
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(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
856
|
+
const iterator = runtime.run(body.event, {
|
|
386
857
|
runId,
|
|
387
858
|
state,
|
|
388
859
|
});
|
|
389
860
|
const stopStreaming = () => {
|
|
390
|
-
iterator.return?.(
|
|
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)
|