openbot 0.2.11 → 0.2.13
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/.prettierrc +8 -0
- package/AGENTS.md +68 -0
- package/CONTRIBUTING.md +74 -0
- package/LICENSE +21 -0
- package/README.md +117 -14
- package/dist/agents/system.js +106 -0
- package/dist/app/cli.js +27 -0
- package/dist/app/config.js +64 -0
- package/dist/app/server.js +237 -0
- package/dist/app/utils.js +35 -0
- package/dist/harness/agent-harness.js +45 -0
- package/dist/harness/mcp.js +61 -0
- package/dist/harness/orchestrator.js +273 -0
- package/dist/harness/process.js +7 -0
- package/dist/plugins/ai-sdk.js +141 -0
- package/dist/plugins/delegation.js +52 -0
- package/dist/plugins/mcp.js +140 -0
- package/dist/plugins/storage.js +502 -0
- package/dist/plugins/ui.js +47 -0
- package/dist/registry/plugins.js +73 -0
- package/dist/services/storage.js +724 -0
- package/docs/README.md +7 -0
- package/docs/agents.md +83 -0
- package/docs/architecture.md +34 -0
- package/docs/plugins.md +77 -0
- package/logo-black.png +0 -0
- package/{dist/assets/logo.js → logo-black.svg} +24 -24
- package/{dist/ui/sidebar.js → logo-white.svg} +23 -88
- package/package.json +10 -9
- package/src/agents/system.ts +112 -0
- package/src/app/cli.ts +38 -0
- package/src/app/config.ts +104 -0
- package/src/app/server.ts +284 -0
- package/src/app/types.ts +476 -0
- package/src/app/utils.ts +43 -0
- package/src/assets/icon.svg +1 -0
- package/src/harness/agent-harness.ts +58 -0
- package/src/harness/mcp.ts +78 -0
- package/src/harness/orchestrator.ts +342 -0
- package/src/harness/process.ts +9 -0
- package/src/harness/types.ts +34 -0
- package/src/plugins/ai-sdk.ts +197 -0
- package/src/plugins/delegation.ts +60 -0
- package/src/plugins/mcp.ts +154 -0
- package/src/plugins/storage.ts +725 -0
- package/src/plugins/ui.ts +57 -0
- package/src/registry/plugins.ts +85 -0
- package/src/services/storage.ts +957 -0
- package/tsconfig.json +18 -0
- package/dist/agents/agent-creator.js +0 -74
- package/dist/agents/browser-agent.js +0 -31
- package/dist/agents/os-agent.js +0 -32
- package/dist/agents/planner-agent.js +0 -32
- package/dist/agents/topic-agent.js +0 -46
- package/dist/architecture/execution-engine.js +0 -151
- package/dist/architecture/intent-classifier.js +0 -26
- package/dist/architecture/planner.js +0 -106
- package/dist/automation-worker.js +0 -121
- package/dist/automations.js +0 -52
- package/dist/cli.js +0 -275
- package/dist/config.js +0 -53
- package/dist/core/agents.js +0 -41
- package/dist/core/delegation.js +0 -230
- package/dist/core/manager.js +0 -96
- package/dist/core/plugins.js +0 -74
- package/dist/core/router.js +0 -191
- package/dist/handlers/init.js +0 -29
- package/dist/handlers/session-change.js +0 -21
- package/dist/handlers/settings.js +0 -47
- package/dist/handlers/tab-change.js +0 -14
- package/dist/installers.js +0 -156
- package/dist/marketplace.js +0 -80
- package/dist/model-catalog.js +0 -132
- package/dist/model-defaults.js +0 -25
- package/dist/models.js +0 -47
- package/dist/open-bot.js +0 -51
- package/dist/orchestrator/direct-invocation.js +0 -13
- package/dist/orchestrator/events.js +0 -36
- package/dist/orchestrator/state.js +0 -54
- package/dist/orchestrator.js +0 -422
- package/dist/plugins/agent/index.js +0 -81
- package/dist/plugins/approval/index.js +0 -100
- package/dist/plugins/brain/identity.js +0 -77
- package/dist/plugins/brain/index.js +0 -204
- package/dist/plugins/brain/memory.js +0 -120
- package/dist/plugins/brain/prompt.js +0 -46
- package/dist/plugins/brain/types.js +0 -45
- package/dist/plugins/brain/ui.js +0 -7
- package/dist/plugins/browser/index.js +0 -629
- package/dist/plugins/browser/ui.js +0 -13
- package/dist/plugins/file-system/index.js +0 -171
- package/dist/plugins/file-system/ui.js +0 -6
- package/dist/plugins/llm/context-budget.js +0 -139
- package/dist/plugins/llm/context-shaping.js +0 -177
- package/dist/plugins/llm/index.js +0 -380
- package/dist/plugins/memory/index.js +0 -220
- package/dist/plugins/memory/memory.js +0 -122
- package/dist/plugins/memory/prompt.js +0 -55
- package/dist/plugins/memory/types.js +0 -45
- package/dist/plugins/meta-agent/index.js +0 -570
- package/dist/plugins/meta-agent/ui.js +0 -11
- package/dist/plugins/shell/index.js +0 -100
- package/dist/plugins/shell/ui.js +0 -6
- package/dist/plugins/skills/index.js +0 -286
- package/dist/plugins/skills/types.js +0 -50
- package/dist/plugins/skills/ui.js +0 -12
- package/dist/registry/agent-registry.js +0 -35
- package/dist/registry/index.js +0 -2
- package/dist/registry/plugin-loader.js +0 -499
- package/dist/registry/plugin-registry.js +0 -44
- package/dist/registry/ts-agent-loader.js +0 -82
- package/dist/registry/yaml-agent-loader.js +0 -246
- package/dist/runtime/execution-trace.js +0 -41
- package/dist/runtime/intent-routing.js +0 -26
- package/dist/runtime/openbot-runtime.js +0 -354
- package/dist/server.js +0 -890
- package/dist/session.js +0 -179
- package/dist/ui/block.js +0 -12
- package/dist/ui/header.js +0 -52
- package/dist/ui/layout.js +0 -26
- package/dist/ui/navigation.js +0 -15
- package/dist/ui/settings.js +0 -106
- package/dist/ui/skills.js +0 -7
- package/dist/ui/thread.js +0 -16
- package/dist/ui/widgets/action-list.js +0 -2
- package/dist/ui/widgets/approval-card.js +0 -9
- package/dist/ui/widgets/code-snippet.js +0 -2
- package/dist/ui/widgets/data-block.js +0 -2
- package/dist/ui/widgets/data-table.js +0 -2
- package/dist/ui/widgets/delegation.js +0 -29
- package/dist/ui/widgets/empty-state.js +0 -2
- package/dist/ui/widgets/index.js +0 -23
- package/dist/ui/widgets/inquiry.js +0 -7
- package/dist/ui/widgets/key-value.js +0 -2
- package/dist/ui/widgets/progress-step.js +0 -2
- package/dist/ui/widgets/resource-card.js +0 -2
- package/dist/ui/widgets/status.js +0 -2
- package/dist/ui/widgets/todo-list.js +0 -2
- package/dist/version.js +0 -62
- /package/dist/{types.js → app/types.js} +0 -0
- /package/dist/{architecture/contracts.js → harness/types.js} +0 -0
package/dist/server.js
DELETED
|
@@ -1,890 +0,0 @@
|
|
|
1
|
-
import "dotenv/config";
|
|
2
|
-
import express from "express";
|
|
3
|
-
import cors from "cors";
|
|
4
|
-
import { generateId } from "melony";
|
|
5
|
-
import { createOpenBot } from "./open-bot.js";
|
|
6
|
-
import { loadConfig, saveConfig, isConfigured, resolvePath, DEFAULT_BASE_DIR } from "./config.js";
|
|
7
|
-
import { loadSession, saveSession, logEvent, loadEvents, listSessions } from "./session.js";
|
|
8
|
-
import { listPlugins } from "./registry/index.js";
|
|
9
|
-
import { readAgentConfig } from "./registry/plugin-loader.js";
|
|
10
|
-
import { exec } from "node:child_process";
|
|
11
|
-
import os from "node:os";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import fs from "node:fs/promises";
|
|
14
|
-
import { randomUUID } from "node:crypto";
|
|
15
|
-
import matter from "gray-matter";
|
|
16
|
-
import { fetchProviderModels, getModelCatalog } from "./model-catalog.js";
|
|
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";
|
|
22
|
-
export async function startServer(options = {}) {
|
|
23
|
-
const config = loadConfig();
|
|
24
|
-
const baseDir = config.baseDir || DEFAULT_BASE_DIR;
|
|
25
|
-
const resolvedBaseDir = resolvePath(baseDir);
|
|
26
|
-
const PORT = Number(options.port ?? config.port ?? process.env.PORT ?? 4001);
|
|
27
|
-
const app = express();
|
|
28
|
-
const createRuntime = () => createOpenBot({
|
|
29
|
-
openaiApiKey: options.openaiApiKey,
|
|
30
|
-
anthropicApiKey: options.anthropicApiKey,
|
|
31
|
-
});
|
|
32
|
-
let runtime = await createRuntime();
|
|
33
|
-
let reloadTimer = null;
|
|
34
|
-
let reloadInProgress = false;
|
|
35
|
-
let queuedReload = false;
|
|
36
|
-
const reloadRuntime = async () => {
|
|
37
|
-
if (reloadInProgress) {
|
|
38
|
-
queuedReload = true;
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
reloadInProgress = true;
|
|
42
|
-
try {
|
|
43
|
-
const nextRuntime = await createRuntime();
|
|
44
|
-
runtime = nextRuntime;
|
|
45
|
-
console.log("[hot-reload] Runtime reloaded from ~/.openbot changes");
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
console.error("[hot-reload] Reload failed; keeping previous runtime", error);
|
|
49
|
-
}
|
|
50
|
-
finally {
|
|
51
|
-
reloadInProgress = false;
|
|
52
|
-
if (queuedReload) {
|
|
53
|
-
queuedReload = false;
|
|
54
|
-
scheduleReload();
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
const scheduleReload = () => {
|
|
59
|
-
if (reloadTimer)
|
|
60
|
-
clearTimeout(reloadTimer);
|
|
61
|
-
reloadTimer = setTimeout(() => {
|
|
62
|
-
reloadTimer = null;
|
|
63
|
-
void reloadRuntime();
|
|
64
|
-
}, 800);
|
|
65
|
-
};
|
|
66
|
-
const openBotDir = resolvedBaseDir;
|
|
67
|
-
const agentsDir = path.join(openBotDir, "agents");
|
|
68
|
-
const pluginsDir = path.join(openBotDir, "plugins");
|
|
69
|
-
await fs.mkdir(agentsDir, { recursive: true });
|
|
70
|
-
await fs.mkdir(pluginsDir, { recursive: true });
|
|
71
|
-
const cleanupWatcher = async () => {
|
|
72
|
-
if (reloadTimer) {
|
|
73
|
-
clearTimeout(reloadTimer);
|
|
74
|
-
reloadTimer = null;
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
const runAutomation = async (automation, scheduledAt) => {
|
|
78
|
-
const sessionId = `automation_${automation.id}`;
|
|
79
|
-
const runId = `run_auto_${generateId()}`;
|
|
80
|
-
const state = (await loadSession(sessionId)) ?? {};
|
|
81
|
-
state.sessionId = sessionId;
|
|
82
|
-
if (!state.cwd)
|
|
83
|
-
state.cwd = process.cwd();
|
|
84
|
-
if (!state.workspaceRoot)
|
|
85
|
-
state.workspaceRoot = process.cwd();
|
|
86
|
-
if (!state.title)
|
|
87
|
-
state.title = `Automation: ${automation.name}`;
|
|
88
|
-
const content = automation.targetType === "agent" && automation.agentName
|
|
89
|
-
? `/${automation.agentName} ${automation.prompt}`
|
|
90
|
-
: automation.prompt;
|
|
91
|
-
const iterator = runtime.run({
|
|
92
|
-
type: "agent:input",
|
|
93
|
-
data: { content },
|
|
94
|
-
}, { runId, state });
|
|
95
|
-
try {
|
|
96
|
-
console.log(`[automations] Running "${automation.name}" (${automation.id}) at ${scheduledAt.toISOString()}`);
|
|
97
|
-
for await (const chunk of iterator) {
|
|
98
|
-
await logEvent(sessionId, runId, chunk);
|
|
99
|
-
}
|
|
100
|
-
console.log(`[automations] Completed "${automation.name}" (${automation.id})`);
|
|
101
|
-
}
|
|
102
|
-
catch (error) {
|
|
103
|
-
console.error(`[automations] Run failed for "${automation.name}" (${automation.id})`, error);
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
finally {
|
|
107
|
-
await saveSession(sessionId, state);
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
const stopAutomationWorker = startAutomationWorker({
|
|
111
|
-
listAutomations,
|
|
112
|
-
runAutomation,
|
|
113
|
-
});
|
|
114
|
-
const cleanupBackground = async () => {
|
|
115
|
-
stopAutomationWorker();
|
|
116
|
-
await cleanupWatcher();
|
|
117
|
-
};
|
|
118
|
-
process.once("SIGINT", () => {
|
|
119
|
-
void cleanupBackground().finally(() => process.exit(0));
|
|
120
|
-
});
|
|
121
|
-
process.once("SIGTERM", () => {
|
|
122
|
-
void cleanupBackground().finally(() => process.exit(0));
|
|
123
|
-
});
|
|
124
|
-
app.use(cors());
|
|
125
|
-
app.use(express.json({ limit: "20mb" }));
|
|
126
|
-
const fileExists = async (targetPath) => fs.access(targetPath).then(() => true).catch(() => false);
|
|
127
|
-
const toTitleCaseFromSlug = (value) => value
|
|
128
|
-
.split(/[-_]+/)
|
|
129
|
-
.filter(Boolean)
|
|
130
|
-
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
131
|
-
.join(" ") || "Agent";
|
|
132
|
-
const resolveAgentFolder = async (agentIdOrName, resolvedBaseDir) => {
|
|
133
|
-
const agentsDir = path.join(resolvedBaseDir, "agents");
|
|
134
|
-
const directFolder = path.join(agentsDir, agentIdOrName);
|
|
135
|
-
if (await fileExists(path.join(directFolder, "AGENT.md"))) {
|
|
136
|
-
return directFolder;
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
const allPlugins = await listPlugins(agentsDir);
|
|
140
|
-
const match = allPlugins.find((plugin) => plugin.type === "agent"
|
|
141
|
-
&& (path.basename(plugin.folder) === agentIdOrName || plugin.name === agentIdOrName));
|
|
142
|
-
return match?.folder ?? null;
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
const getUploadsDir = () => {
|
|
149
|
-
const cfg = loadConfig();
|
|
150
|
-
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
151
|
-
const resolvedBaseDir = resolvePath(baseDir);
|
|
152
|
-
return path.join(resolvedBaseDir, "uploads");
|
|
153
|
-
};
|
|
154
|
-
const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
155
|
-
const allowedMimeTypes = new Set([
|
|
156
|
-
"image/png",
|
|
157
|
-
"image/jpeg",
|
|
158
|
-
"image/webp",
|
|
159
|
-
"image/gif",
|
|
160
|
-
"image/svg+xml",
|
|
161
|
-
]);
|
|
162
|
-
const extensionByMimeType = {
|
|
163
|
-
"image/png": ".png",
|
|
164
|
-
"image/jpeg": ".jpg",
|
|
165
|
-
"image/webp": ".webp",
|
|
166
|
-
"image/gif": ".gif",
|
|
167
|
-
"image/svg+xml": ".svg",
|
|
168
|
-
};
|
|
169
|
-
// Return available models to the client.
|
|
170
|
-
// It prefers fresh provider APIs and falls back to bundled defaults.
|
|
171
|
-
app.get("/api/models", async (_req, res) => {
|
|
172
|
-
try {
|
|
173
|
-
const models = await getModelCatalog();
|
|
174
|
-
res.json(models);
|
|
175
|
-
}
|
|
176
|
-
catch (err) {
|
|
177
|
-
console.error("Failed to load models:", err);
|
|
178
|
-
res.json([]);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
app.get("/api/version", async (_req, res) => {
|
|
182
|
-
try {
|
|
183
|
-
const status = await getVersionStatus();
|
|
184
|
-
res.json(status);
|
|
185
|
-
}
|
|
186
|
-
catch (err) {
|
|
187
|
-
console.error("Failed to check version:", err);
|
|
188
|
-
res.status(500).json({ error: "Failed to check version" });
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
app.post("/api/models/preview", async (req, res) => {
|
|
192
|
-
const { provider, apiKey } = req.body;
|
|
193
|
-
if (provider !== "openai" && provider !== "anthropic") {
|
|
194
|
-
return res.status(400).json({ error: "Invalid provider" });
|
|
195
|
-
}
|
|
196
|
-
if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
|
|
197
|
-
return res.status(400).json({ error: "API key is required" });
|
|
198
|
-
}
|
|
199
|
-
try {
|
|
200
|
-
const models = await fetchProviderModels(provider, apiKey.trim());
|
|
201
|
-
res.json(models);
|
|
202
|
-
}
|
|
203
|
-
catch (err) {
|
|
204
|
-
console.error("Failed to preview models:", err);
|
|
205
|
-
res.status(502).json({ error: "Failed to fetch models from provider" });
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
app.get("/", async (_req, res) => {
|
|
209
|
-
res.json({
|
|
210
|
-
message: "OpenBot API server",
|
|
211
|
-
version: "2.0",
|
|
212
|
-
endpoints: {
|
|
213
|
-
chat: "POST /api/chat",
|
|
214
|
-
config: "GET|POST /api/config",
|
|
215
|
-
sessions: "GET /api/sessions",
|
|
216
|
-
agents: "GET /api/agents",
|
|
217
|
-
prompts: "GET /api/prompts",
|
|
218
|
-
version: "GET /api/version",
|
|
219
|
-
},
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
// ─── REST API ───────────────────────────────────────────────────
|
|
223
|
-
app.get("/api/prompts", async (_req, res) => {
|
|
224
|
-
res.json([
|
|
225
|
-
{ label: "Who are you?", icon: "user" },
|
|
226
|
-
{ label: "Who am I?", icon: "help-circle" },
|
|
227
|
-
{ label: "How can you help me?", icon: "sparkles" },
|
|
228
|
-
{ label: "What is the weather in Tokyo?", icon: "sun" },
|
|
229
|
-
]);
|
|
230
|
-
});
|
|
231
|
-
app.get("/api/automations", async (_req, res) => {
|
|
232
|
-
const items = await listAutomations();
|
|
233
|
-
res.json(items);
|
|
234
|
-
});
|
|
235
|
-
app.post("/api/automations", async (req, res) => {
|
|
236
|
-
const { name, prompt, cron, targetType, agentName } = req.body;
|
|
237
|
-
const normalizedTargetType = targetType === "agent" ? "agent" : "orchestrator";
|
|
238
|
-
const normalizedAgentName = typeof agentName === "string" ? agentName.trim() : "";
|
|
239
|
-
if (typeof name !== "string" ||
|
|
240
|
-
typeof prompt !== "string" ||
|
|
241
|
-
typeof cron !== "string" ||
|
|
242
|
-
!name.trim() ||
|
|
243
|
-
!prompt.trim() ||
|
|
244
|
-
!cron.trim() ||
|
|
245
|
-
(normalizedTargetType === "agent" && !normalizedAgentName)) {
|
|
246
|
-
return res.status(400).json({ error: "Invalid automation payload" });
|
|
247
|
-
}
|
|
248
|
-
const now = new Date().toISOString();
|
|
249
|
-
const next = {
|
|
250
|
-
id: `auto_${randomUUID()}`,
|
|
251
|
-
name: name.trim(),
|
|
252
|
-
prompt: prompt.trim(),
|
|
253
|
-
cron: cron.trim(),
|
|
254
|
-
targetType: normalizedTargetType,
|
|
255
|
-
agentName: normalizedTargetType === "agent" ? normalizedAgentName : undefined,
|
|
256
|
-
enabled: true,
|
|
257
|
-
createdAt: now,
|
|
258
|
-
updatedAt: now,
|
|
259
|
-
};
|
|
260
|
-
const current = await listAutomations();
|
|
261
|
-
await saveAutomations([next, ...current]);
|
|
262
|
-
res.status(201).json(next);
|
|
263
|
-
});
|
|
264
|
-
app.put("/api/automations/:id", async (req, res) => {
|
|
265
|
-
const { id } = req.params;
|
|
266
|
-
const { name, prompt, cron, enabled, targetType, agentName } = req.body;
|
|
267
|
-
const current = await listAutomations();
|
|
268
|
-
const index = current.findIndex((item) => item.id === id);
|
|
269
|
-
if (index < 0) {
|
|
270
|
-
return res.status(404).json({ error: "Automation not found" });
|
|
271
|
-
}
|
|
272
|
-
const existing = current[index];
|
|
273
|
-
const nextTargetType = targetType === "agent"
|
|
274
|
-
? "agent"
|
|
275
|
-
: targetType === "orchestrator"
|
|
276
|
-
? "orchestrator"
|
|
277
|
-
: existing.targetType;
|
|
278
|
-
const nextAgentName = typeof agentName === "string"
|
|
279
|
-
? agentName.trim()
|
|
280
|
-
: (existing.agentName ?? "");
|
|
281
|
-
if (nextTargetType === "agent" && !nextAgentName) {
|
|
282
|
-
return res.status(400).json({ error: "agentName is required when targetType is agent" });
|
|
283
|
-
}
|
|
284
|
-
const updated = {
|
|
285
|
-
...existing,
|
|
286
|
-
name: typeof name === "string" ? name.trim() || existing.name : existing.name,
|
|
287
|
-
prompt: typeof prompt === "string" ? prompt.trim() || existing.prompt : existing.prompt,
|
|
288
|
-
cron: typeof cron === "string" ? cron.trim() || existing.cron : existing.cron,
|
|
289
|
-
targetType: nextTargetType,
|
|
290
|
-
agentName: nextTargetType === "agent" ? nextAgentName : undefined,
|
|
291
|
-
enabled: typeof enabled === "boolean" ? enabled : existing.enabled,
|
|
292
|
-
updatedAt: new Date().toISOString(),
|
|
293
|
-
};
|
|
294
|
-
current[index] = updated;
|
|
295
|
-
await saveAutomations(current);
|
|
296
|
-
res.json(updated);
|
|
297
|
-
});
|
|
298
|
-
app.delete("/api/automations/:id", async (req, res) => {
|
|
299
|
-
const { id } = req.params;
|
|
300
|
-
const current = await listAutomations();
|
|
301
|
-
const next = current.filter((item) => item.id !== id);
|
|
302
|
-
if (next.length === current.length) {
|
|
303
|
-
return res.status(404).json({ error: "Automation not found" });
|
|
304
|
-
}
|
|
305
|
-
await saveAutomations(next);
|
|
306
|
-
res.json({ success: true });
|
|
307
|
-
});
|
|
308
|
-
app.post("/api/uploads/image", async (req, res) => {
|
|
309
|
-
const { name, mimeType, dataBase64 } = req.body;
|
|
310
|
-
if (!mimeType || !allowedMimeTypes.has(mimeType)) {
|
|
311
|
-
return res.status(400).json({ error: "Unsupported image mime type" });
|
|
312
|
-
}
|
|
313
|
-
if (!dataBase64 || typeof dataBase64 !== "string") {
|
|
314
|
-
return res.status(400).json({ error: "Image payload is required" });
|
|
315
|
-
}
|
|
316
|
-
const bytes = Buffer.from(dataBase64, "base64");
|
|
317
|
-
if (!bytes.length) {
|
|
318
|
-
return res.status(400).json({ error: "Invalid image payload" });
|
|
319
|
-
}
|
|
320
|
-
if (bytes.length > MAX_IMAGE_BYTES) {
|
|
321
|
-
return res.status(413).json({ error: "Image too large (max 8MB)" });
|
|
322
|
-
}
|
|
323
|
-
try {
|
|
324
|
-
const ext = extensionByMimeType[mimeType] ?? ".bin";
|
|
325
|
-
const now = new Date();
|
|
326
|
-
const y = now.getFullYear().toString();
|
|
327
|
-
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
328
|
-
const datePath = path.join(y, m);
|
|
329
|
-
const fileName = `${Date.now()}-${randomUUID()}${ext}`;
|
|
330
|
-
const id = path.posix.join(y, m, fileName);
|
|
331
|
-
const uploadsDir = getUploadsDir();
|
|
332
|
-
const datedDir = path.join(uploadsDir, datePath);
|
|
333
|
-
await fs.mkdir(datedDir, { recursive: true });
|
|
334
|
-
await fs.writeFile(path.join(datedDir, fileName), bytes);
|
|
335
|
-
const origin = `${req.protocol}://${req.get("host")}`;
|
|
336
|
-
const encodedId = id.split("/").map(encodeURIComponent).join("/");
|
|
337
|
-
res.json({
|
|
338
|
-
id,
|
|
339
|
-
name: typeof name === "string" && name.trim() ? name.trim() : `image${ext}`,
|
|
340
|
-
mimeType,
|
|
341
|
-
size: bytes.length,
|
|
342
|
-
url: `${origin}/api/uploads/${encodedId}`,
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
catch (error) {
|
|
346
|
-
console.error("Image upload failed:", error);
|
|
347
|
-
res.status(500).json({ error: "Failed to store image" });
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
app.get("/api/uploads/*", async (req, res) => {
|
|
351
|
-
const rawPath = req.params[0];
|
|
352
|
-
if (!rawPath || rawPath.includes("\\")) {
|
|
353
|
-
return res.status(400).send("Invalid upload id");
|
|
354
|
-
}
|
|
355
|
-
const normalized = path.posix.normalize(rawPath);
|
|
356
|
-
if (normalized.startsWith("../") || normalized === "..") {
|
|
357
|
-
return res.status(400).send("Invalid upload id");
|
|
358
|
-
}
|
|
359
|
-
const uploadsDir = getUploadsDir();
|
|
360
|
-
const filePath = path.join(uploadsDir, normalized);
|
|
361
|
-
try {
|
|
362
|
-
await fs.access(filePath);
|
|
363
|
-
res.sendFile(filePath);
|
|
364
|
-
}
|
|
365
|
-
catch {
|
|
366
|
-
res.status(404).send("Upload not found");
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
app.get("/api/config", async (_req, res) => {
|
|
370
|
-
const cfg = loadConfig();
|
|
371
|
-
res.json({
|
|
372
|
-
configured: isConfigured(),
|
|
373
|
-
name: cfg.name || "OpenBot",
|
|
374
|
-
description: cfg.description || "The main orchestrator and system settings",
|
|
375
|
-
model: cfg.model || DEFAULT_MODEL_ID,
|
|
376
|
-
defaultModelId: DEFAULT_MODEL_ID,
|
|
377
|
-
defaultModels: DEFAULT_MODEL_BY_PROVIDER,
|
|
378
|
-
hasOpenAIKey: !!cfg.openaiApiKey,
|
|
379
|
-
hasAnthropicKey: !!cfg.anthropicApiKey,
|
|
380
|
-
});
|
|
381
|
-
});
|
|
382
|
-
app.post("/api/config", async (req, res) => {
|
|
383
|
-
const { openai_api_key, anthropic_api_key, model, name, description, image } = req.body;
|
|
384
|
-
const updates = {};
|
|
385
|
-
if (name)
|
|
386
|
-
updates.name = name.trim();
|
|
387
|
-
if (description)
|
|
388
|
-
updates.description = description.trim();
|
|
389
|
-
if (model)
|
|
390
|
-
updates.model = model.trim();
|
|
391
|
-
if (image !== undefined)
|
|
392
|
-
updates.image = image.trim();
|
|
393
|
-
if (openai_api_key && openai_api_key !== "••••••••••••••••")
|
|
394
|
-
updates.openaiApiKey = openai_api_key.trim();
|
|
395
|
-
if (anthropic_api_key && anthropic_api_key !== "••••••••••••••••")
|
|
396
|
-
updates.anthropicApiKey = anthropic_api_key.trim();
|
|
397
|
-
if (Object.keys(updates).length > 0) {
|
|
398
|
-
saveConfig(updates);
|
|
399
|
-
scheduleReload();
|
|
400
|
-
}
|
|
401
|
-
res.json({ success: true });
|
|
402
|
-
});
|
|
403
|
-
app.get("/api/sessions", async (_req, res) => {
|
|
404
|
-
const sessions = await listSessions();
|
|
405
|
-
res.json(sessions);
|
|
406
|
-
});
|
|
407
|
-
app.get("/api/sessions/:id/events", async (req, res) => {
|
|
408
|
-
const events = await loadEvents(req.params.id);
|
|
409
|
-
res.json(events);
|
|
410
|
-
});
|
|
411
|
-
app.get("/api/agents", async (_req, res) => {
|
|
412
|
-
const cfg = loadConfig();
|
|
413
|
-
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
414
|
-
const resolvedBaseDir = resolvePath(baseDir);
|
|
415
|
-
const agentsDir = path.join(resolvedBaseDir, "agents");
|
|
416
|
-
const defaultName = cfg.name || "OpenBot";
|
|
417
|
-
const defaultDescription = cfg.description || "The main orchestrator and system settings";
|
|
418
|
-
const agents = [
|
|
419
|
-
{
|
|
420
|
-
id: "default",
|
|
421
|
-
name: defaultName,
|
|
422
|
-
description: defaultDescription,
|
|
423
|
-
folder: resolvedBaseDir,
|
|
424
|
-
isDefault: true,
|
|
425
|
-
hasAgentMd: true,
|
|
426
|
-
image: cfg.image,
|
|
427
|
-
},
|
|
428
|
-
];
|
|
429
|
-
try {
|
|
430
|
-
const allPlugins = await listPlugins(agentsDir);
|
|
431
|
-
const agentPlugins = allPlugins.filter(p => p.type === "agent");
|
|
432
|
-
agents.push(...agentPlugins.map((plugin) => {
|
|
433
|
-
const id = path.basename(plugin.folder);
|
|
434
|
-
const hasUnnamedDisplayName = /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(plugin.name);
|
|
435
|
-
return {
|
|
436
|
-
...plugin,
|
|
437
|
-
id,
|
|
438
|
-
name: hasUnnamedDisplayName ? toTitleCaseFromSlug(id) : plugin.name,
|
|
439
|
-
};
|
|
440
|
-
}));
|
|
441
|
-
}
|
|
442
|
-
catch {
|
|
443
|
-
// ignore
|
|
444
|
-
}
|
|
445
|
-
res.json(agents);
|
|
446
|
-
});
|
|
447
|
-
app.get("/api/plugins", async (_req, res) => {
|
|
448
|
-
const cfg = loadConfig();
|
|
449
|
-
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
450
|
-
const resolvedBaseDir = resolvePath(baseDir);
|
|
451
|
-
const pluginsDir = path.join(resolvedBaseDir, "plugins");
|
|
452
|
-
try {
|
|
453
|
-
const allPlugins = await listPlugins(pluginsDir);
|
|
454
|
-
const toolPlugins = allPlugins.filter((plugin) => plugin.type === "tool");
|
|
455
|
-
res.json(toolPlugins.map((plugin) => {
|
|
456
|
-
const id = path.basename(plugin.folder);
|
|
457
|
-
const hasUnnamedDisplayName = /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(plugin.name);
|
|
458
|
-
return {
|
|
459
|
-
...plugin,
|
|
460
|
-
id,
|
|
461
|
-
name: hasUnnamedDisplayName ? toTitleCaseFromSlug(id) : plugin.name,
|
|
462
|
-
};
|
|
463
|
-
}));
|
|
464
|
-
}
|
|
465
|
-
catch (error) {
|
|
466
|
-
console.error(error);
|
|
467
|
-
res.status(500).json({ error: "Failed to list plugins" });
|
|
468
|
-
}
|
|
469
|
-
});
|
|
470
|
-
app.get("/api/registry/plugins", async (_req, res) => {
|
|
471
|
-
try {
|
|
472
|
-
const tools = runtime.registry.getTools();
|
|
473
|
-
res.json(tools.map((t) => ({
|
|
474
|
-
name: t.name,
|
|
475
|
-
description: t.description,
|
|
476
|
-
isBuiltIn: !!t.isBuiltIn,
|
|
477
|
-
})));
|
|
478
|
-
}
|
|
479
|
-
catch (error) {
|
|
480
|
-
console.error(error);
|
|
481
|
-
res.status(500).json({ error: "Failed to list registry plugins" });
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
app.get("/api/marketplace/agents", async (_req, res) => {
|
|
485
|
-
try {
|
|
486
|
-
const registry = await getMarketplaceRegistry();
|
|
487
|
-
res.json(registry.agents);
|
|
488
|
-
}
|
|
489
|
-
catch (error) {
|
|
490
|
-
console.error(error);
|
|
491
|
-
res.status(500).json({ error: "Failed to load marketplace agents" });
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
app.get("/api/marketplace/plugins", async (_req, res) => {
|
|
495
|
-
try {
|
|
496
|
-
const registry = await getMarketplaceRegistry();
|
|
497
|
-
res.json(registry.plugins);
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
console.error(error);
|
|
501
|
-
res.status(500).json({ error: "Failed to load marketplace plugins" });
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
app.post("/api/marketplace/install-agent", async (req, res) => {
|
|
505
|
-
const { id } = req.body;
|
|
506
|
-
if (typeof id !== "string" || !id.trim()) {
|
|
507
|
-
return res.status(400).json({ error: "Marketplace agent id is required" });
|
|
508
|
-
}
|
|
509
|
-
try {
|
|
510
|
-
const result = await installMarketplaceAgent(id.trim());
|
|
511
|
-
scheduleReload();
|
|
512
|
-
res.json({ success: true, installedName: result.installedName, item: result.agent });
|
|
513
|
-
}
|
|
514
|
-
catch (error) {
|
|
515
|
-
console.error(error);
|
|
516
|
-
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to install agent" });
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
app.post("/api/marketplace/install-plugin", async (req, res) => {
|
|
520
|
-
const { id } = req.body;
|
|
521
|
-
if (typeof id !== "string" || !id.trim()) {
|
|
522
|
-
return res.status(400).json({ error: "Marketplace plugin id is required" });
|
|
523
|
-
}
|
|
524
|
-
try {
|
|
525
|
-
const result = await installMarketplacePlugin(id.trim());
|
|
526
|
-
scheduleReload();
|
|
527
|
-
res.json({ success: true, installedName: result.installedName, item: result.plugin });
|
|
528
|
-
}
|
|
529
|
-
catch (error) {
|
|
530
|
-
console.error(error);
|
|
531
|
-
res.status(500).json({ error: error instanceof Error ? error.message : "Failed to install plugin" });
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
app.get("/api/agents/:agentId/md", async (req, res) => {
|
|
535
|
-
const { agentId } = req.params;
|
|
536
|
-
const cfg = loadConfig();
|
|
537
|
-
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
538
|
-
const resolvedBaseDir = resolvePath(baseDir);
|
|
539
|
-
const defaultName = cfg.name || "OpenBot";
|
|
540
|
-
let mdPath;
|
|
541
|
-
if (agentId === defaultName || agentId === "default") {
|
|
542
|
-
mdPath = path.join(resolvedBaseDir, "AGENT.md");
|
|
543
|
-
}
|
|
544
|
-
else {
|
|
545
|
-
const pluginFolder = await resolveAgentFolder(agentId, resolvedBaseDir);
|
|
546
|
-
if (!pluginFolder) {
|
|
547
|
-
return res.status(404).send("");
|
|
548
|
-
}
|
|
549
|
-
mdPath = path.join(pluginFolder, "AGENT.md");
|
|
550
|
-
}
|
|
551
|
-
try {
|
|
552
|
-
const content = await fs.readFile(mdPath, "utf-8");
|
|
553
|
-
const { content: body } = matter(content);
|
|
554
|
-
res.send(body.trim());
|
|
555
|
-
}
|
|
556
|
-
catch {
|
|
557
|
-
res.status(404).send("");
|
|
558
|
-
}
|
|
559
|
-
});
|
|
560
|
-
app.put("/api/agents/:agentId/md", async (req, res) => {
|
|
561
|
-
const { agentId } = req.params;
|
|
562
|
-
const { md } = req.body;
|
|
563
|
-
const cfg = loadConfig();
|
|
564
|
-
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
565
|
-
const resolvedBaseDir = resolvePath(baseDir);
|
|
566
|
-
const defaultName = cfg.name || "OpenBot";
|
|
567
|
-
let mdPath;
|
|
568
|
-
let pluginDir;
|
|
569
|
-
if (agentId === defaultName || agentId === "default") {
|
|
570
|
-
pluginDir = resolvedBaseDir;
|
|
571
|
-
mdPath = path.join(resolvedBaseDir, "AGENT.md");
|
|
572
|
-
}
|
|
573
|
-
else {
|
|
574
|
-
const pluginFolder = await resolveAgentFolder(agentId, resolvedBaseDir);
|
|
575
|
-
if (!pluginFolder) {
|
|
576
|
-
return res.status(404).json({ error: "Agent not found" });
|
|
577
|
-
}
|
|
578
|
-
pluginDir = pluginFolder;
|
|
579
|
-
mdPath = path.join(pluginDir, "AGENT.md");
|
|
580
|
-
}
|
|
581
|
-
try {
|
|
582
|
-
await fs.mkdir(pluginDir, { recursive: true });
|
|
583
|
-
let frontmatter = {};
|
|
584
|
-
try {
|
|
585
|
-
const currentContent = await fs.readFile(mdPath, "utf-8");
|
|
586
|
-
const parsed = matter(currentContent);
|
|
587
|
-
frontmatter = parsed.data || {};
|
|
588
|
-
}
|
|
589
|
-
catch {
|
|
590
|
-
// No current AGENT.md, starting with empty frontmatter or defaults
|
|
591
|
-
}
|
|
592
|
-
const consolidated = matter.stringify(md, frontmatter);
|
|
593
|
-
await fs.writeFile(mdPath, consolidated, "utf-8");
|
|
594
|
-
scheduleReload();
|
|
595
|
-
res.json({ success: true });
|
|
596
|
-
}
|
|
597
|
-
catch (err) {
|
|
598
|
-
console.error(err);
|
|
599
|
-
res.status(500).json({ error: "Failed to write AGENT.md" });
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
app.get("/api/agents/:agentId/config", async (req, res) => {
|
|
603
|
-
const { agentId } = req.params;
|
|
604
|
-
const cfg = loadConfig();
|
|
605
|
-
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
606
|
-
const resolvedBaseDir = resolvePath(baseDir);
|
|
607
|
-
const defaultName = cfg.name || "OpenBot";
|
|
608
|
-
let mdPath;
|
|
609
|
-
if (agentId === defaultName || agentId === "default") {
|
|
610
|
-
mdPath = path.join(resolvedBaseDir, "AGENT.md");
|
|
611
|
-
}
|
|
612
|
-
else {
|
|
613
|
-
const pluginFolder = await resolveAgentFolder(agentId, resolvedBaseDir);
|
|
614
|
-
if (!pluginFolder) {
|
|
615
|
-
return res.status(404).json({ error: "Agent not found or invalid format" });
|
|
616
|
-
}
|
|
617
|
-
mdPath = path.join(pluginFolder, "AGENT.md");
|
|
618
|
-
}
|
|
619
|
-
try {
|
|
620
|
-
const content = await fs.readFile(mdPath, "utf-8");
|
|
621
|
-
const { data: parsed, content: body } = matter(content);
|
|
622
|
-
if (!parsed || typeof parsed !== "object") {
|
|
623
|
-
return res.status(400).json({ error: "Invalid AGENT.md frontmatter" });
|
|
624
|
-
}
|
|
625
|
-
res.json({
|
|
626
|
-
name: typeof parsed.name === "string" ? parsed.name : (agentId === defaultName || agentId === "default" ? defaultName : ""),
|
|
627
|
-
description: typeof parsed.description === "string" ? parsed.description : (agentId === defaultName || agentId === "default" ? cfg.description || "" : ""),
|
|
628
|
-
model: typeof parsed.model === "string" ? parsed.model : (agentId === defaultName || agentId === "default" ? cfg.model : undefined),
|
|
629
|
-
plugins: Array.isArray(parsed.plugins) ? parsed.plugins : [],
|
|
630
|
-
subscribe: Array.isArray(parsed.subscribe)
|
|
631
|
-
? parsed.subscribe.filter((item) => typeof item === "string")
|
|
632
|
-
: [],
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
catch {
|
|
636
|
-
if (agentId === defaultName || agentId === "default") {
|
|
637
|
-
// Fallback for default agent if AGENT.md is missing or unreadable
|
|
638
|
-
return res.json({
|
|
639
|
-
name: defaultName,
|
|
640
|
-
description: cfg.description || "",
|
|
641
|
-
model: cfg.model,
|
|
642
|
-
plugins: [],
|
|
643
|
-
systemPrompt: "",
|
|
644
|
-
subscribe: [],
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
res.status(404).json({ error: "Agent not found or invalid format" });
|
|
648
|
-
}
|
|
649
|
-
});
|
|
650
|
-
app.put("/api/agents/:agentId/config", async (req, res) => {
|
|
651
|
-
const { agentId } = req.params;
|
|
652
|
-
const body = req.body;
|
|
653
|
-
if (typeof body.name !== "string" ||
|
|
654
|
-
typeof body.description !== "string" ||
|
|
655
|
-
!Array.isArray(body.plugins)) {
|
|
656
|
-
return res.status(400).json({ error: "Invalid agent config payload" });
|
|
657
|
-
}
|
|
658
|
-
const normalizedPlugins = [];
|
|
659
|
-
for (const plugin of body.plugins) {
|
|
660
|
-
if (typeof plugin === "string") {
|
|
661
|
-
const normalized = plugin.trim();
|
|
662
|
-
if (normalized)
|
|
663
|
-
normalizedPlugins.push(normalized);
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
if (!plugin || typeof plugin !== "object" || typeof plugin.name !== "string") {
|
|
667
|
-
continue;
|
|
668
|
-
}
|
|
669
|
-
const normalizedName = plugin.name.trim();
|
|
670
|
-
if (!normalizedName)
|
|
671
|
-
continue;
|
|
672
|
-
if (typeof plugin.config === "undefined") {
|
|
673
|
-
normalizedPlugins.push({ name: normalizedName });
|
|
674
|
-
}
|
|
675
|
-
else {
|
|
676
|
-
normalizedPlugins.push({ name: normalizedName, config: plugin.config });
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
const normalizedName = body.name.trim();
|
|
680
|
-
const normalizedDescription = body.description.trim();
|
|
681
|
-
if (!normalizedName || !normalizedDescription) {
|
|
682
|
-
return res.status(400).json({ error: "name and description are required" });
|
|
683
|
-
}
|
|
684
|
-
const cfg = loadConfig();
|
|
685
|
-
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
686
|
-
const resolvedBaseDir = resolvePath(baseDir);
|
|
687
|
-
const defaultName = cfg.name || "OpenBot";
|
|
688
|
-
let pluginDir;
|
|
689
|
-
let mdPath;
|
|
690
|
-
if (agentId === defaultName || agentId === "default") {
|
|
691
|
-
pluginDir = resolvedBaseDir;
|
|
692
|
-
mdPath = path.join(resolvedBaseDir, "AGENT.md");
|
|
693
|
-
}
|
|
694
|
-
else {
|
|
695
|
-
const pluginFolder = await resolveAgentFolder(agentId, resolvedBaseDir);
|
|
696
|
-
if (!pluginFolder) {
|
|
697
|
-
return res.status(404).json({ error: "Agent not found" });
|
|
698
|
-
}
|
|
699
|
-
pluginDir = pluginFolder;
|
|
700
|
-
mdPath = path.join(pluginDir, "AGENT.md");
|
|
701
|
-
}
|
|
702
|
-
// Read current content to preserve the body (instructions)
|
|
703
|
-
let currentBody = "";
|
|
704
|
-
try {
|
|
705
|
-
const currentContent = await fs.readFile(mdPath, "utf-8");
|
|
706
|
-
const parsed = matter(currentContent);
|
|
707
|
-
currentBody = parsed.content;
|
|
708
|
-
}
|
|
709
|
-
catch {
|
|
710
|
-
// No current AGENT.md, starting with empty body or defaults
|
|
711
|
-
}
|
|
712
|
-
// Prepare frontmatter
|
|
713
|
-
const frontmatter = {
|
|
714
|
-
name: normalizedName,
|
|
715
|
-
description: normalizedDescription,
|
|
716
|
-
plugins: normalizedPlugins,
|
|
717
|
-
};
|
|
718
|
-
if (typeof body.model === "string" && body.model.trim()) {
|
|
719
|
-
frontmatter.model = body.model.trim();
|
|
720
|
-
}
|
|
721
|
-
if (Array.isArray(body.subscribe) && body.subscribe.length > 0) {
|
|
722
|
-
const normalizedSubscribe = body.subscribe
|
|
723
|
-
.filter((item) => typeof item === "string")
|
|
724
|
-
.map((item) => item.trim())
|
|
725
|
-
.filter(Boolean);
|
|
726
|
-
if (normalizedSubscribe.length > 0) {
|
|
727
|
-
frontmatter.subscribe = normalizedSubscribe;
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
try {
|
|
731
|
-
await fs.mkdir(pluginDir, { recursive: true });
|
|
732
|
-
const consolidated = matter.stringify(currentBody, frontmatter);
|
|
733
|
-
await fs.writeFile(mdPath, consolidated, "utf-8");
|
|
734
|
-
if (agentId === defaultName || agentId === "default") {
|
|
735
|
-
// For the default agent, sync changes back to config.json
|
|
736
|
-
saveConfig({
|
|
737
|
-
name: normalizedName,
|
|
738
|
-
description: normalizedDescription,
|
|
739
|
-
model: (typeof body.model === "string" && body.model.trim()) ? body.model.trim() : undefined,
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
scheduleReload();
|
|
743
|
-
res.json({ success: true });
|
|
744
|
-
}
|
|
745
|
-
catch (err) {
|
|
746
|
-
console.error(err);
|
|
747
|
-
res.status(500).json({ error: "Failed to write AGENT.md" });
|
|
748
|
-
}
|
|
749
|
-
});
|
|
750
|
-
app.post("/api/actions/reload", async (_req, res) => {
|
|
751
|
-
scheduleReload();
|
|
752
|
-
res.json({ success: true, message: "Reload scheduled" });
|
|
753
|
-
});
|
|
754
|
-
app.post("/api/actions/open-folder", async (req, res) => {
|
|
755
|
-
const { folder } = req.body;
|
|
756
|
-
if (folder) {
|
|
757
|
-
const command = os.platform() === "win32"
|
|
758
|
-
? `explorer "${folder}"`
|
|
759
|
-
: os.platform() === "darwin"
|
|
760
|
-
? `open "${folder}"`
|
|
761
|
-
: `xdg-open "${folder}"`;
|
|
762
|
-
exec(command, (error) => {
|
|
763
|
-
if (error)
|
|
764
|
-
console.error(`Failed to open folder: ${error.message}`);
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
res.json({ success: true });
|
|
768
|
-
});
|
|
769
|
-
app.get("/api/agents/:name/avatar", async (req, res) => {
|
|
770
|
-
const { name } = req.params;
|
|
771
|
-
const cfg = loadConfig();
|
|
772
|
-
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
773
|
-
const resolvedBaseDir = resolvePath(baseDir);
|
|
774
|
-
const defaultName = cfg.name || "OpenBot";
|
|
775
|
-
// 1. Resolve agent folder
|
|
776
|
-
let agentFolder = null;
|
|
777
|
-
if (name === defaultName || name === "default") {
|
|
778
|
-
agentFolder = resolvedBaseDir;
|
|
779
|
-
}
|
|
780
|
-
else {
|
|
781
|
-
agentFolder = await resolveAgentFolder(name, resolvedBaseDir);
|
|
782
|
-
}
|
|
783
|
-
// 2. Check for remote image in AGENT.md if folder exists
|
|
784
|
-
if (agentFolder) {
|
|
785
|
-
try {
|
|
786
|
-
const { image } = await readAgentConfig(agentFolder);
|
|
787
|
-
if (image && (image.startsWith("http://") || image.startsWith("https://"))) {
|
|
788
|
-
return res.redirect(image);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
catch {
|
|
792
|
-
// ignore
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
const extensions = [".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif"];
|
|
796
|
-
const fileNames = ["avatar", "icon", "image", "logo"];
|
|
797
|
-
const searchDirs = [
|
|
798
|
-
(name === defaultName || name === "default")
|
|
799
|
-
? path.join(resolvedBaseDir, "assets")
|
|
800
|
-
: (agentFolder ? path.join(agentFolder, "assets") : path.join(resolvedBaseDir, "agents", name, "assets")),
|
|
801
|
-
path.join(process.cwd(), "server", "src", "agents", name, "assets"),
|
|
802
|
-
path.join(process.cwd(), "server", "src", "assets", "agents", name),
|
|
803
|
-
path.join(process.cwd(), "server", "src", "agents", "assets"),
|
|
804
|
-
path.join(process.cwd(), "server", "src", "assets")
|
|
805
|
-
];
|
|
806
|
-
for (const dir of searchDirs) {
|
|
807
|
-
for (const fileName of fileNames) {
|
|
808
|
-
for (const ext of extensions) {
|
|
809
|
-
const isAgentSpecificDir = dir.includes(name) || (agentFolder && dir.includes(agentFolder));
|
|
810
|
-
const baseName = (dir.endsWith("assets") && !isAgentSpecificDir) ? name : fileName;
|
|
811
|
-
const p = path.join(dir, `${baseName}${ext}`);
|
|
812
|
-
try {
|
|
813
|
-
await fs.access(p);
|
|
814
|
-
return res.sendFile(p);
|
|
815
|
-
}
|
|
816
|
-
catch {
|
|
817
|
-
// continue
|
|
818
|
-
}
|
|
819
|
-
if (baseName === name)
|
|
820
|
-
break;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
res.status(404).send("Avatar not found");
|
|
825
|
-
});
|
|
826
|
-
// ─── Chat SSE endpoint ──────────────────────────────────────────
|
|
827
|
-
app.post("/api/chat", async (req, res) => {
|
|
828
|
-
const body = req.body;
|
|
829
|
-
if (!body.event || typeof body.event.type !== "string") {
|
|
830
|
-
return res.status(400).json({
|
|
831
|
-
error: "The request body must contain an `event` with a string `type`.",
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
res.set({
|
|
835
|
-
"Content-Type": "text/event-stream",
|
|
836
|
-
"Cache-Control": "no-cache, no-transform",
|
|
837
|
-
Connection: "keep-alive",
|
|
838
|
-
});
|
|
839
|
-
res.flushHeaders?.();
|
|
840
|
-
const sessionId = body.sessionId ?? "default";
|
|
841
|
-
const runId = body.runId ?? `run_${generateId()}`;
|
|
842
|
-
const state = (await loadSession(sessionId)) ?? {};
|
|
843
|
-
state.sessionId = sessionId;
|
|
844
|
-
if (!state.cwd)
|
|
845
|
-
state.cwd = process.cwd();
|
|
846
|
-
if (!state.workspaceRoot)
|
|
847
|
-
state.workspaceRoot = process.cwd();
|
|
848
|
-
const iterator = runtime.run(body.event, {
|
|
849
|
-
runId,
|
|
850
|
-
state,
|
|
851
|
-
});
|
|
852
|
-
const stopStreaming = () => {
|
|
853
|
-
void iterator.return?.();
|
|
854
|
-
};
|
|
855
|
-
res.on("close", stopStreaming);
|
|
856
|
-
try {
|
|
857
|
-
for await (const chunk of iterator) {
|
|
858
|
-
if (res.writableEnded)
|
|
859
|
-
break;
|
|
860
|
-
await logEvent(sessionId, runId, chunk);
|
|
861
|
-
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
862
|
-
}
|
|
863
|
-
await saveSession(sessionId, state);
|
|
864
|
-
}
|
|
865
|
-
catch (error) {
|
|
866
|
-
console.error("Melony stream error:", error);
|
|
867
|
-
if (!res.writableEnded) {
|
|
868
|
-
res.write(`event: error\ndata: ${JSON.stringify({
|
|
869
|
-
message: error instanceof Error ? error.message : String(error),
|
|
870
|
-
})}\n\n`);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
finally {
|
|
874
|
-
res.off("close", stopStreaming);
|
|
875
|
-
if (!res.writableEnded) {
|
|
876
|
-
res.write("event: done\ndata: {}\n\n");
|
|
877
|
-
res.end();
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
});
|
|
881
|
-
app.listen(PORT, () => {
|
|
882
|
-
console.log(`OpenBot server listening at http://localhost:${PORT}`);
|
|
883
|
-
console.log(` - Chat endpoint: POST /api/chat`);
|
|
884
|
-
console.log(` - REST endpoints: /api/config, /api/sessions, /api/agents`);
|
|
885
|
-
if (options.openaiApiKey)
|
|
886
|
-
console.log(" - Using OpenAI API Key from CLI");
|
|
887
|
-
if (options.anthropicApiKey)
|
|
888
|
-
console.log(" - Using Anthropic API Key from CLI");
|
|
889
|
-
});
|
|
890
|
-
}
|