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.
@@ -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
- console.warn(`[agents] Error loading "${entry.name}/agent.yaml":`, err);
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
- // Initialize the agent instance once at startup
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
- app.get("/", async (req, res) => {
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: "Melony Express demo server",
22
- chatEndpoint: "/api/chat",
23
- stream: "Server-Sent Events (SSE)",
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
- // View endpoint (GET version of chat, returns JSON instead of SSE)
27
- app.get("/api/view", async (req, res) => {
28
- const { type = "init", data: dataStr } = req.query;
29
- // Parse the structured data
30
- const eventData = dataStr ? JSON.parse(dataStr) : {};
31
- const { sessionId = "default", history = false } = eventData;
32
- const state = (await loadSession(sessionId)) ?? {};
33
- state.sessionId = sessionId;
34
- const response = await openBotAgent.jsonResponse({
35
- type: type,
36
- data: eventData,
37
- }, {
38
- state,
39
- runId: generateId(),
40
- });
41
- const result = await response.json();
42
- const uiEvents = result.events.filter((event) => event.type === "ui");
43
- const layout = uiEvents.find((e) => e.meta?.type === "layout")?.data;
44
- const sidebar = uiEvents.find((e) => e.meta?.type === "sidebar")?.data;
45
- const content = uiEvents.find((e) => e.meta?.type === "content")?.data;
46
- const initialEvents = history ? await loadEvents(sessionId) : undefined;
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
- ui: layout,
49
- sidebar,
50
- content,
51
- initialEvents,
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
- // Chat endpoint
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
- const iterator = runtime.run(body.event, {
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,2 @@
1
+ import { ui } from '@melony/ui-kit/server';
2
+ export const codeSnippet = (code, language = 'text') => ui.markdown(`\`\`\`${language}\n${code}\n\`\`\``);
@@ -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
+ ]);
@@ -0,0 +1,5 @@
1
+ import { ui } from '@melony/ui-kit';
2
+ export const statusWidget = (message, severity = 'info') => ui.text(message, {
3
+ color: severity === 'error' ? 'danger' : severity === 'success' ? 'success' : 'muted',
4
+ size: 'xs',
5
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.1.28",
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.5",
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",