openbot 0.5.4 → 0.5.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 (50) hide show
  1. package/CLAUDE.md +5 -4
  2. package/deploy/README.md +2 -4
  3. package/dist/app/cli.js +3 -1
  4. package/dist/app/cloud-mode.js +5 -16
  5. package/dist/app/ensure-default-stack.js +54 -0
  6. package/dist/app/openbot-plugin.js +4 -0
  7. package/dist/app/server.js +2 -3
  8. package/dist/harness/index.js +3 -0
  9. package/dist/plugins/openbot/index.js +6 -5
  10. package/dist/plugins/openbot/model.js +2 -41
  11. package/dist/plugins/openbot/runtime.js +9 -4
  12. package/dist/plugins/storage/index.js +3 -325
  13. package/dist/plugins/storage/service.js +18 -21
  14. package/dist/services/plugins/host.js +21 -0
  15. package/dist/services/plugins/model-registry.js +34 -9
  16. package/dist/services/plugins/registry.js +26 -42
  17. package/dist/services/plugins/service.js +5 -1
  18. package/dist/services/todo/types.js +1 -0
  19. package/docs/plugins.md +14 -12
  20. package/docs/templates/AGENT.example.md +8 -14
  21. package/package.json +2 -2
  22. package/src/app/cli.ts +3 -1
  23. package/src/app/cloud-mode.ts +7 -22
  24. package/src/app/ensure-default-stack.ts +63 -0
  25. package/src/app/openbot-plugin.ts +5 -0
  26. package/src/app/server.ts +2 -5
  27. package/src/app/types.ts +4 -8
  28. package/src/harness/index.ts +4 -0
  29. package/src/plugins/storage/index.ts +3 -368
  30. package/src/plugins/storage/service.ts +17 -22
  31. package/src/services/plugins/host.ts +36 -0
  32. package/src/services/plugins/model-registry.ts +41 -9
  33. package/src/services/plugins/registry.ts +28 -43
  34. package/src/services/plugins/service.ts +13 -12
  35. package/src/services/plugins/types.ts +36 -2
  36. package/src/services/todo/types.ts +12 -0
  37. package/src/plugins/approval/index.ts +0 -147
  38. package/src/plugins/bash/index.ts +0 -545
  39. package/src/plugins/delegation/index.ts +0 -153
  40. package/src/plugins/memory/index.ts +0 -182
  41. package/src/plugins/openbot/context.ts +0 -137
  42. package/src/plugins/openbot/history.ts +0 -158
  43. package/src/plugins/openbot/index.ts +0 -95
  44. package/src/plugins/openbot/model.ts +0 -76
  45. package/src/plugins/openbot/runtime.ts +0 -504
  46. package/src/plugins/openbot/system-prompt.ts +0 -57
  47. package/src/plugins/preview/index.ts +0 -323
  48. package/src/plugins/todo/index.ts +0 -166
  49. package/src/plugins/todo/service.ts +0 -123
  50. package/src/plugins/ui/index.ts +0 -130
@@ -1,90 +1,12 @@
1
- import z from 'zod';
2
1
  import { buildWorkspaceFileUrl } from './files.js';
3
2
  /**
4
- * `storage` — exposes channel/thread/variable mutation tools and provides
5
- * platform-level storage handlers.
3
+ * `storage` — platform infra handlers for `/api/state` and deterministic reads.
4
+ * Agent-facing storage tools live in `@meetopenbot/openbot`.
6
5
  */
