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,17 +1,10 @@
1
- import fs from 'node:fs';
1
+ import fs from 'node:fs/promises';
2
+ import { existsSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { pathToFileURL } from 'node:url';
4
5
  import type { Plugin } from './types.js';
5
- import { openbotPlugin } from '../../plugins/openbot/index.js';
6
- import { bashPlugin } from '../../plugins/bash/index.js';
7
6
  import { storagePlugin } from '../../plugins/storage/index.js';
8
- import { approvalPlugin } from '../../plugins/approval/index.js';
9
- import { memoryPlugin } from '../../plugins/memory/index.js';
10
- import { todoPlugin } from '../../plugins/todo/index.js';
11
- import { delegationPlugin } from '../../plugins/delegation/index.js';
12
- import { uiPlugin } from '../../plugins/ui/index.js';
13
7
  import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
14
- import { previewPlugin } from '../../plugins/preview/index.js';
15
8
  import { DEFAULT_PLUGINS_DIR, getBaseDir } from '../../app/config.js';
16
9
  import {
17
10
  invalidatePlugin as clearResolvedPluginEntry,
@@ -22,16 +15,8 @@ import {
22
15
  let pluginsDir: string | null = null;
23
16
 
24
17
  const BUILT_IN: Record<string, Plugin> = {
25
- [openbotPlugin.id]: openbotPlugin,
26
- [bashPlugin.id]: bashPlugin,
27
18
  [storagePlugin.id]: storagePlugin,
28
- [approvalPlugin.id]: approvalPlugin,
29
- [memoryPlugin.id]: memoryPlugin,
30
- [todoPlugin.id]: todoPlugin,
31
- [delegationPlugin.id]: delegationPlugin,
32
- [uiPlugin.id]: uiPlugin,
33
19
  [pluginManagerPlugin.id]: pluginManagerPlugin,
34
- [previewPlugin.id]: previewPlugin,
35
20
  };
36
21
 
37
22
  /**
@@ -81,16 +66,12 @@ export function initPlugins(dir?: string) {
81
66
 
82
67
  /**
83
68
  * Resolve a Plugin by id. The id is either:
84
- * - a built-in id (e.g. "openbot", "bash"), or
85
- * - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
69
+ * - a built-in id (e.g. "storage", "plugin-manager"), or
70
+ * - an npm package name (e.g. "@meetopenbot/openbot"),
86
71
  * in which case the folder layout is `plugins/<id>/dist/index.js`.
87
72
  */
88
73
  export async function resolvePlugin(id: string): Promise<Plugin | null> {
89
74
  if (resolvedPluginCache.has(id)) return resolvedPluginCache.get(id)!;
90
- if (BUILT_IN[id]) {
91
- resolvedPluginCache.set(id, BUILT_IN[id]);
92
- return BUILT_IN[id];
93
- }
94
75
 
95
76
  if (!pluginsDir) {
96
77
  initPlugins();
@@ -99,29 +80,33 @@ export async function resolvePlugin(id: string): Promise<Plugin | null> {
99
80
 
100
81
  const distPath = path.join(pluginsDir, id, 'dist', 'index.js');
101
82
 
102
- if (!fs.existsSync(distPath)) {
103
- console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
104
- return null;
83
+ if (existsSync(distPath)) {
84
+ try {
85
+ const module = await import(pathToFileURL(distPath).href);
86
+ const parsed = parsePluginModule(module as Record<string, unknown>);
87
+ if (!parsed) {
88
+ console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
89
+ } else {
90
+ const plugin: Plugin = { id, ...parsed, name: parsed.name || id };
91
+ resolvedPluginCache.set(id, plugin);
92
+ if (!loadedCommunityPlugins.has(id)) {
93
+ console.log(`[plugins] Loaded plugin "${id}" from ${distPath}`);
94
+ loadedCommunityPlugins.add(id);
95
+ }
96
+ return plugin;
97
+ }
98
+ } catch (e) {
99
+ console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
100
+ }
105
101
  }
106
102
 
107
- try {
108
- const module = await import(pathToFileURL(distPath).href);
109
- const parsed = parsePluginModule(module as Record<string, unknown>);
110
- if (!parsed) {
111
- console.warn(`[plugins] Plugin "${id}" at ${distPath} has no recognizable export.`);
112
- return null;
113
- }
114
- const plugin: Plugin = { id, ...parsed, name: parsed.name || id };
115
- resolvedPluginCache.set(id, plugin);
116
- if (!loadedCommunityPlugins.has(id)) {
117
- console.log(`[plugins] Loaded community plugin "${id}" from ${distPath}`);
118
- loadedCommunityPlugins.add(id);
119
- }
120
- return plugin;
121
- } catch (e) {
122
- console.warn(`[plugins] Failed to load plugin "${id}" from ${distPath}:`, e);
123
- return null;
103
+ if (BUILT_IN[id]) {
104
+ resolvedPluginCache.set(id, BUILT_IN[id]);
105
+ return BUILT_IN[id];
124
106
  }
107
+
108
+ console.warn(`[plugins] Plugin "${id}" not found at ${distPath}.`);
109
+ return null;
125
110
  }
126
111
 
127
112
  /** Drop a single id from the in-memory cache (e.g. after fresh install). */
@@ -17,9 +17,14 @@ const execAsync = promisify(exec);
17
17
 
18
18
  export interface InstallOptions {
19
19
  packageName: string;
20
+ /** Concrete semver, or `"latest"` to refresh from npm. Omit to ensure present without upgrading. */
20
21
  version?: string;
21
22
  }
22
23
 
24
+ function isLatestVersionAlias(version: string): boolean {
25
+ return version.trim().toLowerCase() === 'latest';
26
+ }
27
+
23
28
  export interface InstalledPlugin {
24
29
  /** npm package name; doubles as the plugin id used everywhere else. */
25
30
  name: string;
@@ -104,7 +109,9 @@ export function parseMarketplaceRegistryJson(data: unknown): MarketplaceRegistry
104
109
  if (!Array.isArray(pluginsRaw)) throw new Error(`agents[${i}].plugins must be an array`);
105
110
  const plugins: PluginRef[] = pluginsRaw.map((p, j) => {
106
111
  if (!isRecord(p) || typeof p.id !== 'string' || !p.id) {
107
- throw new Error(`agents[${i}].plugins[${j}]: expected { "id": string, "config"?: object }`);
112
+ throw new Error(
113
+ `agents[${i}].plugins[${j}]: expected { "id": string, "config"?: object }`,
114
+ );
108
115
  }
109
116
  const ref: PluginRef = { id: p.id };
110
117
  if (p.config !== undefined) {
@@ -227,7 +234,8 @@ export const pluginService = {
227
234
  const pkgJson = JSON.parse(
228
235
  await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'),
229
236
  );
230
- if (!version || pkgJson.version === version) {
237
+ const wantsLatest = version !== undefined && isLatestVersionAlias(version);
238
+ if (!version || (!wantsLatest && pkgJson.version === version)) {
231
239
  console.log(
232
240
  `[plugins] ${packageName}${version ? `@${version}` : ''} is already installed.`,
233
241
  );
@@ -263,17 +271,13 @@ export const pluginService = {
263
271
  console.warn(`[plugins] Failed to run npm install in ${finalPath}:`, e);
264
272
  }
265
273
 
266
- const pkgJson = JSON.parse(
267
- await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'),
268
- );
274
+ const pkgJson = JSON.parse(await fs.readFile(path.join(finalPath, 'package.json'), 'utf-8'));
269
275
 
270
276
  invalidatePlugin(packageName);
271
277
  return { name: pkgJson.name, version: pkgJson.version };
272
278
  } catch (error) {
273
279
  console.error(`[plugins] Failed to install ${packageName}:`, error);
274
- throw new Error(
275
- `Failed to install plugin ${packageName}: ${(error as Error).message}`,
276
- );
280
+ throw new Error(`Failed to install plugin ${packageName}: ${(error as Error).message}`);
277
281
  } finally {
278
282
  await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
279
283
  }
@@ -299,10 +303,7 @@ export const pluginService = {
299
303
  }
300
304
  } catch (error) {
301
305
  console.error(`[plugins] Failed to uninstall ${packageName}:`, error);
302
- throw new Error(
303
- `Failed to uninstall plugin ${packageName}: ${(error as Error).message}`,
304
- );
306
+ throw new Error(`Failed to uninstall plugin ${packageName}: ${(error as Error).message}`);
305
307
  }
306
308
  },
307
309
  };
308
-
@@ -5,8 +5,8 @@ import { AgentDetails, ConfigSchema, Storage } from './domain.js';
5
5
  /**
6
6
  * Reference to a plugin from an agent's AGENT.md frontmatter.
7
7
  *
8
- * The `id` is either a built-in plugin id (e.g. `openbot`, `bash`) or an npm
9
- * package name (e.g. `openbot-plugin-codex`, `@scope/openbot-plugin-foo`).
8
+ * The `id` is either a built-in plugin id (e.g. `storage`, `plugin-manager`) or
9
+ * an npm package name (e.g. `@meetopenbot/openbot`, `openbot-plugin-foo`).
10
10
  * Each entry may carry plugin-specific `config`.
11
11
  */
12
12
  export interface PluginRef {
@@ -20,6 +20,38 @@ export interface ToolDefinition {
20
20
  inputSchema: unknown;
21
21
  }
22
22
 
23
+ /** Options for delegated / nested agent runs via PluginHost.runAgent. */
24
+ export interface RunAgentHostOptions {
25
+ runId: string;
26
+ agentId: string;
27
+ event: OpenBotEvent;
28
+ channelId: string;
29
+ threadId?: string;
30
+ persistEvents?: boolean;
31
+ publicBaseUrl?: string;
32
+ onEvent: (event: OpenBotEvent, state?: OpenBotState) => Promise<void>;
33
+ }
34
+
35
+ /** Host capabilities exposed to community / external agent runtime plugins. */
36
+ export interface PluginHost {
37
+ runAgent: (options: RunAgentHostOptions) => Promise<void>;
38
+ isCloudSystemAgent: (agentId: string) => boolean;
39
+ isCloudMode: () => boolean;
40
+ parseOpenbotAuthMode: (value: unknown) => 'credits' | 'byok';
41
+ resolveModelRegistry: () => ReturnType<
42
+ typeof import('./model-registry.js').resolveModelRegistry
43
+ >;
44
+ listApiKeyProvidersFromRegistry: (
45
+ registry: Awaited<ReturnType<PluginHost['resolveModelRegistry']>>,
46
+ ) => Array<{ id: string; label: string }>;
47
+ saveConfig: (patch: Record<string, unknown>) => void;
48
+ getBaseDir: () => string;
49
+ resolvePath: (p: string) => string;
50
+ orchestratorAgentId: string;
51
+ openbotPluginId: string;
52
+ defaultCloudAuthMode: 'credits' | 'byok';
53
+ }
54
+
23
55
  /**
24
56
  * Context passed to a Plugin factory at runtime.
25
57
  *
@@ -37,6 +69,8 @@ export interface PluginContext {
37
69
  publicBaseUrl: string;
38
70
  /** Signal that fires when this run is stopped; runtimes should pass it to long-running calls. */
39
71
  abortSignal?: AbortSignal;
72
+ /** Platform hooks for external runtime plugins (delegation, cloud auth, config). */
73
+ host: PluginHost;
40
74
  }
41
75
 
42
76
  /**
@@ -0,0 +1,12 @@
1
+ export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled';
2
+
3
+ export type TodoItem = {
4
+ id: string;
5
+ content: string;
6
+ status: TodoStatus;
7
+ };
8
+
9
+ export type TodoList = {
10
+ items: TodoItem[];
11
+ updatedAt: string;
12
+ };
@@ -1,147 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import type { Plugin } from '../../services/plugins/types.js';
3
- import { OpenBotEvent } from '../../app/types.js';
4
-
5
- /**
6
- * `approval` — gates protected tool calls behind a UI confirmation widget.
7
- *
8
- * This is a simplified version that intercepts specified actions (default: bash)
9
- * and requires user approval before they are allowed to proceed.
10
- */
11
-
12
- // In-memory tracking for pending approval IDs with TTL (shared across plugin instances)
13
- const pendingApprovals = new Map<string, number>();
14
- const TTL_MS = 4 * 60 * 60 * 1000; // 4 hours
15
-
16
- export const approvalPlugin: Plugin = {
17
- id: 'approval',
18
- name: 'Approval',
19
- description: 'Gate protected tool calls behind a UI confirmation widget.',
20
- factory: ({ config, storage }) => (builder) => {
21
- // Actions that require approval. Defaults to bash.
22
- const actionsToApprove = (config.actions as string[]) || ['action:shell_exec'];
23
-
24
- for (const action of actionsToApprove) {
25
- builder.intercept(action as OpenBotEvent['type'], (event, context) => {
26
- // If already approved in this flow, let it pass to the actual handler
27
- if (event.meta?.approvalStatus === 'approved') return event;
28
-
29
- // Otherwise, intercept and ask for approval via a UI widget
30
- const displayData = JSON.stringify((event as any)?.data) || '';
31
-
32
- const widgetId = randomUUID();
33
- pendingApprovals.set(widgetId, Date.now());
34
-
35
- return {
36
- type: 'client:ui:widget',
37
- data: {
38
- widgetId,
39
- kind: 'message',
40
- title: `The agent wants to perform \`${action}\``,
41
- body: displayData,
42
- metadata: {
43
- type: 'approval:request',
44
- originalEvent: event,
45
- },
46
- actions: [
47
- { id: 'approve', label: 'Approve', variant: 'primary' },
48
- { id: 'deny', label: 'Deny', variant: 'danger' },
49
- ],
50
- },
51
- meta: { agentId: context.state.agentId, threadId: context.state.threadId },
52
- } as OpenBotEvent;
53
- });
54
- }
55
-
56
- // Handle the user's response from the UI widget
57
- builder.on('client:ui:widget:response', async function* (event, context) {
58
- const { widgetId, actionId } = event.data;
59
- const metadata = event.data?.metadata;
60
- if (metadata?.type !== 'approval:request') return;
61
-
62
- // Verify the widget is still pending and hasn't expired
63
- if (!widgetId || !pendingApprovals.has(widgetId)) {
64
- console.warn(`[approval] Received response for unknown or already handled widget: ${widgetId}`);
65
- return;
66
- }
67
-
68
- const timestamp = pendingApprovals.get(widgetId)!;
69
- if (Date.now() - timestamp > TTL_MS) {
70
- pendingApprovals.delete(widgetId);
71
- console.warn(`[approval] Received response for expired widget: ${widgetId}`);
72
- return;
73
- }
74
-
75
- // Mark as handled
76
- pendingApprovals.delete(widgetId);
77
-
78
- const originalEvent = metadata.originalEvent as OpenBotEvent;
79
- const approved = actionId === 'approve';
80
-
81
- const displayData = JSON.stringify((event as any)?.data) || '';
82
-
83
- // Yield a "responded" widget update to the UI
84
- yield {
85
- type: 'client:ui:widget',
86
- data: {
87
- widgetId,
88
- kind: 'message',
89
- title: `Action ${approved ? 'Approved' : 'Denied'}`,
90
- body: displayData,
91
- state: approved ? 'submitted' : 'cancelled',
92
- display: 'collapsed',
93
- disabled: true,
94
- actions: [], // Clear actions to disable buttons in UI
95
- },
96
- meta: { agentId: context.state.agentId, threadId: context.state.threadId },
97
- } as OpenBotEvent;
98
-
99
- if (approved) {
100
- // Re-emit the original event with approved status so the actual handler can run
101
- yield {
102
- ...originalEvent,
103
- meta: {
104
- ...(originalEvent.meta || {}),
105
- approvalStatus: 'approved',
106
- },
107
- };
108
- } else {
109
- // Manually store the original event with denied status so it's recorded in history
110
- // but NOT re-emitted to the pipeline (to avoid actual execution).
111
- if (storage) {
112
- await storage.storeEvent({
113
- channelId: context.state.channelId,
114
- threadId: context.state.threadId,
115
- event: {
116
- ...originalEvent,
117
- meta: {
118
- ...(originalEvent.meta || {}),
119
- approvalStatus: 'denied',
120
- },
121
- },
122
- });
123
- }
124
-
125
- // Emit a failure result event for the denied action to clear the pending tool batch
126
- yield {
127
- type: `${originalEvent.type}:result` as OpenBotEvent['type'],
128
- data: {
129
- success: false,
130
- error: 'Action denied by user.',
131
- stderr: 'Action denied by user.',
132
- output: 'Action denied by user.',
133
- },
134
- meta: originalEvent.meta,
135
- } as OpenBotEvent;
136
-
137
- yield {
138
- type: 'agent:output',
139
- data: { content: `Action \`${originalEvent.type}\` was denied.` },
140
- meta: { agentId: context.state.agentId },
141
- } as OpenBotEvent;
142
- }
143
- });
144
- },
145
- };
146
-
147
- export default approvalPlugin;