@elizaos/plugin-shell 1.2.0 → 2.0.0-alpha.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.
Files changed (39) hide show
  1. package/LICENSE +2 -2
  2. package/dist/actions/clearHistory.d.ts +4 -0
  3. package/dist/actions/clearHistory.d.ts.map +1 -0
  4. package/dist/actions/executeCommand.d.ts +6 -0
  5. package/dist/actions/executeCommand.d.ts.map +1 -0
  6. package/dist/actions/index.d.ts +3 -0
  7. package/dist/actions/index.d.ts.map +1 -0
  8. package/dist/build.d.ts +2 -0
  9. package/dist/build.d.ts.map +1 -0
  10. package/dist/generated/prompts/typescript/prompts.d.ts +12 -0
  11. package/dist/generated/prompts/typescript/prompts.d.ts.map +1 -0
  12. package/dist/generated/specs/spec-helpers.d.ts +49 -0
  13. package/dist/generated/specs/spec-helpers.d.ts.map +1 -0
  14. package/dist/generated/specs/specs.d.ts +83 -0
  15. package/dist/generated/specs/specs.d.ts.map +1 -0
  16. package/dist/index.browser.d.ts +4 -0
  17. package/dist/index.browser.d.ts.map +1 -0
  18. package/dist/index.d.ts +10 -106
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +779 -911
  21. package/dist/index.js.map +19 -1
  22. package/dist/providers/index.d.ts +2 -0
  23. package/dist/providers/index.d.ts.map +1 -0
  24. package/dist/providers/shellHistoryProvider.d.ts +4 -0
  25. package/dist/providers/shellHistoryProvider.d.ts.map +1 -0
  26. package/dist/services/index.d.ts +2 -0
  27. package/dist/services/index.d.ts.map +1 -0
  28. package/dist/services/shellService.d.ts +24 -0
  29. package/dist/services/shellService.d.ts.map +1 -0
  30. package/dist/types/index.d.ts +30 -0
  31. package/dist/types/index.d.ts.map +1 -0
  32. package/dist/utils/config.d.ts +4 -0
  33. package/dist/utils/config.d.ts.map +1 -0
  34. package/dist/utils/index.d.ts +3 -0
  35. package/dist/utils/index.d.ts.map +1 -0
  36. package/dist/utils/pathUtils.d.ts +5 -0
  37. package/dist/utils/pathUtils.d.ts.map +1 -0
  38. package/package.json +75 -20
  39. package/README.md +0 -352
package/dist/index.js CHANGED
@@ -1,213 +1,656 @@
1
- // src/services/shellService.ts
1
+ // actions/clearHistory.ts
2
2
  import {
3
- Service,
4
- logger as logger3
3
+ logger
5
4
  } from "@elizaos/core";
6
- import spawn from "cross-spawn";
7
- import path3 from "path";
8
5
 
