open-plan-annotator 0.2.16 → 0.2.17

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": "0.2.16",
15
+ "version": "0.2.17",
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": "0.2.16",
4
+ "version": "0.2.17",
5
5
  "author": {
6
6
  "name": "ndom91"
7
7
  },
@@ -175,7 +175,7 @@ export async function runPlanReview(options) {
175
175
 
176
176
  const payload = buildHookPayload(options);
177
177
 
178
- const result = await new Promise((resolve, reject) => {
178
+ const output = await new Promise((resolve, reject) => {
179
179
  let cwd = options.cwd ?? process.cwd();
180
180
 
181
181
  // Guard: ensure cwd is a directory, not a file
@@ -187,19 +187,39 @@ export async function runPlanReview(options) {
187
187
  cwd = PKG_ROOT;
188
188
  }
189
189
 
190
- // Spawn the compiled binary directly (skip the Node wrapper).
191
- // This avoids issues with bun vs node runtime differences.
190
+ // Spawn detached so the binary can outlive this call — it keeps its
191
+ // HTTP server alive for ~10s after emitting the JSON hook response.
192
192
  const child = spawn(BINARY_PATH, [], {
193
193
  cwd,
194
194
  stdio: ["pipe", "pipe", "pipe"],
195
195
  env: process.env,
196
+ detached: true,
196
197
  });
197
198
 
198
199
  let stdout = "";
199
200
  let stderr = "";
201
+ let resolved = false;
200
202
 
201
203
  child.stdout.on("data", (chunk) => {
202
204
  stdout += String(chunk);
205
+ if (resolved) return;
206
+
207
+ // Scan for a complete JSON hook-output line. Once found, resolve
208
+ // immediately and let the binary keep running in the background.
209
+ const lines = stdout.split("\n");
210
+ for (const line of lines) {
211
+ const trimmed = line.trim();
212
+ if (!trimmed) continue;
213
+ try {
214
+ const parsed = validateHookOutput(JSON.parse(trimmed));
215
+ resolved = true;
216
+ child.unref();
217
+ resolve(parsed);
218
+ return;
219
+ } catch {
220
+ // Not valid hook JSON yet, keep buffering
221
+ }
222
+ }
203
223
  });
204
224
 
205
225
  child.stderr.on("data", (chunk) => {
@@ -207,40 +227,38 @@ export async function runPlanReview(options) {
207
227
  });
208
228
 
209
229
  child.on("error", (error) => {
210
- reject(error);
230
+ if (!resolved) reject(error);
211
231
  });
212
232
 
213
233
  child.on("close", (code, signal) => {
214
- resolve({ code, signal, stdout, stderr });
234
+ if (resolved) return;
235
+ // Binary exited without producing valid JSON
236
+ if (signal) {
237
+ reject(
238
+ new Error(
239
+ stderr.trim()
240
+ ? `open-plan-annotator was terminated by signal ${signal}: ${stderr.trim()}`
241
+ : `open-plan-annotator was terminated by signal ${signal}`,
242
+ ),
243
+ );
244
+ } else if (code !== 0) {
245
+ reject(
246
+ new Error(
247
+ stderr.trim()
248
+ ? `open-plan-annotator exited with code ${code}: ${stderr.trim()}`
249
+ : `open-plan-annotator exited with code ${code}`,
250
+ ),
251
+ );
252
+ } else {
253
+ reject(new Error("open-plan-annotator exited without producing hook output"));
254
+ }
215
255
  });
216
256
 
217
257
  child.stdin.write(`${JSON.stringify(payload)}\n`);
218
258
  child.stdin.end();
219
259
  });
220
260
 
221
- const settled =
222
- /** @type {{ code: number | null, signal: NodeJS.Signals | null, stdout: string, stderr: string }} */ (result);
223
-
224
- if (settled.signal) {
225
- const errorText = settled.stderr.trim();
226
- throw new Error(
227
- errorText
228
- ? `open-plan-annotator was terminated by signal ${settled.signal}: ${errorText}`
229
- : `open-plan-annotator was terminated by signal ${settled.signal}`,
230
- );
231
- }
232
-
233
- if (settled.code !== 0) {
234
- const errorText = settled.stderr.trim();
235
- throw new Error(
236
- errorText
237
- ? `open-plan-annotator exited with code ${settled.code}: ${errorText}`
238
- : `open-plan-annotator exited with code ${settled.code}`,
239
- );
240
- }
241
-
242
- const output = parseHookOutput(settled.stdout, settled.stderr);
243
- const decision = output.hookSpecificOutput.decision;
261
+ const decision = /** @type {HookOutput} */ (output).hookSpecificOutput.decision;
244
262
 
245
263
  if (decision.behavior === "allow") {
246
264
  return { approved: true };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-plan-annotator",
3
- "version": "0.2.16",
3
+ "version": "0.2.17",
4
4
  "type": "module",
5
5
  "description": "Fully local plugin for interactive plan annotation from your Agentic assistants",
6
6
  "author": "ndom91",