From b55c3369d25da877ed5ab1fb280cb68711e67297 Mon Sep 17 00:00:00 2001 From: razvan Date: Mon, 30 Mar 2026 09:49:21 +0300 Subject: [PATCH 1/4] fix: smart uninstaller cleanup + prevent nested .ragcode dirs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Uninstaller derives scan roots from registry, Qdrant file_path payloads, JetBrains/VSCode/AI-IDE configs — no more hardcoded directory guessing - findWorkspaceRootFromFilePath: walk up from Qdrant payload to workspace root - detectIDEProjectParents: reads recentProjects.xml, storage.json, resolveIDEPaths - detector: removed .ragcode from workspace markers (prevents hijacking root detection) - resolver: applyNestedOverride centralized — prevents split-indexing on sub-dirs --- cmd/rag-code-mcp/main.go | 2 +- internal/uninstall/uninstall.go | 369 ++++++++++++++++++++++++--- internal/uninstall/uninstall_test.go | 6 +- pkg/workspace/detector/detector.go | 1 - pkg/workspace/resolver/resolver.go | 30 ++- 5 files changed, 353 insertions(+), 55 deletions(-) diff --git a/cmd/rag-code-mcp/main.go b/cmd/rag-code-mcp/main.go index 78d722f..098382c 100644 --- a/cmd/rag-code-mcp/main.go +++ b/cmd/rag-code-mcp/main.go @@ -16,7 +16,7 @@ import ( ) var ( - Version = "2.1.86" + Version = "2.1.87" Commit = "none" Date = "24.10.2025" ) diff --git a/internal/uninstall/uninstall.go b/internal/uninstall/uninstall.go index d16aa70..b46f842 100644 --- a/internal/uninstall/uninstall.go +++ b/internal/uninstall/uninstall.go @@ -2,6 +2,7 @@ package uninstall import ( "encoding/json" + "encoding/xml" "fmt" "net" "os" @@ -307,36 +308,35 @@ func removeDockerResources() { func cleanWorkspaceData(home string) { registryPath := filepath.Join(home, installDirName, "registry.json") + var registryRoots []string data, err := os.ReadFile(registryPath) - if err != nil { - logMsg("Registry not found, scanning common project directories...") - scanAndCleanRagcodeDirs(home) - return - } - - roots := extractWorkspaceRoots(data) - if len(roots) == 0 { - warnMsg("Could not extract workspace paths from registry, scanning common directories...") - scanAndCleanRagcodeDirs(home) - return + if err == nil { + registryRoots = extractWorkspaceRoots(data) } - cleaned := 0 - for _, wsPath := range roots { - ragDir := filepath.Join(wsPath, ".ragcode") - if _, err := os.Stat(ragDir); err == nil { - if err := os.RemoveAll(ragDir); err != nil { - warnMsg(fmt.Sprintf("Failed to remove %s: %v", ragDir, err)) - } else { - successMsg("Removed workspace data: " + ragDir) - cleaned++ + // Step 1: direct delete for each workspace known to the registry. + if len(registryRoots) > 0 { + for _, wsPath := range registryRoots { + ragDir := filepath.Join(wsPath, ".ragcode") + if _, err := os.Stat(ragDir); err == nil { + if err := os.RemoveAll(ragDir); err != nil { + warnMsg(fmt.Sprintf("Failed to remove %s: %v", ragDir, err)) + } else { + successMsg("Removed workspace data (from registry): " + ragDir) + } } } + } else { + logMsg("No valid registry found or no per-workspace .ragcode/ directories in registry.") } - if cleaned == 0 { - logMsg("No per-workspace .ragcode/ directories found in registry entries") - } + // Step 2: catch anything the registry missed — orphaned dirs, workspaces + // outside $HOME, or roots the registry entry was lost for. + // Scan roots are derived from: registry parent dirs, Qdrant file_path payloads, + // IDE project lists, and a shallow $HOME scan. + logMsg("Scanning for any orphaned .ragcode/ directories not covered by registry...") + qdrantRoots := extractWorkspaceRootsFromQdrant() + scanAndCleanRagcodeDirs(home, append(registryRoots, qdrantRoots...)) } // extractWorkspaceRoots tries to parse the registry in all known formats @@ -401,46 +401,333 @@ func extractWorkspaceRoots(data []byte) []string { return nil } -func scanAndCleanRagcodeDirs(home string) { - searchRoots := []string{ - filepath.Join(home, "Projects"), - filepath.Join(home, "projects"), - filepath.Join(home, "go", "src"), - filepath.Join(home, "Code"), - filepath.Join(home, "code"), - filepath.Join(home, "dev"), - filepath.Join(home, "workspace"), +// scanAndCleanRagcodeDirs scans for orphaned .ragcode/ directories not covered +// by the registry cleanup above. +// +// Strategy: instead of guessing user-specific folder names (Projects, code, dev…), +// we derive search roots from data we already know: +// 1. Parent directories of every registered workspace root — any sibling or +// leftover from a workspace that was unregistered but still has cache. +// 2. $HOME itself at depth 1 — catches any .ragcode/ that was accidentally +// written directly under the user's home directory. +// +// This approach works for any user, on any OS, with any folder structure. +func scanAndCleanRagcodeDirs(home string, registryRoots []string) { + // Build a de-duplicated set of directories to scan. + seen := make(map[string]struct{}) + var searchRoots []string + + add := func(dir string) { + dir = filepath.Clean(dir) + if dir == "." || dir == "" { + return + } + if _, ok := seen[dir]; ok { + return + } + seen[dir] = struct{}{} + searchRoots = append(searchRoots, dir) + } + + // Derive parent directories from every registered workspace. + // These are the most likely places to find orphaned cache dirs. + for _, root := range registryRoots { + if root != "" { + add(filepath.Dir(root)) + } + } + + // Derive parent directories from IDE project lists. + // IDEs keep authoritative lists of opened projects in known config files + // — much more reliable than guessing folder names. + for _, ideRoot := range detectIDEProjectParents(home) { + add(ideRoot) } + // Always include $HOME (depth-1 only) to catch top-level orphans. + add(home) + cleaned := 0 for _, root := range searchRoots { if _, err := os.Stat(root); os.IsNotExist(err) { continue } + + // isHomeRoot controls scan depth: for $HOME we only look 1 level deep + // to avoid scanning the entire filesystem; for parent dirs derived from + // known workspace paths we allow a few levels to catch nested leftovers. + maxDepth := 3 + if filepath.Clean(root) == filepath.Clean(home) { + maxDepth = 1 + } + _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } rel, _ := filepath.Rel(root, path) - if strings.Count(rel, string(os.PathSeparator)) > 4 { + if strings.Count(rel, string(os.PathSeparator)) > maxDepth { return filepath.SkipDir } - if info.IsDir() && info.Name() == ".ragcode" { - if err := os.RemoveAll(path); err != nil { - warnMsg(fmt.Sprintf("Failed to remove %s: %v", path, err)) - } else { - successMsg("Removed workspace data: " + path) - cleaned++ + if info.IsDir() { + name := info.Name() + if name == ".git" || name == "node_modules" || name == "vendor" { + return filepath.SkipDir + } + if name == ".ragcode" { + if err := os.RemoveAll(path); err != nil { + warnMsg(fmt.Sprintf("Failed to remove %s: %v", path, err)) + } else { + successMsg("Removed orphaned workspace data: " + path) + cleaned++ + } + return filepath.SkipDir } - return filepath.SkipDir } return nil }) } if cleaned == 0 { - logMsg("No per-workspace .ragcode/ directories found") + logMsg("No orphaned .ragcode/ directories found") + } +} + +// detectIDEProjectParents returns the *parent directories* of projects known +// to installed IDEs. We read IDE config files that list recently opened +// projects — this is authoritative and works regardless of how the user +// organises their filesystem. +// +// Supported: +// - JetBrains family (IntelliJ, GoLand, PyCharm, WebStorm, Rider…) +// Linux/macOS: ~/.config/JetBrains/*/options/recentProjects.xml +// macOS legacy: ~/Library/Application Support/JetBrains/*/options/recentProjects.xml +// - VSCode / VSCodium / Cursor / Windsurf +// Linux: ~/.config/{Code,VSCodium,Cursor,Windsurf}/User/globalStorage/storage.json +// macOS: ~/Library/Application Support/{Code,VSCodium,Cursor,Windsurf}/User/... +func detectIDEProjectParents(home string) []string { + seen := make(map[string]struct{}) + var parents []string + + addProject := func(projectPath string) { + if projectPath == "" { + return + } + parent := filepath.Dir(filepath.Clean(projectPath)) + if _, ok := seen[parent]; ok { + return + } + seen[parent] = struct{}{} + parents = append(parents, parent) + } + + // ── JetBrains ───────────────────────────────────────────────────────────── + // recentProjects.xml contains . + type jbEntry struct { + Key string `xml:"key,attr"` + } + type jbMap struct { + Entries []jbEntry `xml:"entry"` + } + type jbComponent struct { + Name string `xml:"name,attr"` + Map jbMap `xml:"map"` + } + type jbApplication struct { + Components []jbComponent `xml:"component"` + } + + jetbrainsConfigDirs := []string{ + filepath.Join(home, ".config", "JetBrains"), + filepath.Join(home, "Library", "Application Support", "JetBrains"), + } + for _, jbBase := range jetbrainsConfigDirs { + productDirs, _ := os.ReadDir(jbBase) + for _, pd := range productDirs { + if !pd.IsDir() { + continue + } + recent := filepath.Join(jbBase, pd.Name(), "options", "recentProjects.xml") + data, err := os.ReadFile(recent) + if err != nil { + continue + } + var app jbApplication + if err := xml.Unmarshal(data, &app); err != nil { + continue + } + for _, comp := range app.Components { + if comp.Name != "RecentProjectsManager" && comp.Name != "RecentDirectoryProjectsManager" { + continue + } + for _, entry := range comp.Map.Entries { + path := strings.ReplaceAll(entry.Key, "$USER_HOME$", home) + addProject(path) + } + } + } + } + + // ── VSCode family ───────────────────────────────────────────────────────── + // storage.json has {"openedPathsList": {"workspaces3": ["/path", ...]}} + type vscodeStorage struct { + OpenedPathsList struct { + Workspaces []string `json:"workspaces3"` + Folders []string `json:"workspaceFolder"` + } `json:"openedPathsList"` + } + + vscodeApps := []string{"Code", "VSCodium", "Cursor", "Windsurf"} + vscodeConfigBases := []string{ + filepath.Join(home, ".config"), + filepath.Join(home, "Library", "Application Support"), + } + for _, base := range vscodeConfigBases { + for _, app := range vscodeApps { + storagePath := filepath.Join(base, app, "User", "globalStorage", "storage.json") + data, err := os.ReadFile(storagePath) + if err != nil { + continue + } + var st vscodeStorage + if err := json.Unmarshal(data, &st); err != nil { + continue + } + for _, p := range st.OpenedPathsList.Workspaces { + // Entries may be "file:///path" URIs. + p = strings.TrimPrefix(p, "file://") + addProject(p) + } + for _, p := range st.OpenedPathsList.Folders { + p = strings.TrimPrefix(p, "file://") + addProject(p) + } + } + } + + // ── AI IDE config directories ───────────────────────────────────────────── + // resolveIDEPaths already knows the canonical config-file locations for + // every AI IDE (Windsurf, Cursor, Copilot, Antigravity, Claude, Zed…). + // The *parent* of each config file is the IDE's own data directory + // (e.g. ~/.codeium/windsurf, ~/.cursor). Projects opened in those IDEs + // are often stored directly under the grandparent of that config + // (e.g. ~/.codeium, ~/.config/zed), so we add both levels. + for _, ide := range resolveIDEPaths(home) { + if ide.path == "" { + continue + } + // config file's parent dir (the IDE data dir, e.g. ~/.cursor) + addProject(filepath.Dir(ide.path)) + } + + return parents +} + +// extractWorkspaceRootsFromQdrant queries Qdrant (if reachable) to discover +// workspace roots that were indexed but whose .ragcode/registry entry may have +// been lost. +// +// Strategy: for each ragcode-* collection, fetch a single point via Scroll and +// read its file_path payload field. Then walk upward from that file_path until +// we find a directory that contains .git or .ragcode — that is the workspace root. +// We return those roots so the caller can delete their .ragcode/ dirs. +func extractWorkspaceRootsFromQdrant() []string { + const qdrantAddr = "http://localhost:6333" + + conn, err := net.DialTimeout("tcp", "127.0.0.1:6333", 2*time.Second) + if err != nil { + return nil // Qdrant not running — skip silently + } + conn.Close() + + // List all collections. + cmd := exec.Command("curl", "-s", qdrantAddr+"/collections") + output, err := cmd.Output() + if err != nil { + return nil + } + + var listResp struct { + Result struct { + Collections []struct { + Name string `json:"name"` + } `json:"collections"` + } `json:"result"` + } + if err := json.Unmarshal(output, &listResp); err != nil { + return nil + } + + seen := make(map[string]struct{}) + var roots []string + + for _, col := range listResp.Result.Collections { + if !strings.HasPrefix(col.Name, "ragcode-") { + continue + } + + // Scroll 1 point with payload to get a file_path sample. + scrollPayload := `{"limit":1,"with_payload":true,"with_vector":false}` + scrollCmd := exec.Command("curl", "-s", "-X", "POST", + qdrantAddr+"/collections/"+col.Name+"/points/scroll", + "-H", "Content-Type: application/json", + "-d", scrollPayload) + scrollOut, err := scrollCmd.Output() + if err != nil { + continue + } + + var scrollResp struct { + Result struct { + Points []struct { + Payload map[string]interface{} `json:"payload"` + } `json:"points"` + } `json:"result"` + } + if err := json.Unmarshal(scrollOut, &scrollResp); err != nil { + continue + } + if len(scrollResp.Result.Points) == 0 { + continue + } + + filePath, _ := scrollResp.Result.Points[0].Payload["file_path"].(string) + if filePath == "" { + continue + } + + // Walk upward from filePath to find workspace root. + wsRoot := findWorkspaceRootFromFilePath(filePath) + if wsRoot == "" { + continue + } + if _, ok := seen[wsRoot]; ok { + continue + } + seen[wsRoot] = struct{}{} + roots = append(roots, wsRoot) + } + + return roots +} + +// findWorkspaceRootFromFilePath walks upward from a file path until it finds +// a directory containing .git or .ragcode — the canonical workspace root markers. +func findWorkspaceRootFromFilePath(filePath string) string { + dir := filepath.Dir(filePath) + for { + for _, marker := range []string{".git", ".ragcode"} { + if _, err := os.Stat(filepath.Join(dir, marker)); err == nil { + return dir + } + } + parent := filepath.Dir(dir) + if parent == dir { + break // reached filesystem root + } + dir = parent } + return "" } func cleanQdrantCollections() { diff --git a/internal/uninstall/uninstall_test.go b/internal/uninstall/uninstall_test.go index 2593ced..6578ef1 100644 --- a/internal/uninstall/uninstall_test.go +++ b/internal/uninstall/uninstall_test.go @@ -155,8 +155,8 @@ func TestCleanWorkspaceData_WithV2Registry(t *testing.T) { t.Errorf("proj2/.ragcode should have been removed") } - // proj3/.ragcode should still exist (not in registry) - if _, err := os.Stat(filepath.Join(proj3, ".ragcode")); os.IsNotExist(err) { - t.Errorf("proj3/.ragcode should NOT have been removed (not in registry)") + // proj3/.ragcode should be gone too (cleaned by fallback scan) + if _, err := os.Stat(filepath.Join(proj3, ".ragcode")); !os.IsNotExist(err) { + t.Errorf("proj3/.ragcode should have been removed by fallback scan") } } diff --git a/pkg/workspace/detector/detector.go b/pkg/workspace/detector/detector.go index ec6d49d..78c82e2 100644 --- a/pkg/workspace/detector/detector.go +++ b/pkg/workspace/detector/detector.go @@ -48,7 +48,6 @@ func DefaultOptions() Options { "docker-compose.yml", "mix.exs", "artisan", - ".ragcode", ".agent", ".idea", ".vscode", diff --git a/pkg/workspace/resolver/resolver.go b/pkg/workspace/resolver/resolver.go index 7ec184f..d80c241 100644 --- a/pkg/workspace/resolver/resolver.go +++ b/pkg/workspace/resolver/resolver.go @@ -142,6 +142,8 @@ func (r *Resolver) handleWorkspaceRoot(ctx context.Context, root string) (*contr } r.log(ctx, "workspace_root", map[string]any{"root": root, "source": "workspace_root"}) candidate := &contract.WorkspaceCandidate{Root: root, Reason: contract.ReasonExplicitWorkspaceRoot, Source: "workspace_root", Confidence: 1.0} + + r.applyNestedOverride(ctx, candidate) return r.finalize(ctx, candidate) } @@ -215,16 +217,10 @@ func (r *Resolver) handleFilePath(ctx context.Context, path string) (*contract.R // vendored projects, monorepo sub-packages) from being treated as separate // workspaces when they live inside a project that is already indexed. // Skip this check when we already resolved via registry_fallback. - if r.deps.Registry != nil && result.Source != "registry_fallback" { - if parentRoot, found := r.deps.Registry.FindParentWorkspace(result.Root); found { - r.log(ctx, "nested_workspace_override", map[string]any{ - "detected_root": result.Root, - "parent_root": parentRoot, - "reason": "detected root is subdirectory of registered workspace", - }) - result.Root = parentRoot + if result.Source != "registry_fallback" { + r.applyNestedOverride(ctx, result) + if result.Source == "nested_workspace_override" { result.Reason = contract.ReasonFilePath - result.Source = "nested_workspace_override" result.Confidence = 0.90 // slightly lower than direct detection } } @@ -298,12 +294,14 @@ func (r *Resolver) handleRoots(ctx context.Context, req contract.ResolveWorkspac if len(roots) == 1 { candidate := &contract.WorkspaceCandidate{Root: roots[0], Reason: contract.ReasonRootsList, Source: "roots", Confidence: 0.8} r.log(ctx, "roots_single", map[string]any{"root": candidate.Root, "source": "roots"}) + r.applyNestedOverride(ctx, candidate) return r.finalize(ctx, candidate) } if best, ok := selectBestRoot(roots); ok { candidate := &contract.WorkspaceCandidate{Root: best, Reason: contract.ReasonRootsList, Source: "roots", Confidence: 0.75} r.log(ctx, "roots_scored", map[string]any{"root": candidate.Root, "source": "roots", "strategy": "depth"}) + r.applyNestedOverride(ctx, candidate) return r.finalize(ctx, candidate) } @@ -336,6 +334,20 @@ func (r *Resolver) handleRoots(ctx context.Context, req contract.ResolveWorkspac }, nil } +func (r *Resolver) applyNestedOverride(ctx context.Context, candidate *contract.WorkspaceCandidate) { + if r.deps.Registry != nil && candidate.Source != "registry_fallback" { + if parentRoot, found := r.deps.Registry.FindParentWorkspace(candidate.Root); found { + r.log(ctx, "nested_workspace_override", map[string]any{ + "original_root": candidate.Root, + "parent_root": parentRoot, + "reason": "candidate root is subdirectory of registered workspace", + }) + candidate.Root = parentRoot + candidate.Source = "nested_workspace_override" + } + } +} + func selectBestRoot(roots []string) (string, bool) { if len(roots) == 0 { return "", false From 25f45504dd0e2f328ad3166a427fd76670b5b1a4 Mon Sep 17 00:00:00 2001 From: doITmagic Date: Wed, 1 Apr 2026 07:42:47 +0300 Subject: [PATCH 2/4] fix(workspace): treat metadata file as valid marker to avoid unindexable markerless spaces --- pkg/workspace/detector/detector.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/workspace/detector/detector.go b/pkg/workspace/detector/detector.go index 78c82e2..b6c82df 100644 --- a/pkg/workspace/detector/detector.go +++ b/pkg/workspace/detector/detector.go @@ -185,6 +185,20 @@ func (d *Detector) inspectDir(dir string) (*contract.WorkspaceCandidate, *contra markers = append(markers, marker) } } + + if d.opts.MetadataFileName != "" { + found := false + for _, m := range markers { + if m == d.opts.MetadataFileName { + found = true + break + } + } + if !found && exists(filepath.Join(dir, d.opts.MetadataFileName)) { + markers = append(markers, d.opts.MetadataFileName) + } + } + if len(markers) == 0 { return nil, nil } From e3c7f2a913be0632fe02c6d41c8f9f917eb99cab Mon Sep 17 00:00:00 2001 From: doITmagic Date: Wed, 1 Apr 2026 07:51:20 +0300 Subject: [PATCH 3/4] fix: address PR review feedback on timeouts, depth limits, ide paths & nested overrides --- internal/uninstall/uninstall.go | 18 ++++++++++++------ pkg/workspace/resolver/resolver.go | 2 ++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/internal/uninstall/uninstall.go b/internal/uninstall/uninstall.go index b46f842..9012838 100644 --- a/internal/uninstall/uninstall.go +++ b/internal/uninstall/uninstall.go @@ -466,7 +466,12 @@ func scanAndCleanRagcodeDirs(home string, registryRoots []string) { return nil } rel, _ := filepath.Rel(root, path) - if strings.Count(rel, string(os.PathSeparator)) > maxDepth { + + depth := 0 + if rel != "." { + depth = strings.Count(rel, string(os.PathSeparator)) + 1 + } + if depth > maxDepth { return filepath.SkipDir } if info.IsDir() { @@ -617,7 +622,8 @@ func detectIDEProjectParents(home string) []string { continue } // config file's parent dir (the IDE data dir, e.g. ~/.cursor) - addProject(filepath.Dir(ide.path)) + dataDir := filepath.Dir(ide.path) + addProject(filepath.Join(dataDir, ".ragcode-ide-sentinel")) } return parents @@ -641,7 +647,7 @@ func extractWorkspaceRootsFromQdrant() []string { conn.Close() // List all collections. - cmd := exec.Command("curl", "-s", qdrantAddr+"/collections") + cmd := exec.Command("curl", "-s", "--max-time", "5", "--connect-timeout", "2", qdrantAddr+"/collections") output, err := cmd.Output() if err != nil { return nil @@ -668,7 +674,7 @@ func extractWorkspaceRootsFromQdrant() []string { // Scroll 1 point with payload to get a file_path sample. scrollPayload := `{"limit":1,"with_payload":true,"with_vector":false}` - scrollCmd := exec.Command("curl", "-s", "-X", "POST", + scrollCmd := exec.Command("curl", "-s", "--max-time", "5", "--connect-timeout", "2", "-X", "POST", qdrantAddr+"/collections/"+col.Name+"/points/scroll", "-H", "Content-Type: application/json", "-d", scrollPayload) @@ -738,7 +744,7 @@ func cleanQdrantCollections() { } conn.Close() - cmd := exec.Command("curl", "-s", "http://localhost:6333/collections") + cmd := exec.Command("curl", "-s", "--max-time", "5", "--connect-timeout", "2", "http://localhost:6333/collections") output, err := cmd.Output() if err != nil { warnMsg("Could not list Qdrant collections: " + err.Error()) @@ -760,7 +766,7 @@ func cleanQdrantCollections() { deleted := 0 for _, col := range resp.Result.Collections { if strings.HasPrefix(col.Name, "ragcode-") { - delCmd := exec.Command("curl", "-s", "-X", "DELETE", "http://localhost:6333/collections/"+col.Name) + delCmd := exec.Command("curl", "-s", "--max-time", "5", "--connect-timeout", "2", "-X", "DELETE", "http://localhost:6333/collections/"+col.Name) if err := delCmd.Run(); err != nil { warnMsg("Failed to delete collection " + col.Name + ": " + err.Error()) } else { diff --git a/pkg/workspace/resolver/resolver.go b/pkg/workspace/resolver/resolver.go index d80c241..9ee3428 100644 --- a/pkg/workspace/resolver/resolver.go +++ b/pkg/workspace/resolver/resolver.go @@ -344,6 +344,8 @@ func (r *Resolver) applyNestedOverride(ctx context.Context, candidate *contract. }) candidate.Root = parentRoot candidate.Source = "nested_workspace_override" + candidate.Reason = contract.ReasonRegistryFallback + candidate.Confidence = 0.85 } } } From 90c3d872ed81790fd30a3ffdd9b63b2d02ef3af6 Mon Sep 17 00:00:00 2001 From: doITmagic Date: Wed, 1 Apr 2026 08:48:27 +0300 Subject: [PATCH 4/4] fix(uninstall): resolve SkipDir error propagation and proper IDE grandparent mapping --- internal/uninstall/uninstall.go | 11 ++++++++++- pkg/workspace/detector/detector.go | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/uninstall/uninstall.go b/internal/uninstall/uninstall.go index 9012838..f9d368b 100644 --- a/internal/uninstall/uninstall.go +++ b/internal/uninstall/uninstall.go @@ -472,7 +472,10 @@ func scanAndCleanRagcodeDirs(home string, registryRoots []string) { depth = strings.Count(rel, string(os.PathSeparator)) + 1 } if depth > maxDepth { - return filepath.SkipDir + if info != nil && info.IsDir() { + return filepath.SkipDir + } + return nil } if info.IsDir() { name := info.Name() @@ -624,6 +627,12 @@ func detectIDEProjectParents(home string) []string { // config file's parent dir (the IDE data dir, e.g. ~/.cursor) dataDir := filepath.Dir(ide.path) addProject(filepath.Join(dataDir, ".ragcode-ide-sentinel")) + + // also add the grandparent dir (e.g. ~/.codeium, ~/.config/zed) + grandparent := filepath.Dir(dataDir) + if grandparent != "" && grandparent != dataDir { + addProject(filepath.Join(grandparent, ".ragcode-ide-sentinel")) + } } return parents diff --git a/pkg/workspace/detector/detector.go b/pkg/workspace/detector/detector.go index b6c82df..ddbe7cd 100644 --- a/pkg/workspace/detector/detector.go +++ b/pkg/workspace/detector/detector.go @@ -54,6 +54,7 @@ func DefaultOptions() Options { ".vs", ".cursor", ".windsurf", + ".claude", "AGENTS.md", "CLAUDE.md", },