From 46d993824f7d7e14767829f190e158cff418ba5d Mon Sep 17 00:00:00 2001 From: Rock Date: Mon, 24 Aug 2026 01:58:39 +0000 Subject: [PATCH] feat: add Obsidian vault projection with Longhorn storage (#13) --- crates/mem-cli/src/http_server.rs | 100 ++++++++++++++++++++++++++++++ k8s/app/deployment.yaml | 3 +- k8s/app/kustomization.yaml | 1 + k8s/app/vault-pvc.yaml | 12 ++++ 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 k8s/app/vault-pvc.yaml diff --git a/crates/mem-cli/src/http_server.rs b/crates/mem-cli/src/http_server.rs index 4aaad22..5358538 100644 --- a/crates/mem-cli/src/http_server.rs +++ b/crates/mem-cli/src/http_server.rs @@ -1,5 +1,6 @@ use actix_web::{web, App, HttpServer, HttpResponse, HttpRequest, middleware::Logger}; use anyhow::Result; +use chrono; use mem_llm::{EmbeddingsClient, RerankClient}; use mem_store::{init_schema, VectorStore}; use serde_json::json; @@ -74,6 +75,7 @@ pub async fn start_server(port: u16, api_key: String, database_url: &str) -> Res .route("/memory/query", web::get().to(query_handler)) .route("/memory/projects", web::get().to(projects_handler)) .route("/memory/skills", web::get().to(skills_handler)) + .route("/memory/vault", web::post().to(vault_handler)) }) .bind(("0.0.0.0", port))? .run() @@ -298,3 +300,101 @@ pub async fn skills_handler( } } } + +/// POST /memory/vault — generate Obsidian vault from memories +pub async fn vault_handler( + req: HttpRequest, + state: web::Data, +) -> HttpResponse { + if let Err(e) = check_auth(&req, &state) { + return e; + } + + let vault_dir = std::env::var("MEM_HOME").unwrap_or_else(|_| "/data".to_string()); + + // Get all projects from database + let projects_result = sqlx::query_as::<_, (String,)>( + "SELECT DISTINCT project FROM memories_l1 ORDER BY project", + ) + .fetch_all(&state.pool) + .await; + + match projects_result { + Ok(projects) => { + let mut generated = 0; + let mut errors = Vec::new(); + + for (project,) in projects { + let project_vault_dir = format!("{}/vault/{}", vault_dir, project); + + // Create project directory + if let Err(e) = std::fs::create_dir_all(&project_vault_dir) { + errors.push(format!("Failed to create {}: {}", project_vault_dir, e)); + continue; + } + + // Get all L1 memories for this project + let l1s_result = sqlx::query_as::<_, (String, String, String)>( + "SELECT id, query_id, content FROM memories_l1 WHERE project = $1 ORDER BY updated_at DESC", + ) + .bind(&project) + .fetch_all(&state.pool) + .await; + + match l1s_result { + Ok(l1s) => { + for (id, query_id, content) in l1s { + let filename = format!("{}/{}.md", project_vault_dir, query_id); + let note = format!( + "---\nproject: {}\nlevel: L1\nquery_id: {}\nid: {}\nupdated: {}\n---\n\n{}", + project, + query_id, + id, + chrono::Utc::now().to_rfc3339(), + content + ); + if std::fs::write(&filename, note).is_ok() { + generated += 1; + } + } + } + Err(e) => { + errors.push(format!("Failed to fetch L1s for {}: {}", project, e)); + } + } + + // Get L2 synthesis + let l2_result = sqlx::query_as::<_, (String,)>( + "SELECT content FROM memories_l2 WHERE project = $1", + ) + .bind(&project) + .fetch_optional(&state.pool) + .await; + + if let Ok(Some((content,))) = l2_result { + let filename = format!("{}/index.md", project_vault_dir); + let note = format!( + "---\nproject: {}\nlevel: L2\ntitle: {} Synthesis\nupdated: {}\n---\n\n{}", + project, + project, + chrono::Utc::now().to_rfc3339(), + content + ); + if std::fs::write(&filename, note).is_ok() { + generated += 1; + } + } + } + + HttpResponse::Ok().json(json!({ + "status": "generated", + "vault_dir": format!("{}/vault", vault_dir), + "notes_created": generated, + "errors": errors + })) + } + Err(_) => { + HttpResponse::InternalServerError().json(json!({"error": "database_error"})) + } + } +} diff --git a/k8s/app/deployment.yaml b/k8s/app/deployment.yaml index 23d417f..3218ae2 100644 --- a/k8s/app/deployment.yaml +++ b/k8s/app/deployment.yaml @@ -87,7 +87,8 @@ spec: mountPath: /data volumes: - name: data - emptyDir: {} + persistentVolumeClaim: + claimName: poimen-memory-vault # Tolerate control-plane nodes tolerations: - key: node-role.kubernetes.io/control-plane diff --git a/k8s/app/kustomization.yaml b/k8s/app/kustomization.yaml index 3038c22..787cc05 100644 --- a/k8s/app/kustomization.yaml +++ b/k8s/app/kustomization.yaml @@ -2,6 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: poimen resources: + - vault-pvc.yaml - deployment.yaml - service.yaml # Secret managed separately (SealedSecret in homelab) diff --git a/k8s/app/vault-pvc.yaml b/k8s/app/vault-pvc.yaml new file mode 100644 index 0000000..56dd835 --- /dev/null +++ b/k8s/app/vault-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: poimen-memory-vault + namespace: poimen +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 10Gi