openbot 0.1.22 → 0.1.24

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,100 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { execSync } from "node:child_process";
5
+ /**
6
+ * Ensure a plugin is ready to run by installing dependencies and building if necessary.
7
+ */
8
+ export async function ensurePluginReady(pluginDir) {
9
+ try {
10
+ const pkgPath = path.join(pluginDir, "package.json");
11
+ const hasPackageJson = await fs.access(pkgPath).then(() => true).catch(() => false);
12
+ if (!hasPackageJson)
13
+ return;
14
+ const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
15
+ const nodeModulesPath = path.join(pluginDir, "node_modules");
16
+ const hasNodeModules = await fs.access(nodeModulesPath).then(() => true).catch(() => false);
17
+ // 1. Install dependencies if node_modules is missing
18
+ if (!hasNodeModules) {
19
+ console.log(`[plugins] Installing dependencies for ${path.basename(pluginDir)}...`);
20
+ execSync("npm install --production", { cwd: pluginDir, stdio: "inherit" });
21
+ }
22
+ // 2. Run build if dist is missing but build script exists
23
+ const distPath = path.join(pluginDir, "dist");
24
+ const hasDist = await fs.access(distPath).then(() => true).catch(() => false);
25
+ if (!hasDist && pkg.scripts?.build) {
26
+ console.log(`[plugins] Building ${path.basename(pluginDir)}...`);
27
+ execSync("npm run build", { cwd: pluginDir, stdio: "inherit" });
28
+ }
29
+ }
30
+ catch (err) {
31
+ console.error(`[plugins] Failed to prepare plugin in ${pluginDir}:`, err);
32
+ }
33
+ }
34
+ /**
35
+ * Dynamically load plugins from a directory.
36
+ * Scans each subdirectory for an index.js (or index.ts if running via tsx).
37
+ */
38
+ export async function loadPluginsFromDir(dir) {
39
+ const plugins = [];
40
+ try {
41
+ await fs.access(dir);
42
+ }
43
+ catch {
44
+ // Directory doesn't exist
45
+ return plugins;
46
+ }
47
+ try {
48
+ const entries = await fs.readdir(dir, { withFileTypes: true });
49
+ for (const entry of entries) {
50
+ if (!entry.isDirectory())
51
+ continue;
52
+ if (entry.name.startsWith(".") || entry.name.startsWith("_"))
53
+ continue;
54
+ const pluginDir = path.join(dir, entry.name);
55
+ // Ensure plugin is ready (dependencies, build)
56
+ await ensurePluginReady(pluginDir);
57
+ // Try to find index.js or index.ts, prioritizing dist/index.js for pre-built bundles
58
+ let indexPath;
59
+ const possibleIndices = ["dist/index.js", "index.js", "index.ts"];
60
+ for (const file of possibleIndices) {
61
+ try {
62
+ const fullPath = path.join(pluginDir, file);
63
+ await fs.access(fullPath);
64
+ indexPath = fullPath;
65
+ break;
66
+ }
67
+ catch {
68
+ continue;
69
+ }
70
+ }
71
+ if (!indexPath) {
72
+ console.warn(`[plugins] No index.js or index.ts found in ${pluginDir}`);
73
+ continue;
74
+ }
75
+ try {
76
+ // Use pathToFileURL for compatibility with import() on all OSs
77
+ const moduleUrl = pathToFileURL(indexPath).href;
78
+ const module = await import(moduleUrl);
79
+ // The plugin can be exported as 'plugin', 'default', or 'entry'
80
+ const entryData = module.plugin || module.default || module.entry;
81
+ if (entryData && typeof entryData.factory === "function") {
82
+ plugins.push({
83
+ ...entryData,
84
+ name: entryData.name || entry.name, // Fallback to folder name
85
+ });
86
+ }
87
+ else {
88
+ console.warn(`[plugins] "${entry.name}" does not export a valid plugin entry (missing factory)`);
89
+ }
90
+ }
91
+ catch (err) {
92
+ console.error(`[plugins] Failed to load plugin "${entry.name}":`, err);
93
+ }
94
+ }
95
+ }
96
+ catch (err) {
97
+ console.warn(`[plugins] Error reading directory ${dir}:`, err);
98
+ }
99
+ return plugins;
100
+ }
@@ -2,7 +2,29 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import yaml from "js-yaml";
4
4
  import { llmPlugin } from "../plugins/llm/index.js";
