openbot 0.2.14 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/agents/openbot/index.js +76 -0
  2. package/dist/agents/openbot/middleware/approval.js +132 -0
  3. package/dist/agents/openbot/runtime.js +289 -0
  4. package/dist/agents/openbot/system-prompt.js +32 -0
  5. package/dist/agents/openbot/tools/delegation.js +78 -0
  6. package/dist/agents/openbot/tools/mcp.js +99 -0
  7. package/dist/agents/openbot/tools/shell.js +91 -0
  8. package/dist/agents/openbot/tools/storage.js +75 -0
  9. package/dist/agents/openbot/tools/ui.js +176 -0
  10. package/dist/agents/system.js +20 -93
  11. package/dist/app/cli.js +0 -0
  12. package/dist/app/config.js +4 -1
  13. package/dist/app/server.js +15 -8
  14. package/dist/bus/agent-package.js +1 -0
  15. package/dist/bus/plugin.js +1 -0
  16. package/dist/bus/services.js +600 -0
  17. package/dist/bus/types.js +1 -0
  18. package/dist/harness/context.js +131 -0
  19. package/dist/harness/event-normalizer.js +59 -0
  20. package/dist/harness/orchestrator.js +27 -227
  21. package/dist/harness/process.js +25 -3
  22. package/dist/harness/queue-processor.js +227 -0
  23. package/dist/harness/runtime-factory.js +103 -0
  24. package/dist/plugins/ai-sdk/index.js +37 -0
  25. package/dist/plugins/ai-sdk/runtime.js +330 -0
  26. package/dist/plugins/ai-sdk/system-prompt.js +3 -0
  27. package/dist/plugins/ai-sdk.js +277 -87
  28. package/dist/plugins/approval/index.js +159 -0
  29. package/dist/plugins/approval.js +163 -0
  30. package/dist/plugins/delegation/index.js +79 -0
  31. package/dist/plugins/delegation.js +67 -11
  32. package/dist/plugins/mcp/index.js +108 -0
  33. package/dist/plugins/shell/index.js +99 -0
  34. package/dist/plugins/shell.js +123 -0
  35. package/dist/plugins/storage-tools/index.js +85 -0
  36. package/dist/plugins/storage.js +240 -5
  37. package/dist/plugins/ui/index.js +184 -0
  38. package/dist/plugins/ui.js +185 -21
  39. package/dist/registry/agents.js +138 -0
  40. package/dist/registry/plugins.js +91 -50
  41. package/dist/services/agent-packages.js +103 -0
  42. package/dist/services/plugins.js +98 -0
  43. package/dist/services/storage.js +360 -94
  44. package/docs/agents.md +39 -66
  45. package/docs/architecture.md +1 -1
  46. package/docs/plugins.md +70 -58
  47. package/docs/templates/AGENT.example.md +57 -0
  48. package/package.json +8 -7
  49. package/src/app/cli.ts +1 -1
  50. package/src/app/config.ts +14 -4
  51. package/src/app/server.ts +23 -10
  52. package/src/app/types.ts +385 -16
  53. package/src/assets/icon.svg +4 -1
  54. package/src/bus/plugin.ts +67 -0
  55. package/src/bus/services.ts +666 -0
  56. package/src/bus/types.ts +147 -0
  57. package/src/harness/context.ts +160 -0
  58. package/src/harness/event-normalizer.ts +82 -0
  59. package/src/harness/orchestrator.ts +35 -273
  60. package/src/harness/process.ts +28 -4
  61. package/src/harness/queue-processor.ts +309 -0
  62. package/src/harness/runtime-factory.ts +125 -0
  63. package/src/plugins/ai-sdk/index.ts +44 -0
  64. package/src/plugins/ai-sdk/runtime.ts +410 -0
  65. package/src/plugins/ai-sdk/system-prompt.ts +4 -0
  66. package/src/plugins/approval/index.ts +228 -0
  67. package/src/plugins/delegation/index.ts +94 -0
  68. package/src/plugins/mcp/index.ts +128 -0
  69. package/src/plugins/shell/index.ts +123 -0
  70. package/src/plugins/storage-tools/index.ts +101 -0
  71. package/src/plugins/ui/index.ts +227 -0
  72. package/src/registry/plugins.ts +106 -55
  73. package/src/services/plugins.ts +133 -0
  74. package/src/services/storage.ts +465 -137
  75. package/src/agents/system.ts +0 -112
  76. package/src/plugins/ai-sdk.ts +0 -197
  77. package/src/plugins/delegation.ts +0 -60
  78. package/src/plugins/mcp.ts +0 -154
  79. package/src/plugins/storage.ts +0 -725
  80. package/src/plugins/ui.ts +0 -57
