open-plan-annotator 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@
12
12
  "name": "open-plan-annotator",
13
13
  "source": "./",
14
14
  "description": "Interactive plan annotation UI: review, strikethrough, and comment on Claude's plans before approving. Fully local, no external services.",
15
- "version": "0.2.3",
15
+ "version": "0.2.4",
16
16
  "author": {
17
17
  "name": "ndom91"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "open-plan-annotator",
3
3
  "description": "Interactive plan annotation UI: review, strikethrough, and comment on Claude's plans before approving. Fully local, no external services.",
4
- "version": "0.2.3",
4
+ "version": "0.2.4",
5
5
  "author": {
6
6
  "name": "ndom91"
7
7
  },
package/README.md CHANGED
@@ -4,22 +4,21 @@
4
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT)
5
5
  [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-pink?style=flat-square)]()
6
6
 
7
- A fully local agentic coding plugin (claude-code, opencode) that intercepts plan mode, opens an annotation UI in your browser, and feeds structured feedback back to the agent.
7
+ A fully local agentic coding plugin that intercepts plan mode and opens an annotation UI in your browser. Mark up the plan, send structured feedback to the agent, and receive a revised version — iterate as many times as you need until you're ready to accept.
8
8
 
9
9
  Select text to <code style="color: purple">strikethrough</code>, <code style="color: orange">replace</code>, <code style="color: blue">insert</code>, or <code style="color: red">comment</code> — then approve the plan or request changes.
10
10
 
11
11
 
12
- ![](.github/assets/screenshot_01.png)
12
+ ![](.github/assets/screenshot_1.png)
13
13
 
14
14
 
15
15
  ## How it works
16
16
 
17
- 1. Claude calls `ExitPlanMode`
18
- 2. A `PermissionRequest` hook launches the `open-plan-annotator` binary
19
- 3. An ephemeral HTTP server starts and opens a React UI in your browser
20
- 4. You review and annotate the plan
21
- 5. **Approve** Claude proceeds with the plan
22
- 6. **Request Changes** — annotations are serialized as structured feedback and Claude revises
17
+ 1. Host submits a plan to `open-plan-annotator`
18
+ 2. An ephemeral HTTP server starts and opens a React UI in your browser
19
+ 3. You review and annotate the plan
20
+ 4. **Approve** or **Request Changes**
21
+ 5. The tool returns host-specific JSON output (Claude hook output or OpenCode plugin output)
23
22
 
24
23
  The server shuts down after you decide. Everything runs locally, nothing leaves your machine.
25
24
 
@@ -38,7 +37,9 @@ This JS shim downloads the correct binary for your platform (macOS, Linux).
38
37
  > 'open-plan-annotator' manually to trigger a download, or the first invocation
39
38
  > by Claude will also trigger the binary download.
40
39
 
41
- **2. Add the marketplace and install the plugin**
40
+ ### Claude Code
41
+
42
+ Add the marketplace and install the plugin:
42
43
 
43
44
  From within Claude Code:
44
45
 
@@ -54,7 +55,46 @@ This registers the `ExitPlanMode` hook that launches the annotation UI.
54
55
  > The first run might take a few seconds if you hadn't installed the binary, as
55
56
  > Claude will trigger the download then.
56
57
 
57
- ### From source
58
+ ### OpenCode
59
+
60
+ The OpenCode plugin uses the `@opencode-ai/plugin` SDK to register a `submit_plan` tool and inject system prompt instructions that tell the agent to use plan mode.
61
+
62
+ **Option A: Install from npm (recommended)**
63
+
64
+ ```sh
65
+ npm install -g open-plan-annotator
66
+ cd /path/to/your/project
67
+ open-plan-annotator-install-opencode
68
+ ```
69
+
70
+ This installs the plugin to `.opencode/plugins/open-plan-annotator` and runs `bun install` / `npm install` for its dependencies.
71
+
72
+ Then register it in your project's `opencode.json`:
73
+
74
+ ```json
75
+ {
76
+ "plugin": [".opencode/plugins/open-plan-annotator"]
77
+ }
78
+ ```
79
+
80
+ **Option B: From source**
81
+
82
+ ```sh
83
+ git clone https://github.com/ndom91/open-plan-annotator.git
84
+ cd open-plan-annotator
85
+ bun install
86
+ bun run install:opencode-plugin # installs to .opencode/plugins/ in CWD
87
+ ```
88
+
89
+ Then register in `opencode.json` as above.
90
+
91
+ The plugin automatically:
92
+ - Injects plan-mode instructions into the agent's system prompt
93
+ - Registers a `submit_plan` tool that the agent calls after creating a plan
94
+ - Spawns the annotation UI in your browser for review
95
+ - Returns structured feedback to the agent on approval or rejection
96
+
97
+ ### From source (Claude Code)
58
98
 
