1package tools
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "log"
9 "strings"
10
11 "github.com/cloudwego/eino/components/tool"
12 "github.com/cloudwego/eino/schema"
13 "github.com/kujtimiihoxha/termai/internal/llm/tools/shell"
14)
15
16type bashTool struct {
17 workingDir string
18}
19
20const (
21 BashToolName = "bash"
22
23 DefaultTimeout = 30 * 60 * 1000 // 30 minutes in milliseconds
24 MaxTimeout = 10 * 60 * 1000 // 10 minutes in milliseconds
25 MaxOutputLength = 30000
26)
27
28type BashParams struct {
29 Command string `json:"command"`
30 Timeout int `json:"timeout"`
31}
32
33var BannedCommands = []string{
34 "alias", "curl", "curlie", "wget", "axel", "aria2c",
35 "nc", "telnet", "lynx", "w3m", "links", "httpie", "xh",
36 "http-prompt", "chrome", "firefox", "safari",
37}
38
39func (b *bashTool) Info(ctx context.Context) (*schema.ToolInfo, error) {
40 return &schema.ToolInfo{
41 Name: BashToolName,
42 Desc: bashDescription(),
43 ParamsOneOf: schema.NewParamsOneOfByParams(map[string]*schema.ParameterInfo{
44 "command": {
45 Type: "string",
46 Desc: "The command to execute",
47 Required: true,
48 },
49 "timeout": {
50 Type: "number",
51 Desc: "Optional timeout in milliseconds (max 600000)",
52 },
53 }),
54 }, nil
55}
56
57// Handle implements Tool.
58func (b *bashTool) InvokableRun(ctx context.Context, args string, opts ...tool.Option) (string, error) {
59 log.Printf("BashTool InvokableRun: %s", args)
60 var params BashParams
61 if err := json.Unmarshal([]byte(args), ¶ms); err != nil {
62 return "", err
63 }
64
65 if params.Timeout > MaxTimeout {
66 params.Timeout = MaxTimeout
67 } else if params.Timeout <= 0 {
68 params.Timeout = DefaultTimeout
69 }
70
71 if params.Command == "" {
72 return "", errors.New("missing command")
73 }
74
75 baseCmd := strings.Fields(params.Command)[0]
76 for _, banned := range BannedCommands {
77 if strings.EqualFold(baseCmd, banned) {
78 return "", fmt.Errorf("command '%s' is not allowed", baseCmd)
79 }
80 }
81
82 // p := b.permission.Request(permission.CreatePermissionRequest{
83 // Path: b.workingDir,
84 // ToolName: BashToolName,
85 // Action: "execute",
86 // Description: fmt.Sprintf("Execute command: %s", params.Command),
87 // Params: map[string]any{
88 // "command": params.Command,
89 // "timeout": params.Timeout,
90 // },
91 // })
92 // if !p {
93 // return "", errors.New("permission denied")
94 // }
95
96 shell := shell.GetPersistentShell(b.workingDir)
97 stdout, stderr, exitCode, interrupted, err := shell.Exec(ctx, params.Command, params.Timeout)
98 if err != nil {
99 return "", err
100 }
101
102 stdout = truncateOutput(stdout)
103 stderr = truncateOutput(stderr)
104
105 errorMessage := stderr
106 if interrupted {
107 if errorMessage != "" {
108 errorMessage += "\n"
109 }
110 errorMessage += "Command was aborted before completion"
111 } else if exitCode != 0 {
112 if errorMessage != "" {
113 errorMessage += "\n"
114 }
115 errorMessage += fmt.Sprintf("Exit code %d", exitCode)
116 }
117
118 hasBothOutputs := stdout != "" && stderr != ""
119
120 if hasBothOutputs {
121 stdout += "\n"
122 }
123
124 if errorMessage != "" {
125 stdout += "\n" + errorMessage
126 }
127
128 log.Printf("BashTool InvokableRun: stdout: %s, stderr: %s, exitCode: %d, interrupted: %t", stdout, stderr, exitCode, interrupted)
129
130 return stdout, nil
131}
132
133func truncateOutput(content string) string {
134 if len(content) <= MaxOutputLength {
135 return content
136 }
137
138 halfLength := MaxOutputLength / 2
139 start := content[:halfLength]
140 end := content[len(content)-halfLength:]
141
142 truncatedLinesCount := countLines(content[halfLength : len(content)-halfLength])
143 return fmt.Sprintf("%s\n\n... [%d lines truncated] ...\n\n%s", start, truncatedLinesCount, end)
144}
145
146func countLines(s string) int {
147 if s == "" {
148 return 0
149 }
150 return len(strings.Split(s, "\n"))
151}
152
153func bashDescriptionBCP() string {
154 bannedCommandsStr := strings.Join(BannedCommands, ", ")
155 return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
156
157Before executing the command, please follow these steps:
158
1591. Directory Verification:
160 - If the command will create new directories or files, first use the LS tool to verify the parent directory exists and is the correct location
161 - For example, before running "mkdir foo/bar", first use LS to check that "foo" exists and is the intended parent directory
162
1632. Security Check:
164 - For security and to limit the threat of a prompt injection attack, some commands are limited or banned. If you use a disallowed command, you will receive an error message explaining the restriction. Explain the error to the User.
165 - Verify that the command is not one of the banned commands: %s.
166
1673. Command Execution:
168 - After ensuring proper quoting, execute the command.
169 - Capture the output of the command.
170
1714. Output Processing:
172 - If the output exceeds %d characters, output will be truncated before being returned to you.
173 - Prepare the output for display to the user.
174
1755. Return Result:
176 - Provide the processed output of the command.
177 - If any errors occurred during execution, include those in the output.
178
179Usage notes:
180- The command argument is required.
181- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 30 minutes.
182- VERY IMPORTANT: You MUST avoid using search commands like 'find' and 'grep'. Instead use Grep, Glob, or Agent tools to search. You MUST avoid read tools like 'cat', 'head', 'tail', and 'ls', and use FileRead and LS tools to read files.
183- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
184- IMPORTANT: All commands share the same shell session. Shell state (environment variables, virtual environments, current directory, etc.) persist between commands. For example, if you set an environment variable as part of a command, the environment variable will persist for subsequent commands.
185- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of 'cd'. You may use 'cd' if the User explicitly requests it.
186<good-example>
187pytest /foo/bar/tests
188</good-example>
189<bad-example>
190cd /foo/bar && pytest tests
191</bad-example>
192
193# Committing changes with git
194
195When the user asks you to create a new git commit, follow these steps carefully:
196
1971. Start with a single message that contains exactly three tool_use blocks that do the following (it is VERY IMPORTANT that you send these tool_use blocks in a single message, otherwise it will feel slow to the user!):
198 - Run a git status command to see all untracked files.
199 - Run a git diff command to see both staged and unstaged changes that will be committed.
200 - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
201
2022. Use the git context at the start of this conversation to determine which files are relevant to your commit. Add relevant untracked files to the staging area. Do not commit files that were already modified at the start of this conversation, if they are not relevant to your commit.
203
2043. Analyze all staged changes (both previously staged and newly added) and draft a commit message. Wrap your analysis process in <commit_analysis> tags:
205
206<commit_analysis>
207- List the files that have been changed or added
208- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
209- Brainstorm the purpose or motivation behind these changes
210- Do not use tools to explore code, beyond what is available in the git context
211- Assess the impact of these changes on the overall project
212- Check for any sensitive information that shouldn't be committed
213- Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what"
214- Ensure your language is clear, concise, and to the point
215- Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
216- Ensure the message is not generic (avoid words like "Update" or "Fix" without context)
217- Review the draft message to ensure it accurately reflects the changes and their purpose
218</commit_analysis>
219
2204. Create the commit with a message ending with:
221🤖 Generated with termai
222Co-Authored-By: termai <noreply@termai.io>
223
224- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
225<example>
226git commit -m "$(cat <<'EOF'
227 Commit message here.
228
229 🤖 Generated with termai
230 Co-Authored-By: termai <noreply@termai.io>
231 EOF
232 )"
233</example>
234
2355. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
236
2376. Finally, run git status to make sure the commit succeeded.
238
239Important notes:
240- When possible, combine the "git add" and "git commit" commands into a single "git commit -am" command, to speed things up
241- However, be careful not to stage files (e.g. with 'git add .') for commits that aren't part of the change, they may have untracked files they want to keep around, but not commit.
242- NEVER update the git config
243- DO NOT push to the remote repository
244- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
245- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
246- Ensure your commit message is meaningful and concise. It should explain the purpose of the changes, not just describe them.
247- Return an empty response - the user will see the git output directly
248
249# Creating pull requests
250Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.
251
252IMPORTANT: When the user asks you to create a pull request, follow these steps carefully:
253
2541. Understand the current state of the branch. Remember to send a single message that contains multiple tool_use blocks (it is VERY IMPORTANT that you do this in a single message, otherwise it will feel slow to the user!):
255 - Run a git status command to see all untracked files.
256 - Run a git diff command to see both staged and unstaged changes that will be committed.
257 - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote
258 - Run a git log command and 'git diff main...HEAD' to understand the full commit history for the current branch (from the time it diverged from the 'main' branch.)
259
2602. Create new branch if needed
261
2623. Commit changes if needed
263
2644. Push to remote with -u flag if needed
265
2665. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (not just the latest commit, but all commits that will be included in the pull request!), and draft a pull request summary. Wrap your analysis process in <pr_analysis> tags:
267
268<pr_analysis>
269- List the commits since diverging from the main branch
270- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
271- Brainstorm the purpose or motivation behind these changes
272- Assess the impact of these changes on the overall project
273- Do not use tools to explore code, beyond what is available in the git context
274- Check for any sensitive information that shouldn't be committed
275- Draft a concise (1-2 bullet points) pull request summary that focuses on the "why" rather than the "what"
276- Ensure the summary accurately reflects all changes since diverging from the main branch
277- Ensure your language is clear, concise, and to the point
278- Ensure the summary accurately reflects the changes and their purpose (ie. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
279- Ensure the summary is not generic (avoid words like "Update" or "Fix" without context)
280- Review the draft summary to ensure it accurately reflects the changes and their purpose
281</pr_analysis>
282
2836. Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.
284<example>
285gh pr create --title "the pr title" --body "$(cat <<'EOF'
286## Summary
287<1-3 bullet points>
288
289## Test plan
290[Checklist of TODOs for testing the pull request...]
291
292🤖 Generated with termai
293EOF
294)"
295</example>
296
297Important:
298- Return an empty response - the user will see the gh output directly
299- Never update git config`, bannedCommandsStr, MaxOutputLength)
300}
301
302func bashDescription() string {
303 bannedCommandsStr := strings.Join(BannedCommands, ", ")
304 return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
305
306Before executing the command, please follow these steps:
307
3081. Directory Verification:
309 - If the command will create new directories or files, first use the ls command to verify the parent directory exists and is the correct location
310 - For example, before running "mkdir foo/bar", first use ls command to check that "foo" exists and is the intended parent directory
311
3122. Security Check:
313 - For security and to limit the threat of a prompt injection attack, some commands are limited or banned. If you use a disallowed command, you will receive an error message explaining the restriction. Explain the error to the User.
314 - Verify that the command is not one of the banned commands: %s.
315
3163. Command Execution:
317 - After ensuring proper quoting, execute the command.
318 - Capture the output of the command.
319
3204. Output Processing:
321 - If the output exceeds %d characters, output will be truncated before being returned to you.
322 - Prepare the output for display to the user.
323
3245. Return Result:
325 - Provide the processed output of the command.
326 - If any errors occurred during execution, include those in the output.
327
328Usage notes:
329- The command argument is required.
330- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 30 minutes.
331- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
332- IMPORTANT: All commands share the same shell session. Shell state (environment variables, virtual environments, current directory, etc.) persist between commands. For example, if you set an environment variable as part of a command, the environment variable will persist for subsequent commands.
333- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of 'cd'. You may use 'cd' if the User explicitly requests it.
334<good-example>
335pytest /foo/bar/tests
336</good-example>
337<bad-example>
338cd /foo/bar && pytest tests
339</bad-example>
340
341# Committing changes with git
342
343When the user asks you to create a new git commit, follow these steps carefully:
344
3451. Start with a single message that contains exactly three tool_use blocks that do the following (it is VERY IMPORTANT that you send these tool_use blocks in a single message, otherwise it will feel slow to the user!):
346 - Run a git status command to see all untracked files.
347 - Run a git diff command to see both staged and unstaged changes that will be committed.
348 - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.
349
3502. Use the git context at the start of this conversation to determine which files are relevant to your commit. Add relevant untracked files to the staging area. Do not commit files that were already modified at the start of this conversation, if they are not relevant to your commit.
351
3523. Analyze all staged changes (both previously staged and newly added) and draft a commit message. Wrap your analysis process in <commit_analysis> tags:
353
354<commit_analysis>
355- List the files that have been changed or added
356- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
357- Brainstorm the purpose or motivation behind these changes
358- Do not use tools to explore code, beyond what is available in the git context
359- Assess the impact of these changes on the overall project
360- Check for any sensitive information that shouldn't be committed
361- Draft a concise (1-2 sentences) commit message that focuses on the "why" rather than the "what"
362- Ensure your language is clear, concise, and to the point
363- Ensure the message accurately reflects the changes and their purpose (i.e. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
364- Ensure the message is not generic (avoid words like "Update" or "Fix" without context)
365- Review the draft message to ensure it accurately reflects the changes and their purpose
366</commit_analysis>
367
3684. Create the commit with a message ending with:
369🤖 Generated with termai
370Co-Authored-By: termai <noreply@termai.io>
371
372- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
373<example>
374git commit -m "$(cat <<'EOF'
375 Commit message here.
376
377 🤖 Generated with termai
378 Co-Authored-By: termai <noreply@termai.io>
379 EOF
380 )"
381</example>
382
3835. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.
384
3856. Finally, run git status to make sure the commit succeeded.
386
387Important notes:
388- When possible, combine the "git add" and "git commit" commands into a single "git commit -am" command, to speed things up
389- However, be careful not to stage files (e.g. with 'git add .') for commits that aren't part of the change, they may have untracked files they want to keep around, but not commit.
390- NEVER update the git config
391- DO NOT push to the remote repository
392- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.
393- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit
394- Ensure your commit message is meaningful and concise. It should explain the purpose of the changes, not just describe them.
395- Return an empty response - the user will see the git output directly
396
397# Creating pull requests
398Use the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.
399
400IMPORTANT: When the user asks you to create a pull request, follow these steps carefully:
401
4021. Understand the current state of the branch. Remember to send a single message that contains multiple tool_use blocks (it is VERY IMPORTANT that you do this in a single message, otherwise it will feel slow to the user!):
403 - Run a git status command to see all untracked files.
404 - Run a git diff command to see both staged and unstaged changes that will be committed.
405 - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote
406 - Run a git log command and 'git diff main...HEAD' to understand the full commit history for the current branch (from the time it diverged from the 'main' branch.)
407
4082. Create new branch if needed
409
4103. Commit changes if needed
411
4124. Push to remote with -u flag if needed
413
4145. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (not just the latest commit, but all commits that will be included in the pull request!), and draft a pull request summary. Wrap your analysis process in <pr_analysis> tags:
415
416<pr_analysis>
417- List the commits since diverging from the main branch
418- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)
419- Brainstorm the purpose or motivation behind these changes
420- Assess the impact of these changes on the overall project
421- Do not use tools to explore code, beyond what is available in the git context
422- Check for any sensitive information that shouldn't be committed
423- Draft a concise (1-2 bullet points) pull request summary that focuses on the "why" rather than the "what"
424- Ensure the summary accurately reflects all changes since diverging from the main branch
425- Ensure your language is clear, concise, and to the point
426- Ensure the summary accurately reflects the changes and their purpose (ie. "add" means a wholly new feature, "update" means an enhancement to an existing feature, "fix" means a bug fix, etc.)
427- Ensure the summary is not generic (avoid words like "Update" or "Fix" without context)
428- Review the draft summary to ensure it accurately reflects the changes and their purpose
429</pr_analysis>
430
4316. Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.
432<example>
433gh pr create --title "the pr title" --body "$(cat <<'EOF'
434## Summary
435<1-3 bullet points>
436
437## Test plan
438[Checklist of TODOs for testing the pull request...]
439
440🤖 Generated with termai
441EOF
442)"
443</example>
444
445Important:
446- Return an empty response - the user will see the gh output directly
447- Never update git config`, bannedCommandsStr, MaxOutputLength)
448}
449
450func NewBashTool(workingDir string) tool.InvokableTool {
451 return &bashTool{
452 workingDir: workingDir,
453 }
454}