@@ -0,0 +1,228 @@
1
+ import { MelonyPlugin } from 'melony';
2
+ import type { Plugin } from '../../bus/plugin.js';
3
+ import { OpenBotEvent, OpenBotState } from '../../app/types.js';
4
+ import { storageService } from '../../services/storage.js';
5
+
6
+ /**
7
+ * `approval` — gates protected tool calls behind a UI confirmation widget.
8
+ *
9
+ * Configuration is read from the per-agent plugin config in AGENT.md:
10
+ * ```yaml
11
+ * plugins:
12
+ * - id: approval
13
+ * config:
14
+ * rules:
15
+ * - action: action:shell_exec
16
+ * message: The agent wants to run a terminal command.
17
+ * detailKeys: [command, cwd, shell, timeoutMs]
18
+ * ```
19
+ */
20
+
21
+ export type ApprovalRule = {
22
+ action: string;
23
+ message?: string;
24
+ detailKeys?: string[];
25
+ hiddenKeys?: string[];
26
+ executeEvent?: string;
27
+ denyEvent?: string;
28
+ denyData?: Record<string, unknown>;
29
+ };
30
+
31
+ export const DEFAULT_APPROVAL_RULES: ApprovalRule[] = [
32
+ {
33
+ action: 'action:shell_exec',
34
+ denyEvent: 'action:shell_exec:result',
35
+ message: 'The agent wants to run a terminal command.',
36
+ detailKeys: ['command', 'cwd', 'shell', 'timeoutMs'],
37
+ hiddenKeys: ['env'],
38
+ denyData: {
39
+ exitCode: null,
40
+ stdout: '',
41
+ stderr: 'Command execution was denied by the user.',
42
+ timedOut: false,
43
+ },
44
+ },
45
+ ];
46
+
47
+ type PendingApproval = {
48
+ id: string;
49
+ action: string;
50
+ executeEvent: string;
51
+ denyEvent: string;
52
+ denyData: Record<string, unknown>;
53
+ payload: Record<string, unknown>;
54
+ meta?: Record<string, unknown>;
55
+ message: string;
56
+ createdAt: string;
57
+ status: 'pending' | 'approved' | 'denied';
58
+ };
59
+
60
+ const asRecord = (value: unknown): Record<string, unknown> =>
61
+ value && typeof value === 'object' && !Array.isArray(value)
62
+ ? (value as Record<string, unknown>)
63
+ : {};
64
+
65
+ const getApprovalsFromState = (state: OpenBotState): Record<string, PendingApproval> => {
66
+ const source = state.threadDetails?.state ?? state.channelDetails?.state;
67
+ const stateRecord = asRecord(source);
68
+ return asRecord(stateRecord.approvals) as Record<string, PendingApproval>;
69
+ };
70
+
71
+ const persistApprovals = async (
72
+ state: OpenBotState,
73
+ approvals: Record<string, PendingApproval>,
74
+ ): Promise<void> => {
75
+ if (state.threadId) {
76
+ await storageService.patchThreadState({
77
+ channelId: state.channelId,
78
+ threadId: state.threadId,
79
+ state: { approvals },
80
+ });
81
+ return;
82
+ }
83
+ await storageService.patchChannelState({
84
+ channelId: state.channelId,
85
+ state: { approvals },
86
+ });
87
+ };
88
+
89
+ const buildApprovalPlugin =
90
+ (rules: ApprovalRule[]): MelonyPlugin<OpenBotState, OpenBotEvent> =>
91
+ (builder) => {
92
+ for (const rule of rules) {
93
+ builder.on(rule.action as OpenBotEvent['type'], async function* (event, context) {
94
+ const meta = asRecord(event.meta);
95
+ if (meta.approvalStatus === 'approved') return;
96
+
97
+ const eventData = asRecord((event as { data?: unknown }).data);
98
+ const eventMeta = meta;
99
+
100
+ const approvalId = `approval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
101
+ const widgetId = `widget_${approvalId}`;
102
+ const executeEvent = rule.executeEvent || rule.action;
103
+ const denyEvent = rule.denyEvent || `${rule.action}:result`;
104
+ const denyData = rule.denyData || {};
105
+ const hiddenKeys = new Set(rule.hiddenKeys || []);
106
+ const detailKeys = rule.detailKeys || Object.keys(eventData);
107
+ const details = detailKeys
108
+ .filter((key) => !hiddenKeys.has(key))
109
+ .map((key) => `- ${key}: ${String(eventData[key] ?? '')}`)
110
+ .join('\n');
111
+
112
+ const pendingApprovals = getApprovalsFromState(context.state);
113
+ pendingApprovals[approvalId] = {
114
+ id: approvalId,
115
+ action: rule.action,
116
+ executeEvent,
117
+ denyEvent,
118
+ denyData,
119
+ payload: eventData,
120
+ meta: eventMeta,
121
+ message: rule.message || `Approval required for ${rule.action}.`,
122
+ createdAt: new Date().toISOString(),
123
+ status: 'pending',
124
+ };
125
+ await persistApprovals(context.state, pendingApprovals);
126
+
127
+ yield {
128
+ type: 'client:ui:widget',
129
+ data: {
130
+ kind: 'choice',
131
+ widgetId,
132
+ title: 'Approval Required',
133
+ body: `${rule.message || 'A protected action requires approval.'}${
134
+ details ? `\n\n${details}` : ''
135
+ }`,
136
+ metadata: { type: 'approval:request', approvalId, action: rule.action },
137
+ actions: [
138
+ { id: 'approve', label: 'Approve', variant: 'primary' },
139
+ { id: 'deny', label: 'Deny', variant: 'danger' },
140
+ ],
141
+ },
142
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
143
+ } as OpenBotEvent;
144
+
145
+ yield {
146
+ type: 'agent:output',
147
+ data: { content: `Waiting for approval before running \`${rule.action}\`.` },
148
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
149
+ } as OpenBotEvent;
150
+
151
+ context.suspend();
152
+ });
153
+ }
154
+
155
+ builder.on('client:ui:widget:response', async function* (event, context) {
156
+ const metadata = asRecord(event.data?.metadata);
157
+ if (metadata.type !== 'approval:request') return;
158
+
159
+ const approvalId = String(metadata.approvalId || '');
160
+ if (!approvalId) return;
161
+
162
+ const approvals = getApprovalsFromState(context.state);
163
+ const approval = approvals[approvalId];
164
+ if (!approval || approval.status !== 'pending') {
165
+ yield {
166
+ type: 'agent:output',
167
+ data: { content: 'Approval request not found or already resolved.' },
168
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
169
+ } as OpenBotEvent;
170
+ return;
171
+ }
172
+
173
+ const approved = event.data.actionId === 'approve';
174
+ approvals[approvalId] = {
175
+ ...approval,
176
+ status: approved ? 'approved' : 'denied',
177
+ };
178
+ await persistApprovals(context.state, approvals);
179
+
180
+ if (approved) {
181
+ yield {
182
+ type: approval.executeEvent as OpenBotEvent['type'],
183
+ data: approval.payload,
184
+ meta: {
185
+ ...(approval.meta || {}),
186
+ approvalId,
187
+ approvalStatus: 'approved',
188
+ },
189
+ } as OpenBotEvent;
190
+ return;
191
+ }
192
+
193
+ yield {
194
+ type: approval.denyEvent as OpenBotEvent['type'],
195
+ data: {
196
+ success: false,
197
+ approved: false,
198
+ error: 'Action denied by user approval.',
199
+ ...approval.denyData,
200
+ },
201
+ meta: { ...(approval.meta || {}), approvalId },
202
+ } as OpenBotEvent;
203
+
204
+ yield {
205
+ type: 'agent:output',
206
+ data: { content: 'Action denied by user approval.' },
207
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
208
+ } as OpenBotEvent;
209
+ });
210
+ };
211
+
212
+ const readRules = (config: Record<string, unknown>): ApprovalRule[] => {
213
+ const raw = config.rules;
214
+ if (!Array.isArray(raw)) return DEFAULT_APPROVAL_RULES;
215
+ return raw.filter(
216
+ (entry): entry is ApprovalRule =>
217
+ !!entry && typeof entry === 'object' && typeof (entry as { action?: unknown }).action === 'string',
218
+ );
219
+ };
220
+
221
+ export const approvalPlugin: Plugin = {
222
+ id: 'approval',
223
+ name: 'Approval',
224
+ description: 'Gate protected tool calls (e.g. shell_exec) behind a UI confirmation prompt.',
225
+ factory: ({ config }) => buildApprovalPlugin(readRules(config)),
226
+ };
227
+
228
+ export default approvalPlugin;
@@ -0,0 +1,94 @@
1
+ import { MelonyPlugin } from 'melony';
2
+ import z from 'zod';
3
+ import type { Plugin } from '../../bus/plugin.js';
4
+ import { OpenBotEvent, OpenBotState } from '../../app/types.js';
5
+
6
+ const delegationToolDefinitions = {
7
+ handoff: {
8
+ description:
9
+ 'Transfer control to another agent. The target agent continues the task and you do not wait for a tool result.',
10
+ inputSchema: z.object({
11
+ agentId: z.string().describe('The ID of the target agent.'),
12
+ content: z.string().describe('The message or task to hand off.'),
13
+ }),
14
+ },
15
+ delegate: {
16
+ description:
17
+ 'Delegate a subtask to another agent and wait for its result so you can continue.',
18
+ inputSchema: z.object({
19
+ agentId: z.string().describe('The ID of the target agent.'),
20
+ content: z.string().describe('The subtask you want the target agent to execute.'),
21
+ }),
22
+ },
23
+ };
24
+
25
+ const delegationPluginRuntime = (): MelonyPlugin<OpenBotState, OpenBotEvent> => (builder) => {
26
+ builder.on('action:handoff', async function* (event, context) {
27
+ const { agentId, content } = event.data;
28
+
29
+ yield {
30
+ type: 'agent:output',
31
+ data: { content: `Handing off to **${agentId}**: ${content}` },
32
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
33
+ };
34
+
35
+ yield {
36
+ type: 'handoff:request',
37
+ data: { agentId, content },
38
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
39
+ };
40
+
41
+ if (event.meta?.toolCallId) {
42
+ yield {
43
+ type: 'action:handoff:result',
44
+ data: { success: true, agentId, accepted: true },
45
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
46
+ };
47
+ }
48
+ });
49
+
50
+ builder.on('action:delegate', async function* (event, context) {
51
+ const { agentId, content } = event.data;
52
+ const widgetId = event.meta?.toolCallId
53
+ ? `delegate_${event.meta.toolCallId}`
54
+ : `delegate_${Date.now()}`;
55
+
56
+ yield {
57
+ type: 'client:ui:widget',
58
+ data: {
59
+ kind: 'message',
60
+ widgetId,
61
+ title: `Delegation started: ${agentId}`,
62
+ body: `Running delegated task in background.\n\n${content}`,
63
+ state: 'open',
64
+ metadata: {
65
+ type: 'delegation:status',
66
+ phase: 'started',
67
+ delegatedAgentId: agentId,
68
+ },
69
+ },
70
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
71
+ };
72
+
73
+ yield {
74
+ type: 'delegation:request',
75
+ data: { agentId, content },
76
+ meta: {
77
+ ...(event.meta || {}),
78
+ parentAgentId: context.state.agentId,
79
+ delegationWidgetId: widgetId,
80
+ agentId: context.state.agentId,
81
+ },
82
+ };
83
+ });
84
+ };
85
+
86
+ export const delegationPlugin: Plugin = {
87
+ id: 'delegation',
88
+ name: 'Delegation',
89
+ description: 'Hand off or delegate sub-tasks to other agents on the bus.',
90
+ toolDefinitions: delegationToolDefinitions,
91
+ factory: () => delegationPluginRuntime(),
92
+ };
93
+
94
+ export default delegationPlugin;
@@ -0,0 +1,128 @@
1
+ import { MelonyPlugin } from 'melony';
2
+ import z from 'zod';
3
+ import type { Plugin } from '../../bus/plugin.js';
4
+ import { OpenBotEvent, OpenBotState } from '../../app/types.js';
5
+ import { mcpService } from '../../harness/mcp.js';
6
+
7
+ function stringifyResult(value: unknown): string {
8
+ if (typeof value === 'string') return value;
9
+ try {
10
+ return JSON.stringify(value, null, 2);
11
+ } catch {
12
+ return String(value);
13
+ }
14
+ }
15
+
16
+ const mcpToolDefinitions = {
17
+ mcp_list_tools: {
18
+ description:
19
+ 'List available tools from a configured MCP server. Use this first before calling tools on an unknown server.',
20
+ inputSchema: z.object({
21
+ serverId: z.string().describe('Configured MCP server id (e.g. github, notion, linear).'),
22
+ }),
23
+ },
24
+ mcp_call: {
25
+ description:
26
+ 'Call a tool on a configured MCP server. Provide tool arguments as a JSON object. Use mcp_list_tools first when uncertain.',
27
+ inputSchema: z.object({
28
+ serverId: z.string().describe('Configured MCP server id.'),
29
+ toolName: z.string().describe('Exact MCP tool name from mcp_list_tools.'),
30
+ args: z
31
+ .record(z.string(), z.unknown())
32
+ .default({})
33
+ .describe('Tool arguments as a JSON object.'),
34
+ }),
35
+ },
36
+ };
37
+
38
+ const mcpPluginRuntime = (): MelonyPlugin<OpenBotState, OpenBotEvent> => (builder) => {
39
+ builder.on('action:mcp_list_tools', async function* (event, context) {
40
+ const serverId = (event.data as { serverId?: string })?.serverId as string;
41
+
42
+ try {
43
+ const tools = await mcpService.listTools(serverId);
44
+ const toolNames = tools.map(
45
+ (tool) => `- ${tool.name}${tool.description ? `: ${tool.description}` : ''}`,
46
+ );
47
+
48
+ yield {
49
+ type: 'action:mcp_list_tools:result',
50
+ data: { success: true, serverId, tools },
51
+ meta: event.meta,
52
+ } as OpenBotEvent;
53
+
54
+ yield {
55
+ type: 'agent:output',
56
+ data: {
57
+ content:
58
+ toolNames.length > 0
59
+ ? `MCP tools available on \`${serverId}\`:\n${toolNames.join('\n')}`
60
+ : `MCP server \`${serverId}\` has no tools.`,
61
+ },
62
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
63
+ } as OpenBotEvent;
64
+ } catch (error) {
65
+ const message = error instanceof Error ? error.message : 'Unknown MCP error';
66
+ yield {
67
+ type: 'action:mcp_list_tools:result',
68
+ data: { success: false, serverId, tools: [], error: message },
69
+ meta: event.meta,
70
+ } as OpenBotEvent;
71
+ yield {
72
+ type: 'agent:output',
73
+ data: { content: `Failed to list MCP tools for \`${serverId}\`: ${message}` },
74
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
75
+ } as OpenBotEvent;
76
+ }
77
+ });
78
+
79
+ builder.on('action:mcp_call', async function* (event, context) {
80
+ const data = event.data as {
81
+ serverId?: string;
82
+ toolName?: string;
83
+ args?: Record<string, unknown>;
84
+ };
85
+ const serverId = data?.serverId as string;
86
+ const toolName = data?.toolName as string;
87
+ const args = (data?.args || {}) as Record<string, unknown>;
88
+
89
+ try {
90
+ const result = await mcpService.callTool(serverId, toolName, args);
91
+ const rendered = stringifyResult(result);
92
+
93
+ yield {
94
+ type: 'action:mcp_call:result',
95
+ data: { success: true, serverId, toolName, result },
96
+ meta: event.meta,
97
+ } as OpenBotEvent;
98
+
99
+ yield {
100
+ type: 'agent:output',
101
+ data: { content: `MCP \`${serverId}.${toolName}\` result:\n\n${rendered}` },
102
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
103
+ } as OpenBotEvent;
104
+ } catch (error) {
105
+ const message = error instanceof Error ? error.message : 'Unknown MCP error';
106
+ yield {
107
+ type: 'action:mcp_call:result',
108
+ data: { success: false, serverId, toolName, error: message },
109
+ meta: event.meta,
110
+ } as OpenBotEvent;
111
+ yield {
112
+ type: 'agent:output',
113
+ data: { content: `MCP call failed for \`${serverId}.${toolName}\`: ${message}` },
114
+ meta: { ...(event.meta || {}), agentId: context.state.agentId },
115
+ } as OpenBotEvent;
116
+ }
117
+ });
118
+ };
119
+
120
+ export const mcpPlugin: Plugin = {
121
+ id: 'mcp',
122
+ name: 'MCP',
123
+ description: 'Connect to Model Context Protocol servers and call their tools.',
124
+ toolDefinitions: mcpToolDefinitions,
125
+ factory: () => mcpPluginRuntime(),
126
+ };
127
+
128
+ export default mcpPlugin;
@@ -0,0 +1,123 @@
1
+ import { MelonyPlugin } from 'melony';
2
+ import { z } from 'zod';
3
+ import { spawn } from 'node:child_process';
4
+ import type { Plugin } from '../../bus/plugin.js';
5
+ import { OpenBotEvent, OpenBotState } from '../../app/types.js';
6
+
7
+ const shellToolDefinitions = {
8
+ shell_exec: {
9
+ description:
10
+ 'Execute a shell command in the terminal. Use this for file operations, running scripts, or system tasks.',
11
+ inputSchema: z.object({
12
+ command: z.string().describe('The shell command to execute.'),
13
+ cwd: z
14
+ .string()
15
+ .optional()
16
+ .describe(
17
+ 'Working directory. Defaults to the channel cwd or workspace root. Leave empty unless the user requests a specific directory.',
18
+ ),
19
+ shell: z.enum(['bash', 'sh', 'zsh']).optional().describe('Shell to use. Defaults to bash.'),
20
+ timeoutMs: z
21
+ .number()
22
+ .optional()
23
+ .default(30000)
24
+ .describe('Maximum execution time in milliseconds. Defaults to 30000 (30s).'),
25
+ }),
26
+ },
27
+ };
28
+
29
+ const shellPluginRuntime = (): MelonyPlugin<OpenBotState, OpenBotEvent> => (builder) => {
30
+ builder.on('action:shell_exec', async function* (event, context) {
31
+ const { command, cwd, shell = 'bash', timeoutMs = 30000 } = event.data;
32
+
33
+ const actualTimeout = Math.max(1000, Math.min(timeoutMs, 60000));
34
+ const actualCwd = cwd || context.state.channelDetails?.cwd || process.cwd();
35
+
36
+ try {
37
+ const result = await new Promise<{
38
+ exitCode: number | null;
39
+ stdout: string;
40
+ stderr: string;
41
+ timedOut: boolean;
42
+ }>((resolve) => {
43
+ const child = spawn(command, {
44
+ shell,
45
+ cwd: actualCwd,
46
+ env: { ...process.env },
47
+ });
48
+
49
+ let stdout = '';
50
+ let stderr = '';
51
+ let timedOut = false;
52
+
53
+ const timer = setTimeout(() => {
54
+ timedOut = true;
55
+ child.kill();
56
+ }, actualTimeout);
57
+
58
+ child.stdout.on('data', (data) => {
59
+ stdout += data.toString();
60
+ if (stdout.length > 100000) {
61
+ stdout = stdout.substring(0, 100000) + '\n... [output truncated]';
62
+ child.kill();
63
+ }
64
+ });
65
+
66
+ child.stderr.on('data', (data) => {
67
+ stderr += data.toString();
68
+ if (stderr.length > 100000) {
69
+ stderr = stderr.substring(0, 100000) + '\n... [output truncated]';
70
+ }
71
+ });
72
+
73
+ child.on('close', (code) => {
74
+ clearTimeout(timer);
75
+ resolve({ exitCode: code, stdout, stderr, timedOut });
76
+ });
77
+
78
+ child.on('error', (err) => {
79
+ clearTimeout(timer);
80
+ resolve({ exitCode: -1, stdout, stderr: stderr + err.message, timedOut: false });
81
+ });
82
+ });
83
+
84
+ const success = result.exitCode === 0 && !result.timedOut;
85
+
86
+ yield {
87
+ type: 'action:shell_exec:result',
88
+ data: {
89
+ success,
90
+ exitCode: result.exitCode,
91
+ stdout: result.stdout,
92
+ stderr: result.stderr,
93
+ timedOut: result.timedOut,
94
+ },
95
+ meta: event.meta,
96
+ } as OpenBotEvent;
97
+ } catch (error) {
98
+ const message = error instanceof Error ? error.message : 'Unknown shell error';
99
+ yield {
100
+ type: 'action:shell_exec:result',
101
+ data: {
102
+ success: false,
103
+ exitCode: -1,
104
+ stdout: '',
105
+ stderr: message,
106
+ timedOut: false,
107
+ error: message,
108
+ },
109
+ meta: event.meta,
110
+ } as OpenBotEvent;
111
+ }
112
+ });
113
+ };
114
+
115
+ export const shellPlugin: Plugin = {
116
+ id: 'shell',
117
+ name: 'Shell',
118
+ description: 'Execute shell commands in the channel workspace.',
119
+ toolDefinitions: shellToolDefinitions,
120
+ factory: () => shellPluginRuntime(),
121
+ };
122
+
123
+ export default shellPlugin;
@@ -0,0 +1,101 @@
1
+ import z from 'zod';
2
+ import type { Plugin } from '../../bus/plugin.js';
3
+
4
+ /**
5
+ * `storage-tools` — exposes channel/thread/variable mutation tools to runtime
6
+ * plugins. The actual handlers live in `bus/services.ts` because storage is
7
+ * platform infrastructure, not agent behaviour.
8
+ */
9
+ const storageToolDefinitions = {
10
+ create_channel: {
11
+ description:
12
+ '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.',
13
+ inputSchema: z.object({
14
+ channelId: z
15
+ .string()
16
+ .describe('Unique channel ID (e.g. product-launch, backend-platform, channel_roadmap).'),
17
+ spec: z
18
+ .string()
19
+ .optional()
20
+ .describe('Optional initial markdown content for the channel spec.'),
21
+ initialState: z
22
+ .record(z.string(), z.unknown())
23
+ .optional()
24
+ .describe('Optional initial state object for the channel.'),
25
+ cwd: z
26
+ .string()
27
+ .optional()
28
+ .describe('Optional initial current working directory for the channel.'),
29
+ }),
30
+ },
31
+ patch_channel_details: {
32
+ description: 'Patch current channel details (state, spec, cwd).',
33
+ inputSchema: z
34
+ .object({
35
+ state: z
36
+ .record(z.string(), z.unknown())
37
+ .optional()
38
+ .describe(
39
+ 'JSON state object for the channel. Use for structured data like `todos` or metadata.',
40
+ ),
41
+ spec: z
42
+ .string()
43
+ .optional()
44
+ .describe(
45
+ 'Markdown content for the channel specification (SPEC.md). Use for goals and rules.',
46
+ ),
47
+ cwd: z.string().optional().describe('Current working directory for the channel.'),
48
+ })
49
+ .refine(
50
+ (value) => value.state !== undefined || value.spec !== undefined || value.cwd !== undefined,
51
+ { message: 'Provide at least one of state, spec, or cwd.' },
52
+ ),
53
+ },
54
+ patch_thread_details: {
55
+ description: 'Patch current thread details (state and/or spec).',
56
+ inputSchema: z
57
+ .object({
58
+ state: z
59
+ .record(z.string(), z.unknown())
60
+ .optional()
61
+ .describe(
62
+ 'JSON state object for the thread. Use for structured data like `todos` or progress.',
63
+ ),
64
+ spec: z
65
+ .string()
66
+ .optional()
67
+ .describe(
68
+ 'Markdown content for the thread specification (SPEC.md). Use for plans and goals.',
69
+ ),
70
+ })
71
+ .refine((value) => value.state !== undefined || value.spec !== undefined, {
72
+ message: 'Provide at least one of state or spec.',
73
+ }),
74
+ },
75
+ create_variable: {
76
+ description: 'Create or update a variable in the workspace storage.',
77
+ inputSchema: z.object({
78
+ key: z.string().describe('The key of the variable.'),
79
+ value: z.string().describe('The value of the variable.'),
80
+ secret: z.boolean().optional().describe('Whether the variable is a secret.'),
81
+ }),
82
+ },
83
+ delete_variable: {
84
+ description: 'Delete a variable from the workspace storage.',
85
+ inputSchema: z.object({
86
+ key: z.string().describe('The key of the variable to delete.'),
87
+ }),
88
+ },
89
+ };
90
+
91
+ export const storageToolsPlugin: Plugin = {
92
+ id: 'storage-tools',
93
+ name: 'Storage Tools',
94
+ description: 'Tools for creating channels, patching state, and managing workspace variables.',
95
+ toolDefinitions: storageToolDefinitions,
96
+ factory: () => () => {
97
+ // Handlers live in bus/services.ts; this plugin only contributes tool definitions.
98
+ },
99
+ };
100
+
101
+ export default storageToolsPlugin;