openbot 0.4.0 → 0.4.2
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/app/cli.js +1 -1
- package/dist/app/config.js +10 -0
- package/dist/app/server.js +200 -3
- package/dist/harness/index.js +18 -0
- package/dist/plugins/approval/index.js +35 -20
- package/dist/plugins/bash/index.js +195 -0
- package/dist/plugins/delegation/index.js +4 -2
- package/dist/plugins/openbot/context.js +54 -9
- package/dist/plugins/openbot/history.js +47 -1
- package/dist/plugins/openbot/index.js +43 -3
- package/dist/plugins/openbot/runtime.js +91 -27
- package/dist/plugins/openbot/system-prompt.js +21 -1
- package/dist/plugins/plugin-manager/index.js +87 -3
- package/dist/plugins/shell/index.js +2 -1
- package/dist/plugins/storage/files.js +67 -0
- package/dist/plugins/storage/index.js +184 -7
- package/dist/plugins/storage/service.js +201 -44
- package/dist/plugins/ui/index.js +109 -150
- package/dist/services/abort.js +43 -0
- package/dist/services/plugins/registry.js +5 -3
- package/dist/services/plugins/service.js +66 -11
- package/docs/agents.md +5 -8
- package/docs/architecture.md +1 -1
- package/docs/plugins.md +28 -7
- package/docs/templates/AGENT.example.md +4 -4
- package/package.json +1 -1
- package/src/app/cli.ts +1 -1
- package/src/app/config.ts +13 -0
- package/src/app/server.ts +235 -3
- package/src/app/types.ts +284 -14
- package/src/harness/index.ts +21 -0
- package/src/plugins/approval/index.ts +37 -20
- package/src/plugins/bash/index.ts +232 -0
- package/src/plugins/delegation/index.ts +5 -2
- package/src/plugins/openbot/context.ts +58 -9
- package/src/plugins/openbot/history.ts +52 -1
- package/src/plugins/openbot/index.ts +45 -3
- package/src/plugins/openbot/runtime.ts +121 -27
- package/src/plugins/openbot/system-prompt.ts +21 -1
- package/src/plugins/plugin-manager/index.ts +105 -3
- package/src/plugins/storage/files.ts +81 -0
- package/src/plugins/storage/index.ts +198 -8
- package/src/plugins/storage/service.ts +267 -44
- package/src/plugins/ui/index.ts +123 -0
- package/src/services/abort.ts +46 -0
- package/src/services/plugins/domain.ts +34 -1
- package/src/services/plugins/registry.ts +5 -3
- package/src/services/plugins/service.ts +136 -45
- package/src/services/plugins/types.ts +5 -1
- package/src/plugins/shell/index.ts +0 -123
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { resolvePath } from '../../app/config.js';
|
|
5
|
+
const MIME_BY_EXT = {
|
|
6
|
+
'.png': 'image/png',
|
|
7
|
+
'.jpg': 'image/jpeg',
|
|
8
|
+
'.jpeg': 'image/jpeg',
|
|
9
|
+
'.gif': 'image/gif',
|
|
10
|
+
'.webp': 'image/webp',
|
|
11
|
+
'.svg': 'image/svg+xml',
|
|
12
|
+
'.ico': 'image/x-icon',
|
|
13
|
+
'.mp4': 'video/mp4',
|
|
14
|
+
'.webm': 'video/webm',
|
|
15
|
+
'.mov': 'video/quicktime',
|
|
16
|
+
'.mp3': 'audio/mpeg',
|
|
17
|
+
'.wav': 'audio/wav',
|
|
18
|
+
'.ogg': 'audio/ogg',
|
|
19
|
+
'.pdf': 'application/pdf',
|
|
20
|
+
'.json': 'application/json',
|
|
21
|
+
'.txt': 'text/plain',
|
|
22
|
+
'.html': 'text/html',
|
|
23
|
+
'.css': 'text/css',
|
|
24
|
+
'.js': 'text/javascript',
|
|
25
|
+
'.zip': 'application/zip',
|
|
26
|
+
};
|
|
27
|
+
export function guessMimeType(filePath) {
|
|
28
|
+
return MIME_BY_EXT[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';
|
|
29
|
+
}
|
|
30
|
+
/** Resolve a relative path under a channel cwd; rejects directory escape. */
|
|
31
|
+
export function resolveChannelFile(baseCwd, relativePath) {
|
|
32
|
+
const resolvedBase = resolvePath(baseCwd);
|
|
33
|
+
const normalized = relativePath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
34
|
+
const target = path.resolve(resolvedBase, normalized);
|
|
35
|
+
if (target !== resolvedBase && !target.startsWith(resolvedBase + path.sep)) {
|
|
36
|
+
throw new Error('Access denied: directory escape');
|
|
37
|
+
}
|
|
38
|
+
return target;
|
|
39
|
+
}
|
|
40
|
+
export async function statChannelFile(baseCwd, relativePath) {
|
|
41
|
+
const abs = resolveChannelFile(baseCwd, relativePath);
|
|
42
|
+
const stat = await fs.stat(abs);
|
|
43
|
+
if (!stat.isFile()) {
|
|
44
|
+
throw new Error('Not a file');
|
|
45
|
+
}
|
|
46
|
+
return { abs, size: stat.size };
|
|
47
|
+
}
|
|
48
|
+
export function openChannelFileStream(abs) {
|
|
49
|
+
return createReadStream(abs);
|
|
50
|
+
}
|
|
51
|
+
export function buildWorkspaceFileUrl(args) {
|
|
52
|
+
const base = args.baseUrl.replace(/\/$/, '');
|
|
53
|
+
const data = encodeURIComponent(JSON.stringify({ path: args.filePath }));
|
|
54
|
+
const channelId = encodeURIComponent(args.channelId);
|
|
55
|
+
return `${base}/api/state?channelId=${channelId}&type=${encodeURIComponent('action:storage:serve-file')}&data=${data}`;
|
|
56
|
+
}
|
|
57
|
+
export function getPublicBaseUrl(port, configPublicUrl) {
|
|
58
|
+
const fromConfig = configPublicUrl?.trim();
|
|
59
|
+
if (fromConfig) {
|
|
60
|
+
return fromConfig.replace(/\/$/, '');
|
|
61
|
+
}
|
|
62
|
+
const fromEnv = process.env.OPENBOT_PUBLIC_URL?.trim();
|
|
63
|
+
if (fromEnv) {
|
|
64
|
+
return fromEnv.replace(/\/$/, '');
|
|
65
|
+
}
|
|
66
|
+
return `http://localhost:${port}`;
|
|
67
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
|
+
import { buildWorkspaceFileUrl } from './files.js';
|
|
2
3
|
/**
|
|
3
4
|
* `storage` — exposes channel/thread/variable mutation tools and provides
|
|
4
5
|
* platform-level storage handlers.
|
|
@@ -21,7 +22,7 @@ const storageToolDefinitions = {
|
|
|
21
22
|
cwd: z
|
|
22
23
|
.string()
|
|
23
24
|
.optional()
|
|
24
|
-
.describe('Optional initial current working directory for the channel.'),
|
|
25
|
+
.describe('Optional initial current working directory for the channel. Defaults to an absolute path under ~/openbot/{channelId}.'),
|
|
25
26
|
}),
|
|
26
27
|
},
|
|
27
28
|
patch_channel_details: {
|
|
@@ -37,8 +38,15 @@ const storageToolDefinitions = {
|
|
|
37
38
|
.optional()
|
|
38
39
|
.describe('Markdown content for the channel specification (SPEC.md). Use for goals and rules.'),
|
|
39
40
|
cwd: z.string().optional().describe('Current working directory for the channel.'),
|
|
41
|
+
participants: z
|
|
42
|
+
.array(z.string())
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('List of agent IDs that are participants in this channel. When a user tags an agent (e.g. @agent-id), you should ensure they are added to this list if they are not already there.'),
|
|
40
45
|
})
|
|
41
|
-
.refine((value) => value.state !== undefined ||
|
|
46
|
+
.refine((value) => value.state !== undefined ||
|
|
47
|
+
value.spec !== undefined ||
|
|
48
|
+
value.cwd !== undefined ||
|
|
49
|
+
value.participants !== undefined, { message: 'Provide at least one of state, spec, cwd, or participants.' }),
|
|
42
50
|
},
|
|
43
51
|
patch_thread_details: {
|
|
44
52
|
description: 'Patch current thread details (state).',
|
|
@@ -62,13 +70,28 @@ const storageToolDefinitions = {
|
|
|
62
70
|
key: z.string().describe('The key of the variable to delete.'),
|
|
63
71
|
}),
|
|
64
72
|
},
|
|
73
|
+
delete_channel: {
|
|
74
|
+
description: 'Permanently delete a channel and all its threads and events. Always confirm with the user before deleting.',
|
|
75
|
+
inputSchema: z.object({
|
|
76
|
+
channelId: z.string().describe('The channel ID to delete.'),
|
|
77
|
+
}),
|
|
78
|
+
},
|
|
79
|
+
get_workspace_file_url: {
|
|
80
|
+
description: 'Get a fetchable HTTP URL for a file in the current channel workspace (images, video, audio, documents).',
|
|
81
|
+
inputSchema: z.object({
|
|
82
|
+
path: z
|
|
83
|
+
.string()
|
|
84
|
+
.describe('Path relative to the channel working directory, e.g. "uploads/clip.mp4".'),
|
|
85
|
+
}),
|
|
86
|
+
},
|
|
65
87
|
};
|
|
66
88
|
export const storagePlugin = {
|
|
67
89
|
id: 'storage',
|
|
68
90
|
name: 'Storage',
|
|
69
91
|
description: 'Tools for creating channels, patching state, and managing workspace variables.',
|
|
70
92
|
toolDefinitions: storageToolDefinitions,
|
|
71
|
-
factory: ({ storage }) => (builder) => {
|
|
93
|
+
factory: ({ storage, publicBaseUrl }) => (builder) => {
|
|
94
|
+
const resolvePublicBaseUrl = () => publicBaseUrl;
|
|
72
95
|
builder.on('action:create_thread', async function* (event, context) {
|
|
73
96
|
const threadId = event.meta?.threadId;
|
|
74
97
|
const channelId = context.state.channelId;
|
|
@@ -151,6 +174,42 @@ export const storagePlugin = {
|
|
|
151
174
|
};
|
|
152
175
|
}
|
|
153
176
|
});
|
|
177
|
+
builder.on('action:delete_channel', async function* (event, context) {
|
|
178
|
+
const rawChannelId = (event.data?.channelId || '').trim();
|
|
179
|
+
const resultMeta = { ...(event.meta || {}), agentId: context.state.agentId };
|
|
180
|
+
if (!rawChannelId) {
|
|
181
|
+
yield {
|
|
182
|
+
type: 'action:delete_channel:result',
|
|
183
|
+
data: { success: false, channelId: '', error: 'channelId is required' },
|
|
184
|
+
meta: resultMeta,
|
|
185
|
+
};
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
await storage.deleteChannel({ channelId: rawChannelId });
|
|
190
|
+
yield {
|
|
191
|
+
type: 'action:delete_channel:result',
|
|
192
|
+
data: { success: true, channelId: rawChannelId },
|
|
193
|
+
meta: resultMeta,
|
|
194
|
+
};
|
|
195
|
+
yield {
|
|
196
|
+
type: 'agent:output',
|
|
197
|
+
data: { content: `Deleted channel \`${rawChannelId}\`.` },
|
|
198
|
+
meta: resultMeta,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
yield {
|
|
203
|
+
type: 'action:delete_channel:result',
|
|
204
|
+
data: {
|
|
205
|
+
success: false,
|
|
206
|
+
channelId: rawChannelId,
|
|
207
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
208
|
+
},
|
|
209
|
+
meta: resultMeta,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
});
|
|
154
213
|
builder.on('action:update_channel', async function* (event, context) {
|
|
155
214
|
const data = (event.data || {});
|
|
156
215
|
const targetChannelId = (data.channelId || context.state.channelId || '').trim();
|
|
@@ -256,7 +315,8 @@ export const storagePlugin = {
|
|
|
256
315
|
widgetId: "patch-channel-details-result" + Date.now(),
|
|
257
316
|
kind: "message",
|
|
258
317
|
title: "Channel details updated.",
|
|
259
|
-
body:
|
|
318
|
+
body: `The channel details have been updated. ${updatedFields.join(', ')}`,
|
|
319
|
+
display: "collapsed",
|
|
260
320
|
},
|
|
261
321
|
meta: resultMeta,
|
|
262
322
|
};
|
|
@@ -340,6 +400,26 @@ export const storagePlugin = {
|
|
|
340
400
|
const threads = await storage.getThreads({ channelId: event.data.channelId });
|
|
341
401
|
yield { type: 'action:storage:get-threads-result', data: { threads } };
|
|
342
402
|
});
|
|
403
|
+
builder.on('action:storage:set-last-read', async function* (event, context) {
|
|
404
|
+
const { channelId, threadId, lastReadEventId } = event.data;
|
|
405
|
+
try {
|
|
406
|
+
await storage.setLastRead({
|
|
407
|
+
channelId: channelId || context.state.channelId,
|
|
408
|
+
threadId: threadId || context.state.threadId,
|
|
409
|
+
lastReadEventId,
|
|
410
|
+
});
|
|
411
|
+
yield { type: 'action:storage:set-last-read-result', data: { success: true } };
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
yield {
|
|
415
|
+
type: 'action:storage:set-last-read-result',
|
|
416
|
+
data: {
|
|
417
|
+
success: false,
|
|
418
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
});
|
|
343
423
|
builder.on('action:storage:get-channel-details', async function* (_, state) {
|
|
344
424
|
const channelDetails = await storage.getChannelDetails({
|
|
345
425
|
channelId: state.state.channelId,
|
|
@@ -425,6 +505,21 @@ export const storagePlugin = {
|
|
|
425
505
|
};
|
|
426
506
|
}
|
|
427
507
|
});
|
|
508
|
+
builder.on('action:storage:delete-channel', async function* (event) {
|
|
509
|
+
try {
|
|
510
|
+
await storage.deleteChannel({ channelId: event.data.channelId });
|
|
511
|
+
yield { type: 'action:storage:delete-channel-result', data: { success: true } };
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
yield {
|
|
515
|
+
type: 'action:storage:delete-channel-result',
|
|
516
|
+
data: {
|
|
517
|
+
success: false,
|
|
518
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
});
|
|
428
523
|
builder.on('action:storage:delete-agent', async function* (event) {
|
|
429
524
|
try {
|
|
430
525
|
await storage.deleteAgent({ agentId: event.data.agentId });
|
|
@@ -542,7 +637,9 @@ export const storagePlugin = {
|
|
|
542
637
|
});
|
|
543
638
|
builder.on('action:storage:read-file', async function* (event, context) {
|
|
544
639
|
const channelId = context.state.channelId;
|
|
545
|
-
const
|
|
640
|
+
const data = event.data;
|
|
641
|
+
const filePath = data?.path;
|
|
642
|
+
const encoding = data?.encoding ?? 'utf8';
|
|
546
643
|
if (!filePath) {
|
|
547
644
|
yield {
|
|
548
645
|
type: 'action:storage:read-file:result',
|
|
@@ -551,10 +648,22 @@ export const storagePlugin = {
|
|
|
551
648
|
return;
|
|
552
649
|
}
|
|
553
650
|
try {
|
|
554
|
-
|
|
651
|
+
if (encoding === 'utf8') {
|
|
652
|
+
const content = await storage.readFile({ channelId, path: filePath });
|
|
653
|
+
yield {
|
|
654
|
+
type: 'action:storage:read-file:result',
|
|
655
|
+
data: { success: true, content, path: filePath, encoding },
|
|
656
|
+
};
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
const { content, mimeType, size } = await storage.readChannelFile({
|
|
660
|
+
channelId,
|
|
661
|
+
path: filePath,
|
|
662
|
+
encoding,
|
|
663
|
+
});
|
|
555
664
|
yield {
|
|
556
665
|
type: 'action:storage:read-file:result',
|
|
557
|
-
data: { success: true, content, path: filePath },
|
|
666
|
+
data: { success: true, content, path: filePath, encoding, mimeType, size },
|
|
558
667
|
};
|
|
559
668
|
}
|
|
560
669
|
catch (error) {
|
|
@@ -568,6 +677,74 @@ export const storagePlugin = {
|
|
|
568
677
|
};
|
|
569
678
|
}
|
|
570
679
|
});
|
|
680
|
+
builder.on('action:storage:get-file-url', async function* (event, context) {
|
|
681
|
+
const channelId = context.state.channelId;
|
|
682
|
+
const filePath = event.data?.path;
|
|
683
|
+
if (!filePath) {
|
|
684
|
+
yield {
|
|
685
|
+
type: 'action:storage:get-file-url:result',
|
|
686
|
+
data: { success: false, path: '', error: 'Path is required' },
|
|
687
|
+
};
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const { size, mimeType } = await storage.getChannelFileStat({ channelId, path: filePath });
|
|
692
|
+
const url = buildWorkspaceFileUrl({
|
|
693
|
+
baseUrl: resolvePublicBaseUrl(),
|
|
694
|
+
channelId,
|
|
695
|
+
filePath,
|
|
696
|
+
});
|
|
697
|
+
yield {
|
|
698
|
+
type: 'action:storage:get-file-url:result',
|
|
699
|
+
data: { success: true, path: filePath, url, mimeType, size },
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
yield {
|
|
704
|
+
type: 'action:storage:get-file-url:result',
|
|
705
|
+
data: {
|
|
706
|
+
success: false,
|
|
707
|
+
path: filePath,
|
|
708
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
builder.on('action:get_workspace_file_url', async function* (event, context) {
|
|
714
|
+
const channelId = context.state.channelId;
|
|
715
|
+
const filePath = event.data?.path;
|
|
716
|
+
const toolCallId = event.meta?.toolCallId;
|
|
717
|
+
if (!filePath) {
|
|
718
|
+
yield {
|
|
719
|
+
type: 'action:get_workspace_file_url:result',
|
|
720
|
+
data: { success: false, path: '', error: 'Path is required', output: 'Path is required' },
|
|
721
|
+
meta: { ...(event.meta || {}), toolCallId },
|
|
722
|
+
};
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
const { size, mimeType } = await storage.getChannelFileStat({ channelId, path: filePath });
|
|
727
|
+
const url = buildWorkspaceFileUrl({
|
|
728
|
+
baseUrl: resolvePublicBaseUrl(),
|
|
729
|
+
channelId,
|
|
730
|
+
filePath,
|
|
731
|
+
});
|
|
732
|
+
const output = JSON.stringify({ path: filePath, url, mimeType, size });
|
|
733
|
+
yield {
|
|
734
|
+
type: 'action:get_workspace_file_url:result',
|
|
735
|
+
data: { success: true, path: filePath, url, mimeType, size, output },
|
|
736
|
+
meta: { ...(event.meta || {}), toolCallId },
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
741
|
+
yield {
|
|
742
|
+
type: 'action:get_workspace_file_url:result',
|
|
743
|
+
data: { success: false, path: filePath, error: message, output: message },
|
|
744
|
+
meta: { ...(event.meta || {}), toolCallId },
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
});
|
|
571
748
|
},
|
|
572
749
|
};
|
|
573
750
|
export default storagePlugin;
|