Skip to content
Closed
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
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
name: CI

on:
pull_request:
branches: [main]

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: biomejs/setup-biome@v2
- run: biome ci .
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ plannotator/
│ └── vite.config.ts
├── packages/
│ ├── server/ # Shared server implementation
│ │ ├── index.ts # startPlannotatorServer(), handleServerReady()
│ │ ├── review.ts # startReviewServer(), handleReviewServerReady()
│ │ ├── annotate.ts # startAnnotateServer(), handleAnnotateServerReady()
│ │ ├── index.ts # startPlannotatorServer()
│ │ ├── review.ts # startReviewServer()
│ │ ├── annotate.ts # startAnnotateServer()
│ │ ├── shared-handlers.ts # handleServerReady(), image/upload/agents handlers
│ │ ├── reference-handlers.ts # doc/vault file/vault doc handlers, buildFileTree
│ │ ├── storage.ts # Plan saving to disk (getPlanDir, savePlan, etc.)
│ │ ├── share-url.ts # Server-side share URL generation for remote sessions
│ │ ├── remote.ts # isRemoteSession(), getServerPort()
Expand Down
48 changes: 28 additions & 20 deletions apps/hook/dev-mock-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,23 +203,27 @@ export function devMockApi(): Plugin {
server.middlewares.use((req, res, next) => {
if (req.url === '/api/plan') {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
plan: undefined, // Let editor use its own PLAN_CONTENT
origin: 'claude-code',
previousPlan: PLAN_V2,
versionInfo: { version: 3, totalVersions: 3, project: 'demo' },
sharingEnabled: true,
}));
res.end(
JSON.stringify({
plan: undefined, // Let editor use its own PLAN_CONTENT
origin: 'claude-code',
previousPlan: PLAN_V2,
versionInfo: { version: 3, totalVersions: 3, project: 'demo' },
sharingEnabled: true,
}),
);
return;
}

if (req.url === '/api/plan/versions') {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
project: 'demo',
slug: 'implementation-plan-real-time-collab',
versions,
}));
res.end(
JSON.stringify({
project: 'demo',
slug: 'implementation-plan-real-time-collab',
versions,
}),
);
return;
}

Expand All @@ -239,14 +243,18 @@ export function devMockApi(): Plugin {

if (req.url === '/api/plan/history') {
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({
project: 'demo',
plans: [{
slug: 'implementation-plan-real-time-collab',
versions: 3,
lastModified: new Date(now - 60_000).toISOString(),
}],
}));
res.end(
JSON.stringify({
project: 'demo',
plans: [
{
slug: 'implementation-plan-real-time-collab',
versions: 3,
lastModified: new Date(now - 60_000).toISOString(),
},
],
}),
);
return;
}

Expand Down
8 changes: 4 additions & 4 deletions apps/hook/index.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import App from '@plannotator/editor';
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from '@plannotator/editor';
import '@plannotator/editor/styles';

const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error("Could not find root element to mount to");
throw new Error('Could not find root element to mount to');
}

const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
</React.StrictMode>,
);
104 changes: 51 additions & 53 deletions apps/hook/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,43 +23,36 @@
* PLANNOTATOR_PORT - Fixed port to use (default: random locally, 19432 for remote)
*/

import {
startPlannotatorServer,
handleServerReady,
} from "@plannotator/server";
import {
startReviewServer,
handleReviewServerReady,
} from "@plannotator/server/review";
import {
startAnnotateServer,
handleAnnotateServerReady,
} from "@plannotator/server/annotate";
import { getGitContext, runGitDiff } from "@plannotator/server/git";
import { writeRemoteShareLink } from "@plannotator/server/share-url";
import { handleServerReady, startPlannotatorServer } from '@plannotator/server';
import { handleAnnotateServerReady, startAnnotateServer } from '@plannotator/server/annotate';
import { getGitContext, runGitDiff } from '@plannotator/server/git';
import { handleReviewServerReady, startReviewServer } from '@plannotator/server/review';
import { writeRemoteShareLink } from '@plannotator/server/share-url';

// Embed the built HTML at compile time
// @ts-ignore - Bun import attribute for text
import planHtml from "../dist/index.html" with { type: "text" };
// @ts-expect-error - Bun import attribute for text
import planHtml from '../dist/index.html' with { type: 'text' };

const planHtmlContent = planHtml as unknown as string;

// @ts-ignore - Bun import attribute for text
import reviewHtml from "../dist/review.html" with { type: "text" };
// @ts-expect-error - Bun import attribute for text
import reviewHtml from '../dist/review.html' with { type: 'text' };

const reviewHtmlContent = reviewHtml as unknown as string;

// Check for subcommand
const args = process.argv.slice(2);

// Check if URL sharing is enabled (default: true)
const sharingEnabled = process.env.PLANNOTATOR_SHARE !== "disabled";
const sharingEnabled = process.env.PLANNOTATOR_SHARE !== 'disabled';

// Custom share portal URL for self-hosting
const shareBaseUrl = process.env.PLANNOTATOR_SHARE_URL || undefined;

// Paste service URL for short URL sharing
const pasteApiUrl = process.env.PLANNOTATOR_PASTE_URL || undefined;

