Compare commits

...

5 Commits

Author SHA1 Message Date
Test
548e5694ac style: cargo fmt config_seed.rs and getting_started.rs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:46:43 +01:00
Test
0cf8f55a8d fix: clippy doc_lazy_continuation in config_seed.rs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:45:09 +01:00
Test
72b88cef43 docs: add config/ vault type to architecture and abstractions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:42:56 +01:00
Test
8db9f61d5c feat: add Repair Vault command and MCP configFiles
- Add "Repair Vault" to command palette (Cmd+K → "Repair Vault")
- Add "Repair Vault" to macOS Vault menu bar
- Wire repair_vault Tauri command through App → useAppCommands → registry
- Add menu event handler for vault-repair
- Update MCP get_vault_context to include configFiles.agents content
- Add repair_vault mock handler for browser testing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:39:55 +01:00
Test
fb2067ec79 feat: add config/ vault type with agents.md migration
- Add config_seed module: seed_config_files, migrate_agents_md, repair_config_files
- New vaults seed config/agents.md + root AGENTS.md stub (Codex compat)
- Existing vaults auto-migrate root AGENTS.md → config/agents.md on open
- Add type/config.md (icon: gear-six, sidebar label: Config)
- Add "config" → "Config" folder type mapping
- Add repair_vault Tauri command (themes + config files)
- 24 new tests covering seeding, migration, repair, idempotency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 12:35:52 +01:00
14 changed files with 476 additions and 37 deletions

View File

@@ -56,7 +56,8 @@ Entity type is inferred from the folder structure. The vault is organized by typ
├── journal/ → "Journal"
├── essay/ → "Essay"
├── evergreen/ → "Evergreen"
── theme/ → "Theme" ← vault-based themes
── theme/ → "Theme" ← vault-based themes
└── config/ → "Config" ← meta-configuration files (agents.md, etc.)
```
Mapping logic lives in `vault/mod.rs:parse_md_file()`. If a folder doesn't match any known type, the folder name is capitalized and used as-is.

View File

@@ -183,7 +183,7 @@ The MCP server (`mcp-server/`) exposes vault operations as tools for AI assistan
| `delete_note` | `path` | Delete a note file from the vault |
| `link_notes` | `source_path, property, target_title` | Add a target to an array property in frontmatter |
| `list_notes` | `[type_filter], [sort]` | List all notes, optionally filtered by type |
| `vault_context` | — | Get vault summary: entity types + 20 recent notes |
| `vault_context` | — | Get vault summary: entity types + 20 recent notes + configFiles |
| `ui_open_note` | `path` | Open a note in the Laputa UI editor |
| `ui_open_tab` | `path` | Open a note in a new UI tab |
| `ui_highlight` | `element, [path]` | Highlight a UI element (editor, tab, properties, notelist) |
@@ -391,7 +391,7 @@ Backend: `get_vault_pulse` Tauri command parses `git log` with `--name-status`.
```
1. Tauri setup:
a. run_startup_tasks() → purge trash, migrate frontmatter, seed themes, register MCP
a. run_startup_tasks() → purge trash, migrate frontmatter, seed themes, migrate AGENTS.md, seed config files, register MCP
b. spawn_ws_bridge() → start MCP WebSocket bridge (ports 9710, 9711)
2. App mounts
3. useOnboarding checks vault exists → WelcomeScreen if not
@@ -454,6 +454,7 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
| `rename.rs` | `rename_note` — renames files and updates wikilinks across the vault |
| `image.rs` | `save_image` — saves base64-encoded attachments with sanitized filenames |
| `migration.rs` | Frontmatter migration utilities |
| `config_seed.rs` | Seeds `config/` folder, migrates `AGENTS.md`, repairs missing config files |
| `getting_started.rs` | Creates the Getting Started demo vault |
## Rust Backend Modules
@@ -551,6 +552,7 @@ The vault backend (`src-tauri/src/vault/`) is split into focused submodules:
| `create_vault_theme` | Create markdown theme note |
| `ensure_vault_themes` | Seed default themes if missing |
| `restore_default_themes` | Restore all default themes |
| `repair_vault` | Restore default themes + missing config files |
### AI & MCP

View File

