openbot 0.3.6 → 0.4.2

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 (104) hide show
  1. package/README.md +15 -16
  2. package/dist/app/agent-ids.js +4 -0
  3. package/dist/app/cli.js +1 -1
  4. package/dist/app/config.js +10 -19
  5. package/dist/app/server.js +208 -17
  6. package/dist/bus/services.js +34 -124
  7. package/dist/harness/agent-invoke-run.js +44 -0
  8. package/dist/harness/agent-turn.js +99 -0
  9. package/dist/harness/channel-participants.js +40 -0
  10. package/dist/harness/constants.js +2 -0
  11. package/dist/harness/context-meter.js +97 -0
  12. package/dist/harness/context.js +95 -47
  13. package/dist/harness/dispatch.js +144 -0
  14. package/dist/harness/dispatcher.js +45 -156
  15. package/dist/harness/history.js +177 -0
  16. package/dist/harness/index.js +109 -0
  17. package/dist/harness/orchestration.js +88 -0
  18. package/dist/harness/participants.js +22 -0
  19. package/dist/harness/run-harness.js +154 -0
  20. package/dist/harness/run.js +98 -0
  21. package/dist/harness/runtime-factory.js +0 -34
  22. package/dist/harness/runtime.js +57 -0
  23. package/dist/harness/todo-dispatch.js +51 -0
  24. package/dist/harness/todos.js +5 -0
  25. package/dist/harness/turn.js +79 -0
  26. package/dist/plugins/approval/index.js +120 -149
  27. package/dist/plugins/bash/index.js +195 -0
  28. package/dist/plugins/delegation/index.js +121 -32
  29. package/dist/plugins/memory/index.js +103 -14
  30. package/dist/plugins/memory/service.js +152 -0
  31. package/dist/plugins/openbot/context.js +125 -0
  32. package/dist/plugins/openbot/history.js +144 -0
  33. package/dist/plugins/openbot/index.js +71 -0
  34. package/dist/plugins/openbot/runtime.js +381 -0
  35. package/dist/plugins/openbot/system-prompt.js +25 -0
  36. package/dist/plugins/plugin-manager/index.js +189 -0
  37. package/dist/plugins/shell/index.js +2 -1
  38. package/dist/plugins/storage/files.js +67 -0
  39. package/dist/plugins/storage/index.js +750 -0
  40. package/dist/plugins/storage/service.js +1316 -0
  41. package/dist/plugins/storage-tools/index.js +2 -2
  42. package/dist/plugins/thread-namer/index.js +72 -0
  43. package/dist/plugins/thread-naming/generate-title.js +44 -0
  44. package/dist/plugins/thread-naming/index.js +103 -0
  45. package/dist/plugins/threads/index.js +114 -0
  46. package/dist/plugins/todo/index.js +24 -25
  47. package/dist/plugins/ui/index.js +109 -180
  48. package/dist/registry/plugins.js +3 -9
  49. package/dist/services/abort.js +43 -0
  50. package/dist/services/plugins/domain.js +1 -0
  51. package/dist/services/plugins/plugin-cache.js +9 -0
  52. package/dist/services/plugins/registry.js +112 -0
  53. package/dist/services/plugins/service.js +232 -0
  54. package/dist/services/plugins/types.js +1 -0
  55. package/dist/services/process.js +29 -0
  56. package/dist/services/storage.js +11 -10
  57. package/dist/services/thread-naming.js +81 -0
  58. package/docs/agents.md +15 -12
  59. package/docs/architecture.md +2 -2
  60. package/docs/plugins.md +29 -17
  61. package/docs/templates/AGENT.example.md +8 -14
  62. package/package.json +1 -2
  63. package/src/app/agent-ids.ts +5 -0
  64. package/src/app/cli.ts +1 -1
  65. package/src/app/config.ts +14 -31
  66. package/src/app/server.ts +243 -19
  67. package/src/app/types.ts +331 -187
  68. package/src/harness/index.ts +166 -0
  69. package/src/plugins/approval/index.ts +107 -188
  70. package/src/plugins/bash/index.ts +232 -0
  71. package/src/plugins/delegation/index.ts +139 -39
  72. package/src/plugins/memory/index.ts +112 -15
  73. package/src/{services/memory.ts → plugins/memory/service.ts} +1 -1
  74. package/src/plugins/openbot/context.ts +140 -0
  75. package/src/plugins/openbot/history.ts +158 -0
  76. package/src/plugins/openbot/index.ts +79 -0
  77. package/src/plugins/openbot/runtime.ts +478 -0
  78. package/src/plugins/openbot/system-prompt.ts +27 -0
  79. package/src/plugins/plugin-manager/index.ts +224 -0
  80. package/src/plugins/storage/files.ts +81 -0
  81. package/src/plugins/storage/index.ts +823 -0
  82. package/src/{services/storage.ts → plugins/storage/service.ts} +485 -105
  83. package/src/plugins/ui/index.ts +117 -221
  84. package/src/services/abort.ts +46 -0
  85. package/src/{bus/types.ts → services/plugins/domain.ts} +50 -8
  86. package/src/services/plugins/plugin-cache.ts +13 -0
  87. package/src/{registry/plugins.ts → services/plugins/registry.ts} +28 -28
  88. package/src/services/plugins/service.ts +318 -0
  89. package/src/{bus/plugin.ts → services/plugins/types.ts} +7 -3
  90. package/src/bus/services.ts +0 -954
  91. package/src/harness/context.ts +0 -365
  92. package/src/harness/dispatcher.ts +0 -379
  93. package/src/harness/mcp.ts +0 -78
  94. package/src/harness/runtime-factory.ts +0 -129
  95. package/src/harness/todo-advance.ts +0 -128
  96. package/src/plugins/ai-sdk/index.ts +0 -41
  97. package/src/plugins/ai-sdk/runtime.ts +0 -468
  98. package/src/plugins/ai-sdk/system-prompt.ts +0 -18
  99. package/src/plugins/mcp/index.ts +0 -128
  100. package/src/plugins/shell/index.ts +0 -123
  101. package/src/plugins/storage-tools/index.ts +0 -90
  102. package/src/plugins/todo/index.ts +0 -64
  103. package/src/services/plugins.ts +0 -133
  104. /package/src/{harness → services}/process.ts +0 -0
