Skip to content

Commit 83d9861

Browse files
committed
fix(workspace): overhaul workspace detection and tooling
- Extract hardcoded Tier markers into dynamic detector Options - Redesign walkUp to retain highest-priority roots instead of returning eagerly - Refactor rag_index_workspace tool to necessitate a solid 'workspace_root' rather than 'file_path' - Introduce dual-stage AI validation via the 'confirm' tool schema parameter - Eradicate legacy hardcoded workspace paths inside test suite (avoid 'backend' assumptions) - Update documentation and server.json resolving legacy file_path instructions - Expose FindAlternativeCandidates securely via engine wrappers
1 parent e956715 commit 83d9861

8 files changed

Lines changed: 226 additions & 69 deletions

File tree

cmd/rag-code-install/main.go

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,19 @@ func configureIDEs(selected []string, binDir string, transport string, ssePort i
646646
log(fmt.Sprintf(` command = "%s"`, binPath))
647647
log(` args = []`)
648648
}
649+
650+
// OpenClaw / NemoClaw Logging
651+
openclawPath := filepath.Join(home, ".openclaw")
652+
if _, err := os.Stat(openclawPath); err == nil {
653+
log("OpenClaw detected at ~/.openclaw – requires manual config.")
654+
log(fmt.Sprintf(` Run: openclaw mcp set ragcode "%s"`, binPath))
655+
}
656+
657+
nemoclawPath := filepath.Join(home, ".nemoclaw")
658+
if _, err := os.Stat(nemoclawPath); err == nil {
659+
log("NemoClaw sandbox detected – requires manual policy config.")
660+
log(" Please refer to NVIDIA OpenShell documentation to map the MCP tool.")
661+
}
649662
}
650663