7
- const storageToolDefinitions = {
8
- create_channel: {
9
- description: 'Create a new channel. Use when the user intent is clearly different from the current channel and should be split. Always confirm before creating. Skip for simple Q&A.',
10
- inputSchema: z.object({
11
- channelId: z
12
- .string()
13
- .describe('Unique channel ID (e.g. product-launch, backend-platform, channel_roadmap).'),
14
- spec: z
15
- .string()
16
- .optional()
17
- .describe('Optional initial markdown content for the channel spec.'),
18
- initialState: z
19
- .record(z.string(), z.unknown())
20
- .optional()
21
- .describe('Optional initial state object for the channel.'),
22
- cwd: z
23
- .string()
24
- .optional()
25
- .describe('Optional initial current working directory for the channel. Defaults to an absolute path under ~/openbot/{channelId}.'),
26
- }),
27
- },
28
- patch_channel_details: {
29
- description: 'Patch current channel details (state, spec, cwd).',
30
- inputSchema: z
31
- .object({
32
- state: z
33
- .record(z.string(), z.unknown())
34
- .optional()
35
- .describe('JSON state object for the channel.'),
36
- spec: z
37
- .string()
38
- .optional()
39
- .describe('Markdown content for the channel specification (SPEC.md). Use for goals and rules.'),
40
- cwd: z.string().optional().describe('Current working directory for the channel.'),
41
- })
42
- .refine((value) => value.state !== undefined ||
43
- value.spec !== undefined ||
44
- value.cwd !== undefined, { message: 'Provide at least one of state, spec, or cwd.' }),
45
- },
46
- patch_thread_details: {
47
- description: 'Patch current thread details (state). Use for thread metadata such as `name` or `isSmartNamed`. For multi-step task tracking, use `todo_write` instead.',
48
- inputSchema: z.object({
49
- state: z
50
- .record(z.string(), z.unknown())
51
- .describe('JSON state object for the thread. Merges with existing state.'),
52
- }),
53
- },
54
- create_variable: {
55
- description: 'Create or update a variable in the workspace storage.',
56
- inputSchema: z.object({
57
- key: z.string().describe('The key of the variable.'),
58
- value: z.string().describe('The value of the variable.'),
59
- secret: z.boolean().optional().describe('Whether the variable is a secret.'),
60
- }),
61
- },
62
- delete_variable: {
63
- description: 'Delete a variable from the workspace storage.',
64
- inputSchema: z.object({
65
- key: z.string().describe('The key of the variable to delete.'),
66
- }),
67
- },
68
- delete_channel: {
69
- description: 'Permanently delete a channel and all its threads and events. Always confirm with the user before deleting.',
70
- inputSchema: z.object({
71
- channelId: z.string().describe('The channel ID to delete.'),
72
- }),
73
- },
74
- get_workspace_file_url: {
75
- description: 'Get a fetchable HTTP URL for a file in the current channel workspace (images, video, audio, documents).',
76
- inputSchema: z.object({
77
- path: z
78
- .string()
79
- .describe('Path relative to the channel working directory, e.g. "uploads/clip.mp4".'),
80
- }),
81
- },
82
- };
83
6
  export const storagePlugin = {
84
7
  id: 'storage',
85
8
  name: 'Storage',
86
- description: 'Tools for creating channels, patching state, and managing workspace variables.',
87
- toolDefinitions: storageToolDefinitions,
9
+ description: 'Infrastructure storage handlers for channels, threads, agents, and files.',
88
10
  factory: ({ storage, publicBaseUrl }) => (builder) => {
89
11
  const resolvePublicBaseUrl = () => publicBaseUrl;
90
12
  builder.on('action:create_thread', async function* (event, context) {
@@ -119,215 +41,6 @@ export const storagePlugin = {
119
41
  meta: { ...(event.meta || {}), threadId, agentId: context.state.agentId },
120
42
  };
121
43
  });
122
- builder.on('action:create_channel', async function* (event, context) {
123
- const { channelId, spec, initialState, cwd } = event.data;
124
- const rawChannelId = (channelId || '').trim();
125
- const channelSpec = typeof spec === 'string' ? spec : '';
126
- const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
127
- if (!rawChannelId) {
128
- yield {
129
- type: 'action:create_channel:result',
130
- data: { success: false, channelId: '', channelUrl: '' },
131
- meta: resultMeta,
132
- };
133
- return;
134
- }
135
- const channelUrl = `/channels/${rawChannelId}`;
136
- const mergedInitial = { ...(initialState || {}) };
137
- try {
138
- await storage.createChannel({
139
- channelId: rawChannelId,
140
- spec: channelSpec,
141
- initialState: mergedInitial,
142
- cwd,
143
- });
144
- yield {
145
- type: 'action:create_channel:result',
146
- data: { success: true, channelId: rawChannelId, channelUrl },
147
- meta: resultMeta,
148
- };
149
- yield {
150
- type: 'agent:output',
151
- data: { content: `Created channel \`${rawChannelId}\`.` },
152
- meta: resultMeta,
153
- };
154
- }
155
- catch {
156
- yield {
157
- type: 'action:create_channel:result',
158
- data: { success: false, channelId: rawChannelId, channelUrl },
159
- meta: resultMeta,
160
- };
161
- }
162
- });
163
- builder.on('action:delete_channel', async function* (event, context) {
164
- const rawChannelId = (event.data?.channelId || '').trim();
165
- const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
166
- if (!rawChannelId) {
167
- yield {
168
- type: 'action:delete_channel:result',
169
- data: { success: false, channelId: '', error: 'channelId is required' },
170
- meta: resultMeta,
171
- };
172
- return;
173
- }
174
- try {
175
- await storage.deleteChannel({ channelId: rawChannelId });
176
- yield {
177
- type: 'action:delete_channel:result',
178
- data: { success: true, channelId: rawChannelId },
179
- meta: resultMeta,
180
- };
181
- yield {
182
- type: 'agent:output',
183
- data: { content: `Deleted channel \`${rawChannelId}\`.` },
184
- meta: resultMeta,
185
- };
186
- }
187
- catch (error) {
188
- yield {
189
- type: 'action:delete_channel:result',
190
- data: {
191
- success: false,
192
- channelId: rawChannelId,
193
- error: error instanceof Error ? error.message : 'Unknown error',
194
- },
195
- meta: resultMeta,
196
- };
197
- }
198
- });
199
- builder.on('action:update_channel', async function* (event, context) {
200
- const data = (event.data || {});
201
- const targetChannelId = (data.channelId || context.state.channelId || '').trim();
202
- const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
203
- if (!targetChannelId) {
204
- yield {
205
- type: 'action:update_channel:result',
206
- data: { success: false, channelId: '', updatedFields: [] },
207
- meta: resultMeta,
208
- };
209
- return;
210
- }
211
- const patch = {};
212
- const updatedFields = [];
213
- if (typeof data.name === 'string' && data.name.trim()) {
214
- patch.name = data.name.trim();
215
- updatedFields.push('name');
216
- }
217
- if (typeof data.cwd === 'string' && data.cwd.trim()) {
218
- patch.cwd = data.cwd.trim();
219
- updatedFields.push('cwd');
220
- }
221
- try {
222
- if (updatedFields.length > 0) {
223
- await storage.patchChannelState({ channelId: targetChannelId, state: patch });
224
- }
225
- if (targetChannelId === context.state.channelId) {
226
- context.state.channelDetails = await storage.getChannelDetails({
227
- channelId: context.state.channelId,
228
- });
229
- }
230
- yield {
231
- type: 'action:update_channel:result',
232
- data: { success: true, channelId: targetChannelId, updatedFields },
233
- meta: resultMeta,
234
- };
235
- }
236
- catch {
237
- yield {
238
- type: 'action:update_channel:result',
239
- data: { success: false, channelId: targetChannelId, updatedFields },
240
- meta: resultMeta,
241
- };
242
- }
243
- });
244
- builder.on('action:patch_channel_details', async function* (event, context) {
245
- const updatedFields = [];
246
- const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
247
- const data = (event.data || {});
248
- try {
249
- if (data.state !== undefined) {
250
- await storage.patchChannelState({
251
- channelId: context.state.channelId,
252
- state: data.state,
253
- });
254
- updatedFields.push('state');
255
- }
256
- if (typeof data.spec === 'string') {
257
- await storage.patchChannelSpec({
258
- channelId: context.state.channelId,
259
- spec: data.spec,
260
- });
261
- updatedFields.push('spec');
262
- }
263
- if (typeof data.cwd === 'string') {
264
- await storage.patchChannelState({
265
- channelId: context.state.channelId,
266
- state: { cwd: data.cwd },
267
- });
268
- updatedFields.push('cwd');
269
- }
270
- context.state.channelDetails = await storage.getChannelDetails({
271
- channelId: context.state.channelId,
272
- });
273
- yield {
274
- type: "client:ui:widget",
275
- data: {
276
- widgetId: "patch-channel-details-result" + Date.now(),
277
- kind: "message",
278
- title: "Channel details updated.",
279
- body: `The channel details have been updated. ${updatedFields.join(', ')}`,
280
- display: "collapsed",
281
- },
282
- meta: resultMeta,
283
- };
284
- yield {
285
- type: 'action:patch_channel_details:result',
286
- data: { success: true, updatedFields },
287
- meta: resultMeta,
288
- };
289
- }
290
- catch {
291
- yield {
292
- type: 'action:patch_channel_details:result',
293
- data: { success: false, updatedFields },
294
- meta: resultMeta,
295
- };
296
- }
297
- });
298
- builder.on('action:patch_thread_details', async function* (event, context) {
299
- const updatedFields = [];
300
- const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
301
- try {
302
- if (!context.state.threadId) {
303
- throw new Error('Missing threadId in state for patch_thread_details');
304
- }
305
- if (event.data.state !== undefined) {
306
- await storage.patchThreadState({
307
- channelId: context.state.channelId,
308
- threadId: context.state.threadId,
309
- state: event.data.state,
310
- });
311
- updatedFields.push('state');
312
- }
313
- context.state.threadDetails = await storage.getThreadDetails({
314
- channelId: context.state.channelId,
315
- threadId: context.state.threadId,
316
- });
317
- yield {
318
- type: 'action:patch_thread_details:result',
319
- data: { success: true, updatedFields },
320
- meta: resultMeta,
321
- };
322
- }
323
- catch {
324
- yield {
325
- type: 'action:patch_thread_details:result',
326
- data: { success: false, updatedFields },
327
- meta: resultMeta,
328
- };
329
- }
330
- });
331
44
  builder.on('agent:usage', async function* (event, context) {
332
45
  const { usage } = event.data;
333
46
  if (!context.state.threadId)
@@ -671,41 +384,6 @@ export const storagePlugin = {
671
384
  };
672
385
  }
673
386
  });
674
- builder.on('action:get_workspace_file_url', async function* (event, context) {
675
- const channelId = context.state.channelId;
676
- const filePath = event.data?.path;
677
- const toolCallId = event.meta?.toolCallId;
678
- if (!filePath) {
679
- yield {
680
- type: 'action:get_workspace_file_url:result',
681
- data: { success: false, path: '', error: 'Path is required', output: 'Path is required' },
682
- meta: { ...(event.meta || {}), toolCallId },
683
- };
684
- return;
685
- }
686
- try {
687
- const { size, mimeType } = await storage.getChannelFileStat({ channelId, path: filePath });
688
- const url = buildWorkspaceFileUrl({
689
- baseUrl: resolvePublicBaseUrl(),
690
- channelId,
691
- filePath,
692
- });
693
- const output = JSON.stringify({ path: filePath, url, mimeType, size });
694
- yield {
695
- type: 'action:get_workspace_file_url:result',
696
- data: { success: true, path: filePath, url, mimeType, size, output },
697
- meta: { ...(event.meta || {}), toolCallId },
698
- };
699
- }
700
- catch (error) {
701
- const message = error instanceof Error ? error.message : 'Unknown error';
702
- yield {
703
- type: 'action:get_workspace_file_url:result',
704
- data: { success: false, path: filePath, error: message, output: message },
705
- meta: { ...(event.meta || {}), toolCallId },
706
- };
707
- }
708
- });
709
387
  },
