From bef2ea1b25b35e2ae2663778baba4a899d090531 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 28 Jul 2025 21:39:10 -0300 Subject: [PATCH 1/6] feat: add schema command We should probably serve this file in a short URL. --- go.mod | 9 ++++++++- go.sum | 13 +++++++++++-- internal/cmd/schema.go | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 internal/cmd/schema.go diff --git a/go.mod b/go.mod index dfb96ff030dfbcfac1212e033f69448f6f50fab4..cd7376cc280a533849fa00e336ca341719cad6f2 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec github.com/fsnotify/fsnotify v1.9.0 github.com/google/uuid v1.6.0 + github.com/invopop/jsonschema v0.13.0 github.com/joho/godotenv v1.5.1 github.com/mark3labs/mcp-go v0.34.0 github.com/muesli/termenv v0.16.0 @@ -46,7 +47,13 @@ require ( mvdan.cc/sh/v3 v3.12.1-0.20250726150758-e256f53bade8 ) -require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect +) require ( cloud.google.com/go v0.116.0 // indirect diff --git a/go.sum b/go.sum index 5ff202f9e91865375770b23c52d8567f918764d3..b09b170449637de70899f0ae3cd196b22a2e0d2c 100644 --- a/go.sum +++ b/go.sum @@ -64,8 +64,12 @@ github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3v github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bmatcuk/doublestar/v4 v4.9.0 h1:DBvuZxjdKkRP/dr4GVV4w2fnmrk5Hxc90T51LZjv0JA= github.com/bmatcuk/doublestar/v4 v4.9.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr2u/ufj8= github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI= github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250716191546-1e2ffbbcf5c5 h1:GTcMIfDQJKyNKS+xVt7GkNIwz+tBuQtIuiP50WpzNgs= @@ -84,8 +88,6 @@ github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250721205738-ea66aa652ee0 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3.0.20250721205738-ea66aa652ee0/go.mod h1:XIuqKpZTUXtVyeyiN1k9Tc/U7EzfaDnVc34feFHfBws= github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE= github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM= -github.com/charmbracelet/ultraviolet v0.0.0-20250723145313-809e6f5b43a1 h1:tsw1mOuIEIKlmm614bXctvJ3aavaFhyPG+y+wrKtuKQ= -github.com/charmbracelet/ultraviolet v0.0.0-20250723145313-809e6f5b43a1/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc= github.com/charmbracelet/ultraviolet v0.0.0-20250728213340-d73950b67624 h1:D6cSAhkOFnx8TG8pyUiVWTbQ058xM4FXcOdaZXtBz+A= github.com/charmbracelet/ultraviolet v0.0.0-20250728213340-d73950b67624/go.mod h1:XrrgNFfXLrFAyd9DUmrqVc3yQFVv8Uk+okj4PsNNzpc= github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= @@ -156,8 +158,11 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -171,6 +176,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mark3labs/mcp-go v0.34.0 h1:eWy7WBGvhk6EyAAyVzivTCprE52iXJwNtvHV6Cv3bR0= github.com/mark3labs/mcp-go v0.34.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -265,6 +272,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/u-root/u-root v0.14.1-0.20250724181933-b01901710169 h1:f4cp2yGKkMuGpCwAyNEjzcw8szgVXmemK/wfOu4l5gc= github.com/u-root/u-root v0.14.1-0.20250724181933-b01901710169/go.mod h1:/0Qr7qJeDwWxoKku2xKQ4Szc+SwBE3g9VE8jNiamsmc= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= diff --git a/internal/cmd/schema.go b/internal/cmd/schema.go new file mode 100644 index 0000000000000000000000000000000000000000..73b3860d1a8ad464615dd41fe416d0df417e2907 --- /dev/null +++ b/internal/cmd/schema.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/crush/internal/config" + "github.com/invopop/jsonschema" + "github.com/spf13/cobra" +) + +var schemaCmd = &cobra.Command{ + Use: "schema", + Short: "Generate JSON schema for configuration", + Long: "Generate JSON schema for the crush configuration file", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + reflector := jsonschema.Reflector{} + schema := reflector.Reflect(&config.Config{}) + + schemaJSON, err := json.MarshalIndent(schema, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal schema: %w", err) + } + + fmt.Println(string(schemaJSON)) + return nil + }, +} + +func init() { + rootCmd.AddCommand(schemaCmd) +} From 9bf42ced9945c24e94129e842507e348a57d5256 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 28 Jul 2025 22:09:20 -0300 Subject: [PATCH 2/6] ci: auto generate on changes --- .gitattributes | 1 + .github/crush-schema.json | 397 ++++++++++++++++++++++++++++ .github/workflows/schema-update.yml | 26 ++ README.md | 7 + internal/cmd/schema.go | 40 ++- internal/config/config.go | 103 +++++--- 6 files changed, 532 insertions(+), 42 deletions(-) create mode 100644 .github/crush-schema.json create mode 100644 .github/workflows/schema-update.yml diff --git a/.gitattributes b/.gitattributes index d5273520ad5ecc37dd839a3077803b2c6581b2a1..6d364901268bb606a4b4bece3820294279894467 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ *.golden linguist-generated=true -text +.github/crush-schema.json linguist-generated=true diff --git a/.github/crush-schema.json b/.github/crush-schema.json new file mode 100644 index 0000000000000000000000000000000000000000..0f3b01b6f4c452f1badaf5cbb8237406fcc5438b --- /dev/null +++ b/.github/crush-schema.json @@ -0,0 +1,397 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/charmbracelet/crush/internal/config/config", + "$ref": "#/$defs/Config", + "$defs": { + "Config": { + "properties": { + "models": { + "additionalProperties": { + "$ref": "#/$defs/SelectedModel" + }, + "type": "object", + "description": "Model configurations for different model types" + }, + "providers": { + "additionalProperties": { + "$ref": "#/$defs/ProviderConfig" + }, + "type": "object", + "description": "AI provider configurations" + }, + "mcp": { + "$ref": "#/$defs/MCPs", + "description": "Model Context Protocol server configurations" + }, + "lsp": { + "$ref": "#/$defs/LSPs", + "description": "Language Server Protocol configurations" + }, + "options": { + "$ref": "#/$defs/Options", + "description": "General application options" + }, + "permissions": { + "$ref": "#/$defs/Permissions", + "description": "Permission settings for tool usage" + } + }, + "additionalProperties": false, + "type": "object" + }, + "LSPConfig": { + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether this LSP server is disabled", + "default": false + }, + "command": { + "type": "string", + "description": "Command to execute for the LSP server", + "examples": [ + "gopls" + ] + }, + "args": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Arguments to pass to the LSP server command" + }, + "options": { + "description": "LSP server-specific configuration options" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "command" + ] + }, + "LSPs": { + "additionalProperties": { + "$ref": "#/$defs/LSPConfig" + }, + "type": "object" + }, + "MCPConfig": { + "properties": { + "command": { + "type": "string", + "description": "Command to execute for stdio MCP servers", + "examples": [ + "npx" + ] + }, + "env": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Environment variables to set for the MCP server" + }, + "args": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Arguments to pass to the MCP server command" + }, + "type": { + "$ref": "#/$defs/MCPType", + "description": "Type of MCP connection" + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL for HTTP or SSE MCP servers", + "examples": [ + "http://localhost:3000/mcp" + ] + }, + "disabled": { + "type": "boolean", + "description": "Whether this MCP server is disabled", + "default": false + }, + "headers": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "HTTP headers for HTTP/SSE MCP servers" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type" + ] + }, + "MCPType": { + "type": "string", + "enum": [ + "stdio", + "sse", + "http" + ], + "description": "Type of MCP connection protocol", + "default": "stdio" + }, + "MCPs": { + "additionalProperties": { + "$ref": "#/$defs/MCPConfig" + }, + "type": "object" + }, + "Model": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "cost_per_1m_in": { + "type": "number" + }, + "cost_per_1m_out": { + "type": "number" + }, + "cost_per_1m_in_cached": { + "type": "number" + }, + "cost_per_1m_out_cached": { + "type": "number" + }, + "context_window": { + "type": "integer" + }, + "default_max_tokens": { + "type": "integer" + }, + "can_reason": { + "type": "boolean" + }, + "has_reasoning_efforts": { + "type": "boolean" + }, + "default_reasoning_effort": { + "type": "string" + }, + "supports_attachments": { + "type": "boolean" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "id", + "name", + "cost_per_1m_in", + "cost_per_1m_out", + "cost_per_1m_in_cached", + "cost_per_1m_out_cached", + "context_window", + "default_max_tokens", + "can_reason", + "has_reasoning_efforts", + "supports_attachments" + ] + }, + "Options": { + "properties": { + "context_paths": { + "items": { + "type": "string", + "examples": [ + ".cursorrules", + "CRUSH.md" + ] + }, + "type": "array", + "description": "Paths to files containing context information for the AI" + }, + "tui": { + "$ref": "#/$defs/TUIOptions", + "description": "Terminal user interface options" + }, + "debug": { + "type": "boolean", + "description": "Enable debug logging", + "default": false + }, + "debug_lsp": { + "type": "boolean", + "description": "Enable debug logging for LSP servers", + "default": false + }, + "disable_auto_summarize": { + "type": "boolean", + "description": "Disable automatic conversation summarization", + "default": false + }, + "data_directory": { + "type": "string", + "description": "Directory for storing application data (relative to working directory)", + "default": ".crush", + "examples": [ + ".crush" + ] + } + }, + "additionalProperties": false, + "type": "object" + }, + "Permissions": { + "properties": { + "allowed_tools": { + "items": { + "type": "string", + "examples": [ + "bash", + "view" + ] + }, + "type": "array", + "description": "List of tools that don't require permission prompts" + } + }, + "additionalProperties": false, + "type": "object" + }, + "ProviderConfig": { + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the provider", + "examples": [ + "openai" + ] + }, + "name": { + "type": "string", + "description": "Human-readable name for the provider", + "examples": [ + "OpenAI" + ] + }, + "base_url": { + "type": "string", + "format": "uri", + "description": "Base URL for the provider's API", + "examples": [ + "https://api.openai.com/v1" + ] + }, + "type": { + "type": "string", + "enum": [ + "openai", + "anthropic", + "gemini", + "azure", + "vertexai" + ], + "description": "Provider type that determines the API format", + "default": "openai" + }, + "api_key": { + "type": "string", + "description": "API key for authentication with the provider", + "examples": [ + "$OPENAI_API_KEY" + ] + }, + "disable": { + "type": "boolean", + "description": "Whether this provider is disabled", + "default": false + }, + "system_prompt_prefix": { + "type": "string", + "description": "Custom prefix to add to system prompts for this provider" + }, + "extra_headers": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "description": "Additional HTTP headers to send with requests" + }, + "extra_body": { + "type": "object", + "description": "Additional fields to include in request bodies" + }, + "models": { + "items": { + "$ref": "#/$defs/Model" + }, + "type": "array", + "description": "List of models available from this provider" + } + }, + "additionalProperties": false, + "type": "object" + }, + "SelectedModel": { + "properties": { + "model": { + "type": "string", + "description": "The model ID as used by the provider API", + "examples": [ + "gpt-4o" + ] + }, + "provider": { + "type": "string", + "description": "The model provider ID that matches a key in the providers config", + "examples": [ + "openai" + ] + }, + "reasoning_effort": { + "type": "string", + "enum": [ + "low", + "medium", + "high" + ], + "description": "Reasoning effort level for OpenAI models that support it" + }, + "max_tokens": { + "type": "integer", + "maximum": 200000, + "minimum": 1, + "description": "Maximum number of tokens for model responses", + "examples": [ + 4096 + ] + }, + "think": { + "type": "boolean", + "description": "Enable thinking mode for Anthropic models that support reasoning" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "model", + "provider" + ] + }, + "TUIOptions": { + "properties": { + "compact_mode": { + "type": "boolean", + "description": "Enable compact mode for the TUI interface", + "default": false + } + }, + "additionalProperties": false, + "type": "object" + } + } +} diff --git a/.github/workflows/schema-update.yml b/.github/workflows/schema-update.yml new file mode 100644 index 0000000000000000000000000000000000000000..df556aac1e0c84a9f8286ccdfbdde9c912802d4a --- /dev/null +++ b/.github/workflows/schema-update.yml @@ -0,0 +1,26 @@ +name: Update Schema + +on: + push: + branches: [main] + paths: + - "internal/config/**" + +jobs: + update-schema: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - run: go run . schema > .github/crush-schema.json + - uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v5 + with: + commit_message: "chore: auto-update generated files" + branch: main + commit_user_name: actions-user + commit_user_email: actions@github.com + commit_author: actions-user diff --git a/README.md b/README.md index ccc85d1e8605426f7c79d858c28cb06d83203ece..4cb18d3b44a94e09619d9d8f1bc3dfec7f6044a4 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ Crush can use LSPs for additional context to help inform its decisions, just lik ```json { + "$schema": "https://charm.land/crush.json", "lsp": { "go": { "command": "gopls" @@ -106,6 +107,7 @@ Crush supports Model Context Protocol (MCP) servers through three transport type ```json { + "$schema": "https://charm.land/crush.json", "mcp": { "filesystem": { "type": "stdio", @@ -136,6 +138,7 @@ Crush supports Model Context Protocol (MCP) servers through three transport type ### Logging Enable debug logging with the `-d` flag or in config. View logs with `crush logs`. Logs are stored in `.crush/logs/crush.log`. + ```bash # Run with debug logging crush -d @@ -154,6 +157,7 @@ Add to your `crush.json` config file: ```json { + "$schema": "https://charm.land/crush.json", "options": { "debug": true, "debug_lsp": true @@ -167,6 +171,7 @@ Crush includes a permission system to control which tools can be executed withou ```json { + "$schema": "https://charm.land/crush.json", "permissions": { "allowed_tools": [ "view", @@ -196,6 +201,7 @@ Here's an example configuration for Deepseek, which uses an OpenAI-compatible AP ```json { + "$schema": "https://charm.land/crush.json", "providers": { "deepseek": { "type": "openai", @@ -224,6 +230,7 @@ You can also configure custom Anthropic-compatible providers: ```json { + "$schema": "https://charm.land/crush.json", "providers": { "custom-anthropic": { "type": "anthropic", diff --git a/internal/cmd/schema.go b/internal/cmd/schema.go index 73b3860d1a8ad464615dd41fe416d0df417e2907..59b0ae90d22cfa076337b4092180229bebc9db9b 100644 --- a/internal/cmd/schema.go +++ b/internal/cmd/schema.go @@ -3,6 +3,7 @@ package cmd import ( "encoding/json" "fmt" + "reflect" "github.com/charmbracelet/crush/internal/config" "github.com/invopop/jsonschema" @@ -15,8 +16,45 @@ var schemaCmd = &cobra.Command{ Long: "Generate JSON schema for the crush configuration file", Hidden: true, RunE: func(cmd *cobra.Command, args []string) error { - reflector := jsonschema.Reflector{} + reflector := jsonschema.Reflector{ + // Custom type mapper to handle csync.Map + Mapper: func(t reflect.Type) *jsonschema.Schema { + // Handle csync.Map[string, ProviderConfig] specifically + if t.String() == "csync.Map[string,github.com/charmbracelet/crush/internal/config.ProviderConfig]" { + return &jsonschema.Schema{ + Type: "object", + Description: "AI provider configurations", + AdditionalProperties: &jsonschema.Schema{ + Ref: "#/$defs/ProviderConfig", + }, + } + } + return nil + }, + } + + // First reflect the config to get the main schema schema := reflector.Reflect(&config.Config{}) + + // Now manually add the ProviderConfig definition that might be missing + providerConfigSchema := reflector.ReflectFromType(reflect.TypeOf(config.ProviderConfig{})) + if schema.Definitions == nil { + schema.Definitions = make(map[string]*jsonschema.Schema) + } + + // Extract the actual definition from the nested schema + if providerConfigSchema.Definitions != nil && providerConfigSchema.Definitions["ProviderConfig"] != nil { + schema.Definitions["ProviderConfig"] = providerConfigSchema.Definitions["ProviderConfig"] + // Also add any other definitions from the provider config schema + for k, v := range providerConfigSchema.Definitions { + if k != "ProviderConfig" { + schema.Definitions[k] = v + } + } + } else { + // Fallback: use the schema itself if it's not nested + schema.Definitions["ProviderConfig"] = providerConfigSchema + } schemaJSON, err := json.MarshalIndent(schema, "", " ") if err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index 0f9fc99b5ce7677b0009933c447c0f7959825501..fd00f4a31e851ae8060c13dea369cb8a9f33d790 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,6 +13,7 @@ import ( "github.com/charmbracelet/catwalk/pkg/catwalk" "github.com/charmbracelet/crush/internal/csync" "github.com/charmbracelet/crush/internal/env" + "github.com/invopop/jsonschema" "github.com/tidwall/sjson" ) @@ -45,51 +46,61 @@ const ( SelectedModelTypeSmall SelectedModelType = "small" ) +// JSONSchema returns the JSON schema for SelectedModelType +func (SelectedModelType) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Description: "Model type selection for different use cases", + Enum: []any{"large", "small"}, + Default: "large", + } +} + type SelectedModel struct { // The model id as used by the provider API. // Required. - Model string `json:"model"` + Model string `json:"model" jsonschema:"required,description=The model ID as used by the provider API,example=gpt-4o"` // The model provider, same as the key/id used in the providers config. // Required. - Provider string `json:"provider"` + Provider string `json:"provider" jsonschema:"required,description=The model provider ID that matches a key in the providers config,example=openai"` // Only used by models that use the openai provider and need this set. - ReasoningEffort string `json:"reasoning_effort,omitempty"` + ReasoningEffort string `json:"reasoning_effort,omitempty" jsonschema:"description=Reasoning effort level for OpenAI models that support it,enum=low,enum=medium,enum=high"` // Overrides the default model configuration. - MaxTokens int64 `json:"max_tokens,omitempty"` + MaxTokens int64 `json:"max_tokens,omitempty" jsonschema:"description=Maximum number of tokens for model responses,minimum=1,maximum=200000,example=4096"` // Used by anthropic models that can reason to indicate if the model should think. - Think bool `json:"think,omitempty"` + Think bool `json:"think,omitempty" jsonschema:"description=Enable thinking mode for Anthropic models that support reasoning"` } type ProviderConfig struct { // The provider's id. - ID string `json:"id,omitempty"` + ID string `json:"id,omitempty" jsonschema:"description=Unique identifier for the provider,example=openai"` // The provider's name, used for display purposes. - Name string `json:"name,omitempty"` + Name string `json:"name,omitempty" jsonschema:"description=Human-readable name for the provider,example=OpenAI"` // The provider's API endpoint. - BaseURL string `json:"base_url,omitempty"` + BaseURL string `json:"base_url,omitempty" jsonschema:"description=Base URL for the provider's API,format=uri,example=https://api.openai.com/v1"` // The provider type, e.g. "openai", "anthropic", etc. if empty it defaults to openai. - Type catwalk.Type `json:"type,omitempty"` + Type catwalk.Type `json:"type,omitempty" jsonschema:"description=Provider type that determines the API format,enum=openai,enum=anthropic,enum=gemini,enum=azure,enum=vertexai,default=openai"` // The provider's API key. - APIKey string `json:"api_key,omitempty"` + APIKey string `json:"api_key,omitempty" jsonschema:"description=API key for authentication with the provider,example=$OPENAI_API_KEY"` // Marks the provider as disabled. - Disable bool `json:"disable,omitempty"` + Disable bool `json:"disable,omitempty" jsonschema:"description=Whether this provider is disabled,default=false"` // Custom system prompt prefix. - SystemPromptPrefix string `json:"system_prompt_prefix,omitempty"` + SystemPromptPrefix string `json:"system_prompt_prefix,omitempty" jsonschema:"description=Custom prefix to add to system prompts for this provider"` // Extra headers to send with each request to the provider. - ExtraHeaders map[string]string `json:"extra_headers,omitempty"` + ExtraHeaders map[string]string `json:"extra_headers,omitempty" jsonschema:"description=Additional HTTP headers to send with requests"` // Extra body - ExtraBody map[string]any `json:"extra_body,omitempty"` + ExtraBody map[string]any `json:"extra_body,omitempty" jsonschema:"description=Additional fields to include in request bodies"` // Used to pass extra parameters to the provider. ExtraParams map[string]string `json:"-"` // The provider models - Models []catwalk.Model `json:"models,omitempty"` + Models []catwalk.Model `json:"models,omitempty" jsonschema:"description=List of models available from this provider"` } type MCPType string @@ -100,42 +111,52 @@ const ( MCPHttp MCPType = "http" ) +// JSONSchema returns the JSON schema for MCPType +func (MCPType) JSONSchema() *jsonschema.Schema { + return &jsonschema.Schema{ + Type: "string", + Description: "Type of MCP connection protocol", + Enum: []any{"stdio", "sse", "http"}, + Default: "stdio", + } +} + type MCPConfig struct { - Command string `json:"command,omitempty" ` - Env map[string]string `json:"env,omitempty"` - Args []string `json:"args,omitempty"` - Type MCPType `json:"type"` - URL string `json:"url,omitempty"` - Disabled bool `json:"disabled,omitempty"` + Command string `json:"command,omitempty" jsonschema:"description=Command to execute for stdio MCP servers,example=npx"` + Env map[string]string `json:"env,omitempty" jsonschema:"description=Environment variables to set for the MCP server"` + Args []string `json:"args,omitempty" jsonschema:"description=Arguments to pass to the MCP server command"` + Type MCPType `json:"type" jsonschema:"required,description=Type of MCP connection,enum=stdio,enum=sse,enum=http,default=stdio"` + URL string `json:"url,omitempty" jsonschema:"description=URL for HTTP or SSE MCP servers,format=uri,example=http://localhost:3000/mcp"` + Disabled bool `json:"disabled,omitempty" jsonschema:"description=Whether this MCP server is disabled,default=false"` // TODO: maybe make it possible to get the value from the env - Headers map[string]string `json:"headers,omitempty"` + Headers map[string]string `json:"headers,omitempty" jsonschema:"description=HTTP headers for HTTP/SSE MCP servers"` } type LSPConfig struct { - Disabled bool `json:"enabled,omitempty"` - Command string `json:"command"` - Args []string `json:"args,omitempty"` - Options any `json:"options,omitempty"` + Disabled bool `json:"enabled,omitempty" jsonschema:"description=Whether this LSP server is disabled,default=false"` + Command string `json:"command" jsonschema:"required,description=Command to execute for the LSP server,example=gopls"` + Args []string `json:"args,omitempty" jsonschema:"description=Arguments to pass to the LSP server command"` + Options any `json:"options,omitempty" jsonschema:"description=LSP server-specific configuration options"` } type TUIOptions struct { - CompactMode bool `json:"compact_mode,omitempty"` + CompactMode bool `json:"compact_mode,omitempty" jsonschema:"description=Enable compact mode for the TUI interface,default=false"` // Here we can add themes later or any TUI related options } type Permissions struct { - AllowedTools []string `json:"allowed_tools,omitempty"` // Tools that don't require permission prompts - SkipRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode) + AllowedTools []string `json:"allowed_tools,omitempty" jsonschema:"description=List of tools that don't require permission prompts,example=bash,example=view"` // Tools that don't require permission prompts + SkipRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode) } type Options struct { - ContextPaths []string `json:"context_paths,omitempty"` - TUI *TUIOptions `json:"tui,omitempty"` - Debug bool `json:"debug,omitempty"` - DebugLSP bool `json:"debug_lsp,omitempty"` - DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty"` - DataDirectory string `json:"data_directory,omitempty"` // Relative to the cwd + 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 } type MCPs map[string]MCPConfig @@ -241,18 +262,18 @@ type Agent struct { // Config holds the configuration for crush. type Config struct { // We currently only support large/small as values here. - Models map[SelectedModelType]SelectedModel `json:"models,omitempty"` + Models map[SelectedModelType]SelectedModel `json:"models,omitempty" jsonschema:"description=Model configurations for different model types,example={\"large\":{\"model\":\"gpt-4o\",\"provider\":\"openai\"}}"` // The providers that are configured - Providers *csync.Map[string, ProviderConfig] `json:"providers,omitempty"` + Providers *csync.Map[string, ProviderConfig] `json:"providers,omitempty" jsonschema:"description=AI provider configurations"` - MCP MCPs `json:"mcp,omitempty"` + MCP MCPs `json:"mcp,omitempty" jsonschema:"description=Model Context Protocol server configurations"` - LSP LSPs `json:"lsp,omitempty"` + LSP LSPs `json:"lsp,omitempty" jsonschema:"description=Language Server Protocol configurations"` - Options *Options `json:"options,omitempty"` + Options *Options `json:"options,omitempty" jsonschema:"description=General application options"` - Permissions *Permissions `json:"permissions,omitempty"` + Permissions *Permissions `json:"permissions,omitempty" jsonschema:"description=Permission settings for tool usage"` // Internal workingDir string `json:"-"` From 3135b6fa4db00e95f8c17dbec9037875d4b2a88f Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 28 Jul 2025 22:44:20 -0300 Subject: [PATCH 3/6] fix: simplify stuff --- .github/workflows/schema-update.yml | 2 +- internal/cmd/schema.go | 47 ++---------------------- internal/csync/maps.go | 5 +++ .github/crush-schema.json => schema.json | 0 4 files changed, 9 insertions(+), 45 deletions(-) rename .github/crush-schema.json => schema.json (100%) diff --git a/.github/workflows/schema-update.yml b/.github/workflows/schema-update.yml index df556aac1e0c84a9f8286ccdfbdde9c912802d4a..4694981718b98b7d651b56f4be9edd63393d9e4c 100644 --- a/.github/workflows/schema-update.yml +++ b/.github/workflows/schema-update.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: go.mod - - run: go run . schema > .github/crush-schema.json + - run: go run . schema > ./schema.json - uses: stefanzweifel/git-auto-commit-action@778341af668090896ca464160c2def5d1d1a3eb0 # v5 with: commit_message: "chore: auto-update generated files" diff --git a/internal/cmd/schema.go b/internal/cmd/schema.go index 59b0ae90d22cfa076337b4092180229bebc9db9b..f835e250c24ea91a9d5084c9a414ed0e1ae28474 100644 --- a/internal/cmd/schema.go +++ b/internal/cmd/schema.go @@ -3,7 +3,6 @@ package cmd import ( "encoding/json" "fmt" - "reflect" "github.com/charmbracelet/crush/internal/config" "github.com/invopop/jsonschema" @@ -16,52 +15,12 @@ var schemaCmd = &cobra.Command{ Long: "Generate JSON schema for the crush configuration file", Hidden: true, RunE: func(cmd *cobra.Command, args []string) error { - reflector := jsonschema.Reflector{ - // Custom type mapper to handle csync.Map - Mapper: func(t reflect.Type) *jsonschema.Schema { - // Handle csync.Map[string, ProviderConfig] specifically - if t.String() == "csync.Map[string,github.com/charmbracelet/crush/internal/config.ProviderConfig]" { - return &jsonschema.Schema{ - Type: "object", - Description: "AI provider configurations", - AdditionalProperties: &jsonschema.Schema{ - Ref: "#/$defs/ProviderConfig", - }, - } - } - return nil - }, - } - - // First reflect the config to get the main schema - schema := reflector.Reflect(&config.Config{}) - - // Now manually add the ProviderConfig definition that might be missing - providerConfigSchema := reflector.ReflectFromType(reflect.TypeOf(config.ProviderConfig{})) - if schema.Definitions == nil { - schema.Definitions = make(map[string]*jsonschema.Schema) - } - - // Extract the actual definition from the nested schema - if providerConfigSchema.Definitions != nil && providerConfigSchema.Definitions["ProviderConfig"] != nil { - schema.Definitions["ProviderConfig"] = providerConfigSchema.Definitions["ProviderConfig"] - // Also add any other definitions from the provider config schema - for k, v := range providerConfigSchema.Definitions { - if k != "ProviderConfig" { - schema.Definitions[k] = v - } - } - } else { - // Fallback: use the schema itself if it's not nested - schema.Definitions["ProviderConfig"] = providerConfigSchema - } - - schemaJSON, err := json.MarshalIndent(schema, "", " ") + reflector := new(jsonschema.Reflector) + bts, err := json.MarshalIndent(reflector.Reflect(&config.Config{}), "", " ") if err != nil { return fmt.Errorf("failed to marshal schema: %w", err) } - - fmt.Println(string(schemaJSON)) + fmt.Println(string(bts)) return nil }, } diff --git a/internal/csync/maps.go b/internal/csync/maps.go index 108c8a4cbb6f855687d6117b1764b85e27279bc9..67796baff9f68b2a02382de625de70b78e204f4a 100644 --- a/internal/csync/maps.go +++ b/internal/csync/maps.go @@ -96,6 +96,11 @@ var ( _ json.Marshaler = &Map[string, any]{} ) +func (Map[K, V]) JSONSchemaAlias() any { //nolint + m := map[K]V{} + return m +} + // UnmarshalJSON implements json.Unmarshaler. func (m *Map[K, V]) UnmarshalJSON(data []byte) error { m.mu.Lock() diff --git a/.github/crush-schema.json b/schema.json similarity index 100% rename from .github/crush-schema.json rename to schema.json From 820e55ebbab2c9407e272dfcda3f077bb7022d65 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 28 Jul 2025 22:47:41 -0300 Subject: [PATCH 4/6] fix: simpler --- internal/config/config.go | 23 +---------------------- schema.json | 20 ++++++++------------ 2 files changed, 9 insertions(+), 34 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index fd00f4a31e851ae8060c13dea369cb8a9f33d790..9abbc4eef189315ba786992d6f00f374121c0af9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,7 +13,6 @@ import ( "github.com/charmbracelet/catwalk/pkg/catwalk" "github.com/charmbracelet/crush/internal/csync" "github.com/charmbracelet/crush/internal/env" - "github.com/invopop/jsonschema" "github.com/tidwall/sjson" ) @@ -46,16 +45,6 @@ const ( SelectedModelTypeSmall SelectedModelType = "small" ) -// JSONSchema returns the JSON schema for SelectedModelType -func (SelectedModelType) JSONSchema() *jsonschema.Schema { - return &jsonschema.Schema{ - Type: "string", - Description: "Model type selection for different use cases", - Enum: []any{"large", "small"}, - Default: "large", - } -} - type SelectedModel struct { // The model id as used by the provider API. // Required. @@ -111,16 +100,6 @@ const ( MCPHttp MCPType = "http" ) -// JSONSchema returns the JSON schema for MCPType -func (MCPType) JSONSchema() *jsonschema.Schema { - return &jsonschema.Schema{ - Type: "string", - Description: "Type of MCP connection protocol", - Enum: []any{"stdio", "sse", "http"}, - Default: "stdio", - } -} - type MCPConfig struct { Command string `json:"command,omitempty" jsonschema:"description=Command to execute for stdio MCP servers,example=npx"` Env map[string]string `json:"env,omitempty" jsonschema:"description=Environment variables to set for the MCP server"` @@ -239,7 +218,7 @@ type Agent struct { // This is the id of the system prompt used by the agent Disabled bool `json:"disabled,omitempty"` - Model SelectedModelType `json:"model"` + Model SelectedModelType `json:"model" jsonschema:"required,description=The model type to use for this agent,enum=large,enum=small,default=large"` // The available tools for the agent // if this is nil, all tools are available diff --git a/schema.json b/schema.json index 0f3b01b6f4c452f1badaf5cbb8237406fcc5438b..05dcc56c573405c6e4c3eb67762dd7ffd2d38ad7 100644 --- a/schema.json +++ b/schema.json @@ -100,8 +100,14 @@ "description": "Arguments to pass to the MCP server command" }, "type": { - "$ref": "#/$defs/MCPType", - "description": "Type of MCP connection" + "type": "string", + "enum": [ + "stdio", + "sse", + "http" + ], + "description": "Type of MCP connection", + "default": "stdio" }, "url": { "type": "string", @@ -130,16 +136,6 @@ "type" ] }, - "MCPType": { - "type": "string", - "enum": [ - "stdio", - "sse", - "http" - ], - "description": "Type of MCP connection protocol", - "default": "stdio" - }, "MCPs": { "additionalProperties": { "$ref": "#/$defs/MCPConfig" From 387d0d90472a7363f520577e0636015017275b13 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Mon, 28 Jul 2025 23:46:15 -0300 Subject: [PATCH 5/6] docs: add $schema to crush.json --- crush.json | 1 + 1 file changed, 1 insertion(+) diff --git a/crush.json b/crush.json index 1b04ea6c24f8b64a3a12ceb47551f3177fa66302..ba4dc18bc63381ad4bdbca5470a1527986c74205 100644 --- a/crush.json +++ b/crush.json @@ -1,4 +1,5 @@ { + "$schema": "https://charm.land/crush.json", "lsp": { "Go": { "command": "gopls" From fd96ccd5c9632734d6b67fe9e0c7b84cc13c5f7b Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Tue, 29 Jul 2025 09:37:38 -0300 Subject: [PATCH 6/6] docs: update --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f0af69d3ec623eb6e99d20571ab79d1b46e90c14..def4746e289e99feee2b61c3470592ac34f77e7d 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,7 @@ config: ```json { + "$schema": "https://charm.land/crush.json", "options": { "debug": true, "debug_lsp": true