open-plan-annotator 1.1.0 → 1.1.2

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.
@@ -5,19 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "Interactive plan annotation plugin for Claude Code",
8
- "version": "1.1.0"
8
+ "version": "1.1.2"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "open-plan-annotator",
13
- "source": {
14
- "npm": {
15
- "package": "open-plan-annotator",
16
- "version": "1.1.0"
17
- }
18
- },
13
+ "source": "./",
19
14
  "description": "Interactive plan annotation UI: review, strikethrough, and comment on Claude's plans before approving. Fully local, no external services.",
20
- "version": "1.1.0",
15
+ "version": "1.1.2",
21
16
  "author": {
22
17
  "name": "ndom91"
23
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": "1.1.0",
4
+ "version": "1.1.2",
5
5
  "author": {
6
6
  "name": "ndom91"
7
7
  },
package/README.md CHANGED
@@ -1,14 +1,13 @@
1
- # open-plan-annotator
1
+ ![](.github/assets/header.jpg)
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/open-plan-annotator?style=flat-square)](https://www.npmjs.com/package/open-plan-annotator)
4
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT)
5
- [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-pink?style=flat-square)]()
3
+ [![npm version](https://img.shields.io/npm/v/open-plan-annotator?style=for-the-badge&labelColor=black&color=black)](https://www.npmjs.com/package/open-plan-annotator)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-orange.svg?style=for-the-badge&labelColor=black&color=black)](https://opensource.org/licenses/MIT)
5
+ [![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS-pink?style=for-the-badge&labelColor=black&color=black)]()
6
6
 
7
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 approve.
8
8
 
9
9
  Select text to <code>strikethrough</code>, <code>replace</code>, <code>insert</code>, or <code>comment</code> — then approve the plan or request changes
10
10
 
11
- ![](.github/assets/screenshot.png)
12
11
 
13
12
  ## How It Works
14
13
 
@@ -20,6 +19,8 @@ Select text to <code>strikethrough</code>, <code>replace</code>, <code>insert</c
20
19
 
21
20
  Everything runs locally. Nothing leaves your machine.
22
21
 
22
+ ![](.github/assets/screenshot.png)
23
+
23
24
  ## Install
24
25
 
25
26
  > [!NOTE]
@@ -36,7 +37,7 @@ From within Claude Code, add the marketplace and install the plugin:
36
37
  /plugin install open-plan-annotator@ndom91-open-plan-annotator
37
38
  ```
38
39
 
39
- This installs the npm-backed plugin and registers the `ExitPlanMode` hook that launches the annotation UI.
40
+ This installs the npm-backed plugin and registers the `ExitPlanMode` hook that launches the annotation UI. In Claude Code, third-party marketplaces have auto-update disabled by default, so also enable auto-update for the `ndom91-open-plan-annotator` marketplace in the Marketplace UI.
40
41
 
41
42
  ### OpenCode
42
43
 
@@ -44,7 +45,7 @@ Add `open-plan-annotator` to the `plugin` array in your OpenCode config (`openco
44
45
 
45
46
  ```json
46
47
  {
47
- "plugin": ["open-plan-annotator"]
48
+ "plugin": ["open-plan-annotator@latest"]
48
49
  }
49
50
  ```
50
51
 
@@ -57,6 +58,9 @@ OpenCode will install the package and load it automatically. The plugin:
57
58
 
58
59
  To update, refresh the plugin through OpenCode and restart the app so it reloads the latest package-managed runtime.
59
60
 
61
+ > [!NOTE]
62
+ > The update mechanism changed significantly in `1.0.20+`: OpenCode now loads the npm package plus a platform runtime package instead of using the old in-place binary updater. If OpenCode appears to be stuck on an older plugin build, clear the cached `open-plan-annotator` entries under `~/.cache/opencode/node_modules/` and restart OpenCode.
63
+
60
64
  #### Implementation Handoff
61
65
 
62
66
  By default, after a plan is approved the plugin sends "Proceed with implementation." to a `build` agent. To customize or disable this, create `open-plan-annotator.json` in your project's `.opencode/` directory or globally in `~/.config/opencode/`:
@@ -77,6 +81,7 @@ Set `enabled` to `false` to disable auto-handoff. Project config overrides globa
77
81
  If you want to run the CLI standalone or install the package globally:
78
82
 
79
83
  ```sh
84
+ pnpm add -g open-plan-annotator
80
85
  npm install -g open-plan-annotator
81
86
  ```
82
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-plan-annotator",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "type": "module",
5
5
  "description": "Fully local plugin for interactive plan annotation from your Agentic assistants",
6
6
  "author": "ndom91",
@@ -26,9 +26,15 @@
26
26
  },
27
27
  "files": [
28
28
  "bin/open-plan-annotator.mjs",
29
- "shared/",
29
+ "shared/cliHelp.mjs",
30
+ "shared/cliMode.mjs",
31
+ "shared/packageManager.mjs",
32
+ "shared/runtimeResolver.mjs",
33
+ "shared/updateHints.mjs",
30
34
  ".claude-plugin/",
31
- "opencode/",
35
+ "opencode/bridge.js",
36
+ "opencode/config.js",
37
+ "opencode/index.js",
32
38
  "hooks/",
33
39
  "CLAUDE.md",
34
40
  "README.md"
@@ -37,10 +43,10 @@
37
43
  "test": "bun test",
38
44
  "typecheck": "tsgo --noEmit --project ui/tsconfig.json && tsgo --noEmit --project server/tsconfig.json",
39
45
  "build:ui": "cd ui && bun run vite build",
40
- "build:platforms": "node scripts/build-platforms.mjs",
41
- "build": "bun run build:ui && node scripts/build-platforms.mjs",
46
+ "build:platforms": "bun scripts/build-platforms.mjs",
47
+ "build": "bun run build:ui && bun run build:platforms",
42
48
  "release": "bun run build",
43
- "pack:check": "node scripts/check-package-files.mjs",
49
+ "pack:check": "bun scripts/check-package-files.mjs",
44
50
  "dev:ui": "cd ui && bun run vite --port 5173",
45
51
  "dev:server": "NODE_ENV=development bun run server/index.ts",
46
52
  "dev": "NODE_ENV=development bun run server/index.ts & cd ui && bun run vite --port 5173",
@@ -48,11 +54,11 @@
48
54
  "lint:fix": "biome check --write .",
49
55
  "format": "biome format --write .",
50
56
  "do-release": "./scripts/release.sh",
51
- "prepack": "node scripts/claude-pack-docs.mjs prepack",
52
- "postpack": "node scripts/claude-pack-docs.mjs postpack"
57
+ "prepack": "bun scripts/claude-pack-docs.mjs prepack",
58
+ "postpack": "bun scripts/claude-pack-docs.mjs postpack"
53
59
  },
54
60
  "devDependencies": {
55
- "@biomejs/biome": "^2.4.4",
61
+ "@biomejs/biome": "^2.4.6",
56
62
  "@types/node": "^25.3.0",
57
63
  "@types/bun": "^1.3.9",
58
64
  "@typescript/native-preview": "^7.0.0-dev.20260224.1"
@@ -61,9 +67,10 @@
61
67
  "@opencode-ai/plugin": "^1.2.14"
62
68
  },
63
69
  "optionalDependencies": {
64
- "@open-plan-annotator/runtime-darwin-arm64": "1.1.0",
65
- "@open-plan-annotator/runtime-darwin-x64": "1.1.0",
66
- "@open-plan-annotator/runtime-linux-arm64": "1.1.0",
67
- "@open-plan-annotator/runtime-linux-x64": "1.1.0"
68
- }
70
+ "@open-plan-annotator/runtime-darwin-arm64": "1.1.2",
71
+ "@open-plan-annotator/runtime-darwin-x64": "1.1.2",
72
+ "@open-plan-annotator/runtime-linux-arm64": "1.1.2",
73
+ "@open-plan-annotator/runtime-linux-x64": "1.1.2"
74
+ },
75
+ "packageManager": "bun@1.3.9"
69
76
  }
