Merge branch 'main' into server-client

Ayman Bagabas created

Change summary

.github/cla-signatures.json     |  8 +++++
.github/dependabot.yml          |  3 ++
go.mod                          |  2 
go.sum                          |  4 +-
internal/event/event.go         | 12 +-------
internal/event/identifier.go    | 49 ++++++++++++++++++++++++++++++++++
internal/llm/agent/mcp-tools.go |  4 +-
internal/lsp/client.go          | 50 ++++------------------------------
8 files changed, 74 insertions(+), 58 deletions(-)

Detailed changes

.github/cla-signatures.json 🔗

@@ -655,6 +655,14 @@
       "created_at": "2025-09-20T12:37:42Z",
       "repoId": 987670088,
       "pullRequestNo": 1095
+    },
+    {
+      "name": "Kaneki-x",
+      "id": 6857108,
+      "comment_id": 3338743039,
+      "created_at": "2025-09-26T13:30:16Z",
+      "repoId": 987670088,
+      "pullRequestNo": 1135
     }
   ]
 }

.github/dependabot.yml 🔗

@@ -18,6 +18,9 @@ updates:
         patterns:
           - "*"
     ignore:
+      - dependency-name: github.com/charmbracelet/bubbletea/v2
+        versions:
+          - v2.0.0-beta1
       - dependency-name: github.com/charmbracelet/lipgloss/v2
         versions:
           - v2.0.0-beta1

go.mod 🔗

@@ -153,7 +153,7 @@ require (
 	golang.org/x/text v0.29.0
 	golang.org/x/time v0.8.0 // indirect
 	google.golang.org/api v0.211.0 // indirect
-	google.golang.org/genai v1.25.0
+	google.golang.org/genai v1.26.0
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
 	google.golang.org/grpc v1.71.0 // indirect
 	google.golang.org/protobuf v1.36.8 // indirect

go.sum 🔗

@@ -427,8 +427,8 @@ golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxb
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/api v0.211.0 h1:IUpLjq09jxBSV1lACO33CGY3jsRcbctfGzhj+ZSE/Bg=
 google.golang.org/api v0.211.0/go.mod h1:XOloB4MXFH4UTlQSGuNUxw0UT74qdENK8d6JNsXKLi0=
-google.golang.org/genai v1.25.0 h1:Cpyh2nmEoOS1eM3mT9XKuA/qWTEDoktfP2gsN3EduPE=
-google.golang.org/genai v1.25.0/go.mod h1:OClfdf+r5aaD+sCd4aUSkPzJItmg2wD/WON9lQnRPaY=
+google.golang.org/genai v1.26.0 h1:r4HGL54kFv/WCRMTAbZg05Ct+vXfhAbTRlXhFyBkEQo=
+google.golang.org/genai v1.26.0/go.mod h1:OClfdf+r5aaD+sCd4aUSkPzJItmg2wD/WON9lQnRPaY=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=

internal/event/event.go 🔗

@@ -9,7 +9,6 @@ import (
 	"runtime"
 
 	"github.com/charmbracelet/crush/internal/version"
-	"github.com/denisbrodbeck/machineid"
 	"github.com/posthog/posthog-go"
 )
 
@@ -39,6 +38,7 @@ func Init() {
 		slog.Error("Failed to initialize PostHog client", "error", err)
 	}
 	client = c
+	distinctId = getDistinctId()
 }
 
 // send logs an event to PostHog with the given event name and properties.
@@ -47,7 +47,7 @@ func send(event string, props ...any) {
 		return
 	}
 	err := client.Enqueue(posthog.Capture{
-		DistinctId: distinctId(),
+		DistinctId: distinctId,
 		Event:      event,
 		Properties: pairsToProps(props...).Merge(baseProps),
 	})
