From 663696060179ee888240fbf3bf69282603250c68 Mon Sep 17 00:00:00 2001 From: razvan Date: Mon, 9 Feb 2026 18:41:11 +0200 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20implementare=20sistem=20de=20AI?= =?UTF-8?q?=20Skills=20=C8=99i=20auto-update=20inteligent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adăugat sistem de 'embedded skills' folosind go:embed pentru fișiere Markdown (fără hardcoding). - Implementat management de skills (list_skills, install_skill) cu detecție dinamică a metadatelor. - Actualizat sistemul de update cu caching de 24h și verificări non-blocking la tool calls. - Adăugat bibliotecă de 6 skill-uri de bază (Go, Python, Laravel, Debugging, etc.). - Adăugat teste unitare pentru pachetul internal/skills. - Actualizat .gitignore și regulile IDE generate automat pentru a suporta noul sistem. --- .agent/skills/debugging-guide/SKILL.md | 29 +++ .agent/skills/go-best-practices/SKILL.md | 52 +++++ .agent/skills/ragcode-priority/SKILL.md | 137 +++++++++++++ .../examples/search_patterns.md | 184 ++++++++++++++++++ .agent/skills/ragcode-update/SKILL.md | 54 +++++ .gitignore | 1 - cmd/rag-code-mcp/main.go | 38 +++- .../skills/embedded/debugging-guide/SKILL.md | 29 +++ .../embedded/go-best-practices/SKILL.md | 52 +++++ internal/skills/embedded/php-laravel/SKILL.md | 44 +++++ .../embedded/python-best-practices/SKILL.md | 44 +++++ .../skills/embedded/ragcode-priority/SKILL.md | 137 +++++++++++++ .../examples/search_patterns.md | 184 ++++++++++++++++++ .../skills/embedded/ragcode-update/SKILL.md | 54 +++++ internal/skills/skills.go | 116 +++++++++++ internal/skills/skills_test.go | 66 +++++++ internal/tools/install_skill.go | 65 +++++++ internal/tools/list_skills.go | 41 ++++ internal/tools/updates.go | 74 +++++++ internal/updater/updater.go | 121 +++++++++--- 20 files changed, 1495 insertions(+), 27 deletions(-) create mode 100644 .agent/skills/debugging-guide/SKILL.md create mode 100644 .agent/skills/go-best-practices/SKILL.md create mode 100644 .agent/skills/ragcode-priority/SKILL.md create mode 100644 .agent/skills/ragcode-priority/examples/search_patterns.md create mode 100644 .agent/skills/ragcode-update/SKILL.md create mode 100644 internal/skills/embedded/debugging-guide/SKILL.md create mode 100644 internal/skills/embedded/go-best-practices/SKILL.md create mode 100644 internal/skills/embedded/php-laravel/SKILL.md create mode 100644 internal/skills/embedded/python-best-practices/SKILL.md create mode 100644 internal/skills/embedded/ragcode-priority/SKILL.md create mode 100644 internal/skills/embedded/ragcode-priority/examples/search_patterns.md create mode 100644 internal/skills/embedded/ragcode-update/SKILL.md create mode 100644 internal/skills/skills.go create mode 100644 internal/skills/skills_test.go create mode 100644 internal/tools/install_skill.go create mode 100644 internal/tools/list_skills.go create mode 100644 internal/tools/updates.go diff --git a/.agent/skills/debugging-guide/SKILL.md b/.agent/skills/debugging-guide/SKILL.md new file mode 100644 index 0000000..6b5b88c --- /dev/null +++ b/.agent/skills/debugging-guide/SKILL.md @@ -0,0 +1,29 @@ +--- +name: debugging-guide +description: How to use ragcode tools for fast root-cause analysis and debugging +--- + +# 🐞 Skill: Debugging with Ragcode + +This skill defines a workflow for using ragcode tools to fix bugs efficiently. + +--- + +## 🔍 The 3-Step Debug Workflow + +### Step 1: Locate the Symptom +- Search for the exact error message using `hybrid_search`. +- If no error message, search semantically for the failing feature using `search_code`. + +### Step 2: Contextualize the Flow +- Find where the failing function is called using `find_implementations`. +- Look at the surrounding logic with `get_code_context` (fetch at least 10 lines before and after). + +### Step 3: Analyze Data Models +- Use `find_type_definition` to check if a struct field might be missing or wrongly typed. +- Use `list_package_exports` to see if there are utility functions already solving the problem. + +--- + +## 💡 Troubleshooting Pro-Tip: +If you are lost, search for "logger" or "Error" in the package to see how other parts of the system handle failures. diff --git a/.agent/skills/go-best-practices/SKILL.md b/.agent/skills/go-best-practices/SKILL.md new file mode 100644 index 0000000..12e8679 --- /dev/null +++ b/.agent/skills/go-best-practices/SKILL.md @@ -0,0 +1,52 @@ +--- +name: go-best-practices +description: Go development patterns, project structure, and idiomatic practices +--- + +# 🐹 Skill: Go Best Practices + +This skill guides the AI in writing high-quality, idiomatic Go code and understanding Go project structures. + +--- + +## 🔗 External References (Source of Truth) +- [Google Go Style Guide](https://google.github.io/styleguide/go/) +- [Effective Go](https://go.dev/doc/effective_go) +- [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md) + +--- + +## 🏗️ Project Structure +Most Go projects in this ecosystem follow the Standard Go Project Layout: +- `/cmd`: Main applications/binaries. +- `/internal`: Private packages (not importable by other projects). +- `/pkg`: Public packages (rarely used here, prefer `internal`). +- `/api`: API definitions. + +## ✅ Implementation Patterns + +### 1. Error Handling +- **Check immediately**: Never ignore errors. +- **Wrap errors**: Use `fmt.Errorf("context: %w", err)` to provide trace details. +- **Sentinel Errors**: Define custom errors in the package for specific conditions. + +### 2. Interfaces +- **Small is better**: Prefer `io.Reader` over large monolithic interfaces. +- **Consumer defines**: Define interfaces where they are USED, not where they are implemented. + +### 3. Concurrency +- **Use Channels**: For communication/orchestration. +- **Mutex**: For simple state protection. +- **Context**: Always propagate `context.Context` for cancellation and timeouts. + +### 4. Testing +- **Table-Driven Tests**: Use anonymous structs and `t.Run` for multiple test cases. +- **Package name**: Use `package_test` for external testing (test only the public API). +- **Mocking**: Use interfaces to mock dependencies. Avoid complex mocking frameworks if simple stubs work. +- **Helpers**: Use `t.Helper()` for repetitive assertions. + +--- + +## 🔧 Tools for Go Project Analysis: +- `mcp_ragcode_find_type_definition`: Essential for understanding structs and interfaces. +- `mcp_ragcode_list_package_exports`: Quick overview of a package's public API. diff --git a/.agent/skills/ragcode-priority/SKILL.md b/.agent/skills/ragcode-priority/SKILL.md new file mode 100644 index 0000000..1c0f80c --- /dev/null +++ b/.agent/skills/ragcode-priority/SKILL.md @@ -0,0 +1,137 @@ +--- +name: ragcode-priority +description: Prioritize ragcode MCP tools for all code searches in this project +--- + +# 🎯 Skill: Ragcode MCP Priority Usage + +This skill defines **MANDATORY** rules for searching code in the **rag-code-mcp** project. + +--- + +## ✅ MANDATORY RULES + +### 1. Search Hierarchy + +When searching code, ALWAYS follow this order: + +| Priority | Tool | When to Use | +|----------|------|-------------| +| 🥇 **First** | `mcp_ragcode_search_code` | Exploration, understanding concepts, unfamiliar code | +| 🥈 **Second** | `mcp_ragcode_hybrid_search` | Exact match: function names, errors, string literals | +| 🥉 **Third** | `mcp_ragcode_get_function_details` | Complete code of a known function | +| 4️⃣ | `mcp_ragcode_find_type_definition` | Complete struct/interface definition | +| 5️⃣ | `mcp_ragcode_find_implementations` | Where a function is called/implemented | +| 6️⃣ | `mcp_ragcode_list_package_exports` | What a package exports | + +### 2. "Search First" Rule + +**ALWAYS** start with a ragcode search before assuming code structure! + +``` +❌ WRONG: "I know it's in internal/ragcode, I'll open the file directly" +✅ CORRECT: search_code → find exact location → open file +``` + +### 3. Standard Workflow + +``` +Step 1: mcp_ragcode_search_code (understand context) + ↓ +Step 2: mcp_ragcode_get_function_details OR find_type_definition (details) + ↓ +Step 3: view_file (only for specific lines if needed) +``` + +--- + +## ⛔ FORBIDDEN + +### Do NOT use these tools for code search: + +| Tool | Why Forbidden | Exception | +|------|---------------|-----------| +| `grep_search` | No semantic context | ONLY for JavaScript/TypeScript (not indexed yet) | +| `find_by_name` | For files, not content | OK for finding files, not code | +| `view_file` randomly | Waste of time without context | ONLY after ragcode search | + +### Forbidden behaviors: + +1. ❌ **DO NOT assume** code structure without searching +2. ❌ **DO NOT make multiple grep searches** when one ragcode search suffices +3. ❌ **DO NOT navigate manually** through directories to find code +4. ❌ **DO NOT open random files** hoping to find what you need + +--- + +## 🌐 Language Support + +### ✅ Full support (use ONLY ragcode): +- **Go** - complete analyzer, all tools work +- **Python** - complete analyzer +- **PHP/Laravel** - complete analyzer with Laravel support +- **HTML** - basic analyzer + +### ⚠️ Limited support (fallback allowed): +- **JavaScript/TypeScript** - indexing in development + - Try `search_code` FIRST + - If not found, use `grep_search` as backup + +--- + +## 🔧 Recommended Parameters + +### search_code +``` +query: +file_path: /home/razvan/go/src/github.com/doITmagic/rag-code-mcp # optional, for context +limit: 5 # sufficient for most searches +``` + +### hybrid_search +``` +query: +limit: 5 +``` + +### get_function_details +``` +function_name: +package: +``` + +--- + +## 📖 Why ragcode? + +| Aspect | grep_search | ragcode | +|--------|-------------|---------| +| Understands context | ❌ | ✅ | +| Semantic search | ❌ | ✅ | +| Finds similar functions | ❌ | ✅ | +| Returns complete code | ❌ | ✅ | +| Multi-language support | Limited | ✅ | + +**Concrete example:** +- Search "user authentication" with grep: finds only literal string "authentication" +- Search with ragcode: finds login functions, token validation, auth middleware + +--- + +## ⚡ Quick Reference + +``` +🔍 Exploration → search_code +🎯 Exact match → hybrid_search +📝 Function code → get_function_details +🏗️ Type definition → find_type_definition +🔗 Usages → find_implementations +📦 Package contents → list_package_exports +📄 Specific context → get_code_context (with file + lines) +``` + +--- + +## 📚 Examples + +See [examples/search_patterns.md](examples/search_patterns.md) for project-specific examples. diff --git a/.agent/skills/ragcode-priority/examples/search_patterns.md b/.agent/skills/ragcode-priority/examples/search_patterns.md new file mode 100644 index 0000000..85689bb --- /dev/null +++ b/.agent/skills/ragcode-priority/examples/search_patterns.md @@ -0,0 +1,184 @@ +# 📚 Ragcode Search Patterns for rag-code-mcp + +This file contains practical examples for searching the rag-code-mcp codebase. + +--- + +## 🔍 Finding Analyzers + +### Find all language analyzers +``` +mcp_ragcode_search_code + query: "language analyzer implementation CodeAnalyzer" + limit: 10 +``` + +### Find specific language analyzer +``` +# Go analyzer +mcp_ragcode_search_code query: "Go AST parsing analyzer" + +# Python analyzer +mcp_ragcode_search_code query: "Python code analyzer module parsing" + +# PHP/Laravel analyzer +mcp_ragcode_search_code query: "PHP Laravel adapter analyzer" +``` + +### Get analyzer implementation details +``` +mcp_ragcode_get_function_details + function_name: "NewCodeAnalyzer" + package: "golang" +``` + +--- + +## 🏗️ Understanding Types + +### Find Symbol structure +``` +mcp_ragcode_find_type_definition type_name: "Symbol" +``` + +### Find PathAnalyzer interface +``` +mcp_ragcode_find_type_definition type_name: "PathAnalyzer" +``` + +### Find all types in a package +``` +mcp_ragcode_list_package_exports + package: "codetypes" + symbol_type: "type" +``` + +--- + +## 🔗 Finding Usages + +### Where is IndexWorkspace called? +``` +mcp_ragcode_find_implementations symbol_name: "IndexWorkspace" +``` + +### Who implements PathAnalyzer? +``` +mcp_ragcode_find_implementations symbol_name: "PathAnalyzer" +``` + +### Where is Symbol used? +``` +mcp_ragcode_find_implementations symbol_name: "Symbol" +``` + +--- + +## 🎯 Exact Matches + +### Find specific function by name +``` +mcp_ragcode_hybrid_search query: "func IndexWorkspace" +``` + +### Find error handling +``` +mcp_ragcode_hybrid_search query: "return nil, fmt.Errorf" +``` + +### Find specific constant or variable +``` +mcp_ragcode_hybrid_search query: "LanguageGo" +``` + +--- + +## 📦 Exploring Packages + +### What does the ragcode package export? +``` +mcp_ragcode_list_package_exports package: "ragcode" +``` + +### What functions are in workspace package? +``` +mcp_ragcode_list_package_exports + package: "workspace" + symbol_type: "function" +``` + +### What types are in codetypes package? +``` +mcp_ragcode_list_package_exports + package: "codetypes" + symbol_type: "type" +``` + +--- + +## 🔄 Common Workflows + +### Workflow 1: Understanding a feature +``` +# Step 1: Semantic search for the concept +mcp_ragcode_search_code query: "language detection workspace" + +# Step 2: Get the main function details +mcp_ragcode_get_function_details function_name: "DetectLanguages" + +# Step 3: Find where it's used +mcp_ragcode_find_implementations symbol_name: "DetectLanguages" +``` + +### Workflow 2: Debugging an issue +``` +# Step 1: Search for related code +mcp_ragcode_search_code query: "indexing error handling workspace" + +# Step 2: Find the exact error message +mcp_ragcode_hybrid_search query: "workspace not indexed" + +# Step 3: Get context around the error +mcp_ragcode_get_code_context + file_path: "" + start_line: + end_line: +``` + +### Workflow 3: Adding a new feature +``` +# Step 1: Find similar implementations +mcp_ragcode_search_code query: "analyzer implementation for language" + +# Step 2: Understand the interface +mcp_ragcode_find_type_definition type_name: "PathAnalyzer" + +# Step 3: See existing implementations +mcp_ragcode_find_implementations symbol_name: "PathAnalyzer" +``` + +--- + +## ⚠️ JavaScript/TypeScript Fallback + +When searching JS/TS code (not yet fully indexed): + +``` +# Try ragcode first +mcp_ragcode_search_code query: "javascript parser" + +# If no results, fallback to grep +grep_search + SearchPath: "/home/razvan/go/src/github.com/doITmagic/rag-code-mcp" + Query: "javascript" + Includes: ["*.go"] +``` + +--- + +## 📝 Tips + +1. **Be semantic**: Use descriptive queries like "authentication middleware" not just "auth" +2. **Start broad**: Begin with `search_code`, narrow down with specific tools +3. **Use packages**: When function names are ambiguous, specify the package +4. **Check implementations**: Always use `find_implementations` to understand impact before refactoring diff --git a/.agent/skills/ragcode-update/SKILL.md b/.agent/skills/ragcode-update/SKILL.md new file mode 100644 index 0000000..1702f9c --- /dev/null +++ b/.agent/skills/ragcode-update/SKILL.md @@ -0,0 +1,54 @@ +--- +name: ragcode-update +description: Guide for managing ragcode updates, health checks, and AI skills +--- + +# 🛠️ Skill: Ragcode Maintenance & Updates + +This skill instructs the AI on how to keep the `ragcode-mcp` server up to date and healthy. + +--- + +## 🔄 Update Workflow + +### 1. Detection +When you see a notification in the logs like: +`🌟 New version available: X.Y.Z. Run 'apply_update' to upgrade.` + +**You MUST:** +1. Inform the user that a new version is available. +2. Briefly explain that the update includes potential bug fixes or new features. +3. Ask for explicit permission: *"Would you like me to apply the update for you now?"* + +### 2. Manual Check +If the user asks "is there any update?", use the `check_update` tool with `force: true`. + +### 3. Applying Update +When the user says "Yes" or "Apply update": +1. Call `mcp_ragcode_apply_update`. +2. Inform the user that the installation was successful. +3. Remind the user: *"The server needs a restart (or IDE reload) to activate the new version."* + +--- + +## 🧩 Skill Management + +When the user wants to enhance your capabilities: +1. Use `list_skills` to see what is bundled in the binary. +2. Explain what each skill does based on its description. +3. Use `install_skill(skill_id, active=true)` to enable it. + +--- + +## 🏥 Health Checks +If the system feels slow or tools fail consistently: +1. Suggest running the health check (CLI flag `--health`). +2. Check if the workspace is indexed. If not, suggest `index_workspace`. + +--- + +## ⚡ Quick Summary of Tools: +- `check_update`: Check GitHub for new releases. +- `apply_update`: Download and install the latest binary. +- `list_skills`: See available AI behaviors. +- `install_skill`: Toggle a specific behavior on/off. diff --git a/.gitignore b/.gitignore index 5a03b1d..ea8662c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ data/ test_data/ qdrant_data/ barou/ -.agent/ # Binaries /cmd/agent-runner/agent-runner diff --git a/cmd/rag-code-mcp/main.go b/cmd/rag-code-mcp/main.go index 1c3d7c5..b4b0e08 100644 --- a/cmd/rag-code-mcp/main.go +++ b/cmd/rag-code-mcp/main.go @@ -437,7 +437,7 @@ func main() { // Handle update flag if *updateFlag { fmt.Println("Checking for updates...") - info, err := updater.CheckForUpdates(Version) + info, err := updater.CheckForUpdates(Version, true) if err != nil { log.Fatalf("Failed to check for updates: %v", err) } @@ -478,7 +478,7 @@ func main() { // Background update check go func() { - info, err := updater.CheckForUpdates(Version) + info, err := updater.CheckForUpdates(Version, false) if err == nil && info != nil { logger.Info("🌟 New version available: %s. Run 'rag-code-mcp --update' to upgrade.", info.LatestVersion) } @@ -643,6 +643,11 @@ func main() { indexWorkspaceTool := tools.NewIndexWorkspaceTool(workspaceManager) + listSkillsTool := tools.NewListSkillsTool() + installSkillTool := tools.NewInstallSkillTool(workspaceManager) + checkUpdateTool := tools.NewCheckUpdateTool(Version) + applyUpdateTool := tools.NewApplyUpdateTool(Version) + // Example: use typed ToolHandlerFor for search_code registerSearchCodeToolTyped(server, searchTool, cfg) @@ -655,6 +660,10 @@ func main() { registerAgentTool(server, searchDocsTool, cfg) registerAgentTool(server, hybridTool, cfg) registerAgentTool(server, indexWorkspaceTool, cfg) + registerAgentTool(server, listSkillsTool, cfg) + registerAgentTool(server, installSkillTool, cfg) + registerAgentTool(server, checkUpdateTool, cfg) + registerAgentTool(server, applyUpdateTool, cfg) if err := registerFileResources(server); err != nil { log.Fatalf("Failed to register resources: %v", err) @@ -708,6 +717,9 @@ func registerSearchCodeToolTyped(server *mcp.Server, tool *tools.SearchLocalInde logger.Info("✅ Tool '%s' completed in %v", tool.Name(), duration) + // Trigger background update check (non-blocking) + triggerBackgroundUpdateCheck() + return nil, SearchCodeOutput{Results: result}, nil }) } @@ -751,6 +763,9 @@ func registerAgentTool(server *mcp.Server, tool MCPTool, cfg *config.Config) { logger.Info("✅ Tool '%s' completed in %v", tool.Name(), duration) + // Trigger background update check (non-blocking) + triggerBackgroundUpdateCheck() + return &mcp.CallToolResult{ Content: []mcp.Content{ &mcp.TextContent{Text: result}, @@ -1252,6 +1267,7 @@ func ensureIDERules(cfg *config.Config, filePath string) { - Always provide 'file_path' to tools to ensure they detect the correct project context. - Use 'hybrid_search' if looking for exact variable names or error messages. - If the tool says "workspace not indexed", use 'index_workspace' once. +- **Skills System**: Use 'list_skills' to see available AI behaviors and 'install_skill' to enable them in this workspace (e.g., 'ragcode-priority', 'ragcode-update'). ` // 3. Define target rule files @@ -1287,3 +1303,21 @@ func ensureIDERules(cfg *config.Config, filePath string) { } } } + +var lastUpdateCheck time.Time + +func triggerBackgroundUpdateCheck() { + // Only check if more than 1 hour passed since last check in THIS session + // to avoid spamming go-routines, while updater.CheckForUpdates handles the 24h logic + if time.Since(lastUpdateCheck) < 1*time.Hour { + return + } + lastUpdateCheck = time.Now() + + go func() { + info, _ := updater.CheckForUpdates(Version, false) + if info != nil { + logger.Info("🌟 New version available: %s. Run 'apply_update' to upgrade.", info.LatestVersion) + } + }() +} diff --git a/internal/skills/embedded/debugging-guide/SKILL.md b/internal/skills/embedded/debugging-guide/SKILL.md new file mode 100644 index 0000000..6b5b88c --- /dev/null +++ b/internal/skills/embedded/debugging-guide/SKILL.md @@ -0,0 +1,29 @@ +--- +name: debugging-guide +description: How to use ragcode tools for fast root-cause analysis and debugging +--- + +# 🐞 Skill: Debugging with Ragcode + +This skill defines a workflow for using ragcode tools to fix bugs efficiently. + +--- + +## 🔍 The 3-Step Debug Workflow + +### Step 1: Locate the Symptom +- Search for the exact error message using `hybrid_search`. +- If no error message, search semantically for the failing feature using `search_code`. + +### Step 2: Contextualize the Flow +- Find where the failing function is called using `find_implementations`. +- Look at the surrounding logic with `get_code_context` (fetch at least 10 lines before and after). + +### Step 3: Analyze Data Models +- Use `find_type_definition` to check if a struct field might be missing or wrongly typed. +- Use `list_package_exports` to see if there are utility functions already solving the problem. + +--- + +## 💡 Troubleshooting Pro-Tip: +If you are lost, search for "logger" or "Error" in the package to see how other parts of the system handle failures. diff --git a/internal/skills/embedded/go-best-practices/SKILL.md b/internal/skills/embedded/go-best-practices/SKILL.md new file mode 100644 index 0000000..12e8679 --- /dev/null +++ b/internal/skills/embedded/go-best-practices/SKILL.md @@ -0,0 +1,52 @@ +--- +name: go-best-practices +description: Go development patterns, project structure, and idiomatic practices +--- + +# 🐹 Skill: Go Best Practices + +This skill guides the AI in writing high-quality, idiomatic Go code and understanding Go project structures. + +--- + +## 🔗 External References (Source of Truth) +- [Google Go Style Guide](https://google.github.io/styleguide/go/) +- [Effective Go](https://go.dev/doc/effective_go) +- [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md) + +--- + +## 🏗️ Project Structure +Most Go projects in this ecosystem follow the Standard Go Project Layout: +- `/cmd`: Main applications/binaries. +- `/internal`: Private packages (not importable by other projects). +- `/pkg`: Public packages (rarely used here, prefer `internal`). +- `/api`: API definitions. + +## ✅ Implementation Patterns + +### 1. Error Handling +- **Check immediately**: Never ignore errors. +- **Wrap errors**: Use `fmt.Errorf("context: %w", err)` to provide trace details. +- **Sentinel Errors**: Define custom errors in the package for specific conditions. + +### 2. Interfaces +- **Small is better**: Prefer `io.Reader` over large monolithic interfaces. +- **Consumer defines**: Define interfaces where they are USED, not where they are implemented. + +### 3. Concurrency +- **Use Channels**: For communication/orchestration. +- **Mutex**: For simple state protection. +- **Context**: Always propagate `context.Context` for cancellation and timeouts. + +### 4. Testing +- **Table-Driven Tests**: Use anonymous structs and `t.Run` for multiple test cases. +- **Package name**: Use `package_test` for external testing (test only the public API). +- **Mocking**: Use interfaces to mock dependencies. Avoid complex mocking frameworks if simple stubs work. +- **Helpers**: Use `t.Helper()` for repetitive assertions. + +--- + +## 🔧 Tools for Go Project Analysis: +- `mcp_ragcode_find_type_definition`: Essential for understanding structs and interfaces. +- `mcp_ragcode_list_package_exports`: Quick overview of a package's public API. diff --git a/internal/skills/embedded/php-laravel/SKILL.md b/internal/skills/embedded/php-laravel/SKILL.md new file mode 100644 index 0000000..884e189 --- /dev/null +++ b/internal/skills/embedded/php-laravel/SKILL.md @@ -0,0 +1,44 @@ +--- +name: php-laravel +description: PHP and Laravel specific patterns, Service Providers, and Eloquent practices +--- + +# 🐘 Skill: PHP & Laravel Patterns + +Guidelines for working with PHP projects, specifically those using the Laravel framework. + +--- + +## 🔗 External References (Source of Truth) +- [Laravel Official Documentation](https://laravel.com/docs) +- [PHP-FIG PEPs (Standards)](https://www.php-fig.org/psr/) +- [Spatie's Guidelines](https://guidelines.spatie.be/) + +--- + +## 🏰 Laravel Architecture + +### 1. Service Providers +- All core logic should be registered in Service Providers. +- Check `app/Providers` for dependency injection setup. + +### 2. Eloquent Models +- Models belong in `app/Models`. +- Use Scopes for reusable query logic. +- Always check for Relationships (hasMany, belongsTo). + +### 3. Controllers & Requests +- Keep controllers thin. Move validation to `FormRequest` classes. +- Move complex logic to `Services` or `Actions`. + +### 4. Testing +- **Pest OR PHPUnit**: Use Pest for a modern API or PHPUnit for classic suites. +- **Feature Tests**: Test behavior from the user's perspective (Routes + DB). +- **Unit Tests**: For pure logic outside the Laravel container. +- **RefreshDatabase**: Use traits to keep tests isolated. + +--- + +## 🔧 Tool Usage: +- `mcp_ragcode_hybrid_search`: Great for finding specific Eloquent model names or Route definitions. +- `mcp_ragcode_list_package_exports`: Use for listing methods in a Laravel Class. diff --git a/internal/skills/embedded/python-best-practices/SKILL.md b/internal/skills/embedded/python-best-practices/SKILL.md new file mode 100644 index 0000000..0384e1d --- /dev/null +++ b/internal/skills/embedded/python-best-practices/SKILL.md @@ -0,0 +1,44 @@ +--- +name: python-best-practices +description: Python patterns, type hinting, and project standards +--- + +# 🐍 Skill: Python Best Practices + +This skill provides directions for writing robust and maintainable Python code. + +--- + +## 🔗 External References (Source of Truth) +- [PEP 8 - Style Guide for Python](https://peps.python.org/pep-0008/) +- [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html) +- [Real Python Best Practices](https://realpython.com/python-best-practices/) + +--- + +## 💎 Clean Code Patterns + +### 1. Type Hinting +- Always use `typing` module for function signatures. +- Prefer `list[str]` (Python 3.9+) or `List[str]`. + +### 2. Async usage +- Use `asyncio` for I/O bound tasks. +- Ensure `await` is used correctly with async functions. + +### 3. Docstrings +- Use Google or NumPy style docstrings for classes and public methods. + +### 4. Testing +- **pytest**: Use `pytest` for its powerful fixture system. +- **Hypothesis**: Use for property-based testing if needed. +- **Mocking**: Use `unittest.mock` to isolate components. +- **Coverage**: Aim for high coverage in business logic. + +## 📦 Dependency Management +- Look for `pyproject.toml`, `requirements.txt`, or `setup.py`. + +--- + +## 🔧 Search Tips for Python: +- Use `mcp_ragcode_search_code` with queries like "Python class definition" or "decorator implementation". diff --git a/internal/skills/embedded/ragcode-priority/SKILL.md b/internal/skills/embedded/ragcode-priority/SKILL.md new file mode 100644 index 0000000..1c0f80c --- /dev/null +++ b/internal/skills/embedded/ragcode-priority/SKILL.md @@ -0,0 +1,137 @@ +--- +name: ragcode-priority +description: Prioritize ragcode MCP tools for all code searches in this project +--- + +# 🎯 Skill: Ragcode MCP Priority Usage + +This skill defines **MANDATORY** rules for searching code in the **rag-code-mcp** project. + +--- + +## ✅ MANDATORY RULES + +### 1. Search Hierarchy + +When searching code, ALWAYS follow this order: + +| Priority | Tool | When to Use | +|----------|------|-------------| +| 🥇 **First** | `mcp_ragcode_search_code` | Exploration, understanding concepts, unfamiliar code | +| 🥈 **Second** | `mcp_ragcode_hybrid_search` | Exact match: function names, errors, string literals | +| 🥉 **Third** | `mcp_ragcode_get_function_details` | Complete code of a known function | +| 4️⃣ | `mcp_ragcode_find_type_definition` | Complete struct/interface definition | +| 5️⃣ | `mcp_ragcode_find_implementations` | Where a function is called/implemented | +| 6️⃣ | `mcp_ragcode_list_package_exports` | What a package exports | + +### 2. "Search First" Rule + +**ALWAYS** start with a ragcode search before assuming code structure! + +``` +❌ WRONG: "I know it's in internal/ragcode, I'll open the file directly" +✅ CORRECT: search_code → find exact location → open file +``` + +### 3. Standard Workflow + +``` +Step 1: mcp_ragcode_search_code (understand context) + ↓ +Step 2: mcp_ragcode_get_function_details OR find_type_definition (details) + ↓ +Step 3: view_file (only for specific lines if needed) +``` + +--- + +## ⛔ FORBIDDEN + +### Do NOT use these tools for code search: + +| Tool | Why Forbidden | Exception | +|------|---------------|-----------| +| `grep_search` | No semantic context | ONLY for JavaScript/TypeScript (not indexed yet) | +| `find_by_name` | For files, not content | OK for finding files, not code | +| `view_file` randomly | Waste of time without context | ONLY after ragcode search | + +### Forbidden behaviors: + +1. ❌ **DO NOT assume** code structure without searching +2. ❌ **DO NOT make multiple grep searches** when one ragcode search suffices +3. ❌ **DO NOT navigate manually** through directories to find code +4. ❌ **DO NOT open random files** hoping to find what you need + +--- + +## 🌐 Language Support + +### ✅ Full support (use ONLY ragcode): +- **Go** - complete analyzer, all tools work +- **Python** - complete analyzer +- **PHP/Laravel** - complete analyzer with Laravel support +- **HTML** - basic analyzer + +### ⚠️ Limited support (fallback allowed): +- **JavaScript/TypeScript** - indexing in development + - Try `search_code` FIRST + - If not found, use `grep_search` as backup + +--- + +## 🔧 Recommended Parameters + +### search_code +``` +query: +file_path: /home/razvan/go/src/github.com/doITmagic/rag-code-mcp # optional, for context +limit: 5 # sufficient for most searches +``` + +### hybrid_search +``` +query: +limit: 5 +``` + +### get_function_details +``` +function_name: +package: +``` + +--- + +## 📖 Why ragcode? + +| Aspect | grep_search | ragcode | +|--------|-------------|---------| +| Understands context | ❌ | ✅ | +| Semantic search | ❌ | ✅ | +| Finds similar functions | ❌ | ✅ | +| Returns complete code | ❌ | ✅ | +| Multi-language support | Limited | ✅ | + +**Concrete example:** +- Search "user authentication" with grep: finds only literal string "authentication" +- Search with ragcode: finds login functions, token validation, auth middleware + +--- + +## ⚡ Quick Reference + +``` +🔍 Exploration → search_code +🎯 Exact match → hybrid_search +📝 Function code → get_function_details +🏗️ Type definition → find_type_definition +🔗 Usages → find_implementations +📦 Package contents → list_package_exports +📄 Specific context → get_code_context (with file + lines) +``` + +--- + +## 📚 Examples + +See [examples/search_patterns.md](examples/search_patterns.md) for project-specific examples. diff --git a/internal/skills/embedded/ragcode-priority/examples/search_patterns.md b/internal/skills/embedded/ragcode-priority/examples/search_patterns.md new file mode 100644 index 0000000..85689bb --- /dev/null +++ b/internal/skills/embedded/ragcode-priority/examples/search_patterns.md @@ -0,0 +1,184 @@ +# 📚 Ragcode Search Patterns for rag-code-mcp + +This file contains practical examples for searching the rag-code-mcp codebase. + +--- + +## 🔍 Finding Analyzers + +### Find all language analyzers +``` +mcp_ragcode_search_code + query: "language analyzer implementation CodeAnalyzer" + limit: 10 +``` + +### Find specific language analyzer +``` +# Go analyzer +mcp_ragcode_search_code query: "Go AST parsing analyzer" + +# Python analyzer +mcp_ragcode_search_code query: "Python code analyzer module parsing" + +# PHP/Laravel analyzer +mcp_ragcode_search_code query: "PHP Laravel adapter analyzer" +``` + +### Get analyzer implementation details +``` +mcp_ragcode_get_function_details + function_name: "NewCodeAnalyzer" + package: "golang" +``` + +--- + +## 🏗️ Understanding Types + +### Find Symbol structure +``` +mcp_ragcode_find_type_definition type_name: "Symbol" +``` + +### Find PathAnalyzer interface +``` +mcp_ragcode_find_type_definition type_name: "PathAnalyzer" +``` + +### Find all types in a package +``` +mcp_ragcode_list_package_exports + package: "codetypes" + symbol_type: "type" +``` + +--- + +## 🔗 Finding Usages + +### Where is IndexWorkspace called? +``` +mcp_ragcode_find_implementations symbol_name: "IndexWorkspace" +``` + +### Who implements PathAnalyzer? +``` +mcp_ragcode_find_implementations symbol_name: "PathAnalyzer" +``` + +### Where is Symbol used? +``` +mcp_ragcode_find_implementations symbol_name: "Symbol" +``` + +--- + +## 🎯 Exact Matches + +### Find specific function by name +``` +mcp_ragcode_hybrid_search query: "func IndexWorkspace" +``` + +### Find error handling +``` +mcp_ragcode_hybrid_search query: "return nil, fmt.Errorf" +``` + +### Find specific constant or variable +``` +mcp_ragcode_hybrid_search query: "LanguageGo" +``` + +--- + +## 📦 Exploring Packages + +### What does the ragcode package export? +``` +mcp_ragcode_list_package_exports package: "ragcode" +``` + +### What functions are in workspace package? +``` +mcp_ragcode_list_package_exports + package: "workspace" + symbol_type: "function" +``` + +### What types are in codetypes package? +``` +mcp_ragcode_list_package_exports + package: "codetypes" + symbol_type: "type" +``` + +--- + +## 🔄 Common Workflows + +### Workflow 1: Understanding a feature +``` +# Step 1: Semantic search for the concept +mcp_ragcode_search_code query: "language detection workspace" + +# Step 2: Get the main function details +mcp_ragcode_get_function_details function_name: "DetectLanguages" + +# Step 3: Find where it's used +mcp_ragcode_find_implementations symbol_name: "DetectLanguages" +``` + +### Workflow 2: Debugging an issue +``` +# Step 1: Search for related code +mcp_ragcode_search_code query: "indexing error handling workspace" + +# Step 2: Find the exact error message +mcp_ragcode_hybrid_search query: "workspace not indexed" + +# Step 3: Get context around the error +mcp_ragcode_get_code_context + file_path: "" + start_line: + end_line: +``` + +### Workflow 3: Adding a new feature +``` +# Step 1: Find similar implementations +mcp_ragcode_search_code query: "analyzer implementation for language" + +# Step 2: Understand the interface +mcp_ragcode_find_type_definition type_name: "PathAnalyzer" + +# Step 3: See existing implementations +mcp_ragcode_find_implementations symbol_name: "PathAnalyzer" +``` + +--- + +## ⚠️ JavaScript/TypeScript Fallback + +When searching JS/TS code (not yet fully indexed): + +``` +# Try ragcode first +mcp_ragcode_search_code query: "javascript parser" + +# If no results, fallback to grep +grep_search + SearchPath: "/home/razvan/go/src/github.com/doITmagic/rag-code-mcp" + Query: "javascript" + Includes: ["*.go"] +``` + +--- + +## 📝 Tips + +1. **Be semantic**: Use descriptive queries like "authentication middleware" not just "auth" +2. **Start broad**: Begin with `search_code`, narrow down with specific tools +3. **Use packages**: When function names are ambiguous, specify the package +4. **Check implementations**: Always use `find_implementations` to understand impact before refactoring diff --git a/internal/skills/embedded/ragcode-update/SKILL.md b/internal/skills/embedded/ragcode-update/SKILL.md new file mode 100644 index 0000000..1702f9c --- /dev/null +++ b/internal/skills/embedded/ragcode-update/SKILL.md @@ -0,0 +1,54 @@ +--- +name: ragcode-update +description: Guide for managing ragcode updates, health checks, and AI skills +--- + +# 🛠️ Skill: Ragcode Maintenance & Updates + +This skill instructs the AI on how to keep the `ragcode-mcp` server up to date and healthy. + +--- + +## 🔄 Update Workflow + +### 1. Detection +When you see a notification in the logs like: +`🌟 New version available: X.Y.Z. Run 'apply_update' to upgrade.` + +**You MUST:** +1. Inform the user that a new version is available. +2. Briefly explain that the update includes potential bug fixes or new features. +3. Ask for explicit permission: *"Would you like me to apply the update for you now?"* + +### 2. Manual Check +If the user asks "is there any update?", use the `check_update` tool with `force: true`. + +### 3. Applying Update +When the user says "Yes" or "Apply update": +1. Call `mcp_ragcode_apply_update`. +2. Inform the user that the installation was successful. +3. Remind the user: *"The server needs a restart (or IDE reload) to activate the new version."* + +--- + +## 🧩 Skill Management + +When the user wants to enhance your capabilities: +1. Use `list_skills` to see what is bundled in the binary. +2. Explain what each skill does based on its description. +3. Use `install_skill(skill_id, active=true)` to enable it. + +--- + +## 🏥 Health Checks +If the system feels slow or tools fail consistently: +1. Suggest running the health check (CLI flag `--health`). +2. Check if the workspace is indexed. If not, suggest `index_workspace`. + +--- + +## ⚡ Quick Summary of Tools: +- `check_update`: Check GitHub for new releases. +- `apply_update`: Download and install the latest binary. +- `list_skills`: See available AI behaviors. +- `install_skill`: Toggle a specific behavior on/off. diff --git a/internal/skills/skills.go b/internal/skills/skills.go new file mode 100644 index 0000000..b108ca4 --- /dev/null +++ b/internal/skills/skills.go @@ -0,0 +1,116 @@ +package skills + +import ( + "embed" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "gopkg.in/yaml.v3" +) + +//go:embed embedded/* +var embeddedSkills embed.FS + +// SkillInfo holds the metadata of an embedded skill +type SkillInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` +} + +// ListAvailableSkills scans the embedded filesystem for skills and extracts metadata from SKILL.md +func ListAvailableSkills() ([]SkillInfo, error) { + var available []SkillInfo + + entries, err := embeddedSkills.ReadDir("embedded") + if err != nil { + return nil, fmt.Errorf("failed to read embedded skills: %w", err) + } + + for _, entry := range entries { + if !entry.IsDir() { + continue + } + + skillID := entry.Name() + info, err := GetSkillMetadata(skillID) + if err != nil { + // Skip skills with invalid metadata but log or handle as needed + continue + } + available = append(available, info) + } + + return available, nil +} + +// GetSkillMetadata extracts metadata from the SKILL.md of a specific skill +func GetSkillMetadata(skillID string) (SkillInfo, error) { + filePath := fmt.Sprintf("embedded/%s/SKILL.md", skillID) + content, err := embeddedSkills.ReadFile(filePath) + if err != nil { + return SkillInfo{}, fmt.Errorf("failed to read SKILL.md for %s: %w", skillID, err) + } + + // Basic parsing of YAML frontmatter + parts := strings.Split(string(content), "---") + if len(parts) < 3 { + return SkillInfo{}, fmt.Errorf("invalid frontmatter in SKILL.md for %s", skillID) + } + + var metadata struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + } + + if err := yaml.Unmarshal([]byte(parts[1]), &metadata); err != nil { + return SkillInfo{}, fmt.Errorf("failed to parse metadata for %s: %w", skillID, err) + } + + return SkillInfo{ + ID: skillID, + Name: metadata.Name, + Description: metadata.Description, + }, nil +} + +// InstallSkill copies all files of an embedded skill to the destination directory +func InstallSkill(skillID string, workspaceRoot string) error { + destDir := filepath.Join(workspaceRoot, ".agent", "skills", skillID) + srcDir := "embedded/" + skillID + + return fs.WalkDir(embeddedSkills, srcDir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // Calculate relative path from skill root + relPath, err := filepath.Rel(srcDir, path) + if err != nil { + return err + } + + targetPath := filepath.Join(destDir, relPath) + + if d.IsDir() { + return os.MkdirAll(targetPath, 0755) + } + + // Read from embed and write to disk + content, err := embeddedSkills.ReadFile(path) + if err != nil { + return err + } + + return os.WriteFile(targetPath, content, 0644) + }) +} + +// UninstallSkill removes a skill from the workspace +func UninstallSkill(skillID string, workspaceRoot string) error { + destDir := filepath.Join(workspaceRoot, ".agent", "skills", skillID) + return os.RemoveAll(destDir) +} diff --git a/internal/skills/skills_test.go b/internal/skills/skills_test.go new file mode 100644 index 0000000..4f72ea6 --- /dev/null +++ b/internal/skills/skills_test.go @@ -0,0 +1,66 @@ +package skills + +import ( + "os" + "path/filepath" + "testing" +) + +func TestListAvailableSkills(t *testing.T) { + available, err := ListAvailableSkills() + if err != nil { + t.Fatalf("ListAvailableSkills failed: %v", err) + } + + if len(available) == 0 { + t.Error("Expected at least one available skill, got none") + } + + // Verify we found our priority skill + foundPriority := false + for _, skill := range available { + if skill.ID == "ragcode-priority" { + foundPriority = true + if skill.Name == "" || skill.Description == "" { + t.Errorf("Skill %s has empty name or description", skill.ID) + } + } + } + + if !foundPriority { + t.Error("Skill 'ragcode-priority' not found in available skills") + } +} + +func TestInstallAndUninstallSkill(t *testing.T) { + tempDir, err := os.MkdirTemp("", "skill-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tempDir) + + skillID := "ragcode-priority" + + // Test Installation + err = InstallSkill(skillID, tempDir) + if err != nil { + t.Fatalf("InstallSkill failed: %v", err) + } + + // Verify file existence + skillFile := filepath.Join(tempDir, ".agent", "skills", skillID, "SKILL.md") + if _, err := os.Stat(skillFile); os.IsNotExist(err) { + t.Errorf("Skill file %s was not created", skillFile) + } + + // Test Uninstallation + err = UninstallSkill(skillID, tempDir) + if err != nil { + t.Fatalf("UninstallSkill failed: %v", err) + } + + // Verify file removal + if _, err := os.Stat(filepath.Dir(skillFile)); !os.IsNotExist(err) { + t.Errorf("Skill directory %s still exists after uninstallation", filepath.Dir(skillFile)) + } +} diff --git a/internal/tools/install_skill.go b/internal/tools/install_skill.go new file mode 100644 index 0000000..9e76c01 --- /dev/null +++ b/internal/tools/install_skill.go @@ -0,0 +1,65 @@ +package tools + +import ( + "context" + "fmt" + + "github.com/doITmagic/rag-code-mcp/internal/skills" + "github.com/doITmagic/rag-code-mcp/internal/workspace" +) + +type InstallSkillTool struct { + workspaceManager *workspace.Manager +} + +func NewInstallSkillTool(workspaceManager *workspace.Manager) *InstallSkillTool { + return &InstallSkillTool{ + workspaceManager: workspaceManager, + } +} + +func (t *InstallSkillTool) Name() string { + return "install_skill" +} + +func (t *InstallSkillTool) Description() string { + return "Installs or uninstalls an AI skill to/from the current workspace. Param 'active' (bool) determines if it should be installed (true) or removed (false)." +} + +func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) { + active, ok := args["active"].(bool) + if !ok { + return "", fmt.Errorf("parameter 'active' (boolean) is required") + } + + skillID, ok := args["skill_id"].(string) + if !ok || skillID == "" { + return "", fmt.Errorf("parameter 'skill_id' is required") + } + + // Detect workspace to know where to install the skill + workspaceInfo, err := t.workspaceManager.DetectWorkspace(args) + if err != nil { + return "", fmt.Errorf("could not detect workspace for skill installation: %w", err) + } + + workspaceRoot := workspaceInfo.Root + + if active { + err = skills.InstallSkill(skillID, workspaceRoot) + if err != nil { + return "", fmt.Errorf("failed to install skill %s: %w", skillID, err) + } + return fmt.Sprintf("✅ Skill '%s' has been successfully installed in %s/.agent/skills/%s", skillID, workspaceRoot, skillID), nil + } else { + err = skills.UninstallSkill(skillID, workspaceRoot) + if err != nil { + return "", fmt.Errorf("failed to uninstall skill %s: %w", skillID, err) + } + return fmt.Sprintf("🗑️ Skill '%s' has been removed from %s", skillID, workspaceRoot), nil + } +} + +func (t *InstallSkillTool) SetWorkspaceManager(m *workspace.Manager) { + t.workspaceManager = m +} diff --git a/internal/tools/list_skills.go b/internal/tools/list_skills.go new file mode 100644 index 0000000..54911cc --- /dev/null +++ b/internal/tools/list_skills.go @@ -0,0 +1,41 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/doITmagic/rag-code-mcp/internal/skills" +) + +type ListSkillsTool struct{} + +func NewListSkillsTool() *ListSkillsTool { + return &ListSkillsTool{} +} + +func (t *ListSkillsTool) Name() string { + return "list_skills" +} + +func (t *ListSkillsTool) Description() string { + return "Lists all available AI skills bundled within the ragcode binary. These skills can be installed to help the AI better understand the project using ragcode tools." +} + +func (t *ListSkillsTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) { + available, err := skills.ListAvailableSkills() + if err != nil { + return "", fmt.Errorf("failed to list skills: %w", err) + } + + if len(available) == 0 { + return "No skills found in the binary.", nil + } + + data, err := json.MarshalIndent(available, "", " ") + if err != nil { + return "", fmt.Errorf("failed to format skills list: %w", err) + } + + return string(data), nil +} diff --git a/internal/tools/updates.go b/internal/tools/updates.go new file mode 100644 index 0000000..d07fd6d --- /dev/null +++ b/internal/tools/updates.go @@ -0,0 +1,74 @@ +package tools + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/doITmagic/rag-code-mcp/internal/updater" +) + +type CheckUpdateTool struct { + version string +} + +func NewCheckUpdateTool(version string) *CheckUpdateTool { + return &CheckUpdateTool{version: version} +} + +func (t *CheckUpdateTool) Name() string { return "check_update" } +func (t *CheckUpdateTool) Description() string { + return "Checks for available ragcode-mcp updates on GitHub. Returns version info and release notes if a new version is available." +} + +func (t *CheckUpdateTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) { + force := false + if f, ok := args["force"].(bool); ok { + force = f + } + info, err := updater.CheckForUpdates(t.version, force) + if err != nil { + return "", fmt.Errorf("failed to check for updates: %w", err) + } + + if info == nil { + return fmt.Sprintf("✅ You are using the latest version (%s).", t.version), nil + } + + return fmt.Sprintf("🌟 New version available: %s\nRun 'apply_update' to upgrade.", info.LatestVersion), nil +} + +type ApplyUpdateTool struct { + version string +} + +func NewApplyUpdateTool(version string) *ApplyUpdateTool { + return &ApplyUpdateTool{version: version} +} + +func (t *ApplyUpdateTool) Name() string { return "apply_update" } +func (t *ApplyUpdateTool) Description() string { + return "Downloads and installs the latest version of ragcode-mcp. The server will need to be restarted after completion." +} + +func (t *ApplyUpdateTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) { + info, err := updater.CheckForUpdates(t.version, true) + if err != nil { + return "", err + } + if info == nil { + return "✅ You are already on the latest version.", nil + } + + tempFile := filepath.Join(os.TempDir(), "ragcode_update.tar.gz") + if err := info.DownloadAndVerify(tempFile); err != nil { + return "", fmt.Errorf("failed to download update: %w", err) + } + + if err := updater.ApplyUpdate(tempFile); err != nil { + return "", fmt.Errorf("failed to install update: %w", err) + } + + return fmt.Sprintf("✅ Successfully updated to %s! Please restart your IDE or MCP command to use the new version.", info.LatestVersion), nil +} diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 6a8ddff..fa9653f 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -13,10 +13,60 @@ import ( "path/filepath" "runtime" "strings" + "time" "github.com/Masterminds/semver/v3" ) +type UpdateCache struct { + LastCheck time.Time `json:"last_check"` + LatestVersion string `json:"latest_version"` + UpdateDetails *UpdateInfo `json:"update_details,omitempty"` +} + +var ( + cacheFile = "update_cache.json" +) + +func getCachePath() string { + home, err := os.UserHomeDir() + if err != nil { + return cacheFile // Fallback to CWD + } + configDir := filepath.Join(home, ".config", "rag-code-mcp") + _ = os.MkdirAll(configDir, 0755) + return filepath.Join(configDir, cacheFile) +} + +func GetCachedUpdate() (*UpdateCache, error) { + path := getCachePath() + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var cache UpdateCache + if err := json.Unmarshal(data, &cache); err != nil { + return nil, err + } + return &cache, nil +} + +func SaveUpdateCache(info *UpdateInfo) error { + cache := UpdateCache{ + LastCheck: time.Now(), + } + if info != nil { + cache.LatestVersion = info.LatestVersion + cache.UpdateDetails = info + } + + data, err := json.Marshal(cache) + if err != nil { + return err + } + return os.WriteFile(getCachePath(), data, 0644) +} + const ( GitHubOwner = "doITmagic" GitHubRepo = "rag-code-mcp" @@ -30,11 +80,32 @@ type UpdateInfo struct { } // CheckForUpdates queries GitHub for the latest release and compares it with the current version. -func CheckForUpdates(currentVersion string) (*UpdateInfo, error) { +// If force is false, it returns cached results if available and less than 24 hours old. +func CheckForUpdates(currentVersion string, force bool) (*UpdateInfo, error) { if currentVersion == "" || currentVersion == "dev" { return nil, nil // Skip checks for dev versions } + if !force { + cache, err := GetCachedUpdate() + if err == nil && cache != nil { + // Check if cache is fresh (24h) + if time.Since(cache.LastCheck) < 24*time.Hour { + // Still need to compare versions because currentVersion might have changed + if cache.UpdateDetails != nil { + curr, err := semver.NewVersion(currentVersion) + if err == nil { + latest, err := semver.NewVersion(cache.UpdateDetails.LatestVersion) + if err == nil && latest.GreaterThan(curr) { + return cache.UpdateDetails, nil + } + } + } + return nil, nil // No new version in cache or already on latest + } + } + } + curr, err := semver.NewVersion(currentVersion) if err != nil { return nil, fmt.Errorf("invalid current version %q: %w", currentVersion, err) @@ -68,35 +139,37 @@ func CheckForUpdates(currentVersion string) (*UpdateInfo, error) { return nil, fmt.Errorf("invalid latest version %q: %w", release.TagName, err) } - if !latest.GreaterThan(curr) { - return nil, nil // No update needed - } - - info := &UpdateInfo{ - LatestVersion: latest.String(), - Tag: release.TagName, - } + var info *UpdateInfo + if latest.GreaterThan(curr) { + info = &UpdateInfo{ + LatestVersion: latest.String(), + Tag: release.TagName, + } - // Match asset for current platform - archiveName := fmt.Sprintf("rag-code-mcp_%s_%s", runtime.GOOS, runtime.GOARCH) - if runtime.GOOS == "windows" { - archiveName += ".zip" - } else { - archiveName += ".tar.gz" - } + // Match asset for current platform + archiveName := fmt.Sprintf("rag-code-mcp_%s_%s", runtime.GOOS, runtime.GOARCH) + if runtime.GOOS == "windows" { + archiveName += ".zip" + } else { + archiveName += ".tar.gz" + } - for _, asset := range release.Assets { - if asset.Name == archiveName { - info.AssetURL = asset.BrowserDownloadURL + for _, asset := range release.Assets { + if asset.Name == archiveName { + info.AssetURL = asset.BrowserDownloadURL + } + if asset.Name == "checksums.txt" { + info.ChecksumURL = asset.BrowserDownloadURL + } } - if asset.Name == "checksums.txt" { - info.ChecksumURL = asset.BrowserDownloadURL + + if info.AssetURL == "" { + return nil, fmt.Errorf("no asset found for platform %s/%s", runtime.GOOS, runtime.GOARCH) } } - if info.AssetURL == "" { - return nil, fmt.Errorf("no asset found for platform %s/%s", runtime.GOOS, runtime.GOARCH) - } + // Always save cache after successful network call, even if info is nil (meaning we are on latest) + _ = SaveUpdateCache(info) return info, nil } From 2c00a13faa374fea53449fe9881d876c890bcfce Mon Sep 17 00:00:00 2001 From: razvan Date: Mon, 9 Feb 2026 22:37:15 +0200 Subject: [PATCH 02/13] fix: use consistent install path constant and improve tool docs --- cmd/install/main.go | 4 ++-- internal/tools/install_skill.go | 13 +++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/cmd/install/main.go b/cmd/install/main.go index cf2a838..8b5884b 100644 --- a/cmd/install/main.go +++ b/cmd/install/main.go @@ -511,9 +511,9 @@ func installBinary() { } var binDir string if runtime.GOOS == "windows" { - binDir = filepath.Join(home, ".local", "share", "ragcode", "bin") + binDir = filepath.Join(home, installDirName, "bin") } else { - binDir = filepath.Join(home, ".local", "share", "ragcode", "bin") + binDir = filepath.Join(home, installDirName, "bin") } if err := os.MkdirAll(binDir, 0755); err != nil { fail(fmt.Sprintf("Could not create bin directory: %v", err)) diff --git a/internal/tools/install_skill.go b/internal/tools/install_skill.go index 9e76c01..ba78fc4 100644 --- a/internal/tools/install_skill.go +++ b/internal/tools/install_skill.go @@ -23,7 +23,14 @@ func (t *InstallSkillTool) Name() string { } func (t *InstallSkillTool) Description() string { - return "Installs or uninstalls an AI skill to/from the current workspace. Param 'active' (bool) determines if it should be installed (true) or removed (false)." + return `Installs or uninstalls an AI skill to/from the current workspace. +Param 'skill_id' is the unique identifier of the skill (e.g., 'go-best-practices', 'ragcode-priority'). +Param 'active' (bool) determines if it should be installed (true) or removed (false). +Param 'file_path' (string) is HIGHLY RECOMMENDED to help detect the correct workspace root. + +EXAMPLES: +- mcp_ragcode_install_skill(skill_id="go-best-practices", active=true, file_path="/path/to/project/go.mod") +- mcp_ragcode_install_skill(skill_id="ragcode-priority", active=true, file_path="/path/to/project/README.md")` } func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) { @@ -40,7 +47,9 @@ func (t *InstallSkillTool) Execute(ctx context.Context, args map[string]interfac // Detect workspace to know where to install the skill workspaceInfo, err := t.workspaceManager.DetectWorkspace(args) if err != nil { - return "", fmt.Errorf("could not detect workspace for skill installation: %w", err) + return "", fmt.Errorf("could not detect workspace for skill installation: %w\n\n"+ + "TIP: If automatic detection fails, please provide an explicit 'file_path' or 'workspace_root' "+ + "to a directory in your project containing workspace markers (like .git, go.mod, package.json).", err) } workspaceRoot := workspaceInfo.Root From d85f461684818bc64fa30b84c367d42011efd87e Mon Sep 17 00:00:00 2001 From: doITmagic Date: Tue, 10 Feb 2026 00:30:23 +0200 Subject: [PATCH 03/13] fix: synchronize update cache access with mutex --- internal/updater/updater.go | 92 ++++++++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/internal/updater/updater.go b/internal/updater/updater.go index fa9653f..b5bf1c9 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -1,6 +1,7 @@ package updater import ( + "archive/zip" "bufio" "crypto/sha256" "encoding/hex" @@ -13,6 +14,7 @@ import ( "path/filepath" "runtime" "strings" + "sync" "time" "github.com/Masterminds/semver/v3" @@ -25,21 +27,32 @@ type UpdateCache struct { } var ( - cacheFile = "update_cache.json" + cacheFile = "update_cache.json" + cacheMutex sync.Mutex ) -func getCachePath() string { - home, err := os.UserHomeDir() +func getCachePath() (string, error) { + configDir, err := os.UserConfigDir() if err != nil { - return cacheFile // Fallback to CWD + return "", fmt.Errorf("failed to get user config dir: %w", err) } - configDir := filepath.Join(home, ".config", "rag-code-mcp") - _ = os.MkdirAll(configDir, 0755) - return filepath.Join(configDir, cacheFile) + + appConfigDir := filepath.Join(configDir, "rag-code-mcp") + if err := os.MkdirAll(appConfigDir, 0755); err != nil { + return "", fmt.Errorf("failed to create config dir: %w", err) + } + + return filepath.Join(appConfigDir, cacheFile), nil } func GetCachedUpdate() (*UpdateCache, error) { - path := getCachePath() + cacheMutex.Lock() + defer cacheMutex.Unlock() + + path, err := getCachePath() + if err != nil { + return nil, err + } data, err := os.ReadFile(path) if err != nil { return nil, err @@ -52,6 +65,9 @@ func GetCachedUpdate() (*UpdateCache, error) { } func SaveUpdateCache(info *UpdateInfo) error { + cacheMutex.Lock() + defer cacheMutex.Unlock() + cache := UpdateCache{ LastCheck: time.Now(), } @@ -64,7 +80,12 @@ func SaveUpdateCache(info *UpdateInfo) error { if err != nil { return err } - return os.WriteFile(getCachePath(), data, 0644) + + path, err := getCachePath() + if err != nil { + return err + } + return os.WriteFile(path, data, 0644) } const ( @@ -255,9 +276,9 @@ func ApplyUpdate(archivePath string) error { return fmt.Errorf("failed to extract tar.gz: %w", err) } } else if strings.HasSuffix(archivePath, ".zip") { - // Basic unzip command for windows/linux if available - // Ideally use archive/zip in Go for better cross-platform - return fmt.Errorf("zip extraction not yet implemented in updater") + if err := unzip(archivePath, tempDir); err != nil { + return fmt.Errorf("failed to extract zip: %w", err) + } } newBinPath := filepath.Join(tempDir, binaryName) @@ -343,3 +364,50 @@ func getAssetName(url string) string { parts := strings.Split(url, "/") return parts[len(parts)-1] } + +func unzip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + fpath := filepath.Join(dest, f.Name) + + // Check for ZipSlip (Directory traversal) + if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { + return fmt.Errorf("illegal file path: %s", fpath) + } + + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, os.ModePerm) + continue + } + + if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return err + } + + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + + rc, err := f.Open() + if err != nil { + outFile.Close() + return err + } + + _, err = io.Copy(outFile, rc) + + outFile.Close() + rc.Close() + + if err != nil { + return err + } + } + return nil +} From 2a08ed60604a663296b795bf680e15e54d463862 Mon Sep 17 00:00:00 2001 From: doITmagic Date: Tue, 10 Feb 2026 00:31:03 +0200 Subject: [PATCH 04/13] fix: comprehensive updates for PR review - Fix skills path traversal validation - Fix race condition in update check - Add Windows zip support for updates - Improve test cleanup - Add missing tool schemas --- cmd/rag-code-mcp/main.go | 64 +++++++++++++++++++++++++++++-- internal/skills/skills.go | 29 ++++++++++++++ internal/skills/skills_test.go | 29 +++++++++++--- internal/tools/updates.go | 22 +++++++++-- internal/updater/updater_test.go | 66 ++++++++++++++++++++++++++++++++ 5 files changed, 196 insertions(+), 14 deletions(-) create mode 100644 internal/updater/updater_test.go diff --git a/cmd/rag-code-mcp/main.go b/cmd/rag-code-mcp/main.go index b4b0e08..0f54ff4 100644 --- a/cmd/rag-code-mcp/main.go +++ b/cmd/rag-code-mcp/main.go @@ -12,6 +12,7 @@ import ( "os/signal" "path/filepath" "strings" + "sync" "syscall" "time" @@ -447,7 +448,13 @@ func main() { } fmt.Printf("Found new version: %s\nDownloading...\n", info.LatestVersion) - tempFile := filepath.Join(os.TempDir(), "ragcode_update.tar.gz") + + // Determine extension from asset URL + ext := ".tar.gz" + if strings.HasSuffix(info.AssetURL, ".zip") { + ext = ".zip" + } + tempFile := filepath.Join(os.TempDir(), "ragcode_update"+ext) if err := info.DownloadAndVerify(tempFile); err != nil { log.Fatalf("Update failed: %v", err) } @@ -1112,6 +1119,49 @@ func getToolSchema(toolName string) map[string]interface{} { "required": []string{"query"}, } + case "list_skills": + return map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + } + + case "install_skill": + return map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "skill_id": map[string]interface{}{ + "type": "string", + "description": "The ID of the skill to install or uninstall", + }, + "active": map[string]interface{}{ + "type": "boolean", + "description": "True to install the skill, false to uninstall it", + }, + "file_path": map[string]interface{}{ + "type": "string", + "description": "Optional: file path to help detect workspace context", + }, + }, + "required": []string{"skill_id", "active"}, + } + + case "check_update": + return map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "force": map[string]interface{}{ + "type": "boolean", + "description": "Force check ignoring cache (default: false)", + }, + }, + } + + case "apply_update": + return map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + } + default: return map[string]interface{}{ "type": "object", @@ -1304,9 +1354,15 @@ func ensureIDERules(cfg *config.Config, filePath string) { } } -var lastUpdateCheck time.Time +var ( + lastUpdateCheck time.Time + lastUpdateCheckMutex sync.Mutex +) func triggerBackgroundUpdateCheck() { + lastUpdateCheckMutex.Lock() + defer lastUpdateCheckMutex.Unlock() + // Only check if more than 1 hour passed since last check in THIS session // to avoid spamming go-routines, while updater.CheckForUpdates handles the 24h logic if time.Since(lastUpdateCheck) < 1*time.Hour { @@ -1315,8 +1371,8 @@ func triggerBackgroundUpdateCheck() { lastUpdateCheck = time.Now() go func() { - info, _ := updater.CheckForUpdates(Version, false) - if info != nil { + info, err := updater.CheckForUpdates(Version, false) + if err == nil && info != nil { logger.Info("🌟 New version available: %s. Run 'apply_update' to upgrade.", info.LatestVersion) } }() diff --git a/internal/skills/skills.go b/internal/skills/skills.go index b108ca4..a212aef 100644 --- a/internal/skills/skills.go +++ b/internal/skills/skills.go @@ -77,11 +77,36 @@ func GetSkillMetadata(skillID string) (SkillInfo, error) { }, nil } +// validateSkillID ensures the skill ID is safe and does not contain path traversal characters +func validateSkillID(skillID string) error { + if strings.Contains(skillID, "..") || strings.ContainsAny(skillID, `/\`) { + return fmt.Errorf("invalid skill ID: potential path traversal detected") + } + // Check if Clean changes the path, which would indicate traversal or special characters + if filepath.Clean(skillID) != skillID { + return fmt.Errorf("invalid skill ID: format error") + } + return nil +} + // InstallSkill copies all files of an embedded skill to the destination directory func InstallSkill(skillID string, workspaceRoot string) error { + if err := validateSkillID(skillID); err != nil { + return err + } + destDir := filepath.Join(workspaceRoot, ".agent", "skills", skillID) + // Additional safety check: ensure destDir is within workspaceRoot + // Note: This might be redundant with validateSkillID but good for defense in depth + // However, workspaceRoot might be relative or absolute, so we rely on skillID validation primarily. + srcDir := "embedded/" + skillID + // Verify the skill exists in embedded FS before trying to walk + if _, err := embeddedSkills.ReadDir(srcDir); err != nil { + return fmt.Errorf("skill '%s' not found in embedded library", skillID) + } + return fs.WalkDir(embeddedSkills, srcDir, func(path string, d fs.DirEntry, err error) error { if err != nil { return err @@ -93,6 +118,7 @@ func InstallSkill(skillID string, workspaceRoot string) error { return err } + // Join with destination directory targetPath := filepath.Join(destDir, relPath) if d.IsDir() { @@ -111,6 +137,9 @@ func InstallSkill(skillID string, workspaceRoot string) error { // UninstallSkill removes a skill from the workspace func UninstallSkill(skillID string, workspaceRoot string) error { + if err := validateSkillID(skillID); err != nil { + return err + } destDir := filepath.Join(workspaceRoot, ".agent", "skills", skillID) return os.RemoveAll(destDir) } diff --git a/internal/skills/skills_test.go b/internal/skills/skills_test.go index 4f72ea6..987daa9 100644 --- a/internal/skills/skills_test.go +++ b/internal/skills/skills_test.go @@ -33,16 +33,12 @@ func TestListAvailableSkills(t *testing.T) { } func TestInstallAndUninstallSkill(t *testing.T) { - tempDir, err := os.MkdirTemp("", "skill-test-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tempDir) + tempDir := t.TempDir() skillID := "ragcode-priority" // Test Installation - err = InstallSkill(skillID, tempDir) + err := InstallSkill(skillID, tempDir) if err != nil { t.Fatalf("InstallSkill failed: %v", err) } @@ -64,3 +60,24 @@ func TestInstallAndUninstallSkill(t *testing.T) { t.Errorf("Skill directory %s still exists after uninstallation", filepath.Dir(skillFile)) } } + +func TestInstallSkillPathTraversal(t *testing.T) { + tempDir := t.TempDir() + + evilIDs := []string{ + "../foo", + "foo/bar", + "/etc/passwd", + `foo\bar`, + } + + for _, id := range evilIDs { + err := InstallSkill(id, tempDir) + if err == nil { + t.Errorf("InstallSkill should have failed for ID '%s'", id) + } else { + // Optional: verify error message + // t.Logf("Got expected error for %s: %v", id, err) + } + } +} diff --git a/internal/tools/updates.go b/internal/tools/updates.go index d07fd6d..4fccdf0 100644 --- a/internal/tools/updates.go +++ b/internal/tools/updates.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "os" - "path/filepath" + "strings" "github.com/doITmagic/rag-code-mcp/internal/updater" ) @@ -61,12 +61,26 @@ func (t *ApplyUpdateTool) Execute(ctx context.Context, args map[string]interface return "✅ You are already on the latest version.", nil } - tempFile := filepath.Join(os.TempDir(), "ragcode_update.tar.gz") - if err := info.DownloadAndVerify(tempFile); err != nil { + // Create a unique temporary file securely + // Use pattern with extension based on asset type + ext := ".tar.gz" + if strings.HasSuffix(info.AssetURL, ".zip") { + ext = ".zip" + } + + tempFile, err := os.CreateTemp("", "ragcode_update_*"+ext) + if err != nil { + return "", fmt.Errorf("failed to create temp file: %w", err) + } + tempPath := tempFile.Name() + tempFile.Close() // Close immediately, DownloadAndVerify re-opens/overwrites it + defer os.Remove(tempPath) + + if err := info.DownloadAndVerify(tempPath); err != nil { return "", fmt.Errorf("failed to download update: %w", err) } - if err := updater.ApplyUpdate(tempFile); err != nil { + if err := updater.ApplyUpdate(tempPath); err != nil { return "", fmt.Errorf("failed to install update: %w", err) } diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go new file mode 100644 index 0000000..0d9f0d6 --- /dev/null +++ b/internal/updater/updater_test.go @@ -0,0 +1,66 @@ +package updater + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +func TestGetCachePath(t *testing.T) { + path, err := getCachePath() + if err != nil { + t.Fatalf("getCachePath failed: %v", err) + } + + if path == "" { + t.Error("Expected non-empty path") + } + + dir := filepath.Dir(path) + info, err := os.Stat(dir) + if err != nil { + t.Fatalf("Cache directory does not exist: %v", err) + } + + if !info.IsDir() { + t.Errorf("Expected %s to be a directory", dir) + } +} + +func TestSaveAndGetUpdateCache(t *testing.T) { + // Backup existing cache if any + realPath, err := getCachePath() + if err == nil { + if _, err := os.Stat(realPath); err == nil { + backupPath := realPath + ".bak" + os.Rename(realPath, backupPath) + defer os.Rename(backupPath, realPath) + } + } + + info := &UpdateInfo{ + LatestVersion: "v1.2.3", + Tag: "v1.2.3", + AssetURL: "http://example.com/asset", + ChecksumURL: "http://example.com/checksum", + } + + err = SaveUpdateCache(info) + if err != nil { + t.Fatalf("SaveUpdateCache failed: %v", err) + } + + cached, err := GetCachedUpdate() + if err != nil { + t.Fatalf("GetCachedUpdate failed: %v", err) + } + + if cached.LatestVersion != "v1.2.3" { + t.Errorf("Expected version v1.2.3, got %s", cached.LatestVersion) + } + + if time.Since(cached.LastCheck) > time.Minute { + t.Error("LastCheck is too old") + } +} From 1e16971ba4e07f7ee86f0e72a161ff548ccb90b2 Mon Sep 17 00:00:00 2001 From: doITmagic Date: Tue, 10 Feb 2026 00:33:24 +0200 Subject: [PATCH 05/13] fix: resolve linter errors (errcheck, staticcheck) --- internal/skills/skills_test.go | 6 +----- internal/updater/updater.go | 4 +++- internal/updater/updater_test.go | 10 ++++++++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/internal/skills/skills_test.go b/internal/skills/skills_test.go index 987daa9..c89c18c 100644 --- a/internal/skills/skills_test.go +++ b/internal/skills/skills_test.go @@ -72,12 +72,8 @@ func TestInstallSkillPathTraversal(t *testing.T) { } for _, id := range evilIDs { - err := InstallSkill(id, tempDir) - if err == nil { + if err := InstallSkill(id, tempDir); err == nil { t.Errorf("InstallSkill should have failed for ID '%s'", id) - } else { - // Optional: verify error message - // t.Logf("Got expected error for %s: %v", id, err) } } } diff --git a/internal/updater/updater.go b/internal/updater/updater.go index b5bf1c9..ee35011 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -381,7 +381,9 @@ func unzip(src, dest string) error { } if f.FileInfo().IsDir() { - os.MkdirAll(fpath, os.ModePerm) + if err := os.MkdirAll(fpath, os.ModePerm); err != nil { + return err + } continue } diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go index 0d9f0d6..fb066d8 100644 --- a/internal/updater/updater_test.go +++ b/internal/updater/updater_test.go @@ -34,8 +34,14 @@ func TestSaveAndGetUpdateCache(t *testing.T) { if err == nil { if _, err := os.Stat(realPath); err == nil { backupPath := realPath + ".bak" - os.Rename(realPath, backupPath) - defer os.Rename(backupPath, realPath) + if err := os.Rename(realPath, backupPath); err != nil { + t.Logf("Failed to backup existing cache: %v", err) + } + defer func() { + if err := os.Rename(backupPath, realPath); err != nil { + t.Logf("Failed to restore backup cache: %v", err) + } + }() } } From 65c869f4cf1111e25efd4742e34b98e81ad43e79 Mon Sep 17 00:00:00 2001 From: doITmagic Date: Tue, 10 Feb 2026 00:52:38 +0200 Subject: [PATCH 06/13] chore: ignore .agent/skills directory and remove from git index --- .agent/skills/debugging-guide/SKILL.md | 29 --- .agent/skills/go-best-practices/SKILL.md | 52 ----- .agent/skills/ragcode-priority/SKILL.md | 137 ------------- .../examples/search_patterns.md | 184 ------------------ .agent/skills/ragcode-update/SKILL.md | 54 ----- 5 files changed, 456 deletions(-) delete mode 100644 .agent/skills/debugging-guide/SKILL.md delete mode 100644 .agent/skills/go-best-practices/SKILL.md delete mode 100644 .agent/skills/ragcode-priority/SKILL.md delete mode 100644 .agent/skills/ragcode-priority/examples/search_patterns.md delete mode 100644 .agent/skills/ragcode-update/SKILL.md diff --git a/.agent/skills/debugging-guide/SKILL.md b/.agent/skills/debugging-guide/SKILL.md deleted file mode 100644 index 6b5b88c..0000000 --- a/.agent/skills/debugging-guide/SKILL.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -name: debugging-guide -description: How to use ragcode tools for fast root-cause analysis and debugging ---- - -# 🐞 Skill: Debugging with Ragcode - -This skill defines a workflow for using ragcode tools to fix bugs efficiently. - ---- - -## 🔍 The 3-Step Debug Workflow - -### Step 1: Locate the Symptom -- Search for the exact error message using `hybrid_search`. -- If no error message, search semantically for the failing feature using `search_code`. - -### Step 2: Contextualize the Flow -- Find where the failing function is called using `find_implementations`. -- Look at the surrounding logic with `get_code_context` (fetch at least 10 lines before and after). - -### Step 3: Analyze Data Models -- Use `find_type_definition` to check if a struct field might be missing or wrongly typed. -- Use `list_package_exports` to see if there are utility functions already solving the problem. - ---- - -## 💡 Troubleshooting Pro-Tip: -If you are lost, search for "logger" or "Error" in the package to see how other parts of the system handle failures. diff --git a/.agent/skills/go-best-practices/SKILL.md b/.agent/skills/go-best-practices/SKILL.md deleted file mode 100644 index 12e8679..0000000 --- a/.agent/skills/go-best-practices/SKILL.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: go-best-practices -description: Go development patterns, project structure, and idiomatic practices ---- - -# 🐹 Skill: Go Best Practices - -This skill guides the AI in writing high-quality, idiomatic Go code and understanding Go project structures. - ---- - -## 🔗 External References (Source of Truth) -- [Google Go Style Guide](https://google.github.io/styleguide/go/) -- [Effective Go](https://go.dev/doc/effective_go) -- [Uber Go Style Guide](https://github.com/uber-go/guide/blob/master/style.md) - ---- - -## 🏗️ Project Structure -Most Go projects in this ecosystem follow the Standard Go Project Layout: -- `/cmd`: Main applications/binaries. -- `/internal`: Private packages (not importable by other projects). -- `/pkg`: Public packages (rarely used here, prefer `internal`). -- `/api`: API definitions. - -## ✅ Implementation Patterns - -### 1. Error Handling -- **Check immediately**: Never ignore errors. -- **Wrap errors**: Use `fmt.Errorf("context: %w", err)` to provide trace details. -- **Sentinel Errors**: Define custom errors in the package for specific conditions. - -### 2. Interfaces -- **Small is better**: Prefer `io.Reader` over large monolithic interfaces. -- **Consumer defines**: Define interfaces where they are USED, not where they are implemented. - -### 3. Concurrency -- **Use Channels**: For communication/orchestration. -- **Mutex**: For simple state protection. -- **Context**: Always propagate `context.Context` for cancellation and timeouts. - -### 4. Testing -- **Table-Driven Tests**: Use anonymous structs and `t.Run` for multiple test cases. -- **Package name**: Use `package_test` for external testing (test only the public API). -- **Mocking**: Use interfaces to mock dependencies. Avoid complex mocking frameworks if simple stubs work. -- **Helpers**: Use `t.Helper()` for repetitive assertions. - ---- - -## 🔧 Tools for Go Project Analysis: -- `mcp_ragcode_find_type_definition`: Essential for understanding structs and interfaces. -- `mcp_ragcode_list_package_exports`: Quick overview of a package's public API. diff --git a/.agent/skills/ragcode-priority/SKILL.md b/.agent/skills/ragcode-priority/SKILL.md deleted file mode 100644 index 1c0f80c..0000000 --- a/.agent/skills/ragcode-priority/SKILL.md +++ /dev/null @@ -1,137 +0,0 @@ ---- -name: ragcode-priority -description: Prioritize ragcode MCP tools for all code searches in this project ---- - -# 🎯 Skill: Ragcode MCP Priority Usage - -This skill defines **MANDATORY** rules for searching code in the **rag-code-mcp** project. - ---- - -## ✅ MANDATORY RULES - -### 1. Search Hierarchy - -When searching code, ALWAYS follow this order: - -| Priority | Tool | When to Use | -|----------|------|-------------| -| 🥇 **First** | `mcp_ragcode_search_code` | Exploration, understanding concepts, unfamiliar code | -| 🥈 **Second** | `mcp_ragcode_hybrid_search` | Exact match: function names, errors, string literals | -| 🥉 **Third** | `mcp_ragcode_get_function_details` | Complete code of a known function | -| 4️⃣ | `mcp_ragcode_find_type_definition` | Complete struct/interface definition | -| 5️⃣ | `mcp_ragcode_find_implementations` | Where a function is called/implemented | -| 6️⃣ | `mcp_ragcode_list_package_exports` | What a package exports | - -### 2. "Search First" Rule - -**ALWAYS** start with a ragcode search before assuming code structure! - -``` -❌ WRONG: "I know it's in internal/ragcode, I'll open the file directly" -✅ CORRECT: search_code → find exact location → open file -``` - -### 3. Standard Workflow - -``` -Step 1: mcp_ragcode_search_code (understand context) - ↓ -Step 2: mcp_ragcode_get_function_details OR find_type_definition (details) - ↓ -Step 3: view_file (only for specific lines if needed) -``` - ---- - -## ⛔ FORBIDDEN - -### Do NOT use these tools for code search: - -| Tool | Why Forbidden | Exception | -|------|---------------|-----------| -| `grep_search` | No semantic context | ONLY for JavaScript/TypeScript (not indexed yet) | -| `find_by_name` | For files, not content | OK for finding files, not code | -| `view_file` randomly | Waste of time without context | ONLY after ragcode search | - -### Forbidden behaviors: - -1. ❌ **DO NOT assume** code structure without searching -2. ❌ **DO NOT make multiple grep searches** when one ragcode search suffices -3. ❌ **DO NOT navigate manually** through directories to find code -4. ❌ **DO NOT open random files** hoping to find what you need - ---- - -## 🌐 Language Support - -### ✅ Full support (use ONLY ragcode): -- **Go** - complete analyzer, all tools work -- **Python** - complete analyzer -- **PHP/Laravel** - complete analyzer with Laravel support -- **HTML** - basic analyzer - -### ⚠️ Limited support (fallback allowed): -- **JavaScript/TypeScript** - indexing in development - - Try `search_code` FIRST - - If not found, use `grep_search` as backup - ---- - -## 🔧 Recommended Parameters - -### search_code -``` -query: -file_path: /home/razvan/go/src/github.com/doITmagic/rag-code-mcp # optional, for context -limit: 5 # sufficient for most searches -``` - -### hybrid_search -``` -query: -limit: 5 -``` - -### get_function_details -``` -function_name: -package: -``` - ---- - -## 📖 Why ragcode? - -| Aspect | grep_search | ragcode | -|--------|-------------|---------| -| Understands context | ❌ | ✅ | -| Semantic search | ❌ | ✅ | -| Finds similar functions | ❌ | ✅ | -| Returns complete code | ❌ | ✅ | -| Multi-language support | Limited | ✅ | - -**Concrete example:** -- Search "user authentication" with grep: finds only literal string "authentication" -- Search with ragcode: finds login functions, token validation, auth middleware - ---- - -## ⚡ Quick Reference - -``` -🔍 Exploration → search_code -🎯 Exact match → hybrid_search -📝 Function code → get_function_details -🏗️ Type definition → find_type_definition -🔗 Usages → find_implementations -📦 Package contents → list_package_exports -📄 Specific context → get_code_context (with file + lines) -``` - ---- - -## 📚 Examples - -See [examples/search_patterns.md](examples/search_patterns.md) for project-specific examples. diff --git a/.agent/skills/ragcode-priority/examples/search_patterns.md b/.agent/skills/ragcode-priority/examples/search_patterns.md deleted file mode 100644 index 85689bb..0000000 --- a/.agent/skills/ragcode-priority/examples/search_patterns.md +++ /dev/null @@ -1,184 +0,0 @@ -# 📚 Ragcode Search Patterns for rag-code-mcp - -This file contains practical examples for searching the rag-code-mcp codebase. - ---- - -## 🔍 Finding Analyzers - -### Find all language analyzers -``` -mcp_ragcode_search_code - query: "language analyzer implementation CodeAnalyzer" - limit: 10 -``` - -### Find specific language analyzer -``` -# Go analyzer -mcp_ragcode_search_code query: "Go AST parsing analyzer" - -# Python analyzer -mcp_ragcode_search_code query: "Python code analyzer module parsing" - -# PHP/Laravel analyzer -mcp_ragcode_search_code query: "PHP Laravel adapter analyzer" -``` - -### Get analyzer implementation details -``` -mcp_ragcode_get_function_details - function_name: "NewCodeAnalyzer" - package: "golang" -``` - ---- - -## 🏗️ Understanding Types - -### Find Symbol structure -``` -mcp_ragcode_find_type_definition type_name: "Symbol" -``` - -### Find PathAnalyzer interface -``` -mcp_ragcode_find_type_definition type_name: "PathAnalyzer" -``` - -### Find all types in a package -``` -mcp_ragcode_list_package_exports - package: "codetypes" - symbol_type: "type" -``` - ---- - -## 🔗 Finding Usages - -### Where is IndexWorkspace called? -``` -mcp_ragcode_find_implementations symbol_name: "IndexWorkspace" -``` - -### Who implements PathAnalyzer? -``` -mcp_ragcode_find_implementations symbol_name: "PathAnalyzer" -``` - -### Where is Symbol used? -``` -mcp_ragcode_find_implementations symbol_name: "Symbol" -``` - ---- - -## 🎯 Exact Matches - -### Find specific function by name -``` -mcp_ragcode_hybrid_search query: "func IndexWorkspace" -``` - -### Find error handling -``` -mcp_ragcode_hybrid_search query: "return nil, fmt.Errorf" -``` - -### Find specific constant or variable -``` -mcp_ragcode_hybrid_search query: "LanguageGo" -``` - ---- - -## 📦 Exploring Packages - -### What does the ragcode package export? -``` -mcp_ragcode_list_package_exports package: "ragcode" -``` - -### What functions are in workspace package? -``` -mcp_ragcode_list_package_exports - package: "workspace" - symbol_type: "function" -``` - -### What types are in codetypes package? -``` -mcp_ragcode_list_package_exports - package: "codetypes" - symbol_type: "type" -``` - ---- - -## 🔄 Common Workflows - -### Workflow 1: Understanding a feature -``` -# Step 1: Semantic search for the concept -mcp_ragcode_search_code query: "language detection workspace" - -# Step 2: Get the main function details -mcp_ragcode_get_function_details function_name: "DetectLanguages" - -# Step 3: Find where it's used -mcp_ragcode_find_implementations symbol_name: "DetectLanguages" -``` - -### Workflow 2: Debugging an issue -``` -# Step 1: Search for related code -mcp_ragcode_search_code query: "indexing error handling workspace" - -# Step 2: Find the exact error message -mcp_ragcode_hybrid_search query: "workspace not indexed" - -# Step 3: Get context around the error -mcp_ragcode_get_code_context - file_path: "" - start_line: - end_line: -``` - -### Workflow 3: Adding a new feature -``` -# Step 1: Find similar implementations -mcp_ragcode_search_code query: "analyzer implementation for language" - -# Step 2: Understand the interface -mcp_ragcode_find_type_definition type_name: "PathAnalyzer" - -# Step 3: See existing implementations -mcp_ragcode_find_implementations symbol_name: "PathAnalyzer" -``` - ---- - -## ⚠️ JavaScript/TypeScript Fallback - -When searching JS/TS code (not yet fully indexed): - -``` -# Try ragcode first -mcp_ragcode_search_code query: "javascript parser" - -# If no results, fallback to grep -grep_search - SearchPath: "/home/razvan/go/src/github.com/doITmagic/rag-code-mcp" - Query: "javascript" - Includes: ["*.go"] -``` - ---- - -## 📝 Tips - -1. **Be semantic**: Use descriptive queries like "authentication middleware" not just "auth" -2. **Start broad**: Begin with `search_code`, narrow down with specific tools -3. **Use packages**: When function names are ambiguous, specify the package -4. **Check implementations**: Always use `find_implementations` to understand impact before refactoring diff --git a/.agent/skills/ragcode-update/SKILL.md b/.agent/skills/ragcode-update/SKILL.md deleted file mode 100644 index 1702f9c..0000000 --- a/.agent/skills/ragcode-update/SKILL.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: ragcode-update -description: Guide for managing ragcode updates, health checks, and AI skills ---- - -# 🛠️ Skill: Ragcode Maintenance & Updates - -This skill instructs the AI on how to keep the `ragcode-mcp` server up to date and healthy. - ---- - -## 🔄 Update Workflow - -### 1. Detection -When you see a notification in the logs like: -`🌟 New version available: X.Y.Z. Run 'apply_update' to upgrade.` - -**You MUST:** -1. Inform the user that a new version is available. -2. Briefly explain that the update includes potential bug fixes or new features. -3. Ask for explicit permission: *"Would you like me to apply the update for you now?"* - -### 2. Manual Check -If the user asks "is there any update?", use the `check_update` tool with `force: true`. - -### 3. Applying Update -When the user says "Yes" or "Apply update": -1. Call `mcp_ragcode_apply_update`. -2. Inform the user that the installation was successful. -3. Remind the user: *"The server needs a restart (or IDE reload) to activate the new version."* - ---- - -## 🧩 Skill Management - -When the user wants to enhance your capabilities: -1. Use `list_skills` to see what is bundled in the binary. -2. Explain what each skill does based on its description. -3. Use `install_skill(skill_id, active=true)` to enable it. - ---- - -## 🏥 Health Checks -If the system feels slow or tools fail consistently: -1. Suggest running the health check (CLI flag `--health`). -2. Check if the workspace is indexed. If not, suggest `index_workspace`. - ---- - -## ⚡ Quick Summary of Tools: -- `check_update`: Check GitHub for new releases. -- `apply_update`: Download and install the latest binary. -- `list_skills`: See available AI behaviors. -- `install_skill`: Toggle a specific behavior on/off. From cab6f3f5e93b239a70cc68adc73f0cb24e6e8e72 Mon Sep 17 00:00:00 2001 From: doITmagic Date: Tue, 10 Feb 2026 00:53:41 +0200 Subject: [PATCH 07/13] fix/refactor: address PR review comments for update system and security --- .gitignore | 1 + internal/tools/updates.go | 8 +-- internal/updater/updater.go | 113 ++++++++++++++++++++++++++----- internal/updater/updater_test.go | 23 ++----- 4 files changed, 108 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index ea8662c..bd82195 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ temp/ # Scripts scripts/ .ragcode/ +.agent/skills/ diff --git a/internal/tools/updates.go b/internal/tools/updates.go index 4fccdf0..60798c3 100644 --- a/internal/tools/updates.go +++ b/internal/tools/updates.go @@ -19,7 +19,7 @@ func NewCheckUpdateTool(version string) *CheckUpdateTool { func (t *CheckUpdateTool) Name() string { return "check_update" } func (t *CheckUpdateTool) Description() string { - return "Checks for available ragcode-mcp updates on GitHub. Returns version info and release notes if a new version is available." + return "Checks for available ragcode-mcp updates on GitHub and reports if a newer version is available." } func (t *CheckUpdateTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) { @@ -27,7 +27,7 @@ func (t *CheckUpdateTool) Execute(ctx context.Context, args map[string]interface if f, ok := args["force"].(bool); ok { force = f } - info, err := updater.CheckForUpdates(t.version, force) + info, err := updater.CheckForUpdates(ctx, t.version, force) if err != nil { return "", fmt.Errorf("failed to check for updates: %w", err) } @@ -53,7 +53,7 @@ func (t *ApplyUpdateTool) Description() string { } func (t *ApplyUpdateTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) { - info, err := updater.CheckForUpdates(t.version, true) + info, err := updater.CheckForUpdates(ctx, t.version, true) if err != nil { return "", err } @@ -76,7 +76,7 @@ func (t *ApplyUpdateTool) Execute(ctx context.Context, args map[string]interface tempFile.Close() // Close immediately, DownloadAndVerify re-opens/overwrites it defer os.Remove(tempPath) - if err := info.DownloadAndVerify(tempPath); err != nil { + if err := info.DownloadAndVerify(ctx, tempPath); err != nil { return "", fmt.Errorf("failed to download update: %w", err) } diff --git a/internal/updater/updater.go b/internal/updater/updater.go index ee35011..c923ce7 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -3,6 +3,7 @@ package updater import ( "archive/zip" "bufio" + "context" "crypto/sha256" "encoding/hex" "encoding/json" @@ -85,7 +86,44 @@ func SaveUpdateCache(info *UpdateInfo) error { if err != nil { return err } - return os.WriteFile(path, data, 0644) + + // Write to a temporary file in the same directory and atomically replace + dir := filepath.Dir(path) + tmpFile, err := os.CreateTemp(dir, "update_cache_*.tmp") + if err != nil { + return err + } + tmpName := tmpFile.Name() + + // Ensure cleanup if we fail before rename + success := false + defer func() { + if !success { + tmpFile.Close() + os.Remove(tmpName) + } + }() + + if _, err := tmpFile.Write(data); err != nil { + return err + } + if err := tmpFile.Sync(); err != nil { + return err + } + if err := tmpFile.Close(); err != nil { + return err + } + + // On success, rename the temp file to the final path (atomic on same filesystem). + if err := os.Rename(tmpName, path); err != nil { + return err + } + + // Set restrictive permissions + os.Chmod(path, 0600) + + success = true + return nil } const ( @@ -102,7 +140,7 @@ type UpdateInfo struct { // CheckForUpdates queries GitHub for the latest release and compares it with the current version. // If force is false, it returns cached results if available and less than 24 hours old. -func CheckForUpdates(currentVersion string, force bool) (*UpdateInfo, error) { +func CheckForUpdates(ctx context.Context, currentVersion string, force bool) (*UpdateInfo, error) { if currentVersion == "" || currentVersion == "dev" { return nil, nil // Skip checks for dev versions } @@ -133,7 +171,14 @@ func CheckForUpdates(currentVersion string, force bool) (*UpdateInfo, error) { } url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", GitHubOwner, GitHubRepo) - resp, err := http.Get(url) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch latest release: %w", err) } @@ -196,9 +241,9 @@ func CheckForUpdates(currentVersion string, force bool) (*UpdateInfo, error) { } // DownloadAndVerify downloads the archive and checks its integrity. -func (info *UpdateInfo) DownloadAndVerify(destPath string) error { +func (info *UpdateInfo) DownloadAndVerify(ctx context.Context, destPath string) error { // 1. Download archive - if err := downloadFile(info.AssetURL, destPath); err != nil { + if err := downloadFile(ctx, info.AssetURL, destPath); err != nil { return fmt.Errorf("failed to download asset: %w", err) } @@ -207,7 +252,13 @@ func (info *UpdateInfo) DownloadAndVerify(destPath string) error { return fmt.Errorf("no checksum URL available") } - resp, err := http.Get(info.ChecksumURL) + req, err := http.NewRequestWithContext(ctx, "GET", info.ChecksumURL, nil) + if err != nil { + return fmt.Errorf("failed to create checksum request: %w", err) + } + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) if err != nil { return fmt.Errorf("failed to download checksums: %w", err) } @@ -291,6 +342,14 @@ func ApplyUpdate(archivePath string) error { // but it's safer to move the old one to a .old suffix and move the new one in. oldBinPath := self + ".old" if err := os.Rename(self, oldBinPath); err != nil { + if runtime.GOOS == "windows" { + // Fallback: write as .new and tell user + newBinPermanent := self + ".new" + if err := moveFile(newBinPath, newBinPermanent); err != nil { + return fmt.Errorf("failed to write new binary: %w", err) + } + return fmt.Errorf("could not replace running binary on Windows: %w. New version saved to %s. Please close the server and rename it manually.", err, filepath.Base(newBinPermanent)) + } return fmt.Errorf("failed to move current binary to %s: %w", oldBinPath, err) } @@ -324,8 +383,15 @@ func moveFile(src, dst string) error { return os.WriteFile(dst, input, 0755) } -func downloadFile(url, dest string) error { - resp, err := http.Get(url) +func downloadFile(ctx context.Context, url, dest string) error { + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return err + } + + // Longer timeout for binary download + client := &http.Client{Timeout: 5 * time.Minute} + resp, err := client.Do(req) if err != nil { return err } @@ -380,18 +446,25 @@ func unzip(src, dest string) error { return fmt.Errorf("illegal file path: %s", fpath) } - if f.FileInfo().IsDir() { - if err := os.MkdirAll(fpath, os.ModePerm); err != nil { + // Security: Reject non-regular files (symlinks, devices, etc.) + info := f.FileInfo() + if !info.Mode().IsRegular() && !info.IsDir() { + continue + } + + if info.IsDir() { + if err := os.MkdirAll(fpath, 0755); err != nil { return err } continue } - if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { return err } - outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + // Use fixed mode 0644 for extracted files for better security consistency + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } @@ -402,13 +475,19 @@ func unzip(src, dest string) error { return err } - _, err = io.Copy(outFile, rc) + _, copyErr := io.Copy(outFile, rc) - outFile.Close() - rc.Close() + closeErr := outFile.Close() + rcErr := rc.Close() - if err != nil { - return err + if copyErr != nil { + return copyErr + } + if closeErr != nil { + return closeErr + } + if rcErr != nil { + return rcErr } } return nil diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go index fb066d8..616144d 100644 --- a/internal/updater/updater_test.go +++ b/internal/updater/updater_test.go @@ -29,21 +29,12 @@ func TestGetCachePath(t *testing.T) { } func TestSaveAndGetUpdateCache(t *testing.T) { - // Backup existing cache if any - realPath, err := getCachePath() - if err == nil { - if _, err := os.Stat(realPath); err == nil { - backupPath := realPath + ".bak" - if err := os.Rename(realPath, backupPath); err != nil { - t.Logf("Failed to backup existing cache: %v", err) - } - defer func() { - if err := os.Rename(backupPath, realPath); err != nil { - t.Logf("Failed to restore backup cache: %v", err) - } - }() - } - } + tempDir := t.TempDir() + + // Isolate user config dir to the temp directory + t.Setenv("XDG_CONFIG_HOME", tempDir) + t.Setenv("AppData", tempDir) + t.Setenv("HOME", tempDir) // Fallback for some systems info := &UpdateInfo{ LatestVersion: "v1.2.3", @@ -52,7 +43,7 @@ func TestSaveAndGetUpdateCache(t *testing.T) { ChecksumURL: "http://example.com/checksum", } - err = SaveUpdateCache(info) + err := SaveUpdateCache(info) if err != nil { t.Fatalf("SaveUpdateCache failed: %v", err) } From 7be4b788beb4e17cfa6d296cf340f68e33fc083b Mon Sep 17 00:00:00 2001 From: doITmagic Date: Tue, 10 Feb 2026 00:55:02 +0200 Subject: [PATCH 08/13] fix: use os.CreateTemp for --update flag to avoid collisions --- cmd/rag-code-mcp/main.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cmd/rag-code-mcp/main.go b/cmd/rag-code-mcp/main.go index 0f54ff4..7380e4a 100644 --- a/cmd/rag-code-mcp/main.go +++ b/cmd/rag-code-mcp/main.go @@ -438,7 +438,7 @@ func main() { // Handle update flag if *updateFlag { fmt.Println("Checking for updates...") - info, err := updater.CheckForUpdates(Version, true) + info, err := updater.CheckForUpdates(context.Background(), Version, true) if err != nil { log.Fatalf("Failed to check for updates: %v", err) } @@ -454,8 +454,16 @@ func main() { if strings.HasSuffix(info.AssetURL, ".zip") { ext = ".zip" } - tempFile := filepath.Join(os.TempDir(), "ragcode_update"+ext) - if err := info.DownloadAndVerify(tempFile); err != nil { + // Create a unique temporary file securely + tmp, err := os.CreateTemp("", "ragcode_update_*"+ext) + if err != nil { + log.Fatalf("Failed to create temporary file for update: %v", err) + } + tempFile := tmp.Name() + tmp.Close() + defer os.Remove(tempFile) + + if err := info.DownloadAndVerify(context.Background(), tempFile); err != nil { log.Fatalf("Update failed: %v", err) } @@ -485,7 +493,7 @@ func main() { // Background update check go func() { - info, err := updater.CheckForUpdates(Version, false) + info, err := updater.CheckForUpdates(context.Background(), Version, false) if err == nil && info != nil { logger.Info("🌟 New version available: %s. Run 'rag-code-mcp --update' to upgrade.", info.LatestVersion) } @@ -1371,7 +1379,7 @@ func triggerBackgroundUpdateCheck() { lastUpdateCheck = time.Now() go func() { - info, err := updater.CheckForUpdates(Version, false) + info, err := updater.CheckForUpdates(context.Background(), Version, false) if err == nil && info != nil { logger.Info("🌟 New version available: %s. Run 'apply_update' to upgrade.", info.LatestVersion) } From 0d3334f8c299768fe7fee588af800ce3220c48aa Mon Sep 17 00:00:00 2001 From: doITmagic Date: Tue, 10 Feb 2026 01:02:42 +0200 Subject: [PATCH 09/13] fix: check return value of os.Chmod to satisfy linter --- internal/updater/updater.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/updater/updater.go b/internal/updater/updater.go index c923ce7..a2fd5ea 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -120,7 +120,11 @@ func SaveUpdateCache(info *UpdateInfo) error { } // Set restrictive permissions - os.Chmod(path, 0600) + if err := os.Chmod(path, 0600); err != nil { + // Non-fatal, just log or ignore if we really don't care, + // but checking it satisfies the linter and is better practice. + return fmt.Errorf("failed to set restrictive permissions on cache: %w", err) + } success = true return nil From 0eb84b0f56d02a70073b5065566a0e166f1d7622 Mon Sep 17 00:00:00 2001 From: doITmagic Date: Tue, 10 Feb 2026 01:18:49 +0200 Subject: [PATCH 10/13] chore: comprehensive fix for file closing error handling and cleanup --- cmd/install/main.go | 28 ++++++++++++++++++++-------- cmd/rag-code-mcp/main.go | 20 +------------------- internal/updater/updater.go | 14 ++++++++++---- internal/workspace/state.go | 9 +++++++-- startup_debug.txt | 4 ++++ windsurf_debug_log.txt | 3 +++ 6 files changed, 45 insertions(+), 33 deletions(-) create mode 100644 startup_debug.txt create mode 100644 windsurf_debug_log.txt diff --git a/cmd/install/main.go b/cmd/install/main.go index 8b5884b..53ee054 100644 --- a/cmd/install/main.go +++ b/cmd/install/main.go @@ -588,10 +588,13 @@ func copyFile(src, dst string) error { return err } } - defer destFile.Close() + _, copyErr := io.Copy(destFile, sourceFile) + closeErr := destFile.Close() - _, err = io.Copy(destFile, sourceFile) - return err + if copyErr != nil { + return copyErr + } + return closeErr } // downloadAndExtractBinary fetches the release archive and extracts the binary. @@ -661,11 +664,17 @@ func downloadAndExtractBinary(dest string) bool { warn(fmt.Sprintf("Could not create destination file: %v", err)) return false } - defer outFile.Close() cmd.Stdout = outFile - if err := cmd.Run(); err != nil { - warn(fmt.Sprintf("Failed to extract binary: %v", err)) + runErr := cmd.Run() + closeErr := outFile.Close() + + if runErr != nil { + warn(fmt.Sprintf("Failed to extract binary: %v", runErr)) + return false + } + if closeErr != nil { + warn(fmt.Sprintf("Failed to finalise binary file: %v", closeErr)) return false } @@ -707,10 +716,13 @@ func addToPath(binDir string) { warn(fmt.Sprintf("Could not update shell config: %v", err)) return } - defer f.Close() - if _, err := f.WriteString(fmt.Sprintf("\nexport PATH=\"%s:$PATH\"\n", binDir)); err != nil { warn(fmt.Sprintf("Could not write to shell config: %v", err)) + f.Close() + return + } + if err := f.Close(); err != nil { + warn(fmt.Sprintf("Could not finalise shell config: %v", err)) } else { success(fmt.Sprintf("Added to %s (restart shell to apply)", shellConfig)) } diff --git a/cmd/rag-code-mcp/main.go b/cmd/rag-code-mcp/main.go index 7380e4a..5160fe8 100644 --- a/cmd/rag-code-mcp/main.go +++ b/cmd/rag-code-mcp/main.go @@ -365,16 +365,6 @@ workspace: } func main() { - // AGGRESSIVE STARTUP DEBUG - f, _ := os.Create("/tmp/ragcode-startup.txt") - cwd, _ := os.Getwd() - exe, _ := os.Executable() - fmt.Fprintf(f, "Time: %s\n", time.Now()) - fmt.Fprintf(f, "Exe: %s\n", exe) - fmt.Fprintf(f, "CWD: %s\n", cwd) - fmt.Fprintf(f, "Args: %v\n", os.Args) - f.Close() - // Define flags configPath := flag.String("config", "config.yaml", "Path to configuration file") ollamaBaseURLFlag := flag.String("ollama-base-url", "", "Ollama base URL (overrides config/env)") @@ -454,15 +444,7 @@ func main() { if strings.HasSuffix(info.AssetURL, ".zip") { ext = ".zip" } - // Create a unique temporary file securely - tmp, err := os.CreateTemp("", "ragcode_update_*"+ext) - if err != nil { - log.Fatalf("Failed to create temporary file for update: %v", err) - } - tempFile := tmp.Name() - tmp.Close() - defer os.Remove(tempFile) - + tempFile := filepath.Join(os.TempDir(), "ragcode_update"+ext) if err := info.DownloadAndVerify(context.Background(), tempFile); err != nil { log.Fatalf("Update failed: %v", err) } diff --git a/internal/updater/updater.go b/internal/updater/updater.go index a2fd5ea..4ba7bed 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -409,10 +409,14 @@ func downloadFile(ctx context.Context, url, dest string) error { if err != nil { return err } - defer out.Close() - _, err = io.Copy(out, resp.Body) - return err + _, copyErr := io.Copy(out, resp.Body) + closeErr := out.Close() + + if copyErr != nil { + return copyErr + } + return closeErr } func calculateSHA256(path string) (string, error) { @@ -475,7 +479,9 @@ func unzip(src, dest string) error { rc, err := f.Open() if err != nil { - outFile.Close() + if cerr := outFile.Close(); cerr != nil { + return fmt.Errorf("failed to open zip entry: %w; additionally failed to close destination file: %v", err, cerr) + } return err } diff --git a/internal/workspace/state.go b/internal/workspace/state.go index 149720d..eb08e77 100644 --- a/internal/workspace/state.go +++ b/internal/workspace/state.go @@ -67,10 +67,15 @@ func (s *WorkspaceState) Save(path string) error { if err != nil { return err } - defer f.Close() s.LastIndexed = time.Now() - return json.NewEncoder(f).Encode(s) + encodeErr := json.NewEncoder(f).Encode(s) + closeErr := f.Close() + + if encodeErr != nil { + return encodeErr + } + return closeErr } // UpdateFile updates the state for a file diff --git a/startup_debug.txt b/startup_debug.txt new file mode 100644 index 0000000..85e4402 --- /dev/null +++ b/startup_debug.txt @@ -0,0 +1,4 @@ +Time: 2026-02-06 14:49:03.911017622 +0200 EET m=+0.001824994 +Exe: /home/razvan/.local/share/ragcode/bin/rag-code-mcp +CWD: /home/razvan +Args: [/home/razvan/.local/share/ragcode/bin/rag-code-mcp] diff --git a/windsurf_debug_log.txt b/windsurf_debug_log.txt new file mode 100644 index 0000000..ff4a251 --- /dev/null +++ b/windsurf_debug_log.txt @@ -0,0 +1,3 @@ +TIME: 2026-02-06T14:52:48+02:00 | OP: InitializedHandler +Running extract roots... +ERROR: Failed to list roots: calling "roots/list": no roots handler configured From fcb1bbf109c9eb91a3d07f20997325d0bf462af5 Mon Sep 17 00:00:00 2001 From: doITmagic Date: Tue, 10 Feb 2026 01:31:48 +0200 Subject: [PATCH 11/13] chore: address final PR review comments regarding security, caching and consistency --- cmd/rag-code-mcp/main.go | 13 +++++-- internal/skills/skills.go | 13 +++---- internal/updater/updater.go | 66 +++++++++++++++++++++++--------- internal/updater/updater_test.go | 12 +++--- 4 files changed, 69 insertions(+), 35 deletions(-) diff --git a/cmd/rag-code-mcp/main.go b/cmd/rag-code-mcp/main.go index 5160fe8..9150a20 100644 --- a/cmd/rag-code-mcp/main.go +++ b/cmd/rag-code-mcp/main.go @@ -477,7 +477,7 @@ func main() { go func() { info, err := updater.CheckForUpdates(context.Background(), Version, false) if err == nil && info != nil { - logger.Info("🌟 New version available: %s. Run 'rag-code-mcp --update' to upgrade.", info.LatestVersion) + logger.Info("🌟 New version available: %s. Run 'rag-code-mcp --update' or use the 'apply_update' tool to upgrade.", info.LatestVersion) } }() @@ -1148,8 +1148,13 @@ func getToolSchema(toolName string) map[string]interface{} { case "apply_update": return map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{}, + "type": "object", + "properties": map[string]interface{}{ + "force": map[string]interface{}{ + "type": "boolean", + "description": "Force update even if version matches (default: false)", + }, + }, } default: @@ -1363,7 +1368,7 @@ func triggerBackgroundUpdateCheck() { go func() { info, err := updater.CheckForUpdates(context.Background(), Version, false) if err == nil && info != nil { - logger.Info("🌟 New version available: %s. Run 'apply_update' to upgrade.", info.LatestVersion) + logger.Info("🌟 New version available: %s. Run 'rag-code-mcp --update' or use the 'apply_update' tool to upgrade.", info.LatestVersion) } }() } diff --git a/internal/skills/skills.go b/internal/skills/skills.go index a212aef..b30d37a 100644 --- a/internal/skills/skills.go +++ b/internal/skills/skills.go @@ -6,11 +6,14 @@ import ( "io/fs" "os" "path/filepath" + "regexp" "strings" "gopkg.in/yaml.v3" ) +var skillIDRegex = regexp.MustCompile(`^[a-z0-9-]+$`) + //go:embed embedded/* var embeddedSkills embed.FS @@ -77,14 +80,10 @@ func GetSkillMetadata(skillID string) (SkillInfo, error) { }, nil } -// validateSkillID ensures the skill ID is safe and does not contain path traversal characters +// validateSkillID ensures the skill ID is safe and matches a strict allowlist func validateSkillID(skillID string) error { - if strings.Contains(skillID, "..") || strings.ContainsAny(skillID, `/\`) { - return fmt.Errorf("invalid skill ID: potential path traversal detected") - } - // Check if Clean changes the path, which would indicate traversal or special characters - if filepath.Clean(skillID) != skillID { - return fmt.Errorf("invalid skill ID: format error") + if !skillIDRegex.MatchString(skillID) { + return fmt.Errorf("invalid skill ID: must be lowercase alphanumeric with hyphens (e.g. 'my-skill')") } return nil } diff --git a/internal/updater/updater.go b/internal/updater/updater.go index 4ba7bed..c09ef21 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -115,6 +115,14 @@ func SaveUpdateCache(info *UpdateInfo) error { } // On success, rename the temp file to the final path (atomic on same filesystem). + // On Windows, os.Rename may not reliably replace an existing file, so remove it first. + if runtime.GOOS == "windows" { + if _, err := os.Stat(path); err == nil { + if err := os.Remove(path); err != nil { + return fmt.Errorf("failed to remove existing cache file: %w", err) + } + } + } if err := os.Rename(tmpName, path); err != nil { return err } @@ -155,16 +163,22 @@ func CheckForUpdates(ctx context.Context, currentVersion string, force bool) (*U // Check if cache is fresh (24h) if time.Since(cache.LastCheck) < 24*time.Hour { // Still need to compare versions because currentVersion might have changed + curr, errCurr := semver.NewVersion(currentVersion) if cache.UpdateDetails != nil { - curr, err := semver.NewVersion(currentVersion) - if err == nil { - latest, err := semver.NewVersion(cache.UpdateDetails.LatestVersion) - if err == nil && latest.GreaterThan(curr) { + latest, errLatest := semver.NewVersion(cache.UpdateDetails.LatestVersion) + if errCurr == nil && errLatest == nil { + if latest.GreaterThan(curr) { return cache.UpdateDetails, nil } + // Already on latest (or newer) version according to cache + return nil, nil } + // If semver parsing fails for current or cached version, treat as cache miss + // and fall through to the network-based check below. + } else if errCurr == nil { + // Fresh cache with no update details means no update was available last check + return nil, nil } - return nil, nil // No new version in cache or already on latest } } } @@ -209,13 +223,17 @@ func CheckForUpdates(ctx context.Context, currentVersion string, force bool) (*U return nil, fmt.Errorf("invalid latest version %q: %w", release.TagName, err) } - var info *UpdateInfo - if latest.GreaterThan(curr) { - info = &UpdateInfo{ - LatestVersion: latest.String(), - Tag: release.TagName, - } + // Always record the latest version/tag so callers can cache this information + // even when no update is required. Additional update details (asset URLs, + // checksums) are only populated when a newer version is actually available. + info := &UpdateInfo{ + LatestVersion: latest.String(), + Tag: release.TagName, + } + updateAvailable := false + if latest.GreaterThan(curr) { + updateAvailable = true // Match asset for current platform archiveName := fmt.Sprintf("rag-code-mcp_%s_%s", runtime.GOOS, runtime.GOARCH) if runtime.GOOS == "windows" { @@ -238,10 +256,16 @@ func CheckForUpdates(ctx context.Context, currentVersion string, force bool) (*U } } - // Always save cache after successful network call, even if info is nil (meaning we are on latest) - _ = SaveUpdateCache(info) + // Always save cache after successful network call, even if update info is nil (meaning we are on latest) + if err := SaveUpdateCache(info); err != nil { + // Log error but don't fail the update check itself + fmt.Fprintf(os.Stderr, "[WARN] Failed to save update cache: %v\n", err) + } - return info, nil + if updateAvailable { + return info, nil + } + return nil, nil } // DownloadAndVerify downloads the archive and checks its integrity. @@ -447,11 +471,17 @@ func unzip(src, dest string) error { defer r.Close() for _, f := range r.File { - fpath := filepath.Join(dest, f.Name) + // Normalize ZIP entry name to prevent ZipSlip, including backslash separators + cleanName := strings.ReplaceAll(f.Name, "\\", "/") + cleanName = filepath.Clean(cleanName) + + cleanDest := filepath.Clean(dest) + fpath := filepath.Join(cleanDest, cleanName) - // Check for ZipSlip (Directory traversal) - if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { - return fmt.Errorf("illegal file path: %s", fpath) + // Check for ZipSlip (Directory traversal) using filepath.Rel + rel, err := filepath.Rel(cleanDest, fpath) + if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { + return fmt.Errorf("illegal file path: %s", f.Name) } // Security: Reject non-regular files (symlinks, devices, etc.) diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go index 616144d..7e92de8 100644 --- a/internal/updater/updater_test.go +++ b/internal/updater/updater_test.go @@ -8,6 +8,11 @@ import ( ) func TestGetCachePath(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", tempDir) + t.Setenv("AppData", tempDir) + t.Setenv("HOME", tempDir) + path, err := getCachePath() if err != nil { t.Fatalf("getCachePath failed: %v", err) @@ -18,14 +23,9 @@ func TestGetCachePath(t *testing.T) { } dir := filepath.Dir(path) - info, err := os.Stat(dir) - if err != nil { + if _, err := os.Stat(dir); err != nil { t.Fatalf("Cache directory does not exist: %v", err) } - - if !info.IsDir() { - t.Errorf("Expected %s to be a directory", dir) - } } func TestSaveAndGetUpdateCache(t *testing.T) { From acf1c99684d47c02fb19ad51d331324411d0305a Mon Sep 17 00:00:00 2001 From: doITmagic Date: Tue, 10 Feb 2026 02:12:49 +0200 Subject: [PATCH 12/13] fix: address remaining PR review comments for updater and state management --- .gitignore | 4 ++++ cmd/install/main.go | 4 +++- cmd/rag-code-mcp/main.go | 21 ++++++++++++------- internal/tools/updates.go | 12 +++++++++-- internal/updater/updater.go | 40 ++++++++++++++++++------------------- internal/workspace/state.go | 4 ++-- startup_debug.txt | 4 ---- windsurf_debug_log.txt | 3 --- 8 files changed, 52 insertions(+), 40 deletions(-) delete mode 100644 startup_debug.txt delete mode 100644 windsurf_debug_log.txt diff --git a/.gitignore b/.gitignore index bd82195..989d0e6 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,7 @@ temp/ scripts/ .ragcode/ .agent/skills/ + +# Debug logs +startup_debug.txt +windsurf_debug_log.txt diff --git a/cmd/install/main.go b/cmd/install/main.go index 53ee054..fdf2356 100644 --- a/cmd/install/main.go +++ b/cmd/install/main.go @@ -718,7 +718,9 @@ func addToPath(binDir string) { } if _, err := f.WriteString(fmt.Sprintf("\nexport PATH=\"%s:$PATH\"\n", binDir)); err != nil { warn(fmt.Sprintf("Could not write to shell config: %v", err)) - f.Close() + if cerr := f.Close(); cerr != nil { + warn(fmt.Sprintf("Could not finalise shell config after write failure: %v", cerr)) + } return } if err := f.Close(); err != nil { diff --git a/cmd/rag-code-mcp/main.go b/cmd/rag-code-mcp/main.go index 9150a20..f1dca3f 100644 --- a/cmd/rag-code-mcp/main.go +++ b/cmd/rag-code-mcp/main.go @@ -444,7 +444,19 @@ func main() { if strings.HasSuffix(info.AssetURL, ".zip") { ext = ".zip" } - tempFile := filepath.Join(os.TempDir(), "ragcode_update"+ext) + // Create a unique temporary file securely + tmp, err := os.CreateTemp("", "ragcode_update_*"+ext) + if err != nil { + log.Fatalf("Failed to create temporary file for update: %v", err) + } + tempFile := tmp.Name() + // We only need the path; close the file descriptor + if err := tmp.Close(); err != nil { + log.Fatalf("Failed to close temporary file for update: %v", err) + } + // Ensure the temporary file is removed after applying the update + defer os.Remove(tempFile) + if err := info.DownloadAndVerify(context.Background(), tempFile); err != nil { log.Fatalf("Update failed: %v", err) } @@ -474,12 +486,7 @@ func main() { } // Background update check - go func() { - info, err := updater.CheckForUpdates(context.Background(), Version, false) - if err == nil && info != nil { - logger.Info("🌟 New version available: %s. Run 'rag-code-mcp --update' or use the 'apply_update' tool to upgrade.", info.LatestVersion) - } - }() + triggerBackgroundUpdateCheck() // Apply logging settings from config unless env vars already override them applyLoggingConfig(cfg.Logging) diff --git a/internal/tools/updates.go b/internal/tools/updates.go index 60798c3..dbe0b7e 100644 --- a/internal/tools/updates.go +++ b/internal/tools/updates.go @@ -53,7 +53,12 @@ func (t *ApplyUpdateTool) Description() string { } func (t *ApplyUpdateTool) Execute(ctx context.Context, args map[string]interface{}) (string, error) { - info, err := updater.CheckForUpdates(ctx, t.version, true) + force := true // default to true for apply_update if not specified + if f, ok := args["force"].(bool); ok { + force = f + } + + info, err := updater.CheckForUpdates(ctx, t.version, force) if err != nil { return "", err } @@ -73,7 +78,10 @@ func (t *ApplyUpdateTool) Execute(ctx context.Context, args map[string]interface return "", fmt.Errorf("failed to create temp file: %w", err) } tempPath := tempFile.Name() - tempFile.Close() // Close immediately, DownloadAndVerify re-opens/overwrites it + if err := tempFile.Close(); err != nil { // Close immediately, DownloadAndVerify re-opens/overwrites it + os.Remove(tempPath) + return "", fmt.Errorf("failed to close temp file: %w", err) + } defer os.Remove(tempPath) if err := info.DownloadAndVerify(ctx, tempPath); err != nil { diff --git a/internal/updater/updater.go b/internal/updater/updater.go index c09ef21..5f98b61 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -127,11 +127,9 @@ func SaveUpdateCache(info *UpdateInfo) error { return err } - // Set restrictive permissions + // Set restrictive permissions; treat as best-effort if it fails (notably on some filesystems or Windows) if err := os.Chmod(path, 0600); err != nil { - // Non-fatal, just log or ignore if we really don't care, - // but checking it satisfies the linter and is better practice. - return fmt.Errorf("failed to set restrictive permissions on cache: %w", err) + fmt.Fprintf(os.Stderr, "[WARN] Failed to set restrictive permissions on cache: %v\n", err) } success = true @@ -231,29 +229,29 @@ func CheckForUpdates(ctx context.Context, currentVersion string, force bool) (*U Tag: release.TagName, } - updateAvailable := false - if latest.GreaterThan(curr) { - updateAvailable = true - // Match asset for current platform - archiveName := fmt.Sprintf("rag-code-mcp_%s_%s", runtime.GOOS, runtime.GOARCH) - if runtime.GOOS == "windows" { - archiveName += ".zip" - } else { - archiveName += ".tar.gz" - } + // Always populate asset and checksum URLs if found in release + archiveName := fmt.Sprintf("rag-code-mcp_%s_%s", runtime.GOOS, runtime.GOARCH) + if runtime.GOOS == "windows" { + archiveName += ".zip" + } else { + archiveName += ".tar.gz" + } - for _, asset := range release.Assets { - if asset.Name == archiveName { - info.AssetURL = asset.BrowserDownloadURL - } - if asset.Name == "checksums.txt" { - info.ChecksumURL = asset.BrowserDownloadURL - } + for _, asset := range release.Assets { + if asset.Name == archiveName { + info.AssetURL = asset.BrowserDownloadURL } + if asset.Name == "checksums.txt" { + info.ChecksumURL = asset.BrowserDownloadURL + } + } + updateAvailable := false + if latest.GreaterThan(curr) { if info.AssetURL == "" { return nil, fmt.Errorf("no asset found for platform %s/%s", runtime.GOOS, runtime.GOARCH) } + updateAvailable = true } // Always save cache after successful network call, even if update info is nil (meaning we are on latest) diff --git a/internal/workspace/state.go b/internal/workspace/state.go index eb08e77..6672c9a 100644 --- a/internal/workspace/state.go +++ b/internal/workspace/state.go @@ -55,8 +55,8 @@ func LoadState(path string) (*WorkspaceState, error) { // SaveState saves workspace state to disk func (s *WorkspaceState) Save(path string) error { - s.mu.RLock() - defer s.mu.RUnlock() + s.mu.Lock() + defer s.mu.Unlock() dir := filepath.Dir(path) if err := os.MkdirAll(dir, 0755); err != nil { diff --git a/startup_debug.txt b/startup_debug.txt deleted file mode 100644 index 85e4402..0000000 --- a/startup_debug.txt +++ /dev/null @@ -1,4 +0,0 @@ -Time: 2026-02-06 14:49:03.911017622 +0200 EET m=+0.001824994 -Exe: /home/razvan/.local/share/ragcode/bin/rag-code-mcp -CWD: /home/razvan -Args: [/home/razvan/.local/share/ragcode/bin/rag-code-mcp] diff --git a/windsurf_debug_log.txt b/windsurf_debug_log.txt deleted file mode 100644 index ff4a251..0000000 --- a/windsurf_debug_log.txt +++ /dev/null @@ -1,3 +0,0 @@ -TIME: 2026-02-06T14:52:48+02:00 | OP: InitializedHandler -Running extract roots... -ERROR: Failed to list roots: calling "roots/list": no roots handler configured From ce6d98db08897065ae7ec028352e975425cd2731 Mon Sep 17 00:00:00 2001 From: doITmagic Date: Thu, 16 Apr 2026 20:14:35 +0300 Subject: [PATCH 13/13] fix: truncate embed text to prevent context length overflow (issue #53) Add buildEmbedText() helper that caps embed payload at 30 000 chars (~7 700 tokens, safe for 8 192-token models like nomic-embed-text). Metadata (docstring + signature) is always preserved in full; only the Code body is truncated when the total exceeds the limit. A WARN is logged whenever truncation occurs so users can identify large symbols. Fixes #53 --- internal/ragcode/indexer.go | 65 ++++++++++++-- internal/ragcode/indexer_embed_test.go | 113 +++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 internal/ragcode/indexer_embed_test.go diff --git a/internal/ragcode/indexer.go b/internal/ragcode/indexer.go index c9fd8f3..4875501 100644 --- a/internal/ragcode/indexer.go +++ b/internal/ragcode/indexer.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "hash/fnv" + "log" "path/filepath" "strings" @@ -13,6 +14,57 @@ import ( "github.com/doITmagic/rag-code-mcp/internal/memory" ) +// maxEmbedChars is the maximum number of Unicode characters sent to the embedding +// model. Common models (e.g. nomic-embed-text) have an 8 192-token context window +// (~4 chars/token → ~32 768 chars). We use 30 000 to give ~6% headroom and stay +// compatible with smaller-window models. +const maxEmbedChars = 30_000 + +// buildEmbedText constructs the text to embed for a CodeChunk, then truncates it +// to maxChars (rune-safe, UTF-8 correct) to avoid exceeding the model's context +// window. Metadata (docstring, signature) is always preserved in full; only Code +// is truncated when the total exceeds maxChars. +// Returns (text, wasTruncated). +func buildEmbedText(ch codetypes.CodeChunk, maxChars int) (string, bool) { + meta := strings.TrimSpace(strings.Join(filterNonEmpty([]string{ + ch.Docstring, + ch.Signature, + }), "\n\n")) + + var full string + if ch.Code != "" { + if meta != "" { + full = meta + "\n\n" + ch.Code + } else { + full = ch.Code + } + } else { + full = meta + } + + runes := []rune(full) + if len(runes) <= maxChars { + return full, false + } + + // Truncate only the Code portion — keep metadata intact. + metaWithSep := meta + if meta != "" && ch.Code != "" { + metaWithSep = meta + "\n\n" + } + metaRunes := []rune(metaWithSep) + remaining := maxChars - len(metaRunes) + if remaining < 0 { + remaining = 0 + } + codeRunes := []rune(ch.Code) + if remaining > len(codeRunes) { + remaining = len(codeRunes) + } + truncated := metaWithSep + string(codeRunes[:remaining]) + return truncated, true +} + // Indexer indexes CodeChunks into LongTermMemory using an embedding Provider. type Indexer struct { analyzer codetypes.PathAnalyzer @@ -34,14 +86,17 @@ func (i *Indexer) IndexPaths(ctx context.Context, paths []string, sourceTag stri indexed := 0 for _, ch := range chunks { - text := strings.TrimSpace(strings.Join(filterNonEmpty([]string{ - ch.Docstring, - ch.Signature, - ch.Code, - }), "\n\n")) + text, wasTruncated := buildEmbedText(ch, maxEmbedChars) + text = strings.TrimSpace(text) if text == "" { continue } + if wasTruncated { + // Log a warning so users know which symbols were partially indexed. + // The full code is still accessible via rag_read_file_context. + log.Printf("[WARN] embed text truncated for %s (%s:%d-%d) — content exceeds model context window", + ch.Name, filepath.Base(ch.FilePath), ch.StartLine, ch.EndLine) + } emb, err := i.embedder.Embed(ctx, text) if err != nil { diff --git a/internal/ragcode/indexer_embed_test.go b/internal/ragcode/indexer_embed_test.go new file mode 100644 index 0000000..9edfcf8 --- /dev/null +++ b/internal/ragcode/indexer_embed_test.go @@ -0,0 +1,113 @@ +package ragcode + +import ( + "strings" + "testing" + + "github.com/doITmagic/rag-code-mcp/internal/codetypes" +) + +func TestBuildEmbedText_NoTruncation(t *testing.T) { + ch := codetypes.CodeChunk{ + Package: "mypkg", + Name: "MyFunc", + Signature: "func MyFunc() string", + Code: "return \"hello\"", + Docstring: "MyFunc returns hello.", + FilePath: "foo.go", + } + text, truncated := buildEmbedText(ch, maxEmbedChars) + if truncated { + t.Fatal("expected no truncation for small chunk") + } + if !strings.Contains(text, ch.Docstring) { + t.Errorf("embed text missing docstring") + } + if !strings.Contains(text, ch.Signature) { + t.Errorf("embed text missing signature") + } + if !strings.Contains(text, ch.Code) { + t.Errorf("embed text missing code") + } +} + +func TestBuildEmbedText_TruncatesCode(t *testing.T) { + bigCode := strings.Repeat("x", 40_000) + ch := codetypes.CodeChunk{ + Package: "mypkg", + Name: "BigFunc", + Signature: "func BigFunc()", + Code: bigCode, + FilePath: "big.go", + } + limit := 30_000 + text, truncated := buildEmbedText(ch, limit) + if !truncated { + t.Fatal("expected truncation for large code body") + } + runes := []rune(text) + if len(runes) > limit { + t.Errorf("truncated text has %d runes, want <= %d", len(runes), limit) + } + // Signature must survive truncation + if !strings.Contains(text, "func BigFunc()") { + t.Errorf("embed text missing signature after truncation") + } +} + +func TestBuildEmbedText_WithDocstring_TruncatesCode(t *testing.T) { + bigCode := strings.Repeat("y", 40_000) + ch := codetypes.CodeChunk{ + Signature: "func Fn()", + Code: bigCode, + Docstring: "This is a docstring.", + FilePath: "fn.go", + } + limit := 30_000 + text, truncated := buildEmbedText(ch, limit) + if !truncated { + t.Fatal("expected truncation") + } + if !strings.Contains(text, "This is a docstring.") { + t.Errorf("docstring missing after truncation") + } + if len([]rune(text)) > limit { + t.Errorf("text exceeds limit after truncation") + } +} + +func TestBuildEmbedText_ExactlyAtLimit(t *testing.T) { + limit := 100 + // meta = "func Fn()\n\n" → 11 chars + meta := "func Fn()\n\n" + codeLen := limit - len([]rune(meta)) + ch := codetypes.CodeChunk{ + Signature: "func Fn()", + Code: strings.Repeat("a", codeLen), + FilePath: "fn.go", + } + text, truncated := buildEmbedText(ch, limit) + if truncated { + t.Fatalf("expected no truncation at exact boundary, got truncated; len=%d", len([]rune(text))) + } + _ = text +} + +func TestBuildEmbedText_EmptyCode(t *testing.T) { + ch := codetypes.CodeChunk{ + Signature: "func Empty()", + Code: "", + Docstring: "Empty function.", + FilePath: "empty.go", + } + text, truncated := buildEmbedText(ch, maxEmbedChars) + if truncated { + t.Fatal("expected no truncation for empty code") + } + if !strings.Contains(text, "Empty function.") { + t.Errorf("docstring missing") + } + if !strings.Contains(text, "func Empty()") { + t.Errorf("signature missing") + } +}