openbot 0.2.14 → 0.3.1

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 (84) hide show
  1. package/dist/agents/openbot/index.js +76 -0
  2. package/dist/agents/openbot/middleware/approval.js +132 -0
  3. package/dist/agents/openbot/runtime.js +289 -0
  4. package/dist/agents/openbot/system-prompt.js +32 -0
  5. package/dist/agents/openbot/tools/delegation.js +78 -0
  6. package/dist/agents/openbot/tools/mcp.js +99 -0
  7. package/dist/agents/openbot/tools/shell.js +91 -0
  8. package/dist/agents/openbot/tools/storage.js +75 -0
  9. package/dist/agents/openbot/tools/ui.js +176 -0
  10. package/dist/agents/system.js +20 -93
  11. package/dist/app/cli.js +1 -1
  12. package/dist/app/config.js +4 -1
  13. package/dist/app/server.js +15 -8
  14. package/dist/bus/agent-package.js +1 -0
  15. package/dist/bus/plugin.js +1 -0
  16. package/dist/bus/services.js +711 -0
  17. package/dist/bus/types.js +1 -0
  18. package/dist/harness/context.js +250 -0
  19. package/dist/harness/event-normalizer.js +59 -0
  20. package/dist/harness/orchestrator.js +27 -227
  21. package/dist/harness/process.js +25 -3
  22. package/dist/harness/queue-processor.js +227 -0
  23. package/dist/harness/runtime-factory.js +103 -0
  24. package/dist/plugins/ai-sdk/index.js +37 -0
  25. package/dist/plugins/ai-sdk/runtime.js +402 -0
  26. package/dist/plugins/ai-sdk/system-prompt.js +3 -0
  27. package/dist/plugins/ai-sdk.js +277 -87
  28. package/dist/plugins/approval/index.js +159 -0
  29. package/dist/plugins/approval.js +163 -0
  30. package/dist/plugins/delegation/index.js +79 -0
  31. package/dist/plugins/delegation.js +67 -11
  32. package/dist/plugins/mcp/index.js +108 -0
  33. package/dist/plugins/memory/index.js +71 -0
  34. package/dist/plugins/shell/index.js +99 -0
  35. package/dist/plugins/shell.js +123 -0
  36. package/dist/plugins/storage-tools/index.js +85 -0
  37. package/dist/plugins/storage.js +240 -5
  38. package/dist/plugins/ui/index.js +184 -0
  39. package/dist/plugins/ui.js +185 -21
  40. package/dist/registry/agents.js +138 -0
  41. package/dist/registry/plugins.js +93 -50
  42. package/dist/services/agent-packages.js +103 -0
  43. package/dist/services/memory.js +152 -0
  44. package/dist/services/plugins.js +98 -0
  45. package/dist/services/storage.js +366 -94
  46. package/docs/agents.md +52 -65
  47. package/docs/architecture.md +1 -1
  48. package/docs/plugins.md +70 -58
  49. package/docs/templates/AGENT.example.md +57 -0
  50. package/package.json +8 -7
  51. package/src/app/cli.ts +1 -1
  52. package/src/app/config.ts +14 -4
  53. package/src/app/server.ts +23 -10
  54. package/src/app/types.ts +445 -16
  55. package/src/assets/icon.svg +4 -1
  56. package/src/bus/plugin.ts +67 -0
  57. package/src/bus/services.ts +786 -0
  58. package/src/bus/types.ts +160 -0
  59. package/src/harness/context.ts +293 -0
  60. package/src/harness/event-normalizer.ts +82 -0
  61. package/src/harness/orchestrator.ts +35 -273
  62. package/src/harness/process.ts +28 -4
  63. package/src/harness/queue-processor.ts +309 -0
  64. package/src/harness/runtime-factory.ts +125 -0
  65. package/src/plugins/ai-sdk/index.ts +44 -0
  66. package/src/plugins/ai-sdk/runtime.ts +484 -0
  67. package/src/plugins/ai-sdk/system-prompt.ts +4 -0
  68. package/src/plugins/approval/index.ts +228 -0
  69. package/src/plugins/delegation/index.ts +94 -0
  70. package/src/plugins/mcp/index.ts +128 -0
  71. package/src/plugins/memory/index.ts +85 -0
  72. package/src/plugins/shell/index.ts +123 -0
  73. package/src/plugins/storage-tools/index.ts +101 -0
  74. package/src/plugins/ui/index.ts +227 -0
  75. package/src/registry/plugins.ts +108 -55
  76. package/src/services/memory.ts +213 -0
  77. package/src/services/plugins.ts +133 -0
  78. package/src/services/storage.ts +472 -137
  79. package/src/agents/system.ts +0 -112
  80. package/src/plugins/ai-sdk.ts +0 -197
  81. package/src/plugins/delegation.ts +0 -60
  82. package/src/plugins/mcp.ts +0 -154
  83. package/src/plugins/storage.ts +0 -725
  84. package/src/plugins/ui.ts +0 -57