@@ -107,11 +107,22 @@ export async function vaultContext(vaultPath) {
notesWithMtime.sort((a, b) => b.mtime - a.mtime)
const recentNotes = notesWithMtime.slice(0, 20).map(({ mtime: _mtime, ...rest }) => rest)
// Read config files for AI agent context
const configFiles = {}
try {
const agentsPath = path.join(vaultPath, 'config', 'agents.md')
const agentsContent = await fs.readFile(agentsPath, 'utf-8')
configFiles.agents = agentsContent
} catch {
// config/agents.md may not exist yet
}
return {
types: [...typesSet].sort(),
noteCount: files.length,
folders: [...foldersSet].sort(),
recentNotes,
configFiles,
vaultPath,
}
}

View File

@@ -511,6 +511,16 @@ pub fn restore_default_themes(vault_path: String) -> Result<String, String> {
theme::restore_default_themes(&vault_path)
}
#[tauri::command]
pub fn repair_vault(vault_path: String) -> Result<String, String> {
let vault_path = expand_tilde(&vault_path);
// Repair themes
theme::restore_default_themes(&vault_path)?;
// Repair config files (config/agents.md, type/config.md, AGENTS.md stub)
vault::repair_config_files(&vault_path)?;
Ok("Vault repaired".to_string())
}
// ── Settings & config commands ──────────────────────────────────────────────
#[tauri::command]

View File

