@elizaos/plugin-shell 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1071 @@
1
+ // src/services/shellService.ts
2
+ import {
3
+ Service,
4
+ logger as logger3
5
+ } from "@elizaos/core";
6
+ import spawn from "cross-spawn";
7
+ import path3 from "path";
8
+
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;
86
+ }
87
+ }
88
+ if (!enabled) {
89
+ logger.info("Shell plugin is disabled. Set SHELL_ENABLED=true to enable.");
90
+ }
91
+ return value;
92
+ }
93
+
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;
107
+ }
108
+ return normalizedPath;
109
+ } catch (error) {
110
+ logger2.error("Error validating path:", error);
111
+ return null;
112
+ }
113
+ }
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;
143
+ }
144
+ }
145
+ for (const pattern of dangerousPatterns) {
146
+ if (pattern.test(command)) {
147
+ logger2.warn(`Dangerous pattern detected in command: ${command}`);
148
+ return false;
149
+ }
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;
168
+ }
169
+ if (!forbidden.includes(" ")) {
170
+ const baseCommand = extractBaseCommand(command);
171
+ if (baseCommand.toLowerCase() === forbiddenLower) {
172
+ return true;
173
+ }
174
+ }
175
+ return false;
176
+ });
177
+ }
178
+
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");
201
+ }
202
+ get capabilityDescription() {
203
+ return "Execute shell commands within a restricted directory with history tracking";
204
+ }
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
+ async executeCommand(command, conversationId) {
212
+ if (!this.shellConfig.enabled) {
213
+ return {
214
+ success: false,
215
+ stdout: "",
216
+ stderr: "Shell plugin is disabled. Set SHELL_ENABLED=true to enable.",
217
+ exitCode: 1,
218
+ error: "Shell plugin disabled",
219
+ executedIn: this.currentDirectory
220
+ };
221
+ }
222
+ if (!command || typeof command !== "string") {
223
+ return {
224
+ success: false,
225
+ stdout: "",
226
+ stderr: "Invalid command",
227
+ exitCode: 1,
228
+ error: "Command must be a non-empty string",
229
+ executedIn: this.currentDirectory
230
+ };
231
+ }
232
+ const trimmedCommand = command.trim();
233
+ if (!isSafeCommand(trimmedCommand)) {
234
+ return {
235
+ success: false,
236
+ stdout: "",
237
+ stderr: "Command contains forbidden patterns",
238
+ exitCode: 1,
239
+ error: "Security policy violation",
240
+ executedIn: this.currentDirectory
241
+ };
242
+ }
243
+ if (isForbiddenCommand(trimmedCommand, this.shellConfig.forbiddenCommands)) {
244
+ return {
245
+ success: false,
246
+ stdout: "",
247
+ stderr: `Command is forbidden by security policy`,
248
+ exitCode: 1,
249
+ error: "Forbidden command",
250
+ executedIn: this.currentDirectory
251
+ };
252
+ }
253
+ if (trimmedCommand.startsWith("cd ")) {
254
+ const result2 = await this.handleCdCommand(trimmedCommand);
255
+ this.addToHistory(conversationId, trimmedCommand, result2);
256
+ return result2;
257
+ }
258
+ const result = await this.runCommand(trimmedCommand);
259
+ if (result.success) {
260
+ const fileOps = this.detectFileOperations(trimmedCommand, this.currentDirectory);
261
+ if (fileOps && conversationId) {
262
+ this.addToHistory(conversationId, trimmedCommand, result, fileOps);
263
+ } else {
264
+ this.addToHistory(conversationId, trimmedCommand, result);
265
+ }
266
+ } else {
267
+ this.addToHistory(conversationId, trimmedCommand, result);
268
+ }
269
+ return result;
270
+ }
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
+ async handleCdCommand(command) {
277
+ const parts = command.split(/\s+/);
278
+ if (parts.length < 2) {
279
+ this.currentDirectory = this.shellConfig.allowedDirectory;
280
+ return {
281
+ success: true,
282
+ stdout: `Changed directory to: ${this.currentDirectory}`,
283
+ stderr: "",
284
+ exitCode: 0,
285
+ executedIn: this.currentDirectory
286
+ };
287
+ }
288
+ const targetPath = parts.slice(1).join(" ");
289
+ const validatedPath = validatePath(
290
+ targetPath,
291
+ this.shellConfig.allowedDirectory,
292
+ this.currentDirectory
293
+ );
294
+ if (!validatedPath) {
295
+ return {
296
+ success: false,
297
+ stdout: "",
298
+ stderr: "Cannot navigate outside allowed directory",
299
+ exitCode: 1,
300
+ error: "Permission denied",
301
+ executedIn: this.currentDirectory
302
+ };
303
+ }
304
+ this.currentDirectory = validatedPath;
305
+ return {
306
+ success: true,
307
+ stdout: `Changed directory to: ${this.currentDirectory}`,
308
+ stderr: "",
309
+ exitCode: 0,
310
+ executedIn: this.currentDirectory
311
+ };
312
+ }
313
+ /**
314
+ * Runs a command using cross-spawn
315
+ * @param command The command to run
316
+ * @returns The command result
317
+ */
318
+ async runCommand(command) {
319
+ return new Promise((resolve) => {
320
+ const useShell = command.includes(">") || command.includes("<") || command.includes("|");
321
+ let cmd;
322
+ let args;
323
+ if (useShell) {
324
+ cmd = "sh";
325
+ args = ["-c", command];
326
+ logger3.info(`Executing shell command: sh -c "${command}" in ${this.currentDirectory}`);
327
+ } else {
328
+ const parts = command.split(/\s+/);
329
+ cmd = parts[0];
330
+ args = parts.slice(1);
331
+ logger3.info(`Executing command: ${cmd} ${args.join(" ")} in ${this.currentDirectory}`);
332
+ }
333
+ let stdout = "";
334
+ let stderr = "";
335
+ let timedOut = false;
336
+ const child = spawn(cmd, args, {
337
+ cwd: this.currentDirectory,
338
+ env: process.env,
339
+ // Only use shell: false for direct commands, not for sh -c
340
+ shell: false
341
+ });
342
+ const timeout = setTimeout(() => {
343
+ timedOut = true;
344
+ 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
+ }
818
+ }
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
+ }
833
+ }
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"]
847
+ }
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
879
+ });
880
+ return;
881
+ }
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
+ });
897
+ }
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
+ }
943
+ }
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
+ };
972
+ }
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");
1019
+ }
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
+ );
1032
+ }
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
+ };
1052
+ }
1053
+ };
1054
+
1055
+ // src/index.ts
1056
+ var shellPlugin = {
1057
+ name: "shell",
1058
+ description: "Execute shell commands within a restricted directory with history tracking",
1059
+ services: [ShellService],
1060
+ actions: [executeCommand, clearHistory],
1061
+ providers: [shellHistoryProvider]
1062
+ };
1063
+ var index_default = shellPlugin;
1064
+ export {
1065
+ ShellService,
1066
+ index_default as default,
1067
+ executeCommand,
1068
+ loadShellConfig,
1069
+ shellPlugin
1070
+ };
1071
+ //# sourceMappingURL=index.js.map