Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: CI

on:
pull_request:
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Install Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: rustfmt, clippy

# The GUI crate links tauri, which needs webkit at compile time.
- name: Install Linux deps
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf

- name: Cache cargo
uses: swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: src-tauri

- name: Install JS deps
run: bun install

- name: Typecheck frontend
run: bunx tsc --noEmit

- name: Frontend tests
run: bun run test

- name: Frontend build
run: bun run build

- name: Rust format
working-directory: src-tauri
run: cargo fmt --check

- name: Rust lints
working-directory: src-tauri
run: cargo clippy --workspace --all-targets -- -D warnings

- name: Rust tests (unit + MCP stdio integration)
working-directory: src-tauri
run: cargo test --workspace

- name: MCP binary stays tauri-free
working-directory: src-tauri
run: |
COUNT=$(cargo tree -p docsreader-mcp | grep -c " tauri v" || true)
test "$COUNT" -eq 0 || { echo "::error::tauri leaked into docsreader-mcp"; exit 1; }
11 changes: 9 additions & 2 deletions .github/workflows/cut-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,13 @@ jobs:
# Cargo.toml: only the [package] version, not deps
sed -i '0,/^version = "[^"]*"/s//version = "'"$VERSION"'"/' src-tauri/Cargo.toml

# Cargo.lock: docsreader package entry
# docsreader-mcp reports its version in the MCP handshake; keep it
# in lockstep with the app.
sed -i '0,/^version = "[^"]*"/s//version = "'"$VERSION"'"/' src-tauri/mcp/Cargo.toml

# Cargo.lock: docsreader + docsreader-mcp package entries
sed -i '/^name = "docsreader"$/{n;s/^version = "[^"]*"/version = "'"$VERSION"'"/}' src-tauri/Cargo.lock
sed -i '/^name = "docsreader-mcp"$/{n;s/^version = "[^"]*"/version = "'"$VERSION"'"/}' src-tauri/Cargo.lock

# README.md: download URLs embed the version in asset filenames.
# Scoped to /releases/latest/download/ paths so we don't accidentally
Expand All @@ -57,7 +62,9 @@ jobs:
echo "After:"
grep '"version"' src-tauri/tauri.conf.json | head -1
grep '^version' src-tauri/Cargo.toml | head -1
grep '^version' src-tauri/mcp/Cargo.toml | head -1
grep -A1 '^name = "docsreader"$' src-tauri/Cargo.lock | head -2
grep -A1 '^name = "docsreader-mcp"$' src-tauri/Cargo.lock | head -2
grep -E 'DocsReader_[0-9]' README.md | head -2 || true

- name: Commit, tag, push
Expand All @@ -67,7 +74,7 @@ jobs:
set -e
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add src-tauri/tauri.conf.json src-tauri/Cargo.toml src-tauri/Cargo.lock README.md
git add src-tauri/tauri.conf.json src-tauri/Cargo.toml src-tauri/mcp/Cargo.toml src-tauri/Cargo.lock README.md
git commit -m "chore(release): v$VERSION"
git tag "v$VERSION"
git push origin HEAD:main
Expand Down
32 changes: 29 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ jobs:
steps:
- id: pick
run: |
MAC='{"platform":"macos-latest","args":"--target universal-apple-darwin --skip-stapling"}'
LIN='{"platform":"ubuntu-22.04","args":""}'
WIN='{"platform":"windows-latest","args":""}'
# The sidecar overlay adds bundle.externalBin (docsreader-mcp) and a
# beforeBuildCommand that builds it; the base config stays sidecar-free
# so plain `cargo build`/`tauri dev` never require the staged binary.
SIDECAR='--config src-tauri/tauri.sidecar.conf.json'
MAC="{\"platform\":\"macos-latest\",\"args\":\"--target universal-apple-darwin --skip-stapling $SIDECAR\"}"
LIN="{\"platform\":\"ubuntu-22.04\",\"args\":\"$SIDECAR\"}"
WIN="{\"platform\":\"windows-latest\",\"args\":\"$SIDECAR\"}"
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ inputs.tag }}"
case "${{ inputs.platforms }}" in
Expand Down Expand Up @@ -136,6 +140,28 @@ jobs:
prerelease: false
args: ${{ matrix.args }}