@@ -56,6 +56,11 @@ fn run_startup_tasks() {
// Seed type/theme.md so the Theme type has an icon in the sidebar
let _ = theme::ensure_theme_type_definition(vp_str);
// Migrate root AGENTS.md → config/agents.md (one-time, idempotent)
vault::migrate_agents_md(vp_str);
// Seed config/ with default config files if missing
vault::seed_config_files(vp_str);
// Register Laputa MCP server in Claude Code and Cursor configs
match mcp::register_mcp(vp_str) {
Ok(status) => log::info!("MCP registration: {status}"),
@@ -167,6 +172,7 @@ pub fn run() {
commands::create_vault_theme,
commands::ensure_vault_themes,
commands::restore_default_themes,
commands::repair_vault,
commands::get_vault_config,
commands::save_vault_config
])

View File

@@ -49,6 +49,7 @@ const VAULT_RESOLVE_CONFLICTS: &str = "vault-resolve-conflicts";
const VAULT_VIEW_CHANGES: &str = "vault-view-changes";
const VAULT_INSTALL_MCP: &str = "vault-install-mcp";
const VAULT_REINDEX: &str = "vault-reindex";
const VAULT_REPAIR: &str = "vault-repair";
const CUSTOM_IDS: &[&str] = &[
APP_SETTINGS,
@@ -90,6 +91,7 @@ const CUSTOM_IDS: &[&str] = &[
VAULT_VIEW_CHANGES,
VAULT_INSTALL_MCP,
VAULT_REINDEX,
VAULT_REPAIR,
];
/// IDs of menu items that should be disabled when no note tab is active.
@@ -337,6 +339,9 @@ fn build_vault_menu(app: &App) -> MenuResult {
let reindex = MenuItemBuilder::new("Reindex Vault")
.id(VAULT_REINDEX)
.build(app)?;
let repair = MenuItemBuilder::new("Repair Vault")
.id(VAULT_REPAIR)
.build(app)?;
Ok(SubmenuBuilder::new(app, "Vault")
.item(&open_vault)
@@ -351,6 +356,7 @@ fn build_vault_menu(app: &App) -> MenuResult {
.item(&view_changes)
.separator()
.item(&reindex)
.item(&repair)
.item(&install_mcp)
.build()?)
}

View File

@@ -0,0 +1,355 @@
use std::fs;
use std::path::Path;
use super::getting_started::AGENTS_MD;
/// Content for `type/config.md` — gives the Config type a sidebar icon and label.
const CONFIG_TYPE_DEFINITION: &str = "\
---
Is A: Type
icon: gear-six
color: gray
order: 90
sidebar label: Config
---
# Config
Vault configuration files. These control how AI agents, tools, and other integrations interact with this vault.
";
/// Minimal root `AGENTS.md` stub that redirects to `config/agents.md`.
const AGENTS_MD_STUB: &str = "\
# Agent Instructions
See config/agents.md for vault instructions.
";
/// Seed `config/agents.md` if missing or empty (idempotent, per-file).
/// Also seeds `type/config.md` for sidebar visibility.
pub fn seed_config_files(vault_path: &str) {
let vault = Path::new(vault_path);
let config_dir = vault.join("config");
if fs::create_dir_all(&config_dir).is_err() {
return;
}
let agents_path = config_dir.join("agents.md");
let needs_write =
!agents_path.exists() || fs::metadata(&agents_path).map_or(true, |m| m.len() == 0);
if needs_write {
let _ = fs::write(&agents_path, AGENTS_MD);
log::info!("Seeded config/agents.md");
}
ensure_config_type_definition(vault_path);
}
/// Ensure `type/config.md` exists (gives Config type a sidebar icon/color).
fn ensure_config_type_definition(vault_path: &str) {
let type_dir = Path::new(vault_path).join("type");
if fs::create_dir_all(&type_dir).is_err() {
return;
}
let path = type_dir.join("config.md");
let needs_write = !path.exists() || fs::metadata(&path).map_or(true, |m| m.len() == 0);
if needs_write {
let _ = fs::write(&path, CONFIG_TYPE_DEFINITION);
}
}
/// Migrate root `AGENTS.md` → `config/agents.md` for existing vaults.
///
/// - If root `AGENTS.md` exists and `config/agents.md` does not: move content, write stub.
/// - If root `AGENTS.md` exists and `config/agents.md` also exists: just replace root with stub.
/// - If root `AGENTS.md` doesn't exist: write the stub anyway (for Codex discoverability).
///
/// Always idempotent and silent.
pub fn migrate_agents_md(vault_path: &str) {
let vault = Path::new(vault_path);
let root_agents = vault.join("AGENTS.md");
let config_dir = vault.join("config");
let config_agents = config_dir.join("agents.md");
// Ensure config/ directory exists
if fs::create_dir_all(&config_dir).is_err() {
return;
}
// If root AGENTS.md has real content (not already a stub), migrate it
if root_agents.exists() {
let content = fs::read_to_string(&root_agents).unwrap_or_default();
let is_stub = content.contains("See config/agents.md");
if !is_stub {
// Only move content if config/agents.md doesn't exist yet
let config_needs_write = !config_agents.exists()
|| fs::metadata(&config_agents).map_or(true, |m| m.len() == 0);
if config_needs_write {
let _ = fs::write(&config_agents, &content);
log::info!("Migrated AGENTS.md content to config/agents.md");
}
// Replace root with stub
let _ = fs::write(&root_agents, AGENTS_MD_STUB);
log::info!("Replaced root AGENTS.md with stub pointing to config/agents.md");
}
} else {
// No root AGENTS.md — write stub for Codex discoverability
let _ = fs::write(&root_agents, AGENTS_MD_STUB);
}
}
/// Repair config files: re-create missing `config/agents.md` and `type/config.md`.
/// Called by the "Repair Vault" command. Returns a status message.
pub fn repair_config_files(vault_path: &str) -> Result<String, String> {
let vault = Path::new(vault_path);
// Ensure config/ directory
let config_dir = vault.join("config");
fs::create_dir_all(&config_dir)
.map_err(|e| format!("Failed to create config directory: {e}"))?;
let agents_path = config_dir.join("agents.md");
let root_agents = vault.join("AGENTS.md");
// Step 1: Migrate root AGENTS.md content → config/agents.md if needed
if root_agents.exists() {
let root_content = fs::read_to_string(&root_agents).unwrap_or_default();
let is_stub = root_content.contains("See config/agents.md");
if !is_stub && !root_content.is_empty() {
let config_needs_write =
!agents_path.exists() || fs::metadata(&agents_path).map_or(true, |m| m.len() == 0);
if config_needs_write {
fs::write(&agents_path, &root_content)
.map_err(|e| format!("Failed to migrate AGENTS.md: {e}"))?;
}
fs::write(&root_agents, AGENTS_MD_STUB)
.map_err(|e| format!("Failed to write AGENTS.md stub: {e}"))?;
}
}
// Step 2: Seed config/agents.md with defaults if still missing or empty
let needs_write =
!agents_path.exists() || fs::metadata(&agents_path).map_or(true, |m| m.len() == 0);
if needs_write {
fs::write(&agents_path, AGENTS_MD)
.map_err(|e| format!("Failed to write config/agents.md: {e}"))?;
}
// Step 3: Ensure type/config.md
let type_dir = vault.join("type");
fs::create_dir_all(&type_dir).map_err(|e| format!("Failed to create type directory: {e}"))?;
let config_type_path = type_dir.join("config.md");
let type_needs_write = !config_type_path.exists()
|| fs::metadata(&config_type_path).map_or(true, |m| m.len() == 0);
if type_needs_write {
fs::write(&config_type_path, CONFIG_TYPE_DEFINITION)
.map_err(|e| format!("Failed to write type/config.md: {e}"))?;
}
// Step 4: Ensure root AGENTS.md stub exists
let stub_needs_write = !root_agents.exists()
|| fs::read_to_string(&root_agents).map_or(true, |c| !c.contains("See config/agents.md"));
if stub_needs_write {
fs::write(&root_agents, AGENTS_MD_STUB)
.map_err(|e| format!("Failed to write AGENTS.md stub: {e}"))?;
}
Ok("Config files repaired".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_seed_config_files_creates_dir_and_agents() {
let dir = TempDir::new().unwrap();
let vault = dir.path().join("vault");
fs::create_dir_all(&vault).unwrap();
seed_config_files(vault.to_str().unwrap());
assert!(vault.join("config").is_dir());
assert!(vault.join("config/agents.md").exists());
let content = fs::read_to_string(vault.join("config/agents.md")).unwrap();
assert!(content.contains("Vault Instructions for AI Agents"));
}
#[test]
fn test_seed_config_files_creates_type_definition() {
let dir = TempDir::new().unwrap();
let vault = dir.path().join("vault");
fs::create_dir_all(&vault).unwrap();
seed_config_files(vault.to_str().unwrap());
assert!(vault.join("type/config.md").exists());
let content = fs::read_to_string(vault.join("type/config.md")).unwrap();
assert!(content.contains("Is A: Type"));
assert!(content.contains("icon: gear-six"));
}
#[test]
fn test_seed_config_files_is_idempotent() {
let dir = TempDir::new().unwrap();
let vault = dir.path().join("vault");
fs::create_dir_all(&vault).unwrap();
seed_config_files(vault.to_str().unwrap());
// Customize the file
let custom = "---\nIs A: Config\n---\n# Custom Agents\nMy custom instructions\n";
fs::write(vault.join("config/agents.md"), custom).unwrap();
seed_config_files(vault.to_str().unwrap());
let content = fs::read_to_string(vault.join("config/agents.md")).unwrap();
assert!(
content.contains("Custom Agents"),
"must preserve existing content"
);
}
#[test]
fn test_seed_config_files_reseeds_empty() {
let dir = TempDir::new().unwrap();
let vault = dir.path().join("vault");
let config_dir = vault.join("config");
fs::create_dir_all(&config_dir).unwrap();
fs::write(config_dir.join("agents.md"), "").unwrap();
seed_config_files(vault.to_str().unwrap());
let content = fs::read_to_string(config_dir.join("agents.md")).unwrap();
assert!(content.contains("Vault Instructions for AI Agents"));
}
#[test]
fn test_migrate_agents_md_moves_content() {
let dir = TempDir::new().unwrap();
let vault = dir.path().join("vault");
fs::create_dir_all(&vault).unwrap();
fs::write(vault.join("AGENTS.md"), AGENTS_MD).unwrap();
migrate_agents_md(vault.to_str().unwrap());
// config/agents.md should have the original content
let config_content = fs::read_to_string(vault.join("config/agents.md")).unwrap();
assert!(config_content.contains("Vault Instructions for AI Agents"));
// Root AGENTS.md should be a stub
let root_content = fs::read_to_string(vault.join("AGENTS.md")).unwrap();
assert!(root_content.contains("See config/agents.md"));
assert!(!root_content.contains("## Structure"));
}
#[test]
fn test_migrate_agents_md_preserves_existing_config() {
let dir = TempDir::new().unwrap();
let vault = dir.path().join("vault");
let config_dir = vault.join("config");
fs::create_dir_all(&config_dir).unwrap();
let custom = "# Custom agent instructions\n";
fs::write(config_dir.join("agents.md"), custom).unwrap();
fs::write(vault.join("AGENTS.md"), AGENTS_MD).unwrap();
migrate_agents_md(vault.to_str().unwrap());
// config/agents.md should preserve custom content
let content = fs::read_to_string(config_dir.join("agents.md")).unwrap();
assert!(content.contains("Custom agent instructions"));
// Root should be a stub
let root = fs::read_to_string(vault.join("AGENTS.md")).unwrap();
assert!(root.contains("See config/agents.md"));
}
#[test]
fn test_migrate_agents_md_idempotent_on_stub() {
let dir = TempDir::new().unwrap();
let vault = dir.path().join("vault");
fs::create_dir_all(&vault).unwrap();
fs::write(vault.join("AGENTS.md"), AGENTS_MD_STUB).unwrap();
migrate_agents_md(vault.to_str().unwrap());
// Stub should remain unchanged
let root = fs::read_to_string(vault.join("AGENTS.md")).unwrap();
assert!(root.contains("See config/agents.md"));
}
#[test]
fn test_migrate_agents_md_writes_stub_when_no_root() {
let dir = TempDir::new().unwrap();
let vault = dir.path().join("vault");
fs::create_dir_all(&vault).unwrap();
migrate_agents_md(vault.to_str().unwrap());
assert!(vault.join("AGENTS.md").exists());
let root = fs::read_to_string(vault.join("AGENTS.md")).unwrap();
assert!(root.contains("See config/agents.md"));
}
#[test]
fn test_repair_config_files_creates_all() {
let dir = TempDir::new().unwrap();
let vault = dir.path().join("vault");
fs::create_dir_all(&vault).unwrap();
let msg = repair_config_files(vault.to_str().unwrap()).unwrap();
assert_eq!(msg, "Config files repaired");
assert!(vault.join("config/agents.md").exists());
assert!(vault.join("type/config.md").exists());
assert!(vault.join("AGENTS.md").exists());
let agents = fs::read_to_string(vault.join("config/agents.md")).unwrap();
assert!(agents.contains("Vault Instructions for AI Agents"));
let stub = fs::read_to_string(vault.join("AGENTS.md")).unwrap();
assert!(stub.contains("See config/agents.md"));
}
#[test]
fn test_repair_config_files_preserves_custom_content() {
let dir = TempDir::new().unwrap();
let vault = dir.path().join("vault");
let config_dir = vault.join("config");
fs::create_dir_all(&config_dir).unwrap();
let custom = "# My custom agent config\nDo not overwrite me\n";
fs::write(config_dir.join("agents.md"), custom).unwrap();
fs::write(
vault.join("AGENTS.md"),
"# Agent Instructions\nSee config/agents.md for vault instructions.\n",
)
.unwrap();
repair_config_files(vault.to_str().unwrap()).unwrap();
let content = fs::read_to_string(config_dir.join("agents.md")).unwrap();
assert!(
content.contains("My custom agent config"),
"must preserve existing content"
);
}
#[test]
fn test_repair_config_files_migrates_root_agents() {
let dir = TempDir::new().unwrap();
let vault = dir.path().join("vault");
fs::create_dir_all(&vault).unwrap();
let original = "# My vault agents instructions\nCustom content here\n";
fs::write(vault.join("AGENTS.md"), original).unwrap();
repair_config_files(vault.to_str().unwrap()).unwrap();
// Root should be a stub
let root = fs::read_to_string(vault.join("AGENTS.md")).unwrap();
assert!(root.contains("See config/agents.md"));
// config/agents.md should have the original content
let config = fs::read_to_string(vault.join("config/agents.md")).unwrap();
assert!(config.contains("My vault agents instructions"));
}
}

View File

@@ -18,10 +18,10 @@ struct SampleFile {
content: &'static str,
}
/// Content for the AGENTS.md file written to the vault root.
/// Content for config/agents.md — vault instructions for AI agents.
/// This file has no YAML frontmatter — it is a convention file for AI agents,
/// not a vault note. The vault scanner will still pick it up as a regular entry.
const AGENTS_MD: &str = r#"# AGENTS.md — Vault Instructions for AI Agents
pub(super) const AGENTS_MD: &str = r#"# AGENTS.md — Vault Instructions for AI Agents
This is a [Laputa](https://github.com/refactoring-ai/laputa) vault — a folder of markdown files with YAML frontmatter that form a personal knowledge graph.
@@ -137,6 +137,10 @@ const SAMPLE_FILES: &[SampleFile] = &[
rel_path: "type/theme.md",
content: "---\nIs A: Type\nicon: palette\ncolor: purple\norder: 50\n---\n\n# Theme\n\nA visual theme for Laputa. Each theme defines CSS custom properties that control colors, typography, and spacing.\n",
},
SampleFile {
rel_path: "type/config.md",
content: "---\nIs A: Type\nicon: gear-six\ncolor: gray\norder: 90\nsidebar label: Config\n---\n\n# Config\n\nVault configuration files. These control how AI agents, tools, and other integrations interact with this vault.\n",
},
SampleFile {
rel_path: "note/welcome-to-laputa.md",
content: r#"---
@@ -384,9 +388,19 @@ pub fn create_getting_started_vault(target_path: &str) -> Result<String, String>
fs::create_dir_all(vault_dir)
.map_err(|e| format!("Failed to create vault directory: {}", e))?;
// Write AGENTS.md at the vault root
fs::write(vault_dir.join("AGENTS.md"), AGENTS_MD)
.map_err(|e| format!("Failed to write AGENTS.md: {}", e))?;
// Write config/agents.md with vault instructions for AI agents
let config_dir = vault_dir.join("config");
fs::create_dir_all(&config_dir)
.map_err(|e| format!("Failed to create config directory: {}", e))?;
fs::write(config_dir.join("agents.md"), AGENTS_MD)
.map_err(|e| format!("Failed to write config/agents.md: {}", e))?;
// Write root AGENTS.md stub for Codex discoverability
fs::write(
vault_dir.join("AGENTS.md"),
"# Agent Instructions\n\nSee config/agents.md for vault instructions.\n",
)
.map_err(|e| format!("Failed to write AGENTS.md stub: {}", e))?;
for sample in SAMPLE_FILES {
let file_path = vault_dir.join(sample.rel_path);
@@ -462,6 +476,7 @@ mod tests {
assert!(result.is_ok());
// Verify key files exist
assert!(vault_path.join("config/agents.md").exists());
assert!(vault_path.join("AGENTS.md").exists());
assert!(vault_path.join("note/welcome-to-laputa.md").exists());
assert!(vault_path.join("note/editor-basics.md").exists());
@@ -476,6 +491,7 @@ mod tests {
assert!(vault_path.join("type/note.md").exists());
assert!(vault_path.join("type/person.md").exists());
assert!(vault_path.join("type/topic.md").exists());
assert!(vault_path.join("type/config.md").exists());
}
#[test]
@@ -530,57 +546,63 @@ mod tests {
create_getting_started_vault(vault_path.to_str().unwrap()).unwrap();
let entries = crate::vault::scan_vault(&vault_path).unwrap();
// SAMPLE_FILES + AGENTS.md + 3 vault theme notes (theme/default.md, dark.md, minimal.md)
assert_eq!(entries.len(), SAMPLE_FILES.len() + 1 + 3);
// SAMPLE_FILES + config/agents.md + AGENTS.md stub + 3 vault theme notes
assert_eq!(entries.len(), SAMPLE_FILES.len() + 2 + 3);
}
#[test]
fn test_agents_md_present_after_vault_creation() {
fn test_config_agents_md_present_after_vault_creation() {
let dir = tempfile::TempDir::new().unwrap();
let vault_path = dir.path().join("agents-vault");
create_getting_started_vault(vault_path.to_str().unwrap()).unwrap();
let agents_path = vault_path.join("AGENTS.md");
assert!(agents_path.exists(), "AGENTS.md should exist at vault root");
let agents_path = vault_path.join("config/agents.md");
assert!(
agents_path.exists(),
"config/agents.md should exist in vault"
);
let content = fs::read_to_string(&agents_path).unwrap();
assert!(content.contains("Vault Instructions for AI Agents"));
assert!(content.contains("## Structure"));
assert!(content.contains("## Frontmatter"));
assert!(content.contains("## Wikilinks"));
assert!(content.contains("## Type definitions"));
assert!(content.contains("## Conventions"));
}
#[test]
fn test_root_agents_md_is_stub_after_vault_creation() {
let dir = tempfile::TempDir::new().unwrap();
let vault_path = dir.path().join("stub-vault");
create_getting_started_vault(vault_path.to_str().unwrap()).unwrap();
let root_path = vault_path.join("AGENTS.md");
assert!(root_path.exists(), "Root AGENTS.md stub should exist");
let content = fs::read_to_string(&root_path).unwrap();
assert!(
content.contains("Vault Instructions for AI Agents"),
"AGENTS.md should contain instructions header"
content.contains("See config/agents.md"),
"Root AGENTS.md should redirect to config/agents.md"
);
assert!(
content.contains("## Structure"),
"AGENTS.md should describe vault structure"
);
assert!(
content.contains("## Frontmatter"),
"AGENTS.md should describe frontmatter"
);
assert!(
content.contains("## Wikilinks"),
"AGENTS.md should describe wikilinks"
);
assert!(
content.contains("## Type definitions"),
"AGENTS.md should describe type definitions"
);
assert!(
content.contains("## Conventions"),
"AGENTS.md should describe conventions"
!content.contains("## Structure"),
"Root AGENTS.md should not contain full instructions"
);
}
#[test]
fn test_agents_md_parseable_as_vault_entry() {
fn test_config_agents_md_parseable_as_vault_entry() {
let dir = tempfile::TempDir::new().unwrap();
let vault_path = dir.path().join("agents-parse-vault");
create_getting_started_vault(vault_path.to_str().unwrap()).unwrap();
let entry = crate::vault::parse_md_file(&vault_path.join("AGENTS.md")).unwrap();
let entry = crate::vault::parse_md_file(&vault_path.join("config/agents.md")).unwrap();
assert_eq!(
entry.title,
"AGENTS.md \u{2014} Vault Instructions for AI Agents"
);
assert_eq!(entry.is_a.as_deref(), Some("Config"));
}
#[test]

View File

@@ -1,4 +1,5 @@
mod cache;
mod config_seed;
mod getting_started;
mod image;
mod migration;
@@ -7,6 +8,7 @@ mod rename;
mod trash;
pub use cache::scan_vault_cached;
pub use config_seed::{migrate_agents_md, repair_config_files, seed_config_files};
pub use getting_started::{create_getting_started_vault, default_vault_path, vault_exists};
pub use image::{copy_image_to_vault, save_image};
pub use migration::migrate_is_a_to_type;
@@ -277,6 +279,7 @@ fn infer_type_from_folder(folder: &str) -> String {
"target" => "Target",
"journal" => "Journal",
"month" => "Month",
"config" => "Config",
"essay" => "Essay",
"evergreen" => "Evergreen",
_ => return title_case_folder(folder),

View File

@@ -412,6 +412,19 @@ function App() {
}
}, [resolvedPath, vault, themeManager, setToastMessage])
const handleRepairVault = useCallback(async () => {
if (!resolvedPath) return
try {
const tauriInvoke = isTauri() ? invoke : mockInvoke
const msg = await tauriInvoke<string>('repair_vault', { vaultPath: resolvedPath })
await vault.reloadVault()
await themeManager.reloadThemes()
setToastMessage(msg)
} catch (err) {
setToastMessage(`Failed to repair vault: ${err}`)
}
}, [resolvedPath, vault, themeManager, setToastMessage])
const commands = useAppCommands({
activeTabPath: notes.activeTabPath, activeTabPathRef: notes.activeTabPathRef,
handleCloseTabRef: notes.handleCloseTabRef, tabs: notes.tabs,
@@ -468,6 +481,7 @@ function App() {
mcpStatus,
onInstallMcp: installMcp,
onReindexVault: indexing.triggerFullReindex,
onRepairVault: handleRepairVault,
})
const activeTab = notes.tabs.find((t) => t.entry.path === notes.activeTabPath) ?? null

View File

@@ -67,6 +67,7 @@ interface AppCommandsConfig {
mcpStatus?: string
onInstallMcp?: () => void
onReindexVault?: () => void
onRepairVault?: () => void
}
/** Sets up keyboard shortcuts, command registry, menu events, and keyboard navigation. */
@@ -150,6 +151,7 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
onViewChanges: viewChanges,
onInstallMcp: config.onInstallMcp,
onReindexVault: config.onReindexVault,
onRepairVault: config.onRepairVault,
activeTabPathRef: config.activeTabPathRef,
handleCloseTabRef: config.handleCloseTabRef,
activeTabPath: config.activeTabPath,
@@ -204,6 +206,7 @@ export function useAppCommands(config: AppCommandsConfig): CommandAction[] {
mcpStatus: config.mcpStatus,
onInstallMcp: config.onInstallMcp,
onReindexVault: config.onReindexVault,
onRepairVault: config.onRepairVault,
})
useKeyboardNavigation({

View File

@@ -21,6 +21,7 @@ interface CommandRegistryConfig {
mcpStatus?: string
onInstallMcp?: () => void
onReindexVault?: () => void
onRepairVault?: () => void
onQuickOpen: () => void
onCreateNote: () => void
@@ -198,6 +199,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): CommandAction
onRemoveActiveVault, onRestoreGettingStarted, onRestoreDefaultThemes, isGettingStartedHidden, vaultCount,
mcpStatus, onInstallMcp,
onReindexVault,
onRepairVault,
} = config
const hasActiveNote = activeTabPath !== null
@@ -260,6 +262,7 @@ export function useCommandRegistry(config: CommandRegistryConfig): CommandAction
{ id: 'check-updates', label: 'Check for Updates', group: 'Settings', keywords: ['update', 'version', 'upgrade', 'release'], enabled: true, execute: () => onCheckForUpdates?.() },
{ id: 'install-mcp', label: mcpStatus === 'installed' ? 'Restore MCP Server' : 'Install MCP Server', group: 'Settings', keywords: ['mcp', 'claude', 'ai', 'tools', 'install', 'restore', 'fix', 'repair'], enabled: true, execute: () => onInstallMcp?.() },
{ id: 'reindex-vault', label: 'Reindex Vault', group: 'Settings', keywords: ['reindex', 'index', 'search', 'rebuild', 'refresh'], enabled: !!onReindexVault, execute: () => onReindexVault?.() },
{ id: 'repair-vault', label: 'Repair Vault', group: 'Settings', keywords: ['repair', 'fix', 'restore', 'config', 'agents', 'themes', 'missing', 'reset'], enabled: !!onRepairVault, execute: () => onRepairVault?.() },
// Type-aware: "New [Type]" and "List [Type]"
...buildTypeCommands(vaultTypes, onCreateNoteOfType, onSelect),
@@ -278,6 +281,6 @@ export function useCommandRegistry(config: CommandRegistryConfig): CommandAction
vaultTypes, themes, activeThemeId, onSwitchTheme, onCreateTheme, onOpenTheme, onRestoreDefaultThemes,
onRemoveActiveVault, onRestoreGettingStarted, isGettingStartedHidden, vaultCount,
mcpStatus, onInstallMcp,
onReindexVault,
onReindexVault, onRepairVault,
])
}

View File

@@ -36,6 +36,7 @@ export interface MenuEventHandlers {
onViewChanges?: () => void
onInstallMcp?: () => void
onReindexVault?: () => void
onRepairVault?: () => void
activeTabPathRef: React.MutableRefObject<string | null>
handleCloseTabRef: React.MutableRefObject<(path: string) => void>
activeTabPath: string | null
@@ -78,7 +79,7 @@ type OptionalHandler =
| 'onCreateType' | 'onToggleRawEditor' | 'onToggleDiff' | 'onToggleAIChat'
| 'onOpenVault' | 'onRemoveActiveVault' | 'onRestoreGettingStarted'
| 'onCreateTheme' | 'onRestoreDefaultThemes'
| 'onCommitPush' | 'onResolveConflicts' | 'onViewChanges' | 'onInstallMcp' | 'onReindexVault'
| 'onCommitPush' | 'onResolveConflicts' | 'onViewChanges' | 'onInstallMcp' | 'onReindexVault' | 'onRepairVault'
const OPTIONAL_EVENT_MAP: Record<string, OptionalHandler> = {
'view-go-back': 'onGoBack',
@@ -98,6 +99,7 @@ const OPTIONAL_EVENT_MAP: Record<string, OptionalHandler> = {
'vault-view-changes': 'onViewChanges',
'vault-install-mcp': 'onInstallMcp',
'vault-reindex': 'onReindexVault',
'vault-repair': 'onRepairVault',
}
function dispatchActiveTabEvent(id: string, h: MenuEventHandlers): boolean {

View File

@@ -334,6 +334,7 @@ line-height-base: 1.6
},
ensure_vault_themes: (): null => null,
restore_default_themes: (): string => 'Default themes restored',
repair_vault: (): string => 'Vault repaired',
get_vault_config: (): VaultConfig => ({ zoom: null, view_mode: null, tag_colors: null, status_colors: null, property_display_modes: null }),
save_vault_config: (): null => null,
}