openbot 0.4.0 → 0.4.3

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/dist/app/cli.js +1 -1
  2. package/dist/app/config.js +10 -0
  3. package/dist/app/server.js +200 -3
  4. package/dist/harness/index.js +18 -0
  5. package/dist/plugins/approval/index.js +35 -20
  6. package/dist/plugins/bash/index.js +195 -0
  7. package/dist/plugins/delegation/index.js +6 -2
  8. package/dist/plugins/openbot/context.js +54 -9
  9. package/dist/plugins/openbot/history.js +47 -1
  10. package/dist/plugins/openbot/index.js +43 -3
  11. package/dist/plugins/openbot/runtime.js +91 -27
  12. package/dist/plugins/openbot/system-prompt.js +21 -1
  13. package/dist/plugins/plugin-manager/index.js +87 -3
  14. package/dist/plugins/shell/index.js +2 -1
  15. package/dist/plugins/storage/files.js +67 -0
  16. package/dist/plugins/storage/index.js +184 -7
  17. package/dist/plugins/storage/service.js +215 -59
  18. package/dist/plugins/ui/index.js +109 -150
  19. package/dist/services/abort.js +43 -0
  20. package/dist/services/plugins/registry.js +5 -3
  21. package/dist/services/plugins/service.js +66 -11
  22. package/docs/agents.md +5 -8
  23. package/docs/architecture.md +1 -1
  24. package/docs/plugins.md +28 -7
  25. package/docs/templates/AGENT.example.md +4 -4
  26. package/package.json +7 -7
  27. package/src/app/cli.ts +1 -1
  28. package/src/app/config.ts +13 -0
  29. package/src/app/server.ts +235 -3
  30. package/src/app/types.ts +284 -14
  31. package/src/harness/index.ts +21 -0
  32. package/src/plugins/approval/index.ts +37 -20
  33. package/src/plugins/bash/index.ts +232 -0
  34. package/src/plugins/delegation/index.ts +7 -2
  35. package/src/plugins/openbot/context.ts +58 -9
  36. package/src/plugins/openbot/history.ts +52 -1
  37. package/src/plugins/openbot/index.ts +45 -3
  38. package/src/plugins/openbot/runtime.ts +121 -27
  39. package/src/plugins/openbot/system-prompt.ts +21 -1
  40. package/src/plugins/plugin-manager/index.ts +105 -3
  41. package/src/plugins/storage/files.ts +81 -0
  42. package/src/plugins/storage/index.ts +198 -8
  43. package/src/plugins/storage/service.ts +282 -59
  44. package/src/plugins/ui/index.ts +123 -0
  45. package/src/services/abort.ts +46 -0
  46. package/src/services/plugins/domain.ts +34 -1
  47. package/src/services/plugins/registry.ts +5 -3
  48. package/src/services/plugins/service.ts +136 -45
  49. package/src/services/plugins/types.ts +5 -1
  50. package/src/plugins/shell/index.ts +0 -123