@@ -1,39 +1,203 @@
1
1
  import z from 'zod';
2
+ const actionSchema = z.object({
3
+ id: z.string().describe('Stable action ID returned by client:ui:widget:response.'),
4
+ label: z.string().describe('Human-readable button label.'),
5
+ value: z.unknown().optional().describe('Optional machine-readable value for this action.'),
6
+ variant: z.enum(['primary', 'secondary', 'danger']).optional(),
7
+ disabled: z.boolean().optional(),
8
+ });
9
+ const optionSchema = z.object({
10
+ label: z.string(),
11
+ value: z.string(),
12
+ });
13
+ const fieldSchema = z.object({
14
+ id: z.string().describe('Stable field ID used as the submitted value key.'),
15
+ label: z.string(),
16
+ type: z.enum(['text', 'textarea', 'number', 'boolean', 'select', 'multiselect', 'date']),
17
+ description: z.string().optional(),
18
+ placeholder: z.string().optional(),
19
+ required: z.boolean().optional(),
20
+ options: z.array(optionSchema).optional(),
21
+ defaultValue: z.unknown().optional(),
22
+ });
23
+ const listItemSchema = z.object({
24
+ id: z.string(),
25
+ label: z.string(),
26
+ description: z.string().optional(),
27
+ status: z.enum(['pending', 'in_progress', 'done', 'error', 'cancelled']).optional(),
28
+ metadata: z.record(z.string(), z.unknown()).optional(),
29
+ });
30
+ const widgetBaseSchema = {
31
+ widgetId: z.string().optional().describe('Optional stable widget ID. Defaults from toolCallId.'),
32
+ title: z.string().optional(),
33
+ description: z.string().optional(),
34
+ body: z.string().optional(),
35
+ state: z.enum(['open', 'submitted', 'cancelled', 'error']).optional(),
36
+ metadata: z.record(z.string(), z.unknown()).optional(),
37
+ };
38
+ const renderWidgetSchema = z.union([
39
+ z.object({
40
+ ...widgetBaseSchema,
41
+ kind: z.literal('message'),
42
+ actions: z.array(actionSchema).optional(),
43
+ }),
44
+ z.object({
45
+ ...widgetBaseSchema,
46
+ kind: z.literal('choice'),
47
+ actions: z.array(actionSchema).min(1),
48
+ }),
49
+ z.object({
50
+ ...widgetBaseSchema,
51
+ kind: z.literal('form'),
52
+ fields: z.array(fieldSchema).optional(),
53
+ submitLabel: z.string().optional(),
54
+ actions: z.array(actionSchema).optional(),
55
+ props: z
56
+ .record(z.string(), z.unknown())
57
+ .optional()
58
+ .describe('Legacy form props. Prefer fields and submitLabel.'),
59
+ }),
60
+ z.object({
61
+ ...widgetBaseSchema,
62
+ kind: z.literal('list'),
63
+ items: z.array(listItemSchema).optional(),
64
+ actions: z.array(actionSchema).optional(),
65
+ }),
66
+ z.object({
67
+ kind: z.enum(['approval', 'todo_list']).describe('Legacy preset. Prefer choice or list.'),
68
+ widgetId: z.string().optional(),
69
+ title: z.string().optional(),
70
+ props: z.record(z.string(), z.unknown()).optional(),
71
+ metadata: z.record(z.string(), z.unknown()).optional(),
72
+ }),
73
+ ]);
74
+ const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
75
+ const readString = (value) => typeof value === 'string' && value.trim() ? value : undefined;
76
+ const asFields = (value) => Array.isArray(value) ? value : undefined;
77
+ const asListItems = (value) => Array.isArray(value) ? value : undefined;
78
+ const todoToListItem = (todo, index) => {
79
+ if (!isRecord(todo)) {
80
+ return {
81
+ id: `todo_${index + 1}`,
82
+ label: String(todo),
83
+ };
84
+ }
85
+ return {
86
+ id: readString(todo.id) || `todo_${index + 1}`,
87
+ label: readString(todo.label) ||
88
+ readString(todo.task) ||
89
+ readString(todo.title) ||
90
+ `Todo ${index + 1}`,
91
+ description: readString(todo.description),
92
+ status: readString(todo.status),
93
+ metadata: todo,
94
+ };
95
+ };
96
+ const createWidgetId = (data, toolCallId) => {
97
+ if ('widgetId' in data && data.widgetId)
98
+ return data.widgetId;
99
+ if (toolCallId)
100
+ return `widget_${toolCallId}`;
101
+ return `widget_${Date.now()}`;
102
+ };
103
+ const normalizeWidget = (data, state, toolCallId) => {
104
+ const widgetId = createWidgetId(data, toolCallId);
105
+ if (data.kind === 'approval') {
106
+ const props = data.props || {};
107
+ return {
108
+ widgetId,
109
+ kind: 'choice',
110
+ title: data.title || 'Approval Required',
111
+ body: readString(props.message) ||
112
+ readString(props.summary) ||
113
+ 'Please approve or deny this action.',
114
+ metadata: {
115
+ ...(data.metadata || {}),
116
+ legacyKind: 'approval',
117
+ actionId: props.actionId,
118
+ },
119
+ actions: [
120
+ { id: 'approve', label: 'Approve', value: props.actionId || 'approve', variant: 'primary' },
121
+ { id: 'deny', label: 'Deny', value: props.actionId || 'deny', variant: 'danger' },
122
+ ],
123
+ };
124
+ }
125
+ if (data.kind === 'todo_list') {
126
+ const props = data.props || {};
127
+ const stateTodos = isRecord(state.threadDetails?.state)
128
+ ? state.threadDetails.state.todos
129
+ : undefined;
130
+ const todos = asListItems(props.todos) || asListItems(stateTodos) || [];
131
+ return {
132
+ widgetId,
133
+ kind: 'list',
134
+ title: data.title || readString(props.title) || 'Task List',
135
+ description: readString(props.description),
136
+ metadata: {
137
+ ...(data.metadata || {}),
138
+ legacyKind: 'todo_list',
139
+ },
140
+ items: todos.map(todoToListItem),
141
+ };
142
+ }
143
+ if (data.kind === 'form') {
144
+ const propsSource = data.props;
145
+ const props = isRecord(propsSource) ? propsSource : {};
146
+ return {
147
+ widgetId,
148
+ kind: 'form',
149
+ title: data.title || 'Details Required',
150
+ description: data.description,
151
+ body: data.body,
152
+ state: data.state,
153
+ metadata: data.metadata,
154
+ fields: data.fields || asFields(props.schema) || [],
155
+ submitLabel: data.submitLabel || readString(props.submitLabel),
156
+ actions: data.actions,
157
+ };
158
+ }
159
+ if (data.kind === 'list') {
160
+ return {
161
+ ...data,
162
+ widgetId,
163
+ title: data.title || 'Task List',
164
+ items: data.items || [],
165
+ };
166
+ }
167
+ if (data.kind === 'choice') {
168
+ return {
169
+ ...data,
170
+ widgetId,
171
+ title: data.title || 'Choose an Option',
172
+ };
173
+ }
174
+ if (data.kind === 'message') {
175
+ return {
176
+ ...data,
177
+ widgetId,
178
+ title: data.title || 'Message',
179
+ };
180
+ }
181
+ throw new Error(`Unsupported UI widget kind: ${data.kind || 'unknown'}`);
182
+ };
2
183
  /**
3
184
  * UI Plugin for Melony.
4
185
  * Provides tools for agents to trigger interactive UI widgets.
5
186
  */
