Detailed changes
  
  
    
    @@ -110,6 +110,7 @@ homebrew_casks:
   - repository:
       owner: charmbracelet
       name: homebrew-tap
+      token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
 
 npms:
   - name: "@charmland/crush"
@@ -134,6 +135,32 @@ nfpms:
       - src: ./manpages/crush.1.gz
         dst: /usr/share/man/man1/crush.1.gz
 
+nix:
+  - repository:
+      owner: "charmbracelet"
+      name: nur
+      token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
+    extra_install: |-
+      installManPage ./manpages/crush.1.gz.
+      installShellCompletion ./completions/*
+
+winget:
+  - publisher: charmbracelet
+    copyright: Charmbracelet, Inc
+    repository:
+      owner: "charmbracelet"
+      name: winget-pkgs
+      token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
+      branch: "crush-{{.Version}}"
+      pull_request:
+        enabled: true
+        draft: false
+        check_boxes: true
+        base:
+          owner: microsoft
+          name: winget-pkgs
+          branch: master
+
 changelog:
   sort: asc
   disable: "{{ .IsNightly }}"
  
  
  
    
    @@ -17,7 +17,7 @@ import (
 	"github.com/charmbracelet/crush/internal/log"
 )
 
-const catwalkURL = "https://catwalk.charm.sh"
+const defaultCatwalkURL = "https://catwalk.charm.sh"
 
 // LoadReader config via io.Reader.
 func LoadReader(fd io.Reader) (*Config, error) {
  
  
  
    
    @@ -1,6 +1,7 @@
 package config
 
 import (
+	"cmp"
 	"encoding/json"
 	"fmt"
 	"log/slog"
@@ -74,6 +75,7 @@ func loadProvidersFromCache(path string) ([]catwalk.Provider, error) {
 }
 
 func Providers() ([]catwalk.Provider, error) {
+	catwalkURL := cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL)
 	client := catwalk.NewWithURL(catwalkURL)
 	path := providerCacheFileData()
 	return loadProvidersOnce(client, path)
  
  
  
    
    @@ -4,6 +4,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"log/slog"
 	"maps"
 	"sort"
 	"strings"
@@ -118,7 +119,13 @@ func waitForLspDiagnostics(ctx context.Context, filePath string, lsps map[string
 				return
 			}
 
-			if diagParams.URI.Path() == filePath || hasDiagnosticsChanged(client.GetDiagnostics(), originalDiags) {
+			path, err := diagParams.URI.Path()
+			if err != nil {
+				slog.Error("Failed to convert diagnostic URI to path", "uri", diagParams.URI, "error", err)
+				return
+			}
+
+			if path == filePath || hasDiagnosticsChanged(client.GetDiagnostics(), originalDiags) {
 				select {
 				case diagChan <- struct{}{}:
 				default:
@@ -216,10 +223,15 @@ func getDiagnostics(filePath string, lsps map[string]*lsp.Client) string {
 		diagnostics := client.GetDiagnostics()
 		if len(diagnostics) > 0 {
 			for location, diags := range diagnostics {
-				isCurrentFile := location.Path() == filePath
+				path, err := location.Path()
+				if err != nil {
+					slog.Error("Failed to convert diagnostic location URI to path", "uri", location, "error", err)
+					continue
+				}
+				isCurrentFile := path == filePath
 
 				for _, diag := range diags {
-					formattedDiag := formatDiagnostic(location.Path(), diag, lspName)
+					formattedDiag := formatDiagnostic(path, diag, lspName)
 
 					if isCurrentFile {
 						fileDiagnostics = append(fileDiagnostics, formattedDiag)
  
  
  
    
    @@ -449,7 +449,12 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error {
 
 	// If we have any open files, try to get document symbols for one
 	for uri := range c.openFiles {
-		filePath := protocol.DocumentURI(uri).Path()
+		filePath, err := protocol.DocumentURI(uri).Path()
+		if err != nil {
+			slog.Error("Failed to convert URI to path for TypeScript symbol collection", "uri", uri, "error", err)
+			continue
+		}
+
 		if strings.HasSuffix(filePath, ".ts") || strings.HasSuffix(filePath, ".js") ||
 			strings.HasSuffix(filePath, ".tsx") || strings.HasSuffix(filePath, ".jsx") {
 			var symbols []protocol.DocumentSymbol
@@ -712,7 +717,11 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
 	// First collect all URIs that need to be closed
 	for uri := range c.openFiles {
 		// Convert URI back to file path using proper URI handling
-		filePath := protocol.DocumentURI(uri).Path()
+		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
+		}
 		filesToClose = append(filesToClose, filePath)
 	}
 	c.openFilesMu.Unlock()
  
  
  
    
    @@ -2,6 +2,7 @@ package protocol
 
 import (
 	"fmt"
+	"log/slog"
 )
 
 // PatternInfo is an interface for types that represent glob patterns
@@ -36,21 +37,36 @@ func (g *GlobPattern) AsPattern() (PatternInfo, error) {
 		return nil, fmt.Errorf("nil pattern")
 	}
 
+	var err error
+
 	switch v := g.Value.(type) {
 	case string:
 		return StringPattern{Pattern: v}, nil
+
 	case RelativePattern:
 		// Handle BaseURI which could be string or DocumentUri
 		basePath := ""
 		switch baseURI := v.BaseURI.Value.(type) {
 		case string:
-			basePath = DocumentURI(baseURI).Path()
+			basePath, err = DocumentURI(baseURI).Path()
+			if err != nil {
+				slog.Error("Failed to convert URI to path", "uri", baseURI, "error", err)
+				return nil, fmt.Errorf("invalid URI: %s", baseURI)
+			}
+
 		case DocumentURI:
-			basePath = baseURI.Path()
+			basePath, err = baseURI.Path()
+			if err != nil {
+				slog.Error("Failed to convert DocumentURI to path", "uri", baseURI, "error", err)
+				return nil, fmt.Errorf("invalid DocumentURI: %s", baseURI)
+			}
+
 		default:
 			return nil, fmt.Errorf("unknown BaseURI type: %T", v.BaseURI.Value)
 		}
+
 		return RelativePatternInfo{RP: v, BasePath: basePath}, nil
+
 	default:
 		return nil, fmt.Errorf("unknown pattern type: %T", g.Value)
 	}
  
  
  
    
    @@ -70,7 +70,7 @@ func (uri *DocumentURI) UnmarshalText(data []byte) (err error) {
 // DocumentUri("").Path() returns the empty string.
 //
 // Path panics if called on a URI that is not a valid filename.
-func (uri DocumentURI) Path() string {
+func (uri DocumentURI) Path() (string, error) {
 	filename, err := filename(uri)
 	if err != nil {
 		// e.g. ParseRequestURI failed.
@@ -79,22 +79,33 @@ func (uri DocumentURI) Path() string {
 		// direct string manipulation; all DocumentUris
 		// received from the client pass through
 		// ParseRequestURI, which ensures validity.
-		panic(err)
+		return "", fmt.Errorf("invalid URI %q: %w", uri, err)
 	}
-	return filepath.FromSlash(filename)
+	return filepath.FromSlash(filename), nil
 }
 
 // Dir returns the URI for the directory containing the receiver.
-func (uri DocumentURI) Dir() DocumentURI {
+func (uri DocumentURI) Dir() (DocumentURI, error) {
+	// XXX: Legacy comment:
 	// This function could be more efficiently implemented by avoiding any call
 	// to Path(), but at least consolidates URI manipulation.
-	return URIFromPath(uri.DirPath())
+
+	path, err := uri.DirPath()
+	if err != nil {
+		return "", fmt.Errorf("invalid URI %q: %w", uri, err)
+	}
+
+	return URIFromPath(path), nil
 }
 
 // DirPath returns the file path to the directory containing this URI, which
 // must be a file URI.
-func (uri DocumentURI) DirPath() string {
-	return filepath.Dir(uri.Path())
+func (uri DocumentURI) DirPath() (string, error) {
+	path, err := uri.Path()
+	if err != nil {
+		return "", err
+	}
+	return filepath.Dir(path), nil
 }
 
 func filename(uri DocumentURI) (string, error) {
  
  
  
    
    @@ -11,7 +11,10 @@ import (
 )
 
 func applyTextEdits(uri protocol.DocumentURI, edits []protocol.TextEdit) error {
-	path := uri.Path()
+	path, err := uri.Path()
+	if err != nil {
+		return fmt.Errorf("invalid URI: %w", err)
+	}
 
 	// Read the file content
 	content, err := os.ReadFile(path)
@@ -148,7 +151,11 @@ func applyTextEdit(lines []string, edit protocol.TextEdit) ([]string, error) {
 // applyDocumentChange applies a DocumentChange (create/rename/delete operations)
 func applyDocumentChange(change protocol.DocumentChange) error {
 	if change.CreateFile != nil {
-		path := change.CreateFile.URI.Path()
+		path, err := change.CreateFile.URI.Path()
+		if err != nil {
+			return fmt.Errorf("invalid URI: %w", err)
+		}
+
 		if change.CreateFile.Options != nil {
 			if change.CreateFile.Options.Overwrite {
 				// Proceed with overwrite
@@ -164,7 +171,11 @@ func applyDocumentChange(change protocol.DocumentChange) error {
 	}
 
 	if change.DeleteFile != nil {
-		path := change.DeleteFile.URI.Path()
+		path, err := change.DeleteFile.URI.Path()
+		if err != nil {
+			return fmt.Errorf("invalid URI: %w", err)
+		}
+
 		if change.DeleteFile.Options != nil && change.DeleteFile.Options.Recursive {
 			if err := os.RemoveAll(path); err != nil {
 				return fmt.Errorf("failed to delete directory recursively: %w", err)
@@ -177,8 +188,19 @@ func applyDocumentChange(change protocol.DocumentChange) error {
 	}
 
 	if change.RenameFile != nil {
-		oldPath := change.RenameFile.OldURI.Path()
-		newPath := change.RenameFile.NewURI.Path()
+		var newPath, oldPath string
+		var err error
+
+		oldPath, err = change.RenameFile.OldURI.Path()
+		if err != nil {
+			return err
+		}
+
+		newPath, err = change.RenameFile.NewURI.Path()
+		if err != nil {
+			return err
+		}
+
 		if change.RenameFile.Options != nil {
 			if !change.RenameFile.Options.Overwrite {
 				if _, err := os.Stat(newPath); err == nil {
  
  
  
    
    @@ -617,7 +617,11 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
 		return false
 	}
 	// For relative patterns
-	basePath = protocol.DocumentURI(basePath).Path()
+	if basePath, err = protocol.DocumentURI(basePath).Path(); err != nil {
+		// XXX: Do we want to return here, or send the error up the stack?
+		slog.Error("Error converting base path to URI", "basePath", basePath, "error", err)
+	}
+
 	basePath = filepath.ToSlash(basePath)
 
 	// Make path relative to basePath for matching
@@ -660,7 +664,13 @@ func (w *WorkspaceWatcher) debounceHandleFileEvent(ctx context.Context, uri stri
 // handleFileEvent sends file change notifications
 func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
 	// If the file is open and it's a change event, use didChange notification
-	filePath := protocol.DocumentURI(uri).Path()
+	filePath, err := protocol.DocumentURI(uri).Path()
+	if err != nil {
+		// XXX: Do we want to return here, or send the error up the stack?
+		slog.Error("Error converting URI to path", "uri", uri, "error", err)
+		return
+	}
+
 	if changeType == protocol.FileChangeType(protocol.Deleted) {
 		w.client.ClearDiagnosticsForURI(protocol.DocumentURI(uri))
 	} else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
  
  
  
    
    @@ -843,7 +843,7 @@ func (p *chatPage) Help() help.KeyMap {
 					),
 					key.NewBinding(
 						key.WithKeys("g", "home"),
-						key.WithHelp("g", "hone"),
+						key.WithHelp("g", "home"),
 					),
 					key.NewBinding(
 						key.WithKeys("G", "end"),