710
388
  };
711
389
  export default storagePlugin;
@@ -1,5 +1,5 @@
1
1
  import { ORCHESTRATOR_AGENT_ID, STATE_AGENT_ID } from '../../app/agent-ids.js';
2
- import { assertCloudSystemAgentPluginsMutable, CLOUD_SYSTEM_MODEL, isCloudMode, } from '../../app/cloud-mode.js';
2
+ import { assertCloudSystemAgentPluginsMutable, DEFAULT_CLOUD_OPENBOT_AUTH_MODE, isCloudMode, } from '../../app/cloud-mode.js';
3
3
  import { DEFAULT_PLUGINS_DIR, DEFAULT_AGENTS_DIR, DEFAULT_CHANNELS_DIR, getBaseDir, getDefaultChannelCwd, resolvePath, VARIABLES_FILE, } from '../../app/config.js';
4
4
  import fs from 'node:fs/promises';
5
5
  import { readFileSync } from 'node:fs';
@@ -7,7 +7,7 @@ import path from 'node:path';
7
7
  import { fileURLToPath, pathToFileURL } from 'node:url';
8
8
  import crypto from 'node:crypto';
9
9
  import matter from 'gray-matter';
10
- import { openbotPlugin } from '../openbot/index.js';
10
+ import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
11
11
  import { listBuiltInPlugins, parsePluginModule } from '../../services/plugins/registry.js';
