plan-review 1.1.4 → 1.1.6

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/dist/index.js CHANGED
@@ -1,30 +1,30 @@
1
1
  #!/usr/bin/env node
2
- import{Command as rt}from"commander";import{readFileSync as ge,mkdirSync as st,copyFileSync as at}from"node:fs";import{existsSync as ve}from"node:fs";import{homedir as ct}from"node:os";import{dirname as lt,join as N,resolve as ut}from"node:path";import{fileURLToPath as mt}from"node:url";import p from"chalk";function L(e,t="auto"){let n=e.split(`
3
- `),i=we(n),o=Se(n);return t==="auto"?ye(e)?Ce(e,i,o):M(e,i,o):t==="separator"?E(e,i,o):M(e,i,o)}function we(e){let t=e.find(n=>/^# /.test(n));return t?t.replace(/^# /,"").trim():"Untitled"}function Se(e){let t={};for(let n of e.slice(0,20)){let i=n.match(/^\*\*(\w[\w\s]*?):\*\*\s*(.+)/);i&&(t[i[1].trim()]=i[2].trim())}return t}function ye(e){let t=e.replace(/```[\s\S]*?```/g,""),n=/^## /m.test(t)&&/^### /m.test(t),i=/\*\*Depends On:\*\*/m.test(t)||/\*\*Blocks:\*\*/m.test(t)||/\*\*Verification:\*\*/m.test(t)||/\*\*Related Files:\*\*/m.test(t);return n&&i}function M(e,t,n){let i=be(e);return i.length===0?E(e,t,n):{title:t,metadata:n,mode:"generic",sections:i.map((o,r)=>({id:`section-${r+1}`,heading:o.heading,level:o.level,body:o.body})),comments:[]}}function be(e){let t=e.split(`
4
- `),n=[],i="",o=0,r=[],s=(e.match(/^## /gm)||[]).length,a=(e.match(/^### /gm)||[]).length,c=s>0?2:a>0?3:0;if(c===0)return[];let u=new RegExp(`^${"#".repeat(c)} (.+)`),l=!1;for(let m of t){if(m.startsWith("```")){l=!l,i&&r.push(m);continue}if(l){i&&r.push(m);continue}let f=m.match(u);f?(i&&n.push({heading:i,level:o,body:r.join(`
2
+ import{Command as it}from"commander";import{readFileSync as M,mkdirSync as rt,copyFileSync as st}from"node:fs";import{existsSync as N}from"node:fs";import{homedir as at}from"node:os";import{dirname as lt,join as L,resolve as ct}from"node:path";import{fileURLToPath as ut}from"node:url";import p from"chalk";function _(t,e="auto"){let n=t.split(`
3
+ `),i=he(n),o=ge(n);return e==="auto"?ve(t)?Se(t,i,o):B(t,i,o):e==="separator"?J(t,i,o):B(t,i,o)}function he(t){let e=t.find(n=>/^# /.test(n));return e?e.replace(/^# /,"").trim():"Untitled"}function ge(t){let e={};for(let n of t.slice(0,20)){let i=n.match(/^\*\*(\w[\w\s]*?):\*\*\s*(.+)/);i&&(e[i[1].trim()]=i[2].trim())}return e}function ve(t){let e=t.replace(/```[\s\S]*?```/g,""),n=/^## /m.test(e)&&/^### /m.test(e),i=/\*\*Depends On:\*\*/m.test(e)||/\*\*Blocks:\*\*/m.test(e)||/\*\*Verification:\*\*/m.test(e)||/\*\*Related Files:\*\*/m.test(e);return n&&i}function B(t,e,n){let i=we(t);return i.length===0?J(t,e,n):{title:e,metadata:n,mode:"generic",sections:i.map((o,r)=>({id:`section-${r+1}`,heading:o.heading,level:o.level,body:o.body})),comments:[]}}function we(t){let e=t.split(`
4
+ `),n=[],i="",o=0,r=[],s=(t.match(/^## /gm)||[]).length,a=(t.match(/^### /gm)||[]).length,c=s>0?2:a>0?3:0;if(c===0)return[];let l=new RegExp(`^${"#".repeat(c)} (.+)`),u=!1;for(let m of e){if(m.startsWith("```")){u=!u,i&&r.push(m);continue}if(u){i&&r.push(m);continue}let f=m.match(l);f?(i&&n.push({heading:i,level:o,body:r.join(`
5
5
  `).trim()}),i=f[1].trim(),o=c,r=[]):i&&r.push(m)}return i&&n.push({heading:i,level:o,body:r.join(`
6
- `).trim()}),n}function E(e,t,n){let i=e.split(/\n---\n/).filter(o=>o.trim().length>=5);return i.length<=1?{title:t,metadata:n,mode:"generic",sections:[{id:"section-1",heading:t,level:1,body:e.trim()}],comments:[]}:{title:t,metadata:n,mode:"generic",sections:i.map((o,r)=>{let a=o.trim().split(`
7
- `)[0].replace(/^#+\s*/,"").trim();return{id:`section-${r+1}`,heading:a||`Section ${r+1}`,level:2,body:o.trim()}}),comments:[]}}function Ce(e,t,n){let i=e.split(`
8
- `),o=[],r=0,s=0,a="",c="",u=0,l=[],m=!1;function f(){if(!c)return;let h=l.join(`
9
- `).trim();if(u===2){r++,s=0,a=`milestone-${r}`,o.push({id:a,heading:c,level:2,body:h});return}s++;let d=`${r}.${s}`;o.push({id:d,heading:c,level:3,body:h,parent:a,dependencies:$e(h),relatedFiles:Te(h),verification:xe(h)})}for(let h of i){if(h.startsWith("```")){m=!m,l.push(h);continue}if(m){l.push(h);continue}let d=h.match(/^## (.+)/),R=h.match(/^### (.+)/);d?(f(),c=d[1].trim(),u=2,l=[]):R?(f(),c=R[1].trim(),u=3,l=[]):l.push(h)}return f(),{title:t,metadata:n,mode:"plan",sections:o,comments:[]}}function $e(e){let t=e.match(/\*\*Depends On:\*\*\s*(.+)/),n=e.match(/\*\*Blocks:\*\*\s*(.+)/),i=o=>{let r=o.trim();return r==="(none)"||r===""?[]:r.split(/,\s*/).map(s=>s.trim())};return{dependsOn:t?i(t[1]):[],blocks:n?i(n[1]):[]}}function Te(e){let t=[],n=e.split(`
10
- `),i=!1;for(let o of n){if(/\*\*Related Files:\*\*/.test(o)){i=!0;continue}if(i){let r=o.match(/^- `(.+)`(.*)$/);if(r){let s=r[2].trim();t.push(s?`${r[1]} ${s}`:r[1])}else(o.trim()===""||/^\*\*/.test(o.trim()))&&(i=!1)}}return t}function xe(e){let t=e.match(/\*\*Verification:\*\*\s*`(.+?)`/);return t?t[1]:void 0}import{createHash as J}from"node:crypto";import{mkdirSync as Re,readFileSync as H,writeFileSync as He,unlinkSync as _,readdirSync as Pe,existsSync as U}from"node:fs";import{join as P,resolve as ke}from"node:path";import{homedir as Ie}from"node:os";function Oe(e){let t=ke(e);return J("sha256").update(t).digest("hex").slice(0,16)}function k(e){return P(y(),Oe(e)+".json")}function y(){let e=P(Ie(),".plan-review","sessions");return Re(e,{recursive:!0}),e}function I(e){return`sha256:${J("sha256").update(e).digest("hex")}`}function b(e,t,n,i){try{let o={version:1,planPath:e,contentHash:t,comments:n,activeSection:i,lastModified:new Date().toISOString()},r=k(e);He(r,JSON.stringify(o,null,2),"utf-8")}catch(o){console.warn(`[plan-review] Failed to save session: ${o.message}`)}}function W(e,t){let n=k(e);if(!U(n))return null;let i;try{i=H(n,"utf-8")}catch{return null}let o;try{o=JSON.parse(i)}catch{console.warn(`[plan-review] Corrupt session file, removing: ${n}`);try{_(n)}catch{}return null}return{comments:o.comments.map(s=>({...s,timestamp:new Date(s.timestamp)})),activeSection:o.activeSection,stale:o.contentHash!==t}}function C(e){let t=k(e);try{_(t)}catch{}}function z(){let e=y(),t;try{t=Pe(e)}catch{return[]}let n=[];for(let i of t){if(!i.endsWith(".json"))continue;let o=P(e,i),r;try{let a=H(o,"utf-8");r=JSON.parse(a)}catch{console.warn(`[plan-review] Skipping corrupt session file: ${o}`);continue}let s;if(!U(r.planPath))s=null;else try{let a=H(r.planPath,"utf-8");s=I(a)!==r.contentHash}catch{s=null}n.push({planPath:r.planPath,commentCount:r.comments.length,lastModified:r.lastModified,stale:s})}return n}function O(e){return e.replace(/([\\*_`~\[\]#>|])/g,"\\$1")}function De(e){return[...e].sort((t,n)=>{let i=t.anchor?.startLine??1/0,o=n.anchor?.startLine??1/0;return i-o})}function Fe(e){return e==="approved"?"Approved":"Comment"}function V(e,t){let n=new Set(e.comments.map(a=>a.sectionId)),i=e.sections.filter(a=>e.mode==="plan"?a.level===3:a.level>=2),o=i.filter(a=>n.has(a.id)),r=[];r.push(`# Plan Review: ${e.title}`),r.push(""),r.push("## Review Summary"),r.push(`- **Verdict:** ${Fe(t.verdict)}`),r.push(`- **Sections reviewed:** ${o.length}/${i.length}`),r.push(`- **Comments:** ${e.comments.length}`);let s=i.length-o.length;r.push(`- **Skipped:** ${s} section${s===1?"":"s"} without comments`),t.summary.trim()!==""&&(r.push(""),r.push("## Overall Comments"),r.push(""),r.push(O(t.summary))),o.length>0&&(r.push(""),r.push("---"));for(let a of o){let c=De(e.comments.filter(u=>u.sectionId===a.id));if(r.push(""),r.push(`## Section ${a.id}: ${a.heading}`),r.push(""),e.mode==="plan"&&a.dependencies){let u=a.dependencies;u.dependsOn.length>0&&r.push(`Depends on: ${u.dependsOn.join(", ")}`),u.blocks.length>0&&r.push(`Blocks: ${u.blocks.join(", ")}`),r.push("")}for(let u of c){if(u.anchor){r.push("### Reviewer Comment"),r.push("");for(let l of u.anchor.lineTexts)r.push(`> ${l}`);r.push(""),r.push(O(u.text))}else r.push("### Reviewer Comment (entire section)"),r.push(""),r.push(O(u.text));r.push(""),r.push("---")}}return r.join(`
11
- `)}import*as X from"node:readline";import w from"chalk";import{marked as Y}from"marked";import{markedTerminal as je}from"marked-terminal";import g from"chalk";Y.use(je());function q(e){let t=[];e.level===3&&e.dependencies&&(t.push(Ae(e)),t.push(""));let n=`${"#".repeat(e.level)} ${e.heading}`,i=e.body||"",o=`${n}
6
+ `).trim()}),n}function J(t,e,n){let i=t.split(/\n---\n/).filter(o=>o.trim().length>=5);return i.length<=1?{title:e,metadata:n,mode:"generic",sections:[{id:"section-1",heading:e,level:1,body:t.trim()}],comments:[]}:{title:e,metadata:n,mode:"generic",sections:i.map((o,r)=>{let a=o.trim().split(`
7
+ `)[0].replace(/^#+\s*/,"").trim();return{id:`section-${r+1}`,heading:a||`Section ${r+1}`,level:2,body:o.trim()}}),comments:[]}}function Se(t,e,n){let i=t.split(`
8
+ `),o=[],r=0,s=0,a="",c="",l=0,u=[],m=!1;function f(){if(!c)return;let h=u.join(`
9
+ `).trim();if(l===2){r++,s=0,a=`milestone-${r}`,o.push({id:a,heading:c,level:2,body:h});return}s++;let d=`${r}.${s}`;o.push({id:d,heading:c,level:3,body:h,parent:a,dependencies:ye(h),relatedFiles:be(h),verification:Ce(h)})}for(let h of i){if(h.startsWith("```")){m=!m,u.push(h);continue}if(m){u.push(h);continue}let d=h.match(/^## (.+)/),w=h.match(/^### (.+)/);d?(f(),c=d[1].trim(),l=2,u=[]):w?(f(),c=w[1].trim(),l=3,u=[]):u.push(h)}return f(),{title:e,metadata:n,mode:"plan",sections:o,comments:[]}}function ye(t){let e=t.match(/\*\*Depends On:\*\*\s*(.+)/),n=t.match(/\*\*Blocks:\*\*\s*(.+)/),i=o=>{let r=o.trim();return r==="(none)"||r===""?[]:r.split(/,\s*/).map(s=>s.trim())};return{dependsOn:e?i(e[1]):[],blocks:n?i(n[1]):[]}}function be(t){let e=[],n=t.split(`
10
+ `),i=!1;for(let o of n){if(/\*\*Related Files:\*\*/.test(o)){i=!0;continue}if(i){let r=o.match(/^- `(.+)`(.*)$/);if(r){let s=r[2].trim();e.push(s?`${r[1]} ${s}`:r[1])}else(o.trim()===""||/^\*\*/.test(o.trim()))&&(i=!1)}}return e}function Ce(t){let e=t.match(/\*\*Verification:\*\*\s*`(.+?)`/);return e?e[1]:void 0}function I(t){return t.replace(/([\\*_`~\[\]#>|])/g,"\\$1")}function Te(t){return[...t].sort((e,n)=>{let i=e.anchor?.startLine??1/0,o=n.anchor?.startLine??1/0;return i-o})}function Re(t){return t==="approved"?"Approved":"Comment"}function U(t,e){let n=new Set(t.comments.map(a=>a.sectionId)),i=t.sections.filter(a=>t.mode==="plan"?a.level===3:a.level>=2),o=i.filter(a=>n.has(a.id)),r=[];r.push(`# Plan Review: ${t.title}`),r.push(""),r.push("## Review Summary"),r.push(`- **Verdict:** ${Re(e.verdict)}`),r.push(`- **Sections reviewed:** ${o.length}/${i.length}`),r.push(`- **Comments:** ${t.comments.length}`);let s=i.length-o.length;r.push(`- **Skipped:** ${s} section${s===1?"":"s"} without comments`),e.summary.trim()!==""&&(r.push(""),r.push("## Overall Comments"),r.push(""),r.push(I(e.summary))),o.length>0&&(r.push(""),r.push("---"));for(let a of o){let c=Te(t.comments.filter(l=>l.sectionId===a.id));if(r.push(""),r.push(`## Section ${a.id}: ${a.heading}`),r.push(""),t.mode==="plan"&&a.dependencies){let l=a.dependencies;l.dependsOn.length>0&&r.push(`Depends on: ${l.dependsOn.join(", ")}`),l.blocks.length>0&&r.push(`Blocks: ${l.blocks.join(", ")}`),r.push("")}for(let l of c){if(l.anchor){r.push("### Reviewer Comment"),r.push("");for(let u of l.anchor.lineTexts)r.push(`> ${u}`);r.push(""),r.push(I(l.text))}else r.push("### Reviewer Comment (entire section)"),r.push(""),r.push(I(l.text));r.push(""),r.push("---")}}return r.join(`
11
+ `)}import{createHash as V}from"node:crypto";import{existsSync as $e}from"node:fs";import{mkdir as Pe,readFile as W,readdir as xe,unlink as O,writeFile as He}from"node:fs/promises";import{homedir as ke}from"node:os";import{join as D}from"node:path";var y=D(ke(),".plan-review","sessions");function R(t){return`sha256:${V("sha256").update(t).digest("hex")}`}function z(t){if(typeof t!="object"||t===null)return!1;let e=t;return typeof e.version=="number"&&typeof e.planPath=="string"&&typeof e.contentHash=="string"&&Array.isArray(e.comments)&&(typeof e.activeSection=="string"||e.activeSection===null)&&typeof e.lastModified=="string"}var S=class{dir;keyHashLength;constructor(e){this.dir=e.dir,this.keyHashLength=e.keyHashLength??16}filePath(e){let n=V("sha256").update(e).digest("hex").slice(0,this.keyHashLength);return D(this.dir,`${n}.json`)}async save(e,n){await Pe(this.dir,{recursive:!0}),await He(this.filePath(e),JSON.stringify(n,null,2),"utf-8")}async load(e){let n=this.filePath(e);if(!$e(n))return null;let i;try{i=await W(n,"utf-8")}catch{return null}let o;try{o=JSON.parse(i)}catch{console.warn(`[plan-review] Corrupt session file, removing: ${n}`);try{await O(n)}catch{}return null}if(!z(o)){console.warn(`[plan-review] Malformed session file, removing: ${n}`);try{await O(n)}catch{}return null}return{...o,comments:o.comments.map(r=>({...r,timestamp:new Date(r.timestamp)}))}}async clear(e){try{await O(this.filePath(e))}catch{}}async list(){let e;try{e=await xe(this.dir)}catch{return[]}let n=[];for(let i of e){if(!i.endsWith(".json"))continue;let o=D(this.dir,i);try{let r=JSON.parse(await W(o,"utf-8"));if(!z(r)){console.warn(`[plan-review] Skipping malformed session file: ${o}`);continue}n.push({key:r.planPath,commentCount:r.comments.length,lastModified:r.lastModified})}catch{console.warn(`[plan-review] Skipping corrupt session file: ${o}`)}}return n}};import*as X from"node:readline";import b from"chalk";import{marked as Y}from"marked";import{markedTerminal as Ie}from"marked-terminal";import g from"chalk";Y.use(Ie());function q(t){let e=[];t.level===3&&t.dependencies&&(e.push(Oe(t)),e.push(""));let n=`${"#".repeat(t.level)} ${t.heading}`,i=t.body||"",o=`${n}
12
12
 
13
- ${i}`;return t.push(Y.parse(o)),t.join(`
14
- `)}function Ae(e){let t=e.dependencies,n=t.dependsOn.length>0?t.dependsOn.join(", "):"(none)",i=t.blocks.length>0?t.blocks.join(", "):"(none)",o=[`Task ${e.id}: ${e.heading}`,`\u2190 Depends on: ${n}`,`\u2192 Blocks: ${i}`];if(e.relatedFiles&&e.relatedFiles.length>0){let m=e.relatedFiles.length<=2?e.relatedFiles.join(", "):`${e.relatedFiles[0]} (+${e.relatedFiles.length-1} more)`;o.push(`Files: ${m}`)}e.verification&&o.push(`Verify: ${e.verification}`);let r=Math.max(...o.map(m=>m.length)),a=Math.min(r+4,process.stdout.columns||80)-2,c=g.dim(`\u250C${"\u2500".repeat(a)}\u2510`),u=g.dim(`\u2514${"\u2500".repeat(a)}\u2518`),l=o.map(m=>g.dim("\u2502")+" "+g.cyan(m.slice(0,a-2).padEnd(a-2))+" "+g.dim("\u2502"));return[c,...l,u].join(`
15
- `)}function G(e){let t=[],n=new Set(e.comments.map(s=>s.sectionId));if(t.push(""),t.push(g.bold.underline(e.title)),t.push(""),e.mode==="plan"){for(let s of e.sections)if(s.level===2)t.push(g.bold.yellow(` ${s.heading}`));else if(s.level===3){let a=n.has(s.id)?g.green("\u2713"):" ";t.push(` ${a} ${g.dim(s.id)} ${s.heading}`)}}else{let s=e.sections.filter(a=>a.level>=2);for(let a=0;a<s.length;a++){let c=s[a],u=String(a+1).padStart(2),l=n.has(c.id)?g.green("\u2713"):" ";t.push(` ${l} ${g.dim(u)} ${c.heading}`)}}let i=n.size,r=e.sections.filter(s=>e.mode==="plan"?s.level===3:s.level>=2).length-i;return t.push(""),t.push(` ${g.green(`${i} section${i!==1?"s":""} commented`)} ${g.dim(`${r} remaining`)}`),t.push(""),t.join(`
16
- `)}async function Z(e,t=!1,n){let i=t?(await import("node:fs")).createReadStream("/dev/tty"):process.stdin,o=X.createInterface({input:i,output:process.stderr}),r=c=>new Promise(u=>{o.question(c,l=>u(l.trim()))}),s=D(e),a=!0;for(;a;){console.error(G(e));let c=await r(w.cyan("> Enter section (e.g. 1.1), 'all' for linear review, or 'done' to finish: "));if(c==="done"||c==="q")a=!1;else if(c==="all")await K(e,s,r,n);else{let u=Be(e,c);if(u){let l=s.indexOf(u);await K(e,s.slice(l),r,n)}else console.error(w.red(`Section "${c}" not found. Try again.`))}}return o.close(),Ne(e),e}async function K(e,t,n,i){for(let o=0;o<t.length;o++){let r=t[o];console.error(q(r));let s=await n(w.cyan("> Comment (enter to skip, 'toc' for menu, 'back' for previous): "));if(s==="toc")return;if(s==="back"){o-=o>0?2:1;continue}else s!==""&&(e.comments.push({sectionId:r.id,text:s,timestamp:new Date}),i?.())}}function Be(e,t){let n=e.sections.find(o=>o.id===t);if(n)return n;let i=parseInt(t,10);if(!isNaN(i)){let o=D(e);if(i>=1&&i<=o.length)return o[i-1]}}function D(e){return e.sections.filter(t=>e.mode==="plan"?t.level===3:t.level>=2)}function Ne(e){let t=D(e),n=new Set(e.comments.map(i=>i.sectionId));console.error(""),console.error(w.bold("Review Summary")),console.error(` Sections: ${t.length}`),console.error(` Commented: ${w.green(String(n.size))}`),console.error(` Skipped: ${w.dim(String(t.length-n.size))}`),console.error(` Total comments: ${e.comments.length}`),console.error("")}import{execSync as Q,spawn as Me}from"node:child_process";import{writeFileSync as Le}from"node:fs";import{resolve as F}from"node:path";import v from"chalk";function ee(e,t,n={}){switch(t){case"stdout":process.stdout.write(e+`
17
- `);break;case"clipboard":Ee(e);break;case"file":Je(e,n.outputFile,n.inputFile);break;case"claude":_e(e);break}}function Ee(e){let t=Ue(process.platform);if(!t){console.error(v.yellow("Clipboard not supported on this platform. Falling back to stdout.")),process.stdout.write(e+`
18
- `);return}try{Q(t,{input:e,stdio:["pipe","ignore","ignore"]}),console.error(v.green("Review copied to clipboard."))}catch{console.error(v.yellow("Failed to copy to clipboard. Falling back to stdout.")),process.stdout.write(e+`
19
- `)}}function Je(e,t,n){let i=t?F(t):n?F(n.replace(/\.md$/,".review.md")):F("review.md");try{Le(i,e,"utf-8"),console.error(v.green(`Review written to ${i}`))}catch(o){let r=o instanceof Error?o.message:String(o);console.error(v.red(`Failed to write file: ${r}`)),console.error(v.yellow("Falling back to stdout.")),process.stdout.write(e+`
20
- `)}}function _e(e){if(!$()){console.error(v.red("Claude CLI not found in PATH.")),console.error(v.dim("Install: https://docs.anthropic.com/en/docs/claude-code")),console.error(v.yellow("Falling back to stdout.")),process.stdout.write(e+`
21
- `);return}let t=Me("claude",[],{stdio:["pipe","inherit","inherit"]});t.stdin.write(e),t.stdin.end(),t.on("error",n=>{console.error(v.yellow(`Failed to pipe to claude: ${n.message}. Falling back to stdout.`)),process.stdout.write(e+`
22
- `)})}function Ue(e){switch(e){case"darwin":return"pbcopy";case"linux":return"xclip -selection clipboard";case"win32":return"clip";default:return null}}function $(){try{return Q("which claude",{stdio:"ignore"}),!0}catch{return!1}}import{createRequire as dt}from"node:module";import*as j from"node:readline";import te from"chalk";async function ne(e){return e?(await import("node:fs")).createReadStream("/dev/tty"):process.stdin}async function oe(e){let t=j.createInterface({input:await ne(e),output:process.stderr}),n=await new Promise(i=>{t.question(te.cyan("> Output: (s)tdout, (c)lipboard, (f)ile, cl(a)ude? "),o=>i(o.trim().toLowerCase()))});switch(t.close(),n){case"s":case"stdout":return"stdout";case"c":case"clipboard":return"clipboard";case"f":case"file":return"file";case"a":case"claude":return"claude";default:return"stdout"}}async function ie(e,t){let n=j.createInterface({input:await ne(t),output:process.stderr}),i=await new Promise(o=>{n.question(te.yellow(`${e} (y/n) `),r=>o(r.trim().toLowerCase()))});return n.close(),i==="y"||i==="yes"}import{spawnSync as tt}from"node:child_process";import{dirname as nt}from"node:path";import{createServer as Ke}from"node:http";import{readFile as We}from"node:fs";import{join as ze,normalize as Ve,resolve as re,sep as Ye,extname as qe}from"node:path";var T=1024*1024,se={".gif":"image/gif",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".svg":"image/svg+xml",".webp":"image/webp",".avif":"image/avif",".ico":"image/x-icon"};function ae(e){if(typeof e!="object"||e===null)return!1;let t=e;return typeof t.sectionId=="string"&&typeof t.text=="string"}function Ge(e){return e==="approved"||e===null}function ce(e){return(t,n)=>{let{method:i,url:o}=t;if(i==="GET"&&o==="/"){let r=e.getAssetHtml();n.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),n.end(r);return}if(i==="GET"&&o==="/api/doc"){let r=e.getDocument(),s={activeSection:e.getInitialActiveSection?.()??null};n.writeHead(200,{"Content-Type":"application/json"}),n.end(JSON.stringify({document:r,initialState:s}));return}if(i==="POST"&&o==="/api/review"){let r="",s=0;t.on("data",a=>{if(s+=a.length,s>T){n.writeHead(413,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Request body too large"})),t.destroy();return}r+=a.toString()}),t.on("end",()=>{if(!(s>T))try{let a=JSON.parse(r),c=a.comments,u=a.verdict,l=a.summary;if(!Array.isArray(c)){n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"comments must be an array"}));return}if(!Ge(u)){n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:'verdict must be "approved" or null'}));return}if(typeof l!="string"){n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"summary must be a string"}));return}for(let m of c)if(!ae(m)){n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Each comment must have sectionId (string) and text (string)"}));return}e.onSubmit({comments:c,verdict:u,summary:l}),n.writeHead(200,{"Content-Type":"application/json"}),n.end(JSON.stringify({success:!0}))}catch{n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Invalid JSON"}))}});return}if(i==="PUT"&&o==="/api/session"){let r="",s=0;t.on("data",a=>{if(s+=a.length,s>T){n.writeHead(413,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Request body too large"})),t.destroy();return}r+=a.toString()}),t.on("end",()=>{if(!(s>T))try{let a=JSON.parse(r),c=a.comments;if(!Array.isArray(c)){n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"comments must be an array"}));return}for(let l of c)if(!ae(l)){n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Each comment must have sectionId (string) and text (string)"}));return}let u=typeof a.activeSection=="string"?a.activeSection:null;e.onSessionSave?.(c,u),n.writeHead(200,{"Content-Type":"application/json"}),n.end(JSON.stringify({success:!0}))}catch{n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Invalid JSON"}))}});return}if(i==="POST"&&o==="/api/heartbeat"){e.onHeartbeat?.(),n.writeHead(204),n.end();return}if(i==="POST"&&o==="/api/pause"){e.onPause?.(),n.writeHead(204),n.end();return}if(i==="POST"&&o==="/api/cancel"){e.onCancel?.(),n.writeHead(204),n.end();return}if(i==="GET"&&o&&o.startsWith("/_assets/")){let r=e.getAssetBaseDir?.();if(!r){n.writeHead(404,{"Content-Type":"text/plain"}),n.end("No asset base directory");return}let s;try{s=decodeURIComponent(o.slice(9).split("?")[0].split("#")[0])}catch{n.writeHead(400,{"Content-Type":"text/plain"}),n.end("Bad request");return}let a=qe(s).toLowerCase();if(!se[a]){n.writeHead(415,{"Content-Type":"text/plain"}),n.end("Unsupported media type");return}let c=Ve(s),u=re(r),l=re(ze(u,c));if(!l.startsWith(u+Ye)&&l!==u){n.writeHead(403,{"Content-Type":"text/plain"}),n.end("Forbidden");return}We(l,(m,f)=>{if(m){n.writeHead(404,{"Content-Type":"text/plain"}),n.end("Not Found");return}n.writeHead(200,{"Content-Type":se[a],"Content-Length":f.length,"Cache-Control":"no-store"}),n.end(f)});return}n.writeHead(404,{"Content-Type":"text/plain"}),n.end("Not Found")}}function le(e){return Ke(ce(e))}function ue(e,t){return new Promise((n,i)=>{e.on("error",i),e.listen(t,()=>{let o=e.address(),r=typeof o=="object"&&o?o.port:t;n({url:`http://localhost:${r}`})})})}function me(e){return new Promise((t,n)=>{e.close(i=>i?n(i):t()),e.closeAllConnections()})}import{readFileSync as Xe,existsSync as de}from"node:fs";import{join as A,dirname as Ze}from"node:path";import{fileURLToPath as Qe}from"node:url";var pe=Ze(Qe(import.meta.url));function et(){let e=A(pe,"..","browser","index.html");if(de(e))return e;let t=A(pe,"..",".."),n=A(t,"dist","browser","index.html");if(de(n))return n;throw new Error(`Browser HTML not found. Run 'npm run build' first.
13
+ ${i}`;return e.push(Y.parse(o)),e.join(`
14
+ `)}function Oe(t){let e=t.dependencies,n=e.dependsOn.length>0?e.dependsOn.join(", "):"(none)",i=e.blocks.length>0?e.blocks.join(", "):"(none)",o=[`Task ${t.id}: ${t.heading}`,`\u2190 Depends on: ${n}`,`\u2192 Blocks: ${i}`];if(t.relatedFiles&&t.relatedFiles.length>0){let m=t.relatedFiles.length<=2?t.relatedFiles.join(", "):`${t.relatedFiles[0]} (+${t.relatedFiles.length-1} more)`;o.push(`Files: ${m}`)}t.verification&&o.push(`Verify: ${t.verification}`);let r=Math.max(...o.map(m=>m.length)),a=Math.min(r+4,process.stdout.columns||80)-2,c=g.dim(`\u250C${"\u2500".repeat(a)}\u2510`),l=g.dim(`\u2514${"\u2500".repeat(a)}\u2518`),u=o.map(m=>g.dim("\u2502")+" "+g.cyan(m.slice(0,a-2).padEnd(a-2))+" "+g.dim("\u2502"));return[c,...u,l].join(`
15
+ `)}function G(t){let e=[],n=new Set(t.comments.map(s=>s.sectionId));if(e.push(""),e.push(g.bold.underline(t.title)),e.push(""),t.mode==="plan"){for(let s of t.sections)if(s.level===2)e.push(g.bold.yellow(` ${s.heading}`));else if(s.level===3){let a=n.has(s.id)?g.green("\u2713"):" ";e.push(` ${a} ${g.dim(s.id)} ${s.heading}`)}}else{let s=t.sections.filter(a=>a.level>=2);for(let a=0;a<s.length;a++){let c=s[a],l=String(a+1).padStart(2),u=n.has(c.id)?g.green("\u2713"):" ";e.push(` ${u} ${g.dim(l)} ${c.heading}`)}}let i=n.size,r=t.sections.filter(s=>t.mode==="plan"?s.level===3:s.level>=2).length-i;return e.push(""),e.push(` ${g.green(`${i} section${i!==1?"s":""} commented`)} ${g.dim(`${r} remaining`)}`),e.push(""),e.join(`
16
+ `)}async function Q(t,e=!1,n){let i=e?(await import("node:fs")).createReadStream("/dev/tty"):process.stdin,o=X.createInterface({input:i,output:process.stderr}),r=c=>new Promise(l=>{o.question(c,u=>l(u.trim()))}),s=F(t),a=!0;for(;a;){console.error(G(t));let c=await r(b.cyan("> Enter section (e.g. 1.1), 'all' for linear review, or 'done' to finish: "));if(c==="done"||c==="q")a=!1;else if(c==="all")await K(t,s,r,n);else{let l=De(t,c);if(l){let u=s.indexOf(l);await K(t,s.slice(u),r,n)}else console.error(b.red(`Section "${c}" not found. Try again.`))}}return o.close(),Fe(t),t}async function K(t,e,n,i){for(let o=0;o<e.length;o++){let r=e[o];console.error(q(r));let s=await n(b.cyan("> Comment (enter to skip, 'toc' for menu, 'back' for previous): "));if(s==="toc")return;if(s==="back"){o-=o>0?2:1;continue}else s!==""&&(t.comments.push({sectionId:r.id,text:s,timestamp:new Date}),i?.())}}function De(t,e){let n=t.sections.find(o=>o.id===e);if(n)return n;let i=parseInt(e,10);if(!isNaN(i)){let o=F(t);if(i>=1&&i<=o.length)return o[i-1]}}function F(t){return t.sections.filter(e=>t.mode==="plan"?e.level===3:e.level>=2)}function Fe(t){let e=F(t),n=new Set(t.comments.map(i=>i.sectionId));console.error(""),console.error(b.bold("Review Summary")),console.error(` Sections: ${e.length}`),console.error(` Commented: ${b.green(String(n.size))}`),console.error(` Skipped: ${b.dim(String(e.length-n.size))}`),console.error(` Total comments: ${t.comments.length}`),console.error("")}import{execSync as Z,spawn as Ae}from"node:child_process";import{writeFileSync as je}from"node:fs";import{resolve as A}from"node:path";import v from"chalk";function ee(t,e,n={}){switch(e){case"stdout":process.stdout.write(t+`
17
+ `);break;case"clipboard":Ee(t);break;case"file":Le(t,n.outputFile,n.inputFile);break;case"claude":Me(t);break}}function Ee(t){let e=Ne(process.platform);if(!e){console.error(v.yellow("Clipboard not supported on this platform. Falling back to stdout.")),process.stdout.write(t+`
18
+ `);return}try{Z(e,{input:t,stdio:["pipe","ignore","ignore"]}),console.error(v.green("Review copied to clipboard."))}catch{console.error(v.yellow("Failed to copy to clipboard. Falling back to stdout.")),process.stdout.write(t+`
19
+ `)}}function Le(t,e,n){let i=e?A(e):n?A(n.replace(/\.md$/,".review.md")):A("review.md");try{je(i,t,"utf-8"),console.error(v.green(`Review written to ${i}`))}catch(o){let r=o instanceof Error?o.message:String(o);console.error(v.red(`Failed to write file: ${r}`)),console.error(v.yellow("Falling back to stdout.")),process.stdout.write(t+`
20
+ `)}}function Me(t){if(!$()){console.error(v.red("Claude CLI not found in PATH.")),console.error(v.dim("Install: https://docs.anthropic.com/en/docs/claude-code")),console.error(v.yellow("Falling back to stdout.")),process.stdout.write(t+`
21
+ `);return}let e=Ae("claude",[],{stdio:["pipe","inherit","inherit"]});e.stdin.write(t),e.stdin.end(),e.on("error",n=>{console.error(v.yellow(`Failed to pipe to claude: ${n.message}. Falling back to stdout.`)),process.stdout.write(t+`
22
+ `)})}function Ne(t){switch(t){case"darwin":return"pbcopy";case"linux":return"xclip -selection clipboard";case"win32":return"clip";default:return null}}function $(){try{return Z("which claude",{stdio:"ignore"}),!0}catch{return!1}}import{createRequire as mt}from"node:module";import*as j from"node:readline";import te from"chalk";async function ne(t){return t?(await import("node:fs")).createReadStream("/dev/tty"):process.stdin}async function oe(t){let e=j.createInterface({input:await ne(t),output:process.stderr}),n=await new Promise(i=>{e.question(te.cyan("> Output: (s)tdout, (c)lipboard, (f)ile, cl(a)ude? "),o=>i(o.trim().toLowerCase()))});switch(e.close(),n){case"s":case"stdout":return"stdout";case"c":case"clipboard":return"clipboard";case"f":case"file":return"file";case"a":case"claude":return"claude";default:return"stdout"}}async function ie(t,e){let n=j.createInterface({input:await ne(e),output:process.stderr}),i=await new Promise(o=>{n.question(te.yellow(`${t} (y/n) `),r=>o(r.trim().toLowerCase()))});return n.close(),i==="y"||i==="yes"}import{spawnSync as Qe}from"node:child_process";import{dirname as Ze}from"node:path";import{createServer as Ve}from"node:http";import{readFile as Be}from"node:fs";import{join as _e,normalize as Je,resolve as re,sep as Ue,extname as We}from"node:path";var P=1024*1024,se={".gif":"image/gif",".png":"image/png",".jpg":"image/jpeg",".jpeg":"image/jpeg",".svg":"image/svg+xml",".webp":"image/webp",".avif":"image/avif",".ico":"image/x-icon"};function ae(t){if(typeof t!="object"||t===null)return!1;let e=t;return typeof e.sectionId=="string"&&typeof e.text=="string"}function ze(t){return t==="approved"||t===null}function le(t){return(e,n)=>{let{method:i,url:o}=e;if(i==="GET"&&o==="/"){let r=t.getAssetHtml();n.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),n.end(r);return}if(i==="GET"&&o==="/api/doc"){let r=t.getDocument(),s={activeSection:t.getInitialActiveSection?.()??null};n.writeHead(200,{"Content-Type":"application/json"}),n.end(JSON.stringify({document:r,initialState:s}));return}if(i==="POST"&&o==="/api/review"){let r="",s=0;e.on("data",a=>{if(s+=a.length,s>P){n.writeHead(413,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Request body too large"})),e.destroy();return}r+=a.toString()}),e.on("end",()=>{if(!(s>P))try{let a=JSON.parse(r),c=a.comments,l=a.verdict,u=a.summary;if(!Array.isArray(c)){n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"comments must be an array"}));return}if(!ze(l)){n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:'verdict must be "approved" or null'}));return}if(typeof u!="string"){n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"summary must be a string"}));return}for(let m of c)if(!ae(m)){n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Each comment must have sectionId (string) and text (string)"}));return}t.onSubmit({comments:c,verdict:l,summary:u}),n.writeHead(200,{"Content-Type":"application/json"}),n.end(JSON.stringify({success:!0}))}catch{n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Invalid JSON"}))}});return}if(i==="PUT"&&o==="/api/session"){let r="",s=0;e.on("data",a=>{if(s+=a.length,s>P){n.writeHead(413,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Request body too large"})),e.destroy();return}r+=a.toString()}),e.on("end",async()=>{if(s>P)return;let a,c;try{let l=JSON.parse(r);if(a=l.comments,!Array.isArray(a)){n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"comments must be an array"}));return}for(let u of a)if(!ae(u)){n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Each comment must have sectionId (string) and text (string)"}));return}c=typeof l.activeSection=="string"?l.activeSection:null}catch{n.writeHead(400,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Invalid JSON"}));return}try{await t.onSessionSave?.(a,c),n.writeHead(200,{"Content-Type":"application/json"}),n.end(JSON.stringify({success:!0}))}catch{n.writeHead(500,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:"Failed to save session"}))}});return}if(i==="POST"&&o==="/api/heartbeat"){t.onHeartbeat?.(),n.writeHead(204),n.end();return}if(i==="POST"&&o==="/api/pause"){t.onPause?.(),n.writeHead(204),n.end();return}if(i==="POST"&&o==="/api/cancel"){t.onCancel?.(),n.writeHead(204),n.end();return}if(i==="GET"&&o&&o.startsWith("/_assets/")){let r=t.getAssetBaseDir?.();if(!r){n.writeHead(404,{"Content-Type":"text/plain"}),n.end("No asset base directory");return}let s;try{s=decodeURIComponent(o.slice(9).split("?")[0].split("#")[0])}catch{n.writeHead(400,{"Content-Type":"text/plain"}),n.end("Bad request");return}let a=We(s).toLowerCase();if(!se[a]){n.writeHead(415,{"Content-Type":"text/plain"}),n.end("Unsupported media type");return}let c=Je(s),l=re(r),u=re(_e(l,c));if(!u.startsWith(l+Ue)&&u!==l){n.writeHead(403,{"Content-Type":"text/plain"}),n.end("Forbidden");return}Be(u,(m,f)=>{if(m){n.writeHead(404,{"Content-Type":"text/plain"}),n.end("Not Found");return}n.writeHead(200,{"Content-Type":se[a],"Content-Length":f.length,"Cache-Control":"no-store"}),n.end(f)});return}n.writeHead(404,{"Content-Type":"text/plain"}),n.end("Not Found")}}function ce(t){return Ve(le(t))}function ue(t,e){return new Promise((n,i)=>{t.on("error",i),t.listen(e,()=>{let o=t.address(),r=typeof o=="object"&&o?o.port:e;n({url:`http://localhost:${r}`})})})}function me(t){return new Promise((e,n)=>{t.close(i=>i?n(i):e()),t.closeAllConnections()})}import{readFileSync as Ye,existsSync as qe}from"node:fs";import{join as x,dirname as Ge}from"node:path";import{fileURLToPath as Ke}from"node:url";var H=Ge(Ke(import.meta.url));function Xe(){let t=[x(H,"browser","index.html"),x(H,"..","browser","index.html"),x(H,"..","dist","browser","index.html"),x(H,"..","..","dist","browser","index.html")];for(let e of t)if(qe(e))return e;throw new Error(`Browser HTML not found. Run 'npm run build' first.
23
23
  Looked in:
24
- ${e}
25
- ${n}`)}var B=null;function fe(){return B||(B=Xe(et(),"utf-8")),B}var x=class{doc=null;initialActiveSection=null;assetBaseDir=null;submitHandler=null;sessionSaveHandler=null;heartbeatHandler=null;pauseHandler=null;cancelHandler=null;server=null;sendDocument(t){this.doc=t}setInitialActiveSection(t){this.initialActiveSection=t}setAssetBaseDir(t){this.assetBaseDir=t}onReviewSubmit(t){this.submitHandler=t}onSessionSave(t){this.sessionSaveHandler=t}onHeartbeat(t){this.heartbeatHandler=t}onPause(t){this.pauseHandler=t}onCancel(t){this.cancelHandler=t}async start(t){if(!this.doc)throw new Error("No document set");return this.server=le({getDocument:()=>this.doc,getInitialActiveSection:()=>this.initialActiveSection,getAssetBaseDir:()=>this.assetBaseDir,onSubmit:n=>this.submitHandler?.(n),getAssetHtml:()=>fe(),onSessionSave:(n,i)=>this.sessionSaveHandler?.(n,i),onHeartbeat:()=>this.heartbeatHandler?.(),onPause:()=>this.pauseHandler?.(),onCancel:()=>this.cancelHandler?.()}),ue(this.server,t)}async stop(){this.server&&this.server.listening&&(await me(this.server),this.server=null)}};var ot=1800*1e3,it=30*1e3;async function he({doc:e,absPath:t,contentHash:n,restoredActiveSection:i}){let o=new x;o.sendDocument(e),o.setInitialActiveSection(i),o.setAssetBaseDir(t?nt(t):null),t&&o.onSessionSave((a,c)=>{b(t,n,a,c)});let r=new Promise((a,c)=>{let u=setTimeout(()=>c(new Error("Browser review timed out after 30 minutes of inactivity")),ot),l=null,m=()=>{l&&clearTimeout(l),l=setTimeout(()=>{clearTimeout(u),c(new Error("Review cancelled: browser closed (heartbeat lost)"))},it)},f=()=>{clearTimeout(u),l&&clearTimeout(l),l=null};o.onHeartbeat(m),o.onPause(()=>{l&&clearTimeout(l),l=null}),o.onCancel(()=>{f(),c(new Error("Review cancelled: browser closed"))}),o.onReviewSubmit(h=>{f(),a(h)})}),{url:s}=await o.start(0);process.stderr.write(`Review server running at ${s}
26
- `);try{let a=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";tt(a,[s],{stdio:"ignore"})}catch{process.stderr.write(`Open ${s} in your browser
27
- `)}try{return await r}finally{await o.stop()}}var pt=dt(import.meta.url),{version:ft}=pt("../package.json"),S=new rt;S.name("plan-review").description("Interactive CLI for reviewing AI-generated markdown plans").version(ft).argument("[file]","Path to markdown file (omit to read stdin)").option("-o, --output <target>","Output target: stdout, clipboard, file, claude").option("--output-file <path>","Custom output file path (with --output file)").option("--split-by <strategy>","Force split strategy: heading, separator").option("--fresh","Skip session resume, start clean review").option("--cli","Use the terminal review UI instead of the browser (SSH/CI/headless)").action(async(e,t)=>{try{await ht(e,t)}catch(n){if(n instanceof Error){let i=n.message.startsWith("Review cancelled");console.error(i?p.yellow(n.message):p.red(`Error: ${n.message}`))}process.exit(1)}});S.command("install-skill").description("Install Claude Code skill to ~/.claude/skills/plan-review/").action(()=>{let e=lt(mt(import.meta.url)),t=N(e,"..","skills","plan-review","SKILL.md");ve(t)||(console.error(p.red("Skill file not found in package. Expected at: "+t)),process.exit(1));let n=N(ct(),".claude","skills","plan-review");st(n,{recursive:!0}),at(t,N(n,"SKILL.md")),console.error(p.green(`Skill installed to ${n}/SKILL.md`)),console.error(p.dim('Claude Code will auto-discover it. Try: "I want to review this plan"'))});S.command("sessions").description("List all saved review sessions").action(()=>{let e=z(),t=y();e.length===0&&(console.error(p.dim(`No saved sessions. (${t})`)),process.exit(0)),console.error(p.bold(`Saved review sessions (${t}):
28
- `));for(let n of e){let i=vt(n.lastModified),o="";n.stale===!0?o=p.yellow(" | plan file changed since last review"):n.stale===null&&(o=p.red(" | plan file not found")),console.error(` ${n.planPath}`),console.error(p.dim(` ${n.commentCount} comment${n.commentCount!==1?"s":""} | last modified ${i}${o}
29
- `))}});S.parse();async function ht(e,t){let n=["stdout","clipboard","file","claude"];if(t.output!==void 0){let d=t.output;if(!n.includes(d))throw new Error(`Invalid output target: "${t.output}". Use: ${n.join(", ")}`);d==="claude"&&!$()&&(console.error(p.red("Claude CLI not found in PATH.")),console.error(p.dim("Install: https://docs.anthropic.com/en/docs/claude-code")),console.error(p.yellow("Will fall back to stdout after review.")))}let i=!e&&!process.stdin.isTTY,o=gt(e);o.trim()||(console.error(p.yellow("Empty file, nothing to review.")),process.exit(0));let r=t.splitBy==="heading"?"heading":t.splitBy==="separator"?"separator":"auto",s=L(o,r);console.error(p.dim(`Detected mode: ${s.mode} | ${s.sections.length} sections`));let a=e?ut(e):null,c=I(o),u=null;if(a)if(t.fresh)C(a);else{let d=W(a,c);d&&d.comments.length>0&&(d.stale?await ie(`Plan file changed since last review (${d.comments.length} comment${d.comments.length!==1?"s":""}). Resume anyway?`,i)?(console.error(p.yellow("Resuming with stale session.")),s.comments=d.comments,u=d.activeSection):C(a):(console.error(p.green(`Resuming review (${d.comments.length} comment${d.comments.length!==1?"s":""}).`)),s.comments=d.comments,u=d.activeSection))}let l,m={verdict:null,summary:""};if(t.cli)l=await Z(s,i,a?()=>b(a,c,s.comments,null):void 0);else{let d=await he({doc:s,absPath:a,contentHash:c,restoredActiveSection:u});s.comments=d.comments,m={verdict:d.verdict,summary:d.summary},l=s}a&&C(a);let f;t.output!==void 0?f=t.output:(f=await oe(i),f==="claude"&&!$()&&(console.error(p.red("Claude CLI not found in PATH.")),console.error(p.dim("Install: https://docs.anthropic.com/en/docs/claude-code")),console.error(p.yellow("Falling back to stdout.")),f="stdout"));let h=V(l,m);ee(h,f,{outputFile:t.outputFile,inputFile:e})}function gt(e){if(e){if(!ve(e))throw new Error(`File not found: ${e}`);return ge(e,"utf-8")}return process.stdin.isTTY?(S.help(),""):ge("/dev/stdin","utf-8")}function vt(e){let t=Date.now()-new Date(e).getTime(),n=Math.floor(t/1e3);if(n<60)return"just now";let i=Math.floor(n/60);if(i<60)return`${i}m ago`;let o=Math.floor(i/60);if(o<24)return`${o}h ago`;let r=Math.floor(o/24);return r<7?`${r}d ago`:`${Math.floor(r/7)}w ago`}
24
+ ${t.join(`
25
+ `)}`)}var E=null;function de(){return E||(E=Ye(Xe(),"utf-8")),E}var k=class{doc=null;initialActiveSection=null;assetBaseDir=null;submitHandler=null;sessionSaveHandler=null;heartbeatHandler=null;pauseHandler=null;cancelHandler=null;server=null;sendDocument(e){this.doc=e}setInitialActiveSection(e){this.initialActiveSection=e}setAssetBaseDir(e){this.assetBaseDir=e}onReviewSubmit(e){this.submitHandler=e}onSessionSave(e){this.sessionSaveHandler=e}onHeartbeat(e){this.heartbeatHandler=e}onPause(e){this.pauseHandler=e}onCancel(e){this.cancelHandler=e}async start(e){if(!this.doc)throw new Error("No document set");return this.server=ce({getDocument:()=>this.doc,getInitialActiveSection:()=>this.initialActiveSection,getAssetBaseDir:()=>this.assetBaseDir,onSubmit:n=>this.submitHandler?.(n),getAssetHtml:()=>de(),onSessionSave:(n,i)=>this.sessionSaveHandler?.(n,i),onHeartbeat:()=>this.heartbeatHandler?.(),onPause:()=>this.pauseHandler?.(),onCancel:()=>this.cancelHandler?.()}),ue(this.server,e)}async stop(){this.server&&this.server.listening&&(await me(this.server),this.server=null)}};var et=1800*1e3,tt=30*1e3,nt=new S({dir:y});async function pe({doc:t,absPath:e,contentHash:n,restoredActiveSection:i}){let o=new k;o.sendDocument(t),o.setInitialActiveSection(i),o.setAssetBaseDir(e?Ze(e):null),e&&o.onSessionSave((a,c)=>ot(nt,e,n,a,c));let r=new Promise((a,c)=>{let l=setTimeout(()=>c(new Error("Browser review timed out after 30 minutes of inactivity")),et),u=null,m=()=>{u&&clearTimeout(u),u=setTimeout(()=>{clearTimeout(l),c(new Error("Review cancelled: browser closed (heartbeat lost)"))},tt)},f=()=>{clearTimeout(l),u&&clearTimeout(u),u=null};o.onHeartbeat(m),o.onPause(()=>{u&&clearTimeout(u),u=null}),o.onCancel(()=>{f(),c(new Error("Review cancelled: browser closed"))}),o.onReviewSubmit(h=>{f(),a(h)})}),{url:s}=await o.start(0);process.stderr.write(`Review server running at ${s}
26
+ `);try{let a=process.platform==="darwin"?"open":process.platform==="win32"?"start":"xdg-open";Qe(a,[s],{stdio:"ignore"})}catch{process.stderr.write(`Open ${s} in your browser
27
+ `)}try{return await r}finally{await o.stop()}}async function ot(t,e,n,i,o){let r={version:1,planPath:e,contentHash:n,comments:i,activeSection:o,lastModified:new Date().toISOString()};await t.save(e,r)}var dt=mt(import.meta.url),{version:pt}=dt("../package.json"),T=new it,C=new S({dir:y});T.name("plan-review").description("Interactive CLI for reviewing AI-generated markdown plans").version(pt).argument("[file]","Path to markdown file (omit to read stdin)").option("-o, --output <target>","Output target: stdout, clipboard, file, claude").option("--output-file <path>","Custom output file path (with --output file)").option("--split-by <strategy>","Force split strategy: heading, separator").option("--fresh","Skip session resume, start clean review").option("--cli","Use the terminal review UI instead of the browser (SSH/CI/headless)").action(async(t,e)=>{try{await ft(t,e)}catch(n){if(n instanceof Error){let i=n.message.startsWith("Review cancelled");console.error(i?p.yellow(n.message):p.red(`Error: ${n.message}`))}process.exit(1)}});T.command("install-skill").description("Install Claude Code skill to ~/.claude/skills/plan-review/").action(()=>{let t=lt(ut(import.meta.url)),e=L(t,"..","skills","plan-review","SKILL.md");N(e)||(console.error(p.red("Skill file not found in package. Expected at: "+e)),process.exit(1));let n=L(at(),".claude","skills","plan-review");rt(n,{recursive:!0}),st(e,L(n,"SKILL.md")),console.error(p.green(`Skill installed to ${n}/SKILL.md`)),console.error(p.dim('Claude Code will auto-discover it. Try: "I want to review this plan"'))});T.command("sessions").description("List all saved review sessions").action(async()=>{let t=await St(C),e=y;t.length===0&&(console.error(p.dim(`No saved sessions. (${e})`)),process.exit(0)),console.error(p.bold(`Saved review sessions (${e}):
28
+ `));for(let n of t){let i=gt(n.lastModified),o="";n.stale===!0?o=p.yellow(" | plan file changed since last review"):n.stale===null&&(o=p.red(" | plan file not found")),console.error(` ${n.planPath}`),console.error(p.dim(` ${n.commentCount} comment${n.commentCount!==1?"s":""} | last modified ${i}${o}
29
+ `))}});T.parse();async function ft(t,e){let n=["stdout","clipboard","file","claude"];if(e.output!==void 0){let d=e.output;if(!n.includes(d))throw new Error(`Invalid output target: "${e.output}". Use: ${n.join(", ")}`);d==="claude"&&!$()&&(console.error(p.red("Claude CLI not found in PATH.")),console.error(p.dim("Install: https://docs.anthropic.com/en/docs/claude-code")),console.error(p.yellow("Will fall back to stdout after review.")))}let i=!t&&!process.stdin.isTTY,o=ht(t);o.trim()||(console.error(p.yellow("Empty file, nothing to review.")),process.exit(0));let r=e.splitBy==="heading"?"heading":e.splitBy==="separator"?"separator":"auto",s=_(o,r);console.error(p.dim(`Detected mode: ${s.mode} | ${s.sections.length} sections`));let a=t?ct(t):null,c=R(o),l=null;if(a)if(e.fresh)await C.clear(a);else{let d=await wt(C,a,c);d&&d.comments.length>0&&(d.stale?await ie(`Plan file changed since last review (${d.comments.length} comment${d.comments.length!==1?"s":""}). Resume anyway?`,i)?(console.error(p.yellow("Resuming with stale session.")),s.comments=d.comments,l=d.activeSection):await C.clear(a):(console.error(p.green(`Resuming review (${d.comments.length} comment${d.comments.length!==1?"s":""}).`)),s.comments=d.comments,l=d.activeSection))}let u,m={verdict:null,summary:""};if(e.cli)u=await Q(s,i,a?()=>{vt(C,a,c,s.comments,null).catch(w=>{let fe=w instanceof Error?w.message:String(w);console.warn(p.yellow(`Failed to save session: ${fe}`))})}:void 0);else{let d=await pe({doc:s,absPath:a,contentHash:c,restoredActiveSection:l});s.comments=d.comments,m={verdict:d.verdict,summary:d.summary},u=s}a&&await C.clear(a);let f;e.output!==void 0?f=e.output:(f=await oe(i),f==="claude"&&!$()&&(console.error(p.red("Claude CLI not found in PATH.")),console.error(p.dim("Install: https://docs.anthropic.com/en/docs/claude-code")),console.error(p.yellow("Falling back to stdout.")),f="stdout"));let h=U(u,m);ee(h,f,{outputFile:e.outputFile,inputFile:t})}function ht(t){if(t){if(!N(t))throw new Error(`File not found: ${t}`);return M(t,"utf-8")}return process.stdin.isTTY?(T.help(),""):M("/dev/stdin","utf-8")}function gt(t){let e=Date.now()-new Date(t).getTime(),n=Math.floor(e/1e3);if(n<60)return"just now";let i=Math.floor(n/60);if(i<60)return`${i}m ago`;let o=Math.floor(i/60);if(o<24)return`${o}h ago`;let r=Math.floor(o/24);return r<7?`${r}d ago`:`${Math.floor(r/7)}w ago`}async function vt(t,e,n,i,o){let r={version:1,planPath:e,contentHash:n,comments:i,activeSection:o,lastModified:new Date().toISOString()};await t.save(e,r)}async function wt(t,e,n){let i=await t.load(e);return i?{comments:i.comments,activeSection:i.activeSection,stale:i.contentHash!==n}:null}async function St(t){let e=await t.list(),n=[];for(let i of e){let o=await t.load(i.key),r=o?.planPath??i.key,s=null;if(N(r)&&o)try{s=R(M(r,"utf-8"))!==o.contentHash}catch{s=null}n.push({planPath:r,commentCount:i.commentCount,lastModified:i.lastModified,stale:s})}return n}
30
30
  //# sourceMappingURL=index.js.map