diff --git a/Taskfile.yaml b/Taskfile.yaml index 38e8a16313d17b9b1826ce4b6f055d39537916ec..36fe050185f8599f357dba213b2a266ec6fa37e5 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -185,6 +185,19 @@ tasks: - go get charm.land/catwalk - go mod tidy + swag: + desc: Generate OpenAPI spec from swag annotations + cmds: + - go run github.com/swaggo/swag/cmd/swag@v1.16.6 init --generalInfo main.go --dir . --output docs --parseDependency --parseInternal --parseDepth 5 + sources: + - internal/server/*.go + - internal/proto/*.go + - main.go + generates: + - docs/docs.go + - docs/swagger.json + - docs/swagger.yaml + sqlc: desc: Generate code using SQLC cmds: diff --git a/go.mod b/go.mod index 95ab2ac117ac88f43b79673c64b388c4f4a4a0e6..a72a1589505f13246850505211ad610d1b673ef0 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( charm.land/x/vcr v0.1.1 github.com/JohannesKaufmann/html-to-markdown v1.6.0 github.com/MakeNowJust/heredoc v1.0.0 + github.com/Microsoft/go-winio v0.6.2 github.com/PuerkitoBio/goquery v1.12.0 github.com/alecthomas/chroma/v2 v2.23.1 github.com/atotto/clipboard v0.1.4 @@ -59,12 +60,15 @@ require ( github.com/sourcegraph/jsonrpc2 v0.2.1 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 + github.com/swaggo/http-swagger/v2 v2.0.2 + github.com/swaggo/swag v1.16.6 github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 github.com/zeebo/xxh3 v1.1.0 go.uber.org/goleak v1.3.0 golang.org/x/net v0.52.0 golang.org/x/sync v0.20.0 + golang.org/x/sys v0.42.0 golang.org/x/text v0.35.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 @@ -81,7 +85,7 @@ require ( git.sr.ht/~jackmordaunt/go-toast v1.1.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect @@ -119,6 +123,10 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect @@ -135,6 +143,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackmordaunt/icns/v3 v3.0.1 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/kaptinlin/go-i18n v0.2.12 // indirect github.com/kaptinlin/jsonpointer v0.4.17 // indirect github.com/kaptinlin/jsonschema v0.7.5 // indirect @@ -164,6 +173,7 @@ require ( github.com/sergeymakinen/go-ico v1.0.0-beta.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/spf13/pflag v1.0.9 // indirect + github.com/swaggo/files/v2 v2.0.0 // indirect github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect github.com/tetratelabs/wazero v1.11.0 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -186,10 +196,11 @@ require ( golang.org/x/crypto v0.49.0 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/image v0.36.0 // indirect + golang.org/x/mod v0.33.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.41.0 // indirect golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.42.0 // indirect google.golang.org/api v0.269.0 // indirect google.golang.org/genai v1.49.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect @@ -198,6 +209,7 @@ require ( gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect modernc.org/libc v1.68.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index 694482d558aa46357e88f357a213e48c06e41d6c..42ac781d81c3a4946c16aaef972d12c3bf43f079 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mx github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k= github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= @@ -139,6 +141,7 @@ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJ github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -188,6 +191,16 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= @@ -241,6 +254,7 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jordanella/go-ansi-paintbrush v0.0.0-20240728195301-b7ad996ecf3d h1:on25kP+Sx7sxUMRQiA8gdcToAGet4DK/EIA30mXre+4= github.com/jordanella/go-ansi-paintbrush v0.0.0-20240728195301-b7ad996ecf3d/go.mod h1:SV0W0APWP9MZ1/gfDQ/NzzTlWdIgYZ/ZbpN4d/UXRYw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kaptinlin/go-i18n v0.2.12 h1:ywDsvb4KDFddMC2dpI/rrIzGU2mWUSvHmWUm9BMsdl4= github.com/kaptinlin/go-i18n v0.2.12/go.mod h1:pVcu9qsW5pOIOoZFJXesRYmLos1vMQrby70JPAoWmJU= @@ -267,6 +281,9 @@ 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.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -299,6 +316,7 @@ github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM= @@ -359,11 +377,18 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= +github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= +github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= +github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk= github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o= github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= @@ -532,7 +557,9 @@ google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhH google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 h1:g3ah7zaWmw41EtOgBNXpx8zk4HYuH3OMwB+qh1Dt834= @@ -547,6 +574,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= diff --git a/internal/proto/requests.go b/internal/proto/requests.go new file mode 100644 index 0000000000000000000000000000000000000000..fe5327079b4f6514c23b34ce5c8e00666c75ba43 --- /dev/null +++ b/internal/proto/requests.go @@ -0,0 +1,92 @@ +package proto + +import "github.com/charmbracelet/crush/internal/config" + +// ConfigSetRequest represents a request to set a config field. +type ConfigSetRequest struct { + Scope config.Scope `json:"scope"` + Key string `json:"key"` + Value any `json:"value"` +} + +// ConfigRemoveRequest represents a request to remove a config field. +type ConfigRemoveRequest struct { + Scope config.Scope `json:"scope"` + Key string `json:"key"` +} + +// ConfigModelRequest represents a request to update the preferred model. +type ConfigModelRequest struct { + Scope config.Scope `json:"scope"` + ModelType config.SelectedModelType `json:"model_type"` + Model config.SelectedModel `json:"model"` +} + +// ConfigCompactRequest represents a request to set compact mode. +type ConfigCompactRequest struct { + Scope config.Scope `json:"scope"` + Enabled bool `json:"enabled"` +} + +// ConfigProviderKeyRequest represents a request to set a provider API key. +type ConfigProviderKeyRequest struct { + Scope config.Scope `json:"scope"` + ProviderID string `json:"provider_id"` + APIKey any `json:"api_key"` +} + +// ConfigRefreshOAuthRequest represents a request to refresh an OAuth token. +type ConfigRefreshOAuthRequest struct { + Scope config.Scope `json:"scope"` + ProviderID string `json:"provider_id"` +} + +// ImportCopilotResponse represents the response from importing Copilot credentials. +type ImportCopilotResponse struct { + Token any `json:"token"` + Success bool `json:"success"` +} + +// ProjectNeedsInitResponse represents whether a project needs initialization. +type ProjectNeedsInitResponse struct { + NeedsInit bool `json:"needs_init"` +} + +// ProjectInitPromptResponse represents the project initialization prompt. +type ProjectInitPromptResponse struct { + Prompt string `json:"prompt"` +} + +// LSPStartRequest represents a request to start an LSP for a path. +type LSPStartRequest struct { + Path string `json:"path"` +} + +// FileTrackerReadRequest represents a request to record a file read. +type FileTrackerReadRequest struct { + SessionID string `json:"session_id"` + Path string `json:"path"` +} + +// MCPNameRequest represents a request targeting a named MCP server. +type MCPNameRequest struct { + Name string `json:"name"` +} + +// MCPReadResourceRequest represents a request to read an MCP resource. +type MCPReadResourceRequest struct { + Name string `json:"name"` + URI string `json:"uri"` +} + +// MCPGetPromptRequest represents a request to get an MCP prompt. +type MCPGetPromptRequest struct { + ClientID string `json:"client_id"` + PromptID string `json:"prompt_id"` + Args map[string]string `json:"args"` +} + +// MCPGetPromptResponse represents the response from getting an MCP prompt. +type MCPGetPromptResponse struct { + Prompt string `json:"prompt"` +} diff --git a/internal/server/config.go b/internal/server/config.go index d516505fb22dad6b10d3fc9f912eb3e82cb1b3a3..05806663f73276573383aa1d3809121c07fd295f 100644 --- a/internal/server/config.go +++ b/internal/server/config.go @@ -4,18 +4,25 @@ import ( "encoding/json" "net/http" - "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/proto" ) +// handlePostWorkspaceConfigSet sets a configuration field. +// +// @Summary Set a config field +// @Tags config +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.ConfigSetRequest true "Config set request" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/config/set [post] func (c *controllerV1) handlePostWorkspaceConfigSet(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - Scope config.Scope `json:"scope"` - Key string `json:"key"` - Value any `json:"value"` - } + var req proto.ConfigSetRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") @@ -29,13 +36,22 @@ func (c *controllerV1) handlePostWorkspaceConfigSet(w http.ResponseWriter, r *ht w.WriteHeader(http.StatusOK) } +// handlePostWorkspaceConfigRemove removes a configuration field. +// +// @Summary Remove a config field +// @Tags config +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.ConfigRemoveRequest true "Config remove request" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/config/remove [post] func (c *controllerV1) handlePostWorkspaceConfigRemove(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - Scope config.Scope `json:"scope"` - Key string `json:"key"` - } + var req proto.ConfigRemoveRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") @@ -49,14 +65,22 @@ func (c *controllerV1) handlePostWorkspaceConfigRemove(w http.ResponseWriter, r w.WriteHeader(http.StatusOK) } +// handlePostWorkspaceConfigModel updates the preferred model. +// +// @Summary Set the preferred model +// @Tags config +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.ConfigModelRequest true "Config model request" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/config/model [post] func (c *controllerV1) handlePostWorkspaceConfigModel(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - Scope config.Scope `json:"scope"` - ModelType config.SelectedModelType `json:"model_type"` - Model config.SelectedModel `json:"model"` - } + var req proto.ConfigModelRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") @@ -70,13 +94,22 @@ func (c *controllerV1) handlePostWorkspaceConfigModel(w http.ResponseWriter, r * w.WriteHeader(http.StatusOK) } +// handlePostWorkspaceConfigCompact sets compact mode. +// +// @Summary Set compact mode +// @Tags config +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.ConfigCompactRequest true "Config compact request" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/config/compact [post] func (c *controllerV1) handlePostWorkspaceConfigCompact(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - Scope config.Scope `json:"scope"` - Enabled bool `json:"enabled"` - } + var req proto.ConfigCompactRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") @@ -90,14 +123,22 @@ func (c *controllerV1) handlePostWorkspaceConfigCompact(w http.ResponseWriter, r w.WriteHeader(http.StatusOK) } +// handlePostWorkspaceConfigProviderKey sets a provider API key. +// +// @Summary Set provider API key +// @Tags config +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.ConfigProviderKeyRequest true "Config provider key request" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/config/provider-key [post] func (c *controllerV1) handlePostWorkspaceConfigProviderKey(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - Scope config.Scope `json:"scope"` - ProviderID string `json:"provider_id"` - APIKey any `json:"api_key"` - } + var req proto.ConfigProviderKeyRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") @@ -111,6 +152,16 @@ func (c *controllerV1) handlePostWorkspaceConfigProviderKey(w http.ResponseWrite w.WriteHeader(http.StatusOK) } +// handlePostWorkspaceConfigImportCopilot imports Copilot credentials. +// +// @Summary Import Copilot credentials +// @Tags config +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {object} proto.ImportCopilotResponse +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/config/import-copilot [post] func (c *controllerV1) handlePostWorkspaceConfigImportCopilot(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") token, ok, err := c.backend.ImportCopilot(id) @@ -118,19 +169,25 @@ func (c *controllerV1) handlePostWorkspaceConfigImportCopilot(w http.ResponseWri c.handleError(w, r, err) return } - jsonEncode(w, struct { - Token any `json:"token"` - Success bool `json:"success"` - }{Token: token, Success: ok}) + jsonEncode(w, proto.ImportCopilotResponse{Token: token, Success: ok}) } +// handlePostWorkspaceConfigRefreshOAuth refreshes an OAuth token for a provider. +// +// @Summary Refresh OAuth token +// @Tags config +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.ConfigRefreshOAuthRequest true "Refresh OAuth request" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/config/refresh-oauth [post] func (c *controllerV1) handlePostWorkspaceConfigRefreshOAuth(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - Scope config.Scope `json:"scope"` - ProviderID string `json:"provider_id"` - } + var req proto.ConfigRefreshOAuthRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") @@ -144,6 +201,16 @@ func (c *controllerV1) handlePostWorkspaceConfigRefreshOAuth(w http.ResponseWrit w.WriteHeader(http.StatusOK) } +// handleGetWorkspaceProjectNeedsInit reports whether a project needs initialization. +// +// @Summary Check if project needs initialization +// @Tags project +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {object} proto.ProjectNeedsInitResponse +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/project/needs-init [get] func (c *controllerV1) handleGetWorkspaceProjectNeedsInit(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") needs, err := c.backend.ProjectNeedsInitialization(id) @@ -151,11 +218,18 @@ func (c *controllerV1) handleGetWorkspaceProjectNeedsInit(w http.ResponseWriter, c.handleError(w, r, err) return } - jsonEncode(w, struct { - NeedsInit bool `json:"needs_init"` - }{NeedsInit: needs}) + jsonEncode(w, proto.ProjectNeedsInitResponse{NeedsInit: needs}) } +// handlePostWorkspaceProjectInit marks the project as initialized. +// +// @Summary Mark project as initialized +// @Tags project +// @Param id path string true "Workspace ID" +// @Success 200 +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/project/init [post] func (c *controllerV1) handlePostWorkspaceProjectInit(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := c.backend.MarkProjectInitialized(id); err != nil { @@ -165,6 +239,16 @@ func (c *controllerV1) handlePostWorkspaceProjectInit(w http.ResponseWriter, r * w.WriteHeader(http.StatusOK) } +// handleGetWorkspaceProjectInitPrompt returns the project initialization prompt. +// +// @Summary Get project initialization prompt +// @Tags project +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {object} proto.ProjectInitPromptResponse +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/project/init-prompt [get] func (c *controllerV1) handleGetWorkspaceProjectInitPrompt(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") prompt, err := c.backend.InitializePrompt(id) @@ -172,17 +256,25 @@ func (c *controllerV1) handleGetWorkspaceProjectInitPrompt(w http.ResponseWriter c.handleError(w, r, err) return } - jsonEncode(w, struct { - Prompt string `json:"prompt"` - }{Prompt: prompt}) + jsonEncode(w, proto.ProjectInitPromptResponse{Prompt: prompt}) } +// handlePostWorkspaceMCPRefreshTools refreshes tools for a named MCP server. +// +// @Summary Refresh MCP tools +// @Tags mcp +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.MCPNameRequest true "MCP name request" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/mcp/refresh-tools [post] func (c *controllerV1) handlePostWorkspaceMCPRefreshTools(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - Name string `json:"name"` - } + var req proto.MCPNameRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") @@ -196,13 +288,23 @@ func (c *controllerV1) handlePostWorkspaceMCPRefreshTools(w http.ResponseWriter, w.WriteHeader(http.StatusOK) } +// handlePostWorkspaceMCPReadResource reads a resource from an MCP server. +// +// @Summary Read MCP resource +// @Tags mcp +// @Accept json +// @Produce json +// @Param id path string true "Workspace ID" +// @Param request body proto.MCPReadResourceRequest true "MCP read resource request" +// @Success 200 {object} object +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/mcp/read-resource [post] func (c *controllerV1) handlePostWorkspaceMCPReadResource(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - Name string `json:"name"` - URI string `json:"uri"` - } + var req proto.MCPReadResourceRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") @@ -217,14 +319,23 @@ func (c *controllerV1) handlePostWorkspaceMCPReadResource(w http.ResponseWriter, jsonEncode(w, contents) } +// handlePostWorkspaceMCPGetPrompt retrieves a prompt from an MCP server. +// +// @Summary Get MCP prompt +// @Tags mcp +// @Accept json +// @Produce json +// @Param id path string true "Workspace ID" +// @Param request body proto.MCPGetPromptRequest true "MCP get prompt request" +// @Success 200 {object} proto.MCPGetPromptResponse +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/mcp/get-prompt [post] func (c *controllerV1) handlePostWorkspaceMCPGetPrompt(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - ClientID string `json:"client_id"` - PromptID string `json:"prompt_id"` - Args map[string]string `json:"args"` - } + var req proto.MCPGetPromptRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") @@ -236,11 +347,19 @@ func (c *controllerV1) handlePostWorkspaceMCPGetPrompt(w http.ResponseWriter, r c.handleError(w, r, err) return } - jsonEncode(w, struct { - Prompt string `json:"prompt"` - }{Prompt: prompt}) + jsonEncode(w, proto.MCPGetPromptResponse{Prompt: prompt}) } +// handleGetWorkspaceMCPStates returns the state of all MCP clients. +// +// @Summary Get MCP client states +// @Tags mcp +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {object} map[string]proto.MCPClientInfo +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/mcp/states [get] func (c *controllerV1) handleGetWorkspaceMCPStates(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") states := c.backend.MCPGetStates(id) @@ -259,12 +378,22 @@ func (c *controllerV1) handleGetWorkspaceMCPStates(w http.ResponseWriter, r *htt jsonEncode(w, result) } +// handlePostWorkspaceMCPRefreshPrompts refreshes prompts for a named MCP server. +// +// @Summary Refresh MCP prompts +// @Tags mcp +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.MCPNameRequest true "MCP name request" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/mcp/refresh-prompts [post] func (c *controllerV1) handlePostWorkspaceMCPRefreshPrompts(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - Name string `json:"name"` - } + var req proto.MCPNameRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") @@ -275,12 +404,22 @@ func (c *controllerV1) handlePostWorkspaceMCPRefreshPrompts(w http.ResponseWrite w.WriteHeader(http.StatusOK) } +// handlePostWorkspaceMCPRefreshResources refreshes resources for a named MCP server. +// +// @Summary Refresh MCP resources +// @Tags mcp +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.MCPNameRequest true "MCP name request" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/mcp/refresh-resources [post] func (c *controllerV1) handlePostWorkspaceMCPRefreshResources(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - Name string `json:"name"` - } + var req proto.MCPNameRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") diff --git a/internal/server/proto.go b/internal/server/proto.go index 588086960eca2c39f38428f376de7f0b0b83cf0e..5cec441968f1df81a634b6fdd67469435f39bee4 100644 --- a/internal/server/proto.go +++ b/internal/server/proto.go @@ -16,14 +16,36 @@ type controllerV1 struct { server *Server } +// handleGetHealth checks server health. +// +// @Summary Health check +// @Tags system +// @Success 200 +// @Router /health [get] func (c *controllerV1) handleGetHealth(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) } +// handleGetVersion returns server version information. +// +// @Summary Get server version +// @Tags system +// @Produce json +// @Success 200 {object} proto.VersionInfo +// @Router /version [get] func (c *controllerV1) handleGetVersion(w http.ResponseWriter, _ *http.Request) { jsonEncode(w, c.backend.VersionInfo()) } +// handlePostControl sends a control command to the server. +// +// @Summary Send server control command +// @Tags system +// @Accept json +// @Param request body proto.ServerControl true "Control command (e.g. shutdown)" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Router /control [post] func (c *controllerV1) handlePostControl(w http.ResponseWriter, r *http.Request) { var req proto.ServerControl if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -42,14 +64,38 @@ func (c *controllerV1) handlePostControl(w http.ResponseWriter, r *http.Request) } } +// handleGetConfig returns global server configuration. +// +// @Summary Get server config +// @Tags system +// @Produce json +// @Success 200 {object} object +// @Router /config [get] func (c *controllerV1) handleGetConfig(w http.ResponseWriter, _ *http.Request) { jsonEncode(w, c.backend.Config()) } +// handleGetWorkspaces lists all workspaces. +// +// @Summary List workspaces +// @Tags workspaces +// @Produce json +// @Success 200 {array} proto.Workspace +// @Router /workspaces [get] func (c *controllerV1) handleGetWorkspaces(w http.ResponseWriter, _ *http.Request) { jsonEncode(w, c.backend.ListWorkspaces()) } +// handleGetWorkspace returns a single workspace by ID. +// +// @Summary Get workspace +// @Tags workspaces +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {object} proto.Workspace +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id} [get] func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") ws, err := c.backend.GetWorkspaceProto(id) @@ -60,6 +106,17 @@ func (c *controllerV1) handleGetWorkspace(w http.ResponseWriter, r *http.Request jsonEncode(w, ws) } +// handlePostWorkspaces creates a new workspace. +// +// @Summary Create workspace +// @Tags workspaces +// @Accept json +// @Produce json +// @Param request body proto.Workspace true "Workspace creation params" +// @Success 200 {object} proto.Workspace +// @Failure 400 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces [post] func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Request) { var args proto.Workspace if err := json.NewDecoder(r.Body).Decode(&args); err != nil { @@ -76,11 +133,29 @@ func (c *controllerV1) handlePostWorkspaces(w http.ResponseWriter, r *http.Reque jsonEncode(w, result) } +// handleDeleteWorkspaces deletes a workspace. +// +// @Summary Delete workspace +// @Tags workspaces +// @Param id path string true "Workspace ID" +// @Success 200 +// @Failure 404 {object} proto.Error +// @Router /workspaces/{id} [delete] func (c *controllerV1) handleDeleteWorkspaces(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") c.backend.DeleteWorkspace(id) } +// handleGetWorkspaceConfig returns workspace configuration. +// +// @Summary Get workspace config +// @Tags workspaces +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {object} object +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/config [get] func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") cfg, err := c.backend.GetWorkspaceConfig(id) @@ -91,6 +166,16 @@ func (c *controllerV1) handleGetWorkspaceConfig(w http.ResponseWriter, r *http.R jsonEncode(w, cfg) } +// handleGetWorkspaceProviders lists available providers for a workspace. +// +// @Summary Get workspace providers +// @Tags workspaces +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {object} object +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/providers [get] func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") providers, err := c.backend.GetWorkspaceProviders(id) @@ -101,6 +186,16 @@ func (c *controllerV1) handleGetWorkspaceProviders(w http.ResponseWriter, r *htt jsonEncode(w, providers) } +// handleGetWorkspaceEvents streams workspace events as Server-Sent Events. +// +// @Summary Stream workspace events (SSE) +// @Tags workspaces +// @Produce text/event-stream +// @Param id path string true "Workspace ID" +// @Success 200 +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/events [get] func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.Request) { flusher := http.NewResponseController(w) id := r.PathValue("id") @@ -140,6 +235,16 @@ func (c *controllerV1) handleGetWorkspaceEvents(w http.ResponseWriter, r *http.R } } +// handleGetWorkspaceLSPs lists LSP clients for a workspace. +// +// @Summary List LSP clients +// @Tags lsp +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {object} map[string]proto.LSPClientInfo +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/lsps [get] func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") states, err := c.backend.GetLSPStates(id) @@ -160,6 +265,17 @@ func (c *controllerV1) handleGetWorkspaceLSPs(w http.ResponseWriter, r *http.Req jsonEncode(w, result) } +// handleGetWorkspaceLSPDiagnostics returns diagnostics for an LSP client. +// +// @Summary Get LSP diagnostics +// @Tags lsp +// @Produce json +// @Param id path string true "Workspace ID" +// @Param lsp path string true "LSP client name" +// @Success 200 {object} object +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/lsps/{lsp}/diagnostics [get] func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") lspName := r.PathValue("lsp") @@ -171,6 +287,16 @@ func (c *controllerV1) handleGetWorkspaceLSPDiagnostics(w http.ResponseWriter, r jsonEncode(w, diagnostics) } +// handleGetWorkspaceSessions lists sessions for a workspace. +// +// @Summary List sessions +// @Tags sessions +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {array} proto.Session +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/sessions [get] func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sessions, err := c.backend.ListSessions(r.Context(), id) @@ -181,6 +307,19 @@ func (c *controllerV1) handleGetWorkspaceSessions(w http.ResponseWriter, r *http jsonEncode(w, sessions) } +// handlePostWorkspaceSessions creates a new session in a workspace. +// +// @Summary Create session +// @Tags sessions +// @Accept json +// @Produce json +// @Param id path string true "Workspace ID" +// @Param request body proto.Session true "Session creation params (title)" +// @Success 200 {object} proto.Session +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/sessions [post] func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") @@ -199,6 +338,17 @@ func (c *controllerV1) handlePostWorkspaceSessions(w http.ResponseWriter, r *htt jsonEncode(w, sess) } +// handleGetWorkspaceSession returns a single session. +// +// @Summary Get session +// @Tags sessions +// @Produce json +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Success 200 {object} proto.Session +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/sessions/{sid} [get] func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") @@ -210,6 +360,17 @@ func (c *controllerV1) handleGetWorkspaceSession(w http.ResponseWriter, r *http. jsonEncode(w, sess) } +// handleGetWorkspaceSessionHistory returns the history for a session. +// +// @Summary Get session history +// @Tags sessions +// @Produce json +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Success 200 {array} proto.File +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/sessions/{sid}/history [get] func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") @@ -221,6 +382,17 @@ func (c *controllerV1) handleGetWorkspaceSessionHistory(w http.ResponseWriter, r jsonEncode(w, history) } +// handleGetWorkspaceSessionMessages returns all messages for a session. +// +// @Summary Get session messages +// @Tags sessions +// @Produce json +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Success 200 {array} proto.Message +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/sessions/{sid}/messages [get] func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") @@ -232,6 +404,20 @@ func (c *controllerV1) handleGetWorkspaceSessionMessages(w http.ResponseWriter, jsonEncode(w, messagesToProto(messages)) } +// handlePutWorkspaceSession updates a session. +// +// @Summary Update session +// @Tags sessions +// @Accept json +// @Produce json +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Param request body proto.Session true "Updated session" +// @Success 200 {object} proto.Session +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/sessions/{sid} [put] func (c *controllerV1) handlePutWorkspaceSession(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") @@ -250,6 +436,16 @@ func (c *controllerV1) handlePutWorkspaceSession(w http.ResponseWriter, r *http. jsonEncode(w, saved) } +// handleDeleteWorkspaceSession deletes a session. +// +// @Summary Delete session +// @Tags sessions +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Success 200 +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/sessions/{sid} [delete] func (c *controllerV1) handleDeleteWorkspaceSession(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") @@ -260,6 +456,17 @@ func (c *controllerV1) handleDeleteWorkspaceSession(w http.ResponseWriter, r *ht w.WriteHeader(http.StatusOK) } +// handleGetWorkspaceSessionUserMessages returns user messages for a session. +// +// @Summary Get user messages for session +// @Tags sessions +// @Produce json +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Success 200 {array} proto.Message +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/sessions/{sid}/messages/user [get] func (c *controllerV1) handleGetWorkspaceSessionUserMessages(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") @@ -271,6 +478,16 @@ func (c *controllerV1) handleGetWorkspaceSessionUserMessages(w http.ResponseWrit jsonEncode(w, messagesToProto(messages)) } +// handleGetWorkspaceAllUserMessages returns all user messages across sessions. +// +// @Summary Get all user messages for workspace +// @Tags workspaces +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {array} proto.Message +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/messages/user [get] func (c *controllerV1) handleGetWorkspaceAllUserMessages(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") messages, err := c.backend.ListAllUserMessages(r.Context(), id) @@ -281,6 +498,17 @@ func (c *controllerV1) handleGetWorkspaceAllUserMessages(w http.ResponseWriter, jsonEncode(w, messagesToProto(messages)) } +// handleGetWorkspaceSessionFileTrackerFiles lists files read in a session. +// +// @Summary List tracked files for session +// @Tags filetracker +// @Produce json +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Success 200 {array} string +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/sessions/{sid}/filetracker/files [get] func (c *controllerV1) handleGetWorkspaceSessionFileTrackerFiles(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") @@ -292,13 +520,22 @@ func (c *controllerV1) handleGetWorkspaceSessionFileTrackerFiles(w http.Response jsonEncode(w, files) } +// handlePostWorkspaceFileTrackerRead records a file read event. +// +// @Summary Record file read +// @Tags filetracker +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.FileTrackerReadRequest true "File tracker read request" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/filetracker/read [post] func (c *controllerV1) handlePostWorkspaceFileTrackerRead(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - SessionID string `json:"session_id"` - Path string `json:"path"` - } + var req proto.FileTrackerReadRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") @@ -312,6 +549,18 @@ func (c *controllerV1) handlePostWorkspaceFileTrackerRead(w http.ResponseWriter, w.WriteHeader(http.StatusOK) } +// handleGetWorkspaceFileTrackerLastRead returns the last read time for a file. +// +// @Summary Get last read time for file +// @Tags filetracker +// @Produce json +// @Param id path string true "Workspace ID" +// @Param session_id query string false "Session ID" +// @Param path query string true "File path" +// @Success 200 {object} object +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/filetracker/lastread [get] func (c *controllerV1) handleGetWorkspaceFileTrackerLastRead(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.URL.Query().Get("session_id") @@ -325,12 +574,22 @@ func (c *controllerV1) handleGetWorkspaceFileTrackerLastRead(w http.ResponseWrit jsonEncode(w, t) } +// handlePostWorkspaceLSPStart starts an LSP server for a path. +// +// @Summary Start LSP server +// @Tags lsp +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.LSPStartRequest true "LSP start request" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/lsps/start [post] func (c *controllerV1) handlePostWorkspaceLSPStart(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") - var req struct { - Path string `json:"path"` - } + var req proto.LSPStartRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { c.server.logError(r, "Failed to decode request", "error", err) jsonError(w, http.StatusBadRequest, "failed to decode request") @@ -344,6 +603,15 @@ func (c *controllerV1) handlePostWorkspaceLSPStart(w http.ResponseWriter, r *htt w.WriteHeader(http.StatusOK) } +// handlePostWorkspaceLSPStopAll stops all LSP servers. +// +// @Summary Stop all LSP servers +// @Tags lsp +// @Param id path string true "Workspace ID" +// @Success 200 +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/lsps/stop [post] func (c *controllerV1) handlePostWorkspaceLSPStopAll(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := c.backend.LSPStopAll(r.Context(), id); err != nil { @@ -353,6 +621,16 @@ func (c *controllerV1) handlePostWorkspaceLSPStopAll(w http.ResponseWriter, r *h w.WriteHeader(http.StatusOK) } +// handleGetWorkspaceAgent returns agent info for a workspace. +// +// @Summary Get agent info +// @Tags agent +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {object} proto.AgentInfo +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/agent [get] func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") info, err := c.backend.GetAgentInfo(id) @@ -363,6 +641,18 @@ func (c *controllerV1) handleGetWorkspaceAgent(w http.ResponseWriter, r *http.Re jsonEncode(w, info) } +// handlePostWorkspaceAgent sends a message to the agent. +// +// @Summary Send message to agent +// @Tags agent +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.AgentMessage true "Agent message" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/agent [post] func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") @@ -380,6 +670,15 @@ func (c *controllerV1) handlePostWorkspaceAgent(w http.ResponseWriter, r *http.R w.WriteHeader(http.StatusOK) } +// handlePostWorkspaceAgentInit initializes the agent for a workspace. +// +// @Summary Initialize agent +// @Tags agent +// @Param id path string true "Workspace ID" +// @Success 200 +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/agent/init [post] func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := c.backend.InitAgent(r.Context(), id); err != nil { @@ -389,6 +688,15 @@ func (c *controllerV1) handlePostWorkspaceAgentInit(w http.ResponseWriter, r *ht w.WriteHeader(http.StatusOK) } +// handlePostWorkspaceAgentUpdate updates the agent for a workspace. +// +// @Summary Update agent +// @Tags agent +// @Param id path string true "Workspace ID" +// @Success 200 +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/agent/update [post] func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if err := c.backend.UpdateAgent(r.Context(), id); err != nil { @@ -398,6 +706,17 @@ func (c *controllerV1) handlePostWorkspaceAgentUpdate(w http.ResponseWriter, r * w.WriteHeader(http.StatusOK) } +// handleGetWorkspaceAgentSession returns a specific agent session. +// +// @Summary Get agent session +// @Tags agent +// @Produce json +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Success 200 {object} proto.AgentSession +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/agent/sessions/{sid} [get] func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") @@ -409,6 +728,16 @@ func (c *controllerV1) handleGetWorkspaceAgentSession(w http.ResponseWriter, r * jsonEncode(w, agentSession) } +// handlePostWorkspaceAgentSessionCancel cancels a running agent session. +// +// @Summary Cancel agent session +// @Tags agent +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Success 200 +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/agent/sessions/{sid}/cancel [post] func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") @@ -419,6 +748,17 @@ func (c *controllerV1) handlePostWorkspaceAgentSessionCancel(w http.ResponseWrit w.WriteHeader(http.StatusOK) } +// handleGetWorkspaceAgentSessionPromptQueued returns whether a queued prompt exists. +// +// @Summary Get queued prompt status +// @Tags agent +// @Produce json +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Success 200 {object} object +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/agent/sessions/{sid}/prompts/queued [get] func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") @@ -430,6 +770,16 @@ func (c *controllerV1) handleGetWorkspaceAgentSessionPromptQueued(w http.Respons jsonEncode(w, queued) } +// handlePostWorkspaceAgentSessionPromptClear clears the prompt queue for a session. +// +// @Summary Clear prompt queue +// @Tags agent +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Success 200 +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/agent/sessions/{sid}/prompts/clear [post] func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") @@ -440,6 +790,16 @@ func (c *controllerV1) handlePostWorkspaceAgentSessionPromptClear(w http.Respons w.WriteHeader(http.StatusOK) } +// handlePostWorkspaceAgentSessionSummarize summarizes a session. +// +// @Summary Summarize session +// @Tags agent +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Success 200 +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/agent/sessions/{sid}/summarize [post] func (c *controllerV1) handlePostWorkspaceAgentSessionSummarize(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") @@ -450,6 +810,17 @@ func (c *controllerV1) handlePostWorkspaceAgentSessionSummarize(w http.ResponseW w.WriteHeader(http.StatusOK) } +// handleGetWorkspaceAgentSessionPromptList returns the list of queued prompts. +// +// @Summary List queued prompts +// @Tags agent +// @Produce json +// @Param id path string true "Workspace ID" +// @Param sid path string true "Session ID" +// @Success 200 {array} string +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/agent/sessions/{sid}/prompts/list [get] func (c *controllerV1) handleGetWorkspaceAgentSessionPromptList(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") sid := r.PathValue("sid") @@ -461,6 +832,17 @@ func (c *controllerV1) handleGetWorkspaceAgentSessionPromptList(w http.ResponseW jsonEncode(w, prompts) } +// handleGetWorkspaceAgentDefaultSmallModel returns the default small model for a provider. +// +// @Summary Get default small model +// @Tags agent +// @Produce json +// @Param id path string true "Workspace ID" +// @Param provider_id query string false "Provider ID" +// @Success 200 {object} object +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/agent/default-small-model [get] func (c *controllerV1) handleGetWorkspaceAgentDefaultSmallModel(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") providerID := r.URL.Query().Get("provider_id") @@ -472,6 +854,18 @@ func (c *controllerV1) handleGetWorkspaceAgentDefaultSmallModel(w http.ResponseW jsonEncode(w, model) } +// handlePostWorkspacePermissionsGrant grants a permission request. +// +// @Summary Grant permission +// @Tags permissions +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.PermissionGrant true "Permission grant" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/permissions/grant [post] func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") @@ -489,6 +883,18 @@ func (c *controllerV1) handlePostWorkspacePermissionsGrant(w http.ResponseWriter w.WriteHeader(http.StatusOK) } +// handlePostWorkspacePermissionsSkip sets whether to skip permission prompts. +// +// @Summary Set skip permissions +// @Tags permissions +// @Accept json +// @Param id path string true "Workspace ID" +// @Param request body proto.PermissionSkipRequest true "Permission skip request" +// @Success 200 +// @Failure 400 {object} proto.Error +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/permissions/skip [post] func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") @@ -505,6 +911,16 @@ func (c *controllerV1) handlePostWorkspacePermissionsSkip(w http.ResponseWriter, } } +// handleGetWorkspacePermissionsSkip returns whether permission prompts are skipped. +// +// @Summary Get skip permissions status +// @Tags permissions +// @Produce json +// @Param id path string true "Workspace ID" +// @Success 200 {object} proto.PermissionSkipRequest +// @Failure 404 {object} proto.Error +// @Failure 500 {object} proto.Error +// @Router /workspaces/{id}/permissions/skip [get] func (c *controllerV1) handleGetWorkspacePermissionsSkip(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") skip, err := c.backend.GetPermissionsSkip(id) diff --git a/internal/server/server.go b/internal/server/server.go index ec84bef777c1fe6c3c6e1e4daabee4ba841ee259..3b736d3d478155c66496855b4d3c6211484fcaa7 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -11,8 +11,10 @@ import ( "runtime" "strings" + _ "github.com/charmbracelet/crush/docs" "github.com/charmbracelet/crush/internal/backend" "github.com/charmbracelet/crush/internal/config" + httpswagger "github.com/swaggo/http-swagger/v2" ) // ErrServerClosed is returned when the server is closed. @@ -161,6 +163,7 @@ func NewServer(cfg *config.ConfigStore, network, address string) *Server { mux.HandleFunc("GET /v1/workspaces/{id}/mcp/states", c.handleGetWorkspaceMCPStates) mux.HandleFunc("POST /v1/workspaces/{id}/mcp/refresh-prompts", c.handlePostWorkspaceMCPRefreshPrompts) mux.HandleFunc("POST /v1/workspaces/{id}/mcp/refresh-resources", c.handlePostWorkspaceMCPRefreshResources) + mux.Handle("/v1/docs/", httpswagger.WrapHandler) s.h = &http.Server{ Protocols: &p, Handler: s.loggingHandler(mux), diff --git a/main.go b/main.go index e75cb03e3575cf902c2ff4b44ddd15e0405f0b60..83fd1cf96ae8ee34089d5a577711f569ec2faeb6 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,13 @@ +// Package main is the entry point for the Crush CLI. +// +// @title Crush API +// @version 1.0 +// @description Crush is a terminal-based AI coding assistant. This API is served over a Unix socket (or Windows named pipe) and provides programmatic access to workspaces, sessions, agents, LSP, MCP, and more. +// @contact.name Charm +// @contact.url https://charm.sh +// @license.name MIT +// @license.url https://github.com/charmbracelet/crush/blob/main/LICENSE +// @BasePath /v1 package main import (