Skip to content
Open
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
33 changes: 23 additions & 10 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,22 +170,29 @@ jobs:
local ok=0

for _ in $(seq 1 60); do
if curl -sf "http://127.0.0.1:${port}${endpoint}" -o /dev/null 2>/dev/null; then
ok=1
break
local sessions
sessions="$(curl -sf "http://127.0.0.1:${port}/daemon/sessions" 2>/dev/null || true)"
if [ -n "$sessions" ]; then
local session_url
session_url="$(python3 -c 'import json,sys; data=json.load(sys.stdin); sessions=data.get("sessions") or []; print(sessions[0].get("url","") if sessions else "")' <<< "$sessions")"
if [ -n "$session_url" ] && curl -sf "${session_url}${endpoint}" -o /dev/null 2>/dev/null; then
ok=1
break
fi
fi
sleep 0.5
done

kill "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
PLANNOTATOR_PORT="$port" "$BINARY" daemon stop >/dev/null 2>&1 || true

if [ "$ok" = "0" ]; then
echo "FAIL: ${label} did not respond on :${port}${endpoint}"
echo "FAIL: ${label} did not expose a daemon-scoped ${endpoint}"
exit 1
fi

echo "OK: ${label} responded on :${port}${endpoint}"
echo "OK: ${label} exposed daemon-scoped ${endpoint}"
}

# 2. review: exercises server startup, bundled HTML, git diff, and HTTP.
Expand Down Expand Up @@ -232,9 +239,14 @@ jobs:
try {
for ($i = 0; $i -lt 60; $i++) {
try {
Invoke-WebRequest -Uri "http://127.0.0.1:$Port$Endpoint" -UseBasicParsing -TimeoutSec 1 | Out-Null
$ok = $true
break
$sessionsResponse = Invoke-WebRequest -Uri "http://127.0.0.1:$Port/daemon/sessions" -UseBasicParsing -TimeoutSec 1
$sessionsBody = $sessionsResponse.Content | ConvertFrom-Json
if ($sessionsBody.sessions.Count -gt 0) {
$sessionUrl = $sessionsBody.sessions[0].url
Invoke-WebRequest -Uri "$sessionUrl$Endpoint" -UseBasicParsing -TimeoutSec 1 | Out-Null
$ok = $true
break
}
} catch {
if ($process.HasExited) {
break
Expand All @@ -247,6 +259,7 @@ jobs:
Stop-Process -Id $process.Id -Force
Wait-Process -Id $process.Id -ErrorAction SilentlyContinue
}
& $binary daemon stop *> $null
Remove-Item Env:\PLANNOTATOR_PORT -ErrorAction SilentlyContinue
}

Expand All @@ -255,10 +268,10 @@ jobs:
Get-Content $stdout -ErrorAction SilentlyContinue
Write-Host "stderr:"
Get-Content $stderr -ErrorAction SilentlyContinue
throw "FAIL: $Label did not respond on :$Port$Endpoint"
throw "FAIL: $Label did not expose a daemon-scoped $Endpoint"
}

Write-Host "OK: $Label responded on :$Port$Endpoint"
Write-Host "OK: $Label exposed daemon-scoped $Endpoint"
}

# 2. review: exercises server startup, bundled HTML, git diff, and HTTP.
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,4 @@ opencode.json
plannotator-local
# Local research/reference docs (not for repo)
/reference/
# Local goal setup packages generated by the setup-goal skill.
/goals/
*.bun-build
21 changes: 21 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ plannotator/
│ │ ├── index.ts # startPlannotatorServer(), handleServerReady()
│ │ ├── review.ts # startReviewServer(), handleReviewServerReady()
│ │ ├── annotate.ts # startAnnotateServer(), handleAnnotateServerReady()
│ │ ├── daemon/ # Long-running daemon runtime, state, client, and session store
│ │ ├── storage.ts # Re-exports from @plannotator/shared/storage
│ │ ├── share-url.ts # Server-side share URL generation for remote sessions
│ │ ├── remote.ts # isRemoteSession(), getServerPort()
Expand Down Expand Up @@ -99,6 +100,8 @@ Plannotator has one server implementation:

Claude Code runs this server through the released `plannotator` binary entrypoint. OpenCode and Pi do not package their own server implementations; they call the same binary through the plugin protocol in `packages/shared/plugin-protocol.ts`. Runtime-agnostic logic (store, validation, types) lives in `packages/shared/`.

Daemon-backed commands run through one long-running `plannotator` process per user/machine environment. `plannotator daemon start|status|stop` manage that lifecycle, while normal plan/review/annotate/archive commands auto-start a compatible daemon and create session-scoped browser URLs at `/s/<sessionId>`. Browser API calls must use `/s/<sessionId>/api/...`; root `/api/...` routes are not a daemon session boundary.

## Installation

**Via plugin marketplace** (when repo is public):
Expand Down Expand Up @@ -216,6 +219,24 @@ During normal plan review, an Archive sidebar tab provides the same browsing via

## Server API

### Daemon Runtime (`packages/server/daemon/`)

The daemon is the single long-running Bun server used by normal plan/review/annotate/archive commands. It owns a session store and exposes browser sessions at `/s/<sessionId>`. Session browser APIs are scoped under `/s/<sessionId>/api/...`; root `/api/...` is not a valid daemon session API boundary.