@@ -1,184 +1,113 @@
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('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.record(z.string(), z.unknown()).optional(),
56
- }),
57
- z.object({
58
- ...widgetBaseSchema,
59
- kind: z.literal('list'),
60
- items: z.array(listItemSchema).optional(),
61
- actions: z.array(actionSchema).optional(),
62
- }),
63
- z.object({
64
- kind: z.enum(['approval', 'todo_list']).describe('Legacy preset. Prefer choice or list.'),
65
- widgetId: z.string().optional(),
66
- title: z.string().optional(),
67
- props: z.record(z.string(), z.unknown()).optional(),
68
- metadata: z.record(z.string(), z.unknown()).optional(),
69
- }),
70
- ]);
71
- const isRecord = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
72
- const readString = (value) => typeof value === 'string' && value.trim() ? value : undefined;
73
- const asFields = (value) => Array.isArray(value) ? value : undefined;
74
- const asListItems = (value) => Array.isArray(value) ? value : undefined;
75
- const todoToListItem = (todo, index) => {
76
- if (!isRecord(todo)) {
77
- return { id: `todo_${index + 1}`, label: String(todo) };
78
- }
79
- return {
80
- id: readString(todo.id) || `todo_${index + 1}`,
81
- label: readString(todo.label) ||
82
- readString(todo.task) ||
83
- readString(todo.title) ||
84
- `Todo ${index + 1}`,
85
- description: readString(todo.description),
86
- status: readString(todo.status),
87
- metadata: todo,
88
- };
89
- };
90
- const createWidgetId = (data, toolCallId) => {
91
- if ('widgetId' in data && data.widgetId)
92
- return data.widgetId;
93
- if (toolCallId)
94
- return `widget_${toolCallId}`;
95
- return `widget_${Date.now()}`;
96
- };
97
- const normalizeWidget = (data, state, toolCallId) => {
98
- const widgetId = createWidgetId(data, toolCallId);
99
- if (data.kind === 'approval') {
100
- const props = data.props || {};
101
- return {
102
- widgetId,
103
- kind: 'choice',
104
- title: data.title || 'Approval Required',
105
- body: readString(props.message) ||
106
- readString(props.summary) ||
107
- 'Please approve or deny this action.',
108
- metadata: {
109
- ...(data.metadata || {}),
110
- legacyKind: 'approval',
111
- actionId: props.actionId,
112
- },
113
- actions: [
114
- { id: 'approve', label: 'Approve', value: props.actionId || 'approve', variant: 'primary' },
115
- { id: 'deny', label: 'Deny', value: props.actionId || 'deny', variant: 'danger' },
116
- ],
117
- };
118
- }
119
- if (data.kind === 'todo_list') {
120
- const props = data.props || {};
121
- const stateTodos = isRecord(state.threadDetails?.state)
122
- ? state.threadDetails.state.todos
123
- : undefined;
124
- const todos = asListItems(props.todos) || asListItems(stateTodos) || [];
125
- return {
126
- widgetId,
127
- kind: 'list',
128
- title: data.title || readString(props.title) || 'Task List',
129
- description: readString(props.description),
130
- metadata: { ...(data.metadata || {}), legacyKind: 'todo_list' },
131
- items: todos.map(todoToListItem),
132
- };
133
- }
134
- if (data.kind === 'form') {
135
- const propsSource = data.props;
136
- const props = isRecord(propsSource) ? propsSource : {};
137
- return {
138
- widgetId,
139
- kind: 'form',
140
- title: data.title || 'Details Required',
141
- description: data.description,
142
- body: data.body,
143
- state: data.state,
144
- metadata: data.metadata,
145
- fields: data.fields || asFields(props.schema) || [],
146
- submitLabel: data.submitLabel || readString(props.submitLabel),
147
- actions: data.actions,
148
- };
149
- }
150
- if (data.kind === 'list') {
151
- return { ...data, widgetId, title: data.title || 'Task List', items: data.items || [] };
152
- }
153
- if (data.kind === 'choice') {
154
- return { ...data, widgetId, title: data.title || 'Choose an Option' };
155
- }
156
- if (data.kind === 'message') {
157
- return { ...data, widgetId, title: data.title || 'Message' };
158
- }
159
- throw new Error(`Unsupported UI widget kind: ${data.kind || 'unknown'}`);
160
- };
161
- const uiToolDefinitions = {
162
- render_ui_widget: {
163
- 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 accepted.',
164
- inputSchema: renderWidgetSchema,
165
- },
166
- };
167
- const uiPluginRuntime = () => (builder) => {
168
- builder.on('action:render_ui_widget', async function* (event, context) {
169
- const widget = normalizeWidget(event.data, context.state, event.meta?.toolCallId);
170
- yield {
171
- type: 'client:ui:widget',
172
- data: widget,
173
- meta: event.meta,
174
- };
175
- });
176
- };
1
+ import { randomUUID } from 'node:crypto';
2
+ import { z } from 'zod';
3
+ /**
4
+ * `ui` — provides a tool for the agent to render interactive UI widgets.
5
+ *
6
+ * The model can choose which widget to render (form, choice, list, message)
7
+ * depending on the situation.
8
+ */
177
9
  export const uiPlugin = {
178
10
  id: 'ui',
179
- name: 'UI Widgets',
180
- description: 'Render server-driven UI widgets (messages, choices, forms, lists) in the conversation.',
181
- toolDefinitions: uiToolDefinitions,
182
- factory: () => uiPluginRuntime(),
11
+ name: 'UI',
12
+ description: 'Render interactive UI widgets to interact with the user.',
13
+ toolDefinitions: {
14
+ render_widget: {
15
+ description: 'Render a UI widget to the user. Use "form" for data collection, "choice" for simple selection, "list" for displaying items, and "message" for simple notifications with actions. When using form widge to unquire user, do not provide complex forms with many fields, always try to make it simple to keep the experience smooth and straightforward.',
16
+ inputSchema: z.object({
17
+ kind: z.enum(['message', 'choice', 'form', 'list']).describe('The type of widget to render.'),
18
+ title: z.string().describe('The title of the widget.'),
19
+ description: z.string().optional().describe('A description or body text.'),
20
+ fields: z.array(z.object({
21
+ id: z.string().describe('Unique ID for the field.'),
22
+ label: z.string().describe('Label shown to the user.'),
23
+ type: z.enum(['text', 'textarea', 'number', 'boolean', 'select', 'multiselect', 'date']),
24
+ description: z.string().optional(),
25
+ placeholder: z.string().optional(),
26
+ required: z.boolean().optional(),
27
+ options: z.array(z.object({ label: z.string(), value: z.string() })).optional(),
28
+ defaultValue: z.any().optional()
29
+ })).optional().describe('Required for kind="form". List of form fields.'),
30
+ actions: z.array(z.object({
31
+ id: z.string(),
32
+ label: z.string(),
33
+ variant: z.enum(['primary', 'secondary', 'danger']).optional(),
34
+ })).optional().describe('Buttons or actions available on the widget.'),
35
+ items: z.array(z.object({
36
+ id: z.string(),
37
+ label: z.string(),
38
+ description: z.string().optional(),
39
+ status: z.enum(['pending', 'in_progress', 'done', 'error', 'cancelled']).optional(),
40
+ metadata: z.record(z.string(), z.any()).optional()
41
+ })).optional().describe('Required for kind="list". List of items to display.'),
42
+ submitLabel: z.string().optional().describe('Label for the primary action button (e.g. "Submit", "Save").')
43
+ })
44
+ }
45
+ },
46
+ factory: () => (builder) => {
47
+ // Handle the tool call from the agent
48
+ builder.on('action:render_widget', async function* (event, context) {
49
+ const widgetEvent = event;
50
+ const toolCallId = widgetEvent.meta?.toolCallId;
51
+ const threadId = widgetEvent.meta?.threadId || context.state.threadId;
52
+ if (!toolCallId)
53
+ return;
54
+ const widgetId = randomUUID();
55
+ // Emit the UI widget event to the client
56
+ yield {
57
+ type: 'client:ui:widget',
58
+ data: {
59
+ ...widgetEvent.data,
60
+ widgetId,
61
+ metadata: {
62
+ type: 'ui:request',
63
+ originalEvent: widgetEvent
64
+ }
65
+ },
66
+ meta: { agentId: context.state.agentId, threadId }
67
+ };
68
+ });
69
+ // Handle the user's response from the UI widget
70
+ builder.on('client:ui:widget:response', async function* (event, context) {
71
+ const responseEvent = event;
72
+ const { widgetId, actionId, values, metadata } = responseEvent.data;
73
+ if (metadata?.type !== 'ui:request')
74
+ return;
75
+ const originalEvent = metadata.originalEvent;
76
+ const toolCallId = originalEvent?.meta?.toolCallId;
77
+ const threadId = originalEvent?.meta?.threadId || context.state.threadId;
78
+ if (!toolCallId)
79
+ return;
80
+ // Yield a "submitted" widget update to the UI to collapse/disable it
81
+ yield {
82
+ type: 'client:ui:widget',
83
+ data: {
84
+ widgetId,
85
+ title: originalEvent.data.title,
86
+ kind: originalEvent.data.kind,
87
+ state: 'submitted',
88
+ body: "Thank you for your response. We will process it and get back to you soon.",
89
+ display: 'collapsed',
90
+ disabled: true,
91
+ actions: [], // Clear actions to disable buttons in UI
92
+ },
93
+ meta: { agentId: context.state.agentId, threadId },
94
+ };
95
+ // Emit the tool result event so the agent runtime can resume
96
+ yield {
97
+ type: 'action:render_widget:result',
98
+ data: {
99
+ success: true,
100
+ actionId,
101
+ values,
102
+ output: JSON.stringify(values)
103
+ },
104
+ meta: {
105
+ agentId: context.state.agentId,
106
+ threadId,
107
+ toolCallId
108
+ }
109
+ };
110
+ });
111
+ },
183
112
  };
