Keyboard shortcuts

Press ← or β†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

KOGRAL Documentation

Welcome to the KOGRAL documentation! This directory contains comprehensive documentation for KOGRAL (KOwledge GRAphs, Local-first), built with mdBook.

πŸ“š Reading the Documentation

You have several options for reading the documentation:

The best reading experience with navigation, search, and live reload:

# Serve documentation at http://localhost:3000
just docs::serve

This will:

  • Build the mdBook
  • Start a local web server on port 3000
  • Open your browser automatically
  • Watch for changes and auto-reload

Option 2: Build Static HTML

Generate static HTML files you can browse offline:

# Build mdBook to docs/book/
just docs::build

Then open docs/book/index.html in your browser.

Option 3: Read Markdown Files Directly

All documentation is written in Markdown and can be read directly:

  • Browse via GitHub/GitLab web interface
  • Use your editor's Markdown preview
  • Read from terminal with bat, glow, or similar tools

Navigation: See SUMMARY.md for the complete table of contents.

πŸ› οΈ Documentation Commands

We use just recipes for documentation tasks. All commands assume you're in the project root directory.

Build and Serve

# Serve documentation locally (recommended)
just docs::serve

# Build static HTML
just docs::build

# Watch and rebuild on file changes
just docs::watch

Validation

# Test code examples in documentation
just docs::test

# Check for broken links
just docs::check-links

Cleanup

# Remove build artifacts
just docs::clean

View All Documentation Commands

just docs::help

πŸ“¦ Installing mdBook

mdBook is required to build and serve the documentation.

Install via Cargo

cargo install mdbook

Install Optional Tools

For enhanced functionality:

# Link checker (validates internal/external links)
cargo install mdbook-linkcheck

# Mermaid diagram support
cargo install mdbook-mermaid

# PlantUML diagram support
cargo install mdbook-plantuml

Verify Installation

mdbook --version
# Should output: mdbook v0.4.x or later

πŸ“– Documentation Structure

The documentation is organized into the following sections:

1. KOGRAL Definition (kogral/)

  • What is KOGRAL and why it exists
  • Core concepts (nodes, edges, graphs)
  • Design philosophy

2. Guides (guides/)

  • Quick start (5 minutes)
  • Installation guide
  • Use cases with examples

3. Architecture (architecture/)

  • System overview with diagrams
  • Config-driven architecture
  • Graph model details
  • ADRs (Architectural Decision Records)

4. Setup (setup/)

  • Initial setup
  • Development environment
  • Production deployment
  • Testing environment
  • CI/CD integration

5. Configuration (config/)

  • Configuration overview
  • Nickel schema reference
  • Runtime configuration
  • Environment modes (dev/prod/test)

6. Storage (storage/)

  • Storage architecture (hybrid strategy)
  • Filesystem backend
  • SurrealDB backend
  • In-memory backend
  • Sync mechanism

7. AI & Embeddings (ai/)

  • Semantic search
  • Embedding providers
  • Provider comparison
  • Configuration examples

8. Templates (templates/)

  • Template system (Tera)
  • Document templates
  • Export templates
  • Custom templates

9. CLI Reference (cli/)

  • All kb-cli commands
  • Common workflows
  • Advanced usage
  • Troubleshooting

10. Apps & Integrations (apps/)

  • MCP quick guide (Claude Code)
  • Logseq integration
  • Vapora integration

11. API Reference (api/)

  • MCP tools specification
  • Storage trait
  • Embedding trait
  • REST API (future)

12. Contributing (contributing/)

  • Development setup
  • Code guidelines
  • Testing standards
  • Documentation guidelines

🎨 Visual Diagrams

The documentation includes SVG diagrams for visual understanding:

These diagrams are embedded in relevant documentation pages and can be viewed standalone in a browser.

πŸ” Searching the Documentation

When using just docs::serve, you get a built-in search feature:

  1. Click the search icon (πŸ”) in the top-left corner
  2. Type your query
  3. Press Enter to navigate results

The search indexes all documentation content including:

  • Page titles
  • Headers
  • Body text
  • Code examples (optionally)

✏️ Editing Documentation

File Format

All documentation is written in GitHub Flavored Markdown with mdBook extensions.

See contributing/documentation.md for detailed editing guidelines.

Adding a New Page

  1. Create the markdown file in the appropriate directory
  2. Add it to SUMMARY.md for navigation
  3. Build to verify: just docs::build

Adding a New Section

  1. Create the directory
  2. Add a README.md for the section landing page
  3. Add section to SUMMARY.md

πŸ§ͺ Testing Documentation

Test Code Examples

just docs::test

This runs all Rust code examples in the documentation to ensure they compile.

just docs::check-links

This validates all internal and external links.

πŸ“ Documentation Standards

When contributing to documentation:

  1. Use clear, concise language - Write for developers and AI agents
  2. Include code examples - Show, don't just tell
  3. Add diagrams where helpful - Visual aids improve understanding
  4. Link related concepts - Help readers discover related content
  5. Test code examples - Ensure code compiles and works
  6. Use consistent formatting - Follow existing page structure
  7. Update SUMMARY.md - New pages must be in navigation
  8. Run checks before committing:
just docs::build
just docs::test
just docs::check-links

πŸ’‘ Tips

Live Reload While Writing

just docs::watch

This watches for changes and rebuilds automatically. Open http://localhost:3000 in your browser to see updates in real-time.

Markdown Preview in Editor

Most editors have Markdown preview:

  • VS Code: Ctrl+Shift+V (Cmd+Shift+V on Mac)
  • IntelliJ/CLion: Preview pane (right side)
  • Vim/Neovim: Use plugins like markdown-preview.nvim

Quick Reference

  • SUMMARY.md - Table of contents (edit to add/remove pages)
  • book.toml - mdBook configuration
  • theme/ - Custom CSS/JS (if needed)
  • diagrams/ - SVG diagrams

πŸ› Troubleshooting

mdbook command not found

# Install mdBook
cargo install mdbook

# Verify installation
mdbook --version

Port 3000 already in use

# Serve on different port
cd docs
mdbook serve --port 3001
# Check all links
just docs::check-links

# Update internal links in affected files
# Then rebuild
just docs::build

πŸ“š Resources

🀝 Contributing to Documentation

Documentation improvements are always welcome! To contribute:

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Test with just docs::build and just docs::test
  5. Submit a pull request

See contributing/documentation.md for detailed guidelines.


Happy documenting! πŸ“–

If you have questions or need help, please open an issue or reach out to the maintainers.

What is KOGRAL?

KOGRAL (KOwledge GRAphs, Local-first) is a git-native knowledge graph system designed for developer teams to capture, connect, and query structured knowledge.

Purpose

KOGRAL solves the problem of knowledge fragmentation in software development:

  • πŸ“ Notes scattered across tools
  • πŸ€” Decisions lost in chat histories
  • πŸ“š Guidelines buried in wikis
  • πŸ”„ Patterns rediscovered repeatedly
  • πŸ€– AI agents lacking project context

Solution

KOGRAL provides a unified, queryable knowledge graph that:

  1. Captures knowledge in structured, git-friendly markdown
  2. Connects concepts through typed relationships
  3. Queries via text and semantic similarity
  4. Integrates with AI tools (Claude Code via MCP)
  5. Syncs across local (filesystem) and shared (SurrealDB) storage

Core Philosophy

Config-Driven

Behavior defined in Nickel schemas, not hardcoded:

