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.
- 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 +6 -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 +215 -59
- 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 +7 -7
- 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 +7 -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 +282 -59
- 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,46 @@
|
|
|
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
|
+
|
|
9
|
+
export const abortKey = (channelId: string, threadId?: string): string =>
|
|
10
|
+
`${channelId}:${threadId || ''}`;
|
|
11
|
+
|
|
12
|
+
class AbortRegistry {
|
|
13
|
+
private entries = new Map<string, { controller: AbortController; refs: number }>();
|
|
14
|
+
|
|
15
|
+
/** Register interest in a run. Returns a shared signal for the key. */
|
|
16
|
+
acquire(key: string): AbortSignal {
|
|
17
|
+
let entry = this.entries.get(key);
|
|
18
|
+
if (!entry) {
|
|
19
|
+
entry = { controller: new AbortController(), refs: 0 };
|
|
20
|
+
this.entries.set(key, entry);
|
|
21
|
+
}
|
|
22
|
+
entry.refs += 1;
|
|
23
|
+
return entry.controller.signal;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Release interest. Removes the entry once no runs reference it. */
|
|
27
|
+
release(key: string): void {
|
|
28
|
+
const entry = this.entries.get(key);
|
|
29
|
+
if (!entry) return;
|
|
30
|
+
entry.refs -= 1;
|
|
31
|
+
if (entry.refs <= 0) {
|
|
32
|
+
this.entries.delete(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Abort all runs for the key. Returns true if something was active. */
|
|
37
|
+
abort(key: string): boolean {
|
|
38
|
+
const entry = this.entries.get(key);
|
|
39
|
+
if (!entry) return false;
|
|
40
|
+
entry.controller.abort();
|
|
41
|
+
this.entries.delete(key);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const abortRegistry = new AbortRegistry();
|
|
@@ -45,13 +45,15 @@ export type ConfigSchema = {
|
|
|
45
45
|
type: 'object';
|
|
46
46
|
properties: {
|
|
47
47
|
[key: string]: {
|
|
48
|
-
type: 'string' | 'number' | 'boolean' | 'integer';
|
|
48
|
+
type: 'string' | 'number' | 'boolean' | 'integer' | 'object' | 'array';
|
|
49
49
|
description?: string;
|
|
50
50
|
default?: unknown;
|
|
51
51
|
enum?: unknown[];
|
|
52
52
|
minimum?: number;
|
|
53
53
|
maximum?: number;
|
|
54
54
|
format?: 'password' | 'url' | 'email';
|
|
55
|
+
properties?: ConfigSchema['properties'];
|
|
56
|
+
items?: ConfigSchema['properties'][string];
|
|
55
57
|
};
|
|
56
58
|
};
|
|
57
59
|
required?: string[];
|
|
@@ -76,6 +78,7 @@ export type Thread = {
|
|
|
76
78
|
channelId: string;
|
|
77
79
|
createdAt: Date;
|
|
78
80
|
updatedAt: Date;
|
|
81
|
+
hasUnseenMessages?: boolean;
|
|
79
82
|
};
|
|
80
83
|
|
|
81
84
|
export type ThreadDetails = {
|
|
@@ -104,6 +107,8 @@ export interface Storage {
|
|
|
104
107
|
initialState?: Record<string, unknown>;
|
|
105
108
|
cwd?: string;
|
|
106
109
|
}) => Promise<void>;
|
|
110
|
+
/** Removes the channel directory and cleans up `_meta/last-read.json`. */
|
|
111
|
+
deleteChannel: (args: { channelId: string }) => Promise<void>;
|
|
107
112
|
createThread: (args: {
|
|
108
113
|
channelId: string;
|
|
109
114
|
threadId: string;
|
|
@@ -112,6 +117,7 @@ export interface Storage {
|
|
|
112
117
|
}) => Promise<void>;
|
|
113
118
|
getThreads: (args: { channelId: string }) => Promise<Thread[]>;
|
|
114
119
|
getThreadDetails: (args: { channelId: string; threadId: string }) => Promise<ThreadDetails>;
|
|
120
|
+
setLastRead: (args: { channelId: string; threadId?: string; lastReadEventId: string }) => Promise<void>;
|
|
115
121
|
/** User-facing agent list; excludes agents with `hidden: true` (e.g. built-in `state`). */
|
|
116
122
|
getAgents: () => Promise<Agent[]>;
|
|
117
123
|
getPlugins: () => Promise<PluginDescriptor[]>;
|
|
@@ -142,6 +148,11 @@ export interface Storage {
|
|
|
142
148
|
/** For `system` / `state`, removes only `AGENT.md` (reverts to code defaults). */
|
|
143
149
|
deleteAgent: (args: { agentId: string }) => Promise<void>;
|
|
144
150
|
getEvents: (args: { channelId: string; threadId?: string }) => Promise<OpenBotEvent[]>;
|
|
151
|
+
storeEvent: (args: {
|
|
152
|
+
channelId: string;
|
|
153
|
+
threadId?: string;
|
|
154
|
+
event: OpenBotEvent;
|
|
155
|
+
}) => Promise<void>;
|
|
145
156
|
getChannelDetails: (args: { channelId: string }) => Promise<ChannelDetails>;
|
|
146
157
|
patchChannelState: (args: { channelId: string; state: unknown }) => Promise<void>;
|
|
147
158
|
patchThreadState: (args: {
|
|
@@ -158,6 +169,28 @@ export interface Storage {
|
|
|
158
169
|
path?: string;
|
|
159
170
|
}) => Promise<Array<{ name: string; isDirectory: boolean }>>;
|
|
160
171
|
readFile: (args: { channelId: string; path: string }) => Promise<string>;
|
|
172
|
+
readChannelFile: (args: {
|
|
173
|
+
channelId: string;
|
|
174
|
+
path: string;
|
|
175
|
+
encoding?: 'utf8' | 'base64';
|
|
176
|
+
}) => Promise<{ content: string; mimeType: string; size: number }>;
|
|
177
|
+
writeChannelFile: (args: {
|
|
178
|
+
channelId: string;
|
|
179
|
+
path: string;
|
|
180
|
+
content: string;
|
|
181
|
+
encoding?: 'utf8' | 'base64';
|
|
182
|
+
overwrite?: boolean;
|
|
183
|
+
}) => Promise<{ path: string; size: number; mimeType: string }>;
|
|
184
|
+
uploadChannelFile: (args: {
|
|
185
|
+
channelId: string;
|
|
186
|
+
path: string;
|
|
187
|
+
body: Buffer;
|
|
188
|
+
overwrite?: boolean;
|
|
189
|
+
}) => Promise<{ path: string; size: number; mimeType: string }>;
|
|
190
|
+
getChannelFileStat: (args: {
|
|
191
|
+
channelId: string;
|
|
192
|
+
path: string;
|
|
193
|
+
}) => Promise<{ abs: string; size: number; mimeType: string }>;
|
|
161
194
|
/** Persist a memory record into the global memory log. */
|
|
162
195
|
appendMemory: (args: {
|
|
163
196
|
scope: string;
|
|
@@ -3,11 +3,12 @@ import path from 'node:path';
|
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import type { Plugin } from './types.js';
|
|
5
5
|
import { openbotPlugin } from '../../plugins/openbot/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { bashPlugin } from '../../plugins/bash/index.js';
|
|
7
7
|
import { storagePlugin } from '../../plugins/storage/index.js';
|
|
8
8
|
import { approvalPlugin } from '../../plugins/approval/index.js';
|
|
9
9
|
import { memoryPlugin } from '../../plugins/memory/index.js';
|
|
10
10
|
import { delegationPlugin } from '../../plugins/delegation/index.js';
|
|
11
|
+
import { uiPlugin } from '../../plugins/ui/index.js';
|
|
11
12
|
import { pluginManagerPlugin } from '../../plugins/plugin-manager/index.js';
|
|
12
13
|
import { DEFAULT_PLUGINS_DIR, DEFAULT_BASE_DIR, loadConfig, resolvePath } from '../../app/config.js';
|
|
13
14
|
import {
|
|
@@ -20,11 +21,12 @@ let pluginsDir: string | null = null;
|
|
|
20
21
|
|
|
21
22
|
const BUILT_IN: Record<string, Plugin> = {
|
|
22
23
|
[openbotPlugin.id]: openbotPlugin,
|
|
23
|
-
[
|
|
24
|
+
[bashPlugin.id]: bashPlugin,
|
|
24
25
|
[storagePlugin.id]: storagePlugin,
|
|
25
26
|
[approvalPlugin.id]: approvalPlugin,
|
|
26
27
|
[memoryPlugin.id]: memoryPlugin,
|
|
27
28
|
[delegationPlugin.id]: delegationPlugin,
|
|
29
|
+
[uiPlugin.id]: uiPlugin,
|
|
28
30
|
[pluginManagerPlugin.id]: pluginManagerPlugin,
|
|
29
31
|
};
|
|
30
32
|
|
|
@@ -77,7 +79,7 @@ export function initPlugins(dir?: string) {
|
|
|
77
79
|
|
|
78
80
|
/**
|
|
79
81
|
* Resolve a Plugin by id. The id is either:
|
|
80
|
-
* - a built-in id (e.g. "openbot", "
|
|
82
|
+
* - a built-in id (e.g. "openbot", "bash"), or
|
|
81
83
|
* - an npm package name (e.g. "openbot-plugin-foo" or "@scope/foo"),
|
|
82
84
|
* in which case the folder layout is `plugins/<id>/dist/index.js`.
|
|
83
85
|
*/
|
|
@@ -26,7 +26,7 @@ export interface InstalledPlugin {
|
|
|
26
26
|
version: string;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** One marketplace entry; matches `action:marketplace:list:result`
|
|
29
|
+
/** One marketplace entry; matches `action:marketplace:list:result` shape. */
|
|
30
30
|
export type MarketplaceAgentListing = {
|
|
31
31
|
id: string;
|
|
32
32
|
name: string;
|
|
@@ -36,7 +36,32 @@ export type MarketplaceAgentListing = {
|
|
|
36
36
|
plugins: PluginRef[];
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
export type StarterPrompt = {
|
|
40
|
+
label: string;
|
|
41
|
+
prompt: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** One channel entry from the marketplace. */
|
|
45
|
+
export type MarketplaceChannelListing = {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
description: string;
|
|
49
|
+
image?: string;
|
|
50
|
+
spec?: string;
|
|
51
|
+
initialState?: Record<string, unknown>;
|
|
52
|
+
/** List of agent IDs that should be participants in the channel. */
|
|
53
|
+
participants: string[];
|
|
54
|
+
/** Starter prompts for the channel. */
|
|
55
|
+
starterPrompts?: StarterPrompt[];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export interface MarketplaceRegistry {
|
|
59
|
+
agents: MarketplaceAgentListing[];
|
|
60
|
+
channels: MarketplaceChannelListing[];
|
|
61
|
+
}
|
|
62
|
+
|
|
39
63
|
const DEFAULT_MARKETPLACE_AGENTS: MarketplaceAgentListing[] = [];
|
|
64
|
+
const DEFAULT_MARKETPLACE_CHANNELS: MarketplaceChannelListing[] = [];
|
|
40
65
|
|
|
41
66
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
42
67
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
@@ -44,51 +69,108 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
44
69
|
|
|
45
70
|
/**
|
|
46
71
|
* Parses JSON from a remote registry file. Supports either
|
|
47
|
-
* `{ "agents": [ ... ] }` or a top-level array.
|
|
72
|
+
* `{ "agents": [ ... ], "channels": [ ... ] }` or a top-level array (legacy agents-only).
|
|
48
73
|
*/
|
|
49
|
-
export function parseMarketplaceRegistryJson(data: unknown):
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (typeof instructions !== 'string') {
|
|
68
|
-
throw new Error(`agents[${i}].instructions must be a string`);
|
|
69
|
-
}
|
|
70
|
-
if (!Array.isArray(pluginsRaw)) throw new Error(`agents[${i}].plugins must be an array`);
|
|
71
|
-
const plugins: PluginRef[] = pluginsRaw.map((p, j) => {
|
|
72
|
-
if (!isRecord(p) || typeof p.id !== 'string' || !p.id) {
|
|
73
|
-
throw new Error(`agents[${i}].plugins[${j}]: expected { "id": string, "config"?: object }`);
|
|
74
|
+
export function parseMarketplaceRegistryJson(data: unknown): MarketplaceRegistry {
|
|
75
|
+
const isLegacyArray = Array.isArray(data);
|
|
76
|
+
const rawAgents: unknown = isLegacyArray
|
|
77
|
+
? data
|
|
78
|
+
: isRecord(data) && Array.isArray(data.agents)
|
|
79
|
+
? data.agents
|
|
80
|
+
: [];
|
|
81
|
+
const rawChannels: unknown =
|
|
82
|
+
!isLegacyArray && isRecord(data) && Array.isArray(data.channels)
|
|
83
|
+
? data.channels
|
|
84
|
+
: isRecord(data) && Array.isArray((data as any).templates)
|
|
85
|
+
? (data as any).templates
|
|
86
|
+
: [];
|
|
87
|
+
|
|
88
|
+
const agents: MarketplaceAgentListing[] = (Array.isArray(rawAgents) ? rawAgents : []).map(
|
|
89
|
+
(item, i) => {
|
|
90
|
+
if (!isRecord(item)) {
|
|
91
|
+
throw new Error(`agents[${i}]: expected object`);
|
|
74
92
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
const id = item.id;
|
|
94
|
+
const name = item.name;
|
|
95
|
+
const description = item.description;
|
|
96
|
+
const instructions = item.instructions;
|
|
97
|
+
const pluginsRaw = item.plugins;
|
|
98
|
+
if (typeof id !== 'string' || !id)
|
|
99
|
+
throw new Error(`agents[${i}].id must be a non-empty string`);
|
|
100
|
+
if (typeof name !== 'string') throw new Error(`agents[${i}].name must be a string`);
|
|
101
|
+
if (typeof description !== 'string')
|
|
102
|
+
throw new Error(`agents[${i}].description must be a string`);
|
|
103
|
+
if (typeof instructions !== 'string') {
|
|
104
|
+
throw new Error(`agents[${i}].instructions must be a string`);
|
|
79
105
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
if (!Array.isArray(pluginsRaw)) throw new Error(`agents[${i}].plugins must be an array`);
|
|
107
|
+
const plugins: PluginRef[] = pluginsRaw.map((p, j) => {
|
|
108
|
+
if (!isRecord(p) || typeof p.id !== 'string' || !p.id) {
|
|
109
|
+
throw new Error(`agents[${i}].plugins[${j}]: expected { "id": string, "config"?: object }`);
|
|
110
|
+
}
|
|
111
|
+
const ref: PluginRef = { id: p.id };
|
|
112
|
+
if (p.config !== undefined) {
|
|
113
|
+
if (!isRecord(p.config))
|
|
114
|
+
throw new Error(`agents[${i}].plugins[${j}].config must be an object`);
|
|
115
|
+
ref.config = p.config;
|
|
116
|
+
}
|
|
117
|
+
return ref;
|
|
118
|
+
});
|
|
119
|
+
const listing: MarketplaceAgentListing = { id, name, description, instructions, plugins };
|
|
120
|
+
if (item.image !== undefined) {
|
|
121
|
+
if (typeof item.image !== 'string') throw new Error(`agents[${i}].image must be a string`);
|
|
122
|
+
listing.image = item.image;
|
|
123
|
+
}
|
|
124
|
+
return listing;
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const channels: MarketplaceChannelListing[] = (Array.isArray(rawChannels) ? rawChannels : []).map(
|
|
129
|
+
(item, i) => {
|
|
130
|
+
if (!isRecord(item)) {
|
|
131
|
+
throw new Error(`channels[${i}]: expected object`);
|
|
132
|
+
}
|
|
133
|
+
const id = item.id;
|
|
134
|
+
const name = item.name;
|
|
135
|
+
const description = item.description;
|
|
136
|
+
const participants = item.participants;
|
|
137
|
+
|
|
138
|
+
if (typeof id !== 'string' || !id)
|
|
139
|
+
throw new Error(`channels[${i}].id must be a non-empty string`);
|
|
140
|
+
if (typeof name !== 'string') throw new Error(`channels[${i}].name must be a string`);
|
|
141
|
+
if (typeof description !== 'string')
|
|
142
|
+
throw new Error(`channels[${i}].description must be a string`);
|
|
143
|
+
if (!Array.isArray(participants))
|
|
144
|
+
throw new Error(`channels[${i}].participants must be an array`);
|
|
145
|
+
|
|
146
|
+
const listing: MarketplaceChannelListing = {
|
|
147
|
+
id,
|
|
148
|
+
name,
|
|
149
|
+
description,
|
|
150
|
+
participants: participants.filter((p): p is string => typeof p === 'string'),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (typeof item.image === 'string') listing.image = item.image;
|
|
154
|
+
if (typeof item.spec === 'string') listing.spec = item.spec;
|
|
155
|
+
if (isRecord(item.initialState)) listing.initialState = item.initialState;
|
|
156
|
+
|
|
157
|
+
if (Array.isArray(item.starterPrompts)) {
|
|
158
|
+
listing.starterPrompts = item.starterPrompts.map((p: any, j: number) => {
|
|
159
|
+
if (!isRecord(p) || typeof p.label !== 'string' || typeof p.prompt !== 'string') {
|
|
160
|
+
throw new Error(`channels[${i}].starterPrompts[${j}] must have label and prompt`);
|
|
161
|
+
}
|
|
162
|
+
return { label: p.label, prompt: p.prompt };
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return listing;
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return { agents, channels };
|
|
89
171
|
}
|
|
90
172
|
|
|
91
|
-
async function
|
|
173
|
+
async function fetchMarketplaceRegistryFromUrl(url: string): Promise<MarketplaceRegistry> {
|
|
92
174
|
const res = await fetch(url, {
|
|
93
175
|
headers: { Accept: 'application/json' },
|
|
94
176
|
signal: AbortSignal.timeout(15_000),
|
|
@@ -101,22 +183,31 @@ async function fetchMarketplaceAgentsFromUrl(url: string): Promise<MarketplaceAg
|
|
|
101
183
|
}
|
|
102
184
|
|
|
103
185
|
/**
|
|
104
|
-
* Resolves marketplace
|
|
186
|
+
* Resolves marketplace registry (agents and channels) from configured registry URL.
|
|
105
187
|
*/
|
|
106
|
-
export async function
|
|
188
|
+
export async function resolveMarketplaceRegistry(): Promise<MarketplaceRegistry> {
|
|
107
189
|
const { marketplaceRegistryUrl } = loadConfig();
|
|
108
190
|
const registryUrl = marketplaceRegistryUrl?.trim() || DEFAULT_MARKETPLACE_REGISTRY_URL;
|
|
109
191
|
try {
|
|
110
|
-
return await
|
|
192
|
+
return await fetchMarketplaceRegistryFromUrl(registryUrl);
|
|
111
193
|
} catch (err) {
|
|
112
194
|
console.warn(
|
|
113
195
|
`[plugins] marketplace registry fetch failed (${registryUrl}), using built-in list:`,
|
|
114
196
|
err instanceof Error ? err.message : err,
|
|
115
197
|
);
|
|
116
|
-
return DEFAULT_MARKETPLACE_AGENTS;
|
|
198
|
+
return { agents: DEFAULT_MARKETPLACE_AGENTS, channels: DEFAULT_MARKETPLACE_CHANNELS };
|
|
117
199
|
}
|
|
118
200
|
}
|
|
119
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Resolves marketplace agent listings from configured registry URL.
|
|
204
|
+
* @deprecated Use resolveMarketplaceRegistry instead.
|
|
205
|
+
*/
|
|
206
|
+
export async function resolveMarketplaceAgentList(): Promise<MarketplaceAgentListing[]> {
|
|
207
|
+
const registry = await resolveMarketplaceRegistry();
|
|
208
|
+
return registry.agents;
|
|
209
|
+
}
|
|
210
|
+
|
|
120
211
|
const getPluginsDir = (): string => {
|
|
121
212
|
const config = loadConfig();
|
|
122
213
|
const baseDir = resolvePath(config.baseDir || DEFAULT_BASE_DIR);
|
|
@@ -5,7 +5,7 @@ import { AgentDetails, ConfigSchema, Storage } from './domain.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* Reference to a plugin from an agent's AGENT.md frontmatter.
|
|
7
7
|
*
|
|
8
|
-
* The `id` is either a built-in plugin id (e.g. `openbot`, `
|
|
8
|
+
* The `id` is either a built-in plugin id (e.g. `openbot`, `bash`) or an npm
|
|
9
9
|
* package name (e.g. `openbot-plugin-codex`, `@scope/openbot-plugin-foo`).
|
|
10
10
|
* Each entry may carry plugin-specific `config`.
|
|
11
11
|
*/
|
|
@@ -33,6 +33,10 @@ export interface PluginContext {
|
|
|
33
33
|
config: Record<string, unknown>;
|
|
34
34
|
storage: Storage;
|
|
35
35
|
tools: Record<string, ToolDefinition>;
|
|
36
|
+
/** Resolved public base URL for the server (e.g. https://my-host.example or http://localhost:4132). */
|
|
37
|
+
publicBaseUrl: string;
|
|
38
|
+
/** Signal that fires when this run is stopped; runtimes should pass it to long-running calls. */
|
|
39
|
+
abortSignal?: AbortSignal;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/**
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { MelonyPlugin } from 'melony';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
import type { Plugin } from '../../services/plugins/types.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;
|