openbot 0.2.10 → 0.2.11

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.
@@ -3,6 +3,7 @@ import * as path from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
4
  import { execSync } from "node:child_process";
5
5
  import matter from "gray-matter";
6
+ import { z } from "zod";
6
7
  import { PluginRegistry } from "./plugin-registry.js";
7
8
  import { llmPlugin } from "../plugins/llm/index.js";
8
9
  import { createModel } from "../models.js";
@@ -79,6 +80,32 @@ export async function ensurePluginReady(pluginDir) {
79
80
  console.error(`[plugins] Failed to prepare plugin in ${pluginDir}:`, err);
80
81
  }
81
82
  }
83
+ // ── AGENT.md Config ──────────────────────────────────────────────────
84
+ function jsonToZod(schema) {
85
+ if (typeof schema === "string") {
86
+ if (schema === "string")
87
+ return z.string();
88
+ if (schema === "number")
89
+ return z.number();
90
+ if (schema === "boolean")
91
+ return z.boolean();
92
+ }
93
+ if (Array.isArray(schema)) {
94
+ // If it's a simple array like ["string"], take the first item
95
+ if (schema.length === 1)
96
+ return z.array(jsonToZod(schema[0]));
97
+ // For anything else, treat as array of any (or you could improve this)
98
+ return z.array(z.any());
99
+ }
100
+ if (typeof schema === "object" && schema !== null) {
101
+ const shape = {};
102
+ for (const [key, value] of Object.entries(schema)) {
103
+ shape[key] = jsonToZod(value);
104
+ }
105
+ return z.object(shape);
106
+ }
107
+ return z.any();
108
+ }
82
109
  export async function readAgentConfig(agentDir) {
83
110
  const mdPath = path.join(agentDir, "AGENT.md");
84
111
  let mdContent = "";
@@ -95,9 +122,11 @@ export async function readAgentConfig(agentDir) {
95
122
  description: typeof config.description === "string" ? config.description : "",
96
123
  model: config.model,
97
124
  image: config.image,
125
+ base: config.base,
98
126
  plugins: config.plugins || [],
99
127
  instructions: parsed.content.trim() || "",
100
128
  subscribe: config.subscribe,
129
+ outputSchema: config.outputSchema,
101
130
  };
102
131
  }
103
132
  // ── Agent composition (declarative AGENT.md agents) ──────────────────
@@ -118,14 +147,48 @@ function composeAgentFromConfig(config, toolRegistry, model) {
118
147
  Object.assign(allToolDefinitions, entry.toolDefinitions);
119
148
  }
120
149
  const plugin = (builder) => {
150
+ // 1. Initialize all regular tool plugins
121
151
  for (const { plugin: toolPlugin, config: resolvedConfig } of pluginFactories) {
122
152
  builder.use(toolPlugin({ ...resolvedConfig, model }));
123
153
  }
124
- builder.use(llmPlugin({
125
- model,
126
- system: config.instructions,
127
- toolDefinitions: allToolDefinitions,
128
- }));
154
+ // 2. Resolve the "Brain" (Base Plugin)
155
+ const baseInput = config.base || "llm";
156
+ const isBaseString = typeof baseInput === "string";
157
+ const baseName = isBaseString ? baseInput : baseInput.name;
158
+ const baseConfig = isBaseString ? {} : (baseInput.config || {});
159
+ const resolvedBaseConfig = resolveConfigPaths(baseConfig);
160
+ if (baseName === "llm") {
161
+ // Default built-in brain
162
+ builder.use(llmPlugin({
163
+ model,
164
+ system: config.instructions,
165
+ toolDefinitions: allToolDefinitions,
166
+ outputSchema: config.outputSchema ? jsonToZod(config.outputSchema) : undefined,
167
+ }));
168
+ }
169
+ else {
170
+ // Custom autonomous brain (e.g. codex)
171
+ const baseEntry = toolRegistry.get(baseName);
172
+ if (!baseEntry || baseEntry.type !== "tool") {
173
+ console.error(`[plugins] "${config.name}": base plugin "${baseName}" not found or invalid. Falling back to default LLM brain.`);
174
+ builder.use(llmPlugin({
175
+ model,
176
+ system: config.instructions,
177
+ toolDefinitions: allToolDefinitions,
178
+ }));
179
+ }
180
+ else {
181
+ // Use the custom plugin as the autonomous engine.
182
+ // It must follow the Base Plugin contract (receive model, system, tools).
183
+ builder.use(baseEntry.plugin({
184
+ ...resolvedBaseConfig,
185
+ model,
186
+ system: config.instructions,
187
+ toolDefinitions: allToolDefinitions,
188
+ outputSchema: config.outputSchema ? jsonToZod(config.outputSchema) : undefined,
189
+ }));
190
+ }
191
+ }
129
192
  };
