@misterhuydo/cairn-mcp 1.12.0 → 1.14.1

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.
Files changed (59) hide show
  1. package/README.md +69 -3
  2. package/bin/cairn-mcp.js +5 -1
  3. package/bin/cairn.js +6 -2
  4. package/dist/cairn-cli.js +93 -0
  5. package/dist/index.js +233 -0
  6. package/package.json +14 -2
  7. package/.gitattributes +0 -10
  8. package/bin/cairn-cli.js +0 -807
  9. package/index.js +0 -338
  10. package/src/bundler/bundleWriter.js +0 -110
  11. package/src/bundler/minifier.js +0 -74
  12. package/src/bundler/outliner.js +0 -188
  13. package/src/bundler/vueMinifier.js +0 -39
  14. package/src/graph/cwd.js +0 -81
  15. package/src/graph/db.js +0 -375
  16. package/src/graph/edges.js +0 -24
  17. package/src/graph/nodes.js +0 -33
  18. package/src/indexer/buildParsers/gradleParser.js +0 -14
  19. package/src/indexer/buildParsers/mavenParser.js +0 -15
  20. package/src/indexer/buildParsers/npmParser.js +0 -17
  21. package/src/indexer/fileWalker.js +0 -62
  22. package/src/indexer/liveIndex.js +0 -51
  23. package/src/indexer/parserFactory.js +0 -35
  24. package/src/indexer/parsers/configParser.js +0 -17
  25. package/src/indexer/parsers/javaParser.js +0 -38
  26. package/src/indexer/parsers/markdownParser.js +0 -17
  27. package/src/indexer/parsers/pythonParser.js +0 -29
  28. package/src/indexer/parsers/sqlParser.js +0 -14
  29. package/src/indexer/parsers/tsParser.js +0 -49
  30. package/src/indexer/parsers/vueParser.js +0 -25
  31. package/src/indexer/parsers/xmlParser.js +0 -25
  32. package/src/indexer/securityScanner.js +0 -103
  33. package/src/indexer/todoScanner.js +0 -41
  34. package/src/tools/bundle.js +0 -30
  35. package/src/tools/checkpoint.js +0 -101
  36. package/src/tools/codeGraph.js +0 -111
  37. package/src/tools/describe.js +0 -51
  38. package/src/tools/employMemory.js +0 -135
  39. package/src/tools/maintain.js +0 -170
  40. package/src/tools/memo.js +0 -306
  41. package/src/tools/minify.js +0 -40
  42. package/src/tools/minifyMap.js +0 -86
  43. package/src/tools/outline.js +0 -326
  44. package/src/tools/relocate.js +0 -122
  45. package/src/tools/resume.js +0 -245
  46. package/src/tools/roadmap.js +0 -455
  47. package/src/tools/search.js +0 -63
  48. package/src/tools/security.js +0 -53
  49. package/src/tools/switch.js +0 -93
  50. package/src/tools/todos.js +0 -73
  51. package/test/cairn-cli.test.js +0 -409
  52. package/test/checkpoint.test.js +0 -99
  53. package/test/cwd.test.js +0 -36
  54. package/test/memo.test.js +0 -329
  55. package/test/minifier.test.js +0 -116
  56. package/test/outliner.test.js +0 -159
  57. package/test/relocate.test.js +0 -199
  58. package/test/resume-migration.test.js +0 -83
  59. package/test/roadmap.test.js +0 -390
package/README.md CHANGED
@@ -33,10 +33,15 @@ Restart Claude Code once after running this — required for the MCP server to l
33
33
  ## Upgrade
34
34
 
35
35
  ```bash
36
- npm install -g @misterhuydo/cairn-mcp
36
+ npm install -g @misterhuydo/cairn-mcp@latest
37
+ cairn install
37
38
  ```
38
39
 
39
- No need to re-run `cairn install` — hooks and MCP config carry over between versions.
40
+ Updating the package replaces only the binary. Re-run `cairn install` to re-sync the
41
+ hook set — a release that adds new hooks (e.g. the roadmap-capture hooks) won't wire
42
+ them otherwise. It's idempotent and additive, so your MCP config and any existing
43
+ hooks are preserved. Restart Claude Code once afterward so the new hooks and MCP
44
+ server load.
40
45
 
41
46
  ---
42
47
 
@@ -46,7 +51,9 @@ No need to re-run `cairn install` — hooks and MCP config carry over between ve
46
51
  |---|---|---|
47
52
  | `PreToolUse[Read]` | Every file read | Source files compressed ~68% before Claude sees them; large files show structural outline with **line numbers** to save tokens |
48
53
  | `PreToolUse[Edit]` | Every file edit | Blocks Edit if Claude only saw compressed content — requires a full re-read first |
49
- | `Stop` | End of every response | Session auto-saved to `.cairn/session.json`, Claude auto-memory backed up to `.cairn/memory/`, and the roadmap re-surfaced — all confirmed in one line |
54
+ | `PostToolUse[ExitPlanMode]` | A plan is approved | The plan is parsed into `### Phase N` entries in `.cairn/roadmap.md`, synced, and the cursor activated |
55
+ | `PostToolUse[TodoWrite]` | The todo list changes | Todos mirror onto the roadmap as sub-tasks under the current phase (or seed root phases if none exist) |
56
+ | `Stop` | End of every response | Session auto-saved to `.cairn/session.json`, Claude auto-memory backed up to `.cairn/memory/`, the roadmap re-surfaced, and any git-detected completed phases surfaced for you to confirm — all in one line |
50
57
  | `UserPromptSubmit` | First message of a new session | Fresh project: Claude prompted to run `cairn_maintain`. Returning session: Claude prompted to run `cairn_resume`. Memory restored from `.cairn/memory/` if the Claude store is empty (new machine / fresh clone) |
51
58
 
52
59
  ---
@@ -74,6 +81,7 @@ No manual steps. The index lives in `.cairn/index.db` inside your project — li
74
81
  | `cairn_code_graph` | Dependency health — instability, cycles, load-bearing modules |
