From 695ced1118986921448f2b3941877eca4023125d Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Tue, 17 Mar 2026 15:22:34 -0300 Subject: [PATCH 01/26] chore: add support for gpt 5.4 mini and nano (#2419) --- go.mod | 52 +++++++++++++-------------- go.sum | 112 ++++++++++++++++++++++++++++----------------------------- 2 files changed, 82 insertions(+), 82 deletions(-) diff --git a/go.mod b/go.mod index 9bb5dae7ddd3e552d7100a4e97f0ca69239bdfb3..6b8632b78b9da6a775cddaa6e5578139d04ae505 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.26.1 require ( charm.land/bubbles/v2 v2.0.0 charm.land/bubbletea/v2 v2.0.2 - charm.land/catwalk v0.30.3 + charm.land/catwalk v0.30.4 charm.land/fang/v2 v2.0.1 - charm.land/fantasy v0.12.3 + charm.land/fantasy v0.13.1 charm.land/glamour/v2 v2.0.0 charm.land/lipgloss/v2 v2.0.2 charm.land/log/v2 v2.0.0 @@ -82,20 +82,20 @@ require ( 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/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 - github.com/aws/aws-sdk-go-v2/config v1.32.11 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.4 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect + github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect github.com/aws/smithy-go v1.24.2 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -126,7 +126,7 @@ require ( github.com/google/go-cmp v0.7.0 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect @@ -175,11 +175,11 @@ require ( github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark-emoji v1.0.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect golang.org/x/crypto v0.49.0 // indirect @@ -188,11 +188,11 @@ require ( 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 - google.golang.org/api v0.269.0 // indirect - google.golang.org/genai v1.49.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/api v0.270.0 // indirect + google.golang.org/genai v1.50.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect - google.golang.org/grpc v1.79.1 // indirect + google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect diff --git a/go.sum b/go.sum index f2562330e8a0eed6bb4882a3c788798f5d354ab0..29e89e97cb46d13c4e91421e818134fc5e156e09 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,12 @@ charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= -charm.land/catwalk v0.30.3 h1:eCRwVoi1znrNGYiPZoBIbWt8+Q4kDhT3zziqnPO3s2Y= -charm.land/catwalk v0.30.3/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg= +charm.land/catwalk v0.30.4 h1:+XTYOM6VFthEfNiT/Cb3bXXXrn59UKlVCsNaOLEfQc4= +charm.land/catwalk v0.30.4/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg= charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY= charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII= -charm.land/fantasy v0.12.3 h1:gvqRWD7vWmpNN0VcQ+rSku5QdTlLegqrlJDVCDdAh58= -charm.land/fantasy v0.12.3/go.mod h1:noWyUtEgUrfsRXqmGbz7NQlZpf6KFwNwjA+jzdS3No8= +charm.land/fantasy v0.13.1 h1:a6s4Jl3EPEok2PIvjGNhCVISeuRILH8sN28pXCwT5To= +charm.land/fantasy v0.13.1/go.mod h1:9L50Xe+Dvc23rjjOeas4Ez0jOd2NuiXSpqmPxBHQ3mQ= charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U= charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w= charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= @@ -52,34 +52,34 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA= -github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= -github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs= -github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo= -github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI= +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aymanbagabas/go-nativeclipboard v0.1.3 h1:FmAWHPTwneAixu7uGDn3cL42xPlUCdNp2J8egMn3P1k= @@ -214,8 +214,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -398,20 +398,20 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -506,8 +506,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -519,14 +519,14 @@ golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= -google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE= -google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0= -google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/api v0.270.0 h1:4rJZbIuWSTohczG9mG2ukSDdt9qKx4sSSHIydTN26L4= +google.golang.org/api v0.270.0/go.mod h1:5+H3/8DlXpQWrSz4RjGGwz5HfJAQSEI8Bc6JqQNH77U= +google.golang.org/genai v1.50.0 h1:yHKV/vjoeN9PJ3iF0ur4cBZco4N3Kl7j09rMq7XSoWk= +google.golang.org/genai v1.50.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= 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= From c07a7fb9e9c46ab145061dfa2d52d1933a20d206 Mon Sep 17 00:00:00 2001 From: Charm <124303983+charmcli@users.noreply.github.com> Date: Tue, 17 Mar 2026 15:42:16 -0300 Subject: [PATCH 03/26] chore(legal): @nghiant03 has signed the CLA --- .github/cla-signatures.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/cla-signatures.json b/.github/cla-signatures.json index c1f5f7c12ee1d19b064c856d6154d95087674103..28beb0e7b1bdd75859d88eb4f0b1c900483ff7ff 100644 --- a/.github/cla-signatures.json +++ b/.github/cla-signatures.json @@ -1351,6 +1351,14 @@ "created_at": "2026-03-15T02:30:55Z", "repoId": 987670088, "pullRequestNo": 2410 + }, + { + "name": "nghiant03", + "id": 102637959, + "comment_id": 4077213520, + "created_at": "2026-03-17T18:41:50Z", + "repoId": 987670088, + "pullRequestNo": 2421 } ] } \ No newline at end of file From aa253c524ad789c0d0eccad57322b18ae469edbc Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 18 Mar 2026 17:27:06 -0300 Subject: [PATCH 04/26] chore(taskfile): enhance `deps` task to work for recent releases --- Taskfile.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Taskfile.yaml b/Taskfile.yaml index 38e8a16313d17b9b1826ce4b6f055d39537916ec..eaac36ef7a2e634f400ad24433c66cfb6bc2fa7b 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -180,9 +180,14 @@ tasks: deps: desc: Update Fantasy and Catwalk + env: + # The Go proxy takes a bit of time to see the latest release. Setting + # these bypass it to ensure we can update to a release from a minute ago. + GOPROXY: direct + GONOSUMDB: charm.land/* cmds: - - go get charm.land/fantasy - - go get charm.land/catwalk + - go get charm.land/fantasy@latest + - go get charm.land/catwalk@latest - go mod tidy sqlc: From 724102d869564113357fc939c0512cf4e5e37882 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Wed, 18 Mar 2026 17:27:20 -0300 Subject: [PATCH 05/26] chore(deps): update fantasy and catwalk --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 6b8632b78b9da6a775cddaa6e5578139d04ae505..766f22d9cabd576068ae0da2ed65924e9aa7875e 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.26.1 require ( charm.land/bubbles/v2 v2.0.0 charm.land/bubbletea/v2 v2.0.2 - charm.land/catwalk v0.30.4 + charm.land/catwalk v0.30.7 charm.land/fang/v2 v2.0.1 - charm.land/fantasy v0.13.1 + charm.land/fantasy v0.15.0 charm.land/glamour/v2 v2.0.0 charm.land/lipgloss/v2 v2.0.2 charm.land/log/v2 v2.0.0 diff --git a/go.sum b/go.sum index 29e89e97cb46d13c4e91421e818134fc5e156e09..3c56ce5937fbe24747ceef0e1786ded2bf8eeef9 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,12 @@ charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= -charm.land/catwalk v0.30.4 h1:+XTYOM6VFthEfNiT/Cb3bXXXrn59UKlVCsNaOLEfQc4= -charm.land/catwalk v0.30.4/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg= +charm.land/catwalk v0.30.7 h1:wOUCRpLw3hAweBCIxJJ2IrfSFdvNe3HL5tEz1X1Kr+o= +charm.land/catwalk v0.30.7/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg= charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY= charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII= -charm.land/fantasy v0.13.1 h1:a6s4Jl3EPEok2PIvjGNhCVISeuRILH8sN28pXCwT5To= -charm.land/fantasy v0.13.1/go.mod h1:9L50Xe+Dvc23rjjOeas4Ez0jOd2NuiXSpqmPxBHQ3mQ= +charm.land/fantasy v0.15.0 h1:PkwLVXHvbQD1BSBEXCJ/4wx3QqlG140TfLqOM541OKg= +charm.land/fantasy v0.15.0/go.mod h1:9L50Xe+Dvc23rjjOeas4Ez0jOd2NuiXSpqmPxBHQ3mQ= charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U= charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w= charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= From 630ff8c5dfeaabc96b8f05917d28455ad7e1075e Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 19 Mar 2026 11:06:13 -0300 Subject: [PATCH 06/26] feat: add support for `--session` and `--continue` for the tui (#2422) These flags were previously only available for `crush run`. --- internal/cmd/root.go | 23 ++++++++++++++++++++++- internal/ui/model/ui.go | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 060d94c13a3d1cb9b927ebfbddc473aa1593e9dc..39453f76a7dee055c17b3c220128a20242846c48 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -37,6 +37,9 @@ func init() { rootCmd.PersistentFlags().BoolP("debug", "d", false, "Debug") rootCmd.Flags().BoolP("help", "h", false, "Help") rootCmd.Flags().BoolP("yolo", "y", false, "Automatically accept all permissions (dangerous mode)") + rootCmd.Flags().StringP("session", "s", "", "Continue a previous session by ID") + rootCmd.Flags().BoolP("continue", "C", false, "Continue the most recent session") + rootCmd.MarkFlagsMutuallyExclusive("session", "continue") rootCmd.AddCommand( runCmd, @@ -73,21 +76,39 @@ crush --yolo # Run with custom data directory crush --data-dir /path/to/custom/.crush + +# Continue a previous session +crush --session {session-id} + +# Continue the most recent session +crush --continue `, RunE: func(cmd *cobra.Command, args []string) error { + sessionID, _ := cmd.Flags().GetString("session") + continueLast, _ := cmd.Flags().GetBool("continue") + app, err := setupAppWithProgressBar(cmd) if err != nil { return err } defer app.Shutdown() + // Resolve session ID if provided + if sessionID != "" { + sess, err := resolveSessionID(cmd.Context(), app.Sessions, sessionID) + if err != nil { + return err + } + sessionID = sess.ID + } + event.AppInitialized() // Set up the TUI. var env uv.Environ = os.Environ() com := common.DefaultCommon(app) - model := ui.New(com) + model := ui.New(com, sessionID, continueLast) program := tea.NewProgram( model, diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index ed0a7dd6851fbbe0b16540a4b72dc2486e96f438..f126020d32f2c3c974a0a9435bc69ae56380330d 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -139,6 +139,11 @@ type UI struct { // keeps track of read files while we don't have a session id sessionFileReads []string + // initialSessionID is set when loading a specific session on startup. + initialSessionID string + // continueLastSession is set to continue the most recent session on startup. + continueLastSession bool + lastUserMessageTime int64 // The width and height of the terminal in cells. @@ -242,7 +247,7 @@ type UI struct { } // New creates a new instance of the [UI] model. -func New(com *common.Common) *UI { +func New(com *common.Common, initialSessionID string, continueLast bool) *UI { // Editor components ta := textarea.New() ta.SetStyles(com.Styles.TextArea) @@ -298,6 +303,8 @@ func New(com *common.Common) *UI { mcpStates: make(map[string]mcp.ClientInfo), notifyBackend: notification.NoopBackend{}, notifyWindowFocused: true, + initialSessionID: initialSessionID, + continueLastSession: continueLast, } status := NewStatus(com, ui) @@ -346,9 +353,34 @@ func (m *UI) Init() tea.Cmd { cmds = append(cmds, m.loadCustomCommands()) // load prompt history async cmds = append(cmds, m.loadPromptHistory()) + // load initial session if specified + if cmd := m.loadInitialSession(); cmd != nil { + cmds = append(cmds, cmd) + } return tea.Batch(cmds...) } +// loadInitialSession loads the initial session if one was specified on startup. +func (m *UI) loadInitialSession() tea.Cmd { + switch { + case m.state != uiLanding: + // Only load if we're in landing state (i.e., fully configured) + return nil + case m.initialSessionID != "": + return m.loadSession(m.initialSessionID) + case m.continueLastSession: + return func() tea.Msg { + sess, err := m.com.App.Sessions.GetLast(context.Background()) + if err != nil { + return nil + } + return m.loadSession(sess.ID)() + } + default: + return nil + } +} + // sendNotification returns a command that sends a notification if allowed by policy. func (m *UI) sendNotification(n notification.Notification) tea.Cmd { if !m.shouldSendNotification() { From ddd99cd2f650adfa9dc9ec961365266d8512c4a0 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 19 Mar 2026 11:07:16 -0300 Subject: [PATCH 07/26] feat: add aliases: `crush r` -> `crush run`, `crush s` -> `crush session` (#2424) --- internal/cmd/run.go | 5 +++-- internal/cmd/session.go | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/cmd/run.go b/internal/cmd/run.go index 8215438ddef6d06d1a7a3bbb863fc24935835297..a648a6d60fbb07e870d7c66de4b49323e6bedb69 100644 --- a/internal/cmd/run.go +++ b/internal/cmd/run.go @@ -14,8 +14,9 @@ import ( ) var runCmd = &cobra.Command{ - Use: "run [prompt...]", - Short: "Run a single non-interactive prompt", + Aliases: []string{"r"}, + Use: "run [prompt...]", + Short: "Run a single non-interactive prompt", Long: `Run a single prompt in non-interactive mode and exit. The prompt can be provided as arguments or piped from stdin.`, Example: ` diff --git a/internal/cmd/session.go b/internal/cmd/session.go index f4267f7d57300ac91504adbc0da01ff149b71412..d5fa973516b9b63a926cd4a30e81c3be8b4de43a 100644 --- a/internal/cmd/session.go +++ b/internal/cmd/session.go @@ -30,7 +30,7 @@ import ( var sessionCmd = &cobra.Command{ Use: "session", - Aliases: []string{"sessions"}, + Aliases: []string{"sessions", "s"}, Short: "Manage sessions", Long: "Manage Crush sessions. Agents can use --json for machine-readable output.", } From e0dcaba820b1a5f391fad9b89590e8c8932b6404 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Thu, 19 Mar 2026 15:59:19 +0100 Subject: [PATCH 08/26] feat: docker mcp integration (#2026) Co-authored-by: Andrey Nering Co-authored-by: Christian Rocha --- internal/agent/agent.go | 3 + internal/agent/coordinator.go | 16 ++ internal/agent/tools/mcp-tools.go | 48 ++-- internal/agent/tools/mcp/init.go | 122 +++++++---- internal/agent/tools/mcp/tools.go | 38 +++- internal/agent/tools/mcp/tools_test.go | 102 +++++++++ internal/config/docker_mcp.go | 75 +++++++ internal/config/docker_mcp_test.go | 168 ++++++++++++++ internal/ui/AGENTS.md | 1 + internal/ui/chat/docker_mcp.go | 290 +++++++++++++++++++++++++ internal/ui/chat/tools.go | 4 +- internal/ui/dialog/actions.go | 4 + internal/ui/dialog/commands.go | 10 + internal/ui/model/mcp.go | 8 +- internal/ui/model/ui.go | 36 +++ internal/ui/styles/styles.go | 8 + 16 files changed, 870 insertions(+), 63 deletions(-) create mode 100644 internal/agent/tools/mcp/tools_test.go create mode 100644 internal/config/docker_mcp.go create mode 100644 internal/config/docker_mcp_test.go create mode 100644 internal/ui/chat/docker_mcp.go diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 2b09be75f4882c44869428008156772c3cc4ad99..11a87f58554729048cd7a0629979a0c6bb3babd2 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -266,6 +266,9 @@ func (a *sessionAgent) Run(ctx context.Context, call SessionAgentCall) (*fantasy prepared.Messages[i].ProviderOptions = nil } + // Use latest tools (updated by SetTools when MCP tools change). + prepared.Tools = a.tools.Copy() + queuedCalls, _ := a.messageQueue.Get(call.SessionID) a.messageQueue.Del(call.SessionID) for _, queued := range queuedCalls { diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 04a49eebe2aeb110cd0cd55421d9b632480e7461..53fecb0388628caa8da1eed43e39b2898529e768 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -71,6 +71,7 @@ type Coordinator interface { Summarize(context.Context, string) error Model() Model UpdateModels(ctx context.Context) error + RefreshTools(ctx context.Context) error } type coordinator struct { @@ -904,6 +905,21 @@ func (c *coordinator) UpdateModels(ctx context.Context) error { return nil } +func (c *coordinator) RefreshTools(ctx context.Context) error { + agentCfg, ok := c.cfg.Config().Agents[config.AgentCoder] + if !ok { + return errors.New("coder agent not configured") + } + + tools, err := c.buildTools(ctx, agentCfg) + if err != nil { + return err + } + c.currentAgent.SetTools(tools) + slog.Debug("refreshed agent tools", "count", len(tools)) + return nil +} + func (c *coordinator) QueuedPrompts(sessionID string) int { return c.currentAgent.QueuedPrompts(sessionID) } diff --git a/internal/agent/tools/mcp-tools.go b/internal/agent/tools/mcp-tools.go index e1184118552ee62e75f60c6943f59ecca2868563..a7e9dc5d3de24944a91856250e9c6d0920222a00 100644 --- a/internal/agent/tools/mcp-tools.go +++ b/internal/agent/tools/mcp-tools.go @@ -3,6 +3,7 @@ package tools import ( "context" "fmt" + "slices" "charm.land/fantasy" "github.com/charmbracelet/crush/internal/agent/tools/mcp" @@ -10,6 +11,15 @@ import ( "github.com/charmbracelet/crush/internal/permission" ) +// whitelistDockerTools contains Docker MCP tools that don't require permission. +var whitelistDockerTools = []string{ + "mcp_docker_mcp-find", + "mcp_docker_mcp-add", + "mcp_docker_mcp-remove", + "mcp_docker_mcp-config-set", + "mcp_docker_code-mode", +} + // GetMCPTools gets all the currently available MCP tools. func GetMCPTools(permissions permission.Service, cfg *config.ConfigStore, wd string) []*Tool { var result []*Tool @@ -91,23 +101,27 @@ func (m *Tool) Run(ctx context.Context, params fantasy.ToolCall) (fantasy.ToolRe if sessionID == "" { return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file") } - permissionDescription := fmt.Sprintf("execute %s with the following parameters:", m.Info().Name) - p, err := m.permissions.Request(ctx, - permission.CreatePermissionRequest{ - SessionID: sessionID, - ToolCallID: params.ID, - Path: m.workingDir, - ToolName: m.Info().Name, - Action: "execute", - Description: permissionDescription, - Params: params.Input, - }, - ) - if err != nil { - return fantasy.ToolResponse{}, err - } - if !p { - return fantasy.ToolResponse{}, permission.ErrorPermissionDenied + + // Skip permission for whitelisted Docker MCP tools. + if !slices.Contains(whitelistDockerTools, params.Name) { + permissionDescription := fmt.Sprintf("execute %s with the following parameters:", m.Info().Name) + p, err := m.permissions.Request(ctx, + permission.CreatePermissionRequest{ + SessionID: sessionID, + ToolCallID: params.ID, + Path: m.workingDir, + ToolName: m.Info().Name, + Action: "execute", + Description: permissionDescription, + Params: params.Input, + }, + ) + if err != nil { + return fantasy.ToolResponse{}, err + } + if !p { + return fantasy.ToolResponse{}, permission.ErrorPermissionDenied + } } result, err := mcp.RunTool(ctx, m.cfg, m.mcpName, m.tool.Name, params.Input) diff --git a/internal/agent/tools/mcp/init.go b/internal/agent/tools/mcp/init.go index cba9a51c717b1866b823762f85bfadf90e1a7a10..7ad802bae6963540a616bd4cc1576c7b7d7bb0a4 100644 --- a/internal/agent/tools/mcp/init.go +++ b/internal/agent/tools/mcp/init.go @@ -175,8 +175,6 @@ func Initialize(ctx context.Context, permissions permission.Service, cfg *config } // Set initial starting state - updateState(name, StateStarting, nil, nil, Counts{}) - wg.Add(1) go func(name string, m config.MCPConfig) { defer func() { @@ -196,46 +194,9 @@ func Initialize(ctx context.Context, permissions permission.Service, cfg *config } }() - // createSession handles its own timeout internally. - session, err := createSession(ctx, name, m, cfg.Resolver()) - if err != nil { - return - } - - tools, err := getTools(ctx, session) - if err != nil { - slog.Error("Error listing tools", "error", err) - updateState(name, StateError, err, nil, Counts{}) - session.Close() - return - } - - prompts, err := getPrompts(ctx, session) - if err != nil { - slog.Error("Error listing prompts", "error", err) - updateState(name, StateError, err, nil, Counts{}) - session.Close() - return - } - - resources, err := getResources(ctx, session) - if err != nil { - slog.Error("Error listing resources", "error", err) - updateState(name, StateError, err, nil, Counts{}) - session.Close() - return + if err := initClient(ctx, cfg, name, m, cfg.Resolver()); err != nil { + slog.Debug("failed to initialize mcp client", "name", name, "error", err) } - - toolCount := updateTools(cfg, name, tools) - updatePrompts(name, prompts) - resourceCount := updateResources(name, resources) - sessions.Set(name, session) - - updateState(name, StateConnected, nil, session, Counts{ - Tools: toolCount, - Prompts: len(prompts), - Resources: resourceCount, - }) }(name, m) } wg.Wait() @@ -253,6 +214,85 @@ func WaitForInit(ctx context.Context) error { } } +// InitializeSingle initializes a single MCP client by name. +func InitializeSingle(ctx context.Context, name string, cfg *config.ConfigStore) error { + m, exists := cfg.Config().MCP[name] + if !exists { + return fmt.Errorf("mcp '%s' not found in configuration", name) + } + + if m.Disabled { + updateState(name, StateDisabled, nil, nil, Counts{}) + slog.Debug("skipping disabled mcp", "name", name) + return nil + } + + return initClient(ctx, cfg, name, m, cfg.Resolver()) +} + +// initClient initializes a single MCP client with the given configuration. +func initClient(ctx context.Context, cfg *config.ConfigStore, name string, m config.MCPConfig, resolver config.VariableResolver) error { + // Set initial starting state. + updateState(name, StateStarting, nil, nil, Counts{}) + + // createSession handles its own timeout internally. + session, err := createSession(ctx, name, m, resolver) + if err != nil { + return err + } + + tools, err := getTools(ctx, session) + if err != nil { + slog.Error("Error listing tools", "error", err) + updateState(name, StateError, err, nil, Counts{}) + session.Close() + return err + } + + prompts, err := getPrompts(ctx, session) + if err != nil { + slog.Error("Error listing prompts", "error", err) + updateState(name, StateError, err, nil, Counts{}) + session.Close() + return err + } + + toolCount := updateTools(cfg, name, tools) + updatePrompts(name, prompts) + sessions.Set(name, session) + + updateState(name, StateConnected, nil, session, Counts{ + Tools: toolCount, + Prompts: len(prompts), + }) + + return nil +} + +// DisableSingle disables and closes a single MCP client by name. +func DisableSingle(cfg *config.ConfigStore, name string) error { + session, ok := sessions.Get(name) + if ok { + if err := session.Close(); err != nil && + !errors.Is(err, io.EOF) && + !errors.Is(err, context.Canceled) && + err.Error() != "signal: killed" { + slog.Warn("error closing mcp session", "name", name, "error", err) + } + sessions.Del(name) + } + + // Clear tools and prompts for this MCP. + updateTools(cfg, name, nil) + updatePrompts(name, nil) + + // Update state to disabled. + updateState(name, StateDisabled, nil, nil, Counts{}) + + slog.Info("Disabled mcp client", "name", name) + return nil +} + func getOrRenewClient(ctx context.Context, cfg *config.ConfigStore, name string) (*ClientSession, error) { sess, ok := sessions.Get(name) if !ok { diff --git a/internal/agent/tools/mcp/tools.go b/internal/agent/tools/mcp/tools.go index 8d1d2649ba4381e14fa8d99933f1dfb3b42d27ae..ce85e591e55139343e43179bdd33c88b49c274be 100644 --- a/internal/agent/tools/mcp/tools.go +++ b/internal/agent/tools/mcp/tools.go @@ -2,6 +2,7 @@ package mcp import ( "context" + "encoding/base64" "encoding/json" "fmt" "iter" @@ -81,12 +82,13 @@ func RunTool(ctx context.Context, cfg *config.ConfigStore, name, toolName string textContent := strings.Join(textParts, "\n") - // MCP SDK returns Data as already base64-encoded, so we use it directly. + // We need to make sure the data is base64 + // when using something like docker + playwright the data was not returned correctly. if imageData != nil { return ToolResult{ Type: "image", Content: textContent, - Data: imageData, + Data: ensureBase64(imageData), MediaType: imageMimeType, }, nil } @@ -95,7 +97,7 @@ func RunTool(ctx context.Context, cfg *config.ConfigStore, name, toolName string return ToolResult{ Type: "media", Content: textContent, - Data: audioData, + Data: ensureBase64(audioData), MediaType: audioMimeType, }, nil } @@ -164,3 +166,33 @@ func filterDisabledTools(cfg *config.ConfigStore, mcpName string, tools []*Tool) } return filtered } + +// ensureBase64 checks if data is valid base64 and returns it as-is if so, +// otherwise encodes the raw binary data to base64. +func ensureBase64(data []byte) []byte { + // Check if the data is already valid base64 by attempting to decode it. + // Valid base64 should only contain ASCII characters (A-Z, a-z, 0-9, +, /, =). + if isValidBase64(data) { + return data + } + // Data is raw binary, encode it to base64. + encoded := make([]byte, base64.StdEncoding.EncodedLen(len(data))) + base64.StdEncoding.Encode(encoded, data) + return encoded +} + +// isValidBase64 checks if the data appears to be valid base64-encoded content. +func isValidBase64(data []byte) bool { + if len(data) == 0 { + return true + } + // Base64 strings should only contain ASCII characters. + for _, b := range data { + if b > 127 { + return false + } + } + // Try to decode to verify it's valid base64. + _, err := base64.StdEncoding.DecodeString(string(data)) + return err == nil +} diff --git a/internal/agent/tools/mcp/tools_test.go b/internal/agent/tools/mcp/tools_test.go new file mode 100644 index 0000000000000000000000000000000000000000..3795381ebd2a6a54540a6905ef39c97b8ce59575 --- /dev/null +++ b/internal/agent/tools/mcp/tools_test.go @@ -0,0 +1,102 @@ +package mcp + +import ( + "encoding/base64" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEnsureBase64(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input []byte + wantData []byte // expected output + }{ + { + name: "already base64 encoded", + input: []byte("SGVsbG8gV29ybGQh"), // "Hello World!" in base64 + wantData: []byte("SGVsbG8gV29ybGQh"), + }, + { + name: "raw binary data (PNG header)", + input: []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, + wantData: []byte(base64.StdEncoding.EncodeToString([]byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A})), + }, + { + name: "raw binary with high bytes", + input: []byte{0xFF, 0xD8, 0xFF, 0xE0}, // JPEG header + wantData: []byte(base64.StdEncoding.EncodeToString([]byte{0xFF, 0xD8, 0xFF, 0xE0})), + }, + { + name: "empty data", + input: []byte{}, + wantData: []byte{}, + }, + { + name: "base64 with padding", + input: []byte("YQ=="), // "a" in base64 + wantData: []byte("YQ=="), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := ensureBase64(tt.input) + require.Equal(t, tt.wantData, result) + + // Verify the result is valid base64 that can be decoded. + if len(result) > 0 { + _, err := base64.StdEncoding.DecodeString(string(result)) + require.NoError(t, err, "result should be valid base64") + } + }) + } +} + +func TestIsValidBase64(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input []byte + want bool + }{ + { + name: "valid base64", + input: []byte("SGVsbG8gV29ybGQh"), + want: true, + }, + { + name: "valid base64 with padding", + input: []byte("YQ=="), + want: true, + }, + { + name: "raw binary with high bytes", + input: []byte{0xFF, 0xD8, 0xFF}, + want: false, + }, + { + name: "empty", + input: []byte{}, + want: true, + }, + { + name: "invalid base64 characters", + input: []byte("SGVsbG8!@#$"), + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := isValidBase64(tt.input) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/config/docker_mcp.go b/internal/config/docker_mcp.go new file mode 100644 index 0000000000000000000000000000000000000000..e3b352ec275d5cb5fb588367c16774b127e6e6cb --- /dev/null +++ b/internal/config/docker_mcp.go @@ -0,0 +1,75 @@ +package config + +import ( + "context" + "fmt" + "os/exec" + "time" +) + +// DockerMCPName is the name of the Docker MCP configuration. +const DockerMCPName = "docker" + +// IsDockerMCPAvailable checks if Docker MCP is available by running +// 'docker mcp version'. +func IsDockerMCPAvailable() bool { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, "docker", "mcp", "version") + err := cmd.Run() + return err == nil +} + +// IsDockerMCPEnabled checks if Docker MCP is already configured. +func (c *Config) IsDockerMCPEnabled() bool { + if c.MCP == nil { + return false + } + _, exists := c.MCP[DockerMCPName] + return exists +} + +// EnableDockerMCP adds Docker MCP configuration and persists it. +func (s *ConfigStore) EnableDockerMCP() error { + if !IsDockerMCPAvailable() { + return fmt.Errorf("docker mcp is not available, please ensure docker is installed and 'docker mcp version' succeeds") + } + + mcpConfig := MCPConfig{ + Type: MCPStdio, + Command: "docker", + Args: []string{"mcp", "gateway", "run"}, + Disabled: false, + } + + // Add to in-memory config. + if s.config.MCP == nil { + s.config.MCP = make(map[string]MCPConfig) + } + s.config.MCP[DockerMCPName] = mcpConfig + + // Persist to config file. + if err := s.SetConfigField(ScopeGlobal, "mcp."+DockerMCPName, mcpConfig); err != nil { + return fmt.Errorf("failed to persist docker mcp configuration: %w", err) + } + + return nil +} + +// DisableDockerMCP removes Docker MCP configuration and persists the change. +func (s *ConfigStore) DisableDockerMCP() error { + if s.config.MCP == nil { + return nil + } + + // Remove from in-memory config. + delete(s.config.MCP, DockerMCPName) + + // Persist the updated MCP map to the config file. + if err := s.SetConfigField(ScopeGlobal, "mcp", s.config.MCP); err != nil { + return fmt.Errorf("failed to persist docker mcp removal: %w", err) + } + + return nil +} diff --git a/internal/config/docker_mcp_test.go b/internal/config/docker_mcp_test.go new file mode 100644 index 0000000000000000000000000000000000000000..93438777ce8d735a13fb6f14d25dc93ac31e17ac --- /dev/null +++ b/internal/config/docker_mcp_test.go @@ -0,0 +1,168 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/charmbracelet/crush/internal/env" + "github.com/stretchr/testify/require" +) + +func TestIsDockerMCPEnabled(t *testing.T) { + t.Parallel() + + t.Run("returns false when MCP is nil", func(t *testing.T) { + t.Parallel() + cfg := &Config{ + MCP: nil, + } + require.False(t, cfg.IsDockerMCPEnabled()) + }) + + t.Run("returns false when docker mcp not configured", func(t *testing.T) { + t.Parallel() + cfg := &Config{ + MCP: make(map[string]MCPConfig), + } + require.False(t, cfg.IsDockerMCPEnabled()) + }) + + t.Run("returns true when docker mcp is configured", func(t *testing.T) { + t.Parallel() + cfg := &Config{ + MCP: map[string]MCPConfig{ + DockerMCPName: { + Type: MCPStdio, + Command: "docker", + }, + }, + } + require.True(t, cfg.IsDockerMCPEnabled()) + }) +} + +func TestEnableDockerMCP(t *testing.T) { + t.Parallel() + + t.Run("adds docker mcp to config", func(t *testing.T) { + t.Parallel() + + // Create a temporary directory for config. + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "crush.json") + + cfg := &Config{ + MCP: make(map[string]MCPConfig), + } + store := &ConfigStore{ + config: cfg, + globalDataPath: configPath, + resolver: NewShellVariableResolver(env.New()), + } + + // Only run this test if docker mcp is available. + if !IsDockerMCPAvailable() { + t.Skip("Docker MCP not available, skipping test") + } + + err := store.EnableDockerMCP() + require.NoError(t, err) + + // Check in-memory config. + require.True(t, cfg.IsDockerMCPEnabled()) + mcpConfig, exists := cfg.MCP[DockerMCPName] + require.True(t, exists) + require.Equal(t, MCPStdio, mcpConfig.Type) + require.Equal(t, "docker", mcpConfig.Command) + require.Equal(t, []string{"mcp", "gateway", "run"}, mcpConfig.Args) + require.False(t, mcpConfig.Disabled) + + // Check persisted config. + data, err := os.ReadFile(configPath) + require.NoError(t, err) + require.Contains(t, string(data), "docker") + require.Contains(t, string(data), "gateway") + }) + + t.Run("fails when docker mcp not available", func(t *testing.T) { + t.Parallel() + + // Create a temporary directory for config. + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "crush.json") + + cfg := &Config{ + MCP: make(map[string]MCPConfig), + } + store := &ConfigStore{ + config: cfg, + globalDataPath: configPath, + resolver: NewShellVariableResolver(env.New()), + } + + // Skip if docker mcp is actually available. + if IsDockerMCPAvailable() { + t.Skip("Docker MCP is available, skipping unavailable test") + } + + err := store.EnableDockerMCP() + require.Error(t, err) + require.Contains(t, err.Error(), "docker mcp is not available") + }) +} + +func TestDisableDockerMCP(t *testing.T) { + t.Parallel() + + t.Run("removes docker mcp from config", func(t *testing.T) { + t.Parallel() + + // Create a temporary directory for config. + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "crush.json") + + cfg := &Config{ + MCP: map[string]MCPConfig{ + DockerMCPName: { + Type: MCPStdio, + Command: "docker", + Args: []string{"mcp", "gateway", "run"}, + Disabled: false, + }, + }, + } + store := &ConfigStore{ + config: cfg, + globalDataPath: configPath, + resolver: NewShellVariableResolver(env.New()), + } + + // Verify it's enabled first. + require.True(t, cfg.IsDockerMCPEnabled()) + + err := store.DisableDockerMCP() + require.NoError(t, err) + + // Check in-memory config. + require.False(t, cfg.IsDockerMCPEnabled()) + _, exists := cfg.MCP[DockerMCPName] + require.False(t, exists) + }) + + t.Run("does nothing when MCP is nil", func(t *testing.T) { + t.Parallel() + + cfg := &Config{ + MCP: nil, + } + store := &ConfigStore{ + config: cfg, + globalDataPath: filepath.Join(t.TempDir(), "crush.json"), + resolver: NewShellVariableResolver(env.New()), + } + + err := store.DisableDockerMCP() + require.NoError(t, err) + }) +} diff --git a/internal/ui/AGENTS.md b/internal/ui/AGENTS.md index f3720d7b8867d60b30d00089b3b567ac70fd61ac..6395187ac9ca500d42456abb46aaafad1ed85ee2 100644 --- a/internal/ui/AGENTS.md +++ b/internal/ui/AGENTS.md @@ -191,6 +191,7 @@ through all components that need access to app state or styles. - Always account for padding/borders in width calculations. - Use `tea.Batch()` when returning multiple commands. - Pass `*common.Common` to components that need styles or app access. +- When writing tea.Cmd's prefer creating methods in the model instead of writing inline functions. - The `list.List` only renders visible items (lazy). No render cache exists at the list level — items should cache internally if rendering is expensive. diff --git a/internal/ui/chat/docker_mcp.go b/internal/ui/chat/docker_mcp.go new file mode 100644 index 0000000000000000000000000000000000000000..73d10b4803ff7a2e559ce4aba753fbb8e7ebb264 --- /dev/null +++ b/internal/ui/chat/docker_mcp.go @@ -0,0 +1,290 @@ +package chat + +import ( + "encoding/json" + "fmt" + "strings" + + "charm.land/lipgloss/v2" + "charm.land/lipgloss/v2/table" + "github.com/charmbracelet/crush/internal/config" + "github.com/charmbracelet/crush/internal/message" + "github.com/charmbracelet/crush/internal/stringext" + "github.com/charmbracelet/crush/internal/ui/styles" +) + +// DockerMCPToolMessageItem is a message item that represents a Docker MCP tool call. +type DockerMCPToolMessageItem struct { + *baseToolMessageItem +} + +var _ ToolMessageItem = (*DockerMCPToolMessageItem)(nil) + +// NewDockerMCPToolMessageItem creates a new [DockerMCPToolMessageItem]. +func NewDockerMCPToolMessageItem( + sty *styles.Styles, + toolCall message.ToolCall, + result *message.ToolResult, + canceled bool, +) ToolMessageItem { + return newBaseToolMessageItem(sty, toolCall, result, &DockerMCPToolRenderContext{}, canceled) +} + +// DockerMCPToolRenderContext renders Docker MCP tool messages. +type DockerMCPToolRenderContext struct{} + +// RenderTool implements the [ToolRenderer] interface. +func (d *DockerMCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string { + cappedWidth := cappedMessageWidth(width) + + var params map[string]any + if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil { + params = make(map[string]any) + } + + tool := strings.TrimPrefix(opts.ToolCall.Name, "mcp_"+config.DockerMCPName+"_") + + mainParam := opts.ToolCall.Input + extraArgs := map[string]string{} + switch tool { + case "mcp-find": + if query, ok := params["query"]; ok { + if qStr, ok := query.(string); ok { + mainParam = qStr + } + } + for k, v := range params { + if k == "query" { + continue + } + data, _ := json.Marshal(v) + extraArgs[k] = string(data) + } + case "mcp-add": + if name, ok := params["name"]; ok { + if nStr, ok := name.(string); ok { + mainParam = nStr + } + } + for k, v := range params { + if k == "name" { + continue + } + data, _ := json.Marshal(v) + extraArgs[k] = string(data) + } + case "mcp-remove": + if name, ok := params["name"]; ok { + if nStr, ok := name.(string); ok { + mainParam = nStr + } + } + for k, v := range params { + if k == "name" { + continue + } + data, _ := json.Marshal(v) + extraArgs[k] = string(data) + } + case "mcp-exec": + if name, ok := params["name"]; ok { + if nStr, ok := name.(string); ok { + mainParam = nStr + } + } + case "mcp-config-set": + if server, ok := params["server"]; ok { + if sStr, ok := server.(string); ok { + mainParam = sStr + } + } + } + + var toolParams []string + toolParams = append(toolParams, mainParam) + for k, v := range extraArgs { + toolParams = append(toolParams, k, v) + } + + if opts.IsPending() { + return pendingTool(sty, d.formatToolName(sty, tool), opts.Anim, false) + } + + header := d.makeHeader(sty, tool, cappedWidth, opts, toolParams...) + if opts.Compact { + return header + } + + if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok { + return joinToolParts(header, earlyState) + } + + if tool == "mcp-find" { + return joinToolParts(header, d.renderMCPServers(sty, opts, cappedWidth)) + } + + if !opts.HasResult() { + return header + } + + bodyWidth := cappedWidth - toolBodyLeftPaddingTotal + var parts []string + + // Handle text content. + if opts.Result.Content != "" { + var body string + var result json.RawMessage + if err := json.Unmarshal([]byte(opts.Result.Content), &result); err == nil { + prettyResult, err := json.MarshalIndent(result, "", " ") + if err == nil { + body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.json", string(prettyResult), 0, bodyWidth, opts.ExpandedContent)) + } else { + body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent)) + } + } else if looksLikeMarkdown(opts.Result.Content) { + body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.md", opts.Result.Content, 0, bodyWidth, opts.ExpandedContent)) + } else { + body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent)) + } + parts = append(parts, body) + } + + // Handle image content. + if opts.Result.Data != "" && strings.HasPrefix(opts.Result.MIMEType, "image/") { + parts = append(parts, "", toolOutputImageContent(sty, opts.Result.Data, opts.Result.MIMEType)) + } + + if len(parts) == 0 { + return header + } + + return joinToolParts(header, strings.Join(parts, "\n")) +} + +// FindMCPResponse represents the response from mcp-find. +type FindMCPResponse struct { + Servers []struct { + Name string `json:"name"` + Description string `json:"description"` + } `json:"servers"` +} + +func (d *DockerMCPToolRenderContext) renderMCPServers(sty *styles.Styles, opts *ToolRenderOpts, width int) string { + if !opts.HasResult() || opts.Result.Content == "" { + return "" + } + + var result FindMCPResponse + if err := json.Unmarshal([]byte(opts.Result.Content), &result); err != nil { + return toolOutputPlainContent(sty, opts.Result.Content, width-toolBodyLeftPaddingTotal, opts.ExpandedContent) + } + + if len(result.Servers) == 0 { + return sty.Subtle.Render("No MCP servers found.") + } + + bodyWidth := min(120, width) - toolBodyLeftPaddingTotal + rows := [][]string{} + moreServers := "" + for i, server := range result.Servers { + if i > 9 { + moreServers = sty.Subtle.Render(fmt.Sprintf("... and %d more", len(result.Servers)-10)) + break + } + rows = append(rows, []string{sty.Base.Render(server.Name), sty.Subtle.Render(server.Description)}) + } + serverTable := table.New(). + Wrap(false). + BorderTop(false). + BorderBottom(false). + BorderRight(false). + BorderLeft(false). + BorderColumn(false). + BorderRow(false). + StyleFunc(func(row, col int) lipgloss.Style { + if row == table.HeaderRow { + return lipgloss.NewStyle() + } + switch col { + case 0: + return lipgloss.NewStyle().PaddingRight(1) + } + return lipgloss.NewStyle() + }).Rows(rows...).Width(bodyWidth) + if moreServers != "" { + return sty.Tool.Body.Render(serverTable.Render() + "\n" + moreServers) + } + return sty.Tool.Body.Render(serverTable.Render()) +} + +func (d *DockerMCPToolRenderContext) makeHeader(sty *styles.Styles, tool string, width int, opts *ToolRenderOpts, params ...string) string { + if opts.Compact { + return d.makeCompactHeader(sty, tool, width, params...) + } + + icon := toolIcon(sty, opts.Status) + if opts.IsPending() { + icon = sty.Tool.IconPending.Render() + } + prefix := fmt.Sprintf("%s %s ", icon, d.formatToolName(sty, tool)) + return prefix + toolParamList(sty, params, width-lipgloss.Width(prefix)) +} + +func (d *DockerMCPToolRenderContext) formatToolName(sty *styles.Styles, tool string) string { + mainTool := "Docker MCP" + action := tool + actionStyle := sty.Tool.MCPToolName + switch tool { + case "mcp-exec": + action = "Exec" + case "mcp-config-set": + action = "Config Set" + case "mcp-find": + action = "Find" + case "mcp-add": + action = "Add" + actionStyle = sty.Tool.DockerMCPActionAdd + case "mcp-remove": + action = "Remove" + actionStyle = sty.Tool.DockerMCPActionDel + case "code-mode": + action = "Code Mode" + default: + action = strings.ReplaceAll(tool, "-", " ") + action = strings.ReplaceAll(action, "_", " ") + action = stringext.Capitalize(action) + } + + toolNameStyled := sty.Tool.MCPName.Render(mainTool) + arrow := sty.Tool.MCPArrow.String() + return fmt.Sprintf("%s %s %s", toolNameStyled, arrow, actionStyle.Render(action)) +} + +func (d *DockerMCPToolRenderContext) makeCompactHeader(sty *styles.Styles, tool string, width int, params ...string) string { + action := tool + switch tool { + case "mcp-exec": + action = "exec" + case "mcp-config-set": + action = "config-set" + case "mcp-find": + action = "find" + case "mcp-add": + action = "add" + case "mcp-remove": + action = "remove" + case "code-mode": + action = "code-mode" + default: + action = strings.ReplaceAll(tool, "-", " ") + action = strings.ReplaceAll(action, "_", " ") + } + + name := fmt.Sprintf("Docker MCP: %s", action) + return toolHeader(sty, ToolStatusSuccess, name, width, true, params...) +} + +// IsDockerMCPTool returns true if the tool name is a Docker MCP tool. +func IsDockerMCPTool(name string) bool { + return strings.HasPrefix(name, "mcp_"+config.DockerMCPName+"_") +} diff --git a/internal/ui/chat/tools.go b/internal/ui/chat/tools.go index 1342cfba2f6cc1c608298e9695578ae351726160..f91ad8ebd8725c2e2c15a4b9968bb51226c4db12 100644 --- a/internal/ui/chat/tools.go +++ b/internal/ui/chat/tools.go @@ -255,7 +255,9 @@ func NewToolMessageItem( case tools.LSPRestartToolName: item = NewLSPRestartToolMessageItem(sty, toolCall, result, canceled) default: - if strings.HasPrefix(toolCall.Name, "mcp_") { + if IsDockerMCPTool(toolCall.Name) { + item = NewDockerMCPToolMessageItem(sty, toolCall, result, canceled) + } else if strings.HasPrefix(toolCall.Name, "mcp_") { item = NewMCPToolMessageItem(sty, toolCall, result, canceled) } else { item = NewGenericToolMessageItem(sty, toolCall, result, canceled) diff --git a/internal/ui/dialog/actions.go b/internal/ui/dialog/actions.go index 09755417a5a12e4bdb7df1e5f932ade18016fb8f..a2de6513c13a9d00febd8ca510472542e687ce4a 100644 --- a/internal/ui/dialog/actions.go +++ b/internal/ui/dialog/actions.go @@ -81,6 +81,10 @@ type ( Arguments []commands.Argument Args map[string]string // Actual argument values } + // ActionEnableDockerMCP is a message to enable Docker MCP. + ActionEnableDockerMCP struct{} + // ActionDisableDockerMCP is a message to disable Docker MCP. + ActionDisableDockerMCP struct{} ) // Messages for API key input dialog. diff --git a/internal/ui/dialog/commands.go b/internal/ui/dialog/commands.go index a5b555f2b345f51d9624ce87a2dcc6aaa3c1f70e..b29711742ff540434465b9d4d30fa452860c4efc 100644 --- a/internal/ui/dialog/commands.go +++ b/internal/ui/dialog/commands.go @@ -446,6 +446,16 @@ func (c *Commands) defaultCommands() []*CommandItem { commands = append(commands, NewCommandItem(c.com.Styles, "open_external_editor", "Open External Editor", "ctrl+o", ActionExternalEditor{})) } + // Add Docker MCP command if available and not already enabled + if config.IsDockerMCPAvailable() && !cfg.IsDockerMCPEnabled() { + commands = append(commands, NewCommandItem(c.com.Styles, "enable_docker_mcp", "Enable Docker MCP Catalog", "", ActionEnableDockerMCP{})) + } + + // Add disable Docker MCP command if it's currently enabled + if cfg.IsDockerMCPEnabled() { + commands = append(commands, NewCommandItem(c.com.Styles, "disable_docker_mcp", "Disable Docker MCP Catalog", "", ActionDisableDockerMCP{})) + } + if c.hasTodos || c.hasQueue { var label string switch { diff --git a/internal/ui/model/mcp.go b/internal/ui/model/mcp.go index c5c94268d2985fff3c79590d3f432872439962b2..edcdfa6ea840b897a208875348484e3f9d77d2b4 100644 --- a/internal/ui/model/mcp.go +++ b/internal/ui/model/mcp.go @@ -6,6 +6,7 @@ import ( "charm.land/lipgloss/v2" "github.com/charmbracelet/crush/internal/agent/tools/mcp" + "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/ui/common" "github.com/charmbracelet/crush/internal/ui/styles" ) @@ -59,7 +60,12 @@ func mcpList(t *styles.Styles, mcps []mcp.ClientInfo, width, maxItems int) strin for _, m := range mcps { var icon string - title := t.ResourceName.Render(m.Name) + title := m.Name + // Show "Docker MCP" instead of the config name for Docker MCP. + if m.Name == config.DockerMCPName { + title = "Docker MCP" + } + title = t.ResourceName.Render(title) var description string var extraContent string diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index f126020d32f2c3c974a0a9435bc69ae56380330d..7e7c36348337bf0d8299fa9d9eba7e52176be284 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -1383,6 +1383,12 @@ func (m *UI) handleDialogMsg(msg tea.Msg) tea.Cmd { m.dialog.CloseDialog(dialog.CommandsID) case dialog.ActionQuit: cmds = append(cmds, tea.Quit) + case dialog.ActionEnableDockerMCP: + m.dialog.CloseDialog(dialog.CommandsID) + cmds = append(cmds, m.enableDockerMCP) + case dialog.ActionDisableDockerMCP: + m.dialog.CloseDialog(dialog.CommandsID) + cmds = append(cmds, m.disableDockerMCP) case dialog.ActionInitializeProject: if m.isAgentBusy() { cmds = append(cmds, util.ReportWarn("Agent is busy, please wait before summarizing session...")) @@ -3457,6 +3463,36 @@ func (m *UI) copyChatHighlight() tea.Cmd { ) } +func (m *UI) enableDockerMCP() tea.Msg { + store := m.com.Store() + if err := store.EnableDockerMCP(); err != nil { + return util.ReportError(err)() + } + + // Initialize the Docker MCP client immediately. + ctx := context.Background() + if err := mcp.InitializeSingle(ctx, config.DockerMCPName, store); err != nil { + return util.ReportError(fmt.Errorf("docker MCP enabled but failed to start: %w", err))() + } + + return util.NewInfoMsg("Docker MCP enabled and started successfully") +} + +func (m *UI) disableDockerMCP() tea.Msg { + store := m.com.Store() + // Close the Docker MCP client. + if err := mcp.DisableSingle(store, config.DockerMCPName); err != nil { + return util.ReportError(fmt.Errorf("failed to disable docker MCP: %w", err))() + } + + // Remove from config and persist. + if err := store.DisableDockerMCP(); err != nil { + return util.ReportError(err)() + } + + return util.NewInfoMsg("Docker MCP disabled successfully") +} + // renderLogo renders the Crush logo with the given styles and dimensions. func renderLogo(t *styles.Styles, compact bool, width int) string { return logo.Render(t, version.Version, compact, logo.Opts{ diff --git a/internal/ui/styles/styles.go b/internal/ui/styles/styles.go index bc6d7099f66a1e4c0a9d8e7e1c05c32d6974dcec..12c5c99e0e2b9619777d64b746c053e0bd3e165b 100644 --- a/internal/ui/styles/styles.go +++ b/internal/ui/styles/styles.go @@ -328,6 +328,10 @@ type Styles struct { ResourceName lipgloss.Style ResourceSize lipgloss.Style MediaType lipgloss.Style + + // Docker MCP tools + DockerMCPActionAdd lipgloss.Style // Docker MCP add action (green) + DockerMCPActionDel lipgloss.Style // Docker MCP remove action (red) } // Dialog styles @@ -1182,6 +1186,10 @@ func DefaultStyles() Styles { s.Tool.MediaType = base s.Tool.ResourceSize = base.Foreground(fgMuted) + // Docker MCP styles + s.Tool.DockerMCPActionAdd = base.Foreground(greenLight) + s.Tool.DockerMCPActionDel = base.Foreground(red) + // Buttons s.ButtonFocus = lipgloss.NewStyle().Foreground(white).Background(secondary) s.ButtonBlur = s.Base.Background(bgSubtle) From a3288de0d2a6b70aab1c1caa238f307a401b259f Mon Sep 17 00:00:00 2001 From: Charm <124303983+charmcli@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:00:55 +0000 Subject: [PATCH 09/26] chore: auto-update files --- internal/agent/hyper/provider.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/agent/hyper/provider.json b/internal/agent/hyper/provider.json index 05f1a7d15762919adc462fbed9d9b2dd492ea26a..c1f318318ab49177024de26b0409020ae84e6243 100644 --- a/internal/agent/hyper/provider.json +++ b/internal/agent/hyper/provider.json @@ -1 +1 @@ -{"name":"Charm Hyper","id":"hyper","api_endpoint":"https://hyper.charm.land/api/v1/fantasy","type":"hyper","default_large_model_id":"claude-opus-4-5","default_small_model_id":"claude-haiku-4-5","models":[{"id":"claude-haiku-4-5","name":"Claude Haiku 4.5","cost_per_1m_in":1,"cost_per_1m_out":5,"cost_per_1m_in_cached":1.25,"cost_per_1m_out_cached":0.09999999999999999,"context_window":200000,"default_max_tokens":32000,"can_reason":true,"supports_attachments":true,"options":{}},{"id":"claude-opus-4-5","name":"Claude Opus 4.5","cost_per_1m_in":5,"cost_per_1m_out":25,"cost_per_1m_in_cached":6.25,"cost_per_1m_out_cached":0.5,"context_window":200000,"default_max_tokens":50000,"can_reason":true,"supports_attachments":true,"options":{}},{"id":"claude-opus-4-6","name":"Claude Opus 4.6","cost_per_1m_in":5,"cost_per_1m_out":25,"cost_per_1m_in_cached":6.25,"cost_per_1m_out_cached":0.5,"context_window":200000,"default_max_tokens":126000,"can_reason":true,"reasoning_levels":["low","medium","high","max"],"supports_attachments":true,"options":{}},{"id":"claude-sonnet-4-5","name":"Claude Sonnet 4.5","cost_per_1m_in":3,"cost_per_1m_out":15,"cost_per_1m_in_cached":3.75,"cost_per_1m_out_cached":0.3,"context_window":200000,"default_max_tokens":50000,"can_reason":true,"supports_attachments":true,"options":{}},{"id":"gemini-3-flash","name":"Gemini 3 Flash","cost_per_1m_in":0.5,"cost_per_1m_out":3,"cost_per_1m_in_cached":0.049999999999999996,"cost_per_1m_out_cached":0,"context_window":1000000,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gemini-3-pro-preview","name":"Gemini 3 Pro","cost_per_1m_in":2,"cost_per_1m_out":12,"cost_per_1m_in_cached":0.19999999999999998,"cost_per_1m_out_cached":0,"context_window":1000000,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gemini-3.1-pro-preview","name":"Gemini 3.1 Pro","cost_per_1m_in":2,"cost_per_1m_out":12,"cost_per_1m_in_cached":0.19999999999999998,"cost_per_1m_out_cached":0,"context_window":1000000,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"glm-4.7","name":"GLM-4.7","cost_per_1m_in":0.43,"cost_per_1m_out":1.75,"cost_per_1m_in_cached":0.08,"cost_per_1m_out_cached":0,"context_window":202752,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":false,"options":{}},{"id":"glm-4.7-flashx","name":"GLM-4.7 Flash","cost_per_1m_in":0.06,"cost_per_1m_out":0.39999999999999997,"cost_per_1m_in_cached":0.01,"cost_per_1m_out_cached":0,"context_window":200000,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":false,"options":{}},{"id":"glm-5","name":"GLM-5","cost_per_1m_in":1,"cost_per_1m_out":3.1999999999999997,"cost_per_1m_in_cached":0.19999999999999998,"cost_per_1m_out_cached":0,"context_window":202800,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":false,"options":{}},{"id":"gpt-5.1-codex","name":"GPT-5.1 Codex","cost_per_1m_in":1.25,"cost_per_1m_out":10,"cost_per_1m_in_cached":0.125,"cost_per_1m_out_cached":0.125,"context_window":400000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["minimal","low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gpt-5.1-codex-max","name":"GPT-5.1 Codex Max","cost_per_1m_in":1.25,"cost_per_1m_out":10,"cost_per_1m_in_cached":0.125,"cost_per_1m_out_cached":0.125,"context_window":400000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["minimal","low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gpt-5.1-codex-mini","name":"GPT-5.1 Codex Mini","cost_per_1m_in":0.25,"cost_per_1m_out":2,"cost_per_1m_in_cached":0.025,"cost_per_1m_out_cached":0.025,"context_window":400000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gpt-5.2","name":"GPT-5.2","cost_per_1m_in":1.75,"cost_per_1m_out":14,"cost_per_1m_in_cached":0.175,"cost_per_1m_out_cached":0.175,"context_window":400000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["minimal","low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gpt-5.2-codex","name":"GPT-5.2 Codex","cost_per_1m_in":1.75,"cost_per_1m_out":14,"cost_per_1m_in_cached":0.175,"cost_per_1m_out_cached":0.175,"context_window":400000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["minimal","low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gpt-5.3-codex","name":"GPT-5.3 Codex","cost_per_1m_in":1.75,"cost_per_1m_out":14,"cost_per_1m_in_cached":0.175,"cost_per_1m_out_cached":0.175,"context_window":400000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["minimal","low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"grok-4.1-fast-non-reasoning","name":"Grok 4.1 Fast Non Reasoning","cost_per_1m_in":0.2,"cost_per_1m_out":0.5,"cost_per_1m_in_cached":0,"cost_per_1m_out_cached":0.05,"context_window":2000000,"default_max_tokens":200000,"can_reason":false,"supports_attachments":true,"options":{}},{"id":"grok-4.1-fast-reasoning","name":"Grok 4.1 Fast Reasoning","cost_per_1m_in":0.2,"cost_per_1m_out":0.5,"cost_per_1m_in_cached":0,"cost_per_1m_out_cached":0.05,"context_window":2000000,"default_max_tokens":200000,"can_reason":true,"supports_attachments":true,"options":{}},{"id":"grok-code-fast-1","name":"Grok Code Fast","cost_per_1m_in":0.2,"cost_per_1m_out":1.5,"cost_per_1m_in_cached":0,"cost_per_1m_out_cached":0.02,"context_window":256000,"default_max_tokens":20000,"can_reason":true,"supports_attachments":false,"options":{}},{"id":"kimi-k2-0905","name":"Kimi K2","cost_per_1m_in":0.548,"cost_per_1m_out":2.192,"cost_per_1m_in_cached":0,"cost_per_1m_out_cached":0,"context_window":262144,"default_max_tokens":26214,"can_reason":false,"supports_attachments":false,"options":{}},{"id":"kimi-k2.5","name":"Kimi K2.5","cost_per_1m_in":0.5,"cost_per_1m_out":2.8,"cost_per_1m_in_cached":0,"cost_per_1m_out_cached":0,"context_window":256000,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}}]} \ No newline at end of file +{"name":"Charm Hyper","id":"hyper","api_endpoint":"https://hyper.charm.land/api/v1/fantasy","type":"hyper","default_large_model_id":"claude-opus-4-5","default_small_model_id":"claude-haiku-4-5","models":[{"id":"claude-haiku-4-5","name":"Claude Haiku 4.5","cost_per_1m_in":1,"cost_per_1m_out":5,"cost_per_1m_in_cached":1.25,"cost_per_1m_out_cached":0.1,"context_window":200000,"default_max_tokens":64000,"can_reason":true,"supports_attachments":true,"options":{}},{"id":"claude-opus-4-5","name":"Claude Opus 4.5","cost_per_1m_in":5,"cost_per_1m_out":25,"cost_per_1m_in_cached":6.25,"cost_per_1m_out_cached":0.5,"context_window":200000,"default_max_tokens":64000,"can_reason":true,"supports_attachments":true,"options":{}},{"id":"claude-opus-4-6","name":"Claude Opus 4.6","cost_per_1m_in":5,"cost_per_1m_out":25,"cost_per_1m_in_cached":6.25,"cost_per_1m_out_cached":0.5,"context_window":1000000,"default_max_tokens":126000,"can_reason":true,"reasoning_levels":["low","medium","high","max"],"supports_attachments":true,"options":{}},{"id":"claude-sonnet-4-5","name":"Claude Sonnet 4.5","cost_per_1m_in":3,"cost_per_1m_out":15,"cost_per_1m_in_cached":3.75,"cost_per_1m_out_cached":0.3,"context_window":200000,"default_max_tokens":64000,"can_reason":true,"supports_attachments":true,"options":{}},{"id":"claude-sonnet-4-6","name":"Claude Sonnet 4.6","cost_per_1m_in":3,"cost_per_1m_out":15,"cost_per_1m_in_cached":3.75,"cost_per_1m_out_cached":0.3,"context_window":1000000,"default_max_tokens":64000,"can_reason":true,"reasoning_levels":["low","medium","high","max"],"supports_attachments":true,"options":{}},{"id":"gemini-3-flash","name":"Gemini 3 Flash","cost_per_1m_in":0.5,"cost_per_1m_out":3,"cost_per_1m_in_cached":0.05,"cost_per_1m_out_cached":0,"context_window":1000000,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gemini-3-pro-preview","name":"Gemini 3 Pro","cost_per_1m_in":2,"cost_per_1m_out":12,"cost_per_1m_in_cached":0.2,"cost_per_1m_out_cached":0,"context_window":1000000,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gemini-3.1-pro-preview","name":"Gemini 3.1 Pro","cost_per_1m_in":2,"cost_per_1m_out":12,"cost_per_1m_in_cached":0.2,"cost_per_1m_out_cached":0,"context_window":1000000,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"glm-4.7","name":"GLM-4.7","cost_per_1m_in":0.6,"cost_per_1m_out":2.2,"cost_per_1m_in_cached":0,"cost_per_1m_out_cached":0,"context_window":200000,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":false,"options":{}},{"id":"glm-4.7-flashx","name":"GLM-4.7 Flash","cost_per_1m_in":0.06,"cost_per_1m_out":0.4,"cost_per_1m_in_cached":0.01,"cost_per_1m_out_cached":0,"context_window":200000,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":false,"options":{}},{"id":"glm-5","name":"GLM-5","cost_per_1m_in":1,"cost_per_1m_out":3.2,"cost_per_1m_in_cached":0.2,"cost_per_1m_out_cached":0,"context_window":202800,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":false,"options":{}},{"id":"gpt-5.1-codex","name":"GPT-5.1 Codex","cost_per_1m_in":1.25,"cost_per_1m_out":10,"cost_per_1m_in_cached":0.125,"cost_per_1m_out_cached":0.125,"context_window":400000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["minimal","low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gpt-5.1-codex-max","name":"GPT-5.1 Codex Max","cost_per_1m_in":1.25,"cost_per_1m_out":10,"cost_per_1m_in_cached":0.125,"cost_per_1m_out_cached":0.125,"context_window":400000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["minimal","low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gpt-5.1-codex-mini","name":"GPT-5.1 Codex Mini","cost_per_1m_in":0.25,"cost_per_1m_out":2,"cost_per_1m_in_cached":0.025,"cost_per_1m_out_cached":0.025,"context_window":400000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gpt-5.2","name":"GPT-5.2","cost_per_1m_in":1.75,"cost_per_1m_out":14,"cost_per_1m_in_cached":0.175,"cost_per_1m_out_cached":0.175,"context_window":400000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["minimal","low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gpt-5.2-codex","name":"GPT-5.2 Codex","cost_per_1m_in":1.75,"cost_per_1m_out":14,"cost_per_1m_in_cached":0.175,"cost_per_1m_out_cached":0.175,"context_window":400000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["minimal","low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gpt-5.3-codex","name":"GPT-5.3 Codex","cost_per_1m_in":1.75,"cost_per_1m_out":14,"cost_per_1m_in_cached":0.175,"cost_per_1m_out_cached":0.175,"context_window":400000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["minimal","low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gpt-5.4","name":"GPT-5.4","cost_per_1m_in":2.5,"cost_per_1m_out":15,"cost_per_1m_in_cached":0.25,"cost_per_1m_out_cached":0,"context_window":1050000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["low","medium","high","xhigh"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"gpt-5.4-pro","name":"GPT-5.4 Pro","cost_per_1m_in":30,"cost_per_1m_out":180,"cost_per_1m_in_cached":0,"cost_per_1m_out_cached":0,"context_window":1050000,"default_max_tokens":128000,"can_reason":true,"reasoning_levels":["medium","high","xhigh"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}},{"id":"grok-4.1-fast-non-reasoning","name":"Grok 4.1 Fast Non Reasoning","cost_per_1m_in":0.2,"cost_per_1m_out":0.5,"cost_per_1m_in_cached":0,"cost_per_1m_out_cached":0.05,"context_window":2000000,"default_max_tokens":200000,"can_reason":false,"supports_attachments":true,"options":{}},{"id":"grok-4.1-fast-reasoning","name":"Grok 4.1 Fast Reasoning","cost_per_1m_in":0.2,"cost_per_1m_out":0.5,"cost_per_1m_in_cached":0,"cost_per_1m_out_cached":0.05,"context_window":2000000,"default_max_tokens":200000,"can_reason":true,"supports_attachments":true,"options":{}},{"id":"grok-code-fast-1","name":"Grok Code Fast","cost_per_1m_in":0.2,"cost_per_1m_out":1.5,"cost_per_1m_in_cached":0,"cost_per_1m_out_cached":0.02,"context_window":256000,"default_max_tokens":20000,"can_reason":true,"supports_attachments":false,"options":{}},{"id":"kimi-k2-0905","name":"Kimi K2","cost_per_1m_in":0.6,"cost_per_1m_out":2.5,"cost_per_1m_in_cached":0,"cost_per_1m_out_cached":0,"context_window":256000,"default_max_tokens":8000,"can_reason":false,"supports_attachments":false,"options":{}},{"id":"kimi-k2.5","name":"Kimi K2.5","cost_per_1m_in":0.6,"cost_per_1m_out":3,"cost_per_1m_in_cached":0.1,"cost_per_1m_out_cached":0,"context_window":262114,"default_max_tokens":8000,"can_reason":true,"reasoning_levels":["low","medium","high"],"default_reasoning_effort":"medium","supports_attachments":true,"options":{}}]} \ No newline at end of file From ad5368f1d3192e12bca20cecfc0b46b688c294cb Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 19 Mar 2026 16:08:46 -0300 Subject: [PATCH 11/26] fix: update fantasy with a fix for avian (#2438) This change updates Fantasy with a fix for SSE events in some OpenAI-compatible providers. * Avian will now work again * OpenRouter will now send the Crush official `User-Agent` string --- * Closes https://github.com/charmbracelet/crush/issues/2431 * Ref: https://github.com/charmbracelet/fantasy/pull/180 * Ref: https://github.com/openai/openai-go/pull/621 --- go.mod | 6 +++--- go.sum | 12 ++++++------ internal/agent/coordinator.go | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 766f22d9cabd576068ae0da2ed65924e9aa7875e..50fe16c757978360584639104066d0d6d7a7a66a 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.26.1 require ( charm.land/bubbles/v2 v2.0.0 charm.land/bubbletea/v2 v2.0.2 - charm.land/catwalk v0.30.7 + charm.land/catwalk v0.30.8 charm.land/fang/v2 v2.0.1 - charm.land/fantasy v0.15.0 + charm.land/fantasy v0.15.1 charm.land/glamour/v2 v2.0.0 charm.land/lipgloss/v2 v2.0.2 charm.land/log/v2 v2.0.0 @@ -22,6 +22,7 @@ require ( github.com/bmatcuk/doublestar/v4 v4.10.0 github.com/charlievieth/fastwalk v1.0.14 github.com/charmbracelet/colorprofile v0.4.3 + github.com/charmbracelet/openai-go v0.0.0-20260319145158-d0740cc34266 github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 github.com/charmbracelet/x/ansi v0.11.6 github.com/charmbracelet/x/editor v0.2.0 @@ -49,7 +50,6 @@ require ( github.com/modelcontextprotocol/go-sdk v1.4.1 github.com/ncruces/go-sqlite3 v0.32.0 github.com/nxadm/tail v1.4.11 - github.com/openai/openai-go/v3 v3.28.0 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/posthog/posthog-go v1.11.1 github.com/pressly/goose/v3 v3.27.0 diff --git a/go.sum b/go.sum index 3c56ce5937fbe24747ceef0e1786ded2bf8eeef9..d4df5e38592e63ee0739ec867ffd5c552991cf07 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,12 @@ charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= -charm.land/catwalk v0.30.7 h1:wOUCRpLw3hAweBCIxJJ2IrfSFdvNe3HL5tEz1X1Kr+o= -charm.land/catwalk v0.30.7/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg= +charm.land/catwalk v0.30.8 h1:Y5FYPY8iHoejXKxyp2Akbn+3SBb0zocY7aetjHxGs70= +charm.land/catwalk v0.30.8/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg= charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY= charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII= -charm.land/fantasy v0.15.0 h1:PkwLVXHvbQD1BSBEXCJ/4wx3QqlG140TfLqOM541OKg= -charm.land/fantasy v0.15.0/go.mod h1:9L50Xe+Dvc23rjjOeas4Ez0jOd2NuiXSpqmPxBHQ3mQ= +charm.land/fantasy v0.15.1 h1:eojrAXd2hvq5vuLO5Rcobv5P/xgen0YAngDquUawFvo= +charm.land/fantasy v0.15.1/go.mod h1:VZjpXVh7IgeiIzGQybEnKzd68ofDsRj94+kzH1ZCAfQ= charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U= charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w= charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= @@ -102,6 +102,8 @@ github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab h1: github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab/go.mod h1:hqlYqR7uPKOKfnNeicUbZp0Ps0GeYFlKYtwh5HGDCx8= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= +github.com/charmbracelet/openai-go v0.0.0-20260319145158-d0740cc34266 h1:BW/sZtyd1JyYy0h5adMm3tzpNyL857LWjuTRET6OhpY= +github.com/charmbracelet/openai-go v0.0.0-20260319145158-d0740cc34266/go.mod h1:1DahUaExbUZx/jD+FNT2PKP4L9rLE5+ZBRuI8mZjd/E= github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRbAmexyt43hVfeyBofiGSEmJ7krjLOYt/9CF5NKA= github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98= github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= @@ -299,8 +301,6 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= 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= -github.com/openai/openai-go/v3 v3.28.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0= diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 53fecb0388628caa8da1eed43e39b2898529e768..3f22d1474caca34b6a4786d4afd82bfb004d851d 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -41,7 +41,7 @@ import ( "charm.land/fantasy/providers/openaicompat" "charm.land/fantasy/providers/openrouter" "charm.land/fantasy/providers/vercel" - openaisdk "github.com/openai/openai-go/v3/option" + openaisdk "github.com/charmbracelet/openai-go/option" "github.com/qjebbs/go-jsons" ) From 00cefc951a10423db00c1285adccaf124279ca61 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 19 Mar 2026 11:04:27 -0400 Subject: [PATCH 12/26] fix: remove deadcode --- internal/agent/coordinator.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/internal/agent/coordinator.go b/internal/agent/coordinator.go index 3f22d1474caca34b6a4786d4afd82bfb004d851d..91c3a2c7b71757080084d6f6cd64ea0ab5fde602 100644 --- a/internal/agent/coordinator.go +++ b/internal/agent/coordinator.go @@ -71,7 +71,6 @@ type Coordinator interface { Summarize(context.Context, string) error Model() Model UpdateModels(ctx context.Context) error - RefreshTools(ctx context.Context) error } type coordinator struct { @@ -905,21 +904,6 @@ func (c *coordinator) UpdateModels(ctx context.Context) error { return nil } -func (c *coordinator) RefreshTools(ctx context.Context) error { - agentCfg, ok := c.cfg.Config().Agents[config.AgentCoder] - if !ok { - return errors.New("coder agent not configured") - } - - tools, err := c.buildTools(ctx, agentCfg) - if err != nil { - return err - } - c.currentAgent.SetTools(tools) - slog.Debug("refreshed agent tools", "count", len(tools)) - return nil -} - func (c *coordinator) QueuedPrompts(sessionID string) int { return c.currentAgent.QueuedPrompts(sessionID) } From df094bdeca6717cb36c127fddda69a041b4e38f1 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 19 Mar 2026 11:09:54 -0400 Subject: [PATCH 13/26] fix(mcp): handle raw/whitespace base64 --- internal/agent/tools/mcp/tools.go | 54 ++++++++++++++++++++++---- internal/agent/tools/mcp/tools_test.go | 23 +++++++++++ 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/internal/agent/tools/mcp/tools.go b/internal/agent/tools/mcp/tools.go index ce85e591e55139343e43179bdd33c88b49c274be..05d6b2b75d8fadff2e9af8385817ac135722f1a8 100644 --- a/internal/agent/tools/mcp/tools.go +++ b/internal/agent/tools/mcp/tools.go @@ -167,32 +167,70 @@ func filterDisabledTools(cfg *config.ConfigStore, mcpName string, tools []*Tool) return filtered } -// ensureBase64 checks if data is valid base64 and returns it as-is if so, -// otherwise encodes the raw binary data to base64. +// ensureBase64 normalizes valid base64 input and guarantees padded +// base64.StdEncoding output; otherwise it encodes raw binary data. func ensureBase64(data []byte) []byte { - // Check if the data is already valid base64 by attempting to decode it. - // Valid base64 should only contain ASCII characters (A-Z, a-z, 0-9, +, /, =). - if isValidBase64(data) { + if len(data) == 0 { return data } - // Data is raw binary, encode it to base64. + + normalized := normalizeBase64Input(data) + if decoded, ok := decodeBase64(normalized); ok { + encoded := make([]byte, base64.StdEncoding.EncodedLen(len(decoded))) + base64.StdEncoding.Encode(encoded, decoded) + return encoded + } + encoded := make([]byte, base64.StdEncoding.EncodedLen(len(data))) base64.StdEncoding.Encode(encoded, data) return encoded } +func normalizeBase64Input(data []byte) []byte { + normalized := strings.Join(strings.Fields(string(data)), "") + return []byte(normalized) +} + +func decodeBase64(data []byte) ([]byte, bool) { + if len(data) == 0 { + return data, true + } + + for _, b := range data { + if b > 127 { + return nil, false + } + } + + s := string(data) + decoded, err := base64.StdEncoding.DecodeString(s) + if err == nil { + return decoded, true + } + decoded, err = base64.RawStdEncoding.DecodeString(s) + if err == nil { + return decoded, true + } + return nil, false +} + // isValidBase64 checks if the data appears to be valid base64-encoded content. func isValidBase64(data []byte) bool { if len(data) == 0 { return true } + // Base64 strings should only contain ASCII characters. for _, b := range data { if b > 127 { return false } } - // Try to decode to verify it's valid base64. - _, err := base64.StdEncoding.DecodeString(string(data)) + + s := string(data) + if _, err := base64.StdEncoding.DecodeString(s); err == nil { + return true + } + _, err := base64.RawStdEncoding.DecodeString(s) return err == nil } diff --git a/internal/agent/tools/mcp/tools_test.go b/internal/agent/tools/mcp/tools_test.go index 3795381ebd2a6a54540a6905ef39c97b8ce59575..aae4428ed6b830549540611761c22f070eeda925 100644 --- a/internal/agent/tools/mcp/tools_test.go +++ b/internal/agent/tools/mcp/tools_test.go @@ -40,6 +40,16 @@ func TestEnsureBase64(t *testing.T) { input: []byte("YQ=="), // "a" in base64 wantData: []byte("YQ=="), }, + { + name: "base64 without padding", + input: []byte("YQ"), + wantData: []byte("YQ=="), + }, + { + name: "base64 with whitespace", + input: []byte("U0dWc2JHOGdWMjl5YkdRaA==\n"), + wantData: []byte("U0dWc2JHOGdWMjl5YkdRaA=="), + }, } for _, tt := range tests { @@ -51,6 +61,9 @@ func TestEnsureBase64(t *testing.T) { // Verify the result is valid base64 that can be decoded. if len(result) > 0 { _, err := base64.StdEncoding.DecodeString(string(result)) + if err != nil { + _, err = base64.RawStdEncoding.DecodeString(string(result)) + } require.NoError(t, err, "result should be valid base64") } }) @@ -85,6 +98,16 @@ func TestIsValidBase64(t *testing.T) { input: []byte{}, want: true, }, + { + name: "valid raw base64 without padding", + input: []byte("YQ"), + want: true, + }, + { + name: "valid base64 with whitespace", + input: normalizeBase64Input([]byte("U0dWc2JHOGdWMjl5YkdRaA==\n")), + want: true, + }, { name: "invalid base64 characters", input: []byte("SGVsbG8!@#$"), From f6e7a43a81b342bae6c7ed4d8f3c72087bc9de4c Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 19 Mar 2026 11:27:01 -0400 Subject: [PATCH 14/26] fix(mcp/docker): harden tests --- internal/config/docker_mcp.go | 8 +++-- internal/config/docker_mcp_test.go | 53 ++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/internal/config/docker_mcp.go b/internal/config/docker_mcp.go index e3b352ec275d5cb5fb588367c16774b127e6e6cb..7bd1d6eadd202fb2f5a10476f8da062d0b57b64b 100644 --- a/internal/config/docker_mcp.go +++ b/internal/config/docker_mcp.go @@ -7,6 +7,11 @@ import ( "time" ) +var dockerMCPVersionRunner = func(ctx context.Context) error { + cmd := exec.CommandContext(ctx, "docker", "mcp", "version") + return cmd.Run() +} + // DockerMCPName is the name of the Docker MCP configuration. const DockerMCPName = "docker" @@ -16,8 +21,7 @@ func IsDockerMCPAvailable() bool { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, "docker", "mcp", "version") - err := cmd.Run() + err := dockerMCPVersionRunner(ctx) return err == nil } diff --git a/internal/config/docker_mcp_test.go b/internal/config/docker_mcp_test.go index 93438777ce8d735a13fb6f14d25dc93ac31e17ac..1ac5c99bd63ab3855034d7b6e56855396fdd3ed5 100644 --- a/internal/config/docker_mcp_test.go +++ b/internal/config/docker_mcp_test.go @@ -1,6 +1,8 @@ package config import ( + "context" + "errors" "os" "path/filepath" "testing" @@ -9,6 +11,17 @@ import ( "github.com/stretchr/testify/require" ) +var errDockerUnavailable = errors.New("docker unavailable") + +func setDockerMCPVersionRunner(t *testing.T, runner func(context.Context) error) { + t.Helper() + orig := dockerMCPVersionRunner + dockerMCPVersionRunner = runner + t.Cleanup(func() { + dockerMCPVersionRunner = orig + }) +} + func TestIsDockerMCPEnabled(t *testing.T) { t.Parallel() @@ -43,10 +56,8 @@ func TestIsDockerMCPEnabled(t *testing.T) { } func TestEnableDockerMCP(t *testing.T) { - t.Parallel() - t.Run("adds docker mcp to config", func(t *testing.T) { - t.Parallel() + setDockerMCPVersionRunner(t, func(context.Context) error { return nil }) // Create a temporary directory for config. tmpDir := t.TempDir() @@ -61,11 +72,6 @@ func TestEnableDockerMCP(t *testing.T) { resolver: NewShellVariableResolver(env.New()), } - // Only run this test if docker mcp is available. - if !IsDockerMCPAvailable() { - t.Skip("Docker MCP not available, skipping test") - } - err := store.EnableDockerMCP() require.NoError(t, err) @@ -86,7 +92,7 @@ func TestEnableDockerMCP(t *testing.T) { }) t.Run("fails when docker mcp not available", func(t *testing.T) { - t.Parallel() + setDockerMCPVersionRunner(t, func(context.Context) error { return errDockerUnavailable }) // Create a temporary directory for config. tmpDir := t.TempDir() @@ -101,11 +107,6 @@ func TestEnableDockerMCP(t *testing.T) { resolver: NewShellVariableResolver(env.New()), } - // Skip if docker mcp is actually available. - if IsDockerMCPAvailable() { - t.Skip("Docker MCP is available, skipping unavailable test") - } - err := store.EnableDockerMCP() require.Error(t, err) require.Contains(t, err.Error(), "docker mcp is not available") @@ -166,3 +167,27 @@ func TestDisableDockerMCP(t *testing.T) { require.NoError(t, err) }) } + +func TestEnableDockerMCPWithRealDockerWhenAvailable(t *testing.T) { + t.Parallel() + + if !IsDockerMCPAvailable() { + t.Skip("docker mcp not available on this machine") + } + + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "crush.json") + + cfg := &Config{ + MCP: make(map[string]MCPConfig), + } + store := &ConfigStore{ + config: cfg, + globalDataPath: configPath, + resolver: NewShellVariableResolver(env.New()), + } + + err := store.EnableDockerMCP() + require.NoError(t, err) + require.True(t, cfg.IsDockerMCPEnabled()) +} From 6c61cae0003a32efbce087116af67ad65c7c2a8c Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 19 Mar 2026 11:27:26 -0400 Subject: [PATCH 15/26] fix(ui/docker): stable sort mcp parameters --- internal/ui/chat/docker_mcp.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/ui/chat/docker_mcp.go b/internal/ui/chat/docker_mcp.go index 73d10b4803ff7a2e559ce4aba753fbb8e7ebb264..57cd9da55f83e63279413d9801337236290d5cdc 100644 --- a/internal/ui/chat/docker_mcp.go +++ b/internal/ui/chat/docker_mcp.go @@ -3,6 +3,7 @@ package chat import ( "encoding/json" "fmt" + "sort" "strings" "charm.land/lipgloss/v2" @@ -102,8 +103,13 @@ func (d *DockerMCPToolRenderContext) RenderTool(sty *styles.Styles, width int, o var toolParams []string toolParams = append(toolParams, mainParam) - for k, v := range extraArgs { - toolParams = append(toolParams, k, v) + keys := make([]string, 0, len(extraArgs)) + for k := range extraArgs { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + toolParams = append(toolParams, k, extraArgs[k]) } if opts.IsPending() { From 351e5620f7c63cec6c2d2b42ce4b5e17031f6423 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 19 Mar 2026 11:37:09 -0400 Subject: [PATCH 16/26] fix(mcp/docker): only write config after docker startup succeeds --- internal/config/docker_mcp.go | 32 +++++++++++++++++++++++--------- internal/ui/model/ui.go | 14 ++++++++++++-- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/internal/config/docker_mcp.go b/internal/config/docker_mcp.go index 7bd1d6eadd202fb2f5a10476f8da062d0b57b64b..01445a9ae7583b8ac489542d08d745e49907521a 100644 --- a/internal/config/docker_mcp.go +++ b/internal/config/docker_mcp.go @@ -34,30 +34,44 @@ func (c *Config) IsDockerMCPEnabled() bool { return exists } -// EnableDockerMCP adds Docker MCP configuration and persists it. -func (s *ConfigStore) EnableDockerMCP() error { - if !IsDockerMCPAvailable() { - return fmt.Errorf("docker mcp is not available, please ensure docker is installed and 'docker mcp version' succeeds") - } - - mcpConfig := MCPConfig{ +func DockerMCPConfig() MCPConfig { + return MCPConfig{ Type: MCPStdio, Command: "docker", Args: []string{"mcp", "gateway", "run"}, Disabled: false, } +} + +func (s *ConfigStore) PrepareDockerMCPConfig() (MCPConfig, error) { + if !IsDockerMCPAvailable() { + return MCPConfig{}, fmt.Errorf("docker mcp is not available, please ensure docker is installed and 'docker mcp version' succeeds") + } - // Add to in-memory config. + mcpConfig := DockerMCPConfig() if s.config.MCP == nil { s.config.MCP = make(map[string]MCPConfig) } s.config.MCP[DockerMCPName] = mcpConfig + return mcpConfig, nil +} - // Persist to config file. +func (s *ConfigStore) PersistDockerMCPConfig(mcpConfig MCPConfig) error { if err := s.SetConfigField(ScopeGlobal, "mcp."+DockerMCPName, mcpConfig); err != nil { return fmt.Errorf("failed to persist docker mcp configuration: %w", err) } + return nil +} +// EnableDockerMCP adds Docker MCP configuration and persists it. +func (s *ConfigStore) EnableDockerMCP() error { + mcpConfig, err := s.PrepareDockerMCPConfig() + if err != nil { + return err + } + if err := s.PersistDockerMCPConfig(mcpConfig); err != nil { + return err + } return nil } diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index 7e7c36348337bf0d8299fa9d9eba7e52176be284..e87bc148c6a8e0286ed138cbd426697ac039a5d5 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -3465,16 +3465,26 @@ func (m *UI) copyChatHighlight() tea.Cmd { func (m *UI) enableDockerMCP() tea.Msg { store := m.com.Store() - if err := store.EnableDockerMCP(); err != nil { + // Stage Docker MCP in memory first so startup and persistence can be atomic. + mcpConfig, err := store.PrepareDockerMCPConfig() + if err != nil { return util.ReportError(err)() } - // Initialize the Docker MCP client immediately. ctx := context.Background() if err := mcp.InitializeSingle(ctx, config.DockerMCPName, store); err != nil { + // Roll back in-memory state when startup fails. + delete(store.Config().MCP, config.DockerMCPName) return util.ReportError(fmt.Errorf("docker MCP enabled but failed to start: %w", err))() } + if err := store.PersistDockerMCPConfig(mcpConfig); err != nil { + // Roll back runtime and in-memory state if persistence fails. + _ = mcp.DisableSingle(store, config.DockerMCPName) + delete(store.Config().MCP, config.DockerMCPName) + return util.ReportError(fmt.Errorf("docker MCP started but failed to persist configuration: %w", err))() + } + return util.NewInfoMsg("Docker MCP enabled and started successfully") } From 600310508e2b00a933454e3afcea33cd5eda3054 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 19 Mar 2026 11:51:20 -0400 Subject: [PATCH 17/26] fix(ui/docker): don't block ui when checking for docker desktop --- internal/config/docker_mcp.go | 33 +++++++++++++++++++++++++++++++ internal/ui/dialog/commands.go | 36 ++++++++++++++++++++++++++++++++-- internal/ui/model/ui.go | 11 ++++++----- 3 files changed, 73 insertions(+), 7 deletions(-) diff --git a/internal/config/docker_mcp.go b/internal/config/docker_mcp.go index 01445a9ae7583b8ac489542d08d745e49907521a..0bac24dff2aedb4fd27023f604e1978b7f936852 100644 --- a/internal/config/docker_mcp.go +++ b/internal/config/docker_mcp.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os/exec" + "sync" "time" ) @@ -12,6 +13,15 @@ var dockerMCPVersionRunner = func(ctx context.Context) error { return cmd.Run() } +const dockerMCPAvailabilityTTL = 10 * time.Second + +var dockerMCPAvailabilityCache struct { + mu sync.Mutex + available bool + checkedAt time.Time + known bool +} + // DockerMCPName is the name of the Docker MCP configuration. const DockerMCPName = "docker" @@ -25,6 +35,29 @@ func IsDockerMCPAvailable() bool { return err == nil } +func DockerMCPAvailabilityCached() (available bool, known bool) { + dockerMCPAvailabilityCache.mu.Lock() + defer dockerMCPAvailabilityCache.mu.Unlock() + + if !dockerMCPAvailabilityCache.known { + return false, false + } + if time.Since(dockerMCPAvailabilityCache.checkedAt) > dockerMCPAvailabilityTTL { + return dockerMCPAvailabilityCache.available, false + } + return dockerMCPAvailabilityCache.available, true +} + +func RefreshDockerMCPAvailability() bool { + available := IsDockerMCPAvailable() + dockerMCPAvailabilityCache.mu.Lock() + dockerMCPAvailabilityCache.available = available + dockerMCPAvailabilityCache.checkedAt = time.Now() + dockerMCPAvailabilityCache.known = true + dockerMCPAvailabilityCache.mu.Unlock() + return available +} + // IsDockerMCPEnabled checks if Docker MCP is already configured. func (c *Config) IsDockerMCPEnabled() bool { if c.MCP == nil { diff --git a/internal/ui/dialog/commands.go b/internal/ui/dialog/commands.go index b29711742ff540434465b9d4d30fa452860c4efc..429da6a638f1fd361ad8b609f69dbf712c0f6051 100644 --- a/internal/ui/dialog/commands.go +++ b/internal/ui/dialog/commands.go @@ -37,6 +37,10 @@ const ( ) // Commands represents a dialog that shows available commands. +type dockerMCPAvailabilityCheckedMsg struct { + available bool +} + type Commands struct { com *common.Common keyMap struct { @@ -66,6 +70,9 @@ type Commands struct { customCommands []commands.CustomCommand mcpPrompts []commands.MCPPrompt + + dockerMCPAvailable *bool + dockerMCPCheckInFlight bool } var _ Dialog = (*Commands)(nil) @@ -126,6 +133,10 @@ func NewCommands(com *common.Common, sessionID string, hasSession, hasTodos, has closeKey.SetHelp("esc", "cancel") c.keyMap.Close = closeKey + if available, known := config.DockerMCPAvailabilityCached(); known { + c.dockerMCPAvailable = &available + } + // Set initial commands c.setCommandItems(c.selected) @@ -145,6 +156,13 @@ func (c *Commands) ID() string { // HandleMsg implements [Dialog]. func (c *Commands) HandleMsg(msg tea.Msg) Action { switch msg := msg.(type) { + case dockerMCPAvailabilityCheckedMsg: + c.dockerMCPAvailable = &msg.available + c.dockerMCPCheckInFlight = false + if c.selected == SystemCommands { + c.setCommandItems(c.selected) + } + return nil case spinner.TickMsg: if c.loading { var cmd tea.Cmd @@ -207,6 +225,20 @@ func (c *Commands) HandleMsg(msg tea.Msg) Action { return nil } +func checkDockerMCPAvailabilityCmd() tea.Cmd { + return func() tea.Msg { + return dockerMCPAvailabilityCheckedMsg{available: config.RefreshDockerMCPAvailability()} + } +} + +func (c *Commands) InitialCmd() tea.Cmd { + if c.dockerMCPAvailable != nil || c.dockerMCPCheckInFlight { + return nil + } + c.dockerMCPCheckInFlight = true + return checkDockerMCPAvailabilityCmd() +} + // Cursor returns the cursor position relative to the dialog. func (c *Commands) Cursor() *tea.Cursor { return InputCursor(c.com.Styles, c.input.Cursor()) @@ -446,8 +478,8 @@ func (c *Commands) defaultCommands() []*CommandItem { commands = append(commands, NewCommandItem(c.com.Styles, "open_external_editor", "Open External Editor", "ctrl+o", ActionExternalEditor{})) } - // Add Docker MCP command if available and not already enabled - if config.IsDockerMCPAvailable() && !cfg.IsDockerMCPEnabled() { + // Add Docker MCP command if available and not already enabled. + if !cfg.IsDockerMCPEnabled() && c.dockerMCPAvailable != nil && *c.dockerMCPAvailable { commands = append(commands, NewCommandItem(c.com.Styles, "enable_docker_mcp", "Enable Docker MCP Catalog", "", ActionEnableDockerMCP{})) } diff --git a/internal/ui/model/ui.go b/internal/ui/model/ui.go index e87bc148c6a8e0286ed138cbd426697ac039a5d5..5b0c0350998a28b3490a7445d832bfbdec1fd3db 100644 --- a/internal/ui/model/ui.go +++ b/internal/ui/model/ui.go @@ -3017,7 +3017,7 @@ func (m *UI) openCommandsDialog() tea.Cmd { m.dialog.OpenDialog(commands) - return nil + return commands.InitialCmd() } // openReasoningDialog opens the reasoning effort dialog. @@ -3473,16 +3473,17 @@ func (m *UI) enableDockerMCP() tea.Msg { ctx := context.Background() if err := mcp.InitializeSingle(ctx, config.DockerMCPName, store); err != nil { - // Roll back in-memory state when startup fails. + // Roll back runtime and in-memory state when startup fails. + disableErr := mcp.DisableSingle(store, config.DockerMCPName) delete(store.Config().MCP, config.DockerMCPName) - return util.ReportError(fmt.Errorf("docker MCP enabled but failed to start: %w", err))() + return util.ReportError(fmt.Errorf("failed to start docker MCP: %w", errors.Join(err, disableErr)))() } if err := store.PersistDockerMCPConfig(mcpConfig); err != nil { // Roll back runtime and in-memory state if persistence fails. - _ = mcp.DisableSingle(store, config.DockerMCPName) + disableErr := mcp.DisableSingle(store, config.DockerMCPName) delete(store.Config().MCP, config.DockerMCPName) - return util.ReportError(fmt.Errorf("docker MCP started but failed to persist configuration: %w", err))() + return util.ReportError(fmt.Errorf("docker MCP started but failed to persist configuration: %w", errors.Join(err, disableErr)))() } return util.NewInfoMsg("Docker MCP enabled and started successfully") From 12dfcdf25aae3b204247b8912461dfe7e44e10ef Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 19 Mar 2026 12:58:03 -0400 Subject: [PATCH 18/26] docs(mcp/docker): comments --- internal/config/docker_mcp.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/internal/config/docker_mcp.go b/internal/config/docker_mcp.go index 0bac24dff2aedb4fd27023f604e1978b7f936852..fda70e36f2734164c3bf926532b0b85b25e3ef34 100644 --- a/internal/config/docker_mcp.go +++ b/internal/config/docker_mcp.go @@ -35,6 +35,8 @@ func IsDockerMCPAvailable() bool { return err == nil } +// DockerMCPAvailabilityCached returns the cached Docker MCP availability and +// whether the cached value is still fresh. func DockerMCPAvailabilityCached() (available bool, known bool) { dockerMCPAvailabilityCache.mu.Lock() defer dockerMCPAvailabilityCache.mu.Unlock() @@ -48,6 +50,7 @@ func DockerMCPAvailabilityCached() (available bool, known bool) { return dockerMCPAvailabilityCache.available, true } +// RefreshDockerMCPAvailability refreshes and caches Docker MCP availability. func RefreshDockerMCPAvailability() bool { available := IsDockerMCPAvailable() dockerMCPAvailabilityCache.mu.Lock() @@ -67,6 +70,7 @@ func (c *Config) IsDockerMCPEnabled() bool { return exists } +// DockerMCPConfig returns the default Docker MCP stdio configuration. func DockerMCPConfig() MCPConfig { return MCPConfig{ Type: MCPStdio, @@ -76,6 +80,8 @@ func DockerMCPConfig() MCPConfig { } } +// PrepareDockerMCPConfig validates Docker MCP availability and stages the +// Docker MCP configuration in memory. func (s *ConfigStore) PrepareDockerMCPConfig() (MCPConfig, error) { if !IsDockerMCPAvailable() { return MCPConfig{}, fmt.Errorf("docker mcp is not available, please ensure docker is installed and 'docker mcp version' succeeds") @@ -89,6 +95,8 @@ func (s *ConfigStore) PrepareDockerMCPConfig() (MCPConfig, error) { return mcpConfig, nil } +// PersistDockerMCPConfig persists a previously prepared Docker MCP +// configuration to the global config file. func (s *ConfigStore) PersistDockerMCPConfig(mcpConfig MCPConfig) error { if err := s.SetConfigField(ScopeGlobal, "mcp."+DockerMCPName, mcpConfig); err != nil { return fmt.Errorf("failed to persist docker mcp configuration: %w", err) From 7e29c8578f9fa60aeb3bfe67142efcb292726da4 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 19 Mar 2026 17:31:09 -0300 Subject: [PATCH 19/26] chore: gofumpt --- internal/ui/dialog/commands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ui/dialog/commands.go b/internal/ui/dialog/commands.go index 429da6a638f1fd361ad8b609f69dbf712c0f6051..18d6a7599f6d1cfe76830a675aaa6f5d771f9e51 100644 --- a/internal/ui/dialog/commands.go +++ b/internal/ui/dialog/commands.go @@ -71,7 +71,7 @@ type Commands struct { customCommands []commands.CustomCommand mcpPrompts []commands.MCPPrompt - dockerMCPAvailable *bool + dockerMCPAvailable *bool dockerMCPCheckInFlight bool } From a2d45378c20e3747b7a0ad73b4f93cc0c2130ff0 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Fri, 20 Mar 2026 17:13:40 -0300 Subject: [PATCH 21/26] fix(fantasy): fix copilot tool calls, fix ollama compatibility (#2444) * Closes https://github.com/charmbracelet/crush/issues/1767 * Closes https://github.com/charmbracelet/crush/issues/714 * Ref https://github.com/charmbracelet/fantasy/pull/156 * Ref https://github.com/charmbracelet/fantasy/pull/113 --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 50fe16c757978360584639104066d0d6d7a7a66a..eb4353c436878ce65fa33c3fde950ffd7c8b321d 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.26.1 require ( charm.land/bubbles/v2 v2.0.0 charm.land/bubbletea/v2 v2.0.2 - charm.land/catwalk v0.30.8 + charm.land/catwalk v0.31.0 charm.land/fang/v2 v2.0.1 - charm.land/fantasy v0.15.1 + charm.land/fantasy v0.16.0 charm.land/glamour/v2 v2.0.0 charm.land/lipgloss/v2 v2.0.2 charm.land/log/v2 v2.0.0 diff --git a/go.sum b/go.sum index d4df5e38592e63ee0739ec867ffd5c552991cf07..5554357ec911c728506e24479eaa7e0241b37bd5 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,12 @@ charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= -charm.land/catwalk v0.30.8 h1:Y5FYPY8iHoejXKxyp2Akbn+3SBb0zocY7aetjHxGs70= -charm.land/catwalk v0.30.8/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg= +charm.land/catwalk v0.31.0 h1:ci2LRf5Gy5BgbbQDN7cXEXOeNA2lP1sqVuXdUrAph3w= +charm.land/catwalk v0.31.0/go.mod h1:+fqw/6YGNtvapvPy9vhwA/fAMxVjD2K8hVIKYov8Vhg= charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY= charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII= -charm.land/fantasy v0.15.1 h1:eojrAXd2hvq5vuLO5Rcobv5P/xgen0YAngDquUawFvo= -charm.land/fantasy v0.15.1/go.mod h1:VZjpXVh7IgeiIzGQybEnKzd68ofDsRj94+kzH1ZCAfQ= +charm.land/fantasy v0.16.0 h1:vE/6sR9nPcSD8qXJXX6wR8NXjtWlBVAzwQmTh5pHVrs= +charm.land/fantasy v0.16.0/go.mod h1:VZjpXVh7IgeiIzGQybEnKzd68ofDsRj94+kzH1ZCAfQ= charm.land/glamour/v2 v2.0.0 h1:IDBoqLEy7Hdpb9VOXN+khLP/XSxtJy1VsHuW/yF87+U= charm.land/glamour/v2 v2.0.0/go.mod h1:kjq9WB0s8vuUYZNYey2jp4Lgd9f4cKdzAw88FZtpj/w= charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= From 3977e5132d1809845c4d0933847cebea99284136 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Fri, 20 Mar 2026 17:23:26 -0300 Subject: [PATCH 23/26] ci: attempt to fix dependabot --- .github/dependabot.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cf970b5887bc33fd822ab7fc4fe4540df045a6e1..72f70bfaecfca298f6146051068f16e4cdfd7ce1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,6 +24,7 @@ updates: - dependency-name: github.com/charmbracelet/lipgloss/v2 versions: - v2.0.0-beta1 + - dependency-name: mvdan.cc/sh/moreinterp - package-ecosystem: "github-actions" directory: "/" From 2521cb0cf0200142cf8e2ca9d4672763f6bb5c43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:29:13 +0000 Subject: [PATCH 24/26] chore(deps): bump modernc.org/sqlite in the all group (#2445) Bumps the all group with 1 update: [modernc.org/sqlite](https://gitlab.com/cznic/sqlite). Updates `modernc.org/sqlite` from 1.46.1 to 1.47.0 - [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md) - [Commits](https://gitlab.com/cznic/sqlite/compare/v1.46.1...v1.47.0) --- updated-dependencies: - dependency-name: modernc.org/sqlite dependency-version: 1.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index eb4353c436878ce65fa33c3fde950ffd7c8b321d..23ae71446c7c5d50d54da84aa424b094556c82fd 100644 --- a/go.mod +++ b/go.mod @@ -68,7 +68,7 @@ require ( golang.org/x/text v0.35.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 - modernc.org/sqlite v1.46.1 + modernc.org/sqlite v1.47.0 mvdan.cc/sh/moreinterp v0.0.0-20250902163504-3cf4fd5717a5 mvdan.cc/sh/v3 v3.13.0 ) @@ -197,7 +197,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 - modernc.org/libc v1.68.0 // indirect + modernc.org/libc v1.70.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 5554357ec911c728506e24479eaa7e0241b37bd5..8fd76ce3c8bc0808222ccecd97b66770dc23d4e3 100644 --- a/go.sum +++ b/go.sum @@ -549,18 +549,18 @@ 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= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so= -modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw= -modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= -modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= +modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ= -modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0= +modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= +modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -569,8 +569,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= -modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk= +modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From de33ce79e5d28ec5b9094b0eff7ffc7aad361069 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:29:37 +0000 Subject: [PATCH 25/26] chore(deps): bump the all group with 2 updates (#2446) Bumps the all group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [anchore/scan-action](https://github.com/anchore/scan-action). Updates `github/codeql-action` from 4.33.0 to 4.34.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/b1bff81932f5cdfc8695c7752dcee935dcd061c8...38697555549f1db7851b81482ff19f1fa5c4fedc) Updates `anchore/scan-action` from 7.3.2 to 7.4.0 - [Release notes](https://github.com/anchore/scan-action/releases) - [Changelog](https://github.com/anchore/scan-action/blob/main/RELEASE.md) - [Commits](https://github.com/anchore/scan-action/compare/7037fa011853d5a11690026fb85feee79f4c946c...e1165082ffb1fe366ebaf02d8526e7c4989ea9d2) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.34.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all - dependency-name: anchore/scan-action dependency-version: 7.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/security.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index cf4a9b535b0522833cabc9a6906dcdd7c0193124..9e590b9e00fb39d13d20842624d3fd6955a38bae 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -30,11 +30,11 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + - uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 with: languages: ${{ matrix.language }} - - uses: github/codeql-action/autobuild@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 - - uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + - uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 + - uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 grype: runs-on: ubuntu-latest @@ -46,13 +46,13 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2 + - uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 id: scan with: path: "." fail-build: true severity-cutoff: critical - - uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + - uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 with: sarif_file: ${{ steps.scan.outputs.sarif }} @@ -73,7 +73,7 @@ jobs: - name: Run govulncheck run: | govulncheck -C . -format sarif ./... > results.sarif - - uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + - uses: github/codeql-action/upload-sarif@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1 with: sarif_file: results.sarif From c4f0f1db5fb6411b517d7d4d69d855de256e9f65 Mon Sep 17 00:00:00 2001 From: Charm <124303983+charmcli@users.noreply.github.com> Date: Sat, 21 Mar 2026 00:49:21 -0300 Subject: [PATCH 26/26] chore(legal): @whatnick has signed the CLA --- .github/cla-signatures.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/cla-signatures.json b/.github/cla-signatures.json index 28beb0e7b1bdd75859d88eb4f0b1c900483ff7ff..89a3128c2e2ad02a26e06468975e20cbd82fa32f 100644 --- a/.github/cla-signatures.json +++ b/.github/cla-signatures.json @@ -1359,6 +1359,14 @@ "created_at": "2026-03-17T18:41:50Z", "repoId": 987670088, "pullRequestNo": 2421 + }, + { + "name": "whatnick", + "id": 491396, + "comment_id": 4102155868, + "created_at": "2026-03-21T03:49:12Z", + "repoId": 987670088, + "pullRequestNo": 2449 } ] } \ No newline at end of file