infernoflow 0.44.19 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import*as C from"node:fs";import*as I from"node:path";import{execSync as M}from"node:child_process";import{bold as b,cyan as y,gray as t,green as k,yellow as $,red as D}from"../ui/output.mjs";import{readEntries as R}from"../amp/io.mjs";const N="inferno",v=I.join(N,"contract.json");function L(n){try{return JSON.parse(C.readFileSync(n,"utf8"))}catch{return null}}function A(n,s){if(s){const g=s.match(/^(\d+)h$/i),f=s.match(/^(\d+)d$/i);if(g)return new Date(Date.now()-parseInt(g[1])*36e5);if(f)return new Date(Date.now()-parseInt(f[1])*864e5);const r=new Date(s);if(!isNaN(r.getTime()))return r}const o=new Date(Date.now()-864e5),e=[];for(const g of n)if(g.type==="handoff"){const f=new Date(g.ts||0);isNaN(f.getTime())||e.push(f)}if(e.length===0)return o;const l=300*1e3,u=e[e.length-1];if(Date.now()-u.getTime()<l){if(e.length>=2){const g=e[e.length-2];return g>o?g:o}return o}return u>o?u:o}function P(n){const s=process.cwd(),o=f=>{try{return M(f,{cwd:s,encoding:"utf8",timeout:5e3,stdio:["pipe","pipe","pipe"]}).trim()}catch{return""}},e=n.toISOString().slice(0,19),l=o("git diff --cached --name-only"),u=o("git diff --name-only"),a=o(`git log --since="${e}" --name-only --pretty=format:""`);return[...new Set([...l.split(`
1
+ import{describeStore as M}from"../amp/io.mjs";import*as C from"node:fs";import*as I from"node:path";import{execSync as R}from"node:child_process";import{bold as b,cyan as y,gray as t,green as k,yellow as $,red as D}from"../ui/output.mjs";import{readEntries as v}from"../amp/io.mjs";const N="inferno",L=I.join(N,"contract.json");function A(n){try{return JSON.parse(C.readFileSync(n,"utf8"))}catch{return null}}function P(n,s){if(s){const g=s.match(/^(\d+)h$/i),f=s.match(/^(\d+)d$/i);if(g)return new Date(Date.now()-parseInt(g[1])*36e5);if(f)return new Date(Date.now()-parseInt(f[1])*864e5);const a=new Date(s);if(!isNaN(a.getTime()))return a}const o=new Date(Date.now()-864e5),e=[];for(const g of n)if(g.type==="handoff"){const f=new Date(g.ts||0);isNaN(f.getTime())||e.push(f)}if(e.length===0)return o;const l=300*1e3,u=e[e.length-1];if(Date.now()-u.getTime()<l){if(e.length>=2){const g=e[e.length-2];return g>o?g:o}return o}return u>o?u:o}function _(n){const s=process.cwd(),o=f=>{try{return R(f,{cwd:s,encoding:"utf8",timeout:5e3,stdio:["pipe","pipe","pipe"]}).trim()}catch{return""}},e=n.toISOString().slice(0,19),l=o("git diff --cached --name-only"),u=o("git diff --name-only"),r=o(`git log --since="${e}" --name-only --pretty=format:""`);return[...new Set([...l.split(`
2
2
  `),...u.split(`
3
- `),...a.split(`
4
- `)].map(f=>f.trim()).filter(Boolean))]}function _(n,s){const o=[{keywords:["auth","login","logout","session","jwt","token","password"],topic:"authentication"},{keywords:["stripe","payment","checkout","billing","subscription"],topic:"payments"},{keywords:["upload","file","s3","storage","bucket","cdn"],topic:"file handling"},{keywords:["email","sendgrid","ses","smtp","nodemailer","twilio"],topic:"notifications"},{keywords:["db","database","prisma","mongoose","postgres","mysql","migration"],topic:"database"},{keywords:["deploy","docker","ci","workflow","action","kubernetes"],topic:"deployment"},{keywords:["cache","redis","memcache"],topic:"caching"},{keywords:["test","spec","jest","vitest","cypress","playwright"],topic:"testing"},{keywords:["config","env",".env","environment","secret"],topic:"configuration"},{keywords:["api","route","endpoint","controller","handler"],topic:"API routes"},{keywords:["ui","component","style","css","tailwind","theme"],topic:"UI/styles"}],e=s.map(a=>(a.summary||"").toLowerCase()).join(" "),l=[],u=new Set;for(const a of o){if(u.has(a.topic))continue;const g=n.filter(r=>a.keywords.some(w=>r.toLowerCase().includes(w)));!g.length||a.keywords.some(r=>e.includes(r))||(u.add(a.topic),l.push({topic:a.topic,files:g.slice(0,3),suggestedType:"gotcha"}))}return l}function B(n){const s=new Set(n.map(l=>l.type));let o=0;const e=[];return n.length>0?(o+=20,e.push({ok:!0,label:`${n.length} entr${n.length!==1?"ies":"y"} logged`})):e.push({ok:!1,label:"nothing logged this session"}),s.has("gotcha")?(o+=35,e.push({ok:!0,label:"gotchas captured"})):e.push({ok:!1,label:"no gotchas (most valuable \u2014 log landmines!)"}),s.has("decision")?(o+=25,e.push({ok:!0,label:"decisions recorded"})):e.push({ok:!1,label:"no decisions recorded"}),s.has("attempt")&&(o+=10,e.push({ok:!0,label:"attempts tracked"})),s.has("preference")&&(o+=10,e.push({ok:!0,label:"preferences noted"})),{score:Math.min(o,100),checks:e}}function H(n){if(!n)return"";const s=new Date(n),o=Date.now()-s.getTime(),e=Math.floor(o/6e4);if(e<60)return`${e}m ago`;const l=Math.floor(o/36e5);return l<24?`${l}h ago`:`${Math.floor(o/864e5)}d ago`}const J={gotcha:"\u26A0",decision:"\u2713",attempt:"\u21BA",preference:"\u2666",theme:"\u25C8",note:"\xB7",error:"\u2717",handoff:"\u2192"},U={gotcha:$,decision:k,attempt:y,preference:y,theme:y,note:t,error:D,handoff:t};function G(n){const s=U[n.type]||t,o=J[n.type]||"\xB7",e=n.result?t(` [${n.result}]`):"",l=t(` (${H(n.ts)})`);console.log(` ${s(o+" "+(n.type||"note").padEnd(11))}${e}${l}`),console.log(` ${n.summary}`)}async function Q(n=[]){const s=n,o=s.includes("--json"),e=s.includes("--brief"),l=s.indexOf("--since"),u=l!==-1?s[l+1]:null,a=process.cwd();!C.existsSync(I.join(a,N))&&!C.existsSync(I.join(a,".ai-memory"))&&(o||console.error(D(` \u2718 not initialized \u2014 run: infernoflow init
5
- `)),process.exit(1));const g=R(a),f=A(g,u),r=g.filter(d=>new Date(d.ts||0)>f),w=P(f),m=_(w,r),{score:i,checks:O}=B(r),F=L(I.join(a,v));if(o){console.log(JSON.stringify({sessionStart:f.toISOString(),entries:r,changedFiles:w,unloggedTopics:m,health:{score:i,checks:O}},null,2));return}if(e){const d=i>=80?"A":i>=60?"B":i>=40?"C":"D",p=i>=60?k:i>=40?$:D;console.log(p(`Session health: ${d} (${i}/100)`)+t(` \u2014 ${r.length} entries logged`)),m.length&&console.log($(` ${m.length} topic${m.length!==1?"s":""} changed but not logged: `)+m.map(c=>c.topic).join(", "));return}const S=t(" "+"\u2500".repeat(52));console.log(),console.log(" "+b("\u{1F525} infernoflow recap")),F?.policyId&&console.log(t(` Project: ${F.policyId}`));const j=f.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"});if(console.log(t(` Session since: ${j}`)),console.log(S),console.log(),console.log(" "+b("Captured this session")),console.log(),r.length===0)console.log(t(" Nothing logged yet this session."));else{const d=["gotcha","decision","attempt","preference","theme","note","error"],p=new Map;for(const c of r){const h=c.type||"note";p.has(h)||p.set(h,[]),p.get(h).push(c)}for(const c of d){const h=p.get(c);if(h?.length)for(const x of h)console.log(),G(x)}}if(m.length>0){console.log(),console.log(S),console.log(),console.log(" "+b("Changed but not logged")+t(" (git diff since session start)")),console.log();for(const{topic:d,files:p}of m){console.log($(` ? ${d}`));for(const c of p)console.log(t(` ${c}`))}console.log(),console.log(t(" Any gotchas or decisions from these areas worth capturing?")),console.log(t(" Run: ")+y('infernoflow log "<what happened>" --type gotcha'))}else w.length>0&&(console.log(),console.log(S),console.log(),console.log(k(" \u2714 ")+t(`${w.length} changed files \u2014 all topics appear to be logged`)));console.log(),console.log(S),console.log(),console.log(" "+b("Session health")),console.log();const E=i>=80?"A":i>=60?"B":i>=40?"C":"D",T=i>=60?k:i>=40?$:D;console.log(` ${T(b(`${E}`))} ${T(`${i}/100`)}`),console.log();for(const{ok:d,label:p}of O){const c=d?k(" \u2714"):$(" \xB7");console.log(`${c} ${d?p:t(p)}`)}{const d=r.filter(h=>h.type==="gotcha").length,p=r.filter(h=>h.type==="decision").length,c=[];if(d===0?c.push(y('infernoflow log "..." --type gotcha')+t(" \u2014 adds 35 pts")):d<3&&i<80&&c.push(t(` ${3-d} more gotcha(s) would push you higher`)),p===0&&c.push(y('infernoflow log "..." --type decision')+t(" \u2014 adds 25 pts")),i>=60&&i<80&&c.push(t(" Almost B! One more entry gets you there.")),i>=80&&c.push(k(" Great session \u2014 your handoff will be excellent.")),c.length){console.log(),console.log(t(" How to improve:"));for(const h of c)console.log(" "+h)}}(r.length>0||m.length>0)&&(console.log(),console.log(S),console.log(),console.log(t(" Before your next session:")),console.log(t(" ")+y("infernoflow switch")+t(" \u2014 generate a handoff summary for the next AI agent")),console.log(t(" ")+y("infernoflow ask --recent")+t(" \u2014 review what's in memory before starting"))),console.log()}export{Q as recapCommand};
3
+ `),...r.split(`
4
+ `)].map(f=>f.trim()).filter(Boolean))]}function B(n,s){const o=[{keywords:["auth","login","logout","session","jwt","token","password"],topic:"authentication"},{keywords:["stripe","payment","checkout","billing","subscription"],topic:"payments"},{keywords:["upload","file","s3","storage","bucket","cdn"],topic:"file handling"},{keywords:["email","sendgrid","ses","smtp","nodemailer","twilio"],topic:"notifications"},{keywords:["db","database","prisma","mongoose","postgres","mysql","migration"],topic:"database"},{keywords:["deploy","docker","ci","workflow","action","kubernetes"],topic:"deployment"},{keywords:["cache","redis","memcache"],topic:"caching"},{keywords:["test","spec","jest","vitest","cypress","playwright"],topic:"testing"},{keywords:["config","env",".env","environment","secret"],topic:"configuration"},{keywords:["api","route","endpoint","controller","handler"],topic:"API routes"},{keywords:["ui","component","style","css","tailwind","theme"],topic:"UI/styles"}],e=s.map(r=>(r.summary||"").toLowerCase()).join(" "),l=[],u=new Set;for(const r of o){if(u.has(r.topic))continue;const g=n.filter(a=>r.keywords.some(w=>a.toLowerCase().includes(w)));!g.length||r.keywords.some(a=>e.includes(a))||(u.add(r.topic),l.push({topic:r.topic,files:g.slice(0,3),suggestedType:"gotcha"}))}return l}function H(n){const s=new Set(n.map(l=>l.type));let o=0;const e=[];return n.length>0?(o+=20,e.push({ok:!0,label:`${n.length} entr${n.length!==1?"ies":"y"} logged`})):e.push({ok:!1,label:"nothing logged this session"}),s.has("gotcha")?(o+=35,e.push({ok:!0,label:"gotchas captured"})):e.push({ok:!1,label:"no gotchas (most valuable \u2014 log landmines!)"}),s.has("decision")?(o+=25,e.push({ok:!0,label:"decisions recorded"})):e.push({ok:!1,label:"no decisions recorded"}),s.has("attempt")&&(o+=10,e.push({ok:!0,label:"attempts tracked"})),s.has("preference")&&(o+=10,e.push({ok:!0,label:"preferences noted"})),{score:Math.min(o,100),checks:e}}function J(n){if(!n)return"";const s=new Date(n),o=Date.now()-s.getTime(),e=Math.floor(o/6e4);if(e<60)return`${e}m ago`;const l=Math.floor(o/36e5);return l<24?`${l}h ago`:`${Math.floor(o/864e5)}d ago`}const U={gotcha:"\u26A0",decision:"\u2713",attempt:"\u21BA",preference:"\u2666",theme:"\u25C8",note:"\xB7",error:"\u2717",handoff:"\u2192"},G={gotcha:$,decision:k,attempt:y,preference:y,theme:y,note:t,error:D,handoff:t};function Y(n){const s=G[n.type]||t,o=U[n.type]||"\xB7",e=n.result?t(` [${n.result}]`):"",l=t(` (${J(n.ts)})`);console.log(` ${s(o+" "+(n.type||"note").padEnd(11))}${e}${l}`),console.log(` ${n.summary}`)}async function W(n=[]){const s=n,o=s.includes("--json"),e=s.includes("--brief"),l=s.indexOf("--since"),u=l!==-1?s[l+1]:null,r=process.cwd();!C.existsSync(I.join(r,N))&&!C.existsSync(I.join(r,".ai-memory"))&&(o||console.error(D(` \u2718 not initialized \u2014 run: infernoflow init
5
+ `)),process.exit(1));const g=v(r),f=P(g,u),a=g.filter(d=>new Date(d.ts||0)>f),w=_(f),m=B(w,a),{score:i,checks:O}=H(a),F=A(I.join(r,L));if(o){console.log(JSON.stringify({sessionStart:f.toISOString(),entries:a,changedFiles:w,unloggedTopics:m,health:{score:i,checks:O}},null,2));return}if(e){const d=i>=80?"A":i>=60?"B":i>=40?"C":"D",p=i>=60?k:i>=40?$:D;console.log(p(`Session health: ${d} (${i}/100)`)+t(` \u2014 ${a.length} entries logged`)),m.length&&console.log($(` ${m.length} topic${m.length!==1?"s":""} changed but not logged: `)+m.map(c=>c.topic).join(", "));return}const S=t(" "+"\u2500".repeat(52));console.log(),console.log(" "+b("\u{1F525} infernoflow recap")),console.log(" "+t(M(r))),F?.policyId&&console.log(t(` Project: ${F.policyId}`));const j=f.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"});if(console.log(t(` Session since: ${j}`)),console.log(S),console.log(),console.log(" "+b("Captured this session")),console.log(),a.length===0)console.log(t(" Nothing logged yet this session."));else{const d=["gotcha","decision","attempt","preference","theme","note","error"],p=new Map;for(const c of a){const h=c.type||"note";p.has(h)||p.set(h,[]),p.get(h).push(c)}for(const c of d){const h=p.get(c);if(h?.length)for(const x of h)console.log(),Y(x)}}if(m.length>0){console.log(),console.log(S),console.log(),console.log(" "+b("Changed but not logged")+t(" (git diff since session start)")),console.log();for(const{topic:d,files:p}of m){console.log($(` ? ${d}`));for(const c of p)console.log(t(` ${c}`))}console.log(),console.log(t(" Any gotchas or decisions from these areas worth capturing?")),console.log(t(" Run: ")+y('infernoflow log "<what happened>" --type gotcha'))}else w.length>0&&(console.log(),console.log(S),console.log(),console.log(k(" \u2714 ")+t(`${w.length} changed files \u2014 all topics appear to be logged`)));console.log(),console.log(S),console.log(),console.log(" "+b("Session health")),console.log();const E=i>=80?"A":i>=60?"B":i>=40?"C":"D",T=i>=60?k:i>=40?$:D;console.log(` ${T(b(`${E}`))} ${T(`${i}/100`)}`),console.log();for(const{ok:d,label:p}of O){const c=d?k(" \u2714"):$(" \xB7");console.log(`${c} ${d?p:t(p)}`)}{const d=a.filter(h=>h.type==="gotcha").length,p=a.filter(h=>h.type==="decision").length,c=[];if(d===0?c.push(y('infernoflow log "..." --type gotcha')+t(" \u2014 adds 35 pts")):d<3&&i<80&&c.push(t(` ${3-d} more gotcha(s) would push you higher`)),p===0&&c.push(y('infernoflow log "..." --type decision')+t(" \u2014 adds 25 pts")),i>=60&&i<80&&c.push(t(" Almost B! One more entry gets you there.")),i>=80&&c.push(k(" Great session \u2014 your handoff will be excellent.")),c.length){console.log(),console.log(t(" How to improve:"));for(const h of c)console.log(" "+h)}}(a.length>0||m.length>0)&&(console.log(),console.log(S),console.log(),console.log(t(" Before your next session:")),console.log(t(" ")+y("infernoflow switch")+t(" \u2014 generate a handoff summary for the next AI agent")),console.log(t(" ")+y("infernoflow ask --recent")+t(" \u2014 review what's in memory before starting"))),console.log()}export{W as recapCommand};
@@ -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 l from"node:fs";import*as d from"node:path";import"node:os";import{fileURLToPath as F}from"node:url";import{execSync as J}from"node:child_process";import{detectIdeContext as H}from"../ai/ideDetection.mjs";import{header as O,ok as y,warn as I,info as w,done as L,cyan as i,yellow as j,bold as h,green as f,gray as m}from"../ui/output.mjs";import"../cursorHooksInstall.mjs";import{refreshSecuritySensitiveCopies as N}from"../securityRefresh.mjs";import{updateProjectMcpJson as T,updateIdeMcpJson as U,updateClaudeDesktopPerProject as E,removeLegacyUserLevelEntry as S,claudeJsonPath as V,claudeDesktopConfigPath as C,runningFromNpxCache as W}from"../mcpRegistration.mjs";import"../vsCodeCopilotHooksInstall.mjs";import{updateInjectionConfig as K}from"../amp/io.mjs";import{findProjectRoot as q}from"../projectRoot.mjs";import{injectionPatchFromArgs as B}from"./refresh.mjs";const G=d.dirname(F(import.meta.url));function v(){return d.resolve(G,"../../templates")}function z(n){try{return J(`npx infernoflow ${n}`,{encoding:"utf8",cwd:process.cwd(),timeout:6e4,stdio:["inherit","pipe","pipe"]})}catch(s){return s.stdout||s.stderr||s.message}}function b(n){if(!l.existsSync(n))return{data:{},existed:!1,corrupt:!1,backup:null};let s;try{s=l.readFileSync(n,"utf8")}catch{return{data:{},existed:!0,corrupt:!1,backup:null}}try{const c=JSON.parse(s);return!c||typeof c!="object"||Array.isArray(c)?{data:{},existed:!0,corrupt:!1,backup:null}:{data:c,existed:!0,corrupt:!1,backup:null}}catch{let c=null;try{c=`${n}.corrupt-${Date.now()}.bak`,l.writeFileSync(c,s,"utf8")}catch{c=null}return{data:{},existed:!0,corrupt:!0,backup:c}}}function Q(n){const s=d.join(n,".git");if(!l.existsSync(s))return{installed:!1,skipped:"not-a-git-repo"};const c=d.join(s,"hooks"),o=d.join(c,"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(l.mkdirSync(c,{recursive:!0}),l.existsSync(o)){const t=l.readFileSync(o,"utf8");if(t.includes("infernoflow"))return{installed:!1,already:!0};const u=t.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
+ `;l.writeFileSync(o,u,"utf8")}else{const t=["#!/bin/sh","# infernoflow: auto-capture the commit subject into session memory.","# Best-effort and non-blocking \u2014 never fails a commit.",r,""].join(`
6
+ `);l.writeFileSync(o,t,"utf8")}try{l.chmodSync(o,493)}catch{}return{installed:!0,path:o}}catch(t){return{installed:!1,error:t.message}}}const X=2,_=`#!/usr/bin/env node
7
7
  // infernoflow UserPromptSubmit hook (Claude Code / Cursor).
8
+ // infernoflow-hook-version: ${X}
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,41 @@ 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(`
33
- ${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};
56
+ `;function Y(n){const s=d.join(n,".claude","hooks"),c=d.join(s,"log-frustration.mjs");try{l.mkdirSync(s,{recursive:!0}),l.writeFileSync(c,_,"utf8");try{l.chmodSync(c,493)}catch{}}catch(p){return{installed:!1,error:p.message}}const o=d.join(n,".claude","settings.json"),{data:r}=b(o);(!r.hooks||typeof r.hooks!="object")&&(r.hooks={}),Array.isArray(r.hooks.UserPromptSubmit)||(r.hooks.UserPromptSubmit=[]);const t="node .claude/hooks/log-frustration.mjs",u=r.hooks.UserPromptSubmit.some(p=>Array.isArray(p?.hooks)&&p.hooks.some(e=>typeof e?.command=="string"&&e.command.includes("log-frustration.mjs")));u||r.hooks.UserPromptSubmit.push({hooks:[{type:"command",command:t}]});try{l.mkdirSync(d.dirname(o),{recursive:!0}),l.writeFileSync(o,JSON.stringify(r,null,2),"utf8")}catch(p){return{installed:!0,registered:!1,error:p.message}}return{installed:!0,registered:!u}}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 ee(n,s){const c=d.join(n,".claude"),o=d.join(c,"settings.json"),{data:r}=b(o),t=new Set(r.allowedTools||[]);for(const p of Z)t.add(`mcp__infernoflow__${p}`);const u={...r,allowedTools:[...t]};return l.mkdirSync(c,{recursive:!0}),l.writeFileSync(o,JSON.stringify(u,null,2),"utf8"),o}function oe(n,{silent:s=!1}={}){const c=v(),o=s?()=>{}:e=>y(e),r=s?()=>{}:e=>I(e),t={mcpServer:!1,projectMcpJson:!1,claudeJson:!1,claudeSettings:!1,claudeDesktop:!1,gitHooks:!1,captureHook:!1,backups:[]},u=(()=>{try{return q(n)}catch{return n}})();try{const e=N(n,{captureHookScript:_});e.length&&(t.securityRefreshed=e,e.some(a=>a.endsWith("inferno-mcp-server.mjs"))&&(t.mcpServer=!0),o("Security update: replaced outdated "+e.map(a=>i(a)).join(", ")))}catch{}W()&&r("infernoflow is running from the npx cache \u2014 MCP configs will point there and break when the cache is cleared. Install it: npm i -g infernoflow, then run: infernoflow setup --yes");let p=!1;try{const e=T(u);e.skipped==="tracked"?r(".mcp.json is tracked by git \u2014 not adding this machine's paths to it. Add the infernoflow server with: claude mcp add infernoflow -s local -- node <infernoflow>/bin/infernoflow.mjs mcp"):e.skipped?r(".mcp.json is unreadable \u2014 left unchanged"):(p=!0,e.updated&&(t.projectMcpJson=!0,o("Registered MCP server in "+i(".mcp.json")+m(" (Claude Code, this project only)"))),e.gitignored&&o("Added "+i(".mcp.json")+" to .gitignore (it holds this machine's paths)"))}catch(e){r(".mcp.json update skipped: "+e.message)}if(p)try{const e=S(V());e.removed&&(t.legacyClaudeJsonRemoved=e,o("Removed the old user-level infernoflow entry from "+i("~/.claude.json")+m(` (was pinned to ${e.pinnedTo||"one project"}; backup: ${e.backup})`)))}catch(e){r("~/.claude.json cleanup skipped: "+e.message)}for(const[e,a]of[["vscode",".vscode/mcp.json"],["cursor",".cursor/mcp.json"]])try{U(u,e).updated&&(t[e==="vscode"?"vscodeMcp":"cursorMcp"]=!0,o("Registered MCP server in "+i(a)))}catch(g){r(a+" update skipped: "+g.message)}try{const e=E(u);if(e.updated&&(t.claudeDesktop=!0,o("Registered MCP server "+i(e.name)+" in "+i("claude_desktop_config.json")+m(" (Claude Desktop app)"))),!e.skipped){const a=S(C());a.removed&&(t.legacyDesktopRemoved=a,o("Removed the old pinned infernoflow entry from "+i("claude_desktop_config.json")+m(` (backup: ${a.backup})`)))}}catch(e){r("Claude Desktop config skipped: "+e.message)}try{ee(n,!1),t.claudeSettings=!0,o("Pre-approved infernoflow tools in "+i(".claude/settings.json"))}catch(e){r(".claude/settings.json skipped: "+e.message)}try{Q(n).installed&&(t.gitHooks=!0,o("Installed git post-commit hook \u2192 "+i(".git/hooks/post-commit")))}catch(e){r("git hook install skipped: "+e.message)}try{Y(n).installed&&(t.captureHook=!0,o("Installed capture hook \u2192 "+i(".claude/hooks/log-frustration.mjs")))}catch(e){r("capture hook install skipped: "+e.message)}return t}async function he(n){const s=process.cwd(),c=n.includes("--force")||n.includes("-f"),o=n.includes("--yes")||n.includes("-y"),r=v();O("infernoflow setup");const{ideDetected:t}=H("auto");w(`IDE detected: ${h(t==="cursor"?"Cursor":t==="vscode"?"VS Code":t==="windsurf"?"Windsurf":"unknown")}`);const p=d.join(s,".ai-memory");l.existsSync(p)?y(".ai-memory/ already exists \u2014 skipping init"):(console.log(`
57
+ ${j(".ai-memory/")} not found \u2014 running init ...
58
+ `),z(o?"init --yes":"init"));const e=B(n);if(Object.keys(e).length)try{K(s,e),y("Injection config updated \u2192 "+JSON.stringify(e))}catch{}console.log(),w("Wiring up MCP servers for Cursor / VS Code Copilot / Claude Code ...");const a=oe(s,{silent:!1});console.log(),L("infernoflow ready"),console.log(`
59
+ ${h("What was set up:")}`),console.log(` ${f("\u2714")} MCP server \u2192 ${i("infernoflow mcp")} ${m("(runs from the installed package)")}`),a.projectMcpJson&&console.log(` ${f("\u2714")} Claude Code MCP config \u2192 ${i(".mcp.json")} ${m("(this project)")}`),a.cursorMcp&&console.log(` ${f("\u2714")} Cursor MCP config \u2192 ${i(".cursor/mcp.json")}`),a.vscodeMcp&&console.log(` ${f("\u2714")} VS Code Copilot MCP config \u2192 ${i(".vscode/mcp.json")}`),a.legacyClaudeJsonRemoved&&console.log(` ${f("\u2714")} Removed old pinned entry from ${i("~/.claude.json")} ${m("(backup saved)")}`),a.claudeSettings&&console.log(` ${f("\u2714")} Auto-approved tools \u2192 ${i(".claude/settings.json")}`),a.claudeDesktop&&console.log(` ${f("\u2714")} Claude Desktop MCP config \u2192 ${i("claude_desktop_config.json")}`),a.gitHooks&&console.log(` ${f("\u2714")} Git post-commit hook \u2192 ${i(".git/hooks/post-commit")}`),a.captureHook&&console.log(` ${f("\u2714")} Capture hook (Claude Code/Cursor) \u2192 ${i(".claude/hooks/log-frustration.mjs")}`);try{const{detectStaleMcpRuntime:g}=await import("../mcpRuntime.mjs"),{readFileSync:$}=await import("node:fs"),{dirname:x,join:P}=await import("node:path"),{fileURLToPath:M}=await import("node:url"),R=x(M(import.meta.url)),A=P(R,"..","..","package.json"),D=JSON.parse($(A,"utf8")).version,k=g(s,D);k&&(console.log(),console.log(` ${j("\u26A0")} ${h("Restart required:")} ${k.message}`))}catch{}console.log(),console.log(` ${h("Next step:")} Restart your AI tool. Test by asking:`),console.log(` ${i('"call the amp_write tool with a test note"')}`),console.log()}export{_ as CAPTURE_HOOK_SCRIPT,X as CAPTURE_HOOK_VERSION,Z as MCP_TOOLS,oe as autoSetupMcp,C as claudeDesktopConfigPath,Y as installClaudeCodeCaptureHook,Q as installGitHooks,he as setupCommand,ee as writeClaudeSettings};
@@ -1,6 +1,6 @@
1
- import*as e from"node:fs";import*as l from"node:path";import{header as I,ok as L,fail as b,warn as A,section as $,bold as r,cyan as v,yellow as u,gray as n,green as h,red as R,white as V}from"../ui/output.mjs";import{readEntries as W}from"../amp/io.mjs";function M(f){const t=Math.floor((Date.now()-f)/1e3);return t<60?"just now":t<3600?`${Math.floor(t/60)}m ago`:t<86400?`${Math.floor(t/3600)}h ago`:`${Math.floor(t/86400)}d ago`}function _(f,t){const g=new Set;if(e.existsSync(f))for(const i of e.readdirSync(f).filter(y=>y.endsWith(".json")))try{(JSON.parse(e.readFileSync(l.join(f,i),"utf8")).capabilitiesCovered||[]).forEach(E=>g.add(E))}catch{}return{covered:t.filter(i=>g.has(i)),uncovered:t.filter(i=>!g.has(i))}}async function K(f=[]){const t=f.includes("--json"),g=process.cwd(),i=l.join(g,"inferno"),y=l.join(g,".ai-memory");t||I("status"),!e.existsSync(i)&&!e.existsSync(y)&&(t&&(console.log(JSON.stringify({ok:!1,error:"not_initialized",hint:"Run: infernoflow init"},null,2)),process.exit(1)),b("not initialized \u2014 neither .ai-memory/ nor inferno/ found","Run: infernoflow init"),console.log(),process.exit(1));const E=l.join(y,"amp.json"),J=l.join(i,"config.json"),m=l.join(i,"contract.json"),U=e.existsSync(E)||e.existsSync(y),D=(()=>{try{return JSON.parse(e.readFileSync(J,"utf8")).mode||null}catch{return null}})();if(U||D==="memory"||!e.existsSync(m)&&e.existsSync(J)){const o=W(g),s=o.filter(d=>d.type==="gotcha").length,c=o.filter(d=>d.type==="decision").length,p=o.filter(d=>d.type==="attempt").length,a=o[o.length-1];if(t){console.log(JSON.stringify({ok:!0,mode:"memory",entries:o.length,gotchas:s,decisions:c,attempts:p,lastEntry:a?a.ts:null},null,2));return}$("Session memory"),console.log(` ${n("entries")} ${r(String(o.length))}`),console.log(` ${n("gotchas")} ${r(String(s))}`),console.log(` ${n("decisions")} ${r(String(c))}`),console.log(` ${n("attempts")} ${r(String(p))}`),a&&console.log(` ${n("last entry")} ${n(M(new Date(a.ts).getTime()))}`),console.log(),o.length===0?console.log(` ${u("\u25CF")} ${r(u("empty"))} ${n("\u2014 log your first gotcha:")} ${v('infernoflow log "..." --type gotcha')}`):console.log(` ${h("\u25CF")} ${r(h("ready"))} ${n("\u2014 run")} ${v("infernoflow recap")} ${n("for the full session summary")}`),e.existsSync(m)?console.log():console.log(`
1
+ import*as e from"node:fs";import{describeStore as I}from"../amp/io.mjs";import*as l from"node:path";import{header as L,ok as V,fail as O,warn as A,section as $,bold as r,cyan as v,yellow as u,gray as n,green as h,red as R,white as W}from"../ui/output.mjs";import{readEntries as _}from"../amp/io.mjs";function M(g){const t=Math.floor((Date.now()-g)/1e3);return t<60?"just now":t<3600?`${Math.floor(t/60)}m ago`:t<86400?`${Math.floor(t/3600)}h ago`:`${Math.floor(t/86400)}d ago`}function z(g,t){const a=new Set;if(e.existsSync(g))for(const i of e.readdirSync(g).filter(y=>y.endsWith(".json")))try{(JSON.parse(e.readFileSync(l.join(g,i),"utf8")).capabilitiesCovered||[]).forEach(E=>a.add(E))}catch{}return{covered:t.filter(i=>a.has(i)),uncovered:t.filter(i=>!a.has(i))}}async function X(g=[]){const t=g.includes("--json"),a=process.cwd(),i=l.join(a,"inferno"),y=l.join(a,".ai-memory");t||L("status"),!e.existsSync(i)&&!e.existsSync(y)&&(t&&(console.log(JSON.stringify({ok:!1,error:"not_initialized",hint:"Run: infernoflow init"},null,2)),process.exit(1)),O("not initialized \u2014 neither .ai-memory/ nor inferno/ found","Run: infernoflow init"),console.log(),process.exit(1));const E=l.join(y,"amp.json"),J=l.join(i,"config.json"),m=l.join(i,"contract.json"),U=e.existsSync(E)||e.existsSync(y),D=(()=>{try{return JSON.parse(e.readFileSync(J,"utf8")).mode||null}catch{return null}})();if(U||D==="memory"||!e.existsSync(m)&&e.existsSync(J)){const o=_(a),s=o.filter(d=>d.type==="gotcha").length,c=o.filter(d=>d.type==="decision").length,p=o.filter(d=>d.type==="attempt").length,f=o[o.length-1];if(t){console.log(JSON.stringify({ok:!0,mode:"memory",entries:o.length,gotchas:s,decisions:c,attempts:p,lastEntry:f?f.ts:null},null,2));return}$("Session memory"),console.log(` ${n(I(a))}`),console.log(` ${n("entries")} ${r(String(o.length))}`),console.log(` ${n("gotchas")} ${r(String(s))}`),console.log(` ${n("decisions")} ${r(String(c))}`),console.log(` ${n("attempts")} ${r(String(p))}`),f&&console.log(` ${n("last entry")} ${n(M(new Date(f.ts).getTime()))}`),console.log(),o.length===0?console.log(` ${u("\u25CF")} ${r(u("empty"))} ${n("\u2014 log your first gotcha:")} ${v('infernoflow log "..." --type gotcha')}`):console.log(` ${h("\u25CF")} ${r(h("ready"))} ${n("\u2014 run")} ${v("infernoflow recap")} ${n("for the full session summary")}`),e.existsSync(m)?console.log():console.log(`
2
2
  ${n("Want capability contracts + CI gates? Run:")} ${v("infernoflow init --mode full")}
3
- `);return}e.existsSync(m)||(t&&(console.log(JSON.stringify({ok:!1,error:"contract_not_found"},null,2)),process.exit(1)),b("contract.json not found"),console.log(),process.exit(1));const S=JSON.parse(e.readFileSync(m,"utf8")),w=S.capabilities||[],k=e.statSync(m),C=l.join(i,"scenarios"),N=l.join(i,"CHANGELOG.md"),F=l.join(i,"capabilities.json"),{covered:H,uncovered:j}=_(C,w),G=e.existsSync(N)&&/##\s+Unreleased/i.test(e.readFileSync(N,"utf8")),x=[];j.length>0&&x.push(`${j.length} capabilities without scenario coverage`),G||x.push("CHANGELOG missing ## Unreleased section");const O=x.length===0;if(t){const o={ok:O,driftReasons:x,project:{policyId:S.policyId||null,policyVersion:S.policyVersion||null,lastChange:M(k.mtimeMs)},capabilities:{total:w.length,uncovered:j},changelog:{hasUnreleased:G}};console.log(JSON.stringify(o,null,2)),process.exit(O?0:1)}O||($("Drift"),x.forEach(o=>console.log(` ${u("\u26A0")} ${o}`))),$("Project"),console.log(` ${n("policy")} ${r(S.policyId||"\u2014")}`),console.log(` ${n("version")} ${r("v"+(S.policyVersion||"?"))}`),console.log(` ${n("last change")} ${n(M(k.mtimeMs))}`),$(`Capabilities ${n("("+w.length+")")}`);let P={};if(e.existsSync(F))try{(JSON.parse(e.readFileSync(F,"utf8")).capabilities||[]).forEach(s=>{P[s.id]=s})}catch{}if(w.forEach(o=>{const s=P[o],p=H.includes(o)?h("\u2714"):R("\u2718"),a=s?.title?n(` \u2014 ${s.title}`):"",d=s?.since?n(` [${s.since}]`):"";console.log(` ${p} ${V(o)}${a}${d}`)}),j.length>0?console.log(`
3
+ `);return}e.existsSync(m)||(t&&(console.log(JSON.stringify({ok:!1,error:"contract_not_found"},null,2)),process.exit(1)),O("contract.json not found"),console.log(),process.exit(1));const S=JSON.parse(e.readFileSync(m,"utf8")),w=S.capabilities||[],k=e.statSync(m),C=l.join(i,"scenarios"),N=l.join(i,"CHANGELOG.md"),F=l.join(i,"capabilities.json"),{covered:H,uncovered:j}=z(C,w),G=e.existsSync(N)&&/##\s+Unreleased/i.test(e.readFileSync(N,"utf8")),x=[];j.length>0&&x.push(`${j.length} capabilities without scenario coverage`),G||x.push("CHANGELOG missing ## Unreleased section");const b=x.length===0;if(t){const o={ok:b,driftReasons:x,project:{policyId:S.policyId||null,policyVersion:S.policyVersion||null,lastChange:M(k.mtimeMs)},capabilities:{total:w.length,uncovered:j},changelog:{hasUnreleased:G}};console.log(JSON.stringify(o,null,2)),process.exit(b?0:1)}b||($("Drift"),x.forEach(o=>console.log(` ${u("\u26A0")} ${o}`))),$("Project"),console.log(` ${n("policy")} ${r(S.policyId||"\u2014")}`),console.log(` ${n("version")} ${r("v"+(S.policyVersion||"?"))}`),console.log(` ${n("last change")} ${n(M(k.mtimeMs))}`),$(`Capabilities ${n("("+w.length+")")}`);let P={};if(e.existsSync(F))try{(JSON.parse(e.readFileSync(F,"utf8")).capabilities||[]).forEach(s=>{P[s.id]=s})}catch{}if(w.forEach(o=>{const s=P[o],p=H.includes(o)?h("\u2714"):R("\u2718"),f=s?.title?n(` \u2014 ${s.title}`):"",d=s?.since?n(` [${s.since}]`):"";console.log(` ${p} ${W(o)}${f}${d}`)}),j.length>0?console.log(`
4
4
  ${u("\u26A0")} ${j.length} capability(ies) lack scenario coverage`):console.log(`
5
- ${h("\u2714")} All capabilities have scenario coverage`),$("Scenarios"),e.existsSync(C)){const o=e.readdirSync(C).filter(s=>s.endsWith(".json"));o.length===0?A("No scenario files \u2014 add .json files to inferno/scenarios/"):o.forEach(s=>{try{const c=JSON.parse(e.readFileSync(l.join(C,s),"utf8")),p=c.steps?.length||0,a=(c.capabilitiesCovered||[]).length;console.log(` ${h("\u2714")} ${v(s)} ${n(`\u2014 ${p} steps, ${a} caps covered`)}`)}catch{console.log(` ${R("\u2718")} ${v(s)} ${n("\u2014 invalid JSON")}`)}})}else A("scenarios/ directory not found");if($("Changelog"),e.existsSync(N)){const o=e.readFileSync(N,"utf8");/##\s+Unreleased/i.test(o)?L("Has ## Unreleased section"):b("Missing ## Unreleased section"),o.split(`
6
- `).filter(c=>/^##\s/.test(c)).slice(0,3).forEach(c=>console.log(` ${n(c)}`))}else b("inferno/CHANGELOG.md not found");console.log(),console.log(O?` ${h("\u25CF")} ${r(h("ready"))} ${n("\u2014 run infernoflow check for full validation")}`:` ${u("\u25CF")} ${r(u("needs attention"))} ${n("\u2014 run infernoflow check for details")}`),console.log()}export{K as statusCommand};
5
+ ${h("\u2714")} All capabilities have scenario coverage`),$("Scenarios"),e.existsSync(C)){const o=e.readdirSync(C).filter(s=>s.endsWith(".json"));o.length===0?A("No scenario files \u2014 add .json files to inferno/scenarios/"):o.forEach(s=>{try{const c=JSON.parse(e.readFileSync(l.join(C,s),"utf8")),p=c.steps?.length||0,f=(c.capabilitiesCovered||[]).length;console.log(` ${h("\u2714")} ${v(s)} ${n(`\u2014 ${p} steps, ${f} caps covered`)}`)}catch{console.log(` ${R("\u2718")} ${v(s)} ${n("\u2014 invalid JSON")}`)}})}else A("scenarios/ directory not found");if($("Changelog"),e.existsSync(N)){const o=e.readFileSync(N,"utf8");/##\s+Unreleased/i.test(o)?V("Has ## Unreleased section"):O("Missing ## Unreleased section"),o.split(`
6
+ `).filter(c=>/^##\s/.test(c)).slice(0,3).forEach(c=>console.log(` ${n(c)}`))}else O("inferno/CHANGELOG.md not found");console.log(),console.log(b?` ${h("\u25CF")} ${r(h("ready"))} ${n("\u2014 run infernoflow check for full validation")}`:` ${u("\u25CF")} ${r(u("needs attention"))} ${n("\u2014 run infernoflow check for details")}`),console.log()}export{X as statusCommand};
@@ -1,13 +1,13 @@
1
- import*as a from"node:fs";import*as i from"node:path";import*as x from"node:os";import*as K from"node:readline";import{bold as I,gray as u,green as D,yellow as J,red as N}from"../ui/output.mjs";const y="inferno",O="CLAUDE.md",k=".claude",S=".cursor",b=i.join(S,"inferno-mcp-server.mjs"),E="inferno-mcp-server.mjs",d=i.join(S,"hooks.json"),P=i.join(S,"hooks","inferno-session-draft.mjs"),v=i.join(S,"mcp.json"),w=i.join(x.homedir(),".claude.json"),L=[".git/hooks/post-commit",".git/hooks/pre-push"],C="# infernoflow";function h(t){return a.existsSync(t)}function m(t){try{return JSON.parse(a.readFileSync(t,"utf8"))}catch{return null}}function F(t){try{return a.readFileSync(t,"utf8")}catch{return null}}function W(t){return new Promise(e=>{const s=K.createInterface({input:process.stdin,output:process.stdout});s.question(t,o=>{s.close(),e(o)})})}function $(t,e,s){const o=[],n=i.join(t,y);if(!h(n))return o;if(s)return o.push({type:"skip",path:y,reason:"--keep-inferno"}),o;if(e){const c=a.readdirSync(n);for(const l of c)if(l==="sessions.jsonl")o.push({type:"skip",path:i.join(y,l),reason:"--keep-memory"});else{const r=i.join(n,l);a.statSync(r).isDirectory()?o.push({type:"rmdir",path:i.join(y,l)}):o.push({type:"rm",path:i.join(y,l)})}}else o.push({type:"rmdir",path:y});return o}function G(t){const e=i.join(t,O);return h(e)?[{type:"rm",path:O}]:[]}function T(t){const e=[],s=i.join(t,k,"settings.json");if(!h(s))return e;const o=m(s),n=o?.tools?.some?.(l=>l.startsWith?.("mcp__infernoflow")),c=o&&Object.keys(o).some(l=>l==="tools"?(o.tools||[]).some(r=>!r.startsWith("mcp__infernoflow")):l!=="tools");return n&&!c?e.push({type:"rm",path:i.join(k,"settings.json"),desc:"auto-approved tools"}):n&&e.push({type:"edit",path:i.join(k,"settings.json"),desc:"remove infernoflow tools (preserve other content)"}),e}function V(t){const e=[];h(i.join(t,b))&&e.push({type:"rm",path:b}),h(i.join(t,E))&&e.push({type:"rm",path:E}),h(i.join(t,P))&&e.push({type:"rm",path:P});const s=i.join(t,d);if(h(s)){const n=m(s)?.hooks,c=Array.isArray(n)?n:n&&typeof n=="object"?Object.values(n).flatMap(r=>Array.isArray(r)?r:[r]).filter(Boolean):[];c.length>0&&c.every(r=>(r?.name||r?.command||"").includes("inferno"))?e.push({type:"rm",path:d,desc:"infernoflow-only hooks config"}):e.push({type:"edit",path:d,desc:"remove infernoflow hook entry (preserve others)"})}return e}function q(t){const e=i.join(t,v);if(!h(e))return[];const s=m(e);return s?.mcpServers?.infernoflow?Object.keys(s.mcpServers||{}).filter(n=>n!=="infernoflow").length===0&&Object.keys(s).length===1?[{type:"rm",path:v,desc:"infernoflow-only file"}]:[{type:"edit",path:v,desc:'remove "infernoflow" key (preserve other servers)'}]:[]}function B(){return h(w)?m(w)?.mcpServers?.infernoflow?[{type:"edit",path:"~/.claude.json",desc:'remove "infernoflow" MCP entry (preserve other entries)',_realPath:w}]:[]:[]}function Y(t){const e=[];for(const s of L){const o=i.join(t,s);if(!h(o))continue;const n=F(o);if(!n?.includes(C))continue;const c=n.split(`
2
- `),l=c.findIndex(f=>f.includes(C)),r=c.slice(0,l).join(`
3
- `).trim();!r||r==="#!/bin/sh"||r==="#!/bin/bash"?e.push({type:"rm",path:s,desc:"infernoflow-only hook"}):e.push({type:"edit",path:s,desc:"remove infernoflow section (preserve existing hooks)"})}return e}function z(t,e,s){for(const o of e){if(o.type==="skip")continue;const n=i.join(t,o.path);if(!s)try{o.type==="rmdir"?a.rmSync(n,{recursive:!0,force:!0}):a.unlinkSync(n)}catch{}}}function Q(t,e){if(!e)try{a.unlinkSync(i.join(t,O))}catch{}}function X(t,e,s){const o=i.join(t,k,"settings.json");for(const n of e)if(!s){if(n.type==="rm")try{a.unlinkSync(i.join(t,n.path))}catch{}else if(n.type==="edit")try{const c=m(o);c?.tools&&(c.tools=c.tools.filter(l=>!l.startsWith("mcp__infernoflow"))),a.writeFileSync(o,JSON.stringify(c,null,2)+`
4
- `,"utf8")}catch{}}}function Z(t,e,s){if(!s){for(const o of e)if(o.type==="rm")try{a.unlinkSync(i.join(t,o.path))}catch{}else if(o.type==="edit"&&o.path===d)try{const n=m(i.join(t,d));n?.hooks&&(n.hooks=n.hooks.filter(c=>!(c.name||c.command||"").includes("inferno"))),a.writeFileSync(i.join(t,d),JSON.stringify(n,null,2)+`
5
- `,"utf8")}catch{}}}function oo(t,e,s){const o=i.join(t,v);for(const n of e)if(!s){if(n.type==="rm")try{a.unlinkSync(o)}catch{}else if(n.type==="edit")try{const c=m(o);c?.mcpServers?.infernoflow&&delete c.mcpServers.infernoflow,a.writeFileSync(o,JSON.stringify(c,null,2)+`
6
- `,"utf8")}catch{}}}function eo(t,e){for(const s of t){if(e)continue;const o=s._realPath||w;if(s.type==="edit")try{const n=m(o);n?.mcpServers?.infernoflow&&delete n.mcpServers.infernoflow,a.writeFileSync(o,JSON.stringify(n,null,2)+`
7
- `,"utf8")}catch{}}}function no(t,e,s){for(const o of e){const n=i.join(t,o.path);if(!s){if(o.type==="rm")try{a.unlinkSync(n)}catch{}else if(o.type==="edit")try{const l=F(n).split(`
8
- `),r=l.findIndex(g=>g.includes(C)),f=l.slice(0,r).join(`
1
+ import{claudeDesktopConfigPath as U,isInfernoflowEntry as x}from"../mcpRegistration.mjs";import{findProjectRoot as L}from"../projectRoot.mjs";import*as a from"node:fs";import*as i from"node:path";import*as W from"node:os";import*as $ from"node:readline";import{bold as I,gray as h,green as D,yellow as P,red as J}from"../ui/output.mjs";const y="inferno",_="CLAUDE.md",v=".claude",k=".cursor",E=i.join(k,"inferno-mcp-server.mjs"),N="inferno-mcp-server.mjs",d=i.join(k,"hooks.json"),b=i.join(k,"hooks","inferno-session-draft.mjs"),S=i.join(k,"mcp.json"),w=i.join(W.homedir(),".claude.json"),T=[".git/hooks/post-commit",".git/hooks/pre-push"],O="# infernoflow";function u(t){return a.existsSync(t)}function m(t){try{return JSON.parse(a.readFileSync(t,"utf8"))}catch{return null}}function F(t){try{return a.readFileSync(t,"utf8")}catch{return null}}function G(t){return new Promise(e=>{const s=$.createInterface({input:process.stdin,output:process.stdout});s.question(t,o=>{s.close(),e(o)})})}function V(t,e,s){const o=[],n=i.join(t,y);if(!u(n))return o;if(s)return o.push({type:"skip",path:y,reason:"--keep-inferno"}),o;if(e){const c=a.readdirSync(n);for(const l of c)if(l==="sessions.jsonl")o.push({type:"skip",path:i.join(y,l),reason:"--keep-memory"});else{const r=i.join(n,l);a.statSync(r).isDirectory()?o.push({type:"rmdir",path:i.join(y,l)}):o.push({type:"rm",path:i.join(y,l)})}}else o.push({type:"rmdir",path:y});return o}function q(t){const e=i.join(t,_);return u(e)?[{type:"rm",path:_}]:[]}function B(t){const e=[],s=i.join(t,v,"settings.json");if(!u(s))return e;const o=m(s),n=o?.tools?.some?.(l=>l.startsWith?.("mcp__infernoflow")),c=o&&Object.keys(o).some(l=>l==="tools"?(o.tools||[]).some(r=>!r.startsWith("mcp__infernoflow")):l!=="tools");return n&&!c?e.push({type:"rm",path:i.join(v,"settings.json"),desc:"auto-approved tools"}):n&&e.push({type:"edit",path:i.join(v,"settings.json"),desc:"remove infernoflow tools (preserve other content)"}),e}function Y(t){const e=[];u(i.join(t,E))&&e.push({type:"rm",path:E}),u(i.join(t,N))&&e.push({type:"rm",path:N}),u(i.join(t,b))&&e.push({type:"rm",path:b});const s=i.join(t,d);if(u(s)){const n=m(s)?.hooks,c=Array.isArray(n)?n:n&&typeof n=="object"?Object.values(n).flatMap(r=>Array.isArray(r)?r:[r]).filter(Boolean):[];c.length>0&&c.every(r=>(r?.name||r?.command||"").includes("inferno"))?e.push({type:"rm",path:d,desc:"infernoflow-only hooks config"}):e.push({type:"edit",path:d,desc:"remove infernoflow hook entry (preserve others)"})}return e}function z(t){const e=i.join(t,S);if(!u(e))return[];const s=m(e);return s?.mcpServers?.infernoflow?Object.keys(s.mcpServers||{}).filter(n=>n!=="infernoflow").length===0&&Object.keys(s).length===1?[{type:"rm",path:S,desc:"infernoflow-only file"}]:[{type:"edit",path:S,desc:'remove "infernoflow" key (preserve other servers)'}]:[]}function Q(t){const e=[];u(w)&&x(m(w)?.mcpServers?.infernoflow)&&e.push({type:"edit",path:"~/.claude.json",desc:'remove "infernoflow" MCP entry (preserve other entries)',_realPath:w,_key:"infernoflow"});const s=i.join(t,".mcp.json");u(s)&&m(s)?.mcpServers?.infernoflow&&e.push({type:"edit",path:".mcp.json",desc:'remove "infernoflow" MCP entry (preserve other servers)',_realPath:s,_key:"infernoflow"});const o=i.join(t,".vscode","mcp.json");u(o)&&m(o)?.servers?.infernoflow&&e.push({type:"edit",path:".vscode/mcp.json",desc:'remove "infernoflow" MCP entry (preserve other servers)',_realPath:o,_key:"infernoflow",_serversKey:"servers"});const n=U(),c=u(n)?m(n):null;for(const[l,r]of Object.entries(c?.mcpServers||{})){if(!l.startsWith("infernoflow"))continue;const f=r?.env?.INFERNOFLOW_PROJECT_DIR;f&&i.resolve(f)===i.resolve(t)&&e.push({type:"edit",path:"claude_desktop_config.json",desc:`remove "${l}" MCP entry (preserve other entries)`,_realPath:n,_key:l})}return e}function X(t){const e=[];for(const s of T){const o=i.join(t,s);if(!u(o))continue;const n=F(o);if(!n?.includes(O))continue;const c=n.split(`
2
+ `),l=c.findIndex(f=>f.includes(O)),r=c.slice(0,l).join(`
3
+ `).trim();!r||r==="#!/bin/sh"||r==="#!/bin/bash"?e.push({type:"rm",path:s,desc:"infernoflow-only hook"}):e.push({type:"edit",path:s,desc:"remove infernoflow section (preserve existing hooks)"})}return e}function Z(t,e,s){for(const o of e){if(o.type==="skip")continue;const n=i.join(t,o.path);if(!s)try{o.type==="rmdir"?a.rmSync(n,{recursive:!0,force:!0}):a.unlinkSync(n)}catch{}}}function ee(t,e){if(!e)try{a.unlinkSync(i.join(t,_))}catch{}}function oe(t,e,s){const o=i.join(t,v,"settings.json");for(const n of e)if(!s){if(n.type==="rm")try{a.unlinkSync(i.join(t,n.path))}catch{}else if(n.type==="edit")try{const c=m(o);c?.tools&&(c.tools=c.tools.filter(l=>!l.startsWith("mcp__infernoflow"))),a.writeFileSync(o,JSON.stringify(c,null,2)+`
4
+ `,"utf8")}catch{}}}function ne(t,e,s){if(!s){for(const o of e)if(o.type==="rm")try{a.unlinkSync(i.join(t,o.path))}catch{}else if(o.type==="edit"&&o.path===d)try{const n=m(i.join(t,d));n?.hooks&&(n.hooks=n.hooks.filter(c=>!(c.name||c.command||"").includes("inferno"))),a.writeFileSync(i.join(t,d),JSON.stringify(n,null,2)+`
5
+ `,"utf8")}catch{}}}function te(t,e,s){const o=i.join(t,S);for(const n of e)if(!s){if(n.type==="rm")try{a.unlinkSync(o)}catch{}else if(n.type==="edit")try{const c=m(o);if(!c)continue;c?.mcpServers?.infernoflow&&delete c.mcpServers.infernoflow,a.writeFileSync(o,JSON.stringify(c,null,2)+`
6
+ `,"utf8")}catch{}}}function se(t,e){for(const s of t){if(e)continue;const o=s._realPath||w;if(s.type==="edit")try{const n=m(o);if(!n)continue;const c=s._key||"infernoflow",l=s._serversKey?n?.[s._serversKey]:n?.mcpServers;if(!l?.[c])continue;delete l[c],a.writeFileSync(o,JSON.stringify(n,null,2)+`
7
+ `,"utf8")}catch{}}}function re(t,e,s){for(const o of e){const n=i.join(t,o.path);if(!s){if(o.type==="rm")try{a.unlinkSync(n)}catch{}else if(o.type==="edit")try{const l=F(n).split(`
8
+ `),r=l.findIndex(g=>g.includes(O)),f=l.slice(0,r).join(`
9
9
  `).trimEnd();a.writeFileSync(n,f+`
10
- `,"utf8")}catch{}}}}async function ro(t=[]){const e=p=>t.includes(p),s=e("--dry-run")||e("--dry"),o=e("--keep-memory"),n=e("--keep-inferno"),c=e("--yes")||e("-y"),l=e("--json"),r=process.cwd(),f={infernoDir:$(r,o,n),claudeMd:G(r),claudeDir:T(r),cursorMcpServer:V(r),cursorMcpJson:q(r),claudeJson:B(),gitHooks:Y(r)},g=Object.values(f).flat(),j=g.filter(p=>p.type!=="skip");if(l){console.log(JSON.stringify({dryRun:s,keepMemory:o,keepInferno:n,plan:f,actionCount:j.length},null,2));return}const M=u(" "+"\u2500".repeat(52));if(console.log(),console.log(" "+I("\u{1F525} infernoflow uninstall")),s&&console.log(J(" DRY RUN \u2014 nothing will be changed")),console.log(M),j.length===0){console.log(),console.log(D(" \u2714 Nothing to remove \u2014 infernoflow is not installed in this project")),console.log();return}console.log(),console.log(" "+I("Will remove:")),console.log();const A={rm:N(" \u2716"),rmdir:N(" \u2716"),edit:J(" ~"),skip:u(" \xB7")};for(const p of g){const H=A[p.type]||" ?",U=p.desc?u(` (${p.desc})`):"";console.log(`${H} ${p.path}${U}`)}if(o&&(console.log(),console.log(u(" \u2139 inferno/sessions.jsonl will be preserved (--keep-memory)"))),console.log(),!s&&!c){if(!(await W(" Continue? "+u("[y/N] "))).trim().toLowerCase().startsWith("y")){console.log(u(`
10
+ `,"utf8")}catch{}}}}async function pe(t=[]){const e=p=>t.includes(p),s=e("--dry-run")||e("--dry"),o=e("--keep-memory"),n=e("--keep-inferno"),c=e("--yes")||e("-y"),l=e("--json"),r=process.cwd(),f={infernoDir:V(r,o,n),claudeMd:q(r),claudeDir:B(r),cursorMcpServer:Y(r),cursorMcpJson:z(r),claudeJson:Q((()=>{try{return L(r)}catch{return r}})()),gitHooks:X(r)},g=Object.values(f).flat(),j=g.filter(p=>p.type!=="skip");if(l){console.log(JSON.stringify({dryRun:s,keepMemory:o,keepInferno:n,plan:f,actionCount:j.length},null,2));return}const C=h(" "+"\u2500".repeat(52));if(console.log(),console.log(" "+I("\u{1F525} infernoflow uninstall")),s&&console.log(P(" DRY RUN \u2014 nothing will be changed")),console.log(C),j.length===0){console.log(),console.log(D(" \u2714 Nothing to remove \u2014 infernoflow is not installed in this project")),console.log();return}console.log(),console.log(" "+I("Will remove:")),console.log();const A={rm:J(" \u2716"),rmdir:J(" \u2716"),edit:P(" ~"),skip:h(" \xB7")};for(const p of g){const H=A[p.type]||" ?",K=p.desc?h(` (${p.desc})`):"";console.log(`${H} ${p.path}${K}`)}if(o&&(console.log(),console.log(h(" \u2139 inferno/sessions.jsonl will be preserved (--keep-memory)"))),console.log(),!s&&!c){if(!(await G(" Continue? "+h("[y/N] "))).trim().toLowerCase().startsWith("y")){console.log(h(`
11
11
  Aborted \u2014 nothing changed.
12
- `));return}console.log()}if(s){console.log(u(` \u2191 Dry run complete \u2014 run without --dry-run to apply
13
- `));return}z(r,f.infernoDir,!1),f.claudeMd.length&&Q(r,!1),f.claudeDir.length&&X(r,f.claudeDir,!1),f.cursorMcpServer.length&&Z(r,f.cursorMcpServer,!1),f.cursorMcpJson.length&&oo(r,f.cursorMcpJson,!1),f.claudeJson.length&&eo(f.claudeJson,!1),f.gitHooks.length&&no(r,f.gitHooks,!1),console.log(M),console.log(),console.log(D(" \u2714 infernoflow removed from this project")),console.log();const R=j.filter(p=>p.type==="edit"),_=j.filter(p=>p.type==="rm"||p.type==="rmdir");_.length&&console.log(u(" Deleted: ")+_.map(p=>p.path).join(", ")),R.length&&console.log(u(" Edited: ")+R.map(p=>p.path).join(", ")),o&&(console.log(),console.log(u(" Session memory kept \u2192 inferno/sessions.jsonl")),console.log(u(" Re-run infernoflow init to restore the rest."))),console.log()}export{ro as uninstallCommand};
12
+ `));return}console.log()}if(s){console.log(h(` \u2191 Dry run complete \u2014 run without --dry-run to apply
13
+ `));return}Z(r,f.infernoDir,!1),f.claudeMd.length&&ee(r,!1),f.claudeDir.length&&oe(r,f.claudeDir,!1),f.cursorMcpServer.length&&ne(r,f.cursorMcpServer,!1),f.cursorMcpJson.length&&te(r,f.cursorMcpJson,!1),f.claudeJson.length&&se(f.claudeJson,!1),f.gitHooks.length&&re(r,f.gitHooks,!1),console.log(C),console.log(),console.log(D(" \u2714 infernoflow removed from this project")),console.log();const M=j.filter(p=>p.type==="edit"),R=j.filter(p=>p.type==="rm"||p.type==="rmdir");R.length&&console.log(h(" Deleted: ")+R.map(p=>p.path).join(", ")),M.length&&console.log(h(" Edited: ")+M.map(p=>p.path).join(", ")),o&&(console.log(),console.log(h(" Session memory kept \u2192 inferno/sessions.jsonl")),console.log(h(" Re-run infernoflow init to restore the rest."))),console.log()}export{pe as uninstallCommand};
@@ -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 f from"node:fs";import*as s from"node:path";import{installInfernoDraftTooling as h}from"./draftToolingInstall.mjs";import{refreshSecuritySensitiveCopies as j}from"./securityRefresh.mjs";import{updateIdeMcpJson as y}from"./mcpRegistration.mjs";function v(c){const{cwd:r,templatesRoot:a,force:p,silent:e}=c,t=c.logOk||(()=>{}),i=c.logWarn||(()=>{});function u(o,n){return f.existsSync(n)&&!p?(e||i("Skipped (exists): "+s.relative(r,n)),!1):(f.mkdirSync(s.dirname(n),{recursive:!0}),f.copyFileSync(o,n),e||t("Created: "+s.relative(r,n)),!0)}h({cwd:r,templatesRoot:a,force:p,silent:e,logOk:t,logWarn:i});const d=s.join(a,"cursor","hooks.json"),l=s.join(r,".cursor","hooks.json"),m=s.join(a,"cursor","hooks","inferno-session-draft.mjs"),k=s.join(r,".cursor","hooks","inferno-session-draft.mjs");u(d,l),u(m,k);try{const o=j(r);o.length&&!e&&t("Security update: replaced "+o.join(", "))}catch{}try{const o=y(r,"cursor");e||(o.updated?t:i)((o.updated?"Updated: ":"Unchanged: ")+".cursor/mcp.json")}catch(o){e||i(".cursor/mcp.json skipped: "+o.message)}}export{v as installCursorHooksArtifacts};
@@ -0,0 +1,6 @@
1
+ import*as a from"node:fs";import*as l from"node:os";import*as s from"node:path";import*as S from"node:crypto";import{execFileSync as h}from"node:child_process";import{fileURLToPath as g}from"node:url";const k=s.dirname(g(import.meta.url));function v(){return s.resolve(k,"..","bin","infernoflow.mjs")}function E(){return/[\\/]_npx[\\/]/.test(v())}function f(e){return{command:"node",args:[v(),"mcp"],env:{INFERNOFLOW_PROJECT_DIR:e}}}function d(e){if(!a.existsSync(e))return{data:{},existed:!1,ok:!0};try{const t=JSON.parse(a.readFileSync(e,"utf8"));return!t||typeof t!="object"||Array.isArray(t)?{data:{},existed:!0,ok:!1}:{data:t,existed:!0,ok:!0}}catch{return{data:{},existed:!0,ok:!1}}}function m(e,t){a.mkdirSync(s.dirname(e),{recursive:!0});const r=`${e}.infernoflow-${process.pid}-${Date.now()}.tmp`;let o;try{o=a.statSync(e).mode&511}catch{}a.writeFileSync(r,JSON.stringify(t,null,2).replace(/\u0000+/g,"")+`
2
+ `,{encoding:"utf8",...o?{mode:o}:{}});try{a.renameSync(r,e)}catch(n){try{a.unlinkSync(r)}catch{}throw n}}function y(e,t){return!!e&&!!t&&e.command===t.command&&JSON.stringify(e.args)===JSON.stringify(t.args)&&(e.env||{}).INFERNOFLOW_PROJECT_DIR===(t.env||{}).INFERNOFLOW_PROJECT_DIR}function w(e,t){try{return h("git",["ls-files","--error-unmatch","--",t],{cwd:e,stdio:"ignore",windowsHide:!0,timeout:5e3}),!0}catch{return!1}}function x(e,t,r){const o=s.join(e,".gitignore");let n="";try{n=a.readFileSync(o,"utf8")}catch{}if(n.split(/\r?\n/).some(i=>i.trim()===t))return!1;const c=(n&&!n.endsWith(`
3
+ `)?`
4
+ `:"")+(r?`# ${r}
5
+ `:"")+t+`
6
+ `;return a.writeFileSync(o,n+c,"utf8"),!0}function O(e){try{const t=s.join(process.env.INFERNOFLOW_HOME||s.join(l.homedir(),".infernoflow"),"backups");a.mkdirSync(t,{recursive:!0,mode:448});const r=s.join(t,`${s.basename(e)}.${new Date().toISOString().replace(/[:.]/g,"-")}.bak`);a.copyFileSync(e,r);try{a.chmodSync(r,384)}catch{}return r}catch{return null}}function b(e){const t=".mcp.json",r=s.join(e,t),o=a.existsSync(r);if(o&&w(e,t))return{updated:!1,skipped:"tracked",path:r};const{data:n,ok:c}=d(r);if(!c)return{updated:!1,skipped:"unreadable",path:r};(!n.mcpServers||typeof n.mcpServers!="object")&&(n.mcpServers={});const i=f(e);let p=!1;try{p=x(e,".mcp.json","infernoflow: machine-specific MCP config (absolute paths) \u2014 not for git")}catch{}return y(n.mcpServers.infernoflow,i)?{updated:!1,path:r,gitignored:p}:(n.mcpServers.infernoflow=i,m(r,n),{updated:!0,path:r,created:!o,gitignored:p})}function I(e,t){const r=t==="vscode",o=s.join(e,r?".vscode":".cursor","mcp.json"),{data:n,ok:c}=d(o);if(!c)return{updated:!1,skipped:"unreadable",path:o};const i=r?"servers":"mcpServers";(!n[i]||typeof n[i]!="object")&&(n[i]={});const p=r?{type:"stdio",...f(e)}:f(e),u=n[i].infernoflow;return y(u,p)&&(!r||u.type==="stdio")?{updated:!1,path:o}:(p.env={...u&&u.env||{},...p.env},n[i].infernoflow=p,m(o,n),{updated:!0,path:o})}function C(){return s.join(l.homedir(),".claude.json")}function j(){const e=l.homedir();return process.platform==="win32"?s.join(process.env.APPDATA||s.join(e,"AppData","Roaming"),"Claude","claude_desktop_config.json"):process.platform==="darwin"?s.join(e,"Library","Application Support","Claude","claude_desktop_config.json"):s.join(process.env.XDG_CONFIG_HOME||s.join(e,".config"),"Claude","claude_desktop_config.json")}function _(e){if(!e||typeof e!="object")return!1;const t=Array.isArray(e.args)?e.args.join(" "):"";return/inferno-mcp-server\.mjs/.test(t)||/infernoflow(\.mjs)?["']?\s+mcp\b/.test(t)||/bin[\\/]infernoflow\.mjs/.test(t)||!!(e.env&&e.env.INFERNOFLOW_PROJECT_DIR)}function P(e){if(!a.existsSync(e))return{removed:!1};const{data:t,ok:r}=d(e);if(!r||!t.mcpServers||!t.mcpServers.infernoflow)return{removed:!1};const o=t.mcpServers.infernoflow;if(!_(o))return{removed:!1,skipped:"not-ours"};const n=O(e);return n?(delete t.mcpServers.infernoflow,m(e,t),{removed:!0,backup:n,pinnedTo:(o.env||{}).INFERNOFLOW_PROJECT_DIR||null}):{removed:!1,skipped:"backup-failed"}}function F(e,t={}){const r="infernoflow-"+(s.basename(e).toLowerCase().replace(/[^a-z0-9-]+/g,"-").replace(/^-+|-+$/g,"")||"project"),o=t[r];if(!o||(o.env||{}).INFERNOFLOW_PROJECT_DIR===e)return r;const n=S.createHash("sha1").update(e).digest("hex").slice(0,6);return`${r}-${n}`}function J(e){const t=j();if(!a.existsSync(s.dirname(t))&&!a.existsSync(t))return{updated:!1,skipped:"not-installed"};const{data:r,ok:o}=d(t);if(!o)return{updated:!1,skipped:"unreadable",path:t};(!r.mcpServers||typeof r.mcpServers!="object")&&(r.mcpServers={});const n=F(e,r.mcpServers),c=f(e);return y(r.mcpServers[n],c)?{updated:!1,name:n,path:t}:(r.mcpServers[n]=c,m(t,r),{updated:!0,name:n,path:t})}export{j as claudeDesktopConfigPath,C as claudeJsonPath,v as cliEntryPath,F as desktopServerName,x as ensureGitignoreLine,w as isGitTracked,_ as isInfernoflowEntry,f as mcpServerEntry,P as removeLegacyUserLevelEntry,E as runningFromNpxCache,J as updateClaudeDesktopPerProject,I as updateIdeMcpJson,b as updateProjectMcpJson};
@@ -0,0 +1 @@
1
+ const A="(?:api[_-]?key|apikey|secret[_-]?key|client[_-]?secret|secret|access[_-]?token|auth[_-]?token|refresh[_-]?token|bearer[_-]?token|token|passwd|password|pwd|private[_-]?key|aws[_-]?secret[_-]?access[_-]?key|[A-Z][A-Z0-9_]{0,40}_(?:SECRET|TOKEN|PASSWORD|API_KEY|ACCESS_KEY|PRIVATE_KEY))",c=`(?=[^\\s"',;]{0,200}[0-9])(?=[^\\s"',;]{0,200}[A-Za-z])[^\\s"',;]{8,200}|[A-Za-z0-9_\\-+/=]{24,400}`,_=[["private-key",/-----BEGIN [A-Z0-9 ]{0,40}PRIVATE KEY-----[\s\S]{0,20000}?(?:-----END [A-Z0-9 ]{0,40}PRIVATE KEY-----|$)/g],["anthropic-key",/\bsk-ant-[A-Za-z0-9_-]{16,300}/g],["openrouter-key",/\bsk-or-[A-Za-z0-9_-]{16,300}/g],["openai-key",/\bsk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,300}/g],["github-token",/\b(?:gh[pousr]_[A-Za-z0-9]{30,255}|github_pat_[A-Za-z0-9_]{40,255})/g],["npm-token",/\bnpm_[A-Za-z0-9]{30,255}/g],["aws-access-key",/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g],["google-api-key",/\bAIza[0-9A-Za-z_-]{35}\b/g],["slack-token",/\bxox[abposr]-[A-Za-z0-9-]{10,255}/g],["stripe-key",/\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9]{16,255}/g],["jwt",/\beyJ[A-Za-z0-9_-]{8,4000}\.eyJ[A-Za-z0-9_-]{8,4000}\.[A-Za-z0-9_-]{8,4000}/g],["bearer-token",/\b(Bearer\s{1,5})(?!\[REDACTED)[A-Za-z0-9._~+/=-]{16,4000}/g],["url-credentials",/(?<![a-z0-9+.-])([a-z][a-z0-9+.-]{0,30}:\/\/)[^\s:/@]{1,200}:[^\s@/]{3,200}(?=@)/gi],["connection-string",/\b((?:Password|Pwd|AccountKey|SharedAccessKey)\s{0,5}=\s{0,5})(?!\[REDACTED)[^;\s"']{4,500}/gi],["credential",new RegExp(`(["']?\\b${A}["']?\\s{0,5}[=:]\\s{0,5}["']?)(?!\\[REDACTED)(?:${c})`,"gi")]];function k(e){if(typeof e!="string"||!e)return{text:e,kinds:[]};const a=new Set;let s=e;for(const[n,r]of _)r.lastIndex=0,s=s.replace(r,(o,t)=>(a.add(n),(typeof t=="string"&&o.startsWith(t)?t:"")+`[REDACTED:${n}]`));return{text:s,kinds:[...a]}}function i(e){return k(e).kinds.length>0}export{i as containsSecret,k as redactSecrets};
@@ -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,7 @@
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}
3
- `)}}export{m as runUpgradeBackfillIfNeeded};
1
+ import*as s from"node:fs";import*as a from"node:path";const u=new Set(["--help","-h","--version","-v","commands","doctor","setup","init","uninstall","mcp"]);function y(r){const n=a.join(r,".ai-memory"),o=a.join(r,"inferno");return s.existsSync(n)?a.join(n,".last-cli-version"):s.existsSync(o)?a.join(o,".last-cli-version"):null}function m(r){try{return s.readFileSync(r,"utf8").trim()}catch{return""}}async function h(r,n){if(!r||n&&u.has(n))return;const o=process.cwd(),c=y(o);if(!c)return;const i=m(c);if(i===r)return;let f=!1,l=[],p=[],d=i==="";try{const{refreshRuleFilesFromMemory:t}=await import("./ruleFiles.mjs");t(o)}catch{}try{const{applyCleanTreePolicy:t}=await import("./cleanTree.mjs");t(o)}catch{}try{const{autoSetupMcp:t}=await import("./commands/setup.mjs"),e=t(o,{silent:!0});f=!!(e&&(e.mcpServer||e.projectMcpJson||e.cursorMcp||e.vscodeMcp||e.claudeSettings||e.claudeDesktop)),l=e&&e.securityRefreshed||[],p=[e&&e.legacyClaudeJsonRemoved,e&&e.legacyDesktopRemoved].filter(Boolean)}catch{}try{s.writeFileSync(c,r,"utf8")}catch{}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
+ `);for(const t of p)process.stderr.write(` infernoflow: removed the old user-level MCP entry (it pinned every project to ${t.pinnedTo||"one repo"}). Backup: ${t.backup}
4
+ infernoflow: each project now has its own registration \u2014 run any infernoflow command in your other projects to set theirs up.
5
+ `);if(f||d)try{const{gray:t}=await import("./ui/output.mjs"),e=i?`upgraded ${i} \u2192 ${r}`:`initialized for ${r}`;process.stderr.write(t(` infernoflow: ${e}, wired MCP servers + rule files
6
+ `))}catch{process.stderr.write(` infernoflow: refreshed for v${r}
7
+ `)}}export{h 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 {