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,167 @@
use crate::entity::VaultEntity;
use crate::error::VaultError;
use serde::{de::DeserializeOwned, Serialize};
use std::path::{Path, PathBuf};
/// Read and parse a vault entity from a markdown file.
pub fn read_entity<T: DeserializeOwned + Serialize>(path: &Path) -> Result<VaultEntity<T>, VaultError> {
let content =
std::fs::read_to_string(path).map_err(|e| VaultError::io(e, path))?;
VaultEntity::from_content(path.to_path_buf(), &content)
}
/// Write a vault entity to disk.
pub fn write_entity<T: DeserializeOwned + Serialize>(entity: &VaultEntity<T>) -> Result<(), VaultError> {
let content = entity.to_string()?;
if let Some(parent) = entity.path.parent() {
std::fs::create_dir_all(parent).map_err(|e| VaultError::io(e, parent))?;
}
std::fs::write(&entity.path, content).map_err(|e| VaultError::io(e, &entity.path))
}
/// Move a file from one path to another, creating parent dirs as needed.
pub fn move_file(from: &Path, to: &Path) -> Result<(), VaultError> {
if let Some(parent) = to.parent() {
std::fs::create_dir_all(parent).map_err(|e| VaultError::io(e, parent))?;
}
std::fs::rename(from, to).map_err(|e| VaultError::io(e, from))
}
/// Ensure the standard vault directory structure exists.
pub fn ensure_vault_structure(vault_root: &Path) -> Result<(), VaultError> {
let dirs = [
"agents",
"skills/vault",
"crons/active",
"crons/paused",
"crons/templates",
"todos/harald/urgent",
"todos/harald/open",
"todos/harald/in-progress",
"todos/harald/done",
"todos/agent/queued",
"todos/agent/running",
"todos/agent/done",
"todos/agent/failed",
"knowledge",
"views/pages",
"views/widgets",
"views/layouts",
"views/custom",
"views/notifications",
".vault/logs",
".vault/templates",
];
for dir in &dirs {
let path = vault_root.join(dir);
std::fs::create_dir_all(&path).map_err(|e| VaultError::io(e, &path))?;
}
Ok(())
}
/// List all .md files in a directory (non-recursive).
pub fn list_md_files(dir: &Path) -> Result<Vec<PathBuf>, VaultError> {
if !dir.exists() {
return Ok(vec![]);
}
let mut files = Vec::new();
let entries = std::fs::read_dir(dir).map_err(|e| VaultError::io(e, dir))?;
for entry in entries {
let entry = entry.map_err(|e| VaultError::io(e, dir))?;
let path = entry.path();
if path.is_file() && path.extension().is_some_and(|e| e == "md") {
files.push(path);
}
}
files.sort();
Ok(files)
}
/// List all .md files in a directory tree (recursive).
pub fn list_md_files_recursive(dir: &Path) -> Result<Vec<PathBuf>, VaultError> {
if !dir.exists() {
return Ok(vec![]);
}
let mut files = Vec::new();
walk_dir_recursive(dir, &mut files)?;
files.sort();
Ok(files)
}
fn walk_dir_recursive(dir: &Path, files: &mut Vec<PathBuf>) -> Result<(), VaultError> {
let entries = std::fs::read_dir(dir).map_err(|e| VaultError::io(e, dir))?;
for entry in entries {
let entry = entry.map_err(|e| VaultError::io(e, dir))?;
let path = entry.path();
if path.is_dir() {
// Skip dotfiles/dirs
if path
.file_name()
.is_some_and(|n| n.to_str().is_some_and(|s| s.starts_with('.')))
{
continue;
}
walk_dir_recursive(&path, files)?;
} else if path.is_file() && path.extension().is_some_and(|e| e == "md") {
files.push(path);
}
}
Ok(())
}
/// Convert a string to a URL-safe slug.
pub fn slugify(s: &str) -> String {
s.to_lowercase()
.chars()
.map(|c| {
if c.is_alphanumeric() {
c
} else {
'-'
}
})
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("-")
}
/// Create a timestamped slug: `YYYYMMDD-HHMMSS-slug`
pub fn timestamped_slug(title: &str) -> String {
let now = chrono::Utc::now();
format!("{}-{}", now.format("%Y%m%d-%H%M%S"), slugify(title))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_slugify() {
assert_eq!(slugify("Hello World!"), "hello-world");
assert_eq!(slugify("Review PR #1234"), "review-pr-1234");
assert_eq!(slugify(" spaces everywhere "), "spaces-everywhere");
}
#[test]
fn test_timestamped_slug() {
let slug = timestamped_slug("My Task");
assert!(slug.ends_with("-my-task"));
assert!(slug.len() > 20);
}
#[test]
fn test_ensure_vault_structure() {
let tmp = std::env::temp_dir().join("vault-os-test-structure");
let _ = std::fs::remove_dir_all(&tmp);
ensure_vault_structure(&tmp).unwrap();
assert!(tmp.join("agents").is_dir());
assert!(tmp.join("todos/harald/urgent").is_dir());
assert!(tmp.join("todos/agent/queued").is_dir());
assert!(tmp.join(".vault/logs").is_dir());
let _ = std::fs::remove_dir_all(&tmp);
}
}