@@ -7,6 +7,7 @@ import (
10+ "github.com/charmbracelet/crush/internal/config"
10 "github.com/charmbracelet/crush/internal/permission"
11 "github.com/charmbracelet/crush/internal/permission"
11 "github.com/charmbracelet/crush/internal/shell"
12 "github.com/charmbracelet/crush/internal/shell"
@@ -30,6 +31,7 @@ type BashResponseMetadata struct {
30type bashTool struct {
31type bashTool struct {
31 permissions permission.Service
32 permissions permission.Service
32 workingDir string
33 workingDir string
34+ attribution *config.Attribution
@@ -114,8 +116,53 @@ var bannedCommands = []string{
117โfunc bashDescription() string {
119+func (b *bashTool) bashDescription() string {
118 bannedCommandsStr := strings.Join(bannedCommands, ", ")
120 bannedCommandsStr := strings.Join(bannedCommands, ", ")
122+ // Build attribution text based on settings
123+ var attributionStep, attributionExample, prAttribution string
125+ // Default to true if attribution is nil (backward compatibility)
126+ generatedWith := b.attribution == nil || b.attribution.GeneratedWith
127+ coAuthoredBy := b.attribution == nil || b.attribution.CoAuthoredBy
129+ // Build PR attribution
131+ prAttribution = "๐ Generated with Crush"
134+ if generatedWith || coAuthoredBy {
135+ attributionParts := []string{}
137+ attributionParts = append(attributionParts, "๐ Generated with Crush")
140+ attributionParts = append(attributionParts, "Co-Authored-By: Crush <crush@charm.land>")
143+ if len(attributionParts) > 0 {
144+ attributionStep = fmt.Sprintf("4. Create the commit with a message ending with:\n%s", strings.Join(attributionParts, "\n"))
146+ attributionText := strings.Join(attributionParts, "\n ")
147+ attributionExample = fmt.Sprintf(`<example>
148+git commit -m "$(cat <<'EOF'
149+ Commit message here.
153+)"</example>`, attributionText)
157+ if attributionStep == "" {
158+ attributionStep = "4. Create the commit with your commit message."
159+ attributionExample = `<example>
160+git commit -m "$(cat <<'EOF'
161+ Commit message here.
119 return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
166 return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
121CROSS-PLATFORM SHELL SUPPORT:
168CROSS-PLATFORM SHELL SUPPORT:
@@ -190,20 +237,10 @@ When the user asks you to create a new git commit, follow these steps carefully:
190- Review the draft message to ensure it accurately reflects the changes and their purpose
237- Review the draft message to ensure it accurately reflects the changes and their purpose
191</commit_analysis>
238</commit_analysis>
193โ4. Create the commit with a message ending with:
240+%s
194โ๐ Generated with Crush
195โCo-Authored-By: Crush <crush@charm.land>
197- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
242- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
199โgit commit -m "$(cat <<'EOF'
200โ Commit message here.
202โ ๐ Generated with Crush
203โ Co-Authored-By: ๐ Crush <crush@charm.land>
2085. 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.
2455. 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.
@@ -262,14 +299,14 @@ gh pr create --title "the pr title" --body "$(cat <<'EOF'
262## Test plan
299## Test plan
263[Checklist of TODOs for testing the pull request...]
300[Checklist of TODOs for testing the pull request...]
265โ๐ Generated with Crush
302+%s
268</example>
305</example>
270Important:
307Important:
271- Return an empty response - the user will see the gh output directly
308- Return an empty response - the user will see the gh output directly
272โ- Never update git config`, bannedCommandsStr, MaxOutputLength)
309+- Never update git config`, bannedCommandsStr, MaxOutputLength, attributionStep, attributionExample, prAttribution)
275func blockFuncs() []shell.BlockFunc {
312func blockFuncs() []shell.BlockFunc {
@@ -304,7 +341,7 @@ func blockFuncs() []shell.BlockFunc {
307โfunc NewBashTool(permission permission.Service, workingDir string) BaseTool {
344+func NewBashTool(permission permission.Service, workingDir string, attribution *config.Attribution) BaseTool {
308 // Set up command blocking on the persistent shell
345 // Set up command blocking on the persistent shell
309 persistentShell := shell.GetPersistentShell(workingDir)
346 persistentShell := shell.GetPersistentShell(workingDir)
310 persistentShell.SetBlockFuncs(blockFuncs())
347 persistentShell.SetBlockFuncs(blockFuncs())
@@ -312,6 +349,7 @@ func NewBashTool(permission permission.Service, workingDir string) BaseTool {
312 return &bashTool{
349 return &bashTool{
313 permissions: permission,
350 permissions: permission,
314 workingDir: workingDir,
351 workingDir: workingDir,
352+ attribution: attribution,
@@ -322,7 +360,7 @@ func (b *bashTool) Name() string {
322func (b *bashTool) Info() ToolInfo {
360func (b *bashTool) Info() ToolInfo {
323 return ToolInfo{
361 return ToolInfo{
324 Name: BashToolName,
362 Name: BashToolName,
325โ Description: bashDescription(),
363+ Description: b.bashDescription(),
326 Parameters: map[string]any{
364 Parameters: map[string]any{
327 "command": map[string]any{
365 "command": map[string]any{
328 "type": "string",
366 "type": "string",