@misterhuydo/cairn-mcp 1.31.0 → 1.32.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.
- package/README.md +31 -13
- package/dist/cairn-cli.js +50 -50
- package/dist/index.js +43 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -374,19 +374,37 @@ is least reason to speak. Bytes survive now only as a fallback for when no usage
|
|
|
374
374
|
record is reachable, and when that happens the line says so instead of dressing a
|
|
375
375
|
file size up as a token count.
|
|
376
376
|
|
|
377
|
-
**It only speaks at 90% of the window and above
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
377
|
+
**It only speaks at 90% of the window and above** — and the window is a setting, not
|
|
378
|
+
a measurement. Nothing cairn can read carries it: no hook input has it, the
|
|
379
|
+
transcript records usage but never a limit, and the model id is no help either
|
|
380
|
+
because a `claude-opus-5[1m]` session writes the base `claude-opus-5` into both the
|
|
381
|
+
transcript's `message.model` and `lastModelUsage` in `~/.claude.json`.
|
|
382
|
+
|
|
383
|
+
Through v1.31.0 cairn guessed the *smallest* window that could hold what it had
|
|
384
|
+
measured, which meant an unmeasured session was assumed to be 200k. That produced a
|
|
385
|
+
guaranteed false-alarm band: on a 1M session, every reading between 180k and 196k
|
|
386
|
+
looked like a nearly-full 200k one. It was measured live at 192,634 tokens, where
|
|
387
|
+
cairn reported **96%** and `/context` said **19%**. The band is structural rather
|
|
388
|
+
than a tuning error — any firing point for a 200k window is a point a 1M session
|
|
389
|
+
also passes through — so no threshold can serve both, and the tie-break has to come
|
|
390
|
+
from outside.
|
|
391
|
+
|
|
392
|
+
Since v1.32.0 cairn never guesses downward. Unset, it assumes the widest window it
|
|
393
|
+
knows about and stays quiet; the cost of that being wrong is one missed nudge on a
|
|
394
|
+
genuinely full small window, against an interruption at a fifth of capacity that
|
|
395
|
+
teaches you to ignore the line for good. Measurement still raises the floor — having
|
|
396
|
+
*seen* 482k tokens proves the window is bigger than that — it simply can never lower
|
|
397
|
+
it.
|
|
398
|
+
|
|
399
|
+
If your window really is 200k, say so and both the threshold and the reported
|
|
400
|
+
percentage become exact:
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
export CAIRN_CONTEXT_WINDOW=200000
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`CAIRN_CONTEXT_HINT_TOKENS` still overrides the trigger point outright if you want a
|
|
407
|
+
different bar rather than a different window.
|
|
390
408
|
|
|
391
409
|
Deliberately not built: no full transcript walk (the tail only; a full walk in this
|
|
392
410
|
hot path once froze every terminal in the cockpit for 4.3 seconds), no counting
|
package/dist/cairn-cli.js
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import h from"fs";import te from"os";import w from"path";import{execSync as
|
|
2
|
+
import h from"fs";import te from"os";import w from"path";import{execSync as is}from"child_process";import ds from"fast-glob";var Fe={".java":"java",".ts":"typescript",".tsx":"typescript",".js":"javascript",".jsx":"javascript",".mjs":"javascript",".vue":"vue",".py":"python",".sql":"sql",".yml":"config",".yaml":"config",".properties":"config",".env":"config",".xml":"xml",".html":"html",".md":"markdown"};function pe(e,t,n={}){let{noComments:s=!0,noEmptyLines:o=!0,aggressive:r=!1}=n,a=e;return s&&(a=Tt(a,t)),o&&(a=jt(a)),a=Pt(a),r&&(a=Nt(a)),a.trim()}function Tt(e,t){switch(t){case"java":case"typescript":case"javascript":case"vue":return e.replace(/\/\*[\s\S]*?\*\//g,"").replace(/(["'`])(?:(?!\1)[^\\]|\\.)*\1|\/(?![/*])(?:[^/\\\n]|\\.)*\/[gimsuy]*|\/\/[^\n]*/g,(n,s)=>s||!n.startsWith("//")?n:"");case"python":return e.replace(/#.*$/gm,"").replace(/'''[\s\S]*?'''/g,"").replace(/"""[\s\S]*?"""/g,"");case"sql":return e.replace(/--.*$/gm,"").replace(/\/\*[\s\S]*?\*\//g,"");case"xml":case"html":return e.replace(/<!--[\s\S]*?-->/g,"");case"config":return e.replace(/#.*$/gm,"");default:return e}}function jt(e){return e.split(`
|
|
3
3
|
`).filter(t=>t.trim().length>0).join(`
|
|
4
|
-
`)}function
|
|
4
|
+
`)}function Pt(e){return e.split(`
|
|
5
5
|
`).map(t=>t.trimEnd()).join(`
|
|
6
|
-
`)}function
|
|
6
|
+
`)}function Nt(e){return e.replace(/\s+/g," ").replace(/\s*([=+\-*/%<>!&|^~?:;,{}()[\]])\s*/g,"$1").replace(/;\s*}/g,"}").replace(/\(\s+/g,"(").replace(/\s+\)/g,")").trim()}function Le(e,t={}){let{noStyle:n=!1,noComments:s=!0,noEmptyLines:o=!0,aggressive:r=!1}=t,a=[],c=e.match(/<template>([\s\S]*?)<\/template>/i);if(c){let i=c[1];s&&(i=i.replace(/<!--[\s\S]*?-->/g,"")),o&&(i=i.split(`
|
|
7
7
|
`).filter(l=>l.trim()).join(`
|
|
8
8
|
`)),i=i.replace(/\s+/g," ").trim(),i&&a.push(`<template>${i}</template>`)}let u=e.match(/<script([^>]*)>([\s\S]*?)<\/script>/i);if(u){let i=u[1],l=pe(u[2],"javascript",{noComments:s,noEmptyLines:o,aggressive:r}),p=/setup/i.test(i)?"<script setup>":"<script>";l&&a.push(`${p}${l}</script>`)}if(!n){let i=e.match(/<style([^>]*)>([\s\S]*?)<\/style>/i);if(i){let l=i[2].replace(/\/\*[\s\S]*?\*\//g,"").replace(/\s+/g," ").trim();l&&a.push(`<style${i[1]}>${l}</style>`)}}return a.join(`
|
|
9
9
|
|
|
10
|
-
`)}import U from"fs";import ne from"path";import L from"fs";import
|
|
11
|
-
`,"utf8")}function
|
|
12
|
-
`).length:0}catch{return null}}function ce(e,t){let n=typeof t?.[P]=="string"?t[P].trim():"";if(!n)return{recorded:!1,text:"",at:null,commits:null,stale:!1};let s=t[`${P}_at`]||t.checkpoint_at||null,o=s?
|
|
13
|
-
`),s=t?1:0;for(let o=n.length-1;o>=s;o--){let r=n[o].trim();if(!r.startsWith("{"))continue;let a;try{a=JSON.parse(r)}catch{continue}if(a?.isSidechain===!0)continue;let c=a?.message?.usage;if(!c)continue;let u=(c.input_tokens||0)+(c.cache_read_input_tokens||0)+(c.cache_creation_input_tokens||0)+(c.output_tokens||0);if(u>0)return u}return null}function
|
|
14
|
-
`),b=[u,r.source,l.recorded?l.stale?"stale":"current":"missing",l.commits??"?",p,n?
|
|
15
|
-
`)[0].slice(0,120),ref:ne.join(".cairn","notes",u)}}function
|
|
10
|
+
`)}import U from"fs";import ne from"path";import L from"fs";import Mt from"os";import W from"path";var ge=null;function q(){return ge||process.cwd()}function De(e){let t=ge;return ge=e,t}function V(){let e=W.join(q(),".cairn");L.mkdirSync(W.join(e,"bundles"),{recursive:!0});let t=W.join(e,".cairn-project");return L.existsSync(t)||L.writeFileSync(t,"","utf8"),e}function _e(e){return e.replace(/[:/\\]/g,"-").replace(/^-+/,"")}function Se(){let e=W.join(Mt.homedir(),".claude","projects",_e(q()));return L.existsSync(e)?W.join(e,"memory"):null}function Ce(e){let t=W.join(e,"MEMORY.md");L.existsSync(t)||L.writeFileSync(t,`# Memory Index
|
|
11
|
+
`,"utf8")}function xe(){let e=W.join(V(),"memory");L.mkdirSync(e,{recursive:!0}),Ce(e);let t=Se();if(!t)return e;L.mkdirSync(t,{recursive:!0}),Ce(t);let n=s=>L.readdirSync(s).some(o=>o.endsWith(".md")&&o!=="MEMORY.md");if(!n(t)&&n(e))for(let s of L.readdirSync(e)){let o=W.join(e,s);try{if(!L.statSync(o).isFile())continue;let r=L.statSync(o);L.copyFileSync(o,W.join(t,s));try{L.utimesSync(W.join(t,s),r.atime,r.mtime)}catch{}}catch{}}return t}import Y from"fs";import be from"path";import{execSync as At}from"child_process";var P="would_be_lost";var Ie=2*1024*1024,We=[2e5,1e6],Ue=We[We.length-1],He=.9;function Je(e=0){let t=Number(process.env.CAIRN_CONTEXT_WINDOW);return Number.isFinite(t)&&t>0?t:Math.max(Ue,Math.ceil(e/.98))}var Es=Ue*He,Ft=[256*1024,1024*1024],Be=".handoff-hint",Lt=220;function we(e,t=Lt){let n=String(e).replace(/\s+/g," ").trim();return n.length<=t?n:n.slice(0,t-1)+"\u2026"}function Ct(e,t){try{let n=At(`git log --oneline --after="${t}"`,{cwd:e,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim();return n?n.split(`
|
|
12
|
+
`).length:0}catch{return null}}function ce(e,t){let n=typeof t?.[P]=="string"?t[P].trim():"";if(!n)return{recorded:!1,text:"",at:null,commits:null,stale:!1};let s=t[`${P}_at`]||t.checkpoint_at||null,o=s?Ct(e,s):null;return{recorded:!0,text:n,at:s,commits:o,stale:(o??0)>0}}function ke(e){if(!e.recorded)return"No handoff recorded \u2014 nothing was written down about what the last session would lose.";let t=e.stale?` (written before the ${e.commits} commit(s) since \u2014 may be stale)`:"";return`Carried forward: ${we(e.text)}${t}`}function Dt(e=0){let t=Number(process.env.CAIRN_CONTEXT_HINT_TOKENS);return Number.isFinite(t)&&t>0?t:Math.floor(Je(e)*He)}function It(e,t){let n=Y.openSync(e,"r");try{let s=Y.fstatSync(n).size,o=Math.min(s,t),r=Buffer.alloc(o);return Y.readSync(n,r,0,o,s-o),{text:r.toString("utf8"),truncated:o<s}}finally{Y.closeSync(n)}}function Wt(e,t){let n=e.split(`
|
|
13
|
+
`),s=t?1:0;for(let o=n.length-1;o>=s;o--){let r=n[o].trim();if(!r.startsWith("{"))continue;let a;try{a=JSON.parse(r)}catch{continue}if(a?.isSidechain===!0)continue;let c=a?.message?.usage;if(!c)continue;let u=(c.input_tokens||0)+(c.cache_read_input_tokens||0)+(c.cache_creation_input_tokens||0)+(c.output_tokens||0);if(u>0)return u}return null}function Ut(e){if(!e)return{tokens:0,source:"none",bytes:0};let t;try{t=Y.statSync(e).size}catch{return{tokens:0,source:"none",bytes:0}}for(let n of Ft){let s;try{s=It(e,n)}catch{break}let o=Wt(s.text,s.truncated);if(o!=null)return{tokens:o,source:"transcript",bytes:t};if(!s.truncated)break}return{tokens:0,source:"bytes",bytes:t}}function Ht(e){return e.recorded&&!e.stale?"clear":"compact"}function Jt(e,t){return e.recorded?e.stale?`${t}, and the recorded handoff predates the ${e.commits} commit(s) since, so it no longer describes the work in flight`:`${t}, and the handoff is current as of the last commit, so a reset costs only what is not already in that line`:`${t}, and nothing is written down about what this session would lose, so a hard reset would drop it with no way to get it back`}function Ye({cwd:e,session:t,transcriptPath:n,bytes:s,usage:o}){let r=o??Ut(n);r.source==="none"&&typeof s=="number"&&(r={tokens:0,source:"bytes",bytes:s});let a=s??r.bytes,c=Dt(r.tokens);if(r.source==="none")return null;let u,i;if(r.source==="transcript"){if(r.tokens<c)return null;u=Math.floor(r.tokens/c),i="this session is at roughly "+Math.round(r.tokens/Je(r.tokens)*100)+"% of its context window (about "+r.tokens.toLocaleString()+" tokens)"}else{if(a<Ie)return null;u=Math.floor(a/Ie),i=`this session's transcript is ${(a/1024/1024).toFixed(1)} MB (token count unreadable, so this is a rough size signal only)`}let l=ce(e,t),p=Ht(l),f=Jt(l,i),d;l.recorded?l.stale?d=`[cairn] Stopping point: ${i}. Clearing now would lose: ${we(l.text)} \u2014 written before the ${l.commits} commit(s) since.`:d=`[cairn] Stopping point: ${i}. The handoff is current: ${we(l.text)} \u2014 clearing costs only what is not in that line.`:d=`[cairn] Stopping point: ${i}. Nothing is recorded about what clearing would lose \u2014 ask the user first.`;let m=$=>$===p?" (Recommended)":"",_=[` - "Checkpoint + /clear${m("clear")}" \u2014 I write the handoff now, then you type /clear.`," Full reset and maximum headroom; everything not written down is gone."],k=[` - "Checkpoint + /compact${m("compact")}" \u2014 I write the handoff now, then you type /compact.`," Nothing has to be retyped, but the summary is lossy and the window stays partly full."],S=["STOPPING POINT \u2014 this is the user's call, not yours. Ask them before doing anything else.","",`Use AskUserQuestion. Label the options as ACTIONS, put "(Recommended)" on the ${p}`,`option and list it FIRST, and give them cairn's reason in your own words: ${f}.`,"","Offer exactly these:",...p==="clear"?[..._,...k]:[...k,..._],' - "Keep going" \u2014 decline. Cairn will not raise this again until the situation changes.',"",`Whichever they pick, call cairn_checkpoint FIRST and answer ${P} \u2014 including for`,"/clear, which needs it most, not least. That field is the only part of this session that","survives either option.","","You CANNOT run /clear or /compact yourself: no tool or hook can trigger a slash command.","Say so plainly and ask the user to type it.","","After they type it, cairn's SessionStart hook re-injects the checkpoint automatically, so the","next session starts already knowing where it is. If that does not happen the hook is not",'installed (`cairn install-hooks`) \u2014 in that case tell them to type "resume".',"",`Never write ${P} on their behalf, and never tell them there is nothing outstanding:`,"cairn cannot see what was never written down, so it can never issue an all-clear."].join(`
|
|
14
|
+
`),b=[u,r.source,l.recorded?l.stale?"stale":"current":"missing",l.commits??"?",p,n?be.basename(n):"-"].join("|");return{line:d,guidance:S,signature:b,state:l,recommend:p,reason:f,usage:r,bytes:a}}function ze(e,t){let n=be.join(e,Be);try{if(Y.readFileSync(n,"utf8").trim()===t)return!1}catch{}try{Y.writeFileSync(n,t,"utf8")}catch{}return!0}function Ge(e){try{Y.unlinkSync(be.join(e,Be))}catch{}}var Bt=1e3,Yt=100*1024;function zt(e,t,n,s){if(e&&typeof e=="object"&&e.ref)return e;let o=typeof e=="string"?e:String(e),r=/^\[\d{4}-\d{2}-\d{2}\]/.test(o)?o:`[${n}] ${o}`;if(r.length<=Bt)return r;let a=ne.join(t,"notes");U.mkdirSync(a,{recursive:!0});let u=`note-${new Date().toISOString().replace(/[:.]/g,"-")}-${s}.md`,i=ne.join(a,u);return U.writeFileSync(i,r,"utf8"),{description:r.split(`
|
|
15
|
+
`)[0].slice(0,120),ref:ne.join(".cairn","notes",u)}}function Ee(e,{message:t,active_files:n=[],notes:s=[],auto:o=!1,...r}){if(typeof s=="string")try{s=JSON.parse(s)}catch{s=s?[s]:[]}Array.isArray(s)||(s=[]);let a=V(),c=ne.join(a,"session.json"),u=new Date().toISOString().slice(0,10),i=new Date().toISOString(),l={};try{l=JSON.parse(U.readFileSync(c,"utf8"))||{}}catch{}let p=typeof r[P]=="string"&&r[P].trim()?r[P].trim():null,f=p??(typeof l[P]=="string"?l[P]:null),d=p?i:l[`${P}_at`]??null,m={};try{let S=e.prepare("SELECT path FROM main.files").all();for(let{path:b}of S)try{m[b]=U.statSync(b).mtimeMs}catch{}}catch{}for(let S of n)try{m[S]=U.statSync(S).mtimeMs}catch{}let _=s.map((S,b)=>zt(S,a,u,b));if(U.existsSync(c))try{if(U.statSync(c).size>Yt){let b=1;for(;U.existsSync(ne.join(a,`session.${b}.json`));)b++;U.renameSync(c,ne.join(a,`session.${b}.json`))}}catch{}let k={message:t,checkpoint_at:i,last_manual_checkpoint_at:o?l.last_manual_checkpoint_at??null:i,last_manual_message:o?l.last_manual_message??null:t,active_files:n,notes:_,[P]:f,[`${P}_at`]:d,mtime_snapshot:m};return U.writeFileSync(c,JSON.stringify(k,null,2),"utf8"),{content:[{type:"text",text:JSON.stringify({saved:!0,checkpoint_at:k.checkpoint_at,active_files_tracked:n.length,notes_saved:_.length,[P]:f?p?"recorded":"carried forward from the previous checkpoint":`not recorded \u2014 nothing will tell the next session what this one would lose (pass ${P})`},null,2)}]}}import B from"fs";import C from"path";import Gt from"crypto";var Ke=80;function $e(e){let t=C.dirname(C.resolve(e)),n=C.parse(t).root;for(;;){let s=C.join(t,".cairn");try{if(B.statSync(s).isDirectory()&&B.existsSync(C.join(s,".cairn-project"))&&s.split(C.sep).filter(r=>r===".cairn").length===1)return s}catch{}if(t===n)return null;t=C.dirname(t)}}function qe(e,t){let n=Gt.createHash("sha256").update(C.resolve(t)).digest("hex").slice(0,6);return C.join(e,"views",`${n}_${C.basename(t)}`)}function X(e){try{return JSON.parse(B.readFileSync(C.join(e,"minify-map.json"),"utf8"))}catch{return{}}}function I(e,t){B.mkdirSync(e,{recursive:!0}),B.writeFileSync(C.join(e,"minify-map.json"),JSON.stringify(t,null,2),"utf8")}function de(e,t){let n=0;for(let[s,o]of Object.entries(t)){let r=!1;if(!B.existsSync(s))r=!0;else if(!B.existsSync(o.tempPath))r=!0;else try{B.statSync(s).mtimeMs>o.minifiedAt&&(r=!0)}catch{r=!0}if(r){if(o.tempPath)try{B.unlinkSync(o.tempPath)}catch{}delete t[s],n++}}return n}function Ve(e,t){switch(t){case"javascript":case"typescript":return Re(e);case"java":return Re(e,{annotations:!0});case"python":return Xt(e);case"vue":return Zt(e);case"sql":return Qt(e);default:return e.split(`
|
|
16
16
|
`).slice(0,30).join(`
|
|
17
17
|
`)+`
|
|
18
|
-
...`}}function
|
|
19
|
-
`),s=[],o=0;for(let r=0;r<n.length;r++){let a=n[r].trim();if(!a)continue;let{opens:c,closes:u}=
|
|
20
|
-
`)}function
|
|
18
|
+
...`}}function Re(e,{annotations:t=!1}={}){let n=e.split(`
|
|
19
|
+
`),s=[],o=0;for(let r=0;r<n.length;r++){let a=n[r].trim();if(!a)continue;let{opens:c,closes:u}=Vt(a),i=c-u;if(o===0){let l=Kt(a);l!==null?s.push(`${r+1}: ${l}`):a==="}"&&s.push("}")}else if(o===1){let l=qt(a,t);l!==null&&s.push(` ${r+1}: ${l}`)}o=Math.max(0,o+i)}return s.join(`
|
|
20
|
+
`)}function Kt(e){return/^import\s/.test(e)||/^@\w+/.test(e)||/^package\s/.test(e)?e:/^export\s/.test(e)?/^export\s+(default\s+)?(async\s+)?function[\s*]|^export\s+(default\s+)?(abstract\s+)?class\s/.test(e)?H(e):e:/^(async\s+)?function[\s*]/.test(e)||/^(abstract\s+)?class\s/.test(e)||/^(public\s+|private\s+|protected\s+)?(interface|enum|type|record)\s/.test(e)||/^(const|let|var)\s+\w+/.test(e)||/^(public|private|protected|abstract|final)\s+(class|interface|enum)\s/.test(e)?H(e):e==="}"?"}":null}function qt(e,t){return t&&/^@\w+/.test(e)?e:e==="}"?"}":/^(public|private|protected|static|final|abstract|async|override|readonly|synchronized|native)\s/.test(e)||/^constructor\s*\(/.test(e)||/^(get|set)\s+\w+\s*\(/.test(e)||/^\w[\w$]*\s*[<(]/.test(e)&&!e.startsWith("return")&&!e.startsWith("throw")&&!e.startsWith("if")&&!e.startsWith("for")&&!e.startsWith("while")&&!e.startsWith("switch")&&!e.startsWith("const")&&!e.startsWith("let")&&!e.startsWith("var")?H(e):null}function H(e){let t=0,n=!1,s="";for(let o=0;o<e.length;o++){let r=e[o];if(n)r===s&&e[o-1]!=="\\"&&(n=!1);else if(r==='"'||r==="'"||r==="`")n=!0,s=r;else if(r==="{"){if(t===0)return e.slice(0,o).trimEnd();t++}else r==="}"&&t--}return e}function Vt(e){let t=0,n=0,s=!1,o="";for(let r=0;r<e.length;r++){let a=e[r];s?a===o&&e[r-1]!=="\\"&&(s=!1):a==='"'||a==="'"||a==="`"?(s=!0,o=a):a==="{"?t++:a==="}"&&n++}return{opens:t,closes:n}}function Xt(e){let t=[],n=e.split(`
|
|
21
21
|
`);for(let s=0;s<n.length;s++){let o=n[s];if(!o.trim())continue;let r=o.trimStart();r.startsWith("import ")||r.startsWith("from ")?t.push(o):(r.startsWith("class ")||r.startsWith("def ")||r.startsWith("async def "))&&t.push(`${s+1}: ${o.trim().replace(/:(\s*)$/,":")}`)}return t.join(`
|
|
22
|
-
`)}function
|
|
23
|
-
`)}function
|
|
22
|
+
`)}function Zt(e){let t=[];for(let s of e.matchAll(/<(template|script|style)(\s[^>]*)?\s*>/g))t.push(s[0]);let n=e.match(/<script(?:\s[^>]*)?\s*>([\s\S]*?)<\/script>/);if(n){let s=Re(n[1]);s.trim()&&(t.push("// script:"),t.push(s))}return t.join(`
|
|
23
|
+
`)}function Qt(e){let t=[],n=e.split(`
|
|
24
24
|
`);for(let s=0;s<n.length;s++){let o=n[s].trim();/^(CREATE|ALTER|DROP|INSERT|UPDATE|DELETE|SELECT|WITH|GRANT|REVOKE|BEGIN|COMMIT)\b/i.test(o)&&t.push(`${s+1}: ${o.slice(0,120)}${o.length>120?"...":""}`)}return t.join(`
|
|
25
|
-
`)}import D from"fs";import z from"path";var
|
|
25
|
+
`)}import D from"fs";import z from"path";var en=["preference","experience","decision","knowledge"];function le(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),n=!1,s=null,o=[],r=new Set;for(let a of t){if(/^##\s+Phases\s*$/i.test(a)){n=!0;continue}if(/^##\s+/.test(a)&&!/^###/.test(a)){s&&(o.push(s),s=null),n=!1;continue}if(!n)continue;let c=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(a);if(c){s&&o.push(s);let u=parseInt(c[1],10),i=c[2];if(r.has(u)){process.stderr.write(`[cairn] memo phase parser: duplicate Phase ${u} ignored
|
|
26
26
|
`),s=null;continue}r.add(u),s={phase_number:u,text:i,bodyLines:[]};continue}s&&s.bodyLines.push(a)}return s&&o.push(s),o.map(a=>({phase_number:a.phase_number,text:a.text,body:a.bodyLines.join(`
|
|
27
|
-
`).trim()||null}))}var
|
|
28
|
-
`)){let o=s.indexOf(":");o!==-1&&(n[s.slice(0,o).trim()]=s.slice(o+1).trim())}return n}function
|
|
29
|
-
`):["# Memory Index",""],a=[];for(let c of r){let u=
|
|
27
|
+
`).trim()||null}))}var ve=(e,t)=>e+" :: "+t;function Ze(e,t,n,s={}){let o=e.prepare("SELECT id, phase_number, status, text FROM main.roadmap_phases WHERE decision_name = ? AND phase_number IS NOT NULL ORDER BY phase_number").all(t),r=new Map(o.map(x=>[x.phase_number,x])),a=new Set,c=e.prepare("UPDATE main.roadmap_phases SET text = ?, body = ?, status = CASE WHEN status = ? THEN ? ELSE status END WHERE id = ?"),u=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, body, status, position) VALUES (?, ?, NULL, ?, ?, ?, ?)"),i=e.prepare("UPDATE main.roadmap_phases SET status = 'stale' WHERE id = ?"),l=e.prepare("UPDATE main.roadmap_phases SET phase_number = NULL WHERE id = ?"),p=e.prepare("UPDATE main.roadmap_phases SET text = ?, body = ?, status = 'open', resolved_at = NULL WHERE id = ?"),f=s.completed??null,d=f===null?null:new Set(f.map(x=>ve(x.number,x.title))),m=new Map;for(let x of f??[])x.id!=null&&m.set(ve(x.number,x.title),x.id);let _=n.some(x=>{let g=r.get(x.phase_number);return g&&g.status==="done"&&g.text!==x.text}),k=[],S=[],b=[],$=[],T=[];for(let x=0;x<n.length;x++){let g=n[x];a.add(g.phase_number);let E=r.get(g.phase_number),ie=!!E&&E.status==="done",v=ve(g.phase_number,g.text),R=d!==null&&d.has(v),O=R&&m.has(v)&&m.get(v)!==E.id,ae=R&&(_||O);if(ie&&E.text===g.text&&d!==null&&!R)p.run(g.text,g.body,E.id),k.push({id:E.id,phase_number:g.phase_number,text:g.text});else if(ie&&(E.text!==g.text||ae)){l.run(E.id);let K=u.run(t,g.phase_number,g.text,g.body,"open",x);$.push({id:Number(K.lastInsertRowid),phase_number:g.phase_number,text:g.text,replaced_shipped:E.id})}else if(E)c.run(g.text,g.body,"stale","open",E.id),b.push({id:E.id,phase_number:g.phase_number,text:g.text,reactivated:E.status==="stale"});else{let K=u.run(t,g.phase_number,g.text,g.body,"open",x);$.push({id:Number(K.lastInsertRowid),phase_number:g.phase_number,text:g.text})}}for(let[x,g]of r)!a.has(x)&&g.status!=="stale"&&g.status!=="done"&&(i.run(g.id),T.push({id:g.id,phase_number:x}));if(k.length>0||$.some(x=>x.replaced_shipped!=null)){let x=n.reduce((g,E)=>Math.max(g,E.phase_number),0);for(let[g,E]of r)E.status==="done"&&g>x&&(l.run(E.id),S.push({id:E.id,phase_number:g,text:E.text}))}return{updated:b.length,inserted:$.length,staled:T.length,repaired:k.length,retired:S.length,updated_phases:b,inserted_phases:$,staled_phases:T,repaired_phases:k,retired_phases:S}}function tn(e){let t=e.match(/^---\n([\s\S]*?)\n---/);if(!t)return{};let n={};for(let s of t[1].split(`
|
|
28
|
+
`)){let o=s.indexOf(":");o!==-1&&(n[s.slice(0,o).trim()]=s.slice(o+1).trim())}return n}function nn(e){let t=z.join(e,"MEMORY.md");return D.existsSync(t)?D.readFileSync(t,"utf8"):""}var sn=/^- \[(.+?)\]\((.+?)\)\s+—\s+(.*?)(?:\s+`\[(.+?)\]`)?\s*$/;function Qe(e){let t=sn.exec(e);return t?{name:t[1],file:t[2],description:t[3],type:t[4]||null}:null}function Xe(e){let t="- ["+e.name+"]("+e.file+") \u2014 "+e.description;return e.type?t+" `["+e.type+"]`":t}function on(e,t){let n=z.join(e,"MEMORY.md"),s=new Map(t.map(c=>[c.file,c])),o=new Set,r=D.existsSync(n)?D.readFileSync(n,"utf8").split(`
|
|
29
|
+
`):["# Memory Index",""],a=[];for(let c of r){let u=Qe(c);if(!u){a.push(c);continue}let i=s.get(u.file);!i||o.has(i.file)||(o.add(i.file),a.push(Xe(i)))}for(;a.length>0&&a[a.length-1].trim()==="";)a.pop();for(let c of t)o.has(c.file)||(o.add(c.file),a.push(Xe(c)));D.writeFileSync(n,a.join(`
|
|
30
30
|
`).replace(/\n{3,}/g,`
|
|
31
31
|
|
|
32
32
|
`)+`
|
|
33
|
-
`,"utf8")}function
|
|
34
|
-
`)){let s=
|
|
33
|
+
`,"utf8")}function rn(e){let t=[];for(let n of nn(e).split(`
|
|
34
|
+
`)){let s=Qe(n);s&&t.push(s)}return t}function an(e){let t=z.join(e,"MEMORY.bak");if(!D.existsSync(t))return t;for(let n=2;n<=1e3;n++){let s=z.join(e,"MEMORY.bak."+n);if(!D.existsSync(s))return s}return null}function et(e,{dryRun:t=!1}={}){if(!D.existsSync(e))return{repaired:!1,reason:"no memory directory",dir:e};let n=rn(e),s=new Set(n.map(i=>i.file)),o=D.readdirSync(e).filter(i=>i.endsWith(".md")&&i!=="MEMORY.md"&&!/^MEMORY\.bak(\.\d+)?\.md$/.test(i)).sort(),r=[],a=[];for(let i of o){if(s.has(i))continue;let l;try{l=tn(D.readFileSync(z.join(e,i),"utf8"))}catch(p){a.push({file:i,reason:p.message});continue}r.push({name:l.name||i.replace(/\.md$/,""),file:i,description:l.description||"(no description in frontmatter)",type:en.includes(l.type)?l.type:null})}let c=n.filter(i=>!D.existsSync(z.join(e,i.file))).map(i=>({name:i.name,file:i.file})),u=null;if(r.length>0&&!t){let i=an(e);if(i)try{D.copyFileSync(z.join(e,"MEMORY.md"),i),u=z.basename(i)}catch{}on(e,[...n,...r])}return{repaired:r.length>0&&!t,dry_run:t,dir:e,files_on_disk:o.length,already_indexed:n.length,restored:r.length,restored_entries:r.map(i=>({name:i.name,file:i.file,type:i.type})),dangling:c,unreadable:a,backup:u}}import j from"fs";import se from"path";var M="roadmap",cn=`# Roadmap
|
|
35
35
|
|
|
36
36
|
The forward-looking plan: only upcoming and in-progress phases live here. When a
|
|
37
37
|
phase is marked done (cairn_roadmap set_status <id> done) it is removed from this
|
|
@@ -42,16 +42,16 @@ the plan \u2014 Cairn syncs it into the live tracker on the next cairn_roadmap r
|
|
|
42
42
|
|
|
43
43
|
### Phase 1: First phase title
|
|
44
44
|
What this phase delivers.
|
|
45
|
-
`;function Z(){return se.join(V(),"roadmap.md")}function
|
|
46
|
-
`),"utf8")}function
|
|
45
|
+
`;function Z(){return se.join(V(),"roadmap.md")}function ln(){return se.join(V(),"roadmap_completed.md")}function fe(){let e=Z();return j.existsSync(e)?j.readFileSync(e,"utf8"):""}function Te(){let e=Z();return j.existsSync(e)?!1:(j.writeFileSync(e,cn,"utf8"),!0)}function un(e){let t=[];for(let n of e){let s=(n.text||"").trim();s&&(!Number.isInteger(n.phase_number)||n.phase_number<1||t.push({phase_number:n.phase_number,text:s,body:n.body??null}))}return t}var Oe="decision_production_roadmap.md";function pn(e){let t=e.match(/^---\n[\s\S]*?\n---\n?/);return t?e.slice(t[0].length):e}function dn(e,t){let n=se.join(e,"MEMORY.md");if(!j.existsSync(n))return;let s=j.readFileSync(n,"utf8").split(/\r?\n/).filter(o=>!o.includes(`](${t})`));j.writeFileSync(n,s.join(`
|
|
46
|
+
`),"utf8")}function fn(){let e=Z();if(j.existsSync(e))return null;let t=[],n=Se();n&&t.push(n),t.push(se.join(V(),"memory"));let s=null,o=null;for(let a of t){let c=se.join(a,Oe);s==null&&j.existsSync(c)&&(s=pn(j.readFileSync(c,"utf8")).trim(),o=c)}if(s==null)return null;let r=/^#\s/m.test(s)?"":`# Roadmap
|
|
47
47
|
|
|
48
48
|
`;j.writeFileSync(e,r+s+`
|
|
49
|
-
`,"utf8");for(let a of t){let c=se.join(a,
|
|
50
|
-
`).trim()||null})).filter(l=>l.text),s=[],o=null;for(let i of t){let l=/^(#{1,6})\s+(.+?)\s*$/.exec(i);if(l&&l[1].length>=2&&l[1].length<=4){o&&s.push(o),o={titleRaw:l[2],bodyLines:[]};continue}o&&o.bodyLines.push(i)}o&&s.push(o);let r=[];o=null;for(let i of t){let l=/^(\d+)[.)]\s+(.+?)\s*$/.exec(i);if(l){o&&r.push(o),o={titleRaw:l[2],bodyLines:[]};continue}o&&o.bodyLines.push(i)}o&&r.push(o);let a=[n(s),n(r)].filter(i=>i.length>=2);if(a.length)return a.sort((i,l)=>l.length-i.length),a[0];let c=t.find(i=>i.trim());return[{text:
|
|
49
|
+
`,"utf8");for(let a of t){let c=se.join(a,Oe);try{j.existsSync(c)&&j.unlinkSync(c)}catch{}try{dn(a,Oe)}catch{}}return{migrated:!0,from:o,to:e}}function oe(e){fn();let t=Z();if(!j.existsSync(t))return null;let n=j.readFileSync(t,"utf8");if(!/^##\s+Phases\s*$/im.test(n))return null;let s=un(le(n));return Ze(e,M,s,{completed:me()})}var mn=/^-\s+(\d{4}-\d{2}-\d{2})\s+—\s+Phase\s+(\d+):\s*(.+?)(?:\s+\(([^)]*)\))?\s*$/,tt=/^phase-(\d+)$/,hn=/^[0-9a-f]{7,40}$/;function me(){let e=ln();if(!j.existsSync(e))return null;let t=[];for(let n of j.readFileSync(e,"utf8").split(/\r?\n/)){let s=mn.exec(n);if(!s)continue;let o=s[3],r=null,a=null;if(s[4]!=null){let c=s[4].split(/\s*,\s*/).filter(Boolean);if(c.length>0&&c.every(i=>tt.test(i)||hn.test(i)))for(let i of c){let l=tt.exec(i);l?r=parseInt(l[1],10):a=i}else o=`${o} (${s[4]})`}t.push({date:s[1],number:parseInt(s[2],10),title:o,id:r,commit:a})}return t}import re from"fs";import je from"path";import{execSync as yn}from"child_process";var nt=120;function Ne(e){if(!e)return"";let t=String(e).replace(/`([^`]*)`/g,"$1").replace(/\*\*([^*]+)\*\*/g,"$1").replace(/\*([^*]+)\*/g,"$1").replace(/\[([^\]]+)\]\([^)]*\)/g,"$1").replace(/^[-*+\s]+/,"").replace(/^#{1,6}\s*/,"").replace(/[*~]/g,"").replace(/\s+/g," ").trim().replace(/[::.\s]+$/,"");return t.length>nt&&(t=t.slice(0,nt-1).trimEnd()+"\u2026"),t}function G(e){return String(e||"").toLowerCase().replace(/[^a-z0-9]+/g," ").trim()}function gn(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),n=i=>i.map(l=>({text:Ne(l.titleRaw),body:l.bodyLines.join(`
|
|
50
|
+
`).trim()||null})).filter(l=>l.text),s=[],o=null;for(let i of t){let l=/^(#{1,6})\s+(.+?)\s*$/.exec(i);if(l&&l[1].length>=2&&l[1].length<=4){o&&s.push(o),o={titleRaw:l[2],bodyLines:[]};continue}o&&o.bodyLines.push(i)}o&&s.push(o);let r=[];o=null;for(let i of t){let l=/^(\d+)[.)]\s+(.+?)\s*$/.exec(i);if(l){o&&r.push(o),o={titleRaw:l[2],bodyLines:[]};continue}o&&o.bodyLines.push(i)}o&&r.push(o);let a=[n(s),n(r)].filter(i=>i.length>=2);if(a.length)return a.sort((i,l)=>l.length-i.length),a[0];let c=t.find(i=>i.trim());return[{text:Ne(c||"Plan")||"Plan",body:e.trim()||null}]}function _n(e){let t=[];return e.forEach((n,s)=>{t.push(`### Phase ${s+1}: ${n.text}`),n.body&&n.body.trim()&&t.push(n.body.trim()),t.push("")}),t.join(`
|
|
51
51
|
`).replace(/\n{3,}/g,`
|
|
52
52
|
|
|
53
53
|
`).trimEnd()+`
|
|
54
|
-
`}function
|
|
54
|
+
`}function st(e){let t=fe(),n=t.split(/\r?\n/),s=n.findIndex(i=>/^##\s+Phases\s*$/i.test(i)),o=_n(e);if(s===-1){let i=t.replace(/\s*$/,"");re.writeFileSync(Z(),`${i}
|
|
55
55
|
|
|
56
56
|
## Phases
|
|
57
57
|
|
|
@@ -60,55 +60,55 @@ ${o}`,"utf8");return}let r=n.length;for(let i=s+1;i<n.length;i++)if(/^##\s+/.tes
|
|
|
60
60
|
`).replace(/^\n+/,""),u=`
|
|
61
61
|
|
|
62
62
|
${o}${c.trim()?`
|
|
63
|
-
`+c:""}`;re.writeFileSync(Z(),a+u,"utf8")}function
|
|
64
|
-
`).filter(Boolean)}catch{c=[]}try{re.writeFileSync(s,r,"utf8")}catch{}let u=[];try{let i=JSON.parse(re.readFileSync(o,"utf8"));Array.isArray(i)&&(u=i)}catch{}if(c.length){let i=e.prepare("SELECT id, phase_number, text FROM main.roadmap_phases WHERE decision_name = ? AND parent_phase_id IS NULL AND status IN ('open','in_progress') AND phase_number IS NOT NULL").all(M);for(let l of c){let p="";try{p=
|
|
65
|
-
`);return(t===-1?e:e.slice(0,t)).trim()}function
|
|
66
|
-
`);if(n===-1)return null;let s=e.indexOf("-->",n),o=s===-1?e.slice(0,512):e.slice(n,s),r=/^\s*migrated-from=(.+?)\s*$/m.exec(o);return r?r[1]:null}var
|
|
67
|
-
`)){let c=a.trim();if(!c){if(t.length)break;continue}t.push(c)}if(!t.length)return null;let n=
|
|
63
|
+
`+c:""}`;re.writeFileSync(Z(),a+u,"utf8")}function ot(e){let t=gn(e);if(t.length===0)return{added:[],skipped:[],total:0};let s=Te()?[]:le(fe()).map(u=>({text:u.text,body:u.body})),o=new Set(s.map(u=>G(u.text))),r=[...s],a=[],c=[];for(let u of t){let i=G(u.text);if(o.has(i)){c.push(u.text);continue}o.add(i),r.push(u),a.push(u.text)}return a.length>0&&st(r),{added:a,skipped:c,total:r.length}}function Sn(e){return e==="completed"?"done":e==="in_progress"?"in_progress":"open"}function xn(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function rt(e,t){if(!Array.isArray(t)||t.length===0)return{changed:!1};let n=t.map(f=>({text:Ne(f.content||f.activeForm||""),status:Sn(f.status)})).filter(f=>f.text);if(n.length===0)return{changed:!1};let s=e.prepare("SELECT id, status FROM main.roadmap_phases WHERE decision_name = ? AND parent_phase_id IS NULL").all(M).filter(f=>f.status!=="stale");if(s.length===0){let d=Te()?[]:le(fe()).map(b=>({text:b.text,body:b.body})),m=new Set(d.map(b=>G(b.text))),_=[...d],k=0;for(let b of n){let $=G(b.text);m.has($)||(m.add($),_.push({text:b.text,body:null}),k++)}if(k===0)return{changed:!1};st(_);let S=oe(e);return{changed:!0,seeded:k,diff:S}}let o=xn(e);o==null&&(o=s.find(f=>f.status==="in_progress")?.id??s.find(f=>f.status==="open")?.id??s[0].id);let r=e.prepare("SELECT id, text, status FROM main.roadmap_phases WHERE parent_phase_id = ?").all(o),a=new Map(r.map(f=>[G(f.text),f])),c=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (NULL, NULL, ?, ?, ?, ?)"),u=e.prepare("UPDATE main.roadmap_phases SET status = ? WHERE id = ?"),i=e.prepare("SELECT COALESCE(MAX(position), -1) + 1 AS n FROM main.roadmap_phases WHERE parent_phase_id = ?").get(o).n,l=0,p=0;for(let f of n){let d=a.get(G(f.text));if(d){if(d.status==="done"&&f.status!=="done")continue;d.status!==f.status&&(u.run(f.status,d.id),p++)}else c.run(o,f.text,f.status,i++),l++}return{changed:l>0||p>0,parentId:o,inserted:l,updated:p}}var wn=new Set(["the","and","for","with","from","into","that","this","then","your","add","fix","update","make","phase","support","implement","feature","refactor","wire","wires","should","will","also","use","using","via"]);function bn(e){return[...new Set(G(e).split(" ").filter(t=>t.length>=4&&!wn.has(t)))]}function kn(e,t){if(!e||!t?.length)return null;let n=String(e),s=/\bphase\s*#?\s*(\d+)\b/i.exec(n);if(s){let a=parseInt(s[1],10),c=t.find(u=>u.phase_number===a);if(c)return{...c,confidence:"high"}}let o=new Set(G(n).split(" ").filter(Boolean)),r=null;for(let a of t){let c=bn(a.text);if(c.length<2)continue;let u=c.filter(l=>o.has(l)),i=u.length/c.length;u.length>=2&&i>=.6&&(!r||i>r.ratio)&&(r={...a,confidence:"medium",ratio:i})}return r?(delete r.ratio,r):null}function Pe(e,t){return yn(`git ${t}`,{cwd:e,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim()}function it(e,t){let n=je.join(t,".cairn"),s=je.join(n,".last-commit"),o=je.join(n,"roadmap-pending.json"),r;try{r=Pe(t,"rev-parse HEAD")}catch{return{candidates:[],newCommits:0}}let a=null;try{a=re.readFileSync(s,"utf8").trim()||null}catch{}let c=[];if(a&&a!==r)try{c=Pe(t,`rev-list --no-merges --reverse ${a}..HEAD`).split(`
|
|
64
|
+
`).filter(Boolean)}catch{c=[]}try{re.writeFileSync(s,r,"utf8")}catch{}let u=[];try{let i=JSON.parse(re.readFileSync(o,"utf8"));Array.isArray(i)&&(u=i)}catch{}if(c.length){let i=e.prepare("SELECT id, phase_number, text FROM main.roadmap_phases WHERE decision_name = ? AND parent_phase_id IS NULL AND status IN ('open','in_progress') AND phase_number IS NOT NULL").all(M);for(let l of c){let p="";try{p=Pe(t,`show -s --format=%s%n%b ${l}`)}catch{}let f=kn(p,i);f&&!u.some(d=>d.phase_id===f.id)&&u.push({phase_id:f.id,phase_number:f.phase_number,text:f.text,commit:l.slice(0,7),confidence:f.confidence})}}if(u.length){let i=new Set(e.prepare("SELECT id FROM main.roadmap_phases WHERE status IN ('open','in_progress')").all().map(l=>l.id));u=u.filter(l=>i.has(l.phase_id))}try{re.writeFileSync(o,JSON.stringify(u),"utf8")}catch{}return{candidates:u,newCommits:c.length}}import J from"fs";import A from"path";var he="cairn",ee="roadmap/1",En="<!-- slot:roadmap";function $n(e){return`${En} format=${ee} provider=${he} generated=${e} -->`}var Rn=`<!-- ${he}:roadmap -->`,vn=/^<!--\s*slot:roadmap\b([^>]*?)-->/,On=/^<!--\s*([A-Za-z0-9_.-]+)\s*:\s*roadmap\s*-->/,Tn=/([A-Za-z_][A-Za-z0-9_-]*)=("[^"]*"|\S+)/g;function jn(e){if(typeof e!="string")return null;let t=e.indexOf(`
|
|
65
|
+
`);return(t===-1?e:e.slice(0,t)).trim()}function Pn(e){let t=jn(e);if(!t)return null;let n=vn.exec(t);if(n){let o={};for(let r of n[1].matchAll(Tn))o[r[1]]=r[2].replace(/^"|"$/g,"");return o.provider?{provider:o.provider.toLowerCase(),format:o.format||null,generated:o.generated||null}:null}let s=On.exec(t);return s?{provider:s[1].toLowerCase(),format:null,generated:null}:null}var Nn=/^```roadmap-json[ \t]*\r?\n([\s\S]*?)\r?\n```[ \t]*$/m;function lt(e){if(typeof e!="string")return null;let t=Nn.exec(e);if(!t)return null;try{let n=JSON.parse(t[1]);return n&&typeof n=="object"&&!Array.isArray(n)?n:null}catch{return null}}var Mn=new Set(["format","provider","generated","title","cursor","cursor_phase","cursor_path","phases","shipped_phases","migrated_from","migrated"]),An=new Set(["id","number","title","status","shipped","commit","goal","blocked_by","slices","detail","links"]);function ut(e,t){let n={};if(!e||typeof e!="object")return n;for(let s of Object.keys(e).sort())t.has(s)||(n[s]=e[s]);return n}function pt(e,t=new Map){if(!Array.isArray(e))return t;for(let n of e)n&&typeof n=="object"&&typeof n.id=="string"&&(t.set(n.id,n),pt(n.slices,t));return t}function Fn(e){if(typeof e!="string")return null;let t=lt(e)?.migrated_from;if(typeof t=="string"&&t)return t;let n=e.indexOf(`
|
|
66
|
+
`);if(n===-1)return null;let s=e.indexOf("-->",n),o=s===-1?e.slice(0,512):e.slice(n,s),r=/^\s*migrated-from=(.+?)\s*$/m.exec(o);return r?r[1]:null}var Ln=/\[\[([^\]|#\n]+?)(?:[|#][^\]\n]*)?\]\]/g,Cn=["decision","knowledge","experience","preference"];function Dn(e){let t=A.relative(q(),e).split(A.sep).join("/");return!t||t.startsWith("..")||A.isAbsolute(t)?null:t}function In(e){let t=A.join(q(),".cairn","memory");return[A.join(t,`${e}.md`),...Cn.map(n=>A.join(t,`${n}_${e}.md`))]}function Wn(e){if(!e)return[];let t=[],n=new Set;for(let s of String(e).matchAll(Ln)){let o=s[1].trim();if(!(!o||n.has(o))){n.add(o);for(let r of In(o)){if(!J.existsSync(r))continue;let a=Dn(r);a&&t.push({kind:"memory",title:o,path:a});break}}}return t}var Un={open:"open",in_progress:"in progress",done:"done",blocked:"blocked",stale:"stale"},Hn=new Set(["open","in_progress","done","blocked","stale"]);function Jn(e){return e==="done"?"x":e==="in_progress"?"~":e==="blocked"?"!":" "}function dt(){return A.join(q(),"ROADMAP.md")}function ft(){return new Date().toISOString().replace(/\.\d{3}Z$/,"Z")}function Bn(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function Yn(e){let t=new Map,n;try{n=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let s of n)t.has(s.blocked_id)||t.set(s.blocked_id,new Set),t.get(s.blocked_id).add(s.blocker_id);return t}function zn(e,t){let n=e.decision_name??"",s=t.decision_name??"";return n!==s?n<s?-1:1:e.phase_number!=null&&t.phase_number!=null?e.phase_number-t.phase_number:e.position-t.position||e.id-t.id}function Gn(e){let t;try{t=e.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text, body, status, position FROM main.roadmap_phases ORDER BY decision_name, phase_number, position, id").all()}catch{return null}if(t.filter(y=>y.status!=="stale").length===0)return null;let n=Bn(e),s=Yn(e),o=new Map(t.map(y=>[y.id,y.status])),r=new Map(t.map(y=>[y.id,y.phase_number!=null?`Phase ${y.phase_number}`:y.text])),a=new Map(t.map(y=>[y.id,y])),c=new Map;for(let y of t){let x=y.parent_phase_id??"root";c.has(x)||c.set(x,[]),c.get(x).push(y)}for(let y of c.values())y.sort(zn);let u=new Map,i=[];for(let y of c.get("root")||[])if(y.decision_name!=null){if(y.decision_name===M&&y.phase_number==null){y.status==="done"&&i.push(y);continue}u.has(y.decision_name)||u.set(y.decision_name,[]),u.get(y.decision_name).push(y)}let l=[...u.keys()].sort((y,x)=>y===M?-1:x===M?1:y<x?-1:y>x?1:0),p=new Map,f=!1;for(let y of l){let x=u.get(y).filter(E=>E.status!=="stale"),g=y===M?x.filter(E=>E.status!=="done"):x;g.length!==0&&(f=!0,p.set(y,{visible:g,doneCount:x.filter(E=>E.status==="done").length,total:x.length}))}if(!f)return null;let d=y=>{let x=s.get(y);return x?[...x].filter(g=>o.get(g)!=="done").map(g=>r.get(g)||`#${g}`):[]},m=y=>{let x=s.get(y);return x?[...x].filter(g=>a.has(g)).sort((g,E)=>g-E).map(g=>`phase-${g}`):[]},_=y=>(c.get(y)||[]).filter(x=>x.status!=="stale"),k=y=>c.get(y)||[],S=new Map(l.map(y=>[y,u.get(y)])),b=new Map;for(let y of me()||[])y.id!=null&&b.set(y.id,{shipped:y.date,commit:y.commit||null});return{cursor:n,decisionNames:l,visibleRoots:p,children:_,unfinishedBlockers:d,blockerIds:m,ancestry:y=>{let x=[],g=new Set,E=y;for(;E!=null&&a.has(E)&&!g.has(E);)g.add(E),x.unshift(E),E=a.get(E).parent_phase_id;return x},allChildren:k,jsonRoots:S,archivedRoots:i,shipped:y=>b.get(y)||null}}var Me=120;function Kn(e){return e.replace(/\s*\((?:see\s+)?\[\[[^\]]+\]\](?:\s*(?:,|and)\s*\[\[[^\]]+\]\])*\s*\)/gi,"").replace(/\[\[([^\]]+)\]\]/g,(t,n)=>n.replace(/_/g," ").trim()).replace(/\s{2,}/g," ").replace(/\s+([.,;:!?])/g,"$1").trim()}function mt(e){if(!e)return null;let t=[];for(let a of e.split(`
|
|
67
|
+
`)){let c=a.trim();if(!c){if(t.length)break;continue}t.push(c)}if(!t.length)return null;let n=Kn(t.join(" ").replace(/\s+/g," ")),s=null,o=/[.?!](?=\s|$)/g,r;for(;(r=o.exec(n))!==null&&(s=n.slice(0,r.index+1),!(s.length>=40)););if(s||(s=n),s.length>Me){let a=s.lastIndexOf(" ",Me-1);s=s.slice(0,a>60?a:Me-1).trimEnd()+"\u2026"}return s}var qn=8;function at(){let e=me();if(!e||e.length===0)return[];let t=e.slice(-qn).reverse(),n=["","---","","## Recently shipped",""];for(let o of t)n.push(`- ${o.date} \u2014 ${o.title}${o.commit?` \`${o.commit}\``:""}`);let s=e.length-t.length;return n.push("",s>0?`_${e.length} phases shipped in total; ${s} earlier one${s===1?"":"s"} not listed._`:`_${e.length} phase${e.length===1?"":"s"} shipped in total._`),n}function ct(e){let t=e.doneCount,n=e.total;if(!n)return"_No phases yet._";let s=Math.round(t/n*10);return`**${t} of ${n} done** \`${"\u2588".repeat(s)}${"\u2591".repeat(10-s)}\``}function Vn(e,t){let n=[$n(t.generated),"<!--"," Generated by cairn \u2014 do NOT edit by hand."," The plan lives in .cairn/roadmap.md; status and cursor come from cairn_roadmap."," This file is regenerated whenever the roadmap changes; manual edits are overwritten."," Per-phase detail and links are in the roadmap-json block at the end of this file."];if(t.migratedFrom&&(n.push(" The ROADMAP.md that was here before cairn adopted this file is kept at:"),n.push(` migrated-from=${t.migratedFrom}`)),n.push("-->","","# Roadmap",""),e==null)return n.push("_No active plan right now._"),n.push(...at()),n;let{cursor:s,decisionNames:o,visibleRoots:r,children:a,unfinishedBlockers:c}=e;n.push("Legend: `[x]` done \xB7 `[~]` in progress \xB7 `[ ]` open \xB7 `[!]` blocked.");let u=(i,l)=>{let p=" ".repeat(l),f=i.phase_number!=null?`Phase ${i.phase_number}: ${i.text}`:i.text,d=Un[i.status]||i.status,m=c(i.id);m.length&&(d+=` \xB7 waiting on ${m.join(", ")}`),s===i.id&&(d+=" \xB7 \u2190 current"),n.push(`${p}- [${Jn(i.status)}] **${f}** \u2014 ${d}`);let _=mt(i.body);_&&n.push(`${p} ${_}`);for(let k of a(i.id))u(k,l+1)};for(let i of o){let l=r.get(i);if(l){n.push(""),i===M?n.push("## Active plan","",ct(l)):n.push(`## ${i}`,"",ct(l)),n.push("");for(let p of l.visible)u(p,0)}}return n.push(...at()),n}function ht(e,t,n,s){let o=n.get(`phase-${e.id}`),r=mt(e.body),a=Wn(e.body),c=t.blockerIds(e.id),u={id:`phase-${e.id}`};e.phase_number!=null&&(u.number=e.phase_number),u.title=e.text,u.status=Hn.has(e.status)?e.status:"open";let i=e.status==="done"?t.shipped(e.id):null;if(i&&(u.shipped=i.shipped,i.commit&&(u.commit=i.commit)),r&&(u.goal=r),c.length&&(u.blocked_by=c),!s){let l=[],p=f=>{for(let d of t.allChildren(f))l.push(ht(d,t,n,!0)),p(d.id)};p(e.id),l.length&&(u.slices=l)}return e.body&&e.body.trim()&&(u.detail=e.body.trim()),a.length&&(u.links=a),{...u,...ut(o,An)}}function Xn(e,t){let n=pt(t.previous?.phases),s=[];for(let l of e?.decisionNames??[]){let p=e.jsonRoots.get(l);if(p)for(let f of p)s.push(ht(f,e,n,!1))}let o=(e?.archivedRoots??[]).map(l=>{let p=e.shipped(l.id),f={id:`phase-${l.id}`,title:l.text,status:"done"};return p&&(f.shipped=p.shipped,p.commit&&(f.commit=p.commit)),f}),r=new Set(s.map(l=>l.id)),a=e?.cursor!=null?e.ancestry(e.cursor):[],c=a.length?`phase-${a[0]}`:null,u={format:ee,provider:he,generated:t.generated,title:`${A.basename(q())} roadmap`,cursor:e?.cursor!=null?`phase-${e.cursor}`:null,cursor_phase:c!==null&&r.has(c)?c:null,cursor_path:a.length?a.map(l=>`phase-${l}`):null};t.migratedFrom&&(u.migrated_from=t.migratedFrom,t.migrated&&(u.migrated=t.migrated)),u.phases=s,u.shipped_phases=o;let i={...u,...ut(t.previous,Mn)};return["","<details>","<summary>Machine-readable plan (roadmap/1)</summary>","","```roadmap-json",JSON.stringify(i,null,2),"```","","</details>"]}function Ae(e,t={}){let n={generated:t.generated||ft(),migratedFrom:t.migratedFrom||null,migrated:t.migrated||null,previous:t.previous&&typeof t.previous=="object"?t.previous:null},s=Gn(e);return s==null&&!t.allowEmpty?null:[...Vn(s,n),...Xn(s,n)].join(`
|
|
68
68
|
`)+`
|
|
69
|
-
`}function
|
|
70
|
-
`);let n=null;try{n=
|
|
69
|
+
`}function Zn(e){let t=A.join(e,"ROADMAP.bak.md");if(!J.existsSync(t))return t;for(let n=2;n<=1e3;n++){let s=A.join(e,`ROADMAP.bak.${n}.md`);if(!J.existsSync(s))return s}return null}function Qn(e,t){let n=A.join(A.dirname(e),`.${A.basename(e)}.${process.pid}.tmp`);try{J.writeFileSync(n,t,"utf8"),J.renameSync(n,e)}catch(s){try{J.unlinkSync(n)}catch{}throw s}}function Q(e,t,n={}){return{written:!1,reason:e,warning:`[cairn] ROADMAP.md was not updated \u2014 ${e}`,path:t,...n}}function yt(e){let t=dt(),n=null;if(J.existsSync(t))try{n=J.readFileSync(t,"utf8")}catch(_){return Q(`the existing file could not be read (${_.message})`,t)}let s=Pn(n),o=n!=null&&(s?.provider===he||n.startsWith(Rn));if(n!=null&&s&&!o)return{written:!1,reason:`ROADMAP.md is generated by another provider (${s.provider}) \u2014 refusing to overwrite`,conflict_provider:s.provider,warning:`[cairn] ROADMAP.md is published by "${s.provider}", so cairn left it untouched and the file may not reflect cairn's plan. Two roadmap providers are claiming the same file \u2014 decide which one owns it.`,path:t};let r=o?lt(n):null,a=o?Fn(n):null,c=o&&typeof r?.migrated=="string"?r.migrated:null,u=typeof r?.generated=="string"?r.generated:null,i=!1,l=Ae(e,{generated:u||"1970-01-01T00:00:00Z",migratedFrom:a,migrated:c,previous:r});if(l==null){if(!o)return{written:!1,reason:"empty roadmap \u2014 no file written",path:t};i=!0,l=Ae(e,{generated:u||"1970-01-01T00:00:00Z",migratedFrom:a,migrated:c,previous:r,allowEmpty:!0})}if(o&&n===l)return{written:!1,unchanged:!0,path:t,bytes:Buffer.byteLength(l)};let p=ft(),f=null,d=null;if(n!=null&&!o){let _=Zn(A.dirname(t));if(_==null)return Q("no free ROADMAP.bak name was available to preserve the existing file",t);try{J.renameSync(t,_)}catch(k){return Q(`the existing file could not be preserved (${k.message})`,t)}f=A.basename(_),d=_,a=f,c=p}l=Ae(e,{generated:p,migratedFrom:a,migrated:c,previous:r,allowEmpty:i});try{Qn(t,l)}catch(_){if(d)try{return J.renameSync(d,t),Q(`write failed (${_.message}); the original ROADMAP.md was restored`,t)}catch{return Q(`write failed (${_.message}); the original ROADMAP.md is at ${f} and must be restored by hand`,t,{backed_up_to:f})}return Q(`write failed (${_.message})`,t)}let m={written:!0,path:t,bytes:Buffer.byteLength(l),format:ee,generated:p};return f&&(m.backed_up_to=f,m.warning=`[cairn] The existing ROADMAP.md was not cairn-generated. It has been preserved as ${f}, and ROADMAP.md is now generated from cairn's roadmap.`),m}function es(e){try{return yt(e)}catch(t){process.stderr.write(`[cairn] ROADMAP.md publish skipped: ${t.message}
|
|
70
|
+
`);let n=null;try{n=dt()}catch{}return Q(`publish failed (${t.message})`,n)}}function ye(e){let t=es(e);return t&&t.warning?t.warning:null}var ts=new Set(["open","in_progress"]);function ns(e,t){e.prepare("INSERT INTO main.roadmap_cursor (singleton, phase_id) VALUES (1, ?) ON CONFLICT(singleton) DO UPDATE SET phase_id = excluded.phase_id").run(t)}function ss(e,t){let n=e.prepare("SELECT parent_phase_id, status FROM main.roadmap_phases WHERE id = ?"),s=e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'"),o=n.get(t);for(;o&&o.parent_phase_id!=null;)s.run(o.parent_phase_id),o=n.get(o.parent_phase_id)}function gt(e,t){ns(e,t),e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'").run(t),ss(e,t)}function os(e){let t=new Map,n;try{n=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let s of n)t.has(s.blocked_id)||t.set(s.blocked_id,new Set),t.get(s.blocked_id).add(s.blocker_id);return t}function rs(e,t,n){let s=t.get(e);if(!s||s.size===0)return[];let o=[];for(let r of s)n.get(r)!=="done"&&o.push(r);return o}function _t(e,t){let n=e.prepare("SELECT id, decision_name, phase_number, parent_phase_id, status, position, text FROM main.roadmap_phases ORDER BY decision_name, phase_number, position, id").all();if(n.length===0)return{id:null,reason:"No phases \u2014 add a ## Phases section to .cairn/roadmap.md."};let s=new Map(n.map(p=>[p.id,p.status])),o=os(e),r=new Map;for(let p of n){let f=p.parent_phase_id??`root:${p.decision_name}`;r.has(f)||r.set(f,[]),r.get(f).push(p)}for(let p of r.values())p.sort((f,d)=>f.phase_number!=null&&d.phase_number!=null?f.phase_number-d.phase_number:f.position-d.position||f.id-d.id);let a=[],c=new Set;for(let p of n){if(p.parent_phase_id!=null||c.has(p.decision_name))continue;c.add(p.decision_name);let f=[],d=r.get(`root:${p.decision_name}`)||[];for(let m=d.length-1;m>=0;m--)f.push(d[m]);for(;f.length;){let m=f.pop();a.push(m);let _=r.get(m.id)||[];for(let k=_.length-1;k>=0;k--)f.push(_[k])}}let u=p=>ts.has(p.status),i=[],l=0;if(t!=null){let p=a.findIndex(f=>f.id===t);l=p===-1?0:p+1}for(let p=l;p<a.length;p++){let f=a[p];if(!u(f))continue;let d=rs(f.id,o,s);if(d.length>0){i.push({id:f.id,blockers:d});continue}let m=t!=null?n.find(S=>S.id===t):null,_=m&&m.decision_name!==f.decision_name,k;return t==null?k=`First actionable phase: ${f.decision_name??"(sub-task)"} / Phase ${f.phase_number??"\xB7"}.`:_?k=`Previous decision "${m.decision_name}" is complete \u2014 advanced to "${f.decision_name}".`:k=`Next open phase in "${f.decision_name??"(sub-task)"}" after id ${t}.`,i.length>0&&(k+=` Skipped ${i.length} dep-blocked phase(s): ${i.map(S=>`id ${S.id} (blocked by ${S.blockers.join(",")})`).join("; ")}.`),{id:f.id,reason:k,skipped_blocked:i}}return i.length>0?{id:null,reason:`All remaining open phases are blocked by unfinished dependencies: ${i.map(p=>`id ${p.id} (blocked by ${p.blockers.join(",")})`).join("; ")}.`,skipped_blocked:i}:{id:null,reason:"No actionable phases remaining."}}var as=new Set(["java","typescript","javascript","vue","python","sql"]);function ue(e,t){let n={hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:e};t&&(n.additionalContext=t),process.stdout.write(JSON.stringify({decision:"block",reason:e,hookSpecificOutput:n})),process.exit(0)}var bt={name:"cairn",contract:1,title:"cairn",slots:["roadmap"],provides:{roadmap:{path:"ROADMAP.md",format:ee}},spec:"Project memory and roadmap. Publishes the plan as ROADMAP.md in the roadmap/1 slot format."};function cs(){let e=w.join(te.homedir(),".claude.json");if(!h.existsSync(e))return!1;let t;try{t=JSON.parse(h.readFileSync(e,"utf8"))}catch{return!1}let n=t?.mcpServers?.cairn;if(!n||typeof n!="object")return!1;let s=n.terminalConnect,o=s?.provides?.roadmap?.format;if(s&&o===ee)return!1;n.terminalConnect=bt;try{return h.writeFileSync(e,JSON.stringify(t,null,2),"utf8"),!0}catch{return!1}}function kt(e){let t=w.join(te.homedir(),".claude","projects",_e(e));return h.existsSync(t)?w.join(t,"memory"):null}function Et(e){try{return h.readdirSync(e).filter(t=>t.endsWith(".md"))}catch{return[]}}function ls(e){let t=kt(e);if(!t||!h.existsSync(t))return 0;let n=w.join(e,".cairn","memory");h.mkdirSync(n,{recursive:!0});let s=0;for(let o of Et(t)){let r=w.join(t,o),a;try{a=h.statSync(r)}catch{continue}if(!a.isFile())continue;let c=w.join(n,o),u=!h.existsSync(c);if(!u)try{u=a.mtimeMs>h.statSync(c).mtimeMs}catch{u=!0}if(u){h.copyFileSync(r,c);try{h.utimesSync(c,a.atime,a.mtime)}catch{}s++}}return s}function us(e){let t=kt(e);if(!t||(h.mkdirSync(t,{recursive:!0}),Et(t).some(o=>o!=="MEMORY.md")))return 0;let n=w.join(e,".cairn","memory");if(!h.existsSync(n))return 0;let s=0;for(let o of h.readdirSync(n)){let r=w.join(n,o),a;try{a=h.statSync(r)}catch{continue}if(a.isFile()){h.copyFileSync(r,w.join(t,o));try{h.utimesSync(w.join(t,o),a.atime,a.mtime)}catch{}s++}}return s}function St(e,t){let n=new Date(t.checkpoint_at).toLocaleString(),r=[` message: ${/^Auto-checkpoint\b/.test(t.message||"")&&t.last_manual_message||t.message}`];r.push(` would_lose: ${ke(ce(e,t))}`),r.push(` checkpoint: ${n}`),t.active_files?.length>0&&r.push(` active_files: ${t.active_files.join(", ")}`),t.notes?.length>0&&r.push(` notes: ${t.notes.join(" | ")}`);let a=i=>{try{return is(i,{cwd:e,encoding:"utf8",timeout:5e3,stdio:["ignore","pipe","ignore"]}).trim()}catch{return null}};try{let i=new Date(t.checkpoint_at).toISOString(),l=a(`git log --oneline --after="${i}"`);l!==null&&r.push(` commits_since: ${l?l.split(`
|
|
71
71
|
`).join("; "):"(none)"}`)}catch{}let c=a("git diff --name-only HEAD");c&&r.push(` uncommitted: ${c.split(`
|
|
72
72
|
`).join(", ")}`);let u=a("git status --short");return u&&r.push(` git_status:
|
|
73
73
|
${u.split(`
|
|
74
74
|
`).map(i=>" "+i).join(`
|
|
75
|
-
`)}`),r.push(" \u2192 Call cairn_resume for full index status and open todos."),r}async function
|
|
76
|
-
`).trim()||null}try{let u=o.prepare("SELECT phase_id FROM roadmap_cursor WHERE singleton = 1").get()?.phase_id??null;if(u==null)return o.close(),t;let i=o.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text, status, position FROM roadmap_phases ORDER BY decision_name, phase_number, position, id").all(),l={open:"[ ]",in_progress:"[~]",done:"[x]",blocked:"[!]",stale:"[s]"},p=new Map;for(let v of i){let R=v.parent_phase_id??"root";p.has(R)||p.set(R,[]),p.get(R).push(v)}for(let v of p.values())v.sort((R,O)=>R.decision_name&&O.decision_name&&R.decision_name!==O.decision_name?R.decision_name.localeCompare(O.decision_name):R.phase_number!=null&&O.phase_number!=null?R.phase_number-O.phase_number:R.position-O.position||R.id-O.id);let f=p.get("root")||[],d=new Map;for(let v of f)v.decision_name!=null&&(d.has(v.decision_name)||d.set(v.decision_name,[]),d.get(v.decision_name).push(v));let m=[],_=(v,R)=>{for(let O of p.get(v)||[]){if(O.status==="stale")continue;let ae=u===O.id?" \u2192":"",K=O.phase_number!=null?`Phase ${O.phase_number}: ${O.text}`:O.text;m.push(`${" ".repeat(R)}${l[O.status]||"[?]"}${ae} ${K}`),_(O.id,R+1)}};for(let[v,R]of d){let O=R.filter(N=>N.status!=="stale");if(O.length===0)continue;let ae=O.every(N=>N.status==="done"),K=O.some(N=>N.status==="in_progress")
|
|
75
|
+
`)}`),r.push(" \u2192 Call cairn_resume for full index status and open todos."),r}async function xt(e){let t={active:!1},n=w.join(e,"index.db");if(!h.existsSync(n))return t;let s;try{({DatabaseSync:s}=await import("node:sqlite"))}catch{return t}let o;try{o=new s(n,{readonly:!0})}catch{return t}let r=c=>c.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"");function a(c,u){if(!h.existsSync(c))return null;let i=h.readFileSync(c,"utf8"),l=!1,p=!1,f=[];for(let d of i.split(/\r?\n/)){if(/^##\s+Phases\s*$/i.test(d)){l=!0;continue}if(l&&/^##\s+/.test(d)&&!/^###/.test(d))break;if(!l)continue;let m=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(d);if(m){if(p)break;parseInt(m[1],10)===u&&(f.push(`### Phase ${m[1]}: ${m[2]}`),p=!0);continue}p&&f.push(d)}return f.join(`
|
|
76
|
+
`).trim()||null}try{let u=o.prepare("SELECT phase_id FROM roadmap_cursor WHERE singleton = 1").get()?.phase_id??null;if(u==null)return o.close(),t;let i=o.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text, status, position FROM roadmap_phases ORDER BY decision_name, phase_number, position, id").all(),l={open:"[ ]",in_progress:"[~]",done:"[x]",blocked:"[!]",stale:"[s]"},p=new Map;for(let v of i){let R=v.parent_phase_id??"root";p.has(R)||p.set(R,[]),p.get(R).push(v)}for(let v of p.values())v.sort((R,O)=>R.decision_name&&O.decision_name&&R.decision_name!==O.decision_name?R.decision_name.localeCompare(O.decision_name):R.phase_number!=null&&O.phase_number!=null?R.phase_number-O.phase_number:R.position-O.position||R.id-O.id);let f=p.get("root")||[],d=new Map;for(let v of f)v.decision_name!=null&&(d.has(v.decision_name)||d.set(v.decision_name,[]),d.get(v.decision_name).push(v));let m=[],_=(v,R)=>{for(let O of p.get(v)||[]){if(O.status==="stale")continue;let ae=u===O.id?" \u2192":"",K=O.phase_number!=null?`Phase ${O.phase_number}: ${O.text}`:O.text;m.push(`${" ".repeat(R)}${l[O.status]||"[?]"}${ae} ${K}`),_(O.id,R+1)}};for(let[v,R]of d){let O=R.filter(N=>N.status!=="stale");if(O.length===0)continue;let ae=O.every(N=>N.status==="done"),K=O.some(N=>N.status==="in_progress"),$t=ae?"done":K?"in_progress":"open",Rt=O.filter(N=>N.status==="done").length,vt=v===M?"Roadmap":`Decision: ${v}`;m.push(`${l[$t]} ${vt} (${Rt}/${O.length} done)`);for(let N of R){if(N.status==="stale"||v===M&&N.status==="done")continue;let Ot=u===N.id?" \u2192":"";m.push(` ${l[N.status]||"[?]"}${Ot} Phase ${N.phase_number}: ${N.text}`),_(N.id,2)}}let k=v=>i.find(R=>R.id===v),S=k(u),b=S?.decision_name??null,$=S?.phase_number??null,T=S;for(;T&&T.parent_phase_id!=null&&(T=k(T.parent_phase_id),!!T);)b=T.decision_name??b,$=T.phase_number??$;o.close();let y=S?S.phase_number!=null?`${S.decision_name} / Phase ${S.phase_number}: ${S.text}`:`${b} / Phase ${$} \u2192 ${S.text}`:"(unknown)",x=w.join(e,".last-roadmap-cursor"),g=null;try{g=parseInt(h.readFileSync(x,"utf8").trim(),10)}catch{}let E=g!==u;try{h.writeFileSync(x,String(u),"utf8")}catch{}let ie=null;if(E&&b!=null&&$!=null){let v=b===M?w.join(e,"roadmap.md"):w.join(e,"memory",`decision_${r(b)}.md`),R=a(v,$);R&&(ie=R.split(`
|
|
77
77
|
`))}return{active:!0,currentLabel:y,treeLines:m,advanced:E,memoLines:ie}}catch{try{o?.close()}catch{}return t}}process.on("uncaughtException",e=>{process.stderr.write(`cairn: ${e.message}
|
|
78
|
-
`),process.exit(0)});var F=process.argv[2];if(F==="--version"||F==="-v"){let e=JSON.parse(h.readFileSync(new URL("../package.json",import.meta.url),"utf8"));console.log(e.version),process.exit(0)}else if(F==="minify"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t,n;try{n=JSON.parse(e)?.tool_input||{},t=n.file_path}catch{process.exit(0)}t||process.exit(0),(n.offset!=null||n.limit!=null)&&process.exit(0),t=w.resolve(t);let s
|
|
79
|
-
`).length,m=p==="vue"?
|
|
80
|
-
`).length,k=d>0?Math.round((1-_/d)*100):0,S=
|
|
78
|
+
`),process.exit(0)});var F=process.argv[2];if(F==="--version"||F==="-v"){let e=JSON.parse(h.readFileSync(new URL("../package.json",import.meta.url),"utf8"));console.log(e.version),process.exit(0)}else if(F==="minify"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t,n;try{n=JSON.parse(e)?.tool_input||{},t=n.file_path}catch{process.exit(0)}t||process.exit(0),(n.offset!=null||n.limit!=null)&&process.exit(0),t=w.resolve(t);let s=$e(t);s||process.exit(0);let o=w.join(s,"views"),r=w.relative(o,t);!r.startsWith("..")&&!w.isAbsolute(r)&&process.exit(0);let a=X(s),u=de(s,a)>0,i=a[t];i?.state==="edit-ready"&&(u&&I(s,a),process.exit(0)),i?.state==="compressed"&&(i.state="edit-ready",I(s,a),process.exit(0));let l=w.extname(t).toLowerCase(),p=Fe[l];(!p||!as.has(p))&&(u&&I(s,a),process.exit(0));let f;try{f=h.readFileSync(t,"utf8")}catch{u&&I(s,a),process.exit(0)}let d=f.split(`
|
|
79
|
+
`).length,m=p==="vue"?Le(f):pe(f,p),_=m.split(`
|
|
80
|
+
`).length,k=d>0?Math.round((1-_/d)*100):0,S=qe(s,t);h.mkdirSync(w.dirname(S),{recursive:!0}),h.writeFileSync(S,m,"utf8");let b=i?.readCount??0;a[t]={tempPath:S,state:"compressed",minifiedAt:h.statSync(t).mtimeMs,readCount:b+1},I(s,a);let $=_>Ke,T=b>0;if(T||$){let y=Ve(m,p),x=T?`[cairn: ${d} \u2192 ${_} lines | seen \xD7${b+1} this session]`:`[cairn: ${d} \u2192 outline (${_} minified lines)]`;ue(`[cairn] Expected step, not a failure: ${w.basename(t)} was served as an outline (${d} lines \u2192 ${_} minified). The outline is in context; Read("${S}") for the full minified view.`,`${x}
|
|
81
81
|
${y}
|
|
82
82
|
Full minified view: Read("${S}")`)}else ue(`[cairn] Expected step, not a failure: ${w.basename(t)} was served minified (${d} \u2192 ${_} lines, -${k}%). The content is in context; Read("${S}") for the cached view.`,`[cairn: ${d} \u2192 ${_} lines (-${k}%)]
|
|
83
83
|
${m}
|
|
84
|
-
Cached view: Read("${S}")`)})}else if(F==="edit-guard"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t="(unknown)",n="Edit";try{let p=JSON.parse(e);t=p?.tool_input?.file_path||"(unknown)",n=p?.tool_name||"Edit"}catch{}let s=w.resolve(t),o
|
|
84
|
+
Cached view: Read("${S}")`)})}else if(F==="edit-guard"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t="(unknown)",n="Edit";try{let p=JSON.parse(e);t=p?.tool_input?.file_path||"(unknown)",n=p?.tool_name||"Edit"}catch{}let s=w.resolve(t),o=$e(s);o||process.exit(0);let r=w.join(o,"views"),a=w.relative(r,s);if(!a.startsWith("..")&&!w.isAbsolute(a)){let p=X(o),f=Object.entries(p).find(([,m])=>m.tempPath===s),d=f?`Edit the source file instead: Edit("${f[0]}")`:"Edit the original source file, not the .cairn/views/ cache.";ue(`[cairn] That path is a cached view, not the source \u2014 it is read-only and is overwritten on the next read. ${d}`)}let c=X(o),u=de(o,c),i=c[s];i||(u>0&&I(o,c),process.exit(0)),n==="Write"&&(delete c[s],I(o,c),process.exit(0)),i.state==="edit-ready"&&(delete c[s],I(o,c),process.exit(0)),i.state="edit-ready",I(o,c);let l=null;try{let p=h.readFileSync(s,"utf8");p.split(`
|
|
85
85
|
`).length<=300&&(l=p)}catch{}l!==null?ue(`[cairn] Expected step, not a failure: this file was shown compressed, so the edit was built from partial text. The full source has been provided in context \u2014 re-apply the same edit, or Read("${t}") first if you do not see it.`,`[cairn] Full source of ${t} (the compressed view you saw is not editable text \u2014 use the exact content below, then re-apply your Edit; the guard is now cleared):
|
|
86
86
|
|
|
87
|
-
${l}`):ue(`[cairn] Expected step, not a failure: this file was shown compressed. Read("${t}") to load the real source (it comes through in full now), then re-apply the same edit.`)})}else if(F==="roadmap-capture"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",async()=>{let t;try{t=JSON.parse(e)}catch{process.exit(0)}let n=t?.tool_name||"",s=t?.tool_input||{},o=process.cwd(),r=w.join(o,".cairn");h.existsSync(r)||process.exit(0);let a=w.join(r,"index.db");h.existsSync(a)||process.exit(0);let c;try{({DatabaseSync:c}=await import("node:sqlite"))}catch{process.exit(0)}let u=/exit.?plan.?mode/i.test(n),i=/todo.?write/i.test(n);!u&&!i&&process.exit(0);let l=null,p;try{if(u){let d=s.plan;(typeof d!="string"||!d.trim())&&process.exit(0);let m=
|
|
88
|
-
`);try{p?.close()}catch{}process.exit(0)}let f=p?
|
|
89
|
-
`),process.exit(0)}else if(F==="checkpoint"&&process.argv[3]==="--auto"){let e=`Auto-checkpoint at ${new Date().toISOString()}`,t=[],n=[];try{let s=w.join(process.cwd(),".cairn"),o=w.join(s,"session.json");h.existsSync(o)&&(t=JSON.parse(h.readFileSync(o,"utf8")).notes||[]);let r=X(s);n=Object.entries(r).filter(([,a])=>(a.readCount??0)>0).map(([a])=>a)}catch{}
|
|
87
|
+
${l}`):ue(`[cairn] Expected step, not a failure: this file was shown compressed. Read("${t}") to load the real source (it comes through in full now), then re-apply the same edit.`)})}else if(F==="roadmap-capture"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",async()=>{let t;try{t=JSON.parse(e)}catch{process.exit(0)}let n=t?.tool_name||"",s=t?.tool_input||{},o=process.cwd(),r=w.join(o,".cairn");h.existsSync(r)||process.exit(0);let a=w.join(r,"index.db");h.existsSync(a)||process.exit(0);let c;try{({DatabaseSync:c}=await import("node:sqlite"))}catch{process.exit(0)}let u=/exit.?plan.?mode/i.test(n),i=/todo.?write/i.test(n);!u&&!i&&process.exit(0);let l=null,p;try{if(u){let d=s.plan;(typeof d!="string"||!d.trim())&&process.exit(0);let m=ot(d);if(p=new c(a),oe(p),(p.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null)==null){let k=_t(p,null);k.id!=null&>(p,k.id)}m.added.length>0&&(l=`[cairn] roadmap: captured ${m.added.length} phase(s) from plan${m.skipped.length?` (${m.skipped.length} already present)`:""}.`)}else if(i){p=new c(a);let d=rt(p,s.todos);d.seeded?l=`[cairn] roadmap: seeded ${d.seeded} phase(s) from todos.`:d.inserted>0&&(l=`[cairn] roadmap: linked ${d.inserted} new sub-task(s) to the current phase.`)}}catch(d){process.stderr.write(`[cairn] roadmap-capture skipped: ${d.message}
|
|
88
|
+
`);try{p?.close()}catch{}process.exit(0)}let f=p?ye(p):null;try{p?.close()}catch{}f&&(l=l?`${l} ${f}`:f),l&&process.stdout.write(JSON.stringify({systemMessage:l})),process.exit(0)})}else if(F==="validate-map"){let e=w.join(process.cwd(),".cairn");h.existsSync(e)||process.exit(0);let t=X(e),n=de(e,t);for(let s of Object.values(t))s.readCount=0;I(e,t),n>0&&process.stdout.write(`[cairn] Cleaned minify map: ${n} stale entr${n===1?"y":"ies"} removed
|
|
89
|
+
`),process.exit(0)}else if(F==="checkpoint"&&process.argv[3]==="--auto"){let e=`Auto-checkpoint at ${new Date().toISOString()}`,t=[],n=[];try{let s=w.join(process.cwd(),".cairn"),o=w.join(s,"session.json");h.existsSync(o)&&(t=JSON.parse(h.readFileSync(o,"utf8")).notes||[]);let r=X(s);n=Object.entries(r).filter(([,a])=>(a.readCount??0)>0).map(([a])=>a)}catch{}Ee(null,{message:e,active_files:n,notes:t,auto:!0}),process.exit(0)}else if(F==="session-tick"){let e=process.cwd(),t=w.join(e,".cairn");if(h.existsSync(t)||process.exit(0),!process.stdin.isTTY)try{h.readFileSync(0,"utf8")}catch{}let n=[];try{let c=`Auto-checkpoint at ${new Date().toISOString()}`,u=[],i=w.join(t,"session.json");if(h.existsSync(i))try{u=JSON.parse(h.readFileSync(i,"utf8")).notes||[]}catch{}let l=X(t),p=Object.entries(l).filter(([,f])=>(f.readCount??0)>0).map(([f])=>f);Ee(null,{message:c,active_files:p,notes:u,auto:!0}),n.push("\u2713 checkpoint saved")}catch{}try{let c=ls(e);c>0&&n.push(`memory backed up (${c})`)}catch{}let s=[],o=null;try{let c=w.join(t,"index.db");if(h.existsSync(c)){let{DatabaseSync:u}=await import("node:sqlite"),i=new u(c);try{oe(i),o=ye(i);try{s=it(i,e).candidates}catch{}}finally{i.close()}}}catch{}let r="";try{let c=await xt(t);if(c.active){n.push(`roadmap ${c.advanced?"updated":"on track"}: ${c.currentLabel}`);let u=["[cairn] Production roadmap (\u2192 = cursor / current phase):",...c.treeLines.map(i=>" "+i)];c.memoLines&&u.push("","\u2500\u2500 Current phase detail (from roadmap.md) \u2500\u2500",...c.memoLines,"\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500"),u.push("","Keep this current via cairn_roadmap on MATERIAL change (phase done / blocked / new) \u2014 not every turn."),r=u.join(`
|
|
90
90
|
`)}}catch{}if(o&&(n.push("ROADMAP.md not updated \u2014 see warning"),r=r?`${r}
|
|
91
91
|
|
|
92
92
|
${o}`:o),s.length>0){n.push(`${s.length} phase(s) may be done \u2014 confirm with user`);let c=["","[cairn] Possible COMPLETED phase(s) detected from recent commits \u2014","CONFIRM WITH THE USER before marking done; do NOT mark done unilaterally:"];for(let u of s)c.push(` \u2022 Phase ${u.phase_number}: ${u.text} (commit ${u.commit}, ${u.confidence}) \u2192 if confirmed: cairn_roadmap set_status ${u.phase_id} done`);r=r?r+`
|
|
93
93
|
`+c.join(`
|
|
94
94
|
`):c.join(`
|
|
95
|
-
`).trimStart()}n.length===0&&process.exit(0);let a={systemMessage:`[cairn] ${n.join(" \xB7 ")}`};r&&(a.additionalContext=r),process.stdout.write(JSON.stringify(a)),process.exit(0)}else if(F==="repair-memory"){let e=process.argv.includes("--dry-run"),t=process.cwd();h.existsSync(w.join(t,".cairn"))||(console.error("[cairn] Not a cairn project (no .cairn/ here). Run this from the project root."),process.exit(1)),
|
|
95
|
+
`).trimStart()}n.length===0&&process.exit(0);let a={systemMessage:`[cairn] ${n.join(" \xB7 ")}`};r&&(a.additionalContext=r),process.stdout.write(JSON.stringify(a)),process.exit(0)}else if(F==="repair-memory"){let e=process.argv.includes("--dry-run"),t=process.cwd();h.existsSync(w.join(t,".cairn"))||(console.error("[cairn] Not a cairn project (no .cairn/ here). Run this from the project root."),process.exit(1)),De(t);let n=et(xe(),{dryRun:e});if(n.reason&&(console.error(`[cairn] ${n.reason}: ${n.dir}`),process.exit(1)),console.log(`Memory index: ${n.dir}`),console.log(` files on disk : ${n.files_on_disk}`),console.log(` already listed: ${n.already_indexed}`),console.log(` missing : ${n.restored}`),n.restored>0){for(let s of n.restored_entries.slice(0,20))console.log(` + ${s.name}${s.type?" ["+s.type+"]":""} (${s.file})`);n.restored_entries.length>20&&console.log(` \u2026 and ${n.restored_entries.length-20} more`)}if(n.dangling.length>0){console.log(` dangling : ${n.dangling.length} pointer(s) whose file is gone (left alone)`);for(let s of n.dangling.slice(0,10))console.log(` ? ${s.name} (${s.file})`)}n.unreadable.length>0&&console.log(` unreadable : ${n.unreadable.length} file(s) skipped`),e?console.log(n.restored>0?`
|
|
96
96
|
Dry run \u2014 nothing written. Re-run without --dry-run to restore them.`:`
|
|
97
97
|
Dry run \u2014 nothing to restore.`):n.repaired?console.log(`
|
|
98
98
|
Restored ${n.restored} pointer(s). Previous index kept as ${n.backup}.`):console.log(`
|
|
99
|
-
Index already lists every memory file. Nothing to do.`),process.exit(0)}else if(F==="memory-restore"){let e=process.cwd();h.existsSync(w.join(e,".cairn"))||process.exit(0);try{let t=
|
|
99
|
+
Index already lists every memory file. Nothing to do.`),process.exit(0)}else if(F==="memory-restore"){let e=process.cwd();h.existsSync(w.join(e,".cairn"))||process.exit(0);try{let t=us(e);t>0&&process.stdout.write(`[cairn] Restored ${t} memory file(s) from .cairn/memory backup (Claude store was empty)
|
|
100
100
|
`)}catch{}process.exit(0)}else if(F==="resume-hint"){let e=w.join(process.cwd(),".cairn"),t=w.join(e,"session.json"),n=w.join(e,"index.db"),s=w.join(e,".hint-lock"),o=1800*1e3,r=process.cwd()===te.homedir();if(!r&&h.existsSync(e)&&(h.existsSync(n)||h.existsSync(t))){let S=w.join(e,".cairn-project");if(!h.existsSync(S))try{h.writeFileSync(S,"","utf8"),process.stdout.write(`[cairn] Repaired: wrote .cairn-project sentinel for hook compatibility.
|
|
101
|
-
`)}catch{}}if(!r&&h.existsSync(e))try{
|
|
102
|
-
`)}catch{}if(!r&&h.existsSync(n))try{let{DatabaseSync:S}=await import("node:sqlite"),b=new S(n,{readonly:!0}),$=null;try{$=
|
|
101
|
+
`)}catch{}}if(!r&&h.existsSync(e))try{cs()&&process.stdout.write(`[cairn] Repaired: declared the roadmap slot (${ee}) in ~/.claude.json.
|
|
102
|
+
`)}catch{}if(!r&&h.existsSync(n))try{let{DatabaseSync:S}=await import("node:sqlite"),b=new S(n,{readonly:!0}),$=null;try{$=ye(b)}finally{b.close()}$&&process.stdout.write(`${$}
|
|
103
103
|
`)}catch{}{let S=[w.join(process.cwd(),".claude","settings.json"),w.join(te.homedir(),".claude","settings.json")];for(let b of S)try{if(!h.existsSync(b))continue;let $=h.readFileSync(b,"utf8"),T=JSON.parse($),y=T?.hooks?.PreToolUse;if(!Array.isArray(y))continue;let x=!1;for(let g of y)if(!(g.matcher!=="Edit"||!Array.isArray(g.hooks)))for(let E of g.hooks)E.command==="cairn set-mode edit"&&(E.command="cairn edit-guard",x=!0);x&&(h.writeFileSync(b,JSON.stringify(T,null,2),"utf8"),process.stdout.write(`[cairn] Repaired: migrated cairn set-mode \u2192 cairn edit-guard in ${b}
|
|
104
104
|
`))}catch{}}let a="",c="";if(!process.stdin.isTTY)try{let S=JSON.parse(h.readFileSync(0,"utf8"));a=(S?.prompt||"").toLowerCase(),typeof S?.transcript_path=="string"&&(c=S.transcript_path)}catch{}let i=/\b(resume|continue|pick up|where (were|did) (we|i)|last session|carry on|what('s| is) next|what (were|was) (we|i)|start|begin)\b/.test(a);h.existsSync(t)||(h.existsSync(n)||(h.existsSync(s)&&Date.now()-h.statSync(s).mtimeMs<o&&process.exit(0),h.mkdirSync(e,{recursive:!0}),h.writeFileSync(s,new Date().toISOString(),"utf8"),process.stdout.write(`[cairn] No index found. Run cairn_maintain to index this project.
|
|
105
|
-
`)),process.exit(0));let l=null;try{let S=JSON.parse(h.readFileSync(t,"utf8")),b=
|
|
105
|
+
`)),process.exit(0));let l=null;try{let S=JSON.parse(h.readFileSync(t,"utf8")),b=Ye({cwd:process.cwd(),session:S,transcriptPath:c});b&&ze(e,b.signature)&&(l=`${b.line}
|
|
106
106
|
${b.guidance}`)}catch{}h.existsSync(s)&&Date.now()-h.statSync(s).mtimeMs<o&&(l&&process.stdout.write(l+`
|
|
107
|
-
`),process.exit(0));let p=JSON.parse(h.readFileSync(t,"utf8")),f=new Date(p.checkpoint_at).toLocaleString(),d=/^Auto-checkpoint\b/.test(p.message||""),m=d&&p.last_manual_message||p.message,_=d&&p.last_manual_checkpoint_at?new Date(p.last_manual_checkpoint_at).toLocaleString():f,k=[`[cairn] Prior session: "${m}" (${_}).`,"[cairn] SESSION: ALWAYS call cairn_resume FIRST when asked to resume, continue, or recall prior work \u2014 do NOT substitute git log or file reads. cairn_resume includes commits_since_checkpoint so you can cross-reference checkpoint notes against what was shipped. At end of session, and before any /clear or /compact, call cairn_checkpoint with message + active_files + notes + would_be_lost (what a fresh session would NOT know).","[cairn] TOOLS: NEVER use Bash to run cat, grep, head, tail, find, or ls for file inspection \u2014 always use the Read, Grep, or Glob tools instead. Prefer cairn_search / cairn_bundle / cairn_describe over manual file browsing. Use cairn_outline for structural overview, cairn_todos for backlog, cairn_security before PRs.","[cairn] MEMORY: IMPORTANT \u2014 call cairn_memo IMMEDIATELY and SILENTLY (do NOT ask the user, do NOT list what you are about to save, do NOT confirm afterward) when you observe \u2014 preference: user corrects/confirms your behaviour; experience: non-trivial problem solved; decision: architectural choice made; knowledge: non-obvious codebase fact discovered. cairn_employ_memory is explicit only \u2014 never call it unless asked.",`[cairn] CONTEXT: you cannot see your own token usage, but cairn can and does \u2014 it measures this session's real usage from the transcript on EVERY prompt and prints a "[cairn] Stopping point:" block here once it passes 90% of the context window, with instructions to offer the user a checkpoint + /clear or /compact. So: if that block is absent you are not near the limit, and if the user asks about context, say cairn is watching it and will say so at 90% \u2014 do NOT reply that cairn has no such feature, and do NOT estimate a percentage yourself. When the block does appear, act on it before anything else.`];if(!i){let S=ce(process.cwd(),p);S.recorded&&k.push(`[cairn] ${
|
|
107
|
+
`),process.exit(0));let p=JSON.parse(h.readFileSync(t,"utf8")),f=new Date(p.checkpoint_at).toLocaleString(),d=/^Auto-checkpoint\b/.test(p.message||""),m=d&&p.last_manual_message||p.message,_=d&&p.last_manual_checkpoint_at?new Date(p.last_manual_checkpoint_at).toLocaleString():f,k=[`[cairn] Prior session: "${m}" (${_}).`,"[cairn] SESSION: ALWAYS call cairn_resume FIRST when asked to resume, continue, or recall prior work \u2014 do NOT substitute git log or file reads. cairn_resume includes commits_since_checkpoint so you can cross-reference checkpoint notes against what was shipped. At end of session, and before any /clear or /compact, call cairn_checkpoint with message + active_files + notes + would_be_lost (what a fresh session would NOT know).","[cairn] TOOLS: NEVER use Bash to run cat, grep, head, tail, find, or ls for file inspection \u2014 always use the Read, Grep, or Glob tools instead. Prefer cairn_search / cairn_bundle / cairn_describe over manual file browsing. Use cairn_outline for structural overview, cairn_todos for backlog, cairn_security before PRs.","[cairn] MEMORY: IMPORTANT \u2014 call cairn_memo IMMEDIATELY and SILENTLY (do NOT ask the user, do NOT list what you are about to save, do NOT confirm afterward) when you observe \u2014 preference: user corrects/confirms your behaviour; experience: non-trivial problem solved; decision: architectural choice made; knowledge: non-obvious codebase fact discovered. cairn_employ_memory is explicit only \u2014 never call it unless asked.",`[cairn] CONTEXT: you cannot see your own token usage, but cairn can and does \u2014 it measures this session's real usage from the transcript on EVERY prompt and prints a "[cairn] Stopping point:" block here once it passes 90% of the context window, with instructions to offer the user a checkpoint + /clear or /compact. So: if that block is absent you are not near the limit, and if the user asks about context, say cairn is watching it and will say so at 90% \u2014 do NOT reply that cairn has no such feature, and do NOT estimate a percentage yourself. When the block does appear, act on it before anything else.`];if(!i){let S=ce(process.cwd(),p);S.recorded&&k.push(`[cairn] ${ke(S)}`)}i&&k.push(["[cairn] RESUME \u2014 checkpoint loaded inline:",...St(process.cwd(),p)].join(`
|
|
108
108
|
`)),l&&k.push(l),process.stdout.write(k.join(`
|
|
109
109
|
`)+`
|
|
110
|
-
`),h.writeFileSync(s,new Date().toISOString(),"utf8"),process.exit(0)}else if(F==="session-start"){let e={};if(!process.stdin.isTTY)try{e=JSON.parse(h.readFileSync(0,"utf8"))||{}}catch{}let t=typeof e.cwd=="string"&&e.cwd?e.cwd:process.cwd(),n=w.join(t,".cairn"),s=w.join(n,"session.json");h.existsSync(s)||process.exit(0);let o=typeof e.source=="string"?e.source:"",r;try{r=JSON.parse(h.readFileSync(s,"utf8"))}catch{process.exit(0)}
|
|
110
|
+
`),h.writeFileSync(s,new Date().toISOString(),"utf8"),process.exit(0)}else if(F==="session-start"){let e={};if(!process.stdin.isTTY)try{e=JSON.parse(h.readFileSync(0,"utf8"))||{}}catch{}let t=typeof e.cwd=="string"&&e.cwd?e.cwd:process.cwd(),n=w.join(t,".cairn"),s=w.join(n,"session.json");h.existsSync(s)||process.exit(0);let o=typeof e.source=="string"?e.source:"",r;try{r=JSON.parse(h.readFileSync(s,"utf8"))}catch{process.exit(0)}Ge(n);let a=/^Auto-checkpoint\b/.test(r.message||""),c=a&&r.last_manual_message||r.message||"(no message recorded)",u=o==="compact"?"a /compact":o==="clear"?"a /clear":o==="fork"?"a fork":o==="resume"?"a resume":"a restart",i=[`[cairn] Session restored automatically after ${u} \u2014 the user did not have to ask:`,...St(t,r),"","This IS the prior session state. Do not re-derive it from git log or file reads, do not ask","the user what they were doing, and do not ask whether to continue \u2014 it is above, and the",'client that opened this session already said "resume" on their behalf. If their first',"message continues this work, carry on from would_lose without making them ask twice.","","If would_lose says nothing was recorded, treat that as a gap rather than an all-clear: cairn","cannot see what the previous session never wrote down."].join(`
|
|
111
111
|
`),l=(b,$)=>{let T=String(b).replace(/\s+/g," ").trim();return T.length<=$?T:T.slice(0,$-1)+"\u2026"},p=ce(t,r),f=a&&r.last_manual_checkpoint_at||r.checkpoint_at,d=f?new Date(f).toLocaleString():"time unknown",m=[`[cairn] Checkpoint restored after ${u} \u2014 saved ${d}`];m.push(` Working on: ${l(c,140)}`),m.push(p.recorded?` Would be lost: ${l(p.text,200)}`+(p.stale?` (written before the ${p.commits} commit(s) since \u2014 may be stale)`:""):" Would be lost: NOT RECORDED \u2014 nothing says what the last session would lose.");let _=[];r.active_files?.length&&_.push(`${r.active_files.length} active file(s)`),r.notes?.length&&_.push(`${r.notes.length} note(s)`),_.push("full detail"),m.push(` Type "resume" for: ${_.join(", ")}.`);let k=m.join(`
|
|
112
|
-
`),S=["[cairn] Session restored from checkpoint.","Lead your next reply with ONE short sentence naming what the previous session left","unfinished, then carry straight on with it. Do NOT ask whether to continue, do NOT ask","what they were working on, and do NOT offer a menu \u2014 the answer is in the context you","were given, so use it rather than re-deriving it. If the user has asked for something,","answer that instead, immediately after the sentence: this must never delay, replace or","override their request."].join(" ");process.stdout.write(JSON.stringify({systemMessage:k,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:i,initialUserMessage:S}})),process.exit(0)}else if(F==="roadmap-hint"){let e=w.join(process.cwd(),".cairn"),t=await
|
|
112
|
+
`),S=["[cairn] Session restored from checkpoint.","Lead your next reply with ONE short sentence naming what the previous session left","unfinished, then carry straight on with it. Do NOT ask whether to continue, do NOT ask","what they were working on, and do NOT offer a menu \u2014 the answer is in the context you","were given, so use it rather than re-deriving it. If the user has asked for something,","answer that instead, immediately after the sentence: this must never delay, replace or","override their request."].join(" ");process.stdout.write(JSON.stringify({systemMessage:k,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:i,initialUserMessage:S}})),process.exit(0)}else if(F==="roadmap-hint"){let e=w.join(process.cwd(),".cairn"),t=await xt(e);t.active||process.exit(0);let n=["[cairn] ROADMAP \u2014 cursor is active.",...t.treeLines.map(s=>"[cairn] "+s),`[cairn] Current: ${t.currentLabel}`];if(t.memoLines){n.push("[cairn] \u2500\u2500\u2500\u2500\u2500 Decision memo (current phase) \u2500\u2500\u2500\u2500\u2500");for(let s of t.memoLines)n.push("[cairn] "+s);n.push("[cairn] \u2500\u2500\u2500\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500\u2500\u2500\u2500")}n.push("[cairn] Use cairn_roadmap set_status <id> done (auto-advances), focus <id>, or next to move cursor."),process.stdout.write(n.join(`
|
|
113
113
|
`)+`
|
|
114
|
-
`),process.exit(0)}else if(F==="install"){let e=process.platform,t=w.join(te.homedir(),".claude.json"),n={};if(h.existsSync(t))try{n=JSON.parse(h.readFileSync(t,"utf8"))}catch{}n.mcpServers=n.mcpServers||{};let s=!!n.mcpServers.cairn;n.mcpServers.cairn={...s&&typeof n.mcpServers.cairn=="object"?n.mcpServers.cairn:{},command:"cairn-mcp",terminalConnect:
|
|
114
|
+
`),process.exit(0)}else if(F==="install"){let e=process.platform,t=w.join(te.homedir(),".claude.json"),n={};if(h.existsSync(t))try{n=JSON.parse(h.readFileSync(t,"utf8"))}catch{}n.mcpServers=n.mcpServers||{};let s=!!n.mcpServers.cairn;n.mcpServers.cairn={...s&&typeof n.mcpServers.cairn=="object"?n.mcpServers.cairn:{},command:"cairn-mcp",terminalConnect:bt},h.writeFileSync(t,JSON.stringify(n,null,2),"utf8");let o=w.join(te.homedir(),".claude");wt(o,!0),console.log("Cairn installed successfully!"),console.log(""),console.log(` Platform : ${e}`),console.log(` MCP : cairn-mcp registered in ${t}${s?" (updated)":""}`),console.log(` Hooks : installed in ${w.join(o,"settings.json")}`),console.log(""),console.log("Active hooks:"),console.log(" PreToolUse[Read] -> cairn minify (compress source files for reading)"),console.log(" PreToolUse[Edit] -> cairn edit-guard (block Edit until re-read with full content)"),console.log(" PreToolUse[Write] -> cairn edit-guard (clear minify state before full file overwrite)"),console.log(" PostToolUse[ExitPlanMode] -> cairn roadmap-capture (approved plan -> roadmap phases)"),console.log(" PostToolUse[TodoWrite] -> cairn roadmap-capture (todos -> roadmap sub-tasks)"),console.log(" Stop -> cairn session-tick (checkpoint + memory backup + roadmap, surfaced)"),console.log(" UserPromptSubmit -> cairn resume-hint (remind Claude of prior session)"),console.log(" UserPromptSubmit -> cairn memory-restore (restore .cairn/memory -> Claude store if empty)"),console.log(" SessionStart -> cairn session-start (re-inject checkpoint on restart / clear / compact / fork)"),console.log(""),console.log("Restart Claude Code to activate the MCP server, then open any project and start working."),console.log("Upgrade any time with: npm install -g @misterhuydo/cairn-mcp"),process.exit(0)}else if(F==="install-hooks"){let e=process.argv.includes("--global"),t=e?w.join(te.homedir(),".claude"):w.join(process.cwd(),".claude");wt(t,e),console.log(`Cairn hooks installed in ${e?"global (~/.claude/settings.json)":"project (.claude/settings.json)"}`),console.log(""),console.log("Active hooks:"),console.log(" PreToolUse[Read] -> cairn minify (compress source files for reading)"),console.log(" PreToolUse[Edit] -> cairn edit-guard (block Edit until re-read with full content)"),console.log(" PreToolUse[Write] -> cairn edit-guard (clear minify state before full file overwrite)"),console.log(" PostToolUse[ExitPlanMode] -> cairn roadmap-capture (approved plan -> roadmap phases)"),console.log(" PostToolUse[TodoWrite] -> cairn roadmap-capture (todos -> roadmap sub-tasks)"),console.log(" Stop -> cairn session-tick (checkpoint + memory backup + roadmap, surfaced)"),console.log(" UserPromptSubmit -> cairn resume-hint (remind Claude of prior session)"),console.log(" UserPromptSubmit -> cairn memory-restore (restore .cairn/memory -> Claude store if empty)"),console.log(" SessionStart -> cairn session-start (re-inject checkpoint on restart / clear / compact / fork)"),process.exit(0)}else console.error("Usage: cairn <--version | install | install-hooks [--global] | minify | edit-guard | validate-map | checkpoint --auto | session-tick | session-start | resume-hint | roadmap-hint | roadmap-capture | memory-restore | repair-memory [--dry-run]>"),process.exit(1);function wt(e,t){let n=w.join(e,"settings.json"),s={};if(h.existsSync(n))try{s=JSON.parse(h.readFileSync(n,"utf8"))}catch{}s.hooks=s.hooks||{};let o=s.hooks.PreToolUse||[];o.some(d=>d.matcher==="Read"&&d.hooks?.some(m=>m.command==="cairn minify"))||o.push({matcher:"Read",hooks:[{type:"command",command:"cairn minify"}]});let r=o.filter(d=>!(d.matcher==="Edit"&&d.hooks?.some(m=>m.command==="cairn set-mode edit")));r.some(d=>d.matcher==="Edit"&&d.hooks?.some(m=>m.command==="cairn edit-guard"))||r.push({matcher:"Edit",hooks:[{type:"command",command:"cairn edit-guard"}]}),r.some(d=>d.matcher==="Write"&&d.hooks?.some(m=>m.command==="cairn edit-guard"))||r.push({matcher:"Write",hooks:[{type:"command",command:"cairn edit-guard"}]}),s.hooks.PreToolUse=r;let a=(d,m)=>(d||[]).map(_=>({..._,hooks:(_.hooks||[]).filter(k=>!m(k.command))})).filter(_=>(_.hooks||[]).length>0),c=d=>typeof d=="string"&&/sync-memory-(forward|restore)\.(mjs|py)/.test(d),u=a(s.hooks.Stop,d=>d==="cairn checkpoint --auto"||d==="cairn roadmap-hint");u.some(d=>d.hooks?.some(m=>m.command==="cairn session-tick"))||u.push({hooks:[{type:"command",command:"cairn session-tick"}]}),s.hooks.Stop=u;let i=a(s.hooks.UserPromptSubmit,c);i.some(d=>d.hooks?.some(m=>m.command==="cairn resume-hint"))||i.push({hooks:[{type:"command",command:"cairn resume-hint"}]}),i.some(d=>d.hooks?.some(m=>m.command==="cairn memory-restore"))||i.push({hooks:[{type:"command",command:"cairn memory-restore"}]}),s.hooks.UserPromptSubmit=i;let l=a(s.hooks.SessionStart,d=>d==="cairn session-start");l.push({hooks:[{type:"command",command:"cairn session-start"}]}),s.hooks.SessionStart=l;let p=a(s.hooks.PostToolUse,c),f=d=>{p.some(m=>m.matcher===d&&m.hooks?.some(_=>_.command==="cairn roadmap-capture"))||p.push({matcher:d,hooks:[{type:"command",command:"cairn roadmap-capture"}]})};if(f("ExitPlanMode"),f("TodoWrite"),s.hooks.PostToolUse=p,t){let d=w.join(e,"scripts");for(let m of["sync-memory-forward.mjs","sync-memory-restore.mjs","sync-memory-forward.py","sync-memory-restore.py"]){let _=w.join(d,m);if(h.existsSync(_))try{h.unlinkSync(_)}catch{}}}if(!t){let d=w.join(process.cwd(),".cairn");h.mkdirSync(d,{recursive:!0});let m=w.join(d,".cairn-project");h.existsSync(m)||h.writeFileSync(m,"","utf8")}h.mkdirSync(e,{recursive:!0}),h.writeFileSync(n,JSON.stringify(s,null,2),"utf8")}
|