6
187
  export const uiPlugin = () => (builder) => {
7
188
  builder.on('action:render_ui_widget', async function* (event, context) {
8
- const { kind, title, props } = event.data;
9
- const finalProps = { ...props };
10
- // Auto-inject todos if it's a todo_list and they aren't provided
11
- if (kind === 'todo_list' && !finalProps.todos) {
12
- finalProps.todos = context.state.threadDetails?.state?.todos || [];
13
- }
189
+ const widget = normalizeWidget(event.data, context.state, event.meta?.toolCallId);
14
190
  yield {
15
191
  type: 'client:ui:widget',
16
- data: {
17
- widgetId: `${kind}_${Date.now()}`,
18
- kind,
19
- title: title || (kind === 'approval' ? 'Approval Required' : kind === 'todo_list' ? 'Task List' : 'Details Required'),
20
- props: finalProps,
21
- },
192
+ data: widget,
22
193
  meta: event.meta,
23
194
  };
24
195
  });
25
196
  };
26
197
  export const uiToolDefinitions = {
27
198
  render_ui_widget: {
28
- description: 'Render an interactive UI widget (approval, todo_list, or form) in the conversation.',
29
- inputSchema: z.object({
30
- kind: z.enum(['approval', 'todo_list', 'form']).describe('The type of widget to render.'),
31
- title: z.string().optional().describe('Optional title for the widget.'),
32
- props: z.record(z.string(), z.unknown()).describe('Properties for the widget. \n' +
33
- '- For "approval": { message: string, actionId: string }\n' +
34
- '- For "todo_list": { title?: string } (Note: current thread todos are auto-injected if not provided)\n' +
35
- '- For "form": { schema: Array<{ id, label, type, options?, required? }>, submitLabel?: string }'),
36
- }),
199
+ description: 'Render a small server-driven UI widget in the conversation. Prefer primitive kinds: message, choice, form, or list. Legacy presets approval and todo_list are still accepted.',
200
+ inputSchema: renderWidgetSchema,
37
201
  },
38
202
  };
