@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/LICENSE +21 -0
- package/README.md +352 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +1071 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
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
|