2025-12-27 18:14:10 -08:00
|
|
|
/**
|
2026-01-12 19:36:09 -08:00
|
|
|
* Plannotator CLI for Claude Code
|
2025-12-27 18:14:10 -08:00
|
|
|
*
|
2026-01-12 19:36:09 -08:00
|
|
|
* Supports two modes:
|
|
|
|
|
*
|
|
|
|
|
* 1. Plan Review (default, no args):
|
|
|
|
|
* - Spawned by ExitPlanMode hook
|
|
|
|
|
* - Reads hook event from stdin, extracts plan content
|
|
|
|
|
* - Serves UI, returns approve/deny decision to stdout
|
|
|
|
|
*
|
|
|
|
|
* 2. Code Review (`plannotator review`):
|
|
|
|
|
* - Triggered by /review slash command
|
|
|
|
|
* - Runs git diff, opens review UI
|
|
|
|
|
* - Outputs feedback to stdout (captured by slash command)
|
2025-12-31 11:00:03 -07:00
|
|
|
*
|
|
|
|
|
* Environment variables:
|
2026-01-06 14:25:19 -08:00
|
|
|
* PLANNOTATOR_REMOTE - Set to "1" or "true" for remote mode (preferred)
|
|
|
|
|
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
|
2025-12-27 18:14:10 -08:00
|
|
|
*/
|
|
|
|
|
|
2026-01-06 14:25:19 -08:00
|
|
|
import {
|
|
|
|
|
startPlannotatorServer,
|
|
|
|
|
handleServerReady,
|
|
|
|
|
} from "@plannotator/server";
|
2026-01-12 19:36:09 -08:00
|
|
|
import {
|
|
|
|
|
startReviewServer,
|
|
|
|
|
handleReviewServerReady,
|
|
|
|
|
} from "@plannotator/server/review";
|
|
|
|
|
import { getGitContext, runGitDiff } from "@plannotator/server/git";
|
2026-01-04 10:14:36 -08:00
|
|
|
|
2025-12-28 15:28:58 -08:00
|
|
|
// Embed the built HTML at compile time
|
2026-01-06 14:25:19 -08:00
|
|
|
// @ts-ignore - Bun import attribute for text
|
2026-01-12 19:36:09 -08:00
|
|
|
import planHtml from "../dist/index.html" with { type: "text" };
|
|
|
|
|
const planHtmlContent = planHtml as unknown as string;
|
2025-12-27 18:14:10 -08:00
|
|
|
|
2026-01-12 19:36:09 -08:00
|
|
|
// @ts-ignore - Bun import attribute for text
|
|
|
|
|
import reviewHtml from "../dist/review.html" with { type: "text" };
|
|
|
|
|
const reviewHtmlContent = reviewHtml as unknown as string;
|
2025-12-27 18:14:10 -08:00
|
|
|
|
2026-01-12 19:36:09 -08:00
|
|
|
// Check for subcommand
|
|
|
|
|
const args = process.argv.slice(2);
|
|
|
|
|
|
2026-01-14 13:12:37 -08:00
|
|
|
// Check if URL sharing is enabled (default: true)
|
|
|
|
|
const sharingEnabled = process.env.PLANNOTATOR_SHARE !== "disabled";
|
|
|
|
|
|
2026-02-12 15:07:43 -08:00
|
|
|
// Custom share portal URL for self-hosting
|
|
|
|
|
const shareBaseUrl = process.env.PLANNOTATOR_SHARE_URL || undefined;
|
|
|
|
|
|
2026-01-12 19:36:09 -08:00
|
|
|
if (args[0] === "review") {
|
|
|
|
|
// ============================================
|
|
|
|
|
// CODE REVIEW MODE
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
// Get git context (branches, available diff options)
|
|
|
|
|
const gitContext = await getGitContext();
|
|
|
|
|
|
|
|
|
|
// Run git diff HEAD (uncommitted changes - default)
|
|
|
|
|
const { patch: rawPatch, label: gitRef } = await runGitDiff(
|
|
|
|
|
"uncommitted",
|
|
|
|
|
gitContext.defaultBranch
|
2026-01-06 14:25:19 -08:00
|
|
|
);
|
2026-01-12 19:36:09 -08:00
|
|
|
|
2026-01-12 20:16:07 -08:00
|
|
|
// Start review server (even if empty - user can switch diff types)
|
2026-01-12 19:36:09 -08:00
|
|
|
const server = await startReviewServer({
|
|
|
|
|
rawPatch,
|
|
|
|
|
gitRef,
|
|
|
|
|
origin: "claude-code",
|
|
|
|
|
diffType: "uncommitted",
|
|
|
|
|
gitContext,
|
2026-01-14 13:12:37 -08:00
|
|
|
sharingEnabled,
|
2026-02-12 15:07:43 -08:00
|
|
|
shareBaseUrl,
|
2026-01-12 19:36:09 -08:00
|
|
|
htmlContent: reviewHtmlContent,
|
|
|
|
|
onReady: handleReviewServerReady,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Wait for user feedback
|
|
|
|
|
const result = await server.waitForDecision();
|
|
|
|
|
|
|
|
|
|
// Give browser time to receive response and update UI
|
|
|
|
|
await Bun.sleep(1500);
|
|
|
|
|
|
|
|
|
|
// Cleanup
|
|
|
|
|
server.stop();
|
|
|
|
|
|
|
|
|
|
// Output feedback (captured by slash command)
|
|
|
|
|
console.log(result.feedback || "No feedback provided.");
|
|
|
|
|
process.exit(0);
|
|
|
|
|
|
2025-12-27 18:14:10 -08:00
|
|
|
} else {
|
2026-01-12 19:36:09 -08:00
|
|
|
// ============================================
|
|
|
|
|
// PLAN REVIEW MODE (default)
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
// Read hook event from stdin
|
|
|
|
|
const eventJson = await Bun.stdin.text();
|
|
|
|
|
|
|
|
|
|
let planContent = "";
|
2026-01-13 20:31:23 -08:00
|
|
|
let permissionMode = "default";
|
2026-01-12 19:36:09 -08:00
|
|
|
try {
|
|
|
|
|
const event = JSON.parse(eventJson);
|
|
|
|
|
planContent = event.tool_input?.plan || "";
|
2026-01-13 20:31:23 -08:00
|
|
|
permissionMode = event.permission_mode || "default";
|
2026-01-12 19:36:09 -08:00
|
|
|
} catch {
|
|
|
|
|
console.error("Failed to parse hook event from stdin");
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!planContent) {
|
|
|
|
|
console.error("No plan content in hook event");
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Start the plan review server
|
|
|
|
|
const server = await startPlannotatorServer({
|
|
|
|
|
plan: planContent,
|
|
|
|
|
origin: "claude-code",
|
2026-01-13 20:31:23 -08:00
|
|
|
permissionMode,
|
2026-01-14 13:12:37 -08:00
|
|
|
sharingEnabled,
|
2026-02-12 15:07:43 -08:00
|
|
|
shareBaseUrl,
|
2026-01-12 19:36:09 -08:00
|
|
|
htmlContent: planHtmlContent,
|
|
|
|
|
onReady: (url, isRemote, port) => {
|
|
|
|
|
handleServerReady(url, isRemote, port);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Wait for user decision (blocks until approve/deny)
|
|
|
|
|
const result = await server.waitForDecision();
|
|
|
|
|
|
|
|
|
|
// Give browser time to receive response and update UI
|
|
|
|
|
await Bun.sleep(1500);
|
|
|
|
|
|
|
|
|
|
// Cleanup
|
|
|
|
|
server.stop();
|
|
|
|
|
|
|
|
|
|
// Output JSON for PermissionRequest hook decision control
|
|
|
|
|
if (result.approved) {
|
2026-01-13 20:31:23 -08:00
|
|
|
// Build updatedPermissions to preserve the current permission mode
|
|
|
|
|
const updatedPermissions = [];
|
|
|
|
|
if (result.permissionMode) {
|
|
|
|
|
updatedPermissions.push({
|
|
|
|
|
type: "setMode",
|
|
|
|
|
mode: result.permissionMode,
|
|
|
|
|
destination: "session",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 19:36:09 -08:00
|
|
|
console.log(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
hookSpecificOutput: {
|
|
|
|
|
hookEventName: "PermissionRequest",
|
|
|
|
|
decision: {
|
|
|
|
|
behavior: "allow",
|
2026-01-13 20:31:23 -08:00
|
|
|
...(updatedPermissions.length > 0 && { updatedPermissions }),
|
2026-01-12 19:36:09 -08:00
|
|
|
},
|
2026-01-06 14:25:19 -08:00
|
|
|
},
|
2026-01-12 19:36:09 -08:00
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
console.log(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
hookSpecificOutput: {
|
|
|
|
|
hookEventName: "PermissionRequest",
|
|
|
|
|
decision: {
|
|
|
|
|
behavior: "deny",
|
|
|
|
|
message: result.feedback || "Plan changes requested",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-12-27 18:14:10 -08:00
|
|
|
|
2026-01-12 19:36:09 -08:00
|
|
|
process.exit(0);
|
|
|
|
|
}
|