This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
mo is a CLI tool that opens Markdown files in a browser with live-reload. It runs a Go HTTP server that embeds a React SPA as a single binary. The Go module is github.com/k1LoW/mo.
Requires Go 1.26+ and pnpm. Node.js version is managed via pnpm.executionEnv.nodeVersion in internal/frontend/package.json.
# Full build (frontend + Go binary, with ldflags)
make build
# Dev: build frontend then run with args (uses port 16275, foreground mode)
make dev ARGS="testdata/basic.md"
# Dev with tab groups (-t can only specify one group per invocation)
make dev ARGS="-t design testdata/basic.md"
# Frontend code generation only (called by make build/dev via go generate)
make generate
# Run all tests (frontend + Go)
make test
# Run a single frontend test (vitest)
cd internal/frontend && pnpm test src/utils/buildTree.test.ts
# Run Go tests only
go test ./...
# Run a single Go test
go test ./internal/server/ -run TestHandleFiles
# Run linters (oxlint for frontend, golangci-lint + gostyle for Go)
make lint
# Format frontend code (oxfmt)
make fmt
# Check frontend formatting without modifying
make fmt-check
# Take screenshots for README (requires Chrome)
make screenshot
# CI target (install dev deps + generate + test)
make ci
# Frontend dev server with backend proxy (proxies /_/ to localhost:6275)
cd internal/frontend && pnpm run dev--port/-p— Server port (default: 6275)--target/-t— Tab group name (default:"default")--open— Always open browser--no-open— Never open browser--watch/-w— Boolean flag that turns on watch mode; directory and glob positional arguments are registered as watch patterns--unwatch— Boolean flag that removes watched patterns; directory and glob positional arguments specify which patterns to unwatch (with-R, a directory removes all patterns under it)--recursive/-R— Recurse into subdirectories when a directory is given as an argument (expands*.md→**/*.md)--close— Close files instead of opening them--clear— Clear saved session for the specified port--status— Show status of all running mo servers--shutdown— Shut down the running mo server--restart— Restart the running mo server--foreground— Run mo server in foreground (do not background)--json— Output structured data as JSON to stdout--dangerously-allow-remote-access— Allow remote access without authentication (trusted networks only)
Go backend + embedded React SPA, single binary.
cmd/root.go— CLI entry point (Cobra). Handles single-instance detection: if a server is already running on the port, adds files via HTTP API instead of starting a new server. Supports directory arguments: directories are expanded to*.mdfiles (or converted to watch patterns with--watch). Supports stdin pipe input.cmd/stdin.go— Stdin pipe detection (os.Stdin.Stat()withModeCharDevice), content reading, deterministic name generation (stdin-<hash>.md), and upload to running server.internal/server/server.go— HTTP server, state management (mutex-guarded), SSE for live-reload, file watcher (fsnotify). All API routes use/_/prefix to avoid collision with SPA route paths (group names).internal/static/static.go—go:generateruns the frontend build, thengo:embedembeds the output frominternal/static/dist/.internal/frontend/— Vite + React 19 + TypeScript + Tailwind CSS v4 SPA. Build output goes tointernal/static/dist/(configured invite.config.ts).internal/backup/— State persistence for open files/groups using atomic JSON writes to$XDG_STATE_HOME/mo/backup/. Enables session restoration across server restarts.internal/logfile/— Rotating JSON logging to$XDG_STATE_HOME/mo/log/(max 10MB, 3 backups, 7-day retention).internal/xdg/— XDG Base Directory helper.StateHome()returns$XDG_STATE_HOMEor default~/.local/state.version/version.go— Version info, updated by tagpr on release. Build embeds revision via ldflags.testdata/— Sample Markdown files (GFM, mermaid, math, alerts, etc.) and fixture projects for tests and dev. Reuse these for new test cases.
- Package manager: pnpm (version specified in
internal/frontend/package.jsonpackageManagerfield) - Markdown rendering:
react-markdown+remark-gfm+rehype-raw+rehype-slug(heading IDs) +rehype-sanitize+@shikijs/rehype(syntax highlighting) +mermaid(diagram rendering) +remark-math+rehype-katex(math/LaTeX) +rehype-github-alerts(GitHub-style alerts) +react-zoom-pan-pinch(image zoom) - SPA routing via
window.location.pathname(no router library) - Key components:
App.tsx(routing/state),Sidebar.tsx(file list with flat/tree view, resizable, drag-and-drop reorder),TreeView.tsx(tree view with collapsible directories),MarkdownViewer.tsx(rendering + raw view toggle),TocPanel.tsx(table of contents, resizable),GroupDropdown.tsx(group switcher),FileContextMenu.tsx(shared kebab menu for file operations),WidthToggle.tsx(wide/narrow content width toggle) - Custom hooks:
useSSE.ts(SSE subscription with auto-reconnect),useApi.ts(typed API fetch wrappers),useActiveHeading.ts(scroll-based active heading tracking via IntersectionObserver) - Utilities:
buildTree.ts(converts flat file list to hierarchical tree with common prefix removal and single-child directory collapsing) - Theme: GitHub-style light/dark via CSS custom properties (
--color-gh-*) instyles/app.css, toggled bydata-themeattribute on<html>. UI components use Tailwind classes likebg-gh-bg-sidebar,text-gh-text-secondary, etc. - Toggle button pattern:
RawToggle.tsxandTocToggle.tsxfollow the same style (bg-transparent border border-gh-border rounded-md p-1.5 text-gh-text-secondary). Header buttons (ViewModeToggle,ThemeToggle,WidthToggle, sidebar toggle) usetext-gh-header-textinstead. New buttons should match the appropriate variant.
- Single instance: CLI probes
/_/api/statuson the target port viaprobeServer(). If already running, pushes files viaPOST /_/api/groups/{group}/filesand exits. - File IDs: Files get deterministic string IDs derived from the SHA-256 hash of the absolute path (first 8 hex characters). IDs are stable across server restarts, enabling deep linking. The frontend primarily references files by ID. Absolute paths are available via
FileEntry.pathfor display (e.g., tooltip, tree view). - Tab groups: Files are organized into named groups. Group name maps to the URL path (e.g.,
/design). Default group name is"default". - Live-reload via SSE: fsnotify watches files;
file-changedevents trigger frontend to re-fetch content by file ID. - Sidebar view modes: Flat (default, with drag-and-drop reorder via dnd-kit) and tree (hierarchical directory view). View mode is persisted per-group in localStorage. Collapsed directory state is managed inside
TreeViewand also persisted per-group. - Resizable panels: Both
Sidebar.tsx(left) andTocPanel.tsx(right) use the same drag-to-resize pattern with localStorage persistence. Left sidebar usese.clientX, right panel useswindow.innerWidth - e.clientX. - Toolbar buttons in content area: The toolbar column (ToC + Raw toggles) lives inside
MarkdownViewer.tsx, positioned withshrink-0 flex flex-col gap-2 -mr-4 -mt-4to align with the header. - State persistence: Server state (files, groups, patterns) is backed up to
$XDG_STATE_HOME/mo/backup/mo-<port>.jsonviainternal/backup. On--restart, the server reloads this state to preserve the session. When starting a new server, backup is always restored and merged with CLI-specified files/patterns (restored entries first, CLI entries appended, duplicates skipped). The backup file is preserved across clean--shutdownand is only removed via the--clearpath in the CLI. - Positional arguments:
resolveArgs(args, watchMode, recursive)classifies each positional arg as a glob (viahasGlobChars), directory, or file. With--watch, globs and directories become watch patterns (dir/*.mdordir/**/*.mdwhen-R). Without--watch, they are expanded once viadoublestar.Glob/filepath.Globand treated as files. Plain files are added directly.--watchalone without a glob/dir positional errors out (with a shell-expansion hint if only files were given). - Stdin pipe: When no file arguments are given and stdin is a pipe (not a terminal), content is read from stdin and treated as an uploaded file. Name is
stdin-<first 7 hex of SHA-256>.md(deterministic, consistent with upload dedup). If a server is already running, content is POSTed to the upload API; otherwise it is passed asUploadedFileDatato the new server. Combining stdin with file arguments or--watchreturns an error. Max stdin size is 10MB (same as server upload limit). - Glob pattern watching:
--watchturns on watch mode; directory and glob positional arguments are registered as patterns, then expanded to matching files and monitored for new files via fsnotify directory watches. Patterns are stored with reference-counted directory watches (watchedDirs map[string]int).--unwatchis a boolean flag that uses positional arguments (globs or directories) to determine which patterns to remove; with-R, a directory argument removes all registered patterns under that directory prefix. Ref counts are decremented accordingly. Groups persist as long as they have files or patterns. - localStorage conventions: All keys use
mo-prefix (e.g.,mo-sidebar-width,mo-sidebar-viewmode,mo-sidebar-tree-collapsed,mo-theme). Read patterns usetry/catcharoundJSON.parsewith fallback defaults.
All internal endpoints use /_/api/ prefix and SSE uses /_/events. The /_/ prefix avoids collisions with user-facing group name routes. File-scoped endpoints are nested under /_/api/groups/{group}/ so the group context is always explicit in the URL path.
Key endpoints:
GET /_/api/groups— List all groups with filesPOST /_/api/groups/{group}/files— Add fileDELETE /_/api/groups/{group}/files/{id}— Remove fileGET /_/api/groups/{group}/files/{id}/content— File content (markdown)PUT /_/api/groups/{group}/files/{id}/group— Move file to another group (target group in body)PUT /_/api/groups/{group}/reorder— Reorder files in a groupPOST /_/api/groups/{group}/files/open— Open relative file linkPOST /_/api/groups/{group}/files/upload— Upload file (drag-and-drop)GET /_/api/groups/{group}/files/{id}/raw/{path...}— Raw file assets (images, etc.)POST /_/api/patterns— Add glob watch patternDELETE /_/api/patterns— Remove glob watch patternGET /_/api/status— Server status (version, pid, groups with patterns)GET /_/events— SSE (event types:update,file-changed,restart)
- CI: golangci-lint (via reviewdog), gostyle,
make ci(test + coverage), octocov - Release: tagpr for automated tagging, goreleaser for cross-platform builds. The
go generatestep (frontend build) runs in goreleaser'sbefore.hooks. - License check: Trivy scans for license issues
- CI requires pnpm setup (
pnpm/action-setup) before any Go build step becausego generatetriggers the frontend build.