openbot 0.2.2 → 0.2.5
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 +54 -141
- 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 +170 -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 +339 -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 +549 -31
- 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 +8 -7
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
// --- Prompt Builder ---
|
|
4
|
+
/**
|
|
5
|
+
* Build the memory's section of the system prompt.
|
|
6
|
+
*
|
|
7
|
+
* Includes only what the memory owns:
|
|
8
|
+
* - Environment context
|
|
9
|
+
* - Agent definition (from AGENT.md)
|
|
10
|
+
* - A handful of the most recent memories
|
|
11
|
+
* - Memory capability instructions
|
|
12
|
+
*
|
|
13
|
+
* Skills are handled by the separate skills plugin and composed
|
|
14
|
+
* at the top level in open-bot.ts.
|
|
15
|
+
*/
|
|
16
|
+
export async function buildMemoryPrompt(baseDir, modules, context) {
|
|
17
|
+
const parts = [];
|
|
18
|
+
const state = context?.state;
|
|
19
|
+
const currentCwd = state?.cwd || process.cwd();
|
|
20
|
+
// 1. Environment context
|
|
21
|
+
const now = new Date();
|
|
22
|
+
parts.push(`<environment>
|
|
23
|
+
- Time: ${now.toLocaleString()} (${Intl.DateTimeFormat().resolvedOptions().timeZone})
|
|
24
|
+
- CWD: ${currentCwd}
|
|
25
|
+
- Bot Home: ${baseDir}
|
|
26
|
+
</environment>`);
|
|
27
|
+
// 2. Agent definition (manual edit only)
|
|
28
|
+
try {
|
|
29
|
+
const agentPath = path.join(baseDir, "AGENT.md");
|
|
30
|
+
const agentMd = await fs.readFile(agentPath, "utf-8");
|
|
31
|
+
if (agentMd.trim()) {
|
|
32
|
+
parts.push(`<agent_definition>\n${agentMd.trim()}\n</agent_definition>`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Skip if AGENT.md doesn't exist yet
|
|
37
|
+
}
|
|
38
|
+
// 3. Recent memories (lean — just a few to keep context fresh)
|
|
39
|
+
const recentFacts = await modules.memory.getRecentFacts(5);
|
|
40
|
+
if (recentFacts.length > 0) {
|
|
41
|
+
const factsList = recentFacts
|
|
42
|
+
.map((f) => `- ${f.content}${f.tags.length > 0 ? ` [${f.tags.join(", ")}]` : ""}`)
|
|
43
|
+
.join("\n");
|
|
44
|
+
parts.push(`<recent_memories>\n${factsList}\n</recent_memories>`);
|
|
45
|
+
}
|
|
46
|
+
// 4. Memory capabilities
|
|
47
|
+
parts.push(`<memory_tools>
|
|
48
|
+
Use these to manage your persistent state:
|
|
49
|
+
- \`remember(content, tags)\`: Store facts/preferences
|
|
50
|
+
- \`recall(query, tags)\`: Search long-term memory
|
|
51
|
+
- \`forget(memoryId)\`: Remove outdated info
|
|
52
|
+
- \`journal(content)\`: Record session reflections
|
|
53
|
+
</memory_tools>`);
|
|
54
|
+
return `\n${parts.join("\n\n")}\n`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
// --- Tool Definitions ---
|
|
3
|
+
export const memoryToolDefinitions = {
|
|
4
|
+
// Memory tools
|
|
5
|
+
remember: {
|
|
6
|
+
description: "Store something important in long-term memory. Use for user preferences, learned facts, project context, etc.",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
content: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe("The information to remember"),
|
|
11
|
+
tags: z
|
|
12
|
+
.array(z.string())
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Tags for categorization (e.g., 'user-preference', 'project', 'learning')"),
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
recall: {
|
|
18
|
+
description: "Search your memory for relevant information. Use before answering questions that might relate to past interactions.",
|
|
19
|
+
inputSchema: z.object({
|
|
20
|
+
query: z.string().describe("What to search for in memory"),
|
|
21
|
+
tags: z
|
|
22
|
+
.array(z.string())
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Filter by specific tags"),
|
|
25
|
+
limit: z
|
|
26
|
+
.number()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe("Max results to return (default: 5)"),
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
forget: {
|
|
32
|
+
description: "Remove a specific memory entry by ID.",
|
|
33
|
+
inputSchema: z.object({
|
|
34
|
+
memoryId: z
|
|
35
|
+
.string()
|
|
36
|
+
.describe("The ID of the memory entry to remove"),
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
journal: {
|
|
40
|
+
description: "Add a journal entry for today. Use for session notes, learnings, and reflections.",
|
|
41
|
+
inputSchema: z.object({
|
|
42
|
+
content: z.string().describe("Journal entry content"),
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -42,7 +42,7 @@ export const shellPlugin = (options = {}) => (builder) => {
|
|
|
42
42
|
data: { message: `Directory changed to ${newCwd}`, severity: "success" }
|
|
43
43
|
};
|
|
44
44
|
yield {
|
|
45
|
-
type: "action:
|
|
45
|
+
type: "action:result",
|
|
46
46
|
data: {
|
|
47
47
|
action: "executeCommand",
|
|
48
48
|
toolCallId,
|
|
@@ -62,7 +62,7 @@ export const shellPlugin = (options = {}) => (builder) => {
|
|
|
62
62
|
data: { message: `Command executed successfully`, severity: "success" }
|
|
63
63
|
};
|
|
64
64
|
yield {
|
|
65
|
-
type: "action:
|
|
65
|
+
type: "action:result",
|
|
66
66
|
data: {
|
|
67
67
|
action: "executeCommand",
|
|
68
68
|
toolCallId,
|
|
@@ -76,7 +76,7 @@ export const shellPlugin = (options = {}) => (builder) => {
|
|
|
76
76
|
}
|
|
77
77
|
catch (error) {
|
|
78
78
|
yield {
|
|
79
|
-
type: "action:
|
|
79
|
+
type: "action:result",
|
|
80
80
|
data: {
|
|
81
81
|
action: "executeCommand",
|
|
82
82
|
toolCallId,
|
|
@@ -116,7 +116,7 @@ You have no skills yet. When you learn reusable patterns, create skills using \`
|
|
|
116
116
|
// --- Prompt Builder ---
|
|
117
117
|
/**
|
|
118
118
|
* Create a prompt builder that generates the skills section
|
|
119
|
-
* of the system prompt. Use alongside the
|
|
119
|
+
* of the system prompt. Use alongside the memory prompt builder.
|
|
120
120
|
*/
|
|
121
121
|
export function createSkillsPromptBuilder(baseDir) {
|
|
122
122
|
const expandedBase = expandPath(baseDir);
|
|
@@ -128,7 +128,7 @@ export function createSkillsPromptBuilder(baseDir) {
|
|
|
128
128
|
* Skills Plugin for Melony
|
|
129
129
|
*
|
|
130
130
|
* Manages reusable skill definitions: load, create, update, list.
|
|
131
|
-
* Fully independent from the
|
|
131
|
+
* Fully independent from the memory plugin.
|
|
132
132
|
*/
|
|
133
133
|
export const skillsPlugin = (options) => (builder) => {
|
|
134
134
|
const expandedBase = expandPath(options.baseDir);
|
|
@@ -155,7 +155,7 @@ export const skillsPlugin = (options) => (builder) => {
|
|
|
155
155
|
},
|
|
156
156
|
};
|
|
157
157
|
yield {
|
|
158
|
-
type: "action:
|
|
158
|
+
type: "action:result",
|
|
159
159
|
data: {
|
|
160
160
|
action: "loadSkill",
|
|
161
161
|
toolCallId,
|
|
@@ -165,7 +165,7 @@ export const skillsPlugin = (options) => (builder) => {
|
|
|
165
165
|
}
|
|
166
166
|
catch {
|
|
167
167
|
yield {
|
|
168
|
-
type: "action:
|
|
168
|
+
type: "action:result",
|
|
169
169
|
data: {
|
|
170
170
|
action: "loadSkill",
|
|
171
171
|
toolCallId,
|
|
@@ -179,7 +179,7 @@ export const skillsPlugin = (options) => (builder) => {
|
|
|
179
179
|
const { toolCallId } = event.data;
|
|
180
180
|
const skillsList = await skills.list();
|
|
181
181
|
yield {
|
|
182
|
-
type: "action:
|
|
182
|
+
type: "action:result",
|
|
183
183
|
data: {
|
|
184
184
|
action: "listSkills",
|
|
185
185
|
toolCallId,
|
|
@@ -197,7 +197,7 @@ export const skillsPlugin = (options) => (builder) => {
|
|
|
197
197
|
data: { message: `Skill "${title}" created`, severity: "success" },
|
|
198
198
|
};
|
|
199
199
|
yield {
|
|
200
|
-
type: "action:
|
|
200
|
+
type: "action:result",
|
|
201
201
|
data: {
|
|
202
202
|
action: "createSkill",
|
|
203
203
|
toolCallId,
|
|
@@ -218,7 +218,7 @@ export const skillsPlugin = (options) => (builder) => {
|
|
|
218
218
|
},
|
|
219
219
|
};
|
|
220
220
|
yield {
|
|
221
|
-
type: "action:
|
|
221
|
+
type: "action:result",
|
|
222
222
|
data: {
|
|
223
223
|
action: "createSkill",
|
|
224
224
|
toolCallId,
|
|
@@ -240,7 +240,7 @@ export const skillsPlugin = (options) => (builder) => {
|
|
|
240
240
|
},
|
|
241
241
|
};
|
|
242
242
|
yield {
|
|
243
|
-
type: "action:
|
|
243
|
+
type: "action:result",
|
|
244
244
|
data: {
|
|
245
245
|
action: "updateSkill",
|
|
246
246
|
toolCallId,
|
|
@@ -265,7 +265,7 @@ export const skillsPlugin = (options) => (builder) => {
|
|
|
265
265
|
},
|
|
266
266
|
};
|
|
267
267
|
yield {
|
|
268
|
-
type: "action:
|
|
268
|
+
type: "action:result",
|
|
269
269
|
data: {
|
|
270
270
|
action: "updateSkill",
|
|
271
271
|
toolCallId,
|
package/dist/registry/index.js
CHANGED
|
@@ -1,5 +1,2 @@
|
|
|
1
1
|
export { PluginRegistry } from "./plugin-registry.js";
|
|
2
|
-
export {
|
|
3
|
-
export { discoverYamlAgents, listYamlAgents } from "./yaml-agent-loader.js";
|
|
4
|
-
export { discoverTsAgents } from "./ts-agent-loader.js";
|
|
5
|
-
export { loadPluginsFromDir } from "./plugin-loader.js";
|
|
2
|
+
export { discoverPlugins, listPlugins, readAgentConfig, getPluginMetadata, ensurePluginReady, } from "./plugin-loader.js";
|
|
@@ -2,13 +2,42 @@ 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
|
+
async function fileExists(filePath) {
|
|
12
|
+
return fs.access(filePath).then(() => true).catch(() => false);
|
|
13
|
+
}
|
|
14
|
+
async function findIndexFile(dir) {
|
|
15
|
+
for (const file of ["dist/index.js", "index.js", "index.ts"]) {
|
|
16
|
+
if (await fileExists(path.join(dir, file))) {
|
|
17
|
+
return path.join(dir, file);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
function resolveConfigPaths(config) {
|
|
23
|
+
if (typeof config === "string")
|
|
24
|
+
return resolvePath(config);
|
|
25
|
+
if (Array.isArray(config))
|
|
26
|
+
return config.map(resolveConfigPaths);
|
|
27
|
+
if (config !== null && typeof config === "object") {
|
|
28
|
+
const resolved = {};
|
|
29
|
+
for (const [key, value] of Object.entries(config)) {
|
|
30
|
+
resolved[key] = resolveConfigPaths(value);
|
|
31
|
+
}
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
return config;
|
|
35
|
+
}
|
|
36
|
+
// ── Metadata ─────────────────────────────────────────────────────────
|
|
8
37
|
export async function getPluginMetadata(pluginDir) {
|
|
9
38
|
const pkgPath = path.join(pluginDir, "package.json");
|
|
10
|
-
const hasPackageJson = await
|
|
11
|
-
let name =
|
|
39
|
+
const hasPackageJson = await fileExists(pkgPath);
|
|
40
|
+
let name = "Unnamed Plugin";
|
|
12
41
|
let description = "No description";
|
|
13
42
|
let version = "0.0.0";
|
|
14
43
|
if (hasPackageJson) {
|
|
@@ -18,33 +47,23 @@ export async function getPluginMetadata(pluginDir) {
|
|
|
18
47
|
description = pkg.description || description;
|
|
19
48
|
version = pkg.version || version;
|
|
20
49
|
}
|
|
21
|
-
catch {
|
|
22
|
-
// Fallback to defaults
|
|
23
|
-
}
|
|
50
|
+
catch { /* fallback to defaults */ }
|
|
24
51
|
}
|
|
25
52
|
return { name, description, version };
|
|
26
53
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Ensure a plugin is ready to run by installing dependencies and building if necessary.
|
|
29
|
-
*/
|
|
30
54
|
export async function ensurePluginReady(pluginDir) {
|
|
31
55
|
try {
|
|
32
56
|
const pkgPath = path.join(pluginDir, "package.json");
|
|
33
|
-
|
|
34
|
-
if (!hasPackageJson)
|
|
57
|
+
if (!(await fileExists(pkgPath)))
|
|
35
58
|
return;
|
|
36
59
|
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
37
60
|
const nodeModulesPath = path.join(pluginDir, "node_modules");
|
|
38
|
-
|
|
39
|
-
// 1. Install dependencies if node_modules is missing
|
|
40
|
-
if (!hasNodeModules) {
|
|
61
|
+
if (!(await fileExists(nodeModulesPath))) {
|
|
41
62
|
console.log(`[plugins] Installing dependencies for ${path.basename(pluginDir)}...`);
|
|
42
63
|
execSync("npm install", { cwd: pluginDir, stdio: "inherit" });
|
|
43
64
|
}
|
|
44
|
-
// 2. Run build if dist is missing but build script exists
|
|
45
65
|
const distPath = path.join(pluginDir, "dist");
|
|
46
|
-
|
|
47
|
-
if (!hasDist && pkg.scripts?.build) {
|
|
66
|
+
if (!(await fileExists(distPath)) && pkg.scripts?.build) {
|
|
48
67
|
console.log(`[plugins] Building ${path.basename(pluginDir)}...`);
|
|
49
68
|
execSync("npm run build", { cwd: pluginDir, stdio: "inherit" });
|
|
50
69
|
}
|
|
@@ -53,57 +72,81 @@ export async function ensurePluginReady(pluginDir) {
|
|
|
53
72
|
console.error(`[plugins] Failed to prepare plugin in ${pluginDir}:`, err);
|
|
54
73
|
}
|
|
55
74
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*/
|
|
60
|
-
export async function loadPluginsFromDir(dir) {
|
|
61
|
-
const plugins = [];
|
|
75
|
+
export async function readAgentConfig(agentDir) {
|
|
76
|
+
const mdPath = path.join(agentDir, "AGENT.md");
|
|
77
|
+
let mdContent = "";
|
|
62
78
|
try {
|
|
63
|
-
await fs.
|
|
79
|
+
mdContent = await fs.readFile(mdPath, "utf-8");
|
|
64
80
|
}
|
|
65
81
|
catch {
|
|
66
|
-
|
|
67
|
-
return plugins;
|
|
82
|
+
mdContent = DEFAULT_AGENT_MD;
|
|
68
83
|
}
|
|
84
|
+
const parsed = matter(mdContent);
|
|
85
|
+
const config = (parsed.data || {});
|
|
86
|
+
return {
|
|
87
|
+
name: typeof config.name === "string" ? config.name : "",
|
|
88
|
+
description: typeof config.description === "string" ? config.description : "",
|
|
89
|
+
model: config.model,
|
|
90
|
+
image: config.image,
|
|
91
|
+
plugins: config.plugins || [],
|
|
92
|
+
instructions: parsed.content.trim() || "",
|
|
93
|
+
subscribe: config.subscribe,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// ── Agent composition (declarative AGENT.md agents) ──────────────────
|
|
97
|
+
function composeAgentFromConfig(config, toolRegistry, model) {
|
|
98
|
+
const allToolDefinitions = {};
|
|
99
|
+
const pluginFactories = [];
|
|
100
|
+
for (const pluginItem of config.plugins) {
|
|
101
|
+
const isString = typeof pluginItem === "string";
|
|
102
|
+
const pluginName = isString ? pluginItem : pluginItem.name;
|
|
103
|
+
const pluginConfig = isString ? {} : (pluginItem.config || {});
|
|
104
|
+
const resolvedConfig = resolveConfigPaths(pluginConfig);
|
|
105
|
+
const entry = toolRegistry.get(pluginName);
|
|
106
|
+
if (!entry || entry.type !== "tool") {
|
|
107
|
+
console.warn(`[plugins] "${config.name}": tool "${pluginName}" not found — skipping`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
pluginFactories.push({ plugin: entry.plugin, config: resolvedConfig });
|
|
111
|
+
Object.assign(allToolDefinitions, entry.toolDefinitions);
|
|
112
|
+
}
|
|
113
|
+
const plugin = (builder) => {
|
|
114
|
+
for (const { plugin: toolPlugin, config: resolvedConfig } of pluginFactories) {
|
|
115
|
+
builder.use(toolPlugin({ ...resolvedConfig, model }));
|
|
116
|
+
}
|
|
117
|
+
builder.use(llmPlugin({
|
|
118
|
+
model,
|
|
119
|
+
system: config.instructions,
|
|
120
|
+
toolDefinitions: allToolDefinitions,
|
|
121
|
+
}));
|
|
122
|
+
};
|
|
123
|
+
return { plugin, toolDefinitions: allToolDefinitions };
|
|
124
|
+
}
|
|
125
|
+
// ── Load tool plugins from a subdirectory (used for agent-local tools) ─
|
|
126
|
+
async function loadToolPluginsFromDir(dir) {
|
|
127
|
+
const plugins = [];
|
|
128
|
+
if (!(await fileExists(dir)))
|
|
129
|
+
return plugins;
|
|
69
130
|
try {
|
|
70
131
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
71
132
|
for (const entry of entries) {
|
|
72
|
-
if (!entry.isDirectory())
|
|
73
|
-
continue;
|
|
74
|
-
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
133
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
75
134
|
continue;
|
|
76
135
|
const pluginDir = path.join(dir, entry.name);
|
|
77
|
-
// Ensure plugin is ready (dependencies, build)
|
|
78
136
|
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}`);
|
|
137
|
+
const indexPath = await findIndexFile(pluginDir);
|
|
138
|
+
if (!indexPath)
|
|
95
139
|
continue;
|
|
96
|
-
}
|
|
97
140
|
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'
|
|
141
|
+
const module = await import(pathToFileURL(indexPath).href);
|
|
102
142
|
const entryData = module.plugin || module.default || module.entry;
|
|
103
143
|
if (entryData && typeof entryData.factory === "function") {
|
|
104
144
|
plugins.push({
|
|
105
|
-
|
|
106
|
-
|
|
145
|
+
name: entryData.name || entry.name,
|
|
146
|
+
description: entryData.description || `Tool plugin ${entry.name}`,
|
|
147
|
+
type: "tool",
|
|
148
|
+
plugin: entryData.factory,
|
|
149
|
+
toolDefinitions: entryData.toolDefinitions || {},
|
|
107
150
|
});
|
|
108
151
|
}
|
|
109
152
|
else {
|
|
@@ -111,7 +154,7 @@ export async function loadPluginsFromDir(dir) {
|
|
|
111
154
|
}
|
|
112
155
|
}
|
|
113
156
|
catch (err) {
|
|
114
|
-
console.error(`[plugins] Failed to load plugin "${entry.name}":`, err);
|
|
157
|
+
console.error(`[plugins] Failed to load tool plugin "${entry.name}":`, err);
|
|
115
158
|
}
|
|
116
159
|
}
|
|
117
160
|
}
|
|
@@ -120,3 +163,243 @@ export async function loadPluginsFromDir(dir) {
|
|
|
120
163
|
}
|
|
121
164
|
return plugins;
|
|
122
165
|
}
|
|
166
|
+
// ── Main unified discovery ───────────────────────────────────────────
|
|
167
|
+
/**
|
|
168
|
+
* Discover all plugins (tools + agents) from a directory.
|
|
169
|
+
*
|
|
170
|
+
* Pass 1: Load code plugins in folders without AGENT.md.
|
|
171
|
+
* - module.agent export → code-only agent
|
|
172
|
+
* - plugin/default/entry export → tool plugin
|
|
173
|
+
* Pass 2: Load agent-type plugins (folders WITH AGENT.md).
|
|
174
|
+
* - AGENT.md only → declarative agent (auto-wrapped with llmPlugin)
|
|
175
|
+
* - AGENT.md + index.ts → TS agent (user controls logic, AGENT.md for UI editing)
|
|
176
|
+
*
|
|
177
|
+
* Discovered entries are registered directly into the provided registry.
|
|
178
|
+
*/
|
|
179
|
+
export async function discoverPlugins(dir, registry, defaultModel, options) {
|
|
180
|
+
try {
|
|
181
|
+
await fs.mkdir(dir, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
catch { /* best effort */ }
|
|
184
|
+
let entries;
|
|
185
|
+
try {
|
|
186
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Classify each subdirectory
|
|
192
|
+
const codeDirs = [];
|
|
193
|
+
const agentDirs = [];
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
196
|
+
continue;
|
|
197
|
+
const pluginDir = path.join(dir, entry.name);
|
|
198
|
+
const hasAgentMd = await fileExists(path.join(pluginDir, "AGENT.md"));
|
|
199
|
+
const hasIndex = !!(await findIndexFile(pluginDir));
|
|
200
|
+
const hasPkg = await fileExists(path.join(pluginDir, "package.json"));
|
|
201
|
+
if (hasAgentMd) {
|
|
202
|
+
agentDirs.push({ dir: pluginDir, hasIndex: hasIndex || hasPkg });
|
|
203
|
+
}
|
|
204
|
+
else if (hasIndex || hasPkg) {
|
|
205
|
+
codeDirs.push(pluginDir);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Pass 1: code-only agents and tool plugins
|
|
209
|
+
for (const pluginDir of codeDirs) {
|
|
210
|
+
await ensurePluginReady(pluginDir);
|
|
211
|
+
const indexPath = await findIndexFile(pluginDir);
|
|
212
|
+
if (!indexPath)
|
|
213
|
+
continue;
|
|
214
|
+
try {
|
|
215
|
+
const module = await import(pathToFileURL(indexPath).href);
|
|
216
|
+
const codeAgentDef = module.agent;
|
|
217
|
+
const entryData = module.plugin || module.default || module.entry;
|
|
218
|
+
if (codeAgentDef && typeof codeAgentDef.factory === "function") {
|
|
219
|
+
const meta = await getPluginMetadata(pluginDir);
|
|
220
|
+
const name = codeAgentDef.name || meta.name || "Unnamed Agent";
|
|
221
|
+
const description = codeAgentDef.description || meta.description || "Code Agent";
|
|
222
|
+
registry.register({
|
|
223
|
+
name,
|
|
224
|
+
description,
|
|
225
|
+
type: "agent",
|
|
226
|
+
plugin: codeAgentDef.factory({ ...options, model: defaultModel }),
|
|
227
|
+
capabilities: codeAgentDef.capabilities,
|
|
228
|
+
subscribe: codeAgentDef.subscribe,
|
|
229
|
+
folder: pluginDir,
|
|
230
|
+
});
|
|
231
|
+
console.log(`[plugins] Loaded code-only agent: ${name} — ${description}`);
|
|
232
|
+
}
|
|
233
|
+
else if (entryData && typeof entryData.factory === "function") {
|
|
234
|
+
const meta = await getPluginMetadata(pluginDir);
|
|
235
|
+
const pluginEntry = {
|
|
236
|
+
name: entryData.name || meta.name || "Unnamed Tool",
|
|
237
|
+
description: entryData.description || meta.description || "Tool plugin",
|
|
238
|
+
type: "tool",
|
|
239
|
+
plugin: entryData.factory,
|
|
240
|
+
toolDefinitions: entryData.toolDefinitions || {},
|
|
241
|
+
folder: pluginDir,
|
|
242
|
+
};
|
|
243
|
+
registry.register(pluginEntry);
|
|
244
|
+
console.log(`[plugins] Loaded tool: ${pluginEntry.name}`);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
console.warn(`[plugins] "${path.basename(pluginDir)}" does not export a valid plugin (missing factory)`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
console.error(`[plugins] Failed to load "${path.basename(pluginDir)}":`, err);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// Pass 2: agent plugins
|
|
255
|
+
for (const { dir: agentDir, hasIndex } of agentDirs) {
|
|
256
|
+
const folderName = path.basename(agentDir);
|
|
257
|
+
try {
|
|
258
|
+
if (hasIndex) {
|
|
259
|
+
// TS Agent — has AGENT.md + code. User controls logic; AGENT.md is for UI editing.
|
|
260
|
+
await ensurePluginReady(agentDir);
|
|
261
|
+
const indexPath = await findIndexFile(agentDir);
|
|
262
|
+
if (!indexPath)
|
|
263
|
+
continue;
|
|
264
|
+
const module = await import(pathToFileURL(indexPath).href);
|
|
265
|
+
const definition = module.agent || module.plugin || module.default || module.entry;
|
|
266
|
+
if (definition && typeof definition.factory === "function") {
|
|
267
|
+
const config = await readAgentConfig(agentDir);
|
|
268
|
+
const meta = await getPluginMetadata(agentDir);
|
|
269
|
+
const name = config.name || definition.name || meta.name || "Unnamed Agent";
|
|
270
|
+
const description = definition.description || config.description || "TS Agent";
|
|
271
|
+
registry.register({
|
|
272
|
+
name,
|
|
273
|
+
description,
|
|
274
|
+
type: "agent",
|
|
275
|
+
plugin: definition.factory({ ...options, model: defaultModel }),
|
|
276
|
+
capabilities: definition.capabilities,
|
|
277
|
+
subscribe: definition.subscribe || config.subscribe,
|
|
278
|
+
folder: agentDir,
|
|
279
|
+
});
|
|
280
|
+
console.log(`[plugins] Loaded TS agent: ${name} — ${description}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
// Declarative Agent — AGENT.md only, auto-wrapped with llmPlugin.
|
|
285
|
+
const config = await readAgentConfig(agentDir);
|
|
286
|
+
const meta = await getPluginMetadata(agentDir);
|
|
287
|
+
const resolvedName = config.name || meta.name || "Unnamed Agent";
|
|
288
|
+
const resolvedDescription = config.description || meta.description || "No description";
|
|
289
|
+
const agentModel = config.model
|
|
290
|
+
? createModel({ ...options, model: config.model })
|
|
291
|
+
: defaultModel;
|
|
292
|
+
// Load agent-local tool plugins
|
|
293
|
+
const localPlugins = await loadToolPluginsFromDir(path.join(agentDir, "plugins"));
|
|
294
|
+
// Scoped registry: global tools + local tools
|
|
295
|
+
const scopedRegistry = new PluginRegistry();
|
|
296
|
+
for (const p of registry.getTools()) {
|
|
297
|
+
scopedRegistry.register(p);
|
|
298
|
+
}
|
|
299
|
+
for (const p of localPlugins) {
|
|
300
|
+
scopedRegistry.register(p);
|
|
301
|
+
}
|
|
302
|
+
// Initialize AGENT.md if missing
|
|
303
|
+
const agentMdPath = path.join(agentDir, "AGENT.md");
|
|
304
|
+
if (!(await fileExists(agentMdPath))) {
|
|
305
|
+
const content = DEFAULT_AGENT_MD.replace("name: Agent", `name: ${resolvedName}`);
|
|
306
|
+
await fs.writeFile(agentMdPath, content, "utf-8");
|
|
307
|
+
console.log(`[plugins] Initialized ${resolvedName}/AGENT.md`);
|
|
308
|
+
}
|
|
309
|
+
const { plugin, toolDefinitions } = composeAgentFromConfig(config, scopedRegistry, agentModel);
|
|
310
|
+
registry.register({
|
|
311
|
+
name: resolvedName,
|
|
312
|
+
description: resolvedDescription,
|
|
313
|
+
type: "agent",
|
|
314
|
+
plugin,
|
|
315
|
+
capabilities: Object.fromEntries(Object.entries(toolDefinitions).map(([name, def]) => [name, def.description])),
|
|
316
|
+
subscribe: config.subscribe,
|
|
317
|
+
folder: agentDir,
|
|
318
|
+
});
|
|
319
|
+
console.log(`[plugins] Loaded agent: ${resolvedName} — ${resolvedDescription}${config.model ? ` (model: ${config.model})` : ""}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
if (err.code !== "ENOENT") {
|
|
324
|
+
console.warn(`[plugins] Error loading "${folderName}":`, err);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// ── Lightweight listing (for API) ────────────────────────────────────
|
|
330
|
+
export async function listPlugins(dir) {
|
|
331
|
+
const plugins = [];
|
|
332
|
+
try {
|
|
333
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
334
|
+
for (const entry of entries) {
|
|
335
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
336
|
+
continue;
|
|
337
|
+
const pluginDir = path.join(dir, entry.name);
|
|
338
|
+
const hasAgentMd = await fileExists(path.join(pluginDir, "AGENT.md"));
|
|
339
|
+
const hasCode = await fileExists(path.join(pluginDir, "package.json"))
|
|
340
|
+
|| !!(await findIndexFile(pluginDir));
|
|
341
|
+
if (hasAgentMd) {
|
|
342
|
+
const config = await readAgentConfig(pluginDir);
|
|
343
|
+
const { name: fallbackName, description: fallbackDescription } = await getPluginMetadata(pluginDir);
|
|
344
|
+
plugins.push({
|
|
345
|
+
name: config.name || fallbackName || "Unnamed Agent",
|
|
346
|
+
description: config.description || fallbackDescription || "No description",
|
|
347
|
+
folder: pluginDir,
|
|
348
|
+
type: "agent",
|
|
349
|
+
hasAgentMd: true,
|
|
350
|
+
image: config.image,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
else if (hasCode) {
|
|
354
|
+
await ensurePluginReady(pluginDir);
|
|
355
|
+
const indexPath = await findIndexFile(pluginDir);
|
|
356
|
+
const { name: fallbackName, description: fallbackDescription } = await getPluginMetadata(pluginDir);
|
|
357
|
+
if (!indexPath) {
|
|
358
|
+
plugins.push({
|
|
359
|
+
name: fallbackName,
|
|
360
|
+
description: fallbackDescription,
|
|
361
|
+
folder: pluginDir,
|
|
362
|
+
type: "tool",
|
|
363
|
+
hasAgentMd: false,
|
|
364
|
+
});
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
const module = await import(pathToFileURL(indexPath).href);
|
|
369
|
+
const codeAgentDef = module.agent;
|
|
370
|
+
const toolEntry = module.plugin || module.default || module.entry;
|
|
371
|
+
if (codeAgentDef && typeof codeAgentDef.factory === "function") {
|
|
372
|
+
plugins.push({
|
|
373
|
+
name: codeAgentDef.name || fallbackName || "Unnamed Agent",
|
|
374
|
+
description: codeAgentDef.description || fallbackDescription || "Code Agent",
|
|
375
|
+
folder: pluginDir,
|
|
376
|
+
type: "agent",
|
|
377
|
+
hasAgentMd: false,
|
|
378
|
+
image: codeAgentDef.image,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
else if (toolEntry && typeof toolEntry.factory === "function") {
|
|
382
|
+
plugins.push({
|
|
383
|
+
name: toolEntry.name || fallbackName,
|
|
384
|
+
description: toolEntry.description || fallbackDescription,
|
|
385
|
+
folder: pluginDir,
|
|
386
|
+
type: "tool",
|
|
387
|
+
hasAgentMd: false,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
plugins.push({
|
|
393
|
+
name: fallbackName,
|
|
394
|
+
description: fallbackDescription,
|
|
395
|
+
folder: pluginDir,
|
|
396
|
+
type: "tool",
|
|
397
|
+
hasAgentMd: false,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch { /* directory doesn't exist */ }
|
|
404
|
+
return plugins;
|
|
405
|
+
}
|