@@ -1,110 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { resolveImplementationHandoff } from "./config.js";
6
-
7
- function withEnv(key: string, value: string | undefined, fn: () => Promise<void>) {
8
- const original = process.env[key];
9
- if (value === undefined) {
10
- delete process.env[key];
11
- } else {
12
- process.env[key] = value;
13
- }
14
-
15
- return fn().finally(() => {
16
- if (original === undefined) {
17
- delete process.env[key];
18
- } else {
19
- process.env[key] = original;
20
- }
21
- });
22
- }
23
-
24
- describe("resolveImplementationHandoff", () => {
25
- test("returns defaults when no config files exist", async () => {
26
- const tempRoot = mkdtempSync(join(tmpdir(), "open-plan-annotator-config-"));
27
-
28
- try {
29
- await withEnv("XDG_CONFIG_HOME", join(tempRoot, "xdg"), async () => {
30
- const result = await resolveImplementationHandoff(join(tempRoot, "project"));
31
- expect(result.enabled).toBe(true);
32
- expect(result.agent).toBe("build");
33
- });
34
- } finally {
35
- rmSync(tempRoot, { recursive: true, force: true });
36
- }
37
- });
38
-
39
- test("uses global config when project config is missing", async () => {
40
- const tempRoot = mkdtempSync(join(tmpdir(), "open-plan-annotator-config-"));
41
- const xdgHome = join(tempRoot, "xdg");
42
- const globalConfigDir = join(xdgHome, "opencode");
43
- mkdirSync(globalConfigDir, { recursive: true });
44
- writeFileSync(
45
- join(globalConfigDir, "open-plan-annotator.json"),
46
- JSON.stringify({ implementationHandoff: { enabled: false, agent: "explore" } }),
47
- "utf8",
48
- );
49
-
50
- try {
51
- await withEnv("XDG_CONFIG_HOME", xdgHome, async () => {
52
- const result = await resolveImplementationHandoff(join(tempRoot, "project"));
53
- expect(result.enabled).toBe(false);
54
- expect(result.agent).toBe("explore");
55
- });
56
- } finally {
57
- rmSync(tempRoot, { recursive: true, force: true });
58
- }
59
- });
60
-
61
- test("project config overrides global config", async () => {
62
- const tempRoot = mkdtempSync(join(tmpdir(), "open-plan-annotator-config-"));
63
- const xdgHome = join(tempRoot, "xdg");
64
- const globalConfigDir = join(xdgHome, "opencode");
65
- const projectDir = join(tempRoot, "project");
66
- const projectConfigDir = join(projectDir, ".opencode");
67
-
68
- mkdirSync(globalConfigDir, { recursive: true });
69
- mkdirSync(projectConfigDir, { recursive: true });
70
-
71
- writeFileSync(
72
- join(globalConfigDir, "open-plan-annotator.json"),
73
- JSON.stringify({ implementationHandoff: { enabled: true, agent: "explore" } }),
74
- "utf8",
75
- );
76
- writeFileSync(
77
- join(projectConfigDir, "open-plan-annotator.json"),
78
- JSON.stringify({ implementationHandoff: { enabled: false, agent: "build" } }),
79
- "utf8",
80
- );
81
-
82
- try {
83
- await withEnv("XDG_CONFIG_HOME", xdgHome, async () => {
84
- const result = await resolveImplementationHandoff(projectDir);
85
- expect(result.enabled).toBe(false);
86
- expect(result.agent).toBe("build");
87
- });
88
- } finally {
89
- rmSync(tempRoot, { recursive: true, force: true });
90
- }
91
- });
92
-
93
- test("ignores malformed config and falls back", async () => {
94
- const tempRoot = mkdtempSync(join(tmpdir(), "open-plan-annotator-config-"));
95
- const xdgHome = join(tempRoot, "xdg");
96
- const globalConfigDir = join(xdgHome, "opencode");
97
- mkdirSync(globalConfigDir, { recursive: true });
98
- writeFileSync(join(globalConfigDir, "open-plan-annotator.json"), "not-json", "utf8");
99
-
100
- try {
101
- await withEnv("XDG_CONFIG_HOME", xdgHome, async () => {
102
- const result = await resolveImplementationHandoff(join(tempRoot, "project"));
103
- expect(result.enabled).toBe(true);
104
- expect(result.agent).toBe("build");
105
- });
106
- } finally {
107
- rmSync(tempRoot, { recursive: true, force: true });
108
- }
109
- });
110
- });
@@ -1,54 +0,0 @@
1
- import { afterEach, describe, expect, mock, test } from "bun:test";
2
-
3
- afterEach(() => {
4
- mock.restore();
5
- });
6
-
7
- function createPluginContext() {
8
- return {
9
- directory: process.cwd(),
10
- client: {
11
- session: {
12
- messages: async () => ({ data: [] }),
13
- prompt: async () => ({ data: null }),
14
- },
15
- app: {
16
- agents: async () => ({ data: [] }),
17
- },
18
- },
19
- };
20
- }
21
-
22
- describe("submit_plan tool output", () => {
23
- test("returns plain text execution instructions after approval", async () => {
24
- mock.module("./bridge.js", () => ({
25
- runPlanReview: async () => ({ approved: true }),
26
- }));
27
-
28
- const { OpenPlanAnnotatorPlugin } = await import(`./index.js?approved-${Date.now()}`);
29
- const plugin = await OpenPlanAnnotatorPlugin(createPluginContext());
30
-
31
- const result = await plugin.tool.submit_plan.execute({ plan: "# Plan" }, { sessionID: "session-1" });
32
-
33
- expect(typeof result).toBe("string");
34
- expect(result).toContain("plan_status=approved");
35
- expect(result).toContain("next_state=EXECUTION");
36
- expect(result).toContain("Do not call `submit_plan` again");
37
- });
38
-
39
- test("returns plain text revision instructions after rejection", async () => {
40
- mock.module("./bridge.js", () => ({
41
- runPlanReview: async () => ({ approved: false, feedback: "Need rollback steps." }),
42
- }));
43
-
44
- const { OpenPlanAnnotatorPlugin } = await import(`./index.js?rejected-${Date.now()}`);
45
- const plugin = await OpenPlanAnnotatorPlugin(createPluginContext());
46
-
47
- const result = await plugin.tool.submit_plan.execute({ plan: "# Plan" }, { sessionID: "session-2" });
48
-
49
- expect(typeof result).toBe("string");
50
- expect(result).toContain("plan_status=rejected");
51
- expect(result).toContain("next_state=PLAN_DRAFT");
52
- expect(result).toContain("Need rollback steps.");
53
- });
54
- });
@@ -1,29 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { buildCliHelpText, buildUnknownCommandPrefix } from "./cliHelp.mjs";
3
-
4
- describe("buildCliHelpText", () => {
5
- test("builds canonical help text for all entrypoints", () => {
6
- expect(buildCliHelpText("1.2.3")).toBe(`open-plan-annotator v1.2.3
7
-
8
- Usage:
9
- open-plan-annotator Show this help
10
- open-plan-annotator < event.json Run as a Claude Code hook (debug)
11
- open-plan-annotator doctor Show resolved runtime details
12
- open-plan-annotator update Show package-managed update guidance
13
- open-plan-annotator upgrade Alias for update
14
- open-plan-annotator --version Print version
15
- open-plan-annotator --help Show this help
16
-
17
- https://github.com/ndom91/open-plan-annotator`);
18
- });
19
- });
20
-
21
- describe("buildUnknownCommandPrefix", () => {
22
- test("formats unknown command prefix consistently", () => {
23
- expect(buildUnknownCommandPrefix("wat")).toBe("open-plan-annotator: unknown command `wat`");
24
- });
25
-
26
- test("handles missing command safely", () => {
27
- expect(buildUnknownCommandPrefix(undefined)).toBe("open-plan-annotator: unknown command ``");
28
- });
29
- });
@@ -1,35 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { resolveCliMode } from "./cliMode.mjs";
3
-
4
- describe("resolveCliMode", () => {
5
- test("returns help for no-arg interactive invocation", () => {
6
- expect(resolveCliMode(undefined, { stdinIsTTY: true })).toBe("help");
7
- });
8
-
9
- test("returns hook for no-arg piped invocation", () => {
10
- expect(resolveCliMode(undefined, { stdinIsTTY: false })).toBe("hook");
11
- });
12
-
13
- test("defaults no-arg invocation to hook mode", () => {
14
- expect(resolveCliMode(undefined)).toBe("hook");
15
- });
16
-
17
- test("treats upgrade as update alias", () => {
18
- expect(resolveCliMode("upgrade")).toBe("update");
19
- });
20
-
21
- test("recognizes doctor subcommand", () => {
22
- expect(resolveCliMode("doctor")).toBe("doctor");
23
- });
24
-
25
- test("recognizes help and version flags", () => {
26
- expect(resolveCliMode("--help")).toBe("help");
27
- expect(resolveCliMode("-h")).toBe("help");
28
- expect(resolveCliMode("--version")).toBe("version");
29
- expect(resolveCliMode("-v")).toBe("version");
30
- });
31
-
32
- test("marks unknown commands explicitly", () => {
33
- expect(resolveCliMode("noop")).toBe("unknown");
34
- });
35
- });
@@ -1,33 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { detectPackageManager } from "./packageManager.mjs";
3
-
4
- describe("detectPackageManager", () => {
5
- test("prefers explicit OPEN_PLAN_PKG_MANAGER", () => {
6
- expect(detectPackageManager({ env: { OPEN_PLAN_PKG_MANAGER: "pnpm" } })).toBe("pnpm");
7
- });
8
-
9
- test("detects package manager from npm user agent", () => {
10
- expect(detectPackageManager({ env: { npm_config_user_agent: "pnpm/10.0.0 node/v22.0.0" } })).toBe("pnpm");
11
- });
12
-
13
- test("detects package manager from npm execpath", () => {
14
- expect(detectPackageManager({ env: { npm_execpath: "/usr/local/lib/node_modules/pnpm/bin/pnpm.cjs" } })).toBe(
15
- "pnpm",
16
- );
17
- });
18
-
19
- test("detects package manager from install path hints", () => {
20
- expect(
21
- detectPackageManager({
22
- env: {},
23
- installPath: "/Users/test/Library/pnpm/global/5/node_modules/open-plan-annotator/bin/open-plan-annotator.mjs",
24
- }),
25
- ).toBe("pnpm");
26
- });
27
-
28
- test("falls back to npm", () => {
29
- expect(detectPackageManager({ env: {}, installPath: "/tmp/open-plan-annotator/bin/open-plan-annotator.mjs" })).toBe(
30
- "npm",
31
- );
32
- });
33
- });
@@ -1,36 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { pathToFileURL } from "node:url";
6
- import { getRuntimePackageName, resolveRuntimeBinary } from "./runtimeResolver.mjs";
7
-
8
- describe("runtimeResolver", () => {
9
- test("maps supported platforms to runtime packages", () => {
10
- expect(getRuntimePackageName("darwin", "arm64")).toBe("@open-plan-annotator/runtime-darwin-arm64");
11
- expect(getRuntimePackageName("linux", "x64")).toBe("@open-plan-annotator/runtime-linux-x64");
12
- });
13
-
14
- test("throws for unsupported platforms", () => {
15
- expect(() => resolveRuntimeBinary({ platform: "win32", arch: "x64" })).toThrow("Unsupported platform win32-x64");
16
- });
17
-
18
- test("resolves binary from installed runtime package", () => {
19
- const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "opa-runtime-"));
20
- const packageRoot = path.join(tempRoot, "node_modules", "@open-plan-annotator", "runtime-linux-x64");
21
- const binaryPath = path.join(packageRoot, "bin", "open-plan-annotator");
22
-
23
- fs.mkdirSync(path.dirname(binaryPath), { recursive: true });
24
- fs.writeFileSync(path.join(packageRoot, "package.json"), '{"name":"@open-plan-annotator/runtime-linux-x64"}');
25
- fs.writeFileSync(binaryPath, "binary");
26
-
27
- const resolved = resolveRuntimeBinary({
28
- platform: "linux",
29
- arch: "x64",
30
- parentUrl: pathToFileURL(path.join(tempRoot, "index.mjs")).href,
31
- });
32
-
33
- expect(resolved.packageName).toBe("@open-plan-annotator/runtime-linux-x64");
34
- expect(fs.realpathSync(resolved.binaryPath)).toBe(fs.realpathSync(binaryPath));
35
- });
36
- });