75
82
  | `cairn_security` | Scan for XSS, SQLi, hardcoded secrets, weak crypto, and more |
76
83
  | `cairn_todos` | Scan codebase for TODO/FIXME/HACK comments, add manual items, resolve and list them |
84
+ | `cairn_roadmap` | The active project plan: phases authored in `.cairn/roadmap.md`, with a cursor, dependencies, and auto-pruning of shipped phases into `.cairn/roadmap_completed.md` |
77
85
  | `cairn_bundle` | Minified source snapshot (auto-handled by hooks) |
78
86
  | `cairn_checkpoint` | Save session state (auto-handled by hooks) |
79
87
  | `cairn_minify` | Minify a single file on demand (fallback when hooks are not installed) |
@@ -133,6 +141,64 @@ Stats are included automatically in `cairn_maintain` (`todos_found`) and `cairn_
133
141
 
134
142
  ---
135
143
 
144
+ ## Roadmap
145
+
146
+ TODOs are a long-tail backlog; the **roadmap** is the active plan. It's a first-class
147
+ document at `.cairn/roadmap.md` (not a memory) — a `## Phases` section of
148
+ `### Phase N: title` blocks, plus whatever overview prose you like:
149
+
150
+ ```markdown
151
+ ## Phases
152
+
153
+ ### Phase 1: Decouple roadmap from memory
154
+ Move phases out of decision memos into this file.
155
+
156
+ ### Phase 2: Completed log
157
+ Prune shipped phases, append them to roadmap_completed.md.
158
+ ```
159
+
160
+ `cairn_roadmap` reads that file and tracks it:
161
+
162
+ ```
163
+ cairn_roadmap { action: "tree" } → render the plan (auto-syncs roadmap.md)
164
+ cairn_roadmap { action: "next" } → advance the cursor to the next actionable phase
165
+ cairn_roadmap { action: "focus", id: 3 } → make phase 3 the current focus
166
+ cairn_roadmap { action: "set_status", id: 3, status: "done" } → ship it
167
+ cairn_roadmap { action: "add", parent: 3, text: "..." } → ad-hoc sub-task under a phase
168
+ cairn_roadmap { action: "deps_add", from: 2, to: 4 } → phase 2 blocks phase 4
169
+ ```
170
+
171
+ Editing `roadmap.md` *is* editing the plan — read actions re-seed from it, and the
172
+ end-of-turn hook folds in any hand-edits. Marking a phase **done** prunes it from
173
+ `roadmap.md` and appends it to the append-only `.cairn/roadmap_completed.md`, so the
174
+ file always shows only what's next. The shipped phase stays in the database (counted
175
+ in the `M/N done` rollup, recoverable) but is hidden from the tree.
176
+
177
+ If a project still has its roadmap in the old `decision_production_roadmap.md` memo,
178
+ the first roadmap read migrates it automatically: the content moves to
179
+ `.cairn/roadmap.md` and the memo (plus its index entry) is removed from the memory
180
+ store. Content is preserved verbatim; the migration is one-time and idempotent.
181
+
182
+ ### Automatic capture
183
+
184
+ You don't have to register phases by hand. Two `PostToolUse` hooks fold work into
185
+ the roadmap mechanically:
186
+
187
+ - **Approve a plan** (Claude's plan mode → `ExitPlanMode`) and its steps are parsed
188
+ into `### Phase N` entries in `roadmap.md`. Headings become phases; failing that, a
189
+ top-level numbered list; failing that, the whole plan becomes one phase. Duplicate
190
+ titles are skipped, and the cursor activates so the roadmap surfaces immediately.
191
+ - **Write a todo list** (`TodoWrite`) and the items mirror onto the roadmap — as
192
+ ad-hoc sub-tasks under the current phase, or as seed root phases when no roadmap
193
+ exists yet. A sub-task that's already `done` is never downgraded.
194
+
195
+ Completion is **proposed, never auto-applied**. The end-of-turn hook inspects new git
196
+ commits; when a commit references a phase (explicit `phase N`, or a strong overlap
197
+ with the phase title) the candidate is surfaced for you to confirm — Claude asks
198
+ before calling `set_status … done`, so a stray keyword can't silently prune your plan.
199
+
200
+ ---
201
+
136
202
  ## Supported languages
137
203
 
138
204
  | Language | What Cairn extracts |
package/bin/cairn-mcp.js CHANGED
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'child_process';
3
+ import { existsSync } from 'fs';
3
4
  import { fileURLToPath } from 'url';
4
5
  import { dirname, join } from 'path';
5
6
 
6
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const indexPath = join(__dirname, '..', 'index.js');
8
+ // Prefer readable source when present (local dev = always live); the published
9
+ // package ships only dist/, so there we fall back to the bundled build.
10
+ const srcPath = join(__dirname, '..', 'index.js');
11
+ const indexPath = existsSync(srcPath) ? srcPath : join(__dirname, '..', 'dist', 'index.js');
8
12
 
9
13
  const child = spawn(
10
14
  process.execPath,
package/bin/cairn.js CHANGED
@@ -1,10 +1,14 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import { spawn } from 'child_process';
3
+ import { existsSync } from 'fs';
3
4
  import { fileURLToPath } from 'url';
4
5
  import { dirname, join } from 'path';
5
6
 
6
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const cliPath = join(__dirname, 'cairn-cli.js');
8
+ // Prefer readable source when present (local dev = always live); the published
9
+ // package ships only dist/, so there we fall back to the bundled build.
10
+ const srcCli = join(__dirname, 'cairn-cli.js');
11
+ const cliPath = existsSync(srcCli) ? srcCli : join(__dirname, '..', 'dist', 'cairn-cli.js');
8
12
 
9
13
  const child = spawn(
10
14
  process.execPath,
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ import f from"fs";import U from"os";import y from"path";import{execSync as pe}from"child_process";import xt from"fast-glob";var me={".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=Ce(i,t)),o&&(i=De(i)),i=Ae(i),a&&(i=Ie(i)),i.trim()}function Ce(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 De(e){return e.split(`
3
+ `).filter(t=>t.trim().length>0).join(`
4
+ `)}function Ae(e){return e.split(`
5
+ `).map(t=>t.trimEnd()).join(`
6
+ `)}function Ie(e){return e.replace(/\s+/g," ").replace(/\s*([=+\-*/%<>!&|^~?:;,{}()[\]])\s*/g,"$1").replace(/;\s*}/g,"}").replace(/\(\s+/g,"(").replace(/\s+\)/g,")").trim()}function fe(e,t={}){let{noStyle:s=!1,noComments:n=!0,noEmptyLines:o=!0,aggressive:a=!1}=t,i=[],l=e.match(/<template>([\s\S]*?)<\/template>/i);if(l){let c=l[1];n&&(c=c.replace(/<!--[\s\S]*?-->/g,"")),o&&(c=c.split(`
7
+ `).filter(m=>m.trim()).join(`
8
+ `)),c=c.replace(/\s+/g," ").trim(),c&&i.push(`<template>${c}</template>`)}let u=e.match(/<script([^>]*)>([\s\S]*?)<\/script>/i);if(u){let c=u[1],m=V(u[2],"javascript",{noComments:n,noEmptyLines:o,aggressive:a}),d=/setup/i.test(c)?"<script setup>":"<script>";m&&i.push(`${d}${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
+
10
+ `)}import L from"fs";import A from"path";import z from"fs";import We from"os";import H from"path";var Ue=null;function he(){return Ue||process.cwd()}function J(){let e=H.join(he(),".cairn");z.mkdirSync(H.join(e,"bundles"),{recursive:!0});let t=H.join(e,".cairn-project");return z.existsSync(t)||z.writeFileSync(t,"","utf8"),e}function ee(e){return e.replace(/[:/\\]/g,"-").replace(/^-+/,"")}function ye(){let e=H.join(We.homedir(),".claude","projects",ee(he()));return z.existsSync(e)?H.join(e,"memory"):null}var He=1e3,Je=100*1024;function Be(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<=He)return a;let i=A.join(t,"notes");L.mkdirSync(i,{recursive:!0});let u=`note-${new Date().toISOString().replace(/[:.]/g,"-")}-${n}.md`,c=A.join(i,u);return L.writeFileSync(c,a,"utf8"),{description:a.split(`
11
+ `)[0].slice(0,120),ref:A.join(".cairn","notes",u)}}function te(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=J(),a=A.join(o,"session.json"),i=new Date().toISOString().slice(0,10),l={};try{let m=e.prepare("SELECT path FROM main.files").all();for(let{path:d}of m)try{l[d]=L.statSync(d).mtimeMs}catch{}}catch{}for(let m of s)try{l[m]=L.statSync(m).mtimeMs}catch{}let u=n.map((m,d)=>Be(m,o,i,d));if(L.existsSync(a))try{if(L.statSync(a).size>Je){let d=1;for(;L.existsSync(A.join(o,`session.${d}.json`));)d++;L.renameSync(a,A.join(o,`session.${d}.json`))}}catch{}let c={message:t,checkpoint_at:new Date().toISOString(),active_files:s,notes:u,mtime_snapshot:l};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:u.length},null,2)}]}}import M from"fs";import j from"path";import Ye from"crypto";var ge=80;function ne(e){let t=j.dirname(j.resolve(e)),s=j.parse(t).root;for(;;){let n=j.join(t,".cairn");try{if(M.statSync(n).isDirectory()&&M.existsSync(j.join(n,".cairn-project"))&&n.split(j.sep).filter(a=>a===".cairn").length===1)return n}catch{}if(t===s)return null;t=j.dirname(t)}}function _e(e,t){let s=Ye.createHash("sha256").update(j.resolve(t)).digest("hex").slice(0,6);return j.join(e,"views",`${s}_${j.basename(t)}`)}function C(e){try{return JSON.parse(M.readFileSync(j.join(e,"minify-map.json"),"utf8"))}catch{return{}}}function P(e,t){M.mkdirSync(e,{recursive:!0}),M.writeFileSync(j.join(e,"minify-map.json"),JSON.stringify(t,null,2),"utf8")}function K(e,t){let s=0;for(let[n,o]of Object.entries(t)){let a=!1;if(!M.existsSync(n))a=!0;else if(!M.existsSync(o.tempPath))a=!0;else try{M.statSync(n).mtimeMs>o.minifiedAt&&(a=!0)}catch{a=!0}if(a){if(o.tempPath)try{M.unlinkSync(o.tempPath)}catch{}delete t[n],s++}}return s}function Se(e,t){switch(t){case"javascript":case"typescript":return se(e);case"java":return se(e,{annotations:!0});case"python":return ze(e);case"vue":return Ke(e);case"sql":return Xe(e);default:return e.split(`
12
+ `).slice(0,30).join(`
13
+ `)+`
14
+ ...`}}function se(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:l,closes:u}=Ve(i),c=l-u;if(o===0){let m=Ge(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 Ge(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)?T(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)?T(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")?T(e):null}function T(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 Ve(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 ze(e){let t=[],s=e.split(`
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 Ke(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=se(s[1]);n.trim()&&(t.push("// script:"),t.push(n))}return t.join(`
19
+ `)}function Xe(e){let t=[],s=e.split(`
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 b from"fs";import Y from"path";function B(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 l=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(i);if(l){n&&o.push(n);let u=parseInt(l[1],10),c=l[2];if(a.has(u)){process.stderr.write(`[cairn] memo phase parser: duplicate Phase ${u} ignored
22
+ `),n=null;continue}a.add(u),n={phase_number:u,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 xe(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 = ?"),l=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, body, status, position) VALUES (?, ?, NULL, ?, ?, ?, ?)"),u=e.prepare("UPDATE main.roadmap_phases SET status = 'stale' WHERE id = ?"),c=[],m=[],d=[];for(let r=0;r<s.length;r++){let p=s[r];a.add(p.phase_number);let h=o.get(p.phase_number);if(h)i.run(p.text,p.body,"stale","open",h.id),c.push({id:h.id,phase_number:p.phase_number,text:p.text,reactivated:h.status==="stale"});else{let g=l.run(t,p.phase_number,p.text,p.body,"open",r);m.push({id:Number(g.lastInsertRowid),phase_number:p.phase_number,text:p.text})}}for(let[r,p]of o)!a.has(r)&&p.status!=="stale"&&p.status!=="done"&&(u.run(p.id),d.push({id:p.id,phase_number:r}));return{updated:c.length,inserted:m.length,staled:d.length,updated_phases:c,inserted_phases:m,staled_phases:d}}var N="roadmap",Qe=`# Roadmap
24
+
25
+ The forward-looking plan: only upcoming and in-progress phases live here. When a
26
+ phase is marked done (cairn_roadmap set_status <id> done) it is removed from this
27
+ file and recorded in roadmap_completed.md. Edit the \`## Phases\` list to change
28
+ the plan \u2014 Cairn syncs it into the live tracker on the next cairn_roadmap read.
29
+
30
+ ## Phases
31
+
32
+ ### Phase 1: First phase title
33
+ What this phase delivers.
34
+ `;function D(){return Y.join(J(),"roadmap.md")}function X(){let e=D();return b.existsSync(e)?b.readFileSync(e,"utf8"):""}function re(){let e=D();return b.existsSync(e)?!1:(b.writeFileSync(e,Qe,"utf8"),!0)}function Ze(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 oe="decision_production_roadmap.md";function et(e){let t=e.match(/^---\n[\s\S]*?\n---\n?/);return t?e.slice(t[0].length):e}function tt(e,t){let s=Y.join(e,"MEMORY.md");if(!b.existsSync(s))return;let n=b.readFileSync(s,"utf8").split(/\r?\n/).filter(o=>!o.includes(`](${t})`));b.writeFileSync(s,n.join(`
35
+ `),"utf8")}function nt(){let e=D();if(b.existsSync(e))return null;let t=[],s=ye();s&&t.push(s),t.push(Y.join(J(),"memory"));let n=null,o=null;for(let i of t){let l=Y.join(i,oe);n==null&&b.existsSync(l)&&(n=et(b.readFileSync(l,"utf8")).trim(),o=l)}if(n==null)return null;let a=/^#\s/m.test(n)?"":`# Roadmap
36
+
37
+ `;b.writeFileSync(e,a+n+`
38
+ `,"utf8");for(let i of t){let l=Y.join(i,oe);try{b.existsSync(l)&&b.unlinkSync(l)}catch{}try{tt(i,oe)}catch{}}return{migrated:!0,from:o,to:e}}function I(e){nt();let t=D();if(!b.existsSync(t))return null;let s=b.readFileSync(t,"utf8");if(!/^##\s+Phases\s*$/im.test(s))return null;let n=Ze(B(s));return xe(e,N,n)}import W from"fs";import ie from"path";import{execSync as st}from"child_process";var Ee=120;function ce(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>Ee&&(t=t.slice(0,Ee-1).trimEnd()+"\u2026"),t}function F(e){return String(e||"").toLowerCase().replace(/[^a-z0-9]+/g," ").trim()}function ot(e){if(!e||typeof e!="string")return[];let t=e.split(/\r?\n/),s=c=>c.map(m=>({text:ce(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 l=t.find(c=>c.trim());return[{text:ce(l||"Plan")||"Plan",body:e.trim()||null}]}function rt(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
+ `).replace(/\n{3,}/g,`
41
+
42
+ `).trimEnd()+`
43
+ `}function we(e){let t=X(),s=t.split(/\r?\n/),n=s.findIndex(c=>/^##\s+Phases\s*$/i.test(c)),o=rt(e);if(n===-1){let c=t.replace(/\s*$/,"");W.writeFileSync(D(),`${c}
44
+
45
+ ## Phases
46
+
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
+ `),l=s.slice(a).join(`
49
+ `).replace(/^\n+/,""),u=`
50
+
51
+ ${o}${l.trim()?`
52
+ `+l:""}`;W.writeFileSync(D(),i+u,"utf8")}function ke(e){let t=ot(e);if(t.length===0)return{added:[],skipped:[],total:0};let n=re()?[]:B(X()).map(u=>({text:u.text,body:u.body})),o=new Set(n.map(u=>F(u.text))),a=[...n],i=[],l=[];for(let u of t){let c=F(u.text);if(o.has(c)){l.push(u.text);continue}o.add(c),a.push(u),i.push(u.text)}return i.length>0&&we(a),{added:i,skipped:l,total:a.length}}function it(e){return e==="completed"?"done":e==="in_progress"?"in_progress":"open"}function at(e){try{return e.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null}catch{return null}}function $e(e,t){if(!Array.isArray(t)||t.length===0)return{changed:!1};let s=t.map(r=>({text:ce(r.content||r.activeForm||""),status:it(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(N).filter(r=>r.status!=="stale");if(n.length===0){let p=re()?[]:B(X()).map(S=>({text:S.text,body:S.body})),h=new Set(p.map(S=>F(S.text))),g=[...p],w=0;for(let S of s){let v=F(S.text);h.has(v)||(h.add(v),g.push({text:S.text,body:null}),w++)}if(w===0)return{changed:!1};we(g);let _=I(e);return{changed:!0,seeded:w,diff:_}}let o=at(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=>[F(r.text),r])),l=e.prepare("INSERT INTO main.roadmap_phases (decision_name, phase_number, parent_phase_id, text, status, position) VALUES (NULL, NULL, ?, ?, ?, ?)"),u=e.prepare("UPDATE main.roadmap_phases SET status = ? WHERE id = ?"),c=e.prepare("SELECT COALESCE(MAX(position), -1) + 1 AS n FROM main.roadmap_phases WHERE parent_phase_id = ?").get(o).n,m=0,d=0;for(let r of s){let p=i.get(F(r.text));if(p){if(p.status==="done"&&r.status!=="done")continue;p.status!==r.status&&(u.run(r.status,p.id),d++)}else l.run(o,r.text,r.status,c++),m++}return{changed:m>0||d>0,parentId:o,inserted:m,updated:d}}var ct=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 pt(e){return[...new Set(F(e).split(" ").filter(t=>t.length>=4&&!ct.has(t)))]}function lt(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),l=t.find(u=>u.phase_number===i);if(l)return{...l,confidence:"high"}}let o=new Set(F(s).split(" ").filter(Boolean)),a=null;for(let i of t){let l=pt(i.text);if(l.length<2)continue;let u=l.filter(m=>o.has(m)),c=u.length/l.length;u.length>=2&&c>=.6&&(!a||c>a.ratio)&&(a={...i,confidence:"medium",ratio:c})}return a?(delete a.ratio,a):null}function ae(e,t){return st(`git ${t}`,{cwd:e,encoding:"utf8",timeout:5e3}).trim()}function be(e,t){let s=ie.join(t,".cairn"),n=ie.join(s,".last-commit"),o=ie.join(s,"roadmap-pending.json"),a;try{a=ae(t,"rev-parse HEAD")}catch{return{candidates:[],newCommits:0}}let i=null;try{i=W.readFileSync(n,"utf8").trim()||null}catch{}let l=[];if(i&&i!==a)try{l=ae(t,`rev-list --no-merges --reverse ${i}..HEAD`).split(`
53
+ `).filter(Boolean)}catch{l=[]}try{W.writeFileSync(n,a,"utf8")}catch{}let u=[];try{let c=JSON.parse(W.readFileSync(o,"utf8"));Array.isArray(c)&&(u=c)}catch{}if(l.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(N);for(let m of l){let d="";try{d=ae(t,`show -s --format=%s%n%b ${m}`)}catch{}let r=lt(d,c);r&&!u.some(p=>p.phase_id===r.id)&&u.push({phase_id:r.id,phase_number:r.phase_number,text:r.text,commit:m.slice(0,7),confidence:r.confidence})}}if(u.length){let c=new Set(e.prepare("SELECT id FROM main.roadmap_phases WHERE status IN ('open','in_progress')").all().map(m=>m.id));u=u.filter(m=>c.has(m.phase_id))}try{W.writeFileSync(o,JSON.stringify(u),"utf8")}catch{}return{candidates:u,newCommits:l.length}}var dt=new Set(["open","in_progress"]);function ut(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 mt(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 ve(e,t){ut(e,t),e.prepare("UPDATE main.roadmap_phases SET status = 'in_progress' WHERE id = ? AND status = 'open'").run(t),mt(e,t)}function ft(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 ht(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 Re(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(d=>[d.id,d.status])),o=ft(e),a=new Map;for(let d of s){let r=d.parent_phase_id??`root:${d.decision_name}`;a.has(r)||a.set(r,[]),a.get(r).push(d)}for(let d of a.values())d.sort((r,p)=>r.phase_number!=null&&p.phase_number!=null?r.phase_number-p.phase_number:r.position-p.position||r.id-p.id);let i=[],l=new Set;for(let d of s){if(d.parent_phase_id!=null||l.has(d.decision_name))continue;l.add(d.decision_name);let r=[],p=a.get(`root:${d.decision_name}`)||[];for(let h=p.length-1;h>=0;h--)r.push(p[h]);for(;r.length;){let h=r.pop();i.push(h);let g=a.get(h.id)||[];for(let w=g.length-1;w>=0;w--)r.push(g[w])}}let u=d=>dt.has(d.status),c=[],m=0;if(t!=null){let d=i.findIndex(r=>r.id===t);m=d===-1?0:d+1}for(let d=m;d<i.length;d++){let r=i[d];if(!u(r))continue;let p=ht(r.id,o,n);if(p.length>0){c.push({id:r.id,blockers:p});continue}let h=t!=null?s.find(_=>_.id===t):null,g=h&&h.decision_name!==r.decision_name,w;return t==null?w=`First actionable phase: ${r.decision_name??"(sub-task)"} / Phase ${r.phase_number??"\xB7"}.`:g?w=`Previous decision "${h.decision_name}" is complete \u2014 advanced to "${r.decision_name}".`:w=`Next open phase in "${r.decision_name??"(sub-task)"}" after id ${t}.`,c.length>0&&(w+=` Skipped ${c.length} dep-blocked phase(s): ${c.map(_=>`id ${_.id} (blocked by ${_.blockers.join(",")})`).join("; ")}.`),{id:r.id,reason:w,skipped_blocked:c}}return c.length>0?{id:null,reason:`All remaining open phases are blocked by unfinished dependencies: ${c.map(d=>`id ${d.id} (blocked by ${d.blockers.join(",")})`).join("; ")}.`,skipped_blocked:c}:{id:null,reason:"No actionable phases remaining."}}var yt=new Set(["java","typescript","javascript","vue","python","sql"]);function Oe(e){let t=y.join(U.homedir(),".claude","projects",ee(e));return f.existsSync(t)?y.join(t,"memory"):null}function Te(e){try{return f.readdirSync(e).filter(t=>t.endsWith(".md"))}catch{return[]}}function gt(e){let t=Oe(e);if(!t||!f.existsSync(t))return 0;let s=y.join(e,".cairn","memory");f.mkdirSync(s,{recursive:!0});let n=0;for(let o of Te(t)){let a=y.join(t,o),i;try{i=f.statSync(a)}catch{continue}if(!i.isFile())continue;let l=y.join(s,o),u=!f.existsSync(l);if(!u)try{u=i.mtimeMs>f.statSync(l).mtimeMs}catch{u=!0}if(u){f.copyFileSync(a,l);try{f.utimesSync(l,i.atime,i.mtime)}catch{}n++}}return n}function _t(e){let t=Oe(e);if(!t||(f.mkdirSync(t,{recursive:!0}),Te(t).some(o=>o!=="MEMORY.md")))return 0;let s=y.join(e,".cairn","memory");if(!f.existsSync(s))return 0;let n=0;for(let o of f.readdirSync(s)){let a=y.join(s,o),i;try{i=f.statSync(a)}catch{continue}if(i.isFile()){f.copyFileSync(a,y.join(t,o));try{f.utimesSync(y.join(t,o),i.atime,i.mtime)}catch{}n++}}return n}async function je(e){let t={active:!1},s=y.join(e,"index.db");if(!f.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=l=>l.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"");function i(l,u){if(!f.existsSync(l))return null;let c=f.readFileSync(l,"utf8"),m=!1,d=!1,r=[];for(let p of c.split(/\r?\n/)){if(/^##\s+Phases\s*$/i.test(p)){m=!0;continue}if(m&&/^##\s+/.test(p)&&!/^###/.test(p))break;if(!m)continue;let h=/^###\s+Phase\s+(\d+)\s*:\s*(.+?)\s*$/.exec(p);if(h){if(d)break;parseInt(h[1],10)===u&&(r.push(`### Phase ${h[1]}: ${h[2]}`),d=!0);continue}d&&r.push(p)}return r.join(`
54
+ `).trim()||null}try{let u=o.prepare("SELECT phase_id FROM roadmap_cursor WHERE singleton = 1").get()?.phase_id??null;if(u==null)return o.close(),t;let 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]"},d=new Map;for(let k of c){let x=k.parent_phase_id??"root";d.has(x)||d.set(x,[]),d.get(x).push(k)}for(let k of d.values())k.sort((x,E)=>x.decision_name&&E.decision_name&&x.decision_name!==E.decision_name?x.decision_name.localeCompare(E.decision_name):x.phase_number!=null&&E.phase_number!=null?x.phase_number-E.phase_number:x.position-E.position||x.id-E.id);let r=d.get("root")||[],p=new Map;for(let k of r)k.decision_name!=null&&(p.has(k.decision_name)||p.set(k.decision_name,[]),p.get(k.decision_name).push(k));let h=[],g=(k,x)=>{for(let E of d.get(k)||[]){if(E.status==="stale")continue;let Q=u===E.id?" \u2192":"",Z=E.phase_number!=null?`Phase ${E.phase_number}: ${E.text}`:E.text;h.push(`${" ".repeat(x)}${m[E.status]||"[?]"}${Q} ${Z}`),g(E.id,x+1)}};for(let[k,x]of p){let E=x.filter($=>$.status!=="stale");if(E.length===0)continue;let Q=E.every($=>$.status==="done"),Z=E.some($=>$.status==="in_progress"),Le=Q?"done":Z?"in_progress":"open",Me=E.filter($=>$.status==="done").length,Ne=k===N?"Roadmap":`Decision: ${k}`;h.push(`${m[Le]} ${Ne} (${Me}/${E.length} done)`);for(let $ of x){if($.status==="stale"||k===N&&$.status==="done")continue;let Fe=u===$.id?" \u2192":"";h.push(` ${m[$.status]||"[?]"}${Fe} Phase ${$.phase_number}: ${$.text}`),g($.id,2)}}let w=k=>c.find(x=>x.id===k),_=w(u),S=_?.decision_name??null,v=_?.phase_number??null,O=_;for(;O&&O.parent_phase_id!=null&&(O=w(O.parent_phase_id),!!O);)S=O.decision_name??S,v=O.phase_number??v;o.close();let G=_?_.phase_number!=null?`${_.decision_name} / Phase ${_.phase_number}: ${_.text}`:`${S} / Phase ${v} \u2192 ${_.text}`:"(unknown)",q=y.join(e,".last-roadmap-cursor"),le=null;try{le=parseInt(f.readFileSync(q,"utf8").trim(),10)}catch{}let de=le!==u;try{f.writeFileSync(q,String(u),"utf8")}catch{}let ue=null;if(de&&S!=null&&v!=null){let k=S===N?y.join(e,"roadmap.md"):y.join(e,"memory",`decision_${a(S)}.md`),x=i(k,v);x&&(ue=x.split(`
55
+ `))}return{active:!0,currentLabel:G,treeLines:h,advanced:de,memoLines:ue}}catch{try{o?.close()}catch{}return t}}process.on("uncaughtException",e=>{process.stderr.write(`cairn: ${e.message}
56
+ `),process.exit(0)});var R=process.argv[2];if(R==="--version"||R==="-v"){let e=JSON.parse(f.readFileSync(new URL("../package.json",import.meta.url),"utf8"));console.log(e.version),process.exit(0)}else if(R==="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=ne(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=C(n),u=K(n,i)>0,c=i[t];c?.state==="edit-ready"&&(u&&P(n,i),process.exit(0)),c?.state==="compressed"&&(c.state="edit-ready",P(n,i),process.exit(0));let m=y.extname(t).toLowerCase(),d=me[m];(!d||!yt.has(d))&&(u&&P(n,i),process.exit(0));let r;try{r=f.readFileSync(t,"utf8")}catch{u&&P(n,i),process.exit(0)}let p=r.split(`
57
+ `).length,h=d==="vue"?fe(r):V(r,d),g=h.split(`
58
+ `).length,w=p>0?Math.round((1-g/p)*100):0,_=_e(n,t);f.mkdirSync(y.dirname(_),{recursive:!0}),f.writeFileSync(_,h,"utf8");let S=c?.readCount??0;i[t]={tempPath:_,state:"compressed",minifiedAt:f.statSync(t).mtimeMs,readCount:S+1},P(n,i);let v=g>ge,O=S>0;if(O||v){let G=Se(h,d),q=O?`[cairn: ${p} \u2192 ${g} lines | seen \xD7${S+1} this session]`:`[cairn: ${p} \u2192 outline (${g} minified lines)]`;process.stderr.write(`${q}
59
+ ${G}
60
+ Full minified view: Read("${_}")
61
+ `)}else process.stderr.write(`[cairn: ${p} \u2192 ${g} lines (-${w}%)]
62
+ ${h}
63
+ Cached view: Read("${_}")
64
+ `);process.exit(2)})}else if(R==="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 d=JSON.parse(e);t=d?.tool_input?.file_path||"(unknown)",s=d?.tool_name||"Edit"}catch{}let n=y.resolve(t),o=ne(n);o||process.exit(0);let a=y.join(o,"views"),i=y.relative(a,n);if(!i.startsWith("..")&&!y.isAbsolute(i)){let d=C(o),r=Object.entries(d).find(([,h])=>h.tempPath===n),p=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.
65
+ ${p}
66
+ `),process.exit(2)}let l=C(o),u=K(o,l),c=l[n];c||(u>0&&P(o,l),process.exit(0)),s==="Write"&&(delete l[n],P(o,l),process.exit(0)),c.state==="edit-ready"&&(delete l[n],P(o,l),process.exit(0)),c.state="edit-ready",P(o,l);let m=null;try{let d=f.readFileSync(n,"utf8");d.split(`
67
+ `).length<=300&&(m=d)}catch{}m!==null?process.stderr.write(`[cairn] File was compressed in context. Full source content:
68
+
69
+ ${m}
70
+
71
+ Re-apply your Edit using the exact content above (state is now edit-ready).
72
+ `):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(R==="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");f.existsSync(a)||process.exit(0);let i=y.join(a,"index.db");f.existsSync(i)||process.exit(0);let l;try{({DatabaseSync:l}=await import("node:sqlite"))}catch{process.exit(0)}let u=/exit.?plan.?mode/i.test(s),c=/todo.?write/i.test(s);!u&&!c&&process.exit(0);let m=null,d;try{if(u){let r=n.plan;(typeof r!="string"||!r.trim())&&process.exit(0);let p=ke(r);if(d=new l(i),I(d),(d.prepare("SELECT phase_id FROM main.roadmap_cursor WHERE singleton = 1").get()?.phase_id??null)==null){let g=Re(d,null);g.id!=null&&ve(d,g.id)}p.added.length>0&&(m=`[cairn] roadmap: captured ${p.added.length} phase(s) from plan${p.skipped.length?` (${p.skipped.length} already present)`:""}.`)}else if(c){d=new l(i);let r=$e(d,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}
74
+ `);try{d?.close()}catch{}process.exit(0)}try{d?.close()}catch{}m&&process.stdout.write(JSON.stringify({systemMessage:m})),process.exit(0)})}else if(R==="validate-map"){let e=y.join(process.cwd(),".cairn");f.existsSync(e)||process.exit(0);let t=C(e),s=K(e,t);for(let n of Object.values(t))n.readCount=0;P(e,t),s>0&&process.stdout.write(`[cairn] Cleaned minify map: ${s} stale entr${s===1?"y":"ies"} removed
75
+ `),process.exit(0)}else if(R==="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");f.existsSync(o)&&(t=JSON.parse(f.readFileSync(o,"utf8")).notes||[]);let a=C(n);s=Object.entries(a).filter(([,i])=>(i.readCount??0)>0).map(([i])=>i)}catch{}te(null,{message:e,active_files:s,notes:t}),process.exit(0)}else if(R==="session-tick"){let e=process.cwd(),t=y.join(e,".cairn");f.existsSync(t)||process.exit(0);let s=[];try{let i=`Auto-checkpoint at ${new Date().toISOString()}`,l=[],u=y.join(t,"session.json");if(f.existsSync(u))try{l=JSON.parse(f.readFileSync(u,"utf8")).notes||[]}catch{}let c=C(t),m=Object.entries(c).filter(([,d])=>(d.readCount??0)>0).map(([d])=>d);te(null,{message:i,active_files:m,notes:l}),s.push("\u2713 checkpoint saved")}catch{}try{let i=gt(e);i>0&&s.push(`memory backed up (${i})`)}catch{}let n=[];try{let i=y.join(t,"index.db");if(f.existsSync(i)){let{DatabaseSync:l}=await import("node:sqlite"),u=new l(i);try{I(u);try{n=be(u,e).candidates}catch{}}finally{u.close()}}}catch{}let o="";try{let i=await je(t);if(i.active){s.push(`roadmap ${i.advanced?"updated":"on track"}: ${i.currentLabel}`);let l=["[cairn] Production roadmap (\u2192 = cursor / current phase):",...i.treeLines.map(u=>" "+u)];i.memoLines&&l.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"),l.push("","Keep this current via cairn_roadmap on MATERIAL change (phase done / blocked / new) \u2014 not every turn."),o=l.join(`
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 l of n)i.push(` \u2022 Phase ${l.phase_number}: ${l.text} (commit ${l.commit}, ${l.confidence}) \u2192 if confirmed: cairn_roadmap set_status ${l.phase_id} done`);o=o?o+`
77
+ `+i.join(`
78
+ `):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(R==="memory-restore"){let e=process.cwd();f.existsSync(y.join(e,".cairn"))||process.exit(0);try{let t=_t(e);t>0&&process.stdout.write(`[cairn] Restored ${t} memory file(s) from .cairn/memory backup (Claude store was empty)
80
+ `)}catch{}process.exit(0)}else if(R==="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;if(!(process.cwd()===U.homedir())&&f.existsSync(e)&&(f.existsSync(s)||f.existsSync(t))){let r=y.join(e,".cairn-project");if(!f.existsSync(r))try{f.writeFileSync(r,"","utf8"),process.stdout.write(`[cairn] Repaired: wrote .cairn-project sentinel for hook compatibility.
81
+ `)}catch{}}{let r=[y.join(process.cwd(),".claude","settings.json"),y.join(U.homedir(),".claude","settings.json")];for(let p of r)try{if(!f.existsSync(p))continue;let h=f.readFileSync(p,"utf8"),g=JSON.parse(h),w=g?.hooks?.PreToolUse;if(!Array.isArray(w))continue;let _=!1;for(let S of w)if(!(S.matcher!=="Edit"||!Array.isArray(S.hooks)))for(let v of S.hooks)v.command==="cairn set-mode edit"&&(v.command="cairn edit-guard",_=!0);_&&(f.writeFileSync(p,JSON.stringify(g,null,2),"utf8"),process.stdout.write(`[cairn] Repaired: migrated cairn set-mode \u2192 cairn edit-guard in ${p}
82
+ `))}catch{}}let i="";try{i=(JSON.parse(f.readFileSync("/dev/stdin","utf8"))?.prompt||"").toLowerCase()}catch{}let u=/\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);f.existsSync(t)||(f.existsSync(s)||(f.existsSync(n)&&Date.now()-f.statSync(n).mtimeMs<o&&process.exit(0),f.mkdirSync(e,{recursive:!0}),f.writeFileSync(n,new Date().toISOString(),"utf8"),process.stdout.write(`[cairn] No index found. Run cairn_maintain to index this project.
83
+ `)),process.exit(0)),f.existsSync(n)&&Date.now()-f.statSync(n).mtimeMs<o&&process.exit(0);let c=JSON.parse(f.readFileSync(t,"utf8")),m=new Date(c.checkpoint_at).toLocaleString(),d=[`[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(u){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 p=new Date(c.checkpoint_at).toISOString(),h=pe(`git log --oneline --after="${p}"`,{cwd:process.cwd(),encoding:"utf8",timeout:5e3}).trim();r.push(` commits_since: ${h?h.split(`
84
+ `).join("; "):"(none)"}`)}catch{}try{let p=pe("git diff --name-only HEAD",{cwd:process.cwd(),encoding:"utf8",timeout:5e3}).trim();p&&r.push(` uncommitted: ${p.split(`
85
+ `).join(", ")}`)}catch{}try{let p=pe("git status --short",{cwd:process.cwd(),encoding:"utf8",timeout:5e3}).trim();p&&r.push(` git_status:
86
+ ${p.split(`
87
+ `).map(h=>" "+h).join(`
88
+ `)}`)}catch{}r.push(" \u2192 Call cairn_resume for full index status and open todos."),d.push(r.join(`
89
+ `))}process.stdout.write(d.join(`
90
+ `)+`
91
+ `),f.writeFileSync(n,new Date().toISOString(),"utf8"),process.exit(0)}else if(R==="roadmap-hint"){let e=y.join(process.cwd(),".cairn"),t=await je(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
+ `)+`
93
+ `),process.exit(0)}else if(R==="install"){let e=process.platform,t=y.join(U.homedir(),".claude.json"),s={};if(f.existsSync(t))try{s=JSON.parse(f.readFileSync(t,"utf8"))}catch{}s.mcpServers=s.mcpServers||{};let n=!!s.mcpServers.cairn;s.mcpServers.cairn={command:"cairn-mcp"},f.writeFileSync(t,JSON.stringify(s,null,2),"utf8");let o=y.join(U.homedir(),".claude");Pe(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(R==="install-hooks"){let e=process.argv.includes("--global"),t=e?y.join(U.homedir(),".claude"):y.join(process.cwd(),".claude");Pe(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 Pe(e,t){let s=y.join(e,"settings.json"),n={};if(f.existsSync(s))try{n=JSON.parse(f.readFileSync(s,"utf8"))}catch{}n.hooks=n.hooks||{};let o=n.hooks.PreToolUse||[];o.some(r=>r.matcher==="Read"&&r.hooks?.some(p=>p.command==="cairn minify"))||o.push({matcher:"Read",hooks:[{type:"command",command:"cairn minify"}]});let a=o.filter(r=>!(r.matcher==="Edit"&&r.hooks?.some(p=>p.command==="cairn set-mode edit")));a.some(r=>r.matcher==="Edit"&&r.hooks?.some(p=>p.command==="cairn edit-guard"))||a.push({matcher:"Edit",hooks:[{type:"command",command:"cairn edit-guard"}]}),a.some(r=>r.matcher==="Write"&&r.hooks?.some(p=>p.command==="cairn edit-guard"))||a.push({matcher:"Write",hooks:[{type:"command",command:"cairn edit-guard"}]}),n.hooks.PreToolUse=a;let i=(r,p)=>(r||[]).map(h=>({...h,hooks:(h.hooks||[]).filter(g=>!p(g.command))})).filter(h=>(h.hooks||[]).length>0),l=r=>typeof r=="string"&&/sync-memory-(forward|restore)\.(mjs|py)/.test(r),u=i(n.hooks.Stop,r=>r==="cairn checkpoint --auto"||r==="cairn roadmap-hint");u.some(r=>r.hooks?.some(p=>p.command==="cairn session-tick"))||u.push({hooks:[{type:"command",command:"cairn session-tick"}]}),n.hooks.Stop=u;let c=i(n.hooks.UserPromptSubmit,l);c.some(r=>r.hooks?.some(p=>p.command==="cairn resume-hint"))||c.push({hooks:[{type:"command",command:"cairn resume-hint"}]}),c.some(r=>r.hooks?.some(p=>p.command==="cairn memory-restore"))||c.push({hooks:[{type:"command",command:"cairn memory-restore"}]}),n.hooks.UserPromptSubmit=c;let m=i(n.hooks.PostToolUse,l),d=r=>{m.some(p=>p.matcher===r&&p.hooks?.some(h=>h.command==="cairn roadmap-capture"))||m.push({matcher:r,hooks:[{type:"command",command:"cairn roadmap-capture"}]})};if(d("ExitPlanMode"),d("TodoWrite"),n.hooks.PostToolUse=m,t){let r=y.join(e,"scripts");for(let p of["sync-memory-forward.mjs","sync-memory-restore.mjs","sync-memory-forward.py","sync-memory-restore.py"]){let h=y.join(r,p);if(f.existsSync(h))try{f.unlinkSync(h)}catch{}}}if(!t){let r=y.join(process.cwd(),".cairn");f.mkdirSync(r,{recursive:!0});let p=y.join(r,".cairn-project");f.existsSync(p)||f.writeFileSync(p,"","utf8")}f.mkdirSync(e,{recursive:!0}),f.writeFileSync(s,JSON.stringify(n,null,2),"utf8")}