infernoflow 0.44.18 → 0.44.20

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 CHANGED
@@ -143,7 +143,7 @@ infernoflow prune --apply --max-age-days 14 # one-off cleanup
143
143
 
144
144
  **Rotation** archives stale `note` / `attempt` / `detection` entries to `.ai-memory/archive/sessions-YYYY-MM.jsonl` — invisible to the merged read (so the AI, sidebar, `ask`, and `refresh` stop surfacing them) but still on disk if you want them back. `gotcha`, `decision`, `pattern`, and `bookmark` entries are **never auto-pruned** — that's the knowledge you logged infernoflow FOR.
145
145
 
146
- **Two-tier bodies (new in 0.44.10):** any entry can carry a rich `detail` — stored in `.ai-memory/details/<id>.md`, loaded on demand via `readDetail()`, and **never injected into rule files**. The lean index stays lean; you pay for the body only when you open it. `log --detail`, `--detail-file`, MCP `amp_write` `detail`, and the new `amp_bookmark` tool all feed it.
146
+ **Two-tier bodies:** any entry can carry a rich `detail` — stored in a single `.ai-memory/details.jsonl` (consolidated in 0.44.18; older per-entry `details/<id>.md` sidecars auto-migrate on the next write), loaded on demand via `readDetail()`, and **never injected into rule files**. The lean index stays lean; you pay for the body only when you open it. `log --detail`, `--detail-file`, MCP `amp_write` `detail`, and the new `amp_bookmark` tool all feed it.
147
147
 
148
148
  ---
149
149
 
@@ -267,13 +267,14 @@ That's the whole product. No vendor lock-in (it's JSONL on disk). No SaaS. One C
267
267
 
268
268
  Local-first by design:
269
269
 
270
- - 🚫 **No telemetry.** No analytics, no error reporting, no install pings.
270
+ - ✅ **Telemetry is opt-in and off by default.** After a few runs in an interactive terminal infernoflow asks once; nothing is sent unless you answer yes. It never sends code, file paths or memory content. Check or change it with `infernoflow telemetry status`.
271
271
  - 🚫 **No `postinstall` script.** `npm install -g infernoflow` runs no code — it only copies files.
272
- - 🚫 **No network calls in any default command path.** Everything runs on your machine.
273
- - 🚫 **No auto-updates, no background processes, no cloud sync.**
274
- - ✅ **Reads and writes only inside your project directory** (`.ai-memory/`, plus the three rule files at repo root).
272
+ - 🚫 **No network calls in any default command path** (with telemetry off, which is the default).
273
+ - 🚫 **No auto-updates of the package, no background processes, no cloud sync.**
274
+ - ⚠️ **Writes outside the project:** `setup` (and the automatic refresh after an upgrade) registers the MCP server in your AI tools' config files — `~/.claude.json` and the Claude Desktop config. Everything else stays in the project (`.ai-memory/`, `.cursor/`, `.claude/`, `.vscode/`, the three rule files). Moving these registrations into per-project config is in progress.
275
275
  - ✅ **Auto-injected content is wrapped in markers** (`<!-- infernoflow:start -->` / `<!-- infernoflow:end -->`) — your manual edits outside the block are never touched.
276
- - ✅ **Secret patterns rejected on capture** — entries matching `sk-`, `ghp_`, `-----BEGIN` are refused at the AMP writer.
276
+ - ⚠️ **Memory files are plain text and are committed with your repo.** Don't paste secrets into prompts or log messages — automatic secret redaction is not implemented yet.
277
+ - ✅ **No shell is used to run commands** from the MCP server or the prompt hooks (since 0.44.20), and MCP tool arguments are validated against their schema.
277
278
 
278
279
  The optional `infernoflow ai setup` command wires an AI provider (Anthropic / OpenAI / Google / Ollama) for a few enrichment commands — same trust model as using that provider directly. Off by default.
279
280
 