@@ -1,154 +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.literal('approval').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 createWidgetId = (data, toolCallId) => {
76
- if ('widgetId' in data && data.widgetId)
77
- return data.widgetId;
78
- if (toolCallId)
79
- return `widget_${toolCallId}`;
80
- return `widget_${Date.now()}`;
81
- };
82
- const normalizeWidget = (data, state, toolCallId) => {
83
- const widgetId = createWidgetId(data, toolCallId);
84
- if (data.kind === 'approval') {
85
- const props = data.props || {};
86
- return {
87
- widgetId,
88
- kind: 'choice',
89
- title: data.title || 'Approval Required',
90
- body: readString(props.message) ||
91
- readString(props.summary) ||
92
- 'Please approve or deny this action.',
93
- metadata: {
94
- ...(data.metadata || {}),
95
- legacyKind: 'approval',
96
- actionId: props.actionId,
97
- },
98
- actions: [
99
- { id: 'approve', label: 'Approve', value: props.actionId || 'approve', variant: 'primary' },
100
- { id: 'deny', label: 'Deny', value: props.actionId || 'deny', variant: 'danger' },
101
- ],
102
- };
103
- }
104
- if (data.kind === 'form') {
105
- const propsSource = data.props;
106
- const props = isRecord(propsSource) ? propsSource : {};
107
- return {
108
- widgetId,
109
- kind: 'form',
110
- title: data.title || 'Details Required',
111
- description: data.description,
112
- body: data.body,
113
- state: data.state,
114
- metadata: data.metadata,
115
- fields: data.fields || asFields(props.schema) || [],
116
- submitLabel: data.submitLabel || readString(props.submitLabel),
117
- actions: data.actions,
118
- };
119
- }
120
- if (data.kind === 'list') {
121
- return { ...data, widgetId, title: data.title || 'Task List', items: data.items || [] };
122
- }
123
- if (data.kind === 'choice') {
124
- return { ...data, widgetId, title: data.title || 'Choose an Option' };
125
- }
126
- if (data.kind === 'message') {
127
- return { ...data, widgetId, title: data.title || 'Message' };
128
- }
129
- throw new Error(`Unsupported UI widget kind: ${data.kind || 'unknown'}`);
130
- };
131
- const uiToolDefinitions = {
132
- render_ui_widget: {
133
- description: 'Render a small server-driven UI widget in the conversation. Prefer primitive kinds: message, choice, form, or list. Legacy preset approval is accepted.',
134
- inputSchema: renderWidgetSchema,
135
- },
136
- };
137
- const uiPluginRuntime = () => (builder) => {
138
- builder.on('action:render_ui_widget', async function* (event, context) {
139
- const widget = normalizeWidget(event.data, context.state, event.meta?.toolCallId);
140
- yield {
141
- type: 'client:ui:widget',
142
- data: widget,
143
- meta: event.meta,
144
- };
145
- });
146
- };
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
+ */
147
9
  export const uiPlugin = {
148
10
  id: 'ui',
149
- name: 'UI Widgets',
150
- description: 'Render server-driven UI widgets (messages, choices, forms, lists) in the conversation.',
151
- toolDefinitions: uiToolDefinitions,
152
- 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
+ },
153
112
  };
154
113
  export default uiPlugin;
@@ -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();
@@ -2,22 +2,24 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { openbotPlugin } from '../../plugins/openbot/index.js';
5
- import { shellPlugin } from '../../plugins/shell/index.js';
5
+ import { bashPlugin } from '../../plugins/bash/index.js';
6
6
  import { storagePlugin } from '../../plugins/storage/index.js';
7
7
  import { approvalPlugin } from '../../plugins/approval/index.js';
8
8
  import { memoryPlugin } from '../../plugins/memory/index.js';
9
9
  import { delegationPlugin } from '../../plugins/delegation/index.js';
10
+ import { uiPlugin } from '../../plugins/ui/index.js';
10
11
  import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
11
12
  import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../../app/config.js';
12
13
  import { invalidatePlugin as clearResolvedPluginEntry, loadedCommunityPlugins, resolvedPluginCache, } from './plugin-cache.js';
13
14
  let pluginsDir = null;
14
15
  const BUILT_IN = {
15
16
  [openbotPlugin.id]: openbotPlugin,
16
- [shellPlugin.id]: shellPlugin,
17
+ [bashPlugin.id]: bashPlugin,
17
18
  [storagePlugin.id]: storagePlugin,
18
19
  [approvalPlugin.id]: approvalPlugin,
19
20
  [memoryPlugin.id]: memoryPlugin,
20
21
  [delegationPlugin.id]: delegationPlugin,
22
+ [uiPlugin.id]: uiPlugin,
21
23
  [pluginManagerPlugin.id]: pluginManagerPlugin,
22
24
  };
23
25
  /** Normalize a dynamically imported plugin module. Supports `plugin`, `default`. */
@@ -56,7 +58,7 @@ export function initPlugins(dir) {
56
58
  }
57
59
  /**
58
60
  * Resolve a Plugin by id. The id is either:
59
- * - a built-in id (e.g. "openbot", "shell"), or
61
+ * - a built-in id (e.g. "openbot", "bash"), or
60
62
  * - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
61
63
  * in which case the folder layout is `plugins/<id>/dist/index.js`.
62
64
  */
