openbot 0.2.13 → 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.
- package/dist/agents/openbot/index.js +76 -0
- package/dist/agents/openbot/middleware/approval.js +132 -0
- package/dist/agents/openbot/runtime.js +289 -0
- package/dist/agents/openbot/system-prompt.js +32 -0
- package/dist/agents/openbot/tools/delegation.js +78 -0
- package/dist/agents/openbot/tools/mcp.js +99 -0
- package/dist/agents/openbot/tools/shell.js +91 -0
- package/dist/agents/openbot/tools/storage.js +75 -0
- package/dist/agents/openbot/tools/ui.js +176 -0
- package/dist/agents/system.js +20 -93
- package/dist/app/cli.js +1 -1
- package/dist/app/config.js +4 -1
- package/dist/app/server.js +15 -8
- package/dist/bus/agent-package.js +1 -0
- package/dist/bus/plugin.js +1 -0
- package/dist/bus/services.js +600 -0
- package/dist/bus/types.js +1 -0
- package/dist/harness/context.js +131 -0
- package/dist/harness/event-normalizer.js +59 -0
- package/dist/harness/orchestrator.js +27 -227
- package/dist/harness/process.js +25 -3
- package/dist/harness/queue-processor.js +227 -0
- package/dist/harness/runtime-factory.js +103 -0
- package/dist/plugins/ai-sdk/index.js +37 -0
- package/dist/plugins/ai-sdk/runtime.js +330 -0
- package/dist/plugins/ai-sdk/system-prompt.js +3 -0
- package/dist/plugins/ai-sdk.js +277 -87
- package/dist/plugins/approval/index.js +159 -0
- package/dist/plugins/approval.js +163 -0
- package/dist/plugins/delegation/index.js +79 -0
- package/dist/plugins/delegation.js +67 -11
- package/dist/plugins/mcp/index.js +108 -0
- package/dist/plugins/shell/index.js +99 -0
- package/dist/plugins/shell.js +123 -0
- package/dist/plugins/storage-tools/index.js +85 -0
- package/dist/plugins/storage.js +240 -5
- package/dist/plugins/ui/index.js +184 -0
- package/dist/plugins/ui.js +185 -21
- package/dist/registry/agents.js +138 -0
- package/dist/registry/plugins.js +91 -50
- package/dist/services/agent-packages.js +103 -0
- package/dist/services/plugins.js +98 -0
- package/dist/services/storage.js +360 -94
- package/docs/agents.md +39 -66
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +70 -58
- package/docs/templates/AGENT.example.md +57 -0
- package/package.json +3 -2
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +14 -4
- package/src/app/server.ts +23 -10
- package/src/app/types.ts +385 -16
- package/src/assets/icon.svg +4 -1
- package/src/bus/plugin.ts +67 -0
- package/src/bus/services.ts +666 -0
- package/src/bus/types.ts +147 -0
- package/src/harness/context.ts +160 -0
- package/src/harness/event-normalizer.ts +82 -0
- package/src/harness/orchestrator.ts +35 -273
- package/src/harness/process.ts +28 -4
- package/src/harness/queue-processor.ts +309 -0
- package/src/harness/runtime-factory.ts +125 -0
- package/src/plugins/ai-sdk/index.ts +44 -0
- package/src/plugins/ai-sdk/runtime.ts +410 -0
- package/src/plugins/ai-sdk/system-prompt.ts +4 -0
- package/src/plugins/approval/index.ts +228 -0
- package/src/plugins/delegation/index.ts +94 -0
- package/src/plugins/mcp/index.ts +128 -0
- package/src/plugins/shell/index.ts +123 -0
- package/src/plugins/storage-tools/index.ts +101 -0
- package/src/plugins/ui/index.ts +227 -0
- package/src/registry/plugins.ts +106 -55
- package/src/services/plugins.ts +133 -0
- package/src/services/storage.ts +465 -137
- package/src/agents/system.ts +0 -112
- package/src/plugins/ai-sdk.ts +0 -197
- package/src/plugins/delegation.ts +0 -60
- package/src/plugins/mcp.ts +0 -154
- package/src/plugins/storage.ts +0 -725
- package/src/plugins/ui.ts +0 -57
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { storageService } from '../services/storage.js';
|
|
2
|
+
const DEFAULT_RULES = [];
|
|
3
|
+
const asRecord = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
4
|
+
const getApprovalsFromState = (state) => {
|
|
5
|
+
const source = state.threadDetails?.state ?? state.channelDetails?.state;
|
|
6
|
+
const stateRecord = asRecord(source);
|
|
7
|
+
return asRecord(stateRecord.approvals);
|
|
8
|
+
};
|
|
9
|
+
const persistApprovals = async (state, approvals) => {
|
|
10
|
+
if (state.threadId) {
|
|
11
|
+
await storageService.patchThreadState({
|
|
12
|
+
channelId: state.channelId,
|
|
13
|
+
threadId: state.threadId,
|
|
14
|
+
state: { approvals },
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
await storageService.patchChannelState({
|
|
19
|
+
channelId: state.channelId,
|
|
20
|
+
state: { approvals },
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
export const approvalPlugin = (options = {}) => (builder) => {
|
|
24
|
+
const rules = options.rules && options.rules.length > 0 ? options.rules : DEFAULT_RULES;
|
|
25
|
+
for (const rule of rules) {
|
|
26
|
+
builder.on(rule.action, async function* (event, context) {
|
|
27
|
+
const meta = asRecord(event.meta);
|
|
28
|
+
if (meta.approvalStatus === 'approved') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const eventData = asRecord(event.data);
|
|
32
|
+
const eventMeta = meta;
|
|
33
|
+
const approvalId = `approval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
34
|
+
const widgetId = `widget_${approvalId}`;
|
|
35
|
+
const executeEvent = rule.executeEvent || rule.action;
|
|
36
|
+
const denyEvent = rule.denyEvent || `${rule.action}:result`;
|
|
37
|
+
const denyData = rule.denyData || {};
|
|
38
|
+
const hiddenKeys = new Set(rule.hiddenKeys || []);
|
|
39
|
+
const detailKeys = rule.detailKeys || Object.keys(eventData);
|
|
40
|
+
const details = detailKeys
|
|
41
|
+
.filter((key) => !hiddenKeys.has(key))
|
|
42
|
+
.map((key) => `- ${key}: ${String(eventData[key] ?? '')}`)
|
|
43
|
+
.join('\n');
|
|
44
|
+
const pendingApprovals = getApprovalsFromState(context.state);
|
|
45
|
+
pendingApprovals[approvalId] = {
|
|
46
|
+
id: approvalId,
|
|
47
|
+
action: rule.action,
|
|
48
|
+
executeEvent,
|
|
49
|
+
denyEvent,
|
|
50
|
+
denyData,
|
|
51
|
+
payload: eventData,
|
|
52
|
+
meta: eventMeta,
|
|
53
|
+
message: rule.message || `Approval required for ${rule.action}.`,
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
status: 'pending',
|
|
56
|
+
};
|
|
57
|
+
await persistApprovals(context.state, pendingApprovals);
|
|
58
|
+
yield {
|
|
59
|
+
type: 'client:ui:widget',
|
|
60
|
+
data: {
|
|
61
|
+
kind: 'choice',
|
|
62
|
+
widgetId,
|
|
63
|
+
title: 'Approval Required',
|
|
64
|
+
body: `${rule.message || 'A protected action requires approval.'}${details ? `\n\n${details}` : ''}`,
|
|
65
|
+
metadata: {
|
|
66
|
+
type: 'approval:request',
|
|
67
|
+
approvalId,
|
|
68
|
+
action: rule.action,
|
|
69
|
+
},
|
|
70
|
+
actions: [
|
|
71
|
+
{ id: 'approve', label: 'Approve', variant: 'primary' },
|
|
72
|
+
{ id: 'deny', label: 'Deny', variant: 'danger' },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
meta: {
|
|
76
|
+
...(event.meta || {}),
|
|
77
|
+
agentId: context.state.agentId,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
yield {
|
|
81
|
+
type: 'agent:output',
|
|
82
|
+
data: {
|
|
83
|
+
content: `Waiting for approval before running \`${rule.action}\`.`,
|
|
84
|
+
},
|
|
85
|
+
meta: {
|
|
86
|
+
...(event.meta || {}),
|
|
87
|
+
agentId: context.state.agentId,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
context.suspend();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
builder.on('client:ui:widget:response', async function* (event, context) {
|
|
94
|
+
const metadata = asRecord(event.data?.metadata);
|
|
95
|
+
if (metadata.type !== 'approval:request') {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const approvalId = String(metadata.approvalId || '');
|
|
99
|
+
if (!approvalId) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const approvals = getApprovalsFromState(context.state);
|
|
103
|
+
const approval = approvals[approvalId];
|
|
104
|
+
if (!approval || approval.status !== 'pending') {
|
|
105
|
+
yield {
|
|
106
|
+
type: 'agent:output',
|
|
107
|
+
data: { content: 'Approval request not found or already resolved.' },
|
|
108
|
+
meta: {
|
|
109
|
+
...(event.meta || {}),
|
|
110
|
+
agentId: context.state.agentId,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const approved = event.data.actionId === 'approve';
|
|
116
|
+
approvals[approvalId] = {
|
|
117
|
+
...approval,
|
|
118
|
+
status: approved ? 'approved' : 'denied',
|
|
119
|
+
};
|
|
120
|
+
await persistApprovals(context.state, approvals);
|
|
121
|
+
if (approved) {
|
|
122
|
+
yield {
|
|
123
|
+
type: approval.executeEvent,
|
|
124
|
+
data: approval.payload,
|
|
125
|
+
meta: {
|
|
126
|
+
...(approval.meta || {}),
|
|
127
|
+
approvalId,
|
|
128
|
+
approvalStatus: 'approved',
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
yield {
|
|
134
|
+
type: approval.denyEvent,
|
|
135
|
+
data: {
|
|
136
|
+
success: false,
|
|
137
|
+
approved: false,
|
|
138
|
+
error: 'Action denied by user approval.',
|
|
139
|
+
...approval.denyData,
|
|
140
|
+
},
|
|
141
|
+
meta: {
|
|
142
|
+
...(approval.meta || {}),
|
|
143
|
+
approvalId,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
yield {
|
|
147
|
+
type: 'agent:output',
|
|
148
|
+
data: { content: 'Action denied by user approval.' },
|
|
149
|
+
meta: {
|
|
150
|
+
...(event.meta || {}),
|
|
151
|
+
agentId: context.state.agentId,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
export const plugin = {
|
|
157
|
+
name: 'approval',
|
|
158
|
+
description: 'Approval workflow for protected actions',
|
|
159
|
+
version: '1.0.0',
|
|
160
|
+
author: 'OpenBot',
|
|
161
|
+
license: 'MIT',
|
|
162
|
+
factory: approvalPlugin,
|
|
163
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
const delegationToolDefinitions = {
|
|
3
|
+
handoff: {
|
|
4
|
+
description: 'Transfer control to another agent. The target agent continues the task and you do not wait for a tool result.',
|
|
5
|
+
inputSchema: z.object({
|
|
6
|
+
agentId: z.string().describe('The ID of the target agent.'),
|
|
7
|
+
content: z.string().describe('The message or task to hand off.'),
|
|
8
|
+
}),
|
|
9
|
+
},
|
|
10
|
+
delegate: {
|
|
11
|
+
description: 'Delegate a subtask to another agent and wait for its result so you can continue.',
|
|
12
|
+
inputSchema: z.object({
|
|
13
|
+
agentId: z.string().describe('The ID of the target agent.'),
|
|
14
|
+
content: z.string().describe('The subtask you want the target agent to execute.'),
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
const delegationPluginRuntime = () => (builder) => {
|
|
19
|
+
builder.on('action:handoff', async function* (event, context) {
|
|
20
|
+
const { agentId, content } = event.data;
|
|
21
|
+
yield {
|
|
22
|
+
type: 'agent:output',
|
|
23
|
+
data: { content: `Handing off to **${agentId}**: ${content}` },
|
|
24
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
25
|
+
};
|
|
26
|
+
yield {
|
|
27
|
+
type: 'handoff:request',
|
|
28
|
+
data: { agentId, content },
|
|
29
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
30
|
+
};
|
|
31
|
+
if (event.meta?.toolCallId) {
|
|
32
|
+
yield {
|
|
33
|
+
type: 'action:handoff:result',
|
|
34
|
+
data: { success: true, agentId, accepted: true },
|
|
35
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
builder.on('action:delegate', async function* (event, context) {
|
|
40
|
+
const { agentId, content } = event.data;
|
|
41
|
+
const widgetId = event.meta?.toolCallId
|
|
42
|
+
? `delegate_${event.meta.toolCallId}`
|
|
43
|
+
: `delegate_${Date.now()}`;
|
|
44
|
+
yield {
|
|
45
|
+
type: 'client:ui:widget',
|
|
46
|
+
data: {
|
|
47
|
+
kind: 'message',
|
|
48
|
+
widgetId,
|
|
49
|
+
title: `Delegation started: ${agentId}`,
|
|
50
|
+
body: `Running delegated task in background.\n\n${content}`,
|
|
51
|
+
state: 'open',
|
|
52
|
+
metadata: {
|
|
53
|
+
type: 'delegation:status',
|
|
54
|
+
phase: 'started',
|
|
55
|
+
delegatedAgentId: agentId,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
59
|
+
};
|
|
60
|
+
yield {
|
|
61
|
+
type: 'delegation:request',
|
|
62
|
+
data: { agentId, content },
|
|
63
|
+
meta: {
|
|
64
|
+
...(event.meta || {}),
|
|
65
|
+
parentAgentId: context.state.agentId,
|
|
66
|
+
delegationWidgetId: widgetId,
|
|
67
|
+
agentId: context.state.agentId,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
export const delegationPlugin = {
|
|
73
|
+
id: 'delegation',
|
|
74
|
+
name: 'Delegation',
|
|
75
|
+
description: 'Hand off or delegate sub-tasks to other agents on the bus.',
|
|
76
|
+
toolDefinitions: delegationToolDefinitions,
|
|
77
|
+
factory: () => delegationPluginRuntime(),
|
|
78
|
+
};
|
|
79
|
+
export default delegationPlugin;
|
|
@@ -1,42 +1,98 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
2
|
/**
|
|
3
3
|
* Delegation Plugin for Melony.
|
|
4
|
-
*
|
|
4
|
+
* Handles handoff/delegation events and routes internal control events to the orchestrator.
|
|
5
5
|
*/
|
|
6
6
|
export const delegationPlugin = () => (builder) => {
|
|
7
|
-
builder.on('action:
|
|
7
|
+
builder.on('action:handoff', async function* (event, context) {
|
|
8
8
|
const { agentId, content } = event.data;
|
|
9
|
-
// 1. Show the
|
|
9
|
+
// 1. Show the handoff in the UI
|
|
10
10
|
yield {
|
|
11
11
|
type: 'agent:output',
|
|
12
12
|
data: {
|
|
13
|
-
content: `
|
|
13
|
+
content: `Handing off to **${agentId}**: ${content}`,
|
|
14
|
+
},
|
|
15
|
+
meta: {
|
|
16
|
+
...(event.meta || {}),
|
|
17
|
+
agentId: context.state.agentId,
|
|
14
18
|
},
|
|
19
|
+
};
|
|
20
|
+
// 2. Orchestrator turns this into a real agent:invoke for the target.
|
|
21
|
+
yield {
|
|
22
|
+
type: 'handoff:request',
|
|
23
|
+
data: { agentId, content },
|
|
15
24
|
meta: {
|
|
16
25
|
...(event.meta || {}),
|
|
17
26
|
agentId: context.state.agentId,
|
|
18
27
|
},
|
|
19
28
|
};
|
|
20
|
-
//
|
|
29
|
+
// 3. Acknowledge tool completion so providers requiring tool-result pairing stay consistent.
|
|
30
|
+
if (event.meta?.toolCallId) {
|
|
31
|
+
yield {
|
|
32
|
+
type: 'action:handoff:result',
|
|
33
|
+
data: {
|
|
34
|
+
success: true,
|
|
35
|
+
agentId,
|
|
36
|
+
accepted: true,
|
|
37
|
+
},
|
|
38
|
+
meta: {
|
|
39
|
+
...(event.meta || {}),
|
|
40
|
+
agentId: context.state.agentId,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
builder.on('action:delegate', async function* (event, context) {
|
|
46
|
+
const { agentId, content } = event.data;
|
|
47
|
+
const widgetId = event.meta?.toolCallId
|
|
48
|
+
? `delegate_${event.meta.toolCallId}`
|
|
49
|
+
: `delegate_${Date.now()}`;
|
|
50
|
+
// 1. Show delegation progress in UI (child output stays hidden for delegate mode).
|
|
21
51
|
yield {
|
|
22
|
-
type: '
|
|
52
|
+
type: 'client:ui:widget',
|
|
23
53
|
data: {
|
|
24
|
-
|
|
25
|
-
|
|
54
|
+
kind: 'message',
|
|
55
|
+
widgetId,
|
|
56
|
+
title: `Delegation started: ${agentId}`,
|
|
57
|
+
body: `Running delegated task in background.\n\n${content}`,
|
|
58
|
+
state: 'open',
|
|
59
|
+
metadata: {
|
|
60
|
+
type: 'delegation:status',
|
|
61
|
+
phase: 'started',
|
|
62
|
+
delegatedAgentId: agentId,
|
|
63
|
+
},
|
|
26
64
|
},
|
|
27
65
|
meta: {
|
|
28
66
|
...(event.meta || {}),
|
|
29
67
|
agentId: context.state.agentId,
|
|
30
68
|
},
|
|
31
69
|
};
|
|
70
|
+
// 2. Orchestrator executes target agent and feeds result back as action:delegate:result.
|
|
71
|
+
yield {
|
|
72
|
+
type: 'delegation:request',
|
|
73
|
+
data: { agentId, content },
|
|
74
|
+
meta: {
|
|
75
|
+
...(event.meta || {}),
|
|
76
|
+
parentAgentId: context.state.agentId,
|
|
77
|
+
delegationWidgetId: widgetId,
|
|
78
|
+
agentId: context.state.agentId,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
32
81
|
});
|
|
33
82
|
};
|
|
34
83
|
export const delegationToolDefinitions = {
|
|
84
|
+
handoff: {
|
|
85
|
+
description: 'Transfer control to another agent. The target agent continues the task and you do not wait for a tool result.',
|
|
86
|
+
inputSchema: z.object({
|
|
87
|
+
agentId: z.string().describe('The ID of the target agent (e.g. "os", "browser", "tavily").'),
|
|
88
|
+
content: z.string().describe('The message or task to hand off.'),
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
35
91
|
delegate: {
|
|
36
|
-
description: 'Delegate a
|
|
92
|
+
description: 'Delegate a subtask to another agent and wait for its result so you can continue.',
|
|
37
93
|
inputSchema: z.object({
|
|
38
|
-
agentId: z.string().describe('The ID of the agent (e.g. "os", "browser", "tavily").'),
|
|
39
|
-
content: z.string().describe('The
|
|
94
|
+
agentId: z.string().describe('The ID of the target agent (e.g. "os", "browser", "tavily").'),
|
|
95
|
+
content: z.string().describe('The subtask you want the target agent to execute.'),
|
|
40
96
|
}),
|
|
41
97
|
},
|
|
42
98
|
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { mcpService } from '../../harness/mcp.js';
|
|
3
|
+
function stringifyResult(value) {
|
|
4
|
+
if (typeof value === 'string')
|
|
5
|
+
return value;
|
|
6
|
+
try {
|
|
7
|
+
return JSON.stringify(value, null, 2);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return String(value);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const mcpToolDefinitions = {
|
|
14
|
+
mcp_list_tools: {
|
|
15
|
+
description: 'List available tools from a configured MCP server. Use this first before calling tools on an unknown server.',
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
serverId: z.string().describe('Configured MCP server id (e.g. github, notion, linear).'),
|
|
18
|
+
}),
|
|
19
|
+
},
|
|
20
|
+
mcp_call: {
|
|
21
|
+
description: 'Call a tool on a configured MCP server. Provide tool arguments as a JSON object. Use mcp_list_tools first when uncertain.',
|
|
22
|
+
inputSchema: z.object({
|
|
23
|
+
serverId: z.string().describe('Configured MCP server id.'),
|
|
24
|
+
toolName: z.string().describe('Exact MCP tool name from mcp_list_tools.'),
|
|
25
|
+
args: z
|
|
26
|
+
.record(z.string(), z.unknown())
|
|
27
|
+
.default({})
|
|
28
|
+
.describe('Tool arguments as a JSON object.'),
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const mcpPluginRuntime = () => (builder) => {
|
|
33
|
+
builder.on('action:mcp_list_tools', async function* (event, context) {
|
|
34
|
+
const serverId = event.data?.serverId;
|
|
35
|
+
try {
|
|
36
|
+
const tools = await mcpService.listTools(serverId);
|
|
37
|
+
const toolNames = tools.map((tool) => `- ${tool.name}${tool.description ? `: ${tool.description}` : ''}`);
|
|
38
|
+
yield {
|
|
39
|
+
type: 'action:mcp_list_tools:result',
|
|
40
|
+
data: { success: true, serverId, tools },
|
|
41
|
+
meta: event.meta,
|
|
42
|
+
};
|
|
43
|
+
yield {
|
|
44
|
+
type: 'agent:output',
|
|
45
|
+
data: {
|
|
46
|
+
content: toolNames.length > 0
|
|
47
|
+
? `MCP tools available on \`${serverId}\`:\n${toolNames.join('\n')}`
|
|
48
|
+
: `MCP server \`${serverId}\` has no tools.`,
|
|
49
|
+
},
|
|
50
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const message = error instanceof Error ? error.message : 'Unknown MCP error';
|
|
55
|
+
yield {
|
|
56
|
+
type: 'action:mcp_list_tools:result',
|
|
57
|
+
data: { success: false, serverId, tools: [], error: message },
|
|
58
|
+
meta: event.meta,
|
|
59
|
+
};
|
|
60
|
+
yield {
|
|
61
|
+
type: 'agent:output',
|
|
62
|
+
data: { content: `Failed to list MCP tools for \`${serverId}\`: ${message}` },
|
|
63
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
builder.on('action:mcp_call', async function* (event, context) {
|
|
68
|
+
const data = event.data;
|
|
69
|
+
const serverId = data?.serverId;
|
|
70
|
+
const toolName = data?.toolName;
|
|
71
|
+
const args = (data?.args || {});
|
|
72
|
+
try {
|
|
73
|
+
const result = await mcpService.callTool(serverId, toolName, args);
|
|
74
|
+
const rendered = stringifyResult(result);
|
|
75
|
+
yield {
|
|
76
|
+
type: 'action:mcp_call:result',
|
|
77
|
+
data: { success: true, serverId, toolName, result },
|
|
78
|
+
meta: event.meta,
|
|
79
|
+
};
|
|
80
|
+
yield {
|
|
81
|
+
type: 'agent:output',
|
|
82
|
+
data: { content: `MCP \`${serverId}.${toolName}\` result:\n\n${rendered}` },
|
|
83
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : 'Unknown MCP error';
|
|
88
|
+
yield {
|
|
89
|
+
type: 'action:mcp_call:result',
|
|
90
|
+
data: { success: false, serverId, toolName, error: message },
|
|
91
|
+
meta: event.meta,
|
|
92
|
+
};
|
|
93
|
+
yield {
|
|
94
|
+
type: 'agent:output',
|
|
95
|
+
data: { content: `MCP call failed for \`${serverId}.${toolName}\`: ${message}` },
|
|
96
|
+
meta: { ...(event.meta || {}), agentId: context.state.agentId },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
export const mcpPlugin = {
|
|
102
|
+
id: 'mcp',
|
|
103
|
+
name: 'MCP',
|
|
104
|
+
description: 'Connect to Model Context Protocol servers and call their tools.',
|
|
105
|
+
toolDefinitions: mcpToolDefinitions,
|
|
106
|
+
factory: () => mcpPluginRuntime(),
|
|
107
|
+
};
|
|
108
|
+
export default mcpPlugin;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
const shellToolDefinitions = {
|
|
4
|
+
shell_exec: {
|
|
5
|
+
description: 'Execute a shell command in the terminal. Use this for file operations, running scripts, or system tasks.',
|
|
6
|
+
inputSchema: z.object({
|
|
7
|
+
command: z.string().describe('The shell command to execute.'),
|
|
8
|
+
cwd: z
|
|
9
|
+
.string()
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Working directory. Defaults to the channel cwd or workspace root. Leave empty unless the user requests a specific directory.'),
|
|
12
|
+
shell: z.enum(['bash', 'sh', 'zsh']).optional().describe('Shell to use. Defaults to bash.'),
|
|
13
|
+
timeoutMs: z
|
|
14
|
+
.number()
|
|
15
|
+
.optional()
|
|
16
|
+
.default(30000)
|
|
17
|
+
.describe('Maximum execution time in milliseconds. Defaults to 30000 (30s).'),
|
|
18
|
+
}),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const shellPluginRuntime = () => (builder) => {
|
|
22
|
+
builder.on('action:shell_exec', async function* (event, context) {
|
|
23
|
+
const { command, cwd, shell = 'bash', timeoutMs = 30000 } = event.data;
|
|
24
|
+
const actualTimeout = Math.max(1000, Math.min(timeoutMs, 60000));
|
|
25
|
+
const actualCwd = cwd || context.state.channelDetails?.cwd || process.cwd();
|
|
26
|
+
try {
|
|
27
|
+
const result = await new Promise((resolve) => {
|
|
28
|
+
const child = spawn(command, {
|
|
29
|
+
shell,
|
|
30
|
+
cwd: actualCwd,
|
|
31
|
+
env: { ...process.env },
|
|
32
|
+
});
|
|
33
|
+
let stdout = '';
|
|
34
|
+
let stderr = '';
|
|
35
|
+
let timedOut = false;
|
|
36
|
+
const timer = setTimeout(() => {
|
|
37
|
+
timedOut = true;
|
|
38
|
+
child.kill();
|
|
39
|
+
}, actualTimeout);
|
|
40
|
+
child.stdout.on('data', (data) => {
|
|
41
|
+
stdout += data.toString();
|
|
42
|
+
if (stdout.length > 100000) {
|
|
43
|
+
stdout = stdout.substring(0, 100000) + '\n... [output truncated]';
|
|
44
|
+
child.kill();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
child.stderr.on('data', (data) => {
|
|
48
|
+
stderr += data.toString();
|
|
49
|
+
if (stderr.length > 100000) {
|
|
50
|
+
stderr = stderr.substring(0, 100000) + '\n... [output truncated]';
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
child.on('close', (code) => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
resolve({ exitCode: code, stdout, stderr, timedOut });
|
|
56
|
+
});
|
|
57
|
+
child.on('error', (err) => {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
resolve({ exitCode: -1, stdout, stderr: stderr + err.message, timedOut: false });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
const success = result.exitCode === 0 && !result.timedOut;
|
|
63
|
+
yield {
|
|
64
|
+
type: 'action:shell_exec:result',
|
|
65
|
+
data: {
|
|
66
|
+
success,
|
|
67
|
+
exitCode: result.exitCode,
|
|
68
|
+
stdout: result.stdout,
|
|
69
|
+
stderr: result.stderr,
|
|
70
|
+
timedOut: result.timedOut,
|
|
71
|
+
},
|
|
72
|
+
meta: event.meta,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : 'Unknown shell error';
|
|
77
|
+
yield {
|
|
78
|
+
type: 'action:shell_exec:result',
|
|
79
|
+
data: {
|
|
80
|
+
success: false,
|
|
81
|
+
exitCode: -1,
|
|
82
|
+
stdout: '',
|
|
83
|
+
stderr: message,
|
|
84
|
+
timedOut: false,
|
|
85
|
+
error: message,
|
|
86
|
+
},
|
|
87
|
+
meta: event.meta,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
export const shellPlugin = {
|
|
93
|
+
id: 'shell',
|
|
94
|
+
name: 'Shell',
|
|
95
|
+
description: 'Execute shell commands in the channel workspace.',
|
|
96
|
+
toolDefinitions: shellToolDefinitions,
|
|
97
|
+
factory: () => shellPluginRuntime(),
|
|
98
|
+
};
|
|
99
|
+
export default shellPlugin;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
export const shellToolDefinitions = {
|
|
4
|
+
shell_exec: {
|
|
5
|
+
description: 'Execute a shell command in the terminal. Use this for file operations, running scripts, or system tasks.',
|
|
6
|
+
inputSchema: z.object({
|
|
7
|
+
command: z.string().describe('The shell command to execute.'),
|
|
8
|
+
cwd: z.string().optional().describe('The working directory for the command. Defaults to the channel cwd or workspace root. Leave it empty unless user asks for a specific directory.'),
|
|
9
|
+
shell: z.enum(['bash', 'sh', 'zsh']).optional().describe('The shell to use. Defaults to bash.'),
|
|
10
|
+
timeoutMs: z.number().optional().default(30000).describe('Maximum execution time in milliseconds. Defaults to 30000 (30s).'),
|
|
11
|
+
}),
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
export const shellPlugin = () => (builder) => {
|
|
15
|
+
builder.on('action:shell_exec', async function* (event, context) {
|
|
16
|
+
const { command, cwd, shell = 'bash', timeoutMs = 30000 } = event.data;
|
|
17
|
+
// Clamp timeout between 1s and 60s
|
|
18
|
+
const actualTimeout = Math.max(1000, Math.min(timeoutMs, 60000));
|
|
19
|
+
// Default CWD to channel CWD if not provided
|
|
20
|
+
const actualCwd = cwd || context.state.channelDetails?.cwd || process.cwd();
|
|
21
|
+
try {
|
|
22
|
+
const result = await new Promise((resolve) => {
|
|
23
|
+
const child = spawn(command, {
|
|
24
|
+
shell,
|
|
25
|
+
cwd: actualCwd,
|
|
26
|
+
env: { ...process.env },
|
|
27
|
+
});
|
|
28
|
+
let stdout = '';
|
|
29
|
+
let stderr = '';
|
|
30
|
+
let timedOut = false;
|
|
31
|
+
const timer = setTimeout(() => {
|
|
32
|
+
timedOut = true;
|
|
33
|
+
child.kill();
|
|
34
|
+
}, actualTimeout);
|
|
35
|
+
child.stdout.on('data', (data) => {
|
|
36
|
+
stdout += data.toString();
|
|
37
|
+
// Cap output at 100KB
|
|
38
|
+
if (stdout.length > 100000) {
|
|
39
|
+
stdout = stdout.substring(0, 100000) + '\n... [output truncated]';
|
|
40
|
+
child.kill();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
child.stderr.on('data', (data) => {
|
|
44
|
+
stderr += data.toString();
|
|
45
|
+
if (stderr.length > 100000) {
|
|
46
|
+
stderr = stderr.substring(0, 100000) + '\n... [output truncated]';
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
child.on('close', (code) => {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
resolve({ exitCode: code, stdout, stderr, timedOut });
|
|
52
|
+
});
|
|
53
|
+
child.on('error', (err) => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
resolve({ exitCode: -1, stdout, stderr: stderr + err.message, timedOut: false });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
const success = result.exitCode === 0 && !result.timedOut;
|
|
59
|
+
yield {
|
|
60
|
+
type: 'action:shell_exec:result',
|
|
61
|
+
data: {
|
|
62
|
+
success,
|
|
63
|
+
exitCode: result.exitCode,
|
|
64
|
+
stdout: result.stdout,
|
|
65
|
+
stderr: result.stderr,
|
|
66
|
+
timedOut: result.timedOut,
|
|
67
|
+
},
|
|
68
|
+
meta: event.meta,
|
|
69
|
+
};
|
|
70
|
+
// const output = [
|
|
71
|
+
// `Command: \`${command}\``,
|
|
72
|
+
// result.exitCode !== null ? `Exit code: ${result.exitCode}` : 'Exit code: unknown',
|
|
73
|
+
// result.timedOut ? '⚠️ Command timed out.' : '',
|
|
74
|
+
// result.stdout ? `\n**STDOUT**:\n${result.stdout}` : '',
|
|
75
|
+
// result.stderr ? `\n**STDERR**:\n${result.stderr}` : '',
|
|
76
|
+
// ].filter(Boolean).join('\n');
|
|
77
|
+
// yield {
|
|
78
|
+
// type: 'agent:output',
|
|
79
|
+
// data: {
|
|
80
|
+
// content: output,
|
|
81
|
+
// },
|
|
82
|
+
// meta: {
|
|
83
|
+
// ...(event.meta || {}),
|
|
84
|
+
// agentId: context.state.agentId,
|
|
85
|
+
// },
|
|
86
|
+
// } as any;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const message = error instanceof Error ? error.message : 'Unknown shell error';
|
|
90
|
+
yield {
|
|
91
|
+
type: 'action:shell_exec:result',
|
|
92
|
+
data: {
|
|
93
|
+
success: false,
|
|
94
|
+
exitCode: -1,
|
|
95
|
+
stdout: '',
|
|
96
|
+
stderr: message,
|
|
97
|
+
timedOut: false,
|
|
98
|
+
error: message,
|
|
99
|
+
},
|
|
100
|
+
meta: event.meta,
|
|
101
|
+
};
|
|
102
|
+
// yield {
|
|
103
|
+
// type: 'agent:output',
|
|
104
|
+
// data: {
|
|
105
|
+
// content: `Failed to execute shell command: ${message}`,
|
|
106
|
+
// },
|
|
107
|
+
// meta: {
|
|
108
|
+
// ...(event.meta || {}),
|
|
109
|
+
// agentId: context.state.agentId,
|
|
110
|
+
// },
|
|
111
|
+
// } as any;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
export const plugin = {
|
|
116
|
+
name: 'shell',
|
|
117
|
+
description: 'Execute shell commands in the terminal',
|
|
118
|
+
version: '1.0.0',
|
|
119
|
+
author: 'OpenBot',
|
|
120
|
+
license: 'MIT',
|
|
121
|
+
factory: shellPlugin,
|
|
122
|
+
toolDefinitions: shellToolDefinitions,
|
|
123
|
+
};
|