@@ -7,6 +7,7 @@ import (
77 "strings"
88 "time"
99
10+ "github.com/charmbracelet/crush/internal/config"
1011 "github.com/charmbracelet/crush/internal/permission"
1112 "github.com/charmbracelet/crush/internal/shell"
1213)
@@ -30,6 +31,7 @@ type BashResponseMetadata struct {
3031type bashTool struct {
3132 permissions permission.Service
3233 workingDir string
34+ attribution *config.Attribution
3335}
3436
3537const (
@@ -114,8 +116,53 @@ var bannedCommands = []string{
114116 "ufw",
115117}
116118
117−func bashDescription() string {
119+func (b *bashTool) bashDescription() string {
118120 bannedCommandsStr := strings.Join(bannedCommands, ", ")
121+
122+ // Build attribution text based on settings
123+ var attributionStep, attributionExample, prAttribution string
124+
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
128+
129+ // Build PR attribution
130+ if generatedWith {
131+ prAttribution = "💘 Generated with Crush"
132+ }
133+
134+ if generatedWith || coAuthoredBy {
135+ attributionParts := []string{}
136+ if generatedWith {
137+ attributionParts = append(attributionParts, "💘 Generated with Crush")
138+ }
139+ if coAuthoredBy {
140+ attributionParts = append(attributionParts, "Co-Authored-By: Crush <crush@charm.land>")
141+ }
142+
143+ if len(attributionParts) > 0 {
144+ attributionStep = fmt.Sprintf("4. Create the commit with a message ending with:\n%s", strings.Join(attributionParts, "\n"))
145+
146+ attributionText := strings.Join(attributionParts, "\n ")
147+ attributionExample = fmt.Sprintf(`<example>
148+git commit -m "$(cat <<'EOF'
149+ Commit message here.
150+
151+ %s
152+ EOF
153+)"</example>`, attributionText)
154+ }
155+ }
156+
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.
162+ EOF
163+)"</example>`
164+ }
165+
119166 return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
120167
121168CROSS-PLATFORM SHELL SUPPORT:
@@ -190,20 +237,10 @@ When the user asks you to create a new git commit, follow these steps carefully:
190237- Review the draft message to ensure it accurately reflects the changes and their purpose
191238</commit_analysis>
192239
193−4. Create the commit with a message ending with:
194−💘 Generated with Crush
195−Co-Authored-By: Crush <crush@charm.land>
240+%s
196241
197242- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:
198−<example>
199−git commit -m "$(cat <<'EOF'
200− Commit message here.
201−
202− 💘 Generated with Crush
203− Co-Authored-By: 💘 Crush <crush@charm.land>
204− EOF
205− )"
206−</example>
243+%s
207244
2082455. 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.
209246
@@ -262,14 +299,14 @@ gh pr create --title "the pr title" --body "$(cat <<'EOF'
262299## Test plan
263300[Checklist of TODOs for testing the pull request...]
264301
265−💘 Generated with Crush
302+%s
266303EOF
267304)"
268305</example>
269306
270307Important:
271308- 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)
273310}
274311
275312func blockFuncs() []shell.BlockFunc {
@@ -304,7 +341,7 @@ func blockFuncs() []shell.BlockFunc {
304341 }
305342}
306343
307−func NewBashTool(permission permission.Service, workingDir string) BaseTool {
344+func NewBashTool(permission permission.Service, workingDir string, attribution *config.Attribution) BaseTool {
308345 // Set up command blocking on the persistent shell
309346 persistentShell := shell.GetPersistentShell(workingDir)
310347 persistentShell.SetBlockFuncs(blockFuncs())
@@ -312,6 +349,7 @@ func NewBashTool(permission permission.Service, workingDir string) BaseTool {
312349 return &bashTool{
313350 permissions: permission,
314351 workingDir: workingDir,
352+ attribution: attribution,
315353 }
316354}
317355
@@ -322,7 +360,7 @@ func (b *bashTool) Name() string {
322360func (b *bashTool) Info() ToolInfo {
323361 return ToolInfo{
324362 Name: BashToolName,
325− Description: bashDescription(),
363+ Description: b.bashDescription(),
326364 Parameters: map[string]any{
327365 "command": map[string]any{
328366 "type": "string",