Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eed9a9165c | ||
|
|
cfcd42f41e | ||
|
|
7c9e901c12 | ||
|
|
9f00d8298f | ||
|
|
e006ef1220 | ||
|
|
26eaf03b6f | ||
|
|
2cfaeda0cb | ||
|
|
8ff43487bc | ||
|
|
a9ed5ae9c8 | ||
|
|
48f49d5d02 | ||
|
|
2ae343d968 | ||
|
|
27416e4ee9 | ||
|
|
4bfb3b6359 | ||
|
|
36b86e5b92 | ||
|
|
0c2fc96ff8 | ||
|
|
4ea8f861cc |
No files matched your search
@@ -1,65 +0,0 @@
|
|||||||
# Integrating MCP Memory: A Strategy Guide for LLMs and Agents
|
|
||||||
|
|
||||||
This guide documents the approach and rationale for integrating the `mcp-memory` server with agentic LLMs (like Antigravity). Because `mcp-memory` is a central hub for context, tasks, and environment state, it is critical that LLMs interact with it efficiently without exhausting their primary context window or causing workflow ambiguity.
|
|
||||||
|
|
||||||
## 1. The Core Philosophy: "The Central Brain"
|
|
||||||
The `mcp-memory` server is the persistence layer for the AI development lifecycle. It holds:
|
|
||||||
* **The Knowledge Graph:** Code changes, bug fixes, architecture decisions, and tech debt.
|
|
||||||
* **Project State:** Milestones, tasks, acceptance criteria, and PR checklists.
|
|
||||||
* **Environment State:** Handoff memos, standup reports, and environment fingerprints.
|
|
||||||
* **Live UI Integrations:** Neovim buffer manipulation and user-action webhooks.
|
|
||||||
|
|
||||||
**Rationale:** The LLM's context window is ephemeral and expensive. By pushing state to the `mcp-memory` server (via a local database and Tantivy index), the LLM can selectively retrieve only what it needs, when it needs it.
|
|
||||||
|
|
||||||
## 2. Global Rules vs. Subagents
|
|
||||||
To maximize efficiency, we split interactions into two categories: **Synchronous Rules** (executed by the primary conversational agent) and **Asynchronous Subagents** (delegated background tasks).
|
|
||||||
|
|
||||||
### A. Synchronous Rules (The Primary Loop)
|
|
||||||
The main LLM interacting with the user should be constrained by global system rules to ensure basic context synchronization. These actions must happen synchronously so the main agent never loses the plot.
|
|
||||||
|
|
||||||
* **Context Initialization (`tasks` list, `get_preflight_context`):** Executed when a session starts. This gives the LLM immediate awareness of the current workflow.
|
|
||||||
* **Context Switching (`manage_checkpoint`):** Executed when moving between branches or large features. This prevents context bleed between disparate tasks.
|
|
||||||
* **End-of-Day Handoff (`add_session_summary`, `generate_standup_report`):** Triggered when the user logs off, seamlessly serializing the mental state of the LLM for tomorrow.
|
|
||||||
|
|
||||||
### B. Subagent Orchestration (The Background Team)
|
|
||||||
Heavy or verbose interactions with the MCP server are delegated to specialized background subagents. This keeps the primary chat fast and focused on the code, while the "team" handles project management.
|
|
||||||
|
|
||||||
#### 1. `MemoryLibrarian` (The Graph Curator)
|
|
||||||
* **Role:** Analyzes git diffs and chat history to structure the Knowledge Graph.
|
|
||||||
* **Tools:** `log_code_change`, `log_error_fix`, `create_entities`, `log_tech_debt`.
|
|
||||||
* **Rationale:** Parsing diffs and determining entity relationships is token-heavy. Delegating this prevents the main agent from wasting reasoning cycles on database normalization.
|
|
||||||
|
|
||||||
#### 2. `ScrumMaster` (The Project Manager)
|
|
||||||
* **Role:** Manages the task lifecycle and acceptance criteria.
|
|
||||||
* **Tools:** `add_task`, `update_task_status`, `add_milestone`, `verify_acceptance_criteria`.
|
|
||||||
* **Rationale:** The main agent shouldn't have to repeatedly query "are we done yet?" The `ScrumMaster` runs alongside the session, validating criteria in the background and updating the board autonomously.
|
|
||||||
|
|
||||||
#### 3. `DevOpsSRE` (The Environment Manager)
|
|
||||||
* **Role:** Monitors dependencies and manages session transitions.
|
|
||||||
* **Tools:** `update_env_fingerprint`, `leave_handoff_memo`.
|
|
||||||
* **Rationale:** Prevents "it works on my machine" failures by passively updating fingerprints when build files (e.g., `Cargo.toml`) change.
|
|
||||||
|
|
||||||
## 3. Graceful Degradation & Server Resilience
|
|
||||||
The `mcp-memory` server is a distinct background process (typically port 3000). The LLM ecosystem must handle server downtime gracefully:
|
|
||||||
|
|
||||||
1. **Event Webhooks:** If the server goes down, waiting webhook tasks (e.g., waiting for a user to save a file in Neovim) will drop. These *do not* self-heal. The LLM must recognize the dropped connection and prompt the user to retry the action.
|
|
||||||
2. **Persistent Storage:** Data (tasks, graph, ledger) is persisted to `mcp_store.redb`. When the server comes back online, no data is lost. The LLM can immediately resume querying.
|
|
||||||
3. **Subagent Fast-Failing:** If the `MemoryLibrarian` attempts to log a change while the server is offline, it will instantly fail. It is designed to abandon the background task and notify the primary agent. To recover, the primary agent can manually re-invoke the Librarian once the connection is restored, instructing it to analyze recent commits to backfill the graph.
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
By treating `mcp-memory` as the durable brain, and enforcing a strict division of labor between the primary agent loop and background subagents, we achieve a highly autonomous, highly resilient AI pair-programming environment that scales across long-running projects and multiple terminal sessions.
|
|
||||||
|
|
||||||
## 4. MCP Feature Differentiation (Cognitive Boundaries)
|
|
||||||
The MCP protocol exposes three primary primitives. To prevent LLM confusion and API hallucination, the LLM must strictly adhere to the following interaction boundaries:
|
|
||||||
|
|
||||||
### A. Tools (For Stateful Mutation)
|
|
||||||
* **When to use:** Use tools *exclusively* for mutating state (e.g., dd_task, log_code_change) or for highly targeted semantic searches (e.g., search_nodes, query_graph_path).
|
|
||||||
* **LLM Awareness:** The LLM must not use tools to repeatedly poll for state changes. Tools represent active, expensive computing steps.
|
|
||||||
|
|
||||||
### B. Resources (For Passive Awareness)
|
|
||||||
* **When to use:** Use URIs (e.g., memory://tasks/active, memory://session/delta) to read holistic project state.
|
|
||||||
* **LLM Awareness:** The client integration should map these URIs to the LLM's context window. Instead of the LLM invoking a list_active_tasks tool (which costs a round-trip), the LLM should simply read the memory://tasks/active resource content if it needs to know what to do next. Resources are for passive, zero-cost reading.
|
|
||||||
|
|
||||||
### C. Prompts (For Macro-Workflows)
|
|
||||||
* **When to use:** Use server-defined prompts to execute complex, multi-step routines that require bundled context.
|
|
||||||
* **LLM Awareness:** Instead of the user or main agent trying to manually figure out the correct sequence of tools to end a session, the LLM should trigger the handoff_routine prompt. The server will respond with a strictly formatted message array that perfectly primes the LLM on exactly what to do next. Prompts act as "macro-instructions" to prevent the LLM from wandering off-script during complex transitions.
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# Architecture Design: MCP Resources & Prompts
|
|
||||||
|
|
||||||
## 1. Current Architecture (Tools)
|
|
||||||
Currently, the `mcp-memory` server handles MCP tools using an elegant trait-based approach in `router.rs`:
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
pub trait McpTool: Send + Sync {
|
|
||||||
fn name(&self) -> &'static str;
|
|
||||||
fn schema(&self) -> Value;
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Tools are registered into a `HashMap<String, Box<dyn McpTool>>` within the `MemoryHandler`. This prevents the main JSON-RPC match block from becoming a monolithic switch statement.
|
|
||||||
|
|
||||||
## 2. The Problem
|
|
||||||
Currently, the `resources/list`, `resources/read`, `prompts/list`, and `prompts/get` endpoints are hardcoded directly inside the `MemoryHandler::handle_request` match block in `router.rs`.
|
|
||||||
As we expand our usage of Resources (to expose the database state dynamically) and Prompts (to bundle complex workflows), continuing to hardcode them in `router.rs` will result in massive code duplication and tearup.
|
|
||||||
|
|
||||||
## 3. The Proposed Solution (Trait Extensibility)
|
|
||||||
We will replicate the success of the `McpTool` trait by introducing `McpResource` and `McpPrompt` traits.
|
|
||||||
|
|
||||||
### A. MCP Resources
|
|
||||||
**Trait Definition (`router.rs` or `resources.rs`):**
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
pub trait McpResource: Send + Sync {
|
|
||||||
/// The exact URI the client requests (e.g. "memory://tasks/active")
|
|
||||||
fn uri(&self) -> &'static str;
|
|
||||||
|
|
||||||
/// Human-readable name for the client UI
|
|
||||||
fn name(&self) -> &'static str;
|
|
||||||
|
|
||||||
/// Description for the client UI
|
|
||||||
fn description(&self) -> Option<&'static str> { None }
|
|
||||||
|
|
||||||
/// Mime type of the content (usually "application/json" or "text/markdown")
|
|
||||||
fn mime_type(&self) -> Option<&'static str> { Some("application/json") }
|
|
||||||
|
|
||||||
/// Retrieve the resource content
|
|
||||||
async fn read(&self, state: Arc<MemoryState>) -> Result<String, String>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Implementation:**
|
|
||||||
* Add `pub resources: std::collections::HashMap<String, Box<dyn McpResource>>` to `MemoryHandler`.
|
|
||||||
* In `handle_request("resources/list")`, iterate over `self.resources.values()` and build the JSON payload.
|
|
||||||
* In `handle_request("resources/read")`, lookup the requested URI in `self.resources` and call `.read(state).await`.
|
|
||||||
* Move the existing `memory://graph/entities` logic into its own handler struct.
|
|
||||||
|
|
||||||
### B. MCP Prompts
|
|
||||||
**Trait Definition (`router.rs` or `prompts.rs`):**
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
pub trait McpPrompt: Send + Sync {
|
|
||||||
/// The unique name of the prompt (e.g. "analyze_tech_debt")
|
|
||||||
fn name(&self) -> &'static str;
|
|
||||||
|
|
||||||
/// Description for the client UI
|
|
||||||
fn description(&self) -> Option<&'static str> { None }
|
|
||||||
|
|
||||||
/// Schema or array defining arguments (can default to empty)
|
|
||||||
fn arguments(&self) -> serde_json::Value { serde_json::json!([]) }
|
|
||||||
|
|
||||||
/// Execute the prompt and return the `messages` array payload
|
|
||||||
async fn get(&self, args: Value, state: Arc<MemoryState>) -> Result<serde_json::Value, String>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**Implementation:**
|
|
||||||
* Add `pub prompts: std::collections::HashMap<String, Box<dyn McpPrompt>>` to `MemoryHandler`.
|
|
||||||
* In `handle_request("prompts/list")`, map over `self.prompts.values()`.
|
|
||||||
* In `handle_request("prompts/get")`, call `.get(args, state).await`.
|
|
||||||
|
|
||||||
## 4. Execution Plan
|
|
||||||
1. **Refactor `router.rs` (No functional changes yet):** Define the `McpResource` and `McpPrompt` traits. Update the `MemoryHandler` struct to hold these HashMaps. Migrate the existing hardcoded stubs (`memory://graph/entities` and `analyze_tech_debt`) into structs implementing these traits.
|
|
||||||
2. **Expand Resources (Phase 1):** Add new handlers for `memory://tasks/active`, `memory://session/delta`, etc.
|
|
||||||
3. **Expand Prompts (Phase 2):** Add new handlers for `handoff_routine`, etc.
|
|
||||||
|
|
||||||
This design guarantees we do not needlessly tear up code—we merely extend the existing robust `McpTool` pattern to the rest of the protocol.
|
|
||||||
Generated
+40
-4
@@ -2651,7 +2651,7 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"rmcp",
|
"rmcp",
|
||||||
"rten",
|
"rten",
|
||||||
"schemars",
|
"schemars 1.2.2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
@@ -3009,6 +3009,7 @@ dependencies = [
|
|||||||
"mcp-stdio",
|
"mcp-stdio",
|
||||||
"rmcp",
|
"rmcp",
|
||||||
"rmpv",
|
"rmpv",
|
||||||
|
"schemars 0.8.22",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -3859,7 +3860,7 @@ dependencies = [
|
|||||||
"pastey 0.2.3",
|
"pastey 0.2.3",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rmcp-macros",
|
"rmcp-macros",
|
||||||
"schemars",
|
"schemars 1.2.2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
@@ -4179,6 +4180,18 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars"
|
||||||
|
version = "0.8.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||||
|
dependencies = [
|
||||||
|
"dyn-clone",
|
||||||
|
"schemars_derive 0.8.22",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -4188,11 +4201,23 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"ref-cast",
|
"ref-cast",
|
||||||
"schemars_derive",
|
"schemars_derive 1.2.2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schemars_derive"
|
||||||
|
version = "0.8.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde_derive_internals 0.29.1",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars_derive"
|
name = "schemars_derive"
|
||||||
version = "1.2.2"
|
version = "1.2.2"
|
||||||
@@ -4201,7 +4226,7 @@ checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde_derive_internals",
|
"serde_derive_internals 0.30.0",
|
||||||
"syn 3.0.6",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4276,6 +4301,17 @@ dependencies = [
|
|||||||
"syn 3.0.6",
|
"syn 3.0.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive_internals"
|
||||||
|
version = "0.29.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive_internals"
|
name = "serde_derive_internals"
|
||||||
version = "0.30.0"
|
version = "0.30.0"
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
# Effective Discourse: LLM Prompting Guide for MCP Memory
|
|
||||||
|
|
||||||
To get the most out of the Antigravity MCP Memory server and its advanced developer tools, use specific phrases that clearly state your intent. This guides the LLM to use the most efficient tools, reducing token consumption, speeding up time-to-resolve (T2R), and avoiding brute-force file reading.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Global Semantic Code Search & Navigation
|
|
||||||
Instead of having the LLM use brute-force text searches or `grep` to find abstract logic, instruct it to use local vector embeddings.
|
|
||||||
* **Don't say:** "Grep the codebase for database connection strings."
|
|
||||||
* **Do say:** "Perform a semantic code search for how database connections are established."
|
|
||||||
* **Tool Triggered:** `semantic_code_search`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Subgraph Expansion & Neighborhood Exploration
|
|
||||||
When inspecting complex system interactions or module dependencies around a target component.
|
|
||||||
* **Don't say:** "Tell me everything connected to the DatabaseTable node."
|
|
||||||
* **Do say:** "Get the subgraph expansion around 'DatabaseTable' up to 2 hops."
|
|
||||||
* **Tool Triggered:** `get_subgraph`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Automated Error Fix Suggestions
|
|
||||||
When encountering build failures, runtime crashes, or stack traces.
|
|
||||||
* **Don't say:** "Here is a stack trace, let's debug from scratch: [paste stack trace]"
|
|
||||||
* **Do say:** "Suggest an error fix for this stack trace before we start debugging."
|
|
||||||
* **Tool Triggered:** `suggest_error_fix`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Memory State Checkpointing & Safety Rollbacks
|
|
||||||
Before initiating risky refactors or running experimental multi-step subagents.
|
|
||||||
* **Don't say:** "Hope this refactor doesn't mess up our task board or memory graph."
|
|
||||||
* **Do say:** "Checkpoint the memory state under 'pre-refactor' before we begin."
|
|
||||||
* **Tool Triggered:** `checkpoint_state` / `restore_state`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Codebase Exploration & Token Efficiency
|
|
||||||
When entering a new file, avoid having the LLM read the entire contents blindly.
|
|
||||||
* **Don't say:** "Read server.rs and tell me what it does." *(Consumes massive tokens)*
|
|
||||||
* **Do say:** "Extract the AST skeleton of server.rs to understand its structure first."
|
|
||||||
* **Tool Triggered:** `read_file_skeleton`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Debugging & Log Parsing
|
|
||||||
Stop copy-pasting giant walls of logs into the chat interface.
|
|
||||||
* **Don't say:** "Here is the error: [paste 500 lines of logs]"
|
|
||||||
* **Do say:** "The daemon crashed. Fetch the recent logs from daemon.log." or "Watch the process logs for server.log."
|
|
||||||
* **Tool Triggered:** `process_logs` (action: "get", action: "watch")
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Git & Context Handoff
|
|
||||||
When you've been working independently and need to loop the LLM back in on your current state.
|
|
||||||
* **Don't say:** "I changed some files, here are the diffs..."
|
|
||||||
* **Do say:** "Get the active git worktree context to review my uncommitted changes before we continue."
|
|
||||||
* **Tool Triggered:** `get_active_worktree_context`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Inter-Agent Signal Bus & Coordination
|
|
||||||
When multiple subagents collaborate or background tasks complete.
|
|
||||||
* **Don't say:** "Let me manually tell the ScrumMaster that testing finished."
|
|
||||||
* **Do say:** "Broadcast an agent signal that unit tests passed and ping the PrePushAuditor."
|
|
||||||
* **Tool Triggered:** `agent_signals` (action: "broadcast", action: "query")
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Structural AST Editing
|
|
||||||
When asking the LLM to modify complex files, prevent indentation bugs and regex failures by guiding it to use tree-sitter.
|
|
||||||
* **Don't say:** "Search for `fn process()` and replace it with this string."
|
|
||||||
* **Do say:** "Use the AST node replacer to swap out the `process` function in `server.rs`."
|
|
||||||
* **Tool Triggered:** `replace_ast_node`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Bird's-Eye Repository Exploration
|
|
||||||
When the LLM is first analyzing a repository, don't let it run `ls -R` and guess.
|
|
||||||
* **Don't say:** "List the files in the directory and guess where the database code is."
|
|
||||||
* **Do say:** "Read the directory architecture to get a summary of what each file is responsible for."
|
|
||||||
* **Tool Triggered:** `read_directory_architecture`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Graph, Memory & Casing Standards
|
|
||||||
Actively instruct the LLM to maintain its memory constraints and use canonical casing.
|
|
||||||
* **Do say:** "Log this architectural decision in the knowledge graph using PascalCase for entity types."
|
|
||||||
* **Do say:** "Leave a handoff memo with the test database credentials and session action items."
|
|
||||||
* **Do say:** "Create a milestone for the 'Rich Clipboard' feature and break it down into active tasks."
|
|
||||||
* **Tools Triggered:** `create_entities`, `decisions` (log), `handoff_memos` (leave), `milestones` (add), `tasks` (add)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Self-Healing Graph Maintenance
|
|
||||||
* **Don't say:** "Search for duplicates and orphaned entities in the graph manually."
|
|
||||||
* **Do say:** "Sweep graph health to identify orphaned entities and duplicate candidates."
|
|
||||||
* **Tool Triggered:** `sweep_graph_health`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 13. Causal Lineage & Provenance
|
|
||||||
* **Don't say:** "Search all tasks, ADRs, and commits to explain why this file was changed."
|
|
||||||
* **Do say:** "Query lineage for `server/src/state.rs` to construct a causal timeline."
|
|
||||||
* **Tool Triggered:** `query_lineage`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 14. Actionable Task Resolution
|
|
||||||
* **Don't say:** "List all tasks and figure out which ones are blocked."
|
|
||||||
* **Do say:** "Get the next actionable tasks to find unblocked work ready for execution."
|
|
||||||
* **Tool Triggered:** `get_next_actionable_tasks`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 15. Diagnostic Hypotheses & Reasoning Traces
|
|
||||||
* **Don't say:** "Let's test three guesses and remember what we tried in chat."
|
|
||||||
* **Do say:** "Log a hypothesis for this memory leak with tested evidence."
|
|
||||||
* **Tool Triggered:** `hypotheses` (action: "log", action: "query")
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 16. State Checkpoints & Rollbacks
|
|
||||||
* **Don't say:** "Save a backup snapshot before we do this refactor."
|
|
||||||
* **Do say:** "Create a checkpoint named 'pre-refactor' before editing the database layer."
|
|
||||||
* **Tool Triggered:** `manage_checkpoint`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
By phrasing requests around *actions* rather than *information retrieval*, the LLM is primed to leverage the rich MCP toolset built into the Antigravity Memory Server.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Role & Competency Constraints
|
|
||||||
|
|
||||||
- **Role**: Junior Developer.
|
|
||||||
- **Discretion**: NO autonomous discretion allowed. The agent must operate strictly under the direct guidance of the user and must not make autonomous decisions or execute sweeping, unchecked actions.
|
|
||||||
@@ -34,9 +34,9 @@ Before making expensive active tool calls, read available MCP resources:
|
|||||||
- **Error Fixes**: The moment a tricky, undocumented, or environment-specific bug is resolved, IMMEDIATELY call `log_error_fix`. Supply `repo_name`, `error_category`, and `stack_trace` so future searches can perform embedding-based match via `search_error_fixes`.
|
- **Error Fixes**: The moment a tricky, undocumented, or environment-specific bug is resolved, IMMEDIATELY call `log_error_fix`. Supply `repo_name`, `error_category`, and `stack_trace` so future searches can perform embedding-based match via `search_error_fixes`.
|
||||||
- **Tech Debt**: If you notice an anti-pattern but deliberately skip fixing it to focus on a feature, IMMEDIATELY call `tech_debt` (action: "log") with `description`, `file_path`, `line_range`, `workaround`, `effort_estimate`, and `severity`.
|
- **Tech Debt**: If you notice an anti-pattern but deliberately skip fixing it to focus on a feature, IMMEDIATELY call `tech_debt` (action: "log") with `description`, `file_path`, `line_range`, `workaround`, `effort_estimate`, and `severity`.
|
||||||
- **Architectural Decisions (ADR) & Lifecycle Closure**:
|
- **Architectural Decisions (ADR) & Lifecycle Closure**:
|
||||||
- When selecting design patterns, crate choices, or system structure, call `decisions` (action: "log") with `author`, `affected_components`, `alternatives_considered`, `decision`, and `consequence`.
|
- When selecting design patterns, crate choices, or system structure, call `decisions` (action: "log") with `author`, `affected_components`, `alternatives_considered`, `decision`, and `consequences`.
|
||||||
- **MANDATORY Definition of Done**: When code implementing an ADR is committed, you MUST IMMEDIATELY call `decisions` (action: "update", id: "ADR-XXXX", status: "implemented", git_commit: <commit_hash>, git_branch: <branch>). NEVER leave an ADR in `accepted` once the implementing code is committed. The repository also executes `scripts/git-reconcile.py` on post-commit hooks (`just install-git-hooks`) to reconcile commit references automatically.
|
- **MANDATORY Definition of Done**: When code implementing an ADR is committed, you MUST IMMEDIATELY call `decisions` (action: "update", id: "ADR-XXXX", status: "implemented", git_commit: <commit_hash>, git_branch: <branch>). NEVER leave an ADR in `accepted` once the implementing code is committed. The repository also executes `scripts/git-reconcile.py` on post-commit hooks (`just install-git-hooks`) to reconcile commit references automatically.
|
||||||
- **Task Management**: When creating tasks, supply `priority` ('low'|'medium'|'high'|'urgent'), `assigned_agent` (e.g. subagent role), `verification_command` (automated test command), and `acceptance_criteria`.
|
- **Task Management**: When creating tasks, supply `priority` ('low'|'medium'|'high'|'urgent'), `assigned_agent` (e.g. subagent role), `verification_command` (automated test command), and `criteria`.
|
||||||
- **VCS & SVN Agnosticism**: Supply `vcs_type` ('git'|'svn'|'hg'), `vcs_revision` (git hash or svn revision like 'r12345'), and `upstream_url` to `log_code_change` and workspace tools.
|
- **VCS & SVN Agnosticism**: Supply `vcs_type` ('git'|'svn'|'hg'), `vcs_revision` (git hash or svn revision like 'r12345'), and `upstream_url` to `log_code_change` and workspace tools.
|
||||||
- **Terminal & Shell Context**: Terminal sessions and commands are automatically tracked in the server over zero-latency UDP. Query `/terminal/history` or `memory://terminal/recent` when analyzing shell execution context.
|
- **Terminal & Shell Context**: Terminal sessions and commands are automatically tracked in the server over zero-latency UDP. Query `/terminal/history` or `memory://terminal/recent` when analyzing shell execution context.
|
||||||
- **Hypotheses & Root Cause Analysis**: When diagnosing complex bugs or race conditions, call `hypotheses` (action: "log" / "query") to record test evidence and maintain reasoning trails across sessions.
|
- **Hypotheses & Root Cause Analysis**: When diagnosing complex bugs or race conditions, call `hypotheses` (action: "log" / "query") to record test evidence and maintain reasoning trails across sessions.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Headless mode (`nvim --headless`) is strictly banned for interactive edits.
|
|||||||
Headless instances are allowed ONLY for non-interactive background batch processing (e.g., project-wide formatting or linting).
|
Headless instances are allowed ONLY for non-interactive background batch processing (e.g., project-wide formatting or linting).
|
||||||
|
|
||||||
## 4. UI Presentation & Chat Console Minimization
|
## 4. UI Presentation & Chat Console Minimization
|
||||||
Never output large plans, context blocks, or architectural discussions to the chat console if Neovim is running. You MUST use the `nvim_buffer` and `nvim_workspace` tools to open a vertical split (e.g., `Antigravity_Plan.md` scratch buffer) and present the markdown natively. Reserve the chat console strictly for brief confirmations.
|
Never output large plans, context blocks, or architectural discussions to the chat console if Neovim is running. You MUST use the `nvim_buffer` and `nvim_workspace` tools to open the markdown natively in the editor (e.g., `Antigravity_Plan.md` scratch buffer). Reserve the chat console strictly for brief confirmations.
|
||||||
|
|
||||||
## 5. Visual Cues & Auto-Save
|
## 5. Visual Cues & Auto-Save
|
||||||
When manipulating buffers via MCP:
|
When manipulating buffers via MCP:
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# Rule 1: Network Connectivity Testing
|
|
||||||
|
|
||||||
- **Mandatory Tooling**: When testing ANY network connectivity, you MUST use the exact same Rust crates used in the production code.
|
|
||||||
- **Methodology**: You must create standalone Rust applications (e.g., small test binaries or examples in the repository) that utilize the exact same networking plumbing as the main application.
|
|
||||||
- **Forbidden Tools**: You CANNOT use bash (e.g., curl, netcat), Python, Java, Perl, wscat, or any other external scripting/CLI tools to validate network connectivity. Testing with these tools creates false positives and bypasses the actual networking crates causing issues.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Rule 2: Empirical Evidence & Logging
|
|
||||||
|
|
||||||
- **Mandatory Logging**: If an issue occurs and there are no logs (or insufficient logs) to diagnose it, you must STOP immediately and add logging.
|
|
||||||
- **Replication**: After adding logging, you must repeat the test to replicate the exact issue so that the logs capture the failure.
|
|
||||||
- **No Blind Changes**: NO business logic can be altered blindly or based on guesses. You must have empirical evidence (derived from the logs) proving the root cause before attempting any changes to the logic.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Rule 3: Resiliency & Cross-OS Load Testing
|
|
||||||
|
|
||||||
- **Resiliency Requirement**: A fix must be resilient and work under load. It is not enough for it to work just once.
|
|
||||||
- **Mandatory 5x Matrix Testing**: Any request/response cycle must be explicitly tested five (5) times for each of the following boundaries:
|
|
||||||
1. Windows to Windows (`win - win`)
|
|
||||||
2. WSL to Windows (`wsl - win`)
|
|
||||||
- **Documentation**: The results of these load tests MUST be recorded permanently (e.g., in an artifact or log) to prove resilience and to avoid repeating needless re-tests.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Rule 4: Component Isolation & Skeletal Troubleshooting
|
|
||||||
|
|
||||||
- **Targeted Testing**: If a bug is found, DO NOT rely on testing the full stack. You must isolate and test the offending class or method directly.
|
|
||||||
- **Skeletal Reproducers**: If the issue spans across boundaries (e.g., between server, stub, or nvim), you must create skeletal (minimal reproducible) versions of those components.
|
|
||||||
- **Purpose**: Troubleshooting must be done on these skeletal versions to isolate the broken feature or aspect without the noise, side-effects, or overhead of the full application stack.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Rule 5: Client-Centric Testing Personas
|
|
||||||
|
|
||||||
- **Avoid Server Bias**: Unit tests and integration tests must NOT be exclusively server-centric.
|
|
||||||
- **Client Personas**: Tests must explicitly adopt the persona, perspective, and constraints of the client components (e.g., the `stub` or `nvim`).
|
|
||||||
- **Validation**: Testing must validate the interaction from the client's side, ensuring that the client component correctly constructs the request, handles the connection lifecycle, and properly parses the response, rather than just verifying that the server successfully processed an isolated payload.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
name: strict-no-verify
|
|
||||||
description: Strictly forbids the use of --no-verify or -n when interacting with git to ensure quality gates are run.
|
|
||||||
always_on: true
|
|
||||||
---
|
|
||||||
|
|
||||||
# STRICT GIT HOOK ENFORCEMENT
|
|
||||||
|
|
||||||
- **NEVER** use `--no-verify` or `-n` with `git commit` or `git push`.
|
|
||||||
- Bypassing git hooks is considered a critical violation of trust.
|
|
||||||
- You must allow the local git hooks (e.g., `pre-push`) to validate the code.
|
|
||||||
- If a hook fails (e.g., `ruff` check, `pytest`), you MUST fix the underlying issues iteratively until the hook passes natively. Do not assume a single fix attempt works without re-verifying.
|
|
||||||
- BEFORE running `git push`, you MUST use the `ask_question` tool to pop up an interactive modal to request explicit user permission. Wait for the user's approval before executing the push.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
Write-Host "Building Windows Server..." -ForegroundColor Cyan
|
|
||||||
Building Windows Server...
|
|
||||||
cargo build --release -p mcp-memory-server
|
|
||||||
Compiling tantivy v0.26.2
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
use std::process::Command;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let mut git_hash = Command::new("git")
|
|
||||||
.args(["rev-parse", "--short", "HEAD"])
|
|
||||||
.output()
|
|
||||||
.ok()
|
|
||||||
.and_then(|out| String::from_utf8(out.stdout).ok())
|
|
||||||
.unwrap_or_else(|| "unknown".to_string());
|
|
||||||
|
|
||||||
git_hash = git_hash.trim().to_string();
|
|
||||||
|
|
||||||
let is_dirty = Command::new("git")
|
|
||||||
.args(["status", "--porcelain"])
|
|
||||||
.output()
|
|
||||||
.is_ok_and(|out| !out.stdout.is_empty());
|
|
||||||
|
|
||||||
if is_dirty {
|
|
||||||
let dirty_ts = chrono::Local::now().format("%y.%m.%d.%H%M%S").to_string();
|
|
||||||
git_hash.push_str(&format!("-dirty-{dirty_ts}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let git_date = Command::new("git")
|
|
||||||
.args(["log", "-1", "--format=%cd", "--date=format:%y.%m.%d.%H%M%S"])
|
|
||||||
.output()
|
|
||||||
.ok()
|
|
||||||
.and_then(|out| String::from_utf8(out.stdout).ok())
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.unwrap_or_else(|| chrono::Local::now().format("%y.%m.%d.%H%M%S").to_string());
|
|
||||||
|
|
||||||
let version = format!("v{git_date} ({git_hash})");
|
|
||||||
println!("cargo:rustc-env=APP_VERSION={version}");
|
|
||||||
println!("cargo:rerun-if-changed=../.git/HEAD");
|
|
||||||
println!("cargo:rerun-if-changed=../.git/index");
|
|
||||||
println!("cargo:rerun-if-changed=src");
|
|
||||||
println!("cargo:rerun-if-changed=Cargo.toml");
|
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 82 KiB |
@@ -1,199 +0,0 @@
|
|||||||
# MCP Memory Server Architecture & Workflow Design
|
|
||||||
|
|
||||||
## 1. Core Architecture
|
|
||||||
The `mcp-memory` system runs as a **single, continuously running background daemon natively on Windows**. It manages the state of the Antigravity knowledge graph and serves as a universal backend for both Windows and WSL environments.
|
|
||||||
|
|
||||||
### Why this design?
|
|
||||||
* **Cross-OS I/O Optimization:** Prevents the WSL agent from performing slow, heavy filesystem writes against the mounted Windows `C:\` drive.
|
|
||||||
* **Concurrency & Locking:** A single daemon holds the lock on the `mcp_memory` JSON files, preventing data corruption and eliminating complex delta-reconciliation between parallel WSL and Windows processes.
|
|
||||||
* **Dual Transport System:** Utilizes both `stdio` and HTTP transports concurrently. The local Windows `agy` instance connects natively via standard `stdio`, while the Axum HTTP server provides synchronous, non-blocking access for WSL clients and external scripts.
|
|
||||||
|
|
||||||
## 2. Server Transport & Endpoints (Axum)
|
|
||||||
The daemon uses the `axum` and `rust-mcp-axum` crates, binding to `0.0.0.0:3000` to serve the network.
|
|
||||||
|
|
||||||
### Standard MCP Endpoints
|
|
||||||
* `GET /ws`: The WebSocket endpoint for efficient, low-latency, full-duplex JSON-RPC communication (preferred for proxies and the UI dashboard).
|
|
||||||
* `GET /sse`: The Server-Sent Events (SSE) endpoint. Antigravity clients connect here to keep a one-way pipe open for receiving pushed responses.
|
|
||||||
* `POST /messages`: The JSON-RPC endpoint. Clients use this to send tool calls and resource reads up to the server when connected via SSE.
|
|
||||||
|
|
||||||
### Dashboard REST API
|
|
||||||
The server hosts a rich Single Page Application (SPA) natively on the root route, backed by a suite of REST endpoints:
|
|
||||||
* `GET /`: The Brain Monitor live HTML dashboard SPA.
|
|
||||||
* `GET /api/graph`: Returns the full node/edge topology for the interactive physics-simulated canvas.
|
|
||||||
* `GET /api/tasks` & `POST /api/tasks/{id}/complete`: Drives the actionable Kanban board.
|
|
||||||
* `GET /api/ledger` & `GET /api/search`: Powers the code change ledger and global fuzzy search UI.
|
|
||||||
* `GET /api/stats`: Returns a JSON snapshot of current entity, relation, task, and tech debt counts.
|
|
||||||
|
|
||||||
### Git & IDE Integration Endpoints
|
|
||||||
* `UDP /nvim/telemetry`: A one-way connectionless datagram listener that Neovim instances hit on `FocusGained` or `BufEnter` to globally broadcast the user's active file to the UI and Agent.
|
|
||||||
* `GET /gate/verify`: A lightweight, deterministic endpoint used by external scripts (like a `git push` wrapper) to verify if an action is authorized based on the current state.
|
|
||||||
|
|
||||||
## 3. Client Connections
|
|
||||||
The Antigravity configurations (`mcp_config.json`) utilize the dual-transport system:
|
|
||||||
* **Windows `agy`:** Spawns and connects to the local daemon natively via `stdio` subprocess execution.
|
|
||||||
* **WSL (Linux) `agy`:** Connects to the running Windows HTTP daemon via the host network proxy, e.g., `http://127.0.0.1:3000/sse`.
|
|
||||||
|
|
||||||
## 4. Git Integration (Global Wrapper)
|
|
||||||
Instead of relying on localized per-repository Git hooks (like `.git/hooks/pre-push`), the system leverages a **global bash/PowerShell alias wrapper** for the `git` command. This intercepts `git` commands universally across the OS.
|
|
||||||
|
|
||||||
### Workflow Example (Push Safety Gate)
|
|
||||||
1. The user types `git push`.
|
|
||||||
2. The global wrapper intercepts the command.
|
|
||||||
3. It makes a synchronous HTTP request to the local daemon: `curl -s http://localhost:3000/gate/verify`.
|
|
||||||
4. If the endpoint returns `200 OK` (indicating the `PrePushAuditor` subagent has verified that unit tests pass and history is squashed), the push proceeds.
|
|
||||||
5. If not `200 OK`, the wrapper blocks the push and alerts the user to fix tests or run `gsquash`.
|
|
||||||
|
|
||||||
### Benefits of the Wrapper Approach
|
|
||||||
* **Universal Enforcement:** The push safety gate is protected across all repositories automatically, without copying hook scripts.
|
|
||||||
* **No File Locks:** Uses safe, lightweight, parallelizable HTTP requests rather than executing the Rust binary directly.
|
|
||||||
* **Action Logging:** The wrapper can be seamlessly extended to log actions (like `checkout` or `commit`) directly into the knowledge graph in real-time.
|
|
||||||
## 5. Storage Architecture & Persistence (Redb LSM-Tree)
|
|
||||||
The daemon has completely eliminated raw JSON file sprawl and fragmented delta-file reconciliation. It now utilizes a pure-Rust, embedded Key-Value engine (`redb`) that implements a robust Log-Structured Merge-Tree (LSM-tree) architecture.
|
|
||||||
|
|
||||||
### Key Principles:
|
|
||||||
* **Embedded Database Engine:** All structured components (Tasks, Snippets, Tech Debt, Checklists, etc.) are stored as binary-encoded values inside a unified `redb` database file (`store.redb`).
|
|
||||||
* **ACID Compliance & File Locks:** The Windows daemon holds an exclusive read-write lock on the database file, guaranteeing zero data corruption, race conditions, or lock contention during concurrent access.
|
|
||||||
* **Atomic Write-Guard Scope:** Store modification methods (`Store::modify` and `Store::modify_async`) retain the write lock through both the in-memory mutation and JSON serialization phases, eliminating lock-release TOCTOU race conditions.
|
|
||||||
* **Store Quarantine Mode:** If deserialization fails during `Store::load_from_db`, the store flags `is_corrupted = true` and refuses to overwrite database keys with default values on subsequent writes.
|
|
||||||
* **Asynchronous Checkpointing:** The core Knowledge Graph (Entities, Relations, Observations) still utilizes a Write-Ahead Logging (WAL) pattern (`wal.jsonl`) and a master snapshot (`master.json`) to allow safe, lock-free memory mutations which are reconciled in the background.
|
|
||||||
|
|
||||||
## 6. Domain Models & Component Stores
|
|
||||||
The system leverages a modular, thread-safe generic `Store<T>` abstraction that automatically transparently serializes and deserializes native Rust structs directly into the underlying `redb` tables.
|
|
||||||
Currently implemented persistent stores include:
|
|
||||||
* **Audit Ledger & Tasks:** Tracks agent actions and active background tasks.
|
|
||||||
* **Context & Handoffs:** Session Summaries, Handoff Memos, Hypotheses, and Agent Signals.
|
|
||||||
* **Engineering Tracking:** ADRs (Architecture Decision Records), Snippets, Error Fixes, Tech Debt, and PR Checklists.
|
|
||||||
* **Environment State:** Pinned Files, Env Fingerprints, Milestones, and Environments.
|
|
||||||
* **Safety Gates:** Authorized execution gates (Push Safety).
|
|
||||||
|
|
||||||
## 7. Full-Text & Semantic Search Engine (Tantivy + FastEmbed)
|
|
||||||
To support blazing-fast, intelligent semantic retrieval across the sprawling knowledge graph, the daemon embeds **Tantivy** (a full-text search engine inspired by Apache Lucene) alongside **FastEmbed** (a local ONNX runtime for vector embeddings).
|
|
||||||
* **The `MemoryIndex`:** Whenever the graph or auxiliary stores mutate, a background thread dynamically rebuilds the Tantivy index (`tantivy_index/` dir) and computes semantic vectors.
|
|
||||||
* **Pre-cached Vector Embeddings:** `SearchService::semantic_search` reuses pre-cached snippet embedding vectors (`snippet.embedding`), bypassing redundant ONNX neural network inference calls during query execution.
|
|
||||||
* **Global Omni-Search:** This architecture powers the `omni_search` tool, allowing subagents to instantly fuzzy-search and semantically rank documents across Entities, Tasks, Snippets, Error Fixes, and ADRs simultaneously in milliseconds, without loading massive JSON arrays into RAM.
|
|
||||||
|
|
||||||
## 8. Webhook Telemetry & Passive Ingestion
|
|
||||||
The server features a suite of webhook listeners that passively ingest development activity to build context without requiring human copy-pasting:
|
|
||||||
* **Terminal Ingestion (`/terminal/telemetry`):** Shell hooks (PowerShell/Nushell) silently POST command execution history and exit codes, allowing agents to read recent stack traces via the `memory://terminal/recent` resource.
|
|
||||||
* **IDE Telemetry (`/nvim/telemetry`):** A connectionless UDP datagram listener receives focus events from Neovim, broadcasting the user's active file to the UI and Agent instantly.
|
|
||||||
|
|
||||||
## 9. The Gate System (Push Safety Verification)
|
|
||||||
The binary supports a flexible safety gate authorization system, accessible both via the HTTP API and CLI subcommands.
|
|
||||||
* **API / CLI set**: Records an authorization status (`authorized`, `blocked`, or `pending`) for a specific target and namespace. Can be invoked via `POST /gate/set` (JSON) or `mcp-memory-stub gate set`.
|
|
||||||
* **API / CLI verify**: Evaluates a pending action against the gate store. It returns standard HTTP status codes (`200 OK`, `403 Forbidden`, `404 Not Found`) via `GET /gate/verify?action=...` or POSIX exit codes (0, 1, 2) via `mcp-memory-stub gate verify`. Both support a `consume` parameter/flag to immediately revoke the authorization after a successful check.
|
|
||||||
|
|
||||||
## 9. Operational Configuration & Paths
|
|
||||||
The physical storage location of the knowledge graph and all persistent stores is strictly controlled by the MCP_MEMORY_STORE_DIR environment variable.
|
|
||||||
* **Default Path:** If not set, the daemon defaults to ~/.gemini/mcp_memory.
|
|
||||||
* **Port Binding:** The Axum HTTP server strictly binds to .0.0.0:3000.
|
|
||||||
|
|
||||||
## 10. Exposed MCP Capabilities (Tools)
|
|
||||||
The server implements the Model Context Protocol (MCP) by exposing a vast suite of tools via the JSON-RPC interface, categorized broadly into:
|
|
||||||
* **Graph Management:** create_entities, create_relations, merge_entities,
|
|
||||||
read_graph, etc.
|
|
||||||
* **Task & Context Tracking:** tasks, milestones, handoff_memos, hypotheses, agent_signals, process_logs, add_session_summary, etc.
|
|
||||||
* **Engineering & DevOps:** log_code_change, log_error_fix, tech_debt, decisions, etc.
|
|
||||||
* **Environment & Workspaces:** environment, manage_checkpoint, manage_subagent_namespace, snippets, etc.
|
|
||||||
|
|
||||||
|
|
||||||
## 11. Concurrency & Thread Safety
|
|
||||||
With the introduction of the Dual Transport System, the daemon must safely handle simultaneous read/write requests from both Stdio (Windows agy) and HTTP (WSL agy) clients.
|
|
||||||
* **Shared State:** The entire server operates on a cloned Arc<MemoryState>.
|
|
||||||
* **Locking Mechanism:** The unified Knowledge Graph and modular Store<T> components are protected by RwLock primitives.
|
|
||||||
* **Safe Mutation:** Store modifications utilize closure-based modify(|store| { ... }) methods to ensure locks are safely acquired, mutations applied, and file writes executed sequentially without deadlocking the asynchronous Tokio runtimes.
|
|
||||||
|
|
||||||
## 12. Lifecycle & Startup Management (Server/Stub Architecture)
|
|
||||||
The mcp-memory daemon employs a strict Server/Client paradigm to maintain separation of concerns. There are no dual roles or morphing executables.
|
|
||||||
|
|
||||||
### The Windows Background Daemon (`mcp-memory-server.exe`)
|
|
||||||
The server executable is responsible exclusively for running the Axum HTTP and WebSockets daemon and managing the Knowledge Graph. It does not contain any proxy logic. If port 3000 is already in use by an existing server instance, it gracefully exits instead of attempting to run.
|
|
||||||
|
|
||||||
* **Non-Blocking Startup & Indexing:** Upon launch, the server uses a `tokio::spawn` task to rebuild the Tantivy index in the background. This ensures the Axum HTTP server binds immediately to port 3000, allowing instantaneous liveness checks via the `/ping` endpoint without waiting for the synchronous I/O operations of the index rebuild to complete.
|
|
||||||
* **Robust Background Detachment:** To ensure the server survives the death of the parent shell that spawns it (such as a deployment script), it uses Windows `WScript.Shell` (via `start_server.ps1`) to spawn the binary with a strictly hidden window style (`SW_HIDE`). This completely detaches it from the parent process tree, preventing `CTRL_CLOSE_EVENT` signals from killing the background daemon when the deploying terminal closes.
|
|
||||||
|
|
||||||
### The Client Proxy (`mcp-memory-stub.exe`)
|
|
||||||
All Antigravity sessions (Windows and WSL) use the lightweight `mcp-memory-stub` as their proxy. The stub connects to the server via WebSockets and acts as the bridge for standard `stdio` JSON-RPC traffic.
|
|
||||||
* **Startup via Interop/Spawn:** Upon launch, the stub attempts to connect to the Windows host on port 3000. If the server is offline, the stub automatically executes a spawn command (e.g., executing `Start-Process` natively, or via WSL interop) to silently wake up the Windows Leader before commencing the proxy loop.
|
|
||||||
* **MPSC Queue Resilience:** The stub utilizes an asynchronous multi-producer, single-consumer (MPSC) channel queue. If the Windows Leader daemon restarts or momentarily drops, the proxy buffers incoming JSON-RPC tool calls and infinitely retries them until the connection is restored. This guarantees **zero message loss** and **zero thread leaks** without crashing the active `agy` session.
|
|
||||||
* **Zero I/O Penalty (WSL):** This ensures the Linux binary never directly touches the Windows NTFS files, reserving all heavy disk operations for the native Windows host.
|
|
||||||
## 13. Cargo Workspace & Binary Artifacts
|
|
||||||
To optimize for different environments, the codebase is structured as a Cargo Workspace containing two distinct crates:
|
|
||||||
|
|
||||||
### 1. mcp-memory-server (The "Full-Fat" Daemon)
|
|
||||||
* **Path:** server/
|
|
||||||
* **Size/Complexity:** Heavy (contains Axum, MCP SDK, JSON parsing, Tokio runtime).
|
|
||||||
* **Role:** This is the primary background daemon. It binds to .0.0.0:3000, holds file locks, and manages the graph.
|
|
||||||
* **Windows Behavior:** It is designed to run in the background as a standalone service.
|
|
||||||
|
|
||||||
### 2. mcp-memory-stub (The Ultra-Lightweight Proxy)
|
|
||||||
* **Path:** stub/
|
|
||||||
* **Size/Complexity:** Extremely light (only relies on
|
|
||||||
eqwest and okio).
|
|
||||||
* **Role:** A dedicated, OS-agnostic proxy binary used strictly for routing stdio JSON-RPC traffic over HTTP to a remote Leader. Windows gy clients point directly to this binary to bypass loading the heavy Server daemon into memory.
|
|
||||||
* **WSL Behavior:** Compiled as a Linux native binary (x86_64-unknown-linux-musl). When executed by WSL agy, it acts as a transparent proxy to http://127.0.0.1:3000. It can also execute wake_cmd (e.g., WSL interop) to silently wake the Windows host if the Leader is offline.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 14. Neovim Integration Architecture & "God Mode"
|
|
||||||
To enable seamless pair-programming inside Neovim, the daemon integrates with Neovim using two complementary systems: a Webhook Telemetry pipeline and dedicated MCP binaries.
|
|
||||||
|
|
||||||
### Telemetry Pipeline (Last Focused Wins)
|
|
||||||
A lightweight Lua script (`gemini-integration.lua`) is loaded into Neovim, which fires an asynchronous UDP datagram to `127.0.0.1:3002` (fire-and-forget via native libuv) whenever the user focuses a buffer or moves the cursor. The server then writes this data (including `session_id`, `file`, `line`, and `col`) to both the Windows and WSL `active_nvim.txt` files and broadcasts it over WebSockets.
|
|
||||||
|
|
||||||
### Native MCP Binaries (win-nvim & linux-nvim)
|
|
||||||
The project compiles two standalone, highly-performant binaries that implement the MCP JSON-RPC protocol over Stdio and bridge it directly to Neovim's Msgpack-RPC Named Pipes/Sockets. These binaries avoid hardcoding infinite tools by utilizing a "God Mode" escape hatch.
|
|
||||||
|
|
||||||
Exposed Neovim Tools:
|
|
||||||
* **
|
|
||||||
vim_get_active_buffer &
|
|
||||||
vim_get_cursor**: Read file state.
|
|
||||||
* **
|
|
||||||
vim_goto_line &
|
|
||||||
vim_set_diagnostics**: Manipulate IDE state.
|
|
||||||
* **
|
|
||||||
vim_get_visual_selection**: Read exact highlight coordinates (handles mode dynamically).
|
|
||||||
* **
|
|
||||||
vim_list_buffers**: Discover unsaved work and context.
|
|
||||||
* **
|
|
||||||
vim_get_diagnostics**: Read live LSP errors dynamically instead of requiring a compiler.
|
|
||||||
* **
|
|
||||||
vim_execute_lua ("God Mode")**: The ultimate fallback tool. Evaluates raw Lua scripts inside the active Neovim instance and returns JSON. This prevents the need to continuously recompile the Rust server whenever a new Neovim capability is required.
|
|
||||||
|
|
||||||
## 15. Build & Deployment Strategy
|
|
||||||
Because the background server operates as an always-on Windows daemon, standard recompilation and file-copying strategies will fail due to active Windows OS file locks.
|
|
||||||
|
|
||||||
### Randomized Lock Bypassing
|
|
||||||
The \uild.ps1\ deployment pipeline intercepts locked \.exe\ files by appending a unique, timestamped/randomized suffix (e.g., \mcp-memory-server.exe.12345.old\) when forcing a \Move-Item\. This guarantees that rapid sequential deployments (where a previous \.old\ file might still be locked by a zombie process) never silently fail or collide.
|
|
||||||
|
|
||||||
### Dynamic Versioning
|
|
||||||
To trace binary provenances during rapid deployment cycles, all binaries embed dynamic versioning directly at compile time (via \uild.rs\ and \uild_template.rs\). The injected \APP_VERSION\ environment variable combines the static Cargo \ersion\ with the live \git\ short hash and UTC timestamp, allowing the CLI \--version\ commands and the HTTP \/api/version\ endpoints to guarantee exactly which iteration of the code is actively executing.
|
|
||||||
## 16. Testing Architecture (Native Rust E2E)
|
|
||||||
Historically, the project relied on a complex Python testing suite (\pytest\ + \mcp_client.py\) to validate the server over HTTP/SSE. This has been fully deprecated in favor of **Native Rust End-to-End Testing**.
|
|
||||||
* **Unit Tests:** Handlers and business logic are tested directly inside \server/src/handlers.rs\ using native \ okio::test\ constructs.
|
|
||||||
* **E2E Tests:** Integration and full-system tests run via \stub/tests/e2e.rs\ and \win-nvim/tests/integration_test.rs\, ensuring type safety, faster execution, and eliminating Python environment dependencies.
|
|
||||||
## 17. Automated Git Context Binding (git2)
|
|
||||||
Instead of forcing the LLM client to manually run `git rev-parse HEAD` and pass `git_branch` / `git_commit` arguments for every single code change, the server integrates the native **`git2`** C bindings.
|
|
||||||
When engineering endpoints (`log_code_change`, `log_error_fix`, `log_tech_debt`) are invoked, the server asynchronously discovers the surrounding Git repository, peels the HEAD reference, and automatically injects the current commit hash, commit message, and branch name directly into the stored entities and audit logs. This guarantees airtight VCS traceability without wasting LLM tokens or relying on the agent's memory.
|
|
||||||
|
|
||||||
## 18. Upcoming Enhancements
|
|
||||||
|
|
||||||
### A. Graph Summarization & Decay
|
|
||||||
To prevent context window bloat, the Redb engine will enforce TTLs on ephemeral nodes (`StickyNotes`, minor `Tasks`). Furthermore, an `archive_routine` prompt will allow subagents to automatically compress older `SessionSummaries` into dense milestone retrospectives.
|
|
||||||
|
|
||||||
### B. Proactive Subagent Triggers
|
|
||||||
The daemon will eventually gain the ability to autonomously spawn background subagents (like the `BugDiagnostician`) when specific events are detected in the webhook telemetry, rather than relying strictly on the human user to initiate the agent.
|
|
||||||
|
|
||||||
## 11. Advanced Clipboard Capabilities
|
|
||||||
The memory server bypasses typical cross-OS Linux/Windows clipboard restrictions by proxying operations from the WSL stub back to the Windows Axum host.
|
|
||||||
It supports reading and writing rich formats natively to the Windows Host OS using the clipboard-win and rboard crates:
|
|
||||||
* **Plain & Rich Text:** CF_UNICODETEXT and CF_HTML are supported for rich copying/pasting.
|
|
||||||
* **File Drops (CF_HDROP):** The server can parse file lists copied from Windows Explorer, and can inversely synthesize file drops into the clipboard from absolute paths.
|
|
||||||
* **Images (CF_BITMAP):** The server natively rasterizes clipboard bitmaps to JPEG on read, and can write raw RgbaImage buffers back to the clipboard on write.
|
|
||||||
* **Developer Tooling:** read_file_skeleton (AST), get_active_worktree_context (Git), get_recent_logs, toggle_clipboard_watch_mode.
|
|
||||||
|
|
||||||
## 19. High-Performance Concurrency & Resilience Guarantees
|
|
||||||
* **Async Channel Backpressure (`push_async`)**: `Store::modify_async` uses `DbWriteQueue::push_async` with `tx.send(task).await` backpressure to guarantee database write persistence under heavy async write loads without dropping write transactions.
|
|
||||||
* **Atomic Search Index Swaps**: `MemoryState::rebuild_index` constructs and populates a new `MemoryIndex` instance in isolation before performing an atomic pointer swap (`*self.search_index.write().await = new_idx`), eliminating transient empty search result windows.
|
|
||||||
* **SIMD-Friendly Single-Pass Cosine Similarity**: `cosine_similarity` calculates dot product and Euclidean norm squares in a single linear pass over float vectors, enabling SIMD compiler auto-vectorization.
|
|
||||||
* **Safe Stream Decoding on Log Tails**: Log tail operations (`get_recent_logs`) read raw bytes and decode using lossy UTF-8 conversion (`String::from_utf8_lossy`) to ensure resilience when seeking across multi-byte UTF-8 boundaries.
|
|
||||||
* **Serde Parameter & Enum Tolerance**: All action enums (`StickyNoteAction`, `SnippetSearchMode`, `Relation`) support case-insensitive variants and field aliases (`source`/`from`, `target`/`to`, `relationType`/`relation_type`) to ensure seamless execution when LLMs pass varied string formatting.
|
|
||||||
@@ -10,18 +10,6 @@ vim.api.nvim_create_autocmd({"VimEnter", "FocusGained", "BufEnter", "BufWritePos
|
|||||||
if #vim.api.nvim_list_uis() > 0 then
|
if #vim.api.nvim_list_uis() > 0 then
|
||||||
local server_name = vim.v.servername
|
local server_name = vim.v.servername
|
||||||
if server_name then
|
if server_name then
|
||||||
-- 1. Legacy Disk Write (Fallback)
|
|
||||||
local home = os.getenv("HOME") or os.getenv("USERPROFILE")
|
|
||||||
if home then
|
|
||||||
os.execute("mkdir -p " .. home .. "/.gemini")
|
|
||||||
local path = home .. "/.gemini/active_nvim.txt"
|
|
||||||
local f = io.open(path, "w")
|
|
||||||
if f then
|
|
||||||
f:write(server_name)
|
|
||||||
f:close()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 2. V2 Telemetry Push (HTTP with Knowledge Projection + UDP Fast Mirror)
|
-- 2. V2 Telemetry Push (HTTP with Knowledge Projection + UDP Fast Mirror)
|
||||||
local file = vim.api.nvim_buf_get_name(0)
|
local file = vim.api.nvim_buf_get_name(0)
|
||||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ The server registers 5 high-signal workflow prompts to initiate standardized age
|
|||||||
|
|
||||||
## 5. Consolidated Smart Tools Architecture (11 Primary Tools)
|
## 5. Consolidated Smart Tools Architecture (11 Primary Tools)
|
||||||
|
|
||||||
The server consolidates granular single-purpose tools into domain-named smart tools. Always prefer the consolidated tools over legacy aliases:
|
The server consolidates granular single-purpose tools into domain-named smart tools.
|
||||||
|
|
||||||
* **`tasks`**: Complete task lifecycle management.
|
* **`tasks`**: Complete task lifecycle management.
|
||||||
- `action: "add"`: Create a new task (requires `title`, optional `description`, `git_branch`, `repo_name`, `priority: "low" | "medium" | "high" | "urgent"`, `assigned_agent`, `verification_command`, `parent_id`, `dependencies`).
|
- `action: "add"`: Create a new task (requires `title`, optional `description`, `git_branch`, `repo_name`, `priority: "low" | "medium" | "high" | "urgent"`, `assigned_agent`, `verification_command`, `parent_id`, `dependencies`).
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
rebuild_index: found 229 entities, 24 tasks
|
|
||||||
spawn_blocking started in rebuild_index
|
|
||||||
add_task_sync called for task: 8da665f7-107c-41c8-b203-d39a9ff1f9a5
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 01a788f0-7adb-456c-bf37-02bbd7e1d2f8
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 5e8d9b26-ae31-4136-a4c4-6ead74887bb5
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: ed184dd6-55ce-4d37-a06a-b34733795e03
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 3cea859d-c1ce-4f6c-bdc5-6eb65dbd8e4f
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 62446699-39b3-496b-9af8-99b331fe6c08
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 4b0e2179-1e70-4b64-9c2d-ff2fe2d05fdf
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 16dd35e4-eadd-4597-a5b6-9844852cfb76
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: c85f352b-f15b-4f90-a6ae-18fe914212b2
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: d1e907a2-89a2-4814-8722-22c754cc943c
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: c382a511-a5d6-4114-ad2c-f09fdd36d66a
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 42ca3765-5c69-4cdd-8324-5a174c34d4ae
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 05792eaa-4bc5-45e5-b9a0-d9027b3b3974
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 15c9fd58-8d8f-4c2f-80f4-15a46ba54588
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 9b55e54c-ac1b-4219-acf3-68f7cb34a1fd
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: a04923ea-6abf-41ba-9c45-f7d9cdac3440
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 4233293e-6d93-4c46-b9aa-f9e712af0e86
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 93451cd9-25c5-4323-8dce-79bba124242d
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 949f1d59-e1e2-421c-9682-43a79f41679b
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: cd680df7-2e1a-4486-bb9b-3881476c762e
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 561daa2b-ddb5-44ae-8fb8-8155405759fb
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: e36b0eba-aa79-493f-b89b-3da46a44fa11
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: a3a717b3-438a-42a7-83ec-2f2eb5d775b4
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 6a1b05d5-0ffd-4b0c-bb65-f45e0cb5540a
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# MCP Tools Review & Enhancement Strategy
|
|
||||||
|
|
||||||
## Part 1: Current Arsenal Review
|
|
||||||
|
|
||||||
Our current MCP ecosystem is highly advanced, utilizing a **Dual-Transport Leader/Stub Architecture** (Windows Host + WSL Proxy) to completely eliminate cross-OS I/O latency.
|
|
||||||
|
|
||||||
### 1. Context & Token Optimization
|
|
||||||
* `read_file_skeleton`: Highly effective. Uses tree-sitter to extract ASTs (Rust, Python, TS). **Score: A+ (Massive token savings)**
|
|
||||||
* `get_active_worktree_context`: Native git2 integration. Bypasses shell parsing for clean JSON diffs. **Score: A**
|
|
||||||
* `process_logs`: Direct file seeking and daemon log management (`watch`, `get`, `clear`). Prevents LLMs from reading multi-megabyte log files. **Score: A**
|
|
||||||
|
|
||||||
### 2. Neovim IDE Integration (nvim-core)
|
|
||||||
* `nvim_buffer`, `nvim_workspace`, `nvim_intelligence`, `nvim_ui`, `nvim_exec`.
|
|
||||||
* **Review:** Exceptional human QoL. The agent interacts with the code where the human's eyes actually are. Ghost text and diagnostic extmarks provide an IDE-like experience usually reserved for closed-source tools like Cursor. **Score: S-Tier**
|
|
||||||
|
|
||||||
### 3. Clipboard & Workflow
|
|
||||||
* `clipboard` (`read`, `write`).
|
|
||||||
* **Review:** Native cross-OS clipboard-win and arboard implementation with on-demand image grab. Bridges the gap between manual human research and the agent's context. **Score: A**
|
|
||||||
|
|
||||||
### 4. Graph & Memory Management
|
|
||||||
* `create_entities`, `hypotheses`, `agent_signals`, `handoff_routine`.
|
|
||||||
* **Review:** Solid foundation for state persistence across branches and days. **Score: A**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 2: Proposed Enhancements (Focus: T2R, Token Cost, QoL)
|
|
||||||
|
|
||||||
To push the system to the absolute bleeding edge of autonomous coding, I propose the following 5 new tools/enhancements.
|
|
||||||
|
|
||||||
### 1. `replace_ast_node` (Robust Structural Editing)
|
|
||||||
* **The Problem:** Standard text replacement uses exact string matching and line numbers. Line numbers change when humans edit simultaneously, and string matching fails on whitespace/indentation.
|
|
||||||
* **The Solution:** An MCP tool that takes `(file_path, node_type, node_name, new_content)`. It uses tree-sitter to find the exact boundary of `fn execute(...)` and replaces just that AST node.
|
|
||||||
* **Impact:** Zero LLM syntax/indentation errors. 100% robust edits. Drastically lowers Time-to-Resolve (T2R) by eliminating failed edit loops.
|
|
||||||
|
|
||||||
### 2. `semantic_code_search` (Local Vector Embeddings)
|
|
||||||
* **The Problem:** Text search relies on exact regex. If the LLM guesses the wrong variable name, it wastes tokens searching and reading the wrong files.
|
|
||||||
* **The Solution:** Using Tantivy and BERT embeddings in our backend. We index the AST blocks of the codebase in the background. The LLM can query *"Where is the auth token validated?"* and get the exact 3 relevant functions instantly.
|
|
||||||
* **Impact:** Massive token cost reduction (no blind file reading). Instant T2R for codebase exploration.
|
|
||||||
|
|
||||||
### 3. `nvim_exec` terminal execution (Interactive Execution QoL)
|
|
||||||
* **The Problem:** When the agent runs a background terminal command (`cargo build`, `npm run dev`), the output is hidden from the human, and interactive prompts cause the background task to hang indefinitely.
|
|
||||||
* **The Solution:** Dispatch to Neovim terminal splits where the human can watch the tests run natively, interact with prompts, see ANSI colors, and interact seamlessly.
|
|
||||||
* **Impact:** Massive Human QoL.
|
|
||||||
|
|
||||||
### 4. `read_directory_architecture` (Bird's-Eye View)
|
|
||||||
* **The Problem:** Single file inspection works for one file. When entering a new repository, the LLM usually runs `ls -R` and then has to guess what files do based on their names.
|
|
||||||
* **The Solution:** A tool that scans a directory structure and returns a clean hierarchical tree alongside summaries of what each directory and key file is responsible for.
|
|
||||||
* **Impact:** Immediate holistic context. Eliminates the "exploration phase" token tax.
|
|
||||||
|
|
||||||
### 5. `query_database_schema` (Introspection)
|
|
||||||
* **The Problem:** Working with databases usually involves the LLM writing clunky scripts to view table definitions, which often fail due to missing env vars or wrong dialects.
|
|
||||||
* **The Solution:** A direct MCP tool that parses the local `.env`, connects to the database (PostgreSQL), and returns a clean Markdown representation of the schema (Tables, Columns, Types, Foreign Keys).
|
|
||||||
* **Impact:** Prevents hallucinations about database structure. Fixes DB-related bugs significantly faster (T2R).
|
|
||||||
@@ -16,6 +16,7 @@ rmcp = { version = "3.4.0", features = ["server"] }
|
|||||||
mcp-stdio = { version = "1.0.0", path = "../mcp-stdio" }
|
mcp-stdio = { version = "1.0.0", path = "../mcp-stdio" }
|
||||||
bytes = "1.12.1"
|
bytes = "1.12.1"
|
||||||
dashmap = "6.2.1"
|
dashmap = "6.2.1"
|
||||||
|
schemars = "0.8"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
chrono = "0.4.45"
|
chrono = "0.4.45"
|
||||||
|
|||||||
+261
-933
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,101 @@
|
|||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
|
pub enum NvimBufferTool {
|
||||||
|
Read {
|
||||||
|
file: String,
|
||||||
|
start_line: Option<i64>,
|
||||||
|
end_line: Option<i64>,
|
||||||
|
},
|
||||||
|
Replace {
|
||||||
|
file: Option<String>,
|
||||||
|
start_line: Option<i64>,
|
||||||
|
end_line: Option<i64>,
|
||||||
|
replacement_content: Option<String>,
|
||||||
|
expected_content: Option<String>,
|
||||||
|
edits: Option<Vec<BufferEdit>>,
|
||||||
|
},
|
||||||
|
Save {
|
||||||
|
file: Option<String>,
|
||||||
|
},
|
||||||
|
Undo {
|
||||||
|
steps: Option<i64>,
|
||||||
|
},
|
||||||
|
Redo {
|
||||||
|
steps: Option<i64>,
|
||||||
|
},
|
||||||
|
CreateScratch {
|
||||||
|
name: Option<String>,
|
||||||
|
content: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
pub struct BufferEdit {
|
||||||
|
pub file: String,
|
||||||
|
pub start_line: i64,
|
||||||
|
pub end_line: i64,
|
||||||
|
pub replacement_content: String,
|
||||||
|
pub expected_content: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
|
pub enum NvimWorkspaceTool {
|
||||||
|
ListBuffers,
|
||||||
|
ListWindows,
|
||||||
|
Focus {
|
||||||
|
win_id: i64,
|
||||||
|
},
|
||||||
|
Split {
|
||||||
|
direction: Option<String>,
|
||||||
|
file: Option<String>,
|
||||||
|
buf_id: Option<i64>,
|
||||||
|
},
|
||||||
|
Cwd {
|
||||||
|
path: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
|
pub enum NvimIntelligenceTool {
|
||||||
|
Hover { file: String, line: i64, col: i64 },
|
||||||
|
Definition { file: String, line: i64, col: i64 },
|
||||||
|
References { file: String, line: i64, col: i64 },
|
||||||
|
CodeAction { file: String, line: i64, col: i64 },
|
||||||
|
Rename { file: String, line: i64, col: i64, new_name: String },
|
||||||
|
Diagnostics { file: String },
|
||||||
|
Outline { file: String },
|
||||||
|
Query { file: String, query: Option<String>, preset: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
|
pub enum NvimUiTool {
|
||||||
|
Highlight {
|
||||||
|
buf_id: Option<i64>,
|
||||||
|
start_line: i64,
|
||||||
|
end_line: Option<i64>,
|
||||||
|
highlight_group: Option<String>,
|
||||||
|
duration_ms: Option<i64>,
|
||||||
|
},
|
||||||
|
GhostText {
|
||||||
|
line: i64,
|
||||||
|
text: String,
|
||||||
|
highlight_group: Option<String>,
|
||||||
|
},
|
||||||
|
Clear {
|
||||||
|
buf_id: Option<i64>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
|
pub enum NvimExecTool {
|
||||||
|
Lua { code: String },
|
||||||
|
Vimscript { code: String },
|
||||||
|
Terminal { command: String },
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"code": "local buf = vim.fn.bufnr('server/src/search.rs')\nif buf == -1 then\n vim.cmd('e server/src/search.rs')\n buf = vim.api.nvim_get_current_buf()\nend\n\nvim.api.nvim_buf_set_lines(buf, 216, 222, false, {\n ' &self,',\n ' entities: &[Entity],',\n ' tasks: &[Task],',\n ' snippets: &[Snippet],',\n ' adrs: &[Adr],'\n})\n\n-- We need to change the loop variables inside the task from values to clones if they are passed as slices\n-- Actually we can just clone the slice data before moving it into spawn_blocking\nvim.api.nvim_buf_set_lines(buf, 222, 223, false, {\n ' ) -> tokio::task::JoinHandle<tantivy::Result<()>> {',\n ' let entities = entities.to_vec();',\n ' let tasks = tasks.to_vec();',\n ' let snippets = snippets.to_vec();',\n ' let adrs = adrs.to_vec();'\n})\n\nvim.cmd('write')\nrequire('notify')('Updated search index_batch signature', 'info', { title = 'Antigravity' })\nreturn 'ok'\n"}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use schemars::schema_for;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(JsonSchema, Deserialize, Serialize)]
|
||||||
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
|
pub enum TasksToolEnum {
|
||||||
|
/// Create task.
|
||||||
|
Add {
|
||||||
|
/// Task title.
|
||||||
|
title: String,
|
||||||
|
/// Task description.
|
||||||
|
description: Option<String>,
|
||||||
|
},
|
||||||
|
/// Delete task.
|
||||||
|
Delete {
|
||||||
|
/// Task ID.
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let schema = schema_for!(TasksToolEnum);
|
||||||
|
println!("{}", serde_json::to_string_pretty(&schema).unwrap());
|
||||||
|
}
|
||||||
-74
@@ -1,74 +0,0 @@
|
|||||||
rebuild_index: found 228 entities, 24 tasks
|
|
||||||
spawn_blocking started in rebuild_index
|
|
||||||
add_task_sync called for task: 8da665f7-107c-41c8-b203-d39a9ff1f9a5
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 01a788f0-7adb-456c-bf37-02bbd7e1d2f8
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 5e8d9b26-ae31-4136-a4c4-6ead74887bb5
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: ed184dd6-55ce-4d37-a06a-b34733795e03
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 3cea859d-c1ce-4f6c-bdc5-6eb65dbd8e4f
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 62446699-39b3-496b-9af8-99b331fe6c08
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 4b0e2179-1e70-4b64-9c2d-ff2fe2d05fdf
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 16dd35e4-eadd-4597-a5b6-9844852cfb76
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: c85f352b-f15b-4f90-a6ae-18fe914212b2
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: d1e907a2-89a2-4814-8722-22c754cc943c
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: c382a511-a5d6-4114-ad2c-f09fdd36d66a
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 42ca3765-5c69-4cdd-8324-5a174c34d4ae
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 05792eaa-4bc5-45e5-b9a0-d9027b3b3974
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 15c9fd58-8d8f-4c2f-80f4-15a46ba54588
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 9b55e54c-ac1b-4219-acf3-68f7cb34a1fd
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: a04923ea-6abf-41ba-9c45-f7d9cdac3440
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 4233293e-6d93-4c46-b9aa-f9e712af0e86
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 93451cd9-25c5-4323-8dce-79bba124242d
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 949f1d59-e1e2-421c-9682-43a79f41679b
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: cd680df7-2e1a-4486-bb9b-3881476c762e
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 561daa2b-ddb5-44ae-8fb8-8155405759fb
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: e36b0eba-aa79-493f-b89b-3da46a44fa11
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: a3a717b3-438a-42a7-83ec-2f2eb5d775b4
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
add_task_sync called for task: 6a1b05d5-0ffd-4b0c-bb65-f45e0cb5540a
|
|
||||||
Writer add_document returned id/result
|
|
||||||
Needs_commit set to true in add_task_sync
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
if let Some(idx) = gates.iter().position(|g| {
|
|
||||||
g.action == q.action
|
|
||||||
&& g.target == q.target
|
|
||||||
&& g.namespace == q.namespace
|
|
||||||
&& g.params == q.params
|
|
||||||
}) {
|
|
||||||
found = Some(gates[idx].clone());
|
|
||||||
if q.consume {
|
|
||||||
to_remove = Some(idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(idx) = to_remove {
|
|
||||||
gates.remove(idx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
match found {
|
|
||||||
Some(record) => {
|
|
||||||
if record.status == "authorized" {
|
|
||||||
(axum::http::StatusCode::OK, "Authorized").into_response()
|
|
||||||
} else {
|
|
||||||
let msg = if let Some(r) = record.reason {
|
|
||||||
format!("Action blocked. Reason: {}", r)
|
|
||||||
} else {
|
|
||||||
"Action blocked.".to_string()
|
|
||||||
};
|
|
||||||
(axum::http::StatusCode::FORBIDDEN, msg).into_response()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => (
|
|
||||||
axum::http::StatusCode::NOT_FOUND,
|
|
||||||
"Action not yet authorized (no gate record found).",
|
|
||||||
)
|
|
||||||
.into_response(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn gate_set_handler(
|
|
||||||
State(app_state): State<Arc<AppState>>,
|
|
||||||
Json(body): Json<GateSetReq>,
|
|
||||||
) -> axum::response::Response {
|
|
||||||
let status = if body.block.unwrap_or(false) {
|
|
||||||
"blocked".to_string()
|
|
||||||
} else if body.authorize.unwrap_or(false) {
|
|
||||||
"authorized".to_string()
|
|
||||||
} else {
|
|
||||||
"pending".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let record = GateRecord {
|
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
|
||||||
action: body.action.clone(),
|
|
||||||
target: body.target.clone(),
|
|
||||||
namespace: body.namespace.clone(),
|
|
||||||
params: body.params.clone(),
|
|
||||||
status,
|
|
||||||
reason: body.reason.clone(),
|
|
||||||
timestamp: crate::handlers::utils::now_secs(),
|
|
||||||
};
|
|
||||||
app_state.handler.state.gates.modify(|gates| {
|
|
||||||
gates.retain(|g| !(g.action == record.action && g.target == record.target));
|
|
||||||
gates.push(record);
|
|
||||||
});
|
|
||||||
(axum::http::StatusCode::OK, "Gate state updated.").into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
state.rebuild_index().await;
|
|
||||||
tokio::spawn(index_committer_worker(Arc::clone(&state)));
|
|
||||||
let app_state = Arc::new(AppState {
|
|
||||||
handler: Arc::new(MemoryHandler::new(Arc::clone(&state))),
|
|
||||||
clients: RwLock::new(HashMap::new()),
|
|
||||||
next_id: AtomicUsize::new(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
let app_state_clone = Arc::clone(&app_state);
|
|
||||||
let mut rx = state.activity_tx.subscribe();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
while let Ok(msg) = rx.recv().await {
|
|
||||||
let senders: Vec<_> = app_state_clone
|
|
||||||
.clients
|
|
||||||
.read()
|
|
||||||
.unwrap_or_else(|e| e.into_inner())
|
|
||||||
.values()
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
for client_tx in senders {
|
|
||||||
let _ = client_tx.try_send(msg.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let app = Router::new()
|
|
||||||
.route(
|
|
||||||
"/api/version",
|
|
||||||
get(|| async move {
|
|
||||||
axum::Json(serde_json::json!({
|
|
||||||
"version": env!("APP_VERSION"),
|
|
||||||
"git_hash": option_env!("GIT_HASH").unwrap_or("unknown")
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route("/ws", get(ws_handler))
|
|
||||||
.route("/health", get(health_handler))
|
|
||||||
.route("/nvim/telemetry", post(nvim_telemetry_handler))
|
|
||||||
.route("/gate/verify", get(gate_verify_handler))
|
|
||||||
.route("/gate/set", post(gate_set_handler))
|
|
||||||
.route(
|
|
||||||
"/shutdown",
|
|
||||||
post(
|
|
||||||
|headers: axum::http::HeaderMap, State(state): State<Arc<AppState>>| async move {
|
|
||||||
let token_path = state.handler.state.base_dir.join("admin.token");
|
|
||||||
let expected_token = tokio::fs::read_to_string(&token_path)
|
|
||||||
.await
|
|
||||||
.unwrap_or_default()
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let auth_header = headers
|
|
||||||
.get(axum::http::header::AUTHORIZATION)
|
|
||||||
.and_then(|h| h.to_str().ok())
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if expected_token.is_empty() || auth_header != format!("Bearer {}", expected_token) {
|
|
||||||
return (axum::http::StatusCode::UNAUTHORIZED, "Unauthorized").into_response();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::thread::spawn(|| {
|
|
||||||
tracing::info!(
|
|
||||||
"Received shutdown request via /shutdown endpoint. Exiting process cleanly."
|
|
||||||
);
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
||||||
std::process::exit(0);
|
|
||||||
});
|
|
||||||
(axum::http::StatusCode::OK, "Shutting down...").into_response()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/",
|
|
||||||
get(|| async move { axum::response::Html(include_str!("dashboard.html")) }),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/graph",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let graph_json = state_clone.read_graph(|g| serde_json::to_string(g).unwrap_or_else(|_| "{}".to_string()));
|
|
||||||
([(axum::http::header::CONTENT_TYPE, "application/json")], graph_json)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/tasks/{id}/complete",
|
|
||||||
post({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move |axum::extract::Path(id): axum::extract::Path<String>| async move {
|
|
||||||
state_clone.tasks.modify(|tasks| {
|
|
||||||
for t in tasks.iter_mut() {
|
|
||||||
if t.id == id {
|
|
||||||
t.status = "completed".to_string();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
axum::Json(serde_json::json!({"status": "success"}))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/tasks",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let tasks_json = state_clone.tasks.read_with(|t| serde_json::to_string(t).unwrap_or_else(|_| "[]".to_string()));
|
|
||||||
([(axum::http::header::CONTENT_TYPE, "application/json")], tasks_json)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/sticky",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let sticky_json = state_clone.sticky.read_with(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string()));
|
|
||||||
([(axum::http::header::CONTENT_TYPE, "application/json")], sticky_json)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/search",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move |axum::extract::Query(params): axum::extract::Query<
|
|
||||||
std::collections::HashMap<String, String>,
|
|
||||||
>| async move {
|
|
||||||
if let Some(q) = params.get("q")
|
|
||||||
&& let Ok(idx) = state_clone.search_index.read()
|
|
||||||
&& let Ok(results) = idx.search(q, None) {
|
|
||||||
let mut formatted_results = Vec::new();
|
|
||||||
for (id, doc_type, title, body, score) in results {
|
|
||||||
formatted_results.push(serde_json::json!({
|
|
||||||
"id": id,
|
|
||||||
"type_name": doc_type,
|
|
||||||
"title": title,
|
|
||||||
"content": body,
|
|
||||||
"score": score
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
return axum::Json(
|
|
||||||
serde_json::json!({ "results": formatted_results }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
axum::Json(serde_json::json!({ "results": [] }))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/activity",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let activities_json = state_clone.recent_activities.read_with(|a| serde_json::to_string(a).unwrap_or_else(|_| "[]".to_string()));
|
|
||||||
([(axum::http::header::CONTENT_TYPE, "application/json")], activities_json)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/api/stats",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let (entities, relations) = state_clone.read_graph(|g| (g.entities.len(), g.relations.len()));
|
|
||||||
let tasks = state_clone.tasks.read_with(|items| items.len());
|
|
||||||
let snippets = state_clone.snippets.read_with(|items| items.len());
|
|
||||||
let tech_debts = state_clone.tech_debts.read_with(|items| items.len());
|
|
||||||
let adrs = state_clone.adrs.read_with(|items| items.len());
|
|
||||||
|
|
||||||
let ledger = state_clone.ledger.read_with(|items| items.len());
|
|
||||||
let error_fixes = state_clone.error_fixes.read_with(|items| items.len());
|
|
||||||
let session_summaries = state_clone.session_summaries.read_with(|items| items.len());
|
|
||||||
let handoff_memos = state_clone.handoff_memos.read_with(|items| items.len());
|
|
||||||
let env_fingerprints = state_clone.env_fingerprints.read_with(|items| items.len());
|
|
||||||
let env_requirements = state_clone.env_requirements.read_with(|items| items.len());
|
|
||||||
let milestones = state_clone.milestones.read_with(|items| items.len());
|
|
||||||
let environments = state_clone.environments.read_with(|items| items.len());
|
|
||||||
let gates = state_clone.gates.read_with(|items| items.len());
|
|
||||||
|
|
||||||
axum::Json(serde_json::json!({
|
|
||||||
"entities": entities,
|
|
||||||
"relations": relations,
|
|
||||||
"tasks": tasks,
|
|
||||||
"snippets": snippets,
|
|
||||||
"tech_debts": tech_debts,
|
|
||||||
"adrs": adrs,
|
|
||||||
"ledger": ledger,
|
|
||||||
"error_fixes": error_fixes,
|
|
||||||
"session_summaries": session_summaries,
|
|
||||||
"handoff_memos": handoff_memos,
|
|
||||||
"env_fingerprints": env_fingerprints,
|
|
||||||
"env_requirements": env_requirements,
|
|
||||||
"milestones": milestones,
|
|
||||||
"environments": environments,
|
|
||||||
"gates": gates
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.with_state(app_state);
|
|
||||||
|
|
||||||
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/sse");
|
|
||||||
let addr = std::env::var("MCP_PORT").unwrap_or_else(|_| "3000".to_string());
|
|
||||||
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", addr)
|
|
||||||
.parse()
|
|
||||||
.expect("Invalid bind address");
|
|
||||||
|
|
||||||
let listener = match tokio::net::TcpListener::bind(&addr).await {
|
|
||||||
Ok(l) => l,
|
|
||||||
Err(e) => {
|
|
||||||
let log_path = dirs::home_dir()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.join(".gemini/mcp_memory/daemon_error.log");
|
|
||||||
let _ =
|
|
||||||
tokio::fs::write(&log_path, format!("Failed to bind to {}: {}\n", addr, e)).await;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Err(e) = axum::serve(listener, app.into_make_service()).await {
|
|
||||||
let log_path = dirs::home_dir()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.join(".gemini/mcp_memory/daemon_error.log");
|
|
||||||
let _ = tokio::fs::write(&log_path, format!("Server crashed: {}\n", e)).await;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
use mcp_memory_server::tools::{DecisionsTool, TechDebtTool, ManageCheckpointTool, HypothesesTool, AgentSignalsTool};
|
||||||
|
use schemars::schema_for;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&schema_for!(AgentSignalsTool)).unwrap());
|
||||||
|
}
|
||||||
+7
-60
@@ -1,5 +1,5 @@
|
|||||||
use crate::store::STORE_TABLE;
|
use crate::store::STORE_TABLE;
|
||||||
use redb::{Database, ReadableTable};
|
use redb::Database;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -117,75 +117,22 @@ pub fn init_redb(base: &Path) -> Arc<Database> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Ensure the table exists and migrate legacy JSON files
|
// Ensure the table exists
|
||||||
match db.begin_write() {
|
match db.begin_write() {
|
||||||
Ok(write_txn) => {
|
Ok(write_txn) => {
|
||||||
let mut opened_ok = false;
|
let mut opened_ok = false;
|
||||||
if let Ok(mut table) = write_txn.open_table(STORE_TABLE) {
|
if let Ok(_) = write_txn.open_table(STORE_TABLE) {
|
||||||
opened_ok = true;
|
opened_ok = true;
|
||||||
if !is_in_memory {
|
|
||||||
let stores = vec![
|
|
||||||
("knowledge_graph_master", "knowledge_graph_master.json"),
|
|
||||||
("audit_ledger", "audit_ledger.json"),
|
|
||||||
("tasks", "tasks.json"),
|
|
||||||
("snippets", "snippets.json"),
|
|
||||||
("adrs", "adrs.json"),
|
|
||||||
("error_fixes", "error_fixes.json"),
|
|
||||||
("session_summaries", "session_summaries.json"),
|
|
||||||
("handoff_memos", "handoff_memos.json"),
|
|
||||||
("env_fingerprints", "env_fingerprints.json"),
|
|
||||||
("env_requirements", "env_requirements.json"),
|
|
||||||
("milestones", "milestones.json"),
|
|
||||||
("environments", "environments.json"),
|
|
||||||
("tech_debts", "tech_debts.json"),
|
|
||||||
("gates", "gates.json"),
|
|
||||||
("state_snapshots", "state_snapshots.json"),
|
|
||||||
("hypotheses", "hypotheses.json"),
|
|
||||||
("agent_signals", "agent_signals.json"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (key, file_name) in stores.iter() {
|
|
||||||
let is_missing = match table.get(*key) {
|
|
||||||
Ok(res) => res.is_none(),
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!("Failed to read key '{}' from redb: {}", key, e);
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
};
|
if opened_ok {
|
||||||
|
if let Err(e) = write_txn.commit() {
|
||||||
if is_missing {
|
tracing::error!("Failed to commit database initialization transaction: {}", e);
|
||||||
let json_path = base.join(file_name);
|
|
||||||
if json_path.exists()
|
|
||||||
&& let Ok(data) = std::fs::read(&json_path)
|
|
||||||
&& serde_json::from_slice::<serde_json::Value>(&data).is_ok()
|
|
||||||
{
|
|
||||||
if let Err(e) = table.insert(*key, data.as_slice()) {
|
|
||||||
tracing::error!(
|
|
||||||
"Failed to insert migrated key '{}': {}",
|
|
||||||
key,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let migrated_path = json_path.with_extension("json.migrated");
|
|
||||||
if std::fs::rename(&json_path, &migrated_path).is_err()
|
|
||||||
&& migrated_path.exists()
|
|
||||||
{
|
|
||||||
let _ = std::fs::remove_file(&migrated_path);
|
|
||||||
let _ = std::fs::rename(&json_path, &migrated_path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if opened_ok && let Err(e) = write_txn.commit() {
|
|
||||||
tracing::error!("Failed to commit database migration transaction: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
"Failed to begin write transaction for redb migration: {}",
|
"Failed to begin write transaction for redb initialization: {}",
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+170
-32
@@ -163,9 +163,15 @@ impl McpTool for CreateEntitiesHandler {
|
|||||||
let req: CreateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: CreateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let mut inserted = Vec::new();
|
let mut inserted = Vec::new();
|
||||||
state.modify_graph(|g| {
|
state.modify_graph(|g| {
|
||||||
for mut entity in req.entities {
|
for entity_input in req.entities {
|
||||||
if !entity.name.is_empty() {
|
if !entity_input.name.is_empty() {
|
||||||
entity.entity_type = crate::models::normalize_entity_type(&entity.entity_type);
|
let entity = crate::models::Entity {
|
||||||
|
name: entity_input.name.clone(),
|
||||||
|
entity_type: crate::models::normalize_entity_type(&entity_input.entity_type),
|
||||||
|
observations: entity_input.observations,
|
||||||
|
namespace: entity_input.namespace.unwrap_or_else(crate::models::default_namespace),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
inserted.push(entity.clone());
|
inserted.push(entity.clone());
|
||||||
g.entities.insert(entity.name.clone(), entity);
|
g.entities.insert(entity.name.clone(), entity);
|
||||||
}
|
}
|
||||||
@@ -187,6 +193,108 @@ impl McpTool for CreateEntitiesHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct UpdateEntitiesHandler;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl McpTool for UpdateEntitiesHandler {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"update_entities"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schema(&self) -> Value {
|
||||||
|
crate::mcp::tool_def::<UpdateEntitiesTool>(
|
||||||
|
"update_entities",
|
||||||
|
"Update existing entities in the knowledge graph (e.g., to rename an entity or change its type).",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
|
let req: UpdateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let mut updated_entities = Vec::new();
|
||||||
|
let mut deleted_names = Vec::new();
|
||||||
|
let mut not_found = Vec::new();
|
||||||
|
let mut conflict_names = Vec::new();
|
||||||
|
|
||||||
|
state.modify_graph(|g| {
|
||||||
|
for update in req.updates {
|
||||||
|
if !g.entities.contains_key(&update.name) {
|
||||||
|
not_found.push(update.name.clone());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(new_name) = &update.new_name {
|
||||||
|
if update.name != *new_name && g.entities.contains_key(new_name) {
|
||||||
|
conflict_names.push(new_name.clone());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mut entity) = g.entities.remove(&update.name) {
|
||||||
|
let mut renamed = false;
|
||||||
|
let old_name = entity.name.clone();
|
||||||
|
|
||||||
|
if let Some(new_name) = update.new_name {
|
||||||
|
if old_name != new_name {
|
||||||
|
entity.name = new_name.clone();
|
||||||
|
renamed = true;
|
||||||
|
deleted_names.push(old_name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(new_type) = update.new_entity_type {
|
||||||
|
entity.entity_type = crate::models::normalize_entity_type(&new_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_name = entity.name.clone();
|
||||||
|
|
||||||
|
if renamed {
|
||||||
|
// update relations
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
g.relations.retain_mut(|r| {
|
||||||
|
if r.from == old_name {
|
||||||
|
r.from = current_name.clone();
|
||||||
|
}
|
||||||
|
if r.to == old_name {
|
||||||
|
r.to = current_name.clone();
|
||||||
|
}
|
||||||
|
// Keep unique relations
|
||||||
|
seen.insert(r.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updated_entities.push(entity.clone());
|
||||||
|
g.entities.insert(current_name, entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if !not_found.is_empty() {
|
||||||
|
return Err(crate::error::AppError::Internal(format!(
|
||||||
|
"Error: Entities not found: {}", not_found.join(", ")
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !conflict_names.is_empty() {
|
||||||
|
return Err(crate::error::AppError::Internal(format!(
|
||||||
|
"Error: Cannot rename to existing entity names: {}", conflict_names.join(", ")
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = state.get_search_index().await;
|
||||||
|
for old_name in deleted_names {
|
||||||
|
drop(idx.delete_document(&old_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updated_entities.is_empty() {
|
||||||
|
let _ = idx.index_entities_batch(&updated_entities).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let names: Vec<String> = updated_entities.iter().map(|e| e.name.clone()).collect();
|
||||||
|
Ok(format!("Successfully updated {} entity/entities: {}", names.len(), names.join(", ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct CreateRelationsHandler;
|
pub struct CreateRelationsHandler;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -256,10 +364,15 @@ impl McpTool for CreateRelationsHandler {
|
|||||||
auto_created.push(node_name);
|
auto_created.push(node_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for mut relation in req.relations {
|
for relation_input in req.relations {
|
||||||
if !relation.from.is_empty() && !relation.to.is_empty() {
|
if !relation_input.from.is_empty() && !relation_input.to.is_empty() {
|
||||||
relation.relation_type =
|
let relation = crate::models::Relation {
|
||||||
crate::models::normalize_relation_type(&relation.relation_type);
|
from: relation_input.from,
|
||||||
|
to: relation_input.to,
|
||||||
|
relation_type: crate::models::normalize_relation_type(&relation_input.relation_type),
|
||||||
|
namespace: relation_input.namespace.unwrap_or_else(crate::models::default_namespace),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
added_relations.push(format!(
|
added_relations.push(format!(
|
||||||
"{} -[{}]-> {}",
|
"{} -[{}]-> {}",
|
||||||
relation.from, relation.relation_type, relation.to
|
relation.from, relation.relation_type, relation.to
|
||||||
@@ -398,30 +511,48 @@ impl McpTool for DeleteObservationsHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: DeleteObservationsTool =
|
let req: DeleteObservationsTool =
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let mut missing = Vec::new();
|
|
||||||
state.read_graph(|g| {
|
|
||||||
for d in &req.deletions {
|
|
||||||
if !g.entities.contains_key(&d.entity_name) {
|
|
||||||
missing.push(d.entity_name.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if !missing.is_empty() {
|
let mut missing_entities = Vec::new();
|
||||||
return Err(crate::error::AppError::Internal(format!(
|
let mut missing_observations = Vec::new();
|
||||||
"Error: Entities not found: {}. Please use the search_nodes or read_graph tools to verify the exact entity names.",
|
|
||||||
missing.join(", ")
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
state.modify_graph(|master| {
|
state.modify_graph(|master| {
|
||||||
for d in req.deletions {
|
for d in req.deletions {
|
||||||
if let Some(e) = master.entities.get_mut(&d.entity_name) {
|
if let Some(e) = master.entities.get_mut(&d.entity_name) {
|
||||||
let to_rem: HashSet<_> = d.observations.into_iter().collect();
|
for target in d.observations {
|
||||||
e.observations.retain(|o| !to_rem.contains(o));
|
let target_clean = target.trim();
|
||||||
|
if target_clean.is_empty() { continue; }
|
||||||
|
|
||||||
|
let initial_len = e.observations.len();
|
||||||
|
e.observations.retain(|o| {
|
||||||
|
let o_clean = o.trim();
|
||||||
|
// Exact match OR substantial substring match (to forgive LLM whitespace/newline errors)
|
||||||
|
!(o_clean == target_clean || (target_clean.len() >= 10 && o_clean.contains(target_clean)))
|
||||||
|
});
|
||||||
|
|
||||||
|
if e.observations.len() == initial_len {
|
||||||
|
missing_observations.push(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
missing_entities.push(d.entity_name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if !missing_entities.is_empty() {
|
||||||
|
return Err(crate::error::AppError::Internal(format!(
|
||||||
|
"Error: Entities not found: {}. Please use search_nodes to verify the exact entity names.",
|
||||||
|
missing_entities.join(", ")
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !missing_observations.is_empty() {
|
||||||
|
return Err(crate::error::AppError::Internal(format!(
|
||||||
|
"Error: Failed to find {} observation(s) to delete. Make sure you provide a sufficiently long exact substring match.",
|
||||||
|
missing_observations.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
Ok("Observations deleted".to_string())
|
Ok("Observations deleted".to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -443,13 +574,21 @@ impl McpTool for DeleteRelationsHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: DeleteRelationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: DeleteRelationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let mut missing_count = 0;
|
let mut deleted_count = 0;
|
||||||
|
let requested_count = req.relations.len();
|
||||||
|
|
||||||
state.modify_graph(|master| {
|
state.modify_graph(|master| {
|
||||||
let to_rem: HashSet<_> = req.relations.into_iter().collect();
|
|
||||||
let initial_len = master.relations.len();
|
let initial_len = master.relations.len();
|
||||||
master.relations.retain(|r| !to_rem.contains(r));
|
master.relations.retain(|r| {
|
||||||
missing_count = to_rem.len() - (initial_len - master.relations.len());
|
let should_delete = req.relations.iter().any(|target| {
|
||||||
|
target.from == r.from && target.to == r.to && target.relation_type == r.relation_type
|
||||||
});
|
});
|
||||||
|
!should_delete
|
||||||
|
});
|
||||||
|
deleted_count = initial_len - master.relations.len();
|
||||||
|
});
|
||||||
|
|
||||||
|
let missing_count = requested_count.saturating_sub(deleted_count);
|
||||||
if missing_count > 0 {
|
if missing_count > 0 {
|
||||||
return Err(crate::error::AppError::Internal(format!(
|
return Err(crate::error::AppError::Internal(format!(
|
||||||
"Error: {} relation(s) not found in graph. Please verify exact relation properties (from, to, relation_type) using read_graph or get_subgraph.",
|
"Error: {} relation(s) not found in graph. Please verify exact relation properties (from, to, relation_type) using read_graph or get_subgraph.",
|
||||||
@@ -977,8 +1116,8 @@ impl McpTool for GetSubgraphHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: GetSubgraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: GetSubgraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let root = req.root_entity.or(req.root_node).ok_or_else(|| {
|
let root = req.root_entity.ok_or_else(|| {
|
||||||
crate::error::AppError::Internal("root_entity or root_node is required".to_string())
|
crate::error::AppError::Internal("root_entity is required".to_string())
|
||||||
})?;
|
})?;
|
||||||
let depth = req.depth.unwrap_or(2);
|
let depth = req.depth.unwrap_or(2);
|
||||||
let format = req.format.unwrap_or(SubgraphFormat::Json);
|
let format = req.format.unwrap_or(SubgraphFormat::Json);
|
||||||
@@ -1043,7 +1182,7 @@ impl McpTool for GetSubgraphHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result = serde_json::json!({
|
let result = serde_json::json!({
|
||||||
"root_node": root,
|
"root_entity": root,
|
||||||
"depth": depth,
|
"depth": depth,
|
||||||
"entities": matched_entities,
|
"entities": matched_entities,
|
||||||
"relations": matched_relations,
|
"relations": matched_relations,
|
||||||
@@ -1369,10 +1508,9 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(res.contains("Successfully created 1 relation(s)"));
|
assert!(res.contains("Successfully created 1 relation(s)"));
|
||||||
|
|
||||||
// Test serde field aliases (source/target/relationType mapped to from/to/relation_type)
|
|
||||||
let alias_args = json!({
|
let alias_args = json!({
|
||||||
"relations": [
|
"relations": [
|
||||||
{"source": "A", "target": "B", "relationType": "knows"}
|
{"from": "A", "to": "B", "relation_type": "knows"}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
let alias_res = handler.execute(alias_args, state.clone()).await.unwrap();
|
let alias_res = handler.execute(alias_args, state.clone()).await.unwrap();
|
||||||
|
|||||||
+98
-172
@@ -24,24 +24,7 @@ impl McpTool for LogErrorFixHandler {
|
|||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution);
|
let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution);
|
||||||
let mut solution = req.solution;
|
let solution = req.solution;
|
||||||
|
|
||||||
if state.ollama.is_available().await {
|
|
||||||
let prompt = format!(
|
|
||||||
"Analyze this error signature and solution. Output 1 sentence summarizing the root cause and fix:\nSignature: {}\nSolution: {}",
|
|
||||||
req.signature, solution
|
|
||||||
);
|
|
||||||
if let Ok(summary) = state
|
|
||||||
.ollama
|
|
||||||
.generate(&prompt, Some(&state.ollama.reasoning_model), None, None)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
let clean = summary.trim();
|
|
||||||
if !clean.is_empty() {
|
|
||||||
solution = format!("{} (AI Analysis: {})", solution, clean);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let embedding = crate::embedding::generate_embedding_async(text_to_embed)
|
let embedding = crate::embedding::generate_embedding_async(text_to_embed)
|
||||||
.await
|
.await
|
||||||
@@ -204,18 +187,6 @@ impl McpTool for LogCodeChangeHandler {
|
|||||||
description = format!("{} [Symbols: {}]", description, symbols.join(", "));
|
description = format!("{} [Symbols: {}]", description, symbols.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.ollama.is_available().await {
|
|
||||||
let prompt = format!(
|
|
||||||
"Summarize in 1 concise sentence the architectural impact of changing file '{}': {}",
|
|
||||||
req.file_path, description
|
|
||||||
);
|
|
||||||
if let Ok(summary) = state.ollama.generate(&prompt, None, None, None).await {
|
|
||||||
let clean = summary.trim();
|
|
||||||
if !clean.is_empty() {
|
|
||||||
description = format!("{} (AI Summary: {})", description, clean);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let change_kind = match req
|
let change_kind = match req
|
||||||
.change_kind
|
.change_kind
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -383,18 +354,14 @@ impl McpTool for DecisionsHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: DecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: DecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let ns = req
|
match req {
|
||||||
.namespace
|
DecisionsTool::Log { title, status, context, decision, consequences, namespace, repo_name, author, affected_components, alternatives_considered, supersedes, git_commit, git_branch, task_id } => {
|
||||||
.unwrap_or_else(crate::models::default_namespace);
|
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
|
||||||
match req.action {
|
let title = title;
|
||||||
DecisionAction::Log => {
|
let status = status.unwrap_or_else(|| "accepted".to_string());
|
||||||
let title = req.title.ok_or_else(|| {
|
let context = context.unwrap_or_default();
|
||||||
crate::error::AppError::Internal("Missing required parameter 'title' for action 'log'. Next step: Provide ADR 'title' string in request and retry.".to_string())
|
let decision = decision.unwrap_or_default();
|
||||||
})?;
|
let consequence = consequences.unwrap_or_default();
|
||||||
let status = req.status.unwrap_or_else(|| "accepted".to_string());
|
|
||||||
let context = req.context.unwrap_or_default();
|
|
||||||
let decision = req.decision.unwrap_or_default();
|
|
||||||
let consequence = req.consequences.unwrap_or_default();
|
|
||||||
let status_lower = status.to_ascii_lowercase();
|
let status_lower = status.to_ascii_lowercase();
|
||||||
let resolved_at = if status_lower == "implemented" || status_lower == "resolved" {
|
let resolved_at = if status_lower == "implemented" || status_lower == "resolved" {
|
||||||
Some(crate::handlers::utils::now_secs())
|
Some(crate::handlers::utils::now_secs())
|
||||||
@@ -407,7 +374,7 @@ impl McpTool for DecisionsHandler {
|
|||||||
let mut adrs_to_index = Vec::new();
|
let mut adrs_to_index = Vec::new();
|
||||||
|
|
||||||
state.code.adrs.modify(|adrs| {
|
state.code.adrs.modify(|adrs| {
|
||||||
if let Some(superseded_id) = &req.supersedes {
|
if let Some(superseded_id) = &supersedes {
|
||||||
for old_adr in adrs.iter_mut() {
|
for old_adr in adrs.iter_mut() {
|
||||||
if old_adr.id.eq_ignore_ascii_case(superseded_id) {
|
if old_adr.id.eq_ignore_ascii_case(superseded_id) {
|
||||||
old_adr.status = "superseded".to_string();
|
old_adr.status = "superseded".to_string();
|
||||||
@@ -425,17 +392,17 @@ impl McpTool for DecisionsHandler {
|
|||||||
decision: decision.clone(),
|
decision: decision.clone(),
|
||||||
consequence,
|
consequence,
|
||||||
status,
|
status,
|
||||||
supersedes: req.supersedes,
|
supersedes: supersedes,
|
||||||
timestamp: crate::handlers::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
namespace: ns,
|
namespace: ns,
|
||||||
repo_name: req.repo_name,
|
repo_name: repo_name,
|
||||||
alternatives_considered: req.alternatives_considered.unwrap_or_default(),
|
alternatives_considered: alternatives_considered.unwrap_or_default(),
|
||||||
affected_components: req.affected_components.unwrap_or_default(),
|
affected_components: affected_components.unwrap_or_default(),
|
||||||
author: req.author,
|
author: author,
|
||||||
git_commit: req.git_commit,
|
git_commit: git_commit,
|
||||||
git_branch: req.git_branch,
|
git_branch: git_branch,
|
||||||
resolved_at,
|
resolved_at,
|
||||||
task_id: req.task_id,
|
task_id: task_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
adrs_to_index.push(a.clone());
|
adrs_to_index.push(a.clone());
|
||||||
@@ -453,17 +420,15 @@ impl McpTool for DecisionsHandler {
|
|||||||
);
|
);
|
||||||
Ok(format!("Logged decision {}: {}", final_id, title))
|
Ok(format!("Logged decision {}: {}", final_id, title))
|
||||||
}
|
}
|
||||||
DecisionAction::Update => {
|
DecisionsTool::Update { id, title, status, context, decision, consequences, git_commit, git_branch, task_id, namespace: _, repo_name, author, affected_components, alternatives_considered, supersedes } => {
|
||||||
let id = req.id.ok_or_else(|| {
|
let id = id;
|
||||||
crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide ADR 'id' string in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let mut updated_adr = None;
|
let mut updated_adr = None;
|
||||||
let mut adrs_to_index = Vec::new();
|
let mut adrs_to_index = Vec::new();
|
||||||
|
|
||||||
state.code.adrs.modify(|adrs| {
|
state.code.adrs.modify(|adrs| {
|
||||||
let target_pos = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(&id));
|
let target_pos = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(&id));
|
||||||
if let Some(pos) = target_pos {
|
if let Some(pos) = target_pos {
|
||||||
if let Some(superseded_id) = &req.supersedes
|
if let Some(superseded_id) = &supersedes
|
||||||
&& let Some(s_pos) = adrs
|
&& let Some(s_pos) = adrs
|
||||||
.iter()
|
.iter()
|
||||||
.position(|a| a.id.eq_ignore_ascii_case(superseded_id))
|
.position(|a| a.id.eq_ignore_ascii_case(superseded_id))
|
||||||
@@ -474,19 +439,19 @@ impl McpTool for DecisionsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let a = &mut adrs[pos];
|
let a = &mut adrs[pos];
|
||||||
if let Some(t) = req.title {
|
if let Some(t) = title {
|
||||||
a.title = t;
|
a.title = t;
|
||||||
}
|
}
|
||||||
if let Some(c) = req.context {
|
if let Some(c) = context {
|
||||||
a.context = c;
|
a.context = c;
|
||||||
}
|
}
|
||||||
if let Some(d) = req.decision {
|
if let Some(d) = decision {
|
||||||
a.decision = d;
|
a.decision = d;
|
||||||
}
|
}
|
||||||
if let Some(cons) = req.consequences {
|
if let Some(cons) = consequences {
|
||||||
a.consequence = cons;
|
a.consequence = cons;
|
||||||
}
|
}
|
||||||
if let Some(s) = req.status {
|
if let Some(s) = status {
|
||||||
let s_lower = s.to_ascii_lowercase();
|
let s_lower = s.to_ascii_lowercase();
|
||||||
if (s_lower == "implemented" || s_lower == "resolved")
|
if (s_lower == "implemented" || s_lower == "resolved")
|
||||||
&& a.resolved_at.is_none()
|
&& a.resolved_at.is_none()
|
||||||
@@ -497,29 +462,29 @@ impl McpTool for DecisionsHandler {
|
|||||||
}
|
}
|
||||||
a.status = s;
|
a.status = s;
|
||||||
}
|
}
|
||||||
if req.supersedes.is_some() {
|
if supersedes.is_some() {
|
||||||
a.supersedes = req.supersedes;
|
a.supersedes = supersedes;
|
||||||
}
|
}
|
||||||
if req.repo_name.is_some() {
|
if repo_name.is_some() {
|
||||||
a.repo_name = req.repo_name;
|
a.repo_name = repo_name;
|
||||||
}
|
}
|
||||||
if let Some(alts) = req.alternatives_considered {
|
if let Some(alts) = alternatives_considered {
|
||||||
a.alternatives_considered = alts;
|
a.alternatives_considered = alts;
|
||||||
}
|
}
|
||||||
if let Some(aff) = req.affected_components {
|
if let Some(aff) = affected_components {
|
||||||
a.affected_components = aff;
|
a.affected_components = aff;
|
||||||
}
|
}
|
||||||
if req.author.is_some() {
|
if author.is_some() {
|
||||||
a.author = req.author;
|
a.author = author;
|
||||||
}
|
}
|
||||||
if req.git_commit.is_some() {
|
if git_commit.is_some() {
|
||||||
a.git_commit = req.git_commit;
|
a.git_commit = git_commit;
|
||||||
}
|
}
|
||||||
if req.git_branch.is_some() {
|
if git_branch.is_some() {
|
||||||
a.git_branch = req.git_branch;
|
a.git_branch = git_branch;
|
||||||
}
|
}
|
||||||
if req.task_id.is_some() {
|
if task_id.is_some() {
|
||||||
a.task_id = req.task_id;
|
a.task_id = task_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
adrs_to_index.push(a.clone());
|
adrs_to_index.push(a.clone());
|
||||||
@@ -548,9 +513,10 @@ impl McpTool for DecisionsHandler {
|
|||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DecisionAction::Query => {
|
DecisionsTool::Query { query, namespace, limit, include_body } => {
|
||||||
let limit = req.limit.unwrap_or(20);
|
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
|
||||||
let include_body = req.include_body.unwrap_or(true);
|
let limit = limit.unwrap_or(20);
|
||||||
|
let include_body = include_body.unwrap_or(true);
|
||||||
let data = state.code.adrs.read_with(|adrs| {
|
let data = state.code.adrs.read_with(|adrs| {
|
||||||
let filtered: Vec<_> = adrs
|
let filtered: Vec<_> = adrs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -558,7 +524,7 @@ impl McpTool for DecisionsHandler {
|
|||||||
if !a.namespace.eq_ignore_ascii_case(&ns) && ns != "global" {
|
if !a.namespace.eq_ignore_ascii_case(&ns) && ns != "global" {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(q) = &req.query {
|
if let Some(q) = &query {
|
||||||
crate::handlers::utils::contains_ignore_ascii_case(&a.title, q)
|
crate::handlers::utils::contains_ignore_ascii_case(&a.title, q)
|
||||||
|| crate::handlers::utils::contains_ignore_ascii_case(
|
|| crate::handlers::utils::contains_ignore_ascii_case(
|
||||||
&a.context, q,
|
&a.context, q,
|
||||||
@@ -601,10 +567,8 @@ impl McpTool for DecisionsHandler {
|
|||||||
})?;
|
})?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
DecisionAction::Delete => {
|
DecisionsTool::Delete { id } => {
|
||||||
let id = req.id.ok_or_else(|| {
|
let id = id;
|
||||||
crate::error::AppError::Internal("Missing required parameter 'id' for action 'delete'. Next step: Provide ADR 'id' string in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
state.code.adrs.modify(|adrs| {
|
state.code.adrs.modify(|adrs| {
|
||||||
if let Some(pos) = adrs.iter().position(|a| a.id == id) {
|
if let Some(pos) = adrs.iter().position(|a| a.id == id) {
|
||||||
@@ -644,15 +608,11 @@ impl McpTool for TechDebtHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: TechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: TechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let ns = req
|
match req {
|
||||||
.namespace
|
TechDebtTool::Log { description, title: _, ideal_solution, git_commit, git_branch, symbol_references: _, line_range, namespace, severity, repo_name, file_path, workaround, effort_estimate } => {
|
||||||
.unwrap_or_else(crate::models::default_namespace);
|
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
|
||||||
match req.action {
|
let desc = description;
|
||||||
TechDebtAction::Log => {
|
let ideal = ideal_solution.unwrap_or_default();
|
||||||
let desc = req.description.or(req.title).ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal("Missing required parameter 'description' for action 'log'. Next step: Provide tech debt 'description' in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let ideal = req.ideal_solution.unwrap_or_default();
|
|
||||||
let text_to_embed = format!("Description: {}\nIdeal Solution: {}", desc, ideal);
|
let text_to_embed = format!("Description: {}\nIdeal Solution: {}", desc, ideal);
|
||||||
let embedding = crate::embedding::generate_embedding_async(text_to_embed)
|
let embedding = crate::embedding::generate_embedding_async(text_to_embed)
|
||||||
.await
|
.await
|
||||||
@@ -665,15 +625,15 @@ impl McpTool for TechDebtHandler {
|
|||||||
ideal_solution: ideal,
|
ideal_solution: ideal,
|
||||||
is_resolved: false,
|
is_resolved: false,
|
||||||
created_at: crate::handlers::utils::now_secs(),
|
created_at: crate::handlers::utils::now_secs(),
|
||||||
git_commit: req.git_commit,
|
git_commit: git_commit,
|
||||||
git_branch: req.git_branch,
|
git_branch: git_branch,
|
||||||
embedding,
|
embedding,
|
||||||
repo_name: req.repo_name,
|
repo_name: repo_name,
|
||||||
severity: req.severity,
|
severity: severity,
|
||||||
file_path: req.file_path,
|
file_path: file_path,
|
||||||
line_range: req.line_range,
|
line_range: line_range,
|
||||||
workaround: req.workaround,
|
workaround: workaround,
|
||||||
effort_estimate: req.effort_estimate,
|
effort_estimate: effort_estimate,
|
||||||
});
|
});
|
||||||
if debts.len() > 300 {
|
if debts.len() > 300 {
|
||||||
let severity_rank =
|
let severity_rank =
|
||||||
@@ -697,10 +657,8 @@ impl McpTool for TechDebtHandler {
|
|||||||
});
|
});
|
||||||
Ok("Tech debt logged".to_string())
|
Ok("Tech debt logged".to_string())
|
||||||
}
|
}
|
||||||
TechDebtAction::Resolve => {
|
TechDebtTool::Resolve { id } => {
|
||||||
let id = req.id.ok_or_else(|| {
|
let id = id;
|
||||||
crate::error::AppError::Internal("Missing required parameter 'id' for action 'resolve'. Next step: Provide tech debt 'id' string in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
state.code.tech_debts.modify(|debts| {
|
state.code.tech_debts.modify(|debts| {
|
||||||
for d in debts.iter_mut() {
|
for d in debts.iter_mut() {
|
||||||
@@ -720,9 +678,10 @@ impl McpTool for TechDebtHandler {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TechDebtAction::List => {
|
TechDebtTool::List { include_resolved, namespace, summary_level, max_tokens } => {
|
||||||
let inc = req.include_resolved.unwrap_or(false);
|
let ns = namespace.clone().unwrap_or_else(crate::models::default_namespace);
|
||||||
let level = req.summary_level.as_deref().unwrap_or("detailed");
|
let inc = include_resolved.unwrap_or(false);
|
||||||
|
let level = summary_level.as_deref().unwrap_or("detailed");
|
||||||
let data = state.code.tech_debts.read_with(|debts| {
|
let data = state.code.tech_debts.read_with(|debts| {
|
||||||
let filtered: Vec<_> = debts
|
let filtered: Vec<_> = debts
|
||||||
.iter()
|
.iter()
|
||||||
@@ -743,7 +702,7 @@ impl McpTool for TechDebtHandler {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let mut json_str = serde_yaml::to_string(&filtered)?;
|
let mut json_str = serde_yaml::to_string(&filtered)?;
|
||||||
if let Some(max_t) = req.max_tokens {
|
if let Some(max_t) = max_tokens {
|
||||||
let char_limit = max_t * 4;
|
let char_limit = max_t * 4;
|
||||||
if json_str.len() > char_limit {
|
if json_str.len() > char_limit {
|
||||||
json_str.truncate(char_limit);
|
json_str.truncate(char_limit);
|
||||||
@@ -1199,13 +1158,9 @@ impl McpTool for ManageCheckpointHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: ManageCheckpointTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: ManageCheckpointTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
match req.action {
|
match req {
|
||||||
CheckpointAction::Create => {
|
ManageCheckpointTool::Create { name_or_id, description, namespace } => {
|
||||||
let name = req.name_or_id.ok_or_else(|| {
|
let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?;
|
||||||
crate::error::AppError::Internal(
|
|
||||||
"name_or_id is required for 'create' action".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let target_dir = state.base_dir.join("checkpoints").join(&name);
|
let target_dir = state.base_dir.join("checkpoints").join(&name);
|
||||||
|
|
||||||
if let Err(e) = std::fs::create_dir_all(&target_dir) {
|
if let Err(e) = std::fs::create_dir_all(&target_dir) {
|
||||||
@@ -1230,15 +1185,12 @@ impl McpTool for ManageCheckpointHandler {
|
|||||||
.read_with(|d| serde_json::to_string(d).unwrap_or_default());
|
.read_with(|d| serde_json::to_string(d).unwrap_or_default());
|
||||||
let _ = std::fs::write(target_dir.join("tech_debts.json"), debts_json);
|
let _ = std::fs::write(target_dir.join("tech_debts.json"), debts_json);
|
||||||
|
|
||||||
if let Some(desc) = &req.description {
|
if let Some(desc) = &description {
|
||||||
let snapshot_id = format!(
|
let snapshot_id = format!(
|
||||||
"SNAP-{}",
|
"SNAP-{}",
|
||||||
uuid::Uuid::new_v4().to_string()[..8].to_uppercase()
|
uuid::Uuid::new_v4().to_string()[..8].to_uppercase()
|
||||||
);
|
);
|
||||||
let ns = req
|
let ns = namespace.clone().unwrap_or_else(|| "global".to_string());
|
||||||
.namespace
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "global".to_string());
|
|
||||||
let snapshot = crate::models::StateSnapshot {
|
let snapshot = crate::models::StateSnapshot {
|
||||||
id: snapshot_id,
|
id: snapshot_id,
|
||||||
timestamp: crate::handlers::utils::now_secs(),
|
timestamp: crate::handlers::utils::now_secs(),
|
||||||
@@ -1251,12 +1203,8 @@ impl McpTool for ManageCheckpointHandler {
|
|||||||
|
|
||||||
Ok(format!("Checkpoint '{}' created successfully.", name))
|
Ok(format!("Checkpoint '{}' created successfully.", name))
|
||||||
}
|
}
|
||||||
CheckpointAction::Restore => {
|
ManageCheckpointTool::Restore { name_or_id, namespace: _ } => {
|
||||||
let name = req.name_or_id.ok_or_else(|| {
|
let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?;
|
||||||
crate::error::AppError::Internal(
|
|
||||||
"name_or_id is required for 'restore' action".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let target_dir = state.base_dir.join("checkpoints").join(&name);
|
let target_dir = state.base_dir.join("checkpoints").join(&name);
|
||||||
|
|
||||||
if !target_dir.exists() {
|
if !target_dir.exists() {
|
||||||
@@ -1297,7 +1245,7 @@ impl McpTool for ManageCheckpointHandler {
|
|||||||
|
|
||||||
Ok(format!("Checkpoint '{}' restored successfully.", name))
|
Ok(format!("Checkpoint '{}' restored successfully.", name))
|
||||||
}
|
}
|
||||||
CheckpointAction::List => {
|
ManageCheckpointTool::List { namespace: _ } => {
|
||||||
let mut list = Vec::new();
|
let mut list = Vec::new();
|
||||||
let checkpoints_dir = state.base_dir.join("checkpoints");
|
let checkpoints_dir = state.base_dir.join("checkpoints");
|
||||||
if let Ok(entries) = std::fs::read_dir(&checkpoints_dir) {
|
if let Ok(entries) = std::fs::read_dir(&checkpoints_dir) {
|
||||||
@@ -1315,12 +1263,8 @@ impl McpTool for ManageCheckpointHandler {
|
|||||||
}
|
}
|
||||||
Ok(serde_yaml::to_string(&list)?)
|
Ok(serde_yaml::to_string(&list)?)
|
||||||
}
|
}
|
||||||
CheckpointAction::Delete => {
|
ManageCheckpointTool::Delete { name_or_id, namespace: _ } => {
|
||||||
let name = req.name_or_id.ok_or_else(|| {
|
let name = name_or_id.ok_or_else(|| crate::error::AppError::Internal("name_or_id is required".to_string()))?;
|
||||||
crate::error::AppError::Internal(
|
|
||||||
"name_or_id is required for 'delete' action".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let target_dir = state.base_dir.join("checkpoints").join(&name);
|
let target_dir = state.base_dir.join("checkpoints").join(&name);
|
||||||
if target_dir.exists() {
|
if target_dir.exists() {
|
||||||
let _ = std::fs::remove_dir_all(&target_dir);
|
let _ = std::fs::remove_dir_all(&target_dir);
|
||||||
@@ -1516,14 +1460,9 @@ impl McpTool for HypothesesHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: HypothesesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: HypothesesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
|
match req {
|
||||||
match req.action {
|
HypothesesTool::Log { hypothesis, task_id, status, evidence } => {
|
||||||
HypothesisAction::Log => {
|
let hyp_text = hypothesis;
|
||||||
let hyp_text = req.hypothesis.ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal(
|
|
||||||
"Missing required 'hypothesis' for action 'log'".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let hyp_id = format!(
|
let hyp_id = format!(
|
||||||
"HYP-{}",
|
"HYP-{}",
|
||||||
uuid::Uuid::new_v4().to_string()[..8].to_uppercase()
|
uuid::Uuid::new_v4().to_string()[..8].to_uppercase()
|
||||||
@@ -1532,10 +1471,10 @@ impl McpTool for HypothesesHandler {
|
|||||||
|
|
||||||
let record = crate::models::Hypothesis {
|
let record = crate::models::Hypothesis {
|
||||||
id: hyp_id.clone(),
|
id: hyp_id.clone(),
|
||||||
task_id: req.task_id,
|
task_id: task_id,
|
||||||
hypothesis: hyp_text,
|
hypothesis: hyp_text,
|
||||||
status: req.status.unwrap_or_else(|| "unverified".to_string()),
|
status: status.unwrap_or_else(|| "unverified".to_string()),
|
||||||
evidence: req.evidence,
|
evidence: evidence,
|
||||||
timestamp,
|
timestamp,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -1544,17 +1483,17 @@ impl McpTool for HypothesesHandler {
|
|||||||
|
|
||||||
Ok(format!("Hypothesis '{}' logged successfully.", hyp_id))
|
Ok(format!("Hypothesis '{}' logged successfully.", hyp_id))
|
||||||
}
|
}
|
||||||
HypothesisAction::Query => {
|
HypothesesTool::Query { query, task_id, status: _ } => {
|
||||||
let hypotheses = state.code.hypotheses.read_with(|h| h.clone());
|
let hypotheses = state.code.hypotheses.read_with(|h| h.clone());
|
||||||
let filtered: Vec<_> = hypotheses
|
let filtered: Vec<_> = hypotheses
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|h| {
|
.filter(|h| {
|
||||||
if let Some(tid) = &req.task_id
|
if let Some(tid) = &task_id
|
||||||
&& h.task_id.as_ref() != Some(tid)
|
&& h.task_id.as_ref() != Some(tid)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(q) = &req.query {
|
if let Some(q) = &query {
|
||||||
let lq = q.to_lowercase();
|
let lq = q.to_lowercase();
|
||||||
return h.hypothesis.to_lowercase().contains(&lq)
|
return h.hypothesis.to_lowercase().contains(&lq)
|
||||||
|| h.evidence
|
|| h.evidence
|
||||||
@@ -1665,24 +1604,11 @@ impl McpTool for AgentSignalsHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: AgentSignalsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: AgentSignalsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
|
match req {
|
||||||
match req.action {
|
AgentSignalsTool::Broadcast { sender, signal_type, payload, ttl_seconds } => {
|
||||||
AgentSignalAction::Broadcast => {
|
let sender = sender;
|
||||||
let sender = req.sender.ok_or_else(|| {
|
let signal_type = signal_type;
|
||||||
crate::error::AppError::Internal(
|
let payload = payload;
|
||||||
"Missing required 'sender' for action 'broadcast'".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let signal_type = req.signal_type.ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal(
|
|
||||||
"Missing required 'signal_type' for action 'broadcast'".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let payload = req.payload.ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal(
|
|
||||||
"Missing required 'payload' for action 'broadcast'".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let timestamp = std::time::SystemTime::now()
|
let timestamp = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
@@ -1696,7 +1622,7 @@ impl McpTool for AgentSignalsHandler {
|
|||||||
signal_type: signal_type.clone(),
|
signal_type: signal_type.clone(),
|
||||||
payload,
|
payload,
|
||||||
timestamp,
|
timestamp,
|
||||||
ttl_seconds: req.ttl_seconds,
|
ttl_seconds: ttl_seconds,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1724,7 +1650,7 @@ impl McpTool for AgentSignalsHandler {
|
|||||||
sig_id, sender
|
sig_id, sender
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
AgentSignalAction::Query => {
|
AgentSignalsTool::Query { sender, signal_type, limit } => {
|
||||||
let now = std::time::SystemTime::now()
|
let now = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -1739,19 +1665,19 @@ impl McpTool for AgentSignalsHandler {
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(sender) = &req.sender
|
if let Some(sender) = &sender
|
||||||
&& s.sender.to_lowercase() != sender.to_lowercase()
|
&& s.sender.to_lowercase() != sender.to_lowercase()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(st) = &req.signal_type
|
if let Some(st) = &signal_type
|
||||||
&& s.signal_type.to_lowercase() != st.to_lowercase()
|
&& s.signal_type.to_lowercase() != st.to_lowercase()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
.take(req.limit.unwrap_or(20))
|
.take(limit.unwrap_or(20))
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
@@ -1900,7 +1826,7 @@ mod tests {
|
|||||||
"title": "Architecture",
|
"title": "Architecture",
|
||||||
"context": "Needs DB",
|
"context": "Needs DB",
|
||||||
"decision": "Use SQLite",
|
"decision": "Use SQLite",
|
||||||
"consequence": "Simple",
|
"consequences": "Simple",
|
||||||
});
|
});
|
||||||
let res1 = decision_handler
|
let res1 = decision_handler
|
||||||
.execute(args_dec, state.clone())
|
.execute(args_dec, state.clone())
|
||||||
@@ -2112,7 +2038,7 @@ mod tests {
|
|||||||
"title": "Use Axum",
|
"title": "Use Axum",
|
||||||
"context": "Architecture choice",
|
"context": "Architecture choice",
|
||||||
"decision": "Adopt Axum for web framework",
|
"decision": "Adopt Axum for web framework",
|
||||||
"consequence": "Fast async API routing"
|
"consequences": "Fast async API routing"
|
||||||
}),
|
}),
|
||||||
state.clone(),
|
state.clone(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -23,15 +23,12 @@ impl McpTool for TasksHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: TasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: TasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
match req.action {
|
match req {
|
||||||
TaskAction::Add => {
|
TasksTool::Add { title, description, parent_id, dependencies, git_branch, namespace, repo_name, priority, assigned_agent, verification_command } => {
|
||||||
let title = req.title.ok_or_else(|| {
|
let description = description.unwrap_or_default();
|
||||||
crate::error::AppError::Internal("Missing required parameter 'title' for action 'add'. Next step: Provide non-empty 'title' string in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let description = req.description.unwrap_or_default();
|
|
||||||
let now = crate::handlers::utils::now_secs();
|
let now = crate::handlers::utils::now_secs();
|
||||||
let task_id = uuid::Uuid::new_v4().to_string();
|
let task_id = uuid::Uuid::new_v4().to_string();
|
||||||
let deps = req.dependencies.unwrap_or_default();
|
let deps = dependencies.unwrap_or_default();
|
||||||
|
|
||||||
let task = Task {
|
let task = Task {
|
||||||
id: task_id.clone(),
|
id: task_id.clone(),
|
||||||
@@ -40,16 +37,16 @@ impl McpTool for TasksHandler {
|
|||||||
description,
|
description,
|
||||||
created_at: now,
|
created_at: now,
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
git_branch: req.git_branch,
|
git_branch,
|
||||||
parent_id: req.parent_id,
|
parent_id,
|
||||||
dependencies: deps,
|
dependencies: deps,
|
||||||
acceptance_criteria: vec![],
|
acceptance_criteria: vec![],
|
||||||
expires_at: None,
|
expires_at: None,
|
||||||
namespace: req.namespace.unwrap_or_else(|| "default".to_string()),
|
namespace: namespace.unwrap_or_else(|| "default".to_string()),
|
||||||
repo_name: req.repo_name,
|
repo_name,
|
||||||
priority: req.priority,
|
priority,
|
||||||
assigned_agent: req.assigned_agent,
|
assigned_agent,
|
||||||
verification_command: req.verification_command,
|
verification_command,
|
||||||
};
|
};
|
||||||
let idx = state.get_search_index().await;
|
let idx = state.get_search_index().await;
|
||||||
drop(idx.index_task(&task));
|
drop(idx.index_task(&task));
|
||||||
@@ -75,13 +72,7 @@ impl McpTool for TasksHandler {
|
|||||||
});
|
});
|
||||||
Ok(format!("Task added with ID: {}", task_id))
|
Ok(format!("Task added with ID: {}", task_id))
|
||||||
}
|
}
|
||||||
TaskAction::Update => {
|
TasksTool::Update { id, status } => {
|
||||||
let id = req.id.ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide task 'id' string in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let status = req.status.ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide valid 'status' ('pending', 'active', 'in_progress', 'blocked', 'completed', or 'cancelled') in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let target_status = status.to_lowercase();
|
let target_status = status.to_lowercase();
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
let mut blocked = false;
|
let mut blocked = false;
|
||||||
@@ -202,10 +193,7 @@ impl McpTool for TasksHandler {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TaskAction::Delete => {
|
TasksTool::Delete { id } => {
|
||||||
let id = req.id.ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal("Missing required parameter 'id' for action 'delete'. Next step: Provide task 'id' string in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let mut deleted_count = 0;
|
let mut deleted_count = 0;
|
||||||
let mut actually_deleted = Vec::new();
|
let mut actually_deleted = Vec::new();
|
||||||
state.project.tasks.modify(|tasks| {
|
state.project.tasks.modify(|tasks| {
|
||||||
@@ -267,19 +255,19 @@ impl McpTool for TasksHandler {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TaskAction::List => {
|
TasksTool::List { git_branch, status, summary_level, max_tokens, offset } => {
|
||||||
let level = req.summary_level.as_deref().unwrap_or("detailed");
|
let level = summary_level.as_deref().unwrap_or("detailed");
|
||||||
let offset = req.offset.unwrap_or(0);
|
let offset = offset.unwrap_or(0);
|
||||||
let data = state.project.tasks.read_with(|tasks| {
|
let data = state.project.tasks.read_with(|tasks| {
|
||||||
let matching: Vec<_> = tasks
|
let matching: Vec<_> = tasks
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|t| {
|
.filter(|t| {
|
||||||
let status_match = match req.status.as_deref() {
|
let status_match = match status.as_deref() {
|
||||||
Some("all") => true,
|
Some("all") => true,
|
||||||
Some(s) => t.status.eq_ignore_ascii_case(s),
|
Some(s) => t.status.eq_ignore_ascii_case(s),
|
||||||
None => t.is_active(),
|
None => t.is_active(),
|
||||||
};
|
};
|
||||||
let branch_match = match &req.git_branch {
|
let branch_match = match &git_branch {
|
||||||
Some(branch) => t.git_branch.is_none() || t.git_branch.as_deref() == Some(branch.as_str()),
|
Some(branch) => t.git_branch.is_none() || t.git_branch.as_deref() == Some(branch.as_str()),
|
||||||
None => true,
|
None => true,
|
||||||
};
|
};
|
||||||
@@ -297,7 +285,7 @@ impl McpTool for TasksHandler {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let mut json_str = serde_yaml::to_string(&filtered)?;
|
let mut json_str = serde_yaml::to_string(&filtered)?;
|
||||||
if let Some(max_t) = req.max_tokens {
|
if let Some(max_t) = max_tokens {
|
||||||
let char_limit = max_t * 4;
|
let char_limit = max_t * 4;
|
||||||
while json_str.len() > char_limit && !filtered.is_empty() {
|
while json_str.len() > char_limit && !filtered.is_empty() {
|
||||||
filtered.pop();
|
filtered.pop();
|
||||||
@@ -308,13 +296,7 @@ impl McpTool for TasksHandler {
|
|||||||
})?;
|
})?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
TaskAction::SetCriteria => {
|
TasksTool::SetCriteria { id, criteria: criteria_list } => {
|
||||||
let id = req.id.or(req.title.clone()).ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal("Missing required parameter 'id' or 'title' for action 'set_criteria'. Next step: Provide task 'id' string in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let criteria_list = req.criteria.ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal("Missing required parameter 'criteria' for action 'set_criteria'. Next step: Provide array of acceptance criteria strings in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let mut success = false;
|
let mut success = false;
|
||||||
let mut updated_task: Option<Task> = None;
|
let mut updated_task: Option<Task> = None;
|
||||||
state.project.tasks.modify(|tasks| {
|
state.project.tasks.modify(|tasks| {
|
||||||
@@ -355,11 +337,8 @@ impl McpTool for TasksHandler {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TaskAction::Verify => {
|
TasksTool::Verify { id, proof } => {
|
||||||
let id = req.id.ok_or_else(|| {
|
let proof_str = proof.unwrap_or_else(|| "Verified".to_string());
|
||||||
crate::error::AppError::Internal("Missing required parameter 'id' for action 'verify'. Next step: Provide task 'id' string in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let proof_str = req.proof.unwrap_or_else(|| "Verified".to_string());
|
|
||||||
let mut success = false;
|
let mut success = false;
|
||||||
let mut already_met = false;
|
let mut already_met = false;
|
||||||
let mut updated_task: Option<Task> = None;
|
let mut updated_task: Option<Task> = None;
|
||||||
@@ -436,41 +415,29 @@ impl McpTool for MilestonesHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: MilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: MilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
match req.action {
|
match req {
|
||||||
MilestoneAction::Add => {
|
MilestonesTool::Add { title, namespace, target_date, description, deliverables, task_ids, repo_name } => {
|
||||||
let title = req.title.ok_or_else(|| {
|
let ns = namespace.unwrap_or_else(crate::models::default_namespace);
|
||||||
crate::error::AppError::Internal("Missing required parameter 'title' for action 'add'. Next step: Provide non-empty 'title' string in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let ns = req
|
|
||||||
.namespace
|
|
||||||
.unwrap_or_else(crate::models::default_namespace);
|
|
||||||
state.project.milestones.modify(|ms| {
|
state.project.milestones.modify(|ms| {
|
||||||
ms.push(crate::models::Milestone {
|
ms.push(crate::models::Milestone {
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
title,
|
title,
|
||||||
status: "pending".to_string(),
|
status: "pending".to_string(),
|
||||||
namespace: ns,
|
namespace: ns,
|
||||||
target_date: req
|
target_date: target_date
|
||||||
.target_date
|
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(|s| s.parse::<u64>().ok()),
|
.and_then(|s| s.parse::<u64>().ok()),
|
||||||
description: req.description,
|
description: description,
|
||||||
task_ids: req.task_ids.unwrap_or_default(),
|
task_ids: task_ids.unwrap_or_default(),
|
||||||
deliverables: req.deliverables.unwrap_or_default(),
|
deliverables: deliverables.unwrap_or_default(),
|
||||||
repo_name: req.repo_name,
|
repo_name,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
crate::handlers::reconciliation::recalculate_milestones(&state);
|
crate::handlers::reconciliation::recalculate_milestones(&state);
|
||||||
Ok("Milestone added".to_string())
|
Ok("Milestone added".to_string())
|
||||||
}
|
}
|
||||||
MilestoneAction::Update => {
|
MilestonesTool::Update { id, status, deliverables, task_ids } => {
|
||||||
let id = req.id.ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide milestone 'id' string in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let status = req.status.ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide milestone 'status' in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
state.project.milestones.modify(|ms| {
|
state.project.milestones.modify(|ms| {
|
||||||
for m in ms.iter_mut() {
|
for m in ms.iter_mut() {
|
||||||
@@ -482,9 +449,9 @@ impl McpTool for MilestonesHandler {
|
|||||||
{
|
{
|
||||||
m.completed_at = Some(crate::handlers::utils::now_secs());
|
m.completed_at = Some(crate::handlers::utils::now_secs());
|
||||||
}
|
}
|
||||||
if let Some(task_ids) = &req.task_ids {
|
if let Some(task_ids) = &task_ids {
|
||||||
m.task_ids = task_ids.clone();
|
m.task_ids = task_ids.clone();
|
||||||
if let Some(deliverables) = &req.deliverables {
|
if let Some(deliverables) = &deliverables {
|
||||||
m.deliverables = deliverables.clone();
|
m.deliverables = deliverables.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -502,12 +469,12 @@ impl McpTool for MilestonesHandler {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MilestoneAction::List => {
|
MilestonesTool::List { namespace } => {
|
||||||
let data = state.project.milestones.read_with(|items| {
|
let data = state.project.milestones.read_with(|items| {
|
||||||
let filtered: Vec<_> = items
|
let filtered: Vec<_> = items
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|i| {
|
.filter(|i| {
|
||||||
if let Some(ns) = &req.namespace {
|
if let Some(ns) = &namespace {
|
||||||
&i.namespace == ns
|
&i.namespace == ns
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -843,69 +843,7 @@ impl McpTool for ClipboardHandler {
|
|||||||
})
|
})
|
||||||
.to_string())
|
.to_string())
|
||||||
}
|
}
|
||||||
ClipboardAction::Read => {
|
|
||||||
let engine = ensure_ocr_engine().await;
|
|
||||||
|
|
||||||
let out = tokio::task::spawn_blocking(
|
|
||||||
move || -> crate::error::Result<serde_json::Map<String, Value>> {
|
|
||||||
let mut out = serde_json::Map::new();
|
|
||||||
|
|
||||||
if let Some(text) = get_native_clipboard_text() {
|
|
||||||
out.insert("text".into(), json!(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(dynamic_img) = get_native_clipboard_image() {
|
|
||||||
let mut img = dynamic_img.clone();
|
|
||||||
let max_dim = 1440;
|
|
||||||
if img.width() > max_dim || img.height() > max_dim {
|
|
||||||
img = img.resize(max_dim, max_dim, FilterType::Lanczos3);
|
|
||||||
}
|
|
||||||
let rgb_img = img.into_rgb8();
|
|
||||||
|
|
||||||
let mut jpeg_bytes = std::io::Cursor::new(Vec::new());
|
|
||||||
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(
|
|
||||||
&mut jpeg_bytes,
|
|
||||||
88,
|
|
||||||
);
|
|
||||||
if encoder
|
|
||||||
.encode(
|
|
||||||
&rgb_img,
|
|
||||||
rgb_img.width(),
|
|
||||||
rgb_img.height(),
|
|
||||||
image::ExtendedColorType::Rgb8,
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
let bytes = jpeg_bytes.into_inner();
|
|
||||||
|
|
||||||
let cache_dir = dirs::home_dir()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.join(".gemini/mcp_memory/clipboard");
|
|
||||||
let _ = std::fs::create_dir_all(&cache_dir);
|
|
||||||
let file_path = cache_dir.join("clipboard_latest_image.jpg");
|
|
||||||
if std::fs::write(&file_path, &bytes).is_ok() {
|
|
||||||
let path_str = file_path.to_string_lossy().to_string();
|
|
||||||
out.insert("image_path".into(), json!(path_str));
|
|
||||||
let wsl_path = to_wsl_path(&path_str);
|
|
||||||
out.insert("image_path_wsl".into(), json!(wsl_path));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(eng) = engine
|
|
||||||
&& let Some(ocr_text) = perform_ocrs_ocr(eng, &dynamic_img)
|
|
||||||
{
|
|
||||||
out.insert("image_analysis".to_string(), json!(ocr_text.trim()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(out)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
|
||||||
|
|
||||||
state.record_activity("clipboard", "Read contents from OS clipboard", None);
|
|
||||||
Ok::<String, crate::error::AppError>(serde_yaml::to_string(&Value::Object(out))?)
|
|
||||||
}
|
|
||||||
ClipboardAction::Write => {
|
ClipboardAction::Write => {
|
||||||
let text_opt = req.text;
|
let text_opt = req.text;
|
||||||
let image_path_opt = req.image_path;
|
let image_path_opt = req.image_path;
|
||||||
@@ -1023,38 +961,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_read_clipboard() {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
|
||||||
let handler = ClipboardHandler;
|
|
||||||
|
|
||||||
let result = handler
|
|
||||||
.execute(json!({"action": "read"}), state)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to read clipboard: {}", e))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Returns a JSON string, possibly {}
|
|
||||||
let parsed: serde_json::Value = serde_yaml::from_str(&result).unwrap();
|
|
||||||
assert!(parsed.is_object());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_read_clipboard_empty() {
|
|
||||||
let dir = tempfile::tempdir().unwrap();
|
|
||||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
|
||||||
let handler = ClipboardHandler;
|
|
||||||
|
|
||||||
let result = handler
|
|
||||||
.execute(serde_json::json!({"action": "read"}), state)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("Failed to read clipboard: {}", e))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let parsed: serde_json::Value = serde_yaml::from_str(&result).unwrap();
|
|
||||||
assert!(parsed.is_object());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_no_subprocess_clipboard_regression() {
|
fn test_no_subprocess_clipboard_regression() {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ The server registers 5 high-signal workflow prompts to initiate standardized age
|
|||||||
|
|
||||||
## 5. Consolidated Smart Tools Architecture (11 Primary Tools)
|
## 5. Consolidated Smart Tools Architecture (11 Primary Tools)
|
||||||
|
|
||||||
The server consolidates granular single-purpose tools into domain-named smart tools. Always prefer the consolidated tools over legacy aliases:
|
The server consolidates granular single-purpose tools into domain-named smart tools.
|
||||||
|
|
||||||
* **`tasks`**: Complete task lifecycle management.
|
* **`tasks`**: Complete task lifecycle management.
|
||||||
- `action: "add"`: Create a new task (requires `title`, optional `description`, `git_branch`, `repo_name`, `priority: "low" | "medium" | "high" | "urgent"`, `assigned_agent`, `verification_command`, `parent_id`, `dependencies`).
|
- `action: "add"`: Create a new task (requires `title`, optional `description`, `git_branch`, `repo_name`, `priority: "low" | "medium" | "high" | "urgent"`, `assigned_agent`, `verification_command`, `parent_id`, `dependencies`).
|
||||||
|
|||||||
+26
-41
@@ -285,58 +285,45 @@ pub async fn memory_consolidation_worker(state: Arc<MemoryState>) {
|
|||||||
_ = interval.tick() => {},
|
_ = interval.tick() => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
let entities: Vec<_> = state.graph.read_with(|g| {
|
let entities: Vec<String> = state.graph.read_with(|g| {
|
||||||
g.entities
|
g.entities.keys().cloned().collect()
|
||||||
.values()
|
|
||||||
.map(|e| (e.name.clone(), e.entity_type.clone()))
|
|
||||||
.collect()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if entities.len() < 2 {
|
if entities.len() < 2 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut entity_summaries = String::new();
|
let mut duplicates = None;
|
||||||
for (name, e_type) in entities.iter().take(50) {
|
let mut seen = std::collections::HashMap::new();
|
||||||
entity_summaries.push_str(&format!("- [{}] {}\n", e_type, name));
|
for name in &entities {
|
||||||
|
let normalized: String = name
|
||||||
|
.chars()
|
||||||
|
.filter(|c| c.is_alphanumeric())
|
||||||
|
.flat_map(|c| c.to_lowercase())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Skip empty normalized names
|
||||||
|
if normalized.is_empty() {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let prompt = format!(
|
if let Some(existing) = seen.insert(normalized, name.clone()) {
|
||||||
"Analyze the following list of entities and identify exactly TWO that represent the exact same concept or item but have slightly different names (e.g. 'auth_service' and 'AuthService'). Return ONLY a valid JSON array containing exactly two strings: the two names to merge. If no obvious duplicates exist, return an empty array []. Do not output any markdown formatting or extra text.\n\nEntities:\n{}",
|
if existing != *name {
|
||||||
entity_summaries
|
duplicates = Some((existing, name.clone()));
|
||||||
);
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(response) = state
|
if let Some((e1_name, e2_name)) = duplicates {
|
||||||
.ollama
|
|
||||||
.generate(
|
|
||||||
&prompt,
|
|
||||||
None,
|
|
||||||
Some("You are a helpful JSON-only data deduplication assistant. Output only JSON."),
|
|
||||||
Some("json"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
let cleaned = response
|
|
||||||
.trim()
|
|
||||||
.trim_start_matches("```json")
|
|
||||||
.trim_start_matches("```")
|
|
||||||
.trim_end_matches("```")
|
|
||||||
.trim();
|
|
||||||
if let Ok(duplicates) = serde_json::from_str::<Vec<String>>(cleaned)
|
|
||||||
&& duplicates.len() == 2
|
|
||||||
{
|
|
||||||
let e1_name = &duplicates[0];
|
|
||||||
let e2_name = &duplicates[1];
|
|
||||||
|
|
||||||
if e1_name != e2_name {
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Memory Consolidation Daemon: Merging '{}' into '{}'",
|
"Memory Consolidation Daemon: Merging '{}' into '{}'",
|
||||||
e2_name,
|
e2_name,
|
||||||
e1_name
|
e1_name
|
||||||
);
|
);
|
||||||
state.modify_graph(|g| {
|
state.modify_graph(|g| {
|
||||||
if let Some(mut e2) = g.entities.remove(e2_name) {
|
if let Some(mut e2) = g.entities.remove(&e2_name) {
|
||||||
if let Some(e1) = g.entities.get_mut(e1_name) {
|
if let Some(e1) = g.entities.get_mut(&e1_name) {
|
||||||
e1.observations.append(&mut e2.observations);
|
e1.observations.append(&mut e2.observations);
|
||||||
} else {
|
} else {
|
||||||
g.entities.insert(e2_name.clone(), e2);
|
g.entities.insert(e2_name.clone(), e2);
|
||||||
@@ -344,18 +331,16 @@ pub async fn memory_consolidation_worker(state: Arc<MemoryState>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for rel in g.relations.iter_mut() {
|
for rel in g.relations.iter_mut() {
|
||||||
if rel.from == *e2_name {
|
if rel.from == e2_name {
|
||||||
rel.from = e1_name.clone();
|
rel.from = e1_name.clone();
|
||||||
}
|
}
|
||||||
if rel.to == *e2_name {
|
if rel.to == e2_name {
|
||||||
rel.to = e1_name.clone();
|
rel.to = e1_name.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|||||||
+9
-1
@@ -22,7 +22,7 @@ pub fn error(id: serde_json::Value, code: i32, message: &str) -> serde_json::Val
|
|||||||
|
|
||||||
pub fn tool_def<T: JsonSchema>(name: &str, fallback_desc: &str) -> serde_json::Value {
|
pub fn tool_def<T: JsonSchema>(name: &str, fallback_desc: &str) -> serde_json::Value {
|
||||||
let schema = schemars::schema_for!(T);
|
let schema = schemars::schema_for!(T);
|
||||||
let schema_val = serde_json::to_value(&schema).unwrap_or_default();
|
let mut schema_val = serde_json::to_value(&schema).unwrap_or_default();
|
||||||
|
|
||||||
let desc = schema_val
|
let desc = schema_val
|
||||||
.get("description")
|
.get("description")
|
||||||
@@ -31,6 +31,14 @@ pub fn tool_def<T: JsonSchema>(name: &str, fallback_desc: &str) -> serde_json::V
|
|||||||
.unwrap_or(fallback_desc)
|
.unwrap_or(fallback_desc)
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
|
// MCP clients strictly expect a top-level `type: "object"` for tool schemas.
|
||||||
|
// Tagged enums usually generate a root `oneOf` or `anyOf`.
|
||||||
|
if let Some(obj) = schema_val.as_object_mut() {
|
||||||
|
if !obj.contains_key("type") && (obj.contains_key("oneOf") || obj.contains_key("anyOf")) {
|
||||||
|
obj.insert("type".to_string(), json!("object"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MCP expects standard JSON schema. Schemars returns draft-07.
|
// MCP expects standard JSON schema. Schemars returns draft-07.
|
||||||
json!({
|
json!({
|
||||||
"name": name,
|
"name": name,
|
||||||
|
|||||||
@@ -185,7 +185,6 @@ impl std::fmt::Display for TaskStatus {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
|
||||||
pub struct Entity {
|
pub struct Entity {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(alias = "entityType", alias = "type", alias = "category")]
|
|
||||||
pub entity_type: String,
|
pub entity_type: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub observations: Vec<String>,
|
pub observations: Vec<String>,
|
||||||
@@ -207,11 +206,8 @@ pub struct Entity {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
|
||||||
pub struct Relation {
|
pub struct Relation {
|
||||||
#[serde(alias = "source", alias = "from_node", alias = "source_node")]
|
|
||||||
pub from: String,
|
pub from: String,
|
||||||
#[serde(alias = "target", alias = "to_node", alias = "target_node")]
|
|
||||||
pub to: String,
|
pub to: String,
|
||||||
#[serde(alias = "relationType", alias = "type", alias = "relation")]
|
|
||||||
pub relation_type: String,
|
pub relation_type: String,
|
||||||
#[serde(default = "default_namespace")]
|
#[serde(default = "default_namespace")]
|
||||||
pub namespace: String,
|
pub namespace: String,
|
||||||
@@ -363,7 +359,6 @@ pub struct Adr {
|
|||||||
pub title: String,
|
pub title: String,
|
||||||
pub context: String,
|
pub context: String,
|
||||||
pub decision: String,
|
pub decision: String,
|
||||||
#[serde(alias = "consequences")]
|
|
||||||
pub consequence: String,
|
pub consequence: String,
|
||||||
#[serde(default = "default_adr_status")]
|
#[serde(default = "default_adr_status")]
|
||||||
pub status: String,
|
pub status: String,
|
||||||
|
|||||||
@@ -479,6 +479,7 @@ impl MemoryHandler {
|
|||||||
|
|
||||||
register!(graph::QueryGraphPathHandler);
|
register!(graph::QueryGraphPathHandler);
|
||||||
register!(graph::CreateEntitiesHandler);
|
register!(graph::CreateEntitiesHandler);
|
||||||
|
register!(graph::UpdateEntitiesHandler);
|
||||||
register!(graph::CreateRelationsHandler);
|
register!(graph::CreateRelationsHandler);
|
||||||
register!(graph::AddObservationsHandler);
|
register!(graph::AddObservationsHandler);
|
||||||
register!(graph::DeleteEntitiesHandler);
|
register!(graph::DeleteEntitiesHandler);
|
||||||
@@ -1428,7 +1429,7 @@ mod tests {
|
|||||||
("decisions", json!({"action": "query"})),
|
("decisions", json!({"action": "query"})),
|
||||||
("tech_debt", json!({"action": "list"})),
|
("tech_debt", json!({"action": "list"})),
|
||||||
("snippets", json!({"action": "search", "query": "test"})),
|
("snippets", json!({"action": "search", "query": "test"})),
|
||||||
("clipboard", json!({"action": "read"})),
|
("clipboard", json!({"action": "history"})),
|
||||||
("environment", json!({"action": "read_fingerprint"})),
|
("environment", json!({"action": "read_fingerprint"})),
|
||||||
("omni_search", json!({"query": "test"})),
|
("omni_search", json!({"query": "test"})),
|
||||||
("get_project_health", json!({})),
|
("get_project_health", json!({})),
|
||||||
|
|||||||
+345
-358
@@ -2,23 +2,60 @@ use schemars::JsonSchema;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Create new entities in the knowledge graph. Note: entity_type MUST ALWAYS be PascalCase (e.g. DatabaseTable, McpTool, File).
|
/// Create new entities in the knowledge graph. Note: entity_type MUST ALWAYS be PascalCase (e.g. DatabaseTable, McpTool, File).
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
pub struct EntityInput {
|
||||||
|
pub name: String,
|
||||||
|
pub entity_type: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub observations: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub namespace: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct CreateEntitiesTool {
|
pub struct CreateEntitiesTool {
|
||||||
/// Array of entities to create.
|
/// Array of entities to create.
|
||||||
pub entities: Vec<crate::models::Entity>,
|
pub entities: Vec<EntityInput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
pub struct UpdateEntityInput {
|
||||||
|
/// The current name of the entity to update.
|
||||||
|
pub name: String,
|
||||||
|
/// Optional new name to rename the entity to.
|
||||||
|
pub new_name: Option<String>,
|
||||||
|
/// Optional new entity type (e.g. PascalCase).
|
||||||
|
pub new_entity_type: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update existing entities in the knowledge graph (e.g., to rename an entity or change its type).
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
pub struct UpdateEntitiesTool {
|
||||||
|
/// Array of entity updates to perform.
|
||||||
|
pub updates: Vec<UpdateEntityInput>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
pub struct RelationInput {
|
||||||
|
pub from: String,
|
||||||
|
pub to: String,
|
||||||
|
pub relation_type: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub namespace: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create new relations between entities in the knowledge graph.
|
/// Create new relations between entities in the knowledge graph.
|
||||||
/// Create new relations between entities in the knowledge graph (accepts 'from' or 'source', 'to' or 'target', and 'relation_type' or 'type').
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct CreateRelationsTool {
|
pub struct CreateRelationsTool {
|
||||||
/// Array of relations to create. Each relation item MUST use 'from', 'to', and 'relation_type'.
|
/// Array of relations to create.
|
||||||
pub relations: Vec<crate::models::Relation>,
|
pub relations: Vec<RelationInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct ObservationInput {
|
pub struct ObservationInput {
|
||||||
|
/// The name of the entity.
|
||||||
pub entity_name: String,
|
pub entity_name: String,
|
||||||
|
/// The observation contents.
|
||||||
pub contents: Vec<String>,
|
pub contents: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +75,9 @@ pub struct DeleteEntitiesTool {
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct DeleteObservationInput {
|
pub struct DeleteObservationInput {
|
||||||
|
/// The name of the entity.
|
||||||
pub entity_name: String,
|
pub entity_name: String,
|
||||||
|
/// The observations to delete.
|
||||||
pub observations: Vec<String>,
|
pub observations: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +92,7 @@ pub struct DeleteObservationsTool {
|
|||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct DeleteRelationsTool {
|
pub struct DeleteRelationsTool {
|
||||||
/// Array of relations to delete.
|
/// Array of relations to delete.
|
||||||
pub relations: Vec<crate::models::Relation>,
|
pub relations: Vec<RelationInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the entire knowledge graph. WARNING: For large graphs, use search_nodes or pagination (limit, offset) to avoid context limits.
|
/// Read the entire knowledge graph. WARNING: For large graphs, use search_nodes or pagination (limit, offset) to avoid context limits.
|
||||||
@@ -172,8 +211,6 @@ pub enum SubgraphFormat {
|
|||||||
pub struct GetSubgraphTool {
|
pub struct GetSubgraphTool {
|
||||||
/// The root entity name to start the subgraph search from.
|
/// The root entity name to start the subgraph search from.
|
||||||
pub root_entity: Option<String>,
|
pub root_entity: Option<String>,
|
||||||
/// Legacy alias for root_entity.
|
|
||||||
pub root_node: Option<String>,
|
|
||||||
/// Maximum search depth (hops). Defaults to 2.
|
/// Maximum search depth (hops). Defaults to 2.
|
||||||
pub depth: Option<u32>,
|
pub depth: Option<u32>,
|
||||||
/// Output format: 'json' (raw entities and relations) or 'markdown_tree' (compact topology tree). Defaults to 'json'.
|
/// Output format: 'json' (raw entities and relations) or 'markdown_tree' (compact topology tree). Defaults to 'json'.
|
||||||
@@ -184,26 +221,38 @@ pub struct GetSubgraphTool {
|
|||||||
pub max_tokens: Option<usize>,
|
pub max_tokens: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum CheckpointAction {
|
|
||||||
Create,
|
|
||||||
Restore,
|
|
||||||
List,
|
|
||||||
Delete,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save, restore, list, or delete point-in-time memory state snapshot checkpoints.
|
/// Save, restore, list, or delete point-in-time memory state snapshot checkpoints.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct ManageCheckpointTool {
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
/// Action to perform: 'create', 'restore', 'list', or 'delete'.
|
pub enum ManageCheckpointTool {
|
||||||
pub action: CheckpointAction,
|
/// Save a checkpoint.
|
||||||
|
Create {
|
||||||
/// Unique name or ID for the snapshot checkpoint.
|
/// Unique name or ID for the snapshot checkpoint.
|
||||||
pub name_or_id: Option<String>,
|
name_or_id: Option<String>,
|
||||||
/// Optional description of why this checkpoint was created.
|
/// Optional description of why this checkpoint was created.
|
||||||
pub description: Option<String>,
|
description: Option<String>,
|
||||||
/// Optional namespace filter.
|
/// Optional namespace filter.
|
||||||
pub namespace: Option<String>,
|
namespace: Option<String>,
|
||||||
|
},
|
||||||
|
/// Restore a checkpoint.
|
||||||
|
Restore {
|
||||||
|
/// Unique name or ID for the snapshot checkpoint.
|
||||||
|
name_or_id: Option<String>,
|
||||||
|
/// Optional namespace filter.
|
||||||
|
namespace: Option<String>,
|
||||||
|
},
|
||||||
|
/// List checkpoints.
|
||||||
|
List {
|
||||||
|
/// Optional namespace filter.
|
||||||
|
namespace: Option<String>,
|
||||||
|
},
|
||||||
|
/// Delete a checkpoint.
|
||||||
|
Delete {
|
||||||
|
/// Unique name or ID for the snapshot checkpoint.
|
||||||
|
name_or_id: Option<String>,
|
||||||
|
/// Optional namespace filter.
|
||||||
|
namespace: Option<String>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge two entities in the knowledge graph into one.
|
/// Merge two entities in the knowledge graph into one.
|
||||||
@@ -315,14 +364,19 @@ pub struct QueryGraphPathTool {
|
|||||||
/// Define a strict checklist of acceptance criteria for a given task or feature before starting work.
|
/// Define a strict checklist of acceptance criteria for a given task or feature before starting work.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct SetAcceptanceCriteriaTool {
|
pub struct SetAcceptanceCriteriaTool {
|
||||||
|
/// The title of the task.
|
||||||
pub task_title: String,
|
pub task_title: String,
|
||||||
|
/// A list of acceptance criteria requirements.
|
||||||
pub criteria: Vec<String>,
|
pub criteria: Vec<String>,
|
||||||
}
|
}
|
||||||
/// Mark a previously defined acceptance criteria as met by providing cryptographic-like proof.
|
/// Mark a previously defined acceptance criteria as met by providing cryptographic-like proof.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct VerifyAcceptanceCriteriaTool {
|
pub struct VerifyAcceptanceCriteriaTool {
|
||||||
|
/// The ID of the task.
|
||||||
pub task_id: String,
|
pub task_id: String,
|
||||||
|
/// The specific criterion being verified.
|
||||||
pub criteria: String,
|
pub criteria: String,
|
||||||
|
/// The proof or evidence that the criterion is met.
|
||||||
pub proof: String,
|
pub proof: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,28 +407,30 @@ pub struct GetNextActionableTasksTool {
|
|||||||
pub limit: Option<usize>,
|
pub limit: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum HypothesisAction {
|
|
||||||
Log,
|
|
||||||
Query,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Manage diagnostic hypotheses, tested evidence, and status during problem solving.
|
/// Manage diagnostic hypotheses, tested evidence, and status during problem solving.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct HypothesesTool {
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
/// Action to perform: 'log' or 'query'.
|
pub enum HypothesesTool {
|
||||||
pub action: HypothesisAction,
|
/// Log a new diagnostic hypothesis.
|
||||||
/// Optional task ID associated with this hypothesis or to filter hypotheses.
|
Log {
|
||||||
pub task_id: Option<String>,
|
/// The diagnostic hypothesis or potential root cause.
|
||||||
/// The diagnostic hypothesis or potential root cause (required for action 'log').
|
hypothesis: String,
|
||||||
pub hypothesis: Option<String>,
|
/// Optional task ID associated with this hypothesis.
|
||||||
|
task_id: Option<String>,
|
||||||
/// Status: 'unverified', 'verified', or 'rejected'. Defaults to 'unverified'.
|
/// Status: 'unverified', 'verified', or 'rejected'. Defaults to 'unverified'.
|
||||||
pub status: Option<String>,
|
status: Option<String>,
|
||||||
/// Evidence or test results supporting or disproving the hypothesis.
|
/// Evidence or test results supporting or disproving the hypothesis.
|
||||||
pub evidence: Option<String>,
|
evidence: Option<String>,
|
||||||
/// Optional search query text (for action 'query').
|
},
|
||||||
pub query: Option<String>,
|
/// Query existing hypotheses.
|
||||||
|
Query {
|
||||||
|
/// Optional search query text.
|
||||||
|
query: Option<String>,
|
||||||
|
/// Optional task ID to filter hypotheses.
|
||||||
|
task_id: Option<String>,
|
||||||
|
/// Optional status filter.
|
||||||
|
status: Option<String>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -436,27 +492,35 @@ pub struct ProcessLogsTool {
|
|||||||
/// Read a file and return only its AST skeleton (Imports, Structs, Enums, Traits, Functions)
|
/// Read a file and return only its AST skeleton (Imports, Structs, Enums, Traits, Functions)
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct ReadFileSkeletonTool {
|
pub struct ReadFileSkeletonTool {
|
||||||
|
/// The path to the file to read.
|
||||||
pub file_path: String,
|
pub file_path: String,
|
||||||
}
|
}
|
||||||
/// Replace a specific AST node in a file (robust structural editing). Use this instead of regex or line-based string replacement to prevent indentation bugs and matching failures.
|
/// Replace a specific AST node in a file (robust structural editing). Use this instead of regex or line-based string replacement to prevent indentation bugs and matching failures.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct ReplaceAstNodeTool {
|
pub struct ReplaceAstNodeTool {
|
||||||
|
/// The path of the file to modify.
|
||||||
pub file_path: String,
|
pub file_path: String,
|
||||||
pub node_type: String, // e.g., "function_item", "impl_item"
|
/// The AST node type to replace (e.g., 'function_item', 'impl_item').
|
||||||
pub node_name: String, // e.g., "execute"
|
pub node_type: String,
|
||||||
|
/// The name of the specific AST node to replace (e.g., 'execute').
|
||||||
|
pub node_name: String,
|
||||||
|
/// The completely new structural content for this AST node.
|
||||||
pub new_content: String,
|
pub new_content: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Semantic code search using local vector embeddings. Use this conceptual search instead of raw regex (grep) when trying to locate abstract logic or exploring new patterns.
|
/// Semantic code search using local vector embeddings. Use this conceptual search instead of raw regex (grep) when trying to locate abstract logic or exploring new patterns.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct SemanticCodeSearchTool {
|
pub struct SemanticCodeSearchTool {
|
||||||
|
/// The conceptual or semantic search query.
|
||||||
pub query: String,
|
pub query: String,
|
||||||
|
/// Optional directory to restrict the search to.
|
||||||
pub directory: Option<String>,
|
pub directory: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a bird's-eye view of directory architecture. Use this when first exploring a new repository to get a summary of what each file is responsible for, instead of blindly reading files.
|
/// Get a bird's-eye view of directory architecture. Use this when first exploring a new repository to get a summary of what each file is responsible for, instead of blindly reading files.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct ReadDirectoryArchitectureTool {
|
pub struct ReadDirectoryArchitectureTool {
|
||||||
|
/// The directory path to analyze.
|
||||||
pub directory: String,
|
pub directory: String,
|
||||||
}
|
}
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||||
@@ -562,28 +626,30 @@ pub struct SummarizeSubgraphTool {
|
|||||||
pub max_tokens: Option<usize>,
|
pub max_tokens: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum AgentSignalAction {
|
|
||||||
Broadcast,
|
|
||||||
Query,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Real-time inter-agent communication bus: broadcast signals or query active signals from peer subagents.
|
/// Real-time inter-agent communication bus: broadcast signals or query active signals from peer subagents.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct AgentSignalsTool {
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
/// Action to perform: 'broadcast' or 'query'.
|
pub enum AgentSignalsTool {
|
||||||
pub action: AgentSignalAction,
|
/// Broadcast a signal to peer subagents.
|
||||||
/// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian'). Required for broadcast; optional filter for query.
|
Broadcast {
|
||||||
pub sender: Option<String>,
|
/// Sender agent ID or role (e.g. 'PrePushAuditor', 'MemoryLibrarian').
|
||||||
/// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED'). Required for broadcast; optional filter for query.
|
sender: String,
|
||||||
pub signal_type: Option<String>,
|
/// Signal type or event category (e.g. 'AUDIT_PASSED', 'REPRODUCER_READY', 'TESTS_FAILED').
|
||||||
/// JSON or text payload containing event details or artifact URIs (required for action 'broadcast').
|
signal_type: String,
|
||||||
pub payload: Option<String>,
|
/// JSON or text payload containing event details or artifact URIs.
|
||||||
|
payload: String,
|
||||||
/// Optional Time-To-Live in seconds for the signal. Defaults to 3600 (1 hour).
|
/// Optional Time-To-Live in seconds for the signal. Defaults to 3600 (1 hour).
|
||||||
pub ttl_seconds: Option<u64>,
|
ttl_seconds: Option<u64>,
|
||||||
/// Optional limit on returned signals (for action 'query'). Defaults to 20.
|
},
|
||||||
pub limit: Option<usize>,
|
/// Query active signals.
|
||||||
|
Query {
|
||||||
|
/// Optional sender agent ID or role filter.
|
||||||
|
sender: Option<String>,
|
||||||
|
/// Optional signal type filter.
|
||||||
|
signal_type: Option<String>,
|
||||||
|
/// Optional limit on returned signals. Defaults to 20.
|
||||||
|
limit: Option<usize>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/// Trigger an automated context checkpoint, summarizing active tasks, hypotheses, recent commits, and open tech debt into a permanent HandoffMemo.
|
/// Trigger an automated context checkpoint, summarizing active tasks, hypotheses, recent commits, and open tech debt into a permanent HandoffMemo.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
@@ -599,21 +665,12 @@ pub struct AutoSessionCheckpointTool {
|
|||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum HandoffMemoAction {
|
pub enum HandoffMemoAction {
|
||||||
#[serde(alias = "leave", alias = "LEAVE", alias = "Leave")]
|
|
||||||
Leave,
|
Leave,
|
||||||
#[serde(alias = "read", alias = "READ", alias = "Read")]
|
|
||||||
Read,
|
Read,
|
||||||
#[serde(alias = "clear", alias = "CLEAR", alias = "Clear")]
|
|
||||||
Clear,
|
Clear,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persistent handoff memos for passing session context to future agents.
|
/// Persistent handoff memos for passing session context to future agents.
|
||||||
/// Actions:
|
|
||||||
/// - 'leave': Post a handoff memo. Required: content. Optional: namespace.
|
|
||||||
/// - 'read': Inspect active memos. Optional: namespace.
|
|
||||||
/// - 'clear': Clear memos. Optional: namespace, ids.
|
|
||||||
///
|
|
||||||
/// Next steps on error: Check parameter requirements for 'leave' or 'clear' actions.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct HandoffMemosTool {
|
pub struct HandoffMemosTool {
|
||||||
/// Action to perform: 'leave', 'read', or 'clear'.
|
/// Action to perform: 'leave', 'read', or 'clear'.
|
||||||
@@ -626,150 +683,124 @@ pub struct HandoffMemosTool {
|
|||||||
pub ids: Option<Vec<String>>,
|
pub ids: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
/// Action-oriented task tracking system.
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum TaskAction {
|
|
||||||
#[serde(alias = "add", alias = "ADD", alias = "Add")]
|
|
||||||
Add,
|
|
||||||
#[serde(alias = "update", alias = "UPDATE", alias = "Update")]
|
|
||||||
Update,
|
|
||||||
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
|
|
||||||
Delete,
|
|
||||||
#[serde(alias = "list", alias = "LIST", alias = "List")]
|
|
||||||
List,
|
|
||||||
#[serde(
|
|
||||||
alias = "set_criteria",
|
|
||||||
alias = "setCriteria",
|
|
||||||
alias = "SET_CRITERIA",
|
|
||||||
alias = "SetCriteria"
|
|
||||||
)]
|
|
||||||
SetCriteria,
|
|
||||||
#[serde(alias = "verify", alias = "VERIFY", alias = "Verify")]
|
|
||||||
Verify,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Action-oriented task tracking system (add, update, delete, list, set_criteria, verify).
|
|
||||||
/// Actions:
|
|
||||||
/// - 'add': Create task. Required: title. Optional: description, git_branch, parent_id, dependencies.
|
|
||||||
/// - 'update': Update task status. Required: id, status ('pending'|'completed'|'cancelled').
|
|
||||||
/// - 'delete': Delete task & subtasks. Required: id.
|
|
||||||
/// - 'list': List active tasks. Optional: git_branch, summary_level ('compact'|'detailed'|'full'), max_tokens.
|
|
||||||
/// - 'set_criteria': Set acceptance criteria. Required: id, criteria (array of strings).
|
|
||||||
/// - 'verify': Verify criteria met. Required: id.
|
|
||||||
///
|
|
||||||
/// Next steps on error: Check required parameters or call list to verify task IDs.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct TasksTool {
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
/// Action to perform: 'add', 'update', 'delete', 'list', 'set_criteria', or 'verify'.
|
pub enum TasksTool {
|
||||||
pub action: TaskAction,
|
/// Create a new task.
|
||||||
/// Task ID (required for 'update', 'delete', 'set_criteria', 'verify').
|
Add {
|
||||||
#[serde(alias = "task_id", alias = "taskId")]
|
/// Task title.
|
||||||
pub id: Option<String>,
|
title: String,
|
||||||
/// Task title (required for 'add').
|
/// Task description.
|
||||||
#[serde(alias = "name")]
|
description: Option<String>,
|
||||||
pub title: Option<String>,
|
/// Optional parent task ID for subtasks.
|
||||||
/// Task description (optional for 'add').
|
parent_id: Option<String>,
|
||||||
#[serde(alias = "desc")]
|
/// List of dependent task IDs.
|
||||||
pub description: Option<String>,
|
dependencies: Option<Vec<String>>,
|
||||||
/// New status: 'pending', 'completed', or 'cancelled' (for 'update').
|
/// Branch associated with this task.
|
||||||
pub status: Option<String>,
|
git_branch: Option<String>,
|
||||||
/// Parent task ID (optional for 'add').
|
|
||||||
#[serde(alias = "parentId", alias = "parent")]
|
|
||||||
pub parent_id: Option<String>,
|
|
||||||
/// List of dependency task IDs (optional for 'add').
|
|
||||||
#[serde(alias = "deps")]
|
|
||||||
pub dependencies: Option<Vec<String>>,
|
|
||||||
/// Git branch filter or assignment.
|
|
||||||
#[serde(alias = "branch")]
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
/// Acceptance criteria (required for 'set_criteria').
|
|
||||||
#[serde(alias = "acceptance_criteria", alias = "acceptanceCriteria")]
|
|
||||||
pub criteria: Option<Vec<String>>,
|
|
||||||
/// Verification proof or details (optional for 'verify').
|
|
||||||
pub proof: Option<String>,
|
|
||||||
/// Summary level: 'compact', 'detailed', or 'full' (for 'list').
|
|
||||||
pub summary_level: Option<String>,
|
|
||||||
/// Maximum tokens budget cap (for 'list').
|
|
||||||
pub max_tokens: Option<usize>,
|
|
||||||
/// Optional namespace filter or assignment (defaults to 'default').
|
|
||||||
pub namespace: Option<String>,
|
|
||||||
/// Optional repository name.
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
/// Optional task priority: 'low', 'medium', 'high', 'urgent'.
|
|
||||||
pub priority: Option<String>,
|
|
||||||
/// Optional assigned subagent role or identifier.
|
|
||||||
pub assigned_agent: Option<String>,
|
|
||||||
/// Optional verification command to validate criteria completion.
|
|
||||||
/// Optional pagination offset for 'list'.
|
|
||||||
pub offset: Option<usize>,
|
|
||||||
pub verification_command: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum MilestoneAction {
|
|
||||||
#[serde(alias = "add", alias = "ADD", alias = "Add")]
|
|
||||||
Add,
|
|
||||||
#[serde(alias = "update", alias = "UPDATE", alias = "Update")]
|
|
||||||
Update,
|
|
||||||
#[serde(alias = "list", alias = "LIST", alias = "List")]
|
|
||||||
List,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Project milestone management (add, update, list).
|
|
||||||
/// Actions:
|
|
||||||
/// - 'add': Create milestone. Required: title. Optional: namespace.
|
|
||||||
/// - 'update': Update milestone status. Required: id, status.
|
|
||||||
/// - 'list': List milestones. Optional: namespace.
|
|
||||||
///
|
|
||||||
/// Next steps on error: Provide required title for 'add' or id/status for 'update'.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct MilestonesTool {
|
|
||||||
/// Action to perform: 'add', 'update', or 'list'.
|
|
||||||
pub action: MilestoneAction,
|
|
||||||
/// Milestone ID (required for 'update').
|
|
||||||
#[serde(alias = "milestone_id", alias = "milestoneId")]
|
|
||||||
pub id: Option<String>,
|
|
||||||
/// Milestone title (required for 'add').
|
|
||||||
#[serde(alias = "name")]
|
|
||||||
pub title: Option<String>,
|
|
||||||
/// New status (for 'update').
|
|
||||||
pub status: Option<String>,
|
|
||||||
/// Optional namespace filter or assignment.
|
/// Optional namespace filter or assignment.
|
||||||
pub namespace: Option<String>,
|
namespace: Option<String>,
|
||||||
|
/// Repository name.
|
||||||
|
repo_name: Option<String>,
|
||||||
|
/// Priority: 'low', 'medium', 'high', 'critical'.
|
||||||
|
priority: Option<String>,
|
||||||
|
/// Assigned agent or subagent.
|
||||||
|
assigned_agent: Option<String>,
|
||||||
|
/// Command used to verify completion.
|
||||||
|
verification_command: Option<String>,
|
||||||
|
},
|
||||||
|
/// Update task status.
|
||||||
|
Update {
|
||||||
|
/// Task ID.
|
||||||
|
id: String,
|
||||||
|
/// New status: 'pending', 'completed', or 'cancelled'.
|
||||||
|
status: String,
|
||||||
|
},
|
||||||
|
/// Delete a task and its subtasks.
|
||||||
|
Delete {
|
||||||
|
/// Task ID.
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
/// List active tasks.
|
||||||
|
List {
|
||||||
|
/// Optional Git branch filter.
|
||||||
|
git_branch: Option<String>,
|
||||||
|
/// Optional status filter.
|
||||||
|
status: Option<String>,
|
||||||
|
/// Summary level: 'compact', 'detailed', or 'full'. Defaults to 'detailed'.
|
||||||
|
summary_level: Option<String>,
|
||||||
|
/// Maximum tokens budget cap.
|
||||||
|
max_tokens: Option<usize>,
|
||||||
|
/// Optional pagination offset.
|
||||||
|
offset: Option<usize>,
|
||||||
|
},
|
||||||
|
/// Set a checklist of acceptance criteria.
|
||||||
|
SetCriteria {
|
||||||
|
/// Task ID or title.
|
||||||
|
id: String,
|
||||||
|
/// Acceptance criteria list.
|
||||||
|
criteria: Vec<String>,
|
||||||
|
},
|
||||||
|
/// Mark an acceptance criteria as met with cryptographic-like proof.
|
||||||
|
Verify {
|
||||||
|
/// Task ID or title.
|
||||||
|
id: String,
|
||||||
|
/// Verification proof or details.
|
||||||
|
proof: Option<String>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Project milestone management.
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
|
pub enum MilestonesTool {
|
||||||
|
/// Create a new milestone.
|
||||||
|
Add {
|
||||||
|
/// Milestone title.
|
||||||
|
title: String,
|
||||||
|
/// Optional namespace filter or assignment.
|
||||||
|
namespace: Option<String>,
|
||||||
/// Optional target completion date.
|
/// Optional target completion date.
|
||||||
pub target_date: Option<String>,
|
target_date: Option<String>,
|
||||||
/// Optional description of milestone scope.
|
/// Optional description of milestone scope.
|
||||||
pub description: Option<String>,
|
description: Option<String>,
|
||||||
/// Optional deliverables or task checklist.
|
/// Optional deliverables or task checklist.
|
||||||
pub deliverables: Option<Vec<String>>,
|
deliverables: Option<Vec<String>>,
|
||||||
/// Optional list of task IDs associated with this milestone.
|
/// Optional list of task IDs associated with this milestone.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub task_ids: Option<Vec<String>>,
|
task_ids: Option<Vec<String>>,
|
||||||
/// Optional repository name.
|
/// Optional repository name.
|
||||||
pub repo_name: Option<String>,
|
repo_name: Option<String>,
|
||||||
|
},
|
||||||
|
/// Update milestone status.
|
||||||
|
Update {
|
||||||
|
/// Milestone ID.
|
||||||
|
id: String,
|
||||||
|
/// New status.
|
||||||
|
status: String,
|
||||||
|
/// Optional deliverables or task checklist.
|
||||||
|
deliverables: Option<Vec<String>>,
|
||||||
|
/// Optional list of task IDs associated with this milestone.
|
||||||
|
#[serde(default)]
|
||||||
|
task_ids: Option<Vec<String>>,
|
||||||
|
},
|
||||||
|
/// List milestones.
|
||||||
|
List {
|
||||||
|
/// Optional namespace.
|
||||||
|
namespace: Option<String>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum SnippetAction {
|
pub enum SnippetAction {
|
||||||
#[serde(alias = "store", alias = "STORE", alias = "Store")]
|
|
||||||
Store,
|
Store,
|
||||||
#[serde(alias = "search", alias = "SEARCH", alias = "Search")]
|
|
||||||
Search,
|
Search,
|
||||||
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
|
|
||||||
Delete,
|
Delete,
|
||||||
#[serde(alias = "tag", alias = "TAG", alias = "Tag")]
|
|
||||||
Tag,
|
Tag,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Syntactically preserved reusable code snippets vault (store, search, delete, tag).
|
/// Syntactically preserved reusable code snippets vault (store, search, delete, tag).
|
||||||
/// Actions:
|
|
||||||
/// - 'store': Store snippet. Required: query (or id) as snippet name. Optional: language, code, description, tags.
|
|
||||||
/// - 'search': Search snippets. Optional: query, tags, hybrid (boolean for BM25+vector search).
|
|
||||||
/// - 'delete': Delete snippet. Required: id (or query) as snippet name.
|
|
||||||
/// - 'tag': Tag snippet. Required: id (or query), tags (array of strings).
|
|
||||||
///
|
|
||||||
/// Next steps on error: Ensure snippet name/query or id is provided.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct SnippetsTool {
|
pub struct SnippetsTool {
|
||||||
/// Action to perform: 'store', 'search', 'delete', or 'tag'.
|
/// Action to perform: 'store', 'search', 'delete', or 'tag'.
|
||||||
@@ -800,177 +831,154 @@ pub struct SnippetsTool {
|
|||||||
pub line_range: Option<String>,
|
pub line_range: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq, Clone)]
|
/// Architectural Decision Records (ADRs) log.
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum DecisionAction {
|
|
||||||
#[serde(alias = "log", alias = "LOG", alias = "Log")]
|
|
||||||
Log,
|
|
||||||
#[serde(alias = "query", alias = "QUERY", alias = "Query")]
|
|
||||||
Query,
|
|
||||||
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
|
|
||||||
Delete,
|
|
||||||
#[serde(alias = "update", alias = "UPDATE", alias = "Update")]
|
|
||||||
Update,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Architectural Decision Records (ADRs) log (log, query, update, delete).
|
|
||||||
/// Actions:
|
|
||||||
/// - 'log': Record ADR. Required: title. Optional: status, context, decision, consequences, namespace.
|
|
||||||
/// - 'query': Search ADRs. Optional: query, namespace.
|
|
||||||
/// - 'update': Update ADR status or implementation info. Required: id. Optional: status, git_commit, git_branch, task_id, context, decision, consequences, title.
|
|
||||||
/// - 'delete': Remove ADR. Required: id.
|
|
||||||
///
|
|
||||||
/// Next steps on error: Provide title for 'log' or id for 'update'/'delete'.
|
|
||||||
#[derive(Deserialize, Serialize, JsonSchema, Debug, Clone)]
|
#[derive(Deserialize, Serialize, JsonSchema, Debug, Clone)]
|
||||||
pub struct DecisionsTool {
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
/// Action to perform: 'log', 'query', or 'delete'.
|
pub enum DecisionsTool {
|
||||||
pub action: DecisionAction,
|
/// Record an ADR.
|
||||||
/// ADR ID (required for 'delete').
|
Log {
|
||||||
pub id: Option<String>,
|
/// ADR title.
|
||||||
/// ADR title (required for 'log').
|
title: String,
|
||||||
pub title: Option<String>,
|
/// ADR status.
|
||||||
/// ADR status (for 'log').
|
status: Option<String>,
|
||||||
pub status: Option<String>,
|
/// Context & problem statement.
|
||||||
/// Context & problem statement (for 'log').
|
context: Option<String>,
|
||||||
pub context: Option<String>,
|
/// Decision made.
|
||||||
/// Decision made (for 'log').
|
decision: Option<String>,
|
||||||
pub decision: Option<String>,
|
/// Consequences & tradeoffs.
|
||||||
/// Consequences & tradeoffs (for 'log').
|
consequences: Option<String>,
|
||||||
#[serde(alias = "consequence")]
|
|
||||||
pub consequences: Option<String>,
|
|
||||||
/// Search query string (for 'query').
|
|
||||||
pub query: Option<String>,
|
|
||||||
/// Optional namespace.
|
/// Optional namespace.
|
||||||
pub namespace: Option<String>,
|
namespace: Option<String>,
|
||||||
/// Optional repository name.
|
/// Optional repository name.
|
||||||
pub repo_name: Option<String>,
|
repo_name: Option<String>,
|
||||||
/// Optional author or architect behind the decision.
|
/// Optional author or architect behind the decision.
|
||||||
pub author: Option<String>,
|
author: Option<String>,
|
||||||
/// Optional affected system components or crates.
|
/// Optional affected system components or crates.
|
||||||
pub affected_components: Option<Vec<String>>,
|
affected_components: Option<Vec<String>>,
|
||||||
/// Optional alternative designs or libraries considered.
|
/// Optional alternative designs or libraries considered.
|
||||||
pub alternatives_considered: Option<Vec<String>>,
|
alternatives_considered: Option<Vec<String>>,
|
||||||
/// Optional superseded ADR ID.
|
/// Optional superseded ADR ID.
|
||||||
pub supersedes: Option<String>,
|
supersedes: Option<String>,
|
||||||
/// Limit the number of query results. Defaults to 20.
|
|
||||||
pub limit: Option<usize>,
|
|
||||||
/// Include the full body of the matched decisions. Defaults to true.
|
|
||||||
pub include_body: Option<bool>,
|
|
||||||
/// Optional Git commit where decision was implemented.
|
/// Optional Git commit where decision was implemented.
|
||||||
pub git_commit: Option<String>,
|
git_commit: Option<String>,
|
||||||
/// Optional Git branch where decision was implemented.
|
/// Optional Git branch where decision was implemented.
|
||||||
pub git_branch: Option<String>,
|
git_branch: Option<String>,
|
||||||
/// Optional task ID linked to this decision.
|
/// Optional task ID linked to this decision.
|
||||||
pub task_id: Option<String>,
|
task_id: Option<String>,
|
||||||
}
|
},
|
||||||
|
/// Search ADRs.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
Query {
|
||||||
#[serde(rename_all = "snake_case")]
|
/// Search query string.
|
||||||
pub enum TechDebtAction {
|
query: Option<String>,
|
||||||
#[serde(alias = "log", alias = "LOG", alias = "Log")]
|
|
||||||
Log,
|
|
||||||
#[serde(alias = "resolve", alias = "RESOLVE", alias = "Resolve")]
|
|
||||||
Resolve,
|
|
||||||
#[serde(alias = "list", alias = "LIST", alias = "List")]
|
|
||||||
List,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Technical debt backlog management (log, resolve, list).
|
|
||||||
/// Actions:
|
|
||||||
/// - 'log': Record technical debt. Required: description. Optional: ideal_solution, git_commit, git_branch, symbol_references, line_range, namespace.
|
|
||||||
/// - 'resolve': Mark tech debt resolved. Required: id.
|
|
||||||
/// - 'list': List tech debt items. Optional: include_resolved, namespace.
|
|
||||||
///
|
|
||||||
/// Next steps on error: Provide description for 'log' or valid ID for 'resolve'.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct TechDebtTool {
|
|
||||||
/// Action to perform: 'log', 'resolve', or 'list'.
|
|
||||||
pub action: TechDebtAction,
|
|
||||||
/// Tech debt ID (required for 'resolve').
|
|
||||||
#[serde(alias = "tech_debt_id", alias = "debt_id")]
|
|
||||||
pub id: Option<String>,
|
|
||||||
/// Tech debt description (required for 'log').
|
|
||||||
#[serde(alias = "desc")]
|
|
||||||
pub description: Option<String>,
|
|
||||||
/// Optional title or summary of tech debt.
|
|
||||||
pub title: Option<String>,
|
|
||||||
/// Ideal solution (optional for 'log').
|
|
||||||
#[serde(alias = "solution", alias = "fix")]
|
|
||||||
pub ideal_solution: Option<String>,
|
|
||||||
/// Include resolved tech debt items (for 'list').
|
|
||||||
pub include_resolved: Option<bool>,
|
|
||||||
/// Associated git commit hash.
|
|
||||||
#[serde(alias = "commit")]
|
|
||||||
pub git_commit: Option<String>,
|
|
||||||
/// Associated git branch.
|
|
||||||
#[serde(alias = "branch")]
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
/// Symbol references associated with the tech debt.
|
|
||||||
pub symbol_references: Option<Vec<String>>,
|
|
||||||
/// Line range associated with the tech debt.
|
|
||||||
pub line_range: Option<String>,
|
|
||||||
/// Optional namespace.
|
/// Optional namespace.
|
||||||
pub namespace: Option<String>,
|
namespace: Option<String>,
|
||||||
/// Optional debt severity: 'low', 'medium', 'high', 'critical'.
|
/// Limit the number of query results. Defaults to 20.
|
||||||
pub severity: Option<String>,
|
limit: Option<usize>,
|
||||||
|
/// Include the full body of the matched decisions. Defaults to true.
|
||||||
|
include_body: Option<bool>,
|
||||||
|
},
|
||||||
|
/// Update ADR status or implementation info.
|
||||||
|
Update {
|
||||||
|
/// ADR ID.
|
||||||
|
id: String,
|
||||||
|
/// ADR title.
|
||||||
|
title: Option<String>,
|
||||||
|
/// ADR status.
|
||||||
|
status: Option<String>,
|
||||||
|
/// Context & problem statement.
|
||||||
|
context: Option<String>,
|
||||||
|
/// Decision made.
|
||||||
|
decision: Option<String>,
|
||||||
|
/// Consequences & tradeoffs.
|
||||||
|
consequences: Option<String>,
|
||||||
|
/// Optional Git commit where decision was implemented.
|
||||||
|
git_commit: Option<String>,
|
||||||
|
/// Optional Git branch where decision was implemented.
|
||||||
|
git_branch: Option<String>,
|
||||||
|
/// Optional task ID linked to this decision.
|
||||||
|
task_id: Option<String>,
|
||||||
|
/// Optional namespace.
|
||||||
|
namespace: Option<String>,
|
||||||
/// Optional repository name.
|
/// Optional repository name.
|
||||||
pub repo_name: Option<String>,
|
repo_name: Option<String>,
|
||||||
|
/// Optional author or architect behind the decision.
|
||||||
|
author: Option<String>,
|
||||||
|
/// Optional affected system components or crates.
|
||||||
|
affected_components: Option<Vec<String>>,
|
||||||
|
/// Optional alternative designs or libraries considered.
|
||||||
|
alternatives_considered: Option<Vec<String>>,
|
||||||
|
/// Optional superseded ADR ID.
|
||||||
|
supersedes: Option<String>,
|
||||||
|
},
|
||||||
|
/// Remove an ADR.
|
||||||
|
Delete {
|
||||||
|
/// ADR ID.
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Technical debt backlog management.
|
||||||
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
|
#[serde(tag = "action", rename_all = "snake_case")]
|
||||||
|
pub enum TechDebtTool {
|
||||||
|
/// Record technical debt.
|
||||||
|
Log {
|
||||||
|
/// Tech debt description.
|
||||||
|
description: String,
|
||||||
|
/// Optional title or summary of tech debt.
|
||||||
|
title: Option<String>,
|
||||||
|
/// Ideal solution.
|
||||||
|
ideal_solution: Option<String>,
|
||||||
|
/// Associated git commit hash.
|
||||||
|
git_commit: Option<String>,
|
||||||
|
/// Associated git branch.
|
||||||
|
git_branch: Option<String>,
|
||||||
|
/// Symbol references associated with the tech debt.
|
||||||
|
symbol_references: Option<Vec<String>>,
|
||||||
|
/// Line range associated with the tech debt.
|
||||||
|
line_range: Option<String>,
|
||||||
|
/// Optional namespace.
|
||||||
|
namespace: Option<String>,
|
||||||
|
/// Optional debt severity: 'low', 'medium', 'high', 'critical'.
|
||||||
|
severity: Option<String>,
|
||||||
|
/// Optional repository name.
|
||||||
|
repo_name: Option<String>,
|
||||||
/// Optional file path where debt exists.
|
/// Optional file path where debt exists.
|
||||||
pub file_path: Option<String>,
|
file_path: Option<String>,
|
||||||
/// Optional workaround currently in place.
|
/// Optional workaround currently in place.
|
||||||
pub workaround: Option<String>,
|
workaround: Option<String>,
|
||||||
/// Optional effort estimate to fix.
|
/// Optional effort estimate to fix.
|
||||||
pub effort_estimate: Option<String>,
|
effort_estimate: Option<String>,
|
||||||
|
},
|
||||||
|
/// Mark tech debt resolved.
|
||||||
|
Resolve {
|
||||||
|
/// Tech debt ID.
|
||||||
|
id: String,
|
||||||
|
},
|
||||||
|
/// List tech debt items.
|
||||||
|
List {
|
||||||
|
/// Include resolved tech debt items.
|
||||||
|
include_resolved: Option<bool>,
|
||||||
|
/// Optional namespace.
|
||||||
|
namespace: Option<String>,
|
||||||
/// Summary level for list action: 'compact', 'detailed', or 'full'.
|
/// Summary level for list action: 'compact', 'detailed', or 'full'.
|
||||||
pub summary_level: Option<String>,
|
summary_level: Option<String>,
|
||||||
/// Optional token budget cap for list output.
|
/// Optional token budget cap for list output.
|
||||||
pub max_tokens: Option<usize>,
|
max_tokens: Option<usize>,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum EnvAction {
|
pub enum EnvAction {
|
||||||
#[serde(
|
|
||||||
alias = "update_fingerprint",
|
|
||||||
alias = "updateFingerprint",
|
|
||||||
alias = "UPDATE_FINGERPRINT",
|
|
||||||
alias = "UpdateFingerprint"
|
|
||||||
)]
|
|
||||||
UpdateFingerprint,
|
UpdateFingerprint,
|
||||||
#[serde(
|
|
||||||
alias = "read_fingerprint",
|
|
||||||
alias = "readFingerprint",
|
|
||||||
alias = "READ_FINGERPRINT",
|
|
||||||
alias = "ReadFingerprint"
|
|
||||||
)]
|
|
||||||
ReadFingerprint,
|
ReadFingerprint,
|
||||||
#[serde(
|
|
||||||
alias = "log_requirement",
|
|
||||||
alias = "logRequirement",
|
|
||||||
alias = "LOG_REQUIREMENT",
|
|
||||||
alias = "LogRequirement"
|
|
||||||
)]
|
|
||||||
LogRequirement,
|
LogRequirement,
|
||||||
#[serde(alias = "register", alias = "REGISTER", alias = "Register")]
|
|
||||||
Register,
|
Register,
|
||||||
#[serde(
|
|
||||||
alias = "get_details",
|
|
||||||
alias = "getDetails",
|
|
||||||
alias = "GET_DETAILS",
|
|
||||||
alias = "GetDetails"
|
|
||||||
)]
|
|
||||||
GetDetails,
|
GetDetails,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Environment requirements, tool fingerprints & infrastructure management.
|
/// Environment requirements, tool fingerprints & infrastructure management.
|
||||||
/// Actions:
|
|
||||||
/// - 'update_fingerprint': Update tool versions. Optional: tool_versions map, namespace.
|
|
||||||
/// - 'read_fingerprint': Read current environment fingerprint. Optional: namespace.
|
|
||||||
/// - 'log_requirement': Log environment variable requirement. Required: key. Optional: description, is_secret, namespace.
|
|
||||||
/// - 'register': Register remote target environment. Required: name. Optional: url, description, requires_vpn, namespace.
|
|
||||||
/// - 'get_details': Get full environment details. Optional: namespace.
|
|
||||||
///
|
|
||||||
/// Next steps on error: Provide key for 'log_requirement' or name for 'register'.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct EnvironmentTool {
|
pub struct EnvironmentTool {
|
||||||
/// Action to perform: 'update_fingerprint', 'read_fingerprint', 'log_requirement', 'register', or 'get_details'.
|
/// Action to perform: 'update_fingerprint', 'read_fingerprint', 'log_requirement', 'register', or 'get_details'.
|
||||||
@@ -1000,40 +1008,19 @@ pub struct EnvironmentTool {
|
|||||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq)]
|
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum ClipboardAction {
|
pub enum ClipboardAction {
|
||||||
#[serde(
|
|
||||||
alias = "image",
|
|
||||||
alias = "IMAGE",
|
|
||||||
alias = "Image",
|
|
||||||
alias = "screenshot",
|
|
||||||
alias = "SCREENSHOT"
|
|
||||||
)]
|
|
||||||
Image,
|
Image,
|
||||||
#[serde(alias = "text", alias = "TEXT", alias = "Text")]
|
|
||||||
Text,
|
Text,
|
||||||
#[serde(alias = "history", alias = "HISTORY", alias = "History")]
|
|
||||||
History,
|
History,
|
||||||
#[serde(alias = "clear", alias = "CLEAR", alias = "Clear")]
|
|
||||||
Clear,
|
Clear,
|
||||||
#[serde(alias = "read", alias = "READ", alias = "Read")]
|
|
||||||
Read,
|
|
||||||
#[serde(alias = "write", alias = "WRITE", alias = "Write")]
|
|
||||||
Write,
|
Write,
|
||||||
}
|
}
|
||||||
/// Smart OS Clipboard management with overwrite-immune screenshot caching and OCR.
|
/// Smart OS Clipboard management with overwrite-immune screenshot caching and OCR.
|
||||||
/// Actions:
|
|
||||||
/// - 'image': Get latest screenshot image path (Windows + WSL) and extracted verbatim OCR text.
|
|
||||||
/// Returns cached screenshot even if text was copied afterwards!
|
/// Returns cached screenshot even if text was copied afterwards!
|
||||||
/// - 'text': Get latest clipboard text (or normalized Markdown if HTML was copied).
|
|
||||||
/// - 'history': View recent clipboard history ring buffer (images and text with timestamps).
|
|
||||||
/// - 'clear': Clear OS clipboard and memory cache.
|
|
||||||
/// - 'read': Read current clipboard contents (legacy alias).
|
|
||||||
/// - 'write': Write content to OS clipboard. Optional: text, html, files, image_path.
|
|
||||||
///
|
|
||||||
/// Triggers: Call 'image' immediately when user says "look at image in clipboard", "see screenshot",
|
/// Triggers: Call 'image' immediately when user says "look at image in clipboard", "see screenshot",
|
||||||
/// "look at clipboard", "what I copied", or shares terminal error captures.
|
/// "look at clipboard", "what I copied", or shares terminal error captures.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct ClipboardTool {
|
pub struct ClipboardTool {
|
||||||
/// Action to perform: 'image', 'text', 'history', 'clear', 'read', or 'write'.
|
/// Action to perform: 'image', 'text', 'history', 'clear', or 'write'.
|
||||||
pub action: ClipboardAction,
|
pub action: ClipboardAction,
|
||||||
/// Plain text content (for 'write').
|
/// Plain text content (for 'write').
|
||||||
pub text: Option<String>,
|
pub text: Option<String>,
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
test
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
test
|
|
||||||
@@ -29,11 +29,10 @@ fn test_eager_tools_parity() {
|
|||||||
let nvim_source =
|
let nvim_source =
|
||||||
std::fs::read_to_string("../nvim-core/src/lib.rs").expect("Failed to read nvim lib.rs");
|
std::fs::read_to_string("../nvim-core/src/lib.rs").expect("Failed to read nvim lib.rs");
|
||||||
let mut nvim_tools = HashSet::new();
|
let mut nvim_tools = HashSet::new();
|
||||||
for line in nvim_source.lines() {
|
let parts: Vec<&str> = nvim_source.split("tool_def").collect();
|
||||||
if line.contains("\"name\": \"nvim_")
|
for part in parts.iter().skip(1) {
|
||||||
&& let Some(start) = line.find("\"name\": \"")
|
if let Some(start) = part.find("(\"") {
|
||||||
{
|
let rest = &part[start + 2..];
|
||||||
let rest = &line[start + 9..];
|
|
||||||
if let Some(end) = rest.find('"') {
|
if let Some(end) = rest.find('"') {
|
||||||
nvim_tools.insert(rest[..end].to_string());
|
nvim_tools.insert(rest[..end].to_string());
|
||||||
}
|
}
|
||||||
|
|||||||
Whitespace-only changes.
Reference in new issue
Block a user