feat: Add Brain Monitor dashboard and live API stats

This commit is contained in:
Riz Ashraf committed 2026-09-08 15:09:44 +01:00
1 parent 3239e32866
commit 18fd05bb11
10 files changed
+672 -80

No files matched your search

Generated
+67 -1
View File
@@ -1171,6 +1171,12 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "levenshtein_automata" name = "levenshtein_automata"
version = "0.2.1" version = "0.2.1"
@@ -1267,10 +1273,11 @@ dependencies = [
"rust-mcp-sdk", "rust-mcp-sdk",
"serde", "serde",
"serde_json", "serde_json",
"strsim",
"tantivy", "tantivy",
"tokio", "tokio",
"tokio-util", "tokio-util",
"tracing",
"tracing-subscriber",
"uuid", "uuid",
] ]
@@ -1358,6 +1365,15 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "num-bigint" name = "num-bigint"
version = "0.4.8" version = "0.4.8"
@@ -2026,6 +2042,15 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "2.0.1" version = "2.0.1"
@@ -2335,6 +2360,15 @@ dependencies = [
"syn 3.0.5", "syn 3.0.5",
] ]
[[package]]
name = "thread_local"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.55" version = "0.3.55"
@@ -2531,6 +2565,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"nu-ansi-term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
] ]
[[package]] [[package]]
@@ -2642,6 +2702,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
+1
View File
@@ -0,0 +1 @@
Finished `dev` profile [unoptimized + debuginfo] target(s) in 14.64s
+312
View File
@@ -0,0 +1,312 @@
Blocking waiting for file lock on package cache
Blocking waiting for file lock on package cache
Compiling proc-macro2 v1.0.107
Compiling quote v1.0.47
Compiling unicode-ident v1.0.24
Compiling getrandom v0.4.3
Compiling serde_core v1.0.229
Checking windows-link v0.2.1
Compiling cfg-if v1.0.4
Compiling shlex v2.0.1
Compiling find-msvc-tools v0.1.12
Checking smallvec v1.16.0
Checking stable_deref_trait v1.2.1
Checking windows-sys v0.61.2
Checking pin-project-lite v0.2.17
Checking futures-core v0.3.34
Checking memchr v2.8.3
Checking itoa v1.0.18
Compiling serde v1.0.229
Checking bytes v1.12.1
Checking once_cell v1.21.4
Checking futures-sink v0.3.34
Compiling parking_lot_core v0.9.12
Checking scopeguard v1.2.0
Checking slab v0.4.12
Checking mio v1.2.3
Checking socket2 v0.6.5
Checking log v0.4.34
Checking futures-channel v0.3.34
Compiling version_check v0.9.5
Compiling jobserver v0.1.35
Checking lock_api v0.4.14
Compiling zmij v1.0.23
Checking futures-task v0.3.34
Checking futures-io v0.3.34
Compiling num-conv v0.2.2
Compiling time-core v0.1.9
Compiling cc v1.4.5
Compiling autocfg v1.5.1
Checking deranged v0.5.8
Checking http v1.5.0
Checking tracing-core v0.1.36
Compiling syn v3.0.5
Compiling syn v2.0.119
Checking percent-encoding v2.3.2
Checking litemap v0.8.3
Compiling time-macros v0.2.32
Checking writeable v0.6.4
Checking powerfmt v0.2.0
Checking equivalent v1.0.2
Checking parking_lot v0.12.5
Checking utf8_iter v1.0.4
Compiling icu_normalizer_data v2.3.0
Compiling icu_properties_data v2.3.0
Checking fnv v1.0.7
Compiling serde_json v1.0.151
Checking http-body v1.1.0
Checking hashbrown v0.17.1
Compiling httparse v1.10.1
Checking base64 v0.22.1
Compiling synstructure v0.13.2
Checking tower-service v0.3.3
Compiling ring v0.17.14
Compiling num-traits v0.2.19
Checking indexmap v2.14.2
Checking time v0.3.55
Checking try-lock v0.2.5
Compiling serde_derive v1.0.229
Compiling zerovec-derive v0.11.6
Compiling tokio-macros v2.7.2
Compiling zerofrom-derive v0.1.7
Compiling yoke-derive v0.8.2
Compiling displaydoc v0.2.7
Compiling futures-macro v0.3.34
Compiling tracing-attributes v0.1.31
Compiling zeroize_derive v1.5.0
Checking tokio v1.53.1
Compiling libc v0.2.189
Checking atomic-waker v1.1.2
Checking futures-util v0.3.34
Checking subtle v2.6.1
Checking zerofrom v0.1.8
Checking want v0.3.1
Checking zeroize v1.9.0
Checking tracing v0.1.44
Checking form_urlencoded v1.2.2
Checking yoke v0.8.3
Checking getrandom v0.2.17
Checking httpdate v1.0.3
Checking untrusted v0.9.0
Checking rustls-pki-types v1.15.1
Compiling generic-array v0.14.7
Checking sync_wrapper v1.0.2
Checking tower-layer v0.3.3
Checking tokio-util v0.7.19
Checking zerovec v0.11.8
Checking zerotrie v0.2.5
Compiling rustls v0.23.44
Compiling pkg-config v0.3.34
Checking ipnet v2.12.2
Compiling async-trait v0.1.92
Compiling cmake v0.1.58
Compiling cookie v0.18.2
Checking h2 v0.4.19
Checking tinystr v0.8.4
Checking potential_utf v0.1.6
Compiling dunce v1.0.5
Checking mime v0.3.17
Compiling zstd-sys v2.1.0+zstd.1.5.7
Compiling unicase v2.9.0
Checking icu_locale_core v2.3.0
Compiling thiserror v2.0.20
Checking icu_collections v2.3.0
Compiling fs_extra v1.3.0
Compiling crossbeam-utils v0.8.23
Checking typenum v1.20.1
Compiling mime_guess v2.0.5
Checking hyper v1.11.1
Checking tower v0.5.3
Checking rustls-webpki v0.103.15
Checking icu_provider v2.3.1
Compiling thiserror-impl v2.0.20
Compiling aws-lc-sys v0.45.0
Checking http-body-util v0.1.5
Checking icu_normalizer v2.3.0
Checking icu_properties v2.3.0
Checking hyper-util v0.1.20
Compiling litrs v1.0.0
Checking ryu v1.0.23
Checking psl-types v2.0.11
Checking either v1.18.0
Compiling crunchy v0.2.4
Checking idna_adapter v1.2.2
Checking serde_urlencoded v0.7.1
Compiling document-features v0.2.12
Checking idna v1.1.0
Checking webpki-roots v1.0.9
Compiling ident_case v1.0.1
Checking tokio-rustls v0.26.5
Checking byteorder v1.5.0
Checking url v2.5.8
Checking publicsuffix v2.3.0
Checking bitflags v2.13.1
Compiling crossbeam-epoch v0.9.21
Compiling windows_x86_64_msvc v0.52.6
Compiling zstd-safe v7.3.0
Checking regex-syntax v0.8.11
Compiling strsim v0.11.1
Checking hyper-rustls v0.27.9
Checking cookie_store v0.22.1
Checking tower-http v0.6.11
Compiling darling_core v0.24.1
Checking block-buffer v0.10.4
Checking crypto-common v0.1.7
Checking num-integer v0.1.47
Checking ownedbytes v0.9.0
Compiling crossbeam-deque v0.8.8
Compiling typeid v1.0.3
Checking once_cell_polyfill v1.70.2
Compiling aws-lc-rs v1.18.1
Checking utf8parse v0.2.2
Compiling prettyplease v0.3.0
Checking anstyle v1.0.14
Checking tantivy-common v0.11.0
Checking anstyle-parse v1.0.0
Compiling darling_macro v0.24.1
Checking anstyle-wincon v3.0.11
Checking num-bigint v0.4.8
Checking digest v0.10.7
Checking reqwest v0.12.28
Checking bitpacking v0.9.3
Checking futures-executor v0.3.34
Compiling fs-err v3.3.1
Checking anstyle-query v1.1.5
Compiling rayon-core v1.13.0
Checking colorchoice v1.0.5
Checking is_terminal_polyfill v1.70.2
Checking utf8-ranges v1.0.5
Compiling rustversion v1.0.23
Checking untrusted v0.7.1
Compiling erased-serde v0.4.10
Checking futures v0.3.34
Checking tantivy-bitpacker v0.10.0
Checking anstream v1.0.0
Checking zstd v0.13.3
Checking tantivy-fst v0.5.0
Checking simple_asn1 v0.6.4
Checking windows-targets v0.52.6
Compiling darling v0.24.1
Checking uuid v1.26.0
Checking itertools v0.14.0
Checking tokio-stream v0.1.19
Checking rust-mcp-schema v2.0.0
Checking pem v3.0.6
Compiling crc32fast v1.5.1
Checking minimal-lexical v0.2.1
Checking signature v2.2.0
Checking murmurhash32 v0.3.1
Checking clap_lex v1.1.0
Compiling heck v0.5.0
Checking cpufeatures v0.2.17
Checking allocator-api2 v0.2.21
Checking foldhash v0.2.0
Compiling typetag v0.2.23
Compiling winapi v0.3.9
Checking nom v7.1.3
Checking tantivy-stacker v0.7.0
Checking sha2 v0.10.9
Compiling clap_derive v4.6.4
Checking tantivy-sstable v0.7.0
Checking hashbrown v0.16.1
Checking clap_builder v4.6.6
Compiling rust-mcp-macros v2.0.0
Compiling bon-macros v3.10.1
Checking rust-mcp-transport v2.0.0
Checking windows-sys v0.59.0
Checking hmac v0.12.1
Checking regex-automata v0.4.18
Checking axum-core v0.5.6
Checking ordered-float v5.5.0
Compiling typetag-impl v0.2.23
Checking serde_path_to_error v0.1.20
Checking fastrand v2.5.0
Checking inventory v0.3.24
Checking downcast-rs v2.0.2
Checking matchit v0.8.4
Checking fastdivide v0.4.2
Checking option-ext v0.2.0
Checking base64 v0.23.1
Checking fs4 v0.13.1
Checking arc-swap v1.9.2
Checking tantivy-columnar v0.7.0
Checking dirs-sys v0.5.0
Checking tempfile v3.27.0
Checking clap v4.6.6
Checking regex v1.13.1
Checking tantivy-query-grammar v0.26.0
Checking axum v0.8.9
Checking rayon v1.12.0
Checking axum-server v0.8.0
Checking lru v0.16.4
Checking crossbeam-channel v0.5.17
Checking rust-stemmers v1.2.0
Checking sketches-ddsketch v0.4.1
Checking tantivy-tokenizer-api v0.7.0
Checking measure_time v0.9.0
Checking aho-corasick v1.1.5
Checking htmlescape v0.3.1
Checking lz4_flex v0.13.1
Checking bon v3.10.1
Checking memmap2 v0.9.11
Checking datasketches v0.2.0
Checking levenshtein_automata v0.2.1
Checking rustc-hash v2.1.3
Checking oneshot v0.1.13
Checking census v0.4.2
Checking dirs v6.0.0
Checking glob v0.3.4
Checking mcp-memory-stub v0.1.0 (C:\Users\reazul.ashraf\workspace\rust\mcp-memory\stub)
Checking tantivy v0.26.1
warning: this can be `std::io::Error::other(_)`
--> stub\src\main.rs:76:72
|
76 | let stream = resp.bytes_stream().map(|res| res.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#io_other_error
= note: `#[warn(clippy::io_other_error)]` on by default
help: use `std::io::Error::other`
|
76 - let stream = resp.bytes_stream().map(|res| res.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)));
76 + let stream = resp.bytes_stream().map(|res| res.map_err(|e| std::io::Error::other(e)));
|
warning: stripping a prefix manually
--> stub\src\main.rs:93:40
|
93 | println!("{}", &trimmed[6..]);
| ^^^^^^^^^^^^^
|
note: the prefix was tested here
--> stub\src\main.rs:91:24
|
91 | } else if trimmed.starts_with("data: ") {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#manual_strip
= note: `#[warn(clippy::manual_strip)]` on by default
help: try using the `strip_prefix` method
|
91 ~ } else if let Some(<stripped>) = trimmed.strip_prefix("data: ") {
92 | if is_message {
93 ~ println!("{}", <stripped>);
94 | is_message = false;
95 | } else if is_endpoint {
96 ~ let ep = <stripped>;
|
warning: redundant pattern matching
--> stub\src\main.rs:117:16
|
117 | if let Err(_) = std::net::TcpStream::connect(cli.target.replace("http://", "").replace("https://", "")) {
| ^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#redundant_pattern_matching
= note: `#[warn(clippy::redundant_pattern_matching)]` on by default
help: consider using `is_err()`
|
117 - if let Err(_) = std::net::TcpStream::connect(cli.target.replace("http://", "").replace("https://", "")) {
117 + if std::net::TcpStream::connect(cli.target.replace("http://", "").replace("https://", "")).is_err() {
|
warning: `mcp-memory-stub` (bin "mcp-memory-stub") generated 3 warnings (run `cargo clippy --fix --bin "mcp-memory-stub" -p mcp-memory-stub -- ` to apply 2 suggestions)
+59
View File
@@ -0,0 +1,59 @@
import urllib.request
import json
import threading
import sys
import time
sse_url = "http://localhost:3000/sse"
def call_mcp(method, params):
req = urllib.request.Request(sse_url)
with urllib.request.urlopen(req) as response:
endpoint = None
# read the SSE stream
for line_bytes in response:
line = line_bytes.decode('utf-8').strip()
if line.startswith("data: /messages"):
endpoint = "http://localhost:3000" + line.split("data: ")[1].strip()
break
if not endpoint:
print("Failed to get endpoint")
sys.exit(1)
print(f"Got endpoint: {endpoint}")
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": method,
"arguments": params
}
}
post_req = urllib.request.Request(endpoint, data=json.dumps(payload).encode('utf-8'), headers={'Content-Type': 'application/json'})
with urllib.request.urlopen(post_req) as post_resp:
print(f"POST status: {post_resp.status}")
# continue reading SSE for response
for line_bytes in response:
line = line_bytes.decode('utf-8').strip()
if line.startswith("data: "):
data = line[6:]
if data == "ping": continue
try:
msg = json.loads(data)
if msg.get("id") == 1:
print("RESULT:")
print(json.dumps(msg, indent=2))
return
except:
pass
import sys
if __name__ == "__main__":
method = sys.argv[1]
params = json.loads(sys.argv[2])
call_mcp(method, params)
+2 -1
View File
@@ -15,8 +15,9 @@ rust-mcp-axum = "2.0.0"
rust-mcp-sdk = "2.0.0" rust-mcp-sdk = "2.0.0"
serde = { version = "1.0.229", features = ["derive"] } serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151" serde_json = "1.0.151"
strsim = "0.11.1"
tantivy = "0.26.1" tantivy = "0.26.1"
tokio = { version = "1.53.1", features = ["full"] } tokio = { version = "1.53.1", features = ["full"] }
tokio-util = { version = "0.7.19", features = ["io"] } tokio-util = { version = "0.7.19", features = ["io"] }
tracing = "0.1.44"
tracing-subscriber = "0.3.23"
uuid = { version = "1.26.0", features = ["v4"] } uuid = { version = "1.26.0", features = ["v4"] }
+73
View File
@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<title>Brain Monitor</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; padding: 40px; background-color: #f0f2f5; color: #333; }
h1 { color: #2c3e50; text-align: center; margin-bottom: 30px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; max-width: 800px; margin: 0 auto; }
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); text-align: center; transition: transform 0.2s ease; }
.stat-card:hover { transform: translateY(-5px); }
.stat-value { font-size: 2.5em; font-weight: bold; color: #3498db; margin: 10px 0; }
.stat-label { font-size: 1.1em; color: #7f8c8d; text-transform: uppercase; letter-spacing: 1px; }
.status-dot { display: inline-block; width: 10px; height: 10px; background-color: #2ecc71; border-radius: 50%; margin-right: 8px; box-shadow: 0 0 5px #2ecc71; }
.header-bar { max-width: 800px; margin: 0 auto 20px; display: flex; justify-content: space-between; align-items: center; }
</style>
</head>
<body>
<div class="header-bar">
<h1>🧠 Antigravity Brain Monitor</h1>
<div><span class="status-dot"></span>System Online</div>
</div>
<div class="grid">
<div class="stat-card">
<div class="stat-label">Entities</div>
<div class="stat-value" id="val-entities">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Relations</div>
<div class="stat-value" id="val-relations">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Active Tasks</div>
<div class="stat-value" id="val-tasks">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Snippets</div>
<div class="stat-value" id="val-snippets">-</div>
</div>
<div class="stat-card">
<div class="stat-label">Tech Debt</div>
<div class="stat-value" id="val-tech_debts">-</div>
</div>
<div class="stat-card">
<div class="stat-label">ADRs</div>
<div class="stat-value" id="val-adrs">-</div>
</div>
</div>
<script>
async function fetchStats() {
try {
const response = await fetch('/api/stats');
const data = await response.json();
document.getElementById('val-entities').innerText = data.entities;
document.getElementById('val-relations').innerText = data.relations;
document.getElementById('val-tasks').innerText = data.tasks;
document.getElementById('val-snippets').innerText = data.snippets;
document.getElementById('val-tech_debts').innerText = data.tech_debts;
document.getElementById('val-adrs').innerText = data.adrs;
} catch (err) {
console.error("Failed to fetch stats:", err);
}
}
// Initial fetch
fetchStats();
// Poll every 2 seconds
setInterval(fetchStats, 2000);
</script>
</body>
</html>
+63 -71
View File
@@ -120,6 +120,9 @@ impl ServerHandler for MemoryHandler {
} { } {
let entity: Entity = e; let entity: Entity = e;
if !entity.name.is_empty() { if !entity.name.is_empty() {
if let Ok(idx) = self.state.search_index.read() {
let _ = idx.index_entity(&entity);
}
g.entities.insert(entity.name.clone(), entity); g.entities.insert(entity.name.clone(), entity);
} }
} }
@@ -267,29 +270,19 @@ impl ServerHandler for MemoryHandler {
} }
"search_nodes" => { "search_nodes" => {
let req: SearchNodesTool = parse_args(args)?; let req: SearchNodesTool = parse_args(args)?;
let query = req.query.to_lowercase(); let matches = if let Ok(idx) = self.state.search_index.read() {
let tokens: Vec<&str> = query.split_whitespace().collect(); idx.search(&req.query, req.namespace.as_deref()).unwrap_or_default()
} else {
vec![]
};
let mut result = KnowledgeGraph::default(); let mut result = KnowledgeGraph::default();
for (name, e) in self.state.get_full_graph().entities { let full = self.state.get_full_graph();
if let Some(ns) = &req.namespace for (id, doc_type) in matches {
&& e.namespace != *ns if doc_type == "entity" {
{ if let Some(e) = full.entities.get(&id) {
continue; result.entities.insert(id, e.clone());
} }
let name_low = name.to_lowercase();
let type_low = e.entity_type.to_lowercase();
let full_txt = format!(
"{} {} {}",
name_low,
type_low,
e.observations.join(" ").to_lowercase()
);
let matched = full_txt.contains(&query)
|| (!tokens.is_empty() && tokens.iter().all(|&t| full_txt.contains(t)))
|| strsim::jaro_winkler(&name_low, &query) > 0.85
|| strsim::jaro_winkler(&type_low, &query) > 0.85;
if matched {
result.entities.insert(name, e);
} }
} }
let data = serde_json::to_string(&result).unwrap_or_default(); let data = serde_json::to_string(&result).unwrap_or_default();
@@ -456,16 +449,20 @@ impl ServerHandler for MemoryHandler {
.unwrap() .unwrap()
.as_secs(); .as_secs();
let id = uuid::Uuid::new_v4().to_string(); let id = uuid::Uuid::new_v4().to_string();
let task = Task {
id: id.clone(),
title: req.title,
status: "pending".to_string(),
description: req.description,
created_at: now,
updated_at: now,
git_branch: req.git_branch,
};
if let Ok(idx) = self.state.search_index.read() {
let _ = idx.index_task(&task);
}
self.state.tasks.modify(|tasks| { self.state.tasks.modify(|tasks| {
tasks.push(Task { tasks.push(task);
id: id.clone(),
title: req.title,
status: "pending".to_string(),
description: req.description,
created_at: now,
updated_at: now,
git_branch: req.git_branch,
});
}); });
Ok(ServerResult::from(CallToolResult::text_content(vec![ Ok(ServerResult::from(CallToolResult::text_content(vec![
format!("Task added with ID: {}", id).into(), format!("Task added with ID: {}", id).into(),
@@ -1098,47 +1095,42 @@ impl ServerHandler for MemoryHandler {
} }
"omni_search" => { "omni_search" => {
let req: OmniSearchTool = parse_args(args)?; let req: OmniSearchTool = parse_args(args)?;
let matches = if let Ok(idx) = self.state.search_index.read() {
idx.search(&req.query, req.namespace.as_deref()).unwrap_or_default()
} else {
vec![]
};
let mut kg = KnowledgeGraph::default();
let mut tasks = Vec::new();
let mut snippets = Vec::new();
let mut adrs = Vec::new();
let full = self.state.get_full_graph();
for (id, doc_type) in &matches {
if doc_type == "entity" {
if let Some(e) = full.entities.get(id) {
kg.entities.insert(id.clone(), e.clone());
}
}
}
for t in self.state.tasks.read() {
if matches.iter().any(|(id, typ)| id == &t.id && typ == "task") {
tasks.push(t);
}
}
for s in self.state.snippets.read() {
if matches.iter().any(|(id, typ)| id == &s.name && typ == "snippet") {
snippets.push(s);
}
}
for a in self.state.adrs.read() {
if matches.iter().any(|(id, typ)| id == &a.id && typ == "adr") {
adrs.push(a);
}
}
let q = req.query.to_lowercase(); let q = req.query.to_lowercase();
let mut kg = self.state.get_full_graph();
kg.entities.retain(|name, e| {
(req.namespace.is_none() || e.namespace == *req.namespace.as_ref().unwrap())
&& (name.to_lowercase().contains(&q)
|| e.entity_type.to_lowercase().contains(&q)
|| e.observations.join(" ").to_lowercase().contains(&q))
});
let tasks: Vec<_> = self
.state
.tasks
.read()
.into_iter()
.filter(|t| {
t.title.to_lowercase().contains(&q)
|| t.description.to_lowercase().contains(&q)
})
.collect();
let snippets: Vec<_> = self
.state
.snippets
.read()
.into_iter()
.filter(|s| {
s.name.to_lowercase().contains(&q)
|| s.description.to_lowercase().contains(&q)
})
.collect();
let adrs: Vec<_> = self
.state
.adrs
.read()
.into_iter()
.filter(|a| {
a.title.to_lowercase().contains(&q)
|| a.context.to_lowercase().contains(&q)
|| a.decision.to_lowercase().contains(&q)
})
.collect();
let tech_debts: Vec<_> = self let tech_debts: Vec<_> = self
.state .state
.tech_debts .tech_debts
+35 -1
View File
@@ -3,6 +3,7 @@ mod models;
mod state; mod state;
mod store; mod store;
mod tools; mod tools;
mod search;
use crate::handlers::MemoryHandler; use crate::handlers::MemoryHandler;
use crate::models::*; use crate::models::*;
@@ -84,6 +85,7 @@ async fn reconcile_worker(state: Arc<MemoryState>) {
let has_files = glob::glob(&pattern).map(|p| p.count() > 0).unwrap_or(false); let has_files = glob::glob(&pattern).map(|p| p.count() > 0).unwrap_or(false);
if has_local || has_files { if has_local || has_files {
state.apply_sync_write(|_master| {}); state.apply_sync_write(|_master| {});
state.rebuild_index();
} }
let now = SystemTime::now() let now = SystemTime::now()
@@ -152,6 +154,35 @@ fn run_server(state: Arc<MemoryState>) -> SdkResult<()> {
axum::routing::get(|| async move { axum::routing::get(|| async move {
axum::http::StatusCode::OK axum::http::StatusCode::OK
}), }),
).with_route(
"/dashboard",
axum::routing::get(|| async move {
axum::response::Html(include_str!("dashboard.html"))
}),
).with_route(
"/api/stats",
axum::routing::get({
let state_clone = state.clone();
move || async move {
let (entities, relations) = {
let graph = state_clone.get_full_graph();
(graph.entities.len(), graph.relations.len())
};
let tasks = state_clone.tasks.read().len();
let snippets = state_clone.snippets.read().len();
let tech_debts = state_clone.tech_debts.read().len();
let adrs = state_clone.adrs.read().len();
axum::Json(serde_json::json!({
"entities": entities,
"relations": relations,
"tasks": tasks,
"snippets": snippets,
"tech_debts": tech_debts,
"adrs": adrs
}))
}
}),
); );
println!("MCP Memory Server running on http://0.0.0.0:3000/sse"); println!("MCP Memory Server running on http://0.0.0.0:3000/sse");
@@ -190,7 +221,7 @@ fn main() -> SdkResult<()> {
{ {
// This shouldn't be executed on linux natively anymore due to workspace split, // This shouldn't be executed on linux natively anymore due to workspace split,
// but keeping it as a fallback. // but keeping it as a fallback.
let target_url = cli.target.as_deref().unwrap_or("http://host.docker.internal:3000"); let target_url = cli.target.as_deref().unwrap_or("http://127.0.0.1:3000");
let _ = proxy::run_proxy(target_url); let _ = proxy::run_proxy(target_url);
return Ok(()); return Ok(());
} }
@@ -211,6 +242,7 @@ fn main() -> SdkResult<()> {
session_graph: RwLock::new(KnowledgeGraph::default()), session_graph: RwLock::new(KnowledgeGraph::default()),
base_dir: base.clone(), base_dir: base.clone(),
master_cache: RwLock::new((KnowledgeGraph::default(), SystemTime::UNIX_EPOCH)), master_cache: RwLock::new((KnowledgeGraph::default(), SystemTime::UNIX_EPOCH)),
search_index: RwLock::new(crate::search::MemoryIndex::new().unwrap()),
ledger: Store::new(base.join("audit_ledger.json")), ledger: Store::new(base.join("audit_ledger.json")),
sticky: Store::new(base.join("sticky_notes.json")), sticky: Store::new(base.join("sticky_notes.json")),
tasks: Store::new(base.join("tasks.json")), tasks: Store::new(base.join("tasks.json")),
@@ -231,6 +263,8 @@ fn main() -> SdkResult<()> {
context_workspaces: Store::new(base.join("context_workspaces.json")), context_workspaces: Store::new(base.join("context_workspaces.json")),
}); });
state.rebuild_index();
if let Some(command) = cli.command { if let Some(command) = cli.command {
match command { match command {
Commands::Gate { subcmd } => { Commands::Gate { subcmd } => {
+36 -6
View File
@@ -1,7 +1,7 @@
use tantivy::schema::*; use tantivy::schema::*;
use tantivy::{doc, Index, IndexWriter, IndexReader, ReloadPolicy}; use tantivy::{doc, Index, IndexWriter, IndexReader, ReloadPolicy};
use std::sync::Mutex; use std::sync::Mutex;
use crate::models::{Entity, Task, StickyNote, Adr, Snippet}; use crate::models::{Entity, Task, Adr, Snippet};
pub struct MemoryIndex { pub struct MemoryIndex {
index: Index, index: Index,
@@ -71,21 +71,51 @@ impl MemoryIndex {
Ok(()) Ok(())
} }
pub fn search(&self, query: &str, namespace: Option<&str>) -> tantivy::Result<Vec<String>> { pub fn search(&self, query: &str, namespace: Option<&str>) -> tantivy::Result<Vec<(String, String)>> {
let searcher = self.reader.searcher(); let searcher = self.reader.searcher();
let query_parser = tantivy::query::QueryParser::for_index(&self.index, vec![self.title_field, self.body_field]); let query_parser = tantivy::query::QueryParser::for_index(&self.index, vec![self.title_field, self.body_field]);
let q = query_parser.parse_query(query)?; let q = query_parser.parse_query(query)?;
let top_docs = searcher.search(&q, &tantivy::collector::TopDocs::with_limit(50))?; let top_docs = searcher.search(&q, &tantivy::collector::TopDocs::with_limit(50).order_by_score())?;
let mut results = Vec::new(); let mut results = Vec::new();
for (_score, doc_address) in top_docs { for (_score, doc_address) in top_docs {
let retrieved_doc = searcher.doc::<tantivy::TantivyDocument>(doc_address)?; let retrieved_doc = searcher.doc::<tantivy::TantivyDocument>(doc_address)?;
if let Some(val) = retrieved_doc.get_first(self.title_field) { let id = retrieved_doc.get_first(self.id_field).and_then(|v| v.as_str()).unwrap_or("").to_string();
if let Some(t) = val.as_str() { let doc_type = retrieved_doc.get_first(self.type_field).and_then(|v| v.as_str()).unwrap_or("").to_string();
results.push(t.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 {
if doc_ns != ns && doc_ns != "global" {
continue;
} }
} }
results.push((id, doc_type));
} }
Ok(results) Ok(results)
} }
pub fn index_snippet(&self, s: &Snippet) -> tantivy::Result<()> {
let mut writer = self.writer.lock().unwrap();
writer.add_document(doc!(
self.id_field => s.name.clone(),
self.title_field => s.name.clone(),
self.body_field => format!("{} {}", s.language, s.description),
self.type_field => "snippet",
self.namespace_field => "global"
))?;
writer.commit()?;
Ok(())
}
pub fn index_adr(&self, a: &Adr) -> tantivy::Result<()> {
let mut writer = self.writer.lock().unwrap();
writer.add_document(doc!(
self.id_field => a.id.clone(),
self.title_field => a.title.clone(),
self.body_field => format!("{} {} {}", a.context, a.decision, a.consequence),
self.type_field => "adr",
self.namespace_field => "global"
))?;
writer.commit()?;
Ok(())
}
} }
+24
View File
@@ -1,5 +1,6 @@
use crate::models::*; use crate::models::*;
use crate::store::{Store, read_json_file, write_json_atomic}; use crate::store::{Store, read_json_file, write_json_atomic};
use crate::search::MemoryIndex;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
@@ -11,6 +12,7 @@ pub struct MemoryState {
pub master_path: PathBuf, pub master_path: PathBuf,
pub session_graph: RwLock<KnowledgeGraph>, pub session_graph: RwLock<KnowledgeGraph>,
pub master_cache: RwLock<(KnowledgeGraph, SystemTime)>, pub master_cache: RwLock<(KnowledgeGraph, SystemTime)>,
pub search_index: RwLock<MemoryIndex>,
pub ledger: Store<Vec<CodeChange>>, pub ledger: Store<Vec<CodeChange>>,
pub sticky: Store<Vec<StickyNote>>, pub sticky: Store<Vec<StickyNote>>,
pub tasks: Store<Vec<Task>>, pub tasks: Store<Vec<Task>>,
@@ -136,5 +138,27 @@ impl MemoryState {
let _ = fs::remove_file(&lock_path); let _ = fs::remove_file(&lock_path);
} }
pub fn rebuild_index(&self) {
if let Ok(new_idx) = MemoryIndex::new() {
let full = self.get_full_graph();
for (_, e) in &full.entities {
let _ = new_idx.index_entity(e);
}
for t in self.tasks.read() {
let _ = new_idx.index_task(&t);
}
for s in self.snippets.read() {
let _ = new_idx.index_snippet(&s);
}
for a in self.adrs.read() {
let _ = new_idx.index_adr(&a);
}
if let Ok(mut w) = self.search_index.write() {
*w = new_idx;
}
}
}
} }