@elizaos/plugin-shell 1.2.0 → 2.0.0-alpha.1
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/actions/clearHistory.d.ts +4 -0
- package/dist/actions/clearHistory.d.ts.map +1 -0
- package/dist/actions/executeCommand.d.ts +6 -0
- package/dist/actions/executeCommand.d.ts.map +1 -0
- package/dist/actions/index.d.ts +3 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/build.d.ts +2 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/generated/prompts/typescript/prompts.d.ts +12 -0
- package/dist/generated/prompts/typescript/prompts.d.ts.map +1 -0
- package/dist/generated/specs/spec-helpers.d.ts +49 -0
- package/dist/generated/specs/spec-helpers.d.ts.map +1 -0
- package/dist/generated/specs/specs.d.ts +83 -0
- package/dist/generated/specs/specs.d.ts.map +1 -0
- package/dist/index.browser.d.ts +4 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.d.ts +10 -106
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +779 -911
- package/dist/index.js.map +19 -1
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/shellHistoryProvider.d.ts +4 -0
- package/dist/providers/shellHistoryProvider.d.ts.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/shellService.d.ts +24 -0
- package/dist/services/shellService.d.ts.map +1 -0
- package/dist/types/index.d.ts +30 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/pathUtils.d.ts +5 -0
- package/dist/utils/pathUtils.d.ts.map +1 -0
- package/package.json +73 -19
- package/LICENSE +0 -21
- package/README.md +0 -352
package/dist/index.js
CHANGED
|
@@ -1,213 +1,656 @@
|
|
|
1
|
-
//
|
|
1
|
+
// actions/clearHistory.ts
|
|
2
2
|
import {
|
|
3
|
-
|
|
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
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
|
129
|
+
return spec;
|
|
92
130
|
}
|
|
93
131
|
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
if (!
|
|
103
|
-
|
|
104
|
-
`Path validation failed: ${normalizedPath} is outside allowed directory ${normalizedAllowed}`
|
|
105
|
-
);
|
|
106
|
-
return null;
|
|
132
|
+
// actions/clearHistory.ts
|
|
133
|
+
var spec = requireActionSpec("CLEAR_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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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("shellHistoryProvider");
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
923
|
+
var typescript_default = shellPlugin;
|
|
1064
924
|
export {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
925
|
+
validatePath,
|
|
926
|
+
shellPlugin,
|
|
927
|
+
shellHistoryProvider,
|
|
1068
928
|
loadShellConfig,
|
|
1069
|
-
|
|
929
|
+
isSafeCommand,
|
|
930
|
+
isForbiddenCommand,
|
|
931
|
+
extractBaseCommand,
|
|
932
|
+
executeCommand,
|
|
933
|
+
typescript_default as default,
|
|
934
|
+
clearHistory,
|
|
935
|
+
ShellService,
|
|
936
|
+
DEFAULT_FORBIDDEN_COMMANDS
|
|
1070
937
|
};
|
|
1071
|
-
|
|
938
|
+
|
|
939
|
+
//# debugId=1AD79E57D2FAFA7D64756E2164756E21
|