# The bundler copies externalBin into the app bundle and signs it with
# the app (tauri-bundler sign_paths), so DMG notarization covers the
# sidecar too. These asserts fail the release if the sidecar is absent.
- name: Verify sidecar in bundle (macOS)
if: matrix.platform == 'macos-latest'
run: |
set -euo pipefail
APP=$(find src-tauri/target/universal-apple-darwin/release/bundle/macos -name '*.app' -maxdepth 1 | head -1)
SIDECAR="$APP/Contents/MacOS/docsreader-mcp"
test -f "$SIDECAR" || { echo "::error::sidecar missing from $APP"; exit 1; }
lipo -archs "$SIDECAR" | grep -q "x86_64 arm64" || { echo "::error::sidecar is not universal"; exit 1; }
codesign --verify --strict "$SIDECAR"
echo "sidecar present, universal, and signed"

- name: Verify sidecar in bundle (Linux)
if: matrix.platform == 'ubuntu-22.04'
run: |
set -euo pipefail
DEB=$(find src-tauri/target/release/bundle/deb -name '*.deb' | head -1)
dpkg-deb -c "$DEB" | grep -q 'usr/bin/docsreader-mcp' || { echo "::error::sidecar missing from $DEB"; exit 1; }
echo "sidecar present in deb"

# macOS-only: submit the DMG to Apple async (no --wait), then stash
# the submission id in the release body alongside a user-facing
# warning. notarize-staple.yml polls Apple, staples the DMG when
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/update-homebrew-tap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ jobs:
homepage "https://github.com/anbturki/docsreader"

app "DocsReader.app"
binary "#{appdir}/DocsReader.app/Contents/MacOS/docsreader-mcp"

zap trash: [
"~/Library/Application Support/com.docsreader.app",
Expand Down
12 changes: 12 additions & 0 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"config": {
// Prose lines are not wrapped in this repo.
"MD013": false,
// The README uses inline HTML for the logo and download badges.
"MD033": false,
"MD041": false,
// Bold group labels inside Features are a deliberate style choice.
"MD036": false
},
"globs": ["README.md", "docs/*.md", "ROADMAP.md", "SECURITY.md"]
}
94 changes: 79 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# DocsReader

Point it at any folder. DocsReader scans the directory for Markdown files, organizes them into a navigable tree, and renders each one in a clean reader. macOS, Linux, Windows.
The human window into an agent-managed markdown corpus. Your AI agents write docs, memory, and tasks through the bundled MCP server; you read the same plain markdown files in a clean native app. And it still works as a fast reader for any folder of markdown - macOS, Linux, Windows.

