open-plan-annotator 1.0.9 → 1.0.12
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -0
- package/bin/{open-plan-annotator.cjs → open-plan-annotator.mjs} +7 -5
- package/install.mjs +218 -0
- package/opencode/bridge.js +1 -1
- package/opencode/index.js +62 -20
- package/opencode/index.test.ts +56 -0
- package/package.json +15 -13
- package/shared/releaseAssets.d.ts +12 -0
- package/shared/releaseAssets.mjs +65 -0
- package/install.cjs +0 -231
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "Interactive plan annotation plugin for Claude Code",
|
|
8
|
-
"version": "1.0.
|
|
8
|
+
"version": "1.0.12"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
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.12",
|
|
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.12",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "ndom91"
|
|
7
7
|
},
|
package/README.md
CHANGED
|
@@ -117,6 +117,12 @@ bun run lint:fix # auto-fix
|
|
|
117
117
|
bun run format # format
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
## Maintainer Docs
|
|
121
|
+
|
|
122
|
+
- Architecture: [`docs/architecture.md`](docs/architecture.md)
|
|
123
|
+
- Operations: [`docs/operations.md`](docs/operations.md)
|
|
124
|
+
- Release process: [`docs/release.md`](docs/release.md)
|
|
125
|
+
|
|
120
126
|
## License
|
|
121
127
|
|
|
122
128
|
MIT
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
9
|
const binaryPath = path.join(__dirname, "open-plan-annotator-binary");
|
|
8
|
-
const installScript = path.join(__dirname, "..", "install.
|
|
9
|
-
const VERSION =
|
|
10
|
+
const installScript = path.join(__dirname, "..", "install.mjs");
|
|
11
|
+
const VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8")).version;
|
|
10
12
|
|
|
11
13
|
const arg = process.argv[2];
|
|
12
14
|
|
package/install.mjs
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import https from "node:https";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import {
|
|
8
|
+
PLATFORM_ASSET_BASENAME_MAP,
|
|
9
|
+
REPO,
|
|
10
|
+
getPlatformAssetArchiveName,
|
|
11
|
+
getPlatformKey,
|
|
12
|
+
parseChecksumManifest,
|
|
13
|
+
selectChecksumAsset,
|
|
14
|
+
} from "./shared/releaseAssets.mjs";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import zlib from "node:zlib";
|
|
17
|
+
|
|
18
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const VERSION = JSON.parse(fs.readFileSync(new URL("./package.json", import.meta.url), "utf8")).version;
|
|
20
|
+
|
|
21
|
+
function getReleaseApiUrl() {
|
|
22
|
+
return `https://api.github.com/repos/${REPO}/releases/tags/v${VERSION}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fetch(url, redirects) {
|
|
26
|
+
if (redirects === undefined) redirects = 5;
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
https
|
|
29
|
+
.get(url, { headers: { "User-Agent": "open-plan-annotator-install" } }, (res) => {
|
|
30
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
31
|
+
if (redirects <= 0) return reject(new Error("Too many redirects"));
|
|
32
|
+
return fetch(res.headers.location, redirects - 1).then(resolve, reject);
|
|
33
|
+
}
|
|
34
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
35
|
+
return reject(new Error(`HTTP ${res.statusCode} from ${url}`));
|
|
36
|
+
}
|
|
37
|
+
const chunks = [];
|
|
38
|
+
res.on("data", (c) => chunks.push(c));
|
|
39
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
40
|
+
})
|
|
41
|
+
.on("error", reject);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function fetchJson(url) {
|
|
46
|
+
const buffer = await fetch(url);
|
|
47
|
+
return JSON.parse(buffer.toString("utf8"));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sha256Hex(buffer) {
|
|
51
|
+
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function resolveReleaseAssetAndChecksum(options) {
|
|
55
|
+
const opts = options || {};
|
|
56
|
+
const fetchJsonImpl = opts.fetchJson || fetchJson;
|
|
57
|
+
const fetchBuffer = opts.fetch || fetch;
|
|
58
|
+
const releaseApiUrl = opts.releaseApiUrl || getReleaseApiUrl();
|
|
59
|
+
const version = opts.version || VERSION;
|
|
60
|
+
|
|
61
|
+
const release = await fetchJsonImpl(releaseApiUrl);
|
|
62
|
+
const releaseAssets = Array.isArray(release.assets) ? release.assets : [];
|
|
63
|
+
const key = opts.platformKey || getPlatformKey();
|
|
64
|
+
const assetName = getPlatformAssetArchiveName(key);
|
|
65
|
+
if (!assetName) {
|
|
66
|
+
throw new Error(`Unsupported platform ${key}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const asset = releaseAssets.find((entry) => entry.name === assetName);
|
|
70
|
+
if (!asset) {
|
|
71
|
+
throw new Error(`Release v${version} is missing asset ${assetName}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const checksumAsset = selectChecksumAsset(releaseAssets);
|
|
75
|
+
if (!checksumAsset) {
|
|
76
|
+
throw new Error(`Release v${version} does not contain a checksum manifest asset`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const checksumManifest = (await fetchBuffer(checksumAsset.browser_download_url)).toString("utf8");
|
|
80
|
+
const checksums = parseChecksumManifest(checksumManifest);
|
|
81
|
+
const expectedSha256 = checksums.get(assetName);
|
|
82
|
+
if (!expectedSha256) {
|
|
83
|
+
throw new Error(`Checksum manifest does not contain ${assetName}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
assetName,
|
|
88
|
+
assetUrl: asset.browser_download_url,
|
|
89
|
+
expectedSha256,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function errorMessage(err) {
|
|
94
|
+
return err && err.message ? err.message : String(err);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function downloadVerifiedArchive(options) {
|
|
98
|
+
const opts = options || {};
|
|
99
|
+
const resolveRelease = opts.resolveReleaseAssetAndChecksum || resolveReleaseAssetAndChecksum;
|
|
100
|
+
const fetchBuffer = opts.fetch || fetch;
|
|
101
|
+
const checksumRequirement =
|
|
102
|
+
"open-plan-annotator requires release checksum/sha256sum availability and will not install without verification.";
|
|
103
|
+
|
|
104
|
+
let releaseInfo;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
releaseInfo = await resolveRelease();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
throw new Error(`Unable to verify release checksums: ${errorMessage(err)} ${checksumRequirement}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { assetName, assetUrl, expectedSha256 } = releaseInfo;
|
|
113
|
+
const archiveBuffer = await fetchBuffer(assetUrl);
|
|
114
|
+
const actualSha256 = sha256Hex(archiveBuffer);
|
|
115
|
+
|
|
116
|
+
if (actualSha256 !== expectedSha256) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Checksum verification failed for ${assetName} (expected ${expectedSha256}, got ${actualSha256}). ${checksumRequirement}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return archiveBuffer;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function extractBinaryFromTarGz(buffer) {
|
|
126
|
+
const tarBuffer = zlib.gunzipSync(buffer);
|
|
127
|
+
let offset = 0;
|
|
128
|
+
|
|
129
|
+
while (offset < tarBuffer.length) {
|
|
130
|
+
const header = tarBuffer.subarray(offset, offset + 512);
|
|
131
|
+
offset += 512;
|
|
132
|
+
|
|
133
|
+
const name = header.toString("utf-8", 0, 100).replace(/\0.*/g, "");
|
|
134
|
+
const sizeStr = header.toString("utf-8", 124, 136).replace(/\0.*/g, "").trim();
|
|
135
|
+
const size = parseInt(sizeStr, 8);
|
|
136
|
+
|
|
137
|
+
if (!name || isNaN(size)) break;
|
|
138
|
+
|
|
139
|
+
if (name === "open-plan-annotator" || name.endsWith("/open-plan-annotator")) {
|
|
140
|
+
return tarBuffer.subarray(offset, offset + size);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
offset += Math.ceil(size / 512) * 512;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw new Error("Binary 'open-plan-annotator' not found in archive");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function main() {
|
|
150
|
+
const destDir = path.join(__dirname, "bin");
|
|
151
|
+
const destPath = path.join(destDir, "open-plan-annotator-binary");
|
|
152
|
+
const tempPath = `${destPath}.tmp-${process.pid}-${Date.now()}`;
|
|
153
|
+
|
|
154
|
+
// Skip if binary already exists
|
|
155
|
+
if (fs.existsSync(destPath)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.error(`Downloading open-plan-annotator for ${getPlatformKey()}...`);
|
|
160
|
+
const archiveBuffer = await downloadVerifiedArchive();
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const binaryBuffer = extractBinaryFromTarGz(archiveBuffer);
|
|
164
|
+
|
|
165
|
+
if (!fs.existsSync(destDir)) {
|
|
166
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fs.writeFileSync(tempPath, binaryBuffer, { mode: 0o755 });
|
|
170
|
+
fs.renameSync(tempPath, destPath);
|
|
171
|
+
fs.chmodSync(destPath, 0o755);
|
|
172
|
+
console.error(`Installed open-plan-annotator to ${destPath}`);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
try {
|
|
175
|
+
fs.unlinkSync(tempPath);
|
|
176
|
+
} catch {
|
|
177
|
+
// Temp file may not exist
|
|
178
|
+
}
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function shouldSkipInstall() {
|
|
184
|
+
return Boolean(process.env.OPEN_PLAN_ANNOTATOR_SKIP_INSTALL || process.env.npm_config_dev);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function runCli() {
|
|
188
|
+
if (shouldSkipInstall()) {
|
|
189
|
+
process.exit(0);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main().catch((err) => {
|
|
193
|
+
console.error("Failed to install open-plan-annotator binary:", err.message);
|
|
194
|
+
console.error("You can try manually running: node", path.join(__dirname, "install.mjs"));
|
|
195
|
+
process.exit(1);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
200
|
+
runCli();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export {
|
|
204
|
+
VERSION,
|
|
205
|
+
PLATFORM_ASSET_BASENAME_MAP as PLATFORM_MAP,
|
|
206
|
+
getPlatformKey,
|
|
207
|
+
getReleaseApiUrl,
|
|
208
|
+
fetch,
|
|
209
|
+
fetchJson,
|
|
210
|
+
sha256Hex,
|
|
211
|
+
parseChecksumManifest,
|
|
212
|
+
selectChecksumAsset,
|
|
213
|
+
resolveReleaseAssetAndChecksum,
|
|
214
|
+
extractBinaryFromTarGz,
|
|
215
|
+
downloadVerifiedArchive,
|
|
216
|
+
shouldSkipInstall,
|
|
217
|
+
main,
|
|
218
|
+
};
|
package/opencode/bridge.js
CHANGED
|
@@ -6,7 +6,7 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
|
|
7
7
|
const PKG_ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
8
8
|
const LOCAL_BINARY_PATH = join(PKG_ROOT, "bin", "open-plan-annotator-binary");
|
|
9
|
-
const INSTALL_SCRIPT = join(PKG_ROOT, "install.
|
|
9
|
+
const INSTALL_SCRIPT = join(PKG_ROOT, "install.mjs");
|
|
10
10
|
|
|
11
11
|
/** Resolved path to the binary (may differ from LOCAL_BINARY_PATH if found on PATH). */
|
|
12
12
|
let BINARY_PATH = LOCAL_BINARY_PATH;
|
package/opencode/index.js
CHANGED
|
@@ -4,15 +4,44 @@ import { resolveImplementationHandoff } from "./config.js";
|
|
|
4
4
|
|
|
5
5
|
const PLAN_REVIEW_INSTRUCTIONS = `## Plan Review Workflow
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
Track planning/execution using this state enum:
|
|
8
|
+
- \`DISCOVERY\`, \`PLAN_DRAFT\`, \`AWAITING_PLAN_DECISION\`, \`EXECUTION\`, \`DONE\`
|
|
9
|
+
|
|
10
|
+
State transitions:
|
|
11
|
+
- Start in \`DISCOVERY\`.
|
|
12
|
+
- Move to \`PLAN_DRAFT\` only when a plan is required.
|
|
13
|
+
- From \`PLAN_DRAFT\`, call \`submit_plan\` exactly once, then move to \`AWAITING_PLAN_DECISION\`.
|
|
14
|
+
- If user approves plan, set \`plan_status=approved\` and move to \`EXECUTION\`.
|
|
15
|
+
- If user rejects or requests plan changes, set \`plan_status=rejected\` and return to \`PLAN_DRAFT\`.
|
|
16
|
+
- When work is complete, move to \`DONE\`.
|
|
17
|
+
|
|
18
|
+
Required flags:
|
|
19
|
+
- \`plan_status\` in \`{none, submitted, approved, rejected}\`
|
|
20
|
+
- \`explicit_replan\` in \`{true,false}\` (default \`false\`)
|
|
21
|
+
- Set \`explicit_replan=true\` only when user clearly asks to replan (for example: revise/change/new/update plan).
|
|
22
|
+
|
|
23
|
+
Hard rules:
|
|
24
|
+
1) \`submit_plan\` is allowed only in \`PLAN_DRAFT\`.
|
|
25
|
+
2) If \`plan_status=approved\`, \`submit_plan\` is forbidden unless \`explicit_replan=true\`.
|
|
26
|
+
3) Call \`submit_plan\` at most once per plan draft/version. If rejected, revise and submit once for the new draft.
|
|
27
|
+
4) After approval, treat follow-up user messages as execution refinements by default, not planning triggers.
|
|
28
|
+
5) On conflict, prioritize the approved plan and execute immediately.
|
|
29
|
+
6) Do not ask permission to proceed after approval; execute and report progress/results.
|
|
30
|
+
7) When delegating to subagents, always pass current \`plan_status\` and \`explicit_replan\` values.
|
|
31
|
+
8) If \`plan_status=approved\` and \`explicit_replan=false\`, subagents must execute and must not call \`submit_plan\`.
|
|
32
|
+
|
|
33
|
+
Tool guard before calling \`submit_plan\`:
|
|
34
|
+
- assert \`state == PLAN_DRAFT\`
|
|
35
|
+
- assert \`plan_status != approved || explicit_replan == true\`
|
|
36
|
+
- if an assertion fails, continue execution without submitting a new plan.`;
|
|
37
|
+
|
|
38
|
+
const IMPLEMENTATION_PROMPT = [
|
|
39
|
+
"Plan review status: plan_status=approved.",
|
|
40
|
+
"State transition: next_state=EXECUTION.",
|
|
41
|
+
"Replan intent: explicit_replan=false unless the user explicitly asks to revise the plan.",
|
|
42
|
+
"Execute the approved plan directly now — write code, create files, and make changes.",
|
|
43
|
+
"Do not call `submit_plan` again unless the user explicitly requests re-planning.",
|
|
44
|
+
].join(" ");
|
|
16
45
|
|
|
17
46
|
function getErrorMessage(error) {
|
|
18
47
|
if (error instanceof Error && error.message) {
|
|
@@ -133,7 +162,7 @@ export const OpenPlanAnnotatorPlugin = async (ctx) => {
|
|
|
133
162
|
tool: {
|
|
134
163
|
submit_plan: tool({
|
|
135
164
|
description:
|
|
136
|
-
"Submit a markdown plan for interactive user review. Returns
|
|
165
|
+
"Submit a markdown plan for interactive user review. Returns a structured result with plan_status, next_state, approved, and feedback fields.",
|
|
137
166
|
|
|
138
167
|
args: {
|
|
139
168
|
plan: tool.schema.string().describe("The complete implementation plan in markdown format"),
|
|
@@ -147,6 +176,13 @@ export const OpenPlanAnnotatorPlugin = async (ctx) => {
|
|
|
147
176
|
cwd: ctx.directory,
|
|
148
177
|
});
|
|
149
178
|
|
|
179
|
+
const basePayload = {
|
|
180
|
+
plan_status: result.approved ? "approved" : "rejected",
|
|
181
|
+
next_state: result.approved ? "EXECUTION" : "PLAN_DRAFT",
|
|
182
|
+
approved: result.approved,
|
|
183
|
+
feedback: result.approved ? null : (result.feedback ?? "Plan changes requested."),
|
|
184
|
+
};
|
|
185
|
+
|
|
150
186
|
if (result.approved) {
|
|
151
187
|
const lines = [
|
|
152
188
|
"Plan approved by the user.",
|
|
@@ -167,18 +203,24 @@ export const OpenPlanAnnotatorPlugin = async (ctx) => {
|
|
|
167
203
|
}
|
|
168
204
|
|
|
169
205
|
lines.push("Begin implementing the approved plan now — write code and make changes.");
|
|
170
|
-
return
|
|
206
|
+
return {
|
|
207
|
+
...basePayload,
|
|
208
|
+
guidance: lines.join("\n\n"),
|
|
209
|
+
};
|
|
171
210
|
}
|
|
172
211
|
|
|
173
|
-
return
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
212
|
+
return {
|
|
213
|
+
...basePayload,
|
|
214
|
+
guidance: [
|
|
215
|
+
"Plan needs revision.",
|
|
216
|
+
"",
|
|
217
|
+
"## User feedback",
|
|
218
|
+
"",
|
|
219
|
+
basePayload.feedback,
|
|
220
|
+
"",
|
|
221
|
+
"Revise the plan using this feedback, then submit the revised draft once via `submit_plan`.",
|
|
222
|
+
].join("\n"),
|
|
223
|
+
};
|
|
182
224
|
},
|
|
183
225
|
}),
|
|
184
226
|
},
|
|
@@ -0,0 +1,56 @@
|
|
|
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 return schema contract", () => {
|
|
23
|
+
test("approved decision maps to approved execution payload", 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(result.plan_status).toBe("approved");
|
|
34
|
+
expect(result.next_state).toBe("EXECUTION");
|
|
35
|
+
expect(result.approved).toBe(true);
|
|
36
|
+
expect(result.feedback).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("rejected decision maps to plan redraft payload with bridge feedback", async () => {
|
|
40
|
+
const bridgeFeedback = "Need to add rollback steps.";
|
|
41
|
+
|
|
42
|
+
mock.module("./bridge.js", () => ({
|
|
43
|
+
runPlanReview: async () => ({ approved: false, feedback: bridgeFeedback }),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const { OpenPlanAnnotatorPlugin } = await import(`./index.js?rejected-${Date.now()}`);
|
|
47
|
+
const plugin = await OpenPlanAnnotatorPlugin(createPluginContext());
|
|
48
|
+
|
|
49
|
+
const result = await plugin.tool.submit_plan.execute({ plan: "# Plan" }, { sessionID: "session-2" });
|
|
50
|
+
|
|
51
|
+
expect(result.plan_status).toBe("rejected");
|
|
52
|
+
expect(result.next_state).toBe("PLAN_DRAFT");
|
|
53
|
+
expect(result.approved).toBe(false);
|
|
54
|
+
expect(result.feedback).toBe(bridgeFeedback);
|
|
55
|
+
});
|
|
56
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-plan-annotator",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fully local plugin for interactive plan annotation from your Agentic assistants",
|
|
6
6
|
"author": "ndom91",
|
|
@@ -19,11 +19,12 @@
|
|
|
19
19
|
],
|
|
20
20
|
"main": "opencode/index.js",
|
|
21
21
|
"bin": {
|
|
22
|
-
"open-plan-annotator": "bin/open-plan-annotator.
|
|
22
|
+
"open-plan-annotator": "bin/open-plan-annotator.mjs"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
|
-
"bin/open-plan-annotator.
|
|
26
|
-
"install.
|
|
25
|
+
"bin/open-plan-annotator.mjs",
|
|
26
|
+
"install.mjs",
|
|
27
|
+
"shared/",
|
|
27
28
|
".claude-plugin/",
|
|
28
29
|
"opencode/",
|
|
29
30
|
"hooks/",
|
|
@@ -31,14 +32,15 @@
|
|
|
31
32
|
"README.md"
|
|
32
33
|
],
|
|
33
34
|
"scripts": {
|
|
34
|
-
"postinstall": "node install.
|
|
35
|
+
"postinstall": "node install.mjs",
|
|
35
36
|
"test": "bun test",
|
|
36
37
|
"typecheck": "tsgo --noEmit --project ui/tsconfig.json && tsgo --noEmit --project server/tsconfig.json",
|
|
37
38
|
"build:ui": "cd ui && bun run vite build",
|
|
38
|
-
"build:platforms": "node scripts/build-platforms.
|
|
39
|
-
"build": "bun run build:ui && node scripts/build-platforms.
|
|
40
|
-
"tarball": "node scripts/tarball.
|
|
41
|
-
"release": "bun run build && node scripts/tarball.
|
|
39
|
+
"build:platforms": "node scripts/build-platforms.mjs",
|
|
40
|
+
"build": "bun run build:ui && node scripts/build-platforms.mjs",
|
|
41
|
+
"tarball": "node scripts/tarball.mjs",
|
|
42
|
+
"release": "bun run build && node scripts/tarball.mjs",
|
|
43
|
+
"pack:check": "node scripts/check-package-files.mjs",
|
|
42
44
|
"dev:ui": "cd ui && bun run vite --port 5173",
|
|
43
45
|
"dev:server": "NODE_ENV=development bun run server/index.ts",
|
|
44
46
|
"dev": "NODE_ENV=development bun run server/index.ts & cd ui && bun run vite --port 5173",
|
|
@@ -46,16 +48,16 @@
|
|
|
46
48
|
"lint:fix": "biome check --write .",
|
|
47
49
|
"format": "biome format --write .",
|
|
48
50
|
"do-release": "./scripts/release.sh",
|
|
49
|
-
"prepack": "
|
|
50
|
-
"postpack": "
|
|
51
|
+
"prepack": "node scripts/claude-pack-docs.mjs prepack",
|
|
52
|
+
"postpack": "node scripts/claude-pack-docs.mjs postpack"
|
|
51
53
|
},
|
|
52
54
|
"devDependencies": {
|
|
53
55
|
"@biomejs/biome": "^2.4.4",
|
|
56
|
+
"@types/node": "^25.3.0",
|
|
54
57
|
"@types/bun": "^1.3.9",
|
|
55
58
|
"@typescript/native-preview": "^7.0.0-dev.20260224.1"
|
|
56
59
|
},
|
|
57
60
|
"dependencies": {
|
|
58
|
-
"@opencode-ai/plugin": "^1.2.14"
|
|
59
|
-
"@types/node": "^25.3.0"
|
|
61
|
+
"@opencode-ai/plugin": "^1.2.14"
|
|
60
62
|
}
|
|
61
63
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ReleaseAsset {
|
|
2
|
+
name: string;
|
|
3
|
+
browser_download_url: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export declare const REPO: string;
|
|
7
|
+
export declare const PLATFORM_ASSET_BASENAME_MAP: Record<string, string>;
|
|
8
|
+
|
|
9
|
+
export declare function getPlatformKey(platform?: string, arch?: string): string;
|
|
10
|
+
export declare function getPlatformAssetArchiveName(platformKey?: string): string | null;
|
|
11
|
+
export declare function parseChecksumManifest(manifestText: string): Map<string, string>;
|
|
12
|
+
export declare function selectChecksumAsset(assets: ReleaseAsset[]): ReleaseAsset | null;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const REPO = "ndom91/open-plan-annotator";
|
|
2
|
+
|
|
3
|
+
const PLATFORM_ASSET_BASENAME_MAP = {
|
|
4
|
+
"darwin-arm64": "open-plan-annotator-darwin-arm64",
|
|
5
|
+
"darwin-x64": "open-plan-annotator-darwin-x64",
|
|
6
|
+
"linux-x64": "open-plan-annotator-linux-x64",
|
|
7
|
+
"linux-arm64": "open-plan-annotator-linux-arm64",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function getPlatformKey(platform = process.platform, arch = process.arch) {
|
|
11
|
+
return `${platform}-${arch}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getPlatformAssetArchiveName(platformKey = getPlatformKey()) {
|
|
15
|
+
const assetBaseName = PLATFORM_ASSET_BASENAME_MAP[platformKey];
|
|
16
|
+
if (!assetBaseName) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return `${assetBaseName}.tar.gz`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseChecksumManifest(manifestText) {
|
|
23
|
+
const checksums = new Map();
|
|
24
|
+
|
|
25
|
+
for (const rawLine of manifestText.split(/\r?\n/)) {
|
|
26
|
+
const line = rawLine.trim();
|
|
27
|
+
if (!line || line.startsWith("#")) continue;
|
|
28
|
+
|
|
29
|
+
const bsdStyle = line.match(/^SHA256\s*\(([^)]+)\)\s*=\s*([a-fA-F0-9]{64})$/);
|
|
30
|
+
if (bsdStyle) {
|
|
31
|
+
checksums.set(bsdStyle[1].trim(), bsdStyle[2].toLowerCase());
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const gnuStyle = line.match(/^([a-fA-F0-9]{64})\s+[* ]?(.+)$/);
|
|
36
|
+
if (gnuStyle) {
|
|
37
|
+
checksums.set(gnuStyle[2].trim(), gnuStyle[1].toLowerCase());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return checksums;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function selectChecksumAsset(assets) {
|
|
45
|
+
const checksumAssets = assets
|
|
46
|
+
.filter((asset) => {
|
|
47
|
+
const lower = asset.name.toLowerCase();
|
|
48
|
+
return (
|
|
49
|
+
(lower.includes("sha256") || lower.includes("checksum")) &&
|
|
50
|
+
(lower.endsWith(".txt") || lower.endsWith(".sha256") || lower.endsWith(".sha256sum") || lower.endsWith(".sha256sums"))
|
|
51
|
+
);
|
|
52
|
+
})
|
|
53
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
54
|
+
|
|
55
|
+
return checksumAssets[0] ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export {
|
|
59
|
+
REPO,
|
|
60
|
+
PLATFORM_ASSET_BASENAME_MAP,
|
|
61
|
+
getPlatformKey,
|
|
62
|
+
getPlatformAssetArchiveName,
|
|
63
|
+
parseChecksumManifest,
|
|
64
|
+
selectChecksumAsset,
|
|
65
|
+
};
|
package/install.cjs
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// Skip postinstall during local development
|
|
4
|
-
if (process.env.OPEN_PLAN_ANNOTATOR_SKIP_INSTALL || process.env.npm_config_dev) {
|
|
5
|
-
process.exit(0);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
const fs = require("fs");
|
|
9
|
-
const path = require("path");
|
|
10
|
-
const zlib = require("zlib");
|
|
11
|
-
const https = require("https");
|
|
12
|
-
const crypto = require("crypto");
|
|
13
|
-
|
|
14
|
-
const VERSION = require("./package.json").version;
|
|
15
|
-
const REPO = "ndom91/open-plan-annotator";
|
|
16
|
-
|
|
17
|
-
const PLATFORM_MAP = {
|
|
18
|
-
"darwin-arm64": "open-plan-annotator-darwin-arm64",
|
|
19
|
-
"darwin-x64": "open-plan-annotator-darwin-x64",
|
|
20
|
-
"linux-x64": "open-plan-annotator-linux-x64",
|
|
21
|
-
"linux-arm64": "open-plan-annotator-linux-arm64",
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
function getPlatformKey() {
|
|
25
|
-
return `${process.platform}-${process.arch}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function getDownloadUrl() {
|
|
29
|
-
const key = getPlatformKey();
|
|
30
|
-
const asset = PLATFORM_MAP[key];
|
|
31
|
-
if (!asset) {
|
|
32
|
-
console.error(`open-plan-annotator: unsupported platform ${key}`);
|
|
33
|
-
console.error(`Supported: ${Object.keys(PLATFORM_MAP).join(", ")}`);
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
return `https://github.com/${REPO}/releases/download/v${VERSION}/${asset}.tar.gz`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function getReleaseApiUrl() {
|
|
40
|
-
return `https://api.github.com/repos/${REPO}/releases/tags/v${VERSION}`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function fetch(url, redirects) {
|
|
44
|
-
if (redirects === undefined) redirects = 5;
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
https
|
|
47
|
-
.get(url, { headers: { "User-Agent": "open-plan-annotator-install" } }, (res) => {
|
|
48
|
-
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
49
|
-
if (redirects <= 0) return reject(new Error("Too many redirects"));
|
|
50
|
-
return fetch(res.headers.location, redirects - 1).then(resolve, reject);
|
|
51
|
-
}
|
|
52
|
-
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
53
|
-
return reject(new Error(`HTTP ${res.statusCode} from ${url}`));
|
|
54
|
-
}
|
|
55
|
-
const chunks = [];
|
|
56
|
-
res.on("data", (c) => chunks.push(c));
|
|
57
|
-
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
58
|
-
})
|
|
59
|
-
.on("error", reject);
|
|
60
|
-
});
|
|
61
|
-
}
|
|
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
|
-
|
|
142
|
-
function extractBinaryFromTarGz(buffer) {
|
|
143
|
-
const tarBuffer = zlib.gunzipSync(buffer);
|
|
144
|
-
let offset = 0;
|
|
145
|
-
|
|
146
|
-
while (offset < tarBuffer.length) {
|
|
147
|
-
const header = tarBuffer.subarray(offset, offset + 512);
|
|
148
|
-
offset += 512;
|
|
149
|
-
|
|
150
|
-
const name = header.toString("utf-8", 0, 100).replace(/\0.*/g, "");
|
|
151
|
-
const sizeStr = header.toString("utf-8", 124, 136).replace(/\0.*/g, "").trim();
|
|
152
|
-
const size = parseInt(sizeStr, 8);
|
|
153
|
-
|
|
154
|
-
if (!name || isNaN(size)) break;
|
|
155
|
-
|
|
156
|
-
if (name === "open-plan-annotator" || name.endsWith("/open-plan-annotator")) {
|
|
157
|
-
return tarBuffer.subarray(offset, offset + size);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
offset += Math.ceil(size / 512) * 512;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
throw new Error("Binary 'open-plan-annotator' not found in archive");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
async function main() {
|
|
167
|
-
const destDir = path.join(__dirname, "bin");
|
|
168
|
-
const destPath = path.join(destDir, "open-plan-annotator-binary");
|
|
169
|
-
const tempPath = `${destPath}.tmp-${process.pid}-${Date.now()}`;
|
|
170
|
-
|
|
171
|
-
// Skip if binary already exists
|
|
172
|
-
if (fs.existsSync(destPath)) {
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const fallbackUrl = getDownloadUrl();
|
|
177
|
-
console.error(`Downloading open-plan-annotator for ${getPlatformKey()}...`);
|
|
178
|
-
|
|
179
|
-
let archiveBuffer;
|
|
180
|
-
|
|
181
|
-
// Try checksum-verified download via GitHub API first, fall back to direct URL
|
|
182
|
-
try {
|
|
183
|
-
const { assetName, assetUrl, expectedSha256 } = await resolveReleaseAssetAndChecksum();
|
|
184
|
-
archiveBuffer = await fetch(assetUrl);
|
|
185
|
-
const actualSha256 = sha256Hex(archiveBuffer);
|
|
186
|
-
|
|
187
|
-
if (actualSha256 !== expectedSha256) {
|
|
188
|
-
throw new Error(
|
|
189
|
-
`Checksum verification failed for ${assetName} (expected ${expectedSha256}, got ${actualSha256})`,
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
} catch (verifiedErr) {
|
|
193
|
-
const message = verifiedErr && verifiedErr.message ? verifiedErr.message : String(verifiedErr);
|
|
194
|
-
console.error(`open-plan-annotator: checksum-verified install failed: ${message}`);
|
|
195
|
-
console.error(`Falling back to direct download (without checksum verification)...`);
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
archiveBuffer = await fetch(fallbackUrl);
|
|
199
|
-
} catch (fallbackErr) {
|
|
200
|
-
const fbMsg = fallbackErr && fallbackErr.message ? fallbackErr.message : String(fallbackErr);
|
|
201
|
-
console.error(`open-plan-annotator: fallback download also failed: ${fbMsg}`);
|
|
202
|
-
throw verifiedErr;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
const binaryBuffer = extractBinaryFromTarGz(archiveBuffer);
|
|
208
|
-
|
|
209
|
-
if (!fs.existsSync(destDir)) {
|
|
210
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
fs.writeFileSync(tempPath, binaryBuffer, { mode: 0o755 });
|
|
214
|
-
fs.renameSync(tempPath, destPath);
|
|
215
|
-
fs.chmodSync(destPath, 0o755);
|
|
216
|
-
console.error(`Installed open-plan-annotator to ${destPath}`);
|
|
217
|
-
} catch (err) {
|
|
218
|
-
try {
|
|
219
|
-
fs.unlinkSync(tempPath);
|
|
220
|
-
} catch {
|
|
221
|
-
// Temp file may not exist
|
|
222
|
-
}
|
|
223
|
-
throw err;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
main().catch((err) => {
|
|
228
|
-
console.error("Failed to install open-plan-annotator binary:", err.message);
|
|
229
|
-
console.error("You can try manually running: node", path.join(__dirname, "install.cjs"));
|
|
230
|
-
process.exit(1);
|
|
231
|
-
});
|