open-plan-annotator 1.7.0 → 1.8.0
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,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "Interactive plan annotation plugin for Claude Code",
|
|
8
|
-
"version": "1.
|
|
8
|
+
"version": "1.8.0"
|
|
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.
|
|
15
|
+
"version": "1.8.0",
|
|
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.
|
|
4
|
+
"version": "1.8.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "ndom91"
|
|
7
7
|
},
|
package/hooks/hooks.json
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/install-runtime.mjs\"",
|
|
10
|
+
"timeout": 60
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
3
15
|
"PermissionRequest": [
|
|
4
16
|
{
|
|
5
17
|
"matcher": "ExitPlanMode",
|
|
6
18
|
"hooks": [
|
|
7
19
|
{
|
|
8
20
|
"type": "command",
|
|
9
|
-
"command": "open-plan-annotator
|
|
21
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/bin/open-plan-annotator.mjs\"",
|
|
10
22
|
"timeout": 345600
|
|
11
23
|
}
|
|
12
24
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "open-plan-annotator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fully local plugin for interactive plan annotation from your Agentic assistants",
|
|
6
6
|
"author": "ndom91",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"bin/open-plan-annotator.mjs",
|
|
30
|
-
"
|
|
30
|
+
"scripts/install-runtime.mjs",
|
|
31
31
|
"shared/cliHelp.mjs",
|
|
32
32
|
"shared/cliMode.mjs",
|
|
33
33
|
"shared/packageManager.mjs",
|
|
@@ -84,10 +84,10 @@
|
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
86
|
"optionalDependencies": {
|
|
87
|
-
"@open-plan-annotator/runtime-darwin-arm64": "1.
|
|
88
|
-
"@open-plan-annotator/runtime-darwin-x64": "1.
|
|
89
|
-
"@open-plan-annotator/runtime-linux-arm64": "1.
|
|
90
|
-
"@open-plan-annotator/runtime-linux-x64": "1.
|
|
87
|
+
"@open-plan-annotator/runtime-darwin-arm64": "1.8.0",
|
|
88
|
+
"@open-plan-annotator/runtime-darwin-x64": "1.8.0",
|
|
89
|
+
"@open-plan-annotator/runtime-linux-arm64": "1.8.0",
|
|
90
|
+
"@open-plan-annotator/runtime-linux-x64": "1.8.0"
|
|
91
91
|
},
|
|
92
92
|
"overrides": {
|
|
93
93
|
"uuid": "^14.0.0"
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Installs the per-platform Bun-compiled runtime binary for open-plan-annotator
|
|
3
|
+
// from the npm registry. Intended to run as a Claude Code SessionStart hook.
|
|
4
|
+
//
|
|
5
|
+
// Behavior:
|
|
6
|
+
// 1. Determines plugin version from the sibling package.json.
|
|
7
|
+
// 2. Picks the runtime package matching the host platform/arch.
|
|
8
|
+
// 3. If the target binary already exists at the resolver's fallback path
|
|
9
|
+
// (`packages/runtime-<platform>-<arch>/bin/open-plan-annotator`), exits 0.
|
|
10
|
+
// 4. Otherwise fetches the package manifest from the npm registry,
|
|
11
|
+
// downloads the tarball, verifies its integrity against `dist.integrity`,
|
|
12
|
+
// extracts it via system `tar`, and atomically moves the binary into place.
|
|
13
|
+
//
|
|
14
|
+
// Logs go to stderr only — stdout is reserved for hook protocol output.
|
|
15
|
+
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import {
|
|
19
|
+
chmodSync,
|
|
20
|
+
existsSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
mkdtempSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
renameSync,
|
|
25
|
+
rmSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
} from "node:fs";
|
|
28
|
+
import { get as httpsGet } from "node:https";
|
|
29
|
+
import { tmpdir } from "node:os";
|
|
30
|
+
import { dirname, join, resolve } from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
|
|
33
|
+
const SUPPORTED_PLATFORMS = new Set([
|
|
34
|
+
"darwin-arm64",
|
|
35
|
+
"darwin-x64",
|
|
36
|
+
"linux-arm64",
|
|
37
|
+
"linux-x64",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
const pluginRoot = resolve(here, "..");
|
|
42
|
+
|
|
43
|
+
const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8"));
|
|
44
|
+
const version = pkg.version;
|
|
45
|
+
|
|
46
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
47
|
+
if (!SUPPORTED_PLATFORMS.has(platformKey)) {
|
|
48
|
+
process.stderr.write(
|
|
49
|
+
`open-plan-annotator: unsupported platform ${platformKey}. Supported: ${[...SUPPORTED_PLATFORMS].join(", ")}\n`,
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const runtimePackage = `@open-plan-annotator/runtime-${platformKey}`;
|
|
55
|
+
const targetDir = join(pluginRoot, "packages", `runtime-${platformKey}`, "bin");
|
|
56
|
+
const targetBinary = join(targetDir, "open-plan-annotator");
|
|
57
|
+
|
|
58
|
+
if (existsSync(targetBinary)) {
|
|
59
|
+
// Fast path: nothing to do.
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
process.stderr.write(
|
|
64
|
+
`open-plan-annotator: installing runtime ${runtimePackage}@${version} for ${platformKey}…\n`,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const start = Date.now();
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const manifest = await fetchJson(
|
|
71
|
+
`https://registry.npmjs.org/${encodeURIComponent(runtimePackage)}/${encodeURIComponent(version)}`,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const tarballUrl = manifest?.dist?.tarball;
|
|
75
|
+
const integrity = manifest?.dist?.integrity;
|
|
76
|
+
if (typeof tarballUrl !== "string" || typeof integrity !== "string") {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`npm manifest for ${runtimePackage}@${version} missing dist.tarball or dist.integrity`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const tarballBuf = await fetchBuffer(tarballUrl);
|
|
83
|
+
verifyIntegrity(tarballBuf, integrity, runtimePackage, version);
|
|
84
|
+
|
|
85
|
+
const tmp = mkdtempSync(join(tmpdir(), "opa-runtime-"));
|
|
86
|
+
try {
|
|
87
|
+
const tarPath = join(tmp, "runtime.tgz");
|
|
88
|
+
writeFileSync(tarPath, tarballBuf);
|
|
89
|
+
execFileSync("tar", ["-xzf", tarPath, "-C", tmp], { stdio: "ignore" });
|
|
90
|
+
|
|
91
|
+
// npm tarballs always extract to a `package/` directory.
|
|
92
|
+
const extractedBinary = join(tmp, "package", "bin", "open-plan-annotator");
|
|
93
|
+
if (!existsSync(extractedBinary)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`expected ${extractedBinary} after extracting ${runtimePackage}@${version}, not found`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
mkdirSync(targetDir, { recursive: true });
|
|
100
|
+
// renameSync may fail across filesystems; tmpdir is usually on the same
|
|
101
|
+
// device as the plugin cache, but if not, fall back to copy+unlink.
|
|
102
|
+
try {
|
|
103
|
+
renameSync(extractedBinary, targetBinary);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (err?.code === "EXDEV") {
|
|
106
|
+
const buf = readFileSync(extractedBinary);
|
|
107
|
+
writeFileSync(targetBinary, buf);
|
|
108
|
+
} else {
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
chmodSync(targetBinary, 0o755);
|
|
113
|
+
} finally {
|
|
114
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const elapsedMs = Date.now() - start;
|
|
118
|
+
process.stderr.write(
|
|
119
|
+
`open-plan-annotator: installed ${targetBinary} (${elapsedMs}ms)\n`,
|
|
120
|
+
);
|
|
121
|
+
process.exit(0);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
124
|
+
process.stderr.write(
|
|
125
|
+
`open-plan-annotator: failed to install runtime ${runtimePackage}@${version}: ${message}\n`,
|
|
126
|
+
);
|
|
127
|
+
process.stderr.write(
|
|
128
|
+
`open-plan-annotator: rerun a Claude Code session to retry; ExitPlanMode will not work until install succeeds.\n`,
|
|
129
|
+
);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Fetch a URL with redirect handling and return the response buffer.
|
|
135
|
+
*
|
|
136
|
+
* @param {string} url
|
|
137
|
+
* @param {number} [maxRedirects]
|
|
138
|
+
* @returns {Promise<Buffer>}
|
|
139
|
+
*/
|
|
140
|
+
function fetchBuffer(url, maxRedirects = 5) {
|
|
141
|
+
return new Promise((resolveFetch, rejectFetch) => {
|
|
142
|
+
httpsGet(
|
|
143
|
+
url,
|
|
144
|
+
{
|
|
145
|
+
headers: {
|
|
146
|
+
"user-agent": `open-plan-annotator-installer/${version}`,
|
|
147
|
+
accept: "*/*",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
(res) => {
|
|
151
|
+
const status = res.statusCode ?? 0;
|
|
152
|
+
if ((status === 301 || status === 302 || status === 307 || status === 308) && res.headers.location) {
|
|
153
|
+
if (maxRedirects <= 0) {
|
|
154
|
+
rejectFetch(new Error(`too many redirects for ${url}`));
|
|
155
|
+
res.resume();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const next = new URL(res.headers.location, url).toString();
|
|
159
|
+
res.resume();
|
|
160
|
+
fetchBuffer(next, maxRedirects - 1).then(resolveFetch, rejectFetch);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (status !== 200) {
|
|
164
|
+
rejectFetch(new Error(`GET ${url} -> HTTP ${status}`));
|
|
165
|
+
res.resume();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const chunks = [];
|
|
169
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
170
|
+
res.on("end", () => resolveFetch(Buffer.concat(chunks)));
|
|
171
|
+
res.on("error", rejectFetch);
|
|
172
|
+
},
|
|
173
|
+
).on("error", rejectFetch);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {string} url
|
|
179
|
+
* @returns {Promise<unknown>}
|
|
180
|
+
*/
|
|
181
|
+
async function fetchJson(url) {
|
|
182
|
+
const buf = await fetchBuffer(url);
|
|
183
|
+
return JSON.parse(buf.toString("utf8"));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Verify a buffer against an npm Subresource Integrity string (e.g. "sha512-…").
|
|
188
|
+
*
|
|
189
|
+
* @param {Buffer} buf
|
|
190
|
+
* @param {string} integrity
|
|
191
|
+
* @param {string} pkgName
|
|
192
|
+
* @param {string} pkgVersion
|
|
193
|
+
*/
|
|
194
|
+
function verifyIntegrity(buf, integrity, pkgName, pkgVersion) {
|
|
195
|
+
const match = integrity.match(/^(sha\d+)-([A-Za-z0-9+/=]+)$/);
|
|
196
|
+
if (!match) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`unrecognized integrity format "${integrity}" for ${pkgName}@${pkgVersion}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const [, algo, expectedB64] = match;
|
|
202
|
+
const actualB64 = createHash(algo).update(buf).digest("base64");
|
|
203
|
+
if (actualB64 !== expectedB64) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`integrity mismatch for ${pkgName}@${pkgVersion}\n expected ${algo}: ${expectedB64}\n actual ${algo}: ${actualB64}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
|
|
6
7
|
export const RUNTIME_PACKAGE_MAP = {
|
|
@@ -19,12 +20,45 @@ export function getRuntimePackageName(platform = process.platform, arch = proces
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function resolveRuntimeBinary(options = {}) {
|
|
23
|
+
const attempt = () => tryResolveRuntimeBinary(options);
|
|
24
|
+
|
|
25
|
+
const first = attempt();
|
|
26
|
+
if (first.ok) return first.value;
|
|
27
|
+
|
|
28
|
+
// Skip installer for hard-failure conditions (unsupported platform): no
|
|
29
|
+
// amount of installing will produce a binary, and we'd waste a network
|
|
30
|
+
// round trip downloading the host's binary unnecessarily.
|
|
31
|
+
const platform = options.platform ?? process.platform;
|
|
32
|
+
const arch = options.arch ?? process.arch;
|
|
33
|
+
if (!getRuntimePackageName(platform, arch)) {
|
|
34
|
+
throw first.error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Last-resort recovery: invoke the SessionStart installer synchronously and
|
|
38
|
+
// retry resolution once. Useful for hosts that fetch this package without
|
|
39
|
+
// running `npm install` (and therefore skip the optionalDependencies that
|
|
40
|
+
// would otherwise pull the matching runtime binary). The Claude Code plugin
|
|
41
|
+
// install path is the canonical example, but it triggers the same script
|
|
42
|
+
// earlier via its SessionStart hook; this branch covers everyone else.
|
|
43
|
+
if (runInstaller(options)) {
|
|
44
|
+
const second = attempt();
|
|
45
|
+
if (second.ok) return second.value;
|
|
46
|
+
throw second.error;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
throw first.error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tryResolveRuntimeBinary(options = {}) {
|
|
22
53
|
const platform = options.platform ?? process.platform;
|
|
23
54
|
const arch = options.arch ?? process.arch;
|
|
24
55
|
const packageName = getRuntimePackageName(platform, arch);
|
|
25
56
|
|
|
26
57
|
if (!packageName) {
|
|
27
|
-
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: new Error(`Unsupported platform ${getRuntimePlatformKey(platform, arch)}`),
|
|
61
|
+
};
|
|
28
62
|
}
|
|
29
63
|
|
|
30
64
|
const requireFrom = createRequire(options.parentUrl ?? import.meta.url);
|
|
@@ -37,27 +71,66 @@ export function resolveRuntimeBinary(options = {}) {
|
|
|
37
71
|
const workspaceBinaryPath = path.join(workspaceRoot, "packages", packageName.split("/").at(-1) ?? "", "bin", "open-plan-annotator");
|
|
38
72
|
if (fs.existsSync(workspaceBinaryPath)) {
|
|
39
73
|
return {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
74
|
+
ok: true,
|
|
75
|
+
value: {
|
|
76
|
+
packageName,
|
|
77
|
+
packageRoot: path.dirname(path.dirname(workspaceBinaryPath)),
|
|
78
|
+
binaryPath: workspaceBinaryPath,
|
|
79
|
+
},
|
|
43
80
|
};
|
|
44
81
|
}
|
|
45
82
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: new Error(
|
|
86
|
+
`Missing runtime package ${packageName}. Reinstall open-plan-annotator for ${getRuntimePlatformKey(platform, arch)}.`,
|
|
87
|
+
),
|
|
88
|
+
};
|
|
49
89
|
}
|
|
50
90
|
|
|
51
91
|
const packageRoot = path.dirname(packageJsonPath);
|
|
52
92
|
const binaryPath = path.join(packageRoot, "bin", "open-plan-annotator");
|
|
53
93
|
|
|
54
94
|
if (!fs.existsSync(binaryPath)) {
|
|
55
|
-
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
error: new Error(`Runtime package ${packageName} is installed but ${binaryPath} is missing. Rebuild or reinstall it.`),
|
|
98
|
+
};
|
|
56
99
|
}
|
|
57
100
|
|
|
58
101
|
return {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
102
|
+
ok: true,
|
|
103
|
+
value: {
|
|
104
|
+
packageName,
|
|
105
|
+
packageRoot,
|
|
106
|
+
binaryPath,
|
|
107
|
+
},
|
|
62
108
|
};
|
|
63
109
|
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Run `scripts/install-runtime.mjs` synchronously to lay down the per-platform
|
|
113
|
+
* runtime binary. Returns true if the installer ran successfully, false if it
|
|
114
|
+
* was unavailable or failed. Diagnostics go to stderr; this function never
|
|
115
|
+
* throws so a failure here falls through to the original missing-runtime
|
|
116
|
+
* error from the caller.
|
|
117
|
+
*/
|
|
118
|
+
function runInstaller(_options = {}) {
|
|
119
|
+
// Opt-out for callers that want to keep the old behavior (e.g. tests that
|
|
120
|
+
// assert the error path) or environments where re-entry would be harmful.
|
|
121
|
+
if (process.env.OPEN_PLAN_ANNOTATOR_SKIP_INSTALL === "1") return false;
|
|
122
|
+
|
|
123
|
+
const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
124
|
+
const installerPath = path.join(workspaceRoot, "scripts", "install-runtime.mjs");
|
|
125
|
+
if (!fs.existsSync(installerPath)) return false;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
execFileSync(process.execPath, [installerPath], {
|
|
129
|
+
stdio: ["ignore", "ignore", "inherit"],
|
|
130
|
+
timeout: 60_000,
|
|
131
|
+
});
|
|
132
|
+
return true;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
Binary file
|