130
193
  return { plugin, toolDefinitions: allToolDefinitions };
131
194
  }
@@ -149,6 +212,7 @@ async function loadToolPluginsFromDir(dir) {
149
212
  const entryData = module.plugin || module.default || module.entry;
150
213
  if (entryData && typeof entryData.factory === "function") {
151
214
  plugins.push({
215
+ id: entry.name,
152
216
  name: entryData.name || entry.name,
153
217
  description: entryData.description || `Tool plugin ${entry.name}`,
154
218
  type: "tool",
@@ -225,12 +289,14 @@ export async function discoverPlugins(dir, registry, defaultModel, options) {
225
289
  if (codeAgentDef && typeof codeAgentDef.factory === "function") {
226
290
  const meta = await getPluginMetadata(pluginDir);
227
291
  const folderName = path.basename(pluginDir);
292
+ const id = folderName; // Folder name as slug
228
293
  let name = codeAgentDef.name || meta.name;
229
294
  if (!name || /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(name)) {
230
295
  name = toTitleCaseFromSlug(folderName);
231
296
  }
232
297
  const description = codeAgentDef.description || meta.description || "Code Agent";
233
298
  registry.register({
299
+ id,
234
300
  name,
235
301
  description,
236
302
  type: "agent",
@@ -239,16 +305,18 @@ export async function discoverPlugins(dir, registry, defaultModel, options) {
239
305
  subscribe: codeAgentDef.subscribe,
240
306
  folder: pluginDir,
241
307
  });
242
- console.log(`[plugins] Loaded code-only agent: ${name} — ${description}`);
308
+ console.log(`[plugins] Loaded code-only agent: ${id} (${name}) — ${description}`);
243
309
  }
244
310
  else if (entryData && typeof entryData.factory === "function") {
245
311
  const meta = await getPluginMetadata(pluginDir);
246
312
  const folderName = path.basename(pluginDir);
313
+ const id = folderName;
247
314
  let name = entryData.name || meta.name;
248
315
  if (!name || /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(name)) {
249
316
  name = toTitleCaseFromSlug(folderName);
250
317
  }
251
318
  const pluginEntry = {
319
+ id,
252
320
  name,
253
321
  description: entryData.description || meta.description || "Tool plugin",
254
322
  type: "tool",
@@ -257,7 +325,7 @@ export async function discoverPlugins(dir, registry, defaultModel, options) {
257
325
  folder: pluginDir,
258
326
  };
259
327
  registry.register(pluginEntry);
260
- console.log(`[plugins] Loaded tool: ${pluginEntry.name}`);
328
+ console.log(`[plugins] Loaded tool: ${id} (${pluginEntry.name})`);
261
329
  }
262
330
  else {
263
331
  console.warn(`[plugins] "${path.basename(pluginDir)}" does not export a valid plugin (missing factory)`);
@@ -282,12 +350,14 @@ export async function discoverPlugins(dir, registry, defaultModel, options) {
282
350
  if (definition && typeof definition.factory === "function") {
283
351
  const config = await readAgentConfig(agentDir);
284
352
  const meta = await getPluginMetadata(agentDir);
353
+ const id = folderName;
285
354
  let name = config.name || definition.name || meta.name;
286
355
  if (!name || /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(name)) {
287
356
  name = toTitleCaseFromSlug(folderName);
288
357
  }
289
358
  const description = definition.description || config.description || "TS Agent";
290
359
  registry.register({
360
+ id,
291
361
  name,
292
362
  description,
293
363
  type: "agent",
@@ -296,13 +366,14 @@ export async function discoverPlugins(dir, registry, defaultModel, options) {
296
366
  subscribe: definition.subscribe || config.subscribe,
297
367
  folder: agentDir,
298
368
  });
299
- console.log(`[plugins] Loaded TS agent: ${name} — ${description}`);
369
+ console.log(`[plugins] Loaded TS agent: ${id} (${name}) — ${description}`);
300
370
  }
301
371
  }
302
372
  else {
303
373
  // Declarative Agent — AGENT.md only, auto-wrapped with llmPlugin.
304
374
  const config = await readAgentConfig(agentDir);
305
375
  const meta = await getPluginMetadata(agentDir);
376
+ const id = folderName;
306
377
  let resolvedName = config.name || meta.name;
307
378
  if (!resolvedName || /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(resolvedName)) {
308
379
  resolvedName = toTitleCaseFromSlug(folderName);
@@ -330,6 +401,7 @@ export async function discoverPlugins(dir, registry, defaultModel, options) {
330
401
  }
331
402
  const { plugin, toolDefinitions } = composeAgentFromConfig(config, scopedRegistry, agentModel);
332
403
  registry.register({
404
+ id,
333
405
  name: resolvedName,
334
406
  description: resolvedDescription,
335
407
  type: "agent",
@@ -338,7 +410,7 @@ export async function discoverPlugins(dir, registry, defaultModel, options) {
338
410
  subscribe: config.subscribe,
339
411
  folder: agentDir,
340
412
  });
341
- console.log(`[plugins] Loaded agent: ${resolvedName} — ${resolvedDescription}${config.model ? ` (model: ${config.model})` : ""}`);
413
+ console.log(`[plugins] Loaded agent: ${id} (${resolvedName}) — ${resolvedDescription}${config.model ? ` (model: ${config.model})` : ""}`);
342
414
  }
343
415
  }
344
416
  catch (err) {
@@ -33,12 +33,12 @@ export class PluginRegistry {
33
33
  getTools() {
34
34
  return this.getAll().filter(p => p.type === "tool");
35
35
  }
36
- /** Returns agent names as a tuple suitable for z.enum(). */
37
- getAgentNames() {
38
- const names = this.getAgents().map(a => a.name);
39
- if (names.length === 0) {
36
+ /** Returns agent IDs as a tuple suitable for z.enum(). */
37
+ getAgentIds() {
38
+ const ids = this.getAgents().map(a => a.id);
39
+ if (ids.length === 0) {
40
40
  throw new Error("No agents registered — at least one agent is required");
41
41
  }
42
- return names;
42
+ return ids;
43
43
  }
44
44
  }
package/dist/server.js CHANGED
@@ -882,7 +882,6 @@ export async function startServer(options = {}) {
882
882
  console.log(`OpenBot server listening at http://localhost:${PORT}`);
883
883
  console.log(` - Chat endpoint: POST /api/chat`);
884
884
  console.log(` - REST endpoints: /api/config, /api/sessions, /api/agents`);
885
- console.log(`\n🚀 TIP: Use 'openbot up' to run both the server and web dashboard together.`);
886
885
  if (options.openaiApiKey)
887
886
  console.log(" - Using OpenAI API Key from CLI");
888
887
  if (options.anthropicApiKey)
package/dist/session.js CHANGED
@@ -41,6 +41,24 @@ function getSessionDir(sessionId) {
41
41
  * System messages at the start are always preserved and don't count toward the limit.
42
42
  */
43
43
  const MAX_MESSAGES = 1000; // aiSdkPlugin defaults to latest 20 messages
44
+ const MAX_LISTED_SESSIONS = 1000;
45
+ function hasPersistedContent(state) {
46
+ if (state.title)
47
+ return true;
48
+ if (state.messages && state.messages.length > 0)
49
+ return true;
50
+ // Direct sub-agent conversations are stored under agentStates.
51
+ // Treat any non-empty agent state as a persisted session.
52
+ if (state.agentStates && typeof state.agentStates === "object") {
53
+ for (const agentState of Object.values(state.agentStates)) {
54
+ if (!agentState || typeof agentState !== "object")
55
+ continue;
56
+ if (Object.keys(agentState).length > 0)
57
+ return true;
58
+ }
59
+ }
60
+ return false;
61
+ }
44
62
  export async function loadSession(sessionId) {
45
63
  const sessionDir = getSessionDir(sessionId);
46
64
  const statePath = path.join(sessionDir, "state.json");
@@ -125,8 +143,7 @@ export async function listSessions() {
125
143
  const statePath = path.join(sessionPath, "state.json");
126
144
  if (fs.existsSync(statePath)) {
127
145
  const state = JSON.parse(fs.readFileSync(statePath, "utf-8"));
128
- // Only include sessions with messages or a title
129
- if ((state.messages && state.messages.length > 0) || state.title) {
146
+ if (hasPersistedContent(state)) {
130
147
  sessions.push({
131
148
  id: subItem,
132
149
  mtime: fs.statSync(statePath).birthtime, // sort by creation time
@@ -141,8 +158,7 @@ export async function listSessions() {
141
158
  const statePath = path.join(itemPath, "state.json");
142
159
  if (fs.existsSync(statePath)) {
143
160
  const state = JSON.parse(fs.readFileSync(statePath, "utf-8"));
144
- // Only include sessions with messages or a title
145
- if ((state.messages && state.messages.length > 0) || state.title) {
161
+ if (hasPersistedContent(state)) {
146
162
  sessions.push({
147
163
  id: item,
148
164
  title: state.title ?? undefined,
@@ -157,5 +173,7 @@ export async function listSessions() {
157
173
  catch (error) {
158
174
  console.error("Failed to list sessions:", error);
159
175
  }
160
- return sessions.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
176
+ return sessions
177
+ .sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
178
+ .slice(0, MAX_LISTED_SESSIONS);
161
179
  }
@@ -0,0 +1,12 @@
1
+ export const block = (widget, props, options = {}) => ({
2
+ type: "ui-block",
3
+ widget,
4
+ props,
5
+ placement: options.placement ?? "thread",
6
+ id: options.id,
7
+ meta: options.meta,
8
+ });
9
+ export const uiEvent = (block) => ({
10
+ type: "ui",
11
+ data: block,
12
+ });
@@ -1,9 +1,2 @@
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
- ]);
1
+ import { block } from '../block.js';
2
+ export const actionList = (title, actions) => block('action-list', { title, actions });
@@ -1,35 +1,9 @@
1
- import { ui } from '@melony/ui-kit/server';
2
- export const approvalCard = (title, data, approveAction, denyAction) => ui.box({ border: true, radius: 'md', padding: 'md' }, [
3
- ui.col({ gap: 'sm' }, [
4
- ui.heading(title, { level: 4 }),
5
- ui.text(data.summary, { size: 'sm', color: 'muted' }),
6
- ...(data.details?.length
7
- ? [
8
- ui.box({ border: true, radius: 'sm', padding: 'sm' }, [
9
- ui.col({ gap: 'xs' }, data.details.map((detail) => ui.row({ gap: 'sm', align: 'start' }, [
10
- ui.text(`${detail.label}:`, { size: 'xs', color: 'muted', weight: 'semibold' }),
11
- ui.text(detail.value, { size: 'xs' }),
12
- ]))),
13
- ]),
14
- ]
15
- : []),
16
- ...(data.rawPayload
17
- ? [
18
- ui.box({ border: true, radius: 'sm', padding: 'sm' }, [
19
- ui.col({ gap: 'xs' }, [
20
- ui.text('Full action payload', { size: 'xs', color: 'muted', weight: 'semibold' }),
21
- ui.text(data.rawPayload, { size: 'xs' }),
22
- ]),
23
- ]),
24
- ]
25
- : []),
26
- ui.row({ gap: 'sm', justify: 'end' }, [
27
- ui.button({ variant: 'outline', onClickAction: denyAction }, [
28
- ui.text('Deny', { size: 'xs' })
29
- ]),
30
- ui.button({ variant: 'primary', onClickAction: approveAction }, [
31
- ui.text('Approve', { size: 'xs' })
32
- ])
33
- ])
34
- ])
35
- ]);
1
+ import { block } from '../block.js';
2
+ export const approvalCard = (title, data, approveAction, denyAction, options = {}) => block('approval-card', {
3
+ title,
4
+ summary: data.summary,
5
+ details: data.details,
6
+ rawPayload: data.rawPayload,
7
+ approveAction,
8
+ denyAction
9
+ }, options);
@@ -1,2 +1,2 @@
1
- import { ui } from '@melony/ui-kit/server';
2
- export const codeSnippet = (code, language = 'text') => ui.markdown(`\`\`\`${language}\n${code}\n\`\`\``);
1
+ import { block } from '../block.js';
2
+ export const codeSnippet = (code, language = 'text') => block('code-snippet', { code, language });
@@ -1,5 +1,2 @@
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
- ])));
1
+ import { block } from '../block.js';
2
+ export const dataBlockWidget = (data) => block('data-block', { data });
@@ -1,8 +1,2 @@
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
- };
1
+ import { block } from '../block.js';
2
+ export const dataTable = (headers, rows) => block('data-table', { headers, rows });
@@ -1,7 +1,2 @@
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
- ]);
1
+ import { block } from '../block.js';
2
+ export const emptyState = (message, iconName) => block('empty-state', { message, iconName });
@@ -6,6 +6,8 @@ import { progressStep } from './progress-step.js';
6
6
  import { approvalCard } from './approval-card.js';
7
7
  import { actionList } from './action-list.js';
8
8
  import { emptyState } from './empty-state.js';
9
+ import { todoList } from './todo-list.js';
10
+ import { inquiryCard } from './inquiry.js';
9
11
  export const widgets = {
10
12
  keyValue,
11
13
  dataTable,
@@ -15,5 +17,7 @@ export const widgets = {
15
17
  approvalCard,
16
18
  actionList,
17
19
  emptyState,
20
+ todoList,
21
+ inquiryCard,
18
22
  };
19
23
  export default widgets;
@@ -0,0 +1,7 @@
1
+ import { block } from '../block.js';
2
+ export const inquiryCard = (title, data, options = {}) => block('inquiry-card', {
3
+ title,
4
+ question: data.question,
5
+ description: data.description,
6
+ options: data.options || [],
7
+ }, options);
@@ -1,12 +1,2 @@
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
- ]);
1
+ import { block } from '../block.js';
2
+ export const keyValue = (title, data) => block('key-value', { title, data });
@@ -1,5 +1,2 @@
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
- ]);
1
+ import { block } from '../block.js';
2
+ export const progressStep = (currentStep, totalSteps, label) => block('progress-step', { currentStep, totalSteps, label });
@@ -1,10 +1,2 @@
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
- ]);
1
+ import { block } from '../block.js';
2
+ export const resourceCardWidget = (title, subtitle, children = []) => block('resource-card', { title, subtitle, children });
@@ -1,5 +1,2 @@
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
- });
1
+ import { block } from '../block.js';
2
+ export const statusWidget = (message, severity = 'info', options = {}) => block('status', { message, severity }, options);
@@ -0,0 +1,2 @@
1
+ import { block } from '../block.js';
2
+ export const todoList = (todos, options = {}) => block('todo-list', { todos }, options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.2.10",
3
+ "version": "0.2.11",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
@@ -12,7 +12,6 @@
12
12
  "dependencies": {
13
13
  "@ai-sdk/anthropic": "^3.0.33",
14
14
  "@ai-sdk/openai": "^3.0.13",
15
- "@melony/ui-kit": "^0.1.30",
16
15
  "@types/cors": "^2.8.19",
17
16
  "ai": "^6.0.42",
18
17
  "commander": "^14.0.2",