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.
Files changed (50) hide show
  1. package/dist/app/cli.js +1 -1
  2. package/dist/app/config.js +10 -0
  3. package/dist/app/server.js +200 -3
  4. package/dist/harness/index.js +18 -0
  5. package/dist/plugins/approval/index.js +35 -20
  6. package/dist/plugins/bash/index.js +195 -0
  7. package/dist/plugins/delegation/index.js +6 -2
  8. package/dist/plugins/openbot/context.js +54 -9
  9. package/dist/plugins/openbot/history.js +47 -1
  10. package/dist/plugins/openbot/index.js +43 -3
  11. package/dist/plugins/openbot/runtime.js +91 -27
  12. package/dist/plugins/openbot/system-prompt.js +21 -1
  13. package/dist/plugins/plugin-manager/index.js +87 -3
  14. package/dist/plugins/shell/index.js +2 -1
  15. package/dist/plugins/storage/files.js +67 -0
  16. package/dist/plugins/storage/index.js +184 -7
  17. package/dist/plugins/storage/service.js +215 -59
  18. package/dist/plugins/ui/index.js +109 -150
  19. package/dist/services/abort.js +43 -0
  20. package/dist/services/plugins/registry.js +5 -3
  21. package/dist/services/plugins/service.js +66 -11
  22. package/docs/agents.md +5 -8
  23. package/docs/architecture.md +1 -1
  24. package/docs/plugins.md +28 -7
  25. package/docs/templates/AGENT.example.md +4 -4
  26. package/package.json +7 -7
  27. package/src/app/cli.ts +1 -1
  28. package/src/app/config.ts +13 -0
  29. package/src/app/server.ts +235 -3
  30. package/src/app/types.ts +284 -14
  31. package/src/harness/index.ts +21 -0
  32. package/src/plugins/approval/index.ts +37 -20
  33. package/src/plugins/bash/index.ts +232 -0
  34. package/src/plugins/delegation/index.ts +7 -2
  35. package/src/plugins/openbot/context.ts +58 -9
  36. package/src/plugins/openbot/history.ts +52 -1
  37. package/src/plugins/openbot/index.ts +45 -3
  38. package/src/plugins/openbot/runtime.ts +121 -27
  39. package/src/plugins/openbot/system-prompt.ts +21 -1
  40. package/src/plugins/plugin-manager/index.ts +105 -3
  41. package/src/plugins/storage/files.ts +81 -0
  42. package/src/plugins/storage/index.ts +198 -8
  43. package/src/plugins/storage/service.ts +282 -59
  44. package/src/plugins/ui/index.ts +123 -0
  45. package/src/services/abort.ts +46 -0
  46. package/src/services/plugins/domain.ts +34 -1
  47. package/src/services/plugins/registry.ts +5 -3
  48. package/src/services/plugins/service.ts +136 -45
  49. package/src/services/plugins/types.ts +5 -1
  50. 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 { shellPlugin } from '../../plugins/shell/index.js';
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
- [shellPlugin.id]: shellPlugin,
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", "shell"), or
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` agent shape. */
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): MarketplaceAgentListing[] {
50
- const rawAgents: unknown =
51
- Array.isArray(data) ? data : isRecord(data) && Array.isArray(data.agents) ? data.agents : null;
52
- if (!Array.isArray(rawAgents)) {
53
- throw new Error('Registry JSON must be an array or an object with an "agents" array');
54
- }
55
- return rawAgents.map((item, i) => {
56
- if (!isRecord(item)) {
57
- throw new Error(`agents[${i}]: expected object`);
58
- }
59
- const id = item.id;
60
- const name = item.name;
61
- const description = item.description;
62
- const instructions = item.instructions;
63
- const pluginsRaw = item.plugins;
64
- if (typeof id !== 'string' || !id) throw new Error(`agents[${i}].id must be a non-empty string`);
65
- if (typeof name !== 'string') throw new Error(`agents[${i}].name must be a string`);
66
- if (typeof description !== 'string') throw new Error(`agents[${i}].description must be a string`);
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 ref: PluginRef = { id: p.id };
76
- if (p.config !== undefined) {
77
- if (!isRecord(p.config)) throw new Error(`agents[${i}].plugins[${j}].config must be an object`);
78
- ref.config = p.config;
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
- return ref;
81
- });
82
- const listing: MarketplaceAgentListing = { id, name, description, instructions, plugins };
83
- if (item.image !== undefined) {
84
- if (typeof item.image !== 'string') throw new Error(`agents[${i}].image must be a string`);
85
- listing.image = item.image;
86
- }
87
- return listing;
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 fetchMarketplaceAgentsFromUrl(url: string): Promise<MarketplaceAgentListing[]> {
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 agent listings from configured registry URL, or falls back to an empty list.
186
+ * Resolves marketplace registry (agents and channels) from configured registry URL.
105
187
  */
106
- export async function resolveMarketplaceAgentList(): Promise<MarketplaceAgentListing[]> {
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 fetchMarketplaceAgentsFromUrl(registryUrl);
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`, `shell`) or an npm
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;