refactor: eradicate deprecated tools (sticky_notes, pinned_files, context_workspaces, pr_checklist, preferences) and dead code
This commit is contained in:
1 parent
d80915635f
commit
79209da711
31 files changed
+1169
-3859
No files matched your search
@@ -17,8 +17,8 @@ To maximize efficiency, we split interactions into two categories: **Synchronous
|
|||||||
### A. Synchronous Rules (The Primary Loop)
|
### 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.
|
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 (`list_active_tasks`, `list_pinned_files`):** Executed when a session starts. This gives the LLM immediate awareness of the current workflow.
|
* **Context Initialization (`tasks` list, `get_preflight_context`):** Executed when a session starts. This gives the LLM immediate awareness of the current workflow.
|
||||||
* **Context Switching (`save_context_workspace`, `load_context_workspace`):** Executed when moving between branches or large features. This prevents context bleed between disparate tasks.
|
* **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.
|
* **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)
|
### B. Subagent Orchestration (The Background Team)
|
||||||
@@ -43,7 +43,7 @@ Heavy or verbose interactions with the MCP server are delegated to specialized b
|
|||||||
The `mcp-memory` server is a distinct background process (typically port 3000). The LLM ecosystem must handle server downtime gracefully:
|
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.
|
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, pins) is persisted to `mcp_store.redb`. When the server comes back online, no data is lost. The LLM can immediately resume querying.
|
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.
|
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
|
## Conclusion
|
||||||
@@ -57,7 +57,7 @@ The MCP protocol exposes three primary primitives. To prevent LLM confusion and
|
|||||||
* **LLM Awareness:** The LLM must not use tools to repeatedly poll for state changes. Tools represent active, expensive computing steps.
|
* **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)
|
### B. Resources (For Passive Awareness)
|
||||||
* **When to use:** Use URIs (e.g., memory://tasks/active, memory://pinned_files) to read holistic project state.
|
* **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.
|
* **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)
|
### C. Prompts (For Macro-Workflows)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ pub trait McpPrompt: Send + Sync {
|
|||||||
|
|
||||||
## 4. Execution Plan
|
## 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.
|
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://pinned_files`, etc.
|
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.
|
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.
|
This design guarantees we do not needlessly tear up code—we merely extend the existing robust `McpTool` pattern to the rest of the protocol.
|
||||||
@@ -87,9 +87,9 @@ When the LLM is first analyzing a repository, don't let it run `ls -R` and guess
|
|||||||
## 11. Graph, Memory & Casing Standards
|
## 11. Graph, Memory & Casing Standards
|
||||||
Actively instruct the LLM to maintain its memory constraints and use canonical casing.
|
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:** "Log this architectural decision in the knowledge graph using PascalCase for entity types."
|
||||||
* **Do say:** "Add a sticky note with the test database credentials for this session with a 1-hour TTL."
|
* **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."
|
* **Do say:** "Create a milestone for the 'Rich Clipboard' feature and break it down into active tasks."
|
||||||
* **Tools Triggered:** `create_entities`, `decisions` (log), `sticky_notes` (add), `milestones` (add), `tasks` (add)
|
* **Tools Triggered:** `create_entities`, `decisions` (log), `handoff_memos` (leave), `milestones` (add), `tasks` (add)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -121,10 +121,10 @@ Actively instruct the LLM to maintain its memory constraints and use canonical c
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 16. Context Workspace Diffing
|
## 16. State Checkpoints & Rollbacks
|
||||||
* **Don't say:** "What is the difference between my feature workspace and the main workspace?"
|
* **Don't say:** "Save a backup snapshot before we do this refactor."
|
||||||
* **Do say:** "Diff context workspaces between 'feature-branch' and 'main'."
|
* **Do say:** "Create a checkpoint named 'pre-refactor' before editing the database layer."
|
||||||
* **Tool Triggered:** `diff_context_workspaces`
|
* **Tool Triggered:** `manage_checkpoint`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -29,20 +29,16 @@ To prevent graph fragmentation and ensure optimal LLM tokenization and retrieval
|
|||||||
|
|
||||||
## 🛠️ Consolidated Smart MCP Tools
|
## 🛠️ Consolidated Smart MCP Tools
|
||||||
|
|
||||||
The server consolidates granular single-purpose tools into 12 concise, action-oriented smart domain handlers with zero prefix clutter:
|
The server consolidates granular single-purpose tools into 8 concise, action-oriented smart domain handlers with zero prefix clutter:
|
||||||
|
|
||||||
* **`tasks`**: Complete task lifecycle management (`add`, `update`, `delete`, `list`, `set_criteria`, `verify`).
|
* **`tasks`**: Complete task lifecycle management (`add`, `update`, `delete`, `list`, `set_criteria`, `verify`).
|
||||||
* **`milestones`**: Milestone tracking (`add`, `update`, `list`).
|
* **`milestones`**: Milestone tracking (`add`, `update`, `list`).
|
||||||
* **`sticky_notes`**: Ephemeral scratchpad notes with TTL (`add`, `read`, `delete`, `clear`).
|
|
||||||
* **`handoff_memos`**: Cross-session handoff notes (`leave`, `read`, `clear`).
|
* **`handoff_memos`**: Cross-session handoff notes (`leave`, `read`, `clear`).
|
||||||
* **`pinned_files`**: Working set file focus management (`pin`, `unpin`, `list`).
|
|
||||||
* **`context_workspaces`**: Workspace context state snapshots (`save`, `load`, `list`, `delete`, `diff`).
|
|
||||||
* **`pr_checklist`**: Pre-commit and PR checklist management (`add`, `get`, `clear`).
|
|
||||||
* **`snippets`**: Reusable code snippet vault with BM25+Vector search (`store`, `search`, `delete`, `tag`).
|
* **`snippets`**: Reusable code snippet vault with BM25+Vector search (`store`, `search`, `delete`, `tag`).
|
||||||
* **`decisions`**: Architectural Decision Records (ADRs) (`log`, `query`, `delete`).
|
* **`decisions`**: Architectural Decision Records (ADRs) (`log`, `query`, `delete`).
|
||||||
* **`tech_debt`**: Engineering technical debt backlog (`log`, `resolve`, `list`).
|
* **`tech_debt`**: Engineering technical debt backlog (`log`, `resolve`, `list`).
|
||||||
* **`environment`**: Infrastructure & tool fingerprints tracking (`update_fingerprint`, `read_fingerprint`, `log_requirement`, `register`, `get_details`).
|
* **`environment`**: Infrastructure & tool fingerprints tracking (`update_fingerprint`, `read_fingerprint`, `log_requirement`, `register`, `get_details`).
|
||||||
* **`clipboard`**: Cross-OS clipboard management (`read`, `write`, `toggle_watch`).
|
* **`clipboard`**: Cross-OS clipboard management (`read`, `write`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -86,9 +82,6 @@ Evaluates task dependency graphs and returns unblocked, ready-to-run tasks for s
|
|||||||
### 🧠 Chain-of-Thought & Diagnostic Hypothesis Memory (`log_hypothesis` / `query_hypotheses`)
|
### 🧠 Chain-of-Thought & Diagnostic Hypothesis Memory (`log_hypothesis` / `query_hypotheses`)
|
||||||
Records structured diagnostic hypotheses, test evidence, and verification statuses to preserve reasoning across sessions.
|
Records structured diagnostic hypotheses, test evidence, and verification statuses to preserve reasoning across sessions.
|
||||||
|
|
||||||
### 🔀 Context Workspace Diffing (`context_workspaces`)
|
|
||||||
Computes structured diffs of pinned files and active task IDs between two saved context workspaces.
|
|
||||||
|
|
||||||
### 🔒 Resilient Storage & Serde Parameter Tolerances
|
### 🔒 Resilient Storage & Serde Parameter Tolerances
|
||||||
* **Explicit Fail-Fast Persistence Safety**: Replaced unsafe silent fallback to temporary databases (`/tmp/mcp_store_fallback_*`) with an explicit open retry and fail-fast panic unless `MCP_ALLOW_TMP_FALLBACK=1` is explicitly set, preventing silent data loss.
|
* **Explicit Fail-Fast Persistence Safety**: Replaced unsafe silent fallback to temporary databases (`/tmp/mcp_store_fallback_*`) with an explicit open retry and fail-fast panic unless `MCP_ALLOW_TMP_FALLBACK=1` is explicitly set, preventing silent data loss.
|
||||||
* **Store Write Lock Minimization**: Releases write lock immediately following in-memory mutation, serializing JSON payloads under read locks to allow non-blocking concurrent readers.
|
* **Store Write Lock Minimization**: Releases write lock immediately following in-memory mutation, serializing JSON payloads under read locks to allow non-blocking concurrent readers.
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ description: Strict guidelines for interacting with the mcp-memory server, ensur
|
|||||||
> 2. **Tier 2 (Structured DB & Telemetry)**: The MCP Memory server specializes in high-volume, dynamic data: file modification ledgers (`audit_ledger`), terminal command history, error resolutions (`log_error_fix`), active tasks, and preflight context aggregation.
|
> 2. **Tier 2 (Structured DB & Telemetry)**: The MCP Memory server specializes in high-volume, dynamic data: file modification ledgers (`audit_ledger`), terminal command history, error resolutions (`log_error_fix`), active tasks, and preflight context aggregation.
|
||||||
|
|
||||||
The MCP Memory server is the central brain. You must be PROACTIVE, not reactive, in using it:
|
The MCP Memory server is the central brain. You must be PROACTIVE, not reactive, in using it:
|
||||||
- **Session Starts & Context Drops**: Always begin by calling `tasks` (action: "list"), `pinned_files` (action: "list"), and `sticky_notes` (action: "read").
|
- **Session Starts & Context Drops**: Always begin by calling `tasks` (action: "list"), `get_preflight_context`, and `omni_search` to regain context.
|
||||||
- **Sticky Notes**: Use `sticky_notes` (action: "add") for transient, session-scoped operational constraints (e.g., "Do not touch file X until Y is done"). Deletion supports both 1-based index (standard) and 0-based index 0.
|
- **Context Switching**: When switching tasks or branches, use `manage_checkpoint` (action: "create") to freeze state, and use `manage_checkpoint` (action: "restore") to restore state for the task.
|
||||||
- **Error Fixes**: The moment a tricky, undocumented, or environment-specific bug is resolved (e.g., Bitbucket markdown rendering quirks, nuanced framework bugs), IMMEDIATELY call `log_error_fix`. Supply `repo_name`, `error_category`, and `stack_trace` so future searches can perform embedding-based match.
|
- **Error Fixes**: The moment a tricky, undocumented, or environment-specific bug is resolved (e.g., Bitbucket markdown rendering quirks, nuanced framework bugs), IMMEDIATELY call `log_error_fix`. Supply `repo_name`, `error_category`, and `stack_trace` so future searches can perform embedding-based match.
|
||||||
- **Tech Debt**: If you notice an anti-pattern (e.g., nested `if` statements, arrow 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 (e.g., nested `if` statements, arrow 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)**: When selecting design patterns, crate choices, or system structure, call `decisions` (action: "log") with `author`, `affected_components`, `alternatives_considered`, `decision`, and `consequence`.
|
- **Architectural Decisions (ADR)**: When selecting design patterns, crate choices, or system structure, call `decisions` (action: "log") with `author`, `affected_components`, `alternatives_considered`, `decision`, and `consequence`.
|
||||||
|
|||||||
@@ -90,11 +90,11 @@ The physical storage location of the knowledge graph and all persistent stores i
|
|||||||
## 10. Exposed MCP Capabilities (Tools)
|
## 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:
|
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,
|
* **Graph Management:** create_entities, create_relations, merge_entities,
|
||||||
ead_graph, etc.
|
read_graph, etc.
|
||||||
* **Task & Context Tracking:** add_task, add_sticky_note, add_session_summary, etc.
|
* **Task & Context Tracking:** tasks, milestones, handoff_memos, add_session_summary, etc.
|
||||||
* **Engineering & DevOps:** log_code_change, log_error_fix, log_tech_debt, add_pr_checklist_item.
|
* **Engineering & DevOps:** log_code_change, log_error_fix, tech_debt, decisions, etc.
|
||||||
* **Environment & Workspaces:**
|
* **Environment & Workspaces:** environment, manage_checkpoint, manage_subagent_namespace, snippets, etc.
|
||||||
egister_environment, save_context_workspace, pin_file.
|
|
||||||
|
|
||||||
## 11. Concurrency & Thread Safety
|
## 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.
|
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.
|
||||||
|
|||||||
+2
-27
@@ -41,34 +41,11 @@ The server consolidates granular single-purpose tools into domain-named smart to
|
|||||||
- `action: "update"`: Update milestone status (requires `id`, `status: "active" | "completed" | "cancelled"`).
|
- `action: "update"`: Update milestone status (requires `id`, `status: "active" | "completed" | "cancelled"`).
|
||||||
- `action: "list"`: List milestones (optional `namespace`).
|
- `action: "list"`: List milestones (optional `namespace`).
|
||||||
|
|
||||||
* **`sticky_notes`**: Ephemeral scratchpad notes with TTL.
|
|
||||||
- `action: "add"`: Add note (requires `content`, optional `ttl_seconds`, `session_only`).
|
|
||||||
- `action: "read"`: Read all active notes.
|
|
||||||
- `action: "delete"`: Delete note by index (requires 1-based `index`).
|
|
||||||
- `action: "clear"`: Clear all sticky notes.
|
|
||||||
|
|
||||||
* **`handoff_memos`**: Session handoff notes for future agents.
|
* **`handoff_memos`**: Session handoff notes for future agents.
|
||||||
- `action: "leave"`: Leave a memo (requires `content`, optional `vcs_revision`, `repo_name`, `git_branch`, `blockers: Vec<String>`, `action_items: Vec<String>`, `expires_at`).
|
- `action: "leave"`: Leave a memo (requires `content`, optional `vcs_revision`, `repo_name`, `git_branch`, `blockers: Vec<String>`, `action_items: Vec<String>`, `expires_at`).
|
||||||
- `action: "read"`: Read active handoff memos.
|
- `action: "read"`: Read active handoff memos.
|
||||||
- `action: "clear"`: Clear memos.
|
- `action: "clear"`: Clear memos.
|
||||||
|
|
||||||
* **`pinned_files`**: Focus file working set.
|
|
||||||
- `action: "pin"`: Pin file to focus set (requires `path`, optional `pin_reason`, `symbol_or_line`, `repo_name`, `git_branch`).
|
|
||||||
- `action: "unpin"`: Unpin file from focus set (requires `path`).
|
|
||||||
- `action: "list"`: List pinned files (optional `namespace`).
|
|
||||||
|
|
||||||
* **`context_workspaces`**: Workspace context state snapshots.
|
|
||||||
- `action: "save"`: Save context workspace (requires `name`, optional `pinned_files`, `active_task_ids`, `description`, `git_branch`, `vcs_revision`, `repo_name`).
|
|
||||||
- `action: "load"`: Restore saved context workspace (requires `name`).
|
|
||||||
- `action: "list"`: List saved context workspaces.
|
|
||||||
- `action: "delete"`: Delete saved context workspace (requires `name`).
|
|
||||||
- `action: "diff"`: Compare two saved context workspaces (requires `name`, `other_name`).
|
|
||||||
|
|
||||||
* **`pr_checklist`**: Pre-commit and PR checklist.
|
|
||||||
- `action: "add"`: Add checklist item (requires `description`, optional `category: "Security" | "Testing" | "Formatting" | ...`, `automated_check`, `is_checked`, `repo_name`).
|
|
||||||
- `action: "get"`: Get PR checklist items.
|
|
||||||
- `action: "clear"`: Clear PR checklist.
|
|
||||||
|
|
||||||
* **`snippets`**: Reusable code snippet vault.
|
* **`snippets`**: Reusable code snippet vault.
|
||||||
- `action: "store"`: Store snippet (requires `query` as name, optional `language`, `code`, `description`, `tags`, `origin_file`, `line_range`, `repo_name`).
|
- `action: "store"`: Store snippet (requires `query` as name, optional `language`, `code`, `description`, `tags`, `origin_file`, `line_range`, `repo_name`).
|
||||||
- `action: "search"`: Search snippet vault (optional `query`, `tags`, `hybrid: true`).
|
- `action: "search"`: Search snippet vault (optional `query`, `tags`, `hybrid: true`).
|
||||||
@@ -95,7 +72,6 @@ The server consolidates granular single-purpose tools into domain-named smart to
|
|||||||
* **`clipboard`**: OS Clipboard management.
|
* **`clipboard`**: OS Clipboard management.
|
||||||
- `action: "read"`: Read OS clipboard.
|
- `action: "read"`: Read OS clipboard.
|
||||||
- `action: "write"`: Write text/html/files/image to clipboard.
|
- `action: "write"`: Write text/html/files/image to clipboard.
|
||||||
- `action: "toggle_watch"`: Toggle auto-clipboard watcher.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -194,9 +170,8 @@ To maintain maximum security, speed, and cross-platform reliability:
|
|||||||
* **Zero-Allocation Stream Formatting**: Graph condensation loops (`condense_graph_worker`) format node/relation subgraphs into stream buffers via `std::fmt::Write`, eliminating intermediate String allocation overhead.
|
* **Zero-Allocation Stream Formatting**: Graph condensation loops (`condense_graph_worker`) format node/relation subgraphs into stream buffers via `std::fmt::Write`, eliminating intermediate String allocation overhead.
|
||||||
* **Single-Handle OS Clipboard Retries & Image Downsampling**: `WriteClipboardHandler` initializes OS clipboard handles once per operation and downsamples images exceeding $2048 \times 2048$ resolution before writing raw RGBA bytes.
|
* **Single-Handle OS Clipboard Retries & Image Downsampling**: `WriteClipboardHandler` initializes OS clipboard handles once per operation and downsamples images exceeding $2048 \times 2048$ resolution before writing raw RGBA bytes.
|
||||||
* **Zero Transaction Drop Persistence Guarantee**: `Store::modify` automatically spawns an async task to execute `push_async` with channel backpressure if `push` encounters queue saturation, ensuring zero data loss under spike write loads.
|
* **Zero Transaction Drop Persistence Guarantee**: `Store::modify` automatically spawns an async task to execute `push_async` with channel backpressure if `push` encounters queue saturation, ensuring zero data loss under spike write loads.
|
||||||
* **Non-Blocking Read Lock Sticky Notes**: `StickyNotesHandler` (`action = "read"`) queries notes using shared read locks, executing write pruning only when expired items exist.
|
|
||||||
* **Token-Budgeted Query Projections**: Decision queries (`query_decisions`) support `limit` caps and compact `include_body: false` projections for token budget optimization.
|
* **Token-Budgeted Query Projections**: Decision queries (`query_decisions`) support `limit` caps and compact `include_body: false` projections for token budget optimization.
|
||||||
* **Serde Parameter & Enum Ergonomics**: Action enums (`StickyNoteAction`, `SnippetSearchMode`, `Relation`) support case-insensitive variants and common synonyms (`create`/`add`, `remove`/`delete`, `list`/`read`, `source`/`from`, `target`/`to`, `relationType`/`relation_type`) ensuring seamless LLM tool execution.
|
* **Serde Parameter & Enum Ergonomics**: Action enums (`SnippetSearchMode`, `Relation`) support case-insensitive variants and common synonyms (`create`/`add`, `remove`/`delete`, `list`/`read`, `source`/`from`, `target`/`to`, `relationType`/`relation_type`) ensuring seamless LLM tool execution.
|
||||||
* **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.
|
* **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.
|
||||||
* **Non-Blocking Tantivy Search Queries**: `MemoryIndex::search` queries current index searcher snapshots without executing synchronous disk commit locks, preventing query stalls during heavy background indexing.
|
* **Non-Blocking Tantivy Search Queries**: `MemoryIndex::search` queries current index searcher snapshots without executing synchronous disk commit locks, preventing query stalls during heavy background indexing.
|
||||||
* **Zero-Allocation HashSet<&str> Snippet Deduplication**: `indexer.rs` utilizes borrowed `HashSet<&str>` name lookups during snippet batch modifications, eliminating heap string re-allocations inside the store write lock.
|
* **Zero-Allocation HashSet<&str> Snippet Deduplication**: `indexer.rs` utilizes borrowed `HashSet<&str>` name lookups during snippet batch modifications, eliminating heap string re-allocations inside the store write lock.
|
||||||
@@ -219,7 +194,7 @@ To maintain maximum security, speed, and cross-platform reliability:
|
|||||||
* **Embedding Input Safeguard**: `generate_embedding_async` returns explicit errors for empty/0-length text inputs instead of returning empty vectors, preventing downstream vector dimension mismatches during cosine similarity calculations.
|
* **Embedding Input Safeguard**: `generate_embedding_async` returns explicit errors for empty/0-length text inputs instead of returning empty vectors, preventing downstream vector dimension mismatches during cosine similarity calculations.
|
||||||
* **Path Traversal Security Guards**: `validate_safe_path` enforces path canonicalization and rejects relative parent traversal components (`..`) across file and process log handlers (`GetRecentLogsTool`, `WatchProcessLogsTool`).
|
* **Path Traversal Security Guards**: `validate_safe_path` enforces path canonicalization and rejects relative parent traversal components (`..`) across file and process log handlers (`GetRecentLogsTool`, `WatchProcessLogsTool`).
|
||||||
* **Watcher Map Memory Eviction**: Proactive daemon file watcher in `watcher.rs` caps `last_processed` map size at 1,000 entries and purges entries older than 10 minutes to prevent monotonic memory leakage.
|
* **Watcher Map Memory Eviction**: Proactive daemon file watcher in `watcher.rs` caps `last_processed` map size at 1,000 entries and purges entries older than 10 minutes to prevent monotonic memory leakage.
|
||||||
* **Comprehensive Serde Casing Aliases**: All 11 consolidated tool action enums (`TaskAction`, `MilestoneAction`, `PinnedFileAction`, `ContextWorkspaceAction`, `PrChecklistAction`, `SnippetAction`, `DecisionAction`, `TechDebtAction`, `EnvAction`, `ClipboardAction`, `HandoffMemoAction`) include serde alias attributes supporting `snake_case`, `camelCase`, `PascalCase`, and uppercase variants for maximum LLM casing resilience.
|
* **Comprehensive Serde Casing Aliases**: All 8 consolidated tool action enums (TaskAction, MilestoneAction, SnippetAction, DecisionAction, TechDebtAction, EnvAction, ClipboardAction, HandoffMemoAction) include serde alias attributes supporting `snake_case`, `camelCase`, `PascalCase`, and uppercase variants for maximum LLM casing resilience.
|
||||||
* **Two-Phase Graph Condensation**: `condense_graph_worker` uses a 2-phase commit (non-destructive `read_with` -> graph insert -> prune by timestamp/content) to prevent data loss if summarization or graph insertion fails.
|
* **Two-Phase Graph Condensation**: `condense_graph_worker` uses a 2-phase commit (non-destructive `read_with` -> graph insert -> prune by timestamp/content) to prevent data loss if summarization or graph insertion fails.
|
||||||
* **Store Write Lock Minimization**: `Store::modify` and `Store::modify_async` unblock concurrent readers during JSON serialization by releasing the write lock immediately after mutating memory state.
|
* **Store Write Lock Minimization**: `Store::modify` and `Store::modify_async` unblock concurrent readers during JSON serialization by releasing the write lock immediately after mutating memory state.
|
||||||
* **Redb Database Lock Retry Backoff**: `init_db` retries transient Redb lock contention with exponential backoff (3 attempts, 150ms delay) before falling back.
|
* **Redb Database Lock Retry Backoff**: `init_db` retries transient Redb lock contention with exponential backoff (3 attempts, 150ms delay) before falling back.
|
||||||
|
|||||||
@@ -8,6 +8,31 @@ pub static ACTIVE_SOCKET: LazyLock<Arc<tokio::sync::Mutex<Option<String>>>> =
|
|||||||
pub static HEADLESS_PROC: LazyLock<Arc<tokio::sync::Mutex<Option<tokio::process::Child>>>> =
|
pub static HEADLESS_PROC: LazyLock<Arc<tokio::sync::Mutex<Option<tokio::process::Child>>>> =
|
||||||
LazyLock::new(|| Arc::new(tokio::sync::Mutex::new(None)));
|
LazyLock::new(|| Arc::new(tokio::sync::Mutex::new(None)));
|
||||||
|
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn atexit(f: unsafe extern "C" fn()) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
static HEADLESS_PID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
||||||
|
static ONCE_INIT: std::sync::Once = std::sync::Once::new();
|
||||||
|
|
||||||
|
unsafe extern "C" fn cleanup_on_exit() {
|
||||||
|
let pid = HEADLESS_PID.swap(0, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
if pid != 0 {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let _ = std::process::Command::new("taskkill")
|
||||||
|
.args(["/F", "/T", "/PID", &pid.to_string()])
|
||||||
|
.output();
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
let _ = std::process::Command::new("kill")
|
||||||
|
.args(["-9", &pid.to_string()])
|
||||||
|
.output();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn spawn_headless_nvim() -> Result<String, String> {
|
pub async fn spawn_headless_nvim() -> Result<String, String> {
|
||||||
if std::process::Command::new("nvim")
|
if std::process::Command::new("nvim")
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
@@ -17,6 +42,10 @@ pub async fn spawn_headless_nvim() -> Result<String, String> {
|
|||||||
return Err("Error: Neovim is not installed or not in PATH. Cannot execute nvim tool. Fallback to basic terminal tools.".to_string());
|
return Err("Error: Neovim is not installed or not in PATH. Cannot execute nvim tool. Fallback to basic terminal tools.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ONCE_INIT.call_once(|| unsafe {
|
||||||
|
atexit(cleanup_on_exit);
|
||||||
|
});
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
let socket_name = format!(
|
let socket_name = format!(
|
||||||
r"\\.\pipe\agy-headless-nvim-{}-{}",
|
r"\\.\pipe\agy-headless-nvim-{}-{}",
|
||||||
@@ -49,6 +78,10 @@ pub async fn spawn_headless_nvim() -> Result<String, String> {
|
|||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| format!("Failed to spawn headless Neovim: {}", e))?;
|
.map_err(|e| format!("Failed to spawn headless Neovim: {}", e))?;
|
||||||
|
|
||||||
|
if let Some(id) = child.id() {
|
||||||
|
HEADLESS_PID.store(id, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(stdout) = child.stdout.take() {
|
if let Some(stdout) = child.stdout.take() {
|
||||||
let mut reader = tokio::io::BufReader::new(stdout);
|
let mut reader = tokio::io::BufReader::new(stdout);
|
||||||
let mut ready_line = String::new();
|
let mut ready_line = String::new();
|
||||||
@@ -60,6 +93,7 @@ pub async fn spawn_headless_nvim() -> Result<String, String> {
|
|||||||
{
|
{
|
||||||
Ok(Ok(_)) => {
|
Ok(Ok(_)) => {
|
||||||
if ready_line.trim() != "READY" {
|
if ready_line.trim() != "READY" {
|
||||||
|
HEADLESS_PID.store(0, std::sync::atomic::Ordering::SeqCst);
|
||||||
let _ = child.kill().await;
|
let _ = child.kill().await;
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Headless Neovim child output unexpected banner: {:?}",
|
"Headless Neovim child output unexpected banner: {:?}",
|
||||||
@@ -68,6 +102,7 @@ pub async fn spawn_headless_nvim() -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
|
HEADLESS_PID.store(0, std::sync::atomic::Ordering::SeqCst);
|
||||||
let _ = child.kill().await;
|
let _ = child.kill().await;
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Failed to read readiness from headless Neovim: {}",
|
"Failed to read readiness from headless Neovim: {}",
|
||||||
@@ -75,6 +110,7 @@ pub async fn spawn_headless_nvim() -> Result<String, String> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
HEADLESS_PID.store(0, std::sync::atomic::Ordering::SeqCst);
|
||||||
let _ = child.kill().await;
|
let _ = child.kill().await;
|
||||||
return Err("Timeout waiting for headless Neovim to become ready".to_string());
|
return Err("Timeout waiting for headless Neovim to become ready".to_string());
|
||||||
}
|
}
|
||||||
@@ -96,6 +132,7 @@ pub async fn spawn_headless_nvim() -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn kill_headless_nvim() {
|
pub async fn kill_headless_nvim() {
|
||||||
|
HEADLESS_PID.store(0, std::sync::atomic::Ordering::SeqCst);
|
||||||
let child_to_kill = {
|
let child_to_kill = {
|
||||||
let mut proc_lock = HEADLESS_PROC.lock().await;
|
let mut proc_lock = HEADLESS_PROC.lock().await;
|
||||||
proc_lock.take()
|
proc_lock.take()
|
||||||
|
|||||||
@@ -167,6 +167,43 @@ async fn get_socket_path() -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
Err("Could not find Neovim socket".to_string())
|
Err("Could not find Neovim socket".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
async fn read_active_socket_file() -> Result<String, String> {
|
||||||
|
let profile = dirs::home_dir()
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.or_else(|| std::env::var("USERPROFILE").ok())
|
||||||
|
.ok_or_else(|| "Could not determine user home directory".to_string())?;
|
||||||
|
let path = format!("{profile}\\.gemini\\active_nvim.txt");
|
||||||
|
if let Ok(content) = tokio::fs::read_to_string(&path).await {
|
||||||
|
let p = content.trim().to_string();
|
||||||
|
if !p.is_empty() {
|
||||||
|
let candidate = if p.starts_with(r"\\.\pipe\") {
|
||||||
|
p
|
||||||
|
} else if p.starts_with("nvim.") {
|
||||||
|
format!(r"\\.\pipe\{p}")
|
||||||
|
} else {
|
||||||
|
p
|
||||||
|
};
|
||||||
|
return Ok(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err("Missing active_nvim.txt".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
async fn read_active_socket_file() -> Result<String, String> {
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
let path = home.join(".gemini/active_nvim.txt");
|
||||||
|
if let Ok(content) = tokio::fs::read_to_string(&path).await {
|
||||||
|
let p = content.trim().to_string();
|
||||||
|
if !p.is_empty() {
|
||||||
|
return Ok(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err("Missing active_nvim.txt".to_string())
|
||||||
|
}
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use tokio::sync::{mpsc, oneshot};
|
use tokio::sync::{mpsc, oneshot};
|
||||||
@@ -222,6 +259,24 @@ fn handle_nvim_notification(params: &[rmpv::Value]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn get_nvim_connection() -> Result<mpsc::Sender<NvimRequest>, String> {
|
async fn get_nvim_connection() -> Result<mpsc::Sender<NvimRequest>, String> {
|
||||||
|
// Fast path: If NVIM_CONN is already active and healthy, check if active_nvim.txt matches
|
||||||
|
// without spawning any external nvim.exe child process!
|
||||||
|
{
|
||||||
|
let conn_lock = NVIM_CONN.lock().await;
|
||||||
|
if let Some(sender) = conn_lock.as_ref() {
|
||||||
|
if !sender.is_closed() {
|
||||||
|
let active_lock = ACTIVE_SOCKET.lock().await;
|
||||||
|
if let Some(ref current_socket) = *active_lock {
|
||||||
|
if let Ok(target) = read_active_socket_file().await {
|
||||||
|
if !target.is_empty() && &target == current_socket {
|
||||||
|
return Ok(sender.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let interactive_socket = get_socket_path().await.ok();
|
let interactive_socket = get_socket_path().await.ok();
|
||||||
let mut should_kill_headless = false;
|
let mut should_kill_headless = false;
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,15 @@ pub async fn wait_for_event_handler(
|
|||||||
return axum::Json(event);
|
return axum::Json(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
|
||||||
|
tracing::warn!("Event bus receiver lagged by {} messages; continuing wait.", skipped);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||||
return axum::Json(GenericEvent {
|
return axum::Json(GenericEvent {
|
||||||
topic: "error".to_string(),
|
topic: "error".to_string(),
|
||||||
session_id: None,
|
session_id: None,
|
||||||
payload: serde_json::json!({"error": "Event bus lagged or closed"}),
|
payload: serde_json::json!({"error": "Event bus closed"}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,18 +237,14 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
|||||||
let adrs = state_clone.adrs.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 ledger = state_clone.ledger.read_with(|items| items.len());
|
||||||
let sticky = state_clone.sticky.read_with(|items| items.len());
|
|
||||||
let error_fixes = state_clone.error_fixes.read_with(|items| items.len());
|
let error_fixes = state_clone.error_fixes.read_with(|items| items.len());
|
||||||
let pinned_files = state_clone.pinned_files.read_with(|items| items.len());
|
|
||||||
let session_summaries = state_clone.session_summaries.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 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_fingerprints = state_clone.env_fingerprints.read_with(|items| items.len());
|
||||||
let env_requirements = state_clone.env_requirements.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 milestones = state_clone.milestones.read_with(|items| items.len());
|
||||||
let environments = state_clone.environments.read_with(|items| items.len());
|
let environments = state_clone.environments.read_with(|items| items.len());
|
||||||
let pr_checklists = state_clone.pr_checklists.read_with(|items| items.len());
|
|
||||||
let gates = state_clone.gates.read_with(|items| items.len());
|
let gates = state_clone.gates.read_with(|items| items.len());
|
||||||
let context_workspaces = state_clone.context_workspaces.read_with(|items| items.len());
|
|
||||||
|
|
||||||
axum::Json(serde_json::json!({
|
axum::Json(serde_json::json!({
|
||||||
"entities": entities,
|
"entities": entities,
|
||||||
@@ -258,18 +254,14 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
|||||||
"tech_debts": tech_debts,
|
"tech_debts": tech_debts,
|
||||||
"adrs": adrs,
|
"adrs": adrs,
|
||||||
"ledger": ledger,
|
"ledger": ledger,
|
||||||
"sticky": sticky,
|
|
||||||
"error_fixes": error_fixes,
|
"error_fixes": error_fixes,
|
||||||
"pinned_files": pinned_files,
|
|
||||||
"session_summaries": session_summaries,
|
"session_summaries": session_summaries,
|
||||||
"handoff_memos": handoff_memos,
|
"handoff_memos": handoff_memos,
|
||||||
"env_fingerprints": env_fingerprints,
|
"env_fingerprints": env_fingerprints,
|
||||||
"env_requirements": env_requirements,
|
"env_requirements": env_requirements,
|
||||||
"milestones": milestones,
|
"milestones": milestones,
|
||||||
"environments": environments,
|
"environments": environments,
|
||||||
"pr_checklists": pr_checklists,
|
"gates": gates
|
||||||
"gates": gates,
|
|
||||||
"context_workspaces": context_workspaces
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
+2
-42
@@ -205,16 +205,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/sticky",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let sticky_json = state_clone.code.sticky.read_with(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string()));
|
|
||||||
([(axum::http::header::CONTENT_TYPE, "application/json")], sticky_json)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/api/search",
|
"/api/search",
|
||||||
get({
|
get({
|
||||||
@@ -264,16 +255,6 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/context_workspaces",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let json = state_clone.project.context_workspaces.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()));
|
|
||||||
([(axum::http::header::CONTENT_TYPE, "application/json")], json)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/api/handoff_memos",
|
"/api/handoff_memos",
|
||||||
get({
|
get({
|
||||||
@@ -304,16 +285,6 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/api/pr_checklists",
|
|
||||||
get({
|
|
||||||
let state_clone = app_state.handler.state.clone();
|
|
||||||
move || async move {
|
|
||||||
let json = state_clone.project.pr_checklists.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()));
|
|
||||||
([(axum::http::header::CONTENT_TYPE, "application/json")], json)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/api/error_fixes",
|
"/api/error_fixes",
|
||||||
get({
|
get({
|
||||||
@@ -336,18 +307,14 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
|||||||
let adrs = state_clone.code.adrs.read_with(|items| items.len());
|
let adrs = state_clone.code.adrs.read_with(|items| items.len());
|
||||||
|
|
||||||
let ledger = state_clone.code.ledger.read_with(|items| items.len());
|
let ledger = state_clone.code.ledger.read_with(|items| items.len());
|
||||||
let sticky = state_clone.code.sticky.read_with(|items| items.len());
|
|
||||||
let error_fixes = state_clone.code.error_fixes.read_with(|items| items.len());
|
let error_fixes = state_clone.code.error_fixes.read_with(|items| items.len());
|
||||||
let pinned_files = state_clone.project.pinned_files.read_with(|items| items.len());
|
|
||||||
let session_summaries = state_clone.telemetry.session_summaries.read_with(|items| items.len());
|
let session_summaries = state_clone.telemetry.session_summaries.read_with(|items| items.len());
|
||||||
let handoff_memos = state_clone.telemetry.handoff_memos.read_with(|items| items.len());
|
let handoff_memos = state_clone.telemetry.handoff_memos.read_with(|items| items.len());
|
||||||
let env_fingerprints = state_clone.env.env_fingerprints.read_with(|items| items.len());
|
let env_fingerprints = state_clone.env.env_fingerprints.read_with(|items| items.len());
|
||||||
let env_requirements = state_clone.env.env_requirements.read_with(|items| items.len());
|
let env_requirements = state_clone.env.env_requirements.read_with(|items| items.len());
|
||||||
let milestones = state_clone.project.milestones.read_with(|items| items.len());
|
let milestones = state_clone.project.milestones.read_with(|items| items.len());
|
||||||
let environments = state_clone.env.environments.read_with(|items| items.len());
|
let environments = state_clone.env.environments.read_with(|items| items.len());
|
||||||
let pr_checklists = state_clone.project.pr_checklists.read_with(|items| items.len());
|
|
||||||
let gates = state_clone.env.gates.read_with(|items| items.len());
|
let gates = state_clone.env.gates.read_with(|items| items.len());
|
||||||
let context_workspaces = state_clone.project.context_workspaces.read_with(|items| items.len());
|
|
||||||
|
|
||||||
axum::Json(serde_json::json!({
|
axum::Json(serde_json::json!({
|
||||||
"entities": entities,
|
"entities": entities,
|
||||||
@@ -357,18 +324,14 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
|||||||
"tech_debts": tech_debts,
|
"tech_debts": tech_debts,
|
||||||
"adrs": adrs,
|
"adrs": adrs,
|
||||||
"ledger": ledger,
|
"ledger": ledger,
|
||||||
"sticky": sticky,
|
|
||||||
"error_fixes": error_fixes,
|
"error_fixes": error_fixes,
|
||||||
"pinned_files": pinned_files,
|
|
||||||
"session_summaries": session_summaries,
|
"session_summaries": session_summaries,
|
||||||
"handoff_memos": handoff_memos,
|
"handoff_memos": handoff_memos,
|
||||||
"env_fingerprints": env_fingerprints,
|
"env_fingerprints": env_fingerprints,
|
||||||
"env_requirements": env_requirements,
|
"env_requirements": env_requirements,
|
||||||
"milestones": milestones,
|
"milestones": milestones,
|
||||||
"environments": environments,
|
"environments": environments,
|
||||||
"pr_checklists": pr_checklists,
|
"gates": gates
|
||||||
"gates": gates,
|
|
||||||
"context_workspaces": context_workspaces
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -640,15 +603,12 @@ mod tests {
|
|||||||
"/",
|
"/",
|
||||||
"/api/graph",
|
"/api/graph",
|
||||||
"/api/tasks",
|
"/api/tasks",
|
||||||
"/api/sticky",
|
|
||||||
"/api/search?q=test",
|
"/api/search?q=test",
|
||||||
"/api/tech_debts",
|
"/api/tech_debts",
|
||||||
"/api/adrs",
|
"/api/adrs",
|
||||||
"/api/context_workspaces",
|
|
||||||
"/api/handoff_memos",
|
"/api/handoff_memos",
|
||||||
"/api/milestones",
|
"/api/milestones",
|
||||||
"/api/snippets",
|
"/api/snippets",
|
||||||
"/api/pr_checklists",
|
|
||||||
"/api/error_fixes",
|
"/api/error_fixes",
|
||||||
"/api/stats",
|
"/api/stats",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -127,23 +127,18 @@ pub fn init_redb(base: &Path) -> Arc<Database> {
|
|||||||
let stores = vec![
|
let stores = vec![
|
||||||
("knowledge_graph_master", "knowledge_graph_master.json"),
|
("knowledge_graph_master", "knowledge_graph_master.json"),
|
||||||
("audit_ledger", "audit_ledger.json"),
|
("audit_ledger", "audit_ledger.json"),
|
||||||
("sticky_notes", "sticky_notes.json"),
|
|
||||||
("tasks", "tasks.json"),
|
("tasks", "tasks.json"),
|
||||||
("snippets", "snippets.json"),
|
("snippets", "snippets.json"),
|
||||||
("adrs", "adrs.json"),
|
("adrs", "adrs.json"),
|
||||||
("preferences", "preferences.json"),
|
|
||||||
("error_fixes", "error_fixes.json"),
|
("error_fixes", "error_fixes.json"),
|
||||||
("pinned_files", "pinned_files.json"),
|
|
||||||
("session_summaries", "session_summaries.json"),
|
("session_summaries", "session_summaries.json"),
|
||||||
("handoff_memos", "handoff_memos.json"),
|
("handoff_memos", "handoff_memos.json"),
|
||||||
("env_fingerprints", "env_fingerprints.json"),
|
("env_fingerprints", "env_fingerprints.json"),
|
||||||
("env_requirements", "env_requirements.json"),
|
("env_requirements", "env_requirements.json"),
|
||||||
("milestones", "milestones.json"),
|
("milestones", "milestones.json"),
|
||||||
("environments", "environments.json"),
|
("environments", "environments.json"),
|
||||||
("pr_checklists", "pr_checklists.json"),
|
|
||||||
("tech_debts", "tech_debts.json"),
|
("tech_debts", "tech_debts.json"),
|
||||||
("gates", "gates.json"),
|
("gates", "gates.json"),
|
||||||
("context_workspaces", "context_workspaces.json"),
|
|
||||||
("state_snapshots", "state_snapshots.json"),
|
("state_snapshots", "state_snapshots.json"),
|
||||||
("hypotheses", "hypotheses.json"),
|
("hypotheses", "hypotheses.json"),
|
||||||
("agent_signals", "agent_signals.json"),
|
("agent_signals", "agent_signals.json"),
|
||||||
|
|||||||
@@ -280,12 +280,16 @@ pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
|
|||||||
norm_b_sq += y * y;
|
norm_b_sq += y * y;
|
||||||
}
|
}
|
||||||
|
|
||||||
let norm_a = norm_a_sq.sqrt();
|
// Fast path: If vectors are already normalized (Candle & fallback embeddings), skip square roots
|
||||||
let norm_b = norm_b_sq.sqrt();
|
if (norm_a_sq - 1.0).abs() < 1e-4 && (norm_b_sq - 1.0).abs() < 1e-4 {
|
||||||
if norm_a == 0.0 || norm_b == 0.0 {
|
return dot_product.clamp(-1.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let norm_product = norm_a_sq * norm_b_sq;
|
||||||
|
if norm_product <= 0.0 {
|
||||||
0.0
|
0.0
|
||||||
} else {
|
} else {
|
||||||
dot_product / (norm_a * norm_b)
|
(dot_product / norm_product.sqrt()).clamp(-1.0, 1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+58
-37
@@ -81,27 +81,64 @@ impl McpTool for ReadFileSkeletonHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let kind = node.kind();
|
let kind = node.kind();
|
||||||
|
let is_container = matches!(
|
||||||
|
kind,
|
||||||
|
"impl_item"
|
||||||
|
| "class_declaration"
|
||||||
|
| "class_definition"
|
||||||
|
| "trait_item"
|
||||||
|
| "interface_declaration"
|
||||||
|
);
|
||||||
|
|
||||||
let is_structural = matches!(
|
let is_structural = is_container
|
||||||
|
|| matches!(
|
||||||
kind,
|
kind,
|
||||||
"use_declaration"
|
"use_declaration"
|
||||||
| "import_statement"
|
| "import_statement"
|
||||||
| "import_from_statement"
|
| "import_from_statement"
|
||||||
| "struct_item"
|
| "struct_item"
|
||||||
| "enum_item"
|
| "enum_item"
|
||||||
| "trait_item"
|
|
||||||
| "impl_item"
|
|
||||||
| "function_item"
|
| "function_item"
|
||||||
| "function_declaration"
|
| "function_declaration"
|
||||||
| "function_definition"
|
| "function_definition"
|
||||||
| "method_definition"
|
| "method_definition"
|
||||||
| "interface_declaration"
|
|
||||||
| "type_alias_declaration"
|
| "type_alias_declaration"
|
||||||
| "class_declaration"
|
|
||||||
| "class_definition"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if is_structural {
|
if is_container {
|
||||||
|
let indent = " ".repeat(depth);
|
||||||
|
let node_text = node.utf8_text(code.as_bytes()).unwrap_or("");
|
||||||
|
let mut header = String::new();
|
||||||
|
for line in node_text.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed.ends_with('{') || trimmed.ends_with(':') {
|
||||||
|
header.push_str(line);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
header.push_str(line);
|
||||||
|
header.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if header.is_empty() {
|
||||||
|
if let Some(first_line) = node_text.lines().next() {
|
||||||
|
header = first_line.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push_str(&indent);
|
||||||
|
out.push_str(header.trim());
|
||||||
|
out.push('\n');
|
||||||
|
|
||||||
|
let mut cursor = node.walk();
|
||||||
|
for child in node.named_children(&mut cursor) {
|
||||||
|
extract_skeleton(child, code, out, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if header.trim().ends_with('{') {
|
||||||
|
out.push_str(&indent);
|
||||||
|
out.push_str("}\n");
|
||||||
|
}
|
||||||
|
} else if is_structural {
|
||||||
let indent = " ".repeat(depth);
|
let indent = " ".repeat(depth);
|
||||||
|
|
||||||
let node_text = node.utf8_text(code.as_bytes()).unwrap_or("");
|
let node_text = node.utf8_text(code.as_bytes()).unwrap_or("");
|
||||||
@@ -127,7 +164,7 @@ impl McpTool for ReadFileSkeletonHandler {
|
|||||||
} else if node.is_named() {
|
} else if node.is_named() {
|
||||||
let mut cursor = node.walk();
|
let mut cursor = node.walk();
|
||||||
for child in node.named_children(&mut cursor) {
|
for child in node.named_children(&mut cursor) {
|
||||||
extract_skeleton(child, code, out, depth + 1);
|
extract_skeleton(child, code, out, depth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,6 +268,7 @@ impl McpTool for ReplaceAstNodeHandler {
|
|||||||
actual_kind,
|
actual_kind,
|
||||||
"trait_item" | "interface_declaration" | "interface_item"
|
"trait_item" | "interface_declaration" | "interface_item"
|
||||||
),
|
),
|
||||||
|
"impl" | "impl_item" => actual_kind == "impl_item",
|
||||||
"type" | "type_alias" => matches!(
|
"type" | "type_alias" => matches!(
|
||||||
actual_kind,
|
actual_kind,
|
||||||
"type_alias_declaration" | "type_item" | "type_definition"
|
"type_alias_declaration" | "type_item" | "type_definition"
|
||||||
@@ -253,6 +291,7 @@ impl McpTool for ReplaceAstNodeHandler {
|
|||||||
let kind = child.kind();
|
let kind = child.kind();
|
||||||
if kind == "identifier"
|
if kind == "identifier"
|
||||||
|| kind == "name"
|
|| kind == "name"
|
||||||
|
|| kind == "type_identifier"
|
||||||
|| kind == "property_identifier"
|
|| kind == "property_identifier"
|
||||||
|| kind == "field_identifier"
|
|| kind == "field_identifier"
|
||||||
{
|
{
|
||||||
@@ -386,6 +425,11 @@ fn scan_workspace_for_symbol(
|
|||||||
if scanned_files > 500 {
|
if scanned_files > 500 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if let Ok(meta) = std::fs::metadata(path) {
|
||||||
|
if meta.len() > 1024 * 1024 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Ok(content) = std::fs::read_to_string(path) {
|
if let Ok(content) = std::fs::read_to_string(path) {
|
||||||
for (line_num, line) in content.lines().enumerate() {
|
for (line_num, line) in content.lines().enumerate() {
|
||||||
let is_match = if filter_fn_call {
|
let is_match = if filter_fn_call {
|
||||||
@@ -450,17 +494,7 @@ impl McpTool for FindSymbolReferencesHandler {
|
|||||||
Ok::<Vec<serde_json::Value>, crate::error::AppError>(refs)
|
Ok::<Vec<serde_json::Value>, crate::error::AppError>(refs)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let custom_dir = if let Some(ref d) = req.workspace_dir {
|
let custom_dir = req.workspace_dir.as_ref().map(std::path::PathBuf::from);
|
||||||
Some(std::path::PathBuf::from(d))
|
|
||||||
} else {
|
|
||||||
state.project.pinned_files.read_with(|pinned| {
|
|
||||||
pinned.first().and_then(|pin| {
|
|
||||||
std::path::Path::new(&pin.file_path)
|
|
||||||
.parent()
|
|
||||||
.map(|p| p.to_path_buf())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
if matches.len() < limit {
|
if matches.len() < limit {
|
||||||
let remaining = limit - matches.len();
|
let remaining = limit - matches.len();
|
||||||
@@ -516,17 +550,7 @@ impl McpTool for GetCallersHandler {
|
|||||||
Ok::<Vec<serde_json::Value>, crate::error::AppError>(matching)
|
Ok::<Vec<serde_json::Value>, crate::error::AppError>(matching)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let custom_dir = if let Some(ref d) = req.workspace_dir {
|
let custom_dir = req.workspace_dir.as_ref().map(std::path::PathBuf::from);
|
||||||
Some(std::path::PathBuf::from(d))
|
|
||||||
} else {
|
|
||||||
state.project.pinned_files.read_with(|pinned| {
|
|
||||||
pinned.first().and_then(|pin| {
|
|
||||||
std::path::Path::new(&pin.file_path)
|
|
||||||
.parent()
|
|
||||||
.map(|p| p.to_path_buf())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
if callers.len() < limit {
|
if callers.len() < limit {
|
||||||
let remaining = limit - callers.len();
|
let remaining = limit - callers.len();
|
||||||
@@ -571,13 +595,10 @@ impl McpTool for AnalyzeImpactHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let custom_dir = state.project.pinned_files.read_with(|pinned| {
|
let custom_dir = req
|
||||||
pinned.first().and_then(|pin| {
|
.file_path
|
||||||
std::path::Path::new(&pin.file_path)
|
.as_ref()
|
||||||
.parent()
|
.and_then(|p| std::path::Path::new(p).parent().map(|p| p.to_path_buf()));
|
||||||
.map(|p| p.to_path_buf())
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let sym_clone = sym.clone();
|
let sym_clone = sym.clone();
|
||||||
let disk_refs = tokio::task::spawn_blocking(move || {
|
let disk_refs = tokio::task::spawn_blocking(move || {
|
||||||
|
|||||||
+74
-183
@@ -5,166 +5,6 @@ use async_trait::async_trait;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub struct UpdateEnvFingerprintHandler;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl McpTool for UpdateEnvFingerprintHandler {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"update_env_fingerprint"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schema(&self) -> Value {
|
|
||||||
crate::mcp::tool_def::<UpdateEnvFingerprintTool>(
|
|
||||||
"update_env_fingerprint",
|
|
||||||
"Update system OS, shell, and installed tool version fingerprints for a workspace namespace.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
||||||
let req: UpdateEnvFingerprintTool =
|
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
||||||
state.env.env_fingerprints.modify(|fps| {
|
|
||||||
fps.insert(
|
|
||||||
req.namespace.clone(),
|
|
||||||
crate::models::EnvFingerprint {
|
|
||||||
namespace: req.namespace.clone(),
|
|
||||||
os: std::env::consts::OS.to_string(),
|
|
||||||
shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()),
|
|
||||||
tool_versions: req.tool_versions,
|
|
||||||
updated_at: crate::handlers::utils::now_secs(),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
Ok("Env fingerprint updated".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ReadEnvFingerprintHandler;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl McpTool for ReadEnvFingerprintHandler {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"read_env_fingerprint"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schema(&self) -> Value {
|
|
||||||
crate::mcp::tool_def::<ReadEnvFingerprintTool>(
|
|
||||||
"read_env_fingerprint",
|
|
||||||
"Read system OS, shell, and tool version fingerprints for a workspace namespace.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
||||||
let req: ReadEnvFingerprintTool =
|
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
||||||
let data = state
|
|
||||||
.env
|
|
||||||
.env_fingerprints
|
|
||||||
.read_with(|fps| fps.get(&req.namespace).cloned());
|
|
||||||
if let Some(fp) = data {
|
|
||||||
let data = Ok::<String, crate::error::AppError>(serde_json::to_string(&fp)?)?;
|
|
||||||
Ok(data)
|
|
||||||
} else {
|
|
||||||
Ok("{}".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LogEnvRequirementHandler;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl McpTool for LogEnvRequirementHandler {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"log_env_requirement"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schema(&self) -> Value {
|
|
||||||
crate::mcp::tool_def::<LogEnvRequirementTool>(
|
|
||||||
"log_env_requirement",
|
|
||||||
"Log required environment variables, configuration keys, or secrets for a namespace.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
||||||
let req: LogEnvRequirementTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
||||||
state.env.env_requirements.modify(|reqs| {
|
|
||||||
reqs.retain(|r| !(r.namespace == req.namespace && r.key == req.key));
|
|
||||||
reqs.push(crate::models::EnvRequirement {
|
|
||||||
namespace: req.namespace,
|
|
||||||
key: req.key,
|
|
||||||
description: req.description,
|
|
||||||
is_secret: req.is_secret,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Ok("Env requirement logged".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RegisterEnvironmentHandler;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl McpTool for RegisterEnvironmentHandler {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"register_environment"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schema(&self) -> Value {
|
|
||||||
crate::mcp::tool_def::<RegisterEnvironmentTool>(
|
|
||||||
"register_environment",
|
|
||||||
"Register infrastructure environment details (URL, VPN requirements, notes) for a namespace.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
||||||
let req: RegisterEnvironmentTool =
|
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
||||||
state.env.environments.modify(|envs| {
|
|
||||||
envs.retain(|e| !(e.namespace == req.namespace && e.name == req.name));
|
|
||||||
envs.push(crate::models::EnvironmentDetail {
|
|
||||||
namespace: req.namespace,
|
|
||||||
name: req.name,
|
|
||||||
url: req.url,
|
|
||||||
description: req.description,
|
|
||||||
requires_vpn: req.requires_vpn,
|
|
||||||
updated_at: crate::handlers::utils::now_secs(),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Ok("Environment registered".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct GetEnvironmentDetailsHandler;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl McpTool for GetEnvironmentDetailsHandler {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"get_environment_details"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schema(&self) -> Value {
|
|
||||||
crate::mcp::tool_def::<GetEnvironmentDetailsTool>(
|
|
||||||
"get_environment_details",
|
|
||||||
"Retrieve registered infrastructure environment details for a namespace.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
||||||
let req: GetEnvironmentDetailsTool =
|
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
||||||
let data = state.env.environments.read_with(|envs| {
|
|
||||||
let filtered: Vec<_> = envs
|
|
||||||
.iter()
|
|
||||||
.filter(|e| e.namespace == req.namespace)
|
|
||||||
.collect();
|
|
||||||
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
|
|
||||||
})?;
|
|
||||||
Ok(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EnvironmentHandler;
|
pub struct EnvironmentHandler;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -186,10 +26,32 @@ impl McpTool for EnvironmentHandler {
|
|||||||
match req.action {
|
match req.action {
|
||||||
EnvAction::UpdateFingerprint => {
|
EnvAction::UpdateFingerprint => {
|
||||||
let tool_versions = req.tool_versions.unwrap_or_default();
|
let tool_versions = req.tool_versions.unwrap_or_default();
|
||||||
UpdateEnvFingerprintHandler.execute(serde_json::json!({"namespace": ns, "tool_versions": tool_versions}), state).await
|
state.env.env_fingerprints.modify(|fps| {
|
||||||
|
fps.insert(
|
||||||
|
ns.clone(),
|
||||||
|
crate::models::EnvFingerprint {
|
||||||
|
namespace: ns.clone(),
|
||||||
|
os: std::env::consts::OS.to_string(),
|
||||||
|
shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()),
|
||||||
|
tool_versions,
|
||||||
|
updated_at: crate::handlers::utils::now_secs(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Ok("Env fingerprint updated".to_string())
|
||||||
}
|
}
|
||||||
EnvAction::ReadFingerprint => {
|
EnvAction::ReadFingerprint => {
|
||||||
ReadEnvFingerprintHandler.execute(serde_json::json!({"namespace": ns}), state).await
|
let data = state
|
||||||
|
.env
|
||||||
|
.env_fingerprints
|
||||||
|
.read_with(|fps| fps.get(&ns).cloned());
|
||||||
|
if let Some(fp) = data {
|
||||||
|
let data = Ok::<String, crate::error::AppError>(serde_json::to_string(&fp)?)?;
|
||||||
|
Ok(data)
|
||||||
|
} else {
|
||||||
|
Ok("{}".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
EnvAction::LogRequirement => {
|
EnvAction::LogRequirement => {
|
||||||
let key = req.key.ok_or_else(|| {
|
let key = req.key.ok_or_else(|| {
|
||||||
@@ -197,7 +59,17 @@ impl McpTool for EnvironmentHandler {
|
|||||||
})?;
|
})?;
|
||||||
let desc = req.description.unwrap_or_default();
|
let desc = req.description.unwrap_or_default();
|
||||||
let secret = req.is_secret.unwrap_or(false);
|
let secret = req.is_secret.unwrap_or(false);
|
||||||
LogEnvRequirementHandler.execute(serde_json::json!({"key": key, "description": desc, "is_secret": secret, "namespace": ns}), state).await
|
state.env.env_requirements.modify(|reqs| {
|
||||||
|
reqs.retain(|r| !(r.namespace == ns && r.key == key));
|
||||||
|
reqs.push(crate::models::EnvRequirement {
|
||||||
|
namespace: ns,
|
||||||
|
key,
|
||||||
|
description: desc,
|
||||||
|
is_secret: secret,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Ok("Env requirement logged".to_string())
|
||||||
}
|
}
|
||||||
EnvAction::Register => {
|
EnvAction::Register => {
|
||||||
let name = req.name.ok_or_else(|| {
|
let name = req.name.ok_or_else(|| {
|
||||||
@@ -206,15 +78,35 @@ impl McpTool for EnvironmentHandler {
|
|||||||
let url = req.url.unwrap_or_default();
|
let url = req.url.unwrap_or_default();
|
||||||
let desc = req.description.unwrap_or_default();
|
let desc = req.description.unwrap_or_default();
|
||||||
let vpn = req.requires_vpn.unwrap_or(false);
|
let vpn = req.requires_vpn.unwrap_or(false);
|
||||||
RegisterEnvironmentHandler.execute(serde_json::json!({"name": name, "url": url, "description": desc, "requires_vpn": vpn, "namespace": ns}), state).await
|
state.env.environments.modify(|envs| {
|
||||||
|
envs.retain(|e| !(e.namespace == ns && e.name == name));
|
||||||
|
envs.push(crate::models::EnvironmentDetail {
|
||||||
|
namespace: ns,
|
||||||
|
name,
|
||||||
|
url,
|
||||||
|
description: desc,
|
||||||
|
requires_vpn: vpn,
|
||||||
|
updated_at: crate::handlers::utils::now_secs(),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Ok("Environment registered".to_string())
|
||||||
}
|
}
|
||||||
EnvAction::GetDetails => {
|
EnvAction::GetDetails => {
|
||||||
GetEnvironmentDetailsHandler.execute(serde_json::json!({"namespace": ns}), state).await
|
let data = state.env.environments.read_with(|envs| {
|
||||||
|
let filtered: Vec<_> = envs
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.namespace == ns)
|
||||||
|
.collect();
|
||||||
|
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
|
||||||
|
})?;
|
||||||
|
Ok(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -225,25 +117,25 @@ mod tests {
|
|||||||
async fn test_env_fingerprint() {
|
async fn test_env_fingerprint() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||||
|
let handler = EnvironmentHandler;
|
||||||
|
|
||||||
let update_handler = UpdateEnvFingerprintHandler;
|
|
||||||
let args = json!({
|
let args = json!({
|
||||||
|
"action": "update_fingerprint",
|
||||||
"namespace": "global",
|
"namespace": "global",
|
||||||
"tool_versions": {
|
"tool_versions": {
|
||||||
"rustc": "1.70.0"
|
"rustc": "1.70.0"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let res = update_handler
|
let res = handler
|
||||||
.execute(args, state.clone())
|
.execute(args, state.clone())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res, "Env fingerprint updated");
|
assert_eq!(res, "Env fingerprint updated");
|
||||||
|
|
||||||
let read_handler = ReadEnvFingerprintHandler;
|
let res2 = handler
|
||||||
let res2 = read_handler
|
.execute(json!({"action": "read_fingerprint", "namespace": "global"}), state.clone())
|
||||||
.execute(json!({"namespace": "global"}), state.clone())
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -269,9 +161,9 @@ mod tests {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let handler = GetEnvironmentDetailsHandler;
|
let handler = EnvironmentHandler;
|
||||||
let res = handler
|
let res = handler
|
||||||
.execute(json!({"namespace": "global"}), state.clone())
|
.execute(json!({"action": "get_details", "namespace": "global"}), state.clone())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -283,41 +175,39 @@ mod tests {
|
|||||||
async fn test_env_requirements_and_registration() {
|
async fn test_env_requirements_and_registration() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||||
|
let handler = EnvironmentHandler;
|
||||||
|
|
||||||
let req_handler = LogEnvRequirementHandler;
|
|
||||||
let args_req = json!({
|
let args_req = json!({
|
||||||
|
"action": "log_requirement",
|
||||||
"key": "vpn",
|
"key": "vpn",
|
||||||
"description": "Requires VPN",
|
"description": "Requires VPN",
|
||||||
"is_secret": false,
|
"is_secret": false,
|
||||||
"requirement": "Needs specific VPN",
|
|
||||||
"context": "For database access",
|
|
||||||
"namespace": "global"
|
"namespace": "global"
|
||||||
});
|
});
|
||||||
let res1 = req_handler
|
let res1 = handler
|
||||||
.execute(args_req, state.clone())
|
.execute(args_req, state.clone())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res1, "Env requirement logged");
|
assert_eq!(res1, "Env requirement logged");
|
||||||
|
|
||||||
let reg_handler = RegisterEnvironmentHandler;
|
|
||||||
let args_reg = json!({
|
let args_reg = json!({
|
||||||
|
"action": "register",
|
||||||
"name": "prod",
|
"name": "prod",
|
||||||
"url": "https://prod.local",
|
"url": "https://prod.local",
|
||||||
"description": "Production",
|
"description": "Production",
|
||||||
"requires_vpn": true,
|
"requires_vpn": true,
|
||||||
"namespace": "global"
|
"namespace": "global"
|
||||||
});
|
});
|
||||||
let res2 = reg_handler
|
let res2 = handler
|
||||||
.execute(args_reg, state.clone())
|
.execute(args_reg, state.clone())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(res2, "Environment registered");
|
assert_eq!(res2, "Environment registered");
|
||||||
|
|
||||||
let get_handler = GetEnvironmentDetailsHandler;
|
let res3 = handler
|
||||||
let res3 = get_handler
|
.execute(json!({"action": "get_details", "namespace": "global"}), state.clone())
|
||||||
.execute(json!({"namespace": "global"}), state.clone())
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -329,16 +219,17 @@ mod tests {
|
|||||||
async fn test_empty_namespace_in_update_env_fingerprint() {
|
async fn test_empty_namespace_in_update_env_fingerprint() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||||
|
let handler = EnvironmentHandler;
|
||||||
|
|
||||||
let update_handler = UpdateEnvFingerprintHandler;
|
|
||||||
let args = serde_json::json!({
|
let args = serde_json::json!({
|
||||||
|
"action": "update_fingerprint",
|
||||||
"namespace": "",
|
"namespace": "",
|
||||||
"tool_versions": {
|
"tool_versions": {
|
||||||
"rustc": "1.70.0"
|
"rustc": "1.70.0"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let res = update_handler
|
let res = handler
|
||||||
.execute(args, state.clone())
|
.execute(args, state.clone())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
+103
-76
@@ -459,7 +459,6 @@ impl McpTool for ReadGraphHandler {
|
|||||||
let offset = req.offset.unwrap_or(0);
|
let offset = req.offset.unwrap_or(0);
|
||||||
|
|
||||||
let data = state.read_graph(|full| -> crate::error::Result<String> {
|
let data = state.read_graph(|full| -> crate::error::Result<String> {
|
||||||
let mut result_json = if limit.is_some() || req.offset.is_some() {
|
|
||||||
let filtered_entities: Vec<(&String, &crate::models::Entity)> = full
|
let filtered_entities: Vec<(&String, &crate::models::Entity)> = full
|
||||||
.entities
|
.entities
|
||||||
.iter()
|
.iter()
|
||||||
@@ -467,6 +466,8 @@ impl McpTool for ReadGraphHandler {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let total_count = filtered_entities.len();
|
let total_count = filtered_entities.len();
|
||||||
|
|
||||||
|
let (entities_map, relations_vec, meta_json) = if limit.is_some() || req.offset.is_some() {
|
||||||
let effective_limit = limit.unwrap_or(50);
|
let effective_limit = limit.unwrap_or(50);
|
||||||
let paged_entities: std::collections::HashMap<&String, &crate::models::Entity> = filtered_entities
|
let paged_entities: std::collections::HashMap<&String, &crate::models::Entity> = filtered_entities
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -485,45 +486,94 @@ impl McpTool for ReadGraphHandler {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
serde_json::to_string(&serde_json::json!({
|
let meta = serde_json::json!({
|
||||||
"entities": paged_entities,
|
|
||||||
"relations": relevant_relations,
|
|
||||||
"total_count": total_count,
|
"total_count": total_count,
|
||||||
"offset": offset,
|
"offset": offset,
|
||||||
"limit": effective_limit,
|
"limit": effective_limit,
|
||||||
"has_more": has_more,
|
"has_more": has_more,
|
||||||
}))?
|
});
|
||||||
} else if let Some(ns) = req.namespace {
|
(paged_entities, relevant_relations, Some(meta))
|
||||||
let mut filtered = BorrowedGraph::default();
|
|
||||||
for (k, v) in &full.entities {
|
|
||||||
if v.namespace == ns {
|
|
||||||
filtered.entities.insert(k, v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for r in &full.relations {
|
|
||||||
if r.namespace == ns {
|
|
||||||
filtered.relations.push(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serde_json::to_string(&filtered)?
|
|
||||||
} else {
|
} else {
|
||||||
serde_json::to_string(full)?
|
let ent_map: std::collections::HashMap<&String, &crate::models::Entity> =
|
||||||
|
filtered_entities.into_iter().collect();
|
||||||
|
let rel_vec: Vec<&crate::models::Relation> = full
|
||||||
|
.relations
|
||||||
|
.iter()
|
||||||
|
.filter(|r| req.namespace.as_ref().map_or(true, |ns| &r.namespace == ns))
|
||||||
|
.collect();
|
||||||
|
(ent_map, rel_vec, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let serialized = if let Some(meta) = &meta_json {
|
||||||
|
serde_json::json!({
|
||||||
|
"entities": entities_map,
|
||||||
|
"relations": relations_vec,
|
||||||
|
"total_count": meta["total_count"],
|
||||||
|
"offset": meta["offset"],
|
||||||
|
"limit": meta["limit"],
|
||||||
|
"has_more": meta["has_more"],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
serde_json::json!({
|
||||||
|
"entities": entities_map,
|
||||||
|
"relations": relations_vec,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut result_json = serde_json::to_string(&serialized)?;
|
||||||
|
|
||||||
if let Some(max_tok) = max_tokens {
|
if let Some(max_tok) = max_tokens {
|
||||||
let max_chars = max_tok * 4;
|
let max_chars = max_tok * 4;
|
||||||
if result_json.len() > max_chars {
|
if result_json.len() > max_chars {
|
||||||
let valid_boundary = result_json.floor_char_boundary(max_chars);
|
let total_ent = entities_map.len();
|
||||||
result_json.truncate(valid_boundary);
|
let total_rel = relations_vec.len();
|
||||||
result_json.push_str("\n... [TRUNCATED_TO_MAX_TOKENS. Use search_nodes, pagination (limit, offset), or get_subgraph for targeted discovery]");
|
let frac = (max_chars as f64) / (result_json.len() as f64);
|
||||||
|
let mut keep_count = (total_ent as f64 * frac * 0.85).max(1.0) as usize;
|
||||||
|
|
||||||
|
let ent_vec: Vec<_> = entities_map.into_iter().collect();
|
||||||
|
|
||||||
|
while keep_count > 0 {
|
||||||
|
let sliced_ent: std::collections::HashMap<_, _> = ent_vec.iter().take(keep_count).cloned().collect();
|
||||||
|
let sliced_rel: Vec<_> = relations_vec.iter().filter(|r| sliced_ent.contains_key(&r.from) && sliced_ent.contains_key(&r.to)).copied().collect();
|
||||||
|
|
||||||
|
let mut truncated_val = serde_json::json!({
|
||||||
|
"entities": sliced_ent,
|
||||||
|
"relations": sliced_rel,
|
||||||
|
"_meta": {
|
||||||
|
"truncated": true,
|
||||||
|
"total_entities": total_ent,
|
||||||
|
"returned_entities": sliced_ent.len(),
|
||||||
|
"total_relations": total_rel,
|
||||||
|
"returned_relations": sliced_rel.len(),
|
||||||
|
"note": "Output structurally truncated to fit within max_tokens. Use pagination (limit, offset), search_nodes, or get_subgraph for targeted discovery."
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(meta) = &meta_json {
|
||||||
|
if let Some(obj) = truncated_val.as_object_mut() {
|
||||||
|
obj.insert("total_count".to_string(), meta["total_count"].clone());
|
||||||
|
obj.insert("offset".to_string(), meta["offset"].clone());
|
||||||
|
obj.insert("limit".to_string(), meta["limit"].clone());
|
||||||
|
obj.insert("has_more".to_string(), serde_json::Value::Bool(true));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Ok(trunc_str) = serde_json::to_string(&truncated_val) {
|
||||||
|
if trunc_str.len() <= max_chars || keep_count == 1 {
|
||||||
|
result_json = trunc_str;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keep_count = keep_count.saturating_sub(keep_count / 3 + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(result_json)
|
Ok(result_json)
|
||||||
})?;
|
})?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SearchNodesHandler;
|
pub struct SearchNodesHandler;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -910,36 +960,49 @@ impl McpTool for GetSubgraphHandler {
|
|||||||
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);
|
||||||
|
|
||||||
if format == SubgraphFormat::MarkdownTree {
|
let (matched_entities, matched_relations) = state.read_graph(|full| {
|
||||||
let (sub_entities, sub_relations) = state.read_graph(|g| {
|
let mut adj: std::collections::HashMap<&str, Vec<&str>> =
|
||||||
|
std::collections::HashMap::with_capacity(full.relations.len() * 2);
|
||||||
|
for r in &full.relations {
|
||||||
|
adj.entry(r.from.as_str()).or_default().push(r.to.as_str());
|
||||||
|
adj.entry(r.to.as_str()).or_default().push(r.from.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
let mut visited = std::collections::HashSet::new();
|
let mut visited = std::collections::HashSet::new();
|
||||||
let mut queue = std::collections::VecDeque::new();
|
let mut queue = std::collections::VecDeque::new();
|
||||||
queue.push_back((root.clone(), 0));
|
queue.push_back((root.as_str(), 0));
|
||||||
visited.insert(root.clone());
|
visited.insert(root.clone());
|
||||||
|
|
||||||
while let Some((curr, d)) = queue.pop_front() {
|
while let Some((curr, d)) = queue.pop_front() {
|
||||||
if d >= depth {
|
if d < depth {
|
||||||
continue;
|
if let Some(neighbors) = adj.get(curr) {
|
||||||
|
for &next in neighbors {
|
||||||
|
if visited.insert(next.to_string()) {
|
||||||
|
queue.push_back((next, d + 1));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for r in &g.relations {
|
|
||||||
if r.from == curr && !visited.contains(&r.to) {
|
|
||||||
visited.insert(r.to.clone());
|
|
||||||
queue.push_back((r.to.clone(), d + 1));
|
|
||||||
} else if r.to == curr && !visited.contains(&r.from) {
|
|
||||||
visited.insert(r.from.clone());
|
|
||||||
queue.push_back((r.from.clone(), d + 1));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ent: Vec<_> = g.entities.values().filter(|e| visited.contains(&e.name)).cloned().collect();
|
let ent: std::collections::HashMap<String, crate::models::Entity> = visited
|
||||||
let rel: Vec<_> = g.relations.iter().filter(|r| visited.contains(&r.from) && visited.contains(&r.to)).cloned().collect();
|
.iter()
|
||||||
|
.filter_map(|name| full.entities.get(name).map(|e| (name.clone(), e.clone())))
|
||||||
|
.collect();
|
||||||
|
let rel: Vec<crate::models::Relation> = full
|
||||||
|
.relations
|
||||||
|
.iter()
|
||||||
|
.filter(|r| visited.contains(&r.from) && visited.contains(&r.to))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
(ent, rel)
|
(ent, rel)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if format == SubgraphFormat::MarkdownTree {
|
||||||
let mut markdown = format!("# Subgraph Topology for `{}`\n\n", root);
|
let mut markdown = format!("# Subgraph Topology for `{}`\n\n", root);
|
||||||
markdown.push_str("## Entities\n");
|
markdown.push_str("## Entities\n");
|
||||||
for e in &sub_entities {
|
for e in matched_entities.values() {
|
||||||
markdown.push_str(&format!("- **{}** (`{}`)\n", e.name, e.entity_type));
|
markdown.push_str(&format!("- **{}** (`{}`)\n", e.name, e.entity_type));
|
||||||
for obs in &e.observations {
|
for obs in &e.observations {
|
||||||
markdown.push_str(&format!(" - {}\n", obs));
|
markdown.push_str(&format!(" - {}\n", obs));
|
||||||
@@ -947,45 +1010,12 @@ impl McpTool for GetSubgraphHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
markdown.push_str("\n## Relations\n");
|
markdown.push_str("\n## Relations\n");
|
||||||
for r in &sub_relations {
|
for r in &matched_relations {
|
||||||
markdown.push_str(&format!("- `{}` --[{}]--> `{}`\n", r.from, r.relation_type, r.to));
|
markdown.push_str(&format!("- `{}` --[{}]--> `{}`\n", r.from, r.relation_type, r.to));
|
||||||
}
|
}
|
||||||
return Ok(markdown);
|
return Ok(markdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
let data = state.read_graph(|full| {
|
|
||||||
let mut visited_nodes: std::collections::HashSet<String> = std::collections::HashSet::new();
|
|
||||||
let mut matched_entities = std::collections::HashMap::new();
|
|
||||||
let mut matched_relations = Vec::new();
|
|
||||||
|
|
||||||
let mut queue = std::collections::VecDeque::new();
|
|
||||||
queue.push_back((root.clone(), 0));
|
|
||||||
visited_nodes.insert(root.clone());
|
|
||||||
|
|
||||||
while let Some((curr, current_depth)) = queue.pop_front() {
|
|
||||||
if let Some(entity) = full.entities.get(&curr) {
|
|
||||||
matched_entities.insert(curr.clone(), entity.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_depth < depth {
|
|
||||||
for rel in &full.relations {
|
|
||||||
if rel.from == curr {
|
|
||||||
matched_relations.push(rel.clone());
|
|
||||||
if !visited_nodes.contains(&rel.to) {
|
|
||||||
visited_nodes.insert(rel.to.clone());
|
|
||||||
queue.push_back((rel.to.clone(), current_depth + 1));
|
|
||||||
}
|
|
||||||
} else if rel.to == curr {
|
|
||||||
matched_relations.push(rel.clone());
|
|
||||||
if !visited_nodes.contains(&rel.from) {
|
|
||||||
visited_nodes.insert(rel.from.clone());
|
|
||||||
queue.push_back((rel.from.clone(), current_depth + 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = serde_json::json!({
|
let result = serde_json::json!({
|
||||||
"root_node": root,
|
"root_node": root,
|
||||||
"depth": depth,
|
"depth": depth,
|
||||||
@@ -993,10 +1023,7 @@ impl McpTool for GetSubgraphHandler {
|
|||||||
"relations": matched_relations,
|
"relations": matched_relations,
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&result)?)
|
Ok(serde_json::to_string_pretty(&result)?)
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+252
-794
File diff suppressed because it is too large.
Load diff
@@ -7,100 +7,6 @@ use serde_json::Value;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub struct StickyNotesHandler;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl McpTool for StickyNotesHandler {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"sticky_notes"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schema(&self) -> Value {
|
|
||||||
crate::mcp::tool_def::<StickyNotesTool>(
|
|
||||||
"sticky_notes",
|
|
||||||
"Ephemeral sticky notes with TTL (add, read, delete, clear)",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
||||||
let req: StickyNotesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
||||||
match req.action {
|
|
||||||
StickyNoteAction::Add => {
|
|
||||||
let content = req.content.ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal("Missing required parameter 'content' for action 'add'. Next step: Provide non-empty 'content' string in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let now = crate::handlers::utils::now_secs();
|
|
||||||
let expires_at = if let Some(ttl) = req.ttl_seconds {
|
|
||||||
Some(now + ttl)
|
|
||||||
} else if req.session_only.unwrap_or(false) {
|
|
||||||
Some(now + 14400)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
state.code.sticky.modify(|notes| {
|
|
||||||
notes.push(StickyNote {
|
|
||||||
timestamp: now,
|
|
||||||
content,
|
|
||||||
expires_at,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Ok("Sticky note added.".to_string())
|
|
||||||
}
|
|
||||||
StickyNoteAction::Read => {
|
|
||||||
let now = crate::handlers::utils::now_secs();
|
|
||||||
let has_expired = state.code.sticky.read_with(|notes| {
|
|
||||||
notes.iter().any(|n| matches!(n.expires_at, Some(exp) if exp <= now))
|
|
||||||
});
|
|
||||||
if has_expired {
|
|
||||||
state.code.sticky.modify(|notes| {
|
|
||||||
notes.retain(|n| {
|
|
||||||
if let Some(exp) = n.expires_at {
|
|
||||||
exp > now
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let active_notes = state.code.sticky.read_with(|notes| notes.clone());
|
|
||||||
Ok(serde_json::to_string(&active_notes)?)
|
|
||||||
}
|
|
||||||
StickyNoteAction::Delete => {
|
|
||||||
let idx = req.index.ok_or_else(|| {
|
|
||||||
crate::error::AppError::Internal("Missing required parameter 'index' for action 'delete'. Next step: Provide 1-based sticky note 'index' integer in request and retry.".to_string())
|
|
||||||
})?;
|
|
||||||
let mut success = false;
|
|
||||||
state.code.sticky.modify(|notes| {
|
|
||||||
if idx > 0 && idx <= notes.len() {
|
|
||||||
notes.remove(idx - 1);
|
|
||||||
success = true;
|
|
||||||
} else if idx == 0 && !notes.is_empty() {
|
|
||||||
notes.remove(0);
|
|
||||||
success = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if success {
|
|
||||||
Ok("Sticky note deleted.".to_string())
|
|
||||||
} else {
|
|
||||||
Err(crate::error::AppError::Internal(
|
|
||||||
"Invalid sticky note index. Next step: Call sticky_notes with action='read' to view active notes and valid indices, then retry.".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StickyNoteAction::Clear => {
|
|
||||||
state.code.sticky.modify(|notes| {
|
|
||||||
notes.clear();
|
|
||||||
});
|
|
||||||
Ok("All sticky notes cleared.".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub struct HandoffMemosHandler;
|
pub struct HandoffMemosHandler;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -255,7 +161,7 @@ impl McpTool for PromoteToEntityHandler {
|
|||||||
fn schema(&self) -> Value {
|
fn schema(&self) -> Value {
|
||||||
crate::mcp::tool_def::<PromoteToEntityTool>(
|
crate::mcp::tool_def::<PromoteToEntityTool>(
|
||||||
"promote_to_entity",
|
"promote_to_entity",
|
||||||
"Promote a transient sticky note or active task observation into a permanent Knowledge Graph entity.",
|
"Promote a task observation or finding into a permanent Knowledge Graph entity.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,10 +191,6 @@ impl McpTool for PromoteToEntityHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
state.code.sticky.modify(|notes| {
|
|
||||||
notes.retain(|n| !n.content.contains(&obs));
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
"Promoted transient note into permanent entity '{}'.",
|
"Promoted transient note into permanent entity '{}'.",
|
||||||
req.entity_name
|
req.entity_name
|
||||||
@@ -302,47 +204,6 @@ mod tests {
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_notes_lifecycle() {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
|
||||||
|
|
||||||
let handler = StickyNotesHandler;
|
|
||||||
let args = json!({
|
|
||||||
"action": "add",
|
|
||||||
"content": "Buy milk",
|
|
||||||
});
|
|
||||||
|
|
||||||
let res = handler
|
|
||||||
.execute(args, state.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
||||||
.unwrap();
|
|
||||||
assert!(res.contains("Sticky note added"));
|
|
||||||
|
|
||||||
let res2 = handler
|
|
||||||
.execute(json!({"action": "read"}), state.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
||||||
.unwrap();
|
|
||||||
assert!(res2.contains("Buy milk"));
|
|
||||||
|
|
||||||
let args2 = json!({"action": "delete", "index": 1});
|
|
||||||
let res3 = handler
|
|
||||||
.execute(args2, state.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(res3, "Sticky note deleted.");
|
|
||||||
|
|
||||||
let res4 = handler
|
|
||||||
.execute(json!({"action": "read"}), state.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
|
||||||
.unwrap();
|
|
||||||
assert!(!res4.contains("Buy milk"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_handoff_and_summaries() {
|
async fn test_handoff_and_summaries() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
@@ -392,22 +253,6 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!res4.is_empty());
|
assert!(!res4.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_invalid_sticky_note_action() {
|
|
||||||
let dir = tempfile::tempdir().unwrap();
|
|
||||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
|
||||||
|
|
||||||
let handler = StickyNotesHandler;
|
|
||||||
let invalid_args = serde_json::json!({
|
|
||||||
"action": "invalid_action"
|
|
||||||
});
|
|
||||||
|
|
||||||
let res = handler
|
|
||||||
.execute(invalid_args, state.clone())
|
|
||||||
.await;
|
|
||||||
assert!(res.is_err());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ impl McpTool for TasksHandler {
|
|||||||
crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide task 'id' string in request and retry.".to_string())
|
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(|| {
|
let status = req.status.ok_or_else(|| {
|
||||||
crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide valid 'status' ('pending', 'completed', or 'cancelled') in request and retry.".to_string())
|
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;
|
||||||
@@ -113,6 +113,7 @@ impl McpTool for TasksHandler {
|
|||||||
if let Some(dep_task) = tasks.iter().find(|dt| dt.id == *dep_id)
|
if let Some(dep_task) = tasks.iter().find(|dt| dt.id == *dep_id)
|
||||||
&& dep_task.status != "completed"
|
&& dep_task.status != "completed"
|
||||||
&& dep_task.status != "done"
|
&& dep_task.status != "done"
|
||||||
|
&& dep_task.status != "cancelled"
|
||||||
{
|
{
|
||||||
uncompleted_deps.push(dep_task.title.as_str());
|
uncompleted_deps.push(dep_task.title.as_str());
|
||||||
}
|
}
|
||||||
@@ -132,7 +133,10 @@ impl McpTool for TasksHandler {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|t| t.parent_id.as_deref() == Some(target_id_ref))
|
.filter(|t| t.parent_id.as_deref() == Some(target_id_ref))
|
||||||
{
|
{
|
||||||
if child.status != "completed" && child.status != "done" {
|
if child.status != "completed"
|
||||||
|
&& child.status != "done"
|
||||||
|
&& child.status != "cancelled"
|
||||||
|
{
|
||||||
uncompleted_children.push(child.title.as_str());
|
uncompleted_children.push(child.title.as_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -303,6 +307,7 @@ impl McpTool for TasksHandler {
|
|||||||
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())
|
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;
|
||||||
state.project.tasks.modify(|tasks| {
|
state.project.tasks.modify(|tasks| {
|
||||||
if let Some(task) = tasks.iter_mut().find(|t| t.id == id || t.title == id) {
|
if let Some(task) = tasks.iter_mut().find(|t| t.id == id || t.title == id) {
|
||||||
task.acceptance_criteria = criteria_list
|
task.acceptance_criteria = criteria_list
|
||||||
@@ -315,10 +320,15 @@ impl McpTool for TasksHandler {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
task.updated_at = crate::handlers::utils::now_secs();
|
task.updated_at = crate::handlers::utils::now_secs();
|
||||||
|
updated_task = Some(task.clone());
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if success {
|
if success {
|
||||||
|
if let Some(ref ut) = updated_task {
|
||||||
|
let idx = state.get_search_index().await;
|
||||||
|
drop(idx.index_task(ut));
|
||||||
|
}
|
||||||
state.broadcast_task_event(TaskEvent {
|
state.broadcast_task_event(TaskEvent {
|
||||||
task_id: id.clone(),
|
task_id: id.clone(),
|
||||||
status: "criteria_set".to_string(),
|
status: "criteria_set".to_string(),
|
||||||
@@ -343,6 +353,7 @@ impl McpTool for TasksHandler {
|
|||||||
let proof_str = req.proof.unwrap_or_else(|| "Verified".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;
|
||||||
state.project.tasks.modify(|tasks| {
|
state.project.tasks.modify(|tasks| {
|
||||||
if let Some(task) = tasks.iter_mut().find(|t| t.id == id || t.title == id) {
|
if let Some(task) = tasks.iter_mut().find(|t| t.id == id || t.title == id) {
|
||||||
if let Some(ac) = task.acceptance_criteria.iter_mut().next() {
|
if let Some(ac) = task.acceptance_criteria.iter_mut().next() {
|
||||||
@@ -352,6 +363,7 @@ impl McpTool for TasksHandler {
|
|||||||
ac.is_met = true;
|
ac.is_met = true;
|
||||||
success = true;
|
success = true;
|
||||||
task.updated_at = crate::handlers::utils::now_secs();
|
task.updated_at = crate::handlers::utils::now_secs();
|
||||||
|
updated_task = Some(task.clone());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
task.acceptance_criteria
|
task.acceptance_criteria
|
||||||
@@ -362,11 +374,16 @@ impl McpTool for TasksHandler {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
task.updated_at = crate::handlers::utils::now_secs();
|
task.updated_at = crate::handlers::utils::now_secs();
|
||||||
|
updated_task = Some(task.clone());
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if success {
|
if success {
|
||||||
|
if let Some(ref ut) = updated_task {
|
||||||
|
let idx = state.get_search_index().await;
|
||||||
|
drop(idx.index_task(ut));
|
||||||
|
}
|
||||||
state.broadcast_task_event(TaskEvent {
|
state.broadcast_task_event(TaskEvent {
|
||||||
task_id: id.clone(),
|
task_id: id.clone(),
|
||||||
status: "verified".to_string(),
|
status: "verified".to_string(),
|
||||||
|
|||||||
+169
-181
@@ -1,6 +1,6 @@
|
|||||||
use crate::router::McpTool;
|
use crate::router::McpTool;
|
||||||
use crate::state::MemoryState;
|
use crate::state::MemoryState;
|
||||||
use crate::tools::{ReadClipboardTool, WriteClipboardTool};
|
use crate::tools::{ClipboardAction, ClipboardTool};
|
||||||
use arboard::{Clipboard, ImageData};
|
use arboard::{Clipboard, ImageData};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use image::{ImageBuffer, imageops::FilterType};
|
use image::{ImageBuffer, imageops::FilterType};
|
||||||
@@ -8,29 +8,181 @@ use serde_json::{Value, json};
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub struct WriteClipboardHandler;
|
|
||||||
|
pub fn get_native_clipboard_text() -> Option<String> {
|
||||||
|
for _ in 0..3 {
|
||||||
|
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||||
|
if let Ok(text) = clipboard.get_text() {
|
||||||
|
if !text.trim().is_empty() {
|
||||||
|
return Some(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn get_windows_png_clipboard_image() -> Option<image::DynamicImage> {
|
||||||
|
use windows_sys::Win32::System::DataExchange::{
|
||||||
|
CloseClipboard, GetClipboardData, IsClipboardFormatAvailable, OpenClipboard,
|
||||||
|
RegisterClipboardFormatW,
|
||||||
|
};
|
||||||
|
use windows_sys::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock};
|
||||||
|
let format_name: Vec<u16> = "PNG\0".encode_utf16().collect();
|
||||||
|
unsafe {
|
||||||
|
let format_id = RegisterClipboardFormatW(format_name.as_ptr());
|
||||||
|
if format_id == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
for _ in 0..3 {
|
||||||
|
if OpenClipboard(std::ptr::null_mut()) != 0 {
|
||||||
|
let has_format = IsClipboardFormatAvailable(format_id) != 0;
|
||||||
|
let mut img_opt = None;
|
||||||
|
if has_format {
|
||||||
|
let handle = GetClipboardData(format_id);
|
||||||
|
if !handle.is_null() {
|
||||||
|
let size = GlobalSize(handle as _);
|
||||||
|
if size > 0 {
|
||||||
|
let ptr = GlobalLock(handle as _);
|
||||||
|
if !ptr.is_null() {
|
||||||
|
let slice = std::slice::from_raw_parts(ptr as *const u8, size);
|
||||||
|
img_opt = image::load_from_memory(slice).ok();
|
||||||
|
GlobalUnlock(handle as _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CloseClipboard();
|
||||||
|
if img_opt.is_some() {
|
||||||
|
return img_opt;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
pub fn get_native_clipboard_image() -> Option<image::DynamicImage> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if let Some(img) = get_windows_png_clipboard_image() {
|
||||||
|
return Some(img);
|
||||||
|
}
|
||||||
|
for _ in 0..3 {
|
||||||
|
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||||
|
if let Ok(image_data) = clipboard.get_image() {
|
||||||
|
if let Some(img) = ImageBuffer::<image::Rgba<u8>, _>::from_raw(
|
||||||
|
image_data.width as u32,
|
||||||
|
image_data.height as u32,
|
||||||
|
image_data.bytes.into_owned(),
|
||||||
|
) {
|
||||||
|
return Some(image::DynamicImage::ImageRgba8(img));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ClipboardHandler;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl McpTool for WriteClipboardHandler {
|
impl McpTool for ClipboardHandler {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"write_clipboard"
|
"clipboard"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schema(&self) -> Value {
|
fn schema(&self) -> Value {
|
||||||
crate::mcp::tool_def::<WriteClipboardTool>(
|
crate::mcp::tool_def::<ClipboardTool>(
|
||||||
"write_clipboard",
|
"clipboard",
|
||||||
"Write text, HTML, files, and/or images directly to the OS clipboard.",
|
"Consolidated OS clipboard management (read, write)",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 tool_args: WriteClipboardTool = serde_json::from_value(args)
|
let req: ClipboardTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
.map_err(|e| crate::error::AppError::Internal(format!("Invalid args: {}", e)))?;
|
match req.action {
|
||||||
|
ClipboardAction::Read => {
|
||||||
|
let result =
|
||||||
|
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::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;
|
||||||
|
let max_dim = 1024;
|
||||||
|
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());
|
||||||
|
if rgb_img.write_to(&mut jpeg_bytes, image::ImageFormat::Jpeg).is_ok() {
|
||||||
|
let bytes = jpeg_bytes.into_inner();
|
||||||
|
use base64::Engine;
|
||||||
|
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
|
||||||
|
out.insert("image_base64".into(), json!(b64));
|
||||||
|
|
||||||
|
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.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));
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
if let Some(rest) = path_str.strip_prefix("C:\\") {
|
||||||
|
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
|
||||||
|
out.insert("image_path_wsl".into(), json!(wsl_path));
|
||||||
|
} else if let Some(rest) = path_str.strip_prefix("c:\\") {
|
||||||
|
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
|
||||||
|
out.insert("image_path_wsl".into(), json!(wsl_path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Value::Object(out))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||||
|
|
||||||
|
let mut final_obj = result;
|
||||||
|
if let Some(b64) = final_obj.get("image_base64").and_then(|v| v.as_str()) {
|
||||||
|
let b64_str = b64.to_string();
|
||||||
|
if let Some(obj) = final_obj.as_object_mut() {
|
||||||
|
obj.remove("image_base64");
|
||||||
|
}
|
||||||
|
if state.ollama.is_available().await
|
||||||
|
&& let Ok(analysis) = state
|
||||||
|
.ollama
|
||||||
|
.generate_vision(
|
||||||
|
"Describe the content, text, or UI layout of this image concisely:",
|
||||||
|
&b64_str,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
&& let Some(obj) = final_obj.as_object_mut()
|
||||||
|
{
|
||||||
|
obj.insert("image_analysis".to_string(), json!(analysis.trim()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.record_activity("clipboard", "Read contents from OS clipboard", None);
|
||||||
|
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&final_obj)?)
|
||||||
|
}
|
||||||
|
ClipboardAction::Write => {
|
||||||
|
let text_opt = req.text;
|
||||||
|
let image_path_opt = req.image_path;
|
||||||
let res = tokio::task::spawn_blocking(move || {
|
let res = tokio::task::spawn_blocking(move || {
|
||||||
let mut msgs = Vec::new();
|
let mut msgs = Vec::new();
|
||||||
|
|
||||||
if let Some(text) = &tool_args.text {
|
if let Some(text) = &text_opt {
|
||||||
let mut written = false;
|
let mut written = false;
|
||||||
if let Ok(mut clipboard) = Clipboard::new() {
|
if let Ok(mut clipboard) = Clipboard::new() {
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
@@ -46,8 +198,7 @@ impl McpTool for WriteClipboardHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle arboard for image
|
if let Some(image_path) = &image_path_opt {
|
||||||
if let Some(image_path) = &tool_args.image_path {
|
|
||||||
match image::open(image_path) {
|
match image::open(image_path) {
|
||||||
Ok(img) => {
|
Ok(img) => {
|
||||||
let img = if img.width() > 2048 || img.height() > 2048 {
|
let img = if img.width() > 2048 || img.height() > 2048 {
|
||||||
@@ -106,173 +257,9 @@ impl McpTool for WriteClipboardHandler {
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_native_clipboard_text() -> Option<String> {
|
|
||||||
for _ in 0..3 {
|
|
||||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
|
||||||
if let Ok(text) = clipboard.get_text() {
|
|
||||||
if !text.trim().is_empty() {
|
|
||||||
return Some(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_native_clipboard_image() -> Option<image::DynamicImage> {
|
|
||||||
for _ in 0..3 {
|
|
||||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
|
||||||
if let Ok(image_data) = clipboard.get_image() {
|
|
||||||
if let Some(img) = ImageBuffer::<image::Rgba<u8>, _>::from_raw(
|
|
||||||
image_data.width as u32,
|
|
||||||
image_data.height as u32,
|
|
||||||
image_data.bytes.into_owned(),
|
|
||||||
) {
|
|
||||||
return Some(image::DynamicImage::ImageRgba8(img));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ReadClipboardHandler;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl McpTool for ReadClipboardHandler {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"read_clipboard"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schema(&self) -> Value {
|
|
||||||
crate::mcp::tool_def::<ReadClipboardTool>(
|
|
||||||
"read_clipboard",
|
|
||||||
"Read the current OS clipboard. Extracts text, HTML, file paths, and/or an image if available.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
||||||
let result =
|
|
||||||
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::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;
|
|
||||||
let max_dim = 1024;
|
|
||||||
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 cache_dir = dirs::home_dir()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.join(".gemini/mcp_memory/clipboard");
|
|
||||||
let _ = std::fs::create_dir_all(&cache_dir);
|
|
||||||
|
|
||||||
let timestamp = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_secs();
|
|
||||||
let file_path = cache_dir.join(format!("clipboard_{}.jpg", timestamp));
|
|
||||||
|
|
||||||
if rgb_img
|
|
||||||
.save_with_format(&file_path, image::ImageFormat::Jpeg)
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
let path_str = file_path.to_string_lossy().to_string();
|
|
||||||
out.insert("image_path".into(), json!(path_str));
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
if let Some(rest) = path_str.strip_prefix("C:\\") {
|
|
||||||
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
|
|
||||||
out.insert("image_path_wsl".into(), json!(wsl_path));
|
|
||||||
} else if let Some(rest) = path_str.strip_prefix("c:\\") {
|
|
||||||
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
|
|
||||||
out.insert("image_path_wsl".into(), json!(wsl_path));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(bytes) = std::fs::read(&file_path) {
|
|
||||||
use base64::Engine;
|
|
||||||
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
|
|
||||||
out.insert("image_base64".into(), json!(b64));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Value::Object(out))
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
|
||||||
|
|
||||||
let mut final_obj = result;
|
|
||||||
if let Some(b64) = final_obj.get("image_base64").and_then(|v| v.as_str()) {
|
|
||||||
let b64_str = b64.to_string();
|
|
||||||
if let Some(obj) = final_obj.as_object_mut() {
|
|
||||||
obj.remove("image_base64");
|
|
||||||
}
|
|
||||||
if state.ollama.is_available().await
|
|
||||||
&& let Ok(analysis) = state
|
|
||||||
.ollama
|
|
||||||
.generate_vision(
|
|
||||||
"Describe the content, text, or UI layout of this image concisely:",
|
|
||||||
&b64_str,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
&& let Some(obj) = final_obj.as_object_mut()
|
|
||||||
{
|
|
||||||
obj.insert("image_analysis".to_string(), json!(analysis.trim()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.record_activity("clipboard", "Read contents from OS clipboard", None);
|
|
||||||
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&final_obj)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
use crate::tools::{ClipboardAction, ClipboardTool};
|
|
||||||
|
|
||||||
pub struct ClipboardHandler;
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl McpTool for ClipboardHandler {
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"clipboard"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn schema(&self) -> Value {
|
|
||||||
crate::mcp::tool_def::<ClipboardTool>(
|
|
||||||
"clipboard",
|
|
||||||
"Consolidated OS clipboard management (read, write)",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
||||||
let req: ClipboardTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
||||||
match req.action {
|
|
||||||
ClipboardAction::Read => {
|
|
||||||
ReadClipboardHandler.execute(serde_json::json!({}), state).await
|
|
||||||
}
|
|
||||||
ClipboardAction::Write => {
|
|
||||||
WriteClipboardHandler.execute(
|
|
||||||
serde_json::json!({
|
|
||||||
"text": req.text,
|
|
||||||
"html": req.html,
|
|
||||||
"files": req.files,
|
|
||||||
"image_path": req.image_path
|
|
||||||
}),
|
|
||||||
state,
|
|
||||||
).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -286,9 +273,10 @@ mod tests {
|
|||||||
async fn test_write_clipboard() {
|
async fn test_write_clipboard() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||||
let handler = WriteClipboardHandler;
|
let handler = ClipboardHandler;
|
||||||
|
|
||||||
let args = json!({
|
let args = json!({
|
||||||
|
"action": "write",
|
||||||
"text": "test_text"
|
"text": "test_text"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -310,10 +298,10 @@ mod tests {
|
|||||||
async fn test_read_clipboard() {
|
async fn test_read_clipboard() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||||
let handler = ReadClipboardHandler;
|
let handler = ClipboardHandler;
|
||||||
|
|
||||||
let result = handler
|
let result = handler
|
||||||
.execute(json!({}), state)
|
.execute(json!({"action": "read"}), state)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to read clipboard: {}", e))
|
.map_err(|e| format!("Failed to read clipboard: {}", e))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -327,10 +315,10 @@ mod tests {
|
|||||||
async fn test_read_clipboard_empty() {
|
async fn test_read_clipboard_empty() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||||
let handler = ReadClipboardHandler;
|
let handler = ClipboardHandler;
|
||||||
|
|
||||||
let result = handler
|
let result = handler
|
||||||
.execute(serde_json::json!({}), state)
|
.execute(serde_json::json!({"action": "read"}), state)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to read clipboard: {}", e))
|
.map_err(|e| format!("Failed to read clipboard: {}", e))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
+258
-1187
File diff suppressed because it is too large.
Load diff
@@ -36,34 +36,11 @@ The server consolidates granular single-purpose tools into domain-named smart to
|
|||||||
- `action: "update"`: Update milestone status (requires `id`, `status: "active" | "completed" | "cancelled"`).
|
- `action: "update"`: Update milestone status (requires `id`, `status: "active" | "completed" | "cancelled"`).
|
||||||
- `action: "list"`: List milestones (optional `namespace`).
|
- `action: "list"`: List milestones (optional `namespace`).
|
||||||
|
|
||||||
* **`sticky_notes`**: Ephemeral scratchpad notes with TTL.
|
|
||||||
- `action: "add"`: Add note (requires `content`, optional `ttl_seconds`, `session_only`).
|
|
||||||
- `action: "read"`: Read all active notes.
|
|
||||||
- `action: "delete"`: Delete note by index (requires 1-based `index`).
|
|
||||||
- `action: "clear"`: Clear all sticky notes.
|
|
||||||
|
|
||||||
* **`handoff_memos`**: Session handoff notes for future agents.
|
* **`handoff_memos`**: Session handoff notes for future agents.
|
||||||
- `action: "leave"`: Leave a memo (requires `content`, optional `vcs_revision`, `repo_name`, `git_branch`, `blockers: Vec<String>`, `action_items: Vec<String>`, `expires_at`).
|
- `action: "leave"`: Leave a memo (requires `content`, optional `vcs_revision`, `repo_name`, `git_branch`, `blockers: Vec<String>`, `action_items: Vec<String>`, `expires_at`).
|
||||||
- `action: "read"`: Read active handoff memos.
|
- `action: "read"`: Read active handoff memos.
|
||||||
- `action: "clear"`: Clear memos.
|
- `action: "clear"`: Clear memos.
|
||||||
|
|
||||||
* **`pinned_files`**: Focus file working set.
|
|
||||||
- `action: "pin"`: Pin file to focus set (requires `path`, optional `pin_reason`, `symbol_or_line`, `repo_name`, `git_branch`).
|
|
||||||
- `action: "unpin"`: Unpin file from focus set (requires `path`).
|
|
||||||
- `action: "list"`: List pinned files (optional `namespace`).
|
|
||||||
|
|
||||||
* **`context_workspaces`**: Workspace context state snapshots.
|
|
||||||
- `action: "save"`: Save context workspace (requires `name`, optional `pinned_files`, `active_task_ids`, `description`, `git_branch`, `vcs_revision`, `repo_name`).
|
|
||||||
- `action: "load"`: Restore saved context workspace (requires `name`).
|
|
||||||
- `action: "list"`: List saved context workspaces.
|
|
||||||
- `action: "delete"`: Delete saved context workspace (requires `name`).
|
|
||||||
- `action: "diff"`: Compare two saved context workspaces (requires `name`, `other_name`).
|
|
||||||
|
|
||||||
* **`pr_checklist`**: Pre-commit and PR checklist.
|
|
||||||
- `action: "add"`: Add checklist item (requires `description`, optional `category: "Security" | "Testing" | "Formatting" | ...`, `automated_check`, `is_checked`, `repo_name`).
|
|
||||||
- `action: "get"`: Get PR checklist items.
|
|
||||||
- `action: "clear"`: Clear PR checklist.
|
|
||||||
|
|
||||||
* **`snippets`**: Reusable code snippet vault.
|
* **`snippets`**: Reusable code snippet vault.
|
||||||
- `action: "store"`: Store snippet (requires `query` as name, optional `language`, `code`, `description`, `tags`, `origin_file`, `line_range`, `repo_name`).
|
- `action: "store"`: Store snippet (requires `query` as name, optional `language`, `code`, `description`, `tags`, `origin_file`, `line_range`, `repo_name`).
|
||||||
- `action: "search"`: Search snippet vault (optional `query`, `tags`, `hybrid: true`).
|
- `action: "search"`: Search snippet vault (optional `query`, `tags`, `hybrid: true`).
|
||||||
@@ -90,7 +67,6 @@ The server consolidates granular single-purpose tools into domain-named smart to
|
|||||||
* **`clipboard`**: OS Clipboard management.
|
* **`clipboard`**: OS Clipboard management.
|
||||||
- `action: "read"`: Read OS clipboard.
|
- `action: "read"`: Read OS clipboard.
|
||||||
- `action: "write"`: Write text/html/files/image to clipboard.
|
- `action: "write"`: Write text/html/files/image to clipboard.
|
||||||
- `action: "toggle_watch"`: Toggle auto-clipboard watcher.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -189,9 +165,8 @@ To maintain maximum security, speed, and cross-platform reliability:
|
|||||||
* **Zero-Allocation Stream Formatting**: Graph condensation loops (`condense_graph_worker`) format node/relation subgraphs into stream buffers via `std::fmt::Write`, eliminating intermediate String allocation overhead.
|
* **Zero-Allocation Stream Formatting**: Graph condensation loops (`condense_graph_worker`) format node/relation subgraphs into stream buffers via `std::fmt::Write`, eliminating intermediate String allocation overhead.
|
||||||
* **Single-Handle OS Clipboard Retries & Image Downsampling**: `WriteClipboardHandler` initializes OS clipboard handles once per operation and downsamples images exceeding $2048 \times 2048$ resolution before writing raw RGBA bytes.
|
* **Single-Handle OS Clipboard Retries & Image Downsampling**: `WriteClipboardHandler` initializes OS clipboard handles once per operation and downsamples images exceeding $2048 \times 2048$ resolution before writing raw RGBA bytes.
|
||||||
* **Zero Transaction Drop Persistence Guarantee**: `Store::modify` automatically spawns an async task to execute `push_async` with channel backpressure if `push` encounters queue saturation, ensuring zero data loss under spike write loads.
|
* **Zero Transaction Drop Persistence Guarantee**: `Store::modify` automatically spawns an async task to execute `push_async` with channel backpressure if `push` encounters queue saturation, ensuring zero data loss under spike write loads.
|
||||||
* **Non-Blocking Read Lock Sticky Notes**: `StickyNotesHandler` (`action = "read"`) queries notes using shared read locks, executing write pruning only when expired items exist.
|
|
||||||
* **Token-Budgeted Query Projections**: Decision queries (`query_decisions`) support `limit` caps and compact `include_body: false` projections for token budget optimization.
|
* **Token-Budgeted Query Projections**: Decision queries (`query_decisions`) support `limit` caps and compact `include_body: false` projections for token budget optimization.
|
||||||
* **Serde Parameter & Enum Ergonomics**: Action enums (`StickyNoteAction`, `SnippetSearchMode`, `Relation`) support case-insensitive variants and common synonyms (`create`/`add`, `remove`/`delete`, `list`/`read`, `source`/`from`, `target`/`to`, `relationType`/`relation_type`) ensuring seamless LLM tool execution.
|
* **Serde Parameter & Enum Ergonomics**: Action enums (`SnippetSearchMode`, `Relation`) support case-insensitive variants and common synonyms (`create`/`add`, `remove`/`delete`, `list`/`read`, `source`/`from`, `target`/`to`, `relationType`/`relation_type`) ensuring seamless LLM tool execution.
|
||||||
* **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.
|
* **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.
|
||||||
* **Non-Blocking Tantivy Search Queries**: `MemoryIndex::search` queries current index searcher snapshots without executing synchronous disk commit locks, preventing query stalls during heavy background indexing.
|
* **Non-Blocking Tantivy Search Queries**: `MemoryIndex::search` queries current index searcher snapshots without executing synchronous disk commit locks, preventing query stalls during heavy background indexing.
|
||||||
* **Zero-Allocation HashSet<&str> Snippet Deduplication**: `indexer.rs` utilizes borrowed `HashSet<&str>` name lookups during snippet batch modifications, eliminating heap string re-allocations inside the store write lock.
|
* **Zero-Allocation HashSet<&str> Snippet Deduplication**: `indexer.rs` utilizes borrowed `HashSet<&str>` name lookups during snippet batch modifications, eliminating heap string re-allocations inside the store write lock.
|
||||||
@@ -214,7 +189,7 @@ To maintain maximum security, speed, and cross-platform reliability:
|
|||||||
* **Embedding Input Safeguard**: `generate_embedding_async` returns explicit errors for empty/0-length text inputs instead of returning empty vectors, preventing downstream vector dimension mismatches during cosine similarity calculations.
|
* **Embedding Input Safeguard**: `generate_embedding_async` returns explicit errors for empty/0-length text inputs instead of returning empty vectors, preventing downstream vector dimension mismatches during cosine similarity calculations.
|
||||||
* **Path Traversal Security Guards**: `validate_safe_path` enforces path canonicalization and rejects relative parent traversal components (`..`) across file and process log handlers (`GetRecentLogsTool`, `WatchProcessLogsTool`).
|
* **Path Traversal Security Guards**: `validate_safe_path` enforces path canonicalization and rejects relative parent traversal components (`..`) across file and process log handlers (`GetRecentLogsTool`, `WatchProcessLogsTool`).
|
||||||
* **Watcher Map Memory Eviction**: Proactive daemon file watcher in `watcher.rs` caps `last_processed` map size at 1,000 entries and purges entries older than 10 minutes to prevent monotonic memory leakage.
|
* **Watcher Map Memory Eviction**: Proactive daemon file watcher in `watcher.rs` caps `last_processed` map size at 1,000 entries and purges entries older than 10 minutes to prevent monotonic memory leakage.
|
||||||
* **Comprehensive Serde Casing Aliases**: All 11 consolidated tool action enums (`TaskAction`, `MilestoneAction`, `PinnedFileAction`, `ContextWorkspaceAction`, `PrChecklistAction`, `SnippetAction`, `DecisionAction`, `TechDebtAction`, `EnvAction`, `ClipboardAction`, `HandoffMemoAction`) include serde alias attributes supporting `snake_case`, `camelCase`, `PascalCase`, and uppercase variants for maximum LLM casing resilience.
|
* **Comprehensive Serde Casing Aliases**: All 8 consolidated tool action enums (TaskAction, MilestoneAction, SnippetAction, DecisionAction, TechDebtAction, EnvAction, ClipboardAction, HandoffMemoAction) include serde alias attributes supporting `snake_case`, `camelCase`, `PascalCase`, and uppercase variants for maximum LLM casing resilience.
|
||||||
* **Two-Phase Graph Condensation**: `condense_graph_worker` uses a 2-phase commit (non-destructive `read_with` -> graph insert -> prune by timestamp/content) to prevent data loss if summarization or graph insertion fails.
|
* **Two-Phase Graph Condensation**: `condense_graph_worker` uses a 2-phase commit (non-destructive `read_with` -> graph insert -> prune by timestamp/content) to prevent data loss if summarization or graph insertion fails.
|
||||||
* **Store Write Lock Minimization**: `Store::modify` and `Store::modify_async` unblock concurrent readers during JSON serialization by releasing the write lock immediately after mutating memory state.
|
* **Store Write Lock Minimization**: `Store::modify` and `Store::modify_async` unblock concurrent readers during JSON serialization by releasing the write lock immediately after mutating memory state.
|
||||||
* **Redb Database Lock Retry Backoff**: `init_db` retries transient Redb lock contention with exponential backoff (3 attempts, 150ms delay) before falling back.
|
* **Redb Database Lock Retry Backoff**: `init_db` retries transient Redb lock contention with exponential backoff (3 attempts, 150ms delay) before falling back.
|
||||||
|
|||||||
+1
-45
@@ -105,9 +105,7 @@ pub async fn ttl_sweeper_worker(state: Arc<MemoryState>) {
|
|||||||
state.project.tasks.modify(|tasks| {
|
state.project.tasks.modify(|tasks| {
|
||||||
tasks.retain(|t| t.expires_at.is_none_or(|exp| exp > now));
|
tasks.retain(|t| t.expires_at.is_none_or(|exp| exp > now));
|
||||||
});
|
});
|
||||||
state.code.sticky.modify(|notes| {
|
|
||||||
notes.retain(|n| n.expires_at.is_none_or(|exp| exp > now));
|
|
||||||
});
|
|
||||||
state.telemetry.handoff_memos.modify(|memos| {
|
state.telemetry.handoff_memos.modify(|memos| {
|
||||||
memos.retain(|m| m.expires_at.is_none_or(|exp| exp > now));
|
memos.retain(|m| m.expires_at.is_none_or(|exp| exp > now));
|
||||||
});
|
});
|
||||||
@@ -139,48 +137,6 @@ pub async fn condense_graph_worker(state: Arc<MemoryState>) {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_secs();
|
.as_secs();
|
||||||
|
|
||||||
let sticky_condensation = state.code.sticky.read_with(|notes| {
|
|
||||||
if notes.len() > threshold {
|
|
||||||
let mut sorted = notes.clone();
|
|
||||||
sorted.sort_by_key(|n| n.timestamp);
|
|
||||||
let to_remove = sorted.len() - (threshold / 2);
|
|
||||||
let removed: Vec<_> = sorted.into_iter().take(to_remove).collect();
|
|
||||||
let mut content = String::new();
|
|
||||||
let mut ids = Vec::new();
|
|
||||||
for r in &removed {
|
|
||||||
content.push_str(&format!("{}\n", r.content));
|
|
||||||
ids.push(r.id.clone());
|
|
||||||
}
|
|
||||||
Some((content, ids))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some((content, ids)) = sticky_condensation {
|
|
||||||
if !content.is_empty() {
|
|
||||||
let name = format!("StickyNote History {}", now);
|
|
||||||
state.modify_graph(|graph| {
|
|
||||||
graph.entities.insert(
|
|
||||||
name.clone(),
|
|
||||||
crate::models::Entity {
|
|
||||||
name: name.clone(),
|
|
||||||
entity_type: "Historical Summary".to_string(),
|
|
||||||
observations: vec![content],
|
|
||||||
namespace: crate::models::default_namespace(),
|
|
||||||
git_branch: None,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
let id_set: std::collections::HashSet<String> = ids.into_iter().collect();
|
|
||||||
state.code.sticky.modify(|notes| {
|
|
||||||
notes.retain(|n| !id_set.contains(&n.id));
|
|
||||||
});
|
|
||||||
tracing::info!("Condensed sticky notes into Historical Summary.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let snippet_condensation = state.code.snippets.read_with(|snippets| {
|
let snippet_condensation = state.code.snippets.read_with(|snippets| {
|
||||||
if snippets.len() > threshold {
|
if snippets.len() > threshold {
|
||||||
let mut sorted = snippets.clone();
|
let mut sorted = snippets.clone();
|
||||||
|
|||||||
+5
-71
@@ -81,21 +81,6 @@ impl CodeChange {
|
|||||||
self.branch.as_deref().or(self.git_branch.as_deref())
|
self.branch.as_deref().or(self.git_branch.as_deref())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct StickyNote {
|
|
||||||
#[serde(default = "generate_uuid")]
|
|
||||||
pub id: String,
|
|
||||||
pub timestamp: u64,
|
|
||||||
pub content: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub expires_at: Option<u64>,
|
|
||||||
#[serde(default = "default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub category: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub author: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn generate_uuid() -> String {
|
pub fn generate_uuid() -> String {
|
||||||
uuid::Uuid::new_v4().to_string()
|
uuid::Uuid::new_v4().to_string()
|
||||||
@@ -162,18 +147,20 @@ pub fn normalize_relation_type(input: &str) -> String {
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum TaskStatus {
|
pub enum TaskStatus {
|
||||||
|
Pending,
|
||||||
#[default]
|
#[default]
|
||||||
Active,
|
Active,
|
||||||
InProgress,
|
InProgress,
|
||||||
Blocked,
|
Blocked,
|
||||||
Completed,
|
Completed,
|
||||||
|
Cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaskStatus {
|
impl TaskStatus {
|
||||||
pub fn is_active(&self) -> bool {
|
pub fn is_active(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
TaskStatus::Active | TaskStatus::InProgress | TaskStatus::Blocked
|
TaskStatus::Pending | TaskStatus::Active | TaskStatus::InProgress | TaskStatus::Blocked
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,10 +170,12 @@ impl TaskStatus {
|
|||||||
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
pub fn as_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
TaskStatus::Pending => "pending",
|
||||||
TaskStatus::Active => "active",
|
TaskStatus::Active => "active",
|
||||||
TaskStatus::InProgress => "in_progress",
|
TaskStatus::InProgress => "in_progress",
|
||||||
TaskStatus::Blocked => "blocked",
|
TaskStatus::Blocked => "blocked",
|
||||||
TaskStatus::Completed => "completed",
|
TaskStatus::Completed => "completed",
|
||||||
|
TaskStatus::Cancelled => "cancelled",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,18 +389,6 @@ fn default_adr_status() -> String {
|
|||||||
"accepted".to_string()
|
"accepted".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct Preference {
|
|
||||||
pub key: String,
|
|
||||||
pub value: String,
|
|
||||||
pub updated_at: u64,
|
|
||||||
#[serde(default = "default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub category: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub rationale: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct ErrorFix {
|
pub struct ErrorFix {
|
||||||
@@ -434,19 +411,6 @@ pub struct ErrorFix {
|
|||||||
pub toolchain: Option<String>,
|
pub toolchain: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct PinnedFile {
|
|
||||||
pub namespace: String,
|
|
||||||
pub file_path: String,
|
|
||||||
pub timestamp: u64,
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub symbol_or_line: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub pin_reason: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct SessionSummary {
|
pub struct SessionSummary {
|
||||||
@@ -554,20 +518,6 @@ pub struct EnvironmentDetail {
|
|||||||
pub ssh_host: Option<String>,
|
pub ssh_host: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct PrChecklistItem {
|
|
||||||
pub namespace: String,
|
|
||||||
pub id: String,
|
|
||||||
pub description: String,
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_checked: bool,
|
|
||||||
#[serde(default)]
|
|
||||||
pub category: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub automated_check: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct TechDebt {
|
pub struct TechDebt {
|
||||||
@@ -595,22 +545,6 @@ pub struct TechDebt {
|
|||||||
pub effort_estimate: Option<String>,
|
pub effort_estimate: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
||||||
pub struct ContextWorkspace {
|
|
||||||
pub name: String,
|
|
||||||
pub namespace: String,
|
|
||||||
pub pinned_files: Vec<String>,
|
|
||||||
pub active_task_ids: Vec<String>,
|
|
||||||
pub saved_at: u64,
|
|
||||||
#[serde(default)]
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub vcs_revision: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct GateRecord {
|
pub struct GateRecord {
|
||||||
|
|||||||
+25
-206
@@ -261,21 +261,9 @@ impl McpResource for SessionDeltaResource {
|
|||||||
.unwrap_or_else(|e| e.into_inner());
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
let active_tasks: Vec<_> = tasks.iter().filter(|t| t.is_active()).collect();
|
let active_tasks: Vec<_> = tasks.iter().filter(|t| t.is_active()).collect();
|
||||||
|
|
||||||
let notes = state
|
|
||||||
.code
|
|
||||||
.sticky
|
|
||||||
.cache
|
|
||||||
.read()
|
|
||||||
.unwrap_or_else(|e| e.into_inner());
|
|
||||||
let active_notes: Vec<_> = notes
|
|
||||||
.iter()
|
|
||||||
.filter(|n| n.expires_at.map_or(true, |exp| exp > now))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let delta = serde_json::json!({
|
let delta = serde_json::json!({
|
||||||
"recent_changes": recent_changes,
|
"recent_changes": recent_changes,
|
||||||
"active_tasks": active_tasks,
|
"active_tasks": active_tasks,
|
||||||
"active_sticky_notes": active_notes,
|
|
||||||
"timestamp": now,
|
"timestamp": now,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -291,7 +279,7 @@ impl McpPrompt for ContextWarmupPrompt {
|
|||||||
}
|
}
|
||||||
fn description(&self) -> Option<&'static str> {
|
fn description(&self) -> Option<&'static str> {
|
||||||
Some(
|
Some(
|
||||||
"Consolidated context warmup prompt providing active tasks, pinned files, open tech debt, and sticky notes",
|
"Consolidated context warmup prompt providing active tasks, active decisions, and unresolved tech debt",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
async fn get(
|
async fn get(
|
||||||
@@ -305,7 +293,7 @@ impl McpPrompt for ContextWarmupPrompt {
|
|||||||
"role": "user",
|
"role": "user",
|
||||||
"content": {
|
"content": {
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"text": "Perform a complete context warmup. Please read memory://session/delta, memory://tasks/active, memory://tech_debt/active, and memory://pinned_files to synthesize current project focus and active workspace constraints."
|
"text": "Perform a complete context warmup. Please read memory://session/delta, memory://tasks/active, memory://decisions/active, and memory://tech_debt/unresolved to synthesize current project focus and active workspace constraints."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -397,28 +385,6 @@ impl MemoryHandler {
|
|||||||
Ok(serde_json::to_string_pretty(&*items)?)
|
Ok(serde_json::to_string_pretty(&*items)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
struct PinnedFilesResource;
|
|
||||||
#[async_trait]
|
|
||||||
impl McpResource for PinnedFilesResource {
|
|
||||||
fn uri(&self) -> &'static str {
|
|
||||||
"memory://pinned_files"
|
|
||||||
}
|
|
||||||
fn name(&self) -> &'static str {
|
|
||||||
"Pinned Files"
|
|
||||||
}
|
|
||||||
fn description(&self) -> Option<&'static str> {
|
|
||||||
Some("Currently pinned files in the workspace")
|
|
||||||
}
|
|
||||||
async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
|
||||||
let items = state
|
|
||||||
.project
|
|
||||||
.pinned_files
|
|
||||||
.cache
|
|
||||||
.read()
|
|
||||||
.unwrap_or_else(|e| e.into_inner());
|
|
||||||
Ok(serde_json::to_string_pretty(&*items)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MilestonesResource;
|
struct MilestonesResource;
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -473,7 +439,6 @@ impl MemoryHandler {
|
|||||||
|
|
||||||
register_resource!(TerminalHistoryResource);
|
register_resource!(TerminalHistoryResource);
|
||||||
register_resource!(RecentActivitiesResource);
|
register_resource!(RecentActivitiesResource);
|
||||||
register_resource!(PinnedFilesResource);
|
|
||||||
register_resource!(MilestonesResource);
|
register_resource!(MilestonesResource);
|
||||||
struct ArchiveRoutinePrompt;
|
struct ArchiveRoutinePrompt;
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -529,8 +494,6 @@ impl MemoryHandler {
|
|||||||
|
|
||||||
register!(tasks::TasksHandler);
|
register!(tasks::TasksHandler);
|
||||||
register!(tasks::MilestonesHandler);
|
register!(tasks::MilestonesHandler);
|
||||||
|
|
||||||
register!(notes::StickyNotesHandler);
|
|
||||||
register!(notes::HandoffMemosHandler);
|
register!(notes::HandoffMemosHandler);
|
||||||
register!(notes::AddSessionSummaryHandler);
|
register!(notes::AddSessionSummaryHandler);
|
||||||
register!(notes::GenerateStandupReportHandler);
|
register!(notes::GenerateStandupReportHandler);
|
||||||
@@ -542,16 +505,11 @@ impl MemoryHandler {
|
|||||||
register!(meta::SearchErrorFixesHandler);
|
register!(meta::SearchErrorFixesHandler);
|
||||||
register!(meta::LogCodeChangeHandler);
|
register!(meta::LogCodeChangeHandler);
|
||||||
register!(meta::QueryRecentChangesHandler);
|
register!(meta::QueryRecentChangesHandler);
|
||||||
register!(meta::LearnPreferenceHandler);
|
|
||||||
register!(meta::ReadPreferencesHandler);
|
|
||||||
register!(meta::OmniSearchHandler);
|
register!(meta::OmniSearchHandler);
|
||||||
register!(meta::GetProjectHealthHandler);
|
register!(meta::GetProjectHealthHandler);
|
||||||
|
|
||||||
register!(env::EnvironmentHandler);
|
register!(env::EnvironmentHandler);
|
||||||
|
|
||||||
register!(workspaces::PinnedFilesHandler);
|
|
||||||
register!(workspaces::ContextWorkspacesHandler);
|
|
||||||
register!(workspaces::PrChecklistHandler);
|
|
||||||
register!(workspaces::SnippetsHandler);
|
register!(workspaces::SnippetsHandler);
|
||||||
register!(vision::ClipboardHandler);
|
register!(vision::ClipboardHandler);
|
||||||
|
|
||||||
@@ -574,7 +532,6 @@ impl MemoryHandler {
|
|||||||
register!(meta::GetNextActionableTasksHandler);
|
register!(meta::GetNextActionableTasksHandler);
|
||||||
register!(meta::LogHypothesisHandler);
|
register!(meta::LogHypothesisHandler);
|
||||||
register!(meta::QueryHypothesesHandler);
|
register!(meta::QueryHypothesesHandler);
|
||||||
register!(workspaces::DiffContextWorkspacesHandler);
|
|
||||||
register!(meta::GetPreflightContextHandler);
|
register!(meta::GetPreflightContextHandler);
|
||||||
register!(graph::ResolveStaleSymbolsHandler);
|
register!(graph::ResolveStaleSymbolsHandler);
|
||||||
register!(graph::SummarizeSubgraphHandler);
|
register!(graph::SummarizeSubgraphHandler);
|
||||||
@@ -774,14 +731,13 @@ impl MemoryHandler {
|
|||||||
"log_error_fix" => "ERROR_FIX",
|
"log_error_fix" => "ERROR_FIX",
|
||||||
"tech_debt" => "TECH_DEBT",
|
"tech_debt" => "TECH_DEBT",
|
||||||
"tasks" | "milestones" => "TASK",
|
"tasks" | "milestones" => "TASK",
|
||||||
"sticky_notes" | "handoff_memos" => "STICKY_NOTE",
|
"handoff_memos" => "STICKY_NOTE",
|
||||||
"manage_checkpoint" => "CHECKPOINT",
|
"manage_checkpoint" => "CHECKPOINT",
|
||||||
"manage_subagent_namespace" => "SUBAGENT",
|
"manage_subagent_namespace" => "SUBAGENT",
|
||||||
"snippets" => "SNIPPET",
|
"snippets" => "SNIPPET",
|
||||||
"search_web" => "WEB_SEARCH",
|
"search_web" => "WEB_SEARCH",
|
||||||
"omni_search" => "OMNI_SEARCH",
|
"omni_search" => "OMNI_SEARCH",
|
||||||
"environment" => "ENVIRONMENT",
|
"environment" => "ENVIRONMENT",
|
||||||
"pinned_files" | "context_workspaces" | "pr_checklist" => "WORKSPACE",
|
|
||||||
_ => "TOOL",
|
_ => "TOOL",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -880,157 +836,25 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"sticky_notes" => {
|
|
||||||
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("add");
|
|
||||||
let preview = args
|
|
||||||
.get("content")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(|c| c.chars().take(40).collect::<String>())
|
|
||||||
.unwrap_or_default();
|
|
||||||
(
|
|
||||||
"Sticky Notes",
|
|
||||||
format!("{}: {}", act, preview)
|
|
||||||
.trim_end_matches(": ")
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
"clipboard" => {
|
|
||||||
let act = args
|
|
||||||
.get("action")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("read");
|
|
||||||
("Clipboard", act.to_string())
|
|
||||||
}
|
|
||||||
"snippets" => {
|
|
||||||
let act = args
|
|
||||||
.get("action")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("search");
|
|
||||||
let q = args.get("query").and_then(|v| v.as_str()).unwrap_or("");
|
|
||||||
(
|
|
||||||
"Snippets",
|
|
||||||
format!("{}: {}", act, q).trim_end_matches(": ").to_string(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
"log_code_change" => {
|
|
||||||
let file = args
|
|
||||||
.get("file_path")
|
|
||||||
.or_else(|| args.get("file"))
|
|
||||||
.or_else(|| args.get("target_file"))
|
|
||||||
.and_then(|v| v.as_str());
|
|
||||||
let summary = args
|
|
||||||
.get("summary")
|
|
||||||
.or_else(|| args.get("description"))
|
|
||||||
.and_then(|v| v.as_str());
|
|
||||||
|
|
||||||
let d = match (file, summary) {
|
|
||||||
(Some(f), Some(s)) => format!("{}: {}", f, s),
|
|
||||||
(Some(f), None) => f.to_string(),
|
|
||||||
(None, Some(s)) => s.to_string(),
|
|
||||||
(None, None) => String::new(),
|
|
||||||
};
|
|
||||||
("Log Code Change", d)
|
|
||||||
}
|
|
||||||
"log_error_fix" => {
|
|
||||||
let d = args
|
|
||||||
.get("error")
|
|
||||||
.or_else(|| args.get("summary"))
|
|
||||||
.or_else(|| args.get("solution"))
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_string();
|
|
||||||
("Log Error Fix", d)
|
|
||||||
}
|
|
||||||
"create_entities" => {
|
"create_entities" => {
|
||||||
let d = if let Some(entities) = args.get("entities").and_then(|v| v.as_array()) {
|
let names = args
|
||||||
let names: Vec<&str> = entities
|
.get("entities")
|
||||||
.iter()
|
.and_then(|v| v.as_array())
|
||||||
.filter_map(|e| {
|
.map(|arr| {
|
||||||
e.get("name")
|
arr.iter()
|
||||||
.or_else(|| e.get("entity_name"))
|
.filter_map(|e| e.get("name").and_then(|n| n.as_str()))
|
||||||
.and_then(|n| n.as_str())
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
})
|
})
|
||||||
.take(3)
|
|
||||||
.collect();
|
|
||||||
if !names.is_empty() {
|
|
||||||
let mut s = names.join(", ");
|
|
||||||
if entities.len() > 3 {
|
|
||||||
s.push_str(&format!(" (+{} more)", entities.len() - 3));
|
|
||||||
}
|
|
||||||
s
|
|
||||||
} else {
|
|
||||||
format!("{} entity/entities", entities.len())
|
|
||||||
}
|
|
||||||
} else if let Some(name) = args
|
|
||||||
.get("name")
|
|
||||||
.or_else(|| args.get("entity_name"))
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
{
|
|
||||||
name.to_string()
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
("Create Entities", d)
|
|
||||||
}
|
|
||||||
"create_relations" => {
|
|
||||||
let d = if let Some(relations) = args.get("relations").and_then(|v| v.as_array()) {
|
|
||||||
if let Some(first) = relations.first() {
|
|
||||||
let from = first.get("from").and_then(|v| v.as_str()).unwrap_or("?");
|
|
||||||
let to = first.get("to").and_then(|v| v.as_str()).unwrap_or("?");
|
|
||||||
let pred = first
|
|
||||||
.get("relation_type")
|
|
||||||
.or_else(|| first.get("predicate"))
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("->");
|
|
||||||
if relations.len() > 1 {
|
|
||||||
format!("{} {} {} (+{} more)", from, pred, to, relations.len() - 1)
|
|
||||||
} else {
|
|
||||||
format!("{} {} {}", from, pred, to)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
format!("{} relation(s)", relations.len())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
("Create Relations", d)
|
|
||||||
}
|
|
||||||
"omni_search" | "search_graph" | "search_web" => {
|
|
||||||
let d = args
|
|
||||||
.get("query")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(|q| format!("\"{}\"", q))
|
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
("Search", d)
|
("Create Entities", names)
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let d = args
|
|
||||||
.get("title")
|
|
||||||
.or_else(|| args.get("action"))
|
|
||||||
.or_else(|| args.get("summary"))
|
|
||||||
.or_else(|| args.get("description"))
|
|
||||||
.or_else(|| args.get("name"))
|
|
||||||
.or_else(|| args.get("query"))
|
|
||||||
.or_else(|| args.get("file_path"))
|
|
||||||
.or_else(|| args.get("file"))
|
|
||||||
.or_else(|| args.get("path"))
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
(name, d.to_string())
|
|
||||||
}
|
}
|
||||||
|
_ => return name.to_string(),
|
||||||
};
|
};
|
||||||
|
if detail.is_empty() {
|
||||||
let mut truncated = detail.trim().to_string();
|
action.to_string()
|
||||||
if truncated.len() > 80 {
|
|
||||||
let valid_len = truncated.floor_char_boundary(77);
|
|
||||||
truncated.truncate(valid_len);
|
|
||||||
truncated.push_str("...");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !truncated.is_empty() {
|
|
||||||
format!("{}: {}", action, truncated)
|
|
||||||
} else {
|
} else {
|
||||||
format!("{}", action)
|
format!("{}: {}", action, detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1047,20 +871,20 @@ async fn execute_tool_span(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use serde_json::json;
|
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_tool_activity_description() {
|
fn test_format_tool_activity_description() {
|
||||||
let msg1 = format_tool_activity_description(
|
let msg = format_tool_activity_description(
|
||||||
"log_code_change",
|
"tasks",
|
||||||
&json!({ "file": "src/lib.rs", "summary": "Refactored library" }),
|
&serde_json::json!({ "action": "add", "title": "Refactor router" }),
|
||||||
);
|
);
|
||||||
assert_eq!(msg1, "Log Code Change: src/lib.rs: Refactored library");
|
assert_eq!(msg, "Tasks: add: Refactor router");
|
||||||
|
|
||||||
let msg2 = format_tool_activity_description(
|
let msg2 = format_tool_activity_description(
|
||||||
"create_entities",
|
"create_entities",
|
||||||
&json!({ "entities": [{ "name": "EntityA" }, { "name": "EntityB" }] }),
|
&serde_json::json!({ "entities": [{ "name": "EntityA" }, { "name": "EntityB" }] }),
|
||||||
);
|
);
|
||||||
assert_eq!(msg2, "Create Entities: EntityA, EntityB");
|
assert_eq!(msg2, "Create Entities: EntityA, EntityB");
|
||||||
|
|
||||||
@@ -1115,7 +939,7 @@ mod tests {
|
|||||||
assert!(
|
assert!(
|
||||||
resources_arr
|
resources_arr
|
||||||
.iter()
|
.iter()
|
||||||
.any(|r| r["uri"] == "memory://pinned_files")
|
.any(|r| r["uri"] == "memory://decisions/active")
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
resources_arr
|
resources_arr
|
||||||
@@ -1292,8 +1116,8 @@ mod tests {
|
|||||||
.as_array()
|
.as_array()
|
||||||
.expect("tools array expected");
|
.expect("tools array expected");
|
||||||
assert!(
|
assert!(
|
||||||
tools.len() >= 40,
|
tools.len() >= 30,
|
||||||
"Expected at least 40 registered MCP tools, found {}",
|
"Expected at least 30 registered MCP tools, found {}",
|
||||||
tools.len()
|
tools.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1353,16 +1177,11 @@ mod tests {
|
|||||||
("tasks", json!({"action": "list"})),
|
("tasks", json!({"action": "list"})),
|
||||||
("decisions", json!({"action": "query"})),
|
("decisions", json!({"action": "query"})),
|
||||||
("tech_debt", json!({"action": "list"})),
|
("tech_debt", json!({"action": "list"})),
|
||||||
("sticky_notes", json!({"action": "read"})),
|
|
||||||
("pinned_files", json!({"action": "list"})),
|
|
||||||
("context_workspaces", json!({"action": "list"})),
|
|
||||||
("pr_checklist", json!({"action": "get"})),
|
|
||||||
("snippets", json!({"action": "search", "query": "test"})),
|
("snippets", json!({"action": "search", "query": "test"})),
|
||||||
("clipboard", json!({"action": "read"})),
|
("clipboard", json!({"action": "read"})),
|
||||||
("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!({})),
|
||||||
("read_preferences", json!({})),
|
|
||||||
("find_orphans", json!({})),
|
("find_orphans", json!({})),
|
||||||
("sweep_graph_health", json!({})),
|
("sweep_graph_health", json!({})),
|
||||||
("get_next_actionable_tasks", json!({})),
|
("get_next_actionable_tasks", json!({})),
|
||||||
|
|||||||
+26
-19
@@ -253,15 +253,35 @@ impl MemoryIndex {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let target_limit = if is_wildcard_fallback { 10 } else { 50 };
|
let target_limit = if is_wildcard_fallback { 10 } else { 50 };
|
||||||
let fetch_limit = if namespace.is_some() {
|
let final_query: Box<dyn tantivy::query::Query> = if let Some(ns) = namespace {
|
||||||
// Fetch deeper candidate pool so other namespaces do not starve the target namespace
|
let ns_term_query: Box<dyn tantivy::query::Query> = Box::new(tantivy::query::TermQuery::new(
|
||||||
1000
|
tantivy::Term::from_field_text(self.namespace_field, ns),
|
||||||
|
tantivy::schema::IndexRecordOption::Basic,
|
||||||
|
));
|
||||||
|
let ns_filter: Box<dyn tantivy::query::Query> = if ns != "global" {
|
||||||
|
let global_term_query = Box::new(tantivy::query::TermQuery::new(
|
||||||
|
tantivy::Term::from_field_text(self.namespace_field, "global"),
|
||||||
|
tantivy::schema::IndexRecordOption::Basic,
|
||||||
|
));
|
||||||
|
Box::new(tantivy::query::BooleanQuery::new(vec![
|
||||||
|
(tantivy::query::Occur::Should, ns_term_query),
|
||||||
|
(tantivy::query::Occur::Should, global_term_query),
|
||||||
|
]))
|
||||||
} else {
|
} else {
|
||||||
target_limit
|
ns_term_query
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Box::new(tantivy::query::BooleanQuery::new(vec![
|
||||||
|
(tantivy::query::Occur::Must, q),
|
||||||
|
(tantivy::query::Occur::Must, ns_filter),
|
||||||
|
]))
|
||||||
|
} else {
|
||||||
|
q
|
||||||
|
};
|
||||||
|
|
||||||
let top_docs = searcher.search(
|
let top_docs = searcher.search(
|
||||||
&q,
|
&final_query,
|
||||||
&tantivy::collector::TopDocs::with_limit(fetch_limit).order_by_score(),
|
&tantivy::collector::TopDocs::with_limit(target_limit).order_by_score(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut results = Vec::with_capacity(target_limit);
|
let mut results = Vec::with_capacity(target_limit);
|
||||||
@@ -287,20 +307,7 @@ impl MemoryIndex {
|
|||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
let doc_ns = retrieved_doc
|
|
||||||
.get_first(self.namespace_field)
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
if let Some(ns) = namespace
|
|
||||||
&& doc_ns != ns
|
|
||||||
&& doc_ns != "global"
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
results.push((id, doc_type, title, body, score));
|
results.push((id, doc_type, title, body, score));
|
||||||
if results.len() >= target_limit {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ pub struct GenericEvent {
|
|||||||
pub struct ProjectStores {
|
pub struct ProjectStores {
|
||||||
pub tasks: Store<Vec<Task>>,
|
pub tasks: Store<Vec<Task>>,
|
||||||
pub milestones: Store<Vec<Milestone>>,
|
pub milestones: Store<Vec<Milestone>>,
|
||||||
pub pr_checklists: Store<Vec<PrChecklistItem>>,
|
|
||||||
pub context_workspaces: Store<Vec<ContextWorkspace>>,
|
|
||||||
pub pinned_files: Store<Vec<PinnedFile>>,
|
|
||||||
pub snapshots: Store<Vec<StateSnapshot>>,
|
pub snapshots: Store<Vec<StateSnapshot>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +24,6 @@ pub struct CodeStores {
|
|||||||
pub adrs: Store<Vec<Adr>>,
|
pub adrs: Store<Vec<Adr>>,
|
||||||
pub error_fixes: Store<Vec<ErrorFix>>,
|
pub error_fixes: Store<Vec<ErrorFix>>,
|
||||||
pub tech_debts: Store<Vec<TechDebt>>,
|
pub tech_debts: Store<Vec<TechDebt>>,
|
||||||
pub sticky: Store<Vec<StickyNote>>,
|
|
||||||
pub hypotheses: Store<Vec<Hypothesis>>,
|
pub hypotheses: Store<Vec<Hypothesis>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +32,6 @@ pub struct EnvironmentStores {
|
|||||||
pub env_requirements: Store<Vec<EnvRequirement>>,
|
pub env_requirements: Store<Vec<EnvRequirement>>,
|
||||||
pub environments: Store<Vec<EnvironmentDetail>>,
|
pub environments: Store<Vec<EnvironmentDetail>>,
|
||||||
pub gates: Store<Vec<GateRecord>>,
|
pub gates: Store<Vec<GateRecord>>,
|
||||||
pub prefs: Store<HashMap<String, Preference>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TelemetryStores {
|
pub struct TelemetryStores {
|
||||||
@@ -112,9 +107,6 @@ impl MemoryState {
|
|||||||
project: ProjectStores {
|
project: ProjectStores {
|
||||||
tasks: Store::new("tasks", db.clone()),
|
tasks: Store::new("tasks", db.clone()),
|
||||||
milestones: Store::new("milestones", db.clone()),
|
milestones: Store::new("milestones", db.clone()),
|
||||||
pr_checklists: Store::new("pr_checklists", db.clone()),
|
|
||||||
context_workspaces: Store::new("context_workspaces", db.clone()),
|
|
||||||
pinned_files: Store::new("pinned_files", db.clone()),
|
|
||||||
snapshots: Store::new("state_snapshots", db.clone()),
|
snapshots: Store::new("state_snapshots", db.clone()),
|
||||||
},
|
},
|
||||||
code: CodeStores {
|
code: CodeStores {
|
||||||
@@ -123,7 +115,6 @@ impl MemoryState {
|
|||||||
adrs: Store::new("adrs", db.clone()),
|
adrs: Store::new("adrs", db.clone()),
|
||||||
error_fixes: Store::new("error_fixes", db.clone()),
|
error_fixes: Store::new("error_fixes", db.clone()),
|
||||||
tech_debts: Store::new("tech_debts", db.clone()),
|
tech_debts: Store::new("tech_debts", db.clone()),
|
||||||
sticky: Store::new("sticky_notes", db.clone()),
|
|
||||||
hypotheses: Store::new("hypotheses", db.clone()),
|
hypotheses: Store::new("hypotheses", db.clone()),
|
||||||
},
|
},
|
||||||
env: EnvironmentStores {
|
env: EnvironmentStores {
|
||||||
@@ -131,7 +122,6 @@ impl MemoryState {
|
|||||||
env_requirements: Store::new("env_requirements", db.clone()),
|
env_requirements: Store::new("env_requirements", db.clone()),
|
||||||
environments: Store::new("environments", db.clone()),
|
environments: Store::new("environments", db.clone()),
|
||||||
gates: Store::new("gates", db.clone()),
|
gates: Store::new("gates", db.clone()),
|
||||||
prefs: Store::new("preferences", db.clone()),
|
|
||||||
},
|
},
|
||||||
telemetry: TelemetryStores {
|
telemetry: TelemetryStores {
|
||||||
session_summaries: Store::new("session_summaries", db.clone()),
|
session_summaries: Store::new("session_summaries", db.clone()),
|
||||||
@@ -527,21 +517,6 @@ impl SearchService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.state.code.sticky.read_with(|sticky| {
|
|
||||||
for note in sticky.iter() {
|
|
||||||
if uncached_texts.len() >= 50 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let content_preview = note.content.chars().take(200).collect::<String>();
|
|
||||||
uncached_texts.push(note.content.clone());
|
|
||||||
uncached_meta.push((
|
|
||||||
"StickyNote".to_string(),
|
|
||||||
"sticky".to_string(),
|
|
||||||
content_preview,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.state.read_graph(|graph| {
|
self.state.read_graph(|graph| {
|
||||||
for entity in graph.entities.values() {
|
for entity in graph.entities.values() {
|
||||||
if let Some(ns) = filter_namespace {
|
if let Some(ns) = filter_namespace {
|
||||||
|
|||||||
+28
-6
@@ -446,7 +446,10 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
|||||||
|
|
||||||
// Expensive serialization and granular extraction run completely unblocked outside the lock
|
// Expensive serialization and granular extraction run completely unblocked outside the lock
|
||||||
let full_bytes_res = serde_json::to_vec(&new_snapshot);
|
let full_bytes_res = serde_json::to_vec(&new_snapshot);
|
||||||
let granular_entries = serde_json::to_value(&new_snapshot)
|
let granular_entries = full_bytes_res
|
||||||
|
.as_ref()
|
||||||
|
.ok()
|
||||||
|
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(bytes).ok())
|
||||||
.map(|val| Self::extract_granular_entries(&self.key, &val))
|
.map(|val| Self::extract_granular_entries(&self.key, &val))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
@@ -454,7 +457,10 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
|||||||
granular_entries.iter().map(|(k, _)| k.clone()).collect();
|
granular_entries.iter().map(|(k, _)| k.clone()).collect();
|
||||||
let mut removed_keys = Vec::new();
|
let mut removed_keys = Vec::new();
|
||||||
{
|
{
|
||||||
let mut known = self.known_granular_keys.write().unwrap_or_else(|e| e.into_inner());
|
let mut known = self
|
||||||
|
.known_granular_keys
|
||||||
|
.write()
|
||||||
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
for old_k in known.iter() {
|
for old_k in known.iter() {
|
||||||
if !new_keys.contains(old_k) {
|
if !new_keys.contains(old_k) {
|
||||||
removed_keys.push(old_k.clone());
|
removed_keys.push(old_k.clone());
|
||||||
@@ -473,7 +479,12 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
|||||||
|
|
||||||
if self
|
if self
|
||||||
.queue
|
.queue
|
||||||
.push_batch(self.key.clone(), batch_inserts.clone(), removed_keys.clone(), self.flushed.clone())
|
.push_batch(
|
||||||
|
self.key.clone(),
|
||||||
|
batch_inserts.clone(),
|
||||||
|
removed_keys.clone(),
|
||||||
|
self.flushed.clone(),
|
||||||
|
)
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
@@ -521,7 +532,10 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
|||||||
};
|
};
|
||||||
|
|
||||||
let full_bytes_res = serde_json::to_vec(&new_snapshot);
|
let full_bytes_res = serde_json::to_vec(&new_snapshot);
|
||||||
let granular_entries = serde_json::to_value(&new_snapshot)
|
let granular_entries = full_bytes_res
|
||||||
|
.as_ref()
|
||||||
|
.ok()
|
||||||
|
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(bytes).ok())
|
||||||
.map(|val| Self::extract_granular_entries(&self.key, &val))
|
.map(|val| Self::extract_granular_entries(&self.key, &val))
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
@@ -529,7 +543,10 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
|||||||
granular_entries.iter().map(|(k, _)| k.clone()).collect();
|
granular_entries.iter().map(|(k, _)| k.clone()).collect();
|
||||||
let mut removed_keys = Vec::new();
|
let mut removed_keys = Vec::new();
|
||||||
{
|
{
|
||||||
let mut known = self.known_granular_keys.write().unwrap_or_else(|e| e.into_inner());
|
let mut known = self
|
||||||
|
.known_granular_keys
|
||||||
|
.write()
|
||||||
|
.unwrap_or_else(|e| e.into_inner());
|
||||||
for old_k in known.iter() {
|
for old_k in known.iter() {
|
||||||
if !new_keys.contains(old_k) {
|
if !new_keys.contains(old_k) {
|
||||||
removed_keys.push(old_k.clone());
|
removed_keys.push(old_k.clone());
|
||||||
@@ -548,7 +565,12 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
|||||||
|
|
||||||
if let Some(rx) = self
|
if let Some(rx) = self
|
||||||
.queue
|
.queue
|
||||||
.push_batch_async(self.key.clone(), batch_inserts, removed_keys, self.flushed.clone())
|
.push_batch_async(
|
||||||
|
self.key.clone(),
|
||||||
|
batch_inserts,
|
||||||
|
removed_keys,
|
||||||
|
self.flushed.clone(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
let _ = rx.await;
|
let _ = rx.await;
|
||||||
|
|||||||
+15
-726
@@ -1,23 +1,6 @@
|
|||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Extracts the current contents of the OS clipboard (text, HTML, files, and image).
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ReadClipboardTool {}
|
|
||||||
|
|
||||||
/// Writes contents to the OS clipboard. You can provide any combination of text, HTML, file paths, or an image path.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct WriteClipboardTool {
|
|
||||||
/// Optional plain text to write to the clipboard.
|
|
||||||
pub text: Option<String>,
|
|
||||||
/// Optional HTML to write to the clipboard (useful for rich text pasting).
|
|
||||||
pub html: Option<String>,
|
|
||||||
/// Optional absolute file paths to write to the clipboard as a Windows File Drop.
|
|
||||||
pub files: Option<Vec<String>>,
|
|
||||||
/// Optional absolute path to a local image to write to the clipboard.
|
|
||||||
pub image_path: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct CreateEntitiesTool {
|
pub struct CreateEntitiesTool {
|
||||||
@@ -166,39 +149,6 @@ pub struct VisualizeGraphTool {
|
|||||||
pub namespace: Option<String>,
|
pub namespace: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum StickyNoteAction {
|
|
||||||
#[serde(alias = "ADD", alias = "Add", alias = "create", alias = "insert")]
|
|
||||||
Add,
|
|
||||||
#[serde(alias = "READ", alias = "Read", alias = "list", alias = "get")]
|
|
||||||
Read,
|
|
||||||
#[serde(alias = "DELETE", alias = "Delete", alias = "remove", alias = "rm")]
|
|
||||||
Delete,
|
|
||||||
#[serde(alias = "CLEAR", alias = "Clear", alias = "purge")]
|
|
||||||
Clear,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ephemeral sticky notes with TTL for scratchpad items, ports, and transient notes.
|
|
||||||
/// Actions:
|
|
||||||
/// - 'add': Add note. Required: content. Optional: ttl_seconds, session_only.
|
|
||||||
/// - 'read': Read all active notes.
|
|
||||||
/// - 'delete': Delete note. Required: index (1-based integer).
|
|
||||||
/// - 'clear': Clear all active notes.
|
|
||||||
/// Next steps on error: Ensure required fields for the specific action are supplied.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct StickyNotesTool {
|
|
||||||
/// Action to perform: 'add', 'read', 'delete', or 'clear'.
|
|
||||||
pub action: StickyNoteAction,
|
|
||||||
/// Content of the sticky note (required for 'add').
|
|
||||||
pub content: Option<String>,
|
|
||||||
/// 1-indexed position of the sticky note to delete (required for 'delete').
|
|
||||||
pub index: Option<usize>,
|
|
||||||
/// Optional time-to-live in seconds (for 'add').
|
|
||||||
pub ttl_seconds: Option<u64>,
|
|
||||||
/// Optional flag to auto-expire the note at the end of the current session (for 'add').
|
|
||||||
pub session_only: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Condense or summarize an entity's observations to reduce size.
|
/// Condense or summarize an entity's observations to reduce size.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
@@ -209,124 +159,6 @@ pub struct CondenseEntityTool {
|
|||||||
pub summarized_observations: Vec<String>,
|
pub summarized_observations: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a new task to the task tracker.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct AddTaskTool {
|
|
||||||
/// The title of the task.
|
|
||||||
pub title: String,
|
|
||||||
/// A detailed description of the task.
|
|
||||||
pub description: String,
|
|
||||||
/// The associated git branch, if any.
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
/// Optional parent task ID to create a nested sub-task.
|
|
||||||
pub parent_id: Option<String>,
|
|
||||||
/// Optional list of task IDs this task depends on.
|
|
||||||
pub dependencies: Option<Vec<String>>,
|
|
||||||
/// Optional namespace (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.
|
|
||||||
pub verification_command: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a task and all its children.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct DeleteTaskTool {
|
|
||||||
/// The ID of the task to delete.
|
|
||||||
pub id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the status of an existing task.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct UpdateTaskStatusTool {
|
|
||||||
/// The ID of the task to update.
|
|
||||||
pub id: String,
|
|
||||||
/// The new status of the task (e.g., 'pending', 'completed', 'cancelled').
|
|
||||||
#[schemars(description = "Must be 'pending', 'completed', or 'cancelled'")]
|
|
||||||
pub status: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List all currently active tasks.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ListActiveTasksTool {
|
|
||||||
/// Optional git branch to filter tasks by.
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
/// Optional summary level: 'compact' (IDs and titles), 'detailed', or 'full'. Defaults to 'detailed'.
|
|
||||||
pub summary_level: Option<String>,
|
|
||||||
/// Optional token budget cap. Truncates results to stay within max_tokens.
|
|
||||||
pub max_tokens: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store a reusable code snippet.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct StoreSnippetTool {
|
|
||||||
/// The name of the snippet.
|
|
||||||
pub name: String,
|
|
||||||
/// The programming language of the snippet.
|
|
||||||
pub language: String,
|
|
||||||
/// The code snippet content.
|
|
||||||
pub code: String,
|
|
||||||
/// A description of what the snippet does.
|
|
||||||
pub description: String,
|
|
||||||
/// Optional classification tags (e.g. ['rust', 'async', 'mcp']).
|
|
||||||
pub tags: Option<Vec<String>>,
|
|
||||||
/// Optional project namespace.
|
|
||||||
pub namespace: Option<String>,
|
|
||||||
/// Optional repository name.
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
/// Optional origin file path where the snippet was extracted from.
|
|
||||||
pub origin_file: Option<String>,
|
|
||||||
/// Optional line range in origin file (e.g. 'L10-L45').
|
|
||||||
pub line_range: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add classification tags to an existing code snippet.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct TagSnippetTool {
|
|
||||||
/// The name of the snippet to tag.
|
|
||||||
pub name: String,
|
|
||||||
/// Array of tags (e.g. ['rust', 'async', 'mcp']).
|
|
||||||
pub tags: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum SnippetSearchMode {
|
|
||||||
#[serde(alias = "HYBRID", alias = "Hybrid")]
|
|
||||||
Hybrid,
|
|
||||||
#[serde(alias = "KEYWORD", alias = "Keyword")]
|
|
||||||
Keyword,
|
|
||||||
#[serde(alias = "SEMANTIC", alias = "Semantic")]
|
|
||||||
Semantic,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Search stored code snippets using Hybrid BM25 + Vector ranking or keyword search.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct SearchSnippetsTool {
|
|
||||||
/// The search query.
|
|
||||||
pub query: String,
|
|
||||||
/// Search mode: 'hybrid' (BM25 + Semantic Tags), 'keyword', or 'semantic'. Defaults to 'hybrid'.
|
|
||||||
pub mode: Option<SnippetSearchMode>,
|
|
||||||
/// Optional classification tags to filter by.
|
|
||||||
pub tags: Option<Vec<String>>,
|
|
||||||
/// Limit the number of results to avoid context limit overflow. Defaults to 10.
|
|
||||||
pub limit: Option<usize>,
|
|
||||||
/// Include the full code content of the matched items. If false, returns only IDs and descriptions. Defaults to false.
|
|
||||||
pub include_body: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a stored code snippet.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct DeleteSnippetTool {
|
|
||||||
/// The name of the snippet to delete.
|
|
||||||
pub name: 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 SubgraphFormat {
|
pub enum SubgraphFormat {
|
||||||
@@ -351,15 +183,6 @@ pub struct GetSubgraphTool {
|
|||||||
pub max_tokens: Option<usize>,
|
pub max_tokens: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search historical error fixes using keyword search or semantic vector similarity and return matching solutions.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct SuggestErrorFixTool {
|
|
||||||
/// The exact stack trace or error signature.
|
|
||||||
pub stack_trace: String,
|
|
||||||
/// Maximum number of suggested fixes to return. Defaults to 3.
|
|
||||||
pub limit: 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 CheckpointAction {
|
pub enum CheckpointAction {
|
||||||
@@ -382,66 +205,6 @@ pub struct ManageCheckpointTool {
|
|||||||
pub namespace: Option<String>,
|
pub namespace: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save a named memory snapshot checkpoint before risky operations or subagent invocations.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct CheckpointStateTool {
|
|
||||||
/// The unique name for this state snapshot checkpoint.
|
|
||||||
pub name: String,
|
|
||||||
/// Optional description of why this checkpoint was created.
|
|
||||||
pub description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restore a previously saved memory snapshot checkpoint.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct RestoreStateTool {
|
|
||||||
/// The name of the checkpoint to restore.
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Log an architectural decision record (ADR).
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct LogDecisionTool {
|
|
||||||
/// The title of the decision.
|
|
||||||
pub title: String,
|
|
||||||
/// The context or problem requiring a decision.
|
|
||||||
pub context: String,
|
|
||||||
/// The decision made.
|
|
||||||
pub decision: String,
|
|
||||||
/// The consequence of the decision.
|
|
||||||
#[serde(alias = "consequences")]
|
|
||||||
pub consequence: String,
|
|
||||||
/// The ID of an older ADR this decision supersedes, if any.
|
|
||||||
pub supersedes: Option<String>,
|
|
||||||
/// Optional project namespace.
|
|
||||||
pub namespace: Option<String>,
|
|
||||||
/// Optional repository name.
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
/// Optional author or architect behind the decision.
|
|
||||||
pub author: Option<String>,
|
|
||||||
/// Optional affected system components or crates.
|
|
||||||
pub affected_components: Option<Vec<String>>,
|
|
||||||
/// Optional alternative designs or libraries considered.
|
|
||||||
pub alternatives_considered: Option<Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Query architectural decision records.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct QueryDecisionsTool {
|
|
||||||
/// Optional search query.
|
|
||||||
pub query: Option<String>,
|
|
||||||
/// Limit the number of results to avoid context limit overflow. Defaults to 5.
|
|
||||||
pub limit: Option<usize>,
|
|
||||||
/// Include the full body/content of the matched ADRs. If false, returns only titles and IDs (recommended for LLMs to prevent context bloat). Defaults to false.
|
|
||||||
pub include_body: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete an architectural decision record (ADR).
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct DeleteDecisionTool {
|
|
||||||
/// The ID of the decision to delete.
|
|
||||||
pub id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Merge two entities in the knowledge graph into one.
|
/// Merge two entities in the knowledge graph into one.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct MergeEntitiesTool {
|
pub struct MergeEntitiesTool {
|
||||||
@@ -455,19 +218,6 @@ pub struct MergeEntitiesTool {
|
|||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct FindOrphansTool {}
|
pub struct FindOrphansTool {}
|
||||||
|
|
||||||
/// Record a user preference or behavior to adapt future interactions.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct LearnPreferenceTool {
|
|
||||||
/// The key for the preference.
|
|
||||||
pub key: String,
|
|
||||||
/// The value of the preference.
|
|
||||||
pub value: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read all learned user preferences.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ReadPreferencesTool {}
|
|
||||||
|
|
||||||
/// Log a complex error and its fix for future reference.
|
/// Log a complex error and its fix for future reference.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct LogErrorFixTool {
|
pub struct LogErrorFixTool {
|
||||||
@@ -497,44 +247,6 @@ pub struct SearchErrorFixesTool {
|
|||||||
/// Include the full solution details. If false, returns only error signatures. Defaults to false.
|
/// Include the full solution details. If false, returns only error signatures. Defaults to false.
|
||||||
pub include_body: Option<bool>,
|
pub include_body: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pin a file to keep it explicitly in the context workspace.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct PinFileTool {
|
|
||||||
/// The namespace to pin the file in.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
/// The absolute path of the file to pin.
|
|
||||||
pub file_path: String,
|
|
||||||
/// The associated git branch, if any.
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
/// Optional reason for pinning this file.
|
|
||||||
pub pin_reason: Option<String>,
|
|
||||||
/// Optional symbol focus or line range.
|
|
||||||
pub symbol_or_line: Option<String>,
|
|
||||||
/// Optional repository name.
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unpin a file from the context workspace.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct UnpinFileTool {
|
|
||||||
/// The namespace the file is pinned in.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
/// The absolute path of the file to unpin.
|
|
||||||
pub file_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List all currently pinned files.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ListPinnedFilesTool {
|
|
||||||
/// Optional namespace to filter by.
|
|
||||||
pub namespace: Option<String>,
|
|
||||||
/// Optional git branch to filter by.
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a summary of the current session.
|
/// Add a summary of the current session.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct AddSessionSummaryTool {
|
pub struct AddSessionSummaryTool {
|
||||||
@@ -544,105 +256,6 @@ pub struct AddSessionSummaryTool {
|
|||||||
#[serde(default = "crate::models::default_namespace")]
|
#[serde(default = "crate::models::default_namespace")]
|
||||||
pub namespace: String,
|
pub namespace: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a timeline of major project events.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub struct GetProjectTimelineTool {
|
|
||||||
/// Optional namespace to restrict the timeline to.
|
|
||||||
pub namespace: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Leave a memo for the next session or agent.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct LeaveHandoffMemoTool {
|
|
||||||
/// The content of the memo.
|
|
||||||
pub content: String,
|
|
||||||
/// The namespace to leave the memo in.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read pending handoff memos.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ReadHandoffMemosTool {
|
|
||||||
/// Optional namespace to restrict the read to.
|
|
||||||
pub namespace: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear handoff memos after reading.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ClearHandoffMemosTool {
|
|
||||||
/// Array of memo IDs to clear.
|
|
||||||
pub ids: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the environment fingerprint (e.g., OS, tool versions).
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct UpdateEnvFingerprintTool {
|
|
||||||
/// The namespace to update the fingerprint for.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
/// A map of tool names to their versions.
|
|
||||||
pub tool_versions: std::collections::HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read the environment fingerprint.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ReadEnvFingerprintTool {
|
|
||||||
/// The namespace to read the fingerprint for.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Log a required environment variable or configuration.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct LogEnvRequirementTool {
|
|
||||||
/// The namespace.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
/// The environment variable key (e.g., DATABASE_URL).
|
|
||||||
pub key: String,
|
|
||||||
/// A description of what the variable is used for.
|
|
||||||
pub description: String,
|
|
||||||
/// Whether the variable contains a secret.
|
|
||||||
pub is_secret: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a new milestone.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct AddMilestoneTool {
|
|
||||||
/// The title of the milestone.
|
|
||||||
pub title: String,
|
|
||||||
/// The namespace for the milestone.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
/// Optional target completion date.
|
|
||||||
pub target_date: Option<String>,
|
|
||||||
/// Optional description of milestone scope.
|
|
||||||
pub description: Option<String>,
|
|
||||||
/// Optional deliverables or task checklist.
|
|
||||||
pub deliverables: Option<Vec<String>>,
|
|
||||||
/// Optional repository name.
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the status of a milestone.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct UpdateMilestoneTool {
|
|
||||||
/// The ID of the milestone to update.
|
|
||||||
pub id: String,
|
|
||||||
/// The new status (e.g., 'active', 'completed').
|
|
||||||
pub status: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List all milestones.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ListMilestonesTool {
|
|
||||||
/// Optional namespace to restrict the list to.
|
|
||||||
pub namespace: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate a standup report for a specific time window.
|
/// Generate a standup report for a specific time window.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct GenerateStandupReportTool {
|
pub struct GenerateStandupReportTool {
|
||||||
@@ -652,165 +265,6 @@ pub struct GenerateStandupReportTool {
|
|||||||
/// The number of hours to look back for activity.
|
/// The number of hours to look back for activity.
|
||||||
pub hours_lookback: u64,
|
pub hours_lookback: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a new infrastructure environment.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct RegisterEnvironmentTool {
|
|
||||||
/// The namespace.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
/// The name of the environment (e.g., 'staging', 'prod').
|
|
||||||
pub name: String,
|
|
||||||
/// The URL or connection string for the environment.
|
|
||||||
pub url: String,
|
|
||||||
/// A description of the environment.
|
|
||||||
pub description: String,
|
|
||||||
/// Whether a VPN is required to access the environment.
|
|
||||||
pub requires_vpn: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get details about a registered environment.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct GetEnvironmentDetailsTool {
|
|
||||||
/// The namespace to retrieve details for.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an item to the PR checklist.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct AddPrChecklistItemTool {
|
|
||||||
/// The namespace.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
/// The description of the checklist item.
|
|
||||||
pub description: String,
|
|
||||||
/// Optional checklist category (e.g. 'Security', 'Testing', 'Formatting').
|
|
||||||
pub category: Option<String>,
|
|
||||||
/// Optional automated verification command or script.
|
|
||||||
pub automated_check: Option<String>,
|
|
||||||
/// Optional checked state.
|
|
||||||
pub is_checked: Option<bool>,
|
|
||||||
/// Optional repository name.
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the PR checklist.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct GetPrChecklistTool {
|
|
||||||
/// The namespace.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear the PR checklist.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ClearPrChecklistTool {
|
|
||||||
/// The namespace.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Log a technical debt record.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct LogTechDebtTool {
|
|
||||||
/// The namespace.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
/// A description of the technical debt.
|
|
||||||
pub description: String,
|
|
||||||
/// The ideal solution to resolve the debt.
|
|
||||||
pub ideal_solution: String,
|
|
||||||
/// The associated git commit hash, if any.
|
|
||||||
pub git_commit: Option<String>,
|
|
||||||
/// The associated git branch, if any.
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
/// Optional symbol references (e.g. ['MemoryIndex', 'switchTab']) associated with the tech debt.
|
|
||||||
pub symbol_references: Option<Vec<String>>,
|
|
||||||
/// Optional line range (e.g. 'L123-L145') associated with the tech debt.
|
|
||||||
pub line_range: Option<String>,
|
|
||||||
/// Optional debt severity: 'low', 'medium', 'high', 'critical'.
|
|
||||||
pub severity: Option<String>,
|
|
||||||
/// Optional repository name.
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
/// Optional file path where debt exists.
|
|
||||||
pub file_path: Option<String>,
|
|
||||||
/// Optional workaround currently in place.
|
|
||||||
pub workaround: Option<String>,
|
|
||||||
/// Optional effort estimate to fix (e.g. '1 hour', '2 days').
|
|
||||||
pub effort_estimate: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve a technical debt record.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ResolveTechDebtTool {
|
|
||||||
/// The ID of the technical debt record to resolve.
|
|
||||||
pub id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List technical debt records.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ListTechDebtTool {
|
|
||||||
/// The namespace.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
/// Whether to include resolved technical debt in the results.
|
|
||||||
pub include_resolved: bool,
|
|
||||||
/// Optional summary level: 'compact' (IDs and descriptions), 'detailed', or 'full'. Defaults to 'detailed'.
|
|
||||||
pub summary_level: Option<String>,
|
|
||||||
/// Optional token budget cap. Truncates results to stay within max_tokens.
|
|
||||||
pub max_tokens: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Save the current context workspace.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct SaveContextWorkspaceTool {
|
|
||||||
/// The namespace for the workspace.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
/// The name to save the workspace as.
|
|
||||||
pub name: String,
|
|
||||||
/// Array of pinned file paths.
|
|
||||||
pub pinned_files: Vec<String>,
|
|
||||||
/// Array of active task IDs.
|
|
||||||
pub active_task_ids: Vec<String>,
|
|
||||||
/// Optional description of the context workspace purpose.
|
|
||||||
pub description: Option<String>,
|
|
||||||
/// Optional git branch associated with this workspace.
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
/// Optional repository name.
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
/// Optional VCS revision or commit hash.
|
|
||||||
pub vcs_revision: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load a saved context workspace.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct LoadContextWorkspaceTool {
|
|
||||||
/// The namespace of the workspace.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
/// The name of the workspace to load.
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List all saved context workspaces.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ListContextWorkspacesTool {
|
|
||||||
/// The namespace to list workspaces for.
|
|
||||||
pub namespace: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a saved context workspace.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct DeleteContextWorkspaceTool {
|
|
||||||
/// The namespace of the workspace.
|
|
||||||
#[serde(default = "crate::models::default_namespace")]
|
|
||||||
pub namespace: String,
|
|
||||||
/// The name of the workspace to delete.
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Search across all memory stores (Graph, Tasks, Snippets, ADRs, Error Fixes, Tech Debt) using Hybrid Reciprocal Rank Fusion (BM25 + Dense Vectors).
|
/// Search across all memory stores (Graph, Tasks, Snippets, ADRs, Error Fixes, Tech Debt) using Hybrid Reciprocal Rank Fusion (BM25 + Dense Vectors).
|
||||||
/// Highly recommended for discovery. Supports Lucene query syntax.
|
/// Highly recommended for discovery. Supports Lucene query syntax.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
@@ -905,16 +359,6 @@ pub struct QueryHypothesesTool {
|
|||||||
/// Optional search query text.
|
/// Optional search query text.
|
||||||
pub query: Option<String>,
|
pub query: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compare two saved context workspaces and return a structured diff of pinned files, active tasks, and metadata.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct DiffContextWorkspacesTool {
|
|
||||||
/// The name of the first context workspace.
|
|
||||||
pub workspace_a: String,
|
|
||||||
/// The name of the second context workspace.
|
|
||||||
pub workspace_b: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -990,20 +434,6 @@ pub struct SemanticCodeSearchTool {
|
|||||||
pub struct ReadDirectoryArchitectureTool {
|
pub struct ReadDirectoryArchitectureTool {
|
||||||
pub directory: String,
|
pub directory: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a point-in-time state snapshot of memory stores for safety.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct CreateSnapshotTool {
|
|
||||||
pub description: String,
|
|
||||||
pub namespace: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Restore a point-in-time state snapshot of memory stores.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct RestoreSnapshotTool {
|
|
||||||
pub snapshot_id: 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 SubagentNamespaceAction {
|
pub enum SubagentNamespaceAction {
|
||||||
@@ -1022,19 +452,6 @@ pub struct ManageSubagentNamespaceTool {
|
|||||||
/// For 'condense': whether to auto-purge the subagent namespace after promotion. Defaults to true.
|
/// For 'condense': whether to auto-purge the subagent namespace after promotion. Defaults to true.
|
||||||
pub purge_after_promotion: Option<bool>,
|
pub purge_after_promotion: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an isolated memory namespace for a subagent session.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct CreateSubagentNamespaceTool {
|
|
||||||
pub subagent_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Purge an isolated subagent memory namespace after session completion.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct PurgeSubagentNamespaceTool {
|
|
||||||
pub subagent_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find all source locations and AST chunks where a specific symbol (function, struct, method, type) is referenced or called.
|
/// Find all source locations and AST chunks where a specific symbol (function, struct, method, type) is referenced or called.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct FindSymbolReferencesTool {
|
pub struct FindSymbolReferencesTool {
|
||||||
@@ -1068,7 +485,7 @@ pub struct QueryGitDiffsTool {
|
|||||||
pub limit: Option<usize>,
|
pub limit: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Promote a transient sticky note or active task observation into a permanent Knowledge Graph entity.
|
/// Promote a task observation or finding into a permanent Knowledge Graph entity.
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct PromoteToEntityTool {
|
pub struct PromoteToEntityTool {
|
||||||
/// Note text or content to promote.
|
/// Note text or content to promote.
|
||||||
@@ -1090,17 +507,7 @@ pub struct AnalyzeImpactTool {
|
|||||||
/// Optional file path.
|
/// Optional file path.
|
||||||
pub file_path: Option<String>,
|
pub file_path: Option<String>,
|
||||||
}
|
}
|
||||||
|
/// Get a consolidated 1-page executive summary of current active branch, active task, open tech debt, and diagnostic hypotheses in 1 turn.
|
||||||
/// Auto-condense a subagent's memory namespace and promote its high-value entities/relations to the global Knowledge Graph.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct CondenseSubagentNamespaceTool {
|
|
||||||
/// The subagent namespace ID to consolidate.
|
|
||||||
pub subagent_id: String,
|
|
||||||
/// Whether to auto-purge the subagent namespace after promotion. Defaults to true.
|
|
||||||
pub purge_after_promotion: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a consolidated 1-page executive summary of current active branch, active task, pinned files, open tech debt, and diagnostic hypotheses in 1 turn.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||||
pub struct GetPreflightContextTool {
|
pub struct GetPreflightContextTool {
|
||||||
/// Optional namespace. Defaults to 'default'.
|
/// Optional namespace. Defaults to 'default'.
|
||||||
@@ -1163,18 +570,6 @@ pub struct AutoSessionCheckpointTool {
|
|||||||
#[serde(default = "crate::models::default_namespace")]
|
#[serde(default = "crate::models::default_namespace")]
|
||||||
pub namespace: String,
|
pub namespace: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hybrid BM25 & semantic tag search across stored code snippets, returning ranked code patterns with relevance scores.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct SearchSnippetsHybridTool {
|
|
||||||
/// Query string (e.g. 'rate limiter', 'websocket client', 'shutdown poll').
|
|
||||||
pub query: String,
|
|
||||||
/// Optional tags to filter by.
|
|
||||||
pub tags: Option<Vec<String>>,
|
|
||||||
/// Maximum number of snippets to return. Defaults to 10.
|
|
||||||
pub limit: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consolidated Smart Management Tools
|
// Consolidated Smart Management Tools
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||||
@@ -1322,124 +717,6 @@ pub struct MilestonesTool {
|
|||||||
/// Optional repository name.
|
/// Optional repository name.
|
||||||
pub repo_name: Option<String>,
|
pub repo_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum PinnedFileAction {
|
|
||||||
#[serde(alias = "pin", alias = "PIN", alias = "Pin")]
|
|
||||||
Pin,
|
|
||||||
#[serde(alias = "unpin", alias = "UNPIN", alias = "Unpin")]
|
|
||||||
Unpin,
|
|
||||||
#[serde(alias = "list", alias = "LIST", alias = "List")]
|
|
||||||
List,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pinned working set files management (pin, unpin, list).
|
|
||||||
/// Actions:
|
|
||||||
/// - 'pin': Pin file to active focus set. Required: path. Optional: namespace.
|
|
||||||
/// - 'unpin': Unpin file from focus set. Required: path. Optional: namespace.
|
|
||||||
/// - 'list': List pinned files. Optional: namespace.
|
|
||||||
/// Next steps on error: Provide exact file path for pin/unpin operations.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct PinnedFilesTool {
|
|
||||||
/// Action to perform: 'pin', 'unpin', or 'list'.
|
|
||||||
pub action: PinnedFileAction,
|
|
||||||
/// Absolute or workspace relative file path (required for 'pin' and 'unpin').
|
|
||||||
pub path: Option<String>,
|
|
||||||
/// Optional namespace.
|
|
||||||
pub namespace: Option<String>,
|
|
||||||
/// Optional reason for pinning this file.
|
|
||||||
pub pin_reason: Option<String>,
|
|
||||||
/// Optional symbol focus or line range.
|
|
||||||
pub symbol_or_line: Option<String>,
|
|
||||||
/// Optional repository name.
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
/// Optional git branch.
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum ContextWorkspaceAction {
|
|
||||||
#[serde(alias = "save", alias = "SAVE", alias = "Save")]
|
|
||||||
Save,
|
|
||||||
#[serde(alias = "load", alias = "LOAD", alias = "Load")]
|
|
||||||
Load,
|
|
||||||
#[serde(alias = "list", alias = "LIST", alias = "List")]
|
|
||||||
List,
|
|
||||||
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
|
|
||||||
Delete,
|
|
||||||
#[serde(alias = "diff", alias = "DIFF", alias = "Diff")]
|
|
||||||
Diff,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Context workspace snapshots management (save, load, list, delete, diff).
|
|
||||||
/// Actions:
|
|
||||||
/// - 'save': Save current pinned files & task IDs. Required: name. Optional: pinned_files, active_task_ids, namespace.
|
|
||||||
/// - 'load': Restore saved workspace state. Required: name. Optional: namespace.
|
|
||||||
/// - 'list': List saved context workspaces. Optional: namespace.
|
|
||||||
/// - 'delete': Delete saved workspace. Required: name. Optional: namespace.
|
|
||||||
/// - 'diff': Compare two context workspaces. Required: name, other_name. Optional: namespace.
|
|
||||||
/// Next steps on error: Check workspace name or list existing workspaces first.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct ContextWorkspacesTool {
|
|
||||||
/// Action to perform: 'save', 'load', 'list', 'delete', or 'diff'.
|
|
||||||
pub action: ContextWorkspaceAction,
|
|
||||||
/// Workspace name (required for 'save', 'load', 'delete', 'diff').
|
|
||||||
pub name: Option<String>,
|
|
||||||
/// Second workspace name (required for 'diff').
|
|
||||||
pub other_name: Option<String>,
|
|
||||||
/// Pinned file paths (for 'save').
|
|
||||||
pub pinned_files: Option<Vec<String>>,
|
|
||||||
/// Active task IDs (for 'save').
|
|
||||||
pub active_task_ids: Option<Vec<String>>,
|
|
||||||
/// Optional namespace.
|
|
||||||
pub namespace: Option<String>,
|
|
||||||
/// Optional description of the context workspace purpose.
|
|
||||||
pub description: Option<String>,
|
|
||||||
/// Optional git branch associated with this workspace.
|
|
||||||
pub git_branch: Option<String>,
|
|
||||||
/// Optional repository name.
|
|
||||||
pub repo_name: Option<String>,
|
|
||||||
/// Optional VCS revision or commit hash.
|
|
||||||
pub vcs_revision: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum PrChecklistAction {
|
|
||||||
#[serde(alias = "add", alias = "ADD", alias = "Add")]
|
|
||||||
Add,
|
|
||||||
#[serde(alias = "get", alias = "GET", alias = "Get")]
|
|
||||||
Get,
|
|
||||||
#[serde(alias = "clear", alias = "CLEAR", alias = "Clear")]
|
|
||||||
Clear,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pre-commit and Pull Request checklist management (add, get, clear).
|
|
||||||
/// Actions:
|
|
||||||
/// - 'add': Add PR checklist item. Required: description. Optional: namespace.
|
|
||||||
/// - 'get': Get PR checklist items. Optional: namespace.
|
|
||||||
/// - 'clear': Clear PR checklist. Optional: namespace.
|
|
||||||
/// Next steps on error: Provide description for 'add' action.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
|
||||||
pub struct PrChecklistTool {
|
|
||||||
/// Action to perform: 'add', 'get', or 'clear'.
|
|
||||||
pub action: PrChecklistAction,
|
|
||||||
/// Checklist item description (required for 'add').
|
|
||||||
pub description: Option<String>,
|
|
||||||
/// Optional namespace.
|
|
||||||
pub namespace: Option<String>,
|
|
||||||
/// Optional checklist category (e.g. 'Security', 'Testing', 'Formatting').
|
|
||||||
pub category: Option<String>,
|
|
||||||
/// Optional automated verification command or script.
|
|
||||||
pub automated_check: Option<String>,
|
|
||||||
/// Optional checked state.
|
|
||||||
pub is_checked: Option<bool>,
|
|
||||||
/// Optional repository name.
|
|
||||||
pub repo_name: 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 {
|
||||||
@@ -1535,6 +812,12 @@ pub struct DecisionsTool {
|
|||||||
pub affected_components: Option<Vec<String>>,
|
pub affected_components: Option<Vec<String>>,
|
||||||
/// Optional alternative designs or libraries considered.
|
/// Optional alternative designs or libraries considered.
|
||||||
pub alternatives_considered: Option<Vec<String>>,
|
pub alternatives_considered: Option<Vec<String>>,
|
||||||
|
/// Optional superseded ADR ID.
|
||||||
|
pub 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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||||
@@ -1562,8 +845,10 @@ pub struct TechDebtTool {
|
|||||||
#[serde(alias = "tech_debt_id", alias = "debt_id")]
|
#[serde(alias = "tech_debt_id", alias = "debt_id")]
|
||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
/// Tech debt description (required for 'log').
|
/// Tech debt description (required for 'log').
|
||||||
#[serde(alias = "desc", alias = "title")]
|
#[serde(alias = "desc")]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
/// Optional title or summary of tech debt.
|
||||||
|
pub title: Option<String>,
|
||||||
/// Ideal solution (optional for 'log').
|
/// Ideal solution (optional for 'log').
|
||||||
#[serde(alias = "solution", alias = "fix")]
|
#[serde(alias = "solution", alias = "fix")]
|
||||||
pub ideal_solution: Option<String>,
|
pub ideal_solution: Option<String>,
|
||||||
@@ -1591,6 +876,10 @@ pub struct TechDebtTool {
|
|||||||
pub workaround: Option<String>,
|
pub workaround: Option<String>,
|
||||||
/// Optional effort estimate to fix.
|
/// Optional effort estimate to fix.
|
||||||
pub effort_estimate: Option<String>,
|
pub effort_estimate: Option<String>,
|
||||||
|
/// Summary level for list action: 'compact', 'detailed', or 'full'.
|
||||||
|
pub summary_level: Option<String>,
|
||||||
|
/// Optional token budget cap for list output.
|
||||||
|
pub max_tokens: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||||
|
|||||||
+2
-2
@@ -114,6 +114,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut recv_task = tokio::spawn(async move {
|
let mut recv_task = tokio::spawn(async move {
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
let mut stdout = tokio::io::BufWriter::new(tokio::io::stdout());
|
||||||
while let Some(Ok(msg)) = read.next().await {
|
while let Some(Ok(msg)) = read.next().await {
|
||||||
if let tokio_tungstenite::tungstenite::Message::Text(text) = msg {
|
if let tokio_tungstenite::tungstenite::Message::Text(text) = msg {
|
||||||
let log_prefix = logger::extract_log_prefix(&text, true);
|
let log_prefix = logger::extract_log_prefix(&text, true);
|
||||||
@@ -124,8 +126,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
&text[..text.floor_char_boundary(1000)],
|
&text[..text.floor_char_boundary(1000)],
|
||||||
if text.len() > 1000 { "..." } else { "" }
|
if text.len() > 1000 { "..." } else { "" }
|
||||||
);
|
);
|
||||||
use tokio::io::AsyncWriteExt;
|
|
||||||
let mut stdout = tokio::io::stdout();
|
|
||||||
let _ = stdout.write_all(text.as_bytes()).await;
|
let _ = stdout.write_all(text.as_bytes()).await;
|
||||||
let _ = stdout.write_all(b"\n").await;
|
let _ = stdout.write_all(b"\n").await;
|
||||||
let _ = stdout.flush().await;
|
let _ = stdout.flush().await;
|
||||||
|
|||||||
Reference in new issue
Block a user