@misterhuydo/cairn-mcp 1.14.1 → 1.16.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 +10 -0
- package/dist/cairn-cli.js +58 -54
- package/dist/index.js +60 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,8 +166,18 @@ cairn_roadmap { action: "focus", id: 3 } → make phase 3 the current f
|
|
|
166
166
|
cairn_roadmap { action: "set_status", id: 3, status: "done" } → ship it
|
|
167
167
|
cairn_roadmap { action: "add", parent: 3, text: "..." } → ad-hoc sub-task under a phase
|
|
168
168
|
cairn_roadmap { action: "deps_add", from: 2, to: 4 } → phase 2 blocks phase 4
|
|
169
|
+
cairn_roadmap { action: "publish" } → (re)write the human-readable ROADMAP.md at the repo root
|
|
169
170
|
```
|
|
170
171
|
|
|
172
|
+
The live plan is also projected into a legible **`ROADMAP.md` at the repo root** — a
|
|
173
|
+
human-readable view with checkboxes, per-item status, and a `← current` marker on the
|
|
174
|
+
cursor phase. It refreshes automatically on session activity (the end-of-turn hook and
|
|
175
|
+
the first prompt of a session) as well as on every `cairn_roadmap` call, so a workspace
|
|
176
|
+
that has a roadmap but hasn't touched the roadmap tool this session still shows a
|
|
177
|
+
populated file. It is deterministic (no timestamps, stable ordering, so an unchanged
|
|
178
|
+
plan re-writes byte-identically and leaves clean git diffs), never written as an empty
|
|
179
|
+
placeholder, and never clobbers a hand-authored `ROADMAP.md`.
|
|
180
|
+
|
|
171
181
|
Editing `roadmap.md` *is* editing the plan — read actions re-seed from it, and the
|
|
172
182
|
end-of-turn hook folds in any hand-edits. Marking a phase **done** prunes it from
|
|
173
183
|
`roadmap.md` and appends it to the append-only `.cairn/roadmap_completed.md`, so the
|
package/dist/cairn-cli.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
2
|
+
import h from"fs";import B from"os";import y from"path";import{execSync as me}from"child_process";import Ft from"fast-glob";var ge={".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 V(e,t,s={}){let{noComments:n=!0,noEmptyLines:o=!0,aggressive:a=!1}=s,i=e;return n&&(i=Ue(i,t)),o&&(i=He(i)),i=Je(i),a&&(i=Be(i)),i.trim()}function Ue(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,(s,n)=>n||!s.startsWith("//")?s:"");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 He(e){return e.split(`
|
|
3
3
|
`).filter(t=>t.trim().length>0).join(`
|
|
4
|
-
`)}function
|
|
4
|
+
`)}function Je(e){return e.split(`
|
|
5
5
|
`).map(t=>t.trimEnd()).join(`
|
|
6
|
-
`)}function
|
|
6
|
+
`)}function Be(e){return e.replace(/\s+/g," ").replace(/\s*([=+\-*/%<>!&|^~?:;,{}()[\]])\s*/g,"$1").replace(/;\s*}/g,"}").replace(/\(\s+/g,"(").replace(/\s+\)/g,")").trim()}function _e(e,t={}){let{noStyle:s=!1,noComments:n=!0,noEmptyLines:o=!0,aggressive:a=!1}=t,i=[],p=e.match(/<template>([\s\S]*?)<\/template>/i);if(p){let c=p[1];n&&(c=c.replace(/<!--[\s\S]*?-->/g,"")),o&&(c=c.split(`
|
|
7
7
|
`).filter(m=>m.trim()).join(`
|
|
8
|
-
`)),c=c.replace(/\s+/g," ").trim(),c&&i.push(`<template>${c}</template>`)}let
|
|
8
|
+
`)),c=c.replace(/\s+/g," ").trim(),c&&i.push(`<template>${c}</template>`)}let f=e.match(/<script([^>]*)>([\s\S]*?)<\/script>/i);if(f){let c=f[1],m=V(f[2],"javascript",{noComments:n,noEmptyLines:o,aggressive:a}),u=/setup/i.test(c)?"<script setup>":"<script>";m&&i.push(`${u}${m}</script>`)}if(!s){let c=e.match(/<style([^>]*)>([\s\S]*?)<\/style>/i);if(c){let m=c[2].replace(/\/\*[\s\S]*?\*\//g,"").replace(/\s+/g," ").trim();m&&i.push(`<style${c[1]}>${m}</style>`)}}return i.join(`
|
|
9
9
|
|
|
10
|
-
`)}import L from"fs";import
|
|
11
|
-
`)[0].slice(0,120),ref:
|
|
10
|
+
`)}import L from"fs";import W from"path";import z from"fs";import Ye from"os";import Y from"path";var Ge=null;function K(){return Ge||process.cwd()}function I(){let e=Y.join(K(),".cairn");z.mkdirSync(Y.join(e,"bundles"),{recursive:!0});let t=Y.join(e,".cairn-project");return z.existsSync(t)||z.writeFileSync(t,"","utf8"),e}function se(e){return e.replace(/[:/\\]/g,"-").replace(/^-+/,"")}function Se(){let e=Y.join(Ye.homedir(),".claude","projects",se(K()));return z.existsSync(e)?Y.join(e,"memory"):null}var qe=1e3,Ve=100*1024;function ze(e,t,s,n){if(e&&typeof e=="object"&&e.ref)return e;let o=typeof e=="string"?e:String(e),a=/^\[\d{4}-\d{2}-\d{2}\]/.test(o)?o:`[${s}] ${o}`;if(a.length<=qe)return a;let i=W.join(t,"notes");L.mkdirSync(i,{recursive:!0});let f=`note-${new Date().toISOString().replace(/[:.]/g,"-")}-${n}.md`,c=W.join(i,f);return L.writeFileSync(c,a,"utf8"),{description:a.split(`
|
|
11
|
+
`)[0].slice(0,120),ref:W.join(".cairn","notes",f)}}function oe(e,{message:t,active_files:s=[],notes:n=[]}){if(typeof n=="string")try{n=JSON.parse(n)}catch{n=n?[n]:[]}Array.isArray(n)||(n=[]);let o=I(),a=W.join(o,"session.json"),i=new Date().toISOString().slice(0,10),p={};try{let m=e.prepare("SELECT path FROM main.files").all();for(let{path:u}of m)try{p[u]=L.statSync(u).mtimeMs}catch{}}catch{}for(let m of s)try{p[m]=L.statSync(m).mtimeMs}catch{}let f=n.map((m,u)=>ze(m,o,i,u));if(L.existsSync(a))try{if(L.statSync(a).size>Ve){let u=1;for(;L.existsSync(W.join(o,`session.${u}.json`));)u++;L.renameSync(a,W.join(o,`session.${u}.json`))}}catch{}let c={message:t,checkpoint_at:new Date().toISOString(),active_files:s,notes:f,mtime_snapshot:p};return L.writeFileSync(a,JSON.stringify(c,null,2),"utf8"),{content:[{type:"text",text:JSON.stringify({saved:!0,checkpoint_at:c.checkpoint_at,active_files_tracked:s.length,notes_saved:f.length},null,2)}]}}import F from"fs";import O from"path";import Ke from"crypto";var xe=80;function re(e){let t=O.dirname(O.resolve(e)),s=O.parse(t).root;for(;;){let n=O.join(t,".cairn");try{if(F.statSync(n).isDirectory()&&F.existsSync(O.join(n,".cairn-project"))&&n.split(O.sep).filter(a=>a===".cairn").length===1)return n}catch{}if(t===s)return null;t=O.dirname(t)}}function we(e,t){let s=Ke.createHash("sha256").update(O.resolve(t)).digest("hex").slice(0,6);return O.join(e,"views",`${s}_${O.basename(t)}`)}function D(e){try{return JSON.parse(F.readFileSync(O.join(e,"minify-map.json"),"utf8"))}catch{return{}}}function T(e,t){F.mkdirSync(e,{recursive:!0}),F.writeFileSync(O.join(e,"minify-map.json"),JSON.stringify(t,null,2),"utf8")}function X(e,t){let s=0;for(let[n,o]of Object.entries(t)){let a=!1;if(!F.existsSync(n))a=!0;else if(!F.existsSync(o.tempPath))a=!0;else try{F.statSync(n).mtimeMs>o.minifiedAt&&(a=!0)}catch{a=!0}if(a){if(o.tempPath)try{F.unlinkSync(o.tempPath)}catch{}delete t[n],s++}}return s}function Ee(e,t){switch(t){case"javascript":case"typescript":return ie(e);case"java":return ie(e,{annotations:!0});case"python":return et(e);case"vue":return tt(e);case"sql":return nt(e);default:return e.split(`
|
|
12
12
|
`).slice(0,30).join(`
|
|
13
13
|
`)+`
|
|
14
|
-
...`}}function
|
|
15
|
-
`),n=[],o=0;for(let a=0;a<s.length;a++){let i=s[a].trim();if(!i)continue;let{opens:
|
|
16
|
-
`)}function
|
|
14
|
+
...`}}function ie(e,{annotations:t=!1}={}){let s=e.split(`
|
|
15
|
+
`),n=[],o=0;for(let a=0;a<s.length;a++){let i=s[a].trim();if(!i)continue;let{opens:p,closes:f}=Ze(i),c=p-f;if(o===0){let m=Xe(i);m!==null?n.push(`${a+1}: ${m}`):i==="}"&&n.push("}")}else if(o===1){let m=Qe(i,t);m!==null&&n.push(` ${a+1}: ${m}`)}o=Math.max(0,o+c)}return n.join(`
|
|
16
|
+
`)}function Xe(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)?M(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)?M(e):e==="}"?"}":null}function Qe(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")?M(e):null}function M(e){let t=0,s=!1,n="";for(let o=0;o<e.length;o++){let a=e[o];if(s)a===n&&e[o-1]!=="\\"&&(s=!1);else if(a==='"'||a==="'"||a==="`")s=!0,n=a;else if(a==="{"){if(t===0)return e.slice(0,o).trimEnd();t++}else a==="}"&&t--}return e}function Ze(e){let t=0,s=0,n=!1,o="";for(let a=0;a<e.length;a++){let i=e[a];n?i===o&&e[a-1]!=="\\"&&(n=!1):i==='"'||i==="'"||i==="`"?(n=!0,o=i):i==="{"?t++:i==="}"&&s++}return{opens:t,closes:s}}function et(e){let t=[],s=e.split(`
|
|
17
17
|
`);for(let n=0;n<s.length;n++){let o=s[n];if(!o.trim())continue;let a=o.trimStart();a.startsWith("import ")||a.startsWith("from ")?t.push(o):(a.startsWith("class ")||a.startsWith("def ")||a.startsWith("async def "))&&t.push(`${n+1}: ${o.trim().replace(/:(\s*)$/,":")}`)}return t.join(`
|
|
18
|
-
`)}function
|
|
19
|
-
`)}function
|
|
18
|
+
`)}function tt(e){let t=[];for(let n of e.matchAll(/<(template|script|style)(\s[^>]*)?\s*>/g))t.push(n[0]);let s=e.match(/<script(?:\s[^>]*)?\s*>([\s\S]*?)<\/script>/);if(s){let n=ie(s[1]);n.trim()&&(t.push("// script:"),t.push(n))}return t.join(`
|
|
19
|
+
`)}function nt(e){let t=[],s=e.split(`
|
|
20
20
|
`);for(let n=0;n<s.length;n++){let o=s[n].trim();/^(CREATE|ALTER|DROP|INSERT|UPDATE|DELETE|SELECT|WITH|GRANT|REVOKE|BEGIN|COMMIT)\b/i.test(o)&&t.push(`${n+1}: ${o.slice(0,120)}${o.length>120?"...":""}`)}return t.join(`
|
|
21
|
-
`)}import
|
|
22
|
-
`),n=null;continue}a.add(
|
|
23
|
-
`).trim()||null}))}function
|
|
21
|
+
`)}import k from"fs";import U from"path";function G(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),s=!1,n=null,o=[],a=new Set;for(let i of t){if(/^##\s+Phases\s*$/i.test(i)){s=!0;continue}if(/^##\s+/.test(i)&&!/^###/.test(i)){n&&(o.push(n),n=null),s=!1;continue}if(!s)continue;let p=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(i);if(p){n&&o.push(n);let f=parseInt(p[1],10),c=p[2];if(a.has(f)){process.stderr.write(`[cairn] memo phase parser: duplicate Phase ${f} ignored
|
|
22
|
+
`),n=null;continue}a.add(f),n={phase_number:f,text:c,bodyLines:[]};continue}n&&n.bodyLines.push(i)}return n&&o.push(n),o.map(i=>({phase_number:i.phase_number,text:i.text,body:i.bodyLines.join(`
|
|
23
|
+
`).trim()||null}))}function be(e,t,s){let n=e.prepare("SELECT id, phase_number, status FROM main.roadmap_phases WHERE decision_name = ? ORDER BY phase_number").all(t),o=new Map(n.map(r=>[r.phase_number,r])),a=new Set,i=e.prepare("UPDATE main.roadmap_phases SET text = ?, body = ?, status = CASE WHEN status = ? THEN ? ELSE status END WHERE id = ?"),p=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, body, status, position) VALUES (?, ?, NULL, ?, ?, ?, ?)"),f=e.prepare("UPDATE main.roadmap_phases SET status = 'stale' WHERE id = ?"),c=[],m=[],u=[];for(let r=0;r<s.length;r++){let l=s[r];a.add(l.phase_number);let d=o.get(l.phase_number);if(d)i.run(l.text,l.body,"stale","open",d.id),c.push({id:d.id,phase_number:l.phase_number,text:l.text,reactivated:d.status==="stale"});else{let g=p.run(t,l.phase_number,l.text,l.body,"open",r);m.push({id:Number(g.lastInsertRowid),phase_number:l.phase_number,text:l.text})}}for(let[r,l]of o)!a.has(r)&&l.status!=="stale"&&l.status!=="done"&&(f.run(l.id),u.push({id:l.id,phase_number:r}));return{updated:c.length,inserted:m.length,staled:u.length,updated_phases:c,inserted_phases:m,staled_phases:u}}var P="roadmap",st=`# Roadmap
|
|
24
24
|
|
|
25
25
|
The forward-looking plan: only upcoming and in-progress phases live here. When a
|
|
26
26
|
phase is marked done (cairn_roadmap set_status <id> done) it is removed from this
|
|
@@ -31,63 +31,67 @@ the plan \u2014 Cairn syncs it into the live tracker on the next cairn_roadmap r
|
|
|
31
31
|
|
|
32
32
|
### Phase 1: First phase title
|
|
33
33
|
What this phase delivers.
|
|
34
|
-
`;function
|
|
35
|
-
`),"utf8")}function
|
|
34
|
+
`;function C(){return U.join(I(),"roadmap.md")}function ot(){return U.join(I(),"roadmap_completed.md")}function Q(){let e=C();return k.existsSync(e)?k.readFileSync(e,"utf8"):""}function ce(){let e=C();return k.existsSync(e)?!1:(k.writeFileSync(e,st,"utf8"),!0)}function rt(e){let t=[];for(let s of e){let n=(s.text||"").trim();n&&(!Number.isInteger(s.phase_number)||s.phase_number<1||t.push({phase_number:s.phase_number,text:n,body:s.body??null}))}return t}var ae="decision_production_roadmap.md";function it(e){let t=e.match(/^---\n[\s\S]*?\n---\n?/);return t?e.slice(t[0].length):e}function at(e,t){let s=U.join(e,"MEMORY.md");if(!k.existsSync(s))return;let n=k.readFileSync(s,"utf8").split(/\r?\n/).filter(o=>!o.includes(`](${t})`));k.writeFileSync(s,n.join(`
|
|
35
|
+
`),"utf8")}function ct(){let e=C();if(k.existsSync(e))return null;let t=[],s=Se();s&&t.push(s),t.push(U.join(I(),"memory"));let n=null,o=null;for(let i of t){let p=U.join(i,ae);n==null&&k.existsSync(p)&&(n=it(k.readFileSync(p,"utf8")).trim(),o=p)}if(n==null)return null;let a=/^#\s/m.test(n)?"":`# Roadmap
|
|
36
36
|
|
|
37
|
-
`;
|
|
38
|
-
`,"utf8");for(let i of t){let
|
|
39
|
-
`).trim()||null})).filter(m=>m.text),n=[],o=null;for(let c of t){let m=/^(#{1,6})\s+(.+?)\s*$/.exec(c);if(m&&m[1].length>=2&&m[1].length<=4){o&&n.push(o),o={titleRaw:m[2],bodyLines:[]};continue}o&&o.bodyLines.push(c)}o&&n.push(o);let a=[];o=null;for(let c of t){let m=/^(\d+)[.)]\s+(.+?)\s*$/.exec(c);if(m){o&&a.push(o),o={titleRaw:m[2],bodyLines:[]};continue}o&&o.bodyLines.push(c)}o&&a.push(o);let i=[s(n),s(a)].filter(c=>c.length>=2);if(i.length)return i.sort((c,m)=>m.length-c.length),i[0];let
|
|
37
|
+
`;k.writeFileSync(e,a+n+`
|
|
38
|
+
`,"utf8");for(let i of t){let p=U.join(i,ae);try{k.existsSync(p)&&k.unlinkSync(p)}catch{}try{at(i,ae)}catch{}}return{migrated:!0,from:o,to:e}}function H(e){ct();let t=C();if(!k.existsSync(t))return null;let s=k.readFileSync(t,"utf8");if(!/^##\s+Phases\s*$/im.test(s))return null;let n=rt(G(s));return be(e,P,n)}function le(){let e=ot();return k.existsSync(e)?k.readFileSync(e,"utf8").split(/\r?\n/).filter(t=>/^-\s+\d{4}-\d{2}-\d{2}\s+—\s+Phase\s/.test(t)).length:0}import J from"fs";import pe from"path";import{execSync as lt}from"child_process";var ke=120;function ue(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>ke&&(t=t.slice(0,ke-1).trimEnd()+"\u2026"),t}function N(e){return String(e||"").toLowerCase().replace(/[^a-z0-9]+/g," ").trim()}function pt(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),s=c=>c.map(m=>({text:ue(m.titleRaw),body:m.bodyLines.join(`
|
|
39
|
+
`).trim()||null})).filter(m=>m.text),n=[],o=null;for(let c of t){let m=/^(#{1,6})\s+(.+?)\s*$/.exec(c);if(m&&m[1].length>=2&&m[1].length<=4){o&&n.push(o),o={titleRaw:m[2],bodyLines:[]};continue}o&&o.bodyLines.push(c)}o&&n.push(o);let a=[];o=null;for(let c of t){let m=/^(\d+)[.)]\s+(.+?)\s*$/.exec(c);if(m){o&&a.push(o),o={titleRaw:m[2],bodyLines:[]};continue}o&&o.bodyLines.push(c)}o&&a.push(o);let i=[s(n),s(a)].filter(c=>c.length>=2);if(i.length)return i.sort((c,m)=>m.length-c.length),i[0];let p=t.find(c=>c.trim());return[{text:ue(p||"Plan")||"Plan",body:e.trim()||null}]}function dt(e){let t=[];return e.forEach((s,n)=>{t.push(`### Phase ${n+1}: ${s.text}`),s.body&&s.body.trim()&&t.push(s.body.trim()),t.push("")}),t.join(`
|
|
40
40
|
`).replace(/\n{3,}/g,`
|
|
41
41
|
|
|
42
42
|
`).trimEnd()+`
|
|
43
|
-
`}function
|
|
43
|
+
`}function $e(e){let t=Q(),s=t.split(/\r?\n/),n=s.findIndex(c=>/^##\s+Phases\s*$/i.test(c)),o=dt(e);if(n===-1){let c=t.replace(/\s*$/,"");J.writeFileSync(C(),`${c}
|
|
44
44
|
|
|
45
45
|
## Phases
|
|
46
46
|
|
|
47
47
|
${o}`,"utf8");return}let a=s.length;for(let c=n+1;c<s.length;c++)if(/^##\s+/.test(s[c])&&!/^###/.test(s[c])){a=c;break}let i=s.slice(0,n+1).join(`
|
|
48
|
-
`),
|
|
49
|
-
`).replace(/^\n+/,""),
|
|
48
|
+
`),p=s.slice(a).join(`
|
|
49
|
+
`).replace(/^\n+/,""),f=`
|
|
50
50
|
|
|
51
|
-
${o}${
|
|
52
|
-
`+
|
|
53
|
-
`).filter(Boolean)}catch{
|
|
54
|
-
`).
|
|
55
|
-
`)
|
|
56
|
-
`
|
|
57
|
-
`).
|
|
58
|
-
`).
|
|
59
|
-
${
|
|
51
|
+
${o}${p.trim()?`
|
|
52
|
+
`+p:""}`;J.writeFileSync(C(),i+f,"utf8")}function Re(e){let t=pt(e);if(t.length===0)return{added:[],skipped:[],total:0};let n=ce()?[]:G(Q()).map(f=>({text:f.text,body:f.body})),o=new Set(n.map(f=>N(f.text))),a=[...n],i=[],p=[];for(let f of t){let c=N(f.text);if(o.has(c)){p.push(f.text);continue}o.add(c),a.push(f),i.push(f.text)}return i.length>0&&$e(a),{added:i,skipped:p,total:a.length}}function ut(e){return e==="completed"?"done":e==="in_progress"?"in_progress":"open"}function mt(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function ve(e,t){if(!Array.isArray(t)||t.length===0)return{changed:!1};let s=t.map(r=>({text:ue(r.content||r.activeForm||""),status:ut(r.status)})).filter(r=>r.text);if(s.length===0)return{changed:!1};let n=e.prepare("SELECT id, status FROM main.roadmap_phases WHERE decision_name = ? AND parent_phase_id IS NULL").all(P).filter(r=>r.status!=="stale");if(n.length===0){let l=ce()?[]:G(Q()).map(x=>({text:x.text,body:x.body})),d=new Set(l.map(x=>N(x.text))),g=[...l],S=0;for(let x of s){let $=N(x.text);d.has($)||(d.add($),g.push({text:x.text,body:null}),S++)}if(S===0)return{changed:!1};$e(g);let _=H(e);return{changed:!0,seeded:S,diff:_}}let o=mt(e);o==null&&(o=n.find(r=>r.status==="in_progress")?.id??n.find(r=>r.status==="open")?.id??n[0].id);let a=e.prepare("SELECT id, text, status FROM main.roadmap_phases WHERE parent_phase_id = ?").all(o),i=new Map(a.map(r=>[N(r.text),r])),p=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (NULL, NULL, ?, ?, ?, ?)"),f=e.prepare("UPDATE main.roadmap_phases SET status = ? WHERE id = ?"),c=e.prepare("SELECT COALESCE(MAX(position), -1) + 1 AS n FROM main.roadmap_phases WHERE parent_phase_id = ?").get(o).n,m=0,u=0;for(let r of s){let l=i.get(N(r.text));if(l){if(l.status==="done"&&r.status!=="done")continue;l.status!==r.status&&(f.run(r.status,l.id),u++)}else p.run(o,r.text,r.status,c++),m++}return{changed:m>0||u>0,parentId:o,inserted:m,updated:u}}var ft=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 ht(e){return[...new Set(N(e).split(" ").filter(t=>t.length>=4&&!ft.has(t)))]}function yt(e,t){if(!e||!t?.length)return null;let s=String(e),n=/\bphase\s*#?\s*(\d+)\b/i.exec(s);if(n){let i=parseInt(n[1],10),p=t.find(f=>f.phase_number===i);if(p)return{...p,confidence:"high"}}let o=new Set(N(s).split(" ").filter(Boolean)),a=null;for(let i of t){let p=ht(i.text);if(p.length<2)continue;let f=p.filter(m=>o.has(m)),c=f.length/p.length;f.length>=2&&c>=.6&&(!a||c>a.ratio)&&(a={...i,confidence:"medium",ratio:c})}return a?(delete a.ratio,a):null}function de(e,t){return lt(`git ${t}`,{cwd:e,encoding:"utf8",timeout:5e3}).trim()}function Pe(e,t){let s=pe.join(t,".cairn"),n=pe.join(s,".last-commit"),o=pe.join(s,"roadmap-pending.json"),a;try{a=de(t,"rev-parse HEAD")}catch{return{candidates:[],newCommits:0}}let i=null;try{i=J.readFileSync(n,"utf8").trim()||null}catch{}let p=[];if(i&&i!==a)try{p=de(t,`rev-list --no-merges --reverse ${i}..HEAD`).split(`
|
|
53
|
+
`).filter(Boolean)}catch{p=[]}try{J.writeFileSync(n,a,"utf8")}catch{}let f=[];try{let c=JSON.parse(J.readFileSync(o,"utf8"));Array.isArray(c)&&(f=c)}catch{}if(p.length){let c=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(P);for(let m of p){let u="";try{u=de(t,`show -s --format=%s%n%b ${m}`)}catch{}let r=yt(u,c);r&&!f.some(l=>l.phase_id===r.id)&&f.push({phase_id:r.id,phase_number:r.phase_number,text:r.text,commit:m.slice(0,7),confidence:r.confidence})}}if(f.length){let c=new Set(e.prepare("SELECT id FROM main.roadmap_phases WHERE status IN ('open','in_progress')").all().map(m=>m.id));f=f.filter(m=>c.has(m.phase_id))}try{J.writeFileSync(o,JSON.stringify(f),"utf8")}catch{}return{candidates:f,newCommits:p.length}}import Z from"fs";import gt from"path";var je="<!-- cairn:roadmap -->",_t={open:"open",in_progress:"in progress",done:"done",blocked:"blocked",stale:"stale"};function St(e){return e==="done"?"x":e==="in_progress"?"~":e==="blocked"?"!":" "}function xt(){return gt.join(K(),"ROADMAP.md")}function wt(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function Et(e){let t=new Map,s;try{s=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let n of s)t.has(n.blocked_id)||t.set(n.blocked_id,new Set),t.get(n.blocked_id).add(n.blocker_id);return t}function bt(e,t){let s=e.decision_name??"",n=t.decision_name??"";return s!==n?s<n?-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 kt(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(d=>d.status!=="stale").length===0)return null;let s=wt(e),n=Et(e),o=new Map(t.map(d=>[d.id,d.status])),a=new Map(t.map(d=>[d.id,d.phase_number!=null?`Phase ${d.phase_number}`:d.text])),i=d=>{let g=n.get(d);return g?[...g].filter(S=>o.get(S)!=="done").map(S=>a.get(S)||`#${S}`):[]},p=new Map;for(let d of t){let g=d.parent_phase_id??"root";p.has(g)||p.set(g,[]),p.get(g).push(d)}for(let d of p.values())d.sort(bt);let f=new Map;for(let d of p.get("root")||[])d.decision_name!=null&&(f.has(d.decision_name)||f.set(d.decision_name,[]),f.get(d.decision_name).push(d));let c=[...f.keys()].sort((d,g)=>d===P?-1:g===P?1:d<g?-1:d>g?1:0),m=[je,"<!--"," 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.","-->","","# Roadmap","","Legend: `[x]` done \xB7 `[~]` in progress \xB7 `[ ]` open \xB7 `[!]` blocked."],u=(d,g)=>{let S=" ".repeat(g),_=d.phase_number!=null?`Phase ${d.phase_number}: ${d.text}`:d.text,x=_t[d.status]||d.status,$=i(d.id);if($.length&&(x+=` \xB7 waiting on ${$.join(", ")}`),s===d.id&&(x+=" \xB7 \u2190 current"),m.push(`${S}- [${St(d.status)}] **${_}** \u2014 ${x}`),d.body){let R=d.body.split(`
|
|
54
|
+
`).map(A=>A.trim()).find(Boolean);R&&m.push(`${S} ${R}`)}for(let R of p.get(d.id)||[])R.status!=="stale"&&u(R,g+1)},r=!1;for(let d of c){let g=f.get(d).filter(_=>_.status!=="stale"),S=d===P?g.filter(_=>_.status!=="done"):g;if(S.length!==0){if(r=!0,m.push(""),d===P)m.push("## Active plan");else{let _=g.filter(x=>x.status==="done").length;m.push(`## ${d} \u2014 ${_}/${g.length} done`)}m.push("");for(let _ of S)u(_,0)}}if(!r)return null;let l=le();return l>0&&m.push("","---","",`**Shipped:** ${l} phase${l===1?"":"s"} \u2014 see \`.cairn/roadmap_completed.md\`.`),m.join(`
|
|
55
|
+
`)+`
|
|
56
|
+
`}function Oe(e){let t=xt(),s=kt(e),n=null;if(Z.existsSync(t))try{n=Z.readFileSync(t,"utf8")}catch{n=null}if(n!=null&&!n.startsWith(je))return{written:!1,reason:"ROADMAP.md exists and is not cairn-generated \u2014 refusing to overwrite",path:t};if(s==null){if(n!=null)try{return Z.unlinkSync(t),{written:!1,removed:!0,path:t}}catch(o){return{written:!1,reason:`remove failed: ${o.message}`,path:t}}return{written:!1,reason:"empty roadmap \u2014 no file written",path:t}}return n===s?{written:!1,unchanged:!0,path:t,bytes:Buffer.byteLength(s)}:(Z.writeFileSync(t,s,"utf8"),{written:!0,path:t,bytes:Buffer.byteLength(s)})}function ee(e){try{return Oe(e)}catch(t){return process.stderr.write(`[cairn] ROADMAP.md publish skipped: ${t.message}
|
|
57
|
+
`),null}}var $t=new Set(["open","in_progress"]);function Rt(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 vt(e,t){let s=e.prepare("SELECT parent_phase_id, status FROM main.roadmap_phases WHERE id = ?"),n=e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'"),o=s.get(t);for(;o&&o.parent_phase_id!=null;)n.run(o.parent_phase_id),o=s.get(o.parent_phase_id)}function Te(e,t){Rt(e,t),e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'").run(t),vt(e,t)}function Pt(e){let t=new Map,s;try{s=e.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return t}for(let n of s)t.has(n.blocked_id)||t.set(n.blocked_id,new Set),t.get(n.blocked_id).add(n.blocker_id);return t}function jt(e,t,s){let n=t.get(e);if(!n||n.size===0)return[];let o=[];for(let a of n)s.get(a)!=="done"&&o.push(a);return o}function Me(e,t){let s=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(s.length===0)return{id:null,reason:"No phases \u2014 add a ## Phases section to .cairn/roadmap.md."};let n=new Map(s.map(u=>[u.id,u.status])),o=Pt(e),a=new Map;for(let u of s){let r=u.parent_phase_id??`root:${u.decision_name}`;a.has(r)||a.set(r,[]),a.get(r).push(u)}for(let u of a.values())u.sort((r,l)=>r.phase_number!=null&&l.phase_number!=null?r.phase_number-l.phase_number:r.position-l.position||r.id-l.id);let i=[],p=new Set;for(let u of s){if(u.parent_phase_id!=null||p.has(u.decision_name))continue;p.add(u.decision_name);let r=[],l=a.get(`root:${u.decision_name}`)||[];for(let d=l.length-1;d>=0;d--)r.push(l[d]);for(;r.length;){let d=r.pop();i.push(d);let g=a.get(d.id)||[];for(let S=g.length-1;S>=0;S--)r.push(g[S])}}let f=u=>$t.has(u.status),c=[],m=0;if(t!=null){let u=i.findIndex(r=>r.id===t);m=u===-1?0:u+1}for(let u=m;u<i.length;u++){let r=i[u];if(!f(r))continue;let l=jt(r.id,o,n);if(l.length>0){c.push({id:r.id,blockers:l});continue}let d=t!=null?s.find(_=>_.id===t):null,g=d&&d.decision_name!==r.decision_name,S;return t==null?S=`First actionable phase: ${r.decision_name??"(sub-task)"} / Phase ${r.phase_number??"\xB7"}.`:g?S=`Previous decision "${d.decision_name}" is complete \u2014 advanced to "${r.decision_name}".`:S=`Next open phase in "${r.decision_name??"(sub-task)"}" after id ${t}.`,c.length>0&&(S+=` Skipped ${c.length} dep-blocked phase(s): ${c.map(_=>`id ${_.id} (blocked by ${_.blockers.join(",")})`).join("; ")}.`),{id:r.id,reason:S,skipped_blocked:c}}return c.length>0?{id:null,reason:`All remaining open phases are blocked by unfinished dependencies: ${c.map(u=>`id ${u.id} (blocked by ${u.blockers.join(",")})`).join("; ")}.`,skipped_blocked:c}:{id:null,reason:"No actionable phases remaining."}}var Ot=new Set(["java","typescript","javascript","vue","python","sql"]);function Ne(e){let t=y.join(B.homedir(),".claude","projects",se(e));return h.existsSync(t)?y.join(t,"memory"):null}function De(e){try{return h.readdirSync(e).filter(t=>t.endsWith(".md"))}catch{return[]}}function Tt(e){let t=Ne(e);if(!t||!h.existsSync(t))return 0;let s=y.join(e,".cairn","memory");h.mkdirSync(s,{recursive:!0});let n=0;for(let o of De(t)){let a=y.join(t,o),i;try{i=h.statSync(a)}catch{continue}if(!i.isFile())continue;let p=y.join(s,o),f=!h.existsSync(p);if(!f)try{f=i.mtimeMs>h.statSync(p).mtimeMs}catch{f=!0}if(f){h.copyFileSync(a,p);try{h.utimesSync(p,i.atime,i.mtime)}catch{}n++}}return n}function Mt(e){let t=Ne(e);if(!t||(h.mkdirSync(t,{recursive:!0}),De(t).some(o=>o!=="MEMORY.md")))return 0;let s=y.join(e,".cairn","memory");if(!h.existsSync(s))return 0;let n=0;for(let o of h.readdirSync(s)){let a=y.join(s,o),i;try{i=h.statSync(a)}catch{continue}if(i.isFile()){h.copyFileSync(a,y.join(t,o));try{h.utimesSync(y.join(t,o),i.atime,i.mtime)}catch{}n++}}return n}async function Le(e){let t={active:!1},s=y.join(e,"index.db");if(!h.existsSync(s))return t;let n;try{({DatabaseSync:n}=await import("node:sqlite"))}catch{return t}let o;try{o=new n(s,{readonly:!0})}catch{return t}let a=p=>p.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"");function i(p,f){if(!h.existsSync(p))return null;let c=h.readFileSync(p,"utf8"),m=!1,u=!1,r=[];for(let l of c.split(/\r?\n/)){if(/^##\s+Phases\s*$/i.test(l)){m=!0;continue}if(m&&/^##\s+/.test(l)&&!/^###/.test(l))break;if(!m)continue;let d=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(l);if(d){if(u)break;parseInt(d[1],10)===f&&(r.push(`### Phase ${d[1]}: ${d[2]}`),u=!0);continue}u&&r.push(l)}return r.join(`
|
|
58
|
+
`).trim()||null}try{let f=o.prepare("SELECT phase_id FROM roadmap_cursor WHERE singleton = 1").get()?.phase_id??null;if(f==null)return o.close(),t;let c=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(),m={open:"[ ]",in_progress:"[~]",done:"[x]",blocked:"[!]",stale:"[s]"},u=new Map;for(let b of c){let w=b.parent_phase_id??"root";u.has(w)||u.set(w,[]),u.get(w).push(b)}for(let b of u.values())b.sort((w,E)=>w.decision_name&&E.decision_name&&w.decision_name!==E.decision_name?w.decision_name.localeCompare(E.decision_name):w.phase_number!=null&&E.phase_number!=null?w.phase_number-E.phase_number:w.position-E.position||w.id-E.id);let r=u.get("root")||[],l=new Map;for(let b of r)b.decision_name!=null&&(l.has(b.decision_name)||l.set(b.decision_name,[]),l.get(b.decision_name).push(b));let d=[],g=(b,w)=>{for(let E of u.get(b)||[]){if(E.status==="stale")continue;let te=f===E.id?" \u2192":"",ne=E.phase_number!=null?`Phase ${E.phase_number}: ${E.text}`:E.text;d.push(`${" ".repeat(w)}${m[E.status]||"[?]"}${te} ${ne}`),g(E.id,w+1)}};for(let[b,w]of l){let E=w.filter(v=>v.status!=="stale");if(E.length===0)continue;let te=E.every(v=>v.status==="done"),ne=E.some(v=>v.status==="in_progress"),Ce=te?"done":ne?"in_progress":"open",Ae=E.filter(v=>v.status==="done").length,Ie=b===P?"Roadmap":`Decision: ${b}`;d.push(`${m[Ce]} ${Ie} (${Ae}/${E.length} done)`);for(let v of w){if(v.status==="stale"||b===P&&v.status==="done")continue;let We=f===v.id?" \u2192":"";d.push(` ${m[v.status]||"[?]"}${We} Phase ${v.phase_number}: ${v.text}`),g(v.id,2)}}let S=b=>c.find(w=>w.id===b),_=S(f),x=_?.decision_name??null,$=_?.phase_number??null,R=_;for(;R&&R.parent_phase_id!=null&&(R=S(R.parent_phase_id),!!R);)x=R.decision_name??x,$=R.phase_number??$;o.close();let A=_?_.phase_number!=null?`${_.decision_name} / Phase ${_.phase_number}: ${_.text}`:`${x} / Phase ${$} \u2192 ${_.text}`:"(unknown)",q=y.join(e,".last-roadmap-cursor"),fe=null;try{fe=parseInt(h.readFileSync(q,"utf8").trim(),10)}catch{}let he=fe!==f;try{h.writeFileSync(q,String(f),"utf8")}catch{}let ye=null;if(he&&x!=null&&$!=null){let b=x===P?y.join(e,"roadmap.md"):y.join(e,"memory",`decision_${a(x)}.md`),w=i(b,$);w&&(ye=w.split(`
|
|
59
|
+
`))}return{active:!0,currentLabel:A,treeLines:d,advanced:he,memoLines:ye}}catch{try{o?.close()}catch{}return t}}process.on("uncaughtException",e=>{process.stderr.write(`cairn: ${e.message}
|
|
60
|
+
`),process.exit(0)});var j=process.argv[2];if(j==="--version"||j==="-v"){let e=JSON.parse(h.readFileSync(new URL("../package.json",import.meta.url),"utf8"));console.log(e.version),process.exit(0)}else if(j==="minify"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t,s;try{s=JSON.parse(e)?.tool_input||{},t=s.file_path}catch{process.exit(0)}t||process.exit(0),(s.offset!=null||s.limit!=null)&&process.exit(0),t=y.resolve(t);let n=re(t);n||process.exit(0);let o=y.join(n,"views"),a=y.relative(o,t);!a.startsWith("..")&&!y.isAbsolute(a)&&process.exit(0);let i=D(n),f=X(n,i)>0,c=i[t];c?.state==="edit-ready"&&(f&&T(n,i),process.exit(0)),c?.state==="compressed"&&(c.state="edit-ready",T(n,i),process.exit(0));let m=y.extname(t).toLowerCase(),u=ge[m];(!u||!Ot.has(u))&&(f&&T(n,i),process.exit(0));let r;try{r=h.readFileSync(t,"utf8")}catch{f&&T(n,i),process.exit(0)}let l=r.split(`
|
|
61
|
+
`).length,d=u==="vue"?_e(r):V(r,u),g=d.split(`
|
|
62
|
+
`).length,S=l>0?Math.round((1-g/l)*100):0,_=we(n,t);h.mkdirSync(y.dirname(_),{recursive:!0}),h.writeFileSync(_,d,"utf8");let x=c?.readCount??0;i[t]={tempPath:_,state:"compressed",minifiedAt:h.statSync(t).mtimeMs,readCount:x+1},T(n,i);let $=g>xe,R=x>0;if(R||$){let A=Ee(d,u),q=R?`[cairn: ${l} \u2192 ${g} lines | seen \xD7${x+1} this session]`:`[cairn: ${l} \u2192 outline (${g} minified lines)]`;process.stderr.write(`${q}
|
|
63
|
+
${A}
|
|
60
64
|
Full minified view: Read("${_}")
|
|
61
|
-
`)}else process.stderr.write(`[cairn: ${
|
|
62
|
-
${
|
|
65
|
+
`)}else process.stderr.write(`[cairn: ${l} \u2192 ${g} lines (-${S}%)]
|
|
66
|
+
${d}
|
|
63
67
|
Cached view: Read("${_}")
|
|
64
|
-
`);process.exit(2)})}else if(
|
|
65
|
-
${
|
|
66
|
-
`),process.exit(2)}let
|
|
67
|
-
`).length<=300&&(m=
|
|
68
|
+
`);process.exit(2)})}else if(j==="edit-guard"){let e="";process.stdin.setEncoding("utf8"),process.stdin.on("data",t=>{e+=t}),process.stdin.on("end",()=>{let t="(unknown)",s="Edit";try{let u=JSON.parse(e);t=u?.tool_input?.file_path||"(unknown)",s=u?.tool_name||"Edit"}catch{}let n=y.resolve(t),o=re(n);o||process.exit(0);let a=y.join(o,"views"),i=y.relative(a,n);if(!i.startsWith("..")&&!y.isAbsolute(i)){let u=D(o),r=Object.entries(u).find(([,d])=>d.tempPath===n),l=r?`Edit the source file instead: Edit("${r[0]}")`:"Edit the original source file, not the .cairn/views/ cache.";process.stderr.write(`[cairn] Cannot edit a cached view file \u2014 it is read-only and will be overwritten on next read.
|
|
69
|
+
${l}
|
|
70
|
+
`),process.exit(2)}let p=D(o),f=X(o,p),c=p[n];c||(f>0&&T(o,p),process.exit(0)),s==="Write"&&(delete p[n],T(o,p),process.exit(0)),c.state==="edit-ready"&&(delete p[n],T(o,p),process.exit(0)),c.state="edit-ready",T(o,p);let m=null;try{let u=h.readFileSync(n,"utf8");u.split(`
|
|
71
|
+
`).length<=300&&(m=u)}catch{}m!==null?process.stderr.write(`[cairn] File was compressed in context. Full source content:
|
|
68
72
|
|
|
69
73
|
${m}
|
|
70
74
|
|
|
71
75
|
Re-apply your Edit using the exact content above (state is now edit-ready).
|
|
72
76
|
`):process.stderr.write(`[cairn] File compressed. Steps: (1) Read("${c.tempPath}") \u2014 loads full content into context. (2) Edit("${t}") \u2014 edit the original source, not the view.
|
|
73
|
-
`),process.exit(2)})}else if(
|
|
74
|
-
`);try{
|
|
75
|
-
`),process.exit(0)}else if(
|
|
76
|
-
`)}}catch{}if(n.length>0){s.push(`${n.length} phase(s) may be done \u2014 confirm with user`);let i=["","[cairn] Possible COMPLETED phase(s) detected from recent commits \u2014","CONFIRM WITH THE USER before marking done; do NOT mark done unilaterally:"];for(let
|
|
77
|
+
`),process.exit(2)})}else if(j==="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 s=t?.tool_name||"",n=t?.tool_input||{},o=process.cwd(),a=y.join(o,".cairn");h.existsSync(a)||process.exit(0);let i=y.join(a,"index.db");h.existsSync(i)||process.exit(0);let p;try{({DatabaseSync:p}=await import("node:sqlite"))}catch{process.exit(0)}let f=/exit.?plan.?mode/i.test(s),c=/todo.?write/i.test(s);!f&&!c&&process.exit(0);let m=null,u;try{if(f){let r=n.plan;(typeof r!="string"||!r.trim())&&process.exit(0);let l=Re(r);if(u=new p(i),H(u),(u.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null)==null){let g=Me(u,null);g.id!=null&&Te(u,g.id)}l.added.length>0&&(m=`[cairn] roadmap: captured ${l.added.length} phase(s) from plan${l.skipped.length?` (${l.skipped.length} already present)`:""}.`)}else if(c){u=new p(i);let r=ve(u,n.todos);r.seeded?m=`[cairn] roadmap: seeded ${r.seeded} phase(s) from todos.`:r.inserted>0&&(m=`[cairn] roadmap: linked ${r.inserted} new sub-task(s) to the current phase.`)}}catch(r){process.stderr.write(`[cairn] roadmap-capture skipped: ${r.message}
|
|
78
|
+
`);try{u?.close()}catch{}process.exit(0)}u&&ee(u);try{u?.close()}catch{}m&&process.stdout.write(JSON.stringify({systemMessage:m})),process.exit(0)})}else if(j==="validate-map"){let e=y.join(process.cwd(),".cairn");h.existsSync(e)||process.exit(0);let t=D(e),s=X(e,t);for(let n of Object.values(t))n.readCount=0;T(e,t),s>0&&process.stdout.write(`[cairn] Cleaned minify map: ${s} stale entr${s===1?"y":"ies"} removed
|
|
79
|
+
`),process.exit(0)}else if(j==="checkpoint"&&process.argv[3]==="--auto"){let e=`Auto-checkpoint at ${new Date().toISOString()}`,t=[],s=[];try{let n=y.join(process.cwd(),".cairn"),o=y.join(n,"session.json");h.existsSync(o)&&(t=JSON.parse(h.readFileSync(o,"utf8")).notes||[]);let a=D(n);s=Object.entries(a).filter(([,i])=>(i.readCount??0)>0).map(([i])=>i)}catch{}oe(null,{message:e,active_files:s,notes:t}),process.exit(0)}else if(j==="session-tick"){let e=process.cwd(),t=y.join(e,".cairn");h.existsSync(t)||process.exit(0);let s=[];try{let i=`Auto-checkpoint at ${new Date().toISOString()}`,p=[],f=y.join(t,"session.json");if(h.existsSync(f))try{p=JSON.parse(h.readFileSync(f,"utf8")).notes||[]}catch{}let c=D(t),m=Object.entries(c).filter(([,u])=>(u.readCount??0)>0).map(([u])=>u);oe(null,{message:i,active_files:m,notes:p}),s.push("\u2713 checkpoint saved")}catch{}try{let i=Tt(e);i>0&&s.push(`memory backed up (${i})`)}catch{}let n=[];try{let i=y.join(t,"index.db");if(h.existsSync(i)){let{DatabaseSync:p}=await import("node:sqlite"),f=new p(i);try{H(f),ee(f);try{n=Pe(f,e).candidates}catch{}}finally{f.close()}}}catch{}let o="";try{let i=await Le(t);if(i.active){s.push(`roadmap ${i.advanced?"updated":"on track"}: ${i.currentLabel}`);let p=["[cairn] Production roadmap (\u2192 = cursor / current phase):",...i.treeLines.map(f=>" "+f)];i.memoLines&&p.push("","\u2500\u2500 Current phase detail (from roadmap.md) \u2500\u2500",...i.memoLines,"\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500"),p.push("","Keep this current via cairn_roadmap on MATERIAL change (phase done / blocked / new) \u2014 not every turn."),o=p.join(`
|
|
80
|
+
`)}}catch{}if(n.length>0){s.push(`${n.length} phase(s) may be done \u2014 confirm with user`);let i=["","[cairn] Possible COMPLETED phase(s) detected from recent commits \u2014","CONFIRM WITH THE USER before marking done; do NOT mark done unilaterally:"];for(let p of n)i.push(` \u2022 Phase ${p.phase_number}: ${p.text} (commit ${p.commit}, ${p.confidence}) \u2192 if confirmed: cairn_roadmap set_status ${p.phase_id} done`);o=o?o+`
|
|
77
81
|
`+i.join(`
|
|
78
82
|
`):i.join(`
|
|
79
|
-
`).trimStart()}s.length===0&&process.exit(0);let a={systemMessage:`[cairn] ${s.join(" \xB7 ")}`};o&&(a.additionalContext=o),process.stdout.write(JSON.stringify(a)),process.exit(0)}else if(
|
|
80
|
-
`)}catch{}process.exit(0)}else if(
|
|
81
|
-
`)}catch{}}{let r=[y.join(process.cwd(),".claude","settings.json"),y.join(
|
|
82
|
-
`))}catch{}}let i="";try{i=(JSON.parse(
|
|
83
|
-
`)),process.exit(0)),
|
|
84
|
-
`).join("; "):"(none)"}`)}catch{}try{let
|
|
85
|
-
`).join(", ")}`)}catch{}try{let
|
|
86
|
-
${
|
|
87
|
-
`).map(
|
|
88
|
-
`)}`)}catch{}r.push(" \u2192 Call cairn_resume for full index status and open todos."),
|
|
89
|
-
`))}process.stdout.write(
|
|
83
|
+
`).trimStart()}s.length===0&&process.exit(0);let a={systemMessage:`[cairn] ${s.join(" \xB7 ")}`};o&&(a.additionalContext=o),process.stdout.write(JSON.stringify(a)),process.exit(0)}else if(j==="memory-restore"){let e=process.cwd();h.existsSync(y.join(e,".cairn"))||process.exit(0);try{let t=Mt(e);t>0&&process.stdout.write(`[cairn] Restored ${t} memory file(s) from .cairn/memory backup (Claude store was empty)
|
|
84
|
+
`)}catch{}process.exit(0)}else if(j==="resume-hint"){let e=y.join(process.cwd(),".cairn"),t=y.join(e,"session.json"),s=y.join(e,"index.db"),n=y.join(e,".hint-lock"),o=1800*1e3,a=process.cwd()===B.homedir();if(!a&&h.existsSync(e)&&(h.existsSync(s)||h.existsSync(t))){let r=y.join(e,".cairn-project");if(!h.existsSync(r))try{h.writeFileSync(r,"","utf8"),process.stdout.write(`[cairn] Repaired: wrote .cairn-project sentinel for hook compatibility.
|
|
85
|
+
`)}catch{}}if(!a&&h.existsSync(s))try{let{DatabaseSync:r}=await import("node:sqlite"),l=new r(s,{readonly:!0});try{ee(l)}finally{l.close()}}catch{}{let r=[y.join(process.cwd(),".claude","settings.json"),y.join(B.homedir(),".claude","settings.json")];for(let l of r)try{if(!h.existsSync(l))continue;let d=h.readFileSync(l,"utf8"),g=JSON.parse(d),S=g?.hooks?.PreToolUse;if(!Array.isArray(S))continue;let _=!1;for(let x of S)if(!(x.matcher!=="Edit"||!Array.isArray(x.hooks)))for(let $ of x.hooks)$.command==="cairn set-mode edit"&&($.command="cairn edit-guard",_=!0);_&&(h.writeFileSync(l,JSON.stringify(g,null,2),"utf8"),process.stdout.write(`[cairn] Repaired: migrated cairn set-mode \u2192 cairn edit-guard in ${l}
|
|
86
|
+
`))}catch{}}let i="";try{i=(JSON.parse(h.readFileSync("/dev/stdin","utf8"))?.prompt||"").toLowerCase()}catch{}let f=/\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(i);h.existsSync(t)||(h.existsSync(s)||(h.existsSync(n)&&Date.now()-h.statSync(n).mtimeMs<o&&process.exit(0),h.mkdirSync(e,{recursive:!0}),h.writeFileSync(n,new Date().toISOString(),"utf8"),process.stdout.write(`[cairn] No index found. Run cairn_maintain to index this project.
|
|
87
|
+
`)),process.exit(0)),h.existsSync(n)&&Date.now()-h.statSync(n).mtimeMs<o&&process.exit(0);let c=JSON.parse(h.readFileSync(t,"utf8")),m=new Date(c.checkpoint_at).toLocaleString(),u=[`[cairn] Prior session: "${c.message}" (${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 call cairn_checkpoint with message + active_files + notes.","[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."];if(f){let r=["[cairn] RESUME \u2014 checkpoint loaded inline:"];r.push(` message: ${c.message}`),r.push(` checkpoint: ${m}`),c.active_files?.length>0&&r.push(` active_files: ${c.active_files.join(", ")}`),c.notes?.length>0&&r.push(` notes: ${c.notes.join(" | ")}`);try{let l=new Date(c.checkpoint_at).toISOString(),d=me(`git log --oneline --after="${l}"`,{cwd:process.cwd(),encoding:"utf8",timeout:5e3}).trim();r.push(` commits_since: ${d?d.split(`
|
|
88
|
+
`).join("; "):"(none)"}`)}catch{}try{let l=me("git diff --name-only HEAD",{cwd:process.cwd(),encoding:"utf8",timeout:5e3}).trim();l&&r.push(` uncommitted: ${l.split(`
|
|
89
|
+
`).join(", ")}`)}catch{}try{let l=me("git status --short",{cwd:process.cwd(),encoding:"utf8",timeout:5e3}).trim();l&&r.push(` git_status:
|
|
90
|
+
${l.split(`
|
|
91
|
+
`).map(d=>" "+d).join(`
|
|
92
|
+
`)}`)}catch{}r.push(" \u2192 Call cairn_resume for full index status and open todos."),u.push(r.join(`
|
|
93
|
+
`))}process.stdout.write(u.join(`
|
|
90
94
|
`)+`
|
|
91
|
-
`),
|
|
95
|
+
`),h.writeFileSync(n,new Date().toISOString(),"utf8"),process.exit(0)}else if(j==="roadmap-hint"){let e=y.join(process.cwd(),".cairn"),t=await Le(e);t.active||process.exit(0);let s=["[cairn] ROADMAP \u2014 cursor is active.",...t.treeLines.map(n=>"[cairn] "+n),`[cairn] Current: ${t.currentLabel}`];if(t.memoLines){s.push("[cairn] \u2500\u2500\u2500\u2500\u2500 Decision memo (current phase) \u2500\u2500\u2500\u2500\u2500");for(let n of t.memoLines)s.push("[cairn] "+n);s.push("[cairn] \u2500\u2500\u2500\u2500\u2500 (re-read after each phase to confirm direction) \u2500\u2500\u2500\u2500\u2500")}s.push("[cairn] Use cairn_roadmap set_status <id> done (auto-advances), focus <id>, or next to move cursor."),process.stdout.write(s.join(`
|
|
92
96
|
`)+`
|
|
93
|
-
`),process.exit(0)}else if(
|
|
97
|
+
`),process.exit(0)}else if(j==="install"){let e=process.platform,t=y.join(B.homedir(),".claude.json"),s={};if(h.existsSync(t))try{s=JSON.parse(h.readFileSync(t,"utf8"))}catch{}s.mcpServers=s.mcpServers||{};let n=!!s.mcpServers.cairn;s.mcpServers.cairn={command:"cairn-mcp"},h.writeFileSync(t,JSON.stringify(s,null,2),"utf8");let o=y.join(B.homedir(),".claude");Fe(o,!0),console.log("Cairn installed successfully!"),console.log(""),console.log(` Platform : ${e}`),console.log(` MCP : cairn-mcp registered in ${t}${n?" (updated)":""}`),console.log(` Hooks : installed in ${y.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(""),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(j==="install-hooks"){let e=process.argv.includes("--global"),t=e?y.join(B.homedir(),".claude"):y.join(process.cwd(),".claude");Fe(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)"),process.exit(0)}else console.error("Usage: cairn <--version | install | install-hooks [--global] | minify | edit-guard | validate-map | checkpoint --auto | session-tick | resume-hint | roadmap-hint | roadmap-capture | memory-restore>"),process.exit(1);function Fe(e,t){let s=y.join(e,"settings.json"),n={};if(h.existsSync(s))try{n=JSON.parse(h.readFileSync(s,"utf8"))}catch{}n.hooks=n.hooks||{};let o=n.hooks.PreToolUse||[];o.some(r=>r.matcher==="Read"&&r.hooks?.some(l=>l.command==="cairn minify"))||o.push({matcher:"Read",hooks:[{type:"command",command:"cairn minify"}]});let a=o.filter(r=>!(r.matcher==="Edit"&&r.hooks?.some(l=>l.command==="cairn set-mode edit")));a.some(r=>r.matcher==="Edit"&&r.hooks?.some(l=>l.command==="cairn edit-guard"))||a.push({matcher:"Edit",hooks:[{type:"command",command:"cairn edit-guard"}]}),a.some(r=>r.matcher==="Write"&&r.hooks?.some(l=>l.command==="cairn edit-guard"))||a.push({matcher:"Write",hooks:[{type:"command",command:"cairn edit-guard"}]}),n.hooks.PreToolUse=a;let i=(r,l)=>(r||[]).map(d=>({...d,hooks:(d.hooks||[]).filter(g=>!l(g.command))})).filter(d=>(d.hooks||[]).length>0),p=r=>typeof r=="string"&&/sync-memory-(forward|restore)\.(mjs|py)/.test(r),f=i(n.hooks.Stop,r=>r==="cairn checkpoint --auto"||r==="cairn roadmap-hint");f.some(r=>r.hooks?.some(l=>l.command==="cairn session-tick"))||f.push({hooks:[{type:"command",command:"cairn session-tick"}]}),n.hooks.Stop=f;let c=i(n.hooks.UserPromptSubmit,p);c.some(r=>r.hooks?.some(l=>l.command==="cairn resume-hint"))||c.push({hooks:[{type:"command",command:"cairn resume-hint"}]}),c.some(r=>r.hooks?.some(l=>l.command==="cairn memory-restore"))||c.push({hooks:[{type:"command",command:"cairn memory-restore"}]}),n.hooks.UserPromptSubmit=c;let m=i(n.hooks.PostToolUse,p),u=r=>{m.some(l=>l.matcher===r&&l.hooks?.some(d=>d.command==="cairn roadmap-capture"))||m.push({matcher:r,hooks:[{type:"command",command:"cairn roadmap-capture"}]})};if(u("ExitPlanMode"),u("TodoWrite"),n.hooks.PostToolUse=m,t){let r=y.join(e,"scripts");for(let l of["sync-memory-forward.mjs","sync-memory-restore.mjs","sync-memory-forward.py","sync-memory-restore.py"]){let d=y.join(r,l);if(h.existsSync(d))try{h.unlinkSync(d)}catch{}}}if(!t){let r=y.join(process.cwd(),".cairn");h.mkdirSync(r,{recursive:!0});let l=y.join(r,".cairn-project");h.existsSync(l)||h.writeFileSync(l,"","utf8")}h.mkdirSync(e,{recursive:!0}),h.writeFileSync(s,JSON.stringify(n,null,2),"utf8")}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{Server as
|
|
2
|
-
`,"utf8")}function
|
|
1
|
+
import{Server as ms}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as fs}from"@modelcontextprotocol/sdk/server/stdio.js";import{CallToolRequestSchema as hs,ListToolsRequestSchema as gs}from"@modelcontextprotocol/sdk/types.js";import{DatabaseSync as Oe}from"node:sqlite";import de from"fs";import v from"path";import M from"fs";import cn from"os";import D from"path";var be=null;function $(){return be||process.cwd()}function Re(t){let e=be;return be=t,e}function A(){let t=D.join($(),".cairn");M.mkdirSync(D.join(t,"bundles"),{recursive:!0});let e=D.join(t,".cairn-project");return M.existsSync(e)||M.writeFileSync(e,"","utf8"),t}function pn(t){return t.replace(/[:/\\]/g,"-").replace(/^-+/,"")}function we(){let t=D.join(cn.homedir(),".claude","projects",pn($()));return M.existsSync(t)?D.join(t,"memory"):null}function Je(t){let e=D.join(t,"MEMORY.md");M.existsSync(e)||M.writeFileSync(e,`# Memory Index
|
|
2
|
+
`,"utf8")}function z(){let t=D.join(A(),"memory");M.mkdirSync(t,{recursive:!0}),Je(t);let e=we();if(!e)return t;M.mkdirSync(e,{recursive:!0}),Je(e);let s=n=>M.readdirSync(n).some(i=>i.endsWith(".md")&&i!=="MEMORY.md");if(!s(e)&&s(t))for(let n of M.readdirSync(t)){let i=D.join(t,n);try{if(!M.statSync(i).isFile())continue;let r=M.statSync(i);M.copyFileSync(i,D.join(e,n));try{M.utimesSync(D.join(e,n),r.atime,r.mtime)}catch{}}catch{}}return e}var ln=`
|
|
3
3
|
CREATE TABLE IF NOT EXISTS files (
|
|
4
4
|
id INTEGER PRIMARY KEY,
|
|
5
5
|
repo TEXT NOT NULL,
|
|
@@ -98,7 +98,7 @@ import{Server as ts}from"@modelcontextprotocol/sdk/server/index.js";import{Stdio
|
|
|
98
98
|
);
|
|
99
99
|
CREATE INDEX IF NOT EXISTS idx_roadmap_phase_deps_blocked
|
|
100
100
|
ON roadmap_phase_deps(blocked_id);
|
|
101
|
-
`,
|
|
101
|
+
`,dn=1e6;function un(t){let e=v.dirname(v.resolve(t));for(;;){let s=v.join(e,".cairn","index.db");if(de.existsSync(s))return s;let n=v.dirname(e);if(n===e)return null;e=n}}function mn(t){let e=v.resolve($()),s=v.resolve(v.join(A(),"index.db")),n=t.prepare("INSERT OR IGNORE INTO main.sub_indexes (path) VALUES (?)"),i=un(e);if(i){try{v.resolve(i)!==s&&n.run(v.resolve(i));let r=new Oe(i,{readonly:!0}),o=r.prepare("SELECT path FROM sub_indexes").all();r.close();for(let{path:a}of o){let c=v.resolve(a);c!==s&&n.run(c)}}catch{}return}try{let r=v.dirname(e),o=de.readdirSync(r,{withFileTypes:!0});for(let a of o){if(!a.isDirectory())continue;let c=v.resolve(v.join(r,a.name,".cairn","index.db"));c!==s&&de.existsSync(c)&&n.run(c)}}catch{}}var fn=["files","symbols","dependencies","security_findings"];function Ye(t,e,s){let n=`"${t}"`;switch(e){case"files":return`SELECT id+${s} AS id, repo, path, language, file_type, last_indexed FROM ${n}.files`;case"symbols":return`SELECT id+${s} AS id, file_id+${s} AS file_id, fqn, name, kind, language, exported, description, line FROM ${n}.symbols`;case"dependencies":return`SELECT from_file_id+${s} AS from_file_id, to_fqn, dep_type FROM ${n}.dependencies`;case"build_deps":return`SELECT repo, manager, group_id, artifact, version, scope FROM ${n}.build_deps`;case"security_findings":return`SELECT id+${s} AS id, file_id+${s} AS file_id, line, severity, cwe, rule_name, description, code_snippet FROM ${n}.security_findings`;default:return`SELECT * FROM ${n}.${e}`}}function hn(t){if(!de.existsSync(t))return{valid:!1,reason:"file not found"};try{let e=new Oe(t,{readonly:!0}),s=e.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(i=>i.name);e.close();let n=fn.filter(i=>!s.includes(i));return n.length>0?{valid:!1,reason:`missing tables: ${n.join(", ")}`}:{valid:!0}}catch(e){return{valid:!1,reason:e.message}}}function Ve(){let t=v.join(A(),"index.db"),e=new Oe(t);e.exec("PRAGMA journal_mode = WAL"),e.exec("PRAGMA synchronous = NORMAL"),e.exec(ln);try{e.exec("ALTER TABLE main.symbols ADD COLUMN line INTEGER")}catch{}let s=!1;try{e.exec("ALTER TABLE main.todos ADD COLUMN parent_id INTEGER REFERENCES todos(id) ON DELETE SET NULL"),s=!0}catch{}try{e.exec("ALTER TABLE main.todos ADD COLUMN position INTEGER NOT NULL DEFAULT 0"),s=!0}catch{}s&&e.exec(`
|
|
102
102
|
UPDATE main.todos AS t
|
|
103
103
|
SET position = (
|
|
104
104
|
SELECT rn - 1 FROM (
|
|
@@ -107,25 +107,25 @@ import{Server as ts}from"@modelcontextprotocol/sdk/server/index.js";import{Stdio
|
|
|
107
107
|
) AS rn FROM main.todos
|
|
108
108
|
) ord WHERE ord.id = t.id
|
|
109
109
|
)
|
|
110
|
-
`);try{e.exec("ALTER TABLE main.roadmap_cursor ADD COLUMN phase_id INTEGER REFERENCES roadmap_phases(id) ON DELETE SET NULL")}catch{}try{if(!e.prepare("SELECT 1 FROM main.roadmap_phases WHERE decision_name = 'legacy_v1_9' LIMIT 1").get()){let i=e.prepare("SELECT id, text, status, position FROM main.todos WHERE parent_id IS NULL AND id IN (SELECT DISTINCT parent_id FROM main.todos WHERE parent_id IS NOT NULL) ORDER BY position, id").all();if(i.length>0){let r=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (?, ?, ?, ?, ?, ?)"),o=new Map,a=1;for(let
|
|
111
|
-
`)}return
|
|
112
|
-
`).length}function
|
|
113
|
-
`).length}function
|
|
114
|
-
`).length}function
|
|
115
|
-
`);for(let r of
|
|
116
|
-
`);if(r.negPattern.test(c))return}n.push({severity:r.severity,cwe:r.id,rule_name:r.name,line:a+1,code_snippet:o.trim().substring(0,120),description:r.fix})}});return n}function
|
|
117
|
-
`);for(let a=0;a<o.length;a++){let c=
|
|
110
|
+
`);try{e.exec("ALTER TABLE main.roadmap_cursor ADD COLUMN phase_id INTEGER REFERENCES roadmap_phases(id) ON DELETE SET NULL")}catch{}try{if(!e.prepare("SELECT 1 FROM main.roadmap_phases WHERE decision_name = 'legacy_v1_9' LIMIT 1").get()){let i=e.prepare("SELECT id, text, status, position FROM main.todos WHERE parent_id IS NULL AND id IN (SELECT DISTINCT parent_id FROM main.todos WHERE parent_id IS NOT NULL) ORDER BY position, id").all();if(i.length>0){let r=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (?, ?, ?, ?, ?, ?)"),o=new Map,a=1;for(let p of i){let h=r.run("legacy_v1_9",a++,null,p.text,p.status,p.position);o.set(p.id,h.lastInsertRowid)}let c=e.prepare("SELECT id, parent_id, text, status, position FROM main.todos WHERE parent_id IS NOT NULL ORDER BY position, id").all();for(let p of c){let h=o.get(p.parent_id);h!=null&&r.run(null,null,h,p.text,p.status,p.position)}let l=e.prepare("SELECT todo_id FROM main.roadmap_cursor WHERE singleton = 1").get();l?.todo_id!=null&&o.has(l.todo_id)&&e.prepare("UPDATE main.roadmap_cursor SET phase_id = ? WHERE singleton = 1").run(o.get(l.todo_id))}}}catch(n){process.stderr.write(`[cairn] v1.9.0\u2192v1.10.0 roadmap migration skipped: ${n.message}
|
|
111
|
+
`)}return mn(e),Ne(e),e}function ze(t,e){let s=t.prepare("INSERT OR IGNORE INTO main.sub_indexes (path) VALUES (?)");for(let n of e)s.run(v.resolve(n))}function ue(t){return t.prepare("PRAGMA database_list").all().filter(e=>e.name!=="temp"&&e.name!=="main").map(e=>e.name)}function Ne(t){let e=t.prepare("PRAGMA database_list").all(),s=new Map(e.filter(p=>p.name!=="temp"&&p.name!=="main"&&p.file).map(p=>[v.resolve(p.file),p.name])),n=[...s.values()],i={attached:[],removed:[],skipped:[]},r=t.prepare("SELECT path FROM main.sub_indexes").all(),o=t.prepare("DELETE FROM main.sub_indexes WHERE path = ?"),a=n.length,c=[...n];for(let{path:p}of r){let h=v.resolve(p);if(s.has(h)){c.includes(s.get(h))||c.push(s.get(h));continue}let m=hn(h);if(!m.valid){o.run(p),i.removed.push(`${p} (${m.reason})`);continue}let d=`sub${a++}`;try{t.exec(`ATTACH DATABASE '${h.replace(/\\/g,"/").replace(/'/g,"''")}' AS "${d}"`),c.push(d),s.set(h,d),i.attached.push(p)}catch(u){i.skipped.push(`${p} (${u.message})`)}}if(c.length===0)return i;let l=["files","symbols","dependencies","build_deps","security_findings"];for(let p of l){t.exec(`DROP VIEW IF EXISTS temp.${p}`);let h=[Ye("main",p,0),...c.map((m,d)=>Ye(m,p,(d+1)*dn))];t.exec(`CREATE TEMP VIEW ${p} AS ${h.join(" UNION ALL ")}`)}return i}import Ie from"fs/promises";import oe from"path";import Ke from"fast-glob";import Le from"path";var J={".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"},Qe=["pom.xml","package.json","build.gradle"],gn=["**/node_modules/**","**/.git/**","**/dist/**","**/build/**","**/target/**","**/.next/**","**/__pycache__/**","**/*.min.js","**/.cairn/**"];async function Ze(t){return(await Ke(["**/.cairn/index.db"],{cwd:t,absolute:!0,dot:!0,ignore:["**/node_modules/**"]})).map(s=>Le.dirname(Le.dirname(s)))}async function P(t,e=[]){let s=Object.keys(J).map(r=>`**/*${r}`),n=e.map(r=>`${Le.relative(t,r).replace(/\\/g,"/")}/**`);return await Ke(s,{cwd:t,ignore:[...gn,...n],absolute:!0})}import _n from"path";function et(t,e){return t.substring(0,e).split(`
|
|
112
|
+
`).length}function tt(t,e,s){let n=e.match(/^package\s+([\w.]+);/m)?.[1]||"",i=[...e.matchAll(/^import\s+([\w.]+);/gm)].map(g=>g[1]),o=/(?:public|protected)?\s+(?:abstract\s+)?(?:class|interface|enum|record)\s+(\w+)/.exec(e),a=o?.[1]||"",c=o?.[0]?.match(/class|interface|enum|record/)?.[0]||"class",l=n?`${n}.${a}`:a,p=o?et(e,o.index):null,h=[...e.matchAll(/(?:public|protected|private)[^{;]*\s+(\w+)\s*\([^)]*\)\s*(?:throws[^{]*)?\{/gm)],m=e.match(/extends\s+([\w.]+)/)?.[1],d=e.match(/implements\s+([\w.,\s]+)/)?.[1]?.split(",").map(g=>g.trim()),u=e.match(/\/\*\*([\s\S]*?)\*\//)?.[1]?.replace(/\s*\*\s?/g," ").trim(),f=l?h.map(g=>({name:g[1],fqn:`${l}.${g[1]}`,kind:"method",exported:!1,description:"",line:et(e,g.index)})):[];return{language:"java",symbols:[{name:a,fqn:l,kind:c,exported:!0,description:u||"",line:p},...f],imports:i,extends:m?[m]:[],implements:d||[]}}function ie(t,e){return t.substring(0,e).split(`
|
|
113
|
+
`).length}function re(t,e,s,n="typescript"){let i=[],r=[];for(let o of e.matchAll(/import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g))r.push(...o[1].split(",").map(a=>a.trim().split(" as ")[0]).filter(Boolean));for(let o of e.matchAll(/import\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g))r.push(o[1]);for(let o of e.matchAll(/(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/g)){let a=o[0].startsWith("export"),c=nt(e,o.index);i.push({name:o[1],fqn:`${t}::${o[1]}`,kind:"class",exported:a,description:c,line:ie(e,o.index)})}for(let o of e.matchAll(/export\s+(?:interface|type)\s+(\w+)/g))i.push({name:o[1],fqn:`${t}::${o[1]}`,kind:"interface",exported:!0,description:"",line:ie(e,o.index)});for(let o of e.matchAll(/export\s+(?:async\s+)?function\s+(\w+)/g)){let a=nt(e,o.index);i.push({name:o[1],fqn:`${t}::${o[1]}`,kind:"function",exported:!0,description:a,line:ie(e,o.index)})}for(let o of e.matchAll(/export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/g))i.push({name:o[1],fqn:`${t}::${o[1]}`,kind:"function",exported:!0,description:"",line:ie(e,o.index)});for(let o of e.matchAll(/export\s+(?:const\s+)?enum\s+(\w+)/g))i.push({name:o[1],fqn:`${t}::${o[1]}`,kind:"enum",exported:!0,description:"",line:ie(e,o.index)});return{language:n,symbols:i,imports:r}}function nt(t,e){let n=t.substring(0,e).match(/\/\*\*([\s\S]*?)\*\/\s*$/);return n?n[1].replace(/\s*\*\s?/g," ").trim():""}function st(t,e,s){let n=t.split("/").pop().replace(".vue",""),r=e.match(/<script(?:\s+setup)?(?:\s+lang=["']ts["'])?>([\s\S]*?)<\/script>/i)?.[1]||"",o=re(t,r,s,"vue"),c=[...e.match(/<template>([\s\S]*?)<\/template>/i)?.[1]?.matchAll(/<([A-Z][A-Za-z]+)/g)||[]].map(l=>l[1]);return o.symbols.unshift({name:n,fqn:`${t}::${n}`,kind:"component",exported:!0,description:`Vue component: ${n}`}),o.childComponents=[...new Set(c)],o}function ke(t,e){return t.substring(0,e).split(`
|
|
114
|
+
`).length}function it(t,e,s){let n=[],i=[];for(let r of e.matchAll(/^from\s+([\w.]+)\s+import\s+([\w,\s*]+)/gm))i.push(...r[2].split(",").map(o=>o.trim()).filter(Boolean));for(let r of e.matchAll(/^import\s+([\w.,\s]+)/gm))i.push(...r[1].split(",").map(o=>o.trim()).filter(Boolean));for(let r of e.matchAll(/^class\s+(\w+)(?:\(([^)]*)\))?:/gm)){let o=e.slice(r.index).match(/:\s*\n\s+"""([\s\S]*?)"""/)?.[1]?.trim();n.push({name:r[1],fqn:`${t}::${r[1]}`,kind:"class",exported:!0,description:o||"",line:ke(e,r.index)})}for(let r of e.matchAll(/^(?:async\s+)?def\s+(\w+)\s*\(/gm)){if(r[1].startsWith("_"))continue;let o=e.slice(r.index).match(/\).*?:\s*\n\s+"""([\s\S]*?)"""/)?.[1]?.trim();n.push({name:r[1],fqn:`${t}::${r[1]}`,kind:"function",exported:!0,description:o||"",line:ke(e,r.index)})}for(let r of e.matchAll(/^@([\w.]+)(?:\(([^)]*)\))?/gm))n.push({name:r[1],fqn:`${t}::@${r[1]}`,kind:"decorator",exported:!1,description:"",line:ke(e,r.index)});return{language:"python",symbols:n,imports:i}}function rt(t,e,s){let n=[];for(let i of e.matchAll(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"\w.]+)/gi))n.push({name:i[1],fqn:`${t}::${i[1]}`,kind:"table",exported:!0,description:""});for(let i of e.matchAll(/CREATE\s+(?:OR\s+REPLACE\s+)?(?:PROCEDURE|FUNCTION)\s+(\w+)/gi))n.push({name:i[1],fqn:`${t}::${i[1]}`,kind:"function",exported:!0,description:""});for(let i of e.matchAll(/CREATE\s+(?:OR\s+REPLACE\s+)?VIEW\s+(\w+)/gi))n.push({name:i[1],fqn:`${t}::${i[1]}`,kind:"view",exported:!0,description:""});return{language:"sql",symbols:n,imports:[]}}function ot(t,e,s){let n=[];for(let i of e.matchAll(/^([\w.-]+)\s*[:=]\s*(.+)/gm)){let r=i[1].trim();r.startsWith("#")||n.push({name:r,fqn:`${t}::${r}`,kind:"config-key",exported:!1,description:i[2].trim().substring(0,80)})}return{language:"config",symbols:n,imports:[]}}var yn=new Set(["div","span","p","a","ul","ol","li","table","tr","td","th","thead","tbody","form","input","button","select","option","textarea","label","img","br","hr","h1","h2","h3","h4","h5","h6","head","body","html","script","style","link","meta","nav","header","footer","main","section","article","aside","figure"]);function ve(t,e,s){let n=[];for(let r of e.matchAll(/\bid\s*=\s*["']([^"']+)["']/g))n.push({name:r[1],fqn:`${t}::${r[1]}`,kind:"bean",exported:!0,description:""});let i=new Set;for(let r of e.matchAll(/<([a-z][a-z0-9-]*)(?:\s|\/?>)/gi)){let o=r[1].toLowerCase();yn.has(o)||i.has(o)||(i.add(o),n.push({name:o,fqn:`${t}::${o}`,kind:"component",exported:!0,description:""}))}return{language:"xml",symbols:n,imports:[]}}function at(t,e,s){let n=[];for(let i of e.matchAll(/^(#{1,3})\s+(.+)/gm)){let r=i[1].length,o=i[2].trim();n.push({name:o,fqn:`${t}::${o.replace(/\s+/g,"-").toLowerCase()}`,kind:r===1?"doc-title":"doc-section",exported:!0,description:""})}return{language:"markdown",symbols:n,imports:[]}}var En={java:tt,typescript:re,javascript:re,vue:st,python:it,sql:rt,config:ot,xml:ve,html:ve,markdown:at};function Sn(t){let e=_n.extname(t).toLowerCase(),s=J[e];return{language:s,parser:En[s]||null}}async function ct(t,e,s){let{language:n,parser:i}=Sn(t);return i?i(t,e,s,n):null}var xn=[{id:"CWE-611",lang:["java"],severity:"HIGH",name:"XXE (XML External Entity)",pattern:/builder\.parse\(/,negPattern:/setFeature.*FEATURE_SECURE_PROCESSING/,fix:"Add dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) before parse()"},{id:"CWE-89",lang:["java"],severity:"HIGH",name:"SQL Injection",pattern:/"(SELECT|INSERT|UPDATE|DELETE)\b.*"\s*\+/i,fix:"Use PreparedStatement with parameterized queries"},{id:"CWE-326",lang:["java"],severity:"HIGH",name:"Weak Cryptography",pattern:/getInstance\("(MD5|SHA1|DES|RC4)"\)/,fix:"Use SHA-256 or AES-256"},{id:"CWE-502",lang:["java"],severity:"HIGH",name:"Unsafe Deserialization",pattern:/new ObjectInputStream|\.readObject\(\)/,fix:"Validate input before deserialization; use safe deserialization libraries"},{id:"CWE-078",lang:["java"],severity:"HIGH",name:"Command Injection",pattern:/Runtime\.getRuntime\(\)\.exec\(/,fix:"Use ProcessBuilder with argument list instead of string concatenation"},{id:"CWE-79",lang:["javascript","typescript","vue"],severity:"HIGH",name:"XSS via innerHTML / dangerouslySetInnerHTML",pattern:/\.innerHTML\s*=|dangerouslySetInnerHTML/,fix:"Use textContent or sanitize with DOMPurify"},{id:"CWE-89",lang:["javascript","typescript"],severity:"HIGH",name:"SQL Injection (JS)",pattern:/`\s*(SELECT|INSERT|UPDATE|DELETE)\b.*\$\{/i,fix:"Use parameterized queries or an ORM"},{id:"CWE-798",lang:["javascript","typescript","vue"],severity:"HIGH",name:"Hardcoded Secret",pattern:/(api_key|apikey|secret|password|token)\s*[:=]\s*['"][^'"]{8,}['"]/i,fix:"Move secrets to environment variables or a secrets manager"},{id:"CWE-327",lang:["javascript","typescript"],severity:"MEDIUM",name:"Weak Crypto (Node)",pattern:/createHash\(['"]md5['"]\)|createHash\(['"]sha1['"]\)/,fix:"Use SHA-256 or stronger"},{id:"CWE-601",lang:["javascript","typescript","vue"],severity:"MEDIUM",name:"Open Redirect",pattern:/res\.redirect\([^)]*req\.(query|params|body)/,fix:"Validate redirect URL against an allowlist"},{id:"CWE-089",lang:["python"],severity:"HIGH",name:"SQL Injection (Python)",pattern:/execute\([f"'].*%(s|d)|execute\(.*format\(/,fix:"Use parameterized queries: cursor.execute(sql, params)"},{id:"CWE-078",lang:["python"],severity:"HIGH",name:"Command Injection (Python)",pattern:/os\.system\(|subprocess\.call\(.*shell=True/,fix:"Use subprocess with a list of args and shell=False"},{id:"CWE-798",lang:["python"],severity:"HIGH",name:"Hardcoded Secret (Python)",pattern:/(api_key|secret|password|token)\s*=\s*['"][^'"]{8,}['"]/i,fix:"Use environment variables or a secrets manager like Vault"},{id:"CWE-312",lang:null,severity:"MEDIUM",name:"Cleartext Sensitive Data in Comments",pattern:/\/\/.*?(password|secret|token)\s*[:=]\s*\S+/i,fix:"Remove sensitive data from comments"}];function pt(t,e,s){let n=[],i=e.split(`
|
|
115
|
+
`);for(let r of xn)r.lang&&!r.lang.includes(s)||i.forEach((o,a)=>{if(r.pattern.test(o)){if(r.negPattern){let c=i.slice(Math.max(0,a-5),a+5).join(`
|
|
116
|
+
`);if(r.negPattern.test(c))return}n.push({severity:r.severity,cwe:r.id,rule_name:r.name,line:a+1,code_snippet:o.trim().substring(0,120),description:r.fix})}});return n}function lt(t,e,s){let n=[];for(let i of e.matchAll(/<dependency>([\s\S]*?)<\/dependency>/g)){let r=i[1],o=r.match(/<groupId>([^<]+)<\/groupId>/)?.[1]||"",a=r.match(/<artifactId>([^<]+)<\/artifactId>/)?.[1]||"",c=r.match(/<version>([^<]+)<\/version>/)?.[1]||"",l=r.match(/<scope>([^<]+)<\/scope>/)?.[1]||"compile";o&&a&&n.push({manager:"maven",group_id:o,artifact:a,version:c,scope:l})}return n}function dt(t,e,s){let n=[];try{let i=JSON.parse(e);for(let[r,o]of Object.entries(i.dependencies||{}))n.push({manager:"npm",group_id:"dependencies",artifact:r,version:o,scope:"runtime"});for(let[r,o]of Object.entries(i.devDependencies||{}))n.push({manager:"npm",group_id:"devDependencies",artifact:r,version:o,scope:"dev"});for(let[r,o]of Object.entries(i.peerDependencies||{}))n.push({manager:"npm",group_id:"peerDependencies",artifact:r,version:o,scope:"peer"})}catch{}return n}function ut(t,e,s){let n=[];for(let i of e.matchAll(/(\w+)\s+['"]([^:'"]+):([^:'"]+):([^'"]+)['"]/g)){let r=i[1],o=i[2],a=i[3],c=i[4];n.push({manager:"gradle",group_id:o,artifact:a,version:c,scope:r})}return n}function mt(t,{repo:e,path:s,language:n,file_type:i}){let r=t.prepare("SELECT id FROM main.files WHERE path = ?").get(s);return r?(t.prepare("UPDATE main.files SET language = ?, file_type = ?, last_indexed = ? WHERE id = ?").run(n,i,Date.now(),r.id),r.id):t.prepare("INSERT INTO main.files (repo, path, language, file_type, last_indexed) VALUES (?, ?, ?, ?, ?)").run(e,s,n,i,Date.now()).lastInsertRowid}function ft(t,{file_id:e,fqn:s,name:n,kind:i,language:r,exported:o,description:a,line:c}){let l=t.prepare("SELECT id FROM main.symbols WHERE fqn = ?").get(s);l?t.prepare("UPDATE main.symbols SET file_id=?, name=?, kind=?, language=?, exported=?, description=?, line=? WHERE id=?").run(e,n,i,r,o?1:0,a||"",c??null,l.id):t.prepare("INSERT INTO main.symbols (file_id, fqn, name, kind, language, exported, description, line) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(e,s,n,i,r,o?1:0,a||"",c??null)}function ht(t,e){t.prepare("DELETE FROM main.dependencies WHERE from_file_id = ?").run(e),t.prepare("DELETE FROM main.security_findings WHERE file_id = ?").run(e)}function gt(t,{from_file_id:e,to_fqn:s,dep_type:n}){t.prepare("INSERT INTO main.dependencies (from_file_id, to_fqn, dep_type) VALUES (?, ?, ?)").run(e,s,n)}function yt(t,{repo:e,manager:s,group_id:n,artifact:i,version:r,scope:o}){t.prepare("SELECT rowid FROM main.build_deps WHERE repo=? AND manager=? AND artifact=?").get(e,s,i)||t.prepare("INSERT INTO main.build_deps (repo, manager, group_id, artifact, version, scope) VALUES (?, ?, ?, ?, ?, ?)").run(e,s,n,i,r,o)}function _t(t,{file_id:e,line:s,severity:n,cwe:i,rule_name:r,description:o,code_snippet:a}){t.prepare("INSERT INTO main.security_findings (file_id, line, severity, cwe, rule_name, description, code_snippet) VALUES (?, ?, ?, ?, ?, ?, ?)").run(e,s,n,i,r,o,a)}import Tn from"fs/promises";var bn=/(?:\/\/|#|\/\*|\*|<!--)\s*(TODO|FIXME|HACK|XXX|NOTE)[:\s]+(.*)/i;async function me(t,e){t.prepare("DELETE FROM main.todos WHERE source = ?").run("scan");let s=t.prepare("INSERT INTO main.todos (source, file, line, kind, text) VALUES (?, ?, ?, ?, ?)"),n=0;for(let i of e)try{let o=(await Tn.readFile(i,"utf-8")).split(`
|
|
117
|
+
`);for(let a=0;a<o.length;a++){let c=bn.exec(o[a]);if(c){let l=c[1].toUpperCase(),p=c[2].trim();p&&(s.run("scan",i,a+1,l,p),n++)}}}catch{}return n}function Rn(t){let e=t.toLowerCase();return e.includes("test")||e.includes("spec")?"test":e.endsWith("pom.xml")||e.endsWith("package.json")||e.endsWith("build.gradle")?"build":e.endsWith(".yml")||e.endsWith(".yaml")||e.endsWith(".properties")||e.endsWith(".env")?"config":e.endsWith(".md")||e.endsWith(".html")?"doc":"source"}async function Et(t,e,s,n,i){try{let r=await Ie.readFile(e,"utf-8"),o=await ct(e,r,s);if(!o)return;let{language:a}=o;if(n&&!n.includes(a))return;i.files_by_language[a]=(i.files_by_language[a]||0)+1;let c=mt(t,{repo:s,path:e,language:a,file_type:Rn(e)});ht(t,c);for(let p of o.symbols||[])ft(t,{...p,file_id:c,language:a}),i.symbols_total++;for(let p of o.imports||[])gt(t,{from_file_id:c,to_fqn:p,dep_type:"imports"}),i.dependencies_mapped++;let l=pt(e,r,a);for(let p of l)_t(t,{file_id:c,...p}),i.security_findings++}catch{}}function St(t){t.exec(`
|
|
118
118
|
DELETE FROM main.fts_index;
|
|
119
119
|
INSERT INTO main.fts_index(fqn, name, description, path, repo, language, kind)
|
|
120
120
|
SELECT s.fqn, s.name, COALESCE(s.description,''), f.path, f.repo, s.language, s.kind
|
|
121
121
|
FROM main.symbols s JOIN main.files f ON s.file_id = f.id;
|
|
122
|
-
`)}async function
|
|
123
|
-
`).map(
|
|
122
|
+
`)}async function xt(t,{languages:e}={}){let s=Date.now(),n=process.cwd(),i=n.replace(/\\/g,"/");if(i.includes("/.cairn/")||i.endsWith("/.cairn"))return{content:[{type:"text",text:JSON.stringify({error:"Cannot run cairn_maintain from inside a .cairn/ directory. Please run from the project root."})}]};let r=oe.basename(n),o=await Ze(n),a=o.map(m=>oe.join(m,".cairn","index.db")),c={repos_indexed:1,sub_indexes_found:o.length,files_by_language:{},symbols_total:0,dependencies_mapped:0,security_findings:0,todos_found:0},l=await P(n,o);for(let m of l)await Et(t,m,r,e,c);for(let m of Qe){let d=oe.join(n,m);try{let u=await Ie.readFile(d,"utf-8"),f=[];m==="pom.xml"?f=lt(d,u,r):m==="package.json"?f=dt(d,u,r):m==="build.gradle"&&(f=ut(d,u,r));for(let g of f)yt(t,{repo:r,...g})}catch{}}c.todos_found=await me(t,l),St(t),a.length>0&&(ze(t,a),Ne(t));let p=null;try{let m=oe.join(n,".gitignore");(await Ie.readFile(m,"utf-8").catch(()=>"")).split(`
|
|
123
|
+
`).map(g=>g.trim()).some(g=>g===".cairn"||g===".cairn/"||g==="/.cairn"||g==="/.cairn/")||(p="Add .cairn/ to your .gitignore to avoid committing the index and session cache.")}catch{}c.duration_ms=Date.now()-s;let h={...c};return p&&(h.gitignore_hint=p),{content:[{type:"text",text:JSON.stringify(h,null,2)}]}}async function fe(t,e){let s=process.cwd(),n=oe.basename(s),i={files_by_language:{},symbols_total:0,dependencies_mapped:0,security_findings:0};for(let r of e)await Et(t,r,n,null,i);return St(t),i}function Tt(t,{query:e,limit:s=10,language:n,kind:i}){if(!e?.trim())return{content:[{type:"text",text:"[]"}]};let r=["main",...ue(t)],o=[];for(let m of r){let d=`
|
|
124
124
|
SELECT fqn, name, kind, language, description, path, repo,
|
|
125
125
|
bm25(fts_index) as score
|
|
126
|
-
FROM ${
|
|
127
|
-
WHERE ${
|
|
128
|
-
`,
|
|
126
|
+
FROM ${m}.fts_index
|
|
127
|
+
WHERE ${m}.fts_index MATCH ?
|
|
128
|
+
`,u=[e];n&&(d+=" AND language = ?",u.push(n)),i&&(d+=" AND kind = ?",u.push(i)),d+=" ORDER BY bm25(fts_index)",d+=` LIMIT ${s*2}`;try{let f=t.prepare(d).all(...u);o.push(...f)}catch{}}o.sort((m,d)=>m.score-d.score);let a=new Set,l=o.filter(m=>a.has(m.fqn)?!1:(a.add(m.fqn),!0)).slice(0,s),p=new Map;for(let m of r){let d=l.filter(u=>!p.has(u.fqn)).map(u=>u.fqn);if(d.length===0)break;try{let u=d.map(()=>"?").join(","),f=t.prepare(`SELECT fqn, line FROM ${m}.symbols WHERE fqn IN (${u})`).all(...d);for(let g of f)g.line!=null&&p.set(g.fqn,g.line)}catch{}}let h=l.map(m=>({...m,line:p.get(m.fqn)??null}));return{content:[{type:"text",text:JSON.stringify(h,null,2)}]}}function bt(t,{path:e}){let s=t.prepare("SELECT * FROM files WHERE path LIKE ?").all(`${e}%`);if(s.length===0)return{content:[{type:"text",text:JSON.stringify({path:e,error:"No files found"})}]};let n=s.map(d=>d.id),i=[...new Set(s.map(d=>d.language).filter(Boolean))],r=n.map(()=>"?").join(","),o=t.prepare(`SELECT * FROM symbols WHERE file_id IN (${r})`).all(...n),a={};for(let d of o){let u=d.kind||"other";a[u]||(a[u]=[]),a[u].push(d.name)}let c=t.prepare(`SELECT DISTINCT to_fqn FROM dependencies WHERE from_file_id IN (${r}) AND dep_type = 'imports'`).all(...n).map(d=>d.to_fqn),l=[];if(o.length>0){let d=o.map(f=>f.fqn),u=d.map(()=>"?").join(",");l=t.prepare(`SELECT DISTINCT f.path FROM dependencies d JOIN files f ON d.from_file_id = f.id WHERE d.to_fqn IN (${u})`).all(...d).map(f=>f.path)}let p=new Set(t.prepare("SELECT fqn FROM symbols").all().map(d=>d.fqn)),h=[...new Set(c.filter(d=>!p.has(d)))].slice(0,20),m={path:e,languages:i,symbols:a,imports_from:c.slice(0,20),imported_by:l.slice(0,20),external_deps:h};return{content:[{type:"text",text:JSON.stringify(m,null,2)}]}}function Rt(t,{module:e,mode:s}){if(s==="health"){let n=t.prepare("SELECT COUNT(*) as c FROM files").get()?.c||0,i=t.prepare("SELECT COUNT(*) as c FROM symbols").get()?.c||0,r=t.prepare("SELECT COUNT(*) as c FROM dependencies").get()?.c||0,o=t.prepare("SELECT COUNT(*) as c FROM security_findings").get()?.c||0,a=t.prepare("SELECT language, COUNT(*) as count FROM files GROUP BY language").all(),c=t.prepare("SELECT MAX(last_indexed) as t FROM files").get()?.t;return{content:[{type:"text",text:JSON.stringify({files:n,symbols:i,dependencies:r,security_findings:o,by_language:a,last_indexed:c?new Date(c).toISOString():null},null,2)}]}}if(s==="instability"){let n=t.prepare("SELECT id, path, language FROM files").all(),i={};for(let a of n){let l=a.path.replace(/\\/g,"/").split("/").slice(0,-1).join("/")||"/";e&&!l.includes(e)||(i[l]||(i[l]={name:l,language:a.language,fileIds:[]}),i[l].fileIds.push(a.id))}let r=[];for(let[,a]of Object.entries(i)){if(a.fileIds.length===0)continue;let c=a.fileIds.map(()=>"?").join(","),l=t.prepare(`SELECT COUNT(DISTINCT to_fqn) as c FROM dependencies WHERE from_file_id IN (${c})`).get(...a.fileIds)?.c||0,p=t.prepare(`SELECT fqn FROM symbols WHERE file_id IN (${c})`).all(...a.fileIds).map(u=>u.fqn),h=0;if(p.length>0){let u=p.map(()=>"?").join(",");h=t.prepare(`SELECT COUNT(DISTINCT from_file_id) as c FROM dependencies WHERE to_fqn IN (${u})`).get(...p)?.c||0}let m=h+l===0?0:l/(h+l),d=m>=.8?"safe_to_refactor":m>=.3?"review_before_change":"load_bearing";r.push({name:a.name,language:a.language,instability:Math.round(m*100)/100,status:d})}r.sort((a,c)=>c.instability-a.instability);let o=t.prepare(`
|
|
129
129
|
SELECT s.name || ' (' || COALESCE(s.language,'?') || ')' as label, COUNT(*) as dep_count
|
|
130
130
|
FROM dependencies d JOIN symbols s ON d.to_fqn = s.fqn
|
|
131
131
|
GROUP BY d.to_fqn ORDER BY dep_count DESC LIMIT 5
|
|
@@ -139,59 +139,63 @@ import{Server as ts}from"@modelcontextprotocol/sdk/server/index.js";import{Stdio
|
|
|
139
139
|
JOIN symbols s2 ON d2.to_fqn = s2.fqn
|
|
140
140
|
WHERE s2.file_id = d1.from_file_id
|
|
141
141
|
LIMIT 20
|
|
142
|
-
`).all();return{content:[{type:"text",text:JSON.stringify({cycles_detected:n},null,2)}]}}return{content:[{type:"text",text:JSON.stringify({error:`Unknown mode: ${s}`})}]}}function
|
|
142
|
+
`).all();return{content:[{type:"text",text:JSON.stringify({cycles_detected:n},null,2)}]}}return{content:[{type:"text",text:JSON.stringify({error:`Unknown mode: ${s}`})}]}}function wt(t,{paths:e,severity:s="HIGH",language:n}){let i=`
|
|
143
143
|
SELECT sf.severity, sf.cwe, sf.rule_name as rule, sf.line,
|
|
144
144
|
sf.code_snippet as snippet, sf.description as fix,
|
|
145
145
|
f.path as file, f.language as lang
|
|
146
146
|
FROM security_findings sf
|
|
147
147
|
JOIN files f ON sf.file_id = f.id
|
|
148
148
|
WHERE 1=1
|
|
149
|
-
`,r=[];s==="HIGH"?i+=" AND sf.severity = 'HIGH'":s==="MEDIUM"?i+=" AND sf.severity IN ('HIGH','MEDIUM')":s!=="ALL"&&(i+=" AND sf.severity = ?",r.push(s)),n&&(i+=" AND f.language = ?",r.push(n)),e&&e.length>0&&(i+=` AND (${e.map(()=>"f.path LIKE ?").join(" OR ")})`,r.push(...e.map(
|
|
149
|
+
`,r=[];s==="HIGH"?i+=" AND sf.severity = 'HIGH'":s==="MEDIUM"?i+=" AND sf.severity IN ('HIGH','MEDIUM')":s!=="ALL"&&(i+=" AND sf.severity = ?",r.push(s)),n&&(i+=" AND f.language = ?",r.push(n)),e&&e.length>0&&(i+=` AND (${e.map(()=>"f.path LIKE ?").join(" OR ")})`,r.push(...e.map(l=>`${l}%`))),i+=" ORDER BY sf.severity, f.path";let o=t.prepare(i).all(...r),a={},c={};for(let l of o)a[l.lang]=(a[l.lang]||0)+1,c[l.severity]=(c[l.severity]||0)+1;return{content:[{type:"text",text:JSON.stringify({total_findings:o.length,by_language:a,by_severity:c,findings:o.slice(0,100)},null,2)}]}}import Q from"fs";import Z from"path";function K(t,e,s={}){let{noComments:n=!0,noEmptyLines:i=!0,aggressive:r=!1}=s,o=t;return n&&(o=wn(o,e)),i&&(o=On(o)),o=Nn(o),r&&(o=Ln(o)),o.trim()}function wn(t,e){switch(e){case"java":case"typescript":case"javascript":case"vue":return t.replace(/\/\*[\s\S]*?\*\//g,"").replace(/(["'`])(?:(?!\1)[^\\]|\\.)*\1|\/(?![/*])(?:[^/\\\n]|\\.)*\/[gimsuy]*|\/\/[^\n]*/g,(s,n)=>n||!s.startsWith("//")?s:"");case"python":return t.replace(/#.*$/gm,"").replace(/'''[\s\S]*?'''/g,"").replace(/"""[\s\S]*?"""/g,"");case"sql":return t.replace(/--.*$/gm,"").replace(/\/\*[\s\S]*?\*\//g,"");case"xml":case"html":return t.replace(/<!--[\s\S]*?-->/g,"");case"config":return t.replace(/#.*$/gm,"");default:return t}}function On(t){return t.split(`
|
|
150
150
|
`).filter(e=>e.trim().length>0).join(`
|
|
151
|
-
`)}function
|
|
151
|
+
`)}function Nn(t){return t.split(`
|
|
152
152
|
`).map(e=>e.trimEnd()).join(`
|
|
153
|
-
`)}function
|
|
154
|
-
`).filter(
|
|
155
|
-
`)),
|
|
156
|
-
|
|
157
|
-
`)}async function
|
|
158
|
-
`),"utf8")/1024>
|
|
159
|
-
`);
|
|
160
|
-
`)[0].slice(0,120),ref:
|
|
161
|
-
`),n=null;continue}r.add(c),n={phase_number:c,text:
|
|
162
|
-
`).trim()||null}))}function
|
|
163
|
-
`)){let i=n.indexOf(":");i!==-1&&(s[n.slice(0,i).trim()]=n.slice(i+1).trim())}return s}function
|
|
153
|
+
`)}function Ln(t){return t.replace(/\s+/g," ").replace(/\s*([=+\-*/%<>!&|^~?:;,{}()[\]])\s*/g,"$1").replace(/;\s*}/g,"}").replace(/\(\s+/g,"(").replace(/\s+\)/g,")").trim()}function he(t,e={}){let{noStyle:s=!1,noComments:n=!0,noEmptyLines:i=!0,aggressive:r=!1}=e,o=[],a=t.match(/<template>([\s\S]*?)<\/template>/i);if(a){let l=a[1];n&&(l=l.replace(/<!--[\s\S]*?-->/g,"")),i&&(l=l.split(`
|
|
154
|
+
`).filter(p=>p.trim()).join(`
|
|
155
|
+
`)),l=l.replace(/\s+/g," ").trim(),l&&o.push(`<template>${l}</template>`)}let c=t.match(/<script([^>]*)>([\s\S]*?)<\/script>/i);if(c){let l=c[1],p=K(c[2],"javascript",{noComments:n,noEmptyLines:i,aggressive:r}),h=/setup/i.test(l)?"<script setup>":"<script>";p&&o.push(`${h}${p}</script>`)}if(!s){let l=t.match(/<style([^>]*)>([\s\S]*?)<\/style>/i);if(l){let p=l[2].replace(/\/\*[\s\S]*?\*\//g,"").replace(/\s+/g," ").trim();p&&o.push(`<style${l[1]}>${p}</style>`)}}return o.join(`
|
|
156
|
+
|
|
157
|
+
`)}async function Ot(t={}){let{bundleName:e="default",noComments:s=!0,noEmptyLines:n=!0,noStyle:i=!1,aggressive:r=!1,onlyChanged:o=!1,filterLang:a=null,filterPaths:c=null,maxSizeKB:l=800}=t,p=Z.join(A(),"bundles"),h=process.cwd(),m=Z.join(p,`${e}.txt`),d=Z.join(p,`${e}.mtime.json`),u=o&&Q.existsSync(d)?JSON.parse(Q.readFileSync(d,"utf8")):{},f={},g=[],y={processed:0,skipped_unchanged:0,skipped_size:0,original_bytes:0,compressed_bytes:0,by_language:{}},_=a?Array.isArray(a)?a:[a]:null,T=await P(h);for(let E of T){let R=Z.extname(E).toLowerCase(),b=J[R];if(!b||_&&!_.includes(b))continue;if(c){let S=Z.relative(h,E).replace(/\\/g,"/");if(!c.some(C=>S.startsWith(C)))continue}let O=Q.statSync(E).mtimeMs;if(f[E]=O,o&&u[E]===O){y.skipped_unchanged++;continue}if(Buffer.byteLength(g.join(`
|
|
158
|
+
`),"utf8")/1024>l){y.skipped_size++;continue}let k;try{k=Q.readFileSync(E,"utf8")}catch{continue}y.original_bytes+=Buffer.byteLength(k,"utf8");let w=b==="vue"?he(k,{noStyle:i,noComments:s,noEmptyLines:n,aggressive:r}):K(k,b,{noComments:s,noEmptyLines:n,aggressive:r});if(!w.trim())continue;let le=Z.relative(h,E).replace(/\\/g,"/");g.push(`### File: ${le}`),g.push(w.trim()),g.push(""),y.compressed_bytes+=Buffer.byteLength(w,"utf8"),y.processed++,y.by_language[b]=(y.by_language[b]||0)+1}let x=g.join(`
|
|
159
|
+
`);Q.writeFileSync(m,x,"utf8"),o&&Q.writeFileSync(d,JSON.stringify(f),"utf8");let I=y.original_bytes>0?Math.round((1-y.compressed_bytes/y.original_bytes)*100):0;return{bundle_path:m,processed:y.processed,skipped_unchanged:y.skipped_unchanged,skipped_size_cap:y.skipped_size,original_kb:Math.round(y.original_bytes/1024),compressed_kb:Math.round(y.compressed_bytes/1024),reduction_pct:I,by_language:y.by_language}}async function Nt(t,e){let{bundle_name:s="default",filter_paths:n=null,filter_language:i=null,no_comments:r=!0,no_empty_lines:o=!0,no_style:a=!1,aggressive:c=!1,only_changed:l=!1,max_size_kb:p=800}=e,h=await Ot({bundleName:s,noComments:r,noEmptyLines:o,noStyle:a,aggressive:c,onlyChanged:l,filterLang:i,filterPaths:n,maxSizeKB:p});return h.next_step=`Read the bundle: Read("${h.bundle_path}")`,{content:[{type:"text",text:JSON.stringify(h,null,2)}]}}import q from"fs";import ee from"path";var kn=1e3,vn=100*1024;function In(t,e,s,n){if(t&&typeof t=="object"&&t.ref)return t;let i=typeof t=="string"?t:String(t),r=/^\[\d{4}-\d{2}-\d{2}\]/.test(i)?i:`[${s}] ${i}`;if(r.length<=kn)return r;let o=ee.join(e,"notes");q.mkdirSync(o,{recursive:!0});let c=`note-${new Date().toISOString().replace(/[:.]/g,"-")}-${n}.md`,l=ee.join(o,c);return q.writeFileSync(l,r,"utf8"),{description:r.split(`
|
|
160
|
+
`)[0].slice(0,120),ref:ee.join(".cairn","notes",c)}}function Lt(t,{message:e,active_files:s=[],notes:n=[]}){if(typeof n=="string")try{n=JSON.parse(n)}catch{n=n?[n]:[]}Array.isArray(n)||(n=[]);let i=A(),r=ee.join(i,"session.json"),o=new Date().toISOString().slice(0,10),a={};try{let p=t.prepare("SELECT path FROM main.files").all();for(let{path:h}of p)try{a[h]=q.statSync(h).mtimeMs}catch{}}catch{}for(let p of s)try{a[p]=q.statSync(p).mtimeMs}catch{}let c=n.map((p,h)=>In(p,i,o,h));if(q.existsSync(r))try{if(q.statSync(r).size>vn){let h=1;for(;q.existsSync(ee.join(i,`session.${h}.json`));)h++;q.renameSync(r,ee.join(i,`session.${h}.json`))}}catch{}let l={message:e,checkpoint_at:new Date().toISOString(),active_files:s,notes:c,mtime_snapshot:a};return q.writeFileSync(r,JSON.stringify(l,null,2),"utf8"),{content:[{type:"text",text:JSON.stringify({saved:!0,checkpoint_at:l.checkpoint_at,active_files_tracked:s.length,notes_saved:c.length},null,2)}]}}import G from"fs";import xe from"path";import{exec as ns}from"child_process";import{promisify as ss}from"util";import Y from"fs";import kt from"path";function vt(t){try{return JSON.parse(Y.readFileSync(kt.join(t,"minify-map.json"),"utf8"))}catch{return{}}}function Ce(t,e){Y.mkdirSync(t,{recursive:!0}),Y.writeFileSync(kt.join(t,"minify-map.json"),JSON.stringify(e,null,2),"utf8")}function It(t,e){let s=0;for(let[n,i]of Object.entries(e)){let r=!1;if(!Y.existsSync(n))r=!0;else if(!Y.existsSync(i.tempPath))r=!0;else try{Y.statSync(n).mtimeMs>i.minifiedAt&&(r=!0)}catch{r=!0}if(r){if(i.tempPath)try{Y.unlinkSync(i.tempPath)}catch{}delete e[n],s++}}return s}import F from"fs";import te from"path";var Mt=["preference","experience"],Ct=["preference","experience","decision","knowledge"];function At(t){if(!t||typeof t!="string")return[];let e=t.split(/\r?\n/),s=!1,n=null,i=[],r=new Set;for(let o of e){if(/^##\s+Phases\s*$/i.test(o)){s=!0;continue}if(/^##\s+/.test(o)&&!/^###/.test(o)){n&&(i.push(n),n=null),s=!1;continue}if(!s)continue;let a=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(o);if(a){n&&i.push(n);let c=parseInt(a[1],10),l=a[2];if(r.has(c)){process.stderr.write(`[cairn] memo phase parser: duplicate Phase ${c} ignored
|
|
161
|
+
`),n=null;continue}r.add(c),n={phase_number:c,text:l,bodyLines:[]};continue}n&&n.bodyLines.push(o)}return n&&i.push(n),i.map(o=>({phase_number:o.phase_number,text:o.text,body:o.bodyLines.join(`
|
|
162
|
+
`).trim()||null}))}function jt(t,e,s){let n=t.prepare("SELECT id, phase_number, status FROM main.roadmap_phases WHERE decision_name = ? ORDER BY phase_number").all(e),i=new Map(n.map(m=>[m.phase_number,m])),r=new Set,o=t.prepare("UPDATE main.roadmap_phases SET text = ?, body = ?, status = CASE WHEN status = ? THEN ? ELSE status END WHERE id = ?"),a=t.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, body, status, position) VALUES (?, ?, NULL, ?, ?, ?, ?)"),c=t.prepare("UPDATE main.roadmap_phases SET status = 'stale' WHERE id = ?"),l=[],p=[],h=[];for(let m=0;m<s.length;m++){let d=s[m];r.add(d.phase_number);let u=i.get(d.phase_number);if(u)o.run(d.text,d.body,"stale","open",u.id),l.push({id:u.id,phase_number:d.phase_number,text:d.text,reactivated:u.status==="stale"});else{let f=a.run(e,d.phase_number,d.text,d.body,"open",m);p.push({id:Number(f.lastInsertRowid),phase_number:d.phase_number,text:d.text})}}for(let[m,d]of i)!r.has(m)&&d.status!=="stale"&&d.status!=="done"&&(c.run(d.id),h.push({id:d.id,phase_number:m}));return{updated:l.length,inserted:p.length,staled:h.length,updated_phases:l,inserted_phases:p,staled_phases:h}}function ae(t){return t.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"")}function Cn(t,e){return`${t}_${ae(e)}.md`}function $n(t){let e=t.match(/^---\n([\s\S]*?)\n---/);if(!e)return{};let s={};for(let n of e[1].split(`
|
|
163
|
+
`)){let i=n.indexOf(":");i!==-1&&(s[n.slice(0,i).trim()]=n.slice(i+1).trim())}return s}function Ft(t){let e=te.join(t,"MEMORY.md");return F.existsSync(e)?F.readFileSync(e,"utf8"):""}function $t(t,e){let s=["# Memory Index",""];for(let n of e)s.push(`- [${n.name}](${n.file}) \u2014 ${n.description} \`[${n.type}]\``);F.writeFileSync(te.join(t,"MEMORY.md"),s.join(`
|
|
164
164
|
`)+`
|
|
165
|
-
`,"utf8")}function U(t){let e=
|
|
166
|
-
`)){let i=n.match(/^- \[(.+?)\]\((.+?)\) — (.*?) `\[(.+?)\]`$/);i&&s.push({name:i[1],file:i[2],description:i[3],type:i[4]})}return s}function
|
|
165
|
+
`,"utf8")}function U(t){let e=Ft(t),s=[];for(let n of e.split(`
|
|
166
|
+
`)){let i=n.match(/^- \[(.+?)\]\((.+?)\) — (.*?) `\[(.+?)\]`$/);i&&s.push({name:i[1],file:i[2],description:i[3],type:i[4]})}return s}function Dt(t){return U(t).filter(s=>s.type==="preference").map(s=>{let n=te.join(t,s.file);return F.existsSync(n)?{name:s.name,description:s.description,file:s.file,content:F.readFileSync(n,"utf8")}:null}).filter(Boolean)}function Pt(t,{action:e,name:s,type:n,content:i,description:r="",origin:o}){let a=z();if(e==="list"){let y=U(a);return{content:[{type:"text",text:JSON.stringify({total:y.length,index:Ft(a)||"(no memories yet)",entries:y},null,2)}]}}if(!s)throw new Error(`name is required for action "${e}". Pass: name (memory identifier), type (preference|experience|decision|knowledge), content (what to remember), and optionally description.`);if(e==="read"){let y=U(a).find(T=>T.name===s||ae(T.name)===ae(s));if(!y)return{content:[{type:"text",text:JSON.stringify({found:!1,name:s},null,2)}]};let _=te.join(a,y.file);return F.existsSync(_)?{content:[{type:"text",text:JSON.stringify({found:!0,name:y.name,type:y.type,description:y.description,content:F.readFileSync(_,"utf8")},null,2)}]}:{content:[{type:"text",text:JSON.stringify({found:!1,name:s,reason:"file missing"},null,2)}]}}if(e==="delete"){let y=U(a),_=y.findIndex(I=>I.name===s||ae(I.name)===ae(s));if(_===-1)return{content:[{type:"text",text:JSON.stringify({deleted:!1,reason:"not found"},null,2)}]};let[T]=y.splice(_,1),x=te.join(a,T.file);return F.existsSync(x)&&F.unlinkSync(x),$t(a,y),{content:[{type:"text",text:JSON.stringify({deleted:!0,name:s},null,2)}]}}if(!n)throw new Error("type is required for write");if(!i)throw new Error("content is required for write");if(!Ct.includes(n))throw new Error(`type must be one of: ${Ct.join(", ")}`);let c=Cn(n,s),l=te.join(a,c),p=F.existsSync(l),h=new Date().toISOString(),m=h;if(p){let y=$n(F.readFileSync(l,"utf8"));y.created_at&&(m=y.created_at)}let d=["---",`name: ${s}`,`description: ${r||s}`,`type: ${n}`];o&&d.push(`origin: ${o}`),d.push(`created_at: ${m}`,`updated_at: ${h}`,"---"),F.writeFileSync(l,d.join(`
|
|
167
167
|
`)+`
|
|
168
168
|
|
|
169
169
|
`+i+`
|
|
170
|
-
`,"utf8");let
|
|
171
|
-
`),"utf8")}function
|
|
170
|
+
`,"utf8");let u=U(a),f=u.findIndex(y=>y.file===c),g={name:s,file:c,description:r||s,type:n};return f>=0?u[f]=g:u.push(g),$t(a,u),{content:[{type:"text",text:JSON.stringify({saved:!0,name:s,type:n,file:l,action:p?"updated":"created"},null,2)}]}}import $e from"fs";import Mn from"path";function An(t){if(t.length===0)return null;let e=t.map(i=>i.replace(/\\/g,"/")),s=e[0].split("/"),n="";for(let i=1;i<=s.length;i++){let r=s.slice(0,i).join("/");if(e.every(o=>o.startsWith(r+"/")))n=r;else break}return n||null}function Ut(t,e){let s=Object.keys(t);if(s.length===0||s.filter(l=>{try{return $e.statSync(l),!1}catch{return!0}}).length<Math.ceil(s.length*.5))return null;let i=An(s);if(!i)return null;let r=e.replace(/\\/g,"/"),o=i.replace(/\\/g,"/");if(r===o)return null;let a=s.slice(0,Math.min(8,s.length));return a.filter(l=>{let p=l.replace(/\\/g,"/").replace(o+"/","");try{return $e.statSync(r+"/"+p),!0}catch{return!1}}).length<Math.ceil(a.length*.6)?null:{oldRoot:o,newRoot:r}}function ge(t,e,s){let n=t.replace(/\\/g,"/");if(n.startsWith(e+"/"))return s+n.slice(e.length);let i=e.replace(/\//g,"\\");return t.startsWith(i+"\\")?s+"/"+t.slice(i.length+1).replace(/\\/g,"/"):t}function Ht(t,e,s,n,i,r){let o={};for(let[p,h]of Object.entries(s.mtime_snapshot||{}))o[ge(p,i,r)]=h;s.mtime_snapshot=o,s.active_files=(s.active_files||[]).map(p=>ge(p,i,r)),$e.writeFileSync(Mn.join(e,"session.json"),JSON.stringify(s,null,2),"utf8");let a=i.replace(/\//g,"\\");function c(p,h){try{t.prepare(`UPDATE ${p} SET ${h} = replace(${h}, ?, ?)`).run(i,r),t.prepare(`UPDATE ${p} SET ${h} = replace(${h}, ?, ?)`).run(a,r)}catch{}}c("files","path"),c("symbols","fqn"),c("todos","file");try{t.prepare("DELETE FROM fts_index_content WHERE c0 LIKE ? OR c0 LIKE ? OR c3 LIKE ? OR c3 LIKE ?").run(i+"%",a+"%",i+"%",a+"%"),t.prepare("INSERT INTO fts_index(fts_index) VALUES('rebuild')").run()}catch{}let l={};for(let[p,h]of Object.entries(n)){let m={...h};m.tempPath&&(m.tempPath=ge(m.tempPath,i,r)),l[ge(p,i,r)]=m}for(let p of Object.keys(n))delete n[p];Object.assign(n,l)}import N from"fs";import ne from"path";var H="roadmap";function se(){return ne.join(A(),"roadmap.md")}function Wt(){return ne.join(A(),"roadmap_completed.md")}function jn(t){let e=[];for(let s of t){let n=(s.text||"").trim();n&&(!Number.isInteger(s.phase_number)||s.phase_number<1||e.push({phase_number:s.phase_number,text:n,body:s.body??null}))}return e}var Me="decision_production_roadmap.md";function Fn(t){let e=t.match(/^---\n[\s\S]*?\n---\n?/);return e?t.slice(e[0].length):t}function Dn(t,e){let s=ne.join(t,"MEMORY.md");if(!N.existsSync(s))return;let n=N.readFileSync(s,"utf8").split(/\r?\n/).filter(i=>!i.includes(`](${e})`));N.writeFileSync(s,n.join(`
|
|
171
|
+
`),"utf8")}function Pn(){let t=se();if(N.existsSync(t))return null;let e=[],s=we();s&&e.push(s),e.push(ne.join(A(),"memory"));let n=null,i=null;for(let o of e){let a=ne.join(o,Me);n==null&&N.existsSync(a)&&(n=Fn(N.readFileSync(a,"utf8")).trim(),i=a)}if(n==null)return null;let r=/^#\s/m.test(n)?"":`# Roadmap
|
|
172
172
|
|
|
173
173
|
`;N.writeFileSync(t,r+n+`
|
|
174
|
-
`,"utf8");for(let o of e){let a=
|
|
174
|
+
`,"utf8");for(let o of e){let a=ne.join(o,Me);try{N.existsSync(a)&&N.unlinkSync(a)}catch{}try{Dn(o,Me)}catch{}}return{migrated:!0,from:i,to:t}}function qt(t){Pn();let e=se();if(!N.existsSync(e))return null;let s=N.readFileSync(e,"utf8");if(!/^##\s+Phases\s*$/im.test(s))return null;let n=jn(At(s));return jt(t,H,n)}function Bt(t){let e=se();if(!N.existsSync(e))return{removed:!1,title:null};let s=N.readFileSync(e,"utf8").split(/\r?\n/),n=[],i=!1,r=!1,o=!1,a=null;for(let c of s){if(/^##\s+Phases\s*$/i.test(c)){i=!0,r=!1,n.push(c);continue}if(/^##\s+/.test(c)&&!/^###/.test(c)){i=!1,r=!1,n.push(c);continue}if(i){let l=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(c);if(l){if(parseInt(l[1],10)===t){r=!0,o=!0,a=l[2];continue}r=!1,n.push(c);continue}if(r)continue;n.push(c);continue}n.push(c)}if(o){let c=n.join(`
|
|
175
175
|
`).replace(/\n{3,}/g,`
|
|
176
176
|
|
|
177
|
-
`);N.writeFileSync(e,c,"utf8")}return{removed:o,title:a}}function
|
|
177
|
+
`);N.writeFileSync(e,c,"utf8")}return{removed:o,title:a}}function Gt(t,e,s={}){let n=Wt(),i=N.existsSync(n)?N.readFileSync(n,"utf8"):`# Completed phases
|
|
178
178
|
|
|
179
179
|
Shipped roadmap phases, newest last.
|
|
180
180
|
`,r=s.date||new Date().toISOString().slice(0,10),o=s.commit?` (${s.commit})`:"",a=`- ${r} \u2014 Phase ${t}: ${e}${o}`;return i.endsWith(`
|
|
181
181
|
`)||(i+=`
|
|
182
182
|
`),i+=a+`
|
|
183
|
-
`,N.writeFileSync(n,i,"utf8"),{logged:a}}function
|
|
184
|
-
`)
|
|
185
|
-
`)
|
|
186
|
-
`
|
|
187
|
-
`)}
|
|
188
|
-
`)
|
|
189
|
-
`)
|
|
190
|
-
`))}if(
|
|
191
|
-
`))}if(
|
|
192
|
-
`)
|
|
183
|
+
`,N.writeFileSync(n,i,"utf8"),{logged:a}}function ye(){let t=Wt();return N.existsSync(t)?N.readFileSync(t,"utf8").split(/\r?\n/).filter(e=>/^-\s+\d{4}-\d{2}-\d{2}\s+—\s+Phase\s/.test(e)).length:0}import _e from"fs";import Un from"path";var Xt="<!-- cairn:roadmap -->",Hn={open:"open",in_progress:"in progress",done:"done",blocked:"blocked",stale:"stale"};function Wn(t){return t==="done"?"x":t==="in_progress"?"~":t==="blocked"?"!":" "}function qn(){return Un.join($(),"ROADMAP.md")}function Bn(t){try{return t.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function Gn(t){let e=new Map,s;try{s=t.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return e}for(let n of s)e.has(n.blocked_id)||e.set(n.blocked_id,new Set),e.get(n.blocked_id).add(n.blocker_id);return e}function Xn(t,e){let s=t.decision_name??"",n=e.decision_name??"";return s!==n?s<n?-1:1:t.phase_number!=null&&e.phase_number!=null?t.phase_number-e.phase_number:t.position-e.position||t.id-e.id}function Jn(t){let e;try{e=t.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(e.filter(u=>u.status!=="stale").length===0)return null;let s=Bn(t),n=Gn(t),i=new Map(e.map(u=>[u.id,u.status])),r=new Map(e.map(u=>[u.id,u.phase_number!=null?`Phase ${u.phase_number}`:u.text])),o=u=>{let f=n.get(u);return f?[...f].filter(g=>i.get(g)!=="done").map(g=>r.get(g)||`#${g}`):[]},a=new Map;for(let u of e){let f=u.parent_phase_id??"root";a.has(f)||a.set(f,[]),a.get(f).push(u)}for(let u of a.values())u.sort(Xn);let c=new Map;for(let u of a.get("root")||[])u.decision_name!=null&&(c.has(u.decision_name)||c.set(u.decision_name,[]),c.get(u.decision_name).push(u));let l=[...c.keys()].sort((u,f)=>u===H?-1:f===H?1:u<f?-1:u>f?1:0),p=[Xt,"<!--"," 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.","-->","","# Roadmap","","Legend: `[x]` done \xB7 `[~]` in progress \xB7 `[ ]` open \xB7 `[!]` blocked."],h=(u,f)=>{let g=" ".repeat(f),y=u.phase_number!=null?`Phase ${u.phase_number}: ${u.text}`:u.text,_=Hn[u.status]||u.status,T=o(u.id);if(T.length&&(_+=` \xB7 waiting on ${T.join(", ")}`),s===u.id&&(_+=" \xB7 \u2190 current"),p.push(`${g}- [${Wn(u.status)}] **${y}** \u2014 ${_}`),u.body){let x=u.body.split(`
|
|
184
|
+
`).map(I=>I.trim()).find(Boolean);x&&p.push(`${g} ${x}`)}for(let x of a.get(u.id)||[])x.status!=="stale"&&h(x,f+1)},m=!1;for(let u of l){let f=c.get(u).filter(y=>y.status!=="stale"),g=u===H?f.filter(y=>y.status!=="done"):f;if(g.length!==0){if(m=!0,p.push(""),u===H)p.push("## Active plan");else{let y=f.filter(_=>_.status==="done").length;p.push(`## ${u} \u2014 ${y}/${f.length} done`)}p.push("");for(let y of g)h(y,0)}}if(!m)return null;let d=ye();return d>0&&p.push("","---","",`**Shipped:** ${d} phase${d===1?"":"s"} \u2014 see \`.cairn/roadmap_completed.md\`.`),p.join(`
|
|
185
|
+
`)+`
|
|
186
|
+
`}function Ae(t){let e=qn(),s=Jn(t),n=null;if(_e.existsSync(e))try{n=_e.readFileSync(e,"utf8")}catch{n=null}if(n!=null&&!n.startsWith(Xt))return{written:!1,reason:"ROADMAP.md exists and is not cairn-generated \u2014 refusing to overwrite",path:e};if(s==null){if(n!=null)try{return _e.unlinkSync(e),{written:!1,removed:!0,path:e}}catch(i){return{written:!1,reason:`remove failed: ${i.message}`,path:e}}return{written:!1,reason:"empty roadmap \u2014 no file written",path:e}}return n===s?{written:!1,unchanged:!0,path:e,bytes:Buffer.byteLength(s)}:(_e.writeFileSync(e,s,"utf8"),{written:!0,path:e,bytes:Buffer.byteLength(s)})}function Jt(t){try{return Ae(t)}catch(e){return process.stderr.write(`[cairn] ROADMAP.md publish skipped: ${e.message}
|
|
187
|
+
`),null}}var je={open:"[ ]",in_progress:"[~]",done:"[x]",blocked:"[!]",stale:"[s]"},Yt=new Set(["open","in_progress","done","blocked","stale"]),Yn=new Set(["open","in_progress"]);function Se(t){return t.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}function Vn(t,e){t.prepare("INSERT INTO main.roadmap_cursor (singleton, phase_id) VALUES (1, ?) ON CONFLICT(singleton) DO UPDATE SET phase_id = excluded.phase_id").run(e)}function Vt(t){t.prepare("DELETE FROM main.roadmap_cursor WHERE singleton = 1").run()}function zn(t,e){let s=t.prepare("SELECT parent_phase_id, status FROM main.roadmap_phases WHERE id = ?"),n=t.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'"),i=s.get(e);for(;i&&i.parent_phase_id!=null;)n.run(i.parent_phase_id),i=s.get(i.parent_phase_id)}function Fe(t,e){Vn(t,e),t.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'").run(e),zn(t,e)}function Qt(t){let e=t.filter(s=>s.status!=="stale");return e.length===0?"stale":e.every(s=>s.status==="done")?"done":e.some(s=>s.status==="in_progress")?"in_progress":e.some(s=>s.status==="blocked")&&!e.some(s=>s.status==="in_progress")?"blocked":"open"}function De(t){let e=new Map,s;try{s=t.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all()}catch{return e}for(let n of s)e.has(n.blocked_id)||e.set(n.blocked_id,new Set),e.get(n.blocked_id).add(n.blocker_id);return e}function Pe(t,e,s){let n=e.get(t);if(!n||n.size===0)return[];let i=[];for(let r of n)s.get(r)!=="done"&&i.push(r);return i}function zt(t,e){let s=t.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(s.length===0)return{id:null,reason:"No phases \u2014 add a ## Phases section to .cairn/roadmap.md."};let n=new Map(s.map(h=>[h.id,h.status])),i=De(t),r=new Map;for(let h of s){let m=h.parent_phase_id??`root:${h.decision_name}`;r.has(m)||r.set(m,[]),r.get(m).push(h)}for(let h of r.values())h.sort((m,d)=>m.phase_number!=null&&d.phase_number!=null?m.phase_number-d.phase_number:m.position-d.position||m.id-d.id);let o=[],a=new Set;for(let h of s){if(h.parent_phase_id!=null||a.has(h.decision_name))continue;a.add(h.decision_name);let m=[],d=r.get(`root:${h.decision_name}`)||[];for(let u=d.length-1;u>=0;u--)m.push(d[u]);for(;m.length;){let u=m.pop();o.push(u);let f=r.get(u.id)||[];for(let g=f.length-1;g>=0;g--)m.push(f[g])}}let c=h=>Yn.has(h.status),l=[],p=0;if(e!=null){let h=o.findIndex(m=>m.id===e);p=h===-1?0:h+1}for(let h=p;h<o.length;h++){let m=o[h];if(!c(m))continue;let d=Pe(m.id,i,n);if(d.length>0){l.push({id:m.id,blockers:d});continue}let u=e!=null?s.find(y=>y.id===e):null,f=u&&u.decision_name!==m.decision_name,g;return e==null?g=`First actionable phase: ${m.decision_name??"(sub-task)"} / Phase ${m.phase_number??"\xB7"}.`:f?g=`Previous decision "${u.decision_name}" is complete \u2014 advanced to "${m.decision_name}".`:g=`Next open phase in "${m.decision_name??"(sub-task)"}" after id ${e}.`,l.length>0&&(g+=` Skipped ${l.length} dep-blocked phase(s): ${l.map(y=>`id ${y.id} (blocked by ${y.blockers.join(",")})`).join("; ")}.`),{id:m.id,reason:g,skipped_blocked:l}}return l.length>0?{id:null,reason:`All remaining open phases are blocked by unfinished dependencies: ${l.map(h=>`id ${h.id} (blocked by ${h.blockers.join(",")})`).join("; ")}.`,skipped_blocked:l}:{id:null,reason:"No actionable phases remaining."}}function Kn(t){let e=t.filter(n=>n.status!=="stale");return{done:e.filter(n=>n.status==="done").length,total:e.length}}function Qn(t,{compact:e=!1,showStale:s=!1,decisionFilter:n=null}={}){let i=t.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text, status, position FROM main.roadmap_phases ORDER BY decision_name, phase_number, position, id").all();if(i.length===0)return"(no roadmap \u2014 add a `## Phases` section to .cairn/roadmap.md)";let r=Se(t),o=De(t),a=new Map(i.map(f=>[f.id,f.status])),c=new Map;for(let f of i){let g=f.parent_phase_id??"root";c.has(g)||c.set(g,[]),c.get(g).push(f)}for(let f of c.values())f.sort((g,y)=>g.decision_name&&y.decision_name&&g.decision_name!==y.decision_name?g.decision_name.localeCompare(y.decision_name):g.phase_number!=null&&y.phase_number!=null?g.phase_number-y.phase_number:g.position-y.position||g.id-y.id);let l=c.get("root")||[],p=new Map;for(let f of l)f.decision_name!=null&&(n!=null&&f.decision_name!==n||(p.has(f.decision_name)||p.set(f.decision_name,[]),p.get(f.decision_name).push(f)));if(n!=null&&p.size===0)return`(no decision named "${n}" \u2014 try cairn_roadmap action=tree without filter to list all)`;let h=0,m=[],d=f=>{let g=Pe(f.id,o,a);return g.length>0?` blocked-by:${g.join(",")}`:""},u=(f,g)=>{let y=c.get(f)||[];for(let _ of y){if(!s&&_.status==="stale"){h++;continue}let T=je[_.status]||"[?]",x=r===_.id?" \u2192":"",I=_.phase_number!=null?`Phase ${_.phase_number}: ${_.text}`:_.text,E=e?"":` (id:${_.id})`;m.push(`${" ".repeat(g)}${T}${x} ${I}${E}${d(_)}`),u(_.id,g+1)}};for(let[f,g]of p){if(!s&&g.every(E=>E.status==="stale")){h+=g.length;continue}let y=Qt(g),{done:_,total:T}=Kn(g),x=T>0?` (${_}/${T} done)`:"",I=f===H?"Roadmap":`Decision: ${f}`;m.push(`${je[y]} ${I}${x}`);for(let E of g){if(!s&&E.status==="stale"){h++;continue}if(f===H&&E.status==="done")continue;let R=je[E.status]||"[?]",b=r===E.id?" \u2192":"",O=`Phase ${E.phase_number}: ${E.text}`,j=e?"":` (id:${E.id})`;m.push(` ${R}${b} ${O}${j}${d(E)}`),u(E.id,2)}}return h>0&&m.push(`(${h} stale phase${h===1?"":"s"} hidden \u2014 pass show_stale: true to view)`),m.join(`
|
|
188
|
+
`)}function Kt(t){let e=t.prepare("SELECT decision_name, status FROM main.roadmap_phases WHERE parent_phase_id IS NULL").all(),s=new Map;for(let o of e)s.has(o.decision_name)||s.set(o.decision_name,[]),s.get(o.decision_name).push(o);let n=0;for(let o of s.values()){let a=Qt(o);(a==="in_progress"||a==="open")&&n++}let i=Se(t),r=null;if(i!=null){let o=t.prepare("SELECT id, decision_name, phase_number, parent_phase_id, text FROM main.roadmap_phases WHERE id = ?").get(i);if(o){let a=o.decision_name,c=o.phase_number,l=o;for(;l.parent_phase_id!=null&&(l=t.prepare("SELECT id, decision_name, phase_number, parent_phase_id FROM main.roadmap_phases WHERE id = ?").get(l.parent_phase_id),!!l);)a=l.decision_name??a,c=l.phase_number??c;r=o.phase_number!=null?`${o.decision_name} / Phase ${o.phase_number} (${o.text})`:`${a} / Phase ${c} \u2192 sub: ${o.text}`}}return{decisions_total:s.size,decisions_in_flight:n,cursor_phase_id:i,current:r}}function Zn(t,e,s){if(e===s)throw new Error("A phase cannot block itself");if(t.prepare("SELECT id FROM main.roadmap_phases WHERE id IN (?, ?)").all(e,s).length!==2)throw new Error(`One or both phases not found (from=${e}, to=${s})`);let i=t.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps").all(),r=new Map;for(let c of i)r.has(c.blocked_id)||r.set(c.blocked_id,[]),r.get(c.blocked_id).push(c.blocker_id);let o=[e],a=new Set;for(;o.length;){let c=o.pop();if(c===s)throw new Error(`Cycle: phase ${s} already (transitively) blocks phase ${e}`);if(!a.has(c)){a.add(c);for(let l of r.get(c)||[])o.push(l)}}return t.prepare("INSERT OR IGNORE INTO main.roadmap_phase_deps (blocker_id, blocked_id) VALUES (?, ?)").run(e,s),{added:{blocker_id:e,blocked_id:s}}}function es(t,e,s){if(t.prepare("DELETE FROM main.roadmap_phase_deps WHERE blocker_id = ? AND blocked_id = ?").run(e,s).changes===0)throw new Error(`No dep edge ${e} \u2192 ${s}`);return{removed:{blocker_id:e,blocked_id:s}}}function ts(t,e){if(e!=null){let n=t.prepare("SELECT blocker_id FROM main.roadmap_phase_deps WHERE blocked_id = ?").all(e).map(r=>r.blocker_id),i=t.prepare("SELECT blocked_id FROM main.roadmap_phase_deps WHERE blocker_id = ?").all(e).map(r=>r.blocked_id);return{phase_id:e,blocked_by:n,blocks:i}}return{edges:t.prepare("SELECT blocker_id, blocked_id FROM main.roadmap_phase_deps ORDER BY blocker_id, blocked_id").all()}}function Ee(t){try{return qt(t)}catch(e){return process.stderr.write(`[cairn] roadmap.md sync skipped: ${e.message}
|
|
189
|
+
`),null}}function ce(t,e={}){let s=e.action??"tree",{id:n,status:i,parent:r,text:o}=e;switch(s){case"tree":{Ee(t);let a=e.show_stale===!0||e.hide_stale===!1,c=Qn(t,{compact:e.compact===!0,showStale:a,decisionFilter:e.decision??null}),l=Kt(t),p=[c,"",l.current?`Current: ${l.current}`:"(no cursor set \u2014 call cairn_roadmap focus <id>)"];l.decisions_in_flight>1&&p.push(`In flight: ${l.decisions_in_flight} decisions`);let h=ye();return h>0&&p.push(`Shipped: ${h} (see .cairn/roadmap_completed.md)`),{content:[{type:"text",text:p.join(`
|
|
190
|
+
`)}]}}case"status":return Ee(t),{content:[{type:"text",text:JSON.stringify(Kt(t),null,2)}]};case"sync":{let a=Ee(t);return{content:[{type:"text",text:JSON.stringify(a==null?{synced:!1,reason:"No .cairn/roadmap.md with a `## Phases` section."}:{synced:!0,...a},null,2)}]}}case"publish":return Ee(t),{content:[{type:"text",text:JSON.stringify(Ae(t),null,2)}]};case"focus":{if(!n)throw new Error("id is required for focus");let a=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(n);if(!a)throw new Error(`No phase with id ${n}`);if(a.status==="stale")throw new Error(`Phase ${n} is stale \u2014 restore by re-adding it to .cairn/roadmap.md`);let c=De(t),l=new Map(t.prepare("SELECT id, status FROM main.roadmap_phases").all().map(d=>[d.id,d.status])),p=Pe(n,c,l);Fe(t,n);let h=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(n),m=p.length>0?`Focused phase ${n} despite unfinished blockers: ${p.join(", ")}.`:`Focused phase ${n}.`;return{content:[{type:"text",text:JSON.stringify({focused:h,reason:m,blocked_by:p},null,2)}]}}case"next":{let a=Se(t),c=zt(t,a);if(c.id==null)return Vt(t),{content:[{type:"text",text:JSON.stringify({cursor:null,reason:c.reason,skipped_blocked:c.skipped_blocked??[]},null,2)}]};Fe(t,c.id);let l=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(c.id);return{content:[{type:"text",text:JSON.stringify({cursor:c.id,phase:l,reason:c.reason,skipped_blocked:c.skipped_blocked??[]},null,2)}]}}case"set_status":{if(!n)throw new Error("id is required for set_status");if(!i)throw new Error("status is required for set_status");if(!Yt.has(i))throw new Error(`Invalid status "${i}". Use: ${[...Yt].join(", ")}`);let a=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(n);if(!a)throw new Error(`No phase with id ${n}`);let c=i==="done"?"datetime('now')":"NULL";t.prepare(`UPDATE main.roadmap_phases SET status = ?, resolved_at = ${c} WHERE id = ?`).run(i,n);let l=null,p=null;if(i==="done"&&Se(t)===n){let u=zt(t,n);u.id!=null?(Fe(t,u.id),l=u.id,p=u.reason):(Vt(t),p=u.reason)}let h=null,m=a.decision_name===H&&a.phase_number!=null;if(i==="done"&&m)try{let u=Gt(a.phase_number,a.text),f=Bt(a.phase_number).removed;h={logged:u.logged,pruned_from_roadmap_md:f}}catch(u){process.stderr.write(`[cairn] ship bookkeeping failed for phase ${n}: ${u.message}
|
|
191
|
+
`)}let d=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(n);return{content:[{type:"text",text:JSON.stringify({updated:d,cursor_advanced_to:l,reason:p,completed:h},null,2)}]}}case"add":{if(!o)throw new Error("text is required for add");if(r==null)throw new Error("parent (phase_id) is required \u2014 ad-hoc sub-tasks must hang under an existing phase. For freestanding work use cairn_todos add.");let a=t.prepare("SELECT id, decision_name FROM main.roadmap_phases WHERE id = ?").get(r);if(!a)throw new Error(`Parent phase ${r} not found`);let c=t.prepare("SELECT COALESCE(MAX(position), -1) + 1 AS n FROM main.roadmap_phases WHERE parent_phase_id = ?").get(r).n,l=t.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (NULL, NULL, ?, ?, ?, ?)").run(r,o,"open",c),p=t.prepare("SELECT * FROM main.roadmap_phases WHERE id = ?").get(l.lastInsertRowid);return{content:[{type:"text",text:JSON.stringify({added:p,inherits_decision_from:a.decision_name},null,2)}]}}case"deps_add":{let a=e.from,c=e.to;if(a==null||c==null)throw new Error("deps_add requires from (blocker phase id) and to (blocked phase id)");return{content:[{type:"text",text:JSON.stringify(Zn(t,a,c),null,2)}]}}case"deps_remove":{let a=e.from,c=e.to;if(a==null||c==null)throw new Error("deps_remove requires from and to");return{content:[{type:"text",text:JSON.stringify(es(t,a,c),null,2)}]}}case"deps_list":return{content:[{type:"text",text:JSON.stringify(ts(t,e.id??null),null,2)}]};default:throw new Error(`Unknown action: "${s}". Use tree, status, sync, publish, focus, next, set_status, add, deps_add, deps_remove, or deps_list.`)}}var Ue=ss(ns);async function Zt(t){let e=A(),s=vt(e);It(e,s)>0&&Ce(e,s);let i=xe.join(e,"session.json");if(!G.existsSync(i))return{content:[{type:"text",text:JSON.stringify({error:"No checkpoint found. Run cairn_maintain to index the project, then cairn_checkpoint to save session state."},null,2)}]};let r=JSON.parse(G.readFileSync(i,"utf8"));if(typeof r.notes=="string")try{r.notes=JSON.parse(r.notes)}catch{r.notes=[]}Array.isArray(r.notes)||(r.notes=[]);let o=r.notes.filter(S=>typeof S=="string"&&S.includes("git-snapshot:"));if(o.length>0){let S=xe.join(e,"session.bak.json");G.existsSync(S)||G.copyFileSync(i,S),r.notes=r.notes.filter(C=>!(typeof C=="string"&&C.includes("git-snapshot:"))),G.writeFileSync(i,JSON.stringify(r,null,2),"utf8")}let a=null,c=r.mtime_snapshot||{};Object.keys(c).length>0&&(a=Ut(c,$()),a&&(Ht(t,e,r,s,a.oldRoot,a.newRoot),Ce(e,s)));let l=r.mtime_snapshot||{},p=[];for(let[S,C]of Object.entries(l))try{G.statSync(S).mtimeMs!==C&&p.push({file:S,change:"modified"})}catch{p.push({file:S,change:"deleted"})}try{let S=await P($());for(let C of S)C in l||p.push({file:C,change:"new"})}catch{}let h=p.filter(S=>S.change==="deleted").map(S=>S.file);if(h.length>0){let S=t.prepare("DELETE FROM main.files WHERE path = ?");for(let C of h)S.run(C)}let m=p.filter(S=>S.change!=="deleted").map(S=>S.file),d={files_reindexed:0,files_unchanged:0};m.length>0&&(await fe(t,m),d.files_reindexed=m.length),d.files_unchanged=Object.keys(l).length-p.filter(S=>S.change!=="new").length;let u=z(),f=Dt(u),g=U(u),y=g.length>0?g.map(S=>`[${S.type}] ${S.name} \u2014 ${S.description}`).join(`
|
|
192
|
+
`):null,_=ue(t),T=0;try{T=t.prepare("SELECT COUNT(*) AS n FROM main.todos WHERE status IN ('open', 'in_progress')").get().n}catch{}let x=null;try{let S=G.existsSync(se()),C=JSON.parse(ce(t,{action:"status"}).content[0].text);if(S||C.decisions_total>0){let W=ce(t,{action:"tree",compact:!0}).content[0].text;x={file:xe.relative($(),se()).replace(/\\/g,"/"),current:C.current,decisions_in_flight:C.decisions_in_flight,tree:W}}}catch{}let I=x?x.current?`Roadmap (${x.file}) \u2014 current: ${x.current}`:`Roadmap (${x.file}) \u2014 no cursor set (call cairn_roadmap next)`:"",E=[`Last session: ${r.message}`,a?`Project relocated from ${a.oldRoot} \u2192 repaired automatically.`:"",I,p.length>0?`${p.length} file(s) changed since checkpoint`:"No files changed since checkpoint",T>0?`${T} open todo(s)`:"",_.length>0?`${_.length} sub-index(es)`:""].filter(Boolean).join(" \xB7 "),R=null;try{G.readFileSync(xe.join($(),".gitignore"),"utf8").split(`
|
|
193
|
+
`).map(W=>W.trim()).some(W=>W===".cairn"||W===".cairn/"||W==="/.cairn"||W==="/.cairn/")||(R="Add .cairn/ to your .gitignore to avoid committing the index and session cache.")}catch{}let b={last_checkpoint:r.checkpoint_at,message:r.message,index_status:m.length>0||h.length>0?"incremental":"up_to_date",files_reindexed:d.files_reindexed,files_deleted:h.length,files_unchanged:Math.max(0,d.files_unchanged),sub_indexes_active:_.length,open_todos:T,changed_since_checkpoint:p.slice(0,20),active_files:r.active_files||[],notes:r.notes||[],resume_summary:E};R&&(b.gitignore_hint=R),x&&(b.roadmap=x),o.length>0&&(b.migrated=`Removed ${o.length} legacy git-snapshot note(s). Original session backed up to session.bak.json.`);let O=$(),j=new Date(r.checkpoint_at).toISOString(),[k,w,le]=await Promise.allSettled([Ue(`git log --oneline --after="${j}"`,{cwd:O,timeout:5e3}),Ue("git diff --name-only HEAD",{cwd:O,timeout:5e3}),Ue("git status --short",{cwd:O,timeout:5e3})]);if(k.status==="fulfilled"){let S=k.value.stdout.trim();S&&(b.commits_since_checkpoint=S.split(`
|
|
194
|
+
`))}if(w.status==="fulfilled"){let S=w.value.stdout.trim();S&&(b.uncommitted_changes=S.split(`
|
|
195
|
+
`))}if(le.status==="fulfilled"){let S=le.value.stdout.trim();S&&(b.git_status=S)}return a&&(b.relocated=a),f.length>0&&(b.preferences=f.map(S=>({name:S.name,description:S.description,file:S.file}))),y&&(b.memory_index=y),{content:[{type:"text",text:JSON.stringify(b,null,2)}]}}import is from"fs";import rs from"path";var os=new Set(["java","typescript","javascript","vue","python","sql"]);function en(t,{file_path:e}){if(!e)throw new Error("file_path is required");let s=rs.extname(e).toLowerCase(),n=J[s];if(!n||!os.has(n))return{content:[{type:"text",text:JSON.stringify({skipped:!0,reason:`language '${n??s}' is not minified`})}]};let i=is.readFileSync(e,"utf8"),r=i.split(`
|
|
196
|
+
`).length,o=n==="vue"?he(i):K(i,n),a=o.split(`
|
|
193
197
|
`).length,c=r>0?Math.round((1-a/r)*100):0;return{content:[{type:"text",text:`[cairn: ${r} \u2192 ${a} lines (-${c}%)]
|
|
194
|
-
${o}`}]}}import
|
|
198
|
+
${o}`}]}}import Te from"path";var as=[{group:"read",verbs:["get","fetch","find","load","retrieve","read","query","lookup","select"]},{group:"write",verbs:["create","add","insert","save","persist","store","put","post"]},{group:"update",verbs:["update","modify","edit","change","patch","set","replace","upsert"]},{group:"delete",verbs:["delete","remove","destroy","clear","purge","drop","erase","reset"]}],cs=[["open","close"],["connect","disconnect"],["start","stop"],["init","destroy"],["init","cleanup"],["create","delete"],["enable","disable"],["lock","unlock"],["encode","decode"],["encrypt","decrypt"],["serialize","deserialize"],["subscribe","unsubscribe"],["register","unregister"],["login","logout"],["mount","unmount"],["acquire","release"],["push","pop"],["enqueue","dequeue"]],ps=5e3;function ls(t){let e=t.prepare(`SELECT f.repo,
|
|
195
199
|
COUNT(DISTINCT f.id) AS file_count,
|
|
196
200
|
COUNT(DISTINCT s.id) AS symbol_count,
|
|
197
201
|
GROUP_CONCAT(DISTINCT f.language) AS languages
|
|
@@ -199,19 +203,19 @@ ${o}`}]}}import _e from"path";var Vn=[{group:"read",verbs:["get","fetch","find",
|
|
|
199
203
|
LEFT JOIN symbols s ON s.file_id = f.id
|
|
200
204
|
GROUP BY f.repo
|
|
201
205
|
ORDER BY f.repo`).all(),s=e.map(i=>` ${i.repo.padEnd(40)} ${String(i.file_count).padStart(5)} files ${String(i.symbol_count).padStart(6)} symbols [${i.languages}]`),n=e.reduce((i,r)=>({files:i.files+r.file_count,symbols:i.symbols+r.symbol_count}),{files:0,symbols:0});return{content:[{type:"text",text:JSON.stringify({mode:"federation_summary",note:"Index is too large for a full outline. Use repo parameter to drill into a specific service.",total_repos:e.length,total_files:n.files,total_symbols:n.symbols,repos:e,usage:'Call cairn_outline with { repo: "<repo-name>" } to get the full outline for one service.',outline:s.join(`
|
|
202
|
-
`)},null,2)}]}}function
|
|
203
|
-
${f.relPath} [${f.language}]`);let
|
|
204
|
-
`).trim()},null,2)}]}}function
|
|
206
|
+
`)},null,2)}]}}function tn(t,{paths:e,repo:s,checks:n}={}){let i=process.cwd();if(!e?.length&&!s&&t.prepare("SELECT COUNT(*) AS n FROM symbols").get().n>ps)return ls(t);let r;if(s)r=t.prepare("SELECT id, path, language FROM files WHERE repo = ? ORDER BY path").all(s);else if(e&&e.length>0){let f=e.map(()=>"f.path LIKE ?").join(" OR ");r=t.prepare(`SELECT id, path, language FROM files f WHERE (${f}) ORDER BY path`).all(...e.map(g=>`%${g}%`))}else r=t.prepare("SELECT id, path, language FROM files ORDER BY path").all();let o=new Map,a=new Set;for(let f of r){let g=Te.relative(i,f.path).replace(/\\/g,"/");a.has(g)||(a.add(g),o.set(f.id,{id:f.id,path:f.path,relPath:g,language:f.language,symbols:[]}))}let c=t.prepare("SELECT file_id, fqn, name, kind, exported, line FROM symbols ORDER BY file_id, kind, name").all();for(let f of c)o.has(f.file_id)&&o.get(f.file_id).symbols.push(f);let l=[];for(let f of o.values()){if(f.symbols.length===0)continue;l.push(`
|
|
207
|
+
${f.relPath} [${f.language}]`);let g=new Map,y=[];for(let _ of f.symbols)if(_.kind==="class"||_.kind==="interface"||_.kind==="enum")g.has(_.name)||g.set(_.name,{meta:_,members:[]});else if(_.fqn&&_.fqn.includes(".")){let T=_.fqn.split("."),x=T[T.length-2];g.has(x)?g.get(x).members.push(_):y.push(_)}else y.push(_);for(let[_,{meta:T,members:x}]of g){let I=T.exported?"+":"-",E=T.line?`:${T.line}`:"";l.push(` ${I} ${T.kind} ${_}${E}`);for(let R of x){let b=R.exported?"+":"-",O=R.line?`:${R.line}`:"";l.push(` ${b} ${R.name}${O} (${R.kind})`)}}for(let _ of y){if(_.kind==="class")continue;let T=_.exported?"+":"-",x=_.line?`:${_.line}`:"";l.push(` ${T} ${_.name}${x} (${_.kind})`)}}let p=[],h=new Set;for(let f of o.values())/[/\\](test|spec|__tests__)[/\\]|\.test\.|\.spec\./.test(f.path)&&h.add(Te.basename(f.path).replace(/\.(test|spec)\.[a-z]+$/,"").toLowerCase());for(let f of o.values()){let g=/[/\\](test|spec|__tests__)[/\\]|\.test\.|\.spec\./.test(f.path),y=f.symbols.filter(E=>E.kind==="method"||E.kind==="function"),_=y.map(E=>E.name),T=_.map(E=>E.toLowerCase());y.length>20&&p.push({file:f.relPath,issue:"god_class",detail:`${y.length} methods \u2014 likely violates single responsibility`,confidence:y.length>35?"HIGH":"MEDIUM",jump_to:f.symbols.find(E=>E.kind==="class")?.name});let x=new Set;for(let[E,R]of cs){let b=`${E}/${R}`;if(x.has(b))continue;let O=T.some(w=>w===E||w.startsWith(E+"_")||w.startsWith(E[0].toUpperCase()+E.slice(1))),j=T.some(w=>w===R||w.startsWith(R+"_")||w.startsWith(R[0].toUpperCase()+R.slice(1))),k=_.find(w=>w.toLowerCase()===E||w.toLowerCase().startsWith(E));O&&!j&&k&&(x.add(b),p.push({file:f.relPath,issue:"symmetry_gap",detail:`has \`${k}\` but no \`${R}*\` \u2014 possible incomplete lifecycle`,confidence:"MEDIUM",jump_to:k}))}for(let{group:E,verbs:R}of as){let b=new Set,O=new Map;for(let j of _){let k=j.toLowerCase();for(let w of R)if(k===w||k.startsWith(w)&&(k.length===w.length||/[A-Z_]/.test(j[w.length]))){b.add(w),O.has(w)||O.set(w,j);break}}if(b.size>=3){let j=[...b].map(k=>`\`${O.get(k)}\``).join(", ");p.push({file:f.relPath,issue:"naming_inconsistency",detail:`Mixed "${E}" verbs: ${j} \u2014 pick one convention`,confidence:"LOW",jump_to:O.get([...b][0])})}}if(!g&&f.symbols.some(E=>E.kind==="class")){let E=Te.basename(f.path).replace(/\.[^.]+$/,"").toLowerCase();h.has(E)||p.push({file:f.relPath,issue:"missing_tests",detail:`No test file found for ${Te.basename(f.path)}`,confidence:"LOW",jump_to:f.symbols.find(R=>R.kind==="class")?.name})}let I=f.symbols.filter(E=>E.kind==="class");for(let E of I){let R=E.fqn?E.fqn+".":E.name+".";!f.symbols.some(O=>O.fqn&&O.fqn.startsWith(R))&&!g&&p.push({file:f.relPath,issue:"empty_class",detail:`\`${E.name}\` has no indexed members \u2014 may be a stub or anemic model`,confidence:"LOW",jump_to:E.name})}}let m=new Map;for(let f of o.values())for(let g of f.symbols.filter(y=>y.kind==="class"))m.has(g.name)||m.set(g.name,new Set),m.get(g.name).add(f.relPath);for(let[f,g]of m){let y=[...g];y.length>1&&p.push({file:y.join(", "),issue:"duplicate_class_name",detail:`\`${f}\` defined in ${y.length} files \u2014 risk of confusion or shadowing`,confidence:"MEDIUM",jump_to:f})}let d={HIGH:0,MEDIUM:1,LOW:2};p.sort((f,g)=>(d[f.confidence]??3)-(d[g.confidence]??3));let u=ds(p);return{content:[{type:"text",text:JSON.stringify({file_count:o.size,symbol_count:c.length,suspect_count:p.length,summary:u,suspects:p,outline:l.join(`
|
|
208
|
+
`).trim()},null,2)}]}}function ds(t){let e={};for(let o of t)e[o.issue]=(e[o.issue]||0)+1;let s=t.filter(o=>o.confidence==="HIGH").length,n=t.filter(o=>o.confidence==="MEDIUM").length,i=[];s&&i.push(`${s} HIGH`),n&&i.push(`${n} MEDIUM`);let r=t.length-s-n;return r&&i.push(`${r} LOW`),i.length?`${t.length} suspects (${i.join(", ")}): ${Object.entries(e).map(([o,a])=>`${a} ${o}`).join(", ")}`:"No structural suspects detected"}async function nn(t,e={}){let{text:s,id:n,status:i,source:r}=e,o=e.action??"list",a=e.kind;switch(o){case"list":{let c="SELECT * FROM main.todos WHERE 1=1",l=[];i&&(c+=" AND status = ?",l.push(i)),r&&(c+=" AND source = ?",l.push(r)),a&&(c+=" AND kind = ?",l.push(a.toUpperCase())),c+=" ORDER BY CASE status WHEN 'open' THEN 0 ELSE 1 END, source, file, line";let p=t.prepare(c).all(...l),h=p.filter(d=>d.status==="open").length,m=p.filter(d=>d.status==="done").length;return{content:[{type:"text",text:JSON.stringify({total:p.length,open:h,done:m,todos:p},null,2)}]}}case"add":{if(!s)throw new Error("text is required for add action");let c=(a||"TODO").toUpperCase(),l=t.prepare("INSERT INTO main.todos (source, kind, text) VALUES (?, ?, ?)").run("manual",c,s),p=t.prepare("SELECT * FROM main.todos WHERE id = ?").get(l.lastInsertRowid);return{content:[{type:"text",text:JSON.stringify({added:p},null,2)}]}}case"resolve":{if(!n)throw new Error("id is required for resolve action");if(t.prepare("UPDATE main.todos SET status = ?, resolved_at = datetime('now') WHERE id = ?").run("done",n).changes===0)throw new Error(`No todo found with id ${n}`);let l=t.prepare("SELECT * FROM main.todos WHERE id = ?").get(n);return{content:[{type:"text",text:JSON.stringify({resolved:l},null,2)}]}}case"scan":{let c=await P(process.cwd()),l=await me(t,c),p=t.prepare("SELECT COUNT(*) AS n FROM main.todos WHERE status = 'open'").get().n;return{content:[{type:"text",text:JSON.stringify({scanned_files:c.length,todos_found:l,open_todos:p},null,2)}]}}default:throw new Error(`Unknown action: "${o}". Use list, add, resolve, or scan.`)}}import X from"fs";import B from"path";var sn=["package.json","pom.xml","Cargo.toml","go.mod","pyproject.toml","build.gradle","build.gradle.kts","composer.json","Gemfile","CMakeLists.txt"];function He(t){let e;try{e=X.statSync(t).isDirectory()?t:B.dirname(t)}catch{return null}for(;;){if(X.existsSync(B.join(e,".cairn",".cairn-project"))||X.existsSync(B.join(e,".git")))return e;for(let n of sn)if(X.existsSync(B.join(e,n)))return e;let s=B.dirname(e);if(s===e)return null;e=s}}function rn(t,{project_path:e}={}){let s=process.cwd(),n=$();if(!e)return Re(null),{content:[{type:"text",text:JSON.stringify({switched:!0,project_root:s,previous_root:n,action:"restored_to_session_root"},null,2)}]};let i=He(e);if(!i)return{content:[{type:"text",text:JSON.stringify({switched:!1,reason:"Could not detect a project root from the given path. No .git, package.json, or other project markers found.",path_given:e},null,2)}]};let r=X.existsSync(B.join(i,".cairn","index.db")),o=[];X.existsSync(B.join(i,".cairn",".cairn-project"))&&o.push("cairn project"),X.existsSync(B.join(i,".git"))&&o.push("git repo");for(let a of sn)if(X.existsSync(B.join(i,a))){o.push(a);break}return Re(i),{content:[{type:"text",text:JSON.stringify({switched:!0,project_root:i,previous_root:n,detected_by:o,has_cairn_index:r,tip:r?"Call cairn_resume to restore this project's session, or cairn_search to query its index.":"No cairn index found. Call cairn_maintain to index this project first."},null,2)}]}}import pe from"fs";import V from"path";function on(t,{source_path:e,types:s,overwrite:n=!1}){if(!e)throw new Error("source_path is required");let i,r=e.replace(/\\/g,"/");if(r.endsWith("/.cairn/memory")||r.endsWith("\\.cairn\\memory"))i=e;else if(r.endsWith("/.cairn")||r.endsWith("\\.cairn"))i=V.join(e,"memory");else{let d=He(e);if(!d)return{content:[{type:"text",text:JSON.stringify({migrated:!1,reason:"Could not detect a project root from source_path.",source_path:e},null,2)}]};i=V.join(d,".cairn","memory")}if(!pe.existsSync(i))return{content:[{type:"text",text:JSON.stringify({migrated:!1,reason:"Source project has no .cairn/memory directory. Run cairn_maintain or cairn_resume there first.",source_memory_dir:i},null,2)}]};let o=s&&s.length>0?s:Mt,a=U(i).filter(d=>o.includes(d.type));if(a.length===0)return{content:[{type:"text",text:JSON.stringify({migrated:!1,reason:`No memories of types [${o.join(", ")}] found in source project.`,source_memory_dir:i},null,2)}]};let c=z(),l=V.dirname(V.dirname(i)),p=U(c),h=new Set(p.map(d=>d.file)),m={copied:[],skipped:[],errors:[]};for(let d of a){let u=V.join(i,d.file);if(!pe.existsSync(u)){m.errors.push({name:d.name,reason:"source file missing"});continue}let f=V.join(c,d.file),g=h.has(d.file);if(g&&!n){m.skipped.push({name:d.name,reason:"already exists (use overwrite: true to replace)"});continue}let y=pe.readFileSync(u,"utf8");y.includes(`
|
|
205
209
|
origin:`)||(y=y.replace(`---
|
|
206
210
|
|
|
207
211
|
`,`---
|
|
208
|
-
origin: ${
|
|
212
|
+
origin: ${l}
|
|
209
213
|
|
|
210
|
-
`),y.includes(`origin: ${
|
|
211
|
-
origin: ${
|
|
212
|
-
`)));try{pe.writeFileSync(f,y,"utf8"),
|
|
214
|
+
`),y.includes(`origin: ${l}`)||(y=y.replace(/^---\n/,`---
|
|
215
|
+
origin: ${l}
|
|
216
|
+
`)));try{pe.writeFileSync(f,y,"utf8"),g||(p.push({...d}),h.add(d.file)),m.copied.push({name:d.name,type:d.type,origin:l})}catch(_){m.errors.push({name:d.name,reason:_.message})}}if(m.copied.length>0){let d=["# Memory Index",""];for(let u of p)d.push(`- [${u.name}](${u.file}) \u2014 ${u.description} \`[${u.type}]\``);pe.writeFileSync(V.join(c,"MEMORY.md"),d.join(`
|
|
213
217
|
`)+`
|
|
214
|
-
`,"utf8")}return{content:[{type:"text",text:JSON.stringify({migrated:
|
|
218
|
+
`,"utf8")}return{content:[{type:"text",text:JSON.stringify({migrated:m.copied.length>0,source:l,types_migrated:o,copied:m.copied.length,skipped:m.skipped.length,errors:m.errors.length,details:m},null,2)}]}}import us from"fs";var qe=Date.now(),We=!1;function Be(){qe=Date.now()}async function Ge(t){if(We)return;We=!0;let e=qe;qe=Date.now();try{let s=await P($()),n=[];for(let i of s)try{us.statSync(i).mtimeMs>e&&n.push(i)}catch{}n.length>0&&await fe(t,n)}finally{We=!1}}function an(t,e=3e4){let s=setInterval(()=>Ge(t),e);s.unref&&s.unref()}var L=Ve();an(L);var Xe=new ms({name:"cairn",version:"1.0.0"},{capabilities:{tools:{}}});Xe.setRequestHandler(gs,async()=>({tools:[{name:"cairn_maintain",description:"Index the current project into Cairn's polyglot knowledge graph. Supports Java, TypeScript, JavaScript, Vue, Python, SQL, config, and Markdown. Indexes from the current working directory. Run at session start or after major changes.",inputSchema:{type:"object",properties:{languages:{type:"array",items:{type:"string"},description:'Optional: limit to specific languages e.g. ["java","typescript"]'}}}},{name:"cairn_search",description:"Search the codebase by concept across all languages. Returns ranked symbols with scores.",inputSchema:{type:"object",properties:{query:{type:"string"},limit:{type:"number",default:10},language:{type:"string",description:"Optional: filter by language"},kind:{type:"string",description:"Optional: class/function/component/table/..."}},required:["query"]}},{name:"cairn_describe",description:"Describe what a directory/module does: symbols, responsibilities, upstream/downstream deps.",inputSchema:{type:"object",properties:{path:{type:"string",description:"Directory or file path to describe"}},required:["path"]}},{name:"cairn_code_graph",description:"Analyze module dependencies and instability metrics across all languages. Use before refactoring.",inputSchema:{type:"object",properties:{module:{type:"string",description:"Optional: scope to a specific module/package"},mode:{type:"string",enum:["instability","health","cycles"]}},required:["mode"]}},{name:"cairn_security",description:"Scan for security vulnerabilities across all languages (XSS, XXE, SQLi, weak crypto, hardcoded secrets, etc.)",inputSchema:{type:"object",properties:{paths:{type:"array",items:{type:"string"},description:"Optional: scope to specific paths"},severity:{type:"string",enum:["HIGH","MEDIUM","LOW","ALL"],default:"HIGH"},language:{type:"string",description:"Optional: scope to a specific language"}}}},{name:"cairn_bundle",description:`Produce a minified single-file snapshot of source code for Claude to read.
|
|
215
219
|
Strips comments, empty lines, and optionally styles. Returns the bundle file path and compression stats.
|
|
216
220
|
Use AFTER cairn_search to get a readable version of the files Claude needs to work with.
|
|
217
221
|
Typical workflow: cairn_search \u2192 find files \u2192 cairn_bundle those paths \u2192 Claude reads bundle.`,inputSchema:{type:"object",properties:{bundle_name:{type:"string",default:"default",description:"Name for the bundle file (saved to .cairn/bundles/<name>.txt)"},filter_paths:{type:"array",items:{type:"string"},description:"Optional: only include files under these relative paths"},filter_language:{type:"string",description:"Optional: only bundle this language (java/typescript/vue/python/...)"},no_comments:{type:"boolean",default:!0,description:"Strip code comments"},no_empty_lines:{type:"boolean",default:!0,description:"Remove empty lines"},no_style:{type:"boolean",default:!1,description:"Skip <style> blocks in Vue files"},aggressive:{type:"boolean",default:!1,description:"Aggressive whitespace/punctuation minification"},only_changed:{type:"boolean",default:!1,description:"Incremental: only re-bundle files changed since last run"},max_size_kb:{type:"number",default:800,description:"Safety cap in KB to avoid context window overflow"}}}},{name:"cairn_checkpoint",description:"Save current session state to .cairn/session.json so it can be restored next session with cairn_resume.",inputSchema:{type:"object",properties:{message:{type:"string",description:"What you were working on \u2014 will be shown on resume"},active_files:{type:"array",items:{type:"string"},description:"Optional: absolute paths to files actively being worked on"},notes:{type:"array",items:{type:"string"},description:"Optional: things Claude should remember next session"}},required:["message"]}},{name:"cairn_resume",description:"Restore the last saved session state. Detects which files changed since the checkpoint and incrementally re-indexes only those files. Also loads preference memories and surfaces the memory index. Call at the start of a session instead of cairn_maintain when resuming work. If the project was moved to a different drive or directory, paths are repaired automatically.",inputSchema:{type:"object",properties:{}}},{name:"cairn_outline",description:`Generate a structural outline of the project (or a scoped subset) and run heuristic analysis to detect likely issues.
|
|
@@ -230,4 +234,4 @@ Detects:
|
|
|
230
234
|
- empty_class: class with no indexed members (stub or anemic model)
|
|
231
235
|
- duplicate_class_name: same class name defined in multiple files
|
|
232
236
|
|
|
233
|
-
Use to: get a project-wide bird's-eye view, triage code quality, find where to look first.`,inputSchema:{type:"object",properties:{repo:{type:"string",description:'Optional: scope to a specific repo/service by name (e.g. "elprint-component-service")'},paths:{type:"array",items:{type:"string"},description:"Optional: scope to specific file paths or directories (substring match)"},checks:{type:"array",items:{type:"string"},description:'Optional: limit to specific checks e.g. ["god_class","symmetry_gap"]'}}}},{name:"cairn_minify",description:"Minify a single source file and return compressed content. Strips comments and empty lines. Useful when passive hooks are not installed. Supports java, typescript, javascript, vue, python, sql.",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to the file to minify"}},required:["file_path"]}},{name:"cairn_memo",description:'Persist and retrieve typed memories in .cairn/memory/. Use this \u2014 not the auto-memory system \u2014 for all project-level memories. Triggered by: "save cairn memory", "cairn remember", "build cairn memories", "save experience to cairn". Four types: preference (user working style \u2014 always loaded on resume), experience (transferable lessons learned \u2014 migrated between projects), decision (project-specific architectural choices), knowledge (project-specific codebase facts). Memories survive context loss and accumulate across sessions.',inputSchema:{type:"object",properties:{action:{type:"string",enum:["write","read","list","delete"],description:"write: save/update a memory | read: load a memory by name | list: show all memories with index | delete: remove a memory"},name:{type:"string",description:"Memory identifier (required for write/read/delete)"},type:{type:"string",enum:["preference","experience","decision","knowledge"],description:"Memory type (required for write)"},content:{type:"string",description:"Memory body \u2014 what should be remembered (required for write)"},description:{type:"string",description:"One-line summary shown in the memory index"},origin:{type:"string",description:"Optional: source project path, for traceability of migrated memories"}},required:["action"]}},{name:"cairn_switch",description:"Switch the active Cairn project context to a different directory. Detects the project root automatically from any path within the project (via .git, package.json, .cairn, etc.). Call cairn_resume or cairn_maintain after switching. Call with no arguments to return to the original session's project.",inputSchema:{type:"object",properties:{project_path:{type:"string",description:"Any path inside the target project (file or directory). Omit to restore the original session root."}}}},{name:"cairn_employ_memory",description:"Migrate memories from another project's .cairn/memory/ into the current project. By default copies only transferable types (preference + experience) \u2014 skips project-specific decision and knowledge. Imported memories are tagged with their origin project path for traceability.",inputSchema:{type:"object",properties:{source_path:{type:"string",description:"Path to the source project (any file/directory inside it, or its .cairn directory)"},types:{type:"array",items:{type:"string",enum:["preference","experience","decision","knowledge"]},description:'Optional: memory types to migrate (default: ["preference", "experience"])'},overwrite:{type:"boolean",default:!1,description:"Overwrite existing memories with the same filename (default: false)"}},required:["source_path"]}},{name:"cairn_todos",description:"Long-tail backlog: scanned TODO/FIXME/HACK/XXX comments + manual notes. Stats are included in cairn_maintain and cairn_resume reports. For the active multi-phase plan (authored in .cairn/roadmap.md, with a cursor + deps) use cairn_roadmap instead.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["list","add","resolve","scan"],description:"list: show todos | add: create a manual todo | resolve: mark done by id | scan: re-scan codebase"},text:{type:"string",description:"Text for the new todo (add action)"},kind:{type:"string",enum:["TODO","FIXME","HACK","XXX","NOTE"],description:"Kind for new todo (default: TODO)"},id:{type:"number",description:"Todo ID to resolve (resolve action)"},status:{type:"string",enum:["open","done"],description:"Filter by status (list action)"},source:{type:"string",enum:["scan","manual"],description:"Filter by source (list action)"}},required:["action"]}},{name:"cairn_roadmap",description:"Active project plan: phases with status + cursor + dep graph. Phases are AUTHORED in the first-class document .cairn/roadmap.md (NOT in memory) via a `## Phases` section (### Phase 1: title, ### Phase 2: ...). Read actions (tree/status/next/sync) re-seed from that file, so editing roadmap.md IS editing the plan (existing phases preserved, removed marked stale, added appended). Marking a phase done prunes it from roadmap.md and appends it to .cairn/roadmap_completed.md (append-only log); the done row stays in the DB but is hidden from the tree, so the plan always shows what is next. Sub-tasks under a phase are ad-hoc via add --parent. Inter-phase deps via deps_add \u2014 `next` will skip dep-blocked phases and explain in the reason. The Stop hook re-prints this tree after every assistant turn while a cursor is set.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["tree","status","sync","focus","next","set_status","add","deps_add","deps_remove","deps_list"],description:"tree: render the roadmap (M/N done rollup; stale + shipped-done hidden by default \u2014 pass show_stale: true for stale) | status: one-line summary | sync: re-seed from .cairn/roadmap.md (also runs automatically on tree/status/next) | focus <id>: move cursor, mark in_progress, propagate up (warns if dep-blocked but does not refuse) | next: DFS-advance cursor to next actionable phase (skips stale + dep-blocked, returns reason) | set_status <id> <status>: open|in_progress|done|blocked|stale (done auto-advances cursor; a done roadmap phase is pruned from roadmap.md + logged to roadmap_completed.md) | add <parent> <text>: ad-hoc sub-task under an existing phase | deps_add <from> <to>: declare phase <from> blocks phase <to> (cycle-checked) | deps_remove <from> <to> | deps_list [<id>]: list edges (or just edges touching <id>)"},id:{type:"number",description:"Phase ID (focus, set_status, deps_list)"},status:{type:"string",enum:["open","in_progress","done","blocked","stale"],description:"New status (set_status)"},parent:{type:"number",description:"Parent phase ID (add \u2014 required, ad-hoc sub-tasks must hang under an existing phase)"},text:{type:"string",description:"Sub-task text (add action)"},compact:{type:"boolean",description:"Compact tree render \u2014 strips id tags (tree action)"},show_stale:{type:"boolean",description:"Include stale phases in tree (default: hidden)"},hide_stale:{type:"boolean",description:"Deprecated \u2014 stale is hidden by default. Pass hide_stale: false to show, or use show_stale: true."},decision:{type:"string",description:"Filter tree to a single decision (tree action)"},from:{type:"number",description:"Blocker phase ID (deps_add, deps_remove)"},to:{type:"number",description:"Blocked phase ID (deps_add, deps_remove)"}},required:["action"]}}]}));var
|
|
237
|
+
Use to: get a project-wide bird's-eye view, triage code quality, find where to look first.`,inputSchema:{type:"object",properties:{repo:{type:"string",description:'Optional: scope to a specific repo/service by name (e.g. "elprint-component-service")'},paths:{type:"array",items:{type:"string"},description:"Optional: scope to specific file paths or directories (substring match)"},checks:{type:"array",items:{type:"string"},description:'Optional: limit to specific checks e.g. ["god_class","symmetry_gap"]'}}}},{name:"cairn_minify",description:"Minify a single source file and return compressed content. Strips comments and empty lines. Useful when passive hooks are not installed. Supports java, typescript, javascript, vue, python, sql.",inputSchema:{type:"object",properties:{file_path:{type:"string",description:"Absolute path to the file to minify"}},required:["file_path"]}},{name:"cairn_memo",description:'Persist and retrieve typed memories in .cairn/memory/. Use this \u2014 not the auto-memory system \u2014 for all project-level memories. Triggered by: "save cairn memory", "cairn remember", "build cairn memories", "save experience to cairn". Four types: preference (user working style \u2014 always loaded on resume), experience (transferable lessons learned \u2014 migrated between projects), decision (project-specific architectural choices), knowledge (project-specific codebase facts). Memories survive context loss and accumulate across sessions.',inputSchema:{type:"object",properties:{action:{type:"string",enum:["write","read","list","delete"],description:"write: save/update a memory | read: load a memory by name | list: show all memories with index | delete: remove a memory"},name:{type:"string",description:"Memory identifier (required for write/read/delete)"},type:{type:"string",enum:["preference","experience","decision","knowledge"],description:"Memory type (required for write)"},content:{type:"string",description:"Memory body \u2014 what should be remembered (required for write)"},description:{type:"string",description:"One-line summary shown in the memory index"},origin:{type:"string",description:"Optional: source project path, for traceability of migrated memories"}},required:["action"]}},{name:"cairn_switch",description:"Switch the active Cairn project context to a different directory. Detects the project root automatically from any path within the project (via .git, package.json, .cairn, etc.). Call cairn_resume or cairn_maintain after switching. Call with no arguments to return to the original session's project.",inputSchema:{type:"object",properties:{project_path:{type:"string",description:"Any path inside the target project (file or directory). Omit to restore the original session root."}}}},{name:"cairn_employ_memory",description:"Migrate memories from another project's .cairn/memory/ into the current project. By default copies only transferable types (preference + experience) \u2014 skips project-specific decision and knowledge. Imported memories are tagged with their origin project path for traceability.",inputSchema:{type:"object",properties:{source_path:{type:"string",description:"Path to the source project (any file/directory inside it, or its .cairn directory)"},types:{type:"array",items:{type:"string",enum:["preference","experience","decision","knowledge"]},description:'Optional: memory types to migrate (default: ["preference", "experience"])'},overwrite:{type:"boolean",default:!1,description:"Overwrite existing memories with the same filename (default: false)"}},required:["source_path"]}},{name:"cairn_todos",description:"Long-tail backlog: scanned TODO/FIXME/HACK/XXX comments + manual notes. Stats are included in cairn_maintain and cairn_resume reports. For the active multi-phase plan (authored in .cairn/roadmap.md, with a cursor + deps) use cairn_roadmap instead.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["list","add","resolve","scan"],description:"list: show todos | add: create a manual todo | resolve: mark done by id | scan: re-scan codebase"},text:{type:"string",description:"Text for the new todo (add action)"},kind:{type:"string",enum:["TODO","FIXME","HACK","XXX","NOTE"],description:"Kind for new todo (default: TODO)"},id:{type:"number",description:"Todo ID to resolve (resolve action)"},status:{type:"string",enum:["open","done"],description:"Filter by status (list action)"},source:{type:"string",enum:["scan","manual"],description:"Filter by source (list action)"}},required:["action"]}},{name:"cairn_roadmap",description:"Active project plan: phases with status + cursor + dep graph. Phases are AUTHORED in the first-class document .cairn/roadmap.md (NOT in memory) via a `## Phases` section (### Phase 1: title, ### Phase 2: ...). Read actions (tree/status/next/sync) re-seed from that file, so editing roadmap.md IS editing the plan (existing phases preserved, removed marked stale, added appended). Marking a phase done prunes it from roadmap.md and appends it to .cairn/roadmap_completed.md (append-only log); the done row stays in the DB but is hidden from the tree, so the plan always shows what is next. Sub-tasks under a phase are ad-hoc via add --parent. Inter-phase deps via deps_add \u2014 `next` will skip dep-blocked phases and explain in the reason. The Stop hook re-prints this tree after every assistant turn while a cursor is set.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["tree","status","sync","publish","focus","next","set_status","add","deps_add","deps_remove","deps_list"],description:"tree: render the roadmap (M/N done rollup; stale + shipped-done hidden by default \u2014 pass show_stale: true for stale) | status: one-line summary | sync: re-seed from .cairn/roadmap.md (also runs automatically on tree/status/next) | publish: (re)generate the human-readable ROADMAP.md at the repo root from the live plan (also runs automatically after every action; write-if-changed, never a placeholder) | focus <id>: move cursor, mark in_progress, propagate up (warns if dep-blocked but does not refuse) | next: DFS-advance cursor to next actionable phase (skips stale + dep-blocked, returns reason) | set_status <id> <status>: open|in_progress|done|blocked|stale (done auto-advances cursor; a done roadmap phase is pruned from roadmap.md + logged to roadmap_completed.md) | add <parent> <text>: ad-hoc sub-task under an existing phase | deps_add <from> <to>: declare phase <from> blocks phase <to> (cycle-checked) | deps_remove <from> <to> | deps_list [<id>]: list edges (or just edges touching <id>)"},id:{type:"number",description:"Phase ID (focus, set_status, deps_list)"},status:{type:"string",enum:["open","in_progress","done","blocked","stale"],description:"New status (set_status)"},parent:{type:"number",description:"Parent phase ID (add \u2014 required, ad-hoc sub-tasks must hang under an existing phase)"},text:{type:"string",description:"Sub-task text (add action)"},compact:{type:"boolean",description:"Compact tree render \u2014 strips id tags (tree action)"},show_stale:{type:"boolean",description:"Include stale phases in tree (default: hidden)"},hide_stale:{type:"boolean",description:"Deprecated \u2014 stale is hidden by default. Pass hide_stale: false to show, or use show_stale: true."},decision:{type:"string",description:"Filter tree to a single decision (tree action)"},from:{type:"number",description:"Blocker phase ID (deps_add, deps_remove)"},to:{type:"number",description:"Blocked phase ID (deps_add, deps_remove)"}},required:["action"]}}]}));var ys=new Set(["cairn_search","cairn_bundle","cairn_describe","cairn_outline","cairn_code_graph","cairn_security"]);Xe.setRequestHandler(hs,async t=>{let{name:e,arguments:s}=t.params;switch(ys.has(e)&&await Ge(L),e){case"cairn_maintain":{let n=await xt(L,s);return Be(),n}case"cairn_search":return Tt(L,s);case"cairn_describe":return bt(L,s);case"cairn_code_graph":return Rt(L,s);case"cairn_security":return wt(L,s);case"cairn_bundle":return await Nt(L,s);case"cairn_checkpoint":return Lt(L,s);case"cairn_resume":{let n=await Zt(L);return Be(),n}case"cairn_outline":return tn(L,s);case"cairn_minify":return en(L,s);case"cairn_todos":return await nn(L,s);case"cairn_roadmap":{let n=ce(L,s);return Jt(L),n}case"cairn_memo":return Pt(L,s);case"cairn_switch":return rn(L,s);case"cairn_employ_memory":return on(L,s);default:throw new Error(`Unknown tool: ${e}`)}});var _s=new fs;await Xe.connect(_s);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@misterhuydo/cairn-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "MCP server that gives Claude Code persistent memory across sessions. Index your codebase once, search symbols, bundle source, scan for vulnerabilities, and checkpoint/resume work — across Java, TypeScript, Vue, Python, SQL and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|