Architecture map
Architecture map
A 1500-foot view of how Lettuce is put together.
Modules
┌───────────────────────────────────────────────┐
│ Lettuce (app + CLI) │
│ CLI parsing • UI views • run loops │
├───────────────────────────────────────────────┤
│ LettuceServer │
│ Hummingbird 2 HTTP • MCP JSON-RPC • REST • │
│ Auth • VaultService • EditCoordinator │
├───────────────────────────────────────────────┤
│ LettuceKit │
│ Renderer • LinkIndex • TagIndex • │
│ DocsVault • PublishConfig • │
│ GitDeployer • SiteBuilder │
├───────────────────────────────────────────────┤
│ swift-markdown │ Yams │ Hummingbird │ sqlite3 │
└───────────────────────────────────────────────┘
Dependencies are strictly downward — Lettuce depends on LettuceServer and LettuceKit; LettuceServer depends on LettuceKit; LettuceKit depends only on external packages.
LettuceKit (the engine)
Markdown/MarkdownRenderer.swift — Markdown → HTML via swift-markdown, with custom visitors for:
Wikilink expansion (resolves against the link index)
Tag inlining (
#tag→ linked span)Callout transformation (
> [!note]→ styled blockquote)Mermaid / KaTeX preservation (passed through; rendered client-side)
Block ID detection (
^id→ anchor)
Vault/LinkIndex.swift — [from-path → [to-path, link-text, link-type]]. Built in parallel from a vault scan. ~100ms cold for 6000 notes.
Vault/TagIndex.swift — [tag → [paths]] with nested-tag traversal. Built alongside link index.
Vault/VaultWatcher.swift — FSEvents-based incremental updates that drive re-indexing.
DocsVault.swift — see Building from source for the SwiftPM resource model. Loads Docs/ from the bundle, materialises to Application Support, version-stamps.
Publish/PublishConfig.swift, Publish/SiteBuilder.swift — static site generation.
Publish/GitDeployer.swift — lettuce deploy plumbing.
Markdown/HTMLSanitizer.swift — defends against user-content XSS. Whitelisted tags only; live disallowed tags are escaped.
LettuceServer (the agent surface)
LettuceHTTPServer.swift / Router.swift — Hummingbird 2 app. Routes for:
/mcp— POST: JSON-RPC dispatch (MCP over Streamable HTTP)/api/v1/*— REST mirror/health— liveness
There is no SSE / streaming endpoint — every MCP call is a single request/response over POST.
MCP.swift — tool registry. Each tool is dispatched through a Swift switch on the method name (no runtime registry, no reflection).
Auth.swift — derives caller identity from the request:
Token lookup in Keychain (compared in constant time)
Scope evaluation: an
APIKeycarries a set of scopes (read/write; the CLIallscope isread + write), an optionalfolderAllowlist, and aproposeOnlyflagA bad or missing
Authorization: Bearer <token>header yields a flat HTTP 401
Proposals.swift (in LettuceKit) — owns .lettuce/proposals/, one JSON file per proposal. A proposeOnly agent’s write tools land here instead of mutating the vault; the user accepts (re-running the write through the normal commit path, after a staleness check) or rejects (deleting the file) in the GUI. Operations: list, load, save, delete, staleness.
EditCoordinator.swift (actor) — tracks advisory soft-locks (files the user is actively editing get a 409 on agent writes) and records recent agent edits.
VaultService.swift (actor) — single owner of vault state. Every file read/write/move goes through here. Provides atomic file ops, full re-index, and path-safety (rejects .. and absolute paths outside the vault).
SchedulePoller.swift — on lettuce serve, fires due schedules every 60s (the headless equivalent of the app’s timer).
Lettuce (the app)
The SwiftUI/AppKit app. Layers:
Views — SwiftUI for most surfaces; NSViewRepresentable bridges for CodeMirror.
Models —
AppModel(anObservableObject) is the central app state; feature views observe it.Controllers — bridge models to VaultService / LettuceServer.
CLI subcommands live in Sources/Lettuce/Main.swift — a simple switch on the first argument dispatches to runServe, runPublish, runDocs, runRender, etc. Each subcommand reads from LettuceKit and / or starts LettuceServer.
Concurrency model
VaultServiceis an actor. All file ops serialise through it.LinkIndex/TagIndexare reference types (final class) that the actor rebuilds and swaps in on each commit, so callers only ever see a fully-built index.EditCoordinatoris an actor. Soft-locks and the recent-edits log are in-process.Hummingbird uses Swift NIO event loops. Tool handlers are
async; they suspend onawait VaultService.X(...).SchedulePollerruns a single async loop that fires due schedules every 60s.
There are no DispatchQueues. There is no manual lock primitive (besides advisory soft-locks tracked in EditCoordinator).
Storage
| Data | Where | Format |
|---|---|---|
| Notes | <vault>/*.md |
Markdown + YAML frontmatter |
| Sidebar order | <vault>/.lettuce/order.json |
JSON |
| Triggers | <vault>/.lettuce/triggers.yml |
YAML |
| Schedules | <vault>/.lettuce/schedules.yml |
YAML |
| Lint state | <vault>/.lettuce/lint-state.json |
JSON |
| Embeddings | <vault>/.lettuce/vectors.json, <vault>/.lettuce/passages.json |
JSON |
| Proposals | <vault>/.lettuce/proposals/*.json |
JSON |
| Agent-edit timeline | <vault>/.lettuce/agent-edits.json |
JSON |
| Agent tokens | macOS Keychain | system |
| Provider keys | macOS Keychain | system |
| App preferences | UserDefaults |
system |
| Docs vault | ~/Library/Application Support/Lettuce/Documentation/ |
Markdown |
There’s no on-disk SQLite database for vault metadata. sqlite3 is used by SearchIndex.swift for an in-memory FTS5 full-text index that’s rebuilt on vault open and live-reload. All persisted state is plain files.