infernoflow 0.44.17 → 0.44.19
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 +1 -1
- package/dist/lib/amp/io.mjs +21 -14
- package/dist/lib/commands/init.mjs +27 -27
- package/dist/lib/commands/setup.mjs +10 -10
- package/dist/templates/cursor/inferno-mcp-server.mjs +39 -10
- package/package.json +2 -2
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
|
|
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
|
|
package/dist/lib/amp/io.mjs
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import*as
|
|
2
|
-
`).filter(Boolean).map(t=>{try{return JSON.parse(t)}catch{return null}}).filter(Boolean).map(
|
|
3
|
-
|
|
4
|
-
`)
|
|
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"),
|
|
7
|
-
`)
|
|
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"),
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
`,"utf8")
|
|
14
|
-
|
|
15
|
-
`,"utf8"),
|
|
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 ${
|
|
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:
|
|
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
|
|
2
|
-
`,"utf8"),o||
|
|
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){
|
|
6
|
-
`)}function
|
|
7
|
-
`)}function
|
|
8
|
-
`)}function
|
|
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
|
|
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
|
|
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
|
|
23
|
-
> `),l=d=>new Promise(
|
|
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")+" "+
|
|
26
|
-
`),
|
|
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
|
|
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(
|
|
32
|
-
`),
|
|
33
|
-
`+d("\u2714")+" First gotcha logged!")):t&&
|
|
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
|
|
38
|
-
`))}async function
|
|
39
|
-
`),n.writeFileSync(i.join(
|
|
40
|
-
`);for(const m of
|
|
41
|
-
`);
|
|
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 (${
|
|
49
|
-
${
|
|
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)}: ${
|
|
52
|
-
`));const
|
|
53
|
-
`))
|
|
54
|
-
`,"utf8"),
|
|
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,9 +1,9 @@
|
|
|
1
|
-
import*as
|
|
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*$/,"")+`
|
|
2
2
|
|
|
3
3
|
# infernoflow auto-capture
|
|
4
|
-
`+
|
|
5
|
-
`;
|
|
6
|
-
`);
|
|
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
|
|
7
7
|
// infernoflow UserPromptSubmit hook (Claude Code / Cursor).
|
|
8
8
|
// Logs a best-effort 'attempt' entry when the user's prompt shows frustration,
|
|
9
9
|
// so the highest-value capture signal doesn't depend on the model remembering.
|
|
@@ -27,9 +27,9 @@ if (prompt && MARKERS.some((re) => re.test(prompt))) {
|
|
|
27
27
|
} catch {}
|
|
28
28
|
}
|
|
29
29
|
process.exit(0);
|
|
30
|
-
`;function
|
|
31
|
-
`,"utf8"),{updated:!0,path:
|
|
32
|
-
`,"utf8"),
|
|
33
|
-
${
|
|
34
|
-
`),
|
|
35
|
-
${
|
|
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};
|
|
@@ -114,6 +114,7 @@ const INFERNOFLOW_BIN = resolveInfernoflowBin();
|
|
|
114
114
|
let ampIo = null;
|
|
115
115
|
let refreshRuleFiles = null;
|
|
116
116
|
let harvestSnapshot = null;
|
|
117
|
+
let findProjectRoot = null;
|
|
117
118
|
if (INFERNOFLOW_ROOT) {
|
|
118
119
|
try {
|
|
119
120
|
for (const c of [
|
|
@@ -134,9 +135,37 @@ if (INFERNOFLOW_ROOT) {
|
|
|
134
135
|
]) {
|
|
135
136
|
if (fs.existsSync(c)) { harvestSnapshot = (await import(pathToFileURL(c).href)).harvestSnapshot; break; }
|
|
136
137
|
}
|
|
138
|
+
for (const c of [
|
|
139
|
+
path.join(INFERNOFLOW_ROOT, "lib", "projectRoot.mjs"),
|
|
140
|
+
path.join(INFERNOFLOW_ROOT, "dist", "lib", "projectRoot.mjs"),
|
|
141
|
+
]) {
|
|
142
|
+
if (fs.existsSync(c)) { findProjectRoot = (await import(pathToFileURL(c).href)).findProjectRoot; break; }
|
|
143
|
+
}
|
|
137
144
|
} catch { /* swallow — fallback path handles it */ }
|
|
138
145
|
}
|
|
139
146
|
|
|
147
|
+
// ── Project directory resolution ───────────────────────────────────────────
|
|
148
|
+
// NEVER trust process.cwd() to locate .ai-memory. IDEs and the desktop bridge
|
|
149
|
+
// routinely launch this server from the wrong place — a monorepo parent, or
|
|
150
|
+
// C:\WINDOWS\system32 on Windows — so every amp_* tool looked for .ai-memory
|
|
151
|
+
// relative to that wrong cwd and failed with "not initialized". Resolve the
|
|
152
|
+
// real project root once, in priority order:
|
|
153
|
+
// 1. INFERNOFLOW_PROJECT_DIR — explicit override (init can bake this into the MCP config)
|
|
154
|
+
// 2. WORKSPACE_FOLDER_PATHS[0] — the folder the IDE actually opened (Cursor / VS Code set this)
|
|
155
|
+
// 3. findProjectRoot(cwd) — walk up to .ai-memory / .git / a manifest (subfolder case)
|
|
156
|
+
// 4. process.cwd() — last resort
|
|
157
|
+
function resolveProjectDir() {
|
|
158
|
+
const hint = process.env.INFERNOFLOW_PROJECT_DIR
|
|
159
|
+
|| (process.env.WORKSPACE_FOLDER_PATHS || "").split(path.delimiter).filter(Boolean)[0];
|
|
160
|
+
const start = (hint && fs.existsSync(hint)) ? hint : process.cwd();
|
|
161
|
+
if (typeof findProjectRoot === "function") {
|
|
162
|
+
try { return findProjectRoot(start); } catch { /* fall through to start */ }
|
|
163
|
+
}
|
|
164
|
+
return start;
|
|
165
|
+
}
|
|
166
|
+
const PROJECT_DIR = resolveProjectDir();
|
|
167
|
+
process.stderr.write(`[infernoflow MCP] project dir: ${PROJECT_DIR}\n`);
|
|
168
|
+
|
|
140
169
|
// ── Clean-tree policy: regenerate rule files ONCE at boot ──────────────────
|
|
141
170
|
// Historically, every amp_write rewrote CLAUDE.md / .cursorrules. That
|
|
142
171
|
// dirtied tracked files dozens of times per session and blocked git
|
|
@@ -145,7 +174,7 @@ if (INFERNOFLOW_ROOT) {
|
|
|
145
174
|
// the session. amp_read serves runtime queries; rule-file content is
|
|
146
175
|
// for cold-start injection only.
|
|
147
176
|
if (refreshRuleFiles) {
|
|
148
|
-
try { refreshRuleFiles(
|
|
177
|
+
try { refreshRuleFiles(PROJECT_DIR); } catch { /* non-fatal */ }
|
|
149
178
|
}
|
|
150
179
|
|
|
151
180
|
// ── Boot stamp: record which MCP version is running ──────────────────────
|
|
@@ -163,7 +192,7 @@ try {
|
|
|
163
192
|
try { runtimeVersion = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version || runtimeVersion; }
|
|
164
193
|
catch {}
|
|
165
194
|
}
|
|
166
|
-
const memDir = path.join(
|
|
195
|
+
const memDir = path.join(PROJECT_DIR, ".ai-memory");
|
|
167
196
|
if (fs.existsSync(memDir)) {
|
|
168
197
|
fs.writeFileSync(path.join(memDir, ".mcp-runtime.json"), JSON.stringify({
|
|
169
198
|
version: runtimeVersion,
|
|
@@ -200,7 +229,7 @@ function runCmd(args, env = {}) {
|
|
|
200
229
|
: `"${INFERNOFLOW_BIN}" ${args}`;
|
|
201
230
|
return execSync(cmd, {
|
|
202
231
|
encoding: "utf8",
|
|
203
|
-
cwd:
|
|
232
|
+
cwd: PROJECT_DIR,
|
|
204
233
|
timeout: 30000,
|
|
205
234
|
env: { ...process.env, ...env },
|
|
206
235
|
});
|
|
@@ -231,7 +260,7 @@ function isCmdError(result) {
|
|
|
231
260
|
const TOOLS = [
|
|
232
261
|
// ── AMP-spec memory tools (the product) ──────────────────────────────────
|
|
233
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" } } } },
|
|
234
|
-
{ 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
|
|
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 the consolidated details store; NOT injected into rule files. Put the long-form context here; keep 'msg' to one summary sentence." } }, required: ["type","msg"] } },
|
|
235
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"] } },
|
|
236
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"] } },
|
|
237
266
|
{ 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"] } } } },
|
|
@@ -246,7 +275,7 @@ const TOOLS = [
|
|
|
246
275
|
|
|
247
276
|
// ── git drift detection (inline — no external imports in this template file) ─
|
|
248
277
|
function detectGitDrift(sinceCommits) {
|
|
249
|
-
const cwd =
|
|
278
|
+
const cwd = PROJECT_DIR;
|
|
250
279
|
const infernoDir = path.join(cwd, "inferno");
|
|
251
280
|
|
|
252
281
|
const runGit = (cmd) => {
|
|
@@ -402,7 +431,7 @@ function handleTool(id, name, input) {
|
|
|
402
431
|
if (input.tags && input.tags.length) entry.tags = input.tags;
|
|
403
432
|
if (input.detail && String(input.detail).trim()) entry.detail = String(input.detail);
|
|
404
433
|
try {
|
|
405
|
-
const written = ampIo.appendEntry(
|
|
434
|
+
const written = ampIo.appendEntry(PROJECT_DIR, entry);
|
|
406
435
|
// NOTE: rule-file refresh deliberately NOT called here — clean-tree
|
|
407
436
|
// policy regenerates them once at MCP boot only. Doing it on every
|
|
408
437
|
// write dirties tracked files and blocks `git checkout`. Within a
|
|
@@ -446,10 +475,10 @@ function handleTool(id, name, input) {
|
|
|
446
475
|
if (input.note && String(input.note).trim()) {
|
|
447
476
|
entry.detail = String(input.note);
|
|
448
477
|
} else if (harvestSnapshot) {
|
|
449
|
-
try { const snap = harvestSnapshot(
|
|
478
|
+
try { const snap = harvestSnapshot(PROJECT_DIR); if (snap) entry.detail = snap; } catch { /* best-effort */ }
|
|
450
479
|
}
|
|
451
480
|
try {
|
|
452
|
-
const written = ampIo.appendEntry(
|
|
481
|
+
const written = ampIo.appendEntry(PROJECT_DIR, entry);
|
|
453
482
|
text = `🔖 Bookmark saved: ${written.msg} (${written.id})` +
|
|
454
483
|
(written.meta && written.meta.detailRef ? `\n context: ${written.meta.detailRef}` : "");
|
|
455
484
|
} catch (err) {
|
|
@@ -467,8 +496,8 @@ function handleTool(id, name, input) {
|
|
|
467
496
|
return sendError(id, -32000, `infernoflow switch failed: ${switchResult.message}\n${switchResult.stderr || switchResult.stdout || ""}`.trim());
|
|
468
497
|
}
|
|
469
498
|
try {
|
|
470
|
-
const ampPath = path.join(
|
|
471
|
-
const legacyPath = path.join(
|
|
499
|
+
const ampPath = path.join(PROJECT_DIR, ".ai-memory", "handoff.md");
|
|
500
|
+
const legacyPath = path.join(PROJECT_DIR, "inferno", "HANDOFF.md");
|
|
472
501
|
const target = fs.existsSync(ampPath) ? ampPath : legacyPath;
|
|
473
502
|
text = fs.readFileSync(target, "utf8");
|
|
474
503
|
if (input.format === "json") {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infernoflow",
|
|
3
|
-
"version": "0.44.
|
|
4
|
-
"description": "Persistent memory for AI coding sessions
|
|
3
|
+
"version": "0.44.19",
|
|
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": {
|
|
7
7
|
"infernoflow": "dist/bin/infernoflow.mjs"
|