| Endpoint | Method | Purpose |
| --------------------- | ------ | ------------------------------------------ |
| `/daemon/capabilities` | GET | Return daemon protocol/capability metadata |
| `/daemon/status` | GET | Return daemon process, endpoint, and session counts |
| `/daemon/sessions` | GET | List active sessions (`?clean=1` also reaps expired sessions before listing) |
| `/daemon/sessions` | POST | Create a plan/review/annotate/archive session from a plugin-protocol request |
| `/daemon/sessions/:id` | GET | Fetch a session summary |
| `/daemon/sessions/:id/result` | GET | Wait for a session decision/result |
| `/daemon/sessions/:id/cancel` | POST | Cancel a session and dispose its resources |
| `/daemon/sessions/:id` | DELETE | Delete a session record |
| `/daemon/shutdown` | POST | Ask the daemon to stop |
| `/s/:id` | GET | Serve the browser HTML for a session |
| `/s/:id/api/...` | Any | Route browser API requests to that session's plan/review/annotate handler |

### Plan Server (`packages/server/index.ts`)

| Endpoint | Method | Purpose |
Expand Down
15 changes: 14 additions & 1 deletion apps/hook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ curl -fsSL https://plannotator.ai/install.cmd -o install.cmd && install.cmd && d

Released binaries ship with SHA256 sidecars and [SLSA build provenance](https://slsa.dev/) attestations from v0.17.2 onwards. See the [installation docs](https://plannotator.ai/docs/getting-started/installation/#verifying-your-install) for version pinning and verification commands.

The released binary owns Plannotator's browser server runtime for Claude Code, OpenCode, and Pi. See [Single Binary Runtime](../../docs/single-binary-runtime.md) for the plugin client boundary and daemon-next design.
The released binary owns Plannotator's browser server runtime for Claude Code, OpenCode, and Pi. See [Single Binary Runtime](../../docs/single-binary-runtime.md) for the plugin client boundary and daemon runtime design.

---

Expand Down Expand Up @@ -84,6 +84,19 @@ When Claude Code calls `ExitPlanMode`, this hook intercepts and:
| `PLANNOTATOR_BROWSER` | Custom browser to open plans in. macOS: app name or path. Linux/Windows: executable path. |
| `PLANNOTATOR_SHARE_URL` | Custom share portal URL for self-hosting. Default: `https://share.plannotator.ai`. |

## Daemon Runtime

Plan, review, annotate, and archive sessions are created through one long-running `plannotator` daemon. Normal commands auto-start a compatible daemon when needed.

```bash
plannotator daemon status
plannotator daemon stop
plannotator daemon start
plannotator sessions
```

`daemon status` reports the daemon PID, endpoint, protocol version, and active session count. If the running daemon was started with different remote/port settings, stop it and retry with the desired `PLANNOTATOR_REMOTE` / `PLANNOTATOR_PORT` values.

## Remote / Devcontainer Usage

When running Claude Code in a remote environment (SSH, devcontainer, WSL), set `PLANNOTATOR_REMOTE=1` (or `true`) and these environment variables:
Expand Down
6 changes: 4 additions & 2 deletions apps/hook/server/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ describe("CLI top-level help", () => {
expect(output).toContain("plannotator [--browser <name>]");
expect(output).toContain("plannotator review [--git] [PR_URL]");
expect(output).toContain("plannotator annotate <file.md | file.html | https://... | folder/>");
expect(output).toContain("plannotator setup-goal <interview|facts>");
expect(output).toContain("plannotator daemon start|status|stop");
expect(output).toContain("plannotator plugin capabilities");
expect(output).toContain("running 'plannotator' without arguments is for hook integration");
});
});
Expand Down Expand Up @@ -56,8 +57,9 @@ describe("interactive no-arg invocation", () => {
expect(output).toContain("usually launched automatically by Claude Code hooks");
expect(output).toContain("It expects hook JSON on stdin.");
expect(output).toContain("plannotator review");
expect(output).toContain("plannotator setup-goal interview bundle.json --json");
expect(output).toContain("plannotator sessions");
expect(output).toContain("plannotator daemon status");
expect(output).toContain("plannotator plugin capabilities");
expect(output).toContain("Run 'plannotator --help' for top-level usage.");
});
});
5 changes: 3 additions & 2 deletions apps/hook/server/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ export function formatTopLevelHelp(): string {
" plannotator [--browser <name>]",
" plannotator review [--git] [PR_URL]",
" plannotator annotate <file.md | file.html | https://... | folder/> [--no-jina] [--gate] [--json] [--hook]",
" plannotator setup-goal <interview|facts> <bundle.json | -> [--json]",
" plannotator last",
" plannotator archive",
" plannotator setup-goal <interview|facts> <bundle.json | -> [--json]",
" plannotator sessions",
" plannotator daemon start|status|stop",
" plannotator improve-context",
" plannotator plugin capabilities",
"",
Expand All @@ -47,10 +48,10 @@ export function formatInteractiveNoArgClarification(): string {
"For interactive use, try:",
" plannotator review",
" plannotator annotate <file.md | file.html | https://...>",
" plannotator setup-goal interview bundle.json --json",
" plannotator last",
" plannotator archive",
" plannotator sessions",
" plannotator daemon status",
" plannotator plugin capabilities",
"",
"Run 'plannotator --help' for top-level usage.",
Expand Down
Loading