openbot 0.1.28 → 0.2.0
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/dist/agents/agent-creator.js +35 -0
- package/dist/assets/logo.js +24 -0
- package/dist/cli.js +1 -1
- package/dist/config.js +4 -0
- package/dist/model-catalog.js +132 -0
- package/dist/model-defaults.js +20 -0
- package/dist/models.js +2 -1
- package/dist/open-bot.js +94 -127
- package/dist/orchestrator/direct-invocation.js +13 -0
- package/dist/orchestrator/events.js +36 -0
- package/dist/orchestrator/state.js +54 -0
- package/dist/orchestrator.js +226 -0
- package/dist/plugins/approval/index.js +10 -26
- package/dist/plugins/brain/index.js +2 -1
- package/dist/plugins/brain/prompt.js +20 -33
- package/dist/plugins/file-system/index.js +2 -1
- package/dist/plugins/llm/index.js +81 -18
- package/dist/plugins/shell/index.js +2 -1
- package/dist/plugins/skills/index.js +4 -2
- package/dist/registry/index.js +1 -0
- package/dist/registry/ts-agent-loader.js +82 -0
- package/dist/registry/yaml-agent-loader.js +4 -2
- package/dist/server.js +282 -41
- package/dist/ui/widgets/action-list.js +9 -0
- package/dist/ui/widgets/approval-card.js +15 -0
- package/dist/ui/widgets/code-snippet.js +2 -0
- package/dist/ui/widgets/data-block.js +5 -0
- package/dist/ui/widgets/data-table.js +8 -0
- package/dist/ui/widgets/empty-state.js +7 -0
- package/dist/ui/widgets/index.js +19 -0
- package/dist/ui/widgets/key-value.js +12 -0
- package/dist/ui/widgets/progress-step.js +5 -0
- package/dist/ui/widgets/resource-card.js +10 -0
- package/dist/ui/widgets/status.js +5 -0
- package/package.json +2 -2
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { ensurePluginReady } from "./plugin-loader.js";
|
|
5
|
+
/**
|
|
6
|
+
* Discover and load TS-defined agents from a directory.
|
|
7
|
+
*
|
|
8
|
+
* Scans each subdirectory for a package.json and an index file.
|
|
9
|
+
*
|
|
10
|
+
* @param agentsDir Absolute path to the agents directory (e.g. ~/.openbot/agents)
|
|
11
|
+
* @param defaultModel Language model to use for agent LLMs
|
|
12
|
+
* @param options Optional API keys for creating specific models
|
|
13
|
+
* @returns Array of discovered agent entries ready for registration
|
|
14
|
+
*/
|
|
15
|
+
export async function discoverTsAgents(agentsDir, defaultModel, options) {
|
|
16
|
+
const agents = [];
|
|
17
|
+
try {
|
|
18
|
+
const entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (!entry.isDirectory())
|
|
21
|
+
continue;
|
|
22
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
23
|
+
continue;
|
|
24
|
+
const agentDir = path.join(agentsDir, entry.name);
|
|
25
|
+
// We only consider it a TS agent if it doesn't have an agent.yaml
|
|
26
|
+
// (This avoids double-loading if someone has both for some reason)
|
|
27
|
+
const yamlPath = path.join(agentDir, "agent.yaml");
|
|
28
|
+
const hasYaml = await fs.access(yamlPath).then(() => true).catch(() => false);
|
|
29
|
+
if (hasYaml)
|
|
30
|
+
continue;
|
|
31
|
+
// Check for package.json to see if it's a package
|
|
32
|
+
const pkgPath = path.join(agentDir, "package.json");
|
|
33
|
+
const hasPackageJson = await fs.access(pkgPath).then(() => true).catch(() => false);
|
|
34
|
+
if (!hasPackageJson)
|
|
35
|
+
continue;
|
|
36
|
+
try {
|
|
37
|
+
// 1. Ensure dependencies and build are ready
|
|
38
|
+
await ensurePluginReady(agentDir);
|
|
39
|
+
// 2. Find index file
|
|
40
|
+
let indexPath;
|
|
41
|
+
const possibleIndices = ["dist/index.js", "index.js", "index.ts"];
|
|
42
|
+
for (const file of possibleIndices) {
|
|
43
|
+
try {
|
|
44
|
+
const fullPath = path.join(agentDir, file);
|
|
45
|
+
await fs.access(fullPath);
|
|
46
|
+
indexPath = fullPath;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!indexPath)
|
|
54
|
+
continue;
|
|
55
|
+
// 3. Import and instantiate
|
|
56
|
+
const moduleUrl = pathToFileURL(indexPath).href;
|
|
57
|
+
const module = await import(moduleUrl);
|
|
58
|
+
// Support 'agent', 'plugin', 'default', or 'entry'
|
|
59
|
+
const definition = module.agent || module.plugin || module.default || module.entry;
|
|
60
|
+
if (definition && typeof definition.factory === "function") {
|
|
61
|
+
const name = definition.name || entry.name;
|
|
62
|
+
const description = definition.description || "TS Agent";
|
|
63
|
+
agents.push({
|
|
64
|
+
name,
|
|
65
|
+
description,
|
|
66
|
+
plugin: definition.factory({ ...options, model: defaultModel }),
|
|
67
|
+
capabilities: definition.capabilities,
|
|
68
|
+
subscribe: definition.subscribe,
|
|
69
|
+
});
|
|
70
|
+
console.log(`[agents] Loaded TS agent: ${name} — ${description}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
console.warn(`[agents] Failed to load TS agent package "${entry.name}":`, err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Agents directory doesn't exist
|
|
80
|
+
}
|
|
81
|
+
return agents;
|
|
82
|
+
}
|
|
@@ -136,8 +136,10 @@ export async function discoverYamlAgents(agentsDir, pluginRegistry, defaultModel
|
|
|
136
136
|
console.log(`[agents] Loaded: ${config.name} — ${config.description}${config.model ? ` (model: ${config.model})` : ""}`);
|
|
137
137
|
}
|
|
138
138
|
catch (err) {
|
|
139
|
-
// Invalid or missing agent.yaml — silently skip
|
|
140
|
-
|
|
139
|
+
// Invalid or missing agent.yaml — silently skip if missing, warn if invalid
|
|
140
|
+
if (err.code !== 'ENOENT') {
|
|
141
|
+
console.warn(`[agents] Error loading "${entry.name}/agent.yaml":`, err);
|
|
142
|
+
}
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
145
|
}
|
package/dist/server.js
CHANGED
|
@@ -3,55 +3,295 @@ import express from "express";
|
|
|
3
3
|
import cors from "cors";
|
|
4
4
|
import { generateId } from "melony";
|
|
5
5
|
import { createOpenBot } from "./open-bot.js";
|
|
6
|
-
import { loadConfig } from "./config.js";
|
|
7
|
-
import { loadSession, saveSession, logEvent, loadEvents } from "./session.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 { listYamlAgents } from "./registry/index.js";
|
|
9
|
+
import { exec } from "node:child_process";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
import { fetchProviderModels, getModelCatalog } from "./model-catalog.js";
|
|
15
|
+
import { DEFAULT_MODEL_BY_PROVIDER, DEFAULT_MODEL_ID } from "./model-defaults.js";
|
|
8
16
|
export async function startServer(options = {}) {
|
|
9
17
|
const config = loadConfig();
|
|
10
18
|
const PORT = Number(options.port ?? config.port ?? process.env.PORT ?? 4001);
|
|
11
19
|
const app = express();
|
|
12
|
-
|
|
13
|
-
const openBotAgent = await createOpenBot({
|
|
20
|
+
const orchestrator = await createOpenBot({
|
|
14
21
|
openaiApiKey: options.openaiApiKey,
|
|
15
22
|
anthropicApiKey: options.anthropicApiKey,
|
|
16
23
|
});
|
|
17
24
|
app.use(cors());
|
|
18
|
-
app.use(express.json());
|
|
19
|
-
|
|
25
|
+
app.use(express.json({ limit: "20mb" }));
|
|
26
|
+
const getUploadsDir = () => {
|
|
27
|
+
const cfg = loadConfig();
|
|
28
|
+
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
29
|
+
const resolvedBaseDir = resolvePath(baseDir);
|
|
30
|
+
return path.join(resolvedBaseDir, "uploads");
|
|
31
|
+
};
|
|
32
|
+
const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
33
|
+
const allowedMimeTypes = new Set([
|
|
34
|
+
"image/png",
|
|
35
|
+
"image/jpeg",
|
|
36
|
+
"image/webp",
|
|
37
|
+
"image/gif",
|
|
38
|
+
"image/svg+xml",
|
|
39
|
+
]);
|
|
40
|
+
const extensionByMimeType = {
|
|
41
|
+
"image/png": ".png",
|
|
42
|
+
"image/jpeg": ".jpg",
|
|
43
|
+
"image/webp": ".webp",
|
|
44
|
+
"image/gif": ".gif",
|
|
45
|
+
"image/svg+xml": ".svg",
|
|
46
|
+
};
|
|
47
|
+
// Return available models to the client.
|
|
48
|
+
// It prefers fresh provider APIs and falls back to bundled defaults.
|
|
49
|
+
app.get("/api/models", async (_req, res) => {
|
|
50
|
+
try {
|
|
51
|
+
const models = await getModelCatalog();
|
|
52
|
+
res.json(models);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.error("Failed to load models:", err);
|
|
56
|
+
res.json([]);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
app.post("/api/models/preview", async (req, res) => {
|
|
60
|
+
const { provider, apiKey } = req.body;
|
|
61
|
+
if (provider !== "openai" && provider !== "anthropic") {
|
|
62
|
+
return res.status(400).json({ error: "Invalid provider" });
|
|
63
|
+
}
|
|
64
|
+
if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
|
|
65
|
+
return res.status(400).json({ error: "API key is required" });
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const models = await fetchProviderModels(provider, apiKey.trim());
|
|
69
|
+
res.json(models);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
console.error("Failed to preview models:", err);
|
|
73
|
+
res.status(502).json({ error: "Failed to fetch models from provider" });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
app.get("/", async (_req, res) => {
|
|
20
77
|
res.json({
|
|
21
|
-
message: "
|
|
22
|
-
|
|
23
|
-
|
|
78
|
+
message: "OpenBot API server",
|
|
79
|
+
version: "2.0",
|
|
80
|
+
endpoints: {
|
|
81
|
+
chat: "POST /api/chat",
|
|
82
|
+
config: "GET|POST /api/config",
|
|
83
|
+
sessions: "GET /api/sessions",
|
|
84
|
+
agents: "GET /api/agents",
|
|
85
|
+
prompts: "GET /api/prompts",
|
|
86
|
+
},
|
|
24
87
|
});
|
|
25
88
|
});
|
|
26
|
-
//
|
|
27
|
-
app.get("/api/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
89
|
+
// ─── REST API ───────────────────────────────────────────────────
|
|
90
|
+
app.get("/api/prompts", async (_req, res) => {
|
|
91
|
+
res.json([
|
|
92
|
+
{ label: "Who are you?", icon: "user" },
|
|
93
|
+
{ label: "Who am I?", icon: "help-circle" },
|
|
94
|
+
{ label: "How can you help me?", icon: "sparkles" },
|
|
95
|
+
{ label: "What is the weather in Tokyo?", icon: "sun" },
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
app.post("/api/uploads/image", async (req, res) => {
|
|
99
|
+
const { name, mimeType, dataBase64 } = req.body;
|
|
100
|
+
if (!mimeType || !allowedMimeTypes.has(mimeType)) {
|
|
101
|
+
return res.status(400).json({ error: "Unsupported image mime type" });
|
|
102
|
+
}
|
|
103
|
+
if (!dataBase64 || typeof dataBase64 !== "string") {
|
|
104
|
+
return res.status(400).json({ error: "Image payload is required" });
|
|
105
|
+
}
|
|
106
|
+
const bytes = Buffer.from(dataBase64, "base64");
|
|
107
|
+
if (!bytes.length) {
|
|
108
|
+
return res.status(400).json({ error: "Invalid image payload" });
|
|
109
|
+
}
|
|
110
|
+
if (bytes.length > MAX_IMAGE_BYTES) {
|
|
111
|
+
return res.status(413).json({ error: "Image too large (max 8MB)" });
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const ext = extensionByMimeType[mimeType] ?? ".bin";
|
|
115
|
+
const now = new Date();
|
|
116
|
+
const y = now.getFullYear().toString();
|
|
117
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
118
|
+
const datePath = path.join(y, m);
|
|
119
|
+
const fileName = `${Date.now()}-${randomUUID()}${ext}`;
|
|
120
|
+
const id = path.posix.join(y, m, fileName);
|
|
121
|
+
const uploadsDir = getUploadsDir();
|
|
122
|
+
const datedDir = path.join(uploadsDir, datePath);
|
|
123
|
+
await fs.mkdir(datedDir, { recursive: true });
|
|
124
|
+
await fs.writeFile(path.join(datedDir, fileName), bytes);
|
|
125
|
+
const origin = `${req.protocol}://${req.get("host")}`;
|
|
126
|
+
const encodedId = id.split("/").map(encodeURIComponent).join("/");
|
|
127
|
+
res.json({
|
|
128
|
+
id,
|
|
129
|
+
name: typeof name === "string" && name.trim() ? name.trim() : `image${ext}`,
|
|
130
|
+
mimeType,
|
|
131
|
+
size: bytes.length,
|
|
132
|
+
url: `${origin}/api/uploads/${encodedId}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error("Image upload failed:", error);
|
|
137
|
+
res.status(500).json({ error: "Failed to store image" });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
app.get("/api/uploads/*", async (req, res) => {
|
|
141
|
+
const rawPath = req.params[0];
|
|
142
|
+
if (!rawPath || rawPath.includes("\\")) {
|
|
143
|
+
return res.status(400).send("Invalid upload id");
|
|
144
|
+
}
|
|
145
|
+
const normalized = path.posix.normalize(rawPath);
|
|
146
|
+
if (normalized.startsWith("../") || normalized === "..") {
|
|
147
|
+
return res.status(400).send("Invalid upload id");
|
|
148
|
+
}
|
|
149
|
+
const uploadsDir = getUploadsDir();
|
|
150
|
+
const filePath = path.join(uploadsDir, normalized);
|
|
151
|
+
try {
|
|
152
|
+
await fs.access(filePath);
|
|
153
|
+
res.sendFile(filePath);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
res.status(404).send("Upload not found");
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
app.get("/api/config", async (_req, res) => {
|
|
160
|
+
const cfg = loadConfig();
|
|
47
161
|
res.json({
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
162
|
+
configured: isConfigured(),
|
|
163
|
+
model: cfg.model || DEFAULT_MODEL_ID,
|
|
164
|
+
defaultModelId: DEFAULT_MODEL_ID,
|
|
165
|
+
defaultModels: DEFAULT_MODEL_BY_PROVIDER,
|
|
166
|
+
hasOpenAIKey: !!cfg.openaiApiKey,
|
|
167
|
+
hasAnthropicKey: !!cfg.anthropicApiKey,
|
|
52
168
|
});
|
|
53
169
|
});
|
|
54
|
-
|
|
170
|
+
app.post("/api/config", async (req, res) => {
|
|
171
|
+
const { openai_api_key, anthropic_api_key, model } = req.body;
|
|
172
|
+
const updates = {};
|
|
173
|
+
if (model)
|
|
174
|
+
updates.model = model.trim();
|
|
175
|
+
if (openai_api_key && openai_api_key !== "••••••••••••••••")
|
|
176
|
+
updates.openaiApiKey = openai_api_key.trim();
|
|
177
|
+
if (anthropic_api_key && anthropic_api_key !== "••••••••••••••••")
|
|
178
|
+
updates.anthropicApiKey = anthropic_api_key.trim();
|
|
179
|
+
if (Object.keys(updates).length > 0) {
|
|
180
|
+
saveConfig(updates);
|
|
181
|
+
}
|
|
182
|
+
res.json({ success: true });
|
|
183
|
+
});
|
|
184
|
+
app.get("/api/sessions", async (_req, res) => {
|
|
185
|
+
const sessions = await listSessions();
|
|
186
|
+
res.json(sessions);
|
|
187
|
+
});
|
|
188
|
+
app.get("/api/sessions/:id/events", async (req, res) => {
|
|
189
|
+
const events = await loadEvents(req.params.id);
|
|
190
|
+
res.json(events);
|
|
191
|
+
});
|
|
192
|
+
app.get("/api/agents", async (_req, res) => {
|
|
193
|
+
const cfg = loadConfig();
|
|
194
|
+
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
195
|
+
const resolvedBaseDir = resolvePath(baseDir);
|
|
196
|
+
const agentsDir = path.join(resolvedBaseDir, "agents");
|
|
197
|
+
try {
|
|
198
|
+
const agents = await listYamlAgents(agentsDir);
|
|
199
|
+
res.json(agents);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
res.json([]);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
app.get("/api/agents/:name/yaml", async (req, res) => {
|
|
206
|
+
const { name } = req.params;
|
|
207
|
+
const cfg = loadConfig();
|
|
208
|
+
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
209
|
+
const resolvedBaseDir = resolvePath(baseDir);
|
|
210
|
+
const yamlPath = path.join(resolvedBaseDir, "agents", name, "agent.yaml");
|
|
211
|
+
try {
|
|
212
|
+
const content = await fs.readFile(yamlPath, "utf-8");
|
|
213
|
+
res.send(content);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
res.status(404).send("Agent not found or has no agent.yaml");
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
app.put("/api/agents/:name/yaml", async (req, res) => {
|
|
220
|
+
const { name } = req.params;
|
|
221
|
+
const { yaml } = req.body;
|
|
222
|
+
if (!yaml || typeof yaml !== "string") {
|
|
223
|
+
return res.status(400).json({ error: "YAML content is required" });
|
|
224
|
+
}
|
|
225
|
+
const cfg = loadConfig();
|
|
226
|
+
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
227
|
+
const resolvedBaseDir = resolvePath(baseDir);
|
|
228
|
+
const agentDir = path.join(resolvedBaseDir, "agents", name);
|
|
229
|
+
const yamlPath = path.join(agentDir, "agent.yaml");
|
|
230
|
+
try {
|
|
231
|
+
await fs.mkdir(agentDir, { recursive: true });
|
|
232
|
+
await fs.writeFile(yamlPath, yaml, "utf-8");
|
|
233
|
+
// Optionally, hot-reload openBotAgent if needed here.
|
|
234
|
+
// But OpenBot runtime loads agents at startup or dynamically per request?
|
|
235
|
+
// createOpenBot builds the Melony App. Since createOpenBot is called at startup:
|
|
236
|
+
// openBotAgent = await createOpenBot(...) happens once.
|
|
237
|
+
// Restarting server is required unless we hot-reload. We can just leave it as is
|
|
238
|
+
// and instruct the user to restart, or implement a simple hot reload. Let's stick to simple file write first.
|
|
239
|
+
res.json({ success: true });
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
console.error(err);
|
|
243
|
+
res.status(500).json({ error: "Failed to write agent.yaml" });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
app.post("/api/actions/open-folder", async (req, res) => {
|
|
247
|
+
const { folder } = req.body;
|
|
248
|
+
if (folder) {
|
|
249
|
+
const command = os.platform() === "win32"
|
|
250
|
+
? `explorer "${folder}"`
|
|
251
|
+
: os.platform() === "darwin"
|
|
252
|
+
? `open "${folder}"`
|
|
253
|
+
: `xdg-open "${folder}"`;
|
|
254
|
+
exec(command, (error) => {
|
|
255
|
+
if (error)
|
|
256
|
+
console.error(`Failed to open folder: ${error.message}`);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
res.json({ success: true });
|
|
260
|
+
});
|
|
261
|
+
app.get("/api/agents/:name/avatar", async (req, res) => {
|
|
262
|
+
const { name } = req.params;
|
|
263
|
+
const cfg = loadConfig();
|
|
264
|
+
const baseDir = cfg.baseDir || DEFAULT_BASE_DIR;
|
|
265
|
+
const resolvedBaseDir = resolvePath(baseDir);
|
|
266
|
+
const extensions = [".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif"];
|
|
267
|
+
const fileNames = ["avatar", "icon", "image", "logo"];
|
|
268
|
+
const searchDirs = [
|
|
269
|
+
path.join(resolvedBaseDir, "agents", name, "assets"),
|
|
270
|
+
path.join(process.cwd(), "server", "src", "agents", name, "assets"),
|
|
271
|
+
path.join(process.cwd(), "server", "src", "assets", "agents", name),
|
|
272
|
+
path.join(process.cwd(), "server", "src", "agents", "assets"),
|
|
273
|
+
path.join(process.cwd(), "server", "src", "assets")
|
|
274
|
+
];
|
|
275
|
+
for (const dir of searchDirs) {
|
|
276
|
+
for (const fileName of fileNames) {
|
|
277
|
+
for (const ext of extensions) {
|
|
278
|
+
const baseName = (dir.endsWith("assets") && !dir.includes(name)) ? name : fileName;
|
|
279
|
+
const p = path.join(dir, `${baseName}${ext}`);
|
|
280
|
+
try {
|
|
281
|
+
await fs.access(p);
|
|
282
|
+
return res.sendFile(p);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// continue
|
|
286
|
+
}
|
|
287
|
+
if (baseName === name)
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
res.status(404).send("Avatar not found");
|
|
293
|
+
});
|
|
294
|
+
// ─── Chat SSE endpoint ──────────────────────────────────────────
|
|
55
295
|
app.post("/api/chat", async (req, res) => {
|
|
56
296
|
const body = req.body;
|
|
57
297
|
if (!body.event || typeof body.event.type !== "string") {
|
|
@@ -65,12 +305,15 @@ export async function startServer(options = {}) {
|
|
|
65
305
|
Connection: "keep-alive",
|
|
66
306
|
});
|
|
67
307
|
res.flushHeaders?.();
|
|
68
|
-
const runtime = openBotAgent.build();
|
|
69
308
|
const sessionId = body.sessionId ?? "default";
|
|
70
309
|
const runId = body.runId ?? `run_${generateId()}`;
|
|
71
310
|
const state = (await loadSession(sessionId)) ?? {};
|
|
72
311
|
state.sessionId = sessionId;
|
|
73
|
-
|
|
312
|
+
if (!state.cwd)
|
|
313
|
+
state.cwd = process.cwd();
|
|
314
|
+
if (!state.workspaceRoot)
|
|
315
|
+
state.workspaceRoot = process.cwd();
|
|
316
|
+
const iterator = orchestrator.run(body.event, {
|
|
74
317
|
runId,
|
|
75
318
|
state,
|
|
76
319
|
});
|
|
@@ -80,14 +323,11 @@ export async function startServer(options = {}) {
|
|
|
80
323
|
res.on("close", stopStreaming);
|
|
81
324
|
try {
|
|
82
325
|
for await (const chunk of iterator) {
|
|
83
|
-
if (res.writableEnded)
|
|
326
|
+
if (res.writableEnded)
|
|
84
327
|
break;
|
|
85
|
-
}
|
|
86
|
-
// Log each event to the persistent file
|
|
87
328
|
await logEvent(sessionId, runId, chunk);
|
|
88
329
|
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
89
330
|
}
|
|
90
|
-
// After the run finishes, save the final state back to disk
|
|
91
331
|
await saveSession(sessionId, state);
|
|
92
332
|
}
|
|
93
333
|
catch (error) {
|
|
@@ -109,6 +349,7 @@ export async function startServer(options = {}) {
|
|
|
109
349
|
app.listen(PORT, () => {
|
|
110
350
|
console.log(`OpenBot server listening at http://localhost:${PORT}`);
|
|
111
351
|
console.log(` - Chat endpoint: POST /api/chat`);
|
|
352
|
+
console.log(` - REST endpoints: /api/config, /api/sessions, /api/agents`);
|
|
112
353
|
if (options.openaiApiKey)
|
|
113
354
|
console.log(" - Using OpenAI API Key from CLI");
|
|
114
355
|
if (options.anthropicApiKey)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ui } from '@melony/ui-kit/server';
|
|
2
|
+
export const actionList = (title, actions) => ui.box({ border: true, radius: 'md', padding: 'md' }, [
|
|
3
|
+
ui.col({ gap: 'md' }, [
|
|
4
|
+
ui.heading(title, { level: 4 }),
|
|
5
|
+
ui.row({ gap: 'sm', wrap: 'wrap' }, actions.map(a => ui.button({ variant: a.variant || 'outline', onClickAction: a.action }, [
|
|
6
|
+
ui.text(a.label, { size: 'sm' })
|
|
7
|
+
])))
|
|
8
|
+
])
|
|
9
|
+
]);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ui } from '@melony/ui-kit/server';
|
|
2
|
+
export const approvalCard = (title, description, approveAction, denyAction) => ui.box({ border: true, radius: 'md', padding: 'md' }, [
|
|
3
|
+
ui.col({ gap: 'sm' }, [
|
|
4
|
+
ui.heading(title, { level: 4 }),
|
|
5
|
+
ui.text(description, { size: 'sm', color: 'muted' }),
|
|
6
|
+
ui.row({ gap: 'sm', justify: 'end' }, [
|
|
7
|
+
ui.button({ variant: 'outline', onClickAction: denyAction }, [
|
|
8
|
+
ui.text('Deny', { size: 'xs' })
|
|
9
|
+
]),
|
|
10
|
+
ui.button({ variant: 'primary', onClickAction: approveAction }, [
|
|
11
|
+
ui.text('Approve', { size: 'xs' })
|
|
12
|
+
])
|
|
13
|
+
])
|
|
14
|
+
])
|
|
15
|
+
]);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ui } from "@melony/ui-kit";
|
|
2
|
+
export const dataBlockWidget = (data) => ui.col({ gap: 'xs' }, Object.entries(data).filter(([_, v]) => v !== undefined && v !== null).map(([key, value]) => ui.row({ gap: 'sm', align: 'start' }, [
|
|
3
|
+
ui.text(`${key}:`, { weight: 'semibold', size: 'xs', color: 'muted' }),
|
|
4
|
+
ui.text(String(value), { size: 'xs' }),
|
|
5
|
+
])));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ui } from '@melony/ui-kit/server';
|
|
2
|
+
export const dataTable = (headers, rows) => {
|
|
3
|
+
const headerRow = `| ${headers.join(' | ')} |`;
|
|
4
|
+
const separator = `| ${headers.map(() => '---').join(' | ')} |`;
|
|
5
|
+
const dataRows = rows.map(row => `| ${row.join(' | ')} |`).join('\n');
|
|
6
|
+
const markdownTable = `${headerRow}\n${separator}\n${dataRows}`;
|
|
7
|
+
return ui.markdown(markdownTable);
|
|
8
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ui } from '@melony/ui-kit/server';
|
|
2
|
+
export const emptyState = (message, iconName) => ui.box({ padding: 'lg', border: true, radius: 'md' }, [
|
|
3
|
+
ui.col({ align: 'center', justify: 'center', gap: 'sm' }, [
|
|
4
|
+
iconName ? ui.icon(iconName) : ui.text('∅', { size: 'lg', color: 'muted' }),
|
|
5
|
+
ui.text(message, { size: 'sm', color: 'muted' })
|
|
6
|
+
])
|
|
7
|
+
]);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { keyValue } from './key-value.js';
|
|
2
|
+
import { dataTable } from './data-table.js';
|
|
3
|
+
import { codeSnippet } from './code-snippet.js';
|
|
4
|
+
import { statusWidget as status } from './status.js';
|
|
5
|
+
import { progressStep } from './progress-step.js';
|
|
6
|
+
import { approvalCard } from './approval-card.js';
|
|
7
|
+
import { actionList } from './action-list.js';
|
|
8
|
+
import { emptyState } from './empty-state.js';
|
|
9
|
+
export const widgets = {
|
|
10
|
+
keyValue,
|
|
11
|
+
dataTable,
|
|
12
|
+
codeSnippet,
|
|
13
|
+
status,
|
|
14
|
+
progressStep,
|
|
15
|
+
approvalCard,
|
|
16
|
+
actionList,
|
|
17
|
+
emptyState,
|
|
18
|
+
};
|
|
19
|
+
export default widgets;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ui } from '@melony/ui-kit/server';
|
|
2
|
+
export const keyValue = (title, data) => ui.box({ border: true, radius: 'md', padding: 'md' }, [
|
|
3
|
+
ui.col({ gap: 'md' }, [
|
|
4
|
+
ui.heading(title, { level: 4 }),
|
|
5
|
+
ui.col({ gap: 'xs' }, Object.entries(data)
|
|
6
|
+
.filter(([_, v]) => v !== undefined && v !== null)
|
|
7
|
+
.map(([key, value]) => ui.row({ gap: 'sm', align: 'start' }, [
|
|
8
|
+
ui.text(`${key}:`, { weight: 'bold', size: 'sm', color: 'muted' }),
|
|
9
|
+
ui.text(String(value), { size: 'sm' })
|
|
10
|
+
])))
|
|
11
|
+
])
|
|
12
|
+
]);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ui } from '@melony/ui-kit/server';
|
|
2
|
+
export const progressStep = (currentStep, totalSteps, label) => ui.row({ gap: 'md', align: 'center', padding: 'sm' }, [
|
|
3
|
+
ui.text(`Step ${currentStep} of ${totalSteps}`, { weight: 'bold', size: 'sm' }),
|
|
4
|
+
ui.text(label, { size: 'sm', color: 'muted' })
|
|
5
|
+
]);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ui } from '@melony/ui-kit';
|
|
2
|
+
export const resourceCardWidget = (title, subtitle, children = []) => ui.box({
|
|
3
|
+
border: true,
|
|
4
|
+
radius: 'lg',
|
|
5
|
+
padding: 'md',
|
|
6
|
+
}, [
|
|
7
|
+
ui.heading(title, { level: 4 }),
|
|
8
|
+
ui.text(subtitle ?? '', { size: 'sm', color: 'mutedForeground' }),
|
|
9
|
+
...children,
|
|
10
|
+
]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@ai-sdk/anthropic": "^3.0.33",
|
|
11
11
|
"@ai-sdk/openai": "^3.0.13",
|
|
12
|
-
"@melony/ui-kit": "^0.1.
|
|
12
|
+
"@melony/ui-kit": "^0.1.30",
|
|
13
13
|
"@types/cors": "^2.8.19",
|
|
14
14
|
"ai": "^6.0.42",
|
|
15
15
|
"commander": "^14.0.2",
|