@@ -1,20 +1,27 @@
1
- import*as s from"node:fs";import*as J from"node:os";import*as r from"node:path";import{findProjectRoot as O}from"../projectRoot.mjs";import{getBranchInfo as M}from"../git/branch.mjs";const T="1.0",_=new Set(["gotcha","decision","attempt","note","detection","pattern"]),P=new Set(["copilot","cursor","claude","windsurf","other"]);function L(e){let t=r.basename(r.resolve(e));try{const o=r.join(e,".ai-memory","amp.json");if(s.existsSync(o)){const n=JSON.parse(s.readFileSync(o,"utf8"));n&&typeof n.project=="string"&&n.project.trim()&&(t=n.project.trim())}}catch{}return t.toLowerCase().replace(/[^a-z0-9_.\-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,64)||"unnamed-project"}function W(e,t){let o=process.env.INFERNOFLOW_GLOBAL_DIR;if(!o)try{const l=JSON.parse(s.readFileSync(r.join(t,"amp.json"),"utf8"));l&&typeof l.globalDir=="string"&&l.globalDir.trim()&&(o=l.globalDir.trim())}catch{}if(!o)return r.join(t,"global.jsonl");let n=o;n.startsWith("~")&&(n=r.join(J.homedir(),n.slice(1).replace(/^[\/\\]/,""))),r.isAbsolute(n)||(n=r.resolve(e,n));const c=L(e);return r.join(n,c,"global.jsonl")}function m(e,t={}){const o=t.literal?r.resolve(e):O(e),n=r.join(o,".ai-memory"),c=r.join(o,"inferno"),l=t.forWrite||s.existsSync(n)||!s.existsSync(c),a=l?n:c,i=l,u=r.join(a,"branches"),f=M(o),h=r.join(u,`${f.currentSlug}.jsonl`),g=f.defaultSlug?r.join(u,`${f.defaultSlug}.jsonl`):null,S=Y(o,a);return{root:a,projectRoot:o,isAmp:i,sessions:r.join(a,"sessions.jsonl"),config:r.join(a,i?"amp.json":"config.json"),handoff:r.join(a,i?"handoff.md":"HANDOFF.md"),globalFile:S,branchesDir:u,currentBranchFile:h,defaultBranchFile:g,branch:f}}function Y(e,t){try{return W(e,t)}catch{return r.join(t,"global.jsonl")}}function x(e){const t=O(e),o=r.join(t,".ai-memory");return s.existsSync(o)||s.mkdirSync(o,{recursive:!0}),o}const B="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function E(){let e=Date.now(),t="";for(let n=0;n<10;n++)t=B[e%32]+t,e=Math.floor(e/32);let o="";for(let n=0;n<16;n++)o+=B[Math.floor(Math.random()*32)];return t+o}function N(e){const t={...e.meta||{}};let o=e.type||"note";_.has(o)||(t.subtype=o,o="note"),e.result&&(t.result=e.result),e.detailRef&&(t.detailRef=e.detailRef);let n;const c=e.agent;c&&P.has(c)?n=c:c&&(t.agent=c);const l=typeof e.ts=="number"?e.ts:e.ts?Date.parse(e.ts):Date.now(),a=e.confidence!=null?e.confidence:e.auto?.7:void 0,i={type:o,msg:e.summary||e.msg||"",ts:l,id:e.id||`amp_${E()}`};return e.file&&(i.file=e.file),e.line&&(i.line=e.line),e.function&&(i.function=e.function),e.tags&&e.tags.length&&(i.tags=e.tags),e.source&&(i.source=e.source),n&&(i.tool=n),e.session&&(i.session=e.session),a!=null&&(i.confidence=a),Object.keys(t).length&&(i.meta=t),i}function G(e){if(e.summary&&!e.msg)return e;const t=e.meta||{},o=t.subtype||e.type||"note",n={ts:e.ts,type:o,summary:e.msg||""};e.id&&(n.id=e.id),e.file&&(n.file=e.file),e.line&&(n.line=e.line),e.function&&(n.function=e.function),e.tags&&(n.tags=e.tags),e.source&&(n.source=e.source),e.tool&&(n.agent=e.tool),t.agent&&(n.agent=t.agent),t.result&&(n.result=t.result),t.detailRef&&(n.detailRef=t.detailRef),e.confidence!=null&&(n.confidence=e.confidence,e.confidence<1&&(n.auto=!0));const{subtype:c,agent:l,result:a,detailRef:i,...u}=t;return Object.keys(u).length&&(n.meta=u),n}function U(e){if(!e||!s.existsSync(e))return[];try{return s.readFileSync(e,"utf8").split(`
2
- `).filter(Boolean).map(t=>{try{return JSON.parse(t)}catch{return null}}).filter(Boolean).map(G)}catch{return[]}}function ne(e){const t=m(e),o=new Set,n=[],c=[t.sessions,t.globalFile,t.defaultBranchFile,t.currentBranchFile],l=[...new Set(c.filter(Boolean))];for(const a of l)for(const i of U(a)){const u=i.id||`${i.ts}|${i.summary}`;o.has(u)||(o.add(u),n.push(i))}return n.sort((a,i)=>{const u=typeof a.ts=="number"?a.ts:Date.parse(a.ts||0),f=typeof i.ts=="number"?i.ts:Date.parse(i.ts||0);return u-f})}function H(e,t){return t.target==="global"?e.globalFile:t.target==="legacy"?e.sessions:t.target==="branch"?e.currentBranchFile:t.type==="preference"?e.globalFile:e.currentBranchFile}function oe(e,t){x(e);const o=m(e,{forWrite:!0}),n=t.id||`amp_${E()}`,c={...t,id:n};if(typeof t.detail=="string"&&t.detail.trim()){const u=`details/${n}.md`,f=r.join(o.root,"details",`${n}.md`);try{s.mkdirSync(r.dirname(f),{recursive:!0}),s.writeFileSync(f,t.detail,"utf8"),c.detailRef=u}catch{}}delete c.detail;const l=H(o,c),a=N(c),i=JSON.stringify(a)+`
3
- `;if(s.mkdirSync(r.dirname(l),{recursive:!0}),s.appendFileSync(l,i,"utf8"),l!==o.sessions)try{s.mkdirSync(r.dirname(o.sessions),{recursive:!0}),s.appendFileSync(o.sessions,i,"utf8")}catch{}return a}function ie(e,t){const o=m(e);return r.join(o.root,"details",`${t}.md`)}function se(e,t){if(!t)return null;const o=t.detailRef||t.meta&&t.meta.detailRef;if(!o||typeof o!="string")return null;const n=m(e),c=r.isAbsolute(o)?o:r.join(n.root,o);try{return s.readFileSync(c,"utf8")}catch{return null}}function re(e,t){if(!t)return{removed:0,files:[]};const o=m(e),n=[o.sessions,o.globalFile,o.currentBranchFile,o.defaultBranchFile].filter(Boolean);try{if(s.existsSync(o.branchesDir))for(const i of s.readdirSync(o.branchesDir))i.endsWith(".jsonl")&&n.push(r.join(o.branchesDir,i))}catch{}const c=[...new Set(n)];let l=0;const a=[];for(const i of c){if(!s.existsSync(i))continue;let u;try{u=s.readFileSync(i,"utf8").split(`
4
- `)}catch{continue}const f=[];let h=0;for(const g of u){if(!g.trim())continue;let S;try{S=JSON.parse(g).id}catch{f.push(g);continue}if(S===t){h++,l++;continue}f.push(g)}h>0&&(s.writeFileSync(i,f.length?f.join(`
1
+ import*as i from"node:fs";import*as M from"node:os";import*as a from"node:path";import{findProjectRoot as R}from"../projectRoot.mjs";import{getBranchInfo as P}from"../git/branch.mjs";const T="1.0",L=new Set(["gotcha","decision","attempt","note","detection","pattern"]),W=new Set(["copilot","cursor","claude","windsurf","other"]);function Y(e){let t=a.basename(a.resolve(e));try{const o=a.join(e,".ai-memory","amp.json");if(i.existsSync(o)){const n=JSON.parse(i.readFileSync(o,"utf8"));n&&typeof n.project=="string"&&n.project.trim()&&(t=n.project.trim())}}catch{}return t.toLowerCase().replace(/[^a-z0-9_.\-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,64)||"unnamed-project"}function G(e,t){let o=process.env.INFERNOFLOW_GLOBAL_DIR;if(!o)try{const f=JSON.parse(i.readFileSync(a.join(t,"amp.json"),"utf8"));f&&typeof f.globalDir=="string"&&f.globalDir.trim()&&(o=f.globalDir.trim())}catch{}if(!o)return a.join(t,"global.jsonl");let n=o;n.startsWith("~")&&(n=a.join(M.homedir(),n.slice(1).replace(/^[\/\\]/,""))),a.isAbsolute(n)||(n=a.resolve(e,n));const c=Y(e);return a.join(n,c,"global.jsonl")}function m(e,t={}){const o=t.literal?a.resolve(e):R(e),n=a.join(o,".ai-memory"),c=a.join(o,"inferno"),f=t.forWrite||i.existsSync(n)||!i.existsSync(c),s=f?n:c,r=f,u=a.join(s,"branches"),l=P(o),h=a.join(u,`${l.currentSlug}.jsonl`),g=l.defaultSlug?a.join(u,`${l.defaultSlug}.jsonl`):null,S=U(o,s);return{root:s,projectRoot:o,isAmp:r,sessions:a.join(s,"sessions.jsonl"),config:a.join(s,r?"amp.json":"config.json"),handoff:a.join(s,r?"handoff.md":"HANDOFF.md"),globalFile:S,branchesDir:u,currentBranchFile:h,defaultBranchFile:g,branch:l}}function U(e,t){try{return G(e,t)}catch{return a.join(t,"global.jsonl")}}function v(e){const t=R(e),o=a.join(t,".ai-memory");return i.existsSync(o)||i.mkdirSync(o,{recursive:!0}),o}const B="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function w(){let e=Date.now(),t="";for(let n=0;n<10;n++)t=B[e%32]+t,e=Math.floor(e/32);let o="";for(let n=0;n<16;n++)o+=B[Math.floor(Math.random()*32)];return t+o}function E(e){const t={...e.meta||{}};let o=e.type||"note";L.has(o)||(t.subtype=o,o="note"),e.result&&(t.result=e.result),e.detailRef&&(t.detailRef=e.detailRef);let n;const c=e.agent;c&&W.has(c)?n=c:c&&(t.agent=c);const f=typeof e.ts=="number"?e.ts:e.ts?Date.parse(e.ts):Date.now(),s=e.confidence!=null?e.confidence:e.auto?.7:void 0,r={type:o,msg:e.summary||e.msg||"",ts:f,id:e.id||`amp_${w()}`};return e.file&&(r.file=e.file),e.line&&(r.line=e.line),e.function&&(r.function=e.function),e.tags&&e.tags.length&&(r.tags=e.tags),e.source&&(r.source=e.source),n&&(r.tool=n),e.session&&(r.session=e.session),s!=null&&(r.confidence=s),Object.keys(t).length&&(r.meta=t),r}function H(e){if(e.summary&&!e.msg)return e;const t=e.meta||{},o=t.subtype||e.type||"note",n={ts:e.ts,type:o,summary:e.msg||""};e.id&&(n.id=e.id),e.file&&(n.file=e.file),e.line&&(n.line=e.line),e.function&&(n.function=e.function),e.tags&&(n.tags=e.tags),e.source&&(n.source=e.source),e.tool&&(n.agent=e.tool),t.agent&&(n.agent=t.agent),t.result&&(n.result=t.result),t.detailRef&&(n.detailRef=t.detailRef),e.confidence!=null&&(n.confidence=e.confidence,e.confidence<1&&(n.auto=!0));const{subtype:c,agent:f,result:s,detailRef:r,...u}=t;return Object.keys(u).length&&(n.meta=u),n}function V(e){if(!e||!i.existsSync(e))return[];try{return i.readFileSync(e,"utf8").split(`
2
+ `).filter(Boolean).map(t=>{try{return JSON.parse(t)}catch{return null}}).filter(Boolean).map(H)}catch{return[]}}function ae(e){const t=m(e),o=new Set,n=[],c=[t.sessions,t.globalFile,t.defaultBranchFile,t.currentBranchFile],f=[...new Set(c.filter(Boolean))];for(const s of f)for(const r of V(s)){const u=r.id||`${r.ts}|${r.summary}`;o.has(u)||(o.add(u),n.push(r))}return n.sort((s,r)=>{const u=typeof s.ts=="number"?s.ts:Date.parse(s.ts||0),l=typeof r.ts=="number"?r.ts:Date.parse(r.ts||0);return u-l})}function z(e,t){return t.target==="global"?e.globalFile:t.target==="legacy"?e.sessions:t.target==="branch"?e.currentBranchFile:t.type==="preference"?e.globalFile:e.currentBranchFile}const J=new Set;function b(e){return a.join(e,"details.jsonl")}function q(e,t,o){const n=b(e);i.mkdirSync(a.dirname(n),{recursive:!0}),i.appendFileSync(n,JSON.stringify({id:t,body:o})+`
3
+ `,"utf8")}function K(e,t){if(!t)return null;let o;try{o=i.readFileSync(b(e),"utf8")}catch{return null}let n=null;for(const c of o.split(`
4
+ `))if(c.trim())try{const f=JSON.parse(c);f.id===t&&typeof f.body=="string"&&(n=f.body)}catch{}return n}function Q(e,t){const o=b(e);let n;try{n=i.readFileSync(o,"utf8")}catch{return!1}const c=[];let f=!1;for(const s of n.split(`
5
+ `))if(s.trim()){try{if(JSON.parse(s).id===t){f=!0;continue}}catch{}c.push(s)}return f&&i.writeFileSync(o,c.length?c.join(`
5
6
  `)+`
6
- `:"","utf8"),a.push(i))}try{const i=r.join(o.root,"details",`${t}.md`);s.existsSync(i)&&(s.unlinkSync(i),a.push(i))}catch{}return{removed:l,files:a}}const V=30,z=["note","attempt","detection"],w=new Set(["gotcha","decision","pattern"]);function q(e){const t=e&&e.config&&e.config.rotation&&typeof e.config.rotation=="object"?e.config.rotation:{};return{archiveAfterDays:Number.isInteger(t.archiveAfterDays)&&t.archiveAfterDays>0?t.archiveAfterDays:V,archivableTypes:Array.isArray(t.archivableTypes)&&t.archivableTypes.length?t.archivableTypes:z,auto:t.auto===!0}}function K(e){const t=[e.sessions,e.currentBranchFile,e.defaultBranchFile].filter(Boolean);try{if(s.existsSync(e.branchesDir))for(const o of s.readdirSync(e.branchesDir))o.endsWith(".jsonl")&&t.push(r.join(e.branchesDir,o))}catch{}return[...new Set(t)]}function Q(e,t){const o=new Date(t),n=o.getUTCFullYear(),c=String(o.getUTCMonth()+1).padStart(2,"0");return r.join(e,"archive",`sessions-${n}-${c}.jsonl`)}function ce(e,t={}){const o=m(e,{forWrite:!t.dryRun}),n=q(X(e)),c=Number.isFinite(t.maxAgeDays)?t.maxAgeDays:n.archiveAfterDays,l=t.archive!==!1,a=new Set((Array.isArray(t.types)&&t.types.length?t.types:n.archivableTypes).filter(p=>!w.has(p))),i=Date.now()-c*24*60*60*1e3,u=K(o),f={scanned:0,archived:0,kept:0,files:[],byType:{},dryRun:!!t.dryRun},h=new Map,g=new Set,S=new Set,R=new Set;for(const p of u){if(!s.existsSync(p))continue;let b;try{b=s.readFileSync(p,"utf8").split(`
7
- `)}catch{continue}const F=[];let v=0;for(const j of b){if(!j.trim())continue;let y;try{y=JSON.parse(j)}catch{F.push(j);continue}const d=y&&y.id,D=y&&typeof y.type=="string"?y.type:"note",A=typeof y.ts=="number"?y.ts:Date.parse(y.ts||0),I=Number.isFinite(A)&&A<i,$=Array.isArray(y.tags)&&y.tags.includes("bookmark"),C=a.has(D)&&!w.has(D)&&!$;if(d&&!S.has(d)&&(S.add(d),f.scanned++),d||f.scanned++,I&&C){if(v++,(!d||!g.has(d))&&(d&&g.add(d),f.archived++,f.byType[D]=(f.byType[D]||0)+1,l)){const k=Q(o.root,A);h.has(k)||h.set(k,[]),h.get(k).push(j)}}else d&&!R.has(d)&&(R.add(d),f.kept++),d||f.kept++,F.push(j)}v>0&&!t.dryRun?(s.writeFileSync(p,F.length?F.join(`
7
+ `:"","utf8"),f}function X(e){const t=a.join(e,"details");if(!i.existsSync(t))return 0;let o;try{o=i.readdirSync(t).filter(s=>s.endsWith(".md"))}catch{return 0}if(!o.length)return 0;const n=new Set;try{const s=i.readFileSync(b(e),"utf8");for(const r of s.split(`
8
+ `))if(r.trim())try{n.add(JSON.parse(r).id)}catch{}}catch{}let c="",f=0;for(const s of o){const r=s.replace(/\.md$/,"");if(n.has(r))continue;let u;try{u=i.readFileSync(a.join(t,s),"utf8")}catch{continue}c+=JSON.stringify({id:r,body:u})+`
9
+ `,f++}if(c)try{i.appendFileSync(b(e),c,"utf8")}catch{return 0}for(const s of o)try{i.unlinkSync(a.join(t,s))}catch{}try{i.rmdirSync(t)}catch{}return f}function fe(e,t){v(e);const o=m(e,{forWrite:!0});if(!J.has(o.root)){J.add(o.root);try{X(o.root)}catch{}}const n=t.id||`amp_${w()}`,c={...t,id:n};if(typeof t.detail=="string"&&t.detail.trim())try{q(o.root,n,t.detail),c.detailRef="details.jsonl"}catch{}delete c.detail;const f=z(o,c),s=E(c),r=JSON.stringify(s)+`
10
+ `;if(i.mkdirSync(a.dirname(f),{recursive:!0}),i.appendFileSync(f,r,"utf8"),f!==o.sessions)try{i.mkdirSync(a.dirname(o.sessions),{recursive:!0}),i.appendFileSync(o.sessions,r,"utf8")}catch{}return s}function le(e,t){const o=m(e);return a.join(o.root,"details",`${t}.md`)}function ue(e,t){if(!t)return null;const o=t.detailRef||t.meta&&t.meta.detailRef;if(!o)return null;const n=m(e),c=t.id||t.meta&&t.meta.id,f=K(n.root,c);if(f!=null)return f;if(typeof o=="string"&&o!=="details.jsonl"){const s=a.isAbsolute(o)?o:a.join(n.root,o);try{return i.readFileSync(s,"utf8")}catch{return null}}return null}function de(e,t){if(!t)return{removed:0,files:[]};const o=m(e),n=[o.sessions,o.globalFile,o.currentBranchFile,o.defaultBranchFile].filter(Boolean);try{if(i.existsSync(o.branchesDir))for(const r of i.readdirSync(o.branchesDir))r.endsWith(".jsonl")&&n.push(a.join(o.branchesDir,r))}catch{}const c=[...new Set(n)];let f=0;const s=[];for(const r of c){if(!i.existsSync(r))continue;let u;try{u=i.readFileSync(r,"utf8").split(`
11
+ `)}catch{continue}const l=[];let h=0;for(const g of u){if(!g.trim())continue;let S;try{S=JSON.parse(g).id}catch{l.push(g);continue}if(S===t){h++,f++;continue}l.push(g)}h>0&&(i.writeFileSync(r,l.length?l.join(`
8
12
  `)+`
9
- `:"","utf8"),f.files.push(p)):v>0&&f.files.push(p)}if(!t.dryRun&&l&&h.size>0)for(const[p,b]of h)try{s.mkdirSync(r.dirname(p),{recursive:!0}),s.appendFileSync(p,b.join(`
13
+ `:"","utf8"),s.push(r))}try{Q(o.root,t)}catch{}try{const r=a.join(o.root,"details",`${t}.md`);i.existsSync(r)&&(i.unlinkSync(r),s.push(r))}catch{}return{removed:f,files:s}}const Z=30,ee=["note","attempt","detection"],I=new Set(["gotcha","decision","pattern"]);function te(e){const t=e&&e.config&&e.config.rotation&&typeof e.config.rotation=="object"?e.config.rotation:{};return{archiveAfterDays:Number.isInteger(t.archiveAfterDays)&&t.archiveAfterDays>0?t.archiveAfterDays:Z,archivableTypes:Array.isArray(t.archivableTypes)&&t.archivableTypes.length?t.archivableTypes:ee,auto:t.auto===!0}}function ne(e){const t=[e.sessions,e.currentBranchFile,e.defaultBranchFile].filter(Boolean);try{if(i.existsSync(e.branchesDir))for(const o of i.readdirSync(e.branchesDir))o.endsWith(".jsonl")&&t.push(a.join(e.branchesDir,o))}catch{}return[...new Set(t)]}function oe(e,t){const o=new Date(t),n=o.getUTCFullYear(),c=String(o.getUTCMonth()+1).padStart(2,"0");return a.join(e,"archive",`sessions-${n}-${c}.jsonl`)}function ye(e,t={}){const o=m(e,{forWrite:!t.dryRun}),n=te(ie(e)),c=Number.isFinite(t.maxAgeDays)?t.maxAgeDays:n.archiveAfterDays,f=t.archive!==!1,s=new Set((Array.isArray(t.types)&&t.types.length?t.types:n.archivableTypes).filter(p=>!I.has(p))),r=Date.now()-c*24*60*60*1e3,u=ne(o),l={scanned:0,archived:0,kept:0,files:[],byType:{},dryRun:!!t.dryRun},h=new Map,g=new Set,S=new Set,N=new Set;for(const p of u){if(!i.existsSync(p))continue;let F;try{F=i.readFileSync(p,"utf8").split(`
14
+ `)}catch{continue}const D=[];let A=0;for(const j of F){if(!j.trim())continue;let y;try{y=JSON.parse(j)}catch{D.push(j);continue}const d=y&&y.id,x=y&&typeof y.type=="string"?y.type:"note",O=typeof y.ts=="number"?y.ts:Date.parse(y.ts||0),C=Number.isFinite(O)&&O<r,_=Array.isArray(y.tags)&&y.tags.includes("bookmark"),$=s.has(x)&&!I.has(x)&&!_;if(d&&!S.has(d)&&(S.add(d),l.scanned++),d||l.scanned++,C&&$){if(A++,(!d||!g.has(d))&&(d&&g.add(d),l.archived++,l.byType[x]=(l.byType[x]||0)+1,f)){const k=oe(o.root,O);h.has(k)||h.set(k,[]),h.get(k).push(j)}}else d&&!N.has(d)&&(N.add(d),l.kept++),d||l.kept++,D.push(j)}A>0&&!t.dryRun?(i.writeFileSync(p,D.length?D.join(`
10
15
  `)+`
11
- `,"utf8")}catch{}return f}function X(e){const{config:t}=m(e);try{return JSON.parse(s.readFileSync(t,"utf8"))}catch{return null}}function ae(e,t={}){x(e);const{config:o}=m(e,{forWrite:!0});if(s.existsSync(o))return!1;const n={amp:T,project:t.project||r.basename(e),stack:t.stack||{},config:{autoCapture:!0,maxEntries:1e3,rotationStrategy:"archive",inject:["all"],injection:{maxEntries:4,maxCommits:5,maxEntryChars:200,protocolStyle:"compact"},rotation:{archiveAfterDays:30,archivableTypes:["note","attempt","detection"],auto:!1},...t.config||{}}};return s.writeFileSync(o,JSON.stringify(n,null,2)+`
12
- `,"utf8"),!0}function fe(e,t={}){x(e);const{config:o}=m(e,{forWrite:!0});let n={};try{n=JSON.parse(s.readFileSync(o,"utf8"))}catch{n={}}(!n||typeof n!="object")&&(n={}),n.amp||(n.amp=T),n.project||(n.project=r.basename(e)),(!n.config||typeof n.config!="object")&&(n.config={});const c=n.config.injection&&typeof n.config.injection=="object"?n.config.injection:{};return n.config.injection={...c,...t},s.writeFileSync(o,JSON.stringify(n,null,2)+`
13
- `,"utf8"),n.config.injection}function le(e){const t=r.join(e,"inferno"),o=r.join(t,"sessions.jsonl");if(!s.existsSync(o))return{migrated:0,reason:"no legacy sessions.jsonl"};const n=r.join(e,".ai-memory"),c=r.join(n,"sessions.jsonl");if(s.existsSync(c))return{migrated:0,reason:".ai-memory/sessions.jsonl already exists"};x(e);const l=s.readFileSync(o,"utf8").split(`
14
- `).filter(Boolean);let a=0;for(const i of l)try{const u=JSON.parse(i),f=N(u);s.appendFileSync(c,JSON.stringify(f)+`
15
- `,"utf8"),a++}catch{}return s.writeFileSync(r.join(n,"MIGRATED.md"),`# Migrated from inferno/
16
+ `:"","utf8"),l.files.push(p)):A>0&&l.files.push(p)}if(!t.dryRun&&f&&h.size>0)for(const[p,F]of h)try{i.mkdirSync(a.dirname(p),{recursive:!0}),i.appendFileSync(p,F.join(`
17
+ `)+`
18
+ `,"utf8")}catch{}return l}function ie(e){const{config:t}=m(e);try{return JSON.parse(i.readFileSync(t,"utf8"))}catch{return null}}function he(e,t={}){v(e);const{config:o}=m(e,{forWrite:!0});if(i.existsSync(o))return!1;const n={amp:T,project:t.project||a.basename(e),stack:t.stack||{},config:{autoCapture:!0,maxEntries:1e3,rotationStrategy:"archive",inject:["all"],injection:{maxEntries:4,maxCommits:5,maxEntryChars:200,protocolStyle:"compact"},rotation:{archiveAfterDays:30,archivableTypes:["note","attempt","detection"],auto:!1},...t.config||{}}};return i.writeFileSync(o,JSON.stringify(n,null,2)+`
19
+ `,"utf8"),!0}function pe(e,t={}){v(e);const{config:o}=m(e,{forWrite:!0});let n={};try{n=JSON.parse(i.readFileSync(o,"utf8"))}catch{n={}}(!n||typeof n!="object")&&(n={}),n.amp||(n.amp=T),n.project||(n.project=a.basename(e)),(!n.config||typeof n.config!="object")&&(n.config={});const c=n.config.injection&&typeof n.config.injection=="object"?n.config.injection:{};return n.config.injection={...c,...t},i.writeFileSync(o,JSON.stringify(n,null,2)+`
20
+ `,"utf8"),n.config.injection}function ge(e){const t=a.join(e,"inferno"),o=a.join(t,"sessions.jsonl");if(!i.existsSync(o))return{migrated:0,reason:"no legacy sessions.jsonl"};const n=a.join(e,".ai-memory"),c=a.join(n,"sessions.jsonl");if(i.existsSync(c))return{migrated:0,reason:".ai-memory/sessions.jsonl already exists"};v(e);const f=i.readFileSync(o,"utf8").split(`
21
+ `).filter(Boolean);let s=0;for(const r of f)try{const u=JSON.parse(r),l=E(u);i.appendFileSync(c,JSON.stringify(l)+`
22
+ `,"utf8"),s++}catch{}return i.writeFileSync(a.join(n,"MIGRATED.md"),`# Migrated from inferno/
16
23
 
17
- Copied ${a} entries from inferno/sessions.jsonl on ${new Date().toISOString()}.
24
+ Copied ${s} entries from inferno/sessions.jsonl on ${new Date().toISOString()}.
18
25
 
19
26
  The original inferno/sessions.jsonl is untouched. You can delete it once you're confident the new layout works.
20
- `,"utf8"),{migrated:a,reason:"ok"}}export{T as AMP_VERSION,m as ampPaths,oe as appendEntry,re as deleteEntry,ie as detailPath,x as ensureAmpDir,G as fromAmp,E as generateULID,le as migrateLegacy,L as projectSlug,ce as pruneEntries,X as readConfig,se as readDetail,ne as readEntries,W as resolveGlobalFile,q as resolveRotationSettings,N as toAmp,fe as updateInjectionConfig,ae as writeDefaultConfig};
27
+ `,"utf8"),{migrated:s,reason:"ok"}}export{T as AMP_VERSION,m as ampPaths,fe as appendEntry,de as deleteEntry,le as detailPath,v as ensureAmpDir,H as fromAmp,w as generateULID,ge as migrateLegacy,Y as projectSlug,ye as pruneEntries,ie as readConfig,ue as readDetail,ae as readEntries,G as resolveGlobalFile,te as resolveRotationSettings,E as toAmp,pe as updateInjectionConfig,he as writeDefaultConfig};
@@ -1,11 +1,11 @@
1
- import*as n from"node:fs";import*as i from"node:path";import*as H from"node:readline";import{fileURLToPath as ye}from"node:url";import{header as ge,ok as w,warn as T,info as z,done as V,nextSteps as he,bold as J,cyan as f,yellow as G,gray as v}from"../ui/output.mjs";import{discoverProjectSignals as M,reviewCapabilitiesInteractive as we,writeAdoptionBaseline as ke,buildAdoptionReport as je,summarizeCapabilities as Se,buildSignalsReport as ve}from"./adopt.mjs";import{installCursorHooksArtifacts as be}from"../cursorHooksInstall.mjs";import{ensureAmpDir as Ce,appendEntry as X,writeDefaultConfig as xe}from"../amp/io.mjs";import{applyCleanTreePolicy as B}from"../cleanTree.mjs";import{installVsCodeCopilotHooksArtifacts as Oe}from"../vsCodeCopilotHooksInstall.mjs";import{writeInitRuleFiles as Z}from"../ruleFiles.mjs";import{autoSetupMcp as Q}from"./setup.mjs";const Ie=i.dirname(ye(import.meta.url));function W(){return i.resolve(Ie,"../../templates")}function L(e,o,t=""){return new Promise(s=>{const r=t?v(` (${t})`):"";e.question(` ${o}${r}: `,l=>{s(l.trim()||t)})})}function Y(e,...o){for(const t of o){const s=e.indexOf(t);if(s!==-1&&e[s+1]&&!e[s+1].startsWith("-"))return e[s+1]}return null}function U(e,o,t,s=!1){return n.existsSync(o)&&!t?(s||T("Skipped (exists): "+i.relative(process.cwd(),o)),!1):(n.mkdirSync(i.dirname(o),{recursive:!0}),n.copyFileSync(e,o),s||w("Created: "+f(i.relative(process.cwd(),o))),!0)}function Ae(e,o,t){n.mkdirSync(o,{recursive:!0});for(const s of n.readdirSync(e,{withFileTypes:!0})){const r=i.join(e,s.name),l=i.join(o,s.name);s.isDirectory()?Ae(r,l,t):U(r,l,t)}}function ee(e,o){const t=i.join(W(),"skills");if(!n.existsSync(t))return 0;let s=0;const r=(l,d)=>{n.mkdirSync(d,{recursive:!0});for(const y of n.readdirSync(l,{withFileTypes:!0})){const a=i.join(l,y.name),u=i.join(d,y.name);if(y.isDirectory()){r(a,u);continue}n.existsSync(u)&&!o||(n.mkdirSync(i.dirname(u),{recursive:!0}),n.copyFileSync(a,u),s++,w("Created: "+f(i.relative(e,u))))}};return r(t,i.join(e,".claude","skills")),s}function oe(e,o){const t=i.join(W(),"agents");if(!n.existsSync(t))return 0;let s=0;const r=(l,d)=>{n.mkdirSync(d,{recursive:!0});for(const y of n.readdirSync(l,{withFileTypes:!0})){const a=i.join(l,y.name),u=i.join(d,y.name);if(y.isDirectory()){r(a,u);continue}n.existsSync(u)&&!o||(n.mkdirSync(i.dirname(u),{recursive:!0}),n.copyFileSync(a,u),s++,w("Created: "+f(i.relative(e,u))))}};return r(t,i.join(e,".claude","agents")),s}function Te(e,o=!1){const t=i.join(e,"package.json");if(!n.existsSync(t))return;const s=JSON.parse(n.readFileSync(t,"utf8"));s.scripts=s.scripts||{};let r=!1;const l={"inferno:check":"infernoflow check","inferno:status":"infernoflow status","inferno:gate":"infernoflow doc-gate","inferno:impact":"infernoflow pr-impact --json","inferno:sync":"infernoflow sync --auto --json","inferno:run":'infernoflow run "sync check" --provider auto --json',"inferno:hooks":"node scripts/inferno-install-hooks.mjs"};for(const[d,y]of Object.entries(l))s.scripts[d]||(s.scripts[d]=y,r=!0);r&&(n.writeFileSync(t,JSON.stringify(s,null,2)+`
2
- `,"utf8"),o||w("Updated "+f("package.json")+" scripts"))}const ne="# --- infernoflow (developer-local AI memory; do not commit) ---",Ee="# --- /infernoflow ---",te=[".ai-memory/",".cursorrules","CLAUDE.md",".github/copilot-instructions.md"];function oo(e,{silent:o=!1}={}){const t=i.join(e,".gitignore");let s="";if(n.existsSync(t)&&(s=n.readFileSync(t,"utf8")),s.includes(ne))return!1;const r=["",ne,"# Memory is per-developer, not per-branch. These files travel with you, not with git.",...te,Ee,""].join(`
1
+ import*as n from"node:fs";import*as i from"node:path";import*as H from"node:readline";import{fileURLToPath as ge}from"node:url";import{header as he,ok as k,warn as T,info as z,done as X,nextSteps as we,bold as J,cyan as p,yellow as F,gray as j}from"../ui/output.mjs";import{discoverProjectSignals as M,reviewCapabilitiesInteractive as ke,writeAdoptionBaseline as je,buildAdoptionReport as Se,summarizeCapabilities as ve,buildSignalsReport as Ce}from"./adopt.mjs";import{installCursorHooksArtifacts as be}from"../cursorHooksInstall.mjs";import{ensureAmpDir as xe,appendEntry as B,writeDefaultConfig as Oe}from"../amp/io.mjs";import{applyCleanTreePolicy as Z}from"../cleanTree.mjs";import{installVsCodeCopilotHooksArtifacts as Ie}from"../vsCodeCopilotHooksInstall.mjs";import{writeInitRuleFiles as Q}from"../ruleFiles.mjs";import{autoSetupMcp as ee}from"./setup.mjs";const Ae=i.dirname(ge(import.meta.url));function Y(){return i.resolve(Ae,"../../templates")}function L(e,o,t=""){return new Promise(s=>{const r=t?j(` (${t})`):"";e.question(` ${o}${r}: `,l=>{s(l.trim()||t)})})}function q(e,...o){for(const t of o){const s=e.indexOf(t);if(s!==-1&&e[s+1]&&!e[s+1].startsWith("-"))return e[s+1]}return null}function U(e,o,t,s=!1){return n.existsSync(o)&&!t?(s||T("Skipped (exists): "+i.relative(process.cwd(),o)),!1):(n.mkdirSync(i.dirname(o),{recursive:!0}),n.copyFileSync(e,o),s||k("Created: "+p(i.relative(process.cwd(),o))),!0)}function Te(e,o,t){n.mkdirSync(o,{recursive:!0});for(const s of n.readdirSync(e,{withFileTypes:!0})){const r=i.join(e,s.name),l=i.join(o,s.name);s.isDirectory()?Te(r,l,t):U(r,l,t)}}function oe(e,o){const t=i.join(Y(),"skills");if(!n.existsSync(t))return 0;let s=0;const r=(l,d)=>{n.mkdirSync(d,{recursive:!0});for(const g of n.readdirSync(l,{withFileTypes:!0})){const a=i.join(l,g.name),u=i.join(d,g.name);if(g.isDirectory()){r(a,u);continue}n.existsSync(u)&&!o||(n.mkdirSync(i.dirname(u),{recursive:!0}),n.copyFileSync(a,u),s++,k("Created: "+p(i.relative(e,u))))}};return r(t,i.join(e,".claude","skills")),s}function ne(e,o){const t=i.join(Y(),"agents");if(!n.existsSync(t))return 0;let s=0;const r=(l,d)=>{n.mkdirSync(d,{recursive:!0});for(const g of n.readdirSync(l,{withFileTypes:!0})){const a=i.join(l,g.name),u=i.join(d,g.name);if(g.isDirectory()){r(a,u);continue}n.existsSync(u)&&!o||(n.mkdirSync(i.dirname(u),{recursive:!0}),n.copyFileSync(a,u),s++,k("Created: "+p(i.relative(e,u))))}};return r(t,i.join(e,".claude","agents")),s}function te(){console.log(""),console.log(" "+F("\u21BB Restart Claude Code / Cursor")+j(" to activate the infernoflow skill + memory-keeper agent.")),console.log(" "+j("Editors only load newly-added .claude/ skills & agents on restart. Verify with ")+p("/skills")+j(" and ")+p("@agent-")+j("."))}function Ee(e,o=!1){const t=i.join(e,"package.json");if(!n.existsSync(t))return;const s=JSON.parse(n.readFileSync(t,"utf8"));s.scripts=s.scripts||{};let r=!1;const l={"inferno:check":"infernoflow check","inferno:status":"infernoflow status","inferno:gate":"infernoflow doc-gate","inferno:impact":"infernoflow pr-impact --json","inferno:sync":"infernoflow sync --auto --json","inferno:run":'infernoflow run "sync check" --provider auto --json',"inferno:hooks":"node scripts/inferno-install-hooks.mjs"};for(const[d,g]of Object.entries(l))s.scripts[d]||(s.scripts[d]=g,r=!0);r&&(n.writeFileSync(t,JSON.stringify(s,null,2)+`
2
+ `,"utf8"),o||k("Updated "+p("package.json")+" scripts"))}const ie="# --- infernoflow (developer-local AI memory; do not commit) ---",Pe="# --- /infernoflow ---",se=[".ai-memory/",".cursorrules","CLAUDE.md",".github/copilot-instructions.md"];function no(e,{silent:o=!1}={}){const t=i.join(e,".gitignore");let s="";if(n.existsSync(t)&&(s=n.readFileSync(t,"utf8")),s.includes(ie))return!1;const r=["",ie,"# Memory is per-developer, not per-branch. These files travel with you, not with git.",...se,Pe,""].join(`
3
3
  `),l=s.length===0||s.endsWith(`
4
4
  `)?"":`
5
- `;if(n.writeFileSync(t,s+l+r,"utf8"),!o){w("Updated "+f(".gitignore")+v(" \u2014 added entries:"));for(const d of te)console.log(v(" + ")+f(d));console.log(v(" Memory + AI rule files are now per-developer (won't move with branches).")),console.log(v(" To commit them instead: delete the `")+f("# --- infernoflow ---")+v("` block in .gitignore."))}return!0}function K(e){const o=i.join(e,"package.json");if(n.existsSync(o))try{const t=JSON.parse(n.readFileSync(o,"utf8"));if(t.name)return t.name.replace(/[^a-z0-9_-]/gi,"_")}catch{}return i.basename(e)}function Pe(e,o,t){const s={policyId:o,policyVersion:1,capabilities:t,rules:{docsRequiredOnCapabilityChange:!0,requireScenarioForEachCapability:!0,requireChangelogOnCapabilityChange:!0}};n.writeFileSync(e,JSON.stringify(s,null,2)+`
6
- `)}function Fe(e,o){const t={schemaVersion:1,capabilities:o.map(s=>({id:s,title:s.replace(/([A-Z])/g," $1").trim(),since:"0.1.0"}))};n.writeFileSync(e,JSON.stringify(t,null,2)+`
7
- `)}function Ne(e,o){n.mkdirSync(e,{recursive:!0});const t={scenarioId:"happy_path",description:"Basic happy-path flow covering all capabilities",capabilitiesCovered:o,steps:o.map(s=>({action:s,expect:`${s} works as expected`}))};n.writeFileSync(i.join(e,"happy_path.json"),JSON.stringify(t,null,2)+`
8
- `)}function ie(e,o){const t=`# Changelog \u2014 ${o}
5
+ `;if(n.writeFileSync(t,s+l+r,"utf8"),!o){k("Updated "+p(".gitignore")+j(" \u2014 added entries:"));for(const d of se)console.log(j(" + ")+p(d));console.log(j(" Memory + AI rule files are now per-developer (won't move with branches).")),console.log(j(" To commit them instead: delete the `")+p("# --- infernoflow ---")+j("` block in .gitignore."))}return!0}function K(e){const o=i.join(e,"package.json");if(n.existsSync(o))try{const t=JSON.parse(n.readFileSync(o,"utf8"));if(t.name)return t.name.replace(/[^a-z0-9_-]/gi,"_")}catch{}return i.basename(e)}function Fe(e,o,t){const s={policyId:o,policyVersion:1,capabilities:t,rules:{docsRequiredOnCapabilityChange:!0,requireScenarioForEachCapability:!0,requireChangelogOnCapabilityChange:!0}};n.writeFileSync(e,JSON.stringify(s,null,2)+`
6
+ `)}function Ne(e,o){const t={schemaVersion:1,capabilities:o.map(s=>({id:s,title:s.replace(/([A-Z])/g," $1").trim(),since:"0.1.0"}))};n.writeFileSync(e,JSON.stringify(t,null,2)+`
7
+ `)}function Re(e,o){n.mkdirSync(e,{recursive:!0});const t={scenarioId:"happy_path",description:"Basic happy-path flow covering all capabilities",capabilitiesCovered:o,steps:o.map(s=>({action:s,expect:`${s} works as expected`}))};n.writeFileSync(i.join(e,"happy_path.json"),JSON.stringify(t,null,2)+`
8
+ `)}function re(e,o){const t=`# Changelog \u2014 ${o}
9
9
 
10
10
  ## Unreleased
11
11
 
@@ -14,41 +14,41 @@ import*as n from"node:fs";import*as i from"node:path";import*as H from"node:read
14
14
  ## 0.1.0 \u2014 Initial release
15
15
 
16
16
  - Project initialized with infernoflow
17
- `;n.writeFileSync(e,t)}async function Re(e,o){const{bold:t,cyan:s,gray:r,green:l,yellow:d,red:y}=await import("../ui/output.mjs");console.log(`
17
+ `;n.writeFileSync(e,t)}async function _e(e,o){const{bold:t,cyan:s,gray:r,green:l,yellow:d,red:g}=await import("../ui/output.mjs");console.log(`
18
18
  `+t("\u{1F525} infernoflow init --lite")),console.log(" "+"\u2500".repeat(50)+`
19
19
  `),console.log(r(" Lite mode: 3 files, no scripts, no workflows, no hooks.")),console.log(r(" Use `infernoflow upgrade` later to expand to the full setup.\n"));const a=i.join(e,"inferno");n.existsSync(a)&&!o&&(console.log(d(` \u26A0 inferno/ already exists. Use --force to overwrite.
20
- `)),process.exit(0)),n.mkdirSync(a,{recursive:!0});const u=K(e);let S="";if(!process.argv.includes("--yes")&&!process.argv.includes("-y")&&process.stdin.isTTY){const E=H.createInterface({input:process.stdin,output:process.stdout});S=await new Promise(O=>{E.question(r(" What does this project do? (one line, Enter to skip): "),I=>{E.close(),O(I.trim())})})}const x={policyId:u,policyVersion:1,lite:!0,capabilities:[],intent:S||void 0};n.writeFileSync(i.join(a,"contract.json"),JSON.stringify(x,null,2)+`
20
+ `)),process.exit(0)),n.mkdirSync(a,{recursive:!0});const u=K(e);let v="";if(!process.argv.includes("--yes")&&!process.argv.includes("-y")&&process.stdin.isTTY){const E=H.createInterface({input:process.stdin,output:process.stdout});v=await new Promise(x=>{E.question(r(" What does this project do? (one line, Enter to skip): "),O=>{E.close(),x(O.trim())})})}const b={policyId:u,policyVersion:1,lite:!0,capabilities:[],intent:v||void 0};n.writeFileSync(i.join(a,"contract.json"),JSON.stringify(b,null,2)+`
21
21
  `),n.writeFileSync(i.join(a,"capabilities.json"),JSON.stringify([],null,2)+`
22
- `),n.writeFileSync(i.join(a,"sessions.jsonl"),"","utf8"),n.writeFileSync(i.join(a,".lite"),"1","utf8"),console.log(l(" \u2714 Created inferno/contract.json")),console.log(l(" \u2714 Created inferno/capabilities.json")),console.log(l(" \u2714 Created inferno/sessions.jsonl")),console.log(),console.log(" "+t("Ready. Start using it:")),console.log(" "+s("infernoflow log")+r(` "what you're building" --type note`)),console.log(" "+s("infernoflow context")+r(" \u2014 generate AI context to paste")),console.log(" "+s("infernoflow init --mode full")+r(" \u2014 expand to full setup when you need it")),console.log()}const _e=/^(?:node|npm|npx|yarn|pnpm|bun|git|cd|mkdir|rm|ls|cat|echo|type|dir|copy|del|move|python|python3|pip|go|cargo|java|gradle|mvn|docker|kubectl|curl|wget|ssh|scp|chmod|chown|sudo|brew|apt|yum)\b/i,$e=/\s(?:&&|\|\||>>|>|<<|<|\|)\s/,De=/(?:^|\s)[A-Za-z]:\\|\.\.[\\\/]|[\\\/]bin[\\\/]/;function Ge(e){if(!e)return{kind:"empty"};const o=e.replace(/^\s*[>$#]\s+/,"").trim();return o?/[\r\n]/.test(o)?{kind:"multiline",value:o}:_e.test(o)?{kind:"command",value:o}:$e.test(o)?{kind:"command",value:o}:De.test(o)?{kind:"command",value:o}:o.length<3?{kind:"tooShort",value:o}:{kind:"ok",value:o}:{kind:"empty"}}async function Le({yes:e}){if(e||!process.stdin.isTTY)return"";const{gray:o,yellow:t,cyan:s}=await import("../ui/output.mjs"),r=" "+o(`What should the next AI agent know about this project?
23
- > `),l=d=>new Promise(y=>{const a=H.createInterface({input:process.stdin,output:process.stdout});let u=!1;a.on("SIGINT",()=>{u=!0,a.close(),y(null)}),a.on("close",()=>{u&&y(null)}),a.question(d,S=>{a.close(),y(S)})});for(let d=0;d<2;d++){const y=await l(d===0?r:" "+o("> "));if(y===null)return console.log(),"";const a=Ge(y);if(a.kind==="ok")return a.value;if(a.kind==="empty")return"";if(a.kind==="command"){console.log(" "+t("\u26A0")+" That looks like a shell command, not a memory."),console.log(" "+o(" Try a short note like: ")+s('"API returns 202 not 200 on async upload"'));continue}if(a.kind==="multiline"){console.log(" "+t("\u26A0")+" Multi-line paste detected \u2014 log a single gotcha at a time."),console.log(" "+o(" Try one short sentence:"));continue}if(a.kind==="tooShort"){console.log(" "+t("\u26A0")+" Too short to be useful as a memory. Skip with Enter, or try again:");continue}}return""}async function He(e,o,t){const{bold:s,cyan:r,gray:l,green:d,yellow:y}=await import("../ui/output.mjs"),a=i.join(e,".ai-memory"),u=i.join(e,"inferno"),S=i.join(a,"sessions.jsonl"),x=n.existsSync(S),E=n.existsSync(i.join(u,"sessions.jsonl"));if((x||E)&&!o){const k=n.existsSync(a)?".ai-memory/":"inferno/ (legacy)";console.log(`
22
+ `),n.writeFileSync(i.join(a,"sessions.jsonl"),"","utf8"),n.writeFileSync(i.join(a,".lite"),"1","utf8"),console.log(l(" \u2714 Created inferno/contract.json")),console.log(l(" \u2714 Created inferno/capabilities.json")),console.log(l(" \u2714 Created inferno/sessions.jsonl")),console.log(),console.log(" "+t("Ready. Start using it:")),console.log(" "+s("infernoflow log")+r(` "what you're building" --type note`)),console.log(" "+s("infernoflow context")+r(" \u2014 generate AI context to paste")),console.log(" "+s("infernoflow init --mode full")+r(" \u2014 expand to full setup when you need it")),console.log()}const $e=/^(?:node|npm|npx|yarn|pnpm|bun|git|cd|mkdir|rm|ls|cat|echo|type|dir|copy|del|move|python|python3|pip|go|cargo|java|gradle|mvn|docker|kubectl|curl|wget|ssh|scp|chmod|chown|sudo|brew|apt|yum)\b/i,De=/\s(?:&&|\|\||>>|>|<<|<|\|)\s/,Ge=/(?:^|\s)[A-Za-z]:\\|\.\.[\\\/]|[\\\/]bin[\\\/]/;function Le(e){if(!e)return{kind:"empty"};const o=e.replace(/^\s*[>$#]\s+/,"").trim();return o?/[\r\n]/.test(o)?{kind:"multiline",value:o}:$e.test(o)?{kind:"command",value:o}:De.test(o)?{kind:"command",value:o}:Ge.test(o)?{kind:"command",value:o}:o.length<3?{kind:"tooShort",value:o}:{kind:"ok",value:o}:{kind:"empty"}}async function He({yes:e}){if(e||!process.stdin.isTTY)return"";const{gray:o,yellow:t,cyan:s}=await import("../ui/output.mjs"),r=" "+o(`What should the next AI agent know about this project?
23
+ > `),l=d=>new Promise(g=>{const a=H.createInterface({input:process.stdin,output:process.stdout});let u=!1;a.on("SIGINT",()=>{u=!0,a.close(),g(null)}),a.on("close",()=>{u&&g(null)}),a.question(d,v=>{a.close(),g(v)})});for(let d=0;d<2;d++){const g=await l(d===0?r:" "+o("> "));if(g===null)return console.log(),"";const a=Le(g);if(a.kind==="ok")return a.value;if(a.kind==="empty")return"";if(a.kind==="command"){console.log(" "+t("\u26A0")+" That looks like a shell command, not a memory."),console.log(" "+o(" Try a short note like: ")+s('"API returns 202 not 200 on async upload"'));continue}if(a.kind==="multiline"){console.log(" "+t("\u26A0")+" Multi-line paste detected \u2014 log a single gotcha at a time."),console.log(" "+o(" Try one short sentence:"));continue}if(a.kind==="tooShort"){console.log(" "+t("\u26A0")+" Too short to be useful as a memory. Skip with Enter, or try again:");continue}}return""}async function Je(e,o,t){const{bold:s,cyan:r,gray:l,green:d,yellow:g}=await import("../ui/output.mjs"),a=i.join(e,".ai-memory"),u=i.join(e,"inferno"),v=i.join(a,"sessions.jsonl"),b=n.existsSync(v),E=n.existsSync(i.join(u,"sessions.jsonl"));if((b||E)&&!o){const h=n.existsSync(a)?".ai-memory/":"inferno/ (legacy)";console.log(`
24
24
  `+s("\u{1F525} infernoflow")+l(` \u2014 already set up
25
- `)),console.log(" "+d("\u2714")+" "+k+` found
26
- `),B(e),Z(e);try{Q(e,{silent:!0})}catch{}ee(e,o),oe(e,o),console.log(" Quick commands:"),console.log(" "+r('infernoflow log "..."')+l(" \u2014 remember something")),console.log(" "+r("infernoflow switch")+l(" \u2014 handoff to next AI")),console.log(" "+r("infernoflow recap")+l(` \u2014 session summary
25
+ `)),console.log(" "+d("\u2714")+" "+h+` found
26
+ `),Z(e),Q(e);try{ee(e,{silent:!0})}catch{}const W=oe(e,o),I=ne(e,o);W+I>0&&te(),console.log(" Quick commands:"),console.log(" "+r('infernoflow log "..."')+l(" \u2014 remember something")),console.log(" "+r("infernoflow switch")+l(" \u2014 handoff to next AI")),console.log(" "+r("infernoflow recap")+l(` \u2014 session summary
27
27
  `)),n.existsSync(a)||console.log(l(" Tip: run ")+r("infernoflow amp migrate")+l(` to move legacy memory into .ai-memory/
28
28
  `)),console.log(l(` For contracts & CI gates: infernoflow init --mode full
29
- `));return}const I=K(e);console.log(`
29
+ `));return}const O=K(e);console.log(`
30
30
  `+s("\u{1F525} infernoflow")+l(` \u2014 let's get you set up (30 seconds)
31
- `)),console.log(" Detected: "+r(I)+`
32
- `),Ce(e),n.existsSync(S)||n.writeFileSync(S,"","utf8"),xe(e,{project:I,config:{autoCapture:!0}}),B(e);const N=Z(e);for(const k of N)"created"in k&&(k.created||k.updated)&&w((k.created?"Created: ":"Updated: ")+r(k.rel));ee(e,o),oe(e,o);try{Q(e)}catch(k){T("MCP auto-setup skipped: "+k.message)}const P=await Le({yes:t});P?(X(e,{ts:new Date().toISOString(),agent:"user",type:"gotcha",summary:P,source:"init"}),console.log(`
33
- `+d("\u2714")+" First gotcha logged!")):t&&X(e,{ts:new Date().toISOString(),agent:"infernoflow",type:"note",summary:"infernoflow init complete \u2014 memory loop is live. Run `infernoflow status` to verify.",source:"init"}),console.log(`
31
+ `)),console.log(" Detected: "+r(O)+`
32
+ `),xe(e),n.existsSync(v)||n.writeFileSync(v,"","utf8"),Oe(e,{project:O,config:{autoCapture:!0}}),Z(e);const N=Q(e);for(const h of N)"created"in h&&(h.created||h.updated)&&k((h.created?"Created: ":"Updated: ")+r(h.rel));const R=oe(e,o),_=ne(e,o);R+_>0&&te();try{ee(e)}catch(h){T("MCP auto-setup skipped: "+h.message)}const y=await He({yes:t});y?(B(e,{ts:new Date().toISOString(),agent:"user",type:"gotcha",summary:y,source:"init"}),console.log(`
33
+ `+d("\u2714")+" First gotcha logged!")):t&&B(e,{ts:new Date().toISOString(),agent:"infernoflow",type:"note",summary:"infernoflow init complete \u2014 memory loop is live. Run `infernoflow status` to verify.",source:"init"}),console.log(`
34
34
  `+d("\u2714")+` You're set up. Quick commands:
35
35
  `),console.log(" "+r("infernoflow status")+l(" \u2014 verify the loop end-to-end")),console.log(" "+r('infernoflow log "..."')+l(" \u2014 remember something")),console.log(" "+r("infernoflow switch")+l(" \u2014 generate handoff for next AI")),console.log(" "+r("infernoflow recap")+l(` \u2014 session summary
36
36
  `)),console.log(l(` Tip: infernoflow switch --copy puts the handoff on your clipboard.
37
- `))}function Je(){console.log(["infernoflow init \u2014 set up persistent AI memory in this project","","Usage:"," infernoflow init Memory-first setup (default, 60 sec)"," infernoflow init --lite Minimal: 3 files, no scripts, no workflows"," infernoflow init --mode full Contracts + CI gates (advanced)"," infernoflow init --adopt Detect existing capabilities from code"," infernoflow init --template <name> Start from a project template","","Options:"," -y, --yes Non-interactive; accept all defaults"," -f, --force Overwrite existing inferno/ or .ai-memory/"," --cursor-hooks Also install Cursor IDE hooks (advanced)"," --vscode-copilot-hooks Also install VS Code Copilot hooks (advanced)"," --lang <lang> Override language detection (with --adopt)"," --framework <name> Override framework detection (with --adopt)"," --project-type <type> Override project-type detection (with --adopt)"," -h, --help Show this help","","Default flow writes .ai-memory/ + AI rule files + MCP server config, then asks","for one first gotcha. Press Enter to skip \u2014 never blocks."].join(`
38
- `))}async function no(e){if(e.includes("--help")||e.includes("-h")){Je();return}const o=process.cwd(),t=e.includes("--force")||e.includes("-f"),s=e.includes("--yes")||e.includes("-y"),r=e.includes("--adopt"),l=e.find(p=>p.startsWith("--mode="))?.split("=")[1]||(e.indexOf("--mode")!==-1?e[e.indexOf("--mode")+1]:null),d=l==="full"||l==="contract",y=r||e.includes("--template")||e.includes("--cursor-hooks")||e.includes("--vscode-copilot-hooks")||e.includes("--lite");if(!d&&!y){await He(o,t,s);return}if(e.includes("--lite")){await Re(o,t);return}const a=e.indexOf("--template"),u=a!==-1?e[a+1]:null;if(u){let p;try{p=await import("../templates/index.mjs")}catch{}const c=p?.getTemplate(u);if(!c){const m=p?p.listTemplates().map(A=>A.name).join(", "):"rest-api, nextjs, cli, graphql, monorepo";T(`Unknown template: ${u}. Available: ${m}`),process.exit(1)}const h=i.join(o,"inferno"),F=i.join(h,"scenarios");n.existsSync(h)||n.mkdirSync(h,{recursive:!0}),n.existsSync(F)||n.mkdirSync(F,{recursive:!0});const D=K(o),j=c.capabilities;n.writeFileSync(i.join(h,"contract.json"),JSON.stringify({policyId:D,policyVersion:1,capabilities:j.map(m=>m.id)},null,2)+`
39
- `),n.writeFileSync(i.join(h,"capabilities.json"),JSON.stringify({capabilities:j.map(m=>({id:m.id,description:m.description,since:new Date().toISOString().slice(0,10),source:`template:${u}`}))},null,2)+`
40
- `);for(const m of j)n.writeFileSync(i.join(F,`${m.id}.json`),JSON.stringify({id:`${m.id}-happy-path`,capability:m.id,description:`Happy path for ${m.description||m.id}`,steps:[{action:"invoke",target:m.id,input:{}},{action:"assert",field:"status",value:"success"}],capabilitiesCovered:[m.id]},null,2)+`
41
- `);ie(i.join(h,"CHANGELOG.md"),D),n.writeFileSync(i.join(h,"CONTEXT.md"),`# ${D} \u2014 infernoflow context
37
+ `))}function Me(){console.log(["infernoflow init \u2014 set up persistent AI memory in this project","","Usage:"," infernoflow init Memory-first setup (default, 60 sec)"," infernoflow init --lite Minimal: 3 files, no scripts, no workflows"," infernoflow init --mode full Contracts + CI gates (advanced)"," infernoflow init --adopt Detect existing capabilities from code"," infernoflow init --template <name> Start from a project template","","Options:"," -y, --yes Non-interactive; accept all defaults"," -f, --force Overwrite existing inferno/ or .ai-memory/"," --cursor-hooks Also install Cursor IDE hooks (advanced)"," --vscode-copilot-hooks Also install VS Code Copilot hooks (advanced)"," --lang <lang> Override language detection (with --adopt)"," --framework <name> Override framework detection (with --adopt)"," --project-type <type> Override project-type detection (with --adopt)"," -h, --help Show this help","","Default flow writes .ai-memory/ + AI rule files + MCP server config, then asks","for one first gotcha. Press Enter to skip \u2014 never blocks."].join(`
38
+ `))}async function to(e){if(e.includes("--help")||e.includes("-h")){Me();return}const o=process.cwd(),t=e.includes("--force")||e.includes("-f"),s=e.includes("--yes")||e.includes("-y"),r=e.includes("--adopt"),l=e.find(f=>f.startsWith("--mode="))?.split("=")[1]||(e.indexOf("--mode")!==-1?e[e.indexOf("--mode")+1]:null),d=l==="full"||l==="contract",g=r||e.includes("--template")||e.includes("--cursor-hooks")||e.includes("--vscode-copilot-hooks")||e.includes("--lite");if(!d&&!g){await Je(o,t,s);return}if(e.includes("--lite")){await _e(o,t);return}const a=e.indexOf("--template"),u=a!==-1?e[a+1]:null;if(u){let f;try{f=await import("../templates/index.mjs")}catch{}const c=f?.getTemplate(u);if(!c){const m=f?f.listTemplates().map(A=>A.name).join(", "):"rest-api, nextjs, cli, graphql, monorepo";T(`Unknown template: ${u}. Available: ${m}`),process.exit(1)}const w=i.join(o,"inferno"),P=i.join(w,"scenarios");n.existsSync(w)||n.mkdirSync(w,{recursive:!0}),n.existsSync(P)||n.mkdirSync(P,{recursive:!0});const G=K(o),S=c.capabilities;n.writeFileSync(i.join(w,"contract.json"),JSON.stringify({policyId:G,policyVersion:1,capabilities:S.map(m=>m.id)},null,2)+`
39
+ `),n.writeFileSync(i.join(w,"capabilities.json"),JSON.stringify({capabilities:S.map(m=>({id:m.id,description:m.description,since:new Date().toISOString().slice(0,10),source:`template:${u}`}))},null,2)+`
40
+ `);for(const m of S)n.writeFileSync(i.join(P,`${m.id}.json`),JSON.stringify({id:`${m.id}-happy-path`,capability:m.id,description:`Happy path for ${m.description||m.id}`,steps:[{action:"invoke",target:m.id,input:{}},{action:"assert",field:"status",value:"success"}],capabilitiesCovered:[m.id]},null,2)+`
41
+ `);re(i.join(w,"CHANGELOG.md"),G),n.writeFileSync(i.join(w,"CONTEXT.md"),`# ${G} \u2014 infernoflow context
42
42
 
43
43
  > Template: ${u} \u2014 ${c.description}
44
44
 
45
45
  ## Hint
46
46
  ${c.contextHint}
47
47
 
48
- ## Capabilities (${j.length})
49
- ${j.map(m=>`- \`${m.id}\`: ${m.description}`).join(`
48
+ ## Capabilities (${S.length})
49
+ ${S.map(m=>`- \`${m.id}\`: ${m.description}`).join(`
50
50
  `)}
51
- `),c.scripts&&(z("Suggested package.json scripts for this template:"),Object.entries(c.scripts).forEach(([m,A])=>console.log(` ${J(m)}: ${v(A)}`)),console.log()),V(`Initialised from template ${J(f(u))} \u2014 ${J(String(j.length))} capabilities`),console.log(),z(`Run ${f("infernoflow vibe")} to start vibe coding mode`),console.log();return}const S=e.includes("--cursor-hooks"),x=e.includes("--vscode-copilot-hooks"),E=e.includes("--report-json"),O=e.includes("--report-json-only"),I=e.includes("--report-human-only"),N=Y(e,"--lang"),P=Y(e,"--framework"),k=Y(e,"--project-type"),g=O;O&&I&&(console.error("Error: --report-json-only and --report-human-only cannot be used together."),process.exit(1)),g||ge("init");const b=i.join(o,"inferno"),se=i.join(o,".github","workflows");n.existsSync(b)&&!t&&(g&&(console.log(JSON.stringify({ok:!1,error:"inferno_exists",hint:"Use --force to overwrite"},null,2)),process.exit(1)),T("inferno/ already exists. Use --force to overwrite."),console.log(),process.exit(0));const R=K(o),q="CreateTask, ReadTasks, UpdateTask, ToggleComplete, DeleteTask";let _=R,C=q.split(",").map(p=>p.trim());if(r||d){let c=M(o,{language:N||void 0,framework:P||void 0,projectType:k||void 0});if(!s&&!O){const j=H.createInterface({input:process.stdin,output:process.stdout}),m=c.developmentProfile||{},A=m.detected||{};console.log(v(` Review inferred development stack (press Enter to accept detected values)
52
- `));const de=await L(j,"Language",m.language||A.language||"unknown"),ue=await L(j,"Framework",m.framework||A.framework||"unknown"),me=await L(j,"Project type",m.projectType||A.projectType||"unknown");j.close(),c=M(o,{language:de,framework:ue,projectType:me})}const h=c.capabilities,F=Se(h);O?console.log(JSON.stringify({mode:"adopt",policyId:R,inferredCapabilities:F,components:c.components,displayFields:c.displayFields,externalLibraries:c.externalLibraries,uiLayout:c.uiLayout,styling:c.styling,developmentProfile:c.developmentProfile,apiCalls:c.apiCalls},null,2)):(console.log(),console.log(v(je(h))),console.log(),console.log(v(ve(c))),console.log(),E&&!I&&(console.log(JSON.stringify({mode:"adopt",policyId:R,inferredCapabilities:F,components:c.components,displayFields:c.displayFields,externalLibraries:c.externalLibraries,uiLayout:c.uiLayout,styling:c.styling,developmentProfile:c.developmentProfile,apiCalls:c.apiCalls},null,2)),console.log()));const D=await we(h,s);_=R,C=D.map(j=>j.id),C.length===0&&(C=["ExampleCapability"],g||T("No capabilities detected from the code \u2014 wrote a placeholder. Edit "+f("inferno/capabilities.json")+" to describe what this project does."))}else if(!s){const p=H.createInterface({input:process.stdin,output:process.stdout});console.log(v(` Press Enter to accept defaults
53
- `)),_=await L(p,"Project / policy name",R),C=(await L(p,"Capabilities (comma-separated)",q)).split(",").map(h=>h.trim()).filter(Boolean),p.close(),console.log()}if(n.mkdirSync(b,{recursive:!0}),r){const p=C.map(h=>({id:h,title:h.replace(/([A-Z])/g," $1").trim()})),c=M(o,{language:N||void 0,framework:P||void 0,projectType:k||void 0});ke(b,_,p,c),g||(w("Created: "+f("inferno/contract.json")),w("Created: "+f("inferno/capabilities.json")),w("Created: "+f("inferno/scenarios/adoption_baseline.json")),w("Created: "+f("inferno/adoption_profile.json")),w("Created: "+f("inferno/CHANGELOG.md")))}else Pe(i.join(b,"contract.json"),_,C),g||w("Created: "+f("inferno/contract.json")),Fe(i.join(b,"capabilities.json"),C),g||w("Created: "+f("inferno/capabilities.json")),Ne(i.join(b,"scenarios"),C),g||w("Created: "+f("inferno/scenarios/happy_path.json")),ie(i.join(b,"CHANGELOG.md"),_),g||w("Created: "+f("inferno/CHANGELOG.md"));const $=W(),re=i.join($,"scripts","inferno-doc-gate.mjs"),le=i.join(o,"scripts","inferno-doc-gate.mjs");U(re,le,t,g);const ce=i.join($,"scripts","inferno-install-hooks.mjs"),ae=i.join(o,"scripts","inferno-install-hooks.mjs");U(ce,ae,t,g);const pe=i.join($,"ci","github-inferno-check.yml"),fe=i.join(se,"infernoflow-check.yml");if(U(pe,fe,t,g),Te(o,g),S&&be({cwd:o,templatesRoot:$,force:t,silent:g,logOk:p=>{g||w(p)},logWarn:p=>{g||T(p)}}),x&&Oe({cwd:o,templatesRoot:$,force:t,silent:g,logOk:p=>{g||w(p)},logWarn:p=>{g||T(p)}}),r){const p=i.join(b,"context-state.json");let c={};try{c=JSON.parse(n.readFileSync(p,"utf8"))}catch{}const h=M(o,{language:N||void 0,framework:P||void 0,projectType:k||void 0});c.stack=h.developmentProfile,n.writeFileSync(p,JSON.stringify(c,null,2)+`
54
- `,"utf8"),g||w("Created: "+f("inferno/context-state.json"))}if(!g){V("infernoflow initialized!");const p=i.join(b,"integrations.json");!(process.env.ANTHROPIC_API_KEY||process.env.OPENAI_API_KEY||process.env.GOOGLE_AI_API_KEY||process.env.OPENROUTER_API_KEY||process.env.GEMINI_API_KEY)&&!n.existsSync(p)&&(console.log(),console.log(` ${G("\u{1F4A1}")} ${J("Tip:")} connect an AI provider for explain, why, review, and changelog AI.`),console.log(` ${f("infernoflow ai setup")} \u2014 takes 60 seconds`)),he([f("infernoflow status")+" \u2014 see your contract at a glance",f("infernoflow check")+" \u2014 validate everything",(r?"Review inferred baseline in ":"Edit ")+G("inferno/capabilities.json")+(r?" and refine IDs/titles":" to describe each capability in detail"),"Add more "+G("inferno/scenarios/*.json")+" files for edge cases","Add "+f("inferno:check")+" to your CI pipeline",...S?["Restart Cursor \u2014 hooks write assistant text to "+G("inferno/CONTEXT.draft.md"),"Promote when ready: "+f("npm run inferno:promote-draft -- --append-notes")]:[],...x?["Restart VS Code \u2014 Copilot hooks append prompts + assistant (from transcript) to "+G("inferno/CONTEXT.draft.md"),"Promote when ready: "+f("npm run inferno:promote-draft -- --append-notes")]:[],...!S&&!x?["Optional: "+f("infernoflow install-cursor-hooks")+" or "+f("infernoflow install-vscode-copilot-hooks")]:[]])}}export{oo as ensureGitignoreEntries,no as initCommand};
51
+ `),c.scripts&&(z("Suggested package.json scripts for this template:"),Object.entries(c.scripts).forEach(([m,A])=>console.log(` ${J(m)}: ${j(A)}`)),console.log()),X(`Initialised from template ${J(p(u))} \u2014 ${J(String(S.length))} capabilities`),console.log(),z(`Run ${p("infernoflow vibe")} to start vibe coding mode`),console.log();return}const v=e.includes("--cursor-hooks"),b=e.includes("--vscode-copilot-hooks"),E=e.includes("--report-json"),x=e.includes("--report-json-only"),O=e.includes("--report-human-only"),N=q(e,"--lang"),R=q(e,"--framework"),_=q(e,"--project-type"),y=x;x&&O&&(console.error("Error: --report-json-only and --report-human-only cannot be used together."),process.exit(1)),y||he("init");const h=i.join(o,"inferno"),W=i.join(o,".github","workflows");n.existsSync(h)&&!t&&(y&&(console.log(JSON.stringify({ok:!1,error:"inferno_exists",hint:"Use --force to overwrite"},null,2)),process.exit(1)),T("inferno/ already exists. Use --force to overwrite."),console.log(),process.exit(0));const I=K(o),V="CreateTask, ReadTasks, UpdateTask, ToggleComplete, DeleteTask";let $=I,C=V.split(",").map(f=>f.trim());if(r||d){let c=M(o,{language:N||void 0,framework:R||void 0,projectType:_||void 0});if(!s&&!x){const S=H.createInterface({input:process.stdin,output:process.stdout}),m=c.developmentProfile||{},A=m.detected||{};console.log(j(` Review inferred development stack (press Enter to accept detected values)
52
+ `));const ue=await L(S,"Language",m.language||A.language||"unknown"),me=await L(S,"Framework",m.framework||A.framework||"unknown"),ye=await L(S,"Project type",m.projectType||A.projectType||"unknown");S.close(),c=M(o,{language:ue,framework:me,projectType:ye})}const w=c.capabilities,P=ve(w);x?console.log(JSON.stringify({mode:"adopt",policyId:I,inferredCapabilities:P,components:c.components,displayFields:c.displayFields,externalLibraries:c.externalLibraries,uiLayout:c.uiLayout,styling:c.styling,developmentProfile:c.developmentProfile,apiCalls:c.apiCalls},null,2)):(console.log(),console.log(j(Se(w))),console.log(),console.log(j(Ce(c))),console.log(),E&&!O&&(console.log(JSON.stringify({mode:"adopt",policyId:I,inferredCapabilities:P,components:c.components,displayFields:c.displayFields,externalLibraries:c.externalLibraries,uiLayout:c.uiLayout,styling:c.styling,developmentProfile:c.developmentProfile,apiCalls:c.apiCalls},null,2)),console.log()));const G=await ke(w,s);$=I,C=G.map(S=>S.id),C.length===0&&(C=["ExampleCapability"],y||T("No capabilities detected from the code \u2014 wrote a placeholder. Edit "+p("inferno/capabilities.json")+" to describe what this project does."))}else if(!s){const f=H.createInterface({input:process.stdin,output:process.stdout});console.log(j(` Press Enter to accept defaults
53
+ `)),$=await L(f,"Project / policy name",I),C=(await L(f,"Capabilities (comma-separated)",V)).split(",").map(w=>w.trim()).filter(Boolean),f.close(),console.log()}if(n.mkdirSync(h,{recursive:!0}),r){const f=C.map(w=>({id:w,title:w.replace(/([A-Z])/g," $1").trim()})),c=M(o,{language:N||void 0,framework:R||void 0,projectType:_||void 0});je(h,$,f,c),y||(k("Created: "+p("inferno/contract.json")),k("Created: "+p("inferno/capabilities.json")),k("Created: "+p("inferno/scenarios/adoption_baseline.json")),k("Created: "+p("inferno/adoption_profile.json")),k("Created: "+p("inferno/CHANGELOG.md")))}else Fe(i.join(h,"contract.json"),$,C),y||k("Created: "+p("inferno/contract.json")),Ne(i.join(h,"capabilities.json"),C),y||k("Created: "+p("inferno/capabilities.json")),Re(i.join(h,"scenarios"),C),y||k("Created: "+p("inferno/scenarios/happy_path.json")),re(i.join(h,"CHANGELOG.md"),$),y||k("Created: "+p("inferno/CHANGELOG.md"));const D=Y(),le=i.join(D,"scripts","inferno-doc-gate.mjs"),ce=i.join(o,"scripts","inferno-doc-gate.mjs");U(le,ce,t,y);const ae=i.join(D,"scripts","inferno-install-hooks.mjs"),pe=i.join(o,"scripts","inferno-install-hooks.mjs");U(ae,pe,t,y);const fe=i.join(D,"ci","github-inferno-check.yml"),de=i.join(W,"infernoflow-check.yml");if(U(fe,de,t,y),Ee(o,y),v&&be({cwd:o,templatesRoot:D,force:t,silent:y,logOk:f=>{y||k(f)},logWarn:f=>{y||T(f)}}),b&&Ie({cwd:o,templatesRoot:D,force:t,silent:y,logOk:f=>{y||k(f)},logWarn:f=>{y||T(f)}}),r){const f=i.join(h,"context-state.json");let c={};try{c=JSON.parse(n.readFileSync(f,"utf8"))}catch{}const w=M(o,{language:N||void 0,framework:R||void 0,projectType:_||void 0});c.stack=w.developmentProfile,n.writeFileSync(f,JSON.stringify(c,null,2)+`
54
+ `,"utf8"),y||k("Created: "+p("inferno/context-state.json"))}if(!y){X("infernoflow initialized!");const f=i.join(h,"integrations.json");!(process.env.ANTHROPIC_API_KEY||process.env.OPENAI_API_KEY||process.env.GOOGLE_AI_API_KEY||process.env.OPENROUTER_API_KEY||process.env.GEMINI_API_KEY)&&!n.existsSync(f)&&(console.log(),console.log(` ${F("\u{1F4A1}")} ${J("Tip:")} connect an AI provider for explain, why, review, and changelog AI.`),console.log(` ${p("infernoflow ai setup")} \u2014 takes 60 seconds`)),we([p("infernoflow status")+" \u2014 see your contract at a glance",p("infernoflow check")+" \u2014 validate everything",(r?"Review inferred baseline in ":"Edit ")+F("inferno/capabilities.json")+(r?" and refine IDs/titles":" to describe each capability in detail"),"Add more "+F("inferno/scenarios/*.json")+" files for edge cases","Add "+p("inferno:check")+" to your CI pipeline",...v?["Restart Cursor \u2014 hooks write assistant text to "+F("inferno/CONTEXT.draft.md"),"Promote when ready: "+p("npm run inferno:promote-draft -- --append-notes")]:[],...b?["Restart VS Code \u2014 Copilot hooks append prompts + assistant (from transcript) to "+F("inferno/CONTEXT.draft.md"),"Promote when ready: "+p("npm run inferno:promote-draft -- --append-notes")]:[],...!v&&!b?["Optional: "+p("infernoflow install-cursor-hooks")+" or "+p("infernoflow install-vscode-copilot-hooks")]:[]])}}export{no as ensureGitignoreEntries,to as initCommand};
@@ -1,15 +1,17 @@
1
- import*as a from"node:fs";import*as u from"node:path";import*as v from"node:os";import{fileURLToPath as $}from"node:url";import{execSync as N}from"node:child_process";import{detectIdeContext as I}from"../ai/ideDetection.mjs";import{header as J,ok as h,warn as M,info as w,done as D,cyan as d,yellow as C,bold as k,green as m,gray as j}from"../ui/output.mjs";import"../cursorHooksInstall.mjs";import"../vsCodeCopilotHooksInstall.mjs";import{updateInjectionConfig as E}from"../amp/io.mjs";import{findProjectRoot as T}from"../projectRoot.mjs";import{injectionPatchFromArgs as A}from"./refresh.mjs";const L=u.dirname($(import.meta.url));function _(){return u.resolve(L,"../../templates")}function W(t){try{return N(`npx infernoflow ${t}`,{encoding:"utf8",cwd:process.cwd(),timeout:6e4,stdio:["inherit","pipe","pipe"]})}catch(i){return i.stdout||i.stderr||i.message}}function y(t){if(!a.existsSync(t))return{data:{},existed:!1,corrupt:!1,backup:null};let i;try{i=a.readFileSync(t,"utf8")}catch{return{data:{},existed:!0,corrupt:!1,backup:null}}try{const s=JSON.parse(i);return!s||typeof s!="object"||Array.isArray(s)?{data:{},existed:!0,corrupt:!1,backup:null}:{data:s,existed:!0,corrupt:!1,backup:null}}catch{let s=null;try{s=`${t}.corrupt-${Date.now()}.bak`,a.writeFileSync(s,i,"utf8")}catch{s=null}return{data:{},existed:!0,corrupt:!0,backup:s}}}function H(){const t=v.homedir();return process.platform==="win32"?u.join(t,"AppData","Roaming","Claude","claude_desktop_config.json"):process.platform==="darwin"?u.join(t,"Library","Application Support","Claude","claude_desktop_config.json"):u.join(t,".config","Claude","claude_desktop_config.json")}function U(t,i){const s=H(),n=u.dirname(s);if(!a.existsSync(n)&&!a.existsSync(s))return{updated:!1,skipped:"not-installed"};const{data:r,backup:e}=y(s);r.mcpServers||(r.mcpServers={});const l=r.mcpServers.infernoflow;if(l&&Array.isArray(l.args)&&l.args[0]===t&&l.env&&l.env.INFERNOFLOW_PROJECT_DIR===i)return{updated:!1,backup:e};const c={...l&&l.env?l.env:{}};i&&(c.INFERNOFLOW_PROJECT_DIR=i),r.mcpServers.infernoflow={command:"node",args:[t],env:c};const p=JSON.stringify(r,null,2);return a.mkdirSync(n,{recursive:!0}),a.writeFileSync(s,p,"utf8"),{updated:!0,path:s,backup:e}}function V(t){const i=u.join(t,".git");if(!a.existsSync(i))return{installed:!1,skipped:"not-a-git-repo"};const s=u.join(i,"hooks"),n=u.join(s,"post-commit"),r='infernoflow log "commit: $(git log -1 --pretty=%s)" --type note --source git-hook --auto --quiet >/dev/null 2>&1 || true';try{if(a.mkdirSync(s,{recursive:!0}),a.existsSync(n)){const e=a.readFileSync(n,"utf8");if(e.includes("infernoflow"))return{installed:!1,already:!0};const l=e.replace(/\s*$/,"")+`
1
+ import*as a from"node:fs";import*as u from"node:path";import*as w from"node:os";import{fileURLToPath as N}from"node:url";import{execSync as I}from"node:child_process";import{detectIdeContext as M}from"../ai/ideDetection.mjs";import{header as J,ok as k,warn as E,info as v,done as D,cyan as p,yellow as C,bold as h,green as g,gray as j}from"../ui/output.mjs";import"../cursorHooksInstall.mjs";import{refreshSecuritySensitiveCopies as L}from"../securityRefresh.mjs";import"../vsCodeCopilotHooksInstall.mjs";import{updateInjectionConfig as T}from"../amp/io.mjs";import{findProjectRoot as A}from"../projectRoot.mjs";import{injectionPatchFromArgs as H}from"./refresh.mjs";const W=u.dirname(N(import.meta.url));function _(){return u.resolve(W,"../../templates")}function U(t){try{return I(`npx infernoflow ${t}`,{encoding:"utf8",cwd:process.cwd(),timeout:6e4,stdio:["inherit","pipe","pipe"]})}catch(i){return i.stdout||i.stderr||i.message}}function y(t){if(!a.existsSync(t))return{data:{},existed:!1,corrupt:!1,backup:null};let i;try{i=a.readFileSync(t,"utf8")}catch{return{data:{},existed:!0,corrupt:!1,backup:null}}try{const s=JSON.parse(i);return!s||typeof s!="object"||Array.isArray(s)?{data:{},existed:!0,corrupt:!1,backup:null}:{data:s,existed:!0,corrupt:!1,backup:null}}catch{let s=null;try{s=`${t}.corrupt-${Date.now()}.bak`,a.writeFileSync(s,i,"utf8")}catch{s=null}return{data:{},existed:!0,corrupt:!0,backup:s}}}function V(){const t=w.homedir();return process.platform==="win32"?u.join(t,"AppData","Roaming","Claude","claude_desktop_config.json"):process.platform==="darwin"?u.join(t,"Library","Application Support","Claude","claude_desktop_config.json"):u.join(t,".config","Claude","claude_desktop_config.json")}function K(t,i){const s=V(),n=u.dirname(s);if(!a.existsSync(n)&&!a.existsSync(s))return{updated:!1,skipped:"not-installed"};const{data:r,backup:o}=y(s);r.mcpServers||(r.mcpServers={});const l=r.mcpServers.infernoflow;if(l&&Array.isArray(l.args)&&l.args[0]===t&&l.env&&l.env.INFERNOFLOW_PROJECT_DIR===i)return{updated:!1,backup:o};const c={...l&&l.env?l.env:{}};i&&(c.INFERNOFLOW_PROJECT_DIR=i),r.mcpServers.infernoflow={command:"node",args:[t],env:c};const d=JSON.stringify(r,null,2);return a.mkdirSync(n,{recursive:!0}),a.writeFileSync(s,d,"utf8"),{updated:!0,path:s,backup:o}}function q(t){const i=u.join(t,".git");if(!a.existsSync(i))return{installed:!1,skipped:"not-a-git-repo"};const s=u.join(i,"hooks"),n=u.join(s,"post-commit"),r='infernoflow log "commit: $(git log -1 --pretty=%s)" --type note --source git-hook --auto --quiet >/dev/null 2>&1 || true';try{if(a.mkdirSync(s,{recursive:!0}),a.existsSync(n)){const o=a.readFileSync(n,"utf8");if(o.includes("infernoflow"))return{installed:!1,already:!0};const l=o.replace(/\s*$/,"")+`
2
2
 
3
3
  # infernoflow auto-capture
4
4
  `+r+`
5
- `;a.writeFileSync(n,l,"utf8")}else{const e=["#!/bin/sh","# infernoflow: auto-capture the commit subject into session memory.","# Best-effort and non-blocking \u2014 never fails a commit.",r,""].join(`
6
- `);a.writeFileSync(n,e,"utf8")}try{a.chmodSync(n,493)}catch{}return{installed:!0,path:n}}catch(e){return{installed:!1,error:e.message}}}const q=`#!/usr/bin/env node
5
+ `;a.writeFileSync(n,l,"utf8")}else{const o=["#!/bin/sh","# infernoflow: auto-capture the commit subject into session memory.","# Best-effort and non-blocking \u2014 never fails a commit.",r,""].join(`
6
+ `);a.writeFileSync(n,o,"utf8")}try{a.chmodSync(n,493)}catch{}return{installed:!0,path:n}}catch(o){return{installed:!1,error:o.message}}}const B=2,b=`#!/usr/bin/env node
7
7
  // infernoflow UserPromptSubmit hook (Claude Code / Cursor).
8
+ // infernoflow-hook-version: ${B}
8
9
  // Logs a best-effort 'attempt' entry when the user's prompt shows frustration,
9
10
  // so the highest-value capture signal doesn't depend on the model remembering.
10
- // Never blocks the prompt.
11
- import { readFileSync } from "node:fs";
12
- import { spawnSync } from "node:child_process";
11
+ // Never blocks the prompt. Never uses a shell.
12
+ import { readFileSync, existsSync, realpathSync } from "node:fs";
13
+ import { execFileSync, spawnSync } from "node:child_process";
14
+ import { dirname, join } from "node:path";
13
15
 
14
16
  let raw = "";
15
17
  try { raw = readFileSync(0, "utf8"); } catch {}
@@ -17,19 +19,43 @@ let prompt = "";
17
19
  try { const j = JSON.parse(raw); prompt = j.prompt || j.user_prompt || j.userPrompt || ""; }
18
20
  catch { prompt = raw; }
19
21
 
20
- const MARKERS = [/!!+/, /not working/i, /still (broken|failing|not)/i, /does ?n'?t work/i, /\\bbroken\\b/i, /\\bretry(ing)?\\b/i, /same error/i, /no change/i];
21
- if (prompt && MARKERS.some((re) => re.test(prompt))) {
22
- const msg = "User frustration: " + prompt.replace(/\\s+/g, " ").trim().slice(0, 120);
22
+ // Find the CLI's JavaScript entry point (never the .cmd / shell wrapper).
23
+ function findCliMjs() {
24
+ let hits = [];
23
25
  try {
24
- spawnSync("infernoflow", ["log", msg, "--type", "attempt", "--result", "failed", "--auto", "--quiet", "--source", "hook"], {
25
- stdio: "ignore", timeout: 5000, shell: process.platform === "win32",
26
- });
26
+ const finder = process.platform === "win32" ? "where" : "which";
27
+ hits = execFileSync(finder, ["infernoflow"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true, timeout: 3000 })
28
+ .split(/\\r?\\n/).map((s) => s.trim()).filter(Boolean);
27
29
  } catch {}
30
+ for (const c of hits) {
31
+ try { const real = realpathSync(c); if (/\\.m?js$/i.test(real)) return real; } catch {}
32
+ const d = dirname(c);
33
+ for (const pkg of [join(d, "node_modules", "infernoflow"), join(d, "..", "lib", "node_modules", "infernoflow")]) {
34
+ for (const f of [join(pkg, "dist", "bin", "infernoflow.mjs"), join(pkg, "bin", "infernoflow.mjs")]) {
35
+ if (existsSync(f)) return f;
36
+ }
37
+ }
38
+ }
39
+ return null;
40
+ }
41
+
42
+ const MARKERS = [/!!+/, /not working/i, /still (broken|failing|not)/i, /does ?n'?t work/i, /\\bbroken\\b/i, /\\bretry(ing)?\\b/i, /same error/i, /no change/i];
43
+ if (prompt && MARKERS.some((re) => re.test(prompt))) {
44
+ // Leading dashes are stripped so the text can never be read as a CLI flag.
45
+ const msg = "User frustration: " + prompt.replace(/\\s+/g, " ").trim().replace(/^[\\s-]+/, "").slice(0, 120);
46
+ const cli = findCliMjs();
47
+ if (cli) {
48
+ try {
49
+ spawnSync(process.execPath, [cli, "log", msg, "--type", "attempt", "--result", "failed", "--auto", "--quiet", "--source", "hook"], {
50
+ stdio: "ignore", timeout: 5000, windowsHide: true, shell: false,
51
+ });
52
+ } catch {}
53
+ }
28
54
  }
29
55
  process.exit(0);
30
- `;function K(t){const i=u.join(t,".claude","hooks"),s=u.join(i,"log-frustration.mjs");try{a.mkdirSync(i,{recursive:!0}),a.writeFileSync(s,q,"utf8");try{a.chmodSync(s,493)}catch{}}catch(c){return{installed:!1,error:c.message}}const n=u.join(t,".claude","settings.json"),{data:r}=y(n);(!r.hooks||typeof r.hooks!="object")&&(r.hooks={}),Array.isArray(r.hooks.UserPromptSubmit)||(r.hooks.UserPromptSubmit=[]);const e="node .claude/hooks/log-frustration.mjs",l=r.hooks.UserPromptSubmit.some(c=>Array.isArray(c?.hooks)&&c.hooks.some(p=>typeof p?.command=="string"&&p.command.includes("log-frustration.mjs")));l||r.hooks.UserPromptSubmit.push({hooks:[{type:"command",command:e}]});try{a.mkdirSync(u.dirname(n),{recursive:!0}),a.writeFileSync(n,JSON.stringify(r,null,2),"utf8")}catch(c){return{installed:!0,registered:!1,error:c.message}}return{installed:!0,registered:!l}}const G=["infernoflow_status","infernoflow_run","infernoflow_apply","infernoflow_check","infernoflow_context","infernoflow_implement","infernoflow_git_drift","infernoflow_scan_ui","infernoflow_review","amp_read","amp_write","amp_search","amp_handoff","amp_health"];function B(t,i){const s=u.join(v.homedir(),".claude.json"),{data:n,backup:r}=y(s);n.mcpServers||(n.mcpServers={});const e=n.mcpServers.infernoflow;if(e&&e.args&&e.args[0]===t&&e.env&&e.env.INFERNOFLOW_PROJECT_DIR===i)return{updated:!1,backup:r};const l={...e&&e.env?e.env:{}};i&&(l.INFERNOFLOW_PROJECT_DIR=i),n.mcpServers.infernoflow={command:"node",args:[t],env:l};const c=JSON.stringify(n,null,2).replace(/\u0000+/g,"");return a.writeFileSync(s,c,"utf8"),{updated:!0,path:s,backup:r}}function z(t,i,s){const n=u.join(t,".vscode"),r=u.join(n,"mcp.json"),{data:e,backup:l}=y(r);e.servers||(e.servers={});const c=e.servers.infernoflow;if(c&&c.args&&c.args[0]===i&&c.env&&c.env.INFERNOFLOW_PROJECT_DIR===s)return{updated:!1,backup:l};const p={...c&&c.env?c.env:{}};return s&&(p.INFERNOFLOW_PROJECT_DIR=s),e.servers.infernoflow={type:"stdio",command:"node",args:[i],env:p},a.mkdirSync(n,{recursive:!0}),a.writeFileSync(r,JSON.stringify(e,null,2)+`
31
- `,"utf8"),{updated:!0,path:r,backup:l}}function Q(t,i){const s=u.join(t,".claude"),n=u.join(s,"settings.json"),{data:r}=y(n),e=new Set(r.allowedTools||[]);for(const c of G)e.add(`mcp__infernoflow__${c}`);const l={...r,allowedTools:[...e]};return a.mkdirSync(s,{recursive:!0}),a.writeFileSync(n,JSON.stringify(l,null,2),"utf8"),n}function X(t,{silent:i=!1}={}){const s=_(),n=i?()=>{}:o=>h(o),r=i?()=>{}:o=>M(o),e={mcpServer:!1,claudeJson:!1,claudeSettings:!1,claudeDesktop:!1,gitHooks:!1,captureHook:!1,backups:[]},l=u.join(s,"cursor","inferno-mcp-server.mjs"),c=u.join(t,".cursor","inferno-mcp-server.mjs"),p=(()=>{try{return T(t)}catch{return t}})();try{a.existsSync(c)||(a.mkdirSync(u.dirname(c),{recursive:!0}),a.copyFileSync(l,c),e.mcpServer=!0,n("Copied MCP server \u2192 "+d(".cursor/inferno-mcp-server.mjs")))}catch(o){r("MCP server copy skipped: "+o.message)}try{const o=B(c,p);o.updated&&(e.claudeJson=!0,n("Registered MCP server in "+d("~/.claude.json"))),o.backup&&(e.backups.push(o.backup),r("~/.claude.json was unreadable \u2014 backed up to "+o.backup+" before rewrite"))}catch(o){r("~/.claude.json update skipped: "+o.message)}try{z(t,c,p).updated&&(e.vscodeMcp=!0,n("Registered MCP server in "+d(".vscode/mcp.json")+j(" (Copilot Chat)")))}catch(o){r(".vscode/mcp.json update skipped: "+o.message)}try{const o=u.join(t,".cursor","mcp.json"),{data:g}=y(o);g.mcpServers||(g.mcpServers={});const f=g.mcpServers.infernoflow;(!f||!f.args||f.args[0]!==c||!f.env||f.env.INFERNOFLOW_PROJECT_DIR!==p)&&(g.mcpServers.infernoflow={command:"node",args:[c],env:{...f&&f.env?f.env:{},INFERNOFLOW_PROJECT_DIR:p}},a.mkdirSync(u.dirname(o),{recursive:!0}),a.writeFileSync(o,JSON.stringify(g,null,2)+`
32
- `,"utf8"),e.cursorMcp=!0,n("Registered MCP server in "+d(".cursor/mcp.json")))}catch(o){r(".cursor/mcp.json update skipped: "+o.message)}try{Q(t,!1),e.claudeSettings=!0,n("Pre-approved infernoflow tools in "+d(".claude/settings.json"))}catch(o){r(".claude/settings.json skipped: "+o.message)}try{const o=U(c,p);o.updated&&(e.claudeDesktop=!0,n("Registered MCP server in "+d("claude_desktop_config.json")+j(" (Claude Desktop app)"))),o.backup&&(e.backups.push(o.backup),r("Claude Desktop config was unreadable \u2014 backed up to "+o.backup))}catch(o){r("Claude Desktop config skipped: "+o.message)}try{V(t).installed&&(e.gitHooks=!0,n("Installed git post-commit hook \u2192 "+d(".git/hooks/post-commit")))}catch(o){r("git hook install skipped: "+o.message)}try{K(t).installed&&(e.captureHook=!0,n("Installed capture hook \u2192 "+d(".claude/hooks/log-frustration.mjs")))}catch(o){r("capture hook install skipped: "+o.message)}return e}async function le(t){const i=process.cwd(),s=t.includes("--force")||t.includes("-f"),n=t.includes("--yes")||t.includes("-y"),r=_();J("infernoflow setup");const{ideDetected:e}=I("auto");w(`IDE detected: ${k(e==="cursor"?"Cursor":e==="vscode"?"VS Code":e==="windsurf"?"Windsurf":"unknown")}`);const c=u.join(i,".ai-memory");a.existsSync(c)?h(".ai-memory/ already exists \u2014 skipping init"):(console.log(`
56
+ `;function G(t){const i=u.join(t,".claude","hooks"),s=u.join(i,"log-frustration.mjs");try{a.mkdirSync(i,{recursive:!0}),a.writeFileSync(s,b,"utf8");try{a.chmodSync(s,493)}catch{}}catch(c){return{installed:!1,error:c.message}}const n=u.join(t,".claude","settings.json"),{data:r}=y(n);(!r.hooks||typeof r.hooks!="object")&&(r.hooks={}),Array.isArray(r.hooks.UserPromptSubmit)||(r.hooks.UserPromptSubmit=[]);const o="node .claude/hooks/log-frustration.mjs",l=r.hooks.UserPromptSubmit.some(c=>Array.isArray(c?.hooks)&&c.hooks.some(d=>typeof d?.command=="string"&&d.command.includes("log-frustration.mjs")));l||r.hooks.UserPromptSubmit.push({hooks:[{type:"command",command:o}]});try{a.mkdirSync(u.dirname(n),{recursive:!0}),a.writeFileSync(n,JSON.stringify(r,null,2),"utf8")}catch(c){return{installed:!0,registered:!1,error:c.message}}return{installed:!0,registered:!l}}const z=["infernoflow_status","infernoflow_run","infernoflow_apply","infernoflow_check","infernoflow_context","infernoflow_implement","infernoflow_git_drift","infernoflow_scan_ui","infernoflow_review","amp_read","amp_write","amp_search","amp_handoff","amp_health"];function Q(t,i){const s=u.join(w.homedir(),".claude.json"),{data:n,backup:r}=y(s);n.mcpServers||(n.mcpServers={});const o=n.mcpServers.infernoflow;if(o&&o.args&&o.args[0]===t&&o.env&&o.env.INFERNOFLOW_PROJECT_DIR===i)return{updated:!1,backup:r};const l={...o&&o.env?o.env:{}};i&&(l.INFERNOFLOW_PROJECT_DIR=i),n.mcpServers.infernoflow={command:"node",args:[t],env:l};const c=JSON.stringify(n,null,2).replace(/\u0000+/g,"");return a.writeFileSync(s,c,"utf8"),{updated:!0,path:s,backup:r}}function X(t,i,s){const n=u.join(t,".vscode"),r=u.join(n,"mcp.json"),{data:o,backup:l}=y(r);o.servers||(o.servers={});const c=o.servers.infernoflow;if(c&&c.args&&c.args[0]===i&&c.env&&c.env.INFERNOFLOW_PROJECT_DIR===s)return{updated:!1,backup:l};const d={...c&&c.env?c.env:{}};return s&&(d.INFERNOFLOW_PROJECT_DIR=s),o.servers.infernoflow={type:"stdio",command:"node",args:[i],env:d},a.mkdirSync(n,{recursive:!0}),a.writeFileSync(r,JSON.stringify(o,null,2)+`
57
+ `,"utf8"),{updated:!0,path:r,backup:l}}function Y(t,i){const s=u.join(t,".claude"),n=u.join(s,"settings.json"),{data:r}=y(n),o=new Set(r.allowedTools||[]);for(const c of z)o.add(`mcp__infernoflow__${c}`);const l={...r,allowedTools:[...o]};return a.mkdirSync(s,{recursive:!0}),a.writeFileSync(n,JSON.stringify(l,null,2),"utf8"),n}function Z(t,{silent:i=!1}={}){const s=_(),n=i?()=>{}:e=>k(e),r=i?()=>{}:e=>E(e),o={mcpServer:!1,claudeJson:!1,claudeSettings:!1,claudeDesktop:!1,gitHooks:!1,captureHook:!1,backups:[]},l=u.join(s,"cursor","inferno-mcp-server.mjs"),c=u.join(t,".cursor","inferno-mcp-server.mjs"),d=(()=>{try{return A(t)}catch{return t}})();try{a.existsSync(c)||(a.mkdirSync(u.dirname(c),{recursive:!0}),a.copyFileSync(l,c),o.mcpServer=!0,n("Copied MCP server \u2192 "+p(".cursor/inferno-mcp-server.mjs")))}catch(e){r("MCP server copy skipped: "+e.message)}try{const e=L(t,{captureHookScript:b});e.length&&(o.securityRefreshed=e,e.some(f=>f.endsWith("inferno-mcp-server.mjs"))&&(o.mcpServer=!0),n("Security update: replaced outdated "+e.map(f=>p(f)).join(", ")))}catch{}try{const e=Q(c,d);e.updated&&(o.claudeJson=!0,n("Registered MCP server in "+p("~/.claude.json"))),e.backup&&(o.backups.push(e.backup),r("~/.claude.json was unreadable \u2014 backed up to "+e.backup+" before rewrite"))}catch(e){r("~/.claude.json update skipped: "+e.message)}try{X(t,c,d).updated&&(o.vscodeMcp=!0,n("Registered MCP server in "+p(".vscode/mcp.json")+j(" (Copilot Chat)")))}catch(e){r(".vscode/mcp.json update skipped: "+e.message)}try{const e=u.join(t,".cursor","mcp.json"),{data:f}=y(e);f.mcpServers||(f.mcpServers={});const m=f.mcpServers.infernoflow;(!m||!m.args||m.args[0]!==c||!m.env||m.env.INFERNOFLOW_PROJECT_DIR!==d)&&(f.mcpServers.infernoflow={command:"node",args:[c],env:{...m&&m.env?m.env:{},INFERNOFLOW_PROJECT_DIR:d}},a.mkdirSync(u.dirname(e),{recursive:!0}),a.writeFileSync(e,JSON.stringify(f,null,2)+`
58
+ `,"utf8"),o.cursorMcp=!0,n("Registered MCP server in "+p(".cursor/mcp.json")))}catch(e){r(".cursor/mcp.json update skipped: "+e.message)}try{Y(t,!1),o.claudeSettings=!0,n("Pre-approved infernoflow tools in "+p(".claude/settings.json"))}catch(e){r(".claude/settings.json skipped: "+e.message)}try{const e=K(c,d);e.updated&&(o.claudeDesktop=!0,n("Registered MCP server in "+p("claude_desktop_config.json")+j(" (Claude Desktop app)"))),e.backup&&(o.backups.push(e.backup),r("Claude Desktop config was unreadable \u2014 backed up to "+e.backup))}catch(e){r("Claude Desktop config skipped: "+e.message)}try{q(t).installed&&(o.gitHooks=!0,n("Installed git post-commit hook \u2192 "+p(".git/hooks/post-commit")))}catch(e){r("git hook install skipped: "+e.message)}try{G(t).installed&&(o.captureHook=!0,n("Installed capture hook \u2192 "+p(".claude/hooks/log-frustration.mjs")))}catch(e){r("capture hook install skipped: "+e.message)}return o}async function de(t){const i=process.cwd(),s=t.includes("--force")||t.includes("-f"),n=t.includes("--yes")||t.includes("-y"),r=_();J("infernoflow setup");const{ideDetected:o}=M("auto");v(`IDE detected: ${h(o==="cursor"?"Cursor":o==="vscode"?"VS Code":o==="windsurf"?"Windsurf":"unknown")}`);const c=u.join(i,".ai-memory");a.existsSync(c)?k(".ai-memory/ already exists \u2014 skipping init"):(console.log(`
33
59
  ${C(".ai-memory/")} not found \u2014 running init ...
34
- `),W(n?"init --yes":"init"));const p=A(t);if(Object.keys(p).length)try{E(i,p),h("Injection config updated \u2192 "+JSON.stringify(p))}catch{}console.log(),w("Wiring up MCP servers for Cursor / VS Code Copilot / Claude Code ...");const o=X(i,{silent:!1});console.log(),D("infernoflow ready"),console.log(`
35
- ${k("What was set up:")}`),console.log(` ${m("\u2714")} MCP server installed \u2192 ${d(".cursor/inferno-mcp-server.mjs")}`),o.cursorMcp&&console.log(` ${m("\u2714")} Cursor MCP config \u2192 ${d(".cursor/mcp.json")}`),o.vscodeMcp&&console.log(` ${m("\u2714")} VS Code Copilot MCP config \u2192 ${d(".vscode/mcp.json")}`),o.claudeJson&&console.log(` ${m("\u2714")} Claude Code MCP config \u2192 ${d("~/.claude.json")}`),o.claudeSettings&&console.log(` ${m("\u2714")} Auto-approved tools \u2192 ${d(".claude/settings.json")}`),o.claudeDesktop&&console.log(` ${m("\u2714")} Claude Desktop MCP config \u2192 ${d("claude_desktop_config.json")}`),o.gitHooks&&console.log(` ${m("\u2714")} Git post-commit hook \u2192 ${d(".git/hooks/post-commit")}`),o.captureHook&&console.log(` ${m("\u2714")} Capture hook (Claude Code/Cursor) \u2192 ${d(".claude/hooks/log-frustration.mjs")}`);try{const{detectStaleMcpRuntime:g}=await import("../mcpRuntime.mjs"),{readFileSync:f}=await import("node:fs"),{dirname:b,join:R}=await import("node:path"),{fileURLToPath:O}=await import("node:url"),P=b(O(import.meta.url)),x=R(P,"..","..","package.json"),F=JSON.parse(f(x,"utf8")).version,S=g(i,F);S&&(console.log(),console.log(` ${C("\u26A0")} ${k("Restart required:")} ${S.message}`))}catch{}console.log(),console.log(` ${k("Next step:")} Restart your AI tool. Test by asking:`),console.log(` ${d('"call the amp_write tool with a test note"')}`),console.log()}export{G as MCP_TOOLS,X as autoSetupMcp,H as claudeDesktopConfigPath,K as installClaudeCodeCaptureHook,V as installGitHooks,le as setupCommand,U as updateClaudeDesktopConfig,B as updateClaudeJson,z as updateVscodeMcpJson,Q as writeClaudeSettings};
60
+ `),U(n?"init --yes":"init"));const d=H(t);if(Object.keys(d).length)try{T(i,d),k("Injection config updated \u2192 "+JSON.stringify(d))}catch{}console.log(),v("Wiring up MCP servers for Cursor / VS Code Copilot / Claude Code ...");const e=Z(i,{silent:!1});console.log(),D("infernoflow ready"),console.log(`
61
+ ${h("What was set up:")}`),console.log(` ${g("\u2714")} MCP server installed \u2192 ${p(".cursor/inferno-mcp-server.mjs")}`),e.cursorMcp&&console.log(` ${g("\u2714")} Cursor MCP config \u2192 ${p(".cursor/mcp.json")}`),e.vscodeMcp&&console.log(` ${g("\u2714")} VS Code Copilot MCP config \u2192 ${p(".vscode/mcp.json")}`),e.claudeJson&&console.log(` ${g("\u2714")} Claude Code MCP config \u2192 ${p("~/.claude.json")}`),e.claudeSettings&&console.log(` ${g("\u2714")} Auto-approved tools \u2192 ${p(".claude/settings.json")}`),e.claudeDesktop&&console.log(` ${g("\u2714")} Claude Desktop MCP config \u2192 ${p("claude_desktop_config.json")}`),e.gitHooks&&console.log(` ${g("\u2714")} Git post-commit hook \u2192 ${p(".git/hooks/post-commit")}`),e.captureHook&&console.log(` ${g("\u2714")} Capture hook (Claude Code/Cursor) \u2192 ${p(".claude/hooks/log-frustration.mjs")}`);try{const{detectStaleMcpRuntime:f}=await import("../mcpRuntime.mjs"),{readFileSync:m}=await import("node:fs"),{dirname:R,join:x}=await import("node:path"),{fileURLToPath:O}=await import("node:url"),P=R(O(import.meta.url)),F=x(P,"..","..","package.json"),$=JSON.parse(m(F,"utf8")).version,S=f(i,$);S&&(console.log(),console.log(` ${C("\u26A0")} ${h("Restart required:")} ${S.message}`))}catch{}console.log(),console.log(` ${h("Next step:")} Restart your AI tool. Test by asking:`),console.log(` ${p('"call the amp_write tool with a test note"')}`),console.log()}export{b as CAPTURE_HOOK_SCRIPT,B as CAPTURE_HOOK_VERSION,z as MCP_TOOLS,Z as autoSetupMcp,V as claudeDesktopConfigPath,G as installClaudeCodeCaptureHook,q as installGitHooks,de as setupCommand,K as updateClaudeDesktopConfig,Q as updateClaudeJson,X as updateVscodeMcpJson,Y as writeClaudeSettings};
@@ -1 +1 @@
1
- import*as r from"node:fs";import*as o from"node:path";import{installInfernoDraftTooling as v}from"./draftToolingInstall.mjs";function h(f){const{cwd:s,templatesRoot:t,force:m,silent:e}=f,a=f.logOk||(()=>{}),l=f.logWarn||(()=>{});function p(n,c){return r.existsSync(c)&&!m?(e||l("Skipped (exists): "+o.relative(s,c)),!1):(r.mkdirSync(o.dirname(c),{recursive:!0}),r.copyFileSync(n,c),e||a("Created: "+o.relative(s,c)),!0)}v({cwd:s,templatesRoot:t,force:m,silent:e,logOk:a,logWarn:l});const u=o.join(t,"cursor","hooks.json"),j=o.join(s,".cursor","hooks.json"),k=o.join(t,"cursor","hooks","inferno-session-draft.mjs"),S=o.join(s,".cursor","hooks","inferno-session-draft.mjs");p(u,j),p(k,S);const d=o.join(t,"cursor","inferno-mcp-server.mjs"),y=o.join(s,".cursor","inferno-mcp-server.mjs");p(d,y);const i=o.join(s,".cursor","mcp.json");if(!r.existsSync(i)||m){let n={};if(r.existsSync(i))try{n=JSON.parse(r.readFileSync(i,"utf8"))}catch{}n.mcpServers||(n.mcpServers={}),n.mcpServers.infernoflow={command:"node",args:["./inferno-mcp-server.mjs"],env:{}},r.mkdirSync(o.dirname(i),{recursive:!0}),r.writeFileSync(i,JSON.stringify(n,null,2),"utf8"),e||a("Created: .cursor/mcp.json")}else e||l("Skipped (exists): .cursor/mcp.json")}export{h as installCursorHooksArtifacts};
1
+ import*as e from"node:fs";import*as r from"node:path";import{installInfernoDraftTooling as v}from"./draftToolingInstall.mjs";import{refreshSecuritySensitiveCopies as h}from"./securityRefresh.mjs";function H(m){const{cwd:s,templatesRoot:t,force:a,silent:n}=m,f=m.logOk||(()=>{}),p=m.logWarn||(()=>{});function l(o,c){return e.existsSync(c)&&!a?(n||p("Skipped (exists): "+r.relative(s,c)),!1):(e.mkdirSync(r.dirname(c),{recursive:!0}),e.copyFileSync(o,c),n||f("Created: "+r.relative(s,c)),!0)}v({cwd:s,templatesRoot:t,force:a,silent:n,logOk:f,logWarn:p});const u=r.join(t,"cursor","hooks.json"),j=r.join(s,".cursor","hooks.json"),S=r.join(t,"cursor","hooks","inferno-session-draft.mjs"),d=r.join(s,".cursor","hooks","inferno-session-draft.mjs");l(u,j),l(S,d);const k=r.join(t,"cursor","inferno-mcp-server.mjs"),y=r.join(s,".cursor","inferno-mcp-server.mjs");l(k,y);try{const o=h(s);o.length&&!n&&f("Security update: replaced "+o.join(", "))}catch{}const i=r.join(s,".cursor","mcp.json");if(!e.existsSync(i)||a){let o={};if(e.existsSync(i))try{o=JSON.parse(e.readFileSync(i,"utf8"))}catch{}o.mcpServers||(o.mcpServers={}),o.mcpServers.infernoflow={command:"node",args:["./inferno-mcp-server.mjs"],env:{}},e.mkdirSync(r.dirname(i),{recursive:!0}),e.writeFileSync(i,JSON.stringify(o,null,2),"utf8"),n||f("Created: .cursor/mcp.json")}else n||p("Skipped (exists): .cursor/mcp.json")}export{H as installCursorHooksArtifacts};
@@ -0,0 +1,2 @@
1
+ import*as c from"node:fs";import*as n from"node:path";import{fileURLToPath as d}from"node:url";const h=n.dirname(d(import.meta.url));function k(){return n.resolve(h,"../templates")}const p="infernoflow-hook-version: 2";function t(r){try{return c.readFileSync(r,"utf8")}catch{return null}}const j=r=>r==null?r:r.replace(/\r\n/g,`
2
+ `);function v(r){return typeof r=="string"&&(r.includes('serverInfo: { name: "infernoflow"')||r.includes("[infernoflow MCP]"))}function y(r,u={}){const f=[],l=k(),i=(o,e)=>{const a=n.join(r,o);try{c.mkdirSync(n.dirname(a),{recursive:!0}),c.writeFileSync(a,e,"utf8"),f.push(o.split(n.sep).join("/"))}catch{}},s=t(n.join(l,"cursor","inferno-mcp-server.mjs"));if(s)for(const o of[n.join(".cursor","inferno-mcp-server.mjs"),"inferno-mcp-server.mjs"]){const e=t(n.join(r,o));e!=null&&v(e)&&j(e)!==j(s)&&i(o,s)}const m=t(n.join(l,"cursor","hooks","inferno-session-draft.mjs"));if(m){const o=n.join(".cursor","hooks","inferno-session-draft.mjs"),e=t(n.join(r,o));e!=null&&!e.includes(p)&&i(o,m)}if(u.captureHookScript){const o=n.join(".claude","hooks","log-frustration.mjs"),e=t(n.join(r,o));e!=null&&!e.includes(p)&&i(o,u.captureHookScript)}return f}export{p as HOOK_VERSION_MARKER,y as refreshSecuritySensitiveCopies};
@@ -1,3 +1,5 @@
1
- import*as o from"node:fs";import*as a from"node:path";const p=new Set(["--help","-h","--version","-v","commands","doctor","setup","init","uninstall"]);function u(t){const n=a.join(t,".ai-memory"),e=a.join(t,"inferno");return o.existsSync(n)?a.join(n,".last-cli-version"):o.existsSync(e)?a.join(e,".last-cli-version"):null}function d(t){try{return o.readFileSync(t,"utf8").trim()}catch{return""}}async function m(t,n){if(!t||n&&p.has(n))return;const e=process.cwd(),c=u(e);if(!c)return;const s=d(c);if(s===t)return;let l=!1,f=s==="";try{const{refreshRuleFilesFromMemory:i}=await import("./ruleFiles.mjs");i(e)}catch{}try{const{applyCleanTreePolicy:i}=await import("./cleanTree.mjs");i(e)}catch{}try{const{autoSetupMcp:i}=await import("./commands/setup.mjs"),r=i(e,{silent:!0});l=!!(r&&(r.mcpServer||r.claudeJson||r.cursorMcp||r.vscodeMcp||r.claudeSettings))}catch{}try{o.writeFileSync(c,t,"utf8")}catch{}if(l||f)try{const{gray:i}=await import("./ui/output.mjs"),r=s?`upgraded ${s} \u2192 ${t}`:`initialized for ${t}`;process.stderr.write(i(` infernoflow: ${r}, wired MCP servers + rule files
2
- `))}catch{process.stderr.write(` infernoflow: refreshed for v${t}
1
+ import*as n from"node:fs";import*as a from"node:path";const u=new Set(["--help","-h","--version","-v","commands","doctor","setup","init","uninstall"]);function p(e){const o=a.join(e,".ai-memory"),t=a.join(e,"inferno");return n.existsSync(o)?a.join(o,".last-cli-version"):n.existsSync(t)?a.join(t,".last-cli-version"):null}function y(e){try{return n.readFileSync(e,"utf8").trim()}catch{return""}}async function m(e,o){if(!e||o&&u.has(o))return;const t=process.cwd(),c=p(t);if(!c)return;const s=y(c);if(s===e)return;let f=!1,l=[],d=s==="";try{const{refreshRuleFilesFromMemory:i}=await import("./ruleFiles.mjs");i(t)}catch{}try{const{applyCleanTreePolicy:i}=await import("./cleanTree.mjs");i(t)}catch{}try{const{autoSetupMcp:i}=await import("./commands/setup.mjs"),r=i(t,{silent:!0});f=!!(r&&(r.mcpServer||r.claudeJson||r.cursorMcp||r.vscodeMcp||r.claudeSettings)),l=r&&r.securityRefreshed||[]}catch{}try{n.writeFileSync(c,e,"utf8")}catch{}if(l.length&&process.stderr.write(` infernoflow: security update \u2014 replaced outdated ${l.join(", ")}.
2
+ infernoflow: restart your AI tool (Claude Code / Cursor / VS Code) so the new MCP server is loaded.
3
+ `),f||d)try{const{gray:i}=await import("./ui/output.mjs"),r=s?`upgraded ${s} \u2192 ${e}`:`initialized for ${e}`;process.stderr.write(i(` infernoflow: ${r}, wired MCP servers + rule files
4
+ `))}catch{process.stderr.write(` infernoflow: refreshed for v${e}
3
5
  `)}}export{m as runUpgradeBackfillIfNeeded};
@@ -16,7 +16,41 @@
16
16
  */
17
17
  import * as fs from "node:fs";
18
18
  import * as path from "node:path";
19
- import { spawnSync } from "node:child_process";
19
+ import { spawnSync, execFileSync } from "node:child_process";
20
+
21
+ // infernoflow-hook-version: 2
22
+ // SECURITY (0.44.20): the CLI is run as `node infernoflow.mjs ...` with NO
23
+ // shell. Before 0.44.20 this hook used spawnSync("infernoflow.cmd", args,
24
+ // { shell: true }) on Windows, which hands the prompt text to cmd.exe
25
+ // unquoted — a prompt containing `&` or `|` ran as a command.
26
+ function findCliMjs() {
27
+ let hits = [];
28
+ try {
29
+ const finder = process.platform === "win32" ? "where" : "which";
30
+ hits = execFileSync(finder, ["infernoflow"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true, timeout: 3000 })
31
+ .split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
32
+ } catch {}
33
+ for (const c of hits) {
34
+ try { const real = fs.realpathSync(c); if (/\.m?js$/i.test(real)) return real; } catch {}
35
+ const d = path.dirname(c);
36
+ for (const pkg of [path.join(d, "node_modules", "infernoflow"), path.join(d, "..", "lib", "node_modules", "infernoflow")]) {
37
+ for (const f of [path.join(pkg, "dist", "bin", "infernoflow.mjs"), path.join(pkg, "bin", "infernoflow.mjs")]) {
38
+ if (fs.existsSync(f)) return f;
39
+ }
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ /** Run the infernoflow CLI without a shell. Returns the spawnSync result or null. */
46
+ function runCli(args, opts) {
47
+ const cli = findCliMjs();
48
+ if (!cli) return null;
49
+ return spawnSync(process.execPath, [cli, ...args], { ...opts, windowsHide: true, shell: false });
50
+ }
51
+
52
+ /** Text that becomes a CLI argument must never look like a flag. */
53
+ const asCliText = (v) => String(v ?? "").replace(/^[\s-]+/, "");
20
54
 
21
55
  /** Keep in sync with templates/scripts/inferno-promote-draft.mjs */
22
56
  const DRAFT_HEADER = `# CONTEXT draft (gitignored)
@@ -201,11 +235,10 @@ function handleBookmarkTrigger(prompt) {
201
235
  // The `bookmark` command (no --marker) auto-captures the session transcript
202
236
  // as the resume point. Needs infernoflow >= 0.44.10 on PATH; if the global
203
237
  // CLI is older / missing, this no-ops and the AI's amp_bookmark path covers it.
204
- const bin = process.platform === "win32" ? "infernoflow.cmd" : "infernoflow";
205
- const r = spawnSync(bin, ["bookmark", label], {
206
- cwd: projectRoot(), encoding: "utf8", timeout: 12000, shell: process.platform === "win32",
238
+ const r = runCli(["bookmark", asCliText(label)], {
239
+ cwd: projectRoot(), encoding: "utf8", timeout: 12000,
207
240
  });
208
- wrote = r.status === 0;
241
+ wrote = !!r && r.status === 0;
209
242
  } catch { /* CLI unavailable — skip */ }
210
243
 
211
244
  if (wrote) {
@@ -240,11 +273,10 @@ function handleUserPrompt(text) {
240
273
  // direct sessions.jsonl append so capture still works without a global CLI.
241
274
  let wrote = false;
242
275
  try {
243
- const bin = process.platform === "win32" ? "infernoflow.cmd" : "infernoflow";
244
- const r = spawnSync(bin, ["log", msg, "--type", "attempt", "--source", "cursor-trigger", "--tags", "auto-trigger"], {
245
- cwd: projectRoot(), encoding: "utf8", timeout: 8000, shell: process.platform === "win32",
276
+ const r = runCli(["log", asCliText(msg), "--type", "attempt", "--source", "cursor-trigger", "--tags", "auto-trigger"], {
277
+ cwd: projectRoot(), encoding: "utf8", timeout: 8000,
246
278
  });
247
- wrote = r.status === 0;
279
+ wrote = !!r && r.status === 0;
248
280
  } catch { /* fall through to direct write */ }
249
281
  if (!wrote) {
250
282
  try {
@@ -1,4 +1,4 @@
1
- import { execSync } from "node:child_process";
1
+ import { execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import readline from "node:readline";
@@ -37,6 +37,19 @@ function walkUpForInfernoflow(startFile) {
37
37
  return null;
38
38
  }
39
39
 
40
+ /**
41
+ * Locate the global `infernoflow` launcher(s) on PATH WITHOUT a shell.
42
+ * `where` (Windows) / `which` (POSIX) are real executables, so execFileSync
43
+ * with an argument array is enough. Returns [] when nothing is found.
44
+ */
45
+ function lookupOnPath(name) {
46
+ try {
47
+ const finder = process.platform === "win32" ? "where" : "which";
48
+ const out = execFileSync(finder, [name], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true, shell: false });
49
+ return out.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
50
+ } catch { return []; }
51
+ }
52
+
40
53
  function findInfernoflowRoot() {
41
54
  // 1. Walk up from this template's own location.
42
55
  // Works when the template runs from inside infernoflow-pkg/ or from a
@@ -56,9 +69,7 @@ function findInfernoflowRoot() {
56
69
  // and the user's project doesn't depend on infernoflow locally. Without
57
70
  // this branch the MCP server boots with v0.0.0-unknown.
58
71
  try {
59
- const lookup = process.platform === "win32" ? "where infernoflow" : "which infernoflow";
60
- const out = execSync(lookup, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
61
- for (const candidate of out.split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
72
+ for (const candidate of lookupOnPath("infernoflow")) {
62
73
  if (!fs.existsSync(candidate)) continue;
63
74
  const binDir = path.dirname(candidate);
64
75
  // Windows layout: <npm-prefix>/infernoflow.cmd + <npm-prefix>/node_modules/infernoflow/
@@ -89,20 +100,40 @@ function sendError(id, code, message) { send({ jsonrpc: "2.0", id, error: { code
89
100
  // Resolve a deterministic location once at startup, in priority order:
90
101
  // 1. infernoflow installed in the project's node_modules (npm i / npm link)
91
102
  // 2. `where`/`which` the global binary
92
- // Returns null if nothing is found; runCmd surfaces a clear error in that case.
103
+ // Returns null if nothing is found; runCli surfaces a clear error in that case.
104
+ // SECURITY: this must always resolve to the CLI's JavaScript entry point
105
+ // (infernoflow.mjs), which we run as `node <file> ...args` with NO shell.
106
+ // We never execute the npm `.cmd` / shell-script wrapper: running those needs
107
+ // a shell, and a shell turns tool arguments into commands.
93
108
  function resolveInfernoflowBin() {
94
- if (INFERNOFLOW_ROOT) {
109
+ const fromRoot = (root) => {
95
110
  for (const c of [
96
- path.join(INFERNOFLOW_ROOT, "dist", "bin", "infernoflow.mjs"),
97
- path.join(INFERNOFLOW_ROOT, "bin", "infernoflow.mjs"),
111
+ path.join(root, "dist", "bin", "infernoflow.mjs"),
112
+ path.join(root, "bin", "infernoflow.mjs"),
98
113
  ]) if (fs.existsSync(c)) return c;
114
+ return null;
115
+ };
116
+ if (INFERNOFLOW_ROOT) {
117
+ const hit = fromRoot(INFERNOFLOW_ROOT);
118
+ if (hit) return hit;
119
+ }
120
+ for (const candidate of lookupOnPath("infernoflow")) {
121
+ if (!fs.existsSync(candidate)) continue;
122
+ // POSIX: the bin entry is usually a symlink straight to infernoflow.mjs.
123
+ try {
124
+ const real = fs.realpathSync(candidate);
125
+ if (/\.m?js$/i.test(real)) return real;
126
+ } catch {}
127
+ // Windows / wrapper layouts: find the package next to the wrapper.
128
+ const binDir = path.dirname(candidate);
129
+ for (const layout of [
130
+ path.join(binDir, "node_modules", "infernoflow"),
131
+ path.join(binDir, "..", "lib", "node_modules", "infernoflow"),
132
+ ]) {
133
+ const hit = fromRoot(layout);
134
+ if (hit) return hit;
135
+ }
99
136
  }
100
- try {
101
- const lookup = process.platform === "win32" ? "where infernoflow" : "which infernoflow";
102
- const out = execSync(lookup, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
103
- const first = out.split(/\r?\n/)[0];
104
- if (first && fs.existsSync(first)) return first;
105
- } catch {}
106
137
  return null;
107
138
  }
108
139
 
@@ -110,7 +141,7 @@ const INFERNOFLOW_BIN = resolveInfernoflowBin();
110
141
 
111
142
  // In-process AMP I/O loader. When available, amp_write / amp_read bypass the
112
143
  // CLI entirely — no subprocess, no version skew, no flag-mapping field loss.
113
- // Falls back to shell-out via runCmd() if the AMP layer can't be loaded.
144
+ // Falls back to the CLI via runCli() if the AMP layer can't be loaded.
114
145
  let ampIo = null;
115
146
  let refreshRuleFiles = null;
116
147
  let harvestSnapshot = null;
@@ -211,8 +242,16 @@ try {
211
242
  * Run the infernoflow CLI. Returns either the stdout string OR a structured
212
243
  * error object so call sites can decide whether to surface it via JSON-RPC
213
244
  * sendError() instead of returning gibberish text to the agent.
245
+ *
246
+ * SECURITY: `args` is an ARRAY of separate arguments, passed straight to
247
+ * `node infernoflow.mjs` with execFileSync and no shell. Tool input must never
248
+ * be concatenated into a command string — that allowed command injection
249
+ * through tool arguments (fixed in 0.44.20).
214
250
  */
215
- function runCmd(args, env = {}) {
251
+ function runCli(args, env = {}) {
252
+ if (!Array.isArray(args) || !args.every(a => typeof a === "string")) {
253
+ return { __error: true, message: "internal: runCli expects an array of strings", stderr: "", stdout: "", status: 1 };
254
+ }
216
255
  if (!INFERNOFLOW_BIN) {
217
256
  return {
218
257
  __error: true,
@@ -223,15 +262,13 @@ function runCmd(args, env = {}) {
223
262
  };
224
263
  }
225
264
  try {
226
- const isMjs = INFERNOFLOW_BIN.toLowerCase().endsWith(".mjs");
227
- const cmd = isMjs
228
- ? `"${process.execPath}" "${INFERNOFLOW_BIN}" ${args}`
229
- : `"${INFERNOFLOW_BIN}" ${args}`;
230
- return execSync(cmd, {
265
+ return execFileSync(process.execPath, [INFERNOFLOW_BIN, ...args], {
231
266
  encoding: "utf8",
232
267
  cwd: PROJECT_DIR,
233
268
  timeout: 30000,
234
269
  env: { ...process.env, ...env },
270
+ windowsHide: true,
271
+ shell: false,
235
272
  });
236
273
  } catch (err) {
237
274
  return {
@@ -244,7 +281,7 @@ function runCmd(args, env = {}) {
244
281
  }
245
282
  }
246
283
 
247
- /** True if a runCmd() result is actually a structured error. */
284
+ /** True if a runCli() result is actually a structured error. */
248
285
  function isCmdError(result) {
249
286
  return typeof result === "object" && result !== null && result.__error === true;
250
287
  }
@@ -259,36 +296,97 @@ function isCmdError(result) {
259
296
  // helpers that pair cleanly with the kept CLI surface.
260
297
  const TOOLS = [
261
298
  // ── AMP-spec memory tools (the product) ──────────────────────────────────
262
- { name: "amp_read", description: "AMP: read session memory entries with optional filters.", inputSchema: { type: "object", properties: { file: { type: "string" }, type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] }, query: { type: "string" }, limit: { type: "number" } } } },
263
- { name: "amp_write", description: "AMP: log a new entry. Required: type + msg (one sentence). Optional: file, line, tags, detail. Use 'detail' for a rich multi-paragraph body (repro steps, code, full reasoning, or a session snapshot) — it's stored in a sidecar and loaded on demand, so it never bloats the always-on memory index.", inputSchema: { type: "object", properties: { type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] }, msg: { type: "string" }, file: { type: "string" }, line: { type: "number" }, tags: { type: "array", items: { type: "string" } }, detail: { type: "string", description: "Optional rich body (Tier-2). Stored in details/<id>.md; NOT injected into rule files. Put the long-form context here; keep 'msg' to one summary sentence." } }, required: ["type","msg"] } },
264
- { name: "amp_search", description: "AMP: search entries by keyword. Optional type filter.", inputSchema: { type: "object", properties: { query: { type: "string" }, type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] } }, required: ["query"] } },
265
- { name: "amp_bookmark", description: "AMP: drop a named session bookmark — a resume point. Required: label (short name). Optional: note. If note is OMITTED, the current session transcript is auto-captured as the bookmark's context (the 'save everything here' resume point). Use when the user says 'bookmark this' / 'mark this point', or before a risky change / when the context window is filling up, so the exact state can be recalled later and appears in the next session's handoff. Bookmarks are never auto-pruned.", inputSchema: { type: "object", properties: { label: { type: "string" }, note: { type: "string", description: "Optional explicit context. Omit to auto-capture the session transcript instead. Stored in a sidecar; not injected into rule files." } }, required: ["label"] } },
299
+ { name: "amp_read", description: "AMP: read session memory entries with optional filters.", inputSchema: { type: "object", properties: { file: { type: "string", maxLength: 1000 }, type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] }, query: { type: "string", maxLength: 500 }, limit: { type: "integer", minimum: 1, maximum: 200 } } } },
300
+ { name: "amp_write", description: "AMP: log a new entry. Required: type + msg (one sentence). Optional: file, line, tags, detail. Use 'detail' for a rich multi-paragraph body (repro steps, code, full reasoning, or a session snapshot) — it's stored in a sidecar and loaded on demand, so it never bloats the always-on memory index.", inputSchema: { type: "object", properties: { type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] }, msg: { type: "string", maxLength: 2000 }, file: { type: "string", maxLength: 1000 }, line: { type: "integer", minimum: 1, maximum: 10000000 }, tags: { type: "array", maxItems: 20, items: { type: "string", maxLength: 100 } }, detail: { type: "string", maxLength: 200000, description: "Optional rich body (Tier-2). Stored in the consolidated details store; NOT injected into rule files. Put the long-form context here; keep 'msg' to one summary sentence." } }, required: ["type","msg"] } },
301
+ { name: "amp_search", description: "AMP: search entries by keyword. Optional type filter.", inputSchema: { type: "object", properties: { query: { type: "string", maxLength: 500 }, type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] } }, required: ["query"] } },
302
+ { name: "amp_bookmark", description: "AMP: drop a named session bookmark — a resume point. Required: label (short name). Optional: note. If note is OMITTED, the current session transcript is auto-captured as the bookmark's context (the 'save everything here' resume point). Use when the user says 'bookmark this' / 'mark this point', or before a risky change / when the context window is filling up, so the exact state can be recalled later and appears in the next session's handoff. Bookmarks are never auto-pruned.", inputSchema: { type: "object", properties: { label: { type: "string", maxLength: 200 }, note: { type: "string", maxLength: 200000, description: "Optional explicit context. Omit to auto-capture the session transcript instead. Stored in a sidecar; not injected into rule files." } }, required: ["label"] } },
266
303
  { name: "amp_handoff", description: "AMP: generate the handoff document for the next AI session. format=markdown|json (default: markdown).", inputSchema: { type: "object", properties: { format: { type: "string", enum: ["markdown","json"] } } } },
267
304
  { name: "amp_health", description: "AMP: get the session health score (0-100, A-F grade).", inputSchema: { type: "object", properties: {} } },
268
305
 
269
306
  // ── Read-only contract helpers ───────────────────────────────────────────
270
307
  { name: "infernoflow_status", description: "Show project memory + contract health at a glance.", inputSchema: { type: "object", properties: {} } },
271
308
  { name: "infernoflow_check", description: "Validate the capability contract (read-only).", inputSchema: { type: "object", properties: {} } },
272
- { name: "infernoflow_context", description: "Generate AI-ready context for a task.", inputSchema: { type: "object", properties: { intent: { type: "string" }, working: { type: "string" } } } },
273
- { name: "infernoflow_git_drift", description: "Detect which capabilities may be affected by recent code changes — useful when memory needs branch-aware revalidation.", inputSchema: { type: "object", properties: { sinceCommits: { type: "number", description: "How many commits back to check (default: 1)" } } } },
309
+ { name: "infernoflow_context", description: "Generate AI-ready context for a task.", inputSchema: { type: "object", properties: { intent: { type: "string", maxLength: 1000 }, working: { type: "string", maxLength: 1000 } } } },
310
+ { name: "infernoflow_git_drift", description: "Detect which capabilities may be affected by recent code changes — useful when memory needs branch-aware revalidation.", inputSchema: { type: "object", properties: { sinceCommits: { type: "integer", minimum: 1, maximum: 100, description: "How many commits back to check (default: 1, max: 100)" } } } },
274
311
  ];
275
312
 
313
+ // ── Input validation ─────────────────────────────────────────────────────────
314
+ // SECURITY: tool arguments come from the model, and the model can be steered by
315
+ // anything it reads (a README, an issue, a memory entry pulled from git). The
316
+ // inputSchema above is advisory to the client only — nothing enforces it — so
317
+ // we enforce it here, before any argument reaches the CLI, git or the disk.
318
+ // Unknown properties are dropped (not rejected) so a chatty client still works.
319
+ const TOOL_SCHEMAS = new Map(TOOLS.map(t => [t.name, t.inputSchema]));
320
+
321
+ function checkValue(key, v, spec) {
322
+ const t = spec.type;
323
+ if (t === "string") {
324
+ if (typeof v !== "string") return `'${key}' must be a string`;
325
+ if (spec.maxLength && v.length > spec.maxLength) return `'${key}' is too long (max ${spec.maxLength} characters)`;
326
+ if (v.includes("\u0000")) return `'${key}' must not contain NUL characters`;
327
+ } else if (t === "integer" || t === "number") {
328
+ if (typeof v !== "number" || !Number.isFinite(v)) return `'${key}' must be a number`;
329
+ if (t === "integer" && !Number.isInteger(v)) return `'${key}' must be a whole number`;
330
+ if (spec.minimum !== undefined && v < spec.minimum) return `'${key}' must be >= ${spec.minimum}`;
331
+ if (spec.maximum !== undefined && v > spec.maximum) return `'${key}' must be <= ${spec.maximum}`;
332
+ } else if (t === "array") {
333
+ if (!Array.isArray(v)) return `'${key}' must be an array`;
334
+ if (spec.maxItems && v.length > spec.maxItems) return `'${key}' has too many items (max ${spec.maxItems})`;
335
+ for (const item of v) { const e = checkValue(`${key}[]`, item, spec.items || {}); if (e) return e; }
336
+ }
337
+ if (spec.enum && !spec.enum.includes(v)) return `'${key}' must be one of: ${spec.enum.join(", ")}`;
338
+ return null;
339
+ }
340
+
341
+ /** Returns { ok: true, value } with only known, valid properties, or { ok: false, error }. */
342
+ function validateToolInput(name, input) {
343
+ const schema = TOOL_SCHEMAS.get(name);
344
+ if (!schema) return { ok: false, error: `Unknown tool: ${name}` };
345
+ if (input === undefined || input === null) input = {};
346
+ if (typeof input !== "object" || Array.isArray(input)) return { ok: false, error: "arguments must be an object" };
347
+ const props = schema.properties || {};
348
+ const value = {};
349
+ for (const [key, spec] of Object.entries(props)) {
350
+ const v = input[key];
351
+ if (v === undefined || v === null) continue;
352
+ const err = checkValue(key, v, spec);
353
+ if (err) return { ok: false, error: `Invalid arguments for ${name}: ${err}` };
354
+ value[key] = v;
355
+ }
356
+ for (const req of schema.required || []) {
357
+ if (value[req] === undefined || value[req] === "") return { ok: false, error: `Invalid arguments for ${name}: '${req}' is required` };
358
+ }
359
+ return { ok: true, value };
360
+ }
361
+
362
+ /**
363
+ * Free text that is handed to the CLI as an argument. The CLI's own parser
364
+ * treats any argument equal to a flag name (e.g. "--watch", "--auto-push") as
365
+ * that flag, even when it arrives as a separate argv entry. Strip leading
366
+ * dashes so tool text can never be read as a CLI option.
367
+ */
368
+ function asCliText(v) {
369
+ return String(v ?? "").replace(/^[\s-]+/, "");
370
+ }
371
+
276
372
  // ── git drift detection (inline — no external imports in this template file) ─
277
373
  function detectGitDrift(sinceCommits) {
278
374
  const cwd = PROJECT_DIR;
279
375
  const infernoDir = path.join(cwd, "inferno");
280
376
 
281
- const runGit = (cmd) => {
282
- try { return execSync(cmd, { cwd, encoding: "utf8", timeout: 10_000 }); }
377
+ // SECURITY: git is run with an argument array and no shell.
378
+ const runGit = (args) => {
379
+ try { return execFileSync("git", args, { cwd, encoding: "utf8", timeout: 10_000, stdio: ["ignore", "pipe", "ignore"], windowsHide: true, shell: false }); }
283
380
  catch { return ""; }
284
381
  };
382
+ const n = Number.isInteger(sinceCommits) && sinceCommits >= 1 && sinceCommits <= 100 ? sinceCommits : 1;
285
383
 
286
384
  const changedSet = new Set();
287
385
  const addLines = (out) => out.split("\n").map(l => l.trim()).filter(Boolean).forEach(f => changedSet.add(f));
288
386
 
289
- addLines(runGit("git diff --name-only HEAD"));
290
- addLines(runGit(`git diff --name-only HEAD~${sinceCommits} HEAD`));
291
- addLines(runGit("git ls-files --others --exclude-standard"));
387
+ addLines(runGit(["diff", "--name-only", "HEAD"]));
388
+ addLines(runGit(["diff", "--name-only", `HEAD~${n}`, "HEAD"]));
389
+ addLines(runGit(["ls-files", "--others", "--exclude-standard"]));
292
390
 
293
391
  const changedFiles = Array.from(changedSet).sort();
294
392
  if (!changedFiles.length) return "No changed files detected since last commit.";
@@ -388,29 +486,35 @@ function detectGitDrift(sinceCommits) {
388
486
  return lines.join("\n");
389
487
  }
390
488
 
391
- function handleTool(id, name, input) {
489
+ function handleTool(id, name, rawInput) {
392
490
  try {
491
+ const checked = validateToolInput(name, rawInput);
492
+ if (!checked.ok) {
493
+ const code = checked.error.startsWith("Unknown tool") ? -32601 : -32602;
494
+ return sendError(id, code, checked.error);
495
+ }
496
+ const input = checked.value;
393
497
  let text = "";
394
498
  // ── Read-only contract helpers ─────────────────────────────────────────
395
499
  if (name === "infernoflow_check") {
396
- text = runCmd("check");
500
+ text = runCli(["check"]);
397
501
  } else if (name === "infernoflow_status") {
398
- text = runCmd("status");
502
+ text = runCli(["status"]);
399
503
  } else if (name === "infernoflow_context") {
400
- const parts = [];
401
- if (input.intent) parts.push(`--intent "${input.intent}"`);
402
- if (input.working) parts.push(`--working "${input.working}"`);
403
- text = runCmd("context " + parts.join(" "));
504
+ const args = ["context"];
505
+ if (input.intent) args.push("--intent", asCliText(input.intent));
506
+ if (input.working) args.push("--working", asCliText(input.working));
507
+ text = runCli(args);
404
508
  } else if (name === "infernoflow_git_drift") {
405
- text = detectGitDrift(input.sinceCommits || 1);
509
+ text = detectGitDrift(input.sinceCommits ?? 1);
406
510
 
407
511
  // ── AMP-spec memory tools ──────────────────────────────────────────────
408
512
  } else if (name === "amp_read") {
409
- const args = [];
410
- if (input.query) args.push(JSON.stringify(input.query));
513
+ const args = ["ask"];
514
+ if (input.query) args.push(asCliText(input.query));
411
515
  if (input.type) args.push("--type", input.type);
412
516
  if (input.limit) args.push("--limit", String(input.limit));
413
- text = runCmd("ask " + args.join(" "));
517
+ text = runCli(args);
414
518
  } else if (name === "amp_write") {
415
519
  // Prefer in-process write: no subprocess, no `npx` version skew, and
416
520
  // file/line/tags reach disk unchanged. The CLI fallback below is only
@@ -445,15 +549,13 @@ function handleTool(id, name, input) {
445
549
  return sendError(id, -32000, `amp_write failed (in-process): ${err.message}`);
446
550
  }
447
551
  } else {
448
- // Fallback: shell-out. Pass --file/--line/--tags through the CLI so
449
- // they're not silently dropped like in the original implementation.
450
- const t = (input.type || "note").replace(/[^a-z]/g, "");
451
- const m = JSON.stringify(input.msg || "");
452
- const extras = [];
453
- if (input.file) extras.push("--file", JSON.stringify(input.file));
454
- if (input.line) extras.push("--line", String(input.line));
455
- if (input.tags && input.tags.length) extras.push("--tags", JSON.stringify(input.tags.join(",")));
456
- text = runCmd(`log ${m} --type ${t} ${extras.join(" ")}`);
552
+ // Fallback: run the CLI (argument array, no shell). Pass
553
+ // --file/--line/--tags through so they're not silently dropped.
554
+ const args = ["log", asCliText(input.msg), "--type", input.type || "note"];
555
+ if (input.file) args.push("--file", asCliText(input.file));
556
+ if (input.line) args.push("--line", String(input.line));
557
+ if (input.tags && input.tags.length) args.push("--tags", input.tags.map(asCliText).join(","));
558
+ text = runCli(args);
457
559
  }
458
560
  } else if (name === "amp_bookmark") {
459
561
  // A bookmark is a `note` entry tagged "bookmark"; the optional `note`
@@ -485,13 +587,13 @@ function handleTool(id, name, input) {
485
587
  return sendError(id, -32000, `amp_bookmark failed (in-process): ${err.message}`);
486
588
  }
487
589
  } else {
488
- const l = JSON.stringify(input.label || "");
489
- const extras = input.note ? `--note ${JSON.stringify(input.note)}` : "";
490
- text = runCmd(`bookmark ${l} ${extras}`);
590
+ const args = ["bookmark", asCliText(input.label)];
591
+ if (input.note) args.push("--note", asCliText(input.note));
592
+ text = runCli(args);
491
593
  }
492
594
  } else if (name === "amp_handoff") {
493
595
  // switch writes a file; we read it back to return the content
494
- const switchResult = runCmd("switch");
596
+ const switchResult = runCli(["switch"]);
495
597
  if (isCmdError(switchResult)) {
496
598
  return sendError(id, -32000, `infernoflow switch failed: ${switchResult.message}\n${switchResult.stderr || switchResult.stdout || ""}`.trim());
497
599
  }
@@ -508,20 +610,20 @@ function handleTool(id, name, input) {
508
610
  text = "(handoff generated; could not read back: " + err.message + ")";
509
611
  }
510
612
  } else if (name === "amp_search") {
511
- const args = [JSON.stringify(input.query || "")];
613
+ const args = ["ask", asCliText(input.query)];
512
614
  if (input.type) args.push("--type", input.type);
513
- text = runCmd("ask " + args.join(" "));
615
+ text = runCli(args);
514
616
  } else if (name === "amp_health") {
515
- const recap = runCmd("recap --json");
617
+ const recap = runCli(["recap", "--json"]);
516
618
  if (isCmdError(recap)) {
517
- text = runCmd("status");
619
+ text = runCli(["status"]);
518
620
  } else {
519
- text = recap.trim() || runCmd("status");
621
+ text = recap.trim() || runCli(["status"]);
520
622
  }
521
623
 
522
624
  } else { return sendError(id, -32601, `Unknown tool: ${name}`); }
523
625
 
524
- // Central error check — if any runCmd() call produced a structured error,
626
+ // Central error check — if any runCli() call produced a structured error,
525
627
  // surface it as a real JSON-RPC error so the calling AI sees a proper
526
628
  // failure instead of garbled stderr text mixed into a "successful" reply.
527
629
  if (isCmdError(text)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.44.18",
3
+ "version": "0.44.20",
4
4
  "description": "Persistent memory for AI coding sessions — captures what agents can't infer from code alone. Works with Copilot, Cursor, Claude, and Windsurf.",
5
5
  "type": "module",
6
6
  "bin": {