5
+ import { PluginRegistry } from "./plugin-registry.js";
5
6
  import { createModel } from "../models.js";
7
+ import { loadPluginsFromDir } from "./plugin-loader.js";
8
+ import { resolvePath } from "../config.js";
9
+ /**
10
+ * Recursively resolve tilde paths in a configuration object.
11
+ */
12
+ function resolveConfigPaths(config) {
13
+ if (typeof config === "string") {
14
+ return resolvePath(config);
15
+ }
16
+ if (Array.isArray(config)) {
17
+ return config.map(resolveConfigPaths);
18
+ }
19
+ if (config !== null && typeof config === "object") {
20
+ const resolved = {};
21
+ for (const [key, value] of Object.entries(config)) {
22
+ resolved[key] = resolveConfigPaths(value);
23
+ }
24
+ return resolved;
25
+ }
26
+ return config;
27
+ }
6
28
  /**
7
29
  * Discover and load YAML-defined agents from a directory.
8
30
  *
@@ -32,6 +54,7 @@ export async function discoverYamlAgents(agentsDir, pluginRegistry, defaultModel
32
54
  if (entry.name.startsWith(".") || entry.name.startsWith("_"))
33
55
  continue;
34
56
  const yamlPath = path.join(agentsDir, entry.name, "agent.yaml");
57
+ const agentDir = path.join(agentsDir, entry.name);
35
58
  try {
36
59
  const content = await fs.readFile(yamlPath, "utf-8");
37
60
  const config = yaml.load(content);
@@ -40,15 +63,32 @@ export async function discoverYamlAgents(agentsDir, pluginRegistry, defaultModel
40
63
  console.warn(`[agents] "${entry.name}/agent.yaml": missing required fields (name, description, plugins, systemPrompt) — skipping`);
41
64
  continue;
42
65
  }
43
- // Use agent-specific model if defined, otherwise use default
44
66
  const agentModel = config.model
45
67
  ? createModel({ ...options, model: config.model })
46
68
  : defaultModel;
47
- const plugin = composeAgentFromYaml(config, pluginRegistry, agentModel);
69
+ // 1. Load local plugins from agents/<name>/plugins/
70
+ const localPluginsDir = path.join(agentDir, "plugins");
71
+ const localPlugins = await loadPluginsFromDir(localPluginsDir);
72
+ // 2. Create a scoped registry for this agent: global + local
73
+ const scopedRegistry = new PluginRegistry();
74
+ // Add all global plugins
75
+ for (const p of pluginRegistry.getAll()) {
76
+ scopedRegistry.register(p);
77
+ }
78
+ // Add local plugins (overwriting globals if names conflict)
79
+ for (const p of localPlugins) {
80
+ scopedRegistry.register(p);
81
+ }
82
+ const { plugin, toolDefinitions } = composeAgentFromYaml(config, scopedRegistry, agentModel);
48
83
  agents.push({
49
84
  name: config.name,
50
85
  description: config.description,
51
86
  plugin,
87
+ capabilities: Object.fromEntries(Object.entries(toolDefinitions).map(([name, def]) => [
88
+ name,
89
+ def.description,
90
+ ])),
91
+ subscribe: config.subscribe,
52
92
  });
53
93
  console.log(`[agents] Loaded: ${config.name} — ${config.description}${config.model ? ` (model: ${config.model})` : ""}`);
54
94
  }
@@ -70,22 +110,24 @@ export async function discoverYamlAgents(agentsDir, pluginRegistry, defaultModel
70
110
  * and wires them with an agent-scoped LLM plugin.
71
111
  */
