cc-plan-viewer 0.1.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.
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.-left-10{left:-2.5rem}.bottom-0{bottom:0}.left-0{left:0}.right-0{right:0}.top-0{top:0}.top-2{top:.5rem}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.float-right{float:right}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-lg{margin-bottom:24px}.mb-md{margin-bottom:16px}.mb-sm{margin-bottom:8px}.mb-xs{margin-bottom:4px}.ml-4{margin-left:1rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-lg{margin-top:24px}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.hidden{display:none}.h-2{height:.5rem}.h-5{height:1.25rem}.h-7{height:1.75rem}.max-h-\[80vh\]{max-height:80vh}.min-h-\[80px\]{min-height:80px}.min-h-screen{min-height:100vh}.w-2{width:.5rem}.w-5{width:1.25rem}.w-64{width:16rem}.w-7{width:1.75rem}.w-\[300px\]{width:300px}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-\[1200px\]{max-width:1200px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.scale-\[1\.02\]{--tw-scale-x: 1.02;--tw-scale-y: 1.02;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.resize-none{resize:none}.resize-y{resize:vertical}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-lg{gap:24px}.gap-sm{gap:8px}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:20px}.rounded-md{border-radius:12px}.rounded-sm{border-radius:6px}.border{border-width:1px}.border-l-2{border-left-width:2px}.border-t{border-top-width:1px}.border-none{border-style:none}.border-claude-accent-light{--tw-border-opacity: 1;border-color:rgb(205 115 87 / var(--tw-border-opacity, 1))}.border-claude-border-light{--tw-border-opacity: 1;border-color:rgb(223 215 198 / var(--tw-border-opacity, 1))}.bg-black\/40{background-color:#0006}.bg-claude-accent-light{--tw-bg-opacity: 1;background-color:rgb(205 115 87 / var(--tw-bg-opacity, 1))}.bg-claude-bg-light{--tw-bg-opacity: 1;background-color:rgb(245 240 229 / var(--tw-bg-opacity, 1))}.bg-claude-success-light{--tw-bg-opacity: 1;background-color:rgb(72 149 172 / var(--tw-bg-opacity, 1))}.bg-claude-surface-light{--tw-bg-opacity: 1;background-color:rgb(251 250 241 / var(--tw-bg-opacity, 1))}.bg-claude-surface-light\/80{background-color:#fbfaf1cc}.bg-claude-text-tertiary-light{--tw-bg-opacity: 1;background-color:rgb(142 131 112 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.p-1\.5{padding:.375rem}.p-3{padding:.75rem}.p-lg{padding:24px}.p-md{padding:16px}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-md{padding-left:16px;padding-right:16px}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-xl{padding-top:40px;padding-bottom:40px}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-40{padding-bottom:10rem}.pl-2\.5{padding-left:.625rem}.pt-2\.5{padding-top:.625rem}.pt-md{padding-top:16px}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.tracking-wide{letter-spacing:.025em}.text-claude-accent-light{--tw-text-opacity: 1;color:rgb(205 115 87 / var(--tw-text-opacity, 1))}.text-claude-text-primary-light{--tw-text-opacity: 1;color:rgb(40 34 24 / var(--tw-text-opacity, 1))}.text-claude-text-secondary-light{--tw-text-opacity: 1;color:rgb(102 102 86 / var(--tw-text-opacity, 1))}.text-claude-text-tertiary-light{--tw-text-opacity: 1;color:rgb(142 131 112 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-lg{--tw-backdrop-blur: blur(16px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}*{box-sizing:border-box}body{margin:0;font-family:Inter,system-ui,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.prose{color:#282218;line-height:1.75;max-width:none;font-size:15px}@media(prefers-color-scheme:dark){.prose{color:#ede7dc}}.prose h1,.prose h2,.prose h3,.prose h4,.prose h5,.prose h6{font-weight:700;line-height:1.35;margin-top:1.75em;margin-bottom:.6em;color:#1a1610}@media(prefers-color-scheme:dark){.prose h1,.prose h2,.prose h3,.prose h4,.prose h5,.prose h6{color:#f5f0e5}}.prose h1{font-size:1.75rem;margin-top:0}.prose h2{font-size:1.35rem;padding-bottom:.35em;border-bottom:1px solid #DFD7C6}.prose h3{font-size:1.15rem}.prose h4{font-size:1rem;font-weight:600}@media(prefers-color-scheme:dark){.prose h2{border-bottom-color:#44403b}}.prose p{margin-top:.75em;margin-bottom:.75em}.prose ul{margin-top:.5em;margin-bottom:.5em;padding-left:1.6em;list-style-type:disc}.prose ol{margin-top:.5em;margin-bottom:.5em;padding-left:1.6em;list-style-type:decimal}.prose li{margin-top:.3em;margin-bottom:.3em}.prose li::marker{color:#8e8370}@media(prefers-color-scheme:dark){.prose li::marker{color:#6d676a}}.prose strong{font-weight:700;color:#1a1610}@media(prefers-color-scheme:dark){.prose strong{color:#f5f0e5}}.prose em{font-style:italic}.prose a{color:#b05a3c;text-decoration:underline;text-underline-offset:2px;text-decoration-color:#b05a3c66}.prose a:hover{text-decoration-color:#b05a3c}@media(prefers-color-scheme:dark){.prose a{color:#d6896e;text-decoration-color:#d6896e66}.prose a:hover{text-decoration-color:#d6896e}}.prose code{font-family:SF Mono,Fira Code,JetBrains Mono,Cascadia Code,Menlo,Consolas,monospace;font-size:.85em;padding:.2em .45em;border-radius:5px;background:#e8e0d0;color:#5c3d2e;font-weight:500}@media(prefers-color-scheme:dark){.prose code{background:#382f2a;color:#e0c4a8}}.prose pre{margin-top:1em;margin-bottom:1em;padding:1.1em 1.3em;border-radius:10px;overflow-x:auto;background:#2b2420;border:1px solid #3D3530;line-height:1.6}@media(prefers-color-scheme:dark){.prose pre{background:#1a1514;border-color:#332c28}}.prose pre code{padding:0;background:none;border-radius:0;font-size:.8125em;font-weight:400;color:#e8ded0}.prose pre code .hljs-keyword,.prose pre code .hljs-selector-tag,.prose pre code .hljs-built_in{color:#e8a87c}.prose pre code .hljs-string,.prose pre code .hljs-attr{color:#a8c9a0}.prose pre code .hljs-number,.prose pre code .hljs-literal{color:#d4a574}.prose pre code .hljs-type,.prose pre code .hljs-title,.prose pre code .hljs-class .hljs-title{color:#c4b4e0;font-weight:500}.prose pre code .hljs-function,.prose pre code .hljs-title.function_{color:#8cc8d0}.prose pre code .hljs-comment,.prose pre code .hljs-doctag{color:#7a7068;font-style:italic}.prose pre code .hljs-variable,.prose pre code .hljs-template-variable{color:#e0c4a8}.prose pre code .hljs-property{color:#d0b8a0}.prose pre code .hljs-meta,.prose pre code .hljs-preprocessor{color:#b0a090}.prose pre code .hljs-punctuation{color:#9a8e80}.prose pre code .hljs-addition{color:#a8c9a0;background:#a8c9a01a}.prose pre code .hljs-deletion{color:#d08080;background:#d080801a}.prose blockquote{border-left:3px solid #CD7357;padding-left:1em;margin-left:0;margin-top:.75em;margin-bottom:.75em;color:#504838}@media(prefers-color-scheme:dark){.prose blockquote{border-left-color:#d67f63;color:#b8aea0}}.prose table{width:100%;border-collapse:collapse;margin-top:1em;margin-bottom:1em;font-size:.875em}.prose th,.prose td{border:1px solid #DFD7C6;padding:.6em .85em;text-align:left}.prose th{font-weight:600;background:#ede5d5;color:#1a1610}@media(prefers-color-scheme:dark){.prose th,.prose td{border-color:#44403b}.prose th{background:#332c28;color:#f5f0e5}}.prose hr{border:none;border-top:1px solid #DFD7C6;margin:2em 0}@media(prefers-color-scheme:dark){.prose hr{border-top-color:#44403b}}mark.comment-highlight{background-color:#cd735724;border-bottom:2px solid rgba(205,115,87,.45);padding:1px 0;border-radius:2px;cursor:pointer;transition:background-color .2s ease,border-color .2s ease;color:inherit}mark.comment-highlight:hover,mark.comment-highlight.active{background-color:#cd73574d;border-bottom-color:#cd7357cc}mark.comment-highlight-pending{background-color:#cd73572e;border-bottom:2px solid rgba(205,115,87,.5);padding:1px 0;border-radius:2px;color:inherit}@media(prefers-color-scheme:dark){mark.comment-highlight{background-color:#d67f6324;border-bottom-color:#d67f6373}mark.comment-highlight:hover,mark.comment-highlight.active{background-color:#d67f634d;border-bottom-color:#d67f63cc}mark.comment-highlight-pending{background-color:#d67f632e;border-bottom-color:#d67f6380}}::-moz-selection{background:#cd735738;color:inherit}::selection{background:#cd735738;color:inherit}@media(prefers-color-scheme:dark){::-moz-selection{background:#d67f6347}::selection{background:#d67f6347}}.placeholder\:text-claude-text-tertiary-light::-moz-placeholder{--tw-text-opacity: 1;color:rgb(142 131 112 / var(--tw-text-opacity, 1))}.placeholder\:text-claude-text-tertiary-light::placeholder{--tw-text-opacity: 1;color:rgb(142 131 112 / var(--tw-text-opacity, 1))}.hover\:scale-110:hover{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-claude-border-light\/30:hover{background-color:#dfd7c64d}.hover\:bg-claude-border-light\/50:hover{background-color:#dfd7c680}.hover\:bg-red-50:hover{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.hover\:text-claude-accent-light:hover{--tw-text-opacity: 1;color:rgb(205 115 87 / var(--tw-text-opacity, 1))}.hover\:text-claude-text-primary-light:hover{--tw-text-opacity: 1;color:rgb(40 34 24 / var(--tw-text-opacity, 1))}.hover\:text-red-500:hover{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.hover\:opacity-90:hover{opacity:.9}.focus\:border-claude-accent-light:focus{--tw-border-opacity: 1;border-color:rgb(205 115 87 / var(--tw-border-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:opacity-100{opacity:1}@media(min-width:768px){.md\:p-xl{padding:40px}}@media(min-width:1280px){.xl\:block{display:block}.xl\:hidden{display:none}}@media(prefers-color-scheme:dark){.dark\:border-claude-accent-dark{--tw-border-opacity: 1;border-color:rgb(214 127 99 / var(--tw-border-opacity, 1))}.dark\:border-claude-border-dark{--tw-border-opacity: 1;border-color:rgb(68 64 59 / var(--tw-border-opacity, 1))}.dark\:bg-claude-accent-dark{--tw-bg-opacity: 1;background-color:rgb(214 127 99 / var(--tw-bg-opacity, 1))}.dark\:bg-claude-bg-dark{--tw-bg-opacity: 1;background-color:rgb(32 25 24 / var(--tw-bg-opacity, 1))}.dark\:bg-claude-success-dark{--tw-bg-opacity: 1;background-color:rgb(111 187 148 / var(--tw-bg-opacity, 1))}.dark\:bg-claude-surface-dark{--tw-bg-opacity: 1;background-color:rgb(42 39 35 / var(--tw-bg-opacity, 1))}.dark\:bg-claude-surface-dark\/80{background-color:#2a2723cc}.dark\:bg-claude-text-tertiary-dark{--tw-bg-opacity: 1;background-color:rgb(109 103 106 / var(--tw-bg-opacity, 1))}.dark\:text-claude-accent-dark{--tw-text-opacity: 1;color:rgb(214 127 99 / var(--tw-text-opacity, 1))}.dark\:text-claude-text-primary-dark{--tw-text-opacity: 1;color:rgb(237 231 220 / var(--tw-text-opacity, 1))}.dark\:text-claude-text-secondary-dark{--tw-text-opacity: 1;color:rgb(158 153 149 / var(--tw-text-opacity, 1))}.dark\:text-claude-text-tertiary-dark{--tw-text-opacity: 1;color:rgb(109 103 106 / var(--tw-text-opacity, 1))}.dark\:placeholder\:text-claude-text-tertiary-dark::-moz-placeholder{--tw-text-opacity: 1;color:rgb(109 103 106 / var(--tw-text-opacity, 1))}.dark\:placeholder\:text-claude-text-tertiary-dark::placeholder{--tw-text-opacity: 1;color:rgb(109 103 106 / var(--tw-text-opacity, 1))}.dark\:hover\:bg-claude-border-dark\/30:hover{background-color:#44403b4d}.dark\:hover\:bg-claude-border-dark\/50:hover{background-color:#44403b80}.dark\:hover\:bg-red-900\/20:hover{background-color:#7f1d1d33}.dark\:hover\:text-claude-accent-dark:hover{--tw-text-opacity: 1;color:rgb(214 127 99 / var(--tw-text-opacity, 1))}.dark\:hover\:text-claude-text-primary-dark:hover{--tw-text-opacity: 1;color:rgb(237 231 220 / var(--tw-text-opacity, 1))}.dark\:focus\:border-claude-accent-dark:focus{--tw-border-opacity: 1;border-color:rgb(214 127 99 / var(--tw-border-opacity, 1))}}
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>cc-plan-viewer</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
10
+ <script type="module" crossorigin src="/assets/index-B9Ku0w1F.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/assets/index-CFUAZEOC.css">
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ </body>
16
+ </html>
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { execSync } from 'node:child_process';
6
+ const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
7
+ const HOOKS_DIR = path.join(os.homedir(), '.claude', 'hooks');
8
+ // Resolve the hook script path
9
+ // When compiled: dist/server/bin/ → dist/server/ → dist/ → pkg root → hooks/
10
+ // When running via tsx: bin/ → pkg root → hooks/
11
+ const hookSource = path.resolve(import.meta.dirname, '..', '..', '..', 'hooks', 'plan-viewer-hook.cjs');
12
+ // Fallback for dev mode (running from bin/ directly)
13
+ const hookSourceDev = path.resolve(import.meta.dirname, '..', 'hooks', 'plan-viewer-hook.cjs');
14
+ const resolvedHookSource = fs.existsSync(hookSource) ? hookSource : hookSourceDev;
15
+ function readSettings() {
16
+ try {
17
+ return JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ }
23
+ function writeSettings(settings) {
24
+ fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n', 'utf8');
25
+ }
26
+ function getHookCommand() {
27
+ return `node "${resolvedHookSource}"`;
28
+ }
29
+ function install() {
30
+ console.log('[cc-plan-viewer] Installing hook...');
31
+ // Ensure hooks directory exists
32
+ if (!fs.existsSync(HOOKS_DIR)) {
33
+ fs.mkdirSync(HOOKS_DIR, { recursive: true });
34
+ }
35
+ // Read current settings
36
+ const settings = readSettings();
37
+ // Ensure hooks object exists
38
+ if (!settings.hooks || typeof settings.hooks !== 'object') {
39
+ settings.hooks = {};
40
+ }
41
+ const hooks = settings.hooks;
42
+ // Ensure PostToolUse array exists
43
+ if (!Array.isArray(hooks.PostToolUse)) {
44
+ hooks.PostToolUse = [];
45
+ }
46
+ const hookCommand = getHookCommand();
47
+ // Check if already installed
48
+ const alreadyInstalled = hooks.PostToolUse.some((entry) => {
49
+ if (typeof entry !== 'object' || entry === null)
50
+ return false;
51
+ const e = entry;
52
+ if (!Array.isArray(e.hooks))
53
+ return false;
54
+ return e.hooks.some((h) => {
55
+ if (typeof h !== 'object' || h === null)
56
+ return false;
57
+ return h.command === hookCommand;
58
+ });
59
+ });
60
+ if (alreadyInstalled) {
61
+ console.log('[cc-plan-viewer] Hook already installed.');
62
+ return;
63
+ }
64
+ // Add the hook entry
65
+ hooks.PostToolUse.push({
66
+ matcher: 'Write|Edit',
67
+ hooks: [
68
+ {
69
+ type: 'command',
70
+ command: hookCommand,
71
+ },
72
+ ],
73
+ });
74
+ writeSettings(settings);
75
+ console.log('[cc-plan-viewer] Hook installed successfully.');
76
+ console.log(`[cc-plan-viewer] Hook script: ${resolvedHookSource}`);
77
+ console.log('[cc-plan-viewer] Added to: ~/.claude/settings.json (PostToolUse)');
78
+ }
79
+ function uninstall() {
80
+ console.log('[cc-plan-viewer] Uninstalling hook...');
81
+ const settings = readSettings();
82
+ const hooks = settings.hooks;
83
+ if (!hooks?.PostToolUse || !Array.isArray(hooks.PostToolUse)) {
84
+ console.log('[cc-plan-viewer] No hook found to remove.');
85
+ return;
86
+ }
87
+ const hookCommand = getHookCommand();
88
+ const before = hooks.PostToolUse.length;
89
+ hooks.PostToolUse = hooks.PostToolUse.filter((entry) => {
90
+ if (typeof entry !== 'object' || entry === null)
91
+ return true;
92
+ const e = entry;
93
+ if (!Array.isArray(e.hooks))
94
+ return true;
95
+ return !e.hooks.some((h) => {
96
+ if (typeof h !== 'object' || h === null)
97
+ return false;
98
+ const cmd = h.command;
99
+ return typeof cmd === 'string' && cmd.includes('plan-viewer-hook');
100
+ });
101
+ });
102
+ if (hooks.PostToolUse.length === before) {
103
+ console.log('[cc-plan-viewer] No hook found to remove.');
104
+ return;
105
+ }
106
+ writeSettings(settings);
107
+ console.log('[cc-plan-viewer] Hook removed successfully.');
108
+ }
109
+ function start() {
110
+ console.log('[cc-plan-viewer] Starting server...');
111
+ // Import and run the server
112
+ import('../server/index.js');
113
+ }
114
+ function open(filename) {
115
+ const port = 3847;
116
+ const url = filename
117
+ ? `http://localhost:${port}/?plan=${encodeURIComponent(filename)}`
118
+ : `http://localhost:${port}`;
119
+ try {
120
+ execSync(`open "${url}"`, { stdio: 'ignore' });
121
+ console.log(`[cc-plan-viewer] Opened ${url}`);
122
+ }
123
+ catch {
124
+ console.log(`[cc-plan-viewer] Open this URL in your browser: ${url}`);
125
+ }
126
+ }
127
+ // CLI
128
+ const command = process.argv[2];
129
+ switch (command) {
130
+ case 'install':
131
+ install();
132
+ break;
133
+ case 'uninstall':
134
+ uninstall();
135
+ break;
136
+ case 'start':
137
+ start();
138
+ break;
139
+ case 'open':
140
+ open(process.argv[3]);
141
+ break;
142
+ default:
143
+ console.log(`
144
+ cc-plan-viewer — Browser-based review UI for Claude Code plans
145
+
146
+ Commands:
147
+ install Add the PostToolUse hook to ~/.claude/settings.json
148
+ uninstall Remove the hook
149
+ start Start the server (for development)
150
+ open [file] Open a plan in the browser
151
+ `);
152
+ }
@@ -0,0 +1,188 @@
1
+ import express from 'express';
2
+ import { createServer } from 'node:http';
3
+ import { WebSocketServer, WebSocket } from 'ws';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+ import { parsePlan } from './planParser.js';
8
+ import { saveReview, getReview } from './reviewStore.js';
9
+ import { writePidFile, writePortFile, cleanupFiles, resetIdleTimer } from './lifecycle.js';
10
+ import { watchPlansDir } from './planWatcher.js';
11
+ const PORT = parseInt(process.env.PORT || '3847', 10);
12
+ // Auto-detect plans directory
13
+ function findPlansDir() {
14
+ const home = os.homedir();
15
+ const candidates = [
16
+ path.join(home, '.claude-personal', 'plans'),
17
+ path.join(home, '.claude', 'plans'),
18
+ ];
19
+ for (const dir of candidates) {
20
+ if (fs.existsSync(dir))
21
+ return dir;
22
+ }
23
+ // Default to first candidate even if it doesn't exist yet
24
+ return candidates[0];
25
+ }
26
+ const plansDir = findPlansDir();
27
+ const app = express();
28
+ const server = createServer(app);
29
+ const wss = new WebSocketServer({ server });
30
+ app.use(express.json());
31
+ // Reset idle timer on every request
32
+ app.use((_req, _res, next) => {
33
+ resetIdleTimer();
34
+ next();
35
+ });
36
+ // Health check
37
+ app.get('/health', (_req, res) => {
38
+ res.json({ status: 'ok', plansDir });
39
+ });
40
+ // List all plans
41
+ app.get('/api/plans', (_req, res) => {
42
+ try {
43
+ if (!fs.existsSync(plansDir)) {
44
+ res.json([]);
45
+ return;
46
+ }
47
+ const files = fs.readdirSync(plansDir)
48
+ .filter(f => f.endsWith('.md') && !f.endsWith('.review.json'))
49
+ .map(f => {
50
+ const filePath = path.join(plansDir, f);
51
+ const stat = fs.statSync(filePath);
52
+ const review = getReview(filePath);
53
+ return {
54
+ filename: f,
55
+ modified: stat.mtime.toISOString(),
56
+ size: stat.size,
57
+ hasReview: !!review,
58
+ reviewAction: review?.action ?? null,
59
+ };
60
+ })
61
+ .sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
62
+ res.json(files);
63
+ }
64
+ catch (err) {
65
+ res.status(500).json({ error: 'Failed to list plans' });
66
+ }
67
+ });
68
+ // Get a specific plan
69
+ app.get('/api/plans/:filename', (req, res) => {
70
+ const filename = req.params.filename;
71
+ if (!filename.endsWith('.md') || filename.includes('..')) {
72
+ res.status(400).json({ error: 'Invalid filename' });
73
+ return;
74
+ }
75
+ const filePath = path.join(plansDir, filename);
76
+ try {
77
+ const content = fs.readFileSync(filePath, 'utf8');
78
+ const parsed = parsePlan(content);
79
+ const review = getReview(filePath);
80
+ res.json({ filename, parsed, review });
81
+ }
82
+ catch {
83
+ res.status(404).json({ error: 'Plan not found' });
84
+ }
85
+ });
86
+ // Hook notifies of plan update
87
+ app.post('/api/plan-updated', (req, res) => {
88
+ const { filePath, planOptions } = req.body;
89
+ const filename = path.basename(filePath || '');
90
+ // Broadcast to all WebSocket clients
91
+ const message = JSON.stringify({
92
+ type: 'plan-updated',
93
+ filename,
94
+ planOptions: planOptions || null,
95
+ });
96
+ for (const client of wss.clients) {
97
+ if (client.readyState === WebSocket.OPEN) {
98
+ client.send(message);
99
+ }
100
+ }
101
+ res.json({ ok: true });
102
+ });
103
+ // Save a review
104
+ app.post('/api/reviews/:filename', (req, res) => {
105
+ const filename = req.params.filename;
106
+ if (!filename.endsWith('.md') || filename.includes('..')) {
107
+ res.status(400).json({ error: 'Invalid filename' });
108
+ return;
109
+ }
110
+ const filePath = path.join(plansDir, filename);
111
+ if (!fs.existsSync(filePath)) {
112
+ res.status(404).json({ error: 'Plan not found' });
113
+ return;
114
+ }
115
+ const review = {
116
+ planFile: filename,
117
+ action: req.body.action || 'feedback',
118
+ submittedAt: new Date().toISOString(),
119
+ consumedAt: null,
120
+ overallComment: req.body.overallComment || '',
121
+ inlineComments: req.body.inlineComments || [],
122
+ };
123
+ saveReview(filePath, review);
124
+ // Notify clients
125
+ const message = JSON.stringify({
126
+ type: 'review-submitted',
127
+ filename,
128
+ action: review.action,
129
+ });
130
+ for (const client of wss.clients) {
131
+ if (client.readyState === WebSocket.OPEN) {
132
+ client.send(message);
133
+ }
134
+ }
135
+ res.json({ ok: true, review });
136
+ });
137
+ // Get a review
138
+ app.get('/api/reviews/:filename', (req, res) => {
139
+ const filename = req.params.filename;
140
+ const filePath = path.join(plansDir, filename);
141
+ const review = getReview(filePath);
142
+ if (!review) {
143
+ res.status(404).json({ error: 'No review found' });
144
+ return;
145
+ }
146
+ res.json(review);
147
+ });
148
+ // Serve SPA static files
149
+ // Try multiple paths: dist/client relative to project root, or relative to compiled output
150
+ const clientDistCandidates = [
151
+ path.join(import.meta.dirname, '..', '..', 'client'), // prod: dist/server/server/ → dist/client/
152
+ path.join(import.meta.dirname, '..', 'dist', 'client'), // dev: server/ → dist/client/
153
+ ];
154
+ const clientDist = clientDistCandidates.find((d) => fs.existsSync(d));
155
+ if (clientDist) {
156
+ app.use(express.static(clientDist));
157
+ app.get('/{*path}', (_req, res) => {
158
+ res.sendFile(path.join(clientDist, 'index.html'));
159
+ });
160
+ }
161
+ // WebSocket connection
162
+ wss.on('connection', (ws) => {
163
+ resetIdleTimer();
164
+ ws.on('message', () => resetIdleTimer());
165
+ });
166
+ // Start
167
+ server.listen(PORT, () => {
168
+ writePidFile();
169
+ writePortFile(PORT);
170
+ resetIdleTimer();
171
+ console.log(`[cc-plan-viewer] Server running at http://localhost:${PORT}`);
172
+ console.log(`[cc-plan-viewer] Plans directory: ${plansDir}`);
173
+ });
174
+ // Watch for plan file changes
175
+ watchPlansDir(plansDir, (filename, content) => {
176
+ const message = JSON.stringify({
177
+ type: 'plan-updated',
178
+ filename,
179
+ });
180
+ for (const client of wss.clients) {
181
+ if (client.readyState === WebSocket.OPEN) {
182
+ client.send(message);
183
+ }
184
+ }
185
+ });
186
+ // Cleanup on exit
187
+ process.on('SIGINT', () => { cleanupFiles(); process.exit(0); });
188
+ process.on('SIGTERM', () => { cleanupFiles(); process.exit(0); });
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ const TMP_DIR = os.tmpdir();
5
+ const PID_FILE = path.join(TMP_DIR, 'cc-plan-viewer.pid');
6
+ const PORT_FILE = path.join(TMP_DIR, 'cc-plan-viewer-port');
7
+ const IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
8
+ let idleTimer = null;
9
+ export function writePidFile() {
10
+ fs.writeFileSync(PID_FILE, String(process.pid), 'utf8');
11
+ }
12
+ export function writePortFile(port) {
13
+ fs.writeFileSync(PORT_FILE, String(port), 'utf8');
14
+ }
15
+ export function cleanupFiles() {
16
+ try {
17
+ fs.unlinkSync(PID_FILE);
18
+ }
19
+ catch { }
20
+ try {
21
+ fs.unlinkSync(PORT_FILE);
22
+ }
23
+ catch { }
24
+ }
25
+ export function resetIdleTimer() {
26
+ if (idleTimer)
27
+ clearTimeout(idleTimer);
28
+ idleTimer = setTimeout(() => {
29
+ console.log('[cc-plan-viewer] Idle timeout reached, shutting down.');
30
+ cleanupFiles();
31
+ process.exit(0);
32
+ }, IDLE_TIMEOUT_MS);
33
+ // Don't keep the process alive just for the timer
34
+ idleTimer.unref();
35
+ }
36
+ export function readPortFile() {
37
+ try {
38
+ const port = parseInt(fs.readFileSync(PORT_FILE, 'utf8').trim(), 10);
39
+ return isNaN(port) ? null : port;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ export function isServerRunning() {
46
+ try {
47
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim(), 10);
48
+ if (isNaN(pid))
49
+ return false;
50
+ process.kill(pid, 0); // check if alive
51
+ return true;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ }
@@ -0,0 +1,67 @@
1
+ function slugify(text) {
2
+ return text
3
+ .toLowerCase()
4
+ .replace(/[^a-z0-9\s-]/g, '')
5
+ .replace(/\s+/g, '-')
6
+ .replace(/-+/g, '-')
7
+ .trim();
8
+ }
9
+ export function parsePlan(markdown) {
10
+ const lines = markdown.split('\n');
11
+ const flatSections = [];
12
+ let title = '';
13
+ let currentSection = null;
14
+ for (let i = 0; i < lines.length; i++) {
15
+ const line = lines[i];
16
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
17
+ if (headingMatch) {
18
+ const level = headingMatch[1].length;
19
+ const heading = headingMatch[2].trim();
20
+ if (level === 1 && !title) {
21
+ title = heading;
22
+ }
23
+ if (currentSection) {
24
+ currentSection.endLine = i;
25
+ currentSection.rawContent = lines
26
+ .slice(currentSection.startLine, i)
27
+ .join('\n');
28
+ }
29
+ currentSection = {
30
+ id: slugify(heading) || `section-${i}`,
31
+ heading,
32
+ level,
33
+ startLine: i,
34
+ endLine: lines.length,
35
+ rawContent: '',
36
+ children: [],
37
+ };
38
+ flatSections.push(currentSection);
39
+ }
40
+ }
41
+ if (currentSection) {
42
+ currentSection.endLine = lines.length;
43
+ currentSection.rawContent = lines
44
+ .slice(currentSection.startLine)
45
+ .join('\n');
46
+ }
47
+ // Build hierarchy: h3s become children of preceding h2, etc.
48
+ const rootSections = [];
49
+ const stack = [];
50
+ for (const section of flatSections) {
51
+ while (stack.length > 0 && stack[stack.length - 1].level >= section.level) {
52
+ stack.pop();
53
+ }
54
+ if (stack.length > 0) {
55
+ stack[stack.length - 1].children.push(section);
56
+ }
57
+ else {
58
+ rootSections.push(section);
59
+ }
60
+ stack.push(section);
61
+ }
62
+ return {
63
+ title: title || 'Untitled Plan',
64
+ sections: rootSections,
65
+ rawMarkdown: markdown,
66
+ };
67
+ }
@@ -0,0 +1,32 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function watchPlansDir(plansDir, onUpdate) {
4
+ if (!fs.existsSync(plansDir)) {
5
+ console.warn(`[cc-plan-viewer] Plans directory not found: ${plansDir}`);
6
+ return;
7
+ }
8
+ const debounceMap = new Map();
9
+ fs.watch(plansDir, (eventType, filename) => {
10
+ if (!filename || !filename.endsWith('.md'))
11
+ return;
12
+ // Ignore review files
13
+ if (filename.endsWith('.review.json'))
14
+ return;
15
+ // Debounce 300ms per file
16
+ const existing = debounceMap.get(filename);
17
+ if (existing)
18
+ clearTimeout(existing);
19
+ debounceMap.set(filename, setTimeout(() => {
20
+ debounceMap.delete(filename);
21
+ const filePath = path.join(plansDir, filename);
22
+ try {
23
+ const content = fs.readFileSync(filePath, 'utf8');
24
+ onUpdate(filename, content);
25
+ }
26
+ catch {
27
+ // File may have been deleted
28
+ }
29
+ }, 300));
30
+ });
31
+ console.log(`[cc-plan-viewer] Watching plans directory: ${plansDir}`);
32
+ }
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ function reviewPathFor(planPath) {
4
+ const dir = path.dirname(planPath);
5
+ const base = path.basename(planPath, '.md');
6
+ return path.join(dir, `${base}.review.json`);
7
+ }
8
+ export function saveReview(planPath, review) {
9
+ const reviewPath = reviewPathFor(planPath);
10
+ fs.writeFileSync(reviewPath, JSON.stringify(review, null, 2), 'utf8');
11
+ }
12
+ export function getReview(planPath) {
13
+ const reviewPath = reviewPathFor(planPath);
14
+ if (!fs.existsSync(reviewPath))
15
+ return null;
16
+ try {
17
+ return JSON.parse(fs.readFileSync(reviewPath, 'utf8'));
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export function getUnconsumedReview(planPath) {
24
+ const review = getReview(planPath);
25
+ if (review && !review.consumedAt)
26
+ return review;
27
+ return null;
28
+ }
29
+ export function markConsumed(planPath) {
30
+ const review = getReview(planPath);
31
+ if (review) {
32
+ review.consumedAt = new Date().toISOString();
33
+ const reviewPath = reviewPathFor(planPath);
34
+ fs.writeFileSync(reviewPath, JSON.stringify(review, null, 2), 'utf8');
35
+ }
36
+ }