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:
Option 1: Serve Locally with mdBook (Recommended)
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:
- architecture-overview.svg - Complete system architecture
- core-concepts.svg - Node types and relationships
- config-composition.svg - Configuration flow (Nickel β JSON β Rust)
- storage-architecture.svg - Hybrid storage strategy
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:
- Click the search icon (π) in the top-left corner
- Type your query
- 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
- Create the markdown file in the appropriate directory
- Add it to
SUMMARY.mdfor navigation - Build to verify:
just docs::build
Adding a New Section
- Create the directory
- Add a
README.mdfor the section landing page - 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.
Check Links
just docs::check-links
This validates all internal and external links.
π Documentation Standards
When contributing to documentation:
- Use clear, concise language - Write for developers and AI agents
- Include code examples - Show, don't just tell
- Add diagrams where helpful - Visual aids improve understanding
- Link related concepts - Help readers discover related content
- Test code examples - Ensure code compiles and works
- Use consistent formatting - Follow existing page structure
- Update SUMMARY.md - New pages must be in navigation
- 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
Links broken after moving files
# 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:
- Fork the repository
- Create a feature branch
- Make your changes
- Test with
just docs::buildandjust docs::test - 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:
- Captures knowledge in structured, git-friendly markdown
- Connects concepts through typed relationships
- Queries via text and semantic similarity
- Integrates with AI tools (Claude Code via MCP)
- 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:
| Type | Purpose | Example |
|---|---|---|
| Note | General observations | "Rust ownership patterns" |
| Decision | ADRs (Architectural Decision Records) | "Use SurrealDB for storage" |
| Guideline | Code standards | "Error handling with thiserror" |
| Pattern | Reusable solutions | "Repository pattern for DB access" |
| Journal | Daily reflections | "2026-01-17 progress notes" |
| Execution | Agent task records | "Implemented auth module" |
Relationships
6 typed edges connecting nodes:
| Relation | Meaning | Example |
|---|---|---|
relates_to | Conceptual link | Note β Note |
depends_on | Prerequisite | Pattern β Guideline |
implements | Concrete realization | Code β Pattern |
extends | Inheritance | ProjectGuideline β BaseGuideline |
supersedes | Replacement | DecisionV2 β DecisionV1 |
explains | Documentation | Note β 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
| Feature | KOGRAL | Logseq |
|---|---|---|
| Storage | Git-friendly markdown + DB | Local markdown |
| AI Integration | Native MCP protocol | Plugin-based |
| Config | Type-safe Nickel schemas | JSON files |
| Multi-Backend | Filesystem + SurrealDB | Filesystem only |
| Semantic Search | Multiple AI providers | Local only |
| Graph Queries | SurrealDB graph queries | Block references |
Compatibility: KOGRAL can import/export Logseq graphs for visual editing.
vs. Obsidian
| Feature | KOGRAL | Obsidian |
|---|---|---|
| Target Audience | Developers + AI agents | Knowledge workers |
| Focus | Structured knowledge graph | Flexible note-taking |
| Configuration | Config-driven (Nickel) | Settings UI |
| CLI | Full CLI + MCP server | Limited CLI |
| Version Control | Git-native | Git plugin |
Use Case: KOGRAL for developer knowledge, Obsidian for personal notes.
vs. Notion/Confluence
| Feature | KOGRAL | Notion/Confluence |
|---|---|---|
| Storage | Local-first | Cloud-only |
| Format | Plain markdown | Proprietary |
| AI Access | Programmatic API | Web scraping |
| Offline | Full functionality | Limited |
| Privacy | Self-hosted | Third-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:
- kb-core: Core library (models, storage, query)
- kb-cli: Command-line interface
- kb-mcp: MCP server for AI integration
- 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
- New to KOGRAL? β Installation Guide
- Ready to start? β Quick Start
- Want examples? β Use Cases
- Understand design? β Architecture Overview
"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
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
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.
Semantic Search
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:
- Text β Embedding (384 or 1536 dimensional vector)
- Similarity search (cosine distance)
- 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
blocktable with indexes - Indexes: tags, status, parent_id, full-text search
See Also:
Key Principles
- Capture During Work: Don't wait, document as you go
- Link as You Learn: Connect related concepts immediately
- Query When Needed: AI-assisted discovery of relevant knowledge
- Evolve Over Time: Update decisions, supersede patterns
- Share Wisely: Shared guidelines, local specifics
Next Steps
- Understand motivation: Why KOGRAL?
- Learn philosophy: Design Philosophy
- See architecture: System Overview
- Start using: Quick Start
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:
- Read outdated wiki (2 hours)
- Ask teammates for context (1 hour per question Γ 10 questions)
- Discover guidelines by reading code (days)
- Miss important decisions (leads to mistakes)
Time to productivity: 2-4 weeks
With KB
New developer joins:
kb search "architectural decisions"β finds all ADRs- Ask Claude: "What are our coding guidelines?" β gets current guidelines with inheritance
- Browse related notes via graph traversal
- Full context available, no tribal knowledge
Time to productivity: 3-5 days
Before KB
Team makes decision:
- Discussion in Slack (lost after a week)
- Someone documents in wiki (maybe)
- 6 months later: "Why did we choose X?" β nobody remembers
- Re-debate the same decision
With KB
Team makes decision:
- Discussion captured as ADR during meeting
- Context, decision, consequences documented
- Linked to related patterns and guidelines
- 6 months later:
kb show decision-use-xβ full context instantly
Before KB
Solving a bug:
- Encounter race condition in cache
- Debug for 2 hours
- Fix it
- Solution lost in PR comments
Two months later: Different developer, same bug, 2 more hours
With KB
Solving a bug:
- Encounter race condition
- Ask Claude: "Have we seen cache race conditions before?"
- Claude finds journal entry from 2 months ago with solution
- 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?
- Understand concepts: Core Concepts
- Learn philosophy: Design Philosophy
- Quick start: Quick Start Guide
- See examples: Use Cases
"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:
| Feature | Local | Cloud |
|---|---|---|
| Storage | Filesystem | SurrealDB |
| Embeddings | fastembed | OpenAI/Claude |
| Search | Text-based | Semantic |
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-dirto 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:
- Backward Compatibility: Don't break existing configs
- Deprecation Period: Warn before removal (1 major version)
- Migration Tools: Provide automated migrations
- 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
- See it in action: Use Cases
- Understand architecture: System Overview
- Start using: Quick Start
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
Method 1: Install from Source (Recommended)
# 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:
| Feature | Description | Default |
|---|---|---|
filesystem | Filesystem storage backend | β Yes |
surrealdb | SurrealDB storage backend | β No |
fastembed | Local embedding generation | β No |
full | All 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
- Quick Start Guide - Create your first knowledge base
- Configuration Reference - Customize behavior
- MCP Tools - Integrate with Claude Code
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"
Step 4: Link Nodes Together
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 linkdepends_on- Dependency relationshipimplements- Implementation of a conceptextends- Inheritance/extensionsupersedes- Replaces older versionexplains- 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
Use Wikilinks
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
- Configuration Reference - Full configuration options
- CLI Commands - All available commands
- Document Format - Markdown and frontmatter details
- MCP Tools - Claude Code integration
Getting Help
- Check
kb --helpfor 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
- Start simple: Quick Start Guide
- Integrate AI: MCP Quick Guide
- Advanced features: Configuration Reference
Best Practices
System Architecture
Comprehensive overview of the KOGRAL architecture.
High-Level Architecture
The KOGRAL consists of three main layers:
- User Interfaces: kb-cli (terminal), kb-mcp (AI integration), NuShell scripts (automation)
- Core Library (kb-core): Rust library with graph engine, storage abstraction, embeddings, query engine
- 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/directoryadd: Create nodes (note, decision, guideline, pattern, journal)search: Text and semantic searchlink: Create relationships between nodeslist: List all nodesshow: Display node detailsdelete: Remove nodesgraph: Visualize knowledge graphsync: Sync filesystem β SurrealDBserve: Start MCP serverimport: Import from Logseqexport: Export to Logseq/JSONconfig: 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:
-
Tools (7):
kogral/search: Query knowledge basekb/add_note: Create noteskb/add_decision: Create ADRskb/link: Create relationshipskb/get_guidelines: Retrieve guidelines with inheritancekb/list_graphs: List available graphskb/export: Export to formats
-
Resources (6 URIs):
kogral://project/noteskogral://project/decisionskogral://project/guidelineskogral://project/patternskogral://shared/guidelineskogral://shared/patterns
-
Prompts (2):
kb/summarize_project: Generate project summarykb/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 synckb-backup.nu: Archive knowledge basekb-reindex.nu: Rebuild embeddingskb-import-logseq.nu: Import from Logseqkb-export-logseq.nu: Export to Logseqkb-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:
FilesystemStorage: Git-friendly markdown filesMemoryStorage: In-memory with DashMapSurrealDbStorage: 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:
FastEmbedProvider: Local fastembedRigEmbeddingProvider: 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:
- Nickel contracts: Type-safe, enum validation
- 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
Text Search
#![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:
-
Document Templates (6):
note.md.teradecision.md.teraguideline.md.terapattern.md.terajournal.md.teraexecution.md.tera
-
Export Templates (4):
logseq-page.md.teralogseq-journal.md.terasummary.md.teragraph.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 Details: Graph Model
- Storage Deep Dive: Storage Architecture
- ADRs: Architectural Decisions
- Implementation: Development Guide
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:
- Schema contracts (type definitions)
- Defaults (base values)
- Mode overlays (dev/prod/test optimizations)
- 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
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:
- Update contract in
contracts.ncl - Add default in
defaults.ncl - Export validates existing configs
- 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:
- Check
.kb-config/targets/kb-core.json(pre-exported) - Check
.kb-config/core/kb.ncl(export on-demand) - Check environment variable
KB_CONFIG - 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
- Schema Reference: Configuration Schema
- User Guide: Configuration Guide
- ADR: Why Nickel vs TOML
- Examples:
.kb-config/core/kb.ncl,.kb-config/platform/*.ncl
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
#cardproperty - 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
Option C: Hybrid Approach (RECOMMENDED)
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:
-
Filesystem - Store as markdown (Logseq compatible):
- Block 1 #card - Nested block - Block 2 TODO -
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
Recommended 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
blocksfield work as before contentremains source of truthblocksis optional cache/structure- Config flag
blocks.enabledto 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
Blockstruct with properties, hierarchy - Extend
Nodewith optionalblocks: Option<Vec<Block>> - Implement bidirectional parser (markdown β blocks)
- Preserve
contentas 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:
- Review and approve design
- Implement Phase 1 (Block models)
- Implement Phase 2 (BlockParser)
- Update Logseq import/export
- 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:
- Complex nested structures (graph settings, storage configs, embedding providers)
- Type validation (prevent runtime errors from config mistakes)
- Composition and inheritance (shared configs, environment-specific overrides)
- Documentation (self-documenting schemas)
- 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
serdesupport - 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"requiresapi_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
nickelCLI 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 exporttime - 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:
- Define schemas in
schemas/*.nclwith type contracts - Users write configs in
.kogral/config.ncl - Export to JSON via CLI:
nickel export --format json config.ncl - Load JSON in Rust via
serde_jsoninto 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
nickelavailability inkb initcommand
β 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"}vskey = 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
- β
Define base schemas (
schemas/kb-config.ncl) - β
Implement Nickel loader (
kb-core/src/config/nickel.rs) - β
Create example configs (
config/defaults.ncl,config/production.ncl) - β
Document Nickel usage (
docs/config/nickel-schemas.md) - β³ Add LSP recommendations to setup guide
- β³ 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
- Nickel Language
- Nickel User Manual
- platform-config pattern (reference implementation)
- TOML Specification
Revision History
| Date | Author | Change |
|---|---|---|
| 2026-01-17 | Architecture Team | Initial 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:
- Local-First Option: Must work offline without external API dependencies
- Production Scalability: Support cloud AI providers for large-scale deployments
- Multiple Providers: Flexibility to choose based on cost, quality, privacy
- Cost-Effective Development: Free local embeddings for development and testing
- 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-corelibrary - β 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:
- Default:
fastembedwithBAAI/bge-small-en-v1.5(384 dimensions) - Optional: OpenAI, Claude, Ollama via
rig-core(configurable) - Interface:
EmbeddingProvidertrait abstracts provider details - 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
fastembedwithout 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:
EmbeddingProvidertrait 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:
fastembedadds ~5MB to binaryrig-coreadds ~2MB- Total: ~7MB overhead
Not a concern for CLI/MCP server use case.
Provider Comparison
| Provider | Dimensions | Quality | Cost | Privacy | Offline |
|---|---|---|---|---|---|
| fastembed | 384 | Good | Free | β Local | β Yes |
| OpenAI | 1536 | Excellent | $0.0001/1K | β Cloud | β No |
| Claude | 1024 | Excellent | $0.00025/1K | β Cloud | β No |
| Ollama | 768 | Very Good | Free | β 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
- β
Define
EmbeddingProvidertrait - β Implement FastEmbedProvider (stub, feature-gated)
- β Implement RigEmbeddingProvider (stub, feature-gated)
- β³ Complete FastEmbed integration with model download
- β³ Complete rig-core integration (OpenAI, Claude, Ollama)
- β³ Add query engine with similarity search
- β³ 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
- fastembed Documentation
- rig-core Documentation
- OpenAI Embeddings API
- BAAI/bge Models
- Ollama Embeddings
Revision History
| Date | Author | Change |
|---|---|---|
| 2026-01-17 | Architecture Team | Initial 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:
- Git-Friendly: Knowledge should version alongside code
- Scalable: Support small projects (10s of nodes) to large organizations (10,000+ nodes)
- Queryable: Efficient graph queries and relationship traversal
- Offline-Capable: Work without network access
- Collaborative: Support shared organizational knowledge
- 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:
- Parse changed markdown file
- Convert to Node struct
- Upsert to SurrealDB
- Update relationships
Debouncing: 500ms (configurable)
SurrealDB β Filesystem
Trigger: Query for shared knowledge
Process:
- Query SurrealDB for shared nodes
- Cache locally (in-memory or filesystem)
- Merge with local results
- 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
- SurrealDB Documentation
- Storage Trait Implementation
- FilesystemStorage
- SurrealDbStorage
- Sync Mechanism
Revision History
| Date | Author | Change |
|---|---|---|
| 2026-01-17 | Architecture Team | Initial 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
Blockstruct tomodels/block.rs - Add optional
blocksfield toNode - 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
LogseqImporterto parse blocks - Update
LogseqExporterto 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_blocksMCP tool - Add
kb find-cardsCLI command - Add
kb find-todosCLI command
Phase 6: SurrealDB Backend
- Create
blocktable 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:
contentremains source of truthblocksis 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-blocksto 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"
}
}
Link Concepts
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
Find Related Knowledge
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
3. Link as You Go
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?
5. Use Semantic Search
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
- Read: MCP Tools API Reference for all available tools
- Explore: Use Cases for more examples
- Configure: Configuration Reference to customize behavior
- Integrate: Claude Code Integration for advanced setup
Quick Reference Card
| Task | Say 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"
}
}
kb/link
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 relationshipdepends_on: Dependency (from depends on to)implements: Implementation of concept/patternextends: Inherits or extends another nodesupersedes: Replaces an older versionexplains: 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
kb/find_related
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:
| Code | Meaning | Description |
|---|---|---|
| -32700 | Parse error | Invalid JSON |
| -32600 | Invalid Request | Missing required fields |
| -32601 | Method not found | Unknown tool/resource |
| -32602 | Invalid params | Parameter validation failed |
| -32603 | Internal error | Server-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
}
}
2. Link Related Concepts
{
"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"]
}
}