59
99
  ```sh
60
100
  git clone https://github.com/ndom91/open-plan-annotator.git
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ require("../scripts/install-opencode-plugin.cjs");
@@ -0,0 +1,174 @@
1
+ import { execFile } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { type Plugin, tool } from "@opencode-ai/plugin";
5
+
6
+ function resolveRunner(): { command: string; args: string[] } {
7
+ const moduleDir = dirname(new URL(import.meta.url).pathname);
8
+ const repoRoot = join(moduleDir, "..");
9
+ const sourceServer = join(repoRoot, "server", "index.ts");
10
+ const packageBin = join(repoRoot, "bin", "open-plan-annotator.cjs");
11
+ const projectLocal = join(process.cwd(), "node_modules", ".bin", "open-plan-annotator");
12
+
13
+ if (existsSync(sourceServer) && process.env.OPEN_PLAN_ANNOTATOR_FORCE_BINARY !== "1") {
14
+ return { command: "bun", args: ["run", sourceServer] };
15
+ }
16
+
17
+ if (existsSync(packageBin)) {
18
+ return { command: process.execPath, args: [packageBin] };
19
+ }
20
+
21
+ if (existsSync(projectLocal)) {
22
+ return { command: projectLocal, args: [] };
23
+ }
24
+
25
+ return { command: "open-plan-annotator", args: [] };
26
+ }
27
+
28
+ function execAsync(
29
+ command: string,
30
+ args: string[],
31
+ stdinData: string,
32
+ ): Promise<{ stdout: string; stderr: string; status: number | null }> {
33
+ return new Promise((resolve) => {
34
+ const child = execFile(
35
+ command,
36
+ args,
37
+ { encoding: "utf8", maxBuffer: 10 * 1024 * 1024 },
38
+ (error, stdout, stderr) => {
39
+ resolve({
40
+ stdout: stdout ?? "",
41
+ stderr: stderr ?? "",
42
+ status: error ? 1 : 0,
43
+ });
44
+ },
45
+ );
46
+ if (child.stdin) {
47
+ child.stdin.write(stdinData);
48
+ child.stdin.end();
49
+ }
50
+ });
51
+ }
52
+
53
+ function normalizeOutput(
54
+ stdout: string,
55
+ stderr: string,
56
+ _status: number | null,
57
+ ): { ok: boolean; decision: string; feedback?: string; message?: string } {
58
+ const text = (stdout ?? "").trim();
59
+ if (!text) {
60
+ return {
61
+ ok: false,
62
+ decision: "deny",
63
+ feedback: stderr || "open-plan-annotator returned empty output",
64
+ message: "Plan changes requested.",
65
+ };
66
+ }
67
+
68
+ try {
69
+ const parsed = JSON.parse(text);
70
+ if (parsed && typeof parsed === "object") return parsed;
71
+ } catch {
72
+ // fall through
73
+ }
74
+
75
+ return {
76
+ ok: false,
77
+ decision: "deny",
78
+ feedback: `Unexpected response from open-plan-annotator: ${text}`,
79
+ message: "Plan changes requested.",
80
+ };
81
+ }
82
+
83
+ const SYSTEM_INSTRUCTIONS = `
84
+ ## Plan Submission
85
+
86
+ When you have completed your plan, you MUST call the \`submit_plan\` tool to submit it for user review.
87
+ The user will be able to:
88
+ - Review your plan visually in a dedicated UI
89
+ - Annotate specific sections with feedback
90
+ - Approve the plan to proceed with implementation
91
+ - Request changes with detailed feedback
92
+
93
+ If your plan is rejected, you will receive the user's annotated feedback. Revise your plan
94
+ based on their feedback and call submit_plan again.
95
+
96
+ Do NOT proceed with implementation until your plan is approved.
97
+
98
+ ### When to Use Plan Mode
99
+
100
+ Use plan mode (and submit your plan for review) for any task involving:
101
+ - Creating or modifying more than 2 files
102
+ - Architectural or structural changes
103
+ - Anything the user hasn't explicitly described step-by-step
104
+ - Refactoring, migration, or feature additions
105
+ - Bug fixes that require investigation
106
+
107
+ For truly trivial tasks (fix a typo, rename a single variable, answer a factual question), you may skip plan submission.
108
+
109
+ ### Plan Quality Standards
110
+
111
+ When writing a plan, include:
112
+ - A brief summary of what you understood the task to require
113
+ - The specific files you intend to create or modify and why
114
+ - Any assumptions you are making
115
+ - An explicit question if anything is ambiguous
116
+ `;
117
+
118
+ export const OpenPlanAnnotatorPlugin: Plugin = async (_ctx) => {
119
+ return {
120
+ "experimental.chat.system.transform": async (_input, output) => {
121
+ output.system.push(SYSTEM_INSTRUCTIONS);
122
+ },
123
+
124
+ tool: {
125
+ submit_plan: tool({
126
+ description:
127
+ "Submit your completed plan for interactive user review. The user can annotate, approve, or request changes. Call this when you have finished creating your implementation plan.",
128
+ args: {
129
+ plan: tool.schema.string().describe("The complete implementation plan in markdown format"),
130
+ summary: tool.schema
131
+ .string()
132
+ .optional()
133
+ .describe("A brief 1-2 sentence summary of what the plan accomplishes"),
134
+ },
135
+
136
+ async execute(args) {
137
+ const runner = resolveRunner();
138
+ const payload = JSON.stringify({
139
+ host: "opencode",
140
+ command: "submit_plan",
141
+ plan: args.plan,
142
+ cwd: process.cwd(),
143
+ });
144
+
145
+ const result = await execAsync(runner.command, runner.args, payload);
146
+ const output = normalizeOutput(result.stdout, result.stderr, result.status);
147
+
148
+ if (output.ok && output.decision === "approve") {
149
+ return [
150
+ "Plan approved by the user. You may now proceed with implementation.",
151
+ args.summary ? `\nPlan Summary: ${args.summary}` : "",
152
+ ].join("");
153
+ }
154
+
155
+ return [
156
+ "Plan needs revision.",
157
+ "",
158
+ "The user has requested changes to your plan. Please review their feedback below and revise your plan accordingly.",
159
+ "",
160
+ "## User Feedback",
161
+ "",
162
+ output.feedback ?? "Plan changes requested.",
163
+ "",
164
+ "---",
165
+ "",
166
+ "Please revise your plan based on this feedback and call `submit_plan` again when ready.",
167
+ ].join("\n");
168
+ },
169
+ }),
170
+ },
171
+ };
172
+ };
173
+
174
+ export default OpenPlanAnnotatorPlugin;
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "open-plan-annotator-opencode",
3
+ "version": "0.2.3",
4
+ "description": "Open Plan Annotator plugin for OpenCode - interactive plan review with visual annotation",
5
+ "author": "ndom91",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ndom91/open-plan-annotator.git",
10
+ "directory": "opencode-plugin"
11
+ },
12
+ "main": "index.ts",
13
+ "files": [
14
+ "index.ts",
15
+ "package.json"
16
+ ],
17
+ "dependencies": {
18
+ "@opencode-ai/plugin": "^1.0.218"
19
+ },
20
+ "peerDependencies": {
21
+ "bun": ">=1.0.0"
22
+ }
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-plan-annotator",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "description": "Fully local plugin for interactive plan annotation from your Agentic assistants",
6
6
  "author": "ndom91",