651664
type idePath struct {
@@ -659,13 +672,10 @@ func resolveIDEPaths(home string) map[string]idePath {
659672
path: filepath.Join(home, ".codeium", "windsurf", "mcp_config.json"),
660673
displayName: "Windsurf",
661674
},
662-
"cursor": {
663-
path: filepath.Join(home, ".cursor", "mcp.config.json"),
664-
displayName: "Cursor",
665-
},
675+
"cursor": determineCursorPath(home),
666676
"copilot": {
667-
path: filepath.Join(home, ".aitk", "mcp.json"),
668-
displayName: "GitHub Copilot",
677+
path: filepath.Join(home, ".copilot", "mcp-config.json"),
678+
displayName: "GitHub Copilot CLI",
669679
},
670680
"antigravity": {
671681
path: filepath.Join(home, ".gemini", "antigravity", "mcp_config.json"),
@@ -685,6 +695,14 @@ func resolveIDEPaths(home string) map[string]idePath {
685695
path: filepath.Join(home, ".gemini", "settings.json"),
686696
displayName: "Gemini CLI",
687697
},
698+
"openhands": {
699+
path: filepath.Join(home, ".openhands", "mcp.json"),
700+
displayName: "OpenHands",
701+
},
702+
"continue": {
703+
path: filepath.Join(home, ".continue", "mcpServers", "ragcode.json"),
704+
displayName: "Continue.dev",
705+
},
688706
}
689707

690708
// Claude Desktop (GUI app) – OS-specific paths
@@ -711,31 +729,39 @@ func resolveIDEPaths(home string) map[string]idePath {
711729
paths["zed"] = idePath{filepath.Join(home, ".config", "zed", "settings.json"), "Zed Editor"}
712730
}
713731

714-
// VS Code (modern copilot mcp.json)
715-
if vsPath, ok := determineVSCodePath(home); ok {
716-
paths["vs-code"] = vsPath
732+
// VS Code (Copilot & popular extensions like Roo Code, Cline)
733+
if vsCodeUserDir, ok := getVSCodeUserDir(home); ok {
734+
paths["vs-code"] = idePath{path: filepath.Join(vsCodeUserDir, "mcp.json"), displayName: "VS Code (Copilot)"}
735+
paths["roo-code"] = idePath{path: filepath.Join(vsCodeUserDir, "globalStorage", "rooveterinaryinc.roo-cline", "settings", "cline_mcp_settings.json"), displayName: "Roo Code (VS Code)"}
736+
paths["cline"] = idePath{path: filepath.Join(vsCodeUserDir, "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"), displayName: "Cline (VS Code)"}
717737
}
718738

719739
return paths
720740
}
721741

722-
func determineVSCodePath(home string) (idePath, bool) {
723-
var userDir string
742+
func determineCursorPath(home string) idePath {
743+
if runtime.GOOS == "windows" {
744+
appData := os.Getenv("APPDATA")
745+
if appData != "" {
746+
return idePath{path: filepath.Join(appData, "Cursor", "mcp.json"), displayName: "Cursor"}
747+
}
748+
}
749+
return idePath{path: filepath.Join(home, ".cursor", "mcp.json"), displayName: "Cursor"}
750+
}
751+
752+
func getVSCodeUserDir(home string) (string, bool) {
724753
switch runtime.GOOS {
725754
case "windows":
726755
appData := os.Getenv("APPDATA")
727756
if appData == "" {
728-
return idePath{}, false
757+
return "", false
729758
}
730-
userDir = filepath.Join(appData, "Code", "User")
759+
return filepath.Join(appData, "Code", "User"), true
731760
case "darwin":
732-
userDir = filepath.Join(home, "Library", "Application Support", "Code", "User")
761+
return filepath.Join(home, "Library", "Application Support", "Code", "User"), true
733762
default:
734-
userDir = filepath.Join(home, ".config", "Code", "User")
763+
return filepath.Join(home, ".config", "Code", "User"), true
735764
}
736-
737-
newPath := filepath.Join(userDir, "mcp.json") // modern copilot mcp standard
738-
return idePath{path: newPath, displayName: "VS Code"}, true
739765
}
740766

741767
type ideSelection struct {

docs/tools/doc_index_workspace.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
This tool doesn't query the search API. Instead, it triggers the internal **Indexer Service Pipeline**.
1010

11-
1. **Resolution phase**: Resolves the exact boundaries of the Git repository based on `file_path`.
11+
1. **Resolution phase**: Validates the strictly provided `workspace_root` parameter, checks boundaries & dependencies, and enforces an AI confirmation sequence via the `confirm: true` parameter rule before proceeding.
1212
2. **Asynchronous Hand-off**: Executes `engine.StartIndexingAsync(wctx.Root, wctx.ID, nil, recreate)`.
1313

1414
## Features

internal/service/engine/engine.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,13 @@ func (e *Engine) Config() *config.Config {
140140
return e.config
141141
}
142142

143+
// FindAlternativeCandidates wraps detector logic to offer alternative root suggestions internally
144+
func (e *Engine) FindAlternativeCandidates(root string) []string {
145+
// Isolate execution using official detection definitions
146+
det := detector.New(detector.DefaultOptions())
147+
return det.FindAlternativeCandidates(root)
148+
}
149+
143150
// WorkspaceContext provides information about a detected workspace.
144151
type WorkspaceContext struct {
145152
Root string

internal/service/tools/index_workspace.go

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"runtime"
7+
"strings"
78
"time"
89

910
"github.com/doITmagic/rag-code-mcp/internal/logger"
@@ -27,13 +28,14 @@ func NewIndexWorkspaceTool(eng *engine.Engine) *IndexWorkspaceTool {
2728
func (t *IndexWorkspaceTool) Name() string { return "rag_index_workspace" }
2829
func (t *IndexWorkspaceTool) Description() string {
2930
return "Indexes or reindexes a workspace for semantic code search. " +
30-
"Use this tool to manually trigger indexing if search results are stale or 'workspace not indexed'. " +
31-
"Analyzes all supported source files and stores vectors for semantic search. " +
32-
"The process runs in the background."
31+
"Instead of abstract paths, you MUST explicitly provide the absolute 'workspace_root' directory. " +
32+
"The tool first validates the root and returns early. Then, you submit a second call with 'confirm': true " +
33+
"to actually lock-in the registry root and start the background indexing."
3334
}
3435

3536
type IndexWorkspaceInput struct {
36-
FilePath string `json:"file_path,omitempty"`
37+
WorkspaceRoot string `json:"workspace_root,omitempty"`
38+
Confirm bool `json:"confirm,omitempty"`
3739
Recreate bool `json:"recreate,omitempty"`
3840
IncludeRuntimeInfo bool `json:"include_runtime_info,omitempty"`
3941
}
@@ -44,7 +46,8 @@ func (t *IndexWorkspaceTool) Register(server *mcp.Server) {
4446
Description: t.Description(),
4547
}, func(ctx context.Context, req *mcp.CallToolRequest, input IndexWorkspaceInput) (*mcp.CallToolResult, any, error) {
4648
args := map[string]interface{}{
47-
"file_path": input.FilePath,
49+
"workspace_root": input.WorkspaceRoot,
50+
"confirm": input.Confirm,
4851
"recreate": input.Recreate,
4952
"include_runtime_info": input.IncludeRuntimeInfo,
5053
}
@@ -69,17 +72,43 @@ func (t *IndexWorkspaceTool) Register(server *mcp.Server) {
6972
}
7073

7174
func (t *IndexWorkspaceTool) Execute(ctx context.Context, params map[string]interface{}) (string, error) {
72-
filePath, _ := params["file_path"].(string)
75+
workspaceRoot, ok := params["workspace_root"].(string)
76+
if !ok || workspaceRoot == "" {
77+
response := ToolResponse{
78+
Status: "error",
79+
Error: "workspace_root parameter is strictly required to explicitly pinpoint the directory to index.",
80+
}
81+
return response.JSON()
82+
}
83+
84+
confirm, _ := params["confirm"].(bool)
7385

74-
// Detect workspace context
75-
wctx, err := t.engine.DetectContext(ctx, filePath)
86+
// Detect workspace context using explicit root validation
87+
wctx, err := t.engine.DetectContext(ctx, workspaceRoot)
7688
if err != nil {
89+
candidates := t.engine.FindAlternativeCandidates(workspaceRoot)
90+
msg := err.Error()
91+
if len(candidates) > 0 {
92+
msg = fmt.Sprintf("Validation failed: %s.\n\nSuggested local roots (Tiers applied):\n- %s", err.Error(), strings.Join(candidates, "\n- "))
93+
}
94+
7795
response := ToolResponse{
7896
Status: "error",
79-
Error: err.Error(),
97+
Error: msg,
98+
}
99+
return response.JSON()
100+
}
101+
102+
if !confirm {
103+
response := ToolResponse{
104+
Status: "confirmation_required",
105+
Message: fmt.Sprintf("Workspace detected successfully at '%s'. \nVerify this is the correct top-level folder! \n\nCall the tool again passing \"confirm\": true alongside the 'workspace_root' to begin indexing.", wctx.Root),
106+
Context: ContextMetadata{
107+
WorkspaceRoot: wctx.Root,
108+
DetectionSource: wctx.DetectionSource,
109+
},
80110
}
81-
body, _ := response.JSON()
82-
return body, nil
111+
return response.JSON()
83112
}
84113

85114
recreate, _ := params["recreate"].(bool)
@@ -89,7 +118,7 @@ func (t *IndexWorkspaceTool) Execute(ctx context.Context, params map[string]inte
89118

90119
response := ToolResponse{
91120
Status: "indexing_started",
92-
Message: fmt.Sprintf("🚀 Indexing started for workspace '%s'. The process is running in the background. You can use rag_search immediately - results will appear as indexing progresses.", wctx.Root),
121+
Message: fmt.Sprintf("🚀 Indexing started for validated workspace '%s'. The nested constraints were processed and cleanup checks triggered.", wctx.Root),
93122
Context: ContextMetadata{
94123
WorkspaceRoot: wctx.Root,
95124
DetectionSource: wctx.DetectionSource,

llms-full.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,7 +729,7 @@ Find all authentication logic in this codebase.
729729
cd ~/projects/my-awesome-app
730730

731731
# 2. Ask AI to index (using the prompt above)
732-
# AI will call: rag_index_workspace with file_path="/path/to/my-awesome-app/main.go"
732+
# AI will call: rag_index_workspace with workspace_root="/path/to/my-awesome-app" (and must submit a second confirmation call via confirm: true)
733733

734734
# 3. Wait for confirmation (usually 1-5 minutes)
735735
# ✓ Indexing started for workspace '/path/to/my-awesome-app'

0 commit comments

Comments
 (0)