@@ -105,11 +105,3 @@ func pairsToProps(props ...any) posthog.Properties {
 func isEven(n int) bool {
 	return n%2 == 0
 }
-
-func distinctId() string {
-	id, err := machineid.ProtectedID("charm")
-	if err != nil {
-		return "crush-cli"
-	}
-	return id
-}

internal/event/identifier.go 🔗

@@ -0,0 +1,49 @@
+package event
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"net"
+
+	"github.com/denisbrodbeck/machineid"
+)
+
+var distinctId string
+
+const (
+	hashKey    = "charm"
+	fallbackId = "unknown"
+)
+
+func getDistinctId() string {
+	if id, err := machineid.ProtectedID(hashKey); err == nil {
+		return id
+	}
+	if macAddr, err := getMacAddr(); err == nil {
+		return hashString(macAddr)
+	}
+	return fallbackId
+}
+
+func getMacAddr() (string, error) {
+	interfaces, err := net.Interfaces()
+	if err != nil {
+		return "", err
+	}
+	for _, iface := range interfaces {
+		if iface.Flags&net.FlagUp != 0 && iface.Flags&net.FlagLoopback == 0 && len(iface.HardwareAddr) > 0 {
+			if addrs, err := iface.Addrs(); err == nil && len(addrs) > 0 {
+				return iface.HardwareAddr.String(), nil
+			}
+		}
+	}
+	return "", fmt.Errorf("no active interface with mac address found")
+}
+
+func hashString(str string) string {
+	hash := hmac.New(sha256.New, []byte(str))
+	hash.Write([]byte(hashKey))
+	return hex.EncodeToString(hash.Sum(nil))
+}

internal/llm/agent/mcp-tools.go 🔗

@@ -233,9 +233,9 @@ func updateMCPState(name string, state MCPState, err error, client *client.Clien
 // CloseMCPClients closes all MCP clients. This should be called during application shutdown.
 func CloseMCPClients() error {
 	var errs []error
-	for c := range mcpClients.Seq() {
+	for name, c := range mcpClients.Seq2() {
 		if err := c.Close(); err != nil {
-			errs = append(errs, err)
+			errs = append(errs, fmt.Errorf("close mcp: %s: %w", name, err))
 		}
 	}
 	mcpBroker.Shutdown()

internal/lsp/client.go 🔗

@@ -350,29 +350,6 @@ func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
 	return c.client.NotifyDidChangeTextDocument(ctx, uri, int(fileInfo.Version), changes)
 }
 
-// CloseFile closes a file in the LSP server.
-//
-// NOTE: this is only ever called on LSP shutdown.
-func (c *Client) CloseFile(ctx context.Context, filepath string) error {
-	uri := string(protocol.URIFromPath(filepath))
-
-	if _, exists := c.openFiles.Get(uri); !exists {
-		return nil // Already closed
-	}
-
-	if c.cfg.Options.DebugLSP {
-		slog.Debug("Closing file", "file", filepath)
-	}
-
-	if err := c.client.NotifyDidCloseTextDocument(ctx, uri); err != nil {
-		return err
-	}
-
-	c.openFiles.Del(uri)
-
-	return nil
-}
-
 // IsFileOpen checks if a file is currently open.
 func (c *Client) IsFileOpen(filepath string) bool {
 	uri := string(protocol.URIFromPath(filepath))
@@ -382,29 +359,16 @@ func (c *Client) IsFileOpen(filepath string) bool {
 
 // CloseAllFiles closes all currently open files.
 func (c *Client) CloseAllFiles(ctx context.Context) {
-	filesToClose := make([]string, 0, c.openFiles.Len())
-
-	// First collect all URIs that need to be closed
+	debugLSP := c.cfg != nil && c.cfg.Options.DebugLSP
 	for uri := range c.openFiles.Seq2() {
-		// Convert URI back to file path using proper URI handling
-		filePath, err := protocol.DocumentURI(uri).Path()
-		if err != nil {
-			slog.Error("Failed to convert URI to path for file closing", "uri", uri, "error", err)
-			continue
+		if debugLSP {
+			slog.Debug("Closing file", "file", uri)
 		}
-		filesToClose = append(filesToClose, filePath)
-	}
-
-	// Then close them all
-	for _, filePath := range filesToClose {
-		err := c.CloseFile(ctx, filePath)
-		if err != nil && c.cfg != nil && c.cfg.Options.DebugLSP {
-			slog.Warn("Error closing file", "file", filePath, "error", err)
+		if err := c.client.NotifyDidCloseTextDocument(ctx, uri); err != nil {
+			slog.Warn("Error closing rile", "uri", uri, "error", err)
+			continue
 		}
-	}
-
-	if c.cfg != nil && c.cfg.Options.DebugLSP {
-		slog.Debug("Closed all files", "files", filesToClose)
+		c.openFiles.Del(uri)
 	}
 }