[![Latest Release](https://img.shields.io/github/v/release/anbturki/docsreader?label=latest&color=7c3aed)](https://github.com/anbturki/docsreader/releases/latest)
[![Downloads](https://img.shields.io/github/downloads/anbturki/docsreader/total?color=7c3aed)](https://github.com/anbturki/docsreader/releases)
Expand All @@ -19,44 +19,102 @@ Or install via [Homebrew or one-liner](#install).

![DocsReader](docs/screenshots/main.png)

## How it works

Everything is plain markdown on disk - no database, no lock-in, greppable, and versionable with git. Agents are the primary writers; DocsReader is where you watch the corpus grow, and open docs reload live as agents write.

A **workspace** is a folder of markdown. Your default one lives at `~/notes` (created on first write); any project can opt in to its own `<project>/notes`. Three namespaces live inside:

- **Docs** hold the substance: research, plans, decisions. Each doc sits in the folder matching its lifecycle status - `research/`, `in-progress/`, `done/`, `archived/` - so the folder IS the status, and moving the file IS the status change. Optional phase subfolders (`research/v2-launch/`) group work inside a status.
- **Memory** (`memory/`) holds short topic-addressed facts ("user prefers tabs", "we deploy to staging"). One entry per topic; writing a topic again replaces it wholesale.
- **Tasks** (`tasks/`) are [Backlog.md](https://github.com/MrLesk/Backlog.md)-shaped files: `task-N` ids, status in frontmatter, an acceptance-criteria checklist agents tick as they go.

The MCP server exposes tools for all of it (`write_doc`, `search_docs`, `set_status`, `write_memory`, `write_task`, ...), a `docsreader://onboarding` resource that teaches agents the model, and every doc as a readable MCP resource. Tool errors carry recovery hints, so agents self-correct instead of stalling.

## Connect your AI agents

In DocsReader: **Settings → AI agents → Connect**. The app detects installed clients (Claude Code, Cursor, Windsurf, VS Code, Codex) and registers the bundled `docsreader-mcp` server with each - one click, user-wide.

To register manually, point any stdio-capable MCP client at the binary:

| Install | Binary location |
| --- | --- |
| Homebrew (macOS) | `docsreader-mcp` (on PATH) |
| DMG (macOS) | `/Applications/DocsReader.app/Contents/MacOS/docsreader-mcp` |
| deb (Linux) | `/usr/bin/docsreader-mcp` (on PATH) |
| Windows | `docsreader-mcp.exe` next to `DocsReader.exe` in the install folder |

`docsreader-mcp` is a local stdio server - there is no URL to add; each client spawns the binary itself. With Claude Code:

```sh
claude mcp add --scope user docsreader -- docsreader-mcp
```

Codex CLI:

```sh
codex mcp add docsreader -- docsreader-mcp
```

VS Code:

```sh
code --add-mcp '{"name":"docsreader","command":"docsreader-mcp"}'
```

Cursor (`~/.cursor/mcp.json`) and Windsurf (`~/.codeium/windsurf/mcp_config.json`):

```json
{ "mcpServers": { "docsreader": { "command": "docsreader-mcp" } } }
```

If `docsreader-mcp` is not on your PATH (macOS DMG install without Homebrew), use the full binary path from the table above instead.

Then tell your agents how to use it: copy the [AGENTS template](docs/AGENTS-TEMPLATE.md) into your repo's `AGENTS.md` or `CLAUDE.md`.

## Features

**Reading**

- **Rendering:** GitHub-flavored Markdown via remark-gfm (tables, task lists, footnotes, autolinks, strikethrough)
- **Math expressions:** LaTeX rendered inline and in blocks via KaTeX
- **Diagrams:** Mermaid renderer (lazy-loaded, follows theme)
- **Box-drawing art:** svgbob converts ASCII diagrams to SVG (experimental)
- **Code blocks:** 20 bundled language grammars via Shiki, twelve highlighter palettes (5 light, 7 dark)
- **Appearance:** light, dark, or follow-system, with six accent hues
- **Type controls:** font family, body size, and reading column width
- **Quick edit:** a pencil on any open doc flips to the raw markdown for fast human fixes; agents stay the primary writers

**Browsing**

- **Workspaces:** keep multiple unrelated folders open and pivot between them
- **Lenses:** four browsing modes over the same library (Tree, Recent, Tags, Pinned)
- **Jump-to-file:** fuzzy finder across every workspace, opens with Cmd+P (binding configurable)
- **Document outline:** auto-built TOC that follows the active heading as you scroll
- **Backlinks:** the sidebar lists every doc that links to the one you are reading, grouped by folder
- **Tabs:** many docs open at once; scroll position remembered per tab
- **Split view:** read two docs side-by-side or stacked; each pane keeps its own tabs, scroll, and external-change banner. Toggle from the header, drag the splitter to resize, or use Cmd+\ (horizontal), Cmd+Shift+\ (vertical), Cmd+1 / Cmd+2 to focus a pane. "Open in other pane" lives in the file context menu.
- **Search:** filename, path, frontmatter title, or tag
- **Sticky favorites:** pin individual files to the top of any workspace
- **Clutter rules:** glob patterns silently exclude files and folders from the explorer
- **External changes surfaced:** when a file you have open changes on disk (other editor, sync service, AI agent), a banner shows what changed with reload / keep / show-diff actions; configurable to silent reload

**Project-aware**

DocsReader stays "point it at any folder" by default. When a workspace happens to ship a `.docs.yaml` or live inside a git repo, extra signals light up automatically.
**Agent-aware**

- **`.docs.yaml` manifests:** projects shipping a v0.1 manifest get curated navigation (hand-curated `items` and auto-listed `folder` sections with sort, title-from, badges, and nesting), project metadata in the workspace switcher, automatic homepage open on first add, cross-project links between open workspaces, ignore patterns, a visibility toggle for previewing public-only views, and a sidebar pane that surfaces manifest issues
- **Managed workspaces:** a folder with a `.docsreader.yaml` marker gets its display name in the switcher, its homepage opened on first add, and agent writes reloading open docs silently
- **Convert prompt:** opening a plain folder offers to make it a managed workspace; declining keeps it read-only forever
- **External changes surfaced:** in unmanaged folders, when a file you have open changes on disk (other editor, sync service, AI agent), a banner shows what changed with reload / keep / show-diff actions
- **Git status decorations:** in a git repo, the file tree shows per-file status badges (M / A / D / R / ? / U) that refresh as files change
- **Git diff vs HEAD:** right-click any tracked file to see the diff between HEAD and your working tree, with unified or side-by-side views and word-level highlighting

**Quiet by default**

- **Minimal chrome:** flat active states, no badges, only user-initiated motion (ADHD-friendly, low visual load)
- **One thing at a time:** lenses replace the tree instead of stacking on top of it
- **Same place, every launch:** controls do not migrate around the window
- **Resumes where you left off:** tabs, scroll, sidebar, and active lens persist across sessions

**Trust**

- **Stays local:** no telemetry, no sync; the only outbound request is the updater check
- **Signed updates:** every release artifact ships with a minisign signature; the updater verifies before applying
- **Gatekeeper-friendly:** code-signed and Apple-notarized on macOS
Expand All @@ -68,18 +126,18 @@ Found a bug, want a feature, or have feedback? [Open an issue](https://github.co

See [ROADMAP.md](./ROADMAP.md) for the full picture. Short version:

- **Next:** find-in-page, full-text search, focus mode, recognition for markdown task formats (Backlog.md, taskmd, generic frontmatter).
- **Later:** PDF export, kanban view over recognized task files, drag-a-folder-to-add-root, file management.
- **Next:** find-in-page, full-text search, focus mode.
- **Later:** PDF export, kanban view over task files, drag-a-folder-to-add-root, file management.
- **Considering:** plugin API, annotations, drag tabs between panes / N-pane nesting, local "smart" features (related-docs, TL;DR).

## Screenshots

| | |
|---|---|
| ![Light theme](docs/screenshots/light-theme.png) | ![Split view, side-by-side](docs/screenshots/split.png) |
| ![Split view, stacked (dark)](docs/screenshots/horizontal-split.png) | ![Split view, dark](docs/screenshots/split-dark.png) |
| ![Settings](docs/screenshots/settings.png) | ![Search](docs/screenshots/search.png) |
| ![Context menu](docs/screenshots/context-menu.png) | ![Quick Open (Cmd+P)](docs/screenshots/quick-open.png) |
| --- | --- |
| ![Light theme](docs/screenshots/light-theme.png) | ![Split view, dark](docs/screenshots/split-dark.png) |
| ![Split view, stacked (dark)](docs/screenshots/horizontal-split.png) | ![Settings](docs/screenshots/settings.png) |
| ![Search](docs/screenshots/search.png) | ![Context menu](docs/screenshots/context-menu.png) |
| ![Quick Open (Cmd+P)](docs/screenshots/quick-open.png) | ![Images](docs/screenshots/images.png) |

## Install

Expand All @@ -100,9 +158,9 @@ curl -fsSL https://raw.githubusercontent.com/anbturki/docsreader/main/install.sh
From [Releases](https://github.com/anbturki/docsreader/releases/latest):

| OS | File |
|---|---|
| --- | --- |
| macOS (Intel + Apple Silicon) | `DocsReader_*_universal.dmg` |
| Linux | `docsreader_*_amd64.AppImage` or `.deb` |
| Linux | `DocsReader_*_amd64.AppImage` or `.deb` |
| Windows | `DocsReader_*_x64-setup.exe` |

## Security
Expand All @@ -120,6 +178,12 @@ bun install
bun run tauri dev
```

The MCP server is a separate tauri-free binary in the same cargo workspace:

```sh
cargo build --manifest-path src-tauri/Cargo.toml -p docsreader-mcp
```

### Releasing

GitHub → Actions → **Cut Release** → enter the version (e.g. `0.1.2`). The workflow bumps `tauri.conf.json` + `Cargo.toml` + `Cargo.lock`, commits, tags, and triggers the release pipeline (signs, notarizes the macOS bundle, drafts a GitHub Release, updates the Homebrew tap).
Loading
Loading