9
- // src/environment.ts
10
- import { logger } from "@elizaos/core";
11
- import joi from "joi";
12
- import path from "path";
13
- import fs from "fs";
14
- var configSchema = joi.object({
15
- enabled: joi.boolean().required(),
16
- allowedDirectory: joi.string().when("enabled", {
17
- is: true,
18
- then: joi.required(),
19
- otherwise: joi.optional()
20
- }),
21
- timeout: joi.number().positive().default(3e4),
22
- forbiddenCommands: joi.array().items(joi.string()).required()
23
- });
24
- var DEFAULT_FORBIDDEN_COMMANDS = [
25
- "rm -rf /",
26
- // Only block dangerous rm commands, not all rm
27
- "rmdir",
28
- "chmod 777",
29
- // Only block dangerous chmod, not all chmod
30
- "chown",
31
- "chgrp",
32
- "shutdown",
33
- "reboot",
34
- "halt",
35
- "poweroff",
36
- "kill -9",
37
- // Only block force kill, not all kill
38
- "killall",
39
- "pkill",
40
- "sudo rm -rf",
41
- // Block dangerous sudo commands
42
- "su",
43
- "passwd",
44
- "useradd",
45
- "userdel",
46
- "groupadd",
47
- "groupdel",
48
- "format",
49
- "fdisk",
50
- "mkfs",
51
- "dd if=/dev/zero",
52
- // Only block dangerous dd
53
- "shred",
54
- ":(){:|:&};:"
55
- // Fork bomb
56
- ];
57
- function loadShellConfig() {
58
- const enabled = process.env.SHELL_ENABLED === "true";
59
- const allowedDirectory = process.env.SHELL_ALLOWED_DIRECTORY || process.cwd();
60
- const timeout = parseInt(process.env.SHELL_TIMEOUT || "30000", 10);
61
- const customForbidden = process.env.SHELL_FORBIDDEN_COMMANDS ? process.env.SHELL_FORBIDDEN_COMMANDS.split(",").map((cmd) => cmd.trim()) : [];
62
- const forbiddenCommands = [.../* @__PURE__ */ new Set([...DEFAULT_FORBIDDEN_COMMANDS, ...customForbidden])];
63
- const config = {
64
- enabled,
65
- allowedDirectory,
66
- timeout,
67
- forbiddenCommands
68
- };
69
- const { error, value } = configSchema.validate(config);
70
- if (error) {
71
- throw new Error(`Shell plugin configuration error: ${error.message}`);
72
- }
73
- if (enabled && allowedDirectory) {
74
- try {
75
- const stats = fs.statSync(allowedDirectory);
76
- if (!stats.isDirectory()) {
77
- throw new Error(`SHELL_ALLOWED_DIRECTORY is not a directory: ${allowedDirectory}`);
78
- }
79
- value.allowedDirectory = path.resolve(allowedDirectory);
80
- logger.info(`Shell plugin enabled with allowed directory: ${value.allowedDirectory}`);
81
- } catch (error2) {
82
- if (error2.code === "ENOENT") {
83
- throw new Error(`SHELL_ALLOWED_DIRECTORY does not exist: ${allowedDirectory}`);
84
- }
85
- throw error2;
6
+ // generated/specs/specs.ts
7
+ var coreActionsSpec = {
8
+ version: "1.0.0",
9
+ actions: [
10
+ {
11
+ name: "CLEAR_SHELL_HISTORY",
12
+ description: "Clears the recorded history of shell commands for the current conversation",
13
+ similes: ["RESET_SHELL", "CLEAR_TERMINAL", "CLEAR_HISTORY", "RESET_HISTORY"],
14
+ parameters: []
15
+ },
16
+ {
17
+ name: "EXECUTE_COMMAND",
18
+ description: "Execute shell commands including brew install, npm install, apt-get, system commands, file operations, directory navigation, and scripts.",
19
+ similes: [
20
+ "RUN_COMMAND",
21
+ "SHELL_COMMAND",
22
+ "TERMINAL_COMMAND",
23
+ "EXEC",
24
+ "RUN",
25
+ "EXECUTE",
26
+ "CREATE_FILE",
27
+ "WRITE_FILE",
28
+ "MAKE_FILE",
29
+ "INSTALL",
30
+ "BREW_INSTALL",
31
+ "NPM_INSTALL",
32
+ "APT_INSTALL"
33
+ ],
34
+ parameters: []
35
+ }
36
+ ]
37
+ };
38
+ var allActionsSpec = {
39
+ version: "1.0.0",
40
+ actions: [
41
+ {
42
+ name: "CLEAR_SHELL_HISTORY",
43
+ description: "Clears the recorded history of shell commands for the current conversation",
44
+ similes: ["RESET_SHELL", "CLEAR_TERMINAL", "CLEAR_HISTORY", "RESET_HISTORY"],
45
+ parameters: []
46
+ },
47
+ {
48
+ name: "EXECUTE_COMMAND",
49
+ description: "Execute shell commands including brew install, npm install, apt-get, system commands, file operations, directory navigation, and scripts.",
50
+ similes: [
51
+ "RUN_COMMAND",
52
+ "SHELL_COMMAND",
53
+ "TERMINAL_COMMAND",
54
+ "EXEC",
55
+ "RUN",
56
+ "EXECUTE",
57
+ "CREATE_FILE",
58
+ "WRITE_FILE",
59
+ "MAKE_FILE",
60
+ "INSTALL",
61
+ "BREW_INSTALL",
62
+ "NPM_INSTALL",
63
+ "APT_INSTALL"
64
+ ],
65
+ parameters: []
66
+ }
67
+ ]
68
+ };
69
+ var coreProvidersSpec = {
70
+ version: "1.0.0",
71
+ providers: [
72
+ {
73
+ name: "SHELL_HISTORY",
74
+ description: "Provides recent shell command history, current working directory, and file operations within the restricted environment",
75
+ dynamic: true
86
76
  }
77
+ ]
78
+ };
79
+ var allProvidersSpec = {
80
+ version: "1.0.0",
81
+ providers: [
82
+ {
83
+ name: "SHELL_HISTORY",
84
+ description: "Provides recent shell command history, current working directory, and file operations within the restricted environment",
85
+ dynamic: true
86
+ }
87
+ ]
88
+ };
89
+ var coreEvaluatorsSpec = {
90
+ version: "1.0.0",
91
+ evaluators: []
92
+ };
93
+ var allEvaluatorsSpec = {
94
+ version: "1.0.0",
95
+ evaluators: []
96
+ };
97
+ var coreActionDocs = coreActionsSpec.actions;
98
+ var allActionDocs = allActionsSpec.actions;
99
+ var coreProviderDocs = coreProvidersSpec.providers;
100
+ var allProviderDocs = allProvidersSpec.providers;
101
+ var coreEvaluatorDocs = coreEvaluatorsSpec.evaluators;
102
+ var allEvaluatorDocs = allEvaluatorsSpec.evaluators;
103
+
104
+ // generated/specs/spec-helpers.ts
105
+ var coreActionMap = new Map(coreActionDocs.map((doc) => [doc.name, doc]));
106
+ var allActionMap = new Map(allActionDocs.map((doc) => [doc.name, doc]));
107
+ var coreProviderMap = new Map(coreProviderDocs.map((doc) => [doc.name, doc]));
108
+ var allProviderMap = new Map(allProviderDocs.map((doc) => [doc.name, doc]));
109
+ var coreEvaluatorMap = new Map(coreEvaluatorDocs.map((doc) => [doc.name, doc]));
110
+ var allEvaluatorMap = new Map(allEvaluatorDocs.map((doc) => [doc.name, doc]));
111
+ function getActionSpec(name) {
112
+ return coreActionMap.get(name) ?? allActionMap.get(name);
113
+ }
114
+ function requireActionSpec(name) {
115
+ const spec = getActionSpec(name);
116
+ if (!spec) {
117
+ throw new Error(`Action spec not found: ${name}`);
87
118
  }
88
- if (!enabled) {
89
- logger.info("Shell plugin is disabled. Set SHELL_ENABLED=true to enable.");
119
+ return spec;
120
+ }
121
+ function getProviderSpec(name) {
122
+ return coreProviderMap.get(name) ?? allProviderMap.get(name);
123
+ }
124
+ function requireProviderSpec(name) {
125
+ const spec = getProviderSpec(name);
126
+ if (!spec) {
127
+ throw new Error(`Provider spec not found: ${name}`);
90
128
  }
91
- return value;
129
+ return spec;
92
130
  }
93
131
 
94
- // src/utils/pathUtils.ts
95
- import path2 from "path";
96
- import { logger as logger2 } from "@elizaos/core";
97
- function validatePath(commandPath, allowedDir, currentDir) {
98
- try {
99
- const resolvedPath = path2.resolve(currentDir, commandPath);
100
- const normalizedPath = path2.normalize(resolvedPath);
101
- const normalizedAllowed = path2.normalize(allowedDir);
102
- if (!normalizedPath.startsWith(normalizedAllowed)) {
103
- logger2.warn(
104
- `Path validation failed: ${normalizedPath} is outside allowed directory ${normalizedAllowed}`
105
- );
106
- return null;
132
+ // actions/clearHistory.ts
133
+ var spec = requireActionSpec("CLEAR_SHELL_HISTORY");
134
+ var clearHistory = {
135
+ name: spec.name,
136
+ similes: spec.similes ? [...spec.similes] : [],
137
+ description: spec.description,
138
+ validate: async (runtime, message, _state) => {
139
+ const shellService = runtime.getService("shell");
140
+ if (!shellService) {
141
+ return false;
107
142
  }
108
- return normalizedPath;
109
- } catch (error) {
110
- logger2.error("Error validating path:", error);
111
- return null;
112
- }
143
+ const text = message.content.text?.toLowerCase() || "";
144
+ const clearKeywords = ["clear", "reset", "delete", "remove", "clean"];
145
+ const historyKeywords = ["history", "terminal", "shell", "command"];
146
+ const hasClearKeyword = clearKeywords.some((keyword) => text.includes(keyword));
147
+ const hasHistoryKeyword = historyKeywords.some((keyword) => text.includes(keyword));
148
+ return hasClearKeyword && hasHistoryKeyword;
149
+ },
150
+ handler: async (runtime, message, _state, _options, callback) => {
151
+ const shellService = runtime.getService("shell");
152
+ if (!shellService) {
153
+ if (callback) {
154
+ await callback({
155
+ text: "Shell service is not available.",
156
+ source: message.content.source
157
+ });
158
+ }
159
+ return { success: false, error: "Shell service is not available." };
160
+ }
161
+ const conversationId = message.roomId || message.agentId;
162
+ if (!conversationId) {
163
+ const errorMsg = "No conversation ID available";
164
+ if (callback) {
165
+ await callback({
166
+ text: errorMsg,
167
+ source: message.content.source
168
+ });
169
+ }
170
+ return { success: false, error: errorMsg };
171
+ }
172
+ shellService.clearCommandHistory(conversationId);
173
+ logger.info(`Cleared shell history for conversation: ${conversationId}`);
174
+ const response = {
175
+ text: "Shell command history has been cleared.",
176
+ source: message.content.source
177
+ };
178
+ if (callback) {
179
+ await callback(response);
180
+ }
181
+ return { success: true, text: response.text };
182
+ },
183
+ examples: spec.examples ?? []
184
+ };
185
+ // actions/executeCommand.ts
186
+ import {
187
+ composePromptFromState,
188
+ logger as logger2,
189
+ ModelType,
190
+ parseJSONObjectFromText
191
+ } from "@elizaos/core";
192
+
193
+ // generated/prompts/typescript/prompts.ts
194
+ var commandExtractionTemplate = `# Extracting shell command from request
195
+ {{recentMessages}}
196
+
197
+ # Instructions: {{senderName}} wants to execute a shell command. Extract the COMPLETE shell command they want to run.
198
+
199
+ IMPORTANT:
200
+ 1. Always return the FULL executable shell command, not just the content or partial command.
201
+ 2. If the user mentions installing something, create the appropriate brew/npm/apt command.
202
+ 3. If the user directly provides a command (like "brew install X"), use it exactly as provided.
203
+ 4. ALWAYS extract a command if the user is asking for ANY kind of system operation.
204
+
205
+ Common patterns:
206
+ - "run ls -la" -> command: "ls -la"
207
+ - "execute npm test" -> command: "npm test"
208
+ - "show me the files" or "list files" -> command: "ls -la"
209
+ - "what's in this directory" -> command: "ls -la"
210
+ - "check git status" -> command: "git status"
211
+ - "navigate to src folder" -> command: "cd src"
212
+ - "create a file called test.txt" -> command: "touch test.txt"
213
+ - "write hello world to a file" -> command: "echo 'hello world' > file.txt"
214
+ - "create hello.js with javascript code" -> command: "echo 'console.log(\\"Hello, World!\\");' > hello.js"
215
+ - "create hello_world.py and write a python hello world script inside" -> command: "echo 'print(\\"Hello, World!\\")' > hello_world.py"
216
+ - "make a new directory" -> command: "mkdir newdir"
217
+ - "list files inside your filesystem" -> command: "ls -la"
218
+ - "install orbstack" or "brew install orbstack" -> command: "brew install orbstack"
219
+ - "install mullvad vpn" -> command: "brew install --cask mullvad-vpn"
220
+ - "get system info" -> command: "system_profiler SPHardwareDataType"
221
+ - "check memory usage" -> command: "vm_stat"
222
+ - "install package" -> command: "brew install <package>"
223
+
224
+ Special cases:
225
+ - "Run it in your shell" or "execute it" -> Extract the command from previous context
226
+ - "Install these" -> Look for package names in previous messages
227
+ - Direct commands should be used exactly as provided
228
+
229
+ Key rules:
230
+ 1. For file creation with content, use: echo 'content' > filename
231
+ 2. For listing files, use: ls -la (not just ls)
232
+ 3. Always include the echo command when writing to files
233
+ 4. Include all flags and arguments
234
+ 5. When user says "run it", "execute it", or similar, they want you to run the command
235
+
236
+ Your response must be formatted as a JSON block:
237
+ \`\`\`json
238
+ {
239
+ "command": "<complete shell command to execute>"
113
240
  }
114
- function isSafeCommand(command) {
115
- const pathTraversalPatterns = [
116
- /\.\.\//g,
117
- // ../
118
- /\.\.\\/g,
119
- // ..\
120
- /\/\.\./g,
121
- // /..
122
- /\\\.\./g
123
- // \..
124
- ];
125
- const dangerousPatterns = [
126
- /\$\(/g,
127
- // Command substitution $(
128
- /`[^']*`/g,
129
- // Command substitution ` (but allow in quotes)
130
- /\|\s*sudo/g,
131
- // Pipe to sudo
132
- /;\s*sudo/g,
133
- // Chain with sudo
134
- /&\s*&/g,
135
- // && chaining
136
- /\|\s*\|/g
137
- // || chaining
138
- ];
139
- for (const pattern of pathTraversalPatterns) {
140
- if (pattern.test(command)) {
141
- logger2.warn(`Path traversal detected in command: ${command}`);
142
- return false;
241
+ \`\`\``;
242
+
243
+ // actions/executeCommand.ts
244
+ var extractCommand = async (runtime, _message, state) => {
245
+ const prompt = composePromptFromState({
246
+ state,
247
+ template: commandExtractionTemplate
248
+ });
249
+ for (let i = 0;i < 3; i++) {
250
+ const response = await runtime.useModel(ModelType.TEXT_SMALL, {
251
+ prompt
252
+ });
253
+ const parsedResponse = parseJSONObjectFromText(response);
254
+ if (parsedResponse?.command) {
255
+ return { command: parsedResponse.command };
143
256
  }
144
257
  }
145
- for (const pattern of dangerousPatterns) {
146
- if (pattern.test(command)) {
147
- logger2.warn(`Dangerous pattern detected in command: ${command}`);
258
+ return null;
259
+ };
260
+ var spec2 = requireActionSpec("EXECUTE_COMMAND");
261
+ var executeCommand = {
262
+ name: spec2.name,
263
+ similes: spec2.similes ? [...spec2.similes] : [],
264
+ description: spec2.description,
265
+ validate: async (runtime, message, _state) => {
266
+ const shellService = runtime.getService("shell");
267
+ if (!shellService) {
148
268
  return false;
149
269
  }
150
- }
151
- const pipeCount = (command.match(/\|/g) || []).length;
152
- if (pipeCount > 1) {
153
- logger2.warn(`Multiple pipes detected in command: ${command}`);
154
- return false;
155
- }
156
- return true;
157
- }
158
- function extractBaseCommand(fullCommand) {
159
- const parts = fullCommand.trim().split(/\s+/);
160
- return parts[0] || "";
161
- }
162
- function isForbiddenCommand(command, forbiddenCommands) {
163
- const normalizedCommand = command.trim().toLowerCase();
164
- return forbiddenCommands.some((forbidden) => {
165
- const forbiddenLower = forbidden.toLowerCase();
166
- if (normalizedCommand.startsWith(forbiddenLower)) {
167
- return true;
270
+ const text = message.content.text?.toLowerCase() || "";
271
+ const commandKeywords = [
272
+ "run",
273
+ "execute",
274
+ "command",
275
+ "shell",
276
+ "install",
277
+ "brew",
278
+ "npm",
279
+ "create",
280
+ "file",
281
+ "directory",
282
+ "folder",
283
+ "list",
284
+ "show",
285
+ "system",
286
+ "info",
287
+ "check",
288
+ "status",
289
+ "cd",
290
+ "ls",
291
+ "mkdir",
292
+ "echo",
293
+ "cat",
294
+ "touch",
295
+ "git",
296
+ "build",
297
+ "test"
298
+ ];
299
+ const hasCommandKeyword = commandKeywords.some((keyword) => text.includes(keyword));
300
+ const hasDirectCommand = /^(brew|npm|apt|git|ls|cd|echo|cat|touch|mkdir|rm|mv|cp)\s/i.test(message.content.text || "");
301
+ return hasCommandKeyword || hasDirectCommand;
302
+ },
303
+ handler: async (runtime, message, state, _options, callback) => {
304
+ const shellService = runtime.getService("shell");
305
+ if (!shellService) {
306
+ if (callback) {
307
+ await callback({
308
+ text: "Shell service is not available.",
309
+ source: message.content.source
310
+ });
311
+ }
312
+ return { success: false, error: "Shell service is not available." };
168
313
  }
169
- if (!forbidden.includes(" ")) {
170
- const baseCommand = extractBaseCommand(command);
171
- if (baseCommand.toLowerCase() === forbiddenLower) {
172
- return true;
314
+ const commandInfo = await extractCommand(runtime, message, state);
315
+ if (!commandInfo?.command) {
316
+ logger2.error("Failed to extract command from message:", message.content.text);
317
+ if (callback) {
318
+ await callback({
319
+ text: "Could not determine which command to execute. Please specify a shell command.",
320
+ source: message.content.source
321
+ });
173
322
  }
323
+ return { success: false, error: "Could not extract command." };
174
324
  }
175
- return false;
176
- });
177
- }
325
+ logger2.info(`Extracted command: "${commandInfo.command}"`);
326
+ try {
327
+ const conversationId = message.roomId || message.agentId;
328
+ const result = await shellService.executeCommand(commandInfo.command, conversationId);
329
+ let responseText = "";
330
+ if (result.success) {
331
+ responseText = `Command executed successfully in ${result.executedIn}
178
332
 
179
- // src/services/shellService.ts
180
- var ShellService = class _ShellService extends Service {
181
- static serviceType = "shell";
182
- shellConfig;
183
- currentDirectory;
184
- commandHistory;
185
- // conversationId -> history
186
- maxHistoryPerConversation = 100;
187
- constructor(runtime) {
188
- super();
189
- this.runtime = runtime;
190
- this.shellConfig = loadShellConfig();
191
- this.currentDirectory = this.shellConfig.allowedDirectory;
192
- this.commandHistory = /* @__PURE__ */ new Map();
193
- }
194
- static async start(runtime) {
195
- const instance = new _ShellService(runtime);
196
- logger3.info("Shell service initialized with history tracking");
197
- return instance;
198
- }
199
- async stop() {
200
- logger3.info("Shell service stopped");
333
+ `;
334
+ if (result.stdout) {
335
+ responseText += `Output:
336
+ \`\`\`
337
+ ${result.stdout}
338
+ \`\`\``;
339
+ } else {
340
+ responseText += "Command completed with no output.";
341
+ }
342
+ } else {
343
+ responseText = `Command failed with exit code ${result.exitCode} in ${result.executedIn}
344
+
345
+ `;
346
+ if (result.error) {
347
+ responseText += `Error: ${result.error}
348
+ `;
349
+ }
350
+ if (result.stderr) {
351
+ responseText += `
352
+ Error output:
353
+ \`\`\`
354
+ ${result.stderr}
355
+ \`\`\``;
356
+ }
357
+ }
358
+ const response = {
359
+ text: responseText,
360
+ source: message.content.source
361
+ };
362
+ if (callback) {
363
+ await callback(response);
364
+ }
365
+ return { success: result.success, text: responseText };
366
+ } catch (error) {
367
+ logger2.error("Error executing command:", error);
368
+ const errorMessage = error instanceof Error ? error.message : String(error);
369
+ if (callback) {
370
+ await callback({
371
+ text: `Failed to execute command: ${errorMessage}`,
372
+ source: message.content.source
373
+ });
374
+ }
375
+ return { success: false, error: errorMessage };
376
+ }
377
+ },
378
+ examples: spec2.examples ?? []
379
+ };
380
+ // providers/shellHistoryProvider.ts
381
+ import {
382
+ addHeader,
383
+ logger as logger3
384
+ } from "@elizaos/core";
385
+ var MAX_OUTPUT_LENGTH = 8000;
386
+ var TRUNCATE_SEGMENT_LENGTH = 4000;
387
+ var spec3 = requireProviderSpec("SHELL_HISTORY");
388
+ var shellHistoryProvider = {
389
+ name: spec3.name,
390
+ description: "Provides recent shell command history, current working directory, and file operations within the restricted environment",
391
+ position: 99,
392
+ get: async (runtime, message, _state) => {
393
+ const shellService = runtime.getService("shell");
394
+ if (!shellService) {
395
+ logger3.warn("[shellHistoryProvider] Shell service not found");
396
+ return {
397
+ values: {
398
+ shellHistory: "Shell service is not available",
399
+ currentWorkingDirectory: "N/A",
400
+ allowedDirectory: "N/A"
401
+ },
402
+ text: addHeader("# Shell Status", "Shell service is not available"),
403
+ data: { historyCount: 0, cwd: "N/A", allowedDir: "N/A" }
404
+ };
405
+ }
406
+ const conversationId = message.roomId || message.agentId;
407
+ if (!conversationId) {
408
+ return {
409
+ text: "No conversation ID available",
410
+ values: { historyCount: 0, cwd: "N/A", allowedDir: "N/A" },
411
+ data: { historyCount: 0, cwd: "N/A", allowedDir: "N/A" }
412
+ };
413
+ }
414
+ const history = shellService.getCommandHistory(conversationId, 10);
415
+ const cwd = shellService.getCurrentDirectory(conversationId);
416
+ const allowedDir = shellService.getAllowedDirectory();
417
+ let historyText = "No commands in history.";
418
+ if (history.length > 0) {
419
+ historyText = history.map((entry) => {
420
+ let entryStr = `[${new Date(entry.timestamp).toISOString()}] ${entry.workingDirectory}> ${entry.command}`;
421
+ if (entry.stdout) {
422
+ if (entry.stdout.length > MAX_OUTPUT_LENGTH) {
423
+ entryStr += `
424
+ Output: ${entry.stdout.substring(0, TRUNCATE_SEGMENT_LENGTH)}
425
+ ... [TRUNCATED] ...
426
+ ${entry.stdout.substring(entry.stdout.length - TRUNCATE_SEGMENT_LENGTH)}`;
427
+ } else {
428
+ entryStr += `
429
+ Output: ${entry.stdout}`;
430
+ }
431
+ }
432
+ if (entry.stderr) {
433
+ if (entry.stderr.length > MAX_OUTPUT_LENGTH) {
434
+ entryStr += `
435
+ Error: ${entry.stderr.substring(0, TRUNCATE_SEGMENT_LENGTH)}
436
+ ... [TRUNCATED] ...
437
+ ${entry.stderr.substring(entry.stderr.length - TRUNCATE_SEGMENT_LENGTH)}`;
438
+ } else {
439
+ entryStr += `
440
+ Error: ${entry.stderr}`;
441
+ }
442
+ }
443
+ entryStr += `
444
+ Exit Code: ${entry.exitCode}`;
445
+ if (entry.fileOperations && entry.fileOperations.length > 0) {
446
+ entryStr += `
447
+ File Operations:`;
448
+ entry.fileOperations.forEach((op) => {
449
+ if (op.secondaryTarget) {
450
+ entryStr += `
451
+ - ${op.type}: ${op.target} → ${op.secondaryTarget}`;
452
+ } else {
453
+ entryStr += `
454
+ - ${op.type}: ${op.target}`;
455
+ }
456
+ });
457
+ }
458
+ return entryStr;
459
+ }).join(`
460
+
461
+ `);
462
+ }
463
+ const recentFileOps = history.filter((entry) => entry.fileOperations && entry.fileOperations.length > 0).flatMap((entry) => entry.fileOperations ?? []).slice(-5);
464
+ let fileOpsText = "";
465
+ if (recentFileOps.length > 0) {
466
+ fileOpsText = `
467
+
468
+ ` + addHeader("# Recent File Operations", recentFileOps.map((op) => {
469
+ if (op.secondaryTarget) {
470
+ return `- ${op.type}: ${op.target} → ${op.secondaryTarget}`;
471
+ }
472
+ return `- ${op.type}: ${op.target}`;
473
+ }).join(`
474
+ `));
475
+ }
476
+ const text = `Current Directory: ${cwd}
477
+ Allowed Directory: ${allowedDir}
478
+
479
+ ${addHeader("# Shell History (Last 10)", historyText)}${fileOpsText}`;
480
+ return {
481
+ values: {
482
+ shellHistory: historyText,
483
+ currentWorkingDirectory: cwd,
484
+ allowedDirectory: allowedDir
485
+ },
486
+ text,
487
+ data: {
488
+ historyCount: history.length,
489
+ cwd,
490
+ allowedDir
491
+ }
492
+ };
493
+ }
494
+ };
495
+ // services/shellService.ts
496
+ import path3 from "node:path";
497
+ import { logger as logger6, Service } from "@elizaos/core";
498
+ import spawn from "cross-spawn";
499
+
500
+ // utils/config.ts
501
+ import fs from "node:fs";
502
+ import path from "node:path";
503
+ import { logger as logger4 } from "@elizaos/core";
504
+ import { z } from "zod";
505
+ var configSchema = z.object({
506
+ enabled: z.boolean(),
507
+ allowedDirectory: z.string(),
508
+ timeout: z.number().positive().default(30000),
509
+ forbiddenCommands: z.array(z.string())
510
+ });
511
+ var DEFAULT_FORBIDDEN_COMMANDS = [
512
+ "rm -rf /",
513
+ "rmdir",
514
+ "chmod 777",
515
+ "chown",
516
+ "chgrp",
517
+ "shutdown",
518
+ "reboot",
519
+ "halt",
520
+ "poweroff",
521
+ "kill -9",
522
+ "killall",
523
+ "pkill",
524
+ "sudo rm -rf",
525
+ "su",
526
+ "passwd",
527
+ "useradd",
528
+ "userdel",
529
+ "groupadd",
530
+ "groupdel",
531
+ "format",
532
+ "fdisk",
533
+ "mkfs",
534
+ "dd if=/dev/zero",
535
+ "shred",
536
+ ":(){:|:&};:"
537
+ ];
538
+ function loadShellConfig() {
539
+ const enabled = process.env.SHELL_ENABLED === "true";
540
+ const allowedDirectory = process.env.SHELL_ALLOWED_DIRECTORY || process.cwd();
541
+ const timeout = parseInt(process.env.SHELL_TIMEOUT || "30000", 10);
542
+ const customForbidden = process.env.SHELL_FORBIDDEN_COMMANDS ? process.env.SHELL_FORBIDDEN_COMMANDS.split(",").map((cmd) => cmd.trim()) : [];
543
+ const forbiddenCommands = [...new Set([...DEFAULT_FORBIDDEN_COMMANDS, ...customForbidden])];
544
+ const config = {
545
+ enabled,
546
+ allowedDirectory,
547
+ timeout,
548
+ forbiddenCommands
549
+ };
550
+ const parseResult = configSchema.safeParse(config);
551
+ if (!parseResult.success) {
552
+ const errorMessage = parseResult.error.issues?.[0]?.message || parseResult.error.toString();
553
+ throw new Error(`Shell plugin configuration error: ${errorMessage}`);
554
+ }
555
+ if (enabled && allowedDirectory) {
556
+ try {
557
+ const stats = fs.statSync(allowedDirectory);
558
+ if (!stats.isDirectory()) {
559
+ throw new Error(`SHELL_ALLOWED_DIRECTORY is not a directory: ${allowedDirectory}`);
560
+ }
561
+ config.allowedDirectory = path.resolve(allowedDirectory);
562
+ logger4.info(`Shell plugin enabled with allowed directory: ${config.allowedDirectory}`);
563
+ } catch (error) {
564
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
565
+ throw new Error(`SHELL_ALLOWED_DIRECTORY does not exist: ${allowedDirectory}`);
566
+ }
567
+ throw error;
568
+ }
569
+ }
570
+ if (!enabled) {
571
+ logger4.info("Shell plugin is disabled. Set SHELL_ENABLED=true to enable.");
572
+ }
573
+ return config;
574
+ }
575
+ // utils/pathUtils.ts
576
+ import path2 from "node:path";
577
+ import { logger as logger5 } from "@elizaos/core";
578
+ function validatePath(commandPath, allowedDir, currentDir) {
579
+ const resolvedPath = path2.resolve(currentDir, commandPath);
580
+ const normalizedPath = path2.normalize(resolvedPath);
581
+ const normalizedAllowed = path2.normalize(allowedDir);
582
+ if (!normalizedPath.startsWith(normalizedAllowed)) {
583
+ logger5.warn(`Path validation failed: ${normalizedPath} is outside allowed directory ${normalizedAllowed}`);
584
+ return null;
585
+ }
586
+ return normalizedPath;
587
+ }
588
+ function isSafeCommand(command) {
589
+ const pathTraversalPatterns = [/\.\.\//g, /\.\.\\/g, /\/\.\./g, /\\\.\./g];
590
+ const dangerousPatterns = [/\$\(/g, /`[^']*`/g, /\|\s*sudo/g, /;\s*sudo/g, /&\s*&/g, /\|\s*\|/g];
591
+ for (const pattern of pathTraversalPatterns) {
592
+ if (pattern.test(command)) {
593
+ logger5.warn(`Path traversal detected in command: ${command}`);
594
+ return false;
595
+ }
596
+ }
597
+ for (const pattern of dangerousPatterns) {
598
+ if (pattern.test(command)) {
599
+ logger5.warn(`Dangerous pattern detected in command: ${command}`);
600
+ return false;
601
+ }
602
+ }
603
+ const pipeCount = (command.match(/\|/g) || []).length;
604
+ if (pipeCount > 1) {
605
+ logger5.warn(`Multiple pipes detected in command: ${command}`);
606
+ return false;
607
+ }
608
+ return true;
609
+ }
610
+ function extractBaseCommand(fullCommand) {
611
+ const parts = fullCommand.trim().split(/\s+/);
612
+ return parts[0] || "";
613
+ }
614
+ function isForbiddenCommand(command, forbiddenCommands) {
615
+ const normalizedCommand = command.trim().toLowerCase();
616
+ return forbiddenCommands.some((forbidden) => {
617
+ const forbiddenLower = forbidden.toLowerCase();
618
+ if (normalizedCommand.startsWith(forbiddenLower)) {
619
+ return true;
620
+ }
621
+ if (!forbidden.includes(" ")) {
622
+ const baseCommand = extractBaseCommand(command);
623
+ if (baseCommand.toLowerCase() === forbiddenLower) {
624
+ return true;
625
+ }
626
+ }
627
+ return false;
628
+ });
629
+ }
630
+ // services/shellService.ts
631
+ class ShellService extends Service {
632
+ static serviceType = "shell";
633
+ shellConfig;
634
+ currentDirectory;
635
+ commandHistory;
636
+ maxHistoryPerConversation = 100;
637
+ constructor(runtime) {
638
+ super(runtime);
639
+ this.shellConfig = loadShellConfig();
640
+ this.currentDirectory = this.shellConfig.allowedDirectory;
641
+ this.commandHistory = new Map;
642
+ }
643
+ static async start(runtime) {
644
+ const instance = new ShellService(runtime);
645
+ logger6.info("Shell service initialized with history tracking");
646
+ return instance;
647
+ }
648
+ async stop() {
649
+ logger6.info("Shell service stopped");
201
650
  }
202
651
  get capabilityDescription() {
203
652
  return "Execute shell commands within a restricted directory with history tracking";
204
653
  }
205
- /**
206
- * Executes a shell command within the allowed directory
207
- * @param command The command to execute
208
- * @param conversationId Optional conversation ID for history tracking
209
- * @returns The command execution result
210
- */
211
654
  async executeCommand(command, conversationId) {
212
655
  if (!this.shellConfig.enabled) {
213
656
  return {
@@ -244,7 +687,7 @@ var ShellService = class _ShellService extends Service {
244
687
  return {
245
688
  success: false,
246
689
  stdout: "",
247
- stderr: `Command is forbidden by security policy`,
690
+ stderr: "Command is forbidden by security policy",
248
691
  exitCode: 1,
249
692
  error: "Forbidden command",
250
693
  executedIn: this.currentDirectory
@@ -268,11 +711,6 @@ var ShellService = class _ShellService extends Service {
268
711
  }
269
712
  return result;
270
713
  }
271
- /**
272
- * Handles the cd command to change directory within allowed bounds
273
- * @param command The cd command
274
- * @returns The command result
275
- */
276
714
  async handleCdCommand(command) {
277
715
  const parts = command.split(/\s+/);
278
716
  if (parts.length < 2) {
@@ -286,11 +724,7 @@ var ShellService = class _ShellService extends Service {
286
724
  };
287
725
  }
288
726
  const targetPath = parts.slice(1).join(" ");
289
- const validatedPath = validatePath(
290
- targetPath,
291
- this.shellConfig.allowedDirectory,
292
- this.currentDirectory
293
- );
727
+ const validatedPath = validatePath(targetPath, this.shellConfig.allowedDirectory, this.currentDirectory);
294
728
  if (!validatedPath) {
295
729
  return {
296
730
  success: false,
@@ -310,11 +744,6 @@ var ShellService = class _ShellService extends Service {
310
744
  executedIn: this.currentDirectory
311
745
  };
312
746
  }
313
- /**
314
- * Runs a command using cross-spawn
315
- * @param command The command to run
316
- * @returns The command result
317
- */
318
747
  async runCommand(command) {
319
748
  return new Promise((resolve) => {
320
749
  const useShell = command.includes(">") || command.includes("<") || command.includes("|");
@@ -323,12 +752,12 @@ var ShellService = class _ShellService extends Service {
323
752
  if (useShell) {
324
753
  cmd = "sh";
325
754
  args = ["-c", command];
326
- logger3.info(`Executing shell command: sh -c "${command}" in ${this.currentDirectory}`);
755
+ logger6.info(`Executing shell command: sh -c "${command}" in ${this.currentDirectory}`);
327
756
  } else {
328
757
  const parts = command.split(/\s+/);
329
758
  cmd = parts[0];
330
759
  args = parts.slice(1);
331
- logger3.info(`Executing command: ${cmd} ${args.join(" ")} in ${this.currentDirectory}`);
760
+ logger6.info(`Executing command: ${cmd} ${args.join(" ")} in ${this.currentDirectory}`);
332
761
  }
333
762
  let stdout = "";
334
763
  let stderr = "";
@@ -336,723 +765,154 @@ var ShellService = class _ShellService extends Service {
336
765
  const child = spawn(cmd, args, {
337
766
  cwd: this.currentDirectory,
338
767
  env: process.env,
339
- // Only use shell: false for direct commands, not for sh -c
340
768
  shell: false
341
769
  });
342
770
  const timeout = setTimeout(() => {
343
771
  timedOut = true;
344
772
  child.kill("SIGTERM");
345
- setTimeout(() => {
346
- if (!child.killed) {
347
- child.kill("SIGKILL");
348
- }
349
- }, 5e3);
350
- }, this.shellConfig.timeout);
351
- if (child.stdout) {
352
- child.stdout.on("data", (data) => {
353
- stdout += data.toString();
354
- });
355
- }
356
- if (child.stderr) {
357
- child.stderr.on("data", (data) => {
358
- stderr += data.toString();
359
- });
360
- }
361
- child.on("exit", (code) => {
362
- clearTimeout(timeout);
363
- if (timedOut) {
364
- resolve({
365
- success: false,
366
- stdout,
367
- stderr: stderr + "\nCommand timed out",
368
- exitCode: code,
369
- error: "Command execution timeout",
370
- executedIn: this.currentDirectory
371
- });
372
- return;
373
- }
374
- resolve({
375
- success: code === 0,
376
- stdout,
377
- stderr,
378
- exitCode: code,
379
- executedIn: this.currentDirectory
380
- });
381
- });
382
- child.on("error", (err) => {
383
- clearTimeout(timeout);
384
- resolve({
385
- success: false,
386
- stdout,
387
- stderr: err.message,
388
- exitCode: 1,
389
- error: "Failed to execute command",
390
- executedIn: this.currentDirectory
391
- });
392
- });
393
- });
394
- }
395
- /**
396
- * Adds a command to the history
397
- */
398
- addToHistory(conversationId, command, result, fileOperations) {
399
- if (!conversationId) return;
400
- const historyEntry = {
401
- command,
402
- stdout: result.stdout,
403
- stderr: result.stderr,
404
- exitCode: result.exitCode,
405
- timestamp: Date.now(),
406
- workingDirectory: result.executedIn,
407
- fileOperations
408
- };
409
- if (!this.commandHistory.has(conversationId)) {
410
- this.commandHistory.set(conversationId, []);
411
- }
412
- const history = this.commandHistory.get(conversationId);
413
- history.push(historyEntry);
414
- if (history.length > this.maxHistoryPerConversation) {
415
- history.shift();
416
- }
417
- }
418
- /**
419
- * Detects file operations from a command
420
- */
421
- detectFileOperations(command, cwd) {
422
- const operations = [];
423
- const parts = command.trim().split(/\s+/);
424
- const cmd = parts[0].toLowerCase();
425
- if (cmd === "touch" && parts.length > 1) {
426
- operations.push({
427
- type: "create",
428
- target: this.resolvePath(parts[1], cwd)
429
- });
430
- } else if (cmd === "echo" && command.includes(">")) {
431
- const match = command.match(/>\s*([^\s]+)$/);
432
- if (match) {
433
- operations.push({
434
- type: "write",
435
- target: this.resolvePath(match[1], cwd)
436
- });
437
- }
438
- } else if (cmd === "mkdir" && parts.length > 1) {
439
- operations.push({
440
- type: "mkdir",
441
- target: this.resolvePath(parts[1], cwd)
442
- });
443
- } else if (cmd === "cat" && parts.length > 1 && !command.includes(">")) {
444
- operations.push({
445
- type: "read",
446
- target: this.resolvePath(parts[1], cwd)
447
- });
448
- } else if (cmd === "mv" && parts.length > 2) {
449
- operations.push({
450
- type: "move",
451
- target: this.resolvePath(parts[1], cwd),
452
- secondaryTarget: this.resolvePath(parts[2], cwd)
453
- });
454
- } else if (cmd === "cp" && parts.length > 2) {
455
- operations.push({
456
- type: "copy",
457
- target: this.resolvePath(parts[1], cwd),
458
- secondaryTarget: this.resolvePath(parts[2], cwd)
459
- });
460
- }
461
- return operations.length > 0 ? operations : void 0;
462
- }
463
- /**
464
- * Resolves a path relative to the current working directory
465
- */
466
- resolvePath(filePath, cwd) {
467
- if (path3.isAbsolute(filePath)) {
468
- return filePath;
469
- }
470
- return path3.join(cwd, filePath);
471
- }
472
- /**
473
- * Gets command history for a conversation
474
- */
475
- getCommandHistory(conversationId, limit) {
476
- const history = this.commandHistory.get(conversationId) || [];
477
- if (limit && limit > 0) {
478
- return history.slice(-limit);
479
- }
480
- return history;
481
- }
482
- /**
483
- * Clears command history for a conversation
484
- */
485
- clearCommandHistory(conversationId) {
486
- this.commandHistory.delete(conversationId);
487
- logger3.info(`Cleared command history for conversation: ${conversationId}`);
488
- }
489
- /**
490
- * Gets the current working directory
491
- * @param conversationId Optional conversation ID to get conversation-specific directory
492
- * @returns The current directory path
493
- */
494
- getCurrentDirectory(_conversationId) {
495
- return this.currentDirectory;
496
- }
497
- /**
498
- * Gets the allowed directory
499
- * @returns The allowed directory path
500
- */
501
- getAllowedDirectory() {
502
- return this.shellConfig.allowedDirectory;
503
- }
504
- };
505
-
506
- // src/actions/executeCommand.ts
507
- import {
508
- ModelType,
509
- composePromptFromState,
510
- parseJSONObjectFromText,
511
- logger as logger4
512
- } from "@elizaos/core";
513
- var commandExtractionTemplate = `# Extracting shell command from request
514
- {{recentMessages}}
515
-
516
- # Instructions: {{senderName}} wants to execute a shell command. Extract the COMPLETE shell command they want to run.
517
-
518
- IMPORTANT:
519
- 1. Always return the FULL executable shell command, not just the content or partial command.
520
- 2. If the user mentions installing something, create the appropriate brew/npm/apt command.
521
- 3. If the user directly provides a command (like "brew install X"), use it exactly as provided.
522
- 4. ALWAYS extract a command if the user is asking for ANY kind of system operation.
523
-
524
- Common patterns:
525
- - "run ls -la" -> command: "ls -la"
526
- - "execute npm test" -> command: "npm test"
527
- - "show me the files" or "list files" -> command: "ls -la"
528
- - "what's in this directory" -> command: "ls -la"
529
- - "check git status" -> command: "git status"
530
- - "navigate to src folder" -> command: "cd src"
531
- - "create a file called test.txt" -> command: "touch test.txt"
532
- - "write hello world to a file" -> command: "echo 'hello world' > file.txt"
533
- - "create hello.js with javascript code" -> command: "echo 'console.log("Hello, World!");' > hello.js"
534
- - "create hello_world.py and write a python hello world script inside" -> command: "echo 'print("Hello, World!")' > hello_world.py"
535
- - "make a new directory" -> command: "mkdir newdir"
536
- - "list files inside your filesystem" -> command: "ls -la"
537
- - "install orbstack" or "brew install orbstack" -> command: "brew install orbstack"
538
- - "install mullvad vpn" -> command: "brew install --cask mullvad-vpn"
539
- - "get system info" -> command: "system_profiler SPHardwareDataType"
540
- - "check memory usage" -> command: "vm_stat"
541
- - "install package" -> command: "brew install <package>"
542
-
543
- Special cases:
544
- - "Run it in your shell" or "execute it" -> Extract the command from previous context
545
- - "Install these" -> Look for package names in previous messages
546
- - Direct commands should be used exactly as provided
547
-
548
- Key rules:
549
- 1. For file creation with content, use: echo 'content' > filename
550
- 2. For listing files, use: ls -la (not just ls)
551
- 3. Always include the echo command when writing to files
552
- 4. Include all flags and arguments
553
- 5. When user says "run it", "execute it", or similar, they want you to run the command
554
-
555
- Your response must be formatted as a JSON block:
556
- \`\`\`json
557
- {
558
- "command": "<complete shell command to execute>"
559
- }
560
- \`\`\`
561
- `;
562
- var extractCommand = async (runtime, _message, state) => {
563
- const prompt = composePromptFromState({
564
- state,
565
- template: commandExtractionTemplate
566
- });
567
- for (let i = 0; i < 3; i++) {
568
- const response = await runtime.useModel(ModelType.TEXT_SMALL, {
569
- prompt
570
- });
571
- const parsedResponse = parseJSONObjectFromText(response);
572
- if (parsedResponse?.command) {
573
- return { command: parsedResponse.command };
574
- }
575
- }
576
- return null;
577
- };
578
- var executeCommand = {
579
- name: "EXECUTE_COMMAND",
580
- similes: [
581
- "RUN_COMMAND",
582
- "SHELL_COMMAND",
583
- "TERMINAL_COMMAND",
584
- "EXEC",
585
- "RUN",
586
- "EXECUTE",
587
- "CREATE_FILE",
588
- "WRITE_FILE",
589
- "MAKE_FILE",
590
- "INSTALL",
591
- "BREW_INSTALL",
592
- "NPM_INSTALL",
593
- "APT_INSTALL"
594
- ],
595
- description: "Execute ANY shell command in the terminal. Use this to run ANY command including: brew install, npm install, apt-get, system commands, file operations (create, write, delete), navigate directories, execute scripts, or perform any other shell operation. I CAN and SHOULD execute commands when asked. This includes brew, npm, git, ls, cd, echo, touch, cat, mkdir, system_profiler, and literally ANY other terminal command.",
596
- validate: async (runtime, message, _state) => {
597
- const shellService = runtime.getService("shell");
598
- if (!shellService) {
599
- return false;
600
- }
601
- const text = message.content.text?.toLowerCase() || "";
602
- const commandKeywords = [
603
- "run",
604
- "execute",
605
- "command",
606
- "shell",
607
- "install",
608
- "brew",
609
- "npm",
610
- "create",
611
- "file",
612
- "directory",
613
- "folder",
614
- "list",
615
- "show",
616
- "system",
617
- "info",
618
- "check",
619
- "status",
620
- "cd",
621
- "ls",
622
- "mkdir",
623
- "echo",
624
- "cat",
625
- "touch",
626
- "git",
627
- "build",
628
- "test"
629
- ];
630
- const hasCommandKeyword = commandKeywords.some((keyword) => text.includes(keyword));
631
- const hasDirectCommand = /^(brew|npm|apt|git|ls|cd|echo|cat|touch|mkdir|rm|mv|cp)\s/i.test(message.content.text || "");
632
- return hasCommandKeyword || hasDirectCommand;
633
- },
634
- handler: async (runtime, message, state, _options, callback) => {
635
- const shellService = runtime.getService("shell");
636
- if (!shellService) {
637
- await callback({
638
- text: "Shell service is not available.",
639
- source: message.content.source
640
- });
641
- return;
642
- }
643
- const commandInfo = await extractCommand(runtime, message, state);
644
- if (!commandInfo?.command) {
645
- logger4.error("Failed to extract command from message:", message.content.text);
646
- await callback({
647
- text: "I couldn't understand which command you want to execute. Please specify a shell command.",
648
- source: message.content.source
649
- });
650
- return;
651
- }
652
- logger4.info(`User request: "${message.content.text}"`);
653
- logger4.info(`Extracted command: "${commandInfo.command}"`);
654
- try {
655
- const conversationId = message.roomId || message.agentId;
656
- const result = await shellService.executeCommand(commandInfo.command, conversationId);
657
- let responseText = "";
658
- if (result.success) {
659
- responseText = `Command executed successfully in ${result.executedIn}
660
-
661
- `;
662
- if (result.stdout) {
663
- responseText += `Output:
664
- \`\`\`
665
- ${result.stdout}
666
- \`\`\``;
667
- } else {
668
- responseText += "Command completed with no output.";
669
- }
670
- } else {
671
- responseText = `Command failed with exit code ${result.exitCode} in ${result.executedIn}
672
-
673
- `;
674
- if (result.error) {
675
- responseText += `Error: ${result.error}
676
- `;
677
- }
678
- if (result.stderr) {
679
- responseText += `
680
- Error output:
681
- \`\`\`
682
- ${result.stderr}
683
- \`\`\``;
684
- }
685
- }
686
- const response = {
687
- text: responseText,
688
- source: message.content.source
689
- };
690
- await callback(response);
691
- } catch (error) {
692
- logger4.error("Error executing command:", error);
693
- await callback({
694
- text: `Failed to execute command: ${error instanceof Error ? error.message : "Unknown error"}`,
695
- source: message.content.source
696
- });
697
- }
698
- },
699
- examples: [
700
- [
701
- {
702
- name: "{{name1}}",
703
- content: {
704
- text: "run ls -la"
705
- }
706
- },
707
- {
708
- name: "{{name2}}",
709
- content: {
710
- text: "I'll execute that command for you.",
711
- actions: ["EXECUTE_COMMAND"]
712
- }
713
- }
714
- ],
715
- [
716
- {
717
- name: "{{name1}}",
718
- content: {
719
- text: "show me what files are in this directory"
720
- }
721
- },
722
- {
723
- name: "{{name2}}",
724
- content: {
725
- text: "I'll list the files in the current directory.",
726
- actions: ["EXECUTE_COMMAND"]
727
- }
728
- }
729
- ],
730
- [
731
- {
732
- name: "{{name1}}",
733
- content: {
734
- text: "navigate to the src folder"
735
- }
736
- },
737
- {
738
- name: "{{name2}}",
739
- content: {
740
- text: "I'll change to the src directory.",
741
- actions: ["EXECUTE_COMMAND"]
742
- }
743
- }
744
- ],
745
- [
746
- {
747
- name: "{{name1}}",
748
- content: {
749
- text: "check the git status"
750
- }
751
- },
752
- {
753
- name: "{{name2}}",
754
- content: {
755
- text: "I'll check the git repository status.",
756
- actions: ["EXECUTE_COMMAND"]
757
- }
758
- }
759
- ],
760
- [
761
- {
762
- name: "{{name1}}",
763
- content: {
764
- text: "create a file called hello.txt"
765
- }
766
- },
767
- {
768
- name: "{{name2}}",
769
- content: {
770
- text: "I'll create hello.txt for you.",
771
- actions: ["EXECUTE_COMMAND"]
772
- }
773
- }
774
- ],
775
- [
776
- {
777
- name: "{{name1}}",
778
- content: {
779
- text: "create hello_world.py and write a python hello world script inside"
780
- }
781
- },
782
- {
783
- name: "{{name2}}",
784
- content: {
785
- text: "I'll create hello_world.py with a Python hello world script.",
786
- actions: ["EXECUTE_COMMAND"]
787
- }
788
- }
789
- ],
790
- [
791
- {
792
- name: "{{name1}}",
793
- content: {
794
- text: "write some content to a file"
795
- }
796
- },
797
- {
798
- name: "{{name2}}",
799
- content: {
800
- text: "I'll write content to a file for you.",
801
- actions: ["EXECUTE_COMMAND"]
802
- }
803
- }
804
- ],
805
- [
806
- {
807
- name: "{{name1}}",
808
- content: {
809
- text: "brew install orbstack"
810
- }
811
- },
812
- {
813
- name: "{{name2}}",
814
- content: {
815
- text: "I'll install orbstack using brew.",
816
- actions: ["EXECUTE_COMMAND"]
817
- }
773
+ setTimeout(() => {
774
+ if (!child.killed) {
775
+ child.kill("SIGKILL");
776
+ }
777
+ }, 5000);
778
+ }, this.shellConfig.timeout);
779
+ if (child.stdout) {
780
+ child.stdout.on("data", (data) => {
781
+ stdout += data.toString();
782
+ });
818
783
  }
819
- ],
820
- [
821
- {
822
- name: "{{name1}}",
823
- content: {
824
- text: "install mullvad vpn"
825
- }
826
- },
827
- {
828
- name: "{{name2}}",
829
- content: {
830
- text: "I'll install Mullvad VPN for you.",
831
- actions: ["EXECUTE_COMMAND"]
832
- }
784
+ if (child.stderr) {
785
+ child.stderr.on("data", (data) => {
786
+ stderr += data.toString();
787
+ });
833
788
  }
834
- ],
835
- [
836
- {
837
- name: "{{name1}}",
838
- content: {
839
- text: "run the command brew install --cask docker"
840
- }
841
- },
842
- {
843
- name: "{{name2}}",
844
- content: {
845
- text: "I'll run that brew install command for you.",
846
- actions: ["EXECUTE_COMMAND"]
789
+ child.on("exit", (code) => {
790
+ clearTimeout(timeout);
791
+ if (timedOut) {
792
+ resolve({
793
+ success: false,
794
+ stdout,
795
+ stderr: `${stderr}
796
+ Command timed out`,
797
+ exitCode: code,
798
+ error: "Command execution timeout",
799
+ executedIn: this.currentDirectory
800
+ });
801
+ return;
847
802
  }
848
- }
849
- ]
850
- ]
851
- };
852
-
853
- // src/actions/clearHistory.ts
854
- import {
855
- logger as logger5
856
- } from "@elizaos/core";
857
- var clearHistory = {
858
- name: "CLEAR_SHELL_HISTORY",
859
- similes: ["RESET_SHELL", "CLEAR_TERMINAL", "CLEAR_HISTORY", "RESET_HISTORY"],
860
- description: "Clears the recorded history of shell commands for the current conversation",
861
- validate: async (runtime, message, _state) => {
862
- const shellService = runtime.getService("shell");
863
- if (!shellService) {
864
- return false;
865
- }
866
- const text = message.content.text?.toLowerCase() || "";
867
- const clearKeywords = ["clear", "reset", "delete", "remove", "clean"];
868
- const historyKeywords = ["history", "terminal", "shell", "command"];
869
- const hasClearKeyword = clearKeywords.some((keyword) => text.includes(keyword));
870
- const hasHistoryKeyword = historyKeywords.some((keyword) => text.includes(keyword));
871
- return hasClearKeyword && hasHistoryKeyword;
872
- },
873
- handler: async (runtime, message, _state, _options, callback) => {
874
- const shellService = runtime.getService("shell");
875
- if (!shellService) {
876
- await callback({
877
- text: "Shell service is not available.",
878
- source: message.content.source
803
+ resolve({
804
+ success: code === 0,
805
+ stdout,
806
+ stderr,
807
+ exitCode: code,
808
+ executedIn: this.currentDirectory
809
+ });
810
+ });
811
+ child.on("error", (err) => {
812
+ clearTimeout(timeout);
813
+ resolve({
814
+ success: false,
815
+ stdout,
816
+ stderr: err.message,
817
+ exitCode: 1,
818
+ error: "Failed to execute command",
819
+ executedIn: this.currentDirectory
820
+ });
879
821
  });
822
+ });
823
+ }
824
+ addToHistory(conversationId, command, result, fileOperations) {
825
+ if (!conversationId)
880
826
  return;
827
+ const historyEntry = {
828
+ command,
829
+ stdout: result.stdout,
830
+ stderr: result.stderr,
831
+ exitCode: result.exitCode,
832
+ timestamp: Date.now(),
833
+ workingDirectory: result.executedIn,
834
+ fileOperations
835
+ };
836
+ if (!this.commandHistory.has(conversationId)) {
837
+ this.commandHistory.set(conversationId, []);
881
838
  }
882
- try {
883
- const conversationId = message.roomId || message.agentId;
884
- shellService.clearCommandHistory(conversationId);
885
- logger5.info(`Cleared shell history for conversation: ${conversationId}`);
886
- const response = {
887
- text: "Shell command history has been cleared.",
888
- source: message.content.source
889
- };
890
- await callback(response);
891
- } catch (error) {
892
- logger5.error("Error clearing shell history:", error);
893
- await callback({
894
- text: `Failed to clear shell history: ${error instanceof Error ? error.message : "Unknown error"}`,
895
- source: message.content.source
896
- });
839
+ const history = this.commandHistory.get(conversationId);
840
+ if (!history) {
841
+ throw new Error(`No history found for conversation ${conversationId}`);
897
842
  }
898
- },
899
- examples: [
900
- [
901
- {
902
- name: "{{name1}}",
903
- content: {
904
- text: "clear my shell history"
905
- }
906
- },
907
- {
908
- name: "{{name2}}",
909
- content: {
910
- text: "Shell command history has been cleared.",
911
- actions: ["CLEAR_SHELL_HISTORY"]
912
- }
913
- }
914
- ],
915
- [
916
- {
917
- name: "{{name1}}",
918
- content: {
919
- text: "reset the terminal history"
920
- }
921
- },
922
- {
923
- name: "{{name2}}",
924
- content: {
925
- text: "Shell command history has been cleared.",
926
- actions: ["CLEAR_SHELL_HISTORY"]
927
- }
928
- }
929
- ],
930
- [
931
- {
932
- name: "{{name1}}",
933
- content: {
934
- text: "delete command history"
935
- }
936
- },
937
- {
938
- name: "{{name2}}",
939
- content: {
940
- text: "Shell command history has been cleared.",
941
- actions: ["CLEAR_SHELL_HISTORY"]
942
- }
843
+ history.push(historyEntry);
844
+ if (history.length > this.maxHistoryPerConversation) {
845
+ history.shift();
846
+ }
847
+ }
848
+ detectFileOperations(command, cwd) {
849
+ const operations = [];
850
+ const parts = command.trim().split(/\s+/);
851
+ const cmd = parts[0].toLowerCase();
852
+ if (cmd === "touch" && parts.length > 1) {
853
+ operations.push({
854
+ type: "create",
855
+ target: this.resolvePath(parts[1], cwd)
856
+ });
857
+ } else if (cmd === "echo" && command.includes(">")) {
858
+ const match = command.match(/>\s*([^\s]+)$/);
859
+ if (match) {
860
+ operations.push({
861
+ type: "write",
862
+ target: this.resolvePath(match[1], cwd)
863
+ });
943
864
  }
944
- ]
945
- ]
946
- };
947
-
948
- // src/providers/shellHistoryProvider.ts
949
- import {
950
- addHeader,
951
- logger as logger6
952
- } from "@elizaos/core";
953
- var MAX_OUTPUT_LENGTH = 8e3;
954
- var TRUNCATE_SEGMENT_LENGTH = 4e3;
955
- var shellHistoryProvider = {
956
- name: "SHELL_HISTORY",
957
- description: "Provides recent shell command history, current working directory, and file operations within the restricted environment",
958
- position: 99,
959
- get: async (runtime, message, _state) => {
960
- const shellService = runtime.getService("shell");
961
- if (!shellService) {
962
- logger6.warn("[shellHistoryProvider] Shell service not found");
963
- return {
964
- values: {
965
- shellHistory: "Shell service is not available",
966
- currentWorkingDirectory: "N/A",
967
- allowedDirectory: "N/A"
968
- },
969
- text: addHeader("# Shell Status", "Shell service is not available"),
970
- data: { history: [], cwd: "N/A", allowedDir: "N/A" }
971
- };
865
+ } else if (cmd === "mkdir" && parts.length > 1) {
866
+ operations.push({
867
+ type: "mkdir",
868
+ target: this.resolvePath(parts[1], cwd)
869
+ });
870
+ } else if (cmd === "cat" && parts.length > 1 && !command.includes(">")) {
871
+ operations.push({
872
+ type: "read",
873
+ target: this.resolvePath(parts[1], cwd)
874
+ });
875
+ } else if (cmd === "mv" && parts.length > 2) {
876
+ operations.push({
877
+ type: "move",
878
+ target: this.resolvePath(parts[1], cwd),
879
+ secondaryTarget: this.resolvePath(parts[2], cwd)
880
+ });
881
+ } else if (cmd === "cp" && parts.length > 2) {
882
+ operations.push({
883
+ type: "copy",
884
+ target: this.resolvePath(parts[1], cwd),
885
+ secondaryTarget: this.resolvePath(parts[2], cwd)
886
+ });
972
887
  }
973
- const conversationId = message.roomId || message.agentId;
974
- const history = shellService.getCommandHistory(conversationId, 10);
975
- const cwd = shellService.getCurrentDirectory(conversationId);
976
- const allowedDir = shellService.getAllowedDirectory();
977
- let historyText = "No commands in history.";
978
- if (history.length > 0) {
979
- historyText = history.map((entry) => {
980
- let entryStr = `[${new Date(entry.timestamp).toISOString()}] ${entry.workingDirectory}> ${entry.command}`;
981
- if (entry.stdout) {
982
- if (entry.stdout.length > MAX_OUTPUT_LENGTH) {
983
- entryStr += `
984
- Output: ${entry.stdout.substring(0, TRUNCATE_SEGMENT_LENGTH)}
985
- ... [TRUNCATED] ...
986
- ${entry.stdout.substring(entry.stdout.length - TRUNCATE_SEGMENT_LENGTH)}`;
987
- } else {
988
- entryStr += `
989
- Output: ${entry.stdout}`;
990
- }
991
- }
992
- if (entry.stderr) {
993
- if (entry.stderr.length > MAX_OUTPUT_LENGTH) {
994
- entryStr += `
995
- Error: ${entry.stderr.substring(0, TRUNCATE_SEGMENT_LENGTH)}
996
- ... [TRUNCATED] ...
997
- ${entry.stderr.substring(entry.stderr.length - TRUNCATE_SEGMENT_LENGTH)}`;
998
- } else {
999
- entryStr += `
1000
- Error: ${entry.stderr}`;
1001
- }
1002
- }
1003
- entryStr += `
1004
- Exit Code: ${entry.exitCode}`;
1005
- if (entry.fileOperations && entry.fileOperations.length > 0) {
1006
- entryStr += "\n File Operations:";
1007
- entry.fileOperations.forEach((op) => {
1008
- if (op.secondaryTarget) {
1009
- entryStr += `
1010
- - ${op.type}: ${op.target} \u2192 ${op.secondaryTarget}`;
1011
- } else {
1012
- entryStr += `
1013
- - ${op.type}: ${op.target}`;
1014
- }
1015
- });
1016
- }
1017
- return entryStr;
1018
- }).join("\n\n");
888
+ return operations.length > 0 ? operations : undefined;
889
+ }
890
+ resolvePath(filePath, cwd) {
891
+ if (path3.isAbsolute(filePath)) {
892
+ return filePath;
1019
893
  }
1020
- const recentFileOps = history.filter((entry) => entry.fileOperations && entry.fileOperations.length > 0).flatMap((entry) => entry.fileOperations).slice(-5);
1021
- let fileOpsText = "";
1022
- if (recentFileOps.length > 0) {
1023
- fileOpsText = "\n\n" + addHeader(
1024
- "# Recent File Operations",
1025
- recentFileOps.map((op) => {
1026
- if (op.secondaryTarget) {
1027
- return `- ${op.type}: ${op.target} \u2192 ${op.secondaryTarget}`;
1028
- }
1029
- return `- ${op.type}: ${op.target}`;
1030
- }).join("\n")
1031
- );
894
+ return path3.join(cwd, filePath);
895
+ }
896
+ getCommandHistory(conversationId, limit) {
897
+ const history = this.commandHistory.get(conversationId) || [];
898
+ if (limit && limit > 0) {
899
+ return history.slice(-limit);
1032
900
  }
1033
- const text = `Current Directory: ${cwd}
1034
- Allowed Directory: ${allowedDir}
1035
-
1036
- ${addHeader("# Shell History (Last 10)", historyText)}${fileOpsText}`;
1037
- return {
1038
- values: {
1039
- shellHistory: historyText,
1040
- currentWorkingDirectory: cwd,
1041
- allowedDirectory: allowedDir,
1042
- recentFileOperations: recentFileOps
1043
- },
1044
- text,
1045
- data: {
1046
- history,
1047
- cwd,
1048
- allowedDir,
1049
- fileOperations: recentFileOps
1050
- }
1051
- };
901
+ return history;
1052
902
  }
1053
- };
903
+ clearCommandHistory(conversationId) {
904
+ this.commandHistory.delete(conversationId);
905
+ logger6.info(`Cleared command history for conversation: ${conversationId}`);
906
+ }
907
+ getCurrentDirectory(_conversationId) {
908
+ return this.currentDirectory;
909
+ }
910
+ getAllowedDirectory() {
911
+ return this.shellConfig.allowedDirectory;
912
+ }
913
+ }
1054
914
 
1055
- // src/index.ts
915
+ // index.ts
1056
916
  var shellPlugin = {
1057
917
  name: "shell",
1058
918
  description: "Execute shell commands within a restricted directory with history tracking",
@@ -1060,12 +920,20 @@ var shellPlugin = {
1060
920
  actions: [executeCommand, clearHistory],
1061
921
  providers: [shellHistoryProvider]
1062
922
  };
1063
- var index_default = shellPlugin;
923
+ var typescript_default = shellPlugin;
1064
924
  export {
1065
- ShellService,
1066
- index_default as default,
1067
- executeCommand,
925
+ validatePath,
926
+ shellPlugin,
927
+ shellHistoryProvider,
1068
928
  loadShellConfig,
1069
- shellPlugin
929
+ isSafeCommand,
930
+ isForbiddenCommand,
931
+ extractBaseCommand,
932
+ executeCommand,
933
+ typescript_default as default,
934
+ clearHistory,
935
+ ShellService,
936
+ DEFAULT_FORBIDDEN_COMMANDS
1070
937
  };
1071
- //# sourceMappingURL=index.js.map
938
+
939
+ //# debugId=660EC61651C6DFA564756E2164756E21