openbot 0.2.14 → 0.3.0
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/dist/agents/openbot/index.js +76 -0
- package/dist/agents/openbot/middleware/approval.js +132 -0
- package/dist/agents/openbot/runtime.js +289 -0
- package/dist/agents/openbot/system-prompt.js +32 -0
- package/dist/agents/openbot/tools/delegation.js +78 -0
- package/dist/agents/openbot/tools/mcp.js +99 -0
- package/dist/agents/openbot/tools/shell.js +91 -0
- package/dist/agents/openbot/tools/storage.js +75 -0
- package/dist/agents/openbot/tools/ui.js +176 -0
- package/dist/agents/system.js +20 -93
- package/dist/app/cli.js +0 -0
- package/dist/app/config.js +4 -1
- package/dist/app/server.js +15 -8
- package/dist/bus/agent-package.js +1 -0
- package/dist/bus/plugin.js +1 -0
- package/dist/bus/services.js +600 -0
- package/dist/bus/types.js +1 -0
- package/dist/harness/context.js +131 -0
- package/dist/harness/event-normalizer.js +59 -0
- package/dist/harness/orchestrator.js +27 -227
- package/dist/harness/process.js +25 -3
- package/dist/harness/queue-processor.js +227 -0
- package/dist/harness/runtime-factory.js +103 -0
- package/dist/plugins/ai-sdk/index.js +37 -0
- package/dist/plugins/ai-sdk/runtime.js +330 -0
- package/dist/plugins/ai-sdk/system-prompt.js +3 -0
- package/dist/plugins/ai-sdk.js +277 -87
- package/dist/plugins/approval/index.js +159 -0
- package/dist/plugins/approval.js +163 -0
- package/dist/plugins/delegation/index.js +79 -0
- package/dist/plugins/delegation.js +67 -11
- package/dist/plugins/mcp/index.js +108 -0
- package/dist/plugins/shell/index.js +99 -0
- package/dist/plugins/shell.js +123 -0
- package/dist/plugins/storage-tools/index.js +85 -0
- package/dist/plugins/storage.js +240 -5
- package/dist/plugins/ui/index.js +184 -0
- package/dist/plugins/ui.js +185 -21
- package/dist/registry/agents.js +138 -0
- package/dist/registry/plugins.js +91 -50
- package/dist/services/agent-packages.js +103 -0
- package/dist/services/plugins.js +98 -0
- package/dist/services/storage.js +360 -94
- package/docs/agents.md +39 -66
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +70 -58
- package/docs/templates/AGENT.example.md +57 -0
- package/package.json +8 -7
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +14 -4
- package/src/app/server.ts +23 -10
- package/src/app/types.ts +385 -16
- package/src/assets/icon.svg +4 -1
- package/src/bus/plugin.ts +67 -0
- package/src/bus/services.ts +666 -0
- package/src/bus/types.ts +147 -0
- package/src/harness/context.ts +160 -0
- package/src/harness/event-normalizer.ts +82 -0
- package/src/harness/orchestrator.ts +35 -273
- package/src/harness/process.ts +28 -4
- package/src/harness/queue-processor.ts +309 -0
- package/src/harness/runtime-factory.ts +125 -0
- package/src/plugins/ai-sdk/index.ts +44 -0
- package/src/plugins/ai-sdk/runtime.ts +410 -0
- package/src/plugins/ai-sdk/system-prompt.ts +4 -0
- package/src/plugins/approval/index.ts +228 -0
- package/src/plugins/delegation/index.ts +94 -0
- package/src/plugins/mcp/index.ts +128 -0
- package/src/plugins/shell/index.ts +123 -0
- package/src/plugins/storage-tools/index.ts +101 -0
- package/src/plugins/ui/index.ts +227 -0
- package/src/registry/plugins.ts +106 -55
- package/src/services/plugins.ts +133 -0
- package/src/services/storage.ts +465 -137
- package/src/agents/system.ts +0 -112
- package/src/plugins/ai-sdk.ts +0 -197
- package/src/plugins/delegation.ts +0 -60
- package/src/plugins/mcp.ts +0 -154
- package/src/plugins/storage.ts +0 -725
- package/src/plugins/ui.ts +0 -57
package/dist/plugins/ui.js
CHANGED
|
@@ -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
|
|
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
|
|
29
|
-
inputSchema:
|
|
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
|
+
}
|
package/dist/registry/plugins.js
CHANGED
|
@@ -1,18 +1,52 @@
|
|
|
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 {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
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 { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
|
|
11
12
|
let pluginsDir = null;
|
|
12
13
|
const loadedPlugins = new Set();
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const cache = new Map();
|
|
15
|
+
const BUILT_IN = {
|
|
16
|
+
[aiSdkPlugin.id]: aiSdkPlugin,
|
|
17
|
+
[shellPlugin.id]: shellPlugin,
|
|
18
|
+
[mcpPlugin.id]: mcpPlugin,
|
|
19
|
+
[delegationPlugin.id]: delegationPlugin,
|
|
20
|
+
[storageToolsPlugin.id]: storageToolsPlugin,
|
|
21
|
+
[uiPlugin.id]: uiPlugin,
|
|
22
|
+
[approvalPlugin.id]: approvalPlugin,
|
|
23
|
+
};
|
|
24
|
+
/** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
|
|
25
|
+
export function parsePluginModule(module) {
|
|
26
|
+
const raw = module.plugin ??
|
|
27
|
+
module.default;
|
|
28
|
+
if (!raw || typeof raw !== 'object')
|
|
29
|
+
return null;
|
|
30
|
+
const factory = raw.factory;
|
|
31
|
+
if (typeof factory !== 'function')
|
|
32
|
+
return null;
|
|
33
|
+
const name = typeof raw.name === 'string' ? raw.name : '';
|
|
34
|
+
const description = typeof raw.description === 'string' ? raw.description : '';
|
|
35
|
+
const image = typeof raw.image === 'string' ? raw.image : undefined;
|
|
36
|
+
const defaultInstructions = typeof raw.defaultInstructions === 'string' ? raw.defaultInstructions : undefined;
|
|
37
|
+
const configSchema = raw.configSchema;
|
|
38
|
+
const toolDefinitions = raw.toolDefinitions;
|
|
39
|
+
return {
|
|
40
|
+
name,
|
|
41
|
+
description,
|
|
42
|
+
image,
|
|
43
|
+
defaultInstructions,
|
|
44
|
+
configSchema,
|
|
45
|
+
toolDefinitions,
|
|
46
|
+
factory: factory,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Initialize the on-disk plugins directory (defaults to ~/.openbot/plugins). */
|
|
16
50
|
export function initPlugins(dir) {
|
|
17
51
|
if (dir) {
|
|
18
52
|
pluginsDir = dir;
|
|
@@ -20,54 +54,61 @@ export function initPlugins(dir) {
|
|
|
20
54
|
else {
|
|
21
55
|
const config = loadConfig();
|
|
22
56
|
const baseDir = config.baseDir || DEFAULT_BASE_DIR;
|
|
23
|
-
pluginsDir = path.join(resolvePath(baseDir),
|
|
57
|
+
pluginsDir = path.join(resolvePath(baseDir), DEFAULT_PLUGINS_DIR);
|
|
24
58
|
}
|
|
25
59
|
}
|
|
26
60
|
/**
|
|
27
|
-
*
|
|
61
|
+
* Resolve a Plugin by id. The id is either:
|
|
62
|
+
* - a built-in id (e.g. "ai-sdk", "shell"), or
|
|
63
|
+
* - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
|
|
64
|
+
* in which case the folder layout is `plugins/<id>/dist/index.js`.
|
|
28
65
|
*/
|
|
29
|
-
export async function resolvePlugin(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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();
|
|
66
|
+
export async function resolvePlugin(id) {
|
|
67
|
+
if (cache.has(id))
|
|
68
|
+
return cache.get(id);
|
|
69
|
+
if (BUILT_IN[id]) {
|
|
70
|
+
cache.set(id, BUILT_IN[id]);
|
|
71
|
+
return BUILT_IN[id];
|
|
45
72
|
}
|
|
46
|
-
// 2. Search for external plugins in the initialized plugins directory
|
|
47
73
|
if (!pluginsDir) {
|
|
48
74
|
initPlugins();
|
|
49
75
|
}
|
|
50
|
-
if (pluginsDir)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
if (!pluginsDir)
|
|
77
|
+
return null;
|
|
78
|
+
const distPath = path.join(pluginsDir, id, 'dist', 'index.js');
|
|
79
|
+
if (!fs.existsSync(distPath)) {
|
|
80
|
+
console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const module = await import(pathToFileURL(distPath).href);
|
|
85
|
+
const parsed = parsePluginModule(module);
|
|
86
|
+
if (!parsed) {
|
|
87
|
+
console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const plugin = { id, ...parsed, name: parsed.name || id };
|
|
91
|
+
cache.set(id, plugin);
|
|
92
|
+
if (!loadedPlugins.has(id)) {
|
|
93
|
+
console.log(`[plugins] Loaded community plugin "${id}" from ${distPath}`);
|
|
94
|
+
loadedPlugins.add(id);
|
|
69
95
|
}
|
|
96
|
+
return plugin;
|
|
70
97
|
}
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** Drop a single id from the in-memory cache (e.g. after fresh install). */
|
|
104
|
+
export function invalidatePlugin(id) {
|
|
105
|
+
cache.delete(id);
|
|
106
|
+
loadedPlugins.delete(id);
|
|
107
|
+
}
|
|
108
|
+
/** List built-in plugins (for marketplace/registry views). */
|
|
109
|
+
export function listBuiltInPlugins() {
|
|
110
|
+
return Object.values(BUILT_IN);
|
|
111
|
+
}
|
|
112
|
+
export function getPluginsDir() {
|
|
113
|
+
return pluginsDir;
|
|
73
114
|
}
|
|
@@ -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
|
+
};
|