open-plan-annotator 1.0.1 → 1.0.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.
@@ -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": "1.0.1",
15
+ "version": "1.0.2",
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": "1.0.1",
4
+ "version": "1.0.2",
5
5
  "author": {
6
6
  "name": "ndom91"
7
7
  },
@@ -39,12 +39,22 @@ if (!fs.existsSync(binaryPath)) {
39
39
  }
40
40
  }
41
41
 
42
+ // Detect package manager so the binary can suggest the right update command
43
+ function detectPackageManager() {
44
+ const ua = process.env.npm_config_user_agent || "";
45
+ if (ua.startsWith("pnpm")) return "pnpm";
46
+ if (ua.startsWith("yarn")) return "yarn";
47
+ if (ua.startsWith("bun")) return "bun";
48
+ return "npm";
49
+ }
50
+
42
51
  // Spawn the binary with detached so it can outlive this wrapper.
43
52
  // We pipe stdout to detect the JSON hook output, then forward it and exit
44
53
  // immediately — the binary keeps its server alive in the background.
45
54
  const child = spawn(binaryPath, process.argv.slice(2), {
46
55
  stdio: ["pipe", "pipe", "inherit"],
47
56
  detached: true,
57
+ env: { ...process.env, OPEN_PLAN_PKG_MANAGER: detectPackageManager() },
48
58
  });
49
59
 
50
60
  child.stdin.write(stdinBuffer);
package/install.cjs CHANGED
@@ -9,6 +9,7 @@ const fs = require("fs");
9
9
  const path = require("path");
10
10
  const zlib = require("zlib");
11
11
  const https = require("https");
12
+ const crypto = require("crypto");
12
13
 
13
14
  const VERSION = require("./package.json").version;
14
15
  const REPO = "ndom91/open-plan-annotator";
@@ -35,6 +36,10 @@ function getDownloadUrl() {
35
36
  return `https://github.com/${REPO}/releases/download/v${VERSION}/${asset}.tar.gz`;
36
37
  }
37
38
 
