@meetopenbot/openbot 0.2.7 → 1.0.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.
@@ -1,269 +0,0 @@
1
- import { z } from 'zod';
2
- import { spawn } from 'node:child_process';
3
- import { randomUUID } from 'node:crypto';
4
- import { asActionBuilder } from '../types.js';
5
- const TUNNEL_URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
6
- const TUNNEL_READY_TIMEOUT_MS = 60000;
7
- const MAX_LOG_CHARS = 8000;
8
- const previewToolDefinitions = {
9
- expose_port: {
10
- description: 'Expose a local dev server port via a temporary public Cloudflare quick tunnel. Returns a previewUrl stored on the channel. Dev servers must listen on 0.0.0.0 or 127.0.0.1. Call after shell_exec when the server is ready.',
11
- inputSchema: z.object({
12
- port: z
13
- .number()
14
- .int()
15
- .min(1024)
16
- .max(65535)
17
- .describe('Local port of the running dev server (e.g. 5173).'),
18
- }),
19
- },
20
- unexpose_port: {
21
- description: 'Stop the active Cloudflare preview tunnel for this channel and clear previewUrl from channel state.',
22
- inputSchema: z.object({}),
23
- },
24
- };
25
- const tunnels = new Map();
26
- const tunnelByChannel = new Map();
27
- const blockedPorts = () => {
28
- const openbotPort = Number(process.env.PORT ?? 4132);
29
- return new Set([22, 80, 443, openbotPort]);
30
- };
31
- const appendLog = (tunnel, chunk) => {
32
- tunnel.logs += chunk;
33
- if (tunnel.logs.length > MAX_LOG_CHARS) {
34
- tunnel.logs = tunnel.logs.slice(-MAX_LOG_CHARS);
35
- }
36
- };
37
- const killTunnelProcess = (tunnel) => {
38
- const { process: child } = tunnel;
39
- if (!child.pid) {
40
- try {
41
- child.kill();
42
- }
43
- catch {
44
- /* ignore */
45
- }
46
- return;
47
- }
48
- try {
49
- child.kill('SIGTERM');
50
- }
51
- catch {
52
- try {
53
- child.kill();
54
- }
55
- catch {
56
- /* ignore */
57
- }
58
- }
59
- };
60
- const removeTunnel = (tunnelId) => {
61
- const tunnel = tunnels.get(tunnelId);
62
- if (!tunnel)
63
- return;
64
- killTunnelProcess(tunnel);
65
- tunnels.delete(tunnelId);
66
- if (tunnelByChannel.get(tunnel.channelId) === tunnelId) {
67
- tunnelByChannel.delete(tunnel.channelId);
68
- }
69
- };
70
- export const stopPreviewForChannel = (channelId) => {
71
- const tunnelId = tunnelByChannel.get(channelId);
72
- if (tunnelId) {
73
- removeTunnel(tunnelId);
74
- }
75
- };
76
- const waitForTunnelUrl = (child, timeoutMs) => new Promise((resolve, reject) => {
77
- let buffer = '';
78
- let settled = false;
79
- const cleanup = () => {
80
- clearTimeout(timer);
81
- child.stdout?.off('data', onData);
82
- child.stderr?.off('data', onData);
83
- child.off('exit', onExit);
84
- child.off('error', onError);
85
- };
86
- const tryParse = () => {
87
- const match = buffer.match(TUNNEL_URL_PATTERN);
88
- if (match) {
89
- settled = true;
90
- cleanup();
91
- resolve(match[0]);
92
- }
93
- };
94
- const onData = (chunk) => {
95
- buffer += chunk.toString();
96
- if (buffer.length > 16000) {
97
- buffer = buffer.slice(-16000);
98
- }
99
- tryParse();
100
- };
101
- const onExit = (code) => {
102
- if (settled)
103
- return;
104
- settled = true;
105
- cleanup();
106
- reject(new Error(`cloudflared exited before providing a tunnel URL (code ${code ?? 'unknown'})`));
107
- };
108
- const onError = (err) => {
109
- if (settled)
110
- return;
111
- settled = true;
112
- cleanup();
113
- reject(err);
114
- };
115
- const timer = setTimeout(() => {
116
- if (settled)
117
- return;
118
- settled = true;
119
- cleanup();
120
- reject(new Error('Timed out waiting for Cloudflare tunnel URL'));
121
- }, timeoutMs);
122
- child.stdout?.on('data', onData);
123
- child.stderr?.on('data', onData);
124
- child.on('exit', onExit);
125
- child.on('error', onError);
126
- tryParse();
127
- });
128
- const startCloudflaredTunnel = async (channelId, port) => {
129
- const child = spawn('cloudflared', ['tunnel', '--url', `http://127.0.0.1:${port}`, '--no-autoupdate'], {
130
- env: process.env,
131
- stdio: ['ignore', 'pipe', 'pipe'],
132
- });
133
- const tunnel = {
134
- id: randomUUID(),
135
- channelId,
136
- port,
137
- url: '',
138
- process: child,
139
- startedAt: Date.now(),
140
- logs: '',
141
- };
142
- child.stdout?.on('data', (data) => appendLog(tunnel, data.toString()));
143
- child.stderr?.on('data', (data) => appendLog(tunnel, data.toString()));
144
- child.on('exit', () => {
145
- tunnels.delete(tunnel.id);
146
- if (tunnelByChannel.get(channelId) === tunnel.id) {
147
- tunnelByChannel.delete(channelId);
148
- }
149
- });
150
- const url = await waitForTunnelUrl(child, TUNNEL_READY_TIMEOUT_MS);
151
- tunnel.url = url;
152
- tunnels.set(tunnel.id, tunnel);
153
- tunnelByChannel.set(channelId, tunnel.id);
154
- return tunnel;
155
- };
156
- const clearPreviewChannelState = async (storage, channelId) => {
157
- await storage.patchChannelState({
158
- channelId,
159
- state: {
160
- previewUrl: null,
161
- previewPort: null,
162
- previewExposedAt: null,
163
- },
164
- });
165
- };
166
- const previewPluginRuntime = (storage) => (builder) => {
167
- const actions = asActionBuilder(builder);
168
- actions.on('expose_port', async function* (event, context) {
169
- const channelId = context.state.channelId;
170
- const port = event.data?.port;
171
- if (!Number.isInteger(port)) {
172
- yield {
173
- type: 'action:expose_port:result',
174
- data: {
175
- success: false,
176
- output: 'port must be an integer between 1024 and 65535.',
177
- },
178
- meta: event.meta,
179
- };
180
- return;
181
- }
182
- if (blockedPorts().has(port)) {
183
- yield {
184
- type: 'action:expose_port:result',
185
- data: {
186
- success: false,
187
- output: `Port ${port} is reserved and cannot be exposed.`,
188
- },
189
- meta: event.meta,
190
- };
191
- return;
192
- }
193
- const existingTunnelId = tunnelByChannel.get(channelId);
194
- if (existingTunnelId) {
195
- removeTunnel(existingTunnelId);
196
- }
197
- try {
198
- const tunnel = await startCloudflaredTunnel(channelId, port);
199
- await storage.patchChannelState({
200
- channelId,
201
- state: {
202
- previewUrl: tunnel.url,
203
- previewPort: port,
204
- previewExposedAt: tunnel.startedAt,
205
- },
206
- });
207
- if (context.state.channelDetails) {
208
- context.state.channelDetails = await storage.getChannelDetails({ channelId });
209
- }
210
- yield {
211
- type: 'action:expose_port:result',
212
- data: {
213
- success: true,
214
- previewUrl: tunnel.url,
215
- port,
216
- temporary: true,
217
- output: `Preview available at ${tunnel.url} (temporary Cloudflare quick tunnel).`,
218
- },
219
- meta: event.meta,
220
- };
221
- }
222
- catch (error) {
223
- const message = error instanceof Error ? error.message : 'Failed to start Cloudflare tunnel';
224
- const needsCloudflared = message.includes('ENOENT') || message.toLowerCase().includes('cloudflared');
225
- const hint = needsCloudflared
226
- ? ' Install cloudflared and ensure it is on PATH (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/downloads/).'
227
- : '';
228
- yield {
229
- type: 'action:expose_port:result',
230
- data: {
231
- success: false,
232
- error: message,
233
- output: `${message}${hint}`,
234
- },
235
- meta: event.meta,
236
- };
237
- }
238
- });
239
- actions.on('unexpose_port', async function* (event, context) {
240
- const channelId = context.state.channelId;
241
- stopPreviewForChannel(channelId);
242
- await clearPreviewChannelState(storage, channelId);
243
- if (context.state.channelDetails) {
244
- context.state.channelDetails = await storage.getChannelDetails({ channelId });
245
- }
246
- yield {
247
- type: 'action:unexpose_port:result',
248
- data: {
249
- success: true,
250
- output: 'Preview tunnel stopped and previewUrl cleared from channel state.',
251
- },
252
- meta: event.meta,
253
- };
254
- });
255
- actions.on('delete_channel', async function* (event) {
256
- const channelId = event.data?.channelId;
257
- if (channelId) {
258
- stopPreviewForChannel(channelId);
259
- }
260
- });
261
- };
262
- export const previewPlugin = {
263
- id: 'preview',
264
- name: 'Preview',
265
- description: 'Temporary public preview URLs via Cloudflare quick tunnels.',
266
- toolDefinitions: previewToolDefinitions,
267
- factory: ({ storage }) => previewPluginRuntime(storage),
268
- };
269
- export default previewPlugin;
package/dist/tools/ui.js DELETED
@@ -1,120 +0,0 @@
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
- */
9
- export const uiPlugin = {
10
- id: 'ui',
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
40
- .string()
41
- .optional()
42
- .describe('Status label shown on the item (e.g. "Pending", "Shipped").'),
43
- statusVariant: z
44
- .enum(['default', 'success', 'warning', 'danger', 'info'])
45
- .optional()
46
- .describe('Semantic hint for status badge coloring in the client.'),
47
- metadata: z.record(z.string(), z.any()).optional()
48
- })).optional().describe('Required for kind="list". List of items to display.'),
49
- submitLabel: z.string().optional().describe('Label for the primary action button (e.g. "Submit", "Save").')
50
- })
51
- }
52
- },
53
- factory: () => (builder) => {
54
- // Handle the tool call from the agent
55
- builder.on('action:render_widget', async function* (event, context) {
56
- const widgetEvent = event;
57
- const toolCallId = widgetEvent.meta?.toolCallId;
58
- const threadId = widgetEvent.meta?.threadId || context.state.threadId;
59
- if (!toolCallId)
60
- return;
61
- const widgetId = randomUUID();
62
- // Emit the UI widget event to the client
63
- yield {
64
- type: 'client:ui:widget',
65
- data: {
66
- ...widgetEvent.data,
67
- widgetId,
68
- metadata: {
69
- type: 'ui:request',
70
- originalEvent: widgetEvent
71
- }
72
- },
73
- meta: { agentId: context.state.agentId, threadId }
74
- };
75
- });
76
- // Handle the user's response from the UI widget
77
- builder.on('client:ui:widget:response', async function* (event, context) {
78
- const responseEvent = event;
79
- const { widgetId, actionId, values, metadata } = responseEvent.data;
80
- if (metadata?.type !== 'ui:request')
81
- return;
82
- const originalEvent = metadata.originalEvent;
83
- const toolCallId = originalEvent?.meta?.toolCallId;
84
- const threadId = originalEvent?.meta?.threadId || context.state.threadId;
85
- if (!toolCallId)
86
- return;
87
- // Yield a "submitted" widget update to the UI to collapse/disable it
88
- yield {
89
- type: 'client:ui:widget',
90
- data: {
91
- widgetId,
92
- title: originalEvent.data.title,
93
- kind: originalEvent.data.kind,
94
- state: 'submitted',
95
- body: "Thank you for your response. We will process it and get back to you soon.",
96
- display: 'collapsed',
97
- disabled: true,
98
- actions: [], // Clear actions to disable buttons in UI
99
- },
100
- meta: { agentId: context.state.agentId, threadId },
101
- };
102
- // Emit the tool result event so the agent runtime can resume
103
- yield {
104
- type: 'action:render_widget:result',
105
- data: {
106
- success: true,
107
- actionId,
108
- values,
109
- output: JSON.stringify(values)
110
- },
111
- meta: {
112
- agentId: context.state.agentId,
113
- threadId,
114
- toolCallId
115
- }
116
- };
117
- });
118
- },
119
- };
120
- export default uiPlugin;
@@ -1,6 +0,0 @@
1
- export function buildWorkspaceFileUrl(args) {
2
- const base = args.baseUrl.replace(/\/$/, '');
3
- const data = encodeURIComponent(JSON.stringify({ path: args.filePath }));
4
- const channelId = encodeURIComponent(args.channelId);
5
- return `${base}/api/state?channelId=${channelId}&type=${encodeURIComponent('action:storage:serve-file')}&data=${data}`;
6
- }