openbot 0.2.3 → 0.2.6
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/README.md +1 -1
- package/dist/agents/agent-creator.js +58 -19
- package/dist/agents/os-agent.js +1 -4
- package/dist/agents/planner-agent.js +32 -0
- package/dist/agents/topic-agent.js +1 -1
- package/dist/architecture/contracts.js +1 -0
- package/dist/architecture/execution-engine.js +151 -0
- package/dist/architecture/intent-classifier.js +26 -0
- package/dist/architecture/planner.js +106 -0
- package/dist/automation-worker.js +121 -0
- package/dist/automations.js +52 -0
- package/dist/cli.js +116 -146
- package/dist/config.js +20 -0
- package/dist/core/agents.js +41 -0
- package/dist/core/delegation.js +124 -0
- package/dist/core/manager.js +73 -0
- package/dist/core/plugins.js +77 -0
- package/dist/core/router.js +40 -0
- package/dist/installers.js +156 -0
- package/dist/marketplace.js +80 -0
- package/dist/open-bot.js +34 -157
- package/dist/orchestrator.js +247 -51
- package/dist/plugins/approval/index.js +107 -3
- package/dist/plugins/brain/index.js +17 -86
- package/dist/plugins/brain/memory.js +1 -1
- package/dist/plugins/brain/prompt.js +8 -13
- package/dist/plugins/brain/types.js +0 -15
- package/dist/plugins/file-system/index.js +8 -8
- package/dist/plugins/llm/context-shaping.js +177 -0
- package/dist/plugins/llm/index.js +223 -49
- package/dist/plugins/memory/index.js +220 -0
- package/dist/plugins/memory/memory.js +122 -0
- package/dist/plugins/memory/prompt.js +55 -0
- package/dist/plugins/memory/types.js +45 -0
- package/dist/plugins/shell/index.js +3 -3
- package/dist/plugins/skills/index.js +9 -9
- package/dist/registry/index.js +1 -4
- package/dist/registry/plugin-loader.js +361 -56
- package/dist/registry/plugin-registry.js +21 -4
- package/dist/registry/ts-agent-loader.js +4 -4
- package/dist/registry/yaml-agent-loader.js +78 -20
- package/dist/runtime/execution-trace.js +41 -0
- package/dist/runtime/intent-routing.js +26 -0
- package/dist/runtime/openbot-runtime.js +354 -0
- package/dist/server.js +513 -41
- package/dist/ui/widgets/approval-card.js +22 -2
- package/dist/ui/widgets/delegation.js +29 -0
- package/dist/version.js +62 -0
- package/package.json +4 -1
|
@@ -2,13 +2,49 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { execSync } from "node:child_process";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import matter from "gray-matter";
|
|
6
|
+
import { PluginRegistry } from "./plugin-registry.js";
|
|
7
|
+
import { llmPlugin } from "../plugins/llm/index.js";
|
|
8
|
+
import { createModel } from "../models.js";
|
|
9
|
+
import { resolvePath, DEFAULT_AGENT_MD } from "../config.js";
|
|
10
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
11
|
+
function toTitleCaseFromSlug(value) {
|
|
12
|
+
return value
|
|
13
|
+
.split(/[-_]+/)
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
16
|
+
.join(" ") || "Agent";
|
|
17
|
+
}
|
|
18
|
+
async function fileExists(filePath) {
|
|
19
|
+
return fs.access(filePath).then(() => true).catch(() => false);
|
|
20
|
+
}
|
|
21
|
+
async function findIndexFile(dir) {
|
|
22
|
+
for (const file of ["dist/index.js", "index.js", "index.ts"]) {
|
|
23
|
+
if (await fileExists(path.join(dir, file))) {
|
|
24
|
+
return path.join(dir, file);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
function resolveConfigPaths(config) {
|
|
30
|
+
if (typeof config === "string")
|
|
31
|
+
return resolvePath(config);
|
|
32
|
+
if (Array.isArray(config))
|
|
33
|
+
return config.map(resolveConfigPaths);
|
|
34
|
+
if (config !== null && typeof config === "object") {
|
|
35
|
+
const resolved = {};
|
|
36
|
+
for (const [key, value] of Object.entries(config)) {
|
|
37
|
+
resolved[key] = resolveConfigPaths(value);
|
|
38
|
+
}
|
|
39
|
+
return resolved;
|
|
40
|
+
}
|
|
41
|
+
return config;
|
|
42
|
+
}
|
|
43
|
+
// ── Metadata ─────────────────────────────────────────────────────────
|
|
8
44
|
export async function getPluginMetadata(pluginDir) {
|
|
9
45
|
const pkgPath = path.join(pluginDir, "package.json");
|
|
10
|
-
const hasPackageJson = await
|
|
11
|
-
let name =
|
|
46
|
+
const hasPackageJson = await fileExists(pkgPath);
|
|
47
|
+
let name = "Unnamed Plugin";
|
|
12
48
|
let description = "No description";
|
|
13
49
|
let version = "0.0.0";
|
|
14
50
|
if (hasPackageJson) {
|
|
@@ -18,33 +54,23 @@ export async function getPluginMetadata(pluginDir) {
|
|
|
18
54
|
description = pkg.description || description;
|
|
19
55
|
version = pkg.version || version;
|
|
20
56
|
}
|
|
21
|
-
catch {
|
|
22
|
-
// Fallback to defaults
|
|
23
|
-
}
|
|
57
|
+
catch { /* fallback to defaults */ }
|
|
24
58
|
}
|
|
25
59
|
return { name, description, version };
|
|
26
60
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Ensure a plugin is ready to run by installing dependencies and building if necessary.
|
|
29
|
-
*/
|
|
30
61
|
export async function ensurePluginReady(pluginDir) {
|
|
31
62
|
try {
|
|
32
63
|
const pkgPath = path.join(pluginDir, "package.json");
|
|
33
|
-
|
|
34
|
-
if (!hasPackageJson)
|
|
64
|
+
if (!(await fileExists(pkgPath)))
|
|
35
65
|
return;
|
|
36
66
|
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
37
67
|
const nodeModulesPath = path.join(pluginDir, "node_modules");
|
|
38
|
-
|
|
39
|
-
// 1. Install dependencies if node_modules is missing
|
|
40
|
-
if (!hasNodeModules) {
|
|
68
|
+
if (!(await fileExists(nodeModulesPath))) {
|
|
41
69
|
console.log(`[plugins] Installing dependencies for ${path.basename(pluginDir)}...`);
|
|
42
70
|
execSync("npm install", { cwd: pluginDir, stdio: "inherit" });
|
|
43
71
|
}
|
|
44
|
-
// 2. Run build if dist is missing but build script exists
|
|
45
72
|
const distPath = path.join(pluginDir, "dist");
|
|
46
|
-
|
|
47
|
-
if (!hasDist && pkg.scripts?.build) {
|
|
73
|
+
if (!(await fileExists(distPath)) && pkg.scripts?.build) {
|
|
48
74
|
console.log(`[plugins] Building ${path.basename(pluginDir)}...`);
|
|
49
75
|
execSync("npm run build", { cwd: pluginDir, stdio: "inherit" });
|
|
50
76
|
}
|
|
@@ -53,57 +79,81 @@ export async function ensurePluginReady(pluginDir) {
|
|
|
53
79
|
console.error(`[plugins] Failed to prepare plugin in ${pluginDir}:`, err);
|
|
54
80
|
}
|
|
55
81
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*/
|
|
60
|
-
export async function loadPluginsFromDir(dir) {
|
|
61
|
-
const plugins = [];
|
|
82
|
+
export async function readAgentConfig(agentDir) {
|
|
83
|
+
const mdPath = path.join(agentDir, "AGENT.md");
|
|
84
|
+
let mdContent = "";
|
|
62
85
|
try {
|
|
63
|
-
await fs.
|
|
86
|
+
mdContent = await fs.readFile(mdPath, "utf-8");
|
|
64
87
|
}
|
|
65
88
|
catch {
|
|
66
|
-
|
|
67
|
-
return plugins;
|
|
89
|
+
mdContent = DEFAULT_AGENT_MD;
|
|
68
90
|
}
|
|
91
|
+
const parsed = matter(mdContent);
|
|
92
|
+
const config = (parsed.data || {});
|
|
93
|
+
return {
|
|
94
|
+
name: typeof config.name === "string" ? config.name : "",
|
|
95
|
+
description: typeof config.description === "string" ? config.description : "",
|
|
96
|
+
model: config.model,
|
|
97
|
+
image: config.image,
|
|
98
|
+
plugins: config.plugins || [],
|
|
99
|
+
instructions: parsed.content.trim() || "",
|
|
100
|
+
subscribe: config.subscribe,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// ── Agent composition (declarative AGENT.md agents) ──────────────────
|
|
104
|
+
function composeAgentFromConfig(config, toolRegistry, model) {
|
|
105
|
+
const allToolDefinitions = {};
|
|
106
|
+
const pluginFactories = [];
|
|
107
|
+
for (const pluginItem of config.plugins) {
|
|
108
|
+
const isString = typeof pluginItem === "string";
|
|
109
|
+
const pluginName = isString ? pluginItem : pluginItem.name;
|
|
110
|
+
const pluginConfig = isString ? {} : (pluginItem.config || {});
|
|
111
|
+
const resolvedConfig = resolveConfigPaths(pluginConfig);
|
|
112
|
+
const entry = toolRegistry.get(pluginName);
|
|
113
|
+
if (!entry || entry.type !== "tool") {
|
|
114
|
+
console.warn(`[plugins] "${config.name}": tool "${pluginName}" not found — skipping`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
pluginFactories.push({ plugin: entry.plugin, config: resolvedConfig });
|
|
118
|
+
Object.assign(allToolDefinitions, entry.toolDefinitions);
|
|
119
|
+
}
|
|
120
|
+
const plugin = (builder) => {
|
|
121
|
+
for (const { plugin: toolPlugin, config: resolvedConfig } of pluginFactories) {
|
|
122
|
+
builder.use(toolPlugin({ ...resolvedConfig, model }));
|
|
123
|
+
}
|
|
124
|
+
builder.use(llmPlugin({
|
|
125
|
+
model,
|
|
126
|
+
system: config.instructions,
|
|
127
|
+
toolDefinitions: allToolDefinitions,
|
|
128
|
+
}));
|
|
129
|
+
};
|
|
130
|
+
return { plugin, toolDefinitions: allToolDefinitions };
|
|
131
|
+
}
|
|
132
|
+
// ── Load tool plugins from a subdirectory (used for agent-local tools) ─
|
|
133
|
+
async function loadToolPluginsFromDir(dir) {
|
|
134
|
+
const plugins = [];
|
|
135
|
+
if (!(await fileExists(dir)))
|
|
136
|
+
return plugins;
|
|
69
137
|
try {
|
|
70
138
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
71
139
|
for (const entry of entries) {
|
|
72
|
-
if (!entry.isDirectory())
|
|
73
|
-
continue;
|
|
74
|
-
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
140
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
75
141
|
continue;
|
|
76
142
|
const pluginDir = path.join(dir, entry.name);
|
|
77
|
-
// Ensure plugin is ready (dependencies, build)
|
|
78
143
|
await ensurePluginReady(pluginDir);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const possibleIndices = ["dist/index.js", "index.js", "index.ts"];
|
|
82
|
-
for (const file of possibleIndices) {
|
|
83
|
-
try {
|
|
84
|
-
const fullPath = path.join(pluginDir, file);
|
|
85
|
-
await fs.access(fullPath);
|
|
86
|
-
indexPath = fullPath;
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
if (!indexPath) {
|
|
94
|
-
console.warn(`[plugins] No index.js or index.ts found in ${pluginDir}`);
|
|
144
|
+
const indexPath = await findIndexFile(pluginDir);
|
|
145
|
+
if (!indexPath)
|
|
95
146
|
continue;
|
|
96
|
-
}
|
|
97
147
|
try {
|
|
98
|
-
|
|
99
|
-
const moduleUrl = pathToFileURL(indexPath).href;
|
|
100
|
-
const module = await import(moduleUrl);
|
|
101
|
-
// The plugin can be exported as 'plugin', 'default', or 'entry'
|
|
148
|
+
const module = await import(pathToFileURL(indexPath).href);
|
|
102
149
|
const entryData = module.plugin || module.default || module.entry;
|
|
103
150
|
if (entryData && typeof entryData.factory === "function") {
|
|
104
151
|
plugins.push({
|
|
105
|
-
|
|
106
|
-
|
|
152
|
+
name: entryData.name || entry.name,
|
|
153
|
+
description: entryData.description || `Tool plugin ${entry.name}`,
|
|
154
|
+
type: "tool",
|
|
155
|
+
plugin: entryData.factory,
|
|
156
|
+
toolDefinitions: entryData.toolDefinitions || {},
|
|
107
157
|
});
|
|
108
158
|
}
|
|
109
159
|
else {
|
|
@@ -111,7 +161,7 @@ export async function loadPluginsFromDir(dir) {
|
|
|
111
161
|
}
|
|
112
162
|
}
|
|
113
163
|
catch (err) {
|
|
114
|
-
console.error(`[plugins] Failed to load plugin "${entry.name}":`, err);
|
|
164
|
+
console.error(`[plugins] Failed to load tool plugin "${entry.name}":`, err);
|
|
115
165
|
}
|
|
116
166
|
}
|
|
117
167
|
}
|
|
@@ -120,3 +170,258 @@ export async function loadPluginsFromDir(dir) {
|
|
|
120
170
|
}
|
|
121
171
|
return plugins;
|
|
122
172
|
}
|
|
173
|
+
// ── Main unified discovery ───────────────────────────────────────────
|
|
174
|
+
/**
|
|
175
|
+
* Discover all plugins (tools + agents) from a directory.
|
|
176
|
+
*
|
|
177
|
+
* Pass 1: Load code plugins in folders without AGENT.md.
|
|
178
|
+
* - module.agent export → code-only agent
|
|
179
|
+
* - plugin/default/entry export → tool plugin
|
|
180
|
+
* Pass 2: Load agent-type plugins (folders WITH AGENT.md).
|
|
181
|
+
* - AGENT.md only → declarative agent (auto-wrapped with llmPlugin)
|
|
182
|
+
* - AGENT.md + index.ts → TS agent (user controls logic, AGENT.md for UI editing)
|
|
183
|
+
*
|
|
184
|
+
* Discovered entries are registered directly into the provided registry.
|
|
185
|
+
*/
|
|
186
|
+
export async function discoverPlugins(dir, registry, defaultModel, options) {
|
|
187
|
+
try {
|
|
188
|
+
await fs.mkdir(dir, { recursive: true });
|
|
189
|
+
}
|
|
190
|
+
catch { /* best effort */ }
|
|
191
|
+
let entries;
|
|
192
|
+
try {
|
|
193
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Classify each subdirectory
|
|
199
|
+
const codeDirs = [];
|
|
200
|
+
const agentDirs = [];
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
203
|
+
continue;
|
|
204
|
+
const pluginDir = path.join(dir, entry.name);
|
|
205
|
+
const hasAgentMd = await fileExists(path.join(pluginDir, "AGENT.md"));
|
|
206
|
+
const hasIndex = !!(await findIndexFile(pluginDir));
|
|
207
|
+
const hasPkg = await fileExists(path.join(pluginDir, "package.json"));
|
|
208
|
+
if (hasAgentMd) {
|
|
209
|
+
agentDirs.push({ dir: pluginDir, hasIndex: hasIndex || hasPkg });
|
|
210
|
+
}
|
|
211
|
+
else if (hasIndex || hasPkg) {
|
|
212
|
+
codeDirs.push(pluginDir);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Pass 1: code-only agents and tool plugins
|
|
216
|
+
for (const pluginDir of codeDirs) {
|
|
217
|
+
await ensurePluginReady(pluginDir);
|
|
218
|
+
const indexPath = await findIndexFile(pluginDir);
|
|
219
|
+
if (!indexPath)
|
|
220
|
+
continue;
|
|
221
|
+
try {
|
|
222
|
+
const module = await import(pathToFileURL(indexPath).href);
|
|
223
|
+
const codeAgentDef = module.agent;
|
|
224
|
+
const entryData = module.plugin || module.default || module.entry;
|
|
225
|
+
if (codeAgentDef && typeof codeAgentDef.factory === "function") {
|
|
226
|
+
const meta = await getPluginMetadata(pluginDir);
|
|
227
|
+
const folderName = path.basename(pluginDir);
|
|
228
|
+
let name = codeAgentDef.name || meta.name;
|
|
229
|
+
if (!name || /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(name)) {
|
|
230
|
+
name = toTitleCaseFromSlug(folderName);
|
|
231
|
+
}
|
|
232
|
+
const description = codeAgentDef.description || meta.description || "Code Agent";
|
|
233
|
+
registry.register({
|
|
234
|
+
name,
|
|
235
|
+
description,
|
|
236
|
+
type: "agent",
|
|
237
|
+
plugin: codeAgentDef.factory({ ...options, model: defaultModel }),
|
|
238
|
+
capabilities: codeAgentDef.capabilities,
|
|
239
|
+
subscribe: codeAgentDef.subscribe,
|
|
240
|
+
folder: pluginDir,
|
|
241
|
+
});
|
|
242
|
+
console.log(`[plugins] Loaded code-only agent: ${name} — ${description}`);
|
|
243
|
+
}
|
|
244
|
+
else if (entryData && typeof entryData.factory === "function") {
|
|
245
|
+
const meta = await getPluginMetadata(pluginDir);
|
|
246
|
+
const folderName = path.basename(pluginDir);
|
|
247
|
+
let name = entryData.name || meta.name;
|
|
248
|
+
if (!name || /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(name)) {
|
|
249
|
+
name = toTitleCaseFromSlug(folderName);
|
|
250
|
+
}
|
|
251
|
+
const pluginEntry = {
|
|
252
|
+
name,
|
|
253
|
+
description: entryData.description || meta.description || "Tool plugin",
|
|
254
|
+
type: "tool",
|
|
255
|
+
plugin: entryData.factory,
|
|
256
|
+
toolDefinitions: entryData.toolDefinitions || {},
|
|
257
|
+
folder: pluginDir,
|
|
258
|
+
};
|
|
259
|
+
registry.register(pluginEntry);
|
|
260
|
+
console.log(`[plugins] Loaded tool: ${pluginEntry.name}`);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
console.warn(`[plugins] "${path.basename(pluginDir)}" does not export a valid plugin (missing factory)`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
console.error(`[plugins] Failed to load "${path.basename(pluginDir)}":`, err);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
// Pass 2: agent plugins
|
|
271
|
+
for (const { dir: agentDir, hasIndex } of agentDirs) {
|
|
272
|
+
const folderName = path.basename(agentDir);
|
|
273
|
+
try {
|
|
274
|
+
if (hasIndex) {
|
|
275
|
+
// TS Agent — has AGENT.md + code. User controls logic; AGENT.md is for UI editing.
|
|
276
|
+
await ensurePluginReady(agentDir);
|
|
277
|
+
const indexPath = await findIndexFile(agentDir);
|
|
278
|
+
if (!indexPath)
|
|
279
|
+
continue;
|
|
280
|
+
const module = await import(pathToFileURL(indexPath).href);
|
|
281
|
+
const definition = module.agent || module.plugin || module.default || module.entry;
|
|
282
|
+
if (definition && typeof definition.factory === "function") {
|
|
283
|
+
const config = await readAgentConfig(agentDir);
|
|
284
|
+
const meta = await getPluginMetadata(agentDir);
|
|
285
|
+
let name = config.name || definition.name || meta.name;
|
|
286
|
+
if (!name || /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(name)) {
|
|
287
|
+
name = toTitleCaseFromSlug(folderName);
|
|
288
|
+
}
|
|
289
|
+
const description = definition.description || config.description || "TS Agent";
|
|
290
|
+
registry.register({
|
|
291
|
+
name,
|
|
292
|
+
description,
|
|
293
|
+
type: "agent",
|
|
294
|
+
plugin: definition.factory({ ...options, model: defaultModel }),
|
|
295
|
+
capabilities: definition.capabilities,
|
|
296
|
+
subscribe: definition.subscribe || config.subscribe,
|
|
297
|
+
folder: agentDir,
|
|
298
|
+
});
|
|
299
|
+
console.log(`[plugins] Loaded TS agent: ${name} — ${description}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
// Declarative Agent — AGENT.md only, auto-wrapped with llmPlugin.
|
|
304
|
+
const config = await readAgentConfig(agentDir);
|
|
305
|
+
const meta = await getPluginMetadata(agentDir);
|
|
306
|
+
let resolvedName = config.name || meta.name;
|
|
307
|
+
if (!resolvedName || /^Unnamed\s+(Plugin|Tool|Agent)$/i.test(resolvedName)) {
|
|
308
|
+
resolvedName = toTitleCaseFromSlug(folderName);
|
|
309
|
+
}
|
|
310
|
+
const resolvedDescription = config.description || meta.description || "No description";
|
|
311
|
+
const agentModel = config.model
|
|
312
|
+
? createModel({ ...options, model: config.model })
|
|
313
|
+
: defaultModel;
|
|
314
|
+
// Load agent-local tool plugins
|
|
315
|
+
const localPlugins = await loadToolPluginsFromDir(path.join(agentDir, "plugins"));
|
|
316
|
+
// Scoped registry: global tools + local tools
|
|
317
|
+
const scopedRegistry = new PluginRegistry();
|
|
318
|
+
for (const p of registry.getTools()) {
|
|
319
|
+
scopedRegistry.register(p);
|
|
320
|
+
}
|
|
321
|
+
for (const p of localPlugins) {
|
|
322
|
+
scopedRegistry.register(p);
|
|
323
|
+
}
|
|
324
|
+
// Initialize AGENT.md if missing
|
|
325
|
+
const agentMdPath = path.join(agentDir, "AGENT.md");
|
|
326
|
+
if (!(await fileExists(agentMdPath))) {
|
|
327
|
+
const content = DEFAULT_AGENT_MD.replace("name: Agent", `name: ${resolvedName}`);
|
|
328
|
+
await fs.writeFile(agentMdPath, content, "utf-8");
|
|
329
|
+
console.log(`[plugins] Initialized ${resolvedName}/AGENT.md`);
|
|
330
|
+
}
|
|
331
|
+
const { plugin, toolDefinitions } = composeAgentFromConfig(config, scopedRegistry, agentModel);
|
|
332
|
+
registry.register({
|
|
333
|
+
name: resolvedName,
|
|
334
|
+
description: resolvedDescription,
|
|
335
|
+
type: "agent",
|
|
336
|
+
plugin,
|
|
337
|
+
capabilities: Object.fromEntries(Object.entries(toolDefinitions).map(([name, def]) => [name, def.description])),
|
|
338
|
+
subscribe: config.subscribe,
|
|
339
|
+
folder: agentDir,
|
|
340
|
+
});
|
|
341
|
+
console.log(`[plugins] Loaded agent: ${resolvedName} — ${resolvedDescription}${config.model ? ` (model: ${config.model})` : ""}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
if (err.code !== "ENOENT") {
|
|
346
|
+
console.warn(`[plugins] Error loading "${folderName}":`, err);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// ── Lightweight listing (for API) ────────────────────────────────────
|
|
352
|
+
export async function listPlugins(dir) {
|
|
353
|
+
const plugins = [];
|
|
354
|
+
try {
|
|
355
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
358
|
+
continue;
|
|
359
|
+
const pluginDir = path.join(dir, entry.name);
|
|
360
|
+
const hasAgentMd = await fileExists(path.join(pluginDir, "AGENT.md"));
|
|
361
|
+
const hasCode = await fileExists(path.join(pluginDir, "package.json"))
|
|
362
|
+
|| !!(await findIndexFile(pluginDir));
|
|
363
|
+
if (hasAgentMd) {
|
|
364
|
+
const config = await readAgentConfig(pluginDir);
|
|
365
|
+
const { name: fallbackName, description: fallbackDescription } = await getPluginMetadata(pluginDir);
|
|
366
|
+
plugins.push({
|
|
367
|
+
name: config.name || fallbackName || "Unnamed Agent",
|
|
368
|
+
description: config.description || fallbackDescription || "No description",
|
|
369
|
+
folder: pluginDir,
|
|
370
|
+
type: "agent",
|
|
371
|
+
hasAgentMd: true,
|
|
372
|
+
image: config.image,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
else if (hasCode) {
|
|
376
|
+
await ensurePluginReady(pluginDir);
|
|
377
|
+
const indexPath = await findIndexFile(pluginDir);
|
|
378
|
+
const { name: fallbackName, description: fallbackDescription } = await getPluginMetadata(pluginDir);
|
|
379
|
+
if (!indexPath) {
|
|
380
|
+
plugins.push({
|
|
381
|
+
name: fallbackName,
|
|
382
|
+
description: fallbackDescription,
|
|
383
|
+
folder: pluginDir,
|
|
384
|
+
type: "tool",
|
|
385
|
+
hasAgentMd: false,
|
|
386
|
+
});
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const module = await import(pathToFileURL(indexPath).href);
|
|
391
|
+
const codeAgentDef = module.agent;
|
|
392
|
+
const toolEntry = module.plugin || module.default || module.entry;
|
|
393
|
+
if (codeAgentDef && typeof codeAgentDef.factory === "function") {
|
|
394
|
+
plugins.push({
|
|
395
|
+
name: codeAgentDef.name || fallbackName || "Unnamed Agent",
|
|
396
|
+
description: codeAgentDef.description || fallbackDescription || "Code Agent",
|
|
397
|
+
folder: pluginDir,
|
|
398
|
+
type: "agent",
|
|
399
|
+
hasAgentMd: false,
|
|
400
|
+
image: codeAgentDef.image,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
else if (toolEntry && typeof toolEntry.factory === "function") {
|
|
404
|
+
plugins.push({
|
|
405
|
+
name: toolEntry.name || fallbackName,
|
|
406
|
+
description: toolEntry.description || fallbackDescription,
|
|
407
|
+
folder: pluginDir,
|
|
408
|
+
type: "tool",
|
|
409
|
+
hasAgentMd: false,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
plugins.push({
|
|
415
|
+
name: fallbackName,
|
|
416
|
+
description: fallbackDescription,
|
|
417
|
+
folder: pluginDir,
|
|
418
|
+
type: "tool",
|
|
419
|
+
hasAgentMd: false,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
catch { /* directory doesn't exist */ }
|
|
426
|
+
return plugins;
|
|
427
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plugin Registry
|
|
2
|
+
* Unified Plugin Registry
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Built-in
|
|
6
|
-
*
|
|
4
|
+
* Holds both tool plugins and agent plugins in a single registry.
|
|
5
|
+
* Built-in entries are registered at startup; community plugins
|
|
6
|
+
* are discovered from ~/.openbot/plugins/.
|
|
7
7
|
*/
|
|
8
8
|
export class PluginRegistry {
|
|
9
9
|
constructor() {
|
|
10
10
|
this.plugins = new Map();
|
|
11
11
|
}
|
|
12
12
|
register(entry) {
|
|
13
|
+
if (this.plugins.has(entry.name)) {
|
|
14
|
+
console.warn(`Plugin "${entry.name}" is already registered — overwriting`);
|
|
15
|
+
}
|
|
13
16
|
this.plugins.set(entry.name, entry);
|
|
14
17
|
}
|
|
15
18
|
get(name) {
|
|
@@ -24,4 +27,18 @@ export class PluginRegistry {
|
|
|
24
27
|
getNames() {
|
|
25
28
|
return Array.from(this.plugins.keys());
|
|
26
29
|
}
|
|
30
|
+
getAgents() {
|
|
31
|
+
return this.getAll().filter(p => p.type === "agent");
|
|
32
|
+
}
|
|
33
|
+
getTools() {
|
|
34
|
+
return this.getAll().filter(p => p.type === "tool");
|
|
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) {
|
|
40
|
+
throw new Error("No agents registered — at least one agent is required");
|
|
41
|
+
}
|
|
42
|
+
return names;
|
|
43
|
+
}
|
|
27
44
|
}
|
|
@@ -22,11 +22,11 @@ export async function discoverTsAgents(agentsDir, defaultModel, options) {
|
|
|
22
22
|
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
23
23
|
continue;
|
|
24
24
|
const agentDir = path.join(agentsDir, entry.name);
|
|
25
|
-
// We only consider it a TS agent if it doesn't have an
|
|
25
|
+
// We only consider it a TS agent if it doesn't have an AGENT.md
|
|
26
26
|
// (This avoids double-loading if someone has both for some reason)
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
27
|
+
const mdPath = path.join(agentDir, "AGENT.md");
|
|
28
|
+
const hasMd = await fs.access(mdPath).then(() => true).catch(() => false);
|
|
29
|
+
if (hasMd)
|
|
30
30
|
continue;
|
|
31
31
|
// Check for package.json to see if it's a package
|
|
32
32
|
const pkgPath = path.join(agentDir, "package.json");
|