184
113
  export default uiPlugin;
@@ -1,29 +1,23 @@
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/index.js';
4
+ import { openbotPlugin } from '../plugins/openbot/index.js';
5
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
6
  import { storageToolsPlugin } from '../plugins/storage-tools/index.js';
9
7
  import { uiPlugin } from '../plugins/ui/index.js';
10
8
  import { approvalPlugin } from '../plugins/approval/index.js';
11
9
  import { memoryPlugin } from '../plugins/memory/index.js';
12
- import { todoPlugin } from '../plugins/todo/index.js';
13
10
  import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../app/config.js';
14
11
  let pluginsDir = null;
15
12
  const loadedPlugins = new Set();
16
13
  const cache = new Map();
17
14
  const BUILT_IN = {
18
- [aiSdkPlugin.id]: aiSdkPlugin,
15
+ [openbotPlugin.id]: openbotPlugin,
19
16
  [shellPlugin.id]: shellPlugin,
20
- [mcpPlugin.id]: mcpPlugin,
21
- [delegationPlugin.id]: delegationPlugin,
22
17
  [storageToolsPlugin.id]: storageToolsPlugin,
23
18
  [uiPlugin.id]: uiPlugin,
24
19
  [approvalPlugin.id]: approvalPlugin,
25
20
  [memoryPlugin.id]: memoryPlugin,
26
- [todoPlugin.id]: todoPlugin,
27
21
  };
