Initial implementation of vault-os

Complete implementation across all 13 phases:

- vault-core: types, YAML frontmatter parsing, entity classification,
  filesystem ops, config, prompt composition, validation, search
- vault-watch: filesystem watcher with daemon write filtering, event
  classification
- vault-scheduler: cron engine, process executor, task runner with
  retry logic and concurrency limiting
- vault-api: Axum REST API (15 route modules), WebSocket with broadcast,
  AI assistant proxy, validation, templates
- Dashboard: React + TypeScript + Tailwind v4 with kanban, CodeMirror
  editor, dynamic view system, AI chat sidebar
- Nix flake with dev shell and NixOS module
- Graceful shutdown, inotify overflow recovery, tracing instrumentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Harald Hoyer 2026-03-03 01:21:17 +01:00
commit f820a72b04
123 changed files with 18288 additions and 0 deletions

View file

@ -0,0 +1,126 @@
use crate::error::ApiError;
use crate::state::AppState;
use axum::extract::{Path, Query, State};
use axum::routing::get;
use axum::{Json, Router};
use pulldown_cmark::{html, Parser};
use serde::Deserialize;
use serde_json::{json, Value};
use std::sync::Arc;
use vault_core::entity::VaultEntity;
use vault_core::filesystem;
use vault_core::types::KnowledgeNote;
pub fn routes() -> Router<Arc<AppState>> {
Router::new()
.route("/knowledge", get(list_knowledge))
.route("/knowledge/{*path}", get(get_knowledge))
}
#[derive(Deserialize, Default)]
struct SearchQuery {
#[serde(default)]
q: Option<String>,
#[serde(default)]
tag: Option<String>,
}
async fn list_knowledge(
State(state): State<Arc<AppState>>,
Query(query): Query<SearchQuery>,
) -> Result<Json<Value>, ApiError> {
let dir = state.vault_root.join("knowledge");
let files = filesystem::list_md_files_recursive(&dir).map_err(ApiError::Vault)?;
let mut notes = Vec::new();
for file in files {
// Try parsing with frontmatter
let content = std::fs::read_to_string(&file)
.map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file)))?;
let (title, tags) = if let Ok(entity) = VaultEntity::<KnowledgeNote>::from_content(file.clone(), &content) {
(
entity.frontmatter.title.unwrap_or_else(|| {
file.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("untitled")
.to_string()
}),
entity.frontmatter.tags,
)
} else {
(
file.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("untitled")
.to_string(),
vec![],
)
};
// Apply filters
if let Some(ref q) = query.q {
let q_lower = q.to_lowercase();
if !title.to_lowercase().contains(&q_lower)
&& !content.to_lowercase().contains(&q_lower)
{
continue;
}
}
if let Some(ref tag) = query.tag {
if !tags.iter().any(|t| t == tag) {
continue;
}
}
let relative = file.strip_prefix(&state.vault_root).unwrap_or(&file);
notes.push(json!({
"path": relative,
"title": title,
"tags": tags,
}));
}
Ok(Json(json!(notes)))
}
async fn get_knowledge(
State(state): State<Arc<AppState>>,
Path(path): Path<String>,
) -> Result<Json<Value>, ApiError> {
let file_path = state.vault_root.join("knowledge").join(&path);
if !file_path.exists() {
return Err(ApiError::NotFound(format!("Knowledge note '{}' not found", path)));
}
let content = std::fs::read_to_string(&file_path)
.map_err(|e| ApiError::Vault(vault_core::VaultError::io(e, &file_path)))?;
let (frontmatter, body) = if let Ok(entity) = VaultEntity::<KnowledgeNote>::from_content(file_path.clone(), &content) {
(
json!({
"title": entity.frontmatter.title,
"tags": entity.frontmatter.tags,
"source": entity.frontmatter.source,
"created": entity.frontmatter.created,
"related": entity.frontmatter.related,
}),
entity.body,
)
} else {
(json!({}), content.clone())
};
// Render markdown to HTML
let parser = Parser::new(&body);
let mut html_output = String::new();
html::push_html(&mut html_output, parser);
Ok(Json(json!({
"path": path,
"frontmatter": frontmatter,
"body": body,
"html": html_output,
})))
}