openbot 0.2.3 → 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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/agent-creator.js +58 -19
  3. package/dist/agents/os-agent.js +1 -4
  4. package/dist/agents/planner-agent.js +32 -0
  5. package/dist/agents/topic-agent.js +1 -1
  6. package/dist/architecture/contracts.js +1 -0
  7. package/dist/architecture/execution-engine.js +151 -0
  8. package/dist/architecture/intent-classifier.js +26 -0
  9. package/dist/architecture/planner.js +106 -0
  10. package/dist/automation-worker.js +121 -0
  11. package/dist/automations.js +52 -0
  12. package/dist/cli.js +54 -141
  13. package/dist/config.js +20 -0
  14. package/dist/core/agents.js +41 -0
  15. package/dist/core/delegation.js +124 -0
  16. package/dist/core/manager.js +73 -0
  17. package/dist/core/plugins.js +77 -0
  18. package/dist/core/router.js +40 -0
  19. package/dist/installers.js +170 -0
  20. package/dist/marketplace.js +80 -0
  21. package/dist/open-bot.js +34 -157
  22. package/dist/orchestrator.js +247 -51
  23. package/dist/plugins/approval/index.js +107 -3
  24. package/dist/plugins/brain/index.js +17 -86
  25. package/dist/plugins/brain/memory.js +1 -1
  26. package/dist/plugins/brain/prompt.js +8 -13
  27. package/dist/plugins/brain/types.js +0 -15
  28. package/dist/plugins/file-system/index.js +8 -8
  29. package/dist/plugins/llm/context-shaping.js +177 -0
  30. package/dist/plugins/llm/index.js +223 -49
  31. package/dist/plugins/memory/index.js +220 -0
  32. package/dist/plugins/memory/memory.js +122 -0
  33. package/dist/plugins/memory/prompt.js +55 -0
  34. package/dist/plugins/memory/types.js +45 -0
  35. package/dist/plugins/shell/index.js +3 -3
  36. package/dist/plugins/skills/index.js +9 -9
  37. package/dist/registry/index.js +1 -4
  38. package/dist/registry/plugin-loader.js +339 -56
  39. package/dist/registry/plugin-registry.js +21 -4
  40. package/dist/registry/ts-agent-loader.js +4 -4
  41. package/dist/registry/yaml-agent-loader.js +78 -20
  42. package/dist/runtime/execution-trace.js +41 -0
  43. package/dist/runtime/intent-routing.js +26 -0
  44. package/dist/runtime/openbot-runtime.js +354 -0
  45. package/dist/server.js +489 -40
  46. package/dist/ui/widgets/approval-card.js +22 -2
  47. package/dist/ui/widgets/delegation.js +29 -0
  48. package/dist/version.js +62 -0
  49. package/package.json +7 -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:taskResult",
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:taskResult",
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:taskResult",
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 brain prompt builder.
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 brain plugin.
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:taskResult",
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:taskResult",
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:taskResult",
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:taskResult",
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:taskResult",
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:taskResult",
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:taskResult",
268
+ type: "action:result",
269
269
  data: {
270
270
  action: "updateSkill",
271
271
  toolCallId,
@@ -1,5 +1,2 @@
1
1
  export { PluginRegistry } from "./plugin-registry.js";
2
- export { AgentRegistry } from "./agent-registry.js";
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
- * Get metadata (name, description, version) from a plugin directory.
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 fs.access(pkgPath).then(() => true).catch(() => false);
11
- let name = path.basename(pluginDir);
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
- const hasPackageJson = await fs.access(pkgPath).then(() => true).catch(() => false);
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
- const hasNodeModules = await fs.access(nodeModulesPath).then(() => true).catch(() => false);
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
- const hasDist = await fs.access(distPath).then(() => true).catch(() => false);
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
- * Dynamically load plugins from a directory.
58
- * Scans each subdirectory for an index.js (or index.ts if running via tsx).
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.access(dir);
79
+ mdContent = await fs.readFile(mdPath, "utf-8");
64
80
  }
65
81
  catch {
66
- // Directory doesn't exist
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
- // Try to find index.js or index.ts, prioritizing dist/index.js for pre-built bundles
80
- let indexPath;
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
- // Use pathToFileURL for compatibility with import() on all OSs
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
- ...entryData,
106
- name: entryData.name || entry.name, // Fallback to folder name
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
+ }