diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 00000000..a6809e21 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,13 @@ +{ + "name": "plannotator-marketplace", + "owner": { + "name": "backnotprop" + }, + "plugins": [ + { + "name": "plannotator", + "source": "./apps/hook", + "description": "Interactive Plan Review: Mark up and refine your plans using native Claude Code hooks, then easily share for team collaboration." + } + ] +} diff --git a/apps/hook/.claude-plugin/plugin.json b/apps/hook/.claude-plugin/plugin.json new file mode 100644 index 00000000..0acf4cfe --- /dev/null +++ b/apps/hook/.claude-plugin/plugin.json @@ -0,0 +1,10 @@ +{ + "name": "plannotator", + "description": "Interactive Plan Review: Mark up and refine your plans using native Claude Code hooks, then easily share for team collaboration.", + "version": "1.0.0", + "author": { + "name": "backnotprop" + }, + "repository": "https://github.com/backnotprop/plannotator", + "keywords": ["planning", "review", "hooks", "ExitPlanMode", "collaboration"] +} diff --git a/apps/hook/hooks/hooks.json b/apps/hook/hooks/hooks.json new file mode 100644 index 00000000..5ce061a0 --- /dev/null +++ b/apps/hook/hooks/hooks.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "PermissionRequest": [ + { + "matcher": "ExitPlanMode", + "hooks": [ + { + "type": "command", + "command": "bun run ${CLAUDE_PLUGIN_ROOT}/server/index.ts", + "timeout": 600 + } + ] + } + ] + } +} diff --git a/apps/hooks/index.html b/apps/hook/index.html similarity index 100% rename from apps/hooks/index.html rename to apps/hook/index.html diff --git a/apps/hooks/index.tsx b/apps/hook/index.tsx similarity index 100% rename from apps/hooks/index.tsx rename to apps/hook/index.tsx diff --git a/apps/hooks/package.json b/apps/hook/package.json similarity index 100% rename from apps/hooks/package.json rename to apps/hook/package.json diff --git a/apps/hooks/server/index.ts b/apps/hook/server/index.ts similarity index 90% rename from apps/hooks/server/index.ts rename to apps/hook/server/index.ts index a33da314..e6fb8939 100644 --- a/apps/hooks/server/index.ts +++ b/apps/hook/server/index.ts @@ -4,16 +4,26 @@ * Spawned by ExitPlanMode hook to serve Plannotator UI and handle approve/deny decisions. * Uses random port to support multiple concurrent Claude Code sessions. * - * Usage: bun run server/index.ts "" + * Reads hook event from stdin, extracts plan content, serves UI, returns decision. */ import { $ } from "bun"; import { join, dirname } from "path"; -const planContent = process.argv[2] || Bun.env.PLAN_CONTENT || ""; +// Read hook event from stdin +const eventJson = await Bun.stdin.text(); + +let planContent = ""; +try { + const event = JSON.parse(eventJson); + planContent = event.tool_input?.plan || ""; +} catch { + console.error("Failed to parse hook event from stdin"); + process.exit(1); +} if (!planContent) { - console.error("No plan content provided. Usage: bun run server/index.ts \"\""); + console.error("No plan content in hook event"); process.exit(1); } @@ -63,7 +73,6 @@ const server = Bun.serve({ const file = Bun.file(join(distDir, filePath)); if (await file.exists()) { - // Set appropriate content types const contentType = getContentType(filePath); return new Response(file, { headers: contentType ? { "Content-Type": contentType } : {} diff --git a/apps/hooks/tsconfig.json b/apps/hook/tsconfig.json similarity index 100% rename from apps/hooks/tsconfig.json rename to apps/hook/tsconfig.json diff --git a/apps/hooks/vite.config.ts b/apps/hook/vite.config.ts similarity index 100% rename from apps/hooks/vite.config.ts rename to apps/hook/vite.config.ts diff --git a/apps/hooks/shell/exit-plan-mode.sh b/apps/hooks/shell/exit-plan-mode.sh deleted file mode 100755 index d21993b7..00000000 --- a/apps/hooks/shell/exit-plan-mode.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -# -# ExitPlanMode Hook - Launches Plannotator for plan approval/denial -# -# This hook intercepts ExitPlanMode, spawns a Bun server with the plan content, -# and blocks until the user approves or denies (with feedback). -# -# Exit codes: -# 0 = User approved → ExitPlanMode proceeds -# 2 = User denied → feedback sent to Claude via stderr -# - -# Get the directory where this hook lives -HOOK_DIR="$(cd "$(dirname "$0")" && pwd)" -PLANNING_HOOK_DIR="$(dirname "$HOOK_DIR")" -SERVER_SCRIPT="$PLANNING_HOOK_DIR/server/index.ts" - -# Read the event from stdin -EVENT=$(cat) - -# Check if this is ExitPlanMode -TOOL_NAME=$(echo "$EVENT" | jq -r '.tool_name // empty') - -if [ "$TOOL_NAME" = "ExitPlanMode" ]; then - # Extract plan content - PLAN=$(echo "$EVENT" | jq -r '.tool_input.plan // empty') - - if [ -n "$PLAN" ] && [ "$PLAN" != "null" ]; then - # Launch Bun server with plan content - # Server blocks until user makes a decision (approve/deny) - # Server exits 0 for approve, 2 for deny (with feedback to stderr) - cd "$PLANNING_HOOK_DIR" - bun run "$SERVER_SCRIPT" "$PLAN" - exit $? - fi -fi - -# If not ExitPlanMode or no plan, allow through -exit 0 diff --git a/package.json b/package.json index 94334df1..a920a4dc 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,10 @@ "private": true, "workspaces": ["apps/*", "packages/*"], "scripts": { - "dev:hooks": "bun run --cwd apps/hooks dev", + "dev:hook": "bun run --cwd apps/hook dev", "dev:portal": "bun run --cwd apps/portal dev", "dev:marketing": "bun run --cwd apps/marketing dev", - "build:hooks": "bun run --cwd apps/hooks build", + "build:hook": "bun run --cwd apps/hook build", "build:portal": "bun run --cwd apps/portal build", "build:marketing": "bun run --cwd apps/marketing build" }