39
203
  export const plugin = {
@@ -0,0 +1,138 @@
1
+ import fs from 'node:fs';
2
+ import fsPromises from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { openBotAgentPackage } from '../agents/openbot/index.js';
6
+ import { DEFAULT_AGENT_PACKAGES_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
7
+ let agentPackagesDir = null;
8
+ const loadedPackages = new Set();
9
+ const cache = new Map();
10
+ const BUILT_IN = {
11
+ [openBotAgentPackage.id]: openBotAgentPackage,
12
+ };
13
+ /**
14
+ * Normalize a dynamically imported agent package module. Supports:
15
+ * - `agentPackage`, `default` (current AgentPackage layout)
16
+ * - `plugin`: legacy/community bundles that used `{ kind: "runtime", factory: (opts) => ... }`
17
+ * where opts should come from agent config (merged at runtime via AgentPackageContext).
18
+ */
19
+ export function parseAgentPackageModule(module) {
20
+ const raw = module.agentPackage ??
21
+ module.default ??
22
+ module.plugin;
23
+ if (!raw || typeof raw !== 'object')
24
+ return null;
25
+ const id = raw.id;
26
+ const name = raw.name;
27
+ const factory = raw.factory;
28
+ if (typeof id !== 'string' || typeof name !== 'string' || typeof factory !== 'function') {
29
+ return null;
30
+ }
31
+ const description = typeof raw.description === 'string' ? raw.description : '';
32
+ if (raw.kind === 'runtime') {
33
+ const legacyFactory = factory;
34
+ return {
35
+ id,
36
+ name,
37
+ description,
38
+ image: typeof raw.image === 'string' ? raw.image : undefined,
39
+ defaultInstructions: typeof raw.defaultInstructions === 'string' ? raw.defaultInstructions : undefined,
40
+ configSchema: raw.configSchema,
41
+ factory: (ctx) => {
42
+ const opts = ctx.config && typeof ctx.config === 'object' && !Array.isArray(ctx.config)
43
+ ? { ...ctx.config }
44
+ : {};
45
+ return legacyFactory(opts);
46
+ },
47
+ };
48
+ }
49
+ return raw;
50
+ }
51
+ async function resolveCommunityDistPath(agentPackagesDir, id) {
52
+ const direct = path.join(agentPackagesDir, id, 'dist', 'index.js');
53
+ if (fs.existsSync(direct))
54
+ return direct;
55
+ try {
56
+ const entries = await fsPromises.readdir(agentPackagesDir, { withFileTypes: true });
57
+ for (const entry of entries) {
58
+ if (entry.name.startsWith('.'))
59
+ continue;
60
+ if (!entry.isDirectory() && !entry.isSymbolicLink())
61
+ continue;
62
+ const distPath = path.join(agentPackagesDir, entry.name, 'dist', 'index.js');
63
+ if (!fs.existsSync(distPath))
64
+ continue;
65
+ try {
66
+ const mod = await import(pathToFileURL(distPath).href);
67
+ const pkg = parseAgentPackageModule(mod);
68
+ if (pkg?.id === id)
69
+ return distPath;
70
+ }
71
+ catch {
72
+ continue;
73
+ }
74
+ }
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ return null;
80
+ }
81
+ /** Initialize the on-disk agent packages directory (defaults to ~/.openbot/agent-packages). */
82
+ export function initAgentPackages(dir) {
83
+ if (dir) {
84
+ agentPackagesDir = dir;
85
+ }
86
+ else {
87
+ const config = loadConfig();
88
+ const baseDir = config.baseDir || DEFAULT_BASE_DIR;
89
+ agentPackagesDir = path.join(resolvePath(baseDir), DEFAULT_AGENT_PACKAGES_DIR);
90
+ }
91
+ }
92
+ /**
93
+ * Resolve an AgentPackage by id. Looks up built-in packages first, then any
94
+ * community packages installed under the agent-packages directory.
95
+ */
96
+ export async function resolveAgentPackage(id) {
97
+ if (cache.has(id))
98
+ return cache.get(id);
99
+ if (BUILT_IN[id]) {
100
+ cache.set(id, BUILT_IN[id]);
101
+ return BUILT_IN[id];
102
+ }
103
+ if (!agentPackagesDir) {
104
+ initAgentPackages();
105
+ }
106
+ if (!agentPackagesDir)
107
+ return null;
108
+ const distPath = await resolveCommunityDistPath(agentPackagesDir, id);
109
+ if (!distPath) {
110
+ console.warn(`[agents] AgentPackage "${id}" not found in registry or under ${agentPackagesDir}.`);
111
+ return null;
112
+ }
113
+ try {
114
+ const module = await import(pathToFileURL(distPath).href);
115
+ const pkg = parseAgentPackageModule(module);
116
+ if (!pkg) {
117
+ console.warn(`[agents] AgentPackage "${id}" at ${distPath} has no recognizable export.`);
118
+ return null;
119
+ }
120
+ cache.set(id, pkg);
121
+ if (!loadedPackages.has(id)) {
122
+ console.log(`[agents] Loaded community agent package "${id}" from ${distPath}`);
123
+ loadedPackages.add(id);
124
+ }
125
+ return pkg;
126
+ }
127
+ catch (e) {
128
+ console.warn(`[agents] Failed to load agent package "${id}" from ${distPath}:`, e);
129
+ return null;
130
+ }
131
+ }
132
+ /** List built-in agent package descriptors (for marketplace/registry views). */
133
+ export function listBuiltInAgentPackages() {
134
+ return Object.values(BUILT_IN);
135
+ }
136
+ export function getAgentPackagesDir() {
137
+ return agentPackagesDir;
138
+ }
@@ -1,18 +1,54 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
- import { aiSdkPlugin } from '../plugins/ai-sdk.js';
5
- import { storagePlugin } from '../plugins/storage.js';
6
- import { storageService } from '../services/storage.js';
7
- import { DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
8
- import { delegationPlugin } from '../plugins/delegation.js';
9
- import { mcpPlugin } from '../plugins/mcp.js';
10
- import { uiPlugin } from '../plugins/ui.js';
4
+ import { aiSdkPlugin } from '../plugins/ai-sdk/index.js';
5
+ import { shellPlugin } from '../plugins/shell/index.js';
6
+ import { mcpPlugin } from '../plugins/mcp/index.js';
7
+ import { delegationPlugin } from '../plugins/delegation/index.js';
8
+ import { storageToolsPlugin } from '../plugins/storage-tools/index.js';
9
+ import { uiPlugin } from '../plugins/ui/index.js';
10
+ import { approvalPlugin } from '../plugins/approval/index.js';
11
+ import { memoryPlugin } from '../plugins/memory/index.js';
12
+ import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
11
13
  let pluginsDir = null;
12
14
  const loadedPlugins = new Set();
13
- /**
14
- * Initializes the plugins directory.
15
- */
15
+ const cache = new Map();
16
+ const BUILT_IN = {
17
+ [aiSdkPlugin.id]: aiSdkPlugin,
18
+ [shellPlugin.id]: shellPlugin,
19
+ [mcpPlugin.id]: mcpPlugin,
20
+ [delegationPlugin.id]: delegationPlugin,
21
+ [storageToolsPlugin.id]: storageToolsPlugin,
22
+ [uiPlugin.id]: uiPlugin,
23
+ [approvalPlugin.id]: approvalPlugin,
24
+ [memoryPlugin.id]: memoryPlugin,
25
+ };
26
+ /** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
27
+ export function parsePluginModule(module) {
28
+ const raw = module.plugin ??
29
+ module.default;
30
+ if (!raw || typeof raw !== 'object')
31
+ return null;
32
+ const factory = raw.factory;
33
+ if (typeof factory !== 'function')
34
+ return null;
35
+ const name = typeof raw.name === 'string' ? raw.name : '';
36
+ const description = typeof raw.description === 'string' ? raw.description : '';
37
+ const image = typeof raw.image === 'string' ? raw.image : undefined;
38
+ const defaultInstructions = typeof raw.defaultInstructions === 'string' ? raw.defaultInstructions : undefined;
39
+ const configSchema = raw.configSchema;
40
+ const toolDefinitions = raw.toolDefinitions;
41
+ return {
42
+ name,
43
+ description,
44
+ image,
45
+ defaultInstructions,
46
+ configSchema,
47
+ toolDefinitions,
48
+ factory: factory,
49
+ };
50
+ }
51
+ /** Initialize the on-disk plugins directory (defaults to ~/.openbot/plugins). */
16
52
  export function initPlugins(dir) {
17
53
  if (dir) {
18
54
  pluginsDir = dir;
@@ -20,54 +56,61 @@ export function initPlugins(dir) {
20
56
  else {
21
57
  const config = loadConfig();
22
58
  const baseDir = config.baseDir || DEFAULT_BASE_DIR;
23
- pluginsDir = path.join(resolvePath(baseDir), 'plugins');
59
+ pluginsDir = path.join(resolvePath(baseDir), DEFAULT_PLUGINS_DIR);
24
60
  }
25
61
  }
26
62
  /**
27
- * Resolves a plugin from its name and config.
63
+ * Resolve a Plugin by id. The id is either:
64
+ * - a built-in id (e.g. "ai-sdk", "shell"), or
65
+ * - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
66
+ * in which case the folder layout is `plugins/<id>/dist/index.js`.
28
67
  */
29
- export async function resolvePlugin(pluginName, config = {}) {
30
- // 1. Built-in plugins
31
- switch (pluginName) {
32
- case 'storage':
33
- return storagePlugin({ storage: storageService, ...config });
34
- case 'ai-sdk':
35
- return aiSdkPlugin({
36
- storage: storageService,
37
- ...config,
38
- });
39
- case 'delegation':
40
- return delegationPlugin();
41
- case 'mcp':
42
- return mcpPlugin();
43
- case 'ui':
44
- return uiPlugin();
68
+ export async function resolvePlugin(id) {
69
+ if (cache.has(id))
70
+ return cache.get(id);
71
+ if (BUILT_IN[id]) {
72
+ cache.set(id, BUILT_IN[id]);
73
+ return BUILT_IN[id];
45
74
  }
46
- // 2. Search for external plugins in the initialized plugins directory
47
75
  if (!pluginsDir) {
48
76
  initPlugins();
49
77
  }
50
- if (pluginsDir) {
51
- const pluginDir = path.resolve(pluginsDir, pluginName);
52
- const distPath = path.join(pluginDir, 'dist', 'index.js');
53
- if (fs.existsSync(distPath)) {
54
- try {
55
- // Dynamic import needs file:// URL for absolute paths
56
- const module = await import(pathToFileURL(distPath).href);
57
- const factory = module.plugin.factory;
58
- if (typeof factory === 'function') {
59
- if (!loadedPlugins.has(pluginName)) {
60
- console.log(`[plugins] Loaded community plugin "${pluginName}" from ${distPath}`);
61
- loadedPlugins.add(pluginName);
62
- }
63
- return factory(config);
64
- }
65
- }
66
- catch (e) {
67
- console.warn(`[plugins] Failed to load plugin "${pluginName}" from ${distPath}:`, e);
68
- }
78
+ if (!pluginsDir)
79
+ return null;
80
+ const distPath = path.join(pluginsDir, id, 'dist', 'index.js');
81
+ if (!fs.existsSync(distPath)) {
82
+ console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
83
+ return null;
84
+ }
85
+ try {
86
+ const module = await import(pathToFileURL(distPath).href);
87
+ const parsed = parsePluginModule(module);
88
+ if (!parsed) {
89
+ console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
90
+ return null;
91
+ }
92
+ const plugin = { id, ...parsed, name: parsed.name || id };
93
+ cache.set(id, plugin);
94
+ if (!loadedPlugins.has(id)) {
95
+ console.log(`[plugins] Loaded community plugin "${id}" from ${distPath}`);
96
+ loadedPlugins.add(id);
69
97
  }
98
+ return plugin;
70
99
  }
71
- console.warn(`[plugins] Plugin "${pluginName}" not found in registry or external directory.`);
72
- return null;
100
+ catch (e) {
101
+ console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
102
+ return null;
103
+ }
104
+ }
105
+ /** Drop a single id from the in-memory cache (e.g. after fresh install). */
106
+ export function invalidatePlugin(id) {
107
+ cache.delete(id);
108
+ loadedPlugins.delete(id);
109
+ }
110
+ /** List built-in plugins (for marketplace/registry views). */
111
+ export function listBuiltInPlugins() {
112
+ return Object.values(BUILT_IN);
113
+ }
114
+ export function getPluginsDir() {
115
+ return pluginsDir;
73
116
  }
@@ -0,0 +1,103 @@
1
+ import fs from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { exec } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { pathToFileURL } from 'node:url';
7
+ import { DEFAULT_AGENT_PACKAGES_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath, } from '../app/config.js';
8
+ import { parseAgentPackageModule } from '../registry/agents.js';
9
+ const execAsync = promisify(exec);
10
+ /**
11
+ * Lifecycle for community-built agent packages distributed via npm.
12
+ * The package format mirrors a normal npm package whose `dist/index.js`
13
+ * exports `agentPackage` (or `default`) that satisfies the `AgentPackage`
14
+ * interface.
15
+ */
16
+ export const agentPackageService = {
17
+ install: async ({ packageName, version }) => {
18
+ const config = loadConfig();
19
+ const baseDir = resolvePath(config.baseDir || DEFAULT_BASE_DIR);
20
+ const packagesDir = path.join(baseDir, DEFAULT_AGENT_PACKAGES_DIR);
21
+ await fs.mkdir(packagesDir, { recursive: true });
22
+ const target = version ? `${packageName}@${version}` : packageName;
23
+ const entries = await fs.readdir(packagesDir, { withFileTypes: true });
24
+ for (const entry of entries) {
25
+ if (entry.isDirectory() || entry.isSymbolicLink()) {
26
+ const pkgPath = path.join(packagesDir, entry.name, 'package.json');
27
+ if (existsSync(pkgPath)) {
28
+ try {
29
+ const pkgJson = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
30
+ if (pkgJson.name === packageName && (!version || pkgJson.version === version)) {
31
+ console.log(`[agent-packages] ${packageName}${version ? `@${version}` : ''} is already installed at ${entry.name}.`);
32
+ return {
33
+ name: pkgJson.name,
34
+ id: entry.name,
35
+ version: pkgJson.version,
36
+ };
37
+ }
38
+ }
39
+ catch {
40
+ // ignore corrupted package.json
41
+ }
42
+ }
43
+ }
44
+ }
45
+ console.log(`[agent-packages] Installing ${target} to ${packagesDir}...`);
46
+ try {
47
+ const tempDir = path.join(packagesDir, '.tmp_' + Date.now());
48
+ await fs.mkdir(tempDir, { recursive: true });
49
+ await execAsync(`npm install ${target} --no-save --prefix "${tempDir}"`);
50
+ const pkgNameOnly = packageName.includes('/') ? packageName.split('/').pop() : packageName;
51
+ const installedPath = path.join(tempDir, 'node_modules', packageName);
52
+ let finalId = pkgNameOnly;
53
+ try {
54
+ const distPath = path.join(installedPath, 'dist', 'index.js');
55
+ if (existsSync(distPath)) {
56
+ const module = await import(pathToFileURL(distPath).href);
57
+ const exported = parseAgentPackageModule(module);
58
+ if (exported?.id)
59
+ finalId = exported.id;
60
+ }
61
+ }
62
+ catch (e) {
63
+ console.warn(`[agent-packages] Could not read package metadata for ${packageName}; using folder name as id.`, e);
64
+ }
65
+ const finalPath = path.join(packagesDir, finalId);
66
+ await fs.rm(finalPath, { recursive: true, force: true });
67
+ await fs.rename(installedPath, finalPath);
68
+ await fs.rm(tempDir, { recursive: true, force: true });
69
+ console.log(`[agent-packages] Running npm install in ${finalPath}...`);
70
+ try {
71
+ await execAsync(`npm install`, { cwd: finalPath });
72
+ console.log(`[agent-packages] npm install completed in ${finalPath}`);
73
+ }
74
+ catch (e) {
75
+ console.warn(`[agent-packages] Failed to run npm install in ${finalPath}:`, e);
76
+ }
77
+ const pkgJson = JSON.parse(await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'));
78
+ return {
79
+ name: pkgJson.name,
80
+ id: finalId,
81
+ version: pkgJson.version,
82
+ };
83
+ }
84
+ catch (error) {
85
+ console.error(`[agent-packages] Failed to install ${packageName}:`, error);
86
+ throw new Error(`Failed to install agent package ${packageName}: ${error.message}`);
87
+ }
88
+ },
89
+ uninstall: async (id) => {
90
+ const config = loadConfig();
91
+ const baseDir = resolvePath(config.baseDir || DEFAULT_BASE_DIR);
92
+ const packagesDir = path.join(baseDir, DEFAULT_AGENT_PACKAGES_DIR);
93
+ const packagePath = path.join(packagesDir, id);
94
+ try {
95
+ await fs.rm(packagePath, { recursive: true, force: true });
96
+ console.log(`[agent-packages] Uninstalled agent package ${id}`);
97
+ }
98
+ catch (error) {
99
+ console.error(`[agent-packages] Failed to uninstall ${id}:`, error);
100
+ throw new Error(`Failed to uninstall agent package ${id}: ${error.message}`);
101
+ }
102
+ },
103
+ };