Skip to content

Commit 5af7d99

Browse files
committed
Fix Windows file URI path parsing in watcher
parseFileURI returned /C:/Users/... (with leading /) for Windows file:///C:/... URIs, causing watcher.root_gone loop because os.Stat fails on that path. Strip the leading / before drive letters and normalize with filepath.FromSlash. Fixes the remaining watcher bug from #20.
1 parent 1998470 commit 5af7d99

2 files changed

Lines changed: 73 additions & 2 deletions

File tree

internal/tools/tools.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net/http"
1010
"net/url"
1111
"os"
12+
"path/filepath"
1213
"sort"
1314
"strconv"
1415
"strings"
@@ -24,7 +25,7 @@ import (
2425
)
2526

2627
// Version is the current release version, referenced by MCP handshake and update checker.
27-
const Version = "0.4.0"
28+
const Version = "0.4.3"
2829

2930
// releaseURL is the GitHub API endpoint for latest release. Package-level var for test injection.
3031
var releaseURL = "https://api.github.com/repos/DeusData/codebase-memory-mcp/releases/latest"
@@ -180,7 +181,12 @@ func parseFileURI(uri string) (string, bool) {
180181
if err != nil || u.Scheme != "file" {
181182
return "", false
182183
}
183-
return u.Path, true
184+
path := u.Path
185+
// On Windows, file:///C:/path parses to /C:/path — strip leading / before drive letter
186+
if len(path) >= 3 && path[0] == '/' && path[2] == ':' {
187+
path = path[1:]
188+
}
189+
return filepath.FromSlash(path), true
184190
}
185191

186192
// startAutoIndex triggers background indexing for the session project.

internal/tools/tools_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package tools
2+
3+
import (
4+
"runtime"
5+
"testing"
6+
)
7+
8+
func TestParseFileURI(t *testing.T) {
9+
tests := []struct {
10+
uri string
11+
wantPath string
12+
wantOK bool
13+
}{
14+
// Unix paths
15+
{"file:///home/user/project", "/home/user/project", true},
16+
{"file:///tmp/test", "/tmp/test", true},
17+
18+
// Windows paths — url.Parse returns /C:/path, must strip leading /
19+
{"file:///C:/Users/project", "C:/Users/project", true},
20+
{"file:///D:/Projects/myapp", "D:/Projects/myapp", true},
21+
22+
// Non-file schemes
23+
{"https://example.com", "", false},
24+
{"", "", false},
25+
26+
// Edge cases
27+
{"file:///", "/", true},
28+
}
29+
30+
for _, tt := range tests {
31+
t.Run(tt.uri, func(t *testing.T) {
32+
got, ok := parseFileURI(tt.uri)
33+
if ok != tt.wantOK {
34+
t.Fatalf("parseFileURI(%q) ok=%v, want %v", tt.uri, ok, tt.wantOK)
35+
}
36+
if !ok {
37+
return
38+
}
39+
40+
want := tt.wantPath
41+
// On Windows, filepath.FromSlash converts / to \
42+
if runtime.GOOS == "windows" {
43+
// paths will use backslashes
44+
want = windowsPath(want)
45+
}
46+
47+
if got != want {
48+
t.Errorf("parseFileURI(%q) = %q, want %q", tt.uri, got, want)
49+
}
50+
})
51+
}
52+
}
53+
54+
// windowsPath converts forward slashes to backslashes for Windows comparison.
55+
func windowsPath(p string) string {
56+
result := make([]byte, len(p))
57+
for i := 0; i < len(p); i++ {
58+
if p[i] == '/' {
59+
result[i] = '\\'
60+
} else {
61+
result[i] = p[i]
62+
}
63+
}
64+
return string(result)
65+
}

0 commit comments

Comments
 (0)