mirror of
https://github.com/samvallad33/vestige.git
synced 2026-05-11 16:52:36 +02:00
Initial commit: Vestige v1.0.0 - Cognitive memory MCP server
FSRS-6 spaced repetition, spreading activation, synaptic tagging, hippocampal indexing, and 130 years of memory research. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
f9c60eb5a7
169 changed files with 97206 additions and 0 deletions
179
crates/vestige-mcp/src/resources/codebase.rs
Normal file
179
crates/vestige-mcp/src/resources/codebase.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
//! Codebase Resources
|
||||
//!
|
||||
//! codebase:// URI scheme resources for the MCP server.
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use vestige_core::{RecallInput, SearchMode, Storage};
|
||||
|
||||
/// Read a codebase:// resource
|
||||
pub async fn read(storage: &Arc<Mutex<Storage>>, uri: &str) -> Result<String, String> {
|
||||
let path = uri.strip_prefix("codebase://").unwrap_or("");
|
||||
|
||||
// Parse query parameters if present
|
||||
let (path, query) = match path.split_once('?') {
|
||||
Some((p, q)) => (p, Some(q)),
|
||||
None => (path, None),
|
||||
};
|
||||
|
||||
match path {
|
||||
"structure" => read_structure(storage).await,
|
||||
"patterns" => read_patterns(storage, query).await,
|
||||
"decisions" => read_decisions(storage, query).await,
|
||||
_ => Err(format!("Unknown codebase resource: {}", path)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_codebase_param(query: Option<&str>) -> Option<String> {
|
||||
query.and_then(|q| {
|
||||
q.split('&').find_map(|pair| {
|
||||
let (k, v) = pair.split_once('=')?;
|
||||
if k == "codebase" {
|
||||
Some(v.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async fn read_structure(storage: &Arc<Mutex<Storage>>) -> Result<String, String> {
|
||||
let storage = storage.lock().await;
|
||||
|
||||
// Get all pattern and decision nodes to infer structure
|
||||
// NOTE: We run separate queries because FTS5 sanitization removes OR operators
|
||||
// and wraps queries in quotes (phrase search), so "pattern OR decision" would
|
||||
// become a phrase search for "pattern decision" instead of matching either term.
|
||||
let search_terms = ["pattern", "decision", "architecture"];
|
||||
let mut all_nodes = Vec::new();
|
||||
let mut seen_ids = std::collections::HashSet::new();
|
||||
|
||||
for term in &search_terms {
|
||||
let input = RecallInput {
|
||||
query: term.to_string(),
|
||||
limit: 100,
|
||||
min_retention: 0.0,
|
||||
search_mode: SearchMode::Keyword,
|
||||
valid_at: None,
|
||||
};
|
||||
|
||||
for node in storage.recall(input).unwrap_or_default() {
|
||||
if seen_ids.insert(node.id.clone()) {
|
||||
all_nodes.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nodes = all_nodes;
|
||||
|
||||
// Extract unique codebases from tags
|
||||
let mut codebases: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
|
||||
for node in &nodes {
|
||||
for tag in &node.tags {
|
||||
if let Some(codebase) = tag.strip_prefix("codebase:") {
|
||||
codebases.insert(codebase.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pattern_count = nodes.iter().filter(|n| n.node_type == "pattern").count();
|
||||
let decision_count = nodes.iter().filter(|n| n.node_type == "decision").count();
|
||||
|
||||
let result = serde_json::json!({
|
||||
"knownCodebases": codebases.into_iter().collect::<Vec<_>>(),
|
||||
"totalPatterns": pattern_count,
|
||||
"totalDecisions": decision_count,
|
||||
"totalMemories": nodes.len(),
|
||||
"hint": "Use codebase://patterns?codebase=NAME or codebase://decisions?codebase=NAME for specific codebase context",
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn read_patterns(storage: &Arc<Mutex<Storage>>, query: Option<&str>) -> Result<String, String> {
|
||||
let storage = storage.lock().await;
|
||||
let codebase = parse_codebase_param(query);
|
||||
|
||||
let search_query = match &codebase {
|
||||
Some(cb) => format!("pattern codebase:{}", cb),
|
||||
None => "pattern".to_string(),
|
||||
};
|
||||
|
||||
let input = RecallInput {
|
||||
query: search_query,
|
||||
limit: 50,
|
||||
min_retention: 0.0,
|
||||
search_mode: SearchMode::Keyword,
|
||||
valid_at: None,
|
||||
};
|
||||
|
||||
let nodes = storage.recall(input).unwrap_or_default();
|
||||
|
||||
let patterns: Vec<serde_json::Value> = nodes
|
||||
.iter()
|
||||
.filter(|n| n.node_type == "pattern")
|
||||
.map(|n| {
|
||||
serde_json::json!({
|
||||
"id": n.id,
|
||||
"content": n.content,
|
||||
"tags": n.tags,
|
||||
"retentionStrength": n.retention_strength,
|
||||
"createdAt": n.created_at.to_rfc3339(),
|
||||
"source": n.source,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let result = serde_json::json!({
|
||||
"codebase": codebase,
|
||||
"total": patterns.len(),
|
||||
"patterns": patterns,
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn read_decisions(storage: &Arc<Mutex<Storage>>, query: Option<&str>) -> Result<String, String> {
|
||||
let storage = storage.lock().await;
|
||||
let codebase = parse_codebase_param(query);
|
||||
|
||||
let search_query = match &codebase {
|
||||
Some(cb) => format!("decision architecture codebase:{}", cb),
|
||||
None => "decision architecture".to_string(),
|
||||
};
|
||||
|
||||
let input = RecallInput {
|
||||
query: search_query,
|
||||
limit: 50,
|
||||
min_retention: 0.0,
|
||||
search_mode: SearchMode::Keyword,
|
||||
valid_at: None,
|
||||
};
|
||||
|
||||
let nodes = storage.recall(input).unwrap_or_default();
|
||||
|
||||
let decisions: Vec<serde_json::Value> = nodes
|
||||
.iter()
|
||||
.filter(|n| n.node_type == "decision")
|
||||
.map(|n| {
|
||||
serde_json::json!({
|
||||
"id": n.id,
|
||||
"content": n.content,
|
||||
"tags": n.tags,
|
||||
"retentionStrength": n.retention_strength,
|
||||
"createdAt": n.created_at.to_rfc3339(),
|
||||
"source": n.source,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let result = serde_json::json!({
|
||||
"codebase": codebase,
|
||||
"total": decisions.len(),
|
||||
"decisions": decisions,
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
358
crates/vestige-mcp/src/resources/memory.rs
Normal file
358
crates/vestige-mcp/src/resources/memory.rs
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
//! Memory Resources
|
||||
//!
|
||||
//! memory:// URI scheme resources for the MCP server.
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use vestige_core::Storage;
|
||||
|
||||
/// Read a memory:// resource
|
||||
pub async fn read(storage: &Arc<Mutex<Storage>>, uri: &str) -> Result<String, String> {
|
||||
let path = uri.strip_prefix("memory://").unwrap_or("");
|
||||
|
||||
// Parse query parameters if present
|
||||
let (path, query) = match path.split_once('?') {
|
||||
Some((p, q)) => (p, Some(q)),
|
||||
None => (path, None),
|
||||
};
|
||||
|
||||
match path {
|
||||
"stats" => read_stats(storage).await,
|
||||
"recent" => {
|
||||
let n = parse_query_param(query, "n", 10);
|
||||
read_recent(storage, n).await
|
||||
}
|
||||
"decaying" => read_decaying(storage).await,
|
||||
"due" => read_due(storage).await,
|
||||
"intentions" => read_intentions(storage).await,
|
||||
"intentions/due" => read_triggered_intentions(storage).await,
|
||||
"insights" => read_insights(storage).await,
|
||||
"consolidation-log" => read_consolidation_log(storage).await,
|
||||
_ => Err(format!("Unknown memory resource: {}", path)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_query_param(query: Option<&str>, key: &str, default: i32) -> i32 {
|
||||
query
|
||||
.and_then(|q| {
|
||||
q.split('&')
|
||||
.find_map(|pair| {
|
||||
let (k, v) = pair.split_once('=')?;
|
||||
if k == key {
|
||||
v.parse().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.unwrap_or(default)
|
||||
.clamp(1, 100)
|
||||
}
|
||||
|
||||
async fn read_stats(storage: &Arc<Mutex<Storage>>) -> Result<String, String> {
|
||||
let storage = storage.lock().await;
|
||||
let stats = storage.get_stats().map_err(|e| e.to_string())?;
|
||||
|
||||
let embedding_coverage = if stats.total_nodes > 0 {
|
||||
(stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let status = if stats.total_nodes == 0 {
|
||||
"empty"
|
||||
} else if stats.average_retention < 0.3 {
|
||||
"critical"
|
||||
} else if stats.average_retention < 0.5 {
|
||||
"degraded"
|
||||
} else {
|
||||
"healthy"
|
||||
};
|
||||
|
||||
let result = serde_json::json!({
|
||||
"status": status,
|
||||
"totalNodes": stats.total_nodes,
|
||||
"nodesDueForReview": stats.nodes_due_for_review,
|
||||
"averageRetention": stats.average_retention,
|
||||
"averageStorageStrength": stats.average_storage_strength,
|
||||
"averageRetrievalStrength": stats.average_retrieval_strength,
|
||||
"oldestMemory": stats.oldest_memory.map(|d| d.to_rfc3339()),
|
||||
"newestMemory": stats.newest_memory.map(|d| d.to_rfc3339()),
|
||||
"nodesWithEmbeddings": stats.nodes_with_embeddings,
|
||||
"embeddingCoverage": format!("{:.1}%", embedding_coverage),
|
||||
"embeddingModel": stats.embedding_model,
|
||||
"embeddingServiceReady": storage.is_embedding_ready(),
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn read_recent(storage: &Arc<Mutex<Storage>>, limit: i32) -> Result<String, String> {
|
||||
let storage = storage.lock().await;
|
||||
let nodes = storage.get_all_nodes(limit, 0).map_err(|e| e.to_string())?;
|
||||
|
||||
let items: Vec<serde_json::Value> = nodes
|
||||
.iter()
|
||||
.map(|n| {
|
||||
serde_json::json!({
|
||||
"id": n.id,
|
||||
"summary": if n.content.len() > 200 {
|
||||
format!("{}...", &n.content[..200])
|
||||
} else {
|
||||
n.content.clone()
|
||||
},
|
||||
"nodeType": n.node_type,
|
||||
"tags": n.tags,
|
||||
"createdAt": n.created_at.to_rfc3339(),
|
||||
"retentionStrength": n.retention_strength,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let result = serde_json::json!({
|
||||
"total": nodes.len(),
|
||||
"items": items,
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn read_decaying(storage: &Arc<Mutex<Storage>>) -> Result<String, String> {
|
||||
let storage = storage.lock().await;
|
||||
|
||||
// Get nodes with low retention (below 0.5)
|
||||
let all_nodes = storage.get_all_nodes(100, 0).map_err(|e| e.to_string())?;
|
||||
|
||||
let mut decaying: Vec<_> = all_nodes
|
||||
.into_iter()
|
||||
.filter(|n| n.retention_strength < 0.5)
|
||||
.collect();
|
||||
|
||||
// Sort by retention strength (lowest first)
|
||||
decaying.sort_by(|a, b| {
|
||||
a.retention_strength
|
||||
.partial_cmp(&b.retention_strength)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
|
||||
let items: Vec<serde_json::Value> = decaying
|
||||
.iter()
|
||||
.take(20)
|
||||
.map(|n| {
|
||||
let days_since_access = (chrono::Utc::now() - n.last_accessed).num_days();
|
||||
serde_json::json!({
|
||||
"id": n.id,
|
||||
"summary": if n.content.len() > 200 {
|
||||
format!("{}...", &n.content[..200])
|
||||
} else {
|
||||
n.content.clone()
|
||||
},
|
||||
"retentionStrength": n.retention_strength,
|
||||
"daysSinceAccess": days_since_access,
|
||||
"lastAccessed": n.last_accessed.to_rfc3339(),
|
||||
"hint": if n.retention_strength < 0.2 {
|
||||
"Critical - review immediately!"
|
||||
} else {
|
||||
"Should be reviewed soon"
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let result = serde_json::json!({
|
||||
"total": decaying.len(),
|
||||
"showing": items.len(),
|
||||
"items": items,
|
||||
"recommendation": if decaying.is_empty() {
|
||||
"All memories are healthy!"
|
||||
} else if decaying.len() > 10 {
|
||||
"Many memories are decaying. Consider reviewing the most important ones."
|
||||
} else {
|
||||
"Some memories need attention. Review to strengthen retention."
|
||||
},
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn read_due(storage: &Arc<Mutex<Storage>>) -> Result<String, String> {
|
||||
let storage = storage.lock().await;
|
||||
let nodes = storage.get_review_queue(20).map_err(|e| e.to_string())?;
|
||||
|
||||
let items: Vec<serde_json::Value> = nodes
|
||||
.iter()
|
||||
.map(|n| {
|
||||
serde_json::json!({
|
||||
"id": n.id,
|
||||
"summary": if n.content.len() > 200 {
|
||||
format!("{}...", &n.content[..200])
|
||||
} else {
|
||||
n.content.clone()
|
||||
},
|
||||
"nodeType": n.node_type,
|
||||
"retentionStrength": n.retention_strength,
|
||||
"difficulty": n.difficulty,
|
||||
"reps": n.reps,
|
||||
"nextReview": n.next_review.map(|d| d.to_rfc3339()),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let result = serde_json::json!({
|
||||
"total": nodes.len(),
|
||||
"items": items,
|
||||
"instruction": "Use mark_reviewed with rating 1-4 to complete review",
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn read_intentions(storage: &Arc<Mutex<Storage>>) -> Result<String, String> {
|
||||
let storage = storage.lock().await;
|
||||
let intentions = storage.get_active_intentions().map_err(|e| e.to_string())?;
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let items: Vec<serde_json::Value> = intentions
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let is_overdue = i.deadline.map(|d| d < now).unwrap_or(false);
|
||||
serde_json::json!({
|
||||
"id": i.id,
|
||||
"description": i.content,
|
||||
"status": i.status,
|
||||
"priority": match i.priority {
|
||||
1 => "low",
|
||||
3 => "high",
|
||||
4 => "critical",
|
||||
_ => "normal",
|
||||
},
|
||||
"createdAt": i.created_at.to_rfc3339(),
|
||||
"deadline": i.deadline.map(|d| d.to_rfc3339()),
|
||||
"isOverdue": is_overdue,
|
||||
"snoozedUntil": i.snoozed_until.map(|d| d.to_rfc3339()),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let overdue_count = items.iter().filter(|i| i["isOverdue"].as_bool().unwrap_or(false)).count();
|
||||
|
||||
let result = serde_json::json!({
|
||||
"total": intentions.len(),
|
||||
"overdueCount": overdue_count,
|
||||
"items": items,
|
||||
"tip": "Use set_intention to add new intentions, complete_intention to mark done",
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn read_triggered_intentions(storage: &Arc<Mutex<Storage>>) -> Result<String, String> {
|
||||
let storage = storage.lock().await;
|
||||
let overdue = storage.get_overdue_intentions().map_err(|e| e.to_string())?;
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let items: Vec<serde_json::Value> = overdue
|
||||
.iter()
|
||||
.map(|i| {
|
||||
let overdue_by = i.deadline.map(|d| {
|
||||
let duration = now - d;
|
||||
if duration.num_days() > 0 {
|
||||
format!("{} days", duration.num_days())
|
||||
} else if duration.num_hours() > 0 {
|
||||
format!("{} hours", duration.num_hours())
|
||||
} else {
|
||||
format!("{} minutes", duration.num_minutes())
|
||||
}
|
||||
});
|
||||
serde_json::json!({
|
||||
"id": i.id,
|
||||
"description": i.content,
|
||||
"priority": match i.priority {
|
||||
1 => "low",
|
||||
3 => "high",
|
||||
4 => "critical",
|
||||
_ => "normal",
|
||||
},
|
||||
"deadline": i.deadline.map(|d| d.to_rfc3339()),
|
||||
"overdueBy": overdue_by,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let result = serde_json::json!({
|
||||
"triggered": items.len(),
|
||||
"items": items,
|
||||
"message": if items.is_empty() {
|
||||
"No overdue intentions!"
|
||||
} else {
|
||||
"These intentions need attention"
|
||||
},
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn read_insights(storage: &Arc<Mutex<Storage>>) -> Result<String, String> {
|
||||
let storage = storage.lock().await;
|
||||
let insights = storage.get_insights(50).map_err(|e| e.to_string())?;
|
||||
|
||||
let pending: Vec<_> = insights.iter().filter(|i| i.feedback.is_none()).collect();
|
||||
let accepted: Vec<_> = insights.iter().filter(|i| i.feedback.as_deref() == Some("accepted")).collect();
|
||||
|
||||
let items: Vec<serde_json::Value> = insights
|
||||
.iter()
|
||||
.map(|i| {
|
||||
serde_json::json!({
|
||||
"id": i.id,
|
||||
"insight": i.insight,
|
||||
"type": i.insight_type,
|
||||
"confidence": i.confidence,
|
||||
"noveltyScore": i.novelty_score,
|
||||
"sourceMemories": i.source_memories,
|
||||
"generatedAt": i.generated_at.to_rfc3339(),
|
||||
"feedback": i.feedback,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let result = serde_json::json!({
|
||||
"total": insights.len(),
|
||||
"pendingReview": pending.len(),
|
||||
"accepted": accepted.len(),
|
||||
"items": items,
|
||||
"tip": "These insights were discovered during memory consolidation",
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
async fn read_consolidation_log(storage: &Arc<Mutex<Storage>>) -> Result<String, String> {
|
||||
let storage = storage.lock().await;
|
||||
let history = storage.get_consolidation_history(20).map_err(|e| e.to_string())?;
|
||||
let last_run = storage.get_last_consolidation().map_err(|e| e.to_string())?;
|
||||
|
||||
let items: Vec<serde_json::Value> = history
|
||||
.iter()
|
||||
.map(|h| {
|
||||
serde_json::json!({
|
||||
"id": h.id,
|
||||
"completedAt": h.completed_at.to_rfc3339(),
|
||||
"durationMs": h.duration_ms,
|
||||
"memoriesReplayed": h.memories_replayed,
|
||||
"connectionsFound": h.connections_found,
|
||||
"connectionsStrengthened": h.connections_strengthened,
|
||||
"connectionsPruned": h.connections_pruned,
|
||||
"insightsGenerated": h.insights_generated,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let result = serde_json::json!({
|
||||
"lastRun": last_run.map(|d| d.to_rfc3339()),
|
||||
"totalRuns": history.len(),
|
||||
"history": items,
|
||||
});
|
||||
|
||||
serde_json::to_string_pretty(&result).map_err(|e| e.to_string())
|
||||
}
|
||||
6
crates/vestige-mcp/src/resources/mod.rs
Normal file
6
crates/vestige-mcp/src/resources/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
//! MCP Resources
|
||||
//!
|
||||
//! Resource implementations for the Vestige MCP server.
|
||||
|
||||
pub mod codebase;
|
||||
pub mod memory;
|
||||
Loading…
Add table
Add a link
Reference in a new issue