28
22
  /** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
29
23
  export function parsePluginModule(module) {
@@ -61,7 +55,7 @@ export function initPlugins(dir) {
61
55
  }
62
56
  /**
63
57
  * Resolve a Plugin by id. The id is either:
64
- * - a built-in id (e.g. "ai-sdk", "shell"), or
58
+ * - a built-in id (e.g. "openbot", "shell"), or
65
59
  * - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
66
60
  * in which case the folder layout is `plugins/<id>/dist/index.js`.
67
61
  */
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Tracks in-flight agent runs so they can be cancelled.
3
+ *
4
+ * Runs are grouped by `channelId:threadId`. Delegated sub-agents run in the
5
+ * same channel/thread as their parent, so aborting that key stops the whole
6
+ * chain (parent + any delegated runs) in one shot.
7
+ */
8
+ export const abortKey = (channelId, threadId) => `${channelId}:${threadId || ''}`;
9
+ class AbortRegistry {
10
+ constructor() {
11
+ this.entries = new Map();
12
+ }
13
+ /** Register interest in a run. Returns a shared signal for the key. */
14
+ acquire(key) {
15
+ let entry = this.entries.get(key);
16
+ if (!entry) {
17
+ entry = { controller: new AbortController(), refs: 0 };
18
+ this.entries.set(key, entry);
19
+ }
20
+ entry.refs += 1;
21
+ return entry.controller.signal;
22
+ }
23
+ /** Release interest. Removes the entry once no runs reference it. */
24
+ release(key) {
25
+ const entry = this.entries.get(key);
26
+ if (!entry)
27
+ return;
28
+ entry.refs -= 1;
29
+ if (entry.refs <= 0) {
30
+ this.entries.delete(key);
31
+ }
32
+ }
33
+ /** Abort all runs for the key. Returns true if something was active. */
34
+ abort(key) {
35
+ const entry = this.entries.get(key);
36
+ if (!entry)
37
+ return false;
38
+ entry.controller.abort();
39
+ this.entries.delete(key);
40
+ return true;
41
+ }
42
+ }
43
+ export const abortRegistry = new AbortRegistry();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ /** Resolved plugins (built-in + community); shared with registry resolution. */
2
+ export const resolvedPluginCache = new Map();
3
+ /** Community plugin ids that have already been logged as loaded once. */
4
+ export const loadedCommunityPlugins = new Set();
5
+ /** Drop a single id from the in-memory resolver cache (e.g. after install/uninstall). */
6
+ export function invalidatePlugin(id) {
7
+ resolvedPluginCache.delete(id);
8
+ loadedCommunityPlugins.delete(id);
9
+ }
@@ -0,0 +1,112 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { openbotPlugin } from '../../plugins/openbot/index.js';
5
+ import { bashPlugin } from '../../plugins/bash/index.js';
6
+ import { storagePlugin } from '../../plugins/storage/index.js';
7
+ import { approvalPlugin } from '../../plugins/approval/index.js';
8
+ import { memoryPlugin } from '../../plugins/memory/index.js';
9
+ import { delegationPlugin } from '../../plugins/delegation/index.js';
10
+ import { uiPlugin } from '../../plugins/ui/index.js';
11
+ import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
12
+ import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../../app/config.js';
13
+ import { invalidatePlugin as clearResolvedPluginEntry, loadedCommunityPlugins, resolvedPluginCache, } from './plugin-cache.js';
14
+ let pluginsDir = null;
15
+ const BUILT_IN = {
16
+ [openbotPlugin.id]: openbotPlugin,
17
+ [bashPlugin.id]: bashPlugin,
18
+ [storagePlugin.id]: storagePlugin,
19
+ [approvalPlugin.id]: approvalPlugin,
20
+ [memoryPlugin.id]: memoryPlugin,
21
+ [delegationPlugin.id]: delegationPlugin,
22
+ [uiPlugin.id]: uiPlugin,
23
+ [pluginManagerPlugin.id]: pluginManagerPlugin,
24
+ };
25
+ /** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
26
+ export function parsePluginModule(module) {
27
+ const raw = module.plugin ??
28
+ module.default;
29
+ if (!raw || typeof raw !== 'object')
30
+ return null;
31
+ const factory = raw.factory;
32
+ if (typeof factory !== 'function')
33
+ return null;
34
+ const name = typeof raw.name === 'string' ? raw.name : '';
35
+ const description = typeof raw.description === 'string' ? raw.description : '';
36
+ const image = typeof raw.image === 'string' ? raw.image : undefined;
37
+ const configSchema = raw.configSchema;
38
+ const toolDefinitions = raw.toolDefinitions;
39
+ return {
40
+ name,
41
+ description,
42
+ image,
43
+ configSchema,
44
+ toolDefinitions,
45
+ factory: factory,
46
+ };
47
+ }
48
+ /** Initialize the on-disk plugins directory (defaults to ~/.openbot/plugins). */
49
+ export function initPlugins(dir) {
50
+ if (dir) {
51
+ pluginsDir = dir;
52
+ }
53
+ else {
54
+ const config = loadConfig();
55
+ const baseDir = config.baseDir || DEFAULT_BASE_DIR;
56
+ pluginsDir = path.join(resolvePath(baseDir), DEFAULT_PLUGINS_DIR);
57
+ }
58
+ }
59
+ /**
60
+ * Resolve a Plugin by id. The id is either:
61
+ * - a built-in id (e.g. "openbot", "bash"), or
62
+ * - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
63
+ * in which case the folder layout is `plugins/<id>/dist/index.js`.
64
+ */
65
+ export async function resolvePlugin(id) {
66
+ if (resolvedPluginCache.has(id))
67
+ return resolvedPluginCache.get(id);
68
+ if (BUILT_IN[id]) {
69
+ resolvedPluginCache.set(id, BUILT_IN[id]);
70
+ return BUILT_IN[id];
71
+ }
72
+ if (!pluginsDir) {
73
+ initPlugins();
74
+ }
75
+ if (!pluginsDir)
76
+ return null;
77
+ const distPath = path.join(pluginsDir, id, 'dist', 'index.js');
78
+ if (!fs.existsSync(distPath)) {
79
+ console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
80
+ return null;
81
+ }
82
+ try {
83
+ const module = await import(pathToFileURL(distPath).href);
84
+ const parsed = parsePluginModule(module);
85
+ if (!parsed) {
86
+ console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
87
+ return null;
88
+ }
89
+ const plugin = { id, ...parsed, name: parsed.name || id };
90
+ resolvedPluginCache.set(id, plugin);
91
+ if (!loadedCommunityPlugins.has(id)) {
92
+ console.log(`[plugins] Loaded community plugin "${id}" from ${distPath}`);
93
+ loadedCommunityPlugins.add(id);
94
+ }
95
+ return plugin;
96
+ }
97
+ catch (e) {
98
+ console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
99
+ return null;
100
+ }
101
+ }
102
+ /** Drop a single id from the in-memory cache (e.g. after fresh install). */
103
+ export function invalidatePlugin(id) {
104
+ clearResolvedPluginEntry(id);
105
+ }
106
+ /** List built-in plugins (for marketplace/registry views). */
107
+ export function listBuiltInPlugins() {
108
+ return Object.values(BUILT_IN);
109
+ }
110
+ export function getPluginsDir() {
111
+ return pluginsDir;
112
+ }