if (args[0] === "review") {
if (args[0] === 'review') {
// ============================================
// CODE REVIEW MODE
// ============================================
Expand All @@ -68,18 +61,19 @@ if (args[0] === "review") {
const gitContext = await getGitContext();

// Run git diff HEAD (uncommitted changes - default)
const { patch: rawPatch, label: gitRef, error: diffError } = await runGitDiff(
"uncommitted",
gitContext.defaultBranch
);
const {
patch: rawPatch,
label: gitRef,
error: diffError,
} = await runGitDiff('uncommitted', gitContext.defaultBranch);

// Start review server (even if empty - user can switch diff types)
const server = await startReviewServer({
rawPatch,
gitRef,
error: diffError,
origin: "claude-code",
diffType: "uncommitted",
origin: 'claude-code',
diffType: 'uncommitted',
gitContext,
sharingEnabled,
shareBaseUrl,
Expand All @@ -88,7 +82,9 @@ if (args[0] === "review") {
handleReviewServerReady(url, isRemote, port);

if (isRemote && sharingEnabled && rawPatch) {
await writeRemoteShareLink(rawPatch, shareBaseUrl, "review changes", "diff only").catch(() => {});
await writeRemoteShareLink(rawPatch, shareBaseUrl, 'review changes', 'diff only').catch(
() => {},
);
}
},
});
Expand All @@ -103,22 +99,21 @@ if (args[0] === "review") {
server.stop();

// Output feedback (captured by slash command)
console.log(result.feedback || "No feedback provided.");
console.log(result.feedback || 'No feedback provided.');
process.exit(0);

} else if (args[0] === "annotate") {
} else if (args[0] === 'annotate') {
// ============================================
// ANNOTATE MODE
// ============================================

const filePath = args[1];
if (!filePath) {
console.error("Usage: plannotator annotate <file.md>");
console.error('Usage: plannotator annotate <file.md>');
process.exit(1);
}

// Resolve to absolute path
const path = await import("path");
const path = await import('node:path');
const absolutePath = path.resolve(filePath);

// Read the markdown file
Expand All @@ -133,15 +128,17 @@ if (args[0] === "review") {
const server = await startAnnotateServer({
markdown,
filePath: absolutePath,
origin: "claude-code",
origin: 'claude-code',
sharingEnabled,
shareBaseUrl,
htmlContent: planHtmlContent,
onReady: async (url, isRemote, port) => {
handleAnnotateServerReady(url, isRemote, port);

if (isRemote && sharingEnabled) {
await writeRemoteShareLink(markdown, shareBaseUrl, "annotate", "document only").catch(() => {});
await writeRemoteShareLink(markdown, shareBaseUrl, 'annotate', 'document only').catch(
() => {},
);
}
},
});
Expand All @@ -156,9 +153,8 @@ if (args[0] === "review") {
server.stop();

// Output feedback (captured by slash command)
console.log(result.feedback || "No feedback provided.");
console.log(result.feedback || 'No feedback provided.');
process.exit(0);

} else {
// ============================================
// PLAN REVIEW MODE (default)
Expand All @@ -167,26 +163,26 @@ if (args[0] === "review") {
// Read hook event from stdin
const eventJson = await Bun.stdin.text();

let planContent = "";
let permissionMode = "default";
let planContent = '';
let permissionMode = 'default';
try {
const event = JSON.parse(eventJson);
planContent = event.tool_input?.plan || "";
permissionMode = event.permission_mode || "default";
planContent = event.tool_input?.plan || '';
permissionMode = event.permission_mode || 'default';
} catch {
console.error("Failed to parse hook event from stdin");
console.error('Failed to parse hook event from stdin');
process.exit(1);
}

if (!planContent) {
console.error("No plan content in hook event");
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",
origin: 'claude-code',
permissionMode,
sharingEnabled,
shareBaseUrl,
Expand All @@ -196,7 +192,9 @@ if (args[0] === "review") {
handleServerReady(url, isRemote, port);

if (isRemote && sharingEnabled) {
await writeRemoteShareLink(planContent, shareBaseUrl, "review the plan", "plan only").catch(() => {});
await writeRemoteShareLink(planContent, shareBaseUrl, 'review the plan', 'plan only').catch(
() => {},
);
}
},
});
Expand All @@ -216,34 +214,34 @@ if (args[0] === "review") {
const updatedPermissions = [];
if (result.permissionMode) {
updatedPermissions.push({
type: "setMode",
type: 'setMode',
mode: result.permissionMode,
destination: "session",
destination: 'session',
});
}

console.log(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "PermissionRequest",
hookEventName: 'PermissionRequest',
decision: {
behavior: "allow",
behavior: 'allow',
...(updatedPermissions.length > 0 && { updatedPermissions }),
},
},
})
}),
);
} else {
console.log(
JSON.stringify({
hookSpecificOutput: {
hookEventName: "PermissionRequest",
hookEventName: 'PermissionRequest',
decision: {
behavior: "deny",
message: result.feedback || "Plan changes requested",
behavior: 'deny',
message: result.feedback || 'Plan changes requested',
},
},
})
}),
);
}

Expand Down
12 changes: 3 additions & 9 deletions apps/hook/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,9 @@
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": [
"ES2022",
"DOM",
"DOM.Iterable"
],
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"types": [
"node"
],
"types": ["node"],
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
Expand All @@ -27,4 +21,4 @@
"allowImportingTsExtensions": true,
"noEmit": true
}
}
}
Loading