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.
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 +116 -146
  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 +156 -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 +361 -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 +513 -41
  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 +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
- * 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
+ 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 fs.access(pkgPath).then(() => true).catch(() => false);
11
- let name = path.basename(pluginDir);
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
- const hasPackageJson = await fs.access(pkgPath).then(() => true).catch(() => false);
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
- const hasNodeModules = await fs.access(nodeModulesPath).then(() => true).catch(() => false);
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
- const hasDist = await fs.access(distPath).then(() => true).catch(() => false);
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
- * 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 = [];
82
+ export async function readAgentConfig(agentDir) {
83
+ const mdPath = path.join(agentDir, "AGENT.md");
84
+ let mdContent = "";
62
85
  try {
63
- await fs.access(dir);
86
+ mdContent = await fs.readFile(mdPath, "utf-8");
64
87
  }
65
88
  catch {
66
- // Directory doesn't exist
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
- // 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}`);
144
+ const indexPath = await findIndexFile(pluginDir);
145
+ if (!indexPath)
95
146
  continue;
96
- }
97
147
  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'
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
- ...entryData,
106
- name: entryData.name || entry.name, // Fallback to folder name
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
- * Maps plugin names to their factories and tool definitions.
5
- * Built-in plugins are registered at startup; community plugins
6
- * can be added via npm packages or local directories (future).
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 agent.yaml
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 yamlPath = path.join(agentDir, "agent.yaml");
28
- const hasYaml = await fs.access(yamlPath).then(() => true).catch(() => false);
29
- if (hasYaml)
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");