chore: fix clippy warnings, add embedding/omni_patch, update telemetry, and ignore temp files

This commit is contained in:
Riz Ashraf committed 2026-09-29 06:42:29 +01:00
1 parent 495304f3fa
commit 98a1716ac8
23 files changed
+2253 -75

No files matched your search

+2
View File
@@ -24,6 +24,8 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
.route("/ws", get(ws_handler))
.route("/health", get(health_handler))
.route("/nvim/telemetry", post(nvim_telemetry_handler))
.route("/api/terminal/history", get(crate::api::telemetry::get_terminal_history_handler))
.route("/terminal/telemetry", post(crate::api::telemetry::terminal_telemetry_handler))
.route("/events/wait", get(crate::api::events::wait_for_event_handler))
.route("/events", post(crate::api::events::post_event_handler))
.route("/gate/verify", get(gate_verify_handler))
+105
View File
@@ -65,3 +65,108 @@ pub async fn nvim_telemetry_handler(
axum::Json(serde_json::json!({"status": "ok"}))
}
use crate::models::TerminalHistory;
pub async fn get_terminal_history_handler(
State(state): State<Arc<AppState>>,
) -> impl axum::response::IntoResponse {
let history_json = state.handler.state.terminal_history.read_with(|h| serde_json::to_string(h).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], history_json)
}
pub async fn terminal_telemetry_handler(
State(state): State<Arc<AppState>>,
axum::Json(payload): axum::Json<TerminalHistory>,
) -> impl axum::response::IntoResponse {
state.handler.state.terminal_history.modify(|history| {
history.push_front(payload.clone());
if history.len() > 100 {
history.pop_back();
}
});
let ws_msg = serde_json::json!({
"type": "terminal_telemetry",
"data": payload
});
let msg_str = ws_msg.to_string();
let senders: Vec<_> = state
.clients
.read()
.unwrap_or_else(|e| e.into_inner())
.values()
.cloned()
.collect();
for tx in senders {
let _ = tx.try_send(msg_str.clone());
}
axum::Json(serde_json::json!({"status": "ok"}))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::router::MemoryHandler;
use crate::state::MemoryState;
use axum::http::Request;
use tower::ServiceExt;
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use tempfile::tempdir;
use axum::routing::post;
use axum::routing::get;
#[tokio::test]
async fn test_terminal_history() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let app_state = Arc::new(AppState {
handler: Arc::new(MemoryHandler::new(state.clone())),
clients: std::sync::RwLock::new(HashMap::new()),
next_id: AtomicUsize::new(1),
});
let app = axum::Router::new()
.route("/api/terminal/history", get(get_terminal_history_handler))
.route("/terminal/telemetry", post(terminal_telemetry_handler))
.with_state(app_state.clone());
// Create a fake terminal telemetry payload
let payload = TerminalHistory {
command: "echo test".to_string(),
exit_code: 0,
timestamp: 12345,
};
// Post the telemetry
let req = Request::builder()
.method("POST")
.uri("/terminal/telemetry")
.header("content-type", "application/json")
.body(axum::body::Body::from(serde_json::to_string(&payload).unwrap()))
.unwrap();
let response = app.clone().oneshot(req).await.unwrap();
assert_eq!(response.status(), 200);
// Get the telemetry
let req2 = Request::builder()
.method("GET")
.uri("/api/terminal/history")
.body(axum::body::Body::empty())
.unwrap();
let response2 = app.oneshot(req2).await.unwrap();
assert_eq!(response2.status(), 200);
let body_bytes = axum::body::to_bytes(response2.into_body(), usize::MAX).await.unwrap();
let history: Vec<TerminalHistory> = serde_json::from_slice(&body_bytes).unwrap();
assert_eq!(history.len(), 1);
assert_eq!(history[0].command, "echo test");
}
}
+52 -5
View File
@@ -478,6 +478,7 @@
<button class="tab-button" onclick="switchTab('search-tab', this)">Omni-Search</button>
<button class="tab-button" onclick="switchTab('task-tab', this)">Task Board</button>
<button class="tab-button" onclick="switchTab('sticky-tab', this)">Sticky Notes</button>
<button class="tab-button" onclick="switchTab('preferences-tab', this)">Preferences</button>
<button class="tab-button" onclick="switchTab('techdebt-tab', this)">Tech Debt</button>
<button class="tab-button" onclick="switchTab('adrs-tab', this)">Decisions (ADR)</button>
<button class="tab-button" onclick="switchTab('workspaces-tab', this)">Workspaces</button>
@@ -485,6 +486,7 @@
<button class="tab-button" onclick="switchTab('memos-tab', this)">Handoffs</button>
<button class="tab-button" onclick="switchTab('snippets-tab', this)">Snippets</button>
<button class="tab-button" onclick="switchTab('pr-tab', this)">PRs</button>
<button class="tab-button" onclick="switchTab('terminal-tab', this)">Terminal</button>
<button class="tab-button" onclick="switchTab('activity-tab', this)">Live Activity</button>
</div>
@@ -529,6 +531,13 @@
</div>
</div>
<div id="preferences-tab" class="tab-content">
<div class="panel" style="flex:1; display:flex; flex-direction:column; overflow-y:auto;">
<h2>Global Preferences</h2>
<div id="preferences-container" style="display:flex; flex-direction:column; gap:15px; font-size: 0.9em; line-height: 1.4;"></div>
</div>
</div>
<div id="sticky-tab" class="tab-content">
<div class="panel sticky-panel" style="flex:1; display:flex; flex-direction:column;">
<h2>Sticky Notes</h2>
@@ -589,6 +598,13 @@
</div>
</div>
<div id="terminal-tab" class="tab-content">
<div class="panel" style="flex:1; display:flex; flex-direction:column; overflow-y:auto;">
<h2>Terminal History</h2>
<div id="terminal-container" style="display:flex; flex-direction:column; gap:15px;"></div>
</div>
</div>
<div id="activity-tab" class="tab-content">
<div class="panel activity-panel" style="flex:1; display:flex; flex-direction:column;">
<div id="activity-feed" style="flex:1;"></div>
@@ -615,6 +631,8 @@
feed.scrollTop = 0;
} else if (tabId === 'task-tab') {
loadTasks();
} else if (tabId === 'preferences-tab') {
loadPreferences();
} else if (tabId === 'sticky-tab') {
loadStickyNotes();
} else {
@@ -1029,6 +1047,33 @@
};
}
async function loadPreferences() {
try {
const res = await fetch('/api/preferences');
const data = await res.json();
const container = document.getElementById('preferences-container');
container.innerHTML = '';
if (!data || Object.keys(data).length === 0) {
container.innerHTML = '<div style="color:var(--text-secondary); text-align:center; padding:20px;">No global preferences found.</div>';
return;
}
for (const [key, pref] of Object.entries(data)) {
const date = new Date(pref.updated_at * 1000).toLocaleString();
container.innerHTML += `
<div style="background:var(--card-bg); border:1px solid var(--border-color); border-radius:6px; padding:15px; box-shadow:0 1px 3px rgba(0,0,0,0.1);">
<strong style="color:#3498db; display:block; margin-bottom:5px; font-size:1.1em;">${key}</strong>
<div style="margin-bottom:10px;">${pref.value}</div>
<div style="font-size:0.8em; color:var(--text-secondary); text-align:right;">Last Updated: ${date}</div>
</div>`;
}
} catch (e) {
console.error('Error loading preferences:', e);
}
}
async function loadStickyNotes() {
try {
const res = await fetch('/api/sticky');
@@ -1078,7 +1123,10 @@
}
function loadAllExtras() {
loadGenericList('/api/terminal/history', 'terminal-container', item => `
<span style="font-weight:bold; font-size:1.05em; color:var(--text-primary);">${item.command}</span>
<span style="margin-left:15px; font-size:0.85em;"><span style="font-weight:bold; color:var(--text-secondary);">Exit Code:</span> <span style="color:${item.exit_code === 0 ? 'var(--success-color)' : 'var(--error-color)'}; font-weight:bold;">${item.exit_code}</span></span>
`);
loadGenericList('/api/tech_debts', 'techdebt-container', item => `
<strong>${item.id}</strong> ${item.is_resolved ? '<span style="color:var(--success-color);">(Resolved)</span>' : '<span style="color:var(--error-color);">(Open)</span>'}
@@ -1091,9 +1139,9 @@
`);
loadGenericList('/api/error_fixes', 'errorfixes-container', item => `
<strong>${item.error_message || 'Error'}</strong>
<div style="font-size:0.9em; margin-bottom:8px;"><em>Fix:</em> ${item.fix_description || ''}</div>
<div style="font-family:monospace; font-size:0.85em; color:#e74c3c;">Files: ${(item.files_changed || []).join(', ')}</div>
<strong>${item.signature || 'Error'}</strong>
<div style="font-size:0.9em; margin-bottom:8px;"><em>Solution:</em> ${item.solution || ''}</div>
<div style="font-family:monospace; font-size:0.85em; color:#e74c3c;">Commit: ${item.git_commit || 'None'}</div>
`);
loadGenericList('/api/adrs', 'adrs-container', item => `
@@ -1169,4 +1217,3 @@
</script>
</body>
</html>
+42
View File
@@ -0,0 +1,42 @@
#[allow(deprecated)]
use fastembed::{TextEmbedding, EmbeddingModel};
use std::sync::OnceLock;
use std::sync::Mutex;
static EMBEDDING_MODEL: OnceLock<Mutex<TextEmbedding>> = OnceLock::new();
#[allow(deprecated)]
pub fn get_embedding_model() -> Result<&'static Mutex<TextEmbedding>, String> {
if let Some(model) = EMBEDDING_MODEL.get() {
return Ok(model);
}
let options = fastembed::InitOptions::new(EmbeddingModel::AllMiniLML6V2)
.with_show_download_progress(true);
let model = TextEmbedding::try_new(options).map_err(|e| e.to_string())?;
let _ = EMBEDDING_MODEL.set(Mutex::new(model));
Ok(EMBEDDING_MODEL.get().unwrap())
}
pub async fn generate_embedding_async(text: String) -> Result<Vec<f32>, String> {
tokio::task::spawn_blocking(move || {
let model_mutex = get_embedding_model()?;
let mut model = model_mutex.lock().map_err(|e| e.to_string())?;
let embeddings = model.embed(vec![text], None).map_err(|e| e.to_string())?;
Ok(embeddings.into_iter().next().unwrap_or_default())
}).await.map_err(|e| e.to_string())?
}
pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.is_empty() || b.is_empty() || a.len() != b.len() {
return 0.0;
}
let dot_product: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
0.0
} else {
dot_product / (norm_a * norm_b)
}
}
+68 -40
View File
@@ -138,6 +138,8 @@ impl McpTool for LogErrorFixHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution);
let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
state.error_fixes.modify(|fixes| {
fixes.push(crate::models::ErrorFix {
signature: req.signature,
@@ -145,6 +147,7 @@ impl McpTool for LogErrorFixHandler {
timestamp: crate::handlers::utils::now_secs(),
git_commit: req.git_commit,
git_branch: req.git_branch,
embedding,
})
});
Ok("Error fix logged".to_string())
@@ -294,6 +297,8 @@ impl McpTool for LogTechDebtHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
let req: LogTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let text_to_embed = format!("Description: {}\nIdeal Solution: {}", req.description, req.ideal_solution);
let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
state.tech_debts.modify(|debts| {
debts.push(crate::models::TechDebt {
id: uuid::Uuid::new_v4().to_string(),
@@ -304,6 +309,7 @@ impl McpTool for LogTechDebtHandler {
created_at: crate::handlers::utils::now_secs(),
git_commit: req.git_commit,
git_branch: req.git_branch,
embedding,
})
});
Ok("Tech debt logged".to_string())
@@ -404,6 +410,8 @@ impl McpTool for OmniSearchHandler {
}
};
println!("OMNI SEARCH MATCHES: {:?}", matches);
let q = req.query.clone();
let query_emb = crate::embedding::generate_embedding_async(q.clone()).await.unwrap_or_default();
let kg_json = state.read_graph(|full| {
let mut kg_entities = std::collections::HashMap::new();
@@ -466,20 +474,27 @@ impl McpTool for OmniSearchHandler {
})?;
let snippets_json = state.snippets.read_with(|all_snippets| {
let filtered: Vec<_> = all_snippets
.iter()
.filter(|s| matched_snippets.contains(s.name.as_str()))
.take(limit)
.map(|s| {
if !include_body {
let mut summary = s.clone();
summary.code = "".to_string();
summary
} else {
s.clone()
}
})
.collect();
let mut scored: Vec<_> = all_snippets.iter().map(|s| {
let mut score = 0.0;
if matched_snippets.contains(s.name.as_str()) || contains_ignore_ascii_case(&s.name, &q) || contains_ignore_ascii_case(&s.description, &q) {
score += 1.0;
}
if let Some(emb) = &s.embedding {
score += crate::embedding::cosine_similarity(&query_emb, emb);
}
(s, score)
}).filter(|(_, score)| *score > 0.4).collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let filtered: Vec<_> = scored.into_iter().take(limit).map(|(s, _)| {
if !include_body {
let mut summary = s.clone();
summary.code = "".to_string();
summary
} else {
s.clone()
}
}).collect();
serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?;
@@ -505,25 +520,31 @@ impl McpTool for OmniSearchHandler {
let q = req.query;
let tech_debts_json = state.tech_debts.read_with(|debts| {
let filtered: Vec<_> = debts
.iter()
.filter(|d| {
req.namespace.as_ref().is_none_or(|ns| d.namespace == *ns)
&& (contains_ignore_ascii_case(&d.description, &q)
|| contains_ignore_ascii_case(&d.ideal_solution, &q))
})
.take(limit)
.map(|d| {
if !include_body {
let mut summary = d.clone();
summary.description = "".to_string();
summary.ideal_solution = "".to_string();
summary
} else {
d.clone()
}
})
.collect();
let mut scored: Vec<_> = debts.iter().map(|d| {
if req.namespace.as_ref().is_some_and(|ns| d.namespace != *ns) {
return (d, 0.0);
}
let mut score = 0.0;
if contains_ignore_ascii_case(&d.description, &q) || contains_ignore_ascii_case(&d.ideal_solution, &q) {
score += 1.0;
}
if let Some(emb) = &d.embedding {
score += crate::embedding::cosine_similarity(&query_emb, emb);
}
(d, score)
}).filter(|(_, score)| *score > 0.4).collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let filtered: Vec<_> = scored.into_iter().take(limit).map(|(d, _)| {
if !include_body {
let mut summary = d.clone();
summary.description = "".to_string();
summary.ideal_solution = "".to_string();
summary
} else {
d.clone()
}
}).collect();
serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?;
@@ -549,13 +570,19 @@ impl McpTool for OmniSearchHandler {
})?;
let error_fixes_json = state.error_fixes.read_with(|fixes| {
let filtered: Vec<_> = fixes
.iter()
.filter(|f| {
contains_ignore_ascii_case(&f.signature, &q)
|| contains_ignore_ascii_case(&f.solution, &q)
})
.collect();
let mut scored: Vec<_> = fixes.iter().map(|f| {
let mut score = 0.0;
if contains_ignore_ascii_case(&f.signature, &q) || contains_ignore_ascii_case(&f.solution, &q) {
score += 1.0;
}
if let Some(emb) = &f.embedding {
score += crate::embedding::cosine_similarity(&query_emb, emb);
}
(f, score)
}).filter(|(_, score)| *score > 0.4).collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let filtered: Vec<_> = scored.into_iter().take(limit).map(|(f, _)| f.clone()).collect();
serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?;
@@ -800,6 +827,7 @@ mod tests {
updated_at: 0,
git_branch: None,
parent_id: None,
expires_at: None,
dependencies: vec![],
acceptance_criteria: vec![],
};
+3
View File
@@ -25,6 +25,7 @@ impl McpTool for AddStickyNoteHandler {
notes.push(StickyNote {
timestamp: crate::handlers::utils::now_secs(),
content: req.content,
expires_at: None,
});
});
Ok("Sticky note added.".to_string())
@@ -133,6 +134,7 @@ impl McpTool for LeaveHandoffMemoHandler {
content: req.content,
namespace: req.namespace,
timestamp: crate::handlers::utils::now_secs(),
expires_at: None,
})
});
Ok("Handoff memo left".to_string())
@@ -220,6 +222,7 @@ impl McpTool for AddSessionSummaryHandler {
summary: req.summary,
namespace: req.namespace,
timestamp: crate::handlers::utils::now_secs(),
expires_at: None,
})
});
Ok("Session summary added".to_string())
+1
View File
@@ -36,6 +36,7 @@ impl McpTool for AddTaskHandler {
parent_id: req.parent_id,
dependencies: deps,
acceptance_criteria: vec![],
expires_at: None,
};
let idx = state.get_search_index();
drop(idx.index_task(&task));
+3
View File
@@ -110,12 +110,15 @@ impl McpTool for StoreSnippetHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> {
let req: StoreSnippetTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let req_name = req.name.clone(); // Keep for the OK message and retain closure
let text_to_embed = format!("Name: {}\nLanguage: {}\nDescription: {}\nCode: {}", req.name, req.language, req.description, req.code);
let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
let snippet = Snippet {
name: req.name,
language: req.language,
code: req.code,
description: req.description,
updated_at: crate::handlers::utils::now_secs(),
embedding,
};
let idx = state.get_search_index();
+6 -5
View File
@@ -3,21 +3,22 @@
You are Antigravity, connected to the mcp-memory persistence layer. This server provides a persistent knowledge graph, task management, and environment state tracking.
## Core Directives
1. **Always Log Code Changes**: Before completing any coding task or pull request, you MUST call log_code_change with your git branch and git hash to log what you modified.
1. **Always Log Code Changes**: Before completing any coding task or pull request, you MUST invoke the `MemoryLibrarian` subagent to execute `log_code_change`. The server uses `git2` to automatically detect your branch and hash.
2. **Always Log Error Fixes**: If you spend more than one turn fixing an error or bug, call log_error_fix with the stack trace and the exact solution you discovered.
3. **Omni Search**: When starting a session or looking for context, use omni_search to query the knowledge graph, tasks, snippets, ADRs, and tech debt.
4. **Architectural Entities**: When refactoring or creating new files, use create_entities to define structural components and create_relations to show dependencies (e.g. depends_on, implements).
3. **Omni Search (Hybrid Vector)**: When starting a session or looking for context, use omni_search. It uses Vector Embeddings for semantic matching, so you can query conceptually (e.g., "how does auth work") without needing exact keyword matches. It searches the knowledge graph, tasks, snippets, ADRs, and tech debt.
4. **Architectural Entities**: When refactoring or creating new files, delegate to the `MemoryLibrarian` to define structural components via `create_entities` and `create_relations`.
5. **Tech Debt**: Do NOT ignore tech debt. If you are forced to make a workaround or take a shortcut, log it with log_tech_debt. When you fix it later, use resolve_tech_debt.
6. **Decisions**: Use log_decision when you make an architectural choice (e.g. choosing a specific library or pattern).
7. **Workspaces & Context**: When pausing work or shifting focus, use `save_context_workspace` to snapshot your active tasks and pinned files. When resuming, use `list_context_workspaces` and `load_context_workspace`. Keep your working files pinned (`pin_file`)!
8. **Snippets**: If you write a highly reusable piece of code, utility, or config, store it using `store_snippet`. Before writing boilerplate, try `search_snippets`. If outdated, use `delete_snippet`.
9. **PR Checklists**: Aggressively use `add_pr_checklist_item` to build up a list of manual verification steps. Once the PR is merged, use `clear_pr_checklist`.
10. **Tasks & Milestones**: Always track the user's larger goals! Use `add_task` to break down work, `update_task_status` as you progress, and `set_acceptance_criteria` to know when you are done. Use `list_active_tasks` to check what's next.
10. **Tasks & Milestones**: Always track the user's larger goals! Invoke the `ScrumMaster` subagent to manage the board (`add_task`, `update_task_status`, `set_acceptance_criteria`). Use `list_active_tasks` to check what's next.
11. **Handoff Memos**: If you need to stop your session or hand off work to a subagent, use `leave_handoff_memo`. When starting, use `read_handoff_memos` and `clear_handoff_memos` once read.
12. **Preferences**: If the user tells you how they like things done (e.g., "always use fastify", "never use sed"), use `learn_preference`.
13. **Sticky Notes**: Use `add_sticky_note` for ephemeral, temporary scratchpad info (like IP addresses, temporary URLs, or pending command outputs).
Be aggressive about logging state changes in the background! Use the MemoryLibrarian subagent to do this off-thread if needed.
Be aggressive about logging state changes in the background! You MUST delegate this heavy lifting to the `MemoryLibrarian`, `ScrumMaster`, and `DevOpsSRE` subagents in the background.
## Tool Schema Discovery
Do **NOT** grep or search the Rust source code to find tool schemas or arguments. All lazy-loaded MCP tool schemas are automatically cached as JSON files on your disk. To understand a tool`s arguments, directly read `~/.gemini/antigravity-cli/mcp/mcp-memory/<tool_name>.json`. Do not waste tokens inspecting the Rust server code for schemas.
+120
View File
@@ -11,6 +11,7 @@ mod mcp;
mod models;
mod router;
mod search;
pub mod embedding;
mod state;
mod store;
mod tools;
@@ -89,6 +90,28 @@ pub struct AppState {
next_id: AtomicUsize,
}
async fn ttl_sweeper_worker(state: Arc<MemoryState>) {
loop {
tokio::time::sleep(Duration::from_secs(3600)).await;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
state.tasks.modify(|tasks| {
tasks.retain(|t| t.expires_at.is_none_or(|exp| exp > now));
});
state.sticky.modify(|notes| {
notes.retain(|n| n.expires_at.is_none_or(|exp| exp > now));
});
state.handoff_memos.modify(|memos| {
memos.retain(|m| m.expires_at.is_none_or(|exp| exp > now));
});
state.session_summaries.modify(|summaries| {
summaries.retain(|s| s.expires_at.is_none_or(|exp| exp > now));
});
}
}
async fn index_committer_worker(state: Arc<MemoryState>) {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
@@ -103,6 +126,7 @@ async fn index_committer_worker(state: Arc<MemoryState>) {
async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::Error>> {
state.rebuild_index().await;
tokio::spawn(index_committer_worker(Arc::clone(&state)));
tokio::spawn(ttl_sweeper_worker(Arc::clone(&state)));
let app_state = Arc::new(AppState {
handler: Arc::new(MemoryHandler::new(Arc::clone(&state))),
clients: RwLock::new(HashMap::new()),
@@ -132,6 +156,102 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
}
});
// UDP Telemetry Listener on port 3001
let udp_state = Arc::clone(&app_state);
tokio::spawn(async move {
if let Ok(socket) = tokio::net::UdpSocket::bind("127.0.0.1:3001").await {
let mut buf = [0; 4096];
loop {
if let Ok((len, _addr)) = socket.recv_from(&mut buf).await
&& let Ok(payload) = serde_json::from_slice::<crate::models::TerminalHistory>(&buf[..len])
{
udp_state.handler.state.terminal_history.modify(|history| {
history.push_front(payload.clone());
if history.len() > 100 {
history.pop_back();
}
});
let ws_msg = serde_json::json!({
"type": "terminal_telemetry",
"data": payload
});
let msg_str = ws_msg.to_string();
let senders: Vec<_> = udp_state
.clients
.read()
.unwrap_or_else(|e| e.into_inner())
.values()
.cloned()
.collect();
for tx in senders {
let _ = tx.try_send(msg_str.clone());
}
}
}
}
});
// UDP Neovim Telemetry Listener on port 3002
let nvim_udp_state = Arc::clone(&app_state);
tokio::spawn(async move {
if let Ok(socket) = tokio::net::UdpSocket::bind("127.0.0.1:3002").await {
let mut buf = [0; 4096];
loop {
if let Ok((len, _addr)) = socket.recv_from(&mut buf).await
&& let Ok(payload) = serde_json::from_slice::<crate::api::telemetry::NvimTelemetry>(&buf[..len])
{
// 1. Legacy disk write for active_nvim.txt
if payload.event == "FocusGained" || payload.event == "BufEnter" || payload.event == "VimEnter" {
let session = &payload.session_id;
let is_unix_socket = session.starts_with('/') || session.starts_with('~');
if is_unix_socket {
let wsl_path = "\\\\wsl.localhost\\Ubuntu\\home\\riz\\.gemini\\active_nvim.txt";
let _ = tokio::fs::write(wsl_path, session).await;
} else {
let profile = std::env::var("USERPROFILE").unwrap_or_else(|_| "C:\\Users\\reazul.ashraf".into());
let win_path = format!("{}\\.gemini\\active_nvim.txt", profile);
let _ = tokio::fs::write(&win_path, session).await;
}
}
// 2. Broadcast to UI
let ws_msg = serde_json::json!({
"type": "nvim_telemetry",
"data": payload
});
let msg_str = ws_msg.to_string();
let senders: Vec<_> = nvim_udp_state
.clients
.read()
.unwrap_or_else(|e| e.into_inner())
.values()
.cloned()
.collect();
for tx in senders {
let _ = tx.try_send(msg_str.clone());
}
// 3. Event bus trigger for auto-save hook
if payload.event == "BufWritePost"
&& let Some(ref file_path) = payload.file
{
let normalized_file = file_path.replace("\\", "/");
let topic = format!("nvim:save:{}", normalized_file);
let event = crate::state::GenericEvent {
topic,
session_id: Some(payload.session_id.clone()),
payload: serde_json::json!(&payload),
};
let _ = nvim_udp_state.handler.state.event_bus_tx.send(event);
}
}
}
}
});
let app = api::setup::create_router(app_state);
tracing::info!("MCP Memory Server running on http://127.0.0.1:3000/ws");
+22
View File
@@ -14,6 +14,8 @@ pub struct CodeChange {
pub struct StickyNote {
pub timestamp: u64,
pub content: String,
#[serde(default)]
pub expires_at: Option<u64>,
}
pub fn default_namespace() -> String {
"global".to_string()
@@ -63,6 +65,8 @@ pub struct Task {
#[serde(default)]
pub parent_id: Option<String>,
#[serde(default)]
pub expires_at: Option<u64>,
#[serde(default)]
pub dependencies: Vec<String>,
#[serde(default)]
pub acceptance_criteria: Vec<AcceptanceCriteria>,
@@ -74,6 +78,8 @@ pub struct Snippet {
pub code: String,
pub description: String,
pub updated_at: u64,
#[serde(default)]
pub embedding: Option<Vec<f32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Adr {
@@ -105,6 +111,8 @@ pub struct ErrorFix {
pub timestamp: u64,
pub git_commit: Option<String>,
pub git_branch: Option<String>,
#[serde(default)]
pub embedding: Option<Vec<f32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PinnedFile {
@@ -118,12 +126,16 @@ pub struct SessionSummary {
pub timestamp: u64,
pub summary: String,
pub namespace: String,
#[serde(default)]
pub expires_at: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct HandoffMemo {
pub id: String,
pub author: String,
pub content: String,
#[serde(default)]
pub expires_at: Option<u64>,
pub namespace: String,
pub timestamp: u64,
}
@@ -175,6 +187,8 @@ pub struct TechDebt {
pub created_at: u64,
pub git_commit: Option<String>,
pub git_branch: Option<String>,
#[serde(default)]
pub embedding: Option<Vec<f32>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ContextWorkspace {
@@ -196,3 +210,11 @@ pub struct GateRecord {
pub reason: Option<String>,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct TerminalHistory {
pub command: String,
pub exit_code: i32,
pub timestamp: u64,
}
+1
View File
@@ -0,0 +1 @@
pub fn patch() {}
+51
View File
@@ -178,6 +178,28 @@ impl MemoryHandler {
register_resource!(TasksActiveResource);
register_prompt!(AnalyzeTechDebtPrompt);
struct TerminalHistoryResource;
#[async_trait]
impl McpResource for TerminalHistoryResource {
fn uri(&self) -> &'static str {
"memory://terminal/recent"
}
fn name(&self) -> &'static str {
"Terminal History"
}
fn description(&self) -> Option<&'static str> {
Some("Recent terminal execution history and exit codes")
}
async fn read(&self, state: Arc<MemoryState>) -> Result<String, String> {
let state_clone = Arc::clone(&state);
tokio::task::spawn_blocking(move || {
let items = state_clone.terminal_history.cache.read().unwrap();
serde_json::to_string_pretty(&*items).map_err(|e| e.to_string())
})
.await
.unwrap()
}
}
struct PinnedFilesResource;
#[async_trait]
impl McpResource for PinnedFilesResource {
@@ -252,9 +274,38 @@ impl MemoryHandler {
}
}
register_resource!(TerminalHistoryResource);
register_resource!(PinnedFilesResource);
register_resource!(MilestonesResource);
struct ArchiveRoutinePrompt;
#[async_trait]
impl McpPrompt for ArchiveRoutinePrompt {
fn name(&self) -> &'static str {
"archive_routine"
}
fn description(&self) -> Option<&'static str> {
Some("Compress old session summaries into a milestone retrospective")
}
async fn get(
&self,
_args: Value,
_state: Arc<MemoryState>,
) -> Result<serde_json::Value, String> {
Ok(serde_json::json!({"
messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Please read the old session summaries, synthesize them into a dense Milestone Retrospective entity, and then use the appropriate tools to delete the old session summaries."
}
}
]
}))
}
}
register_prompt!(HandoffRoutinePrompt);
register_prompt!(ArchiveRoutinePrompt);
macro_rules! register {
($module:ident::$handler:ident) => {
+2
View File
@@ -335,6 +335,7 @@ mod tests {
acceptance_criteria: vec![],
dependencies: vec![],
parent_id: None,
expires_at: None,
};
let _ = index.index_task(&task).await.unwrap();
@@ -344,6 +345,7 @@ mod tests {
language: "rust".to_string(),
description: "A test snippet".to_string(),
updated_at: 0,
embedding: None,
};
let _ = index.index_snippet(&snippet).await.unwrap();
+3
View File
@@ -35,6 +35,7 @@ pub struct MemoryState {
pub gates: Store<Vec<GateRecord>>,
pub context_workspaces: Store<Vec<ContextWorkspace>>,
pub recent_activities: Store<std::collections::VecDeque<serde_json::Value>>,
pub terminal_history: Store<std::collections::VecDeque<TerminalHistory>>,
pub activity_tx: tokio::sync::broadcast::Sender<String>,
pub event_bus_tx: tokio::sync::broadcast::Sender<GenericEvent>,
}
@@ -79,6 +80,7 @@ impl MemoryState {
gates: Store::new("gates", db.clone()),
context_workspaces: Store::new("context_workspaces", db.clone()),
recent_activities: Store::new("recent_activities", db.clone()),
terminal_history: Store::new("terminal_history", db.clone()),
activity_tx: tokio::sync::broadcast::channel(100).0,
event_bus_tx: tokio::sync::broadcast::channel(1000).0,
}
@@ -200,6 +202,7 @@ mod tests {
acceptance_criteria: vec![],
git_branch: None,
parent_id: None,
expires_at: None,
});
});