on main help sign in
feat: add attribution settings to config and bash tool (#1025)
6c380d4Amolith
README.md+33
@@ -173,6 +173,39 @@ $HOME/.local/share/crush/crush.json
173%LOCALAPPDATA%\crush\crush.json
173%LOCALAPPDATA%\crush\crush.json
174```
174```
175
175
176+### Attribution Settings
177+
178+By default, Crush adds attribution information to git commits and pull requests it creates. You can customize this behavior with the `attribution` option:
179+
180+```json
181+{
182+ "$schema": "https://charm.land/crush.json",
183+ "options": {
184+ "attribution": {
185+ "co_authored_by": true,
186+ "generated_with": true
187+ }
188+ }
189+}
190+```
191+
192+- `co_authored_by`: When true (default), adds `Co-Authored-By: Crush <crush@charm.land>` to commit messages
193+- `generated_with`: When true (default), adds `๐Ÿ’˜ Generated with Crush` line to commit messages and PR descriptions
194+
195+To disable all attribution, set both options to false:
196+
197+```json
198+{
199+ "$schema": "https://charm.land/crush.json",
200+ "options": {
201+ "attribution": {
202+ "co_authored_by": false,
203+ "generated_with": false
204+ }
205+ }
206+}
207+```
208+
176### LSPs
209### LSPs
177
210
178Crush can use LSPs for additional context to help inform its decisions, just
211Crush can use LSPs for additional context to help inform its decisions, just
internal/config/config.go+14โˆ’8
@@ -138,15 +138,21 @@ type Permissions struct {
138 SkipRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode)
138 SkipRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode)
139}
139}
140
140
141+type Attribution struct {
142+ CoAuthoredBy bool `json:"co_authored_by,omitempty" jsonschema:"description=Add Co-Authored-By trailer to commit messages,default=true"`
143+ GeneratedWith bool `json:"generated_with,omitempty" jsonschema:"description=Add Generated with Crush line to commit messages and issues and PRs,default=true"`
144+}
145+
141type Options struct {
146type Options struct {
142โˆ’ ContextPaths []string `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"`
147+ ContextPaths []string `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"`
143โˆ’ TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"`
148+ TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"`
144โˆ’ Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"`
149+ Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"`
145โˆ’ DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"`
150+ DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"`
146โˆ’ DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"`
151+ DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"`
147โˆ’ DataDirectory string `json:"data_directory,omitempty" jsonschema:"description=Directory for storing application data (relative to working directory),default=.crush,example=.crush"` // Relative to the cwd
152+ DataDirectory string `json:"data_directory,omitempty" jsonschema:"description=Directory for storing application data (relative to working directory),default=.crush,example=.crush"` // Relative to the cwd
148โˆ’ DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"`
153+ DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"`
149โˆ’ DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"`
154+ DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"`
155+ Attribution *Attribution `json:"attribution,omitempty" jsonschema:"description=Attribution settings for generated content"`
150}
156}
151
157
152type MCPs map[string]MCPConfig
158type MCPs map[string]MCPConfig
internal/llm/agent/agent.go+1โˆ’1
@@ -184,7 +184,7 @@ func NewAgent(
184
184
185 cwd := cfg.WorkingDir()
185 cwd := cfg.WorkingDir()
186 allTools := []tools.BaseTool{
186 allTools := []tools.BaseTool{
187โˆ’ tools.NewBashTool(permissions, cwd),
187+ tools.NewBashTool(permissions, cwd, cfg.Options.Attribution),
188 tools.NewDownloadTool(permissions, cwd),
188 tools.NewDownloadTool(permissions, cwd),
189 tools.NewEditTool(lspClients, permissions, history, cwd),
189 tools.NewEditTool(lspClients, permissions, history, cwd),
190 tools.NewMultiEditTool(lspClients, permissions, history, cwd),
190 tools.NewMultiEditTool(lspClients, permissions, history, cwd),
internal/llm/tools/bash.go+55โˆ’17
@@ -7,6 +7,7 @@ import (
7 "strings"
7 "strings"
8 "time"
8 "time"
9
9
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"
12)
13)
@@ -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
33}
35}
34
36
35const (
37const (
@@ -114,8 +116,53 @@ var bannedCommands = []string{
114 "ufw",
116 "ufw",
115}
117}
116
118
117โˆ’func bashDescription() string {
119+func (b *bashTool) bashDescription() string {
118 bannedCommandsStr := strings.Join(bannedCommands, ", ")
120 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+
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.
120
167
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>
192
239
193โˆ’4. Create the commit with a message ending with:
240+%s
194โˆ’๐Ÿ’˜ Generated with Crush
195โˆ’Co-Authored-By: Crush <crush@charm.land>
196
241
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:
198โˆ’<example>
243+%s
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>
207
244
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.
209
246
@@ -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...]
264
301
265โˆ’๐Ÿ’˜ Generated with Crush
302+%s
266EOF
303EOF
267)"
304)"
268</example>
305</example>
269
306
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)
273}
310}
274
311
275func blockFuncs() []shell.BlockFunc {
312func blockFuncs() []shell.BlockFunc {
@@ -304,7 +341,7 @@ func blockFuncs() []shell.BlockFunc {
304 }
341 }
305}
342}
306
343
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,
315 }
353 }
316}
354}
317
355
@@ -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",
schema.json+20
@@ -3,6 +3,22 @@
3 "$id": "https://github.com/charmbracelet/crush/internal/config/config",
3 "$id": "https://github.com/charmbracelet/crush/internal/config/config",
4 "$ref": "#/$defs/Config",
4 "$ref": "#/$defs/Config",
5 "$defs": {
5 "$defs": {
6+ "Attribution": {
7+ "properties": {
8+ "co_authored_by": {
9+ "type": "boolean",
10+ "description": "Add Co-Authored-By trailer to commit messages",
11+ "default": true
12+ },
13+ "generated_with": {
14+ "type": "boolean",
15+ "description": "Add Generated with Crush line to commit messages and issues and PRs",
16+ "default": true
17+ }
18+ },
19+ "additionalProperties": false,
20+ "type": "object"
21+ },
6 "Config": {
22 "Config": {
7 "properties": {
23 "properties": {
8 "$schema": {
24 "$schema": {
@@ -300,6 +316,10 @@
300 "type": "boolean",
316 "type": "boolean",
301 "description": "Disable providers auto-update",
317 "description": "Disable providers auto-update",
302 "default": false
318 "default": false
319+ },
320+ "attribution": {
321+ "$ref": "#/$defs/Attribution",
322+ "description": "Attribution settings for generated content"
303 }
323 }
304 },
324 },
305 "additionalProperties": false,
325 "additionalProperties": false,