@@ -11,21 +11,26 @@
11
11
  },
12
12
  "keywords": [
13
13
  "claude-code",
14
+ "opencode",
14
15
  "plugin",
15
16
  "plan-mode",
16
17
  "annotation",
17
18
  "code-review"
18
19
  ],
19
20
  "bin": {
20
- "open-plan-annotator": "bin/open-plan-annotator.cjs"
21
+ "open-plan-annotator": "bin/open-plan-annotator.cjs",
22
+ "open-plan-annotator-install-opencode": "bin/open-plan-annotator-install-opencode.cjs"
21
23
  },
22
24
  "files": [
23
25
  "bin/open-plan-annotator.cjs",
26
+ "bin/open-plan-annotator-install-opencode.cjs",
24
27
  "install.cjs",
25
28
  ".claude-plugin/",
29
+ "opencode-plugin/",
26
30
  "hooks/",
27
31
  "CLAUDE.md",
28
- "README.md"
32
+ "README.md",
33
+ "scripts/install-opencode-plugin.cjs"
29
34
  ],
30
35
  "scripts": {
31
36
  "postinstall": "node install.cjs",
@@ -41,6 +46,7 @@
41
46
  "lint": "biome check .",
42
47
  "lint:fix": "biome check --write .",
43
48
  "format": "biome format --write .",
49
+ "install:opencode-plugin": "node scripts/install-opencode-plugin.cjs",
44
50
  "do-release": "./scripts/release.sh",
45
51
  "prepack": "mv CLAUDE.md CLAUDE.dev.md.bak && cp CLAUDE.plugin.md CLAUDE.md",
46
52
  "postpack": "mv CLAUDE.dev.md.bak CLAUDE.md"
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ function resolveTargetRoot() {
7
+ const fromArg = process.argv[2];
8
+ if (fromArg && fromArg.trim().length > 0) {
9
+ return path.resolve(process.cwd(), fromArg);
10
+ }
11
+ return path.resolve(process.cwd(), ".opencode", "plugins");
12
+ }
13
+
14
+ function ensureParentDir(dir) {
15
+ fs.mkdirSync(dir, { recursive: true });
16
+ }
17
+
18
+ function installPlugin(sourceDir, destinationDir) {
19
+ if (fs.existsSync(destinationDir)) {
20
+ fs.rmSync(destinationDir, { recursive: true, force: true });
21
+ }
22
+
23
+ try {
24
+ fs.symlinkSync(sourceDir, destinationDir, "dir");
25
+ return "symlink";
26
+ } catch {
27
+ fs.cpSync(sourceDir, destinationDir, { recursive: true });
28
+ return "copy";
29
+ }
30
+ }
31
+
32
+ function main() {
33
+ const projectRoot = path.resolve(__dirname, "..");
34
+ const sourceDir = path.join(projectRoot, "opencode-plugin");
35
+ const targetRoot = resolveTargetRoot();
36
+ const destinationDir = path.join(targetRoot, "open-plan-annotator");
37
+
38
+ if (!fs.existsSync(sourceDir)) {
39
+ console.error(`open-plan-annotator: missing opencode-plugin at ${sourceDir}`);
40
+ process.exit(1);
41
+ }
42
+
43
+ ensureParentDir(targetRoot);
44
+ const mode = installPlugin(sourceDir, destinationDir);
45
+
46
+ console.log(
47
+ `open-plan-annotator: installed OpenCode plugin (${mode}) at ${destinationDir}`,
48
+ );
49
+
50
+ // Install plugin dependencies using the first available package manager
51
+ const { execFileSync } = require("child_process");
52
+ const packageManagers = [
53
+ { cmd: "bun", args: ["install"] },
54
+ { cmd: "pnpm", args: ["install"] },
55
+ { cmd: "npm", args: ["install"] },
56
+ ];
57
+
58
+ let installed = false;
59
+ for (const pm of packageManagers) {
60
+ try {
61
+ execFileSync(pm.cmd, pm.args, { cwd: destinationDir, stdio: "inherit" });
62
+ installed = true;
63
+ break;
64
+ } catch {
65
+ // Try the next package manager
66
+ }
67
+ }
68
+
69
+ if (!installed) {
70
+ console.log(
71
+ "open-plan-annotator: could not install dependencies automatically. Run `npm install` in:",
72
+ destinationDir,
73
+ );
74
+ }
75
+
76
+ console.log(
77
+ '\nTo activate, add to your opencode.json:\n { "plugin": [".opencode/plugins/open-plan-annotator"] }',
78
+ );
79
+ }
80
+
81
+ main();