open-plan-annotator 1.0.1 → 1.0.3
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/bin/open-plan-annotator.cjs +10 -0
- package/install.cjs +117 -8
- package/opencode/bridge.js +13 -2
- package/opencode/index.js +10 -7
- package/package.json +1 -1
|
@@ -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.
|
|
15
|
+
"version": "1.0.3",
|
|
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.
|
|
4
|
+
"version": "1.0.3",
|
|
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
|
|
176
|
+
const fallbackUrl = getDownloadUrl();
|
|
92
177
|
console.error(`Downloading open-plan-annotator for ${getPlatformKey()}...`);
|
|
93
178
|
|
|
94
|
-
|
|
95
|
-
|
|
179
|
+
try {
|
|
180
|
+
const { assetName, assetUrl, expectedSha256 } = await resolveReleaseAssetAndChecksum();
|
|
181
|
+
const archiveBuffer = await fetch(assetUrl);
|
|
182
|
+
const actualSha256 = sha256Hex(archiveBuffer);
|
|
96
183
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
184
|
+
if (actualSha256 !== expectedSha256) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Checksum verification failed for ${assetName} (expected ${expectedSha256}, got ${actualSha256})`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
100
189
|
|
|
101
|
-
|
|
102
|
-
|
|
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) => {
|
package/opencode/bridge.js
CHANGED
|
@@ -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:
|
|
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
|
|
11
|
-
- If
|
|
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
|
-
|
|
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 = "
|
|
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 = [
|
|
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("
|
|
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
|
|