Add Claude Code plugin structure for marketplace distribution

- Rename apps/hooks → apps/hook (singular)
- Add .claude-plugin/plugin.json manifest
- Add hooks/hooks.json for PermissionRequest hook
- Add root .claude-plugin/marketplace.json for install via:
  /plugin marketplace add backnotprop/plannotator
- Refactor server to read stdin directly (removes bash/jq dependency)
- Delete shell wrapper script

Plugin tested successfully with: claude --plugin-dir ./apps/hook

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michael Ramos
2025-12-28 09:54:48 -08:00
parent 34297b5c55
commit 4e654f369d
11 changed files with 54 additions and 45 deletions
+13
View File
@@ -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."
}
]
}
+10
View File
@@ -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"]
}
+16
View File
@@ -0,0 +1,16 @@
{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "bun run ${CLAUDE_PLUGIN_ROOT}/server/index.ts",
"timeout": 600
}
]
}
]
}
}
@@ -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 "<plan markdown content>"
* 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 \"<plan>\"");
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 } : {}
-39
View File
@@ -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
+2 -2
View File
@@ -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"
}