From 6c380d4414ee83b49d04aa299ef0616b45408f26 Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 17 Sep 2025 11:29:27 -0600 Subject: [PATCH] feat: add attribution settings to config and bash tool (#1025) * feat: add attribution settings to config and bash tool * fix(readme): move ephemeral data block back to cfg section Closes: #445 --- README.md | 33 +++++++++++++++++ internal/config/config.go | 22 +++++++----- internal/llm/agent/agent.go | 2 +- internal/llm/tools/bash.go | 72 ++++++++++++++++++++++++++++--------- schema.json | 20 +++++++++++ 5 files changed, 123 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index d2a908f83a8ec6f56f3c6223127765049b09754a..7d618ddd178e97cfca70c0a0fd736d8f8fc959a5 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,39 @@ $HOME/.local/share/crush/crush.json %LOCALAPPDATA%\crush\crush.json ``` +### Attribution Settings + +By default, Crush adds attribution information to git commits and pull requests it creates. You can customize this behavior with the `attribution` option: + +```json +{ + "$schema": "https://charm.land/crush.json", + "options": { + "attribution": { + "co_authored_by": true, + "generated_with": true + } + } +} +``` + +- `co_authored_by`: When true (default), adds `Co-Authored-By: Crush ` to commit messages +- `generated_with`: When true (default), adds `💘 Generated with Crush` line to commit messages and PR descriptions + +To disable all attribution, set both options to false: + +```json +{ + "$schema": "https://charm.land/crush.json", + "options": { + "attribution": { + "co_authored_by": false, + "generated_with": false + } + } +} +``` + ### LSPs Crush can use LSPs for additional context to help inform its decisions, just diff --git a/internal/config/config.go b/internal/config/config.go index 05f6f8a10209ca4c5ddb084c04eb873c043f3c2c..02074dc212330e71848b90a01201c29a6525744d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -138,15 +138,21 @@ type Permissions struct { SkipRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode) } +type Attribution struct { + CoAuthoredBy bool `json:"co_authored_by,omitempty" jsonschema:"description=Add Co-Authored-By trailer to commit messages,default=true"` + GeneratedWith bool `json:"generated_with,omitempty" jsonschema:"description=Add Generated with Crush line to commit messages and issues and PRs,default=true"` +} + type Options struct { - ContextPaths []string `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"` - TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"` - Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"` - DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"` - DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"` - 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 - DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"` - DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"` + ContextPaths []string `json:"context_paths,omitempty" jsonschema:"description=Paths to files containing context information for the AI,example=.cursorrules,example=CRUSH.md"` + TUI *TUIOptions `json:"tui,omitempty" jsonschema:"description=Terminal user interface options"` + Debug bool `json:"debug,omitempty" jsonschema:"description=Enable debug logging,default=false"` + DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"` + DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"` + 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 + DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"` + DisableProviderAutoUpdate bool `json:"disable_provider_auto_update,omitempty" jsonschema:"description=Disable providers auto-update,default=false"` + Attribution *Attribution `json:"attribution,omitempty" jsonschema:"description=Attribution settings for generated content"` } type MCPs map[string]MCPConfig diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index 85439c3c0e8cc99ee7c07cfeb669e9402b3acce7..7c09a0be621485962df43e82484b0add4ea63513 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -184,7 +184,7 @@ func NewAgent( cwd := cfg.WorkingDir() allTools := []tools.BaseTool{ - tools.NewBashTool(permissions, cwd), + tools.NewBashTool(permissions, cwd, cfg.Options.Attribution), tools.NewDownloadTool(permissions, cwd), tools.NewEditTool(lspClients, permissions, history, cwd), tools.NewMultiEditTool(lspClients, permissions, history, cwd), diff --git a/internal/llm/tools/bash.go b/internal/llm/tools/bash.go index 6b55820632029e84f9381faa5ca2bd25734abeee..79205b9b142a10ff101da9a657e4b819dc88da4d 100644 --- a/internal/llm/tools/bash.go +++ b/internal/llm/tools/bash.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/permission" "github.com/charmbracelet/crush/internal/shell" ) @@ -30,6 +31,7 @@ type BashResponseMetadata struct { type bashTool struct { permissions permission.Service workingDir string + attribution *config.Attribution } const ( @@ -114,8 +116,53 @@ var bannedCommands = []string{ "ufw", } -func bashDescription() string { +func (b *bashTool) bashDescription() string { bannedCommandsStr := strings.Join(bannedCommands, ", ") + + // Build attribution text based on settings + var attributionStep, attributionExample, prAttribution string + + // Default to true if attribution is nil (backward compatibility) + generatedWith := b.attribution == nil || b.attribution.GeneratedWith + coAuthoredBy := b.attribution == nil || b.attribution.CoAuthoredBy + + // Build PR attribution + if generatedWith { + prAttribution = "💘 Generated with Crush" + } + + if generatedWith || coAuthoredBy { + attributionParts := []string{} + if generatedWith { + attributionParts = append(attributionParts, "💘 Generated with Crush") + } + if coAuthoredBy { + attributionParts = append(attributionParts, "Co-Authored-By: Crush ") + } + + if len(attributionParts) > 0 { + attributionStep = fmt.Sprintf("4. Create the commit with a message ending with:\n%s", strings.Join(attributionParts, "\n")) + + attributionText := strings.Join(attributionParts, "\n ") + attributionExample = fmt.Sprintf(` +git commit -m "$(cat <<'EOF' + Commit message here. + + %s + EOF +)"`, attributionText) + } + } + + if attributionStep == "" { + attributionStep = "4. Create the commit with your commit message." + attributionExample = ` +git commit -m "$(cat <<'EOF' + Commit message here. + EOF +)"` + } + return fmt.Sprintf(`Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures. CROSS-PLATFORM SHELL SUPPORT: @@ -190,20 +237,10 @@ When the user asks you to create a new git commit, follow these steps carefully: - Review the draft message to ensure it accurately reflects the changes and their purpose -4. Create the commit with a message ending with: -💘 Generated with Crush -Co-Authored-By: Crush +%s - In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example: - -git commit -m "$(cat <<'EOF' - Commit message here. - - 💘 Generated with Crush - Co-Authored-By: 💘 Crush - EOF - )" - +%s 5. 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' ## Test plan [Checklist of TODOs for testing the pull request...] -💘 Generated with Crush +%s EOF )" Important: - Return an empty response - the user will see the gh output directly -- Never update git config`, bannedCommandsStr, MaxOutputLength) +- Never update git config`, bannedCommandsStr, MaxOutputLength, attributionStep, attributionExample, prAttribution) } func blockFuncs() []shell.BlockFunc { @@ -304,7 +341,7 @@ func blockFuncs() []shell.BlockFunc { } } -func NewBashTool(permission permission.Service, workingDir string) BaseTool { +func NewBashTool(permission permission.Service, workingDir string, attribution *config.Attribution) BaseTool { // Set up command blocking on the persistent shell persistentShell := shell.GetPersistentShell(workingDir) persistentShell.SetBlockFuncs(blockFuncs()) @@ -312,6 +349,7 @@ func NewBashTool(permission permission.Service, workingDir string) BaseTool { return &bashTool{ permissions: permission, workingDir: workingDir, + attribution: attribution, } } @@ -322,7 +360,7 @@ func (b *bashTool) Name() string { func (b *bashTool) Info() ToolInfo { return ToolInfo{ Name: BashToolName, - Description: bashDescription(), + Description: b.bashDescription(), Parameters: map[string]any{ "command": map[string]any{ "type": "string", diff --git a/schema.json b/schema.json index adb6cc82ca375a45cb8f867da7fa75090d760d5e..f0cb2053e188d918e4c49168080026de5f0bffe5 100644 --- a/schema.json +++ b/schema.json @@ -3,6 +3,22 @@ "$id": "https://github.com/charmbracelet/crush/internal/config/config", "$ref": "#/$defs/Config", "$defs": { + "Attribution": { + "properties": { + "co_authored_by": { + "type": "boolean", + "description": "Add Co-Authored-By trailer to commit messages", + "default": true + }, + "generated_with": { + "type": "boolean", + "description": "Add Generated with Crush line to commit messages and issues and PRs", + "default": true + } + }, + "additionalProperties": false, + "type": "object" + }, "Config": { "properties": { "$schema": { @@ -300,6 +316,10 @@ "type": "boolean", "description": "Disable providers auto-update", "default": false + }, + "attribution": { + "$ref": "#/$defs/Attribution", + "description": "Attribution settings for generated content" } }, "additionalProperties": false,