{
  graph = { name = "my-project" },
  storage = { primary = 'filesystem },
  embeddings = { provider = 'fastembed },
  templates = { templates_dir = "templates" },
}

Every aspect configurable: storage, embeddings, templates, query behavior.

Git-Friendly

Knowledge stored as markdown files with YAML frontmatter:

---
id: note-123
type: note
title: Error Handling Patterns
tags: [rust, error-handling]
---

# Error Handling Patterns

Use `thiserror` for custom error types...

Changes tracked via git, reviewable in PRs, mergeable across branches.

AI-Native

Built for agent collaboration:

  • MCP Protocol: Native integration with Claude Code
  • Semantic Search: Find concepts, not just keywords
  • Auto-Linking: Suggest relationships based on content
  • Context Injection: Agents query relevant guidelines before coding

Key Concepts

Nodes

6 types of knowledge nodes:

TypePurposeExample
NoteGeneral observations"Rust ownership patterns"
DecisionADRs (Architectural Decision Records)"Use SurrealDB for storage"
GuidelineCode standards"Error handling with thiserror"
PatternReusable solutions"Repository pattern for DB access"
JournalDaily reflections"2026-01-17 progress notes"
ExecutionAgent task records"Implemented auth module"

Relationships

6 typed edges connecting nodes:

RelationMeaningExample
relates_toConceptual linkNote ↔ Note
depends_onPrerequisitePattern β†’ Guideline
implementsConcrete realizationCode β†’ Pattern
extendsInheritanceProjectGuideline β†’ BaseGuideline
supersedesReplacementDecisionV2 β†’ DecisionV1
explainsDocumentationNote β†’ Execution

Multi-Graph Architecture

Local Graph (per project):

  • Stored in .kogral/ directory
  • Git-tracked for version control
  • Project-specific knowledge

Shared Graph (organization-wide):

  • Centralized guidelines and patterns
  • SurrealDB for scalability
  • Inherited by projects

Inheritance Resolution:

Shared Guidelines (priority: 50)
    ↓
Project Guidelines (priority: 100)
    ↓
Effective Guidelines (higher priority wins)

Use Cases

For Developers

  • Capture decisions as you make them (ADRs)
  • Document patterns for future reference
  • Track daily progress in journal entries
  • Query past decisions before new implementations

For Teams

  • Share guidelines across projects
  • Standardize patterns organization-wide
  • Onboard new members with searchable knowledge
  • Review decisions in context with git history

For AI Agents

  • Query guidelines before generating code
  • Check past decisions for context
  • Document executions for audit trails
  • Suggest related patterns during implementation

Comparison with Other Tools

vs. Logseq

FeatureKOGRALLogseq
StorageGit-friendly markdown + DBLocal markdown
AI IntegrationNative MCP protocolPlugin-based
ConfigType-safe Nickel schemasJSON files
Multi-BackendFilesystem + SurrealDBFilesystem only
Semantic SearchMultiple AI providersLocal only
Graph QueriesSurrealDB graph queriesBlock references

Compatibility: KOGRAL can import/export Logseq graphs for visual editing.

vs. Obsidian

FeatureKOGRALObsidian
Target AudienceDevelopers + AI agentsKnowledge workers
FocusStructured knowledge graphFlexible note-taking
ConfigurationConfig-driven (Nickel)Settings UI
CLIFull CLI + MCP serverLimited CLI
Version ControlGit-nativeGit plugin

Use Case: KOGRAL for developer knowledge, Obsidian for personal notes.

vs. Notion/Confluence

FeatureKOGRALNotion/Confluence
StorageLocal-firstCloud-only
FormatPlain markdownProprietary
AI AccessProgrammatic APIWeb scraping
OfflineFull functionalityLimited
PrivacySelf-hostedThird-party servers

Advantage: KOGRAL keeps sensitive knowledge on-premises.

Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    kb-core                          β”‚
β”‚  (Rust library: graph, storage, config, query)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚             β”‚
     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
     β”‚                β”‚  β”‚            β”‚
β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β–Όβ”€β”€β–Όβ”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”
β”‚  kb-cli  β”‚  β”‚   kb-mcp     β”‚  β”‚  NuShell  β”‚
β”‚ (13 cmds)β”‚  β”‚ (MCP server) β”‚  β”‚ (scripts) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Layers:

  1. kb-core: Core library (models, storage, query)
  2. kb-cli: Command-line interface
  3. kb-mcp: MCP server for AI integration
  4. Scripts: NuShell automation (sync, backup, etc.)

When to Use KOGRAL

βœ… Good fit:

  • Developer teams building software
  • Projects with architectural decisions
  • Organizations standardizing patterns
  • AI-assisted development workflows
  • Knowledge requiring version control

❌ Not ideal for:

  • Personal journaling (use Obsidian)
  • Team wikis (use Notion)
  • Customer documentation (use mdBook)
  • Simple note-taking (use Logseq)

Next Steps


"Knowledge is only valuable when it's accessible, connected, and queryable."

Core Concepts

Understanding the fundamental concepts behind KOGRAL.

The Knowledge Graph

At its core, KOGRAL is a directed graph where:

  • Nodes = pieces of knowledge (notes, decisions, guidelines, patterns)
  • Edges = typed relationships between concepts

Node Types and Relationships

This graph structure enables:

  • Discovery: Find related concepts through traversal
  • Context: Understand how ideas connect
  • Evolution: Track how knowledge changes over time

Node Types

1. Note

Purpose: Capture general observations, learnings, and discoveries.

When to use:

  • Documenting a concept you learned
  • Recording implementation details
  • Capturing meeting notes
  • Quick knowledge capture

Example:

---
type: note
title: Async Trait Patterns in Rust
tags: [rust, async, patterns]
---

# Async Trait Patterns in Rust

Using async traits with the async-trait crate...

2. Decision (ADR)

Purpose: Record architectural decisions with full context.

When to use:

  • Choosing between alternatives (REST vs GraphQL)
  • Major technical decisions (database selection)
  • Trade-off analysis
  • Explaining "why" for future reference

Structure:

  • Context: Background and problem
  • Decision: What was chosen
  • Consequences: Positive and negative outcomes
  • Alternatives: What was considered but rejected

Example:

---
type: decision
title: Use SurrealDB for Storage
status: accepted
---

## Context
Need a graph database that supports our relationship model...

## Decision
Adopt SurrealDB as the primary storage backend.

## Consequences
+ Better graph query performance
+ Native relationship support
- Additional infrastructure dependency
- Team learning curve

3. Guideline

Purpose: Define coding standards, best practices, and conventions.

When to use:

  • Code style rules
  • Architecture patterns to follow
  • Security requirements
  • Testing standards

Can be:

  • Shared: Organization-wide (in shared KOGRAL)
  • Project-specific: Overrides shared guidelines

Example:

---
type: guideline
language: rust
category: error-handling
---

# Rust Error Handling Guidelines

1. Use `thiserror` for custom error types
2. Never use `unwrap()` in production code
3. Always propagate errors with `?`

4. Pattern

Purpose: Document reusable solutions to common problems.

When to use:

  • Recurring implementation patterns
  • Tested solutions
  • Best practice implementations
  • Code templates

Structure:

  • Problem: What challenge does this solve?
  • Solution: The pattern/approach
  • Context: When to use/not use
  • Example: Working code

Example:

---
type: pattern
title: Repository Pattern for Database Access
tags: [architecture, database, pattern]
---

## Problem
Need consistent database access across modules.

## Solution
Repository pattern with trait abstraction...

## Example
\`\`\`rust
trait UserRepository {
    async fn find_by_id(&self, id: Uuid) -> Result<User>;
}
\`\`\`

5. Journal

Purpose: Daily development log for tracking progress and reflections.

When to use:

  • End of day summaries
  • Daily standup notes
  • Progress tracking
  • Blocker documentation

Auto-linked: KOGRAL can auto-link journal entries to mentioned concepts.

Example:

---
type: journal
date: 2026-01-17
---

## Progress
- Implemented authentication module
- Fixed cache race condition

## Blockers
- Need API versioning discussion

## Learnings
- tokio::select! perfect for timeouts

6. Execution

Purpose: Record AI agent execution results (from Vapora integration).

When to use:

  • Agent task completion
  • Execution metrics
  • Agent decision history
  • Audit trail

Example:

---
type: execution
task_type: code_generation
agent: rust-expert
outcome: success
duration: 45s
---

Generated authentication module following project guidelines.

Relationship Types

1. relates_to

Meaning: General conceptual relationship.

Use: Connect related ideas without specific dependency.

Example:

[Note: Async Patterns] --relates_to--> [Note: Tokio Runtime]

2. depends_on

Meaning: Prerequisite relationship. Source requires target to exist/be understood first.

Use: Learning paths, implementation order.

Example:

[Pattern: Advanced Error Handling] --depends_on--> [Guideline: Basic Errors]

3. implements

Meaning: Concrete implementation of an abstract concept.

Use: Connect code to patterns/guidelines.

Example:

[Note: Auth Module Implementation] --implements--> [Pattern: Repository Pattern]

4. extends

Meaning: Inheritance/extension relationship.

Use: Guideline overrides, pattern variations.

Example:

[Guideline: Project Error Handling] --extends--> [Guideline: Shared Error Handling]

5. supersedes

Meaning: Replacement relationship. Source replaces target.

Use: Track evolution of decisions/patterns.

Example:

[Decision: Use GraphQL v2] --supersedes--> [Decision: Use REST]

6. explains

Meaning: Documentation/clarification relationship.

Use: Connect notes to implementations, executions to rationale.

Example:

[Note: Why We Chose Rust] --explains--> [Decision: Adopt Rust]

Multi-Graph Architecture

KB supports multiple knowledge graphs:

Local Graph (Project-Specific)

Location: .kogral/ in project directory

Purpose: Project-specific knowledge

  • Project decisions
  • Implementation notes
  • Local patterns
  • Daily journals

Storage: Filesystem (git-tracked)

Scope: Single project

Shared Graph (Organization-Wide)

Location: Configurable (e.g., ~/org/.kogral-shared)

Purpose: Shared organizational knowledge

  • Coding guidelines
  • Standard patterns
  • Architecture principles
  • Security policies

Storage: SurrealDB (centralized) or filesystem (synced)

Scope: All projects

Inheritance Model

Shared Guidelines (priority: 50)
        ↓ [inherited by]
Project Guidelines (priority: 100)
        ↓ [effective]
Combined Guidelines (higher priority wins)

Example:

Shared guideline: "Use Result for errors" Project override: "Use Result + log all errors with tracing" Effective: Both rules apply, project adds requirement

Config-Driven Behavior

Everything is configurable via Nickel schemas:

{
  graph = { name = "my-project" },          # Graph metadata
  storage = { primary = 'filesystem },       # Where to store
  embeddings = { provider = 'fastembed },    # AI provider
  templates = { /* ... */ },                 # Document templates
  query = { similarity_threshold = 0.6 },    # Search behavior
}

No hardcoding: Change behavior without code changes.

Beyond keyword matching, KOGRAL uses embeddings to find concepts:

Keyword search: "Find 'error handling'"

  • Matches exact phrase "error handling"

Semantic search: "How to handle failures gracefully?"

  • Finds: error handling, exception patterns, Result types, panic recovery
  • Understands: "failures" = errors, "gracefully" = best practices

How it works:

  1. Text β†’ Embedding (384 or 1536 dimensional vector)
  2. Similarity search (cosine distance)
  3. Return nodes above threshold (e.g., 0.6)

Providers:

  • fastembed: Local, free, offline (384d)
  • OpenAI: Cloud, best quality (1536d)
  • Claude: Cloud, excellent (1024d)
  • Ollama: Self-hosted (768d)

Templates

Tera templates generate consistent documents:

---
id: {{ id }}
type: {{ type }}
title: {{ title }}
tags: [{% for tag in tags %}"{{ tag }}"{% endfor %}]
---

# {{ title }}

{{ content }}

Customizable: Override templates per project.

Export formats: Logseq, JSON, Markdown.

MCP Integration

Model Context Protocol connects KOGRAL to Claude Code:

Claude Code
    ↓ [JSON-RPC 2.0]
kb-mcp Server
    ↓ [Rust API]
kb-core Library
    ↓ [Storage]
Knowledge Graph

Tools: kogral/search, kogral/add_note, kogral/get_guidelines, etc.

Resources: kogral://project/notes, kogral://shared/guidelines

Prompts: kogral/summarize_project, kogral/find_related

Git-Friendly Storage

Markdown + YAML frontmatter = git-tracked knowledge:

---
id: note-123
type: note
title: My Note
---

# Content here

Benefits:

  • βœ… Diffs in PRs (reviewable changes)
  • βœ… Branches (experiment with knowledge)
  • βœ… Merges (combine knowledge from feature branches)
  • βœ… History (track evolution over time)
  • βœ… Blame (who added this knowledge?)

Logseq Content Blocks

KOGRAL provides full support for Logseq-style outliner blocks with rich metadata and structure.

What are Blocks?

Blocks are the fundamental unit of content in Logseq's outliner format:

  • Each bullet point is a block
  • Blocks can have children (nested blocks)
  • Blocks support tasks, tags, and custom properties
  • Blocks can reference other blocks or pages

Block Features

Task Status:

- TODO Implement authentication #high-priority
- DOING Write tests
- DONE Deploy to staging
- LATER Consider GraphQL API
- NOW Fix critical bug
- WAITING Code review from @alice
- CANCELLED Old approach

Inline Tags:

- Learning Rust ownership #rust #learning #card
  - Prevents data races at compile time
  - Borrowing rules enforce safety

Custom Properties:

- Research paper summary
  priority:: high
  reviewed:: 2026-01-17
  source:: https://example.com/paper.pdf
  - Key findings...

Block and Page References:

- Meeting notes from [[2026-01-17]]
  - Discussed architecture ((block-ref-123))
  - Action items: [[Project Roadmap]]

Hierarchical Structure:

- Parent block #top-level
  - Child block 1
    - Nested child
  - Child block 2

Configuration

Blocks support is opt-in via configuration:

{
  blocks = {
    enabled = true,              # Enable blocks parsing
    parse_on_import = true,      # Auto-parse from Logseq imports
    serialize_on_export = true,  # Serialize to outliner format
    enable_mcp_tools = true,     # Enable block-related MCP tools
  },
}

Use Cases

1. Task Management:

- TODO Weekly sprint planning #meeting
  - DONE Review last sprint
  - DOING Plan current sprint
  - TODO Assign tasks

2. Flashcards (Spaced Repetition):

- What is Rust's ownership model? #card #rust
  - Ownership prevents data races at compile time
  - Each value has a single owner
  - When owner goes out of scope, value is dropped

3. Knowledge Capture with Metadata:

- Tokio async runtime patterns #rust #async
  category:: architecture
  difficulty:: intermediate
  - Use tokio::select! for concurrent operations
  - spawn_blocking() for CPU-intensive work

4. Linked Notes:

- Discussed [[ADR-001]] in architecture meeting
  - Decided on SurrealDB
  - See ((meeting-notes-block-id)) for details

Block Queries (MCP Tools)

Query blocks across your knowledge base:

Find blocks by tag:

{
  "tool": "kogral/find_blocks",
  "arguments": { "tag": "card" }
}

Find all TODOs:

{
  "tool": "kogral/find_todos",
  "arguments": { "limit": 20 }
}

Find flashcards:

{
  "tool": "kogral/find_cards",
  "arguments": { "limit": 10 }
}

Find blocks by property:

{
  "tool": "kogral/find_blocks",
  "arguments": {
    "property_key": "priority",
    "property_value": "high"
  }
}

Architecture

Hybrid Model:

  • Content stored as markdown string (source of truth)
  • Blocks lazily parsed on first access
  • Cached block structure for fast queries
  • Bidirectional: markdown ↔ blocks

BlockParser:

  • Parse outliner markdown β†’ Block structures
  • Serialize Block structures β†’ outliner markdown
  • Preserve all metadata (tags, status, properties, references)
  • Round-trip fidelity for Logseq compatibility

Storage:

  • Filesystem: markdown with blocks inline
  • SurrealDB: dedicated block table with indexes
  • Indexes: tags, status, parent_id, full-text search

See Also:

Key Principles

  1. Capture During Work: Don't wait, document as you go
  2. Link as You Learn: Connect related concepts immediately
  3. Query When Needed: AI-assisted discovery of relevant knowledge
  4. Evolve Over Time: Update decisions, supersede patterns
  5. Share Wisely: Shared guidelines, local specifics

Next Steps

Why KB?

Understanding the motivation behind the Knowledge Base system.

The Problem: Knowledge Fragmentation

Modern software development generates enormous amounts of knowledge:

πŸ“ Notes: Implementation details, learnings, discoveries πŸ€” Decisions: Why we chose X over Y πŸ“š Guidelines: How we write code, architecture patterns πŸ”„ Patterns: Reusable solutions we've discovered πŸ’­ Discussions: Slack threads, meeting notes, PR comments πŸ› Bug Fixes: How we solved issues

But where does this knowledge live?

  • Meeting notes in Google Docs
  • Decisions buried in Slack threads
  • Guidelines in a wiki nobody updates
  • Patterns in someone's head
  • PR discussions lost in GitHub history
  • Bug solutions in closed Jira tickets

The result?

❌ Rediscovering solutions to problems we've already solved ❌ Inconsistent practices because guidelines aren't accessible ❌ Slow onboarding as new developers lack context ❌ Lost context when team members leave ❌ Repeated mistakes because past lessons aren't preserved

The Solution: Unified, Queryable Knowledge Graph

Knowledge Base provides a single source of truth for project knowledge:

βœ… Capture once: Notes, decisions, guidelines, patterns βœ… Connect concepts: Typed relationships between ideas βœ… Query naturally: Text and semantic search βœ… AI-assisted: Claude Code integration via MCP βœ… Git-tracked: Version control for knowledge βœ… Shared wisdom: Organization-wide guidelines + project-specific

What Makes KB Different?

1. Git-Native

Other tools: Notion, Confluence, wikis in web apps

KB: Markdown files in .kogral/ directory

Benefits:

# Review knowledge changes in PRs
git diff .kogral/decisions/

# Branch knowledge with code
git checkout feature-branch
# .kogral/ follows the branch

# Merge knowledge from feature work
git merge feature-branch
# Knowledge merges like code

Result: Knowledge versioned alongside code it describes.

2. AI-Native

Other tools: Manual search, browse folders

KB: AI queries via Claude Code

Example:

You: "Find anything about error handling"

Claude: [Searches semantically, not just keywords]
Found 5 concepts:
- Pattern: Error Handling with thiserror
- Guideline: Result Type Best Practices
- Decision: Use anyhow for Application Errors
- Note: Custom Error Types
- Journal: Fixed error propagation bug

[All related, even without exact keyword match]

Result: Find concepts, not just documents with keywords.

3. Config-Driven

Other tools: Hardcoded behavior, limited customization

KB: Nickel schemas define everything

Example:

# Development: local embeddings, no costs
{ embeddings = { provider = 'fastembed } }

# Production: cloud embeddings, best quality
{ embeddings = { provider = 'openai, model = "text-embedding-3-large" } }

Result: Adapt behavior without code changes.

4. Multi-Graph

Other tools: One knowledge base per project

KB: Shared organizational knowledge + project-specific

Structure:

Organization Shared KB
  β”œβ”€β”€ Rust Guidelines (applies to all projects)
  β”œβ”€β”€ Security Patterns (applies to all projects)
  └── Testing Standards (applies to all projects)

Project A KB
  β”œβ”€β”€ Inherits shared guidelines
  β”œβ”€β”€ Project-specific decisions
  └── Can override shared guidelines

Project B KB
  β”œβ”€β”€ Inherits same shared guidelines
  β”œβ”€β”€ Different project decisions
  └── Different overrides

Result: Consistency across organization, flexibility per project.

5. Structured Relationships

Other tools: Backlinks, tags, folders

KB: Typed relationships with meaning

Example:

Pattern: Repository Pattern
    ↑ [implements]
Note: User Service Implementation
    ↑ [relates_to]
Decision: Use PostgreSQL
    ↑ [depends_on]
Guideline: Database Access Patterns

Result: Understand how knowledge connects, not just that it connects.

Real-World Impact

Before KB

New developer joins:

  1. Read outdated wiki (2 hours)
  2. Ask teammates for context (1 hour per question Γ— 10 questions)
  3. Discover guidelines by reading code (days)
  4. Miss important decisions (leads to mistakes)

Time to productivity: 2-4 weeks

With KB

New developer joins:

  1. kb search "architectural decisions" β†’ finds all ADRs
  2. Ask Claude: "What are our coding guidelines?" β†’ gets current guidelines with inheritance
  3. Browse related notes via graph traversal
  4. Full context available, no tribal knowledge

Time to productivity: 3-5 days

Before KB

Team makes decision:

  1. Discussion in Slack (lost after a week)
  2. Someone documents in wiki (maybe)
  3. 6 months later: "Why did we choose X?" β†’ nobody remembers
  4. Re-debate the same decision

With KB

Team makes decision:

  1. Discussion captured as ADR during meeting
  2. Context, decision, consequences documented
  3. Linked to related patterns and guidelines
  4. 6 months later: kb show decision-use-x β†’ full context instantly

Before KB

Solving a bug:

  1. Encounter race condition in cache
  2. Debug for 2 hours
  3. Fix it
  4. Solution lost in PR comments

Two months later: Different developer, same bug, 2 more hours

With KB

Solving a bug:

  1. Encounter race condition
  2. Ask Claude: "Have we seen cache race conditions before?"
  3. Claude finds journal entry from 2 months ago with solution
  4. Apply fix in 10 minutes

Two months later: Same query, same instant solution

Who Benefits?

Individual Developers

βœ… Personal knowledge base: Capture learnings, build expertise βœ… Quick recall: "How did I solve this before?" βœ… Context switching: Return to old projects with full context βœ… Career growth: Document what you learn, portfolio of knowledge

Teams

βœ… Shared context: Everyone has access to team knowledge βœ… Onboarding: New members ramp up faster βœ… Consistency: Follow shared guidelines and patterns βœ… Collaboration: Build on each other's knowledge

Organizations

βœ… Institutional memory: Knowledge persists beyond individual tenure βœ… Best practices: Standardize across teams βœ… Compliance: Document security and architecture decisions βœ… Efficiency: Stop solving the same problems repeatedly

AI Agents

βœ… Context injection: Agents query guidelines before generating code βœ… Decision awareness: Agents check past decisions βœ… Pattern following: Agents use documented patterns βœ… Execution tracking: Agent actions documented automatically

When NOT to Use KB

KB is not ideal for:

❌ Personal journaling: Use Obsidian or Logseq ❌ Team wikis: Use Notion or Confluence ❌ Customer docs: Use mdBook or Docusaurus ❌ Project management: Use Jira or Linear ❌ Code comments: Use inline documentation

KB is perfect for:

βœ… Developer knowledge graphs βœ… Architectural decision records βœ… Pattern libraries βœ… Coding guidelines βœ… Technical context βœ… AI-assisted development

The Vision

Today's development:

Developer β†’ writes code β†’ commits

With KB:

Developer β†’ writes code β†’ documents decisions β†’ links to patterns β†’ commits code + knowledge
                    ↓
            AI Agent queries knowledge β†’ generates better code
                    ↓
            Team discovers patterns β†’ reuses solutions β†’ faster development

Knowledge becomes an active participant in development, not an afterthought.

Design Philosophy

Three core principles drive KB:

1. Knowledge should be captured during work, not after

❌ "I'll document this later" β†’ never happens βœ… "Claude, document this decision" β†’ done in 30 seconds

2. Knowledge should be connected, not isolated

❌ Standalone documents in folders βœ… Graph of interconnected concepts

3. Knowledge should be queryable, not browsable

❌ "Let me look through 50 docs to find..." βœ… "Find anything related to error handling" β†’ instant semantic results

Get Started

Ready to stop losing knowledge?


"The best time to document was during implementation. The second best time is now."

But with KB + AI, "now" is instant.

Design Philosophy

The principles and values guiding Knowledge Base design and implementation.

Core Tenets

1. Config-Driven, Not Hardcoded

Principle: All behavior should be configurable via schemas, not baked into code.

Why: Flexibility without code changes. Users adapt KB to their workflows, not vice versa.

Example:

#![allow(unused)]
fn main() {
// ❌ Bad: Hardcoded
impl EmbeddingProvider {
    fn new() -> Self {
        FastEmbedProvider::new("BAAI/bge-small-en-v1.5") // Can't change
    }
}

// βœ… Good: Config-driven
impl EmbeddingProvider {
    fn from_config(config: &EmbeddingConfig) -> Result<Box<dyn EmbeddingProvider>> {
        match config.provider {
            'fastembed => Ok(Box::new(FastEmbedProvider::new(&config.model)?)),
            'openai => Ok(Box::new(OpenAIProvider::new(&config.model)?)),
        }
    }
}
}

Benefits:

  • Switch embedding providers without recompilation
  • Different configs for dev/prod
  • User choice, not developer mandate

2. Type-Safe Configuration

Principle: Validate configuration before runtime, not during.

Why: Catch errors early, reduce runtime failures.

Implementation: Nickel contracts + serde validation = double validation

# Schema defines valid values
EmbeddingProvider = [| 'openai, 'claude, 'fastembed |]

# Typo caught at export time, not runtime
{ provider = 'opena1 }  # Error: Invalid variant

Benefits:

  • Errors found during nickel export, not during execution
  • Self-documenting: schema is the spec
  • Refactoring safe: change schema, find all usages

3. Local-First, Cloud-Optional

Principle: Core functionality works offline, cloud is enhancement.

Why: Privacy, cost control, offline development.

Examples:

FeatureLocalCloud
StorageFilesystemSurrealDB
EmbeddingsfastembedOpenAI/Claude
SearchText-basedSemantic

Benefits:

  • Works on planes, trains, areas with poor internet
  • No API costs for small projects
  • Privacy-sensitive projects keep data local
  • Production can use cloud for scale

4. Git-Friendly by Default

Principle: Knowledge should version alongside code.

Why: Knowledge describes code, should evolve with it.

Implementation:

  • Markdown + YAML frontmatter (text-based, diffable)
  • One file per node (granular commits)
  • Wikilinks preserved (works in Logseq, Obsidian)

Benefits:

# Review knowledge changes in PRs
git diff .kogral/decisions/

# Knowledge follows branches
git checkout feature-x
# .kogral/ reflects feature-x decisions

# Knowledge merges
git merge feature-x
# Merge conflicts = knowledge conflicts (resolve intentionally)

5. AI-Native, Human-Readable

Principle: Optimized for AI consumption, readable by humans.

Why: Best of both worlds - AI-assisted discovery, human verification.

Implementation:

  • Structured: YAML frontmatter for AI parsing
  • Semantic: Embeddings for AI queries
  • Readable: Markdown for human consumption
  • Linked: Typed relationships for AI traversal

Example:

---
id: note-auth
type: note
title: Authentication Implementation
tags: [security, auth]
relates_to: [guideline-security, pattern-jwt]
---

# Authentication Implementation

Humans read this markdown normally.

AI can:
- Parse frontmatter for metadata
- Extract tags for filtering
- Follow relates_to links
- Generate embeddings for semantic search

6. Composition Over Inheritance

Principle: Build systems by composing small, focused components.

Why: Flexibility, testability, maintainability.

Implementation:

#![allow(unused)]
fn main() {
// Small, focused traits
trait Storage { ... }
trait EmbeddingProvider { ... }
trait TemplateEngine { ... }

// Compose into systems
struct KnowledgeBase {
    storage: Box<dyn Storage>,
    embeddings: Option<Box<dyn EmbeddingProvider>>,
    templates: TemplateEngine,
}
}

Benefits:

  • Swap storage without affecting embeddings
  • Disable embeddings without breaking storage
  • Test components in isolation
  • Add new providers by implementing trait

7. Fail Fast, Fail Clearly

Principle: Detect errors early, provide clear messages.

Why: Developer experience - fast feedback, actionable errors.

Implementation:

#![allow(unused)]
fn main() {
// ❌ Bad: Silent failure
fn load_config(path: &Path) -> Option<Config> {
    std::fs::read_to_string(path)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
}

// βœ… Good: Explicit errors
fn load_config(path: &Path) -> Result<Config, ConfigError> {
    let content = std::fs::read_to_string(path)
        .map_err(|e| ConfigError::ReadFailed(path.to_path_buf(), e))?;

    serde_json::from_str(&content)
        .map_err(|e| ConfigError::ParseFailed(path.to_path_buf(), e))
}
}

Error messages:

❌ "Failed to load config"
βœ… "Failed to read config file '/path/to/config.ncl': Permission denied"

8. Convention Over Configuration (With Escape Hatches)

Principle: Sane defaults, but everything customizable.

Why: Easy to start, flexible as you grow.

Examples:

# Minimal config (uses conventions)
{ graph = { name = "my-project" } }
# Defaults: filesystem storage, no embeddings, standard templates

# Full config (explicit everything)
{
  graph = { name = "my-project" },
  storage = { primary = 'surrealdb, /* ... */ },
  embeddings = { provider = 'openai, /* ... */ },
  templates = { templates_dir = "custom" },
}

Conventions:

  • .kogral/ directory in project root
  • Filesystem storage by default
  • YAML frontmatter + markdown body
  • Standard template names (note.md.tera, decision.md.tera)

Escape hatches:

  • --kb-dir to use different location
  • Configure alternative storage backends
  • Custom frontmatter schemas
  • Override any template

9. Documentation as Code

Principle: Documentation lives with code, versioned together.

Why: Outdated docs are worse than no docs.

Implementation:

  • ADRs in .kogral/decisions/ (alongside code)
  • Guidelines in .kogral/guidelines/ (versioned with code)
  • Patterns in .kogral/patterns/ (evolve with implementations)

Benefits:

# Code and docs branch together
git checkout old-version
# .kogral/ reflects that version's decisions

# Code and docs merge together
git merge feature
# Merge includes new patterns, updated guidelines

# Code and docs reviewed together
# PR shows code + decision + guideline updates

10. Optimize for Discoverability

Principle: Knowledge is useless if you can't find it.

Why: The point is to use knowledge, not just store it.

Features:

  • Text search: Find exact keywords
  • Semantic search: Find related concepts
  • Graph traversal: Follow relationships
  • Tag filtering: Narrow by category
  • MCP integration: AI-assisted discovery

Example:

User doesn't remember exact term, but knows the concept:

"Find anything about handling errors gracefully"

KB finds (semantically):

  • "Error Handling with thiserror" (pattern)
  • "Result Type Best Practices" (guideline)
  • "Panic Recovery" (note)
  • "Graceful Degradation" (pattern)

No exact keyword match needed, concept match sufficient.

11. Build for Humans, Enable AI

Principle: Humans are the primary users, AI is the assistant.

Why: AI should enhance human workflows, not replace them.

Implementation:

  • Human-readable formats: Markdown, YAML
  • Human-editable: Any text editor works
  • Human-discoverable: ls .kogral/notes/ shows files
  • AI-enhanced: MCP for AI-assisted queries

Example:

# Human workflow
vim .kogral/notes/my-note.md    # Edit directly
git add .kogral/notes/my-note.md
git commit -m "Add note about X"

# AI-enhanced workflow
# (in Claude Code)
"Add a note about X with tags Y, Z"
# AI creates file, human reviews

12. Embrace the Graph

Principle: Knowledge is interconnected, embrace the relationships.

Why: Context comes from connections, not isolation.

Implementation:

  • Typed relationships (not just "related")
  • Bidirectional traversal
  • Relationship strength (0.0-1.0)
  • Multi-hop queries

Example:

Find all patterns that:
  - Are implemented by current project
  - Depend on shared guidelines
  - Were added in the last 6 months

# Graph query, not sequential file search

Anti-Patterns to Avoid

1. ❌ Hardcoding Behavior

#![allow(unused)]
fn main() {
// Don't
const EMBEDDING_MODEL: &str = "BAAI/bge-small-en-v1.5";

// Do
let model = config.embeddings.model.as_str();
}

2. ❌ Runtime Schema Validation

// Don't validate at runtime
let provider = config.provider; // Might be invalid

// Do validate at export time (Nickel contracts)
provider | [| 'openai, 'claude, 'fastembed |]

3. ❌ Opaque Errors

#![allow(unused)]
fn main() {
// Don't
Err("Failed".into())

// Do
Err(KbError::NodeNotFound(id.to_string()))
}

4. ❌ Coupling Components

#![allow(unused)]
fn main() {
// Don't
impl KnowledgeBase {
    fn search(&self) -> Vec<Node> {
        let embeddings = FastEmbedProvider::new(); // Hardcoded!
        // ...
    }
}

// Do
impl KnowledgeBase {
    fn search(&self) -> Vec<Node> {
        if let Some(provider) = &self.embeddings {
            // Use configured provider
        }
    }
}
}

5. ❌ Proprietary Formats

// Don't
Binary blob: [0x4B, 0x42, 0x01, ...]

// Do
Markdown + YAML:
---
id: note-1
---
# Content

Influences

KB draws inspiration from:

  • Logseq: Outliner, graph view, wikilinks
  • Obsidian: Markdown-first, local storage, plugins
  • Zettelkasten: Atomic notes, links, emergence
  • ADR: Decision records, context preservation
  • Git: Version control, branching, merging
  • Nickel: Type-safe configuration, contracts
  • MCP: AI integration protocol
  • SurrealDB: Graph database, relationships

Implementation Principles

Rust

  • Zero unsafe code (#![forbid(unsafe_code)])
  • No unwrap() in production code
  • Always use Result<T> for fallibility
  • Comprehensive error types with thiserror
  • Full test coverage (100+ tests)

Nickel

  • Contracts for validation
  • Defaults in schemas
  • Documentation in contracts
  • Composition via imports

NuShell

  • Structured data pipelines
  • Error handling in scripts
  • Colored output for UX
  • Dry-run modes

Evolution Strategy

KB follows these guidelines for evolution:

  1. Backward Compatibility: Don't break existing configs
  2. Deprecation Period: Warn before removal (1 major version)
  3. Migration Tools: Provide automated migrations
  4. Semantic Versioning: MAJOR.MINOR.PATCH strictly

Conclusion

These principles guide every decision:

βœ… Config-driven: Behavior in schemas, not code βœ… Type-safe: Validate before runtime βœ… Local-first: Works offline, cloud optional βœ… Git-friendly: Knowledge versions with code βœ… AI-native: Optimized for AI, readable by humans βœ… Composable: Small pieces, loosely coupled βœ… Fast feedback: Fail early, clear errors βœ… Discoverable: Easy to find what you need

The goal: Knowledge management that developers actually use.

Next Steps

Installation

This guide covers installing and setting up the KOGRAL.

Prerequisites

Required

  • Rust 1.70 or later

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    
  • Nickel CLI for configuration management

    cargo install nickel-lang-cli
    

Optional

  • NuShell for maintenance scripts

    cargo install nu
    
  • SurrealDB for scalable storage backend

    # macOS
    brew install surrealdb/tap/surreal
    
    # Linux/Windows
    curl -sSf https://install.surrealdb.com | sh
    

Installation Methods

# Clone the repository
git clone <repository-url> knowledge-base
cd knowledge-base

# Build the workspace
cargo build --release

# Install CLI tool
cargo install --path crates/kb-cli

# Verify installation
kb --version

Method 2: Build Specific Crates

# Build only kb-core library
cargo build --package kb-core --release

# Build only kb-cli
cargo build --package kb-cli --release

# Build only kogral-mcp server
cargo build --package kb-mcp --release

Method 3: Build with Features

# Build with all features (filesystem + SurrealDB + fastembed)
cargo build --workspace --all-features --release

# Build with specific features
cargo build --package kb-core --features "surrealdb,fastembed" --release

Feature Flags

kb-core supports optional features:

FeatureDescriptionDefault
filesystemFilesystem storage backendβœ… Yes
surrealdbSurrealDB storage backend❌ No
fastembedLocal embedding generation❌ No
fullAll features enabled❌ No

Example usage:

# Enable SurrealDB backend
cargo build --features surrealdb

# Enable all features
cargo build --features full

Environment Setup

1. Initialize a Knowledge Base

# Navigate to your project
cd /path/to/your/project

# Initialize .kb directory
kb init

# Or initialize with custom name
kb init --name "My Project" --description "Project knowledge base"

This creates:

your-project/
└── .kogral/
    β”œβ”€β”€ config.toml
    β”œβ”€β”€ notes/
    β”œβ”€β”€ decisions/
    β”œβ”€β”€ guidelines/
    β”œβ”€β”€ patterns/
    └── journal/

2. Configure Nickel Schemas (Optional)

If you want advanced configuration:

# Copy default config
cp /path/to/knowledge-base/config/defaults.ncl .kogral/config.ncl

# Edit configuration
$EDITOR .kogral/config.ncl

# Export to TOML (for kb-cli compatibility)
nickel export --format json .kogral/config.ncl | kb config import

3. Set Up Shared Knowledge Base (Optional)

For shared guidelines and patterns across projects:

# Create shared KB location
mkdir -p ~/Tools/.kogral-shared
cd ~/Tools/.kogral-shared

# Initialize shared KB
kb init --name "Shared Knowledge" --description "Cross-project guidelines"

# Configure inheritance in project
kb config set inheritance.base ~/Tools/.kogral-shared

Verify Installation

Test CLI

# Check version
kb --version

# Show help
kb --help

# Test initialization (dry-run)
kb init --dry-run

Test MCP Server

# Start MCP server in test mode
kb serve --transport stdio

# In another terminal, test with echo
echo '{"jsonrpc":"2.0","id":1,"method":"kogral/search","params":{"query":"test"}}' | kb serve

Run Tests

# Run all tests
cargo test --workspace

# Run integration tests
cargo test --package kb-core --test '*'

# Run with all features
cargo test --workspace --all-features

SurrealDB Setup (Optional)

If using SurrealDB backend:

Start SurrealDB Server

# Start server
surreal start --log trace --user root --pass root file://data.db

# Or use memory backend for testing
surreal start --log trace --user root --pass root memory

Configure kb-core

Edit .kogral/config.ncl:

{
  storage = {
    primary = 'filesystem,
    secondary = {
      enabled = true,
      type = 'surrealdb,
      url = "ws://localhost:8000",
      namespace = "kb",
      database = "default",
      username = "root",
      password = "root",
    },
  },
}

Initialize Schema

# Run migration script
nu scripts/kb-migrate.nu --target latest

# Or manually import schema
surreal import --conn ws://localhost:8000 --user root --pass root --ns kb --db default schema.surql

Embedding Provider Setup (Optional)

Local fastembed

# Build with fastembed feature
cargo build --package kb-core --features fastembed

# Models will be downloaded on first use

API Providers (OpenAI, Claude, Ollama)

Set environment variables:

# OpenAI
export OPENAI_API_KEY="sk-..."

# Claude (Anthropic)
export ANTHROPIC_API_KEY="sk-ant-..."

# Ollama (local)
export OLLAMA_API_BASE="http://localhost:11434"

Configure provider in .kogral/config.ncl:

{
  embeddings = {
    enabled = true,
    provider = 'openai,  # or 'claude, 'ollama, 'fastembed
    model = "text-embedding-3-small",
    api_key_env = "OPENAI_API_KEY",
  },
}

Claude Code Integration

To use kb-mcp with Claude Code:

1. Build MCP Server

cargo build --package kb-mcp --release

2. Configure Claude Code

Add to ~/.config/claude/config.json:

{
  "mcpServers": {
    "kogral-mcp": {
      "command": "/path/to/knowledge-base/target/release/kb-mcp",
      "args": ["serve"],
      "env": {}
    }
  }
}

3. Test Connection

Start Claude Code and verify:

> kb/search "test"

Troubleshooting

Nickel Not Found

# Verify nickel is installed
nickel --version

# If not, install
cargo install nickel-lang-cli

# Add cargo bin to PATH
export PATH="$HOME/.cargo/bin:$PATH"

Compilation Errors

# Update Rust
rustup update stable

# Clean and rebuild
cargo clean
cargo build --workspace

SurrealDB Connection Failed

# Check SurrealDB is running
curl http://localhost:8000/health

# Start SurrealDB with correct settings
surreal start --bind 0.0.0.0:8000 --user root --pass root memory

MCP Server Not Responding

# Test stdio communication
echo '{"jsonrpc":"2.0","id":1,"method":"ping"}' | kb serve

# Check logs
kb serve --log-level debug

Next Steps

Uninstallation

# Remove installed binary
cargo uninstall kb-cli

# Remove project knowledge base
rm -rf /path/to/project/.kogral

# Remove shared knowledge base
rm -rf ~/Tools/.kogral-shared

Quick Start Guide

Get up and running with the KOGRAL in 5 minutes.

Prerequisites

  • Rust 1.70+ installed
  • Nickel CLI installed (cargo install nickel-lang-cli)
  • kb-cli installed (see Installation)

Step 1: Initialize Your Knowledge Base

Navigate to your project directory and initialize:

cd /path/to/your/project
kb init

This creates a .kogral/ directory with the following structure:

.kogral/
β”œβ”€β”€ config.toml       # Configuration
β”œβ”€β”€ notes/            # General notes
β”œβ”€β”€ decisions/        # Architectural decisions
β”œβ”€β”€ guidelines/       # Project guidelines
β”œβ”€β”€ patterns/         # Reusable patterns
└── journal/          # Daily journal entries

Step 2: Create Your First Note

Add a note to your knowledge base:

kb add note "Getting Started with Rust" \
  --tags rust,programming,learning \
  --content "Key concepts for learning Rust effectively."

Or create interactively:

kb add note
# Follow the prompts to enter title, tags, and content

Step 3: Create a Decision Record

Document an architectural decision:

kb add decision "Use SurrealDB for Storage" \
  --context "Need scalable storage for knowledge graph" \
  --decision "Adopt SurrealDB for its graph capabilities" \
  --consequence "Better query performance" \
  --consequence "Additional infrastructure dependency"

Create relationships between nodes:

# Find node IDs
kb list

# Create a relationship
kb link <note-id> <decision-id> relates_to

Available relationship types:

  • relates_to - General conceptual link
  • depends_on - Dependency relationship
  • implements - Implementation of a concept
  • extends - Inheritance/extension
  • supersedes - Replaces older version
  • explains - Documentation/clarification

Step 5: Search Your Knowledge Base

Search by text:

# Simple search
kb search "rust"

# Filter by type
kb search "architecture" --type decision

# Limit results
kb search "error handling" --limit 5

Search semantically (requires embeddings):

# Semantic search finds conceptually related content
kb search "memory safety" --semantic --threshold 0.7

Step 6: View Node Details

# Show node by ID
kb show <node-id>

# Show node with relationships
kb show <node-id> --with-relationships

Step 7: Edit Documents Directly

All knowledge base documents are markdown files you can edit:

# Open in your editor
$EDITOR .kogral/notes/getting-started-with-rust.md

# Or use your favorite markdown editor
code .kogral/notes/

Document format:

---
id: unique-id
type: note
title: Getting Started with Rust
created: 2026-01-17T10:30:00Z
modified: 2026-01-17T10:30:00Z
tags: [rust, programming, learning]
status: active
---

# Getting Started with Rust

Content goes here with [[wikilinks]] to other nodes.

## Key Concepts

- Ownership
- Borrowing
- Lifetimes

Step 8: Create a Daily Journal Entry

Start journaling your development progress:

kb add journal "Today's Progress" \
  --content "Learned about trait objects and dynamic dispatch in Rust."

Or open today's journal directly:

$EDITOR .kogral/journal/$(date +%Y-%m-%d).md

Step 9: Export to Logseq (Optional)

If you use Logseq, export your knowledge base:

nu scripts/kb-export-logseq.nu /path/to/logseq-graph

This creates a Logseq-compatible graph you can open in Logseq for visual editing.

Step 10: Start MCP Server for Claude Code

Integrate with Claude Code for AI-assisted knowledge management:

# Start MCP server
kb serve

# Configure in ~/.config/claude/config.json
# Then use in Claude Code:
# > kb/search "rust ownership"

Common Workflows

Capture Quick Notes

# Quick note
kb add note "Remember to check error handling in parser module" --tags todo,parser

Document Architectural Decisions

# Create ADR
kb add decision "Adopt Async Rust for I/O Operations" \
  --status accepted \
  --tags architecture,async

Build a Pattern Library

# Add a pattern
kb add pattern "Error Handling with thiserror" \
  --tags rust,error-handling \
  --content "Standard pattern for error types in this project."

Track Daily Progress

# Add journal entry
kb add journal --content "Implemented search functionality. Need to add semantic search next."

Next Steps

Customize Configuration

Edit .kogral/config.ncl for advanced configuration:

{
  graph = {
    name = "My Project",
    version = "1.0.0",
  },

  embeddings = {
    enabled = true,
    provider = 'fastembed,
  },

  templates = {
    templates_dir = "templates",
  },
}

See Configuration Reference for all options.

Set Up Shared Guidelines

Create a shared knowledge base for organization-wide standards:

# Create shared KB
mkdir -p ~/Tools/.kogral-shared
cd ~/Tools/.kogral-shared
kb init --name "Shared Guidelines"

# Add guidelines
kb add guideline "Rust Error Handling" \
  --language rust \
  --category error-handling

# Configure inheritance in projects
kb config set inheritance.base ~/Tools/.kogral-shared

Automate with NuShell Scripts

# Backup regularly
nu scripts/kb-backup.nu --compress

# Sync with SurrealDB
nu scripts/kb-sync.nu --direction bidirectional

# Generate statistics
nu scripts/kb-stats.nu --show-tags

Integrate with Git

# Add to version control
git add .kogral/
git commit -m "docs: Add knowledge base"

# Add to .gitignore (optional: exclude certain types)
echo ".kogral/journal/" >> .gitignore

Tips and Tricks

Link to other nodes naturally in markdown:

See [[getting-started-with-rust]] for basics.
Related decision: [[use-surrealdb-for-storage]].

Reference Code

Link to specific code locations:

Error handling implementation: @src/parser.rs:42

Tag Consistently

Use consistent tagging for better searchability:

# Good tagging
--tags rust,error-handling,pattern

# Avoid
--tags Rust,ErrorHandling,patterns

Leverage Templates

Customize templates for your workflow:

# Copy template
cp templates/note.md.tera templates/meeting-notes.md.tera

# Edit for meeting notes format
$EDITOR templates/meeting-notes.md.tera

Troubleshooting

"KB directory not found"

# Make sure you initialized
kb init

# Or specify KB directory
kb --kb-dir /path/to/.kb search "query"

"Node not found"

# List all nodes to find ID
kb list

# Search for node
kb search "partial title"

"Failed to parse frontmatter"

Check your markdown file has valid YAML frontmatter:

---
id: my-note
type: note
title: My Note
---

Further Reading

Getting Help

  • Check kb --help for command usage
  • Read inline help: kb add --help
  • Report issues on GitHub
  • Join community discussions

Congratulations! You've created your first knowledge base. Start capturing knowledge and building connections.

Daily Workflows

Use Cases

Real-world scenarios showing how KOGRAL solves common development challenges.

Use Case 1: Onboarding New Team Members

Problem

New developer joins the team. Needs to understand:

  • Why we chose SurrealDB over PostgreSQL
  • How error handling works in this project
  • What coding patterns we follow
  • Recent architectural decisions

Solution with KOGRAL

Team lead:

# Export project knowledge to Logseq for visual exploration
kb export logseq ~/onboarding-docs

New developer (with Claude Code):

Summarize our architectural decisions from the last 6 months

Claude (via MCP):

Found 8 decisions:

1. **Use SurrealDB for Storage** (accepted, 3 months ago)
   - Context: Need graph database for knowledge relationships
   - Consequences: Better queries, additional infrastructure

2. **Adopt Rust for Core Logic** (accepted, 5 months ago)
   - Context: Type safety and performance critical
   - Consequences: Steeper learning curve, excellent reliability

... [6 more decisions]

New developer:

What are our error handling guidelines?

Claude (via kb/get_guidelines):

## Rust Error Handling Guidelines

### Project Guidelines (priority: 150)
- Use thiserror for custom error types
- Never unwrap() in production
- Propagate with ? operator

### Shared Guidelines (priority: 50)
- Always use Result<T> for fallible operations
- Add context with .context()

[Links to full guideline documents]

Result: New developer productive in days, not weeks.


Use Case 2: Architectural Decision Records (ADR)

Problem

Team discussing whether to add Redis caching. Multiple opinions, need to document the decision for future reference.

Solution with KOGRAL

During meeting:

Document decision: Add Redis caching for API responses

Context:
- Current database queries average 200ms
- 80% of queries are repeated within 5 minutes
- Need sub-50ms response times for user experience

Decision:
- Add Redis with 5-minute TTL for GET endpoints
- Cache invalidation on POST/PUT/DELETE

Consequences:
- βœ… Expected 80% reduction in database load
- βœ… Sub-50ms response times achieved
- ❌ Additional infrastructure (Redis server)
- ❌ Cache consistency challenges
- ❌ Debugging complexity increases

Status: proposed (pending infrastructure setup)

Claude (creates ADR):

Created: decision-add-redis-caching
Type: decision
Status: proposed

Added relationships:
  - Relates to: note-performance-requirements
  - Depends on: decision-use-docker-compose (infrastructure)

Decision documented and linked to related knowledge.

Two months later, someone asks "Why Redis?":

kb show decision-add-redis-caching

Full context available instantly.


Use Case 3: Pattern Library

Problem

Team solves the same problems repeatedly:

  • How to structure error types
  • Database connection patterns
  • Testing strategies

Each developer rediscovers solutions.

Solution with KOGRAL

First time solving error handling:

kb add pattern "Error Handling with thiserror" \
  --tags rust,error-handling,pattern \
  --content "$(cat docs/error-pattern.md)"

Next time someone needs error handling:

I'm implementing a new module with errors. What's our standard pattern?

Claude (via kogral/search):

Found pattern: Error Handling with thiserror

## Pattern: Error Handling with thiserror

### Problem
Need structured, type-safe error handling with good error messages.

### Solution
```rust
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ModuleError {
    #[error("Database connection failed: {0}")]
    Database(#[from] sqlx::Error),

    #[error("Validation failed: {0}")]
    Validation(String),
}

Usage

#![allow(unused)]
fn main() {
fn do_work() -> Result<(), ModuleError> {
    let conn = connect_db()?; // Auto-converts via #[from]
    validate_input()?;
    Ok(())
}
}

[Full pattern with examples]

Would you like me to create a new error module following this pattern?


**Result**: Consistent error handling across codebase, zero time wasted rediscovering patterns.

---

## Use Case 4: Daily Development Journal

### Problem

Developer wants to track:
- What they worked on each day
- Blockers encountered
- Solutions discovered
- Ideas for future work

### Solution with KOGRAL

**End of each day**:

```bash
kb add journal "2026-01-17" --content "
## Progress
- Implemented user authentication
- Fixed race condition in cache layer
- Started work on API rate limiting

## Blockers
- SurrealDB migration tool not working with nested objects
- Need to discuss API versioning strategy with team

## Learnings
- tokio::select! is perfect for timeout patterns
- serde_json::Value useful for dynamic JSON handling

## Tomorrow
- Finish rate limiting implementation
- Review PR #234
- Team meeting: discuss API versioning
"

With Claude Code (even easier):

End of day journal:

Implemented authentication, fixed cache race condition, started rate limiting.
Blocked on SurrealDB migrations and need API versioning discussion.
Learned about tokio::select! for timeouts.
Tomorrow: finish rate limiting, review PR #234, versioning meeting.

Claude (creates journal entry):

Created journal entry: 2026-01-17

Tags auto-detected: authentication, cache, rate-limiting, surrealdb

Linked to:
  - pattern-timeout-handling (implements)
  - decision-use-surrealdb (relates_to)

Journal entry saved.

Two weeks later (forgot how you solved that race condition):

How did I fix the cache race condition?

Claude (semantic search):

From your journal entry on 2026-01-17:

"Fixed race condition in cache layer by using DashMap instead of
RwLock<HashMap>. DashMap provides lock-free concurrent access."

[Link to full journal entry]

Result: Development history preserved, solutions easy to recall.


Use Case 5: Code Review Preparation

Problem

Preparing for code review. Need to:

  • Document why certain decisions were made
  • Link implementation to requirements
  • Explain trade-offs to reviewers

Solution with KOGRAL

Before creating PR:

I just implemented the caching layer.
Add a note documenting the implementation and link it to our caching decision.

Claude (via kb/add_note + kb/link):

Created note: note-caching-layer-implementation

Content:
# Caching Layer Implementation

## Approach
- Redis with 5-minute TTL
- Cache-aside pattern
- Invalidation on mutations

## Trade-offs
- Chose simplicity over advanced features (no LRU eviction)
- 5-minute TTL balances freshness vs cache hit rate
- Cache-aside pattern chosen over write-through (simpler)

## Testing
- Unit tests for cache logic
- Integration tests with real Redis
- Load test: 1000 req/s sustained

Links:
  - Implements: decision-add-redis-caching
  - Follows: pattern-cache-aside
  - Tests: guideline-integration-testing

Note added and linked to decision-add-redis-caching.

In PR description:

## Implementation

Implements caching layer per decision-add-redis-caching.

See note-caching-layer-implementation for detailed rationale.

## Trade-offs

[Copy from note]

Reviewers:

kb show decision-add-redis-caching
kb show note-caching-layer-implementation

Full context available, review focuses on code quality not "why this approach?"


Use Case 6: Cross-Project Guidelines

Problem

Organization has 10 projects. Each should follow:

  • Same Rust error handling patterns
  • Same security guidelines
  • Same testing standards

Currently: duplicated wikis, inconsistent practices.

Solution with KOGRAL

Setup shared KOGRAL:

# Create organization-wide shared KB
mkdir -p ~/org/.kogral-shared
cd ~/org/.kogral-shared
kb init --name "Organization Guidelines"

# Add shared guidelines
kb add guideline "Rust Error Handling" \
  --language rust \
  --category error-handling \
  --content "$(cat org-rust-errors.md)"

kb add guideline "Security Best Practices" \
  --category security \
  --content "$(cat org-security.md)"

In each project:

# project/.kogral/config.ncl
{
  inheritance = {
    base = "~/org/.kogral-shared",
    priority = 100,  # Project overrides shared
  },
}

Developer in Project A:

What are our error handling guidelines?

Claude (via kb/get_guidelines with inheritance):

## Rust Error Handling Guidelines

### Shared Guidelines (priority: 50)
From ~/org/.kogral-shared:
- Use thiserror for custom errors
- Never unwrap() in production
- Always propagate with ?

### Project Guidelines (priority: 100)
From project .kogral/:
- Additionally: log all errors with tracing
- Use anyhow for application-level errors

[Combined effective guidelines]

Result:

  • Consistent practices across organization
  • Projects can specialize where needed
  • Single source of truth for shared knowledge

Use Case 7: Bug Investigation

Problem

Bug in production. Developer needs to:

  • Understand relevant decisions
  • Find related code patterns
  • Check if similar issues were solved before

Solution with KOGRAL

Developer:

We have a race condition in the cache layer.
What past decisions or patterns might be relevant?

Claude (semantic search):

Found 4 relevant items:

1. **Decision: Use DashMap for Concurrent Access** (accepted, 1 month ago)
   "Chose DashMap over RwLock<HashMap> to avoid race conditions in cache"

2. **Pattern: Safe Concurrent Cache Access**
   "Always use lock-free data structures for high-contention caches"

3. **Note: Cache Race Condition Fix** (from 2026-01-17 journal)
   "Fixed race in cache by switching to DashMap"

4. **Guideline: Concurrency Safety**
   "Use atomic operations and lock-free structures where possible"

It looks like we've seen this before! Check the journal entry from 2026-01-17.

Developer reads journal:

kb show journal-2026-01-17

Finds:

  • Exact same issue
  • Solution: switch to DashMap
  • Code example included
  • Performance benchmarks

Applies fix in 10 minutes instead of 2 hours of debugging.


Use Case 8: API Design Collaboration

Problem

Team discussing new API design. Multiple approaches:

  • REST vs GraphQL
  • Versioning strategy
  • Authentication method

Need to capture discussion and decision.

Solution with KOGRAL

During design meeting (using Claude Code):

We're designing the new API. Let me capture this discussion:

Approaches discussed:
1. REST with versioned endpoints (/v1/, /v2/)
2. GraphQL with schema versioning
3. gRPC for internal services

Team consensus:
- REST for public API (easier for partners)
- GraphQL for internal admin tools (flexibility)
- gRPC for service-to-service (performance)

Create a decision record for each, linking them together.

Claude (creates 3 linked ADRs):

Created 3 decisions:

1. decision-rest-public-api
   Status: accepted
   Context: Public API needs broad compatibility
   Decision: REST with /v1/, /v2/ versioning

2. decision-graphql-admin
   Status: accepted
   Context: Admin tools need flexible queries
   Decision: GraphQL with schema evolution

3. decision-grpc-internal
   Status: accepted
   Context: Service-to-service needs performance
   Decision: gRPC with protobuf

Linked:
  decision-rest-public-api --[relates_to]--> decision-graphql-admin
  decision-rest-public-api --[relates_to]--> decision-grpc-internal
  decision-graphql-admin --[relates_to]--> decision-grpc-internal

All decisions documented and interconnected.

Two months later, someone asks "Why REST not GraphQL for public API?":

kb show decision-rest-public-api

Full context preserved, including relationship to GraphQL decision.


Summary

Knowledge Base excels at:

βœ… Onboarding: Historical context instantly available βœ… Decision Making: ADRs preserve rationale βœ… Pattern Reuse: Solutions documented once, used forever βœ… Daily Tracking: Development journal with semantic search βœ… Code Review: Implementation rationale linked to decisions βœ… Cross-Project: Shared guidelines with project overrides βœ… Bug Investigation: Past solutions easily discovered βœ… Collaboration: Discussions captured and interconnected

Common Theme: Knowledge captured during work, queryable when needed, connected to related concepts.


Next Steps

Best Practices

System Architecture

Comprehensive overview of the KOGRAL architecture.

High-Level Architecture

Architecture Overview

The KOGRAL consists of three main layers:

  1. User Interfaces: kb-cli (terminal), kb-mcp (AI integration), NuShell scripts (automation)
  2. Core Library (kb-core): Rust library with graph engine, storage abstraction, embeddings, query engine
  3. Storage Backends: Filesystem (git-friendly), SurrealDB (scalable), In-Memory (cache/testing)

Component Details

kb-cli (Command-Line Interface)

Purpose: Primary user interface for local knowledge management.

Commands (13 total):

  • init: Initialize .kogral/ directory
  • add: Create nodes (note, decision, guideline, pattern, journal)
  • search: Text and semantic search
  • link: Create relationships between nodes
  • list: List all nodes
  • show: Display node details
  • delete: Remove nodes
  • graph: Visualize knowledge graph
  • sync: Sync filesystem ↔ SurrealDB
  • serve: Start MCP server
  • import: Import from Logseq
  • export: Export to Logseq/JSON
  • config: Manage configuration

Technology: Rust + clap (derive API)

Features:

  • Colored terminal output
  • Interactive prompts
  • Dry-run modes
  • Validation before operations

kb-mcp (MCP Server)

Purpose: AI integration via Model Context Protocol.

Protocol: JSON-RPC 2.0 over stdio

Components:

  1. Tools (7):

    • kogral/search: Query knowledge base
    • kb/add_note: Create notes
    • kb/add_decision: Create ADRs
    • kb/link: Create relationships
    • kb/get_guidelines: Retrieve guidelines with inheritance
    • kb/list_graphs: List available graphs
    • kb/export: Export to formats
  2. Resources (6 URIs):

    • kogral://project/notes
    • kogral://project/decisions
    • kogral://project/guidelines
    • kogral://project/patterns
    • kogral://shared/guidelines
    • kogral://shared/patterns
  3. Prompts (2):

    • kb/summarize_project: Generate project summary
    • kb/find_related: Find related nodes

Integration: Claude Code via ~/.config/claude/config.json

NuShell Scripts

Purpose: Automation and maintenance tasks.

Scripts (6):

  • kb-sync.nu: Filesystem ↔ SurrealDB sync
  • kb-backup.nu: Archive knowledge base
  • kb-reindex.nu: Rebuild embeddings
  • kb-import-logseq.nu: Import from Logseq
  • kb-export-logseq.nu: Export to Logseq
  • kb-stats.nu: Graph statistics

Features:

  • Colored output
  • Dry-run modes
  • Progress indicators
  • Error handling

Core Library (kb-core)

Models

Graph:

#![allow(unused)]
fn main() {
pub struct Graph {
    pub name: String,
    pub version: String,
    pub nodes: HashMap<String, Node>,  // ID β†’ Node
    pub edges: Vec<Edge>,
    pub metadata: HashMap<String, Value>,
}
}

Node:

#![allow(unused)]
fn main() {
pub struct Node {
    pub id: String,
    pub node_type: NodeType,
    pub title: String,
    pub content: String,
    pub tags: Vec<String>,
    pub status: NodeStatus,
    pub created: DateTime<Utc>,
    pub modified: DateTime<Utc>,
    // ... relationships, metadata
}
}

Edge:

#![allow(unused)]
fn main() {
pub struct Edge {
    pub from: String,
    pub to: String,
    pub relation: EdgeType,
    pub strength: f32,
    pub created: DateTime<Utc>,
}
}

Storage Trait

#![allow(unused)]
fn main() {
#[async_trait]
pub trait Storage: Send + Sync {
    async fn save_graph(&self, graph: &Graph) -> Result<()>;
    async fn load_graph(&self, name: &str) -> Result<Graph>;
    async fn delete_graph(&self, name: &str) -> Result<()>;
    async fn list_graphs(&self) -> Result<Vec<String>>;
}
}

Implementations:

  1. FilesystemStorage: Git-friendly markdown files
  2. MemoryStorage: In-memory with DashMap
  3. SurrealDbStorage: Scalable graph database

Embedding Provider Trait

#![allow(unused)]
fn main() {
#[async_trait]
pub trait EmbeddingProvider: Send + Sync {
    async fn embed(&self, texts: Vec<String>) -> Result<Vec<Vec<f32>>>;
    fn dimensions(&self) -> usize;
    fn model_name(&self) -> &str;
}
}

Implementations:

  1. FastEmbedProvider: Local fastembed
  2. RigEmbeddingProvider: OpenAI, Claude, Ollama (via rig-core)

Parser

Input: Markdown file with YAML frontmatter

Output: Node struct

Features:

  • YAML frontmatter extraction
  • Markdown body parsing
  • Wikilink detection ([[linked-note]])
  • Code reference parsing (@file.rs:42)

Example:

---
id: note-123
type: note
title: My Note
tags: [rust, async]
---

# My Note

Content with [[other-note]] and @src/main.rs:10

β†’

#![allow(unused)]
fn main() {
Node {
    id: "note-123",
    node_type: NodeType::Note,
    title: "My Note",
    content: "Content with [[other-note]] and @src/main.rs:10",
    tags: vec!["rust", "async"],
    // ... parsed wikilinks, code refs
}
}

Configuration System

Nickel Schema

# schemas/kb-config.ncl
{
  KbConfig = {
    graph | GraphConfig,
    storage | StorageConfig,
    embeddings | EmbeddingConfig,
    templates | TemplateConfig,
    query | QueryConfig,
    mcp | McpConfig,
    sync | SyncConfig,
  },
}

Loading Process

User writes: .kogral/config.ncl
    ↓ [nickel export --format json]
JSON intermediate
    ↓ [serde_json::from_str]
KbConfig struct (Rust)
    ↓
Runtime behavior

Double Validation:

  1. Nickel contracts: Type-safe, enum validation
  2. Serde deserialization: Rust type checking

Benefits:

  • Errors caught at export time
  • Runtime guaranteed valid config
  • Self-documenting schemas

Storage Architecture

Hybrid Strategy

Local Graph (per project):

  • Storage: Filesystem (.kogral/ directory)
  • Format: Markdown + YAML frontmatter
  • Version control: Git
  • Scope: Project-specific knowledge

Shared Graph (organization):

  • Storage: SurrealDB (or synced filesystem)
  • Format: Same markdown (for compatibility)
  • Version control: Optional
  • Scope: Organization-wide guidelines

Sync:

Filesystem (.kogral/)
    ↕ [bidirectional sync]
SurrealDB (central)

File Layout

.kogral/
β”œβ”€β”€ config.toml              # Graph metadata
β”œβ”€β”€ notes/
β”‚   β”œβ”€β”€ async-patterns.md    # Individual note
β”‚   └── error-handling.md
β”œβ”€β”€ decisions/
β”‚   β”œβ”€β”€ 0001-use-rust.md     # ADR format
β”‚   └── 0002-surrealdb.md
β”œβ”€β”€ guidelines/
β”‚   β”œβ”€β”€ rust-errors.md       # Project guideline
β”‚   └── testing.md
β”œβ”€β”€ patterns/
β”‚   └── repository.md
└── journal/
    β”œβ”€β”€ 2026-01-17.md        # Daily journal
    └── 2026-01-18.md

Query Engine

#![allow(unused)]
fn main() {
let results = graph.nodes.values()
    .filter(|node| {
        node.title.contains(&query) ||
        node.content.contains(&query) ||
        node.tags.iter().any(|tag| tag.contains(&query))
    })
    .collect();
}

Semantic Search

#![allow(unused)]
fn main() {
let query_embedding = embeddings.embed(vec![query]).await?;
let mut scored: Vec<_> = graph.nodes.values()
    .filter_map(|node| {
        let node_embedding = node.embedding.as_ref()?;
        let similarity = cosine_similarity(&query_embedding[0], node_embedding);
        (similarity >= threshold).then_some((node, similarity))
    })
    .collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
}

Cross-Graph Query

#![allow(unused)]
fn main() {
// Query both project and shared graphs
let project_results = project_graph.search(&query).await?;
let shared_results = shared_graph.search(&query).await?;

// Merge with deduplication
let combined = merge_results(project_results, shared_results);
}

MCP Protocol Flow

Claude Code                kb-mcp                    kb-core
    β”‚                         β”‚                         β”‚
    β”œβ”€ JSON-RPC request ───→ β”‚                         β”‚
    β”‚  kb/search              β”‚                         β”‚
    β”‚  {"query": "rust"}      β”‚                         β”‚
    β”‚                         β”œβ”€ search() ──────────→  β”‚
    β”‚                         β”‚                         β”‚
    β”‚                         β”‚                    Query engine
    β”‚                         β”‚                    Text + semantic
    β”‚                         β”‚                         β”‚
    β”‚                         β”‚ ←──── results ──────────
    β”‚                         β”‚                         β”‚
    β”‚ ←─ JSON-RPC response ───                         β”‚
    β”‚  {"results": [...]}     β”‚                         β”‚

Template System

Engine: Tera (Jinja2-like)

Templates:

  1. Document Templates (6):

    • note.md.tera
    • decision.md.tera
    • guideline.md.tera
    • pattern.md.tera
    • journal.md.tera
    • execution.md.tera
  2. Export Templates (4):

    • logseq-page.md.tera
    • logseq-journal.md.tera
    • summary.md.tera
    • graph.json.tera

Usage:

#![allow(unused)]
fn main() {
let mut tera = Tera::new("templates/**/*.tera")?;
let rendered = tera.render("note.md.tera", &context)?;
}

Error Handling

Strategy: thiserror for structured errors

#![allow(unused)]
fn main() {
#[derive(Error, Debug)]
pub enum KbError {
    #[error("Storage error: {0}")]
    Storage(String),

    #[error("Node not found: {0}")]
    NodeNotFound(String),

    #[error("Configuration error: {0}")]
    Config(String),

    #[error("Parse error: {0}")]
    Parse(String),

    #[error("Embedding error: {0}")]
    Embedding(String),
}
}

Propagation: ? operator throughout

Testing Strategy

Unit Tests: Per module (models, parser, storage)

Integration Tests: Full workflow (add β†’ save β†’ load β†’ query)

Test Coverage:

  • kb-core: 48 tests
  • kb-mcp: 5 tests
  • Total: 56 tests

Test Data: Fixtures in tests/fixtures/

Performance Considerations

Node Lookup: O(1) via HashMap

Semantic Search: O(n) with early termination (threshold filter)

Storage:

  • Filesystem: Lazy loading (load on demand)
  • Memory: Full graph in RAM
  • SurrealDB: Query optimization (indexes)

Embeddings:

  • Cache embeddings in node metadata
  • Batch processing (configurable batch size)
  • Async generation (non-blocking)

Security

No unsafe code: #![forbid(unsafe_code)]

Input validation:

  • Nickel contracts validate config
  • serde validates JSON
  • Custom validation for user input

File operations:

  • Path sanitization (no ../ traversal)
  • Permissions checking
  • Atomic writes (temp file + rename)

Scalability

Small Projects (< 1000 nodes):

  • Filesystem storage
  • In-memory search
  • Local embeddings (fastembed)

Medium Projects (1000-10,000 nodes):

  • Filesystem + SurrealDB sync
  • Semantic search with caching
  • Cloud embeddings (OpenAI/Claude)

Large Organizations (> 10,000 nodes):

  • SurrealDB primary
  • Distributed embeddings
  • Multi-graph federation

Next Steps

Graph Model

Config-Driven Architecture

The KOGRAL follows a config-driven architecture where all behavior is defined through Nickel configuration files rather than hardcoded in Rust.

Philosophy

"Configuration, not code, defines behavior"

Instead of hardcoding storage backends, embedding providers, or query parameters, KB uses a layered configuration system that composes settings from multiple sources:

  1. Schema contracts (type definitions)
  2. Defaults (base values)
  3. Mode overlays (dev/prod/test optimizations)
  4. User customizations (project-specific overrides)

This approach provides:

  • βœ… Type safety - Nickel contracts validate configuration before runtime
  • βœ… Composability - Mix and match configurations for different environments
  • βœ… Discoverability - Self-documenting schemas with inline documentation
  • βœ… Hot-reload - Change behavior without recompiling Rust code
  • βœ… Double validation - Nickel contracts + serde ensure correctness

Configuration Composition Flow

Configuration Composition

The configuration system uses a four-layer composition pattern:

Layer 1: Schema Contracts

Location: schemas/kb/contracts.ncl

Purpose: Define types and validation rules using Nickel contracts.

Example:

{
  StorageType = [| 'filesystem, 'memory, 'surrealdb |],

  StorageConfig = {
    primary | StorageType
      | doc "Primary storage backend"
      | default = 'filesystem,

    secondary | SecondaryStorageConfig
      | doc "Optional secondary storage"
      | default = { enabled = false },
  },
}

Benefits:

  • Enum validation (only valid storage types accepted)
  • Required vs optional fields
  • Default values for optional fields
  • Documentation attached to types

Layer 2: Defaults

Location: schemas/kb/defaults.ncl

Purpose: Provide sensible base values for all configuration options.

Example:

{
  base = {
    storage = {
      primary = 'filesystem,
      secondary = {
        enabled = false,
        type = 'surrealdb,
        url = "ws://localhost:8000",
      },
    },
    embeddings = {
      enabled = true,
      provider = 'fastembed,
      model = "BAAI/bge-small-en-v1.5",
      dimensions = 384,
    },
  } | contracts.KbConfig,
}

Validated by: contracts.KbConfig contract ensures defaults are valid.

Layer 3: Mode Overlays

Location: schemas/kb/modes/{dev,prod,test}.ncl

Purpose: Environment-specific optimizations and tuning.

Development Mode (dev.ncl)

Optimized for: Fast iteration, local development, debugging

{
  storage = {
    primary = 'filesystem,
    secondary = { enabled = false },  # No database overhead
  },
  embeddings = {
    provider = 'fastembed,  # Local, no API costs
  },
  sync = {
    auto_index = false,  # Manual control
  },
}

Production Mode (prod.ncl)

Optimized for: Performance, reliability, scalability

{
  storage = {
    secondary = { enabled = true },  # SurrealDB for scale
  },
  embeddings = {
    provider = 'openai,  # High-quality cloud embeddings
    model = "text-embedding-3-small",
    dimensions = 1536,
  },
  sync = {
    auto_index = true,
    debounce_ms = 300,  # Fast response
  },
}

Test Mode (test.ncl)

Optimized for: Fast tests, isolation, determinism

{
  storage = {
    primary = 'memory,  # Ephemeral, no disk I/O
  },
  embeddings = {
    enabled = false,  # Disable for speed
  },
  sync = {
    auto_index = false,
    debounce_ms = 0,  # No delays in tests
  },
}

Layer 4: User Customizations

Location: .kb-config/core/kb.ncl or .kb-config/platform/{dev,prod,test}.ncl

Purpose: Project-specific or deployment-specific overrides.

Example (user project config):

let mode = import "../../schemas/kb/modes/dev.ncl" in

let user_custom = {
  graph = {
    name = "my-project",
  },
  embeddings = {
    provider = 'claude,  # Override to use Claude
    model = "claude-3-haiku-20240307",
  },
  query = {
    similarity_threshold = 0.7,  # Stricter threshold
  },
} in

helpers.compose_config defaults.base mode user_custom
  | contracts.KbConfig

Composition Mechanism

The helpers.ncl module provides the composition function:

{
  # Recursively merge with override precedence
  merge_with_override = fun base override => /* ... */,

  # Compose three layers
  compose_config = fun defaults mode_config user_custom =>
    let with_mode = merge_with_override defaults mode_config in
    merge_with_override with_mode user_custom,
}

Merge behavior:

  • Records are merged recursively
  • Override values take precedence over base values
  • Arrays are not merged, override replaces base
  • Null in override keeps base value

Example merge:

base = { storage = { primary = 'filesystem }, embeddings = { enabled = true } }
override = { storage = { primary = 'memory } }
# Result: { storage = { primary = 'memory }, embeddings = { enabled = true } }

Export to JSON

Once composed, the Nickel configuration is exported to JSON for Rust consumption:

nickel export --format json .kb-config/core/kb.ncl > .kb-config/targets/kb-core.json

Output (.kb-config/targets/kb-core.json):

{
  "graph": {
    "name": "my-project",
    "version": "1.0.0"
  },
  "storage": {
    "primary": "memory",
    "secondary": {
      "enabled": false
    }
  },
  "embeddings": {
    "enabled": true,
    "provider": "claude",
    "model": "claude-3-haiku-20240307",
    "dimensions": 768
  }
}

Rust Integration

The Rust code deserializes the JSON into typed structs using serde:

#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
pub struct KbConfig {
    pub graph: GraphConfig,
    pub storage: StorageConfig,
    pub embeddings: EmbeddingConfig,
    pub templates: TemplateConfig,
    pub query: QueryConfig,
    pub mcp: McpConfig,
    pub sync: SyncConfig,
}

impl KbConfig {
    pub fn from_file(path: &Path) -> Result<Self> {
        let json = std::fs::read_to_string(path)?;
        let config: KbConfig = serde_json::from_str(&json)?;
        Ok(config)
    }
}
}

Usage in kb-core:

#![allow(unused)]
fn main() {
let config = KbConfig::from_file(".kb-config/targets/kb-core.json")?;

// Config drives behavior
let storage: Box<dyn Storage> = match config.storage.primary {
    StorageType::Filesystem => Box::new(FilesystemStorage::new(&config)?),
    StorageType::Memory => Box::new(MemoryStorage::new()),
    StorageType::SurrealDb => Box::new(SurrealDbStorage::new(&config).await?),
};

let embeddings: Box<dyn EmbeddingProvider> = match config.embeddings.provider {
    EmbeddingProviderType::FastEmbed => Box::new(FastEmbedProvider::new()?),
    EmbeddingProviderType::OpenAI => Box::new(RigEmbeddingProvider::openai(&config)?),
    EmbeddingProviderType::Claude => Box::new(RigEmbeddingProvider::claude(&config)?),
    EmbeddingProviderType::Ollama => Box::new(RigEmbeddingProvider::ollama(&config)?),
};
}

Double Validation

Configuration is validated twice:

1. Nickel Contract Validation

At export time, Nickel validates:

  • βœ… Types match contracts (e.g., primary | StorageType)
  • βœ… Required fields are present
  • βœ… Enums have valid values
  • βœ… Nested structure is correct

Error example:

error: contract broken by a value
  β”Œβ”€ .kb-config/core/kb.ncl:15:5
  β”‚
15β”‚     primary = 'invalid,
  β”‚     ^^^^^^^^^^^^^^^^^^^ applied to this expression
  β”‚
  = This value is not in the enum ['filesystem, 'memory, 'surrealdb]

2. Serde Deserialization Validation

At runtime, serde validates:

  • βœ… JSON structure matches Rust types
  • βœ… Field names match (with rename support)
  • βœ… Values can be converted to Rust types
  • βœ… Required fields are not null

Error example:

#![allow(unused)]
fn main() {
Error: missing field `graph` at line 1 column 123
}

Benefits of Config-Driven Architecture

1. Zero Hardcoding

Bad (hardcoded):

#![allow(unused)]
fn main() {
// Hardcoded - requires recompilation to change
let storage = FilesystemStorage::new("/fixed/path");
let threshold = 0.6;
}

Good (config-driven):

#![allow(unused)]
fn main() {
// Config-driven - change via .ncl file
let storage = create_storage(&config)?;
let threshold = config.query.similarity_threshold;
}

2. Environment Flexibility

Same codebase, different behavior:

# Development
nickel export .kb-config/platform/dev.ncl > targets/kb-core.json
# β†’ Filesystem storage, fastembed, no auto-sync

# Production
nickel export .kb-config/platform/prod.ncl > targets/kb-core.json
# β†’ SurrealDB enabled, OpenAI embeddings, auto-sync

# Testing
nickel export .kb-config/platform/test.ncl > targets/kb-core.json
# β†’ In-memory storage, no embeddings, isolated

3. Self-Documenting

Nickel contracts include inline documentation:

StorageType = [| 'filesystem, 'memory, 'surrealdb |]
  | doc "Storage backend type: filesystem (git-tracked), memory (ephemeral), surrealdb (scalable)",

IDEs can show this documentation when editing .ncl files.

4. Type-Safe Evolution

When adding new features:

  1. Update contract in contracts.ncl
  2. Add default in defaults.ncl
  3. Export validates existing configs
  4. Rust compilation validates deserialization

Breaking changes are caught before runtime.

5. Testability

Different test scenarios without code changes:

# test-semantic-search.ncl
let test_config = defaults.base & {
  embeddings = { enabled = true, provider = 'fastembed },
  query = { similarity_threshold = 0.3 },
} in test_config
#![allow(unused)]
fn main() {
#[test]
fn test_semantic_search() {
    let config = KbConfig::from_file("test-semantic-search.json")?;
    // Config drives test behavior
}
}

Configuration Discovery

KB tools automatically discover configuration:

  1. Check .kb-config/targets/kb-core.json (pre-exported)
  2. Check .kb-config/core/kb.ncl (export on-demand)
  3. Check environment variable KB_CONFIG
  4. Fall back to embedded defaults
#![allow(unused)]
fn main() {
impl KbConfig {
    pub fn discover() -> Result<Self> {
        if let Ok(config) = Self::from_file(".kb-config/targets/kb-core.json") {
            return Ok(config);
        }

        if Path::new(".kb-config/core/kb.ncl").exists() {
            // Export and load
            let output = Command::new("nickel")
                .args(["export", "--format", "json", ".kb-config/core/kb.ncl"])
                .output()?;
            return serde_json::from_slice(&output.stdout)?;
        }

        if let Ok(path) = std::env::var("KB_CONFIG") {
            return Self::from_file(&path);
        }

        Ok(Self::default())  // Embedded defaults
    }
}
}

Integration with justfile

The justfile integrates configuration validation:

# Validate all Nickel configs
nickel-validate-all:
    @echo "Validating Nickel schemas..."
    nickel typecheck schemas/kb/contracts.ncl
    nickel typecheck schemas/kb/defaults.ncl
    nickel typecheck schemas/kb/helpers.ncl
    nickel typecheck schemas/kb/modes/dev.ncl
    nickel typecheck schemas/kb/modes/prod.ncl
    nickel typecheck schemas/kb/modes/test.ncl
    nickel typecheck .kb-config/core/kb.ncl

# Export all platform configs
nickel-export-all:
    @echo "Exporting platform configs to JSON..."
    @mkdir -p .kb-config/targets
    nickel export --format json .kb-config/platform/dev.ncl > .kb-config/targets/kb-dev.json
    nickel export --format json .kb-config/platform/prod.ncl > .kb-config/targets/kb-prod.json
    nickel export --format json .kb-config/platform/test.ncl > .kb-config/targets/kb-test.json
    @echo "Exported 3 configurations to .kb-config/targets/"

Usage:

just nickel::validate-all  # Check configs are valid
just nickel::export-all    # Generate JSON for all environments

Best Practices

1. Never Hardcode

If it's behavior, it's config:

  • Storage paths
  • API endpoints
  • Thresholds
  • Timeouts
  • Feature flags
  • Provider selection

2. Use Modes for Environment Differences

Don't put environment-specific values in user config:

Bad:

# user config with env-specific values
{
  storage = {
    url = if env == "prod" then "prod-url" else "dev-url"  # Don't do this
  }
}

Good:

# modes/prod.ncl
{ storage = { url = "prod-url" } }

# modes/dev.ncl
{ storage = { url = "dev-url" } }

# user config is environment-agnostic
{ graph = { name = "my-project" } }

3. Document Complex Fields

Use Nickel's doc metadata:

similarity_threshold | Number
  | doc "Minimum cosine similarity (0-1) for semantic search matches. Higher = stricter."
  | default = 0.6,

4. Validate Early

Run nickel typecheck in CI/CD before building Rust code.

5. Version Configs

Track .ncl files in git, ignore .kb-config/targets/*.json (generated).

See Also

Storage Architecture

Logseq Blocks Support - Architecture Design

Problem Statement

Logseq uses content blocks as the fundamental unit of information, not full documents. Each block can have:

  • Properties: #card, TODO, DONE, custom properties
  • Tags: Inline tags like #flashcard, #important
  • References: Block references ((block-id)), page references [[page]]
  • Nesting: Outliner-style hierarchy (parent-child blocks)
  • Metadata: Block-level properties (unlike page-level frontmatter)

Current KB limitation: Nodes only have content: String (flat markdown). Importing from Logseq loses block structure and properties.

Requirement: Support round-trip import/export with full block fidelity:

Logseq Graph β†’ KOGRAL Import β†’ KOGRAL Storage β†’ KOGRAL Export β†’ Logseq Graph
     (blocks preserved)                    (blocks preserved)

Use Cases

1. Flashcards (#card)

Logseq:

- What is Rust's ownership model? #card
  - Rust uses ownership, borrowing, and lifetimes
  - Three rules: one owner, many borrows XOR one mutable

KB needs to preserve:

  • Block with #card property
  • Nested answer blocks
  • Ability to query all cards

2. Task Tracking (TODO/DONE)

Logseq:

- TODO Implement block parser #rust
  - DONE Research block structure
  - TODO Write parser tests

KB needs to preserve:

  • Task status per block
  • Hierarchical task breakdown
  • Tags on tasks

3. Block References

Logseq:

- Core concept: ((block-uuid-123))
  - See also: [[Related Page]]

KB needs to preserve:

  • Block-to-block links (not just page-to-page)
  • UUID references

4. Block Properties

Logseq:

- This is a block with properties
  property1:: value1
  property2:: value2

KB needs to preserve:

  • Custom key-value properties per block
  • Property inheritance/override

Design Options

Option A: Blocks as First-Class Data Structure

Add blocks field to Node:

#![allow(unused)]
fn main() {
pub struct Node {
    // ... existing fields ...
    pub content: String,              // Backward compat: flat markdown
    pub blocks: Option<Vec<Block>>,   // NEW: Structured blocks
}

pub struct Block {
    pub id: String,                   // UUID or auto-generated
    pub content: String,              // Block text
    pub properties: BlockProperties,  // Tags, status, custom props
    pub children: Vec<Block>,         // Nested blocks
    pub created: DateTime<Utc>,
    pub modified: DateTime<Utc>,
}

pub struct BlockProperties {
    pub tags: Vec<String>,            // #card, #important
    pub status: Option<TaskStatus>,   // TODO, DONE, WAITING
    pub custom: HashMap<String, String>, // property:: value
}

pub enum TaskStatus {
    Todo,
    Doing,
    Done,
    Waiting,
    Cancelled,
}
}

Pros:

  • βœ… Type-safe, explicit structure
  • βœ… Queryable (find all #card blocks)
  • βœ… Preserves hierarchy
  • βœ… Supports block-level operations

Cons:

  • ❌ Adds complexity to Node
  • ❌ Dual representation (content + blocks)
  • ❌ Requires migration of existing data

Option B: Parser-Only Approach

Keep content: String, parse blocks on-demand:

#![allow(unused)]
fn main() {
pub struct BlockParser;

impl BlockParser {
    // Parse markdown content into block structure
    fn parse(content: &str) -> Vec<Block>;

    // Serialize blocks back to markdown
    fn serialize(blocks: &[Block]) -> String;
}

// Usage
let blocks = BlockParser::parse(&node.content);
let filtered = blocks.iter().filter(|b| b.properties.tags.contains("card"));
}

Pros:

  • βœ… No schema changes
  • βœ… Backward compatible
  • βœ… Simple storage (still just String)

Cons:

  • ❌ Parse overhead on every access
  • ❌ Can't query blocks in database (SurrealDB)
  • ❌ Harder to index/search blocks

Combine both: structured storage + lazy parsing:

#![allow(unused)]
fn main() {
pub struct Node {
    // ... existing fields ...
    pub content: String,              // Source of truth (markdown)

    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocks: Option<Vec<Block>>,   // Cached structure (parsed)
}

impl Node {
    // Parse blocks from content if not already cached
    pub fn get_blocks(&mut self) -> &Vec<Block> {
        if self.blocks.is_none() {
            self.blocks = Some(BlockParser::parse(&self.content));
        }
        self.blocks.as_ref().unwrap()
    }

    // Update content from blocks (when blocks modified)
    pub fn sync_blocks_to_content(&mut self) {
        if let Some(ref blocks) = self.blocks {
            self.content = BlockParser::serialize(blocks);
        }
    }
}
}

Storage Strategy:

  1. Filesystem - Store as markdown (Logseq compatible):

    - Block 1 #card
      - Nested block
    - Block 2 TODO
    
  2. SurrealDB - Store both:

    DEFINE TABLE block SCHEMAFULL;
    DEFINE FIELD node_id ON block TYPE record(node);
    DEFINE FIELD block_id ON block TYPE string;
    DEFINE FIELD content ON block TYPE string;
    DEFINE FIELD properties ON block TYPE object;
    DEFINE FIELD parent_id ON block TYPE option<string>;
    
    -- Index for queries
    DEFINE INDEX block_tags ON block COLUMNS properties.tags;
    DEFINE INDEX block_status ON block COLUMNS properties.status;
    

Pros:

  • βœ… Best of both worlds
  • βœ… Filesystem stays Logseq-compatible
  • βœ… SurrealDB can query blocks
  • βœ… Lazy parsing (only when needed)
  • βœ… Backward compatible

Cons:

  • ⚠️ Need to keep content/blocks in sync
  • ⚠️ More complex implementation

Phase 1: Data Model

#![allow(unused)]
fn main() {
// crates/kb-core/src/models/block.rs

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// A content block (Logseq-style)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Block {
    /// Unique block identifier (UUID)
    pub id: String,

    /// Block content (markdown text, excluding nested blocks)
    pub content: String,

    /// Block properties (tags, status, custom)
    pub properties: BlockProperties,

    /// Child blocks (nested hierarchy)
    #[serde(default)]
    pub children: Vec<Block>,

    /// Creation timestamp
    pub created: DateTime<Utc>,

    /// Last modification timestamp
    pub modified: DateTime<Utc>,

    /// Parent block ID (if nested)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parent_id: Option<String>,
}

/// Block-level properties
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct BlockProperties {
    /// Tags (e.g., #card, #important)
    #[serde(default)]
    pub tags: Vec<String>,

    /// Task status (TODO, DONE, etc.)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub status: Option<TaskStatus>,

    /// Custom properties (property:: value)
    #[serde(default)]
    pub custom: HashMap<String, String>,

    /// Block references ((uuid))
    #[serde(default)]
    pub block_refs: Vec<String>,

    /// Page references ([[page]])
    #[serde(default)]
    pub page_refs: Vec<String>,
}

/// Task status for TODO blocks
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum TaskStatus {
    Todo,
    Doing,
    Done,
    Later,
    Now,
    Waiting,
    Cancelled,
}

impl Block {
    /// Create a new block with content
    pub fn new(content: String) -> Self {
        use uuid::Uuid;
        Self {
            id: Uuid::new_v4().to_string(),
            content,
            properties: BlockProperties::default(),
            children: Vec::new(),
            created: Utc::now(),
            modified: Utc::now(),
            parent_id: None,
        }
    }

    /// Add a child block
    pub fn add_child(&mut self, mut child: Block) {
        child.parent_id = Some(self.id.clone());
        self.children.push(child);
        self.modified = Utc::now();
    }

    /// Add a tag to this block
    pub fn add_tag(&mut self, tag: String) {
        if !self.properties.tags.contains(&tag) {
            self.properties.tags.push(tag);
            self.modified = Utc::now();
        }
    }

    /// Set task status
    pub fn set_status(&mut self, status: TaskStatus) {
        self.properties.status = Some(status);
        self.modified = Utc::now();
    }

    /// Get all blocks (self + descendants) as flat list
    pub fn flatten(&self) -> Vec<&Block> {
        let mut result = vec![self];
        for child in &self.children {
            result.extend(child.flatten());
        }
        result
    }

    /// Find block by ID in tree
    pub fn find(&self, id: &str) -> Option<&Block> {
        if self.id == id {
            return Some(self);
        }
        for child in &self.children {
            if let Some(found) = child.find(id) {
                return Some(found);
            }
        }
        None
    }
}
}

Phase 2: Update Node Model

#![allow(unused)]
fn main() {
// crates/kb-core/src/models.rs (modifications)

use crate::models::block::Block;

pub struct Node {
    // ... existing fields ...
    pub content: String,

    /// Structured blocks (optional, parsed from content)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub blocks: Option<Vec<Block>>,
}

impl Node {
    /// Get blocks, parsing from content if needed
    pub fn get_blocks(&mut self) -> Result<&Vec<Block>> {
        if self.blocks.is_none() {
            self.blocks = Some(crate::parser::BlockParser::parse(&self.content)?);
        }
        Ok(self.blocks.as_ref().unwrap())
    }

    /// Update content from blocks
    pub fn sync_blocks_to_content(&mut self) {
        if let Some(ref blocks) = self.blocks {
            self.content = crate::parser::BlockParser::serialize(blocks);
        }
    }

    /// Find all blocks with a specific tag
    pub fn find_blocks_by_tag(&mut self, tag: &str) -> Result<Vec<&Block>> {
        let blocks = self.get_blocks()?;
        let mut result = Vec::new();
        for block in blocks {
            for b in block.flatten() {
                if b.properties.tags.iter().any(|t| t == tag) {
                    result.push(b);
                }
            }
        }
        Ok(result)
    }

    /// Find all TODO blocks
    pub fn find_todos(&mut self) -> Result<Vec<&Block>> {
        let blocks = self.get_blocks()?;
        let mut result = Vec::new();
        for block in blocks {
            for b in block.flatten() {
                if matches!(b.properties.status, Some(TaskStatus::Todo)) {
                    result.push(b);
                }
            }
        }
        Ok(result)
    }
}
}

Phase 3: Block Parser

#![allow(unused)]
fn main() {
// crates/kb-core/src/parser/block_parser.rs

use crate::models::block::{Block, BlockProperties, TaskStatus};
use regex::Regex;

pub struct BlockParser;

impl BlockParser {
    /// Parse markdown content into block structure
    ///
    /// Handles:
    /// - Outliner format (- prefix with indentation)
    /// - Tags (#card, #important)
    /// - Task status (TODO, DONE)
    /// - Properties (property:: value)
    /// - Block references (((uuid)))
    /// - Page references ([[page]])
    pub fn parse(content: &str) -> Result<Vec<Block>> {
        let mut blocks = Vec::new();
        let mut stack: Vec<(usize, Block)> = Vec::new(); // (indent_level, block)

        for line in content.lines() {
            // Detect indentation level
            let indent = count_indent(line);
            let trimmed = line.trim_start();

            // Skip empty lines
            if trimmed.is_empty() {
                continue;
            }

            // Parse block line
            if let Some(block_content) = trimmed.strip_prefix("- ") {
                let mut block = Self::parse_block_line(block_content)?;

                // Pop stack until we find parent level
                while let Some((level, _)) = stack.last() {
                    if *level < indent {
                        break;
                    }
                    stack.pop();
                }

                // Add as child to parent or as root
                if let Some((_, parent)) = stack.last_mut() {
                    parent.add_child(block.clone());
                } else {
                    blocks.push(block.clone());
                }

                stack.push((indent, block));
            }
        }

        Ok(blocks)
    }

    /// Parse a single block line (after "- " prefix)
    fn parse_block_line(line: &str) -> Result<Block> {
        let mut block = Block::new(String::new());
        let mut properties = BlockProperties::default();

        // Extract task status (TODO, DONE, etc.)
        let (status, remaining) = Self::extract_task_status(line);
        properties.status = status;

        // Extract tags (#card, #important)
        let (tags, remaining) = Self::extract_tags(remaining);
        properties.tags = tags;

        // Extract properties (property:: value)
        let (custom_props, remaining) = Self::extract_properties(remaining);
        properties.custom = custom_props;

        // Extract block references (((uuid)))
        let (block_refs, remaining) = Self::extract_block_refs(remaining);
        properties.block_refs = block_refs;

        // Extract page references ([[page]])
        let (page_refs, content) = Self::extract_page_refs(remaining);
        properties.page_refs = page_refs;

        block.content = content.trim().to_string();
        block.properties = properties;

        Ok(block)
    }

    /// Serialize blocks back to markdown
    pub fn serialize(blocks: &[Block]) -> String {
        let mut result = String::new();
        for block in blocks {
            Self::serialize_block(&mut result, block, 0);
        }
        result
    }

    fn serialize_block(output: &mut String, block: &Block, indent: usize) {
        // Write indent
        for _ in 0..indent {
            output.push_str("  ");
        }

        // Write prefix
        output.push_str("- ");

        // Write task status
        if let Some(status) = block.properties.status {
            output.push_str(&format!("{:?} ", status).to_uppercase());
        }

        // Write content
        output.push_str(&block.content);

        // Write tags
        for tag in &block.properties.tags {
            output.push_str(&format!(" #{}", tag));
        }

        // Write properties
        if !block.properties.custom.is_empty() {
            output.push('\n');
            for (key, value) in &block.properties.custom {
                for _ in 0..=indent {
                    output.push_str("  ");
                }
                output.push_str(&format!("{}:: {}\n", key, value));
            }
        }

        output.push('\n');

        // Write children recursively
        for child in &block.children {
            Self::serialize_block(output, child, indent + 1);
        }
    }

    // Helper methods for extraction
    fn extract_task_status(line: &str) -> (Option<TaskStatus>, &str) {
        let line = line.trim_start();
        if let Some(rest) = line.strip_prefix("TODO ") {
            (Some(TaskStatus::Todo), rest)
        } else if let Some(rest) = line.strip_prefix("DONE ") {
            (Some(TaskStatus::Done), rest)
        } else if let Some(rest) = line.strip_prefix("DOING ") {
            (Some(TaskStatus::Doing), rest)
        } else if let Some(rest) = line.strip_prefix("LATER ") {
            (Some(TaskStatus::Later), rest)
        } else if let Some(rest) = line.strip_prefix("NOW ") {
            (Some(TaskStatus::Now), rest)
        } else if let Some(rest) = line.strip_prefix("WAITING ") {
            (Some(TaskStatus::Waiting), rest)
        } else if let Some(rest) = line.strip_prefix("CANCELLED ") {
            (Some(TaskStatus::Cancelled), rest)
        } else {
            (None, line)
        }
    }

    fn extract_tags(line: &str) -> (Vec<String>, String) {
        let tag_regex = Regex::new(r"#(\w+)").unwrap();
        let mut tags = Vec::new();
        let mut result = line.to_string();

        for cap in tag_regex.captures_iter(line) {
            if let Some(tag) = cap.get(1) {
                tags.push(tag.as_str().to_string());
                result = result.replace(&format!("#{}", tag.as_str()), "");
            }
        }

        (tags, result.trim().to_string())
    }

    fn extract_properties(line: &str) -> (HashMap<String, String>, String) {
        let prop_regex = Regex::new(r"(\w+)::\s*([^\n]+)").unwrap();
        let mut props = HashMap::new();
        let mut result = line.to_string();

        for cap in prop_regex.captures_iter(line) {
            if let (Some(key), Some(value)) = (cap.get(1), cap.get(2)) {
                props.insert(key.as_str().to_string(), value.as_str().trim().to_string());
                result = result.replace(&cap[0], "");
            }
        }

        (props, result.trim().to_string())
    }

    fn extract_block_refs(line: &str) -> (Vec<String>, String) {
        let ref_regex = Regex::new(r"\(\(([^)]+)\)\)").unwrap();
        let mut refs = Vec::new();
        let mut result = line.to_string();

        for cap in ref_regex.captures_iter(line) {
            if let Some(uuid) = cap.get(1) {
                refs.push(uuid.as_str().to_string());
                result = result.replace(&cap[0], "");
            }
        }

        (refs, result.trim().to_string())
    }

    fn extract_page_refs(line: &str) -> (Vec<String>, String) {
        let page_regex = Regex::new(r"\[\[([^\]]+)\]\]").unwrap();
        let mut pages = Vec::new();
        let result = line.to_string();

        for cap in page_regex.captures_iter(line) {
            if let Some(page) = cap.get(1) {
                pages.push(page.as_str().to_string());
                // Keep [[page]] in content for now (backward compat)
            }
        }

        (pages, result)
    }
}

fn count_indent(line: &str) -> usize {
    line.chars().take_while(|c| c.is_whitespace()).count() / 2
}
}

Phase 4: Logseq Import/Export

#![allow(unused)]
fn main() {
// crates/kb-core/src/logseq.rs

use crate::models::{Node, NodeType};
use crate::models::block::Block;
use crate::parser::BlockParser;

pub struct LogseqImporter;

impl LogseqImporter {
    /// Import a Logseq page (markdown file) as a Node
    pub fn import_page(path: &Path) -> Result<Node> {
        let content = std::fs::read_to_string(path)?;

        // Extract frontmatter if present
        let (frontmatter, body) = Self::split_frontmatter(&content);

        // Parse blocks from body
        let blocks = BlockParser::parse(&body)?;

        // Create node with blocks
        let mut node = Node::new(NodeType::Note, Self::extract_title(path));
        node.content = body;
        node.blocks = Some(blocks);

        // Apply frontmatter properties
        if let Some(fm) = frontmatter {
            Self::apply_frontmatter(&mut node, &fm)?;
        }

        Ok(node)
    }

    fn split_frontmatter(content: &str) -> (Option<String>, String) {
        if content.starts_with("---\n") {
            if let Some(end) = content[4..].find("\n---\n") {
                let frontmatter = content[4..4 + end].to_string();
                let body = content[4 + end + 5..].to_string();
                return (Some(frontmatter), body);
            }
        }
        (None, content.to_string())
    }

    fn extract_title(path: &Path) -> String {
        path.file_stem()
            .and_then(|s| s.to_str())
            .unwrap_or("Untitled")
            .to_string()
    }

    fn apply_frontmatter(node: &mut Node, frontmatter: &str) -> Result<()> {
        // Parse YAML frontmatter and apply to node
        // ... implementation ...
        Ok(())
    }
}

pub struct LogseqExporter;

impl LogseqExporter {
    /// Export a Node to Logseq page format
    pub fn export_page(node: &Node, path: &Path) -> Result<()> {
        let mut output = String::new();

        // Generate frontmatter
        output.push_str("---\n");
        output.push_str(&Self::generate_frontmatter(node)?);
        output.push_str("---\n\n");

        // Serialize blocks or use content
        if let Some(ref blocks) = node.blocks {
            output.push_str(&BlockParser::serialize(blocks));
        } else {
            output.push_str(&node.content);
        }

        std::fs::write(path, output)?;
        Ok(())
    }

    fn generate_frontmatter(node: &Node) -> Result<String> {
        let mut fm = String::new();
        fm.push_str(&format!("title: {}\n", node.title));
        fm.push_str(&format!("tags: {}\n", node.tags.join(", ")));
        // ... more frontmatter fields ...
        Ok(fm)
    }
}
}

Query API Extensions

#![allow(unused)]
fn main() {
// New methods in Graph or Query module

impl Graph {
    /// Find all blocks with a specific tag across all nodes
    pub fn find_blocks_by_tag(&mut self, tag: &str) -> Vec<(&Node, &Block)> {
        let mut results = Vec::new();
        for node in self.nodes.values_mut() {
            if let Ok(blocks) = node.find_blocks_by_tag(tag) {
                for block in blocks {
                    results.push((node as &Node, block));
                }
            }
        }
        results
    }

    /// Find all flashcards (#card blocks)
    pub fn find_flashcards(&mut self) -> Vec<(&Node, &Block)> {
        self.find_blocks_by_tag("card")
    }

    /// Find all TODO items across knowledge base
    pub fn find_all_todos(&mut self) -> Vec<(&Node, &Block)> {
        let mut results = Vec::new();
        for node in self.nodes.values_mut() {
            if let Ok(todos) = node.find_todos() {
                for block in todos {
                    results.push((node as &Node, block));
                }
            }
        }
        results
    }
}
}

MCP Tool Extensions

{
  "name": "kogral/find_blocks",
  "description": "Find blocks by tag, status, or properties",
  "inputSchema": {
    "type": "object",
    "properties": {
      "tag": { "type": "string", "description": "Filter by tag (e.g., 'card')" },
      "status": { "type": "string", "enum": ["TODO", "DONE", "DOING"] },
      "property": { "type": "string", "description": "Custom property key" },
      "value": { "type": "string", "description": "Property value to match" }
    }
  }
}

Configuration

# schemas/kb/contracts.ncl (additions)

BlockConfig = {
  enabled | Bool
    | doc "Enable block-level parsing and storage"
    | default = true,

  preserve_hierarchy | Bool
    | doc "Preserve block nesting on import/export"
    | default = true,

  parse_on_load | Bool
    | doc "Automatically parse blocks when loading nodes"
    | default = false,  # Lazy parsing by default

  supported_statuses | Array String
    | doc "Supported task statuses"
    | default = ["TODO", "DONE", "DOING", "LATER", "NOW", "WAITING", "CANCELLED"],
}

KbConfig = {
  # ... existing fields ...

  blocks | BlockConfig
    | doc "Block-level features configuration"
    | default = {},
}

Migration Path

Phase 1: Add Block models (no behavior change) Phase 2: Add BlockParser (opt-in via config) Phase 3: Update Logseq import/export Phase 4: Add block queries to CLI/MCP Phase 5: SurrealDB block indexing

Backward Compatibility:

  • Existing nodes without blocks field work as before
  • content remains source of truth
  • blocks is optional cache/structure
  • Config flag blocks.enabled to opt-in

Testing Strategy

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_simple_block() {
        let content = "- This is a block #card";
        let blocks = BlockParser::parse(content).unwrap();

        assert_eq!(blocks.len(), 1);
        assert_eq!(blocks[0].content, "This is a block");
        assert_eq!(blocks[0].properties.tags, vec!["card"]);
    }

    #[test]
    fn test_parse_nested_blocks() {
        let content = r#"
- Parent block
  - Child block 1
  - Child block 2
    - Grandchild
"#;
        let blocks = BlockParser::parse(content).unwrap();

        assert_eq!(blocks.len(), 1);
        assert_eq!(blocks[0].children.len(), 2);
        assert_eq!(blocks[0].children[1].children.len(), 1);
    }

    #[test]
    fn test_parse_todo() {
        let content = "- TODO Implement feature #rust";
        let blocks = BlockParser::parse(content).unwrap();

        assert_eq!(blocks[0].properties.status, Some(TaskStatus::Todo));
        assert_eq!(blocks[0].content, "Implement feature");
        assert_eq!(blocks[0].properties.tags, vec!["rust"]);
    }

    #[test]
    fn test_roundtrip() {
        let original = r#"- Block 1 #card
  - Nested
- TODO Block 2
  priority:: high
"#;
        let blocks = BlockParser::parse(original).unwrap();
        let serialized = BlockParser::serialize(&blocks);
        let reparsed = BlockParser::parse(&serialized).unwrap();

        assert_eq!(blocks.len(), reparsed.len());
        assert_eq!(blocks[0].properties, reparsed[0].properties);
    }
}
}

Summary

Recommended Approach: Hybrid (Option C)

  • Add Block struct with properties, hierarchy
  • Extend Node with optional blocks: Option<Vec<Block>>
  • Implement bidirectional parser (markdown ↔ blocks)
  • Preserve content as source of truth (backward compat)
  • Enable block queries in CLI/MCP
  • Support round-trip Logseq import/export

Benefits:

  • βœ… Full Logseq compatibility
  • βœ… Queryable blocks (find #card, TODO, etc.)
  • βœ… Backward compatible
  • βœ… Extensible (custom properties)
  • βœ… Type-safe structure

Trade-offs:

  • ⚠️ Added complexity
  • ⚠️ Need to sync content ↔ blocks
  • ⚠️ More storage for SurrealDB backend

Next Steps:

  1. Review and approve design
  2. Implement Phase 1 (Block models)
  3. Implement Phase 2 (BlockParser)
  4. Update Logseq import/export
  5. Add block queries to MCP/CLI

ADR-001: Nickel vs TOML for Configuration

Status: Accepted

Date: 2026-01-17

Deciders: Architecture Team

Context: Configuration Strategy for Knowledge Base System


Context

The KOGRAL requires a flexible, type-safe configuration format that supports:

  1. Complex nested structures (graph settings, storage configs, embedding providers)
  2. Type validation (prevent runtime errors from config mistakes)
  3. Composition and inheritance (shared configs, environment-specific overrides)
  4. Documentation (self-documenting schemas)
  5. Validation before runtime (catch errors early)

We evaluated two primary options:

Option 1: TOML (Traditional Config Format)

Pros:

  • Widely adopted in Rust ecosystem (Cargo.toml)
  • Simple, human-readable syntax
  • Native serde support
  • IDE support (syntax highlighting, completion)

Cons:

  • No type system (validation only at runtime)
  • Limited composition (no imports, no functions)
  • No schema validation (errors discovered during execution)
  • Verbose for complex nested structures
  • No documentation in config files

Example TOML:

[graph]
name = "my-project"
version = "1.0.0"

[storage]
primary = "filesystem"  # String, not validated as enum

[storage.secondary]
enabled = true
type = "surrealdb"  # Typo would fail at runtime
url = "ws://localhost:8000"

[embeddings]
enabled = true
provider = "openai"  # No validation of valid providers
model = "text-embedding-3-small"

Problems:

  • Typos in enum values ("surrealdb" vs "surealdb") fail at runtime
  • No validation that provider = "openai" requires api_key_env
  • No documentation of valid options
  • No way to compose configs (e.g., base config + environment override)

Option 2: Nickel (Functional Configuration Language)

Pros:

  • Type system with contracts (validate before runtime)
  • Composition via imports and merging
  • Documentation in schemas (self-documenting)
  • Validation at export time (catch errors early)
  • Functions for conditional logic
  • Default values in schema definitions

Cons:

  • Less familiar to Rust developers
  • Requires separate nickel CLI tool
  • Smaller ecosystem
  • Steeper learning curve

Example Nickel:

# schemas/kb-config.ncl
{
  KbConfig = {
    graph | GraphConfig,
    storage | StorageConfig,
    embeddings | EmbeddingConfig,
  },

  StorageConfig = {
    primary | [| 'filesystem, 'memory |],  # Enum validated at export

    secondary | {
      enabled | Bool,
      type | [| 'surrealdb, 'sqlite |],  # Typos caught immediately
      url | String,
    } | optional,
  },

  EmbeddingConfig = {
    enabled | Bool,
    provider | [| 'openai, 'claude, 'fastembed |],  # Valid providers enforced
    model | String,
    api_key_env | String | doc "Environment variable for API key",
  },
}

Benefits:

  • Typos in enum values caught at nickel export time
  • Schema enforces required fields based on provider
  • Documentation embedded in schema
  • Config can be composed: import "base.ncl" & { /* overrides */ }

Decision

We will use Nickel for configuration.

Implementation:

  1. Define schemas in schemas/*.ncl with type contracts
  2. Users write configs in .kogral/config.ncl
  3. Export to JSON via CLI: nickel export --format json config.ncl
  4. Load JSON in Rust via serde_json into typed structs

Pattern (double validation):

Nickel Config (.ncl)
    ↓ [nickel export]
JSON (validated by Nickel contracts)
    ↓ [serde_json::from_str]
Rust Struct (validated by serde)
    ↓
Runtime (guaranteed valid config)

Bridge Code (kb-core/src/config/nickel.rs):

#![allow(unused)]
fn main() {
pub fn load_config<P: AsRef<Path>>(path: P) -> Result<KbConfig> {
    // Export Nickel to JSON
    let json = export_nickel_to_json(path)?;

    // Deserialize to Rust struct
    let config: KbConfig = serde_json::from_str(&json)?;

    Ok(config)
}

fn export_nickel_to_json<P: AsRef<Path>>(path: P) -> Result<String> {
    let output = Command::new("nickel")
        .arg("export")
        .arg("--format").arg("json")
        .arg(path.as_ref())
        .output()?;

    Ok(String::from_utf8(output.stdout)?)
}
}

Consequences

Positive

βœ… Type Safety: Config errors caught before runtime

  • Invalid enum values fail at export: 'filesystm β†’ error
  • Missing required fields detected: no graph.name β†’ error
  • Type mismatches prevented: enabled = "yes" β†’ error (expects Bool)

βœ… Self-Documenting: Schemas serve as documentation

  • | doc "Environment variable for API key" describes fields
  • Enum options visible in schema: [| 'openai, 'claude, 'fastembed |]
  • Default values explicit: | default = 'filesystem

βœ… Composition: Config reuse and overrides

# base.ncl
{ graph = { version = "1.0.0" } }

# project.ncl
import "base.ncl" & { graph = { name = "my-project" } }

βœ… Validation Before Deployment: Catch errors in CI

# CI pipeline
nickel typecheck config.ncl
nickel export --format json config.ncl > /dev/null

βœ… Conditional Logic: Environment-specific configs

let is_prod = std.string.is_match "prod" (std.env.get "ENV") in
{
  embeddings = {
    provider = if is_prod then 'openai else 'fastembed,
  },
}

Negative

❌ Learning Curve: Team must learn Nickel syntax

  • Mitigation: Provide comprehensive examples in config/ directory
  • Mitigation: Document common patterns in docs/config/

❌ Tool Dependency: Requires nickel CLI installed

  • Mitigation: Document installation in setup guide
  • Mitigation: Check nickel availability in kb init command

❌ IDE Support: Limited compared to TOML

  • Mitigation: Use LSP (nickel-lang-lsp) for VSCode/Neovim
  • Mitigation: Syntax highlighting available for major editors

❌ Ecosystem Size: Smaller than TOML

  • Mitigation: Nickel actively developed by Tweag
  • Mitigation: Stable language specification (v1.0+)

Neutral

βšͺ Two-Stage Loading: Nickel β†’ JSON β†’ Rust

  • Not a performance concern (config loaded once at startup)
  • Adds resilience (double validation)
  • Allows runtime config inspection (read JSON directly)

Alternatives Considered

JSON Schema

Rejected: Not ergonomic for humans to write

  • No comments
  • Verbose syntax ({"key": "value"} vs key = value)
  • JSON Schema separate from config (duplication)

YAML

Rejected: No type system, ambiguous parsing

  • Boolean confusion: yes/no/on/off/true/false
  • Indentation-sensitive (error-prone)
  • No validation without external tools

Dhall

Rejected: More complex than needed

  • Turing-incomplete by design (limits use cases)
  • Smaller ecosystem than Nickel
  • Steeper learning curve

KCL (KusionStack Configuration Language)

Rejected: Kubernetes-focused, less general-purpose

  • Designed for K8s manifests
  • Less mature than Nickel for general config

Implementation Timeline

  1. βœ… Define base schemas (schemas/kb-config.ncl)
  2. βœ… Implement Nickel loader (kb-core/src/config/nickel.rs)
  3. βœ… Create example configs (config/defaults.ncl, config/production.ncl)
  4. βœ… Document Nickel usage (docs/config/nickel-schemas.md)
  5. ⏳ Add LSP recommendations to setup guide
  6. ⏳ Create Nickel β†’ TOML migration tool (for existing users)

Monitoring

Success Criteria:

  • Config errors caught at export time (not runtime)
  • Users can compose configs for different environments
  • Team comfortable with Nickel syntax within 2 weeks

Metrics:

  • Number of config validation errors caught before runtime
  • Time to diagnose config issues (should decrease)
  • User feedback on config complexity

References


Revision History

DateAuthorChange
2026-01-17Architecture TeamInitial decision

Next ADR: ADR-002: FastEmbed via AI Providers

ADR-002: FastEmbed via AI Providers for Embeddings

Status: Accepted

Date: 2026-01-17

Deciders: Architecture Team

Context: Embedding Strategy for Semantic Search


Context

The KOGRAL requires embedding generation for semantic search capabilities. Embeddings convert text into numerical vectors that capture semantic meaning, enabling "find concepts" rather than just "find keywords".

Requirements:

  1. Local-First Option: Must work offline without external API dependencies
  2. Production Scalability: Support cloud AI providers for large-scale deployments
  3. Multiple Providers: Flexibility to choose based on cost, quality, privacy
  4. Cost-Effective Development: Free local embeddings for development and testing
  5. Quality: Good enough embeddings for finding related concepts

Options Evaluated:

Option 1: Only Local Embeddings (fastembed)

Pros:

  • No API costs
  • Works offline
  • Privacy-preserving (no data leaves machine)
  • Fast (local GPU acceleration possible)

Cons:

  • Limited model quality compared to cloud providers
  • Resource-intensive (requires download ~100MB models)
  • Single provider lock-in (fastembed library)

Example:

#![allow(unused)]
fn main() {
use fastembed::{TextEmbedding, InitOptions};

let model = TextEmbedding::try_new(InitOptions {
    model_name: "BAAI/bge-small-en-v1.5",
    ..Default::default()
})?;

let embeddings = model.embed(vec!["Hello world"], None)?;
// Output: Vec<Vec<f32>> with 384 dimensions
}

Option 2: Only Cloud AI Providers (OpenAI, Claude, etc.)

Pros:

  • State-of-the-art embedding quality
  • No local resource usage
  • Latest models available
  • Scalable to millions of documents

Cons:

  • Requires API keys (cost per embedding)
  • Network dependency (no offline mode)
  • Privacy concerns (data sent to third parties)
  • Vendor lock-in risk

Example:

#![allow(unused)]
fn main() {
use rig::providers::openai;

let client = openai::Client::new("sk-...");
let embeddings = client.embeddings("text-embedding-3-small")
    .embed_documents(vec!["Hello world"]).await?;
// Output: Vec<Vec<f32>> with 1536 dimensions
}

Option 3: Hybrid Strategy (fastembed + AI providers via rig-core)

Pros:

  • βœ… Best of both worlds: local dev, cloud production
  • βœ… User choice: privacy-first or quality-first
  • βœ… Cost flexibility: free for small projects, paid for scale
  • βœ… Unified interface via rig-core library
  • βœ… Easy provider switching (config-driven)

Cons:

  • ❌ More complex implementation (multiple providers)
  • ❌ Dimension mismatch between providers (384 vs 1536)
  • ❌ Additional dependencies (rig-core, fastembed)

Decision

We will use a hybrid strategy: fastembed (local) + AI providers (via rig-core).

Implementation:

  1. Default: fastembed with BAAI/bge-small-en-v1.5 (384 dimensions)
  2. Optional: OpenAI, Claude, Ollama via rig-core (configurable)
  3. Interface: EmbeddingProvider trait abstracts provider details
  4. Config-Driven: Provider selection via Nickel configuration

Architecture:

#![allow(unused)]
fn main() {
#[async_trait]
pub trait EmbeddingProvider: Send + Sync {
    async fn embed(&self, texts: Vec<String>) -> Result<Vec<Vec<f32>>>;
    fn dimensions(&self) -> usize;
    fn model_name(&self) -> &str;
}

// Local implementation
pub struct FastEmbedProvider {
    model: TextEmbedding,
}

impl FastEmbedProvider {
    pub fn new(model_name: &str) -> Result<Self> {
        let model = TextEmbedding::try_new(InitOptions {
            model_name: model_name.into(),
            ..Default::default()
        })?;
        Ok(Self { model })
    }
}

#[async_trait]
impl EmbeddingProvider for FastEmbedProvider {
    async fn embed(&self, texts: Vec<String>) -> Result<Vec<Vec<f32>>> {
        Ok(self.model.embed(texts, None)?)
    }

    fn dimensions(&self) -> usize { 384 }
    fn model_name(&self) -> &str { "BAAI/bge-small-en-v1.5" }
}

// Cloud provider implementation (via rig-core)
pub struct RigEmbeddingProvider {
    client: rig::Client,
    model: String,
    dimensions: usize,
}

#[async_trait]
impl EmbeddingProvider for RigEmbeddingProvider {
    async fn embed(&self, texts: Vec<String>) -> Result<Vec<Vec<f32>>> {
        let embeddings = self.client
            .embeddings(&self.model)
            .embed_documents(texts)
            .await?;
        Ok(embeddings)
    }

    fn dimensions(&self) -> usize { self.dimensions }
    fn model_name(&self) -> &str { &self.model }
}
}

Configuration (Nickel):

# Local development (default)
{
  embeddings = {
    enabled = true,
    provider = 'fastembed,
    model = "BAAI/bge-small-en-v1.5",
    dimensions = 384,
  },
}

# Production with OpenAI
{
  embeddings = {
    enabled = true,
    provider = 'openai,
    model = "text-embedding-3-small",
    dimensions = 1536,
    api_key_env = "OPENAI_API_KEY",
  },
}

# Self-hosted with Ollama
{
  embeddings = {
    enabled = true,
    provider = 'ollama,
    model = "nomic-embed-text",
    dimensions = 768,
  },
}

Provider Selection (kb-core/src/embeddings/mod.rs):

#![allow(unused)]
fn main() {
pub fn create_provider(config: &EmbeddingConfig) -> Result<Box<dyn EmbeddingProvider>> {
    match config.provider {
        EmbeddingProviderType::FastEmbed => {
            Ok(Box::new(FastEmbedProvider::new(&config.model)?))
        }
        EmbeddingProviderType::OpenAI => {
            let api_key = std::env::var(&config.api_key_env)?;
            Ok(Box::new(RigEmbeddingProvider::new_openai(api_key, &config.model)?))
        }
        EmbeddingProviderType::Claude => {
            let api_key = std::env::var(&config.api_key_env)?;
            Ok(Box::new(RigEmbeddingProvider::new_claude(api_key, &config.model)?))
        }
        EmbeddingProviderType::Ollama => {
            Ok(Box::new(RigEmbeddingProvider::new_ollama(&config.model)?))
        }
    }
}
}

Consequences

Positive

βœ… Development Flexibility:

  • Developers can use fastembed without API keys
  • Fast feedback loop (local embeddings, no network calls)
  • Works offline (train trips, flights)

βœ… Production Quality:

  • Production deployments can use OpenAI/Claude for better quality
  • Latest embedding models available
  • Scalable to millions of documents

βœ… Privacy Control:

  • Privacy-sensitive projects use local embeddings
  • Public projects can use cloud providers
  • User choice via configuration

βœ… Cost Optimization:

  • Small projects: free (fastembed)
  • Large projects: pay for quality (cloud providers)
  • Hybrid: important docs via cloud, bulk via local

βœ… Unified Interface:

  • EmbeddingProvider trait abstracts provider details
  • Query code doesn't know/care about provider
  • Easy to add new providers

Negative

❌ Dimension Mismatch:

  • fastembed: 384 dimensions
  • OpenAI: 1536 dimensions
  • Cannot mix in same index

Mitigation:

  • Store provider + dimensions in node metadata
  • Rebuild index when changing providers
  • Document dimension constraints

❌ Model Download:

  • First use of fastembed downloads ~100MB model
  • Slow initial startup

Mitigation:

  • Pre-download in Docker images
  • Document model download in setup guide
  • Cache models in ~/.cache/fastembed

❌ Complex Configuration:

  • Multiple provider options may confuse users

Mitigation:

  • Sane default (fastembed)
  • Clear examples for each provider
  • Validation errors explain misconfigurations

Neutral

βšͺ Dependency Trade-off:

  • fastembed adds ~5MB to binary
  • rig-core adds ~2MB
  • Total: ~7MB overhead

Not a concern for CLI/MCP server use case.


Provider Comparison

ProviderDimensionsQualityCostPrivacyOffline
fastembed384GoodFreeβœ… Localβœ… Yes
OpenAI1536Excellent$0.0001/1K❌ Cloud❌ No
Claude1024Excellent$0.00025/1K❌ Cloud❌ No
Ollama768Very GoodFreeβœ… Localβœ… Yes

Recommendation by Use Case:

  • Development: fastembed (fast, free, offline)
  • Small Teams: fastembed or Ollama (privacy, no costs)
  • Enterprise: OpenAI or Claude (best quality, scalable)
  • Self-Hosted: Ollama (good quality, local control)

Implementation Timeline

  1. βœ… Define EmbeddingProvider trait
  2. βœ… Implement FastEmbedProvider (stub, feature-gated)
  3. βœ… Implement RigEmbeddingProvider (stub, feature-gated)
  4. ⏳ Complete FastEmbed integration with model download
  5. ⏳ Complete rig-core integration (OpenAI, Claude, Ollama)
  6. ⏳ Add query engine with similarity search
  7. ⏳ Document provider selection and trade-offs

Monitoring

Success Criteria:

  • Users can switch providers via config change
  • Local embeddings work without API keys
  • Production deployments use cloud providers successfully
  • Query quality acceptable for both local and cloud embeddings

Metrics:

  • Embedding generation latency (local vs cloud)
  • Query accuracy (precision@10 for semantic search)
  • API costs (cloud providers)
  • User satisfaction (feedback on search quality)

References


Revision History

DateAuthorChange
2026-01-17Architecture TeamInitial decision

Previous ADR: ADR-001: Nickel vs TOML Next ADR: ADR-003: Hybrid Storage Strategy

ADR-003: Hybrid Storage Strategy

Status: Accepted

Date: 2026-01-17

Deciders: Architecture Team

Context: Storage Backend Strategy for Knowledge Base


Context

The KOGRAL needs to store knowledge graphs with these requirements:

  1. Git-Friendly: Knowledge should version alongside code
  2. Scalable: Support small projects (10s of nodes) to large organizations (10,000+ nodes)
  3. Queryable: Efficient graph queries and relationship traversal
  4. Offline-Capable: Work without network access
  5. Collaborative: Support shared organizational knowledge
  6. Cost-Effective: Free for small projects, reasonable cost at scale

Constraints:

  • Developers want to edit knowledge in text editors
  • Organizations want centralized guideline management
  • Git workflows essential for code-adjacent knowledge
  • Large graphs need database performance

Option 1: Filesystem Only

Approach: Store everything as markdown files

Pros:

  • βœ… Git-native (perfect for versioning)
  • βœ… Text editor friendly
  • βœ… No dependencies
  • βœ… Works offline
  • βœ… Free

Cons:

  • ❌ Poor performance for large graphs (100 0+ nodes)
  • ❌ No efficient graph queries
  • ❌ Difficult to share across projects
  • ❌ Manual sync for collaboration

Scalability: Good for < 100 nodes, poor beyond

Option 2: Database Only (SurrealDB)

Approach: Store all knowledge in SurrealDB graph database

Pros:

  • βœ… Excellent query performance
  • βœ… Native graph relationships
  • βœ… Scalable to millions of nodes
  • βœ… Centralized for collaboration

Cons:

  • ❌ Not git-trackable
  • ❌ Requires running database server
  • ❌ Can't edit with text editor
  • ❌ Network dependency
  • ❌ Infrastructure cost

Scalability: Excellent, but loses developer workflow benefits

Option 3: Hybrid (Filesystem + SurrealDB)

Approach: Filesystem for local project knowledge, SurrealDB for shared organizational knowledge

Pros:

  • βœ… Git-friendly for project knowledge
  • βœ… Text editor friendly
  • βœ… Scalable for shared knowledge
  • βœ… Works offline (local graph)
  • βœ… Collaborative (shared graph)
  • βœ… Cost-effective (DB only for shared)

Cons:

  • ❌ More complex implementation
  • ❌ Sync mechanism needed
  • ❌ Two storage systems to manage

Scalability: Excellent - best of both worlds


Decision

We will use a hybrid storage strategy: Filesystem (local) + SurrealDB (shared).

Architecture:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Project A (.kogral/)                          β”‚
β”‚  Storage: Filesystem (git-tracked)                          β”‚
β”‚  Scope: Project-specific notes, decisions, patterns         β”‚
β”‚  Access: Local only                                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   β”‚
                   β”‚ [inherits]
                   ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           Shared KB (SurrealDB or synced filesystem)         β”‚
β”‚  Storage: SurrealDB (scalable) or filesystem (synced)       β”‚
β”‚  Scope: Organization-wide guidelines, patterns              β”‚
β”‚  Access: All projects                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementation:

# Project config
{
  storage = {
    primary = 'filesystem,  # Local project knowledge
    secondary = {
      enabled = true,
      type = 'surrealdb,   # Shared knowledge
      url = "ws://kb-central.company.com:8000",
      namespace = "organization",
      database = "shared-kb",
    },
  },

  inheritance = {
    base = "surrealdb://organization/shared-kb",  # Inherit from shared
    priority = 100,  # Project overrides shared
  },
}

Sync Strategy:

.kogral/ (Filesystem)
    ↓ [on save]
Watch for changes
    ↓ [debounced]
Sync to SurrealDB
    ↓
Shared graph updated
    ↓ [on query]
Merge local + shared results

Consequences

Positive

βœ… Developer Workflow Preserved:

# Local knowledge workflow (unchanged)
vim .kogral/notes/my-note.md
git add .kogral/notes/my-note.md
git commit -m "Add implementation note"
git push

βœ… Git Integration:

  • Project knowledge versioned with code
  • Branches include relevant knowledge
  • Merges resolve knowledge conflicts
  • PR reviews include knowledge changes

βœ… Offline Development:

  • Full functionality without network
  • Shared guidelines cached locally
  • Sync when reconnected

βœ… Scalability:

  • Projects: filesystem (100s of nodes, fine performance)
  • Organization: SurrealDB (10,000+ nodes, excellent performance)

βœ… Collaboration:

  • Shared guidelines accessible to all projects
  • Updates to shared knowledge propagate automatically
  • Consistent practices across organization

βœ… Cost-Effective:

  • Small projects: free (filesystem only)
  • Organizations: SurrealDB for shared only (not all project knowledge)

βœ… Gradual Adoption:

  • Start with filesystem only
  • Add SurrealDB when needed
  • Feature-gated (--features surrealdb)

Negative

❌ Complexity:

  • Two storage implementations
  • Sync mechanism required
  • Conflict resolution needed

Mitigation:

  • Storage trait abstracts differences
  • Sync is optional (can disable)
  • Conflicts rare (guidelines change infrequently)

❌ Sync Latency:

  • Changes to shared KB not instant in all projects

Mitigation:

  • Acceptable latency (guidelines don't change rapidly)
  • Manual sync command available (kb sync)
  • Auto-sync on query (fetch latest)

❌ Infrastructure Requirement:

  • SurrealDB server needed for shared KB

Mitigation:

  • Optional (can use synced filesystem instead)
  • Docker Compose for easy setup
  • Managed SurrealDB Cloud option

Neutral

βšͺ Storage Trait Implementation:

#![allow(unused)]
fn main() {
#[async_trait]
pub trait Storage {
    async fn save_graph(&self, graph: &Graph) -> Result<()>;
    async fn load_graph(&self, name: &str) -> Result<Graph>;
    async fn list_graphs(&self) -> Result<Vec<String>>;
}

// Three implementations
impl Storage for FilesystemStorage { /* ... */ }
impl Storage for SurrealDbStorage { /* ... */ }
impl Storage for MemoryStorage { /* ... */ }
}

Abstraction makes multi-backend manageable.


Use Cases

Small Project (Solo Developer)

Config:

{ storage = { primary = 'filesystem } }

Behavior:

  • All knowledge in .kogral/ directory
  • Git-tracked with code
  • No database required
  • Works offline

Medium Project (Team)

Config:

{
  storage = {
    primary = 'filesystem,
    secondary = {
      enabled = true,
      type = 'surrealdb,
      url = "ws://team-kb.local:8000",
    },
  },
}

Behavior:

  • Project knowledge in .kogral/ (git-tracked)
  • Shared team patterns in SurrealDB
  • Automatic sync
  • Offline fallback to cached

Large Organization

Config:

{
  storage = {
    primary = 'filesystem,
    secondary = {
      enabled = true,
      type = 'surrealdb,
      url = "ws://kb.company.com:8000",
      namespace = "engineering",
      database = "shared-kb",
    },
  },

  inheritance = {
    base = "surrealdb://engineering/shared-kb",
    guidelines = [
      "surrealdb://engineering/rust-guidelines",
      "surrealdb://engineering/security-policies",
    ],
  },
}

Behavior:

  • Project-specific in .kogral/
  • Organization guidelines in SurrealDB
  • Security policies enforced
  • Automatic guideline updates

Sync Mechanism

Filesystem β†’ SurrealDB

Trigger: File changes detected (via notify crate)

Process:

  1. Parse changed markdown file
  2. Convert to Node struct
  3. Upsert to SurrealDB
  4. Update relationships

Debouncing: 500ms (configurable)

SurrealDB β†’ Filesystem

Trigger: Query for shared knowledge

Process:

  1. Query SurrealDB for shared nodes
  2. Cache locally (in-memory or filesystem)
  3. Merge with local results
  4. Return combined

Caching: TTL-based (5 minutes default)

Conflict Resolution

Strategy: Last-write-wins with version tracking

Example:

Project A: Updates shared guideline (v1 β†’ v2)
Project B: Has cached v1

On Project B query:
  - Detects v2 available
  - Fetches v2
  - Updates cache
  - Uses v2 going forward

Alternatives Considered

Git Submodules for Shared Knowledge

Rejected: Cumbersome workflow

  • Requires manual submodule update
  • Merge conflicts in shared submodule
  • Not discoverable (need to know submodule exists)

Syncthing for Filesystem Sync

Rejected: Not designed for this use case

  • No query optimization
  • No relationship indexes
  • Sync conflicts difficult to resolve

PostgreSQL with JSON

Rejected: Not a graph database

  • Poor graph query performance
  • Relationship traversal requires complex SQL joins
  • No native graph features

Migration Path

Phase 1: Filesystem Only (Current)

  • All storage via filesystem
  • Git-tracked
  • No database required

Phase 2: Optional SurrealDB

  • Add SurrealDB support (feature-gated)
  • Manual sync command
  • Shared KB opt-in

Phase 3: Automatic Sync

  • File watching
  • Auto-sync on changes
  • Background sync

Phase 4: Multi-Tenant SurrealDB

  • Organization namespaces
  • Access control
  • Audit logs

Monitoring

Success Criteria:

  • Developers don't notice hybrid complexity
  • Sync completes < 1 second for typical changes
  • Shared guidelines accessible in < 100ms
  • Zero data loss in sync

Metrics:

  • Sync latency (P50, P95, P99)
  • Cache hit rate (shared knowledge)
  • Conflict rate (expect < 0.1%)
  • User satisfaction

References


Revision History

DateAuthorChange
2026-01-17Architecture TeamInitial decision

Previous ADR: ADR-002: FastEmbed via AI Providers Next ADR: ADR-004: Logseq Compatibility

ADR-004: Logseq Blocks Support

Status

Proposed (Design phase)

Context

Logseq uses content blocks as the fundamental unit of information, not full documents. KB currently treats Node.content as flat markdown string, which loses block-level features on import/export:

Lost features:

  • Block properties (#card, TODO, custom properties)
  • Block hierarchy (outliner nesting)
  • Block references (((block-uuid)))
  • Block-level queries (find all flashcards, TODOs)

User requirement: Round-trip Logseq import/export with full fidelity:

Logseq β†’ KOGRAL Import β†’ KOGRAL Storage β†’ KOGRAL Export β†’ Logseq
  (blocks preserved at every step)

Decision

Implement Hybrid Block Support (structured + markdown):

1. Add Block Data Structure

#![allow(unused)]
fn main() {
pub struct Block {
    pub id: String,                   // UUID
    pub content: String,              // Block text
    pub properties: BlockProperties,  // Tags, status, custom
    pub children: Vec<Block>,         // Nested blocks
    // ... timestamps ...
}

pub struct BlockProperties {
    pub tags: Vec<String>,            // #card, #important
    pub status: Option<TaskStatus>,   // TODO, DONE, etc.
    pub custom: HashMap<String, String>, // property:: value
    pub block_refs: Vec<String>,      // ((uuid))
    pub page_refs: Vec<String>,       // [[page]]
}
}

2. Extend Node Model

#![allow(unused)]
fn main() {
pub struct Node {
    // ... existing fields ...
    pub content: String,              // Source of truth (markdown)
    pub blocks: Option<Vec<Block>>,   // Cached structure (optional)
}
}

3. Bidirectional Parser

  • Parse: Markdown β†’ Vec<Block> (lazy, on-demand)
  • Serialize: Vec<Block> β†’ Markdown (for export)

4. Storage Strategy

Filesystem (git-friendly, Logseq-compatible):

- Block 1 #card
  - Nested answer
- TODO Block 2
  priority:: high

SurrealDB (queryable):

DEFINE TABLE block;
DEFINE FIELD node_id ON block TYPE record(node);
DEFINE FIELD block_id ON block TYPE string;
DEFINE FIELD properties ON block TYPE object;
DEFINE INDEX block_tags ON block COLUMNS properties.tags;

5. Query Extensions

#![allow(unused)]
fn main() {
// Find all flashcards
graph.find_blocks_by_tag("card")

// Find all TODOs
graph.find_all_todos()

// Find blocks with custom property
node.find_blocks_by_property("priority", "high")
}

Consequences

Positive

βœ… Full Logseq Compatibility - Import/export preserves all block features βœ… Queryable Blocks - Find #card, TODO, custom properties across KOGRAL βœ… Backward Compatible - Existing nodes without blocks still work βœ… Type-Safe - Structured data instead of regex parsing everywhere βœ… Extensible - Custom block properties supported βœ… Hierarchy Preserved - Nested blocks maintain parent-child relationships

Negative

⚠️ Added Complexity - New data structures, parser, sync logic ⚠️ Dual Representation - Must keep content and blocks in sync ⚠️ Storage Overhead - SurrealDB stores both markdown and structure ⚠️ Migration Required - Existing data needs parsing to populate blocks

Neutral

βš™οΈ Lazy Parsing - Blocks parsed on-demand (not stored by default) βš™οΈ Opt-In - Config flag blocks.enabled to activate features βš™οΈ Gradual Adoption - Can implement in phases

Implementation Phases

Phase 1: Foundation (No behavior change)

  • Add Block struct to models/block.rs
  • Add optional blocks field to Node
  • Add config: blocks.enabled = false (default off)

Phase 2: Parser

  • Implement BlockParser::parse() (markdown β†’ blocks)
  • Implement BlockParser::serialize() (blocks β†’ markdown)
  • Add Node::get_blocks() method (lazy parsing)

Phase 3: Logseq Integration

  • Update LogseqImporter to parse blocks
  • Update LogseqExporter to serialize blocks
  • Test round-trip (Logseq β†’ KB β†’ Logseq)

Phase 4: Query API

  • Add Graph::find_blocks_by_tag()
  • Add Graph::find_all_todos()
  • Add Node::find_blocks_by_property()

Phase 5: MCP/CLI Integration

  • Add kb/find_blocks MCP tool
  • Add kb find-cards CLI command
  • Add kb find-todos CLI command

Phase 6: SurrealDB Backend

  • Create block table schema
  • Index on tags, status, properties
  • Store blocks alongside nodes

Alternatives Considered

Alternative 1: Blocks as First-Class Nodes

Convert each Logseq block to a separate KOGRAL Node.

Rejected: Too granular, explosion of nodes, loses document context.

Alternative 2: Parser-Only (No Storage)

Keep content: String, parse blocks on every access.

Rejected: Can't query blocks in database, parse overhead, can't index.

Alternative 3: Metadata Field

Store blocks in metadata: HashMap<String, Value>.

Rejected: Not type-safe, harder to query, no schema validation.

References

Notes

Backward Compatibility Strategy:

  • content remains source of truth
  • blocks is optional enhancement
  • Old code works unchanged
  • New features opt-in via config

Migration Path:

  • Existing users: blocks disabled by default
  • New users: blocks enabled, parsed on import
  • Manual: kb reindex --parse-blocks to populate

Decision Date: 2026-01-17 Approvers: TBD Review Date: After Phase 2 implementation

ADR-005: MCP Protocol for AI Integration

Prerequisites

Installation Methods

Project Initialization

Environment Configuration

Verification

Configuration Overview

Nickel Schemas

Graph Settings

Inheritance

Examples

Storage Backends

Filesystem Storage

SurrealDB Storage

In-Memory Storage

Sync Strategies

Embeddings Overview

Provider Configuration

FastEmbed Local

OpenAI Integration

Claude Integration

Ollama Integration

Semantic Search

Template System

Document Templates

Export Templates

Customization

CLI Overview

Commands Reference

Workflows

NuShell Scripts

MCP Quick Guide

Fast-track guide to integrating KOGRAL with Claude Code via the Model Context Protocol (MCP).

What is MCP?

MCP (Model Context Protocol) is a protocol that allows Claude Code to interact with external tools and data sources. The kogral-mcp server exposes your KOGRAL knowledge base to Claude Code for AI-assisted knowledge management.

Quick Setup (5 Minutes)

Step 1: Build MCP Server

# Build kogral-mcp server
cargo build --package kb-mcp --release

# Verify binary
ls -lh target/release/kb-mcp

Step 2: Configure Claude Code

Add to ~/.config/claude/config.json:

{
  "mcpServers": {
    "kogral-mcp": {
      "command": "/path/to/knowledge-base/target/release/kb-mcp",
      "args": ["serve"],
      "env": {
        "KOGRAL_DIR": "/path/to/your/project/.kogral"
      }
    }
  }
}

Replace /path/to/knowledge-base and /path/to/your/project/.kogral with your actual paths.

Step 3: Initialize KOGRAL

# Navigate to your project
cd /path/to/your/project

# Initialize .kb directory
kb init

Step 4: Start Claude Code

# Start Claude Code (will auto-connect to kb-mcp)
claude-code

Step 5: Test Connection

In Claude Code, try:

Search for "error handling"

Claude will use the kogral/search tool to query your knowledge base.


Essential Commands

Search Knowledge Base

Natural language:

Find notes about Rust error handling

Claude translates to:

{
  "tool": "kogral/search",
  "params": {
    "query": "error handling",
    "type": "note",
    "semantic": true
  }
}

Add Note

Natural language:

Add a note about async Rust patterns with tags rust, async, patterns

Claude translates to:

{
  "tool": "kogral/add_note",
  "params": {
    "title": "Async Rust Patterns",
    "content": "...",
    "tags": ["rust", "async", "patterns"]
  }
}

Add Decision

Natural language:

Document decision to use SurrealDB for storage

Claude translates to:

{
  "tool": "kogral/add_decision",
  "params": {
    "title": "Use SurrealDB for Storage",
    "context": "Need scalable graph database",
    "decision": "Adopt SurrealDB as primary storage backend",
    "consequences": ["Better query performance", "Additional infrastructure"]
  }
}

Get Guidelines

Natural language:

What are our Rust error handling guidelines?

Claude translates to:

{
  "tool": "kogral/get_guidelines",
  "params": {
    "language": "rust",
    "category": "error-handling"
  }
}

Natural language:

Link note-async-patterns to decision-use-tokio as implements

Claude translates to:

{
  "tool": "kogral/link",
  "params": {
    "from": "note-async-patterns",
    "to": "decision-use-tokio",
    "relation": "implements"
  }
}

Common Workflows

Capture Meeting Notes

You:

We just decided in our meeting to migrate from REST to GraphQL.
Context: current REST API is too chatty, requires multiple round trips.
Consequences: better client performance, but team needs to learn GraphQL.
Create a decision record.

Claude (uses kb/add_decision):

Created decision: decision-migrate-to-graphql

Status: proposed
Context: Current REST API requires multiple round trips (chatty)
Decision: Migrate from REST to GraphQL API
Consequences:
  - βœ… Better client performance
  - ❌ Team learning curve for GraphQL

You:

I'm working on error handling. What related notes and patterns do we have?

Claude (uses kogral/search with semantic=true):

Found 5 related items:

1. **Error Handling with thiserror** (pattern)
   Tags: rust, error-handling
   Created: 2026-01-15

2. **Result Type Best Practices** (guideline)
   Tags: rust, error-handling, best-practices
   Created: 2026-01-10

3. **Decision: Use anyhow for Application Errors** (decision)
   Status: accepted
   Created: 2026-01-08

4. **Custom Error Types** (note)
   Tags: rust, error-handling
   Created: 2026-01-05

5. **Error Propagation Patterns** (pattern)
   Tags: rust, error-handling
   Created: 2026-01-03

Would you like me to link your current work to any of these?

Document Implementation

You:

I just implemented the user authentication module following our security guidelines.
Add a note and link it to our security guideline.

Claude (uses kb/add_note + kb/link):

Created note: note-user-auth-implementation
Tags: security, authentication, implementation

Linked:
  note-user-auth-implementation --[implements]--> guideline-security-practices

Done! Your implementation is now documented and connected to our guidelines.

Review Project Knowledge

You:

Summarize our project knowledge: what decisions have we made, what patterns are we using?

Claude (uses kb/summarize_project prompt):

# Project Knowledge Summary

**Total Nodes**: 42

## Recent Decisions (5)
- Use SurrealDB for Storage (accepted, 2 days ago)
- Adopt MCP for AI Integration (accepted, 5 days ago)
- Nickel for Configuration (accepted, 1 week ago)

## Active Patterns (8)
- Error Handling with thiserror
- Repository Pattern for DB
- Builder Pattern for Complex Objects
- ...

## Top Tags
1. rust (23 nodes)
2. architecture (12 nodes)
3. error-handling (8 nodes)
4. patterns (7 nodes)

## Guideline Coverage
- Rust: 5 guidelines
- Testing: 3 guidelines
- Security: 2 guidelines

Advanced Usage

Conditional Queries

You:

Find all decisions made in the last week that are still proposed (not accepted)

Claude (combines kogral/search with filtering):

Found 2 proposed decisions from last week:

1. **Migrate to GraphQL** (proposed)
   Created: 3 days ago
   Context: REST API is too chatty...

2. **Add Redis Caching** (proposed)
   Created: 5 days ago
   Context: Database queries are slow...

Would you like to review these and update their status?

Cross-Reference Check

You:

Before I implement X, check if we have any decisions or guidelines about it

Claude (uses kogral/search + kogral/get_guidelines):

Checking existing knowledge about X...

**Decisions**: None found
**Guidelines**: 1 relevant guideline found
  - guideline-module-structure: "Keep modules focused and small"

**Related Notes**: 2 notes mention similar topics
  - note-module-design-patterns
  - note-separation-of-concerns

Recommendation: Your implementation of X looks aligned with our guidelines.
Consider linking it to guideline-module-structure when done.

Export for Sharing

You:

Export our architectural decisions to Logseq so I can review them visually

Claude (uses kb/export):

Exporting to Logseq format...

βœ“ Exported 12 decisions to /Users/akasha/logseq-export/
  - 12 decision records
  - Preserved wikilinks
  - Added Logseq properties

You can now open /Users/akasha/logseq-export/ in Logseq to visualize the decision graph.

Tips and Tricks

1. Use Natural Language

Don't worry about exact tool syntax. Claude understands:

❌ Don't say: "Use kb/search with query='rust' and type='pattern'" βœ… Do say: "Find Rust patterns in KOGRAL"

2. Be Specific with Tags

When adding notes, use consistent tags:

βœ… Good: tags: rust, error-handling, pattern ❌ Bad: tags: Rust, ErrorHandling, patterns

After creating notes, ask Claude to link them:

Link this note to our error handling guideline as 'implements'

4. Review Regularly

Ask Claude for summaries:

What have we documented this week?

For conceptual queries:

Find anything related to "making code maintainable"

Not just keyword "maintainable", but concepts like refactoring, clean code, patterns, etc.


Troubleshooting

"MCP server not responding"

# Check kb-mcp is built
ls target/release/kb-mcp

# Test manually
echo '{"jsonrpc":"2.0","id":1,"method":"kogral/search","params":{"query":"test"}}' \
  | target/release/kb-mcp serve

"KB directory not found"

# Verify .kb exists
ls -la /path/to/project/.kogral

# Initialize if missing
cd /path/to/project
kb init

"Permission denied"

# Make binary executable
chmod +x target/release/kb-mcp

# Check environment variable
echo $KOGRAL_DIR

"Empty search results"

# Add some test content
kb add note "Test Note" --content "Test content"

# Try search again in Claude Code

Next Steps


Quick Reference Card

TaskSay to Claude
Search"Find notes about X"
Add note"Add a note about X with tags Y, Z"
Add decision"Document decision to use X for Y"
Get guidelines"What are our X guidelines?"
Link nodes"Link A to B as implements"
Summarize"Summarize project knowledge"
Export"Export to Logseq format"

Remember: Claude Code with MCP turns KOGRAL into an active participant in your development workflow. Ask questions, capture decisions, and let AI help you maintain your project's knowledge graph.

Claude Code Integration

Logseq Integration

Obsidian Compatibility

Git Workflows

MCP Protocol

MCP Tools API Reference

Complete reference for the Model Context Protocol (MCP) server tools and resources.

Overview

The kogral-mcp server implements the MCP protocol (JSON-RPC 2.0) for Claude Code integration. It provides:

  • 10 Tools: Operations for querying and modifying knowledge base (7 core + 3 block tools)
  • 6 Resources: Access to knowledge graph content via URIs
  • 2 Prompts: Guided workflows for common tasks

Server Configuration

Start MCP Server

# Stdio transport (local use)
kb serve

# Or run directly
kb-mcp serve

Claude Code Configuration

Add to ~/.config/claude/config.json:

{
  "mcpServers": {
    "kogral-mcp": {
      "command": "/path/to/kb-mcp",
      "args": ["serve"],
      "env": {
        "KOGRAL_DIR": "/path/to/project/.kogral"
      }
    }
  }
}

Tools

kb/search

Search the knowledge base using text and/or semantic similarity.

Input Schema:

{
  "type": "object",
  "properties": {
    "query": {
      "type": "string",
      "description": "Search query"
    },
    "type": {
      "type": "string",
      "enum": ["note", "decision", "guideline", "pattern", "journal", "execution", "all"],
      "description": "Filter by node type",
      "default": "all"
    },
    "project": {
      "type": "string",
      "description": "Limit search to specific project graph"
    },
    "semantic": {
      "type": "boolean",
      "description": "Enable semantic similarity search",
      "default": true
    },
    "threshold": {
      "type": "number",
      "description": "Minimum similarity threshold (0-1)",
      "default": 0.4
    },
    "limit": {
      "type": "integer",
      "description": "Maximum number of results",
      "default": 10
    }
  },
  "required": ["query"]
}

Example Request:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "kogral/search",
  "params": {
    "query": "error handling patterns in Rust",
    "type": "pattern",
    "semantic": true,
    "threshold": 0.6,
    "limit": 5
  }
}

Example Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "type": "text",
    "text": "Found 3 result(s):\n\n1. Error Handling with thiserror (pattern, score: 0.85)\n   Tags: rust, error-handling\n   Created: 2026-01-15\n   \n2. Result Type Best Practices (guideline, score: 0.72)\n   Tags: rust, error-handling, best-practices\n   Created: 2026-01-10\n\n3. Custom Error Types (note, score: 0.65)\n   Tags: rust, error-handling\n   Created: 2026-01-08"
  }
}

kb/add_note

Add a new note to the knowledge base.

Input Schema:

{
  "type": "object",
  "properties": {
    "title": {
      "type": "string",
      "description": "Note title"
    },
    "content": {
      "type": "string",
      "description": "Note content (markdown)"
    },
    "tags": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Tags for categorization",
      "default": []
    },
    "relates_to": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Related node IDs",
      "default": []
    },
    "project": {
      "type": "string",
      "description": "Project graph name",
      "default": "default"
    }
  },
  "required": ["title", "content"]
}

Example Request:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "kogral/add_note",
  "params": {
    "title": "Async Trait Patterns",
    "content": "Common patterns for using async traits in Rust:\n\n1. Use `async-trait` crate\n2. Box return types for flexibility\n3. Consider Send + Sync bounds",
    "tags": ["rust", "async", "patterns"],
    "relates_to": ["pattern-error-handling"]
  }
}

Example Response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "type": "text",
    "text": "Note added successfully: note-async-trait-patterns (ID: note-abc123)"
  }
}

kb/add_decision

Create an Architectural Decision Record (ADR).

Input Schema:

{
  "type": "object",
  "properties": {
    "title": {
      "type": "string",
      "description": "Decision title"
    },
    "context": {
      "type": "string",
      "description": "Decision context and background"
    },
    "decision": {
      "type": "string",
      "description": "The decision made"
    },
    "consequences": {
      "type": "array",
      "items": { "type": "string" },
      "description": "List of consequences",
      "default": []
    },
    "status": {
      "type": "string",
      "enum": ["proposed", "accepted", "rejected", "deprecated", "superseded"],
      "default": "proposed"
    },
    "tags": {
      "type": "array",
      "items": { "type": "string" },
      "default": []
    }
  },
  "required": ["title", "context", "decision"]
}

Example Request:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "kogral/add_decision",
  "params": {
    "title": "Use SurrealDB for Knowledge Graph Storage",
    "context": "Need a scalable storage solution that supports graph queries and can handle growing knowledge base",
    "decision": "Adopt SurrealDB as the primary storage backend for production deployments",
    "consequences": [
      "Better query performance for graph traversal",
      "Native support for relationships",
      "Additional infrastructure dependency",
      "Team needs to learn SurrealDB query language"
    ],
    "status": "accepted",
    "tags": ["architecture", "storage", "surrealdb"]
  }
}

Example Response:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "type": "text",
    "text": "Decision added: decision-use-surrealdb (ID: decision-xyz789)\nStatus: accepted"
  }
}

Create a relationship between two nodes.

Input Schema:

{
  "type": "object",
  "properties": {
    "from": {
      "type": "string",
      "description": "Source node ID"
    },
    "to": {
      "type": "string",
      "description": "Target node ID"
    },
    "relation": {
      "type": "string",
      "enum": ["relates_to", "depends_on", "implements", "extends", "supersedes", "explains"],
      "description": "Relationship type"
    },
    "strength": {
      "type": "number",
      "description": "Relationship strength (0-1)",
      "minimum": 0,
      "maximum": 1,
      "default": 1.0
    }
  },
  "required": ["from", "to", "relation"]
}

Example Request:

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "kogral/link",
  "params": {
    "from": "note-async-trait-patterns",
    "to": "pattern-error-handling",
    "relation": "relates_to",
    "strength": 0.8
  }
}

Example Response:

{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "type": "text",
    "text": "Link created: note-async-trait-patterns --[relates_to]--> pattern-error-handling (strength: 0.8)"
  }
}

Relationship Types:

  • relates_to: General conceptual relationship
  • depends_on: Dependency (from depends on to)
  • implements: Implementation of concept/pattern
  • extends: Inherits or extends another node
  • supersedes: Replaces an older version
  • explains: Provides documentation/clarification

kb/get_guidelines

Retrieve guidelines for current project with inheritance resolution.

Input Schema:

{
  "type": "object",
  "properties": {
    "language": {
      "type": "string",
      "description": "Programming language (e.g., rust, nushell)"
    },
    "category": {
      "type": "string",
      "description": "Guideline category (e.g., error-handling, testing)"
    },
    "include_base": {
      "type": "boolean",
      "description": "Include shared/base guidelines",
      "default": true
    }
  }
}

Example Request:

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "kogral/get_guidelines",
  "params": {
    "language": "rust",
    "category": "error-handling",
    "include_base": true
  }
}

Example Response:

{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "type": "text",
    "text": "Guidelines for rust/error-handling:\n\n## Project Guidelines (priority: 150)\n\n1. Custom Error Types (guideline-custom-errors)\n   - Use thiserror for error definitions\n   - Implement From traits for conversions\n   - Source: .kogral/guidelines/rust-errors.md\n\n## Shared Guidelines (priority: 50)\n\n1. Result Type Best Practices (guideline-result-best-practices)\n   - Always use Result<T> for fallible operations\n   - Never use unwrap() in production\n   - Source: ~/Tools/.kogral-shared/guidelines/rust-errors.md\n\n2. Error Propagation (guideline-error-propagation)\n   - Use ? operator for error propagation\n   - Add context with .context()\n   - Source: ~/Tools/.kogral-shared/guidelines/rust-errors.md"
  }
}

kb/list_graphs

List available knowledge graphs.

Input Schema:

{
  "type": "object",
  "properties": {}
}

Example Request:

{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "kb/list_graphs"
}

Example Response:

{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "type": "text",
    "text": "Available graphs:\n\n- default (Current project)\n  Path: /path/to/project/.kogral\n  Nodes: 42\n  Last modified: 2026-01-17T10:30:00Z\n\n- shared (Shared guidelines)\n  Path: ~/Tools/.kogral-shared\n  Nodes: 156\n  Last modified: 2026-01-15T14:20:00Z"
  }
}

kb/export

Export knowledge base to various formats.

Input Schema:

{
  "type": "object",
  "properties": {
    "format": {
      "type": "string",
      "enum": ["logseq", "json", "markdown"],
      "description": "Export format"
    },
    "output_path": {
      "type": "string",
      "description": "Output file or directory path"
    },
    "include_types": {
      "type": "array",
      "items": {
        "type": "string",
        "enum": ["note", "decision", "guideline", "pattern", "journal", "execution"]
      },
      "description": "Node types to include",
      "default": ["note", "decision", "guideline", "pattern"]
    },
    "skip_journals": {
      "type": "boolean",
      "default": true
    }
  },
  "required": ["format", "output_path"]
}

Example Request:

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "kogral/export",
  "params": {
    "format": "logseq",
    "output_path": "/Users/akasha/logseq-graph",
    "include_types": ["note", "decision", "guideline"],
    "skip_journals": true
  }
}

Example Response:

{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "type": "text",
    "text": "Export completed:\n\nFormat: Logseq\nOutput: /Users/akasha/logseq-graph\nExported: 42 nodes\n  - 23 notes\n  - 12 decisions\n  - 7 guidelines\n\nLogseq graph ready to open."
  }
}

Block Tools

Tools for querying Logseq content blocks. Requires blocks.enable_mcp_tools = true in configuration.

kb/find_blocks

Find blocks by tag, task status, or custom property across the knowledge base.

Input Schema:

{
  "type": "object",
  "properties": {
    "tag": {
      "type": "string",
      "description": "Find blocks with this tag (e.g., 'card', 'important')"
    },
    "status": {
      "type": "string",
      "enum": ["TODO", "DOING", "DONE", "LATER", "NOW", "WAITING", "CANCELLED"],
      "description": "Find blocks with this task status"
    },
    "property_key": {
      "type": "string",
      "description": "Custom property key to search for"
    },
    "property_value": {
      "type": "string",
      "description": "Custom property value to match"
    },
    "limit": {
      "type": "integer",
      "description": "Maximum number of results",
      "default": 20
    }
  }
}

Note: Provide one of: tag, status, or both property_key and property_value.

Example Request (find blocks by tag):

{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "kogral/find_blocks",
  "params": {
    "tag": "high-priority",
    "limit": 10
  }
}

Example Response:

{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {
    "type": "text",
    "text": "Found 3 blocks with tag '#high-priority':\n\n**Project Tasks** (project-tasks-123)\n  - TODO Implement authentication #high-priority\n  - TODO Fix security vulnerability #high-priority\n\n**Sprint Planning** (sprint-planning-456)\n  - DOING Refactor database layer #high-priority"
  }
}

Example Request (find blocks by property):

{
  "jsonrpc": "2.0",
  "id": 9,
  "method": "kogral/find_blocks",
  "params": {
    "property_key": "priority",
    "property_value": "high",
    "limit": 15
  }
}

kb/find_todos

Find all TODO blocks across the knowledge base.

Input Schema:

{
  "type": "object",
  "properties": {
    "limit": {
      "type": "integer",
      "description": "Maximum number of results",
      "default": 20
    }
  }
}

Example Request:

{
  "jsonrpc": "2.0",
  "id": 10,
  "method": "kogral/find_todos",
  "params": {
    "limit": 25
  }
}

Example Response:

{
  "jsonrpc": "2.0",
  "id": 10,
  "result": {
    "type": "text",
    "text": "Found 8 TODO blocks:\n\n**Project Tasks** (project-tasks-123)\n  - TODO Implement authentication\n  - TODO Write integration tests\n  - TODO Update documentation\n\n**Bug Fixes** (bug-fixes-456)\n  - TODO Fix race condition in cache\n  - TODO Address memory leak\n\n**Research** (research-789)\n  - TODO Evaluate GraphQL alternatives\n  - TODO Benchmark new approach\n  - TODO Document findings"
  }
}

kb/find_cards

Find all flashcard blocks (blocks tagged with #card) for spaced repetition learning.

Input Schema:

{
  "type": "object",
  "properties": {
    "limit": {
      "type": "integer",
      "description": "Maximum number of flashcards",
      "default": 10
    }
  }
}

Example Request:

{
  "jsonrpc": "2.0",
  "id": 11,
  "method": "kogral/find_cards",
  "params": {
    "limit": 5
  }
}

Example Response:

{
  "jsonrpc": "2.0",
  "id": 11,
  "result": {
    "type": "text",
    "text": "Found 3 flashcards:\n\n**Rust Learning** (rust-learning-123)\n  - What is Rust's ownership model? #card #rust\n    - Ownership prevents data races at compile time\n    - Each value has a single owner\n\n**System Design** (system-design-456)\n  - What is the CAP theorem? #card #distributed-systems\n    - Consistency, Availability, Partition tolerance\n    - Can only guarantee 2 of 3\n\n**Algorithms** (algorithms-789)\n  - What is the time complexity of quicksort? #card #algorithms\n    - Average: O(n log n)\n    - Worst case: O(nΒ²)"
  }
}

Use Cases:

  • kb/find_blocks: General block search by metadata
  • kb/find_todos: Task management and tracking
  • kb/find_cards: Spaced repetition learning system

See Also:

Resources

Resources provide read-only access to knowledge graph content via URIs.

kogral://project/notes

Access project notes.

URI: kogral://project/notes or kogral://project/notes/{note-id}

Example: Read all project notes

kogral://project/notes

Example: Read specific note

kogral://project/notes/async-trait-patterns

kogral://project/decisions

Access project decisions (ADRs).

URI: kogral://project/decisions or kogral://project/decisions/{decision-id}

kogral://project/guidelines

Access project-specific guidelines.

URI: kogral://project/guidelines or kogral://project/guidelines/{guideline-id}

kogral://project/patterns

Access project patterns.

URI: kogral://project/patterns or kogral://project/patterns/{pattern-id}

kogral://shared/guidelines

Access shared guidelines (inherited).

URI: kogral://shared/guidelines or kogral://shared/guidelines/{guideline-id}

kogral://shared/patterns

Access shared patterns (inherited).

URI: kogral://shared/patterns or kogral://shared/patterns/{pattern-id}

Prompts

Prompts provide guided workflows for common tasks.

kb/summarize_project

Generate a comprehensive project knowledge summary.

Arguments:

{
  "project": {
    "type": "string",
    "description": "Project graph name",
    "default": "default"
  }
}

Example Request:

{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "kogral/summarize_project",
  "params": {
    "project": "default"
  }
}

Returns: Prompt messages with project summary including:

  • Total node counts by type
  • Recent additions
  • Top tags
  • Key decisions
  • Active patterns

Find nodes related to a specific topic or node.

Arguments:

{
  "node_id": {
    "type": "string",
    "description": "Node ID to find relations for"
  },
  "depth": {
    "type": "integer",
    "description": "Maximum traversal depth",
    "default": 2
  }
}

Example Request:

{
  "jsonrpc": "2.0",
  "id": 9,
  "method": "kb/find_related",
  "params": {
    "node_id": "pattern-error-handling",
    "depth": 2
  }
}

Returns: Prompt messages with:

  • Direct relationships
  • Indirect relationships (depth 2+)
  • Common tags
  • Related guidelines

Error Handling

Error Codes

Standard JSON-RPC 2.0 error codes:

CodeMeaningDescription
-32700Parse errorInvalid JSON
-32600Invalid RequestMissing required fields
-32601Method not foundUnknown tool/resource
-32602Invalid paramsParameter validation failed
-32603Internal errorServer-side error

Example Error Response

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "Invalid params",
    "data": {
      "details": "Field 'query' is required but missing",
      "field": "query"
    }
  }
}

Best Practices

1. Use Semantic Search for Discovery

{
  "method": "kogral/search",
  "params": {
    "query": "how to handle database connection errors",
    "semantic": true,
    "threshold": 0.5
  }
}
{
  "method": "kogral/link",
  "params": {
    "from": "note-new-discovery",
    "to": "pattern-related-pattern",
    "relation": "implements"
  }
}

3. Query Guidelines Before Implementation

{
  "method": "kogral/get_guidelines",
  "params": {
    "language": "rust",
    "category": "testing"
  }
}

4. Document Decisions with ADRs

{
  "method": "kogral/add_decision",
  "params": {
    "title": "Use X for Y",
    "context": "Background...",
    "decision": "We will...",
    "consequences": ["Pro 1", "Con 1"]
  }
}

See Also

Resources

Rust API

Development Setup

Code Standards

Testing