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.
- package/dist/agents/os-agent.js +0 -4
- package/dist/agents/topic-agent.js +32 -0
- package/dist/cli.js +115 -9
- package/dist/handlers/settings.js +29 -0
- package/dist/models.js +1 -8
- package/dist/open-bot.js +72 -38
- package/dist/plugins/brain/identity.js +5 -4
- package/dist/plugins/brain/index.js +4 -1
- package/dist/plugins/brain/prompt.js +7 -7
- package/dist/plugins/brain/types.js +1 -1
- package/dist/plugins/file-system/index.js +4 -0
- package/dist/plugins/llm/index.js +10 -2
- package/dist/plugins/shell/index.js +4 -0
- package/dist/plugins/skills/index.js +9 -0
- package/dist/registry/index.js +1 -0
- package/dist/registry/plugin-loader.js +100 -0
- package/dist/registry/yaml-agent-loader.js +61 -18
- package/dist/server.js +0 -2
- package/dist/session.js +18 -8
- package/dist/ui/layout.js +3 -2
- package/dist/ui/settings.js +70 -39
- package/dist/ui/sidebar.js +26 -25
- package/package.json +2 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
};
|
package/dist/ui/settings.js
CHANGED
|
@@ -1,44 +1,75 @@
|
|
|
1
1
|
import { ui } from "@melony/ui-kit";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
ui.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
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
|
+
};
|