39
+ function getReleaseApiUrl() {
40
+ return `https://api.github.com/repos/${REPO}/releases/tags/v${VERSION}`;
41
+ }
42
+
38
43
  function fetch(url, redirects) {
39
44
  if (redirects === undefined) redirects = 5;
40
45
  return new Promise((resolve, reject) => {
@@ -55,6 +60,85 @@ function fetch(url, redirects) {
55
60
  });
56
61
  }
57
62
 
63
+ async function fetchJson(url) {
64
+ const buffer = await fetch(url);
65
+ return JSON.parse(buffer.toString("utf8"));
66
+ }
67
+
68
+ function sha256Hex(buffer) {
69
+ return crypto.createHash("sha256").update(buffer).digest("hex");
70
+ }
71
+
72
+ function parseChecksumManifest(manifestText) {
73
+ const checksums = new Map();
74
+
75
+ for (const rawLine of manifestText.split(/\r?\n/)) {
76
+ const line = rawLine.trim();
77
+ if (!line || line.startsWith("#")) continue;
78
+
79
+ const bsdStyle = line.match(/^SHA256\s*\(([^)]+)\)\s*=\s*([a-fA-F0-9]{64})$/);
80
+ if (bsdStyle) {
81
+ checksums.set(bsdStyle[1].trim(), bsdStyle[2].toLowerCase());
82
+ continue;
83
+ }
84
+
85
+ const gnuStyle = line.match(/^([a-fA-F0-9]{64})\s+[* ]?(.+)$/);
86
+ if (gnuStyle) {
87
+ checksums.set(gnuStyle[2].trim(), gnuStyle[1].toLowerCase());
88
+ }
89
+ }
90
+
91
+ return checksums;
92
+ }
93
+
94
+ function selectChecksumAsset(assets) {
95
+ const checksumAssets = assets
96
+ .filter((asset) => {
97
+ const lower = asset.name.toLowerCase();
98
+ return (
99
+ (lower.includes("sha256") || lower.includes("checksum")) &&
100
+ (lower.endsWith(".txt") || lower.endsWith(".sha256") || lower.endsWith(".sha256sum") || lower.endsWith(".sha256sums"))
101
+ );
102
+ })
103
+ .sort((a, b) => a.name.localeCompare(b.name));
104
+
105
+ return checksumAssets[0] || null;
106
+ }
107
+
108
+ async function resolveReleaseAssetAndChecksum() {
109
+ const release = await fetchJson(getReleaseApiUrl());
110
+ const releaseAssets = Array.isArray(release.assets) ? release.assets : [];
111
+ const key = getPlatformKey();
112
+ const assetBaseName = PLATFORM_MAP[key];
113
+ if (!assetBaseName) {
114
+ throw new Error(`Unsupported platform ${key}`);
115
+ }
116
+
117
+ const assetName = `${assetBaseName}.tar.gz`;
118
+ const asset = releaseAssets.find((entry) => entry.name === assetName);
119
+ if (!asset) {
120
+ throw new Error(`Release v${VERSION} is missing asset ${assetName}`);
121
+ }
122
+
123
+ const checksumAsset = selectChecksumAsset(releaseAssets);
124
+ if (!checksumAsset) {
125
+ throw new Error(`Release v${VERSION} does not contain a checksum manifest asset`);
126
+ }
127
+
128
+ const checksumManifest = (await fetch(checksumAsset.browser_download_url)).toString("utf8");
129
+ const checksums = parseChecksumManifest(checksumManifest);
130
+ const expectedSha256 = checksums.get(assetName);
131
+ if (!expectedSha256) {
132
+ throw new Error(`Checksum manifest does not contain ${assetName}`);
133
+ }
134
+
135
+ return {
136
+ assetName,
137
+ assetUrl: asset.browser_download_url,
138
+ expectedSha256,
139
+ };
140
+ }
141
+
58
142
  function extractBinaryFromTarGz(buffer) {
59
143
  const tarBuffer = zlib.gunzipSync(buffer);
60
144
  let offset = 0;
@@ -82,24 +166,49 @@ function extractBinaryFromTarGz(buffer) {
82
166
  async function main() {
83
167
  const destDir = path.join(__dirname, "bin");
84
168
  const destPath = path.join(destDir, "open-plan-annotator-binary");
169
+ const tempPath = `${destPath}.tmp-${process.pid}-${Date.now()}`;
85
170
 
86
171
  // Skip if binary already exists
87
172
  if (fs.existsSync(destPath)) {
88
173
  return;
89
174
  }
90
175
 
91
- const url = getDownloadUrl();
176
+ const fallbackUrl = getDownloadUrl();
92
177
  console.error(`Downloading open-plan-annotator for ${getPlatformKey()}...`);
93
178
 
94
- const buffer = await fetch(url);
95
- const binaryBuffer = extractBinaryFromTarGz(buffer);
179
+ try {
180
+ const { assetName, assetUrl, expectedSha256 } = await resolveReleaseAssetAndChecksum();
181
+ const archiveBuffer = await fetch(assetUrl);
182
+ const actualSha256 = sha256Hex(archiveBuffer);
96
183
 
97
- if (!fs.existsSync(destDir)) {
98
- fs.mkdirSync(destDir, { recursive: true });
99
- }
184
+ if (actualSha256 !== expectedSha256) {
185
+ throw new Error(
186
+ `Checksum verification failed for ${assetName} (expected ${expectedSha256}, got ${actualSha256})`,
187
+ );
188
+ }
100
189
 
101
- fs.writeFileSync(destPath, binaryBuffer, { mode: 0o755 });
102
- console.error(`Installed open-plan-annotator to ${destPath}`);
190
+ const binaryBuffer = extractBinaryFromTarGz(archiveBuffer);
191
+
192
+ if (!fs.existsSync(destDir)) {
193
+ fs.mkdirSync(destDir, { recursive: true });
194
+ }
195
+
196
+ fs.writeFileSync(tempPath, binaryBuffer, { mode: 0o755 });
197
+ fs.renameSync(tempPath, destPath);
198
+ fs.chmodSync(destPath, 0o755);
199
+ console.error(`Installed open-plan-annotator to ${destPath}`);
200
+ } catch (err) {
201
+ try {
202
+ fs.unlinkSync(tempPath);
203
+ } catch {
204
+ // Temp file may not exist
205
+ }
206
+
207
+ const message = err && err.message ? err.message : String(err);
208
+ console.error(`open-plan-annotator: install failed: ${message}`);
209
+ console.error(`Fallback URL for diagnostics: ${fallbackUrl}`);
210
+ throw err;
211
+ }
103
212
  }
104
213
 
105
214
  main().catch((err) => {
@@ -123,6 +123,14 @@ function findWrapperOnPath() {
123
123
  }
124
124
  }
125
125
 
126
+ function detectPackageManager() {
127
+ const ua = process.env.npm_config_user_agent || "";
128
+ if (ua.startsWith("pnpm")) return "pnpm";
129
+ if (ua.startsWith("yarn")) return "yarn";
130
+ if (ua.startsWith("bun")) return "bun";
131
+ return "npm";
132
+ }
133
+
126
134
  /** Ensure the compiled binary exists, downloading if necessary. */
127
135
  function ensureBinary() {
128
136
  if (existsSync(LOCAL_BINARY_PATH)) {
@@ -192,7 +200,10 @@ export async function runPlanReview(options) {
192
200
  const child = spawn(BINARY_PATH, [], {
193
201
  cwd,
194
202
  stdio: ["pipe", "pipe", "pipe"],
195
- env: process.env,
203
+ env: {
204
+ ...process.env,
205
+ OPEN_PLAN_PKG_MANAGER: process.env.OPEN_PLAN_PKG_MANAGER || detectPackageManager(),
206
+ },
196
207
  detached: true,
197
208
  });
198
209
 
@@ -268,4 +279,4 @@ export async function runPlanReview(options) {
268
279
  approved: false,
269
280
  feedback: decision.message,
270
281
  };
271
- }
282
+ }
package/opencode/index.js CHANGED
@@ -5,14 +5,14 @@ import { resolveImplementationHandoff } from "./config.js";
5
5
  const PLAN_REVIEW_INSTRUCTIONS = `## Plan Review Workflow
6
6
 
7
7
  For non-trivial implementation work, create a plan first and call the \`submit_plan\` tool.
8
- The user will review the plan in a browser and either approve it or request changes.
8
+ The user will review the plan in a browser UI and either approve it or request changes.
9
9
 
10
- - If approved, proceed with implementation.
11
- - If changes are requested, revise the plan and call \`submit_plan\` again.
10
+ - If the tool returns that the plan was **approved**: immediately begin writing code. Do NOT call \`submit_plan\` again — the plan phase is complete.
11
+ - If the tool returns **revision feedback**: revise the plan based on the feedback, then call \`submit_plan\` again with the updated plan.
12
12
 
13
- Do not begin implementation until the plan is approved.`;
13
+ Only call \`submit_plan\` once per plan version. After approval, your sole job is to implement what was approved.`;
14
14
 
15
- const IMPLEMENTATION_PROMPT = "Proceed with implementation.";
15
+ const IMPLEMENTATION_PROMPT = "The plan has been approved by the user. Begin implementing it now — write code, create files, and make changes as described in the plan. Do not re-submit or re-review the plan.";
16
16
 
17
17
  function getErrorMessage(error) {
18
18
  if (error instanceof Error && error.message) {
@@ -148,7 +148,10 @@ export const OpenPlanAnnotatorPlugin = async (ctx) => {
148
148
  });
149
149
 
150
150
  if (result.approved) {
151
- const lines = ["Plan approved by the user."];
151
+ const lines = [
152
+ "Plan approved by the user.",
153
+ "Do NOT call `submit_plan` again. The planning phase is finished.",
154
+ ];
152
155
 
153
156
  if (args.summary) {
154
157
  lines.push(`Summary: ${args.summary}`);
@@ -163,7 +166,7 @@ export const OpenPlanAnnotatorPlugin = async (ctx) => {
163
166
  }
164
167
  }
165
168
 
166
- lines.push("Proceed with implementation.");
169
+ lines.push("Begin implementing the approved plan now — write code and make changes.");
167
170
  return lines.join("\n\n");
168
171
  }
169
172
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-plan-annotator",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "description": "Fully local plugin for interactive plan annotation from your Agentic assistants",
6
6
  "author": "ndom91",