@@ -7,19 +7,27 @@ import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, DEFAULT_MARKETPLACE_REGISTRY_URL
7
7
  import { invalidatePlugin } from './plugin-cache.js';
8
8
  const execAsync = promisify(exec);
9
9
  const DEFAULT_MARKETPLACE_AGENTS = [];
10
+ const DEFAULT_MARKETPLACE_CHANNELS = [];
10
11
  function isRecord(value) {
11
12
  return typeof value === 'object' && value !== null && !Array.isArray(value);
12
13
  }
13
14
  /**
14
15
  * Parses JSON from a remote registry file. Supports either
15
- * `{ "agents": [ ... ] }` or a top-level array.
16
+ * `{ "agents": [ ... ], "channels": [ ... ] }` or a top-level array (legacy agents-only).
16
17
  */
17
18
  export function parseMarketplaceRegistryJson(data) {
18
- const rawAgents = Array.isArray(data) ? data : isRecord(data) && Array.isArray(data.agents) ? data.agents : null;
19
- if (!Array.isArray(rawAgents)) {
20
- throw new Error('Registry JSON must be an array or an object with an "agents" array');
21
- }
22
- return rawAgents.map((item, i) => {
19
+ const isLegacyArray = Array.isArray(data);
20
+ const rawAgents = isLegacyArray
21
+ ? data
22
+ : isRecord(data) && Array.isArray(data.agents)
23
+ ? data.agents
24
+ : [];
25
+ const rawChannels = !isLegacyArray && isRecord(data) && Array.isArray(data.channels)
26
+ ? data.channels
27
+ : isRecord(data) && Array.isArray(data.templates)
28
+ ? data.templates
29
+ : [];
30
+ const agents = (Array.isArray(rawAgents) ? rawAgents : []).map((item, i) => {
23
31
  if (!isRecord(item)) {
24
32
  throw new Error(`agents[${i}]: expected object`);
25
33
  }
@@ -59,8 +67,47 @@ export function parseMarketplaceRegistryJson(data) {
59
67
  }
60
68
  return listing;
61
69
  });
70
+ const channels = (Array.isArray(rawChannels) ? rawChannels : []).map((item, i) => {
71
+ if (!isRecord(item)) {
72
+ throw new Error(`channels[${i}]: expected object`);
73
+ }
74
+ const id = item.id;
75
+ const name = item.name;
76
+ const description = item.description;
77
+ const participants = item.participants;
78
+ if (typeof id !== 'string' || !id)
79
+ throw new Error(`channels[${i}].id must be a non-empty string`);
80
+ if (typeof name !== 'string')
81
+ throw new Error(`channels[${i}].name must be a string`);
82
+ if (typeof description !== 'string')
83
+ throw new Error(`channels[${i}].description must be a string`);
84
+ if (!Array.isArray(participants))
85
+ throw new Error(`channels[${i}].participants must be an array`);
86
+ const listing = {
87
+ id,
88
+ name,
89
+ description,
90
+ participants: participants.filter((p) => typeof p === 'string'),
91
+ };
92
+ if (typeof item.image === 'string')
93
+ listing.image = item.image;
94
+ if (typeof item.spec === 'string')
95
+ listing.spec = item.spec;
96
+ if (isRecord(item.initialState))
97
+ listing.initialState = item.initialState;
98
+ if (Array.isArray(item.starterPrompts)) {
99
+ listing.starterPrompts = item.starterPrompts.map((p, j) => {
100
+ if (!isRecord(p) || typeof p.label !== 'string' || typeof p.prompt !== 'string') {
101
+ throw new Error(`channels[${i}].starterPrompts[${j}] must have label and prompt`);
102
+ }
103
+ return { label: p.label, prompt: p.prompt };
104
+ });
105
+ }
106
+ return listing;
107
+ });
108
+ return { agents, channels };
62
109
  }
63
- async function fetchMarketplaceAgentsFromUrl(url) {
110
+ async function fetchMarketplaceRegistryFromUrl(url) {
64
111
  const res = await fetch(url, {
65
112
  headers: { Accept: 'application/json' },
66
113
  signal: AbortSignal.timeout(15000),
@@ -72,19 +119,27 @@ async function fetchMarketplaceAgentsFromUrl(url) {
72
119
  return parseMarketplaceRegistryJson(json);
73
120
  }
74
121
  /**
75
- * Resolves marketplace agent listings from configured registry URL, or falls back to an empty list.
122
+ * Resolves marketplace registry (agents and channels) from configured registry URL.
76
123
  */
77
- export async function resolveMarketplaceAgentList() {
124
+ export async function resolveMarketplaceRegistry() {
78
125
  const { marketplaceRegistryUrl } = loadConfig();
79
126
  const registryUrl = marketplaceRegistryUrl?.trim() || DEFAULT_MARKETPLACE_REGISTRY_URL;
80
127
  try {
81
- return await fetchMarketplaceAgentsFromUrl(registryUrl);
128
+ return await fetchMarketplaceRegistryFromUrl(registryUrl);
82
129
  }
83
130
  catch (err) {
84
131
  console.warn(`[plugins] marketplace registry fetch failed (${registryUrl}), using built-in list:`, err instanceof Error ? err.message : err);
85
- return DEFAULT_MARKETPLACE_AGENTS;
132
+ return { agents: DEFAULT_MARKETPLACE_AGENTS, channels: DEFAULT_MARKETPLACE_CHANNELS };
86
133
  }
87
134
  }
135
+ /**
136
+ * Resolves marketplace agent listings from configured registry URL.
137
+ * @deprecated Use resolveMarketplaceRegistry instead.
138
+ */
139
+ export async function resolveMarketplaceAgentList() {
140
+ const registry = await resolveMarketplaceRegistry();
141
+ return registry.agents;
142
+ }
88
143
  const getPluginsDir = () => {
89
144
  const config = loadConfig();
90
145
  const baseDir = resolvePath(config.baseDir || DEFAULT_BASE_DIR);
package/docs/agents.md CHANGED
@@ -17,8 +17,6 @@ plugins:
17
17
  - id: openbot
18
18
  config:
19
19
  model: anthropic/claude-3-5-sonnet-20240620
20
- - id: shell
21
- - id: delegation
22
20
  ---
23
21
 
24
22
  You are a web research specialist. Use the available tools to gather and
@@ -37,20 +35,19 @@ the bus). Built-in **`state`** is hidden by default.
37
35
  A runtime plugin is one that handles `agent:invoke` (the LLM loop). Without
38
36
  one, the agent will not respond to user input. Built-in runtime plugins:
39
37
 
40
- - `openbot` — the standard, opinionated OpenBot agent runtime. Consumes tools
41
- from other plugins listed alongside it.
38
+ - `openbot` — the standard, opinionated OpenBot agent runtime. It is
39
+ **batteries-included** and provides inbuilt tools (bash, memory, storage,
40
+ delegation, and approval).
42
41
  - `claude-code` — runs Claude inside the Claude Agent SDK with its own tools.
43
42
  - `gemini-cli` — spawns Google's `gemini` CLI in headless mode.
44
43
 
45
44
  `claude-code` and `gemini-cli` own their own tool loops, so attaching tool
46
- plugins like `shell` to them has no effect. Pair tool plugins with
47
- `openbot`.
45
+ plugins like `bash` to them has no effect.
48
46
 
49
47
  ## Built-in agents
50
48
 
51
49
  OpenBot ships a built-in **`system`** agent (the orchestrator) with the `openbot`
52
- runtime plus the standard tool plugins (storage, shell, delegation,
53
- approval, memory, etc.). A built-in **`state`** agent backs deterministic
50
+ runtime. A built-in **`state`** agent backs deterministic
54
51
  `/api/state` handling and infra events.
55
52
 
56
53
  You can optionally persist overrides for either id at `~/.openbot/agents/system/AGENT.md` or `~/.openbot/agents/state/AGENT.md`. When present, settings are merged on top of the code defaults (`getAgentDetails`). The **`state`** agent is not listed by **`action:storage:get-agents`** (`hidden: true`); **`system`** is listed. Use **`action:storage:create-agent`** to create an overlay once, **`action:storage:update-agent`** for partial updates (creating the file if missing for `system` / `state`), and **`action:storage:delete-agent`** to remove only that `AGENT.md` and revert to defaults (other files under the folder are left untouched).
@@ -18,7 +18,7 @@ A dynamic registry that manages all available agents. Agents can be:
18
18
  - **TS Packages**: Advanced agents with custom logic in `~/.openbot/agents/*/index.ts`.
19
19
 
20
20
  ### 3. Plugin registry
21
- The "capability layer" that provides tools and logic shared across the platform. Plugins (like `shell` or `file-system`) define the actions agents can perform.
21
+ The "capability layer" that provides tools and logic shared across the platform. Plugins (like `bash` or `file-system`) define the actions agents can perform.
22
22
 
23
23
  ### 4. Orchestration layer (Melony)
24
24
  The underlying event bus that handles all communication. It ensures that agents can collaborate asynchronously, share context, and emit real-time updates to the UI.
package/docs/plugins.md CHANGED
@@ -43,15 +43,36 @@ name collisions.
43
43
 
44
44
  | Id | Role | Notes |
45
45
  | --------------- | ---------- | --------------------------------------------------------- |
46
- | `openbot` | Runtime | The standard, opinionated OpenBot agent runtime. |
46
+ | `openbot` | Runtime | Standard batteries-included OpenBot agent runtime. |
47
47
  | `claude-code` | Runtime | Claude Agent SDK; owns its own tool loop |
48
48
  | `gemini-cli` | Runtime | Google `gemini` CLI in headless mode |
49
- | `shell` | Tool | `shell_exec` |
50
- | `storage` | Tool | `create_channel`, `patch_*`, `create_variable`, ... |
51
- | `memory` | Tool | `remember`, `recall`, `forget` |
49
+ | `bash` | Tool | `bash` (inbuilt in `openbot`) |
50
+ | `storage` | Tool | `create_channel`, `patch_*`, ... (inbuilt in `openbot`) |
51
+ | `memory` | Tool | `remember`, `recall`, `forget` (inbuilt in `openbot`) |
52
52
  | `plugin-manager`| Infra | Marketplace list, npm plugin install/uninstall, agent install |
53
53
 
54
- ## Community plugins
54
+ ## Batteries-included: `openbot` runtime
55
+
56
+ The `openbot` plugin is the standard runtime for OpenBot agents. It is designed
57
+ to be isolated and self-contained, providing a core ecosystem of inbuilt tools:
58
+
59
+ - **Bash**: Stateful system tasks and file operations.
60
+ - **Memory**: Long-term durable fact storage.
61
+ - **Storage**: Channel and thread management.
62
+ - **Delegation**: Calling upon other specialized agents.
63
+ - **Approval**: Gating protected actions behind UI confirmation.
64
+
65
+ When you use the `openbot` runtime, these tools are automatically available.
66
+ You can configure the inbuilt `approval` plugin via the `openbot` plugin config:
67
+
68
+ ```yaml
69
+ plugins:
70
+ - id: openbot
71
+ config:
72
+ model: openai/gpt-4o-mini
73
+ approval:
74
+ actions: [action:bash, action:create_channel]
75
+ ```
55
76
 
56
77
  A community plugin is just an npm package whose default export matches the
57
78
  `Plugin` interface. Reference it by its npm package name in AGENT.md:
@@ -69,11 +90,11 @@ On first use OpenBot installs the package into
69
90
 
70
91
  ## Approval plugin
71
92
 
72
- The `approval` plugin gates protected tool calls behind a UI confirmation widget. By default, it gates `action:shell_exec`.
93
+ The `approval` plugin gates protected tool calls behind a UI confirmation widget. By default, it gates `action:bash`.
73
94
 
74
95
  ```yaml
75
96
  plugins:
76
97
  - id: approval
77
98
  config:
78
- actions: [action:shell_exec]
99
+ actions: [action:bash]
79
100
  ```
@@ -9,11 +9,11 @@ description: One-line description shown in agent pickers and lists.
9
9
 
10
10
  # Plugins compose the agent. Order matters for tool collisions (first wins).
11
11
  # At least one plugin must handle `agent:invoke` (a "runtime" plugin like
12
- # `openbot`, `claude-code`, or `gemini-cli`). Tool plugins like `shell`,
12
+ # `openbot`, `claude-code`, or `gemini-cli`). Tool plugins like `bash`,
13
13
  # `delegation`, and `storage-tools` contribute tools to whichever runtime
14
14
  # plugin can consume them.
15
15
  #
16
- # Built-in plugin ids: openbot, claude-code, gemini-cli, shell, delegation,
16
+ # Built-in plugin ids: openbot, claude-code, gemini-cli, bash, delegation,
17
17
  # storage-tools, approval.
18
18
  #
19
19
  # Community plugins are referenced by their npm package name (e.g.
@@ -23,12 +23,12 @@ plugins:
23
23
  - id: openbot
24
24
  config:
25
25
  model: openai/gpt-4o-mini
26
- - id: shell
26
+ - id: bash
27
27
  - id: delegation
28
28
  - id: storage
29
29
  - id: approval
30
30
  config:
31
- actions: [action:shell_exec]
31
+ actions: [action:bash]
32
32
  ---
33
33
 
34
34
  <!--
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "openbot",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=20.12.0"
8
8
  },
9
+ "scripts": {
10
+ "dev": "tsx watch src/app/cli.ts start",
11
+ "build": "tsc && mkdir -p dist/assets && cp src/assets/icon.svg dist/assets/icon.svg",
12
+ "start": "node dist/app/cli.js start"
13
+ },
9
14
  "bin": {
10
15
  "openbot": "./dist/app/cli.js"
11
16
  },
@@ -30,10 +35,5 @@
30
35
  "@types/node": "^20.10.1",
31
36
  "tsx": "^4.21.0",
32
37
  "typescript": "^5.9.3"
33
- },
34
- "scripts": {
35
- "dev": "tsx watch src/app/cli.ts start",
36
- "build": "tsc && mkdir -p dist/assets && cp src/assets/icon.svg dist/assets/icon.svg",
37
- "start": "node dist/app/cli.js start"
38
38
  }
39
- }
39
+ }
package/src/app/cli.ts CHANGED
@@ -25,7 +25,7 @@ function checkNodeVersion() {
25
25
 
26
26
  checkNodeVersion();
27
27
 
28
- program.name('openbot').description('OpenBot CLI').version('0.4.0');
28
+ program.name('openbot').description('OpenBot CLI').version('0.4.3');
29
29
 
30
30
  program
31
31
  .command('start')
package/src/app/config.ts CHANGED
@@ -14,6 +14,8 @@ export interface OpenBotconfig {
14
14
  * {@link DEFAULT_MARKETPLACE_REGISTRY_URL} is used.
15
15
  */
16
16
  marketplaceRegistryUrl?: string;
17
+ /** Public base URL for workspace file links (e.g. https://my-host.example). Falls back to OPENBOT_PUBLIC_URL env or http://localhost:{port}. */
18
+ publicUrl?: string;
17
19
  }
18
20
 
19
21
  export interface StoredVariable {
@@ -23,6 +25,8 @@ export interface StoredVariable {
23
25
  }
24
26
 
25
27
  export const DEFAULT_BASE_DIR = '~/.openbot';
28
+ /** Default parent directory for per-channel working directories (user-facing workspace). */
29
+ export const DEFAULT_CHANNELS_WORKSPACE_DIR = '~/openbot';
26
30
  export const DEFAULT_PLUGINS_DIR = 'plugins';
27
31
  export const DEFAULT_AGENTS_DIR = 'agents';
28
32
  export const DEFAULT_CHANNELS_DIR = 'channels';
@@ -37,6 +41,15 @@ export function resolvePath(p: string) {
37
41
  return p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : path.resolve(p);
38
42
  }
39
43
 
44
+ /** Default absolute cwd for a channel when none is provided at creation time. */
45
+ export function getDefaultChannelCwd(channelId: string): string {
46
+ const id = channelId.trim();
47
+ if (!id) {
48
+ throw new Error('channelId is required');
49
+ }
50
+ return resolvePath(`${DEFAULT_CHANNELS_WORKSPACE_DIR}/${id}`);
51
+ }
52
+
40
53
  export function loadConfig(): OpenBotconfig {
41
54
  const configPath = path.join(os.homedir(), '.openbot', CONFIG_FILE);
42
55
  if (fs.existsSync(configPath)) {