12
12
  import { enrichOpenbotPluginDescriptor, listRegistryModelOptions, } from '../../services/plugins/model-registry.js';
13
13
  import { processService } from '../../services/process.js';
@@ -80,29 +80,28 @@ const getConversationDir = (channelId, threadId) => {
80
80
  const SYSTEM_AGENT_ID = ORCHESTRATOR_AGENT_ID;
81
81
  const SYSTEM_DEFAULT_PLUGINS = [
82
82
  {
83
- id: 'openbot',
83
+ id: OPENBOT_PLUGIN_ID,
84
84
  config: {
85
85
  model: 'openai/gpt-5.4-mini',
86
- approval: {
87
- actions: ['action:shell_exec', 'action:create_channel', 'action:delete_channel'],
88
- },
89
86
  },
90
87
  },
91
88
  ];
92
- const CLOUD_SYSTEM_DEFAULT_PLUGINS = [
93
- {
94
- id: 'openbot',
95
- config: {
96
- model: CLOUD_SYSTEM_MODEL,
97
- approval: {
98
- actions: ['action:shell_exec', 'action:create_channel', 'action:delete_channel'],
89
+ function getCloudSystemDefaultPlugins() {
90
+ return SYSTEM_DEFAULT_PLUGINS.map((ref) => {
91
+ if (ref.id !== OPENBOT_PLUGIN_ID)
92
+ return ref;
93
+ return {
94
+ ...ref,
95
+ config: {
96
+ ...ref.config,
97
+ authMode: DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
99
98
  },
100
- },
101
- },
102
- ];
99
+ };
100
+ });
101
+ }
103
102
  function mergeCloudSystemPluginRefs(defaults, diskRefs) {
104
- const defaultOpenbot = defaults.find((ref) => ref.id === 'openbot');
105
- const diskOpenbot = diskRefs.find((ref) => ref.id === 'openbot');
103
+ const defaultOpenbot = defaults.find((ref) => ref.id === OPENBOT_PLUGIN_ID);
104
+ const diskOpenbot = diskRefs.find((ref) => ref.id === OPENBOT_PLUGIN_ID);
106
105
  if (!defaultOpenbot)
107
106
  return defaults;
108
107
  return [
@@ -122,7 +121,7 @@ const STATE_DEFAULT_PLUGINS = [
122
121
  ];
123
122
  const STATE_AGENT_INSTRUCTIONS = 'Built-in infra agent for deterministic state reads. No conversational model is attached; handle storage, approvals, memory, and plugin marketplace events.';
124
123
  function getSystemAgentDetails(overrides) {
125
- const defaultPlugins = isCloudMode() ? CLOUD_SYSTEM_DEFAULT_PLUGINS : SYSTEM_DEFAULT_PLUGINS;
124
+ const defaultPlugins = isCloudMode() ? getCloudSystemDefaultPlugins() : SYSTEM_DEFAULT_PLUGINS;
126
125
  const defaults = {
127
126
  id: SYSTEM_AGENT_ID,
128
127
  name: 'OpenBot',
@@ -210,8 +209,6 @@ const agentSummaryFromDetails = (details) => ({
210
209
  createdAt: details.createdAt,
211
210
  updatedAt: details.updatedAt,
212
211
  });
213
- // Suppress unused warning until system agent customization re-uses openbotPlugin metadata.
214
- void openbotPlugin;
215
212
  /** Built-in agents may persist optional `agents/<id>/AGENT.md` overlays; read path merges them with defaults. */
216
213
  const isBuiltinOverlayAgentId = (agentId) => agentId === SYSTEM_AGENT_ID || agentId === STATE_AGENT_ID;
217
214
  const assertAgentIdFormat = (agentId) => {
@@ -0,0 +1,21 @@
1
+ import { DEFAULT_CLOUD_OPENBOT_AUTH_MODE, isCloudMode, isCloudSystemAgent, parseOpenbotAuthMode, } from '../../app/cloud-mode.js';
2
+ import { ORCHESTRATOR_AGENT_ID } from '../../app/agent-ids.js';
3
+ import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
4
+ import { getBaseDir, resolvePath, saveConfig } from '../../app/config.js';
5
+ import { listApiKeyProvidersFromRegistry, resolveModelRegistry, } from './model-registry.js';
6
+ export function createPluginHost(runAgent) {
7
+ return {
8
+ runAgent,
9
+ isCloudSystemAgent,
10
+ isCloudMode,
11
+ parseOpenbotAuthMode,
12
+ resolveModelRegistry,
13
+ listApiKeyProvidersFromRegistry,
14
+ saveConfig,
15
+ getBaseDir,
16
+ resolvePath,
17
+ orchestratorAgentId: ORCHESTRATOR_AGENT_ID,
18
+ openbotPluginId: OPENBOT_PLUGIN_ID,
19
+ defaultCloudAuthMode: DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
20
+ };
21
+ }
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_MARKETPLACE_REGISTRY_URL, loadConfig, } from '../../app/config.js';
2
- import { isCloudMode } from '../../app/cloud-mode.js';
2
+ import { OPENBOT_PLUGIN_ID } from '../../app/openbot-plugin.js';
3
+ import { DEFAULT_CLOUD_OPENBOT_AUTH_MODE, isCloudMode, } from '../../app/cloud-mode.js';
3
4
  let cachedRegistry = null;
4
5
  let cacheUrl = null;
5
6
  function getRegistryUrl() {
@@ -57,13 +58,11 @@ function pickDefaultModelValue(options) {
57
58
  if (options.length === 0)
58
59
  return undefined;
59
60
  const values = options.map((option) => option.value);
60
- const preferred = isCloudMode()
61
- ? values.find((value) => value.startsWith('openbot/'))
62
- : values.find((value) => value.startsWith('openai/'));
61
+ const preferred = values.find((value) => value.startsWith('openai/'));
63
62
  return preferred ?? values[0];
64
63
  }
65
64
  export function enrichOpenbotPluginDescriptor(descriptor, modelOptions) {
66
- if (descriptor.id !== 'openbot' || !descriptor.configSchema)
65
+ if (descriptor.id !== OPENBOT_PLUGIN_ID || !descriptor.configSchema)
67
66
  return descriptor;
68
67
  const modelProperty = descriptor.configSchema.properties.model;
69
68
  if (!modelProperty)
@@ -88,14 +87,40 @@ export function enrichOpenbotPluginDescriptor(descriptor, modelOptions) {
88
87
  if (defaultModel) {
89
88
  nextModelProperty.default = defaultModel;
90
89
  }
90
+ const { model: _model, authMode: _authMode, ...otherProperties } = descriptor.configSchema.properties;
91
+ const cloudAuthModeProperty = {
92
+ type: 'string',
93
+ description: 'Credits uses OpenBot platform billing. BYOK uses your own API keys.',
94
+ enum: ['credits', 'byok'],
95
+ default: DEFAULT_CLOUD_OPENBOT_AUTH_MODE,
96
+ options: [
97
+ {
98
+ label: 'Credits',
99
+ value: 'credits',
100
+ description: 'Use OpenBot platform credits.',
101
+ },
102
+ {
103
+ label: 'Bring your own key',
104
+ value: 'byok',
105
+ description: 'Use your own provider API keys.',
106
+ },
107
+ ],
108
+ };
109
+ const nextProperties = isCloudMode()
110
+ ? {
111
+ authMode: cloudAuthModeProperty,
112
+ model: nextModelProperty,
113
+ ...otherProperties,
114
+ }
115
+ : {
116
+ model: nextModelProperty,
117
+ ...otherProperties,
118
+ };
91
119
  return {
92
120
  ...descriptor,
93
121
  configSchema: {
94
122
  ...descriptor.configSchema,
95
- properties: {
96
- ...descriptor.configSchema.properties,
97
- model: nextModelProperty,
98
- },
123
+ properties: nextProperties,
99
124
  },
100
125
  };
101
126
  }
@@ -1,30 +1,14 @@
1
- import fs from 'node:fs';
1
+ import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
- import { openbotPlugin } from '../../plugins/openbot/index.js';
5
- import { bashPlugin } from '../../plugins/bash/index.js';
6
4
  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 { todoPlugin } from '../../plugins/todo/index.js';
10
- import { delegationPlugin } from '../../plugins/delegation/index.js';
11
- import { uiPlugin } from '../../plugins/ui/index.js';
12
5
  import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
13
- import { previewPlugin } from '../../plugins/preview/index.js';
14
6
  import { DEFAULT_PLUGINS_DIR, getBaseDir } from '../../app/config.js';
15
7
  import { invalidatePlugin as clearResolvedPluginEntry, loadedCommunityPlugins, resolvedPluginCache, } from './plugin-cache.js';
16
8
  let pluginsDir = null;
17
9
  const BUILT_IN = {
18
- [openbotPlugin.id]: openbotPlugin,
19
- [bashPlugin.id]: bashPlugin,
20
10
  [storagePlugin.id]: storagePlugin,
21
- [approvalPlugin.id]: approvalPlugin,
22
- [memoryPlugin.id]: memoryPlugin,
23
- [todoPlugin.id]: todoPlugin,
24
- [delegationPlugin.id]: delegationPlugin,
25
- [uiPlugin.id]: uiPlugin,
26
11
  [pluginManagerPlugin.id]: pluginManagerPlugin,
27
- [previewPlugin.id]: previewPlugin,
28
12
  };
29
13
  /** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
30
14
  export function parsePluginModule(module) {
@@ -60,46 +44,46 @@ export function initPlugins(dir) {
60
44
  }
61
45
  /**
62
46
  * Resolve a Plugin by id. The id is either:
63
- * - a built-in id (e.g. "openbot", "bash"), or
64
- * - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
47
+ * - a built-in id (e.g. "storage", "plugin-manager"), or
48
+ * - an npm package name (e.g. "@meetopenbot/openbot"),
65
49
  * in which case the folder layout is `plugins/<id>/dist/index.js`.
66
50
  */
67
51
  export async function resolvePlugin(id) {
68
52
  if (resolvedPluginCache.has(id))
69
53
  return resolvedPluginCache.get(id);
70
- if (BUILT_IN[id]) {
71
- resolvedPluginCache.set(id, BUILT_IN[id]);
72
- return BUILT_IN[id];
73
- }
74
54
  if (!pluginsDir) {
75
55
  initPlugins();
76
56
  }
77
57
  if (!pluginsDir)
78
58
  return null;
79
59
  const distPath = path.join(pluginsDir, id, 'dist', 'index.js');
80
- if (!fs.existsSync(distPath)) {
81
- console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
82
- return null;
83
- }
84
- try {
85
- const module = await import(pathToFileURL(distPath).href);
86
- const parsed = parsePluginModule(module);
87
- if (!parsed) {
88
- console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
89
- return null;
60
+ if (existsSync(distPath)) {
61
+ try {
62
+ const module = await import(pathToFileURL(distPath).href);
63
+ const parsed = parsePluginModule(module);
64
+ if (!parsed) {
65
+ console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
66
+ }
67
+ else {
68
+ const plugin = { id, ...parsed, name: parsed.name || id };
69
+ resolvedPluginCache.set(id, plugin);
70
+ if (!loadedCommunityPlugins.has(id)) {
71
+ console.log(`[plugins] Loaded plugin "${id}" from ${distPath}`);
72
+ loadedCommunityPlugins.add(id);
73
+ }
74
+ return plugin;
75
+ }
90
76
  }
91
- const plugin = { id, ...parsed, name: parsed.name || id };
92
- resolvedPluginCache.set(id, plugin);
93
- if (!loadedCommunityPlugins.has(id)) {
94
- console.log(`[plugins] Loaded community plugin "${id}" from ${distPath}`);
95
- loadedCommunityPlugins.add(id);
77
+ catch (e) {
78
+ console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
96
79
  }
97
- return plugin;
98
80
  }
99
- catch (e) {
100
- console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
101
- return null;
81
+ if (BUILT_IN[id]) {
82
+ resolvedPluginCache.set(id, BUILT_IN[id]);
83
+ return BUILT_IN[id];
102
84
  }
85
+ console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
86
+ return null;
103
87
  }
104
88
  /** Drop a single id from the in-memory cache (e.g. after fresh install). */
105
89
  export function invalidatePlugin(id) {
@@ -6,6 +6,9 @@ import { promisify } from 'node:util';
6
6
  import { DEFAULT_PLUGINS_DIR, DEFAULT_MARKETPLACE_REGISTRY_URL, getBaseDir, loadConfig, } from '../../app/config.js';
7
7
  import { invalidatePlugin } from './plugin-cache.js';
8
8
  const execAsync = promisify(exec);
9
+ function isLatestVersionAlias(version) {
10
+ return version.trim().toLowerCase() === 'latest';
11
+ }
9
12
  const DEFAULT_MARKETPLACE_AGENTS = [];
10
13
  const DEFAULT_MARKETPLACE_CHANNELS = [];
11
14
  function isRecord(value) {
@@ -155,7 +158,8 @@ export const pluginService = {
155
158
  if (existsSync(path.join(finalPath, 'package.json'))) {
156
159
  try {
157
160
  const pkgJson = JSON.parse(await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'));
158
- if (!version || pkgJson.version === version) {
161
+ const wantsLatest = version !== undefined && isLatestVersionAlias(version);
162
+ if (!version || (!wantsLatest && pkgJson.version === version)) {
159
163
  console.log(`[plugins] ${packageName}${version ? `@${version}` : ''} is already installed.`);
160
164
  return { name: pkgJson.name, version: pkgJson.version };
161
165
  }
@@ -0,0 +1 @@
1
+ export {};