Skip to content

Commit 8b8487d

Browse files
backnotpropclaude
andcommitted
docs: add AGENTS.md (copy of CLAUDE.md)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ddb5d53 commit 8b8487d

1 file changed

Lines changed: 395 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
1+
# Plannotator
2+
3+
A plan review UI for Claude Code that intercepts `ExitPlanMode` via hooks, letting users approve or request changes with annotated feedback. Also provides code review for git diffs and annotation of arbitrary markdown files.
4+
5+
## Project Structure
6+
7+
```
8+
plannotator/
9+
├── apps/
10+
│ ├── hook/ # Claude Code plugin
11+
│ │ ├── .claude-plugin/plugin.json
12+
│ │ ├── commands/ # Slash commands (plannotator-review.md, plannotator-annotate.md)
13+
│ │ ├── hooks/hooks.json # PermissionRequest hook config
14+
│ │ ├── server/index.ts # Entry point (plan + review + annotate subcommands)
15+
│ │ └── dist/ # Built single-file apps (index.html, review.html)
16+
│ ├── opencode-plugin/ # OpenCode plugin
17+
│ │ ├── commands/ # Slash commands (plannotator-review.md, plannotator-annotate.md)
18+
│ │ ├── index.ts # Plugin entry with submit_plan tool + review/annotate event handlers
19+
│ │ ├── plannotator.html # Built plan review app
20+
│ │ └── review-editor.html # Built code review app
21+
│ ├── marketing/ # Marketing site, docs, and blog (plannotator.ai)
22+
│ │ └── astro.config.mjs # Astro 5 static site with content collections
23+
│ ├── paste-service/ # Paste service for short URL sharing
24+
│ │ ├── core/ # Platform-agnostic logic (handler, storage interface, cors)
25+
│ │ ├── stores/ # Storage backends (fs, kv, s3)
26+
│ │ └── targets/ # Deployment entries (bun.ts, cloudflare.ts)
27+
│ ├── review/ # Standalone review server (for development)
28+
│ │ ├── index.html
29+
│ │ ├── index.tsx
30+
│ │ └── vite.config.ts
31+
│ └── vscode-extension/ # VS Code extension — opens plans in editor tabs
32+
│ ├── bin/ # Router scripts (open-in-vscode, xdg-open)
33+
│ ├── src/ # extension.ts, cookie-proxy.ts, ipc-server.ts, panel-manager.ts, editor-annotations.ts, vscode-theme.ts
34+
│ └── package.json # Extension manifest (publisher: backnotprop)
35+
├── packages/
36+
│ ├── server/ # Shared server implementation
37+
│ │ ├── index.ts # startPlannotatorServer(), handleServerReady()
38+
│ │ ├── review.ts # startReviewServer(), handleReviewServerReady()
39+
│ │ ├── annotate.ts # startAnnotateServer(), handleAnnotateServerReady()
40+
│ │ ├── storage.ts # Plan saving to disk (getPlanDir, savePlan, etc.)
41+
│ │ ├── share-url.ts # Server-side share URL generation for remote sessions
42+
│ │ ├── remote.ts # isRemoteSession(), getServerPort()
43+
│ │ ├── browser.ts # openBrowser()
44+
│ │ ├── draft.ts # Annotation draft persistence (~/.plannotator/drafts/)
45+
│ │ ├── integrations.ts # Obsidian, Bear integrations
46+
│ │ ├── ide.ts # VS Code diff integration (openEditorDiff)
47+
│ │ ├── editor-annotations.ts # VS Code editor annotation endpoints
48+
│ │ └── project.ts # Project name detection for tags
49+
│ ├── ui/ # Shared React components
50+
│ │ ├── components/ # Viewer, Toolbar, Settings, etc.
51+
│ │ │ ├── plan-diff/ # PlanDiffBadge, PlanDiffViewer, clean/raw diff views
52+
│ │ │ └── sidebar/ # SidebarContainer, SidebarTabs, VersionBrowser
53+
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts, planDiffEngine.ts
54+
│ │ ├── hooks/ # useSharing.ts, usePlanDiff.ts, useSidebar.ts, useLinkedDoc.ts, useAnnotationDraft.ts, useCodeAnnotationDraft.ts
55+
│ │ └── types.ts
56+
│ ├── shared/ # Cross-package types (EditorAnnotation)
57+
│ ├── editor/ # Plan review App.tsx
58+
│ └── review-editor/ # Code review UI
59+
│ ├── App.tsx # Main review app
60+
│ ├── components/ # DiffViewer, FileTree, ReviewPanel
61+
│ ├── demoData.ts # Demo diff for standalone mode
62+
│ └── index.css # Review-specific styles
63+
├── .claude-plugin/marketplace.json # For marketplace install
64+
└── legacy/ # Old pre-monorepo code (reference only)
65+
```
66+
67+
## Installation
68+
69+
**Via plugin marketplace** (when repo is public):
70+
71+
```
72+
/plugin marketplace add backnotprop/plannotator
73+
```
74+
75+
**Local testing:**
76+
77+
```bash
78+
claude --plugin-dir ./apps/hook
79+
```
80+
81+
## Environment Variables
82+
83+
| Variable | Description |
84+
|----------|-------------|
85+
| `PLANNOTATOR_REMOTE` | Set to `1` or `true` for remote mode (devcontainer, SSH). Uses fixed port and skips browser open. |
86+
| `PLANNOTATOR_PORT` | Fixed port to use. Default: random locally, `19432` for remote sessions. |
87+
| `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. |
88+
| `PLANNOTATOR_SHARE_URL` | Custom base URL for share links (self-hosted portal). Default: `https://share.plannotator.ai`. |
89+
| `PLANNOTATOR_PASTE_URL` | Base URL of the paste service API for short URL sharing. Default: `https://plannotator-paste.plannotator.workers.dev`. |
90+
91+
**Legacy:** `SSH_TTY` and `SSH_CONNECTION` are still detected. Prefer `PLANNOTATOR_REMOTE=1` for explicit control.
92+
93+
**Devcontainer/SSH usage:**
94+
```bash
95+
export PLANNOTATOR_REMOTE=1
96+
export PLANNOTATOR_PORT=9999
97+
```
98+
99+
## Plan Review Flow
100+
101+
```
102+
Claude calls ExitPlanMode
103+
104+
PermissionRequest hook fires
105+
106+
Bun server reads plan from stdin JSON (tool_input.plan)
107+
108+
Server starts on random port, opens browser
109+
110+
User reviews plan, optionally adds annotations
111+
112+
Approve → stdout: {"hookSpecificOutput":{"decision":{"behavior":"allow"}}}
113+
Deny → stdout: {"hookSpecificOutput":{"decision":{"behavior":"deny","message":"..."}}}
114+
```
115+
116+
## Code Review Flow
117+
118+
```
119+
User runs /plannotator-review command
120+
121+
Claude Code: plannotator review subcommand runs
122+
OpenCode: event handler intercepts command
123+
124+
git diff captures unstaged changes
125+
126+
Review server starts, opens browser with diff viewer
127+
128+
User annotates code, provides feedback
129+
130+
Send Feedback → feedback sent to agent session
131+
Approve → "LGTM" sent to agent session
132+
```
133+
134+
## Annotate Flow
135+
136+
```
137+
User runs /plannotator-annotate <file.md> command
138+
139+
Claude Code: plannotator annotate subcommand runs
140+
OpenCode: event handler intercepts command
141+
142+
Markdown file read from disk
143+
144+
Annotate server starts (reuses plan editor HTML with mode:"annotate")
145+
146+
User annotates markdown, provides feedback
147+
148+
Send Annotations → feedback sent to agent session
149+
```
150+
151+
## Server API
152+
153+
### Plan Server (`packages/server/index.ts`)
154+
155+
| Endpoint | Method | Purpose |
156+
| --------------------- | ------ | ------------------------------------------ |
157+
| `/api/plan` | GET | Returns `{ plan, origin, previousPlan, versionInfo }` |
158+
| `/api/plan/version` | GET | Fetch specific version (`?v=N`) |
159+
| `/api/plan/versions` | GET | List all versions of current plan |
160+
| `/api/plan/history` | GET | List all plans in current project |
161+
| `/api/approve` | POST | Approve plan (body: planSave, agentSwitch, obsidian, bear, feedback) |
162+
| `/api/deny` | POST | Deny plan (body: feedback, planSave) |
163+
| `/api/image` | GET | Serve image by path query param |
164+
| `/api/upload` | POST | Upload image, returns `{ path, originalName }` |
165+
| `/api/obsidian/vaults`| GET | Detect available Obsidian vaults |
166+
| `/api/reference/obsidian/files` | GET | List vault markdown files as nested tree (`?vaultPath=<path>`) |
167+
| `/api/reference/obsidian/doc` | GET | Read a vault markdown file (`?vaultPath=<path>&path=<file>`) |
168+
| `/api/plan/vscode-diff` | POST | Open diff in VS Code (body: baseVersion) |
169+
| `/api/doc` | GET | Serve linked .md/.mdx file (`?path=<path>`) |
170+
| `/api/draft` | GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
171+
| `/api/editor-annotations` | GET | List editor annotations (VS Code only) |
172+
| `/api/editor-annotation` | POST/DELETE | Add or remove an editor annotation (VS Code only) |
173+
174+
### Review Server (`packages/server/review.ts`)
175+
176+
| Endpoint | Method | Purpose |
177+
| --------------------- | ------ | ------------------------------------------ |
178+
| `/api/diff` | GET | Returns `{ rawPatch, gitRef, origin }` |
179+
| `/api/file-content` | GET | Returns `{ oldContent, newContent }` for expandable diff context |
180+
| `/api/git-add` | POST | Stage/unstage a file (body: `{ filePath, undo? }`) |
181+
| `/api/feedback` | POST | Submit review (body: feedback, annotations, agentSwitch) |
182+
| `/api/image` | GET | Serve image by path query param |
183+
| `/api/upload` | POST | Upload image, returns `{ path, originalName }` |
184+
| `/api/draft` | GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
185+
| `/api/editor-annotations` | GET | List editor annotations (VS Code only) |
186+
| `/api/editor-annotation` | POST/DELETE | Add or remove an editor annotation (VS Code only) |
187+
188+
### Annotate Server (`packages/server/annotate.ts`)
189+
190+
| Endpoint | Method | Purpose |
191+
| --------------------- | ------ | ------------------------------------------ |
192+
| `/api/plan` | GET | Returns `{ plan, origin, mode: "annotate", filePath }` |
193+
| `/api/feedback` | POST | Submit annotations (body: feedback, annotations) |
194+
| `/api/image` | GET | Serve image by path query param |
195+
| `/api/upload` | POST | Upload image, returns `{ path, originalName }` |
196+
| `/api/draft` | GET/POST/DELETE | Auto-save annotation drafts to survive server crashes |
197+
198+
All servers use random ports locally or fixed port (`19432`) in remote mode.
199+
200+
### Paste Service (`apps/paste-service/`)
201+
202+
| Endpoint | Method | Purpose |
203+
| --------------------- | ------ | ------------------------------------------ |
204+
| `/api/paste` | POST | Store compressed plan data, returns `{ id }` |
205+
| `/api/paste/:id` | GET | Retrieve stored compressed data |
206+
207+
Runs as a separate service on port `19433` (self-hosted) or as a Cloudflare Worker (hosted).
208+
209+
## Plan Version History
210+
211+
Every plan is automatically saved to `~/.plannotator/history/{project}/{slug}/` on arrival, before the user sees the UI. Versions are numbered sequentially (`001.md`, `002.md`, etc.). The slug is derived from the plan's first `# Heading` + today's date via `generateSlug()`, scoped by project name (git repo or cwd). Same heading on the same day = same slug = same plan being iterated on. Identical resubmissions are deduplicated (no new file if content matches the latest version).
212+
213+
This powers the version history API (`/api/plan/version`, `/api/plan/versions`, `/api/plan/history`) and the plan diff system.
214+
215+
History saves independently of the `planSave` user setting (which controls decision snapshots in `~/.plannotator/plans/`). Storage functions live in `packages/server/storage.ts`, with Node-compatible duplicates in `apps/pi-extension/server.ts`. Slug format: `{sanitized-heading}-YYYY-MM-DD` (heading first for readability).
216+
217+
## Plan Diff
218+
219+
When a user denies a plan and Claude resubmits, the UI shows what changed between versions. A `+N/-M` badge appears below the document card; clicking it toggles between normal view and diff view.
220+
221+
**Diff engine** (`packages/ui/utils/planDiffEngine.ts`): Uses the `diff` npm package (`diffLines()`) to compute line-level diffs. Groups consecutive remove+add into "modified" blocks. Returns `PlanDiffBlock[]` and `PlanDiffStats`.
222+
223+
**Two view modes** (toggle via `PlanDiffModeSwitcher`):
224+
- **Rendered** (`PlanCleanDiffView`): Color-coded left borders — green (added), red (removed/strikethrough), yellow (modified)
225+
- **Raw** (`PlanRawDiffView`): Monospace `+/-` lines, git-style
226+
227+
**State** (`packages/ui/hooks/usePlanDiff.ts`): Manages base version selection, diff computation, and version fetching. The server sends `previousPlan` with the initial `/api/plan` response; the hook auto-diffs against it. Users can select any prior version from the sidebar Version Browser.
228+
229+
**Sidebar** (`packages/ui/hooks/useSidebar.ts`): Shared left sidebar with two tabs — Table of Contents and Version Browser. The "Auto-open Sidebar" setting controls whether it opens on load (TOC tab only).
230+
231+
## Data Types
232+
233+
**Location:** `packages/ui/types.ts`
234+
235+
```typescript
236+
enum AnnotationType {
237+
DELETION = "DELETION",
238+
INSERTION = "INSERTION",
239+
REPLACEMENT = "REPLACEMENT",
240+
COMMENT = "COMMENT",
241+
GLOBAL_COMMENT = "GLOBAL_COMMENT",
242+
}
243+
244+
interface ImageAttachment {
245+
path: string; // temp file path
246+
name: string; // human-readable label (e.g., "login-mockup")
247+
}
248+
249+
interface Annotation {
250+
id: string;
251+
blockId: string;
252+
startOffset: number;
253+
endOffset: number;
254+
type: AnnotationType;
255+
text?: string; // For comment/replacement/insertion
256+
originalText: string; // The selected text
257+
createdA: number; // Timestamp
258+
author?: string; // Tater identity
259+
images?: ImageAttachment[]; // Attached images with names
260+
startMeta?: { parentTagName; parentIndex; textOffset };
261+
endMeta?: { parentTagName; parentIndex; textOffset };
262+
}
263+
264+
interface Block {
265+
id: string;
266+
type: "paragraph" | "heading" | "blockquote" | "list-item" | "code" | "hr";
267+
content: string;
268+
level?: number; // For headings (1-6)
269+
language?: string; // For code blocks
270+
order: number;
271+
startLine: number;
272+
}
273+
```
274+
275+
## Markdown Parser
276+
277+
**Location:** `packages/ui/utils/parser.ts`
278+
279+
`parseMarkdownToBlocks(markdown)` splits markdown into Block objects. Handles:
280+
281+
- Headings (`#`, `##`, etc.)
282+
- Code blocks (``` with language extraction)
283+
- List items (`-`, `*`, `1.`)
284+
- Blockquotes (`>`)
285+
- Horizontal rules (`---`)
286+
- Paragraphs (default)
287+
288+
`exportAnnotations(blocks, annotations, globalAttachments)` generates human-readable feedback for Claude. Images are referenced by name: `[image-name] /tmp/path...`.
289+
290+
## Annotation System
291+
292+
**Selection mode:** User selects text → toolbar appears → choose annotation type
293+
**Redline mode:** User selects text → auto-creates DELETION annotation
294+
295+
Text highlighting uses `web-highlighter` library. Code blocks use manual `<mark>` wrapping (web-highlighter can't select inside `<pre>`).
296+
297+
## URL Sharing
298+
299+
**Location:** `packages/ui/utils/sharing.ts`, `packages/ui/hooks/useSharing.ts`
300+
301+
Shares full plan + annotations via URL hash using deflate compression. For large plans, short URLs are created via the paste service (user must explicitly confirm).
302+
303+
**Payload format:**
304+
305+
```typescript
306+
// Image in shareable format: plain string (old) or [path, name] tuple (new)
307+
type ShareableImage = string | [string, string];
308+
309+
interface SharePayload {
310+
p: string; // Plan markdown
311+
a: ShareableAnnotation[]; // Compact annotations
312+
g?: ShareableImage[]; // Global attachments
313+
}
314+
315+
type ShareableAnnotation =
316+
| ["D", string, string | null, ShareableImage[]?] // [type, original, author, images?]
317+
| ["R", string, string, string | null, ShareableImage[]?] // [type, original, replacement, author, images?]
318+
| ["C", string, string, string | null, ShareableImage[]?] // [type, original, comment, author, images?]
319+
| ["I", string, string, string | null, ShareableImage[]?] // [type, context, newText, author, images?]
320+
| ["G", string, string | null, ShareableImage[]?]; // [type, comment, author, images?]
321+
```
322+
323+
**Compression pipeline:**
324+
325+
1. `JSON.stringify(payload)`
326+
2. `CompressionStream('deflate-raw')`
327+
3. Base64 encode
328+
4. URL-safe: replace `+/=` with `-_`
329+
330+
**On load from shared URL:**
331+
332+
1. Parse hash, decompress, restore annotations
333+
2. Find text positions in rendered DOM via text search
334+
3. Apply `<mark>` highlights
335+
4. Clear hash from URL (prevents re-parse on refresh)
336+
337+
## Settings Persistence
338+
339+
**Location:** `packages/ui/utils/storage.ts`, `planSave.ts`, `agentSwitch.ts`
340+
341+
Uses cookies (not localStorage) because each hook invocation runs on a random port. Settings include identity, plan saving (enabled/custom path), and agent switching (OpenCode only).
342+
343+
## Syntax Highlighting
344+
345+
Code blocks use bundled `highlight.js`. Language is extracted from fence (```rust) and applied as `language-{lang}`class. Each block highlighted individually via`hljs.highlightElement()`.
346+
347+
## Requirements
348+
349+
- Bun runtime
350+
- Claude Code with plugin/hooks support, or OpenCode
351+
- Cross-platform: macOS (`open`), Linux (`xdg-open`), Windows (`start`)
352+
353+
## Development
354+
355+
```bash
356+
bun install
357+
358+
# Run any app
359+
bun run dev:hook # Hook server (plan review)
360+
bun run dev:review # Review editor (code review)
361+
bun run dev:portal # Portal editor
362+
bun run dev:marketing # Marketing site
363+
bun run dev:vscode # VS Code extension (watch mode)
364+
```
365+
366+
## Build
367+
368+
```bash
369+
bun run build:hook # Single-file HTML for hook server
370+
bun run build:review # Code review editor
371+
bun run build:opencode # OpenCode plugin (copies HTML from hook + review)
372+
bun run build:portal # Static build for share.plannotator.ai
373+
bun run build:marketing # Static build for plannotator.ai
374+
bun run build:vscode # VS Code extension bundle
375+
bun run package:vscode # Package .vsix for marketplace
376+
bun run build # Build hook + opencode (main targets)
377+
```
378+
379+
**Important:** The OpenCode plugin copies pre-built HTML from `apps/hook/dist/` and `apps/review/dist/`. When making UI changes (in `packages/ui/`, `packages/editor/`, or `packages/review-editor/`), you must rebuild the hook/review first:
380+
381+
```bash
382+
bun run build:hook && bun run build:opencode # For UI changes
383+
```
384+
385+
Running only `build:opencode` will copy stale HTML files.
386+
387+
## Marketing Site
388+
389+
`apps/marketing/` is the plannotator.ai website — landing page, documentation, and blog. Built with Astro 5 (static output, zero client JS except a theme toggle island). Docs are markdown files in `src/content/docs/`, blog posts in `src/content/blog/`, both using Astro content collections. Tailwind CSS v4 via `@tailwindcss/vite`. Deploys to S3/CloudFront via GitHub Actions on push to main.
390+
391+
## Test plugin locally
392+
393+
```
394+
claude --plugin-dir ./apps/hook
395+
```

0 commit comments

Comments
 (0)