72
112
  function composeAgentFromYaml(config, pluginRegistry, model) {
73
- return (builder) => {
74
- const allToolDefinitions = {};
75
- for (const pluginName of config.plugins) {
76
- const entry = pluginRegistry.get(pluginName);
77
- if (!entry) {
78
- console.warn(`[agents] "${config.name}": plugin "${pluginName}" not found in registry — skipping`);
79
- continue;
80
- }
81
- // Register the plugin's event handlers
82
- builder.use(entry.factory());
83
- // Register UI plugin if available
84
- if (entry.uiFactory) {
85
- builder.use(entry.uiFactory());
86
- }
87
- // Collect tool definitions for the LLM
88
- Object.assign(allToolDefinitions, entry.toolDefinitions);
113
+ const allToolDefinitions = {};
114
+ const pluginFactories = [];
115
+ for (const pluginItem of config.plugins) {
116
+ const isString = typeof pluginItem === "string";
117
+ const pluginName = isString ? pluginItem : pluginItem.name;
118
+ const pluginConfig = isString ? {} : (pluginItem.config || {});
119
+ const resolvedConfig = resolveConfigPaths(pluginConfig);
120
+ const entry = pluginRegistry.get(pluginName);
121
+ if (!entry) {
122
+ console.warn(`[agents] "${config.name}": plugin "${pluginName}" not found in registry — skipping`);
123
+ continue;
124
+ }
125
+ pluginFactories.push({ factory: entry.factory, config: resolvedConfig });
126
+ Object.assign(allToolDefinitions, entry.toolDefinitions);
127
+ }
128
+ const plugin = (builder) => {
129
+ for (const { factory, config: resolvedConfig } of pluginFactories) {
130
+ builder.use(factory({ ...resolvedConfig, model }));
89
131
  }
90
132
  // Wire up the LLM with agent-scoped event channels
91
133
  builder.use(llmPlugin({
@@ -97,4 +139,5 @@ function composeAgentFromYaml(config, pluginRegistry, model) {
97
139
  completionEventType: `agent:${config.name}:output`,
98
140
  }));
99
141
  };
142
+ return { plugin, toolDefinitions: allToolDefinitions };
100
143
  }
package/dist/server.js CHANGED
@@ -70,8 +70,6 @@ export async function startServer(options = {}) {
70
70
  const runId = body.runId ?? `run_${generateId()}`;
71
71
  const state = (await loadSession(sessionId)) ?? {};
72
72
  state.sessionId = sessionId;
73
- // Log the incoming event
74
- await logEvent(sessionId, runId, body.event);
75
73
  const iterator = runtime.run(body.event, {
76
74
  runId,
77
75
  state,
package/dist/session.js CHANGED
@@ -124,10 +124,15 @@ export async function listSessions() {
124
124
  const sessionPath = path.join(itemPath, subItem);
125
125
  const statePath = path.join(sessionPath, "state.json");
126
126
  if (fs.existsSync(statePath)) {
127
- sessions.push({
128
- id: subItem,
129
- mtime: fs.statSync(statePath).birthtime, // sort by creation time
130
- });
127
+ 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) {
130
+ sessions.push({
131
+ id: subItem,
132
+ mtime: fs.statSync(statePath).birthtime, // sort by creation time
133
+ title: state.title ?? undefined,
134
+ });
135
+ }
131
136
  }
132
137
  }
133
138
  }
@@ -135,10 +140,15 @@ export async function listSessions() {
135
140
  // It's a legacy session folder in root
136
141
  const statePath = path.join(itemPath, "state.json");
137
142
  if (fs.existsSync(statePath)) {
138
- sessions.push({
139
- id: item,
140
- mtime: fs.statSync(statePath).birthtime, // sort by creation time
141
- });
143
+ 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) {
146
+ sessions.push({
147
+ id: item,
148
+ title: state.title ?? undefined,
149
+ mtime: fs.statSync(statePath).birthtime, // sort by creation time
150
+ });
151
+ }
142
152
  }
143
153
  }
144
154
  }
package/dist/ui/layout.js CHANGED
@@ -11,9 +11,10 @@ const tabs = {
11
11
  };
12
12
  export const layoutUI = async ({ tab, sessionId }) => {
13
13
  const sessions = await listSessions();
14
+ const content = typeof tabs[tab] === "function" ? await tabs[tab]() : tabs[tab];
14
15
  return ui.row({ height: "full" }, [
15
16
  sidebarUI({ sessions, sessionId }),
16
- tabs[tab]
17
+ content
17
18
  ]);
18
19
  };
19
20
  export const sidebarOnlyUI = async ({ sessionId }) => {
@@ -21,5 +22,5 @@ export const sidebarOnlyUI = async ({ sessionId }) => {
21
22
  return sidebarUI({ sessions, sessionId });
22
23
  };
23
24
  export const tabOnlyUI = async ({ tab }) => {
24
- return tabs[tab];
25
+ return typeof tabs[tab] === "function" ? await tabs[tab]() : tabs[tab];
25
26
  };
@@ -1,44 +1,75 @@
1
1
  import { ui } from "@melony/ui-kit";
2
- export const settingsUI = ui.box({
3
- width: "full",
4
- height: "full",
5
- padding: "xl",
6
- background: "background",
7
- }, [
8
- ui.col({ gap: "xl", width: "full" }, [
9
- ui.col({ gap: "xs" }, [
10
- ui.heading("Settings", 2),
11
- ui.text("Manage your OpenBot configuration", { color: "mutedForeground" }),
12
- ]),
13
- ui.divider(),
14
- ui.col({ gap: "md" }, [
15
- ui.heading("Model Configuration", 4),
16
- ui.row({ align: "center", gap: "md" }, [
17
- ui.box({ flex: 1 }, [
18
- ui.node("label", { value: "Provider" }),
19
- ui.text("OpenAI (GPT-4o)", { size: "sm", color: "mutedForeground" }),
2
+ import { loadConfig } from "../config.js";
3
+ export const settingsUI = async () => {
4
+ const config = loadConfig();
5
+ const hasOpenAIKey = !!config.openaiApiKey;
6
+ const hasAnthropicKey = !!config.anthropicApiKey;
7
+ return ui.form({
8
+ onSubmitAction: {
9
+ type: "action:updateSettings",
10
+ data: {}
11
+ }
12
+ }, [
13
+ ui.box({
14
+ width: "full",
15
+ height: "full",
16
+ padding: "xl",
17
+ background: "background",
18
+ }, [
19
+ ui.col({ gap: "xl", width: "full" }, [
20
+ ui.col({ gap: "xs" }, [
21
+ ui.heading("Settings", 2),
22
+ ui.text("Manage your OpenBot configuration", { color: "mutedForeground" }),
20
23
  ]),
21
- ui.button({ label: "Change", variant: "outline", size: "sm" })
22
- ]),
23
- ]),
24
- ui.col({ gap: "md" }, [
25
- ui.heading("API Keys", 4),
26
- ui.col({ gap: "sm" }, [
27
- ui.node("label", { value: "OpenAI API Key" }),
28
- ui.row({ gap: "sm" }, [
29
- ui.input("openai_api_key", undefined, {
30
- placeholder: "sk-...",
31
- inputType: "password",
32
- defaultValue: "••••••••••••••••",
33
- width: "full"
34
- }),
35
- ui.button({ label: "Save", size: "sm" })
24
+ ui.divider(),
25
+ ui.col({ gap: "md" }, [
26
+ ui.heading("Model Configuration", 4),
27
+ ui.col({ gap: "sm" }, [
28
+ ui.input("model", undefined, {
29
+ placeholder: "e.g. gpt-4o-mini",
30
+ width: "full",
31
+ defaultValue: config.model || "gpt-4o-mini"
32
+ }),
33
+ ]),
34
+ ]),
35
+ ui.col({ gap: "md" }, [
36
+ ui.heading("API Keys", 4),
37
+ ui.col({ gap: "lg" }, [
38
+ // OpenAI Key
39
+ ui.col({ gap: "sm" }, [
40
+ ui.node("label", { value: "OpenAI API Key" }),
41
+ ui.input("openai_api_key", undefined, {
42
+ placeholder: "sk-...",
43
+ inputType: "password",
44
+ defaultValue: hasOpenAIKey ? "••••••••••••••••" : "",
45
+ width: "full"
46
+ }),
47
+ ]),
48
+ // Anthropic Key
49
+ ui.col({ gap: "sm" }, [
50
+ ui.node("label", { value: "Anthropic API Key" }),
51
+ ui.input("anthropic_api_key", undefined, {
52
+ placeholder: "sk-ant-...",
53
+ inputType: "password",
54
+ defaultValue: hasAnthropicKey ? "••••••••••••••••" : "",
55
+ width: "full"
56
+ }),
57
+ ])
58
+ ])
59
+ ]),
60
+ ui.col({ gap: "md" }, [
61
+ ui.heading("Theme", 4),
62
+ ui.themeToggle(),
63
+ ]),
64
+ ui.divider(),
65
+ ui.row({ justify: "end" }, [
66
+ ui.button({
67
+ label: "Save Settings",
68
+ type: "submit",
69
+ variant: "primary"
70
+ })
36
71
  ])
37
72
  ])
38
- ]),
39
- ui.col({ gap: "md" }, [
40
- ui.heading("Theme", 4),
41
- ui.themeToggle(),
42
73
  ])
43
- ])
44
- ]);
74
+ ]);
75
+ };