infernoflow 0.44.14 → 0.44.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,6 +37,17 @@ Bookmarks are **never auto-pruned**, and they surface in `infernoflow switch` un
37
37
 
38
38
  ---
39
39
 
40
+ ## 🆕 Proactive capture — a Claude skill + a memory-keeper agent
41
+
42
+ `init` now installs two things into `.claude/` so the loop runs itself on Claude Code:
43
+
44
+ - **`infernoflow-memory` skill** (`.claude/skills/`) — teaches the agent to start warm (`recap`), then proactively `log` real gotchas, decisions-with-a-*because*, dead ends, and durable preferences, and drop a `bookmark` at stopping points or when you say *"bookmark this"*. Capture is **balanced** — it skips routine steps, anything obvious from the code, secrets, and duplicates.
45
+ - **`memory-keeper` subagent** (`.claude/agents/`) — a specialist you can delegate to. It sweeps the session, dedupes against what's already stored, logs what's worth keeping, drops bookmarks, and reports back. It never edits code or logs secrets.
46
+
47
+ Both are installed automatically by `infernoflow init` (fresh **and** on re-run of an existing project) and ship in the package — nothing to set up by hand.
48
+
49
+ ---
50
+
40
51
  ## The loop
41
52
 
42
53
  Every new AI session today starts cold. The agent re-reads your code, re-derives the obvious, and re-makes the same wrong move someone else made yesterday. infernoflow closes that loop in four stages:
@@ -59,7 +70,7 @@ infernoflow init --yes
59
70
 
60
71
  Zero runtime dependencies. Works on Node ≥ 18 — macOS, Linux, Windows.
61
72
 
62
- `init --yes` does the whole setup: creates `.ai-memory/`, writes rule files for every supported IDE, wires the MCP server for Cursor / VS Code Copilot / Claude Code in one shot, applies the clean-tree git policy, and drops a visible demo entry so you can confirm the loop is alive with:
73
+ `init --yes` does the whole setup: creates `.ai-memory/`, writes rule files for every supported IDE, wires the MCP server for Cursor / VS Code Copilot / Claude Code in one shot, installs the `infernoflow-memory` skill + `memory-keeper` agent into `.claude/`, applies the clean-tree git policy, and drops a visible demo entry so you can confirm the loop is alive with:
63
74
 
64
75
  ```bash
65
76
  infernoflow status
@@ -1,20 +1,20 @@
1
- import*as i from"node:fs";import*as C from"node:os";import*as r from"node:path";import{findProjectRoot as B}from"../projectRoot.mjs";import{getBranchInfo as J}from"../git/branch.mjs";const E="1.0",M=new Set(["gotcha","decision","attempt","note","detection","pattern"]),_=new Set(["copilot","cursor","claude","windsurf","other"]);function L(e){let t=r.basename(r.resolve(e));try{const o=r.join(e,".ai-memory","amp.json");if(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 P(e,t){let o=process.env.INFERNOFLOW_GLOBAL_DIR;if(!o)try{const l=JSON.parse(i.readFileSync(r.join(t,"amp.json"),"utf8"));l&&typeof l.globalDir=="string"&&l.globalDir.trim()&&(o=l.globalDir.trim())}catch{}if(!o)return r.join(t,"global.jsonl");let n=o;n.startsWith("~")&&(n=r.join(C.homedir(),n.slice(1).replace(/^[\/\\]/,""))),r.isAbsolute(n)||(n=r.resolve(e,n));const c=L(e);return r.join(n,c,"global.jsonl")}function S(e,t={}){const o=t.literal?r.resolve(e):B(e),n=r.join(o,".ai-memory"),c=r.join(o,"inferno"),l=t.forWrite||i.existsSync(n)||!i.existsSync(c),a=l?n:c,s=l,u=r.join(a,"branches"),f=J(o),d=r.join(u,`${f.currentSlug}.jsonl`),p=f.defaultSlug?r.join(u,`${f.defaultSlug}.jsonl`):null,m=W(o,a);return{root:a,projectRoot:o,isAmp:s,sessions:r.join(a,"sessions.jsonl"),config:r.join(a,s?"amp.json":"config.json"),handoff:r.join(a,s?"handoff.md":"HANDOFF.md"),globalFile:m,branchesDir:u,currentBranchFile:d,defaultBranchFile:p,branch:f}}function W(e,t){try{return P(e,t)}catch{return r.join(t,"global.jsonl")}}function v(e){const t=B(e),o=r.join(t,".ai-memory");return i.existsSync(o)||i.mkdirSync(o,{recursive:!0}),o}const N="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function $(){let e=Date.now(),t="";for(let n=0;n<10;n++)t=N[e%32]+t,e=Math.floor(e/32);let o="";for(let n=0;n<16;n++)o+=N[Math.floor(Math.random()*32)];return t+o}function k(e){const t={...e.meta||{}};let o=e.type||"note";M.has(o)||(t.subtype=o,o="note"),e.result&&(t.result=e.result);let n;const c=e.agent;c&&_.has(c)?n=c:c&&(t.agent=c);const l=typeof e.ts=="number"?e.ts:e.ts?Date.parse(e.ts):Date.now(),a=e.confidence!=null?e.confidence:e.auto?.7:void 0,s={type:o,msg:e.summary||e.msg||"",ts:l,id:e.id||`amp_${$()}`};return e.file&&(s.file=e.file),e.line&&(s.line=e.line),e.function&&(s.function=e.function),e.tags&&e.tags.length&&(s.tags=e.tags),e.source&&(s.source=e.source),n&&(s.tool=n),e.session&&(s.session=e.session),a!=null&&(s.confidence=a),Object.keys(t).length&&(s.meta=t),s}function Y(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),e.confidence!=null&&(n.confidence=e.confidence,e.confidence<1&&(n.auto=!0));const{subtype:c,agent:l,result:a,...s}=t;return Object.keys(s).length&&(n.meta=s),n}function G(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(Y)}catch{return[]}}function te(e){const t=S(e),o=new Set,n=[],c=[t.sessions,t.globalFile,t.defaultBranchFile,t.currentBranchFile],l=[...new Set(c.filter(Boolean))];for(const a of l)for(const s of G(a)){const u=s.id||`${s.ts}|${s.summary}`;o.has(u)||(o.add(u),n.push(s))}return n.sort((a,s)=>{const u=typeof a.ts=="number"?a.ts:Date.parse(a.ts||0),f=typeof s.ts=="number"?s.ts:Date.parse(s.ts||0);return u-f})}function U(e,t){return t.target==="global"?e.globalFile:t.target==="legacy"?e.sessions:t.target==="branch"?e.currentBranchFile:t.type==="preference"?e.globalFile:e.currentBranchFile}function ne(e,t){v(e);const o=S(e,{forWrite:!0}),n=U(o,t),c=k(t),l=JSON.stringify(c)+`
3
- `;if(i.mkdirSync(r.dirname(n),{recursive:!0}),i.appendFileSync(n,l,"utf8"),n!==o.sessions)try{i.mkdirSync(r.dirname(o.sessions),{recursive:!0}),i.appendFileSync(o.sessions,l,"utf8")}catch{}return c}function oe(e,t){if(!t)return{removed:0,files:[]};const o=S(e),n=[o.sessions,o.globalFile,o.currentBranchFile,o.defaultBranchFile].filter(Boolean);try{if(i.existsSync(o.branchesDir))for(const s of i.readdirSync(o.branchesDir))s.endsWith(".jsonl")&&n.push(r.join(o.branchesDir,s))}catch{}const c=[...new Set(n)];let l=0;const a=[];for(const s of c){if(!i.existsSync(s))continue;let u;try{u=i.readFileSync(s,"utf8").split(`
4
- `)}catch{continue}const f=[];let d=0;for(const p of u){if(!p.trim())continue;let m;try{m=JSON.parse(p).id}catch{f.push(p);continue}if(m===t){d++,l++;continue}f.push(p)}d>0&&(i.writeFileSync(s,f.length?f.join(`
1
+ import*as s from"node:fs";import*as J from"node:os";import*as r from"node:path";import{findProjectRoot as O}from"../projectRoot.mjs";import{getBranchInfo as M}from"../git/branch.mjs";const T="1.0",_=new Set(["gotcha","decision","attempt","note","detection","pattern"]),P=new Set(["copilot","cursor","claude","windsurf","other"]);function L(e){let t=r.basename(r.resolve(e));try{const o=r.join(e,".ai-memory","amp.json");if(s.existsSync(o)){const n=JSON.parse(s.readFileSync(o,"utf8"));n&&typeof n.project=="string"&&n.project.trim()&&(t=n.project.trim())}}catch{}return t.toLowerCase().replace(/[^a-z0-9_.\-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,64)||"unnamed-project"}function W(e,t){let o=process.env.INFERNOFLOW_GLOBAL_DIR;if(!o)try{const l=JSON.parse(s.readFileSync(r.join(t,"amp.json"),"utf8"));l&&typeof l.globalDir=="string"&&l.globalDir.trim()&&(o=l.globalDir.trim())}catch{}if(!o)return r.join(t,"global.jsonl");let n=o;n.startsWith("~")&&(n=r.join(J.homedir(),n.slice(1).replace(/^[\/\\]/,""))),r.isAbsolute(n)||(n=r.resolve(e,n));const c=L(e);return r.join(n,c,"global.jsonl")}function m(e,t={}){const o=t.literal?r.resolve(e):O(e),n=r.join(o,".ai-memory"),c=r.join(o,"inferno"),l=t.forWrite||s.existsSync(n)||!s.existsSync(c),a=l?n:c,i=l,u=r.join(a,"branches"),f=M(o),h=r.join(u,`${f.currentSlug}.jsonl`),g=f.defaultSlug?r.join(u,`${f.defaultSlug}.jsonl`):null,S=Y(o,a);return{root:a,projectRoot:o,isAmp:i,sessions:r.join(a,"sessions.jsonl"),config:r.join(a,i?"amp.json":"config.json"),handoff:r.join(a,i?"handoff.md":"HANDOFF.md"),globalFile:S,branchesDir:u,currentBranchFile:h,defaultBranchFile:g,branch:f}}function Y(e,t){try{return W(e,t)}catch{return r.join(t,"global.jsonl")}}function x(e){const t=O(e),o=r.join(t,".ai-memory");return s.existsSync(o)||s.mkdirSync(o,{recursive:!0}),o}const B="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function E(){let e=Date.now(),t="";for(let n=0;n<10;n++)t=B[e%32]+t,e=Math.floor(e/32);let o="";for(let n=0;n<16;n++)o+=B[Math.floor(Math.random()*32)];return t+o}function N(e){const t={...e.meta||{}};let o=e.type||"note";_.has(o)||(t.subtype=o,o="note"),e.result&&(t.result=e.result),e.detailRef&&(t.detailRef=e.detailRef);let n;const c=e.agent;c&&P.has(c)?n=c:c&&(t.agent=c);const l=typeof e.ts=="number"?e.ts:e.ts?Date.parse(e.ts):Date.now(),a=e.confidence!=null?e.confidence:e.auto?.7:void 0,i={type:o,msg:e.summary||e.msg||"",ts:l,id:e.id||`amp_${E()}`};return e.file&&(i.file=e.file),e.line&&(i.line=e.line),e.function&&(i.function=e.function),e.tags&&e.tags.length&&(i.tags=e.tags),e.source&&(i.source=e.source),n&&(i.tool=n),e.session&&(i.session=e.session),a!=null&&(i.confidence=a),Object.keys(t).length&&(i.meta=t),i}function G(e){if(e.summary&&!e.msg)return e;const t=e.meta||{},o=t.subtype||e.type||"note",n={ts:e.ts,type:o,summary:e.msg||""};e.id&&(n.id=e.id),e.file&&(n.file=e.file),e.line&&(n.line=e.line),e.function&&(n.function=e.function),e.tags&&(n.tags=e.tags),e.source&&(n.source=e.source),e.tool&&(n.agent=e.tool),t.agent&&(n.agent=t.agent),t.result&&(n.result=t.result),t.detailRef&&(n.detailRef=t.detailRef),e.confidence!=null&&(n.confidence=e.confidence,e.confidence<1&&(n.auto=!0));const{subtype:c,agent:l,result:a,detailRef:i,...u}=t;return Object.keys(u).length&&(n.meta=u),n}function U(e){if(!e||!s.existsSync(e))return[];try{return s.readFileSync(e,"utf8").split(`
2
+ `).filter(Boolean).map(t=>{try{return JSON.parse(t)}catch{return null}}).filter(Boolean).map(G)}catch{return[]}}function ne(e){const t=m(e),o=new Set,n=[],c=[t.sessions,t.globalFile,t.defaultBranchFile,t.currentBranchFile],l=[...new Set(c.filter(Boolean))];for(const a of l)for(const i of U(a)){const u=i.id||`${i.ts}|${i.summary}`;o.has(u)||(o.add(u),n.push(i))}return n.sort((a,i)=>{const u=typeof a.ts=="number"?a.ts:Date.parse(a.ts||0),f=typeof i.ts=="number"?i.ts:Date.parse(i.ts||0);return u-f})}function H(e,t){return t.target==="global"?e.globalFile:t.target==="legacy"?e.sessions:t.target==="branch"?e.currentBranchFile:t.type==="preference"?e.globalFile:e.currentBranchFile}function oe(e,t){x(e);const o=m(e,{forWrite:!0}),n=t.id||`amp_${E()}`,c={...t,id:n};if(typeof t.detail=="string"&&t.detail.trim()){const u=`details/${n}.md`,f=r.join(o.root,"details",`${n}.md`);try{s.mkdirSync(r.dirname(f),{recursive:!0}),s.writeFileSync(f,t.detail,"utf8"),c.detailRef=u}catch{}}delete c.detail;const l=H(o,c),a=N(c),i=JSON.stringify(a)+`
3
+ `;if(s.mkdirSync(r.dirname(l),{recursive:!0}),s.appendFileSync(l,i,"utf8"),l!==o.sessions)try{s.mkdirSync(r.dirname(o.sessions),{recursive:!0}),s.appendFileSync(o.sessions,i,"utf8")}catch{}return a}function ie(e,t){const o=m(e);return r.join(o.root,"details",`${t}.md`)}function se(e,t){if(!t)return null;const o=t.detailRef||t.meta&&t.meta.detailRef;if(!o||typeof o!="string")return null;const n=m(e),c=r.isAbsolute(o)?o:r.join(n.root,o);try{return s.readFileSync(c,"utf8")}catch{return null}}function re(e,t){if(!t)return{removed:0,files:[]};const o=m(e),n=[o.sessions,o.globalFile,o.currentBranchFile,o.defaultBranchFile].filter(Boolean);try{if(s.existsSync(o.branchesDir))for(const i of s.readdirSync(o.branchesDir))i.endsWith(".jsonl")&&n.push(r.join(o.branchesDir,i))}catch{}const c=[...new Set(n)];let l=0;const a=[];for(const i of c){if(!s.existsSync(i))continue;let u;try{u=s.readFileSync(i,"utf8").split(`
4
+ `)}catch{continue}const f=[];let h=0;for(const g of u){if(!g.trim())continue;let S;try{S=JSON.parse(g).id}catch{f.push(g);continue}if(S===t){h++,l++;continue}f.push(g)}h>0&&(s.writeFileSync(i,f.length?f.join(`
5
5
  `)+`
6
- `:"","utf8"),a.push(s))}return{removed:l,files:a}}const H=30,V=["note","attempt","detection"],w=new Set(["gotcha","decision","pattern"]);function z(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:H,archivableTypes:Array.isArray(t.archivableTypes)&&t.archivableTypes.length?t.archivableTypes:V,auto:t.auto===!0}}function q(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(r.join(e.branchesDir,o))}catch{}return[...new Set(t)]}function K(e,t){const o=new Date(t),n=o.getUTCFullYear(),c=String(o.getUTCMonth()+1).padStart(2,"0");return r.join(e,"archive",`sessions-${n}-${c}.jsonl`)}function se(e,t={}){const o=S(e,{forWrite:!t.dryRun}),n=z(Q(e)),c=Number.isFinite(t.maxAgeDays)?t.maxAgeDays:n.archiveAfterDays,l=t.archive!==!1,a=new Set((Array.isArray(t.types)&&t.types.length?t.types:n.archivableTypes).filter(h=>!w.has(h))),s=Date.now()-c*24*60*60*1e3,u=q(o),f={scanned:0,archived:0,kept:0,files:[],byType:{},dryRun:!!t.dryRun},d=new Map,p=new Set,m=new Set,T=new Set;for(const h of u){if(!i.existsSync(h))continue;let b;try{b=i.readFileSync(h,"utf8").split(`
7
- `)}catch{continue}const F=[];let x=0;for(const j of b){if(!j.trim())continue;let g;try{g=JSON.parse(j)}catch{F.push(j);continue}const y=g&&g.id,D=g&&typeof g.type=="string"?g.type:"note",A=typeof g.ts=="number"?g.ts:Date.parse(g.ts||0),I=Number.isFinite(A)&&A<s,R=a.has(D)&&!w.has(D);if(y&&!m.has(y)&&(m.add(y),f.scanned++),y||f.scanned++,I&&R){if(x++,(!y||!p.has(y))&&(y&&p.add(y),f.archived++,f.byType[D]=(f.byType[D]||0)+1,l)){const O=K(o.root,A);d.has(O)||d.set(O,[]),d.get(O).push(j)}}else y&&!T.has(y)&&(T.add(y),f.kept++),y||f.kept++,F.push(j)}x>0&&!t.dryRun?(i.writeFileSync(h,F.length?F.join(`
6
+ `:"","utf8"),a.push(i))}try{const i=r.join(o.root,"details",`${t}.md`);s.existsSync(i)&&(s.unlinkSync(i),a.push(i))}catch{}return{removed:l,files:a}}const V=30,z=["note","attempt","detection"],w=new Set(["gotcha","decision","pattern"]);function q(e){const t=e&&e.config&&e.config.rotation&&typeof e.config.rotation=="object"?e.config.rotation:{};return{archiveAfterDays:Number.isInteger(t.archiveAfterDays)&&t.archiveAfterDays>0?t.archiveAfterDays:V,archivableTypes:Array.isArray(t.archivableTypes)&&t.archivableTypes.length?t.archivableTypes:z,auto:t.auto===!0}}function K(e){const t=[e.sessions,e.currentBranchFile,e.defaultBranchFile].filter(Boolean);try{if(s.existsSync(e.branchesDir))for(const o of s.readdirSync(e.branchesDir))o.endsWith(".jsonl")&&t.push(r.join(e.branchesDir,o))}catch{}return[...new Set(t)]}function Q(e,t){const o=new Date(t),n=o.getUTCFullYear(),c=String(o.getUTCMonth()+1).padStart(2,"0");return r.join(e,"archive",`sessions-${n}-${c}.jsonl`)}function ce(e,t={}){const o=m(e,{forWrite:!t.dryRun}),n=q(X(e)),c=Number.isFinite(t.maxAgeDays)?t.maxAgeDays:n.archiveAfterDays,l=t.archive!==!1,a=new Set((Array.isArray(t.types)&&t.types.length?t.types:n.archivableTypes).filter(p=>!w.has(p))),i=Date.now()-c*24*60*60*1e3,u=K(o),f={scanned:0,archived:0,kept:0,files:[],byType:{},dryRun:!!t.dryRun},h=new Map,g=new Set,S=new Set,R=new Set;for(const p of u){if(!s.existsSync(p))continue;let b;try{b=s.readFileSync(p,"utf8").split(`
7
+ `)}catch{continue}const F=[];let v=0;for(const j of b){if(!j.trim())continue;let y;try{y=JSON.parse(j)}catch{F.push(j);continue}const d=y&&y.id,D=y&&typeof y.type=="string"?y.type:"note",A=typeof y.ts=="number"?y.ts:Date.parse(y.ts||0),I=Number.isFinite(A)&&A<i,$=Array.isArray(y.tags)&&y.tags.includes("bookmark"),C=a.has(D)&&!w.has(D)&&!$;if(d&&!S.has(d)&&(S.add(d),f.scanned++),d||f.scanned++,I&&C){if(v++,(!d||!g.has(d))&&(d&&g.add(d),f.archived++,f.byType[D]=(f.byType[D]||0)+1,l)){const k=Q(o.root,A);h.has(k)||h.set(k,[]),h.get(k).push(j)}}else d&&!R.has(d)&&(R.add(d),f.kept++),d||f.kept++,F.push(j)}v>0&&!t.dryRun?(s.writeFileSync(p,F.length?F.join(`
8
8
  `)+`
9
- `:"","utf8"),f.files.push(h)):x>0&&f.files.push(h)}if(!t.dryRun&&l&&d.size>0)for(const[h,b]of d)try{i.mkdirSync(r.dirname(h),{recursive:!0}),i.appendFileSync(h,b.join(`
9
+ `:"","utf8"),f.files.push(p)):v>0&&f.files.push(p)}if(!t.dryRun&&l&&h.size>0)for(const[p,b]of h)try{s.mkdirSync(r.dirname(p),{recursive:!0}),s.appendFileSync(p,b.join(`
10
10
  `)+`
11
- `,"utf8")}catch{}return f}function Q(e){const{config:t}=S(e);try{return JSON.parse(i.readFileSync(t,"utf8"))}catch{return null}}function ie(e,t={}){v(e);const{config:o}=S(e,{forWrite:!0});if(i.existsSync(o))return!1;const n={amp:E,project:t.project||r.basename(e),stack:t.stack||{},config:{autoCapture:!0,maxEntries:1e3,rotationStrategy:"archive",inject:["all"],injection:{maxEntries:4,maxCommits:5,maxEntryChars:200},rotation:{archiveAfterDays:30,archivableTypes:["note","attempt","detection"],auto:!1},...t.config||{}}};return i.writeFileSync(o,JSON.stringify(n,null,2)+`
12
- `,"utf8"),!0}function re(e,t={}){v(e);const{config:o}=S(e,{forWrite:!0});let n={};try{n=JSON.parse(i.readFileSync(o,"utf8"))}catch{n={}}(!n||typeof n!="object")&&(n={}),n.amp||(n.amp=E),n.project||(n.project=r.basename(e)),(!n.config||typeof n.config!="object")&&(n.config={});const c=n.config.injection&&typeof n.config.injection=="object"?n.config.injection:{};return n.config.injection={...c,...t},i.writeFileSync(o,JSON.stringify(n,null,2)+`
13
- `,"utf8"),n.config.injection}function ce(e){const t=r.join(e,"inferno"),o=r.join(t,"sessions.jsonl");if(!i.existsSync(o))return{migrated:0,reason:"no legacy sessions.jsonl"};const n=r.join(e,".ai-memory"),c=r.join(n,"sessions.jsonl");if(i.existsSync(c))return{migrated:0,reason:".ai-memory/sessions.jsonl already exists"};v(e);const l=i.readFileSync(o,"utf8").split(`
14
- `).filter(Boolean);let a=0;for(const s of l)try{const u=JSON.parse(s),f=k(u);i.appendFileSync(c,JSON.stringify(f)+`
15
- `,"utf8"),a++}catch{}return i.writeFileSync(r.join(n,"MIGRATED.md"),`# Migrated from inferno/
11
+ `,"utf8")}catch{}return f}function X(e){const{config:t}=m(e);try{return JSON.parse(s.readFileSync(t,"utf8"))}catch{return null}}function ae(e,t={}){x(e);const{config:o}=m(e,{forWrite:!0});if(s.existsSync(o))return!1;const n={amp:T,project:t.project||r.basename(e),stack:t.stack||{},config:{autoCapture:!0,maxEntries:1e3,rotationStrategy:"archive",inject:["all"],injection:{maxEntries:4,maxCommits:5,maxEntryChars:200,protocolStyle:"compact"},rotation:{archiveAfterDays:30,archivableTypes:["note","attempt","detection"],auto:!1},...t.config||{}}};return s.writeFileSync(o,JSON.stringify(n,null,2)+`
12
+ `,"utf8"),!0}function fe(e,t={}){x(e);const{config:o}=m(e,{forWrite:!0});let n={};try{n=JSON.parse(s.readFileSync(o,"utf8"))}catch{n={}}(!n||typeof n!="object")&&(n={}),n.amp||(n.amp=T),n.project||(n.project=r.basename(e)),(!n.config||typeof n.config!="object")&&(n.config={});const c=n.config.injection&&typeof n.config.injection=="object"?n.config.injection:{};return n.config.injection={...c,...t},s.writeFileSync(o,JSON.stringify(n,null,2)+`
13
+ `,"utf8"),n.config.injection}function le(e){const t=r.join(e,"inferno"),o=r.join(t,"sessions.jsonl");if(!s.existsSync(o))return{migrated:0,reason:"no legacy sessions.jsonl"};const n=r.join(e,".ai-memory"),c=r.join(n,"sessions.jsonl");if(s.existsSync(c))return{migrated:0,reason:".ai-memory/sessions.jsonl already exists"};x(e);const l=s.readFileSync(o,"utf8").split(`
14
+ `).filter(Boolean);let a=0;for(const i of l)try{const u=JSON.parse(i),f=N(u);s.appendFileSync(c,JSON.stringify(f)+`
15
+ `,"utf8"),a++}catch{}return s.writeFileSync(r.join(n,"MIGRATED.md"),`# Migrated from inferno/
16
16
 
17
17
  Copied ${a} entries from inferno/sessions.jsonl on ${new Date().toISOString()}.
18
18
 
19
19
  The original inferno/sessions.jsonl is untouched. You can delete it once you're confident the new layout works.
20
- `,"utf8"),{migrated:a,reason:"ok"}}export{E as AMP_VERSION,S as ampPaths,ne as appendEntry,oe as deleteEntry,v as ensureAmpDir,Y as fromAmp,$ as generateULID,ce as migrateLegacy,L as projectSlug,se as pruneEntries,Q as readConfig,te as readEntries,P as resolveGlobalFile,z as resolveRotationSettings,k as toAmp,re as updateInjectionConfig,ie as writeDefaultConfig};
20
+ `,"utf8"),{migrated:a,reason:"ok"}}export{T as AMP_VERSION,m as ampPaths,oe as appendEntry,re as deleteEntry,ie as detailPath,x as ensureAmpDir,G as fromAmp,E as generateULID,le as migrateLegacy,L as projectSlug,ce as pruneEntries,X as readConfig,se as readDetail,ne as readEntries,W as resolveGlobalFile,q as resolveRotationSettings,N as toAmp,fe as updateInjectionConfig,ae as writeDefaultConfig};
@@ -0,0 +1,28 @@
1
+ import*as k from"node:fs";import*as x from"node:path";import{bold as u,cyan as f,gray as t,green as h,yellow as d,red as p}from"../ui/output.mjs";import{readEntries as L,appendEntry as R,deleteEntry as D,readDetail as I}from"../amp/io.mjs";import{harvestSnapshot as B}from"../transcript.mjs";const b="bookmark",F=e=>Array.isArray(e.tags)&&e.tags.includes(b);function y(e){return L(e).filter(F)}function $(e){const n=new Date(e);return isNaN(n.getTime())?"":n.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"})}function W(){return process.env.CURSOR_SESSION?"cursor":process.env.COPILOT_SESSION?"copilot":process.env.CLAUDE_CODE_SESSION?"claude":process.env.WINDSURF_SESSION?"windsurf":process.env.INFERNOFLOW_AGENT?process.env.INFERNOFLOW_AGENT:"human"}const w=(e,n)=>{const r=e.indexOf(n);return r!==-1&&e[r+1]?e[r+1]:null};function N(e,n){if(!n)return{error:"no-token"};const r=e.filter(s=>s.id&&(s.id===n||s.id.startsWith(n)));if(r.length===1)return{bookmark:r[0]};if(r.length>1)return{error:"ambiguous",matches:r};const o=n.toLowerCase(),l=e.filter(s=>(s.msg||s.summary||"").toLowerCase().includes(o));return l.length===0?{error:"none"}:l.length===1?{bookmark:l[0]}:{bookmark:l[l.length-1],ambiguousLabel:l}}function O(){console.log(`
2
+ `+u("\u{1F516} infernoflow bookmark")+t(` \u2014 named session resume points
3
+ `)),console.log(t(" Drop: ")+f('infernoflow bookmark "<label>"')+t(" auto-captures the session transcript as context")),console.log(t(" ")+t('add --note "..." / --detail-file <path|-> for explicit context \xB7 --marker for label only')),console.log(t(" List: ")+f("infernoflow bookmark list")),console.log(t(" Recall: ")+f("infernoflow bookmark show <id|label>")),console.log(t(" Remove: ")+f("infernoflow bookmark rm <id|label>")),console.log()}async function M(e=[]){const n=process.cwd(),r=e[0]==="bookmark"?e.slice(1):e;if(r.length===0){O();return}const o=r[0];return o==="list"||o==="ls"?j(n,r.slice(1)):o==="show"||o==="jump"||o==="recall"?U(n,r.slice(1)):o==="rm"||o==="remove"||o==="delete"?T(n,r.slice(1)):_(n,r)}function _(e,n){const r=w(n,"--note"),o=w(n,"--detail-file"),l=w(n,"--tags"),s=new Set([r,o,l].filter(Boolean)),c=n.filter(m=>!m.startsWith("--")&&!s.has(m)).join(" ").trim();c||(O(),process.exit(1));const a=n.includes("--marker");let i=null,g=null;if(o)try{i=o==="-"?k.readFileSync(0,"utf8"):k.readFileSync(o,"utf8"),g="file"}catch(m){console.error(p(`
4
+ \u2718 Could not read --detail-file ${o}: ${m.message}
5
+ `)),process.exit(1)}else if(r)i=r,g="note";else if(!a)try{i=B(e),i&&(g="transcript")}catch{}const v=x.join(e,".ai-memory"),E=x.join(e,"inferno");!k.existsSync(v)&&!k.existsSync(E)&&(console.error(p(`
6
+ \u2718 no .ai-memory/ or inferno/ \u2014 run: infernoflow init
7
+ `)),process.exit(1));const A=l?l.split(",").map(m=>m.trim()).filter(Boolean):[],C=[b,...A.filter(m=>m!==b)],S=R(e,{ts:new Date().toISOString(),agent:W(),type:"note",summary:c,tags:C,...i&&i.trim()?{detail:i}:{}});console.log(),console.log(" "+h("\u{1F516} Bookmark saved: ")+u(c)+t(` ${S.id}`)),i&&i.trim()?console.log(" "+t(`${g==="transcript"?"auto-captured session transcript":"captured context"} (${i.length} chars) \u2014 recall with `)+f(`infernoflow bookmark show ${S.id.slice(0,12)}`)):console.log(" "+t(`${a?"marker only (--marker)":"marker only \u2014 no session transcript found here"} \u2014 recall by label, or add context with --note / --detail-file`)),console.log()}function j(e,n){const r=y(e);if(n.includes("--json")){console.log(JSON.stringify(r.map(o=>({id:o.id,label:o.msg||o.summary,ts:o.ts,hasContext:!!o.detailRef})),null,2));return}if(console.log(`
8
+ `+u("\u{1F516} infernoflow bookmarks")),console.log(" "+"\u2500".repeat(50)),r.length===0){console.log(t(`
9
+ No bookmarks yet. Drop one: `)+f('infernoflow bookmark "<label>"')+`
10
+ `);return}[...r].reverse().forEach((o,l)=>{const s=o.detailRef?h(" \u25CF"):t(" \u25CB");console.log(` ${t(String(l+1).padStart(3))}${s} ${t($(o.ts))} ${o.msg||o.summary} ${t(o.id.slice(0,12))}`)}),console.log(t(`
11
+ \u25CF has saved context \xB7 \u25CB marker only \u2014 recall with: `)+f("infernoflow bookmark show <id|label>")+`
12
+ `)}function U(e,n){const r=n.includes("--json"),o=n.find(i=>i&&!i.startsWith("-")),l=y(e);l.length===0&&(console.error(d(`
13
+ No bookmarks yet.
14
+ `)),process.exit(1));const s=N(l,o);if(s.error==="no-token"&&(console.error(t(`
15
+ Usage: `)+f("infernoflow bookmark show <id|label>")+`
16
+ `),process.exit(1)),s.error==="none"&&(console.error(p(`
17
+ No bookmark matches: ${o}
18
+ `)),process.exit(1)),s.error==="ambiguous"){console.error(d(`
19
+ Ambiguous \u2014 ${s.matches.length} bookmarks match "${o}":`));for(const i of s.matches.slice(0,8))console.error(t(` ${i.id.slice(0,14)} `)+(i.msg||i.summary||""));console.error(""),process.exit(1)}const c=s.bookmark,a=I(e,c);if(r){console.log(JSON.stringify({id:c.id,label:c.msg||c.summary,ts:c.ts,detail:a||null},null,2));return}console.log(),console.log(" "+u("\u{1F516} "+(c.msg||c.summary))),console.log(" "+t(`${$(c.ts)} \xB7 ${c.id}`)),s.ambiguousLabel&&console.log(" "+t(`(newest of ${s.ambiguousLabel.length} matching "${o}")`)),console.log(" "+"\u2500".repeat(50)),a&&a.trim()?(console.log(),console.log(a.trim()),console.log()):console.log(t(`
20
+ (marker only \u2014 no context was captured for this bookmark)
21
+ `))}function T(e,n){const r=n.find(a=>a&&!a.startsWith("-")),o=y(e);o.length===0&&(console.error(d(`
22
+ No bookmarks to remove.
23
+ `)),process.exit(1));const l=N(o,r);if(l.error){if(l.error==="ambiguous"){console.error(d(`
24
+ Ambiguous \u2014 be specific:`));for(const a of l.matches.slice(0,8))console.error(t(` ${a.id.slice(0,14)} `)+(a.msg||a.summary||""));console.error("")}else l.error==="none"?console.error(p(`
25
+ No bookmark matches: ${r}
26
+ `)):console.error(t(`
27
+ Usage: `)+f("infernoflow bookmark rm <id|label>")+`
28
+ `);process.exit(1)}const s=l.bookmark,{removed:c}=D(e,s.id);console.log(),console.log(c>0?" "+h("\u2714")+" Removed bookmark "+u(s.msg||s.summary)+t(` ${s.id}`):" "+d("Nothing removed.")),console.log()}export{b as BOOKMARK_TAG,M as bookmarkCommand};
@@ -1,11 +1,11 @@
1
- import*as i from"node:fs";import*as s from"node:path";import*as H from"node:readline";import{fileURLToPath as de}from"node:url";import{header as ue,ok as g,warn as T,info as q,done as z,nextSteps as me,bold as J,cyan as f,yellow as G,gray as v}from"../ui/output.mjs";import{discoverProjectSignals as M,reviewCapabilitiesInteractive as ye,writeAdoptionBaseline as ge,buildAdoptionReport as he,summarizeCapabilities as we,buildSignalsReport as ke}from"./adopt.mjs";import{installCursorHooksArtifacts as je}from"../cursorHooksInstall.mjs";import{ensureAmpDir as Se,appendEntry as V,writeDefaultConfig as ve}from"../amp/io.mjs";import{applyCleanTreePolicy as X}from"../cleanTree.mjs";import{installVsCodeCopilotHooksArtifacts as be}from"../vsCodeCopilotHooksInstall.mjs";import{writeInitRuleFiles as B}from"../ruleFiles.mjs";import{autoSetupMcp as Z}from"./setup.mjs";const Ce=s.dirname(de(import.meta.url));function Oe(){return s.resolve(Ce,"../../templates")}function L(e,o,n=""){return new Promise(t=>{const r=n?v(` (${n})`):"";e.question(` ${o}${r}: `,c=>{t(c.trim()||n)})})}function W(e,...o){for(const n of o){const t=e.indexOf(n);if(t!==-1&&e[t+1]&&!e[t+1].startsWith("-"))return e[t+1]}return null}function U(e,o,n,t=!1){return i.existsSync(o)&&!n?(t||T("Skipped (exists): "+s.relative(process.cwd(),o)),!1):(i.mkdirSync(s.dirname(o),{recursive:!0}),i.copyFileSync(e,o),t||g("Created: "+f(s.relative(process.cwd(),o))),!0)}function xe(e,o,n){i.mkdirSync(o,{recursive:!0});for(const t of i.readdirSync(e,{withFileTypes:!0})){const r=s.join(e,t.name),c=s.join(o,t.name);t.isDirectory()?xe(r,c,n):U(r,c,n)}}function Ie(e,o=!1){const n=s.join(e,"package.json");if(!i.existsSync(n))return;const t=JSON.parse(i.readFileSync(n,"utf8"));t.scripts=t.scripts||{};let r=!1;const c={"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[m,j]of Object.entries(c))t.scripts[m]||(t.scripts[m]=j,r=!0);r&&(i.writeFileSync(n,JSON.stringify(t,null,2)+`
2
- `,"utf8"),o||g("Updated "+f("package.json")+" scripts"))}const Q="# --- infernoflow (developer-local AI memory; do not commit) ---",Ae="# --- /infernoflow ---",ee=[".ai-memory/",".cursorrules","CLAUDE.md",".github/copilot-instructions.md"];function Qe(e,{silent:o=!1}={}){const n=s.join(e,".gitignore");let t="";if(i.existsSync(n)&&(t=i.readFileSync(n,"utf8")),t.includes(Q))return!1;const r=["",Q,"# Memory is per-developer, not per-branch. These files travel with you, not with git.",...ee,Ae,""].join(`
3
- `),c=t.length===0||t.endsWith(`
1
+ import*as n from"node:fs";import*as i from"node:path";import*as H from"node:readline";import{fileURLToPath as ye}from"node:url";import{header as ge,ok as w,warn as T,info as z,done as V,nextSteps as he,bold as J,cyan as f,yellow as G,gray as v}from"../ui/output.mjs";import{discoverProjectSignals as M,reviewCapabilitiesInteractive as we,writeAdoptionBaseline as ke,buildAdoptionReport as je,summarizeCapabilities as Se,buildSignalsReport as ve}from"./adopt.mjs";import{installCursorHooksArtifacts as be}from"../cursorHooksInstall.mjs";import{ensureAmpDir as Ce,appendEntry as X,writeDefaultConfig as xe}from"../amp/io.mjs";import{applyCleanTreePolicy as B}from"../cleanTree.mjs";import{installVsCodeCopilotHooksArtifacts as Oe}from"../vsCodeCopilotHooksInstall.mjs";import{writeInitRuleFiles as Z}from"../ruleFiles.mjs";import{autoSetupMcp as Q}from"./setup.mjs";const Ie=i.dirname(ye(import.meta.url));function W(){return i.resolve(Ie,"../../templates")}function L(e,o,t=""){return new Promise(s=>{const r=t?v(` (${t})`):"";e.question(` ${o}${r}: `,l=>{s(l.trim()||t)})})}function Y(e,...o){for(const t of o){const s=e.indexOf(t);if(s!==-1&&e[s+1]&&!e[s+1].startsWith("-"))return e[s+1]}return null}function U(e,o,t,s=!1){return n.existsSync(o)&&!t?(s||T("Skipped (exists): "+i.relative(process.cwd(),o)),!1):(n.mkdirSync(i.dirname(o),{recursive:!0}),n.copyFileSync(e,o),s||w("Created: "+f(i.relative(process.cwd(),o))),!0)}function Ae(e,o,t){n.mkdirSync(o,{recursive:!0});for(const s of n.readdirSync(e,{withFileTypes:!0})){const r=i.join(e,s.name),l=i.join(o,s.name);s.isDirectory()?Ae(r,l,t):U(r,l,t)}}function ee(e,o){const t=i.join(W(),"skills");if(!n.existsSync(t))return 0;let s=0;const r=(l,d)=>{n.mkdirSync(d,{recursive:!0});for(const y of n.readdirSync(l,{withFileTypes:!0})){const a=i.join(l,y.name),u=i.join(d,y.name);if(y.isDirectory()){r(a,u);continue}n.existsSync(u)&&!o||(n.mkdirSync(i.dirname(u),{recursive:!0}),n.copyFileSync(a,u),s++,w("Created: "+f(i.relative(e,u))))}};return r(t,i.join(e,".claude","skills")),s}function oe(e,o){const t=i.join(W(),"agents");if(!n.existsSync(t))return 0;let s=0;const r=(l,d)=>{n.mkdirSync(d,{recursive:!0});for(const y of n.readdirSync(l,{withFileTypes:!0})){const a=i.join(l,y.name),u=i.join(d,y.name);if(y.isDirectory()){r(a,u);continue}n.existsSync(u)&&!o||(n.mkdirSync(i.dirname(u),{recursive:!0}),n.copyFileSync(a,u),s++,w("Created: "+f(i.relative(e,u))))}};return r(t,i.join(e,".claude","agents")),s}function Te(e,o=!1){const t=i.join(e,"package.json");if(!n.existsSync(t))return;const s=JSON.parse(n.readFileSync(t,"utf8"));s.scripts=s.scripts||{};let r=!1;const l={"inferno:check":"infernoflow check","inferno:status":"infernoflow status","inferno:gate":"infernoflow doc-gate","inferno:impact":"infernoflow pr-impact --json","inferno:sync":"infernoflow sync --auto --json","inferno:run":'infernoflow run "sync check" --provider auto --json',"inferno:hooks":"node scripts/inferno-install-hooks.mjs"};for(const[d,y]of Object.entries(l))s.scripts[d]||(s.scripts[d]=y,r=!0);r&&(n.writeFileSync(t,JSON.stringify(s,null,2)+`
2
+ `,"utf8"),o||w("Updated "+f("package.json")+" scripts"))}const ne="# --- infernoflow (developer-local AI memory; do not commit) ---",Ee="# --- /infernoflow ---",te=[".ai-memory/",".cursorrules","CLAUDE.md",".github/copilot-instructions.md"];function oo(e,{silent:o=!1}={}){const t=i.join(e,".gitignore");let s="";if(n.existsSync(t)&&(s=n.readFileSync(t,"utf8")),s.includes(ne))return!1;const r=["",ne,"# Memory is per-developer, not per-branch. These files travel with you, not with git.",...te,Ee,""].join(`
3
+ `),l=s.length===0||s.endsWith(`
4
4
  `)?"":`
5
- `;if(i.writeFileSync(n,t+c+r,"utf8"),!o){g("Updated "+f(".gitignore")+v(" \u2014 added entries:"));for(const m of ee)console.log(v(" + ")+f(m));console.log(v(" Memory + AI rule files are now per-developer (won't move with branches).")),console.log(v(" To commit them instead: delete the `")+f("# --- infernoflow ---")+v("` block in .gitignore."))}return!0}function K(e){const o=s.join(e,"package.json");if(i.existsSync(o))try{const n=JSON.parse(i.readFileSync(o,"utf8"));if(n.name)return n.name.replace(/[^a-z0-9_-]/gi,"_")}catch{}return s.basename(e)}function Te(e,o,n){const t={policyId:o,policyVersion:1,capabilities:n,rules:{docsRequiredOnCapabilityChange:!0,requireScenarioForEachCapability:!0,requireChangelogOnCapabilityChange:!0}};i.writeFileSync(e,JSON.stringify(t,null,2)+`
6
- `)}function Ee(e,o){const n={schemaVersion:1,capabilities:o.map(t=>({id:t,title:t.replace(/([A-Z])/g," $1").trim(),since:"0.1.0"}))};i.writeFileSync(e,JSON.stringify(n,null,2)+`
7
- `)}function Pe(e,o){i.mkdirSync(e,{recursive:!0});const n={scenarioId:"happy_path",description:"Basic happy-path flow covering all capabilities",capabilitiesCovered:o,steps:o.map(t=>({action:t,expect:`${t} works as expected`}))};i.writeFileSync(s.join(e,"happy_path.json"),JSON.stringify(n,null,2)+`
8
- `)}function oe(e,o){const n=`# Changelog \u2014 ${o}
5
+ `;if(n.writeFileSync(t,s+l+r,"utf8"),!o){w("Updated "+f(".gitignore")+v(" \u2014 added entries:"));for(const d of te)console.log(v(" + ")+f(d));console.log(v(" Memory + AI rule files are now per-developer (won't move with branches).")),console.log(v(" To commit them instead: delete the `")+f("# --- infernoflow ---")+v("` block in .gitignore."))}return!0}function K(e){const o=i.join(e,"package.json");if(n.existsSync(o))try{const t=JSON.parse(n.readFileSync(o,"utf8"));if(t.name)return t.name.replace(/[^a-z0-9_-]/gi,"_")}catch{}return i.basename(e)}function Pe(e,o,t){const s={policyId:o,policyVersion:1,capabilities:t,rules:{docsRequiredOnCapabilityChange:!0,requireScenarioForEachCapability:!0,requireChangelogOnCapabilityChange:!0}};n.writeFileSync(e,JSON.stringify(s,null,2)+`
6
+ `)}function Fe(e,o){const t={schemaVersion:1,capabilities:o.map(s=>({id:s,title:s.replace(/([A-Z])/g," $1").trim(),since:"0.1.0"}))};n.writeFileSync(e,JSON.stringify(t,null,2)+`
7
+ `)}function Ne(e,o){n.mkdirSync(e,{recursive:!0});const t={scenarioId:"happy_path",description:"Basic happy-path flow covering all capabilities",capabilitiesCovered:o,steps:o.map(s=>({action:s,expect:`${s} works as expected`}))};n.writeFileSync(i.join(e,"happy_path.json"),JSON.stringify(t,null,2)+`
8
+ `)}function ie(e,o){const t=`# Changelog \u2014 ${o}
9
9
 
10
10
  ## Unreleased
11
11
 
@@ -14,41 +14,41 @@ import*as i from"node:fs";import*as s 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
- `;i.writeFileSync(e,n)}async function Ne(e,o){const{bold:n,cyan:t,gray:r,green:c,yellow:m,red:j}=await import("../ui/output.mjs");console.log(`
18
- `+n("\u{1F525} infernoflow init --lite")),console.log(" "+"\u2500".repeat(50)+`
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 p=s.join(e,"inferno");i.existsSync(p)&&!o&&(console.log(m(` \u26A0 inferno/ already exists. Use --force to overwrite.
20
- `)),process.exit(0)),i.mkdirSync(p,{recursive:!0});const h=K(e);let S="";if(!process.argv.includes("--yes")&&!process.argv.includes("-y")&&process.stdin.isTTY){const E=H.createInterface({input:process.stdin,output:process.stdout});S=await new Promise(x=>{E.question(r(" What does this project do? (one line, Enter to skip): "),I=>{E.close(),x(I.trim())})})}const O={policyId:h,policyVersion:1,lite:!0,capabilities:[],intent:S||void 0};i.writeFileSync(s.join(p,"contract.json"),JSON.stringify(O,null,2)+`
21
- `),i.writeFileSync(s.join(p,"capabilities.json"),JSON.stringify([],null,2)+`
22
- `),i.writeFileSync(s.join(p,"sessions.jsonl"),"","utf8"),i.writeFileSync(s.join(p,".lite"),"1","utf8"),console.log(c(" \u2714 Created inferno/contract.json")),console.log(c(" \u2714 Created inferno/capabilities.json")),console.log(c(" \u2714 Created inferno/sessions.jsonl")),console.log(),console.log(" "+n("Ready. Start using it:")),console.log(" "+t("infernoflow log")+r(` "what you're building" --type note`)),console.log(" "+t("infernoflow context")+r(" \u2014 generate AI context to paste")),console.log(" "+t("infernoflow init --mode full")+r(" \u2014 expand to full setup when you need it")),console.log()}const Fe=/^(?: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,Re=/\s(?:&&|\|\||>>|>|<<|<|\|)\s/,_e=/(?:^|\s)[A-Za-z]:\\|\.\.[\\\/]|[\\\/]bin[\\\/]/;function $e(e){if(!e)return{kind:"empty"};const o=e.replace(/^\s*[>$#]\s+/,"").trim();return o?/[\r\n]/.test(o)?{kind:"multiline",value:o}:Fe.test(o)?{kind:"command",value:o}:Re.test(o)?{kind:"command",value:o}:_e.test(o)?{kind:"command",value:o}:o.length<3?{kind:"tooShort",value:o}:{kind:"ok",value:o}:{kind:"empty"}}async function De({yes:e}){if(e||!process.stdin.isTTY)return"";const{gray:o,yellow:n,cyan:t}=await import("../ui/output.mjs"),r=" "+o(`What should the next AI agent know about this project?
23
- > `),c=m=>new Promise(j=>{const p=H.createInterface({input:process.stdin,output:process.stdout});let h=!1;p.on("SIGINT",()=>{h=!0,p.close(),j(null)}),p.on("close",()=>{h&&j(null)}),p.question(m,S=>{p.close(),j(S)})});for(let m=0;m<2;m++){const j=await c(m===0?r:" "+o("> "));if(j===null)return console.log(),"";const p=$e(j);if(p.kind==="ok")return p.value;if(p.kind==="empty")return"";if(p.kind==="command"){console.log(" "+n("\u26A0")+" That looks like a shell command, not a memory."),console.log(" "+o(" Try a short note like: ")+t('"API returns 202 not 200 on async upload"'));continue}if(p.kind==="multiline"){console.log(" "+n("\u26A0")+" Multi-line paste detected \u2014 log a single gotcha at a time."),console.log(" "+o(" Try one short sentence:"));continue}if(p.kind==="tooShort"){console.log(" "+n("\u26A0")+" Too short to be useful as a memory. Skip with Enter, or try again:");continue}}return""}async function Ge(e,o,n){const{bold:t,cyan:r,gray:c,green:m,yellow:j}=await import("../ui/output.mjs"),p=s.join(e,".ai-memory"),h=s.join(e,"inferno"),S=s.join(p,"sessions.jsonl"),O=i.existsSync(S),E=i.existsSync(s.join(h,"sessions.jsonl"));if((O||E)&&!o){const w=i.existsSync(p)?".ai-memory/":"inferno/ (legacy)";console.log(`
24
- `+t("\u{1F525} infernoflow")+c(` \u2014 already set up
25
- `)),console.log(" "+m("\u2714")+" "+w+` found
26
- `),X(e),B(e);try{Z(e,{silent:!0})}catch{}console.log(" Quick commands:"),console.log(" "+r('infernoflow log "..."')+c(" \u2014 remember something")),console.log(" "+r("infernoflow switch")+c(" \u2014 handoff to next AI")),console.log(" "+r("infernoflow recap")+c(` \u2014 session summary
27
- `)),i.existsSync(p)||console.log(c(" Tip: run ")+r("infernoflow amp migrate")+c(` to move legacy memory into .ai-memory/
28
- `)),console.log(c(` For contracts & CI gates: infernoflow init --mode full
17
+ `;n.writeFileSync(e,t)}async function Re(e,o){const{bold:t,cyan:s,gray:r,green:l,yellow:d,red:y}=await import("../ui/output.mjs");console.log(`
18
+ `+t("\u{1F525} infernoflow init --lite")),console.log(" "+"\u2500".repeat(50)+`
19
+ `),console.log(r(" Lite mode: 3 files, no scripts, no workflows, no hooks.")),console.log(r(" Use `infernoflow upgrade` later to expand to the full setup.\n"));const a=i.join(e,"inferno");n.existsSync(a)&&!o&&(console.log(d(` \u26A0 inferno/ already exists. Use --force to overwrite.
20
+ `)),process.exit(0)),n.mkdirSync(a,{recursive:!0});const u=K(e);let S="";if(!process.argv.includes("--yes")&&!process.argv.includes("-y")&&process.stdin.isTTY){const E=H.createInterface({input:process.stdin,output:process.stdout});S=await new Promise(O=>{E.question(r(" What does this project do? (one line, Enter to skip): "),I=>{E.close(),O(I.trim())})})}const x={policyId:u,policyVersion:1,lite:!0,capabilities:[],intent:S||void 0};n.writeFileSync(i.join(a,"contract.json"),JSON.stringify(x,null,2)+`
21
+ `),n.writeFileSync(i.join(a,"capabilities.json"),JSON.stringify([],null,2)+`
22
+ `),n.writeFileSync(i.join(a,"sessions.jsonl"),"","utf8"),n.writeFileSync(i.join(a,".lite"),"1","utf8"),console.log(l(" \u2714 Created inferno/contract.json")),console.log(l(" \u2714 Created inferno/capabilities.json")),console.log(l(" \u2714 Created inferno/sessions.jsonl")),console.log(),console.log(" "+t("Ready. Start using it:")),console.log(" "+s("infernoflow log")+r(` "what you're building" --type note`)),console.log(" "+s("infernoflow context")+r(" \u2014 generate AI context to paste")),console.log(" "+s("infernoflow init --mode full")+r(" \u2014 expand to full setup when you need it")),console.log()}const _e=/^(?:node|npm|npx|yarn|pnpm|bun|git|cd|mkdir|rm|ls|cat|echo|type|dir|copy|del|move|python|python3|pip|go|cargo|java|gradle|mvn|docker|kubectl|curl|wget|ssh|scp|chmod|chown|sudo|brew|apt|yum)\b/i,$e=/\s(?:&&|\|\||>>|>|<<|<|\|)\s/,De=/(?:^|\s)[A-Za-z]:\\|\.\.[\\\/]|[\\\/]bin[\\\/]/;function Ge(e){if(!e)return{kind:"empty"};const o=e.replace(/^\s*[>$#]\s+/,"").trim();return o?/[\r\n]/.test(o)?{kind:"multiline",value:o}:_e.test(o)?{kind:"command",value:o}:$e.test(o)?{kind:"command",value:o}:De.test(o)?{kind:"command",value:o}:o.length<3?{kind:"tooShort",value:o}:{kind:"ok",value:o}:{kind:"empty"}}async function Le({yes:e}){if(e||!process.stdin.isTTY)return"";const{gray:o,yellow:t,cyan:s}=await import("../ui/output.mjs"),r=" "+o(`What should the next AI agent know about this project?
23
+ > `),l=d=>new Promise(y=>{const a=H.createInterface({input:process.stdin,output:process.stdout});let u=!1;a.on("SIGINT",()=>{u=!0,a.close(),y(null)}),a.on("close",()=>{u&&y(null)}),a.question(d,S=>{a.close(),y(S)})});for(let d=0;d<2;d++){const y=await l(d===0?r:" "+o("> "));if(y===null)return console.log(),"";const a=Ge(y);if(a.kind==="ok")return a.value;if(a.kind==="empty")return"";if(a.kind==="command"){console.log(" "+t("\u26A0")+" That looks like a shell command, not a memory."),console.log(" "+o(" Try a short note like: ")+s('"API returns 202 not 200 on async upload"'));continue}if(a.kind==="multiline"){console.log(" "+t("\u26A0")+" Multi-line paste detected \u2014 log a single gotcha at a time."),console.log(" "+o(" Try one short sentence:"));continue}if(a.kind==="tooShort"){console.log(" "+t("\u26A0")+" Too short to be useful as a memory. Skip with Enter, or try again:");continue}}return""}async function He(e,o,t){const{bold:s,cyan:r,gray:l,green:d,yellow:y}=await import("../ui/output.mjs"),a=i.join(e,".ai-memory"),u=i.join(e,"inferno"),S=i.join(a,"sessions.jsonl"),x=n.existsSync(S),E=n.existsSync(i.join(u,"sessions.jsonl"));if((x||E)&&!o){const k=n.existsSync(a)?".ai-memory/":"inferno/ (legacy)";console.log(`
24
+ `+s("\u{1F525} infernoflow")+l(` \u2014 already set up
25
+ `)),console.log(" "+d("\u2714")+" "+k+` found
26
+ `),B(e),Z(e);try{Q(e,{silent:!0})}catch{}ee(e,o),oe(e,o),console.log(" Quick commands:"),console.log(" "+r('infernoflow log "..."')+l(" \u2014 remember something")),console.log(" "+r("infernoflow switch")+l(" \u2014 handoff to next AI")),console.log(" "+r("infernoflow recap")+l(` \u2014 session summary
27
+ `)),n.existsSync(a)||console.log(l(" Tip: run ")+r("infernoflow amp migrate")+l(` to move legacy memory into .ai-memory/
28
+ `)),console.log(l(` For contracts & CI gates: infernoflow init --mode full
29
29
  `));return}const I=K(e);console.log(`
30
- `+t("\u{1F525} infernoflow")+c(` \u2014 let's get you set up (30 seconds)
30
+ `+s("\u{1F525} infernoflow")+l(` \u2014 let's get you set up (30 seconds)
31
31
  `)),console.log(" Detected: "+r(I)+`
32
- `),Se(e),i.existsSync(S)||i.writeFileSync(S,"","utf8"),ve(e,{project:I,config:{autoCapture:!0}}),X(e);const F=B(e);for(const w of F)"created"in w&&(w.created||w.updated)&&g((w.created?"Created: ":"Updated: ")+r(w.rel));try{Z(e)}catch(w){T("MCP auto-setup skipped: "+w.message)}const P=await De({yes:n});P?(V(e,{ts:new Date().toISOString(),agent:"user",type:"gotcha",summary:P,source:"init"}),console.log(`
33
- `+m("\u2714")+" First gotcha logged!")):n&&V(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
- `+m("\u2714")+` You're set up. Quick commands:
35
- `),console.log(" "+r("infernoflow status")+c(" \u2014 verify the loop end-to-end")),console.log(" "+r('infernoflow log "..."')+c(" \u2014 remember something")),console.log(" "+r("infernoflow switch")+c(" \u2014 generate handoff for next AI")),console.log(" "+r("infernoflow recap")+c(` \u2014 session summary
36
- `)),console.log(c(` Tip: infernoflow switch --copy puts the handoff on your clipboard.
37
- `))}function Le(){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 eo(e){if(e.includes("--help")||e.includes("-h")){Le();return}const o=process.cwd(),n=e.includes("--force")||e.includes("-f"),t=e.includes("--yes")||e.includes("-y"),r=e.includes("--adopt"),c=e.find(a=>a.startsWith("--mode="))?.split("=")[1]||(e.indexOf("--mode")!==-1?e[e.indexOf("--mode")+1]:null),m=c==="full"||c==="contract",j=r||e.includes("--template")||e.includes("--cursor-hooks")||e.includes("--vscode-copilot-hooks")||e.includes("--lite");if(!m&&!j){await Ge(o,n,t);return}if(e.includes("--lite")){await Ne(o,n);return}const p=e.indexOf("--template"),h=p!==-1?e[p+1]:null;if(h){let a;try{a=await import("../templates/index.mjs")}catch{}const l=a?.getTemplate(h);if(!l){const d=a?a.listTemplates().map(A=>A.name).join(", "):"rest-api, nextjs, cli, graphql, monorepo";T(`Unknown template: ${h}. Available: ${d}`),process.exit(1)}const y=s.join(o,"inferno"),N=s.join(y,"scenarios");i.existsSync(y)||i.mkdirSync(y,{recursive:!0}),i.existsSync(N)||i.mkdirSync(N,{recursive:!0});const D=K(o),k=l.capabilities;i.writeFileSync(s.join(y,"contract.json"),JSON.stringify({policyId:D,policyVersion:1,capabilities:k.map(d=>d.id)},null,2)+`
39
- `),i.writeFileSync(s.join(y,"capabilities.json"),JSON.stringify({capabilities:k.map(d=>({id:d.id,description:d.description,since:new Date().toISOString().slice(0,10),source:`template:${h}`}))},null,2)+`
40
- `);for(const d of k)i.writeFileSync(s.join(N,`${d.id}.json`),JSON.stringify({id:`${d.id}-happy-path`,capability:d.id,description:`Happy path for ${d.description||d.id}`,steps:[{action:"invoke",target:d.id,input:{}},{action:"assert",field:"status",value:"success"}],capabilitiesCovered:[d.id]},null,2)+`
41
- `);oe(s.join(y,"CHANGELOG.md"),D),i.writeFileSync(s.join(y,"CONTEXT.md"),`# ${D} \u2014 infernoflow context
32
+ `),Ce(e),n.existsSync(S)||n.writeFileSync(S,"","utf8"),xe(e,{project:I,config:{autoCapture:!0}}),B(e);const N=Z(e);for(const k of N)"created"in k&&(k.created||k.updated)&&w((k.created?"Created: ":"Updated: ")+r(k.rel));ee(e,o),oe(e,o);try{Q(e)}catch(k){T("MCP auto-setup skipped: "+k.message)}const P=await Le({yes:t});P?(X(e,{ts:new Date().toISOString(),agent:"user",type:"gotcha",summary:P,source:"init"}),console.log(`
33
+ `+d("\u2714")+" First gotcha logged!")):t&&X(e,{ts:new Date().toISOString(),agent:"infernoflow",type:"note",summary:"infernoflow init complete \u2014 memory loop is live. Run `infernoflow status` to verify.",source:"init"}),console.log(`
34
+ `+d("\u2714")+` You're set up. Quick commands:
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
+ `)),console.log(l(` Tip: infernoflow switch --copy puts the handoff on your clipboard.
37
+ `))}function Je(){console.log(["infernoflow init \u2014 set up persistent AI memory in this project","","Usage:"," infernoflow init Memory-first setup (default, 60 sec)"," infernoflow init --lite Minimal: 3 files, no scripts, no workflows"," infernoflow init --mode full Contracts + CI gates (advanced)"," infernoflow init --adopt Detect existing capabilities from code"," infernoflow init --template <name> Start from a project template","","Options:"," -y, --yes Non-interactive; accept all defaults"," -f, --force Overwrite existing inferno/ or .ai-memory/"," --cursor-hooks Also install Cursor IDE hooks (advanced)"," --vscode-copilot-hooks Also install VS Code Copilot hooks (advanced)"," --lang <lang> Override language detection (with --adopt)"," --framework <name> Override framework detection (with --adopt)"," --project-type <type> Override project-type detection (with --adopt)"," -h, --help Show this help","","Default flow writes .ai-memory/ + AI rule files + MCP server config, then asks","for one first gotcha. Press Enter to skip \u2014 never blocks."].join(`
38
+ `))}async function no(e){if(e.includes("--help")||e.includes("-h")){Je();return}const o=process.cwd(),t=e.includes("--force")||e.includes("-f"),s=e.includes("--yes")||e.includes("-y"),r=e.includes("--adopt"),l=e.find(p=>p.startsWith("--mode="))?.split("=")[1]||(e.indexOf("--mode")!==-1?e[e.indexOf("--mode")+1]:null),d=l==="full"||l==="contract",y=r||e.includes("--template")||e.includes("--cursor-hooks")||e.includes("--vscode-copilot-hooks")||e.includes("--lite");if(!d&&!y){await He(o,t,s);return}if(e.includes("--lite")){await Re(o,t);return}const a=e.indexOf("--template"),u=a!==-1?e[a+1]:null;if(u){let p;try{p=await import("../templates/index.mjs")}catch{}const c=p?.getTemplate(u);if(!c){const m=p?p.listTemplates().map(A=>A.name).join(", "):"rest-api, nextjs, cli, graphql, monorepo";T(`Unknown template: ${u}. Available: ${m}`),process.exit(1)}const h=i.join(o,"inferno"),F=i.join(h,"scenarios");n.existsSync(h)||n.mkdirSync(h,{recursive:!0}),n.existsSync(F)||n.mkdirSync(F,{recursive:!0});const D=K(o),j=c.capabilities;n.writeFileSync(i.join(h,"contract.json"),JSON.stringify({policyId:D,policyVersion:1,capabilities:j.map(m=>m.id)},null,2)+`
39
+ `),n.writeFileSync(i.join(h,"capabilities.json"),JSON.stringify({capabilities:j.map(m=>({id:m.id,description:m.description,since:new Date().toISOString().slice(0,10),source:`template:${u}`}))},null,2)+`
40
+ `);for(const m of j)n.writeFileSync(i.join(F,`${m.id}.json`),JSON.stringify({id:`${m.id}-happy-path`,capability:m.id,description:`Happy path for ${m.description||m.id}`,steps:[{action:"invoke",target:m.id,input:{}},{action:"assert",field:"status",value:"success"}],capabilitiesCovered:[m.id]},null,2)+`
41
+ `);ie(i.join(h,"CHANGELOG.md"),D),n.writeFileSync(i.join(h,"CONTEXT.md"),`# ${D} \u2014 infernoflow context
42
42
 
43
- > Template: ${h} \u2014 ${l.description}
43
+ > Template: ${u} \u2014 ${c.description}
44
44
 
45
45
  ## Hint
46
- ${l.contextHint}
46
+ ${c.contextHint}
47
47
 
48
- ## Capabilities (${k.length})
49
- ${k.map(d=>`- \`${d.id}\`: ${d.description}`).join(`
48
+ ## Capabilities (${j.length})
49
+ ${j.map(m=>`- \`${m.id}\`: ${m.description}`).join(`
50
50
  `)}
51
- `),l.scripts&&(q("Suggested package.json scripts for this template:"),Object.entries(l.scripts).forEach(([d,A])=>console.log(` ${J(d)}: ${v(A)}`)),console.log()),z(`Initialised from template ${J(f(h))} \u2014 ${J(String(k.length))} capabilities`),console.log(),q(`Run ${f("infernoflow vibe")} to start vibe coding mode`),console.log();return}const S=e.includes("--cursor-hooks"),O=e.includes("--vscode-copilot-hooks"),E=e.includes("--report-json"),x=e.includes("--report-json-only"),I=e.includes("--report-human-only"),F=W(e,"--lang"),P=W(e,"--framework"),w=W(e,"--project-type"),u=x;x&&I&&(console.error("Error: --report-json-only and --report-human-only cannot be used together."),process.exit(1)),u||ue("init");const b=s.join(o,"inferno"),ne=s.join(o,".github","workflows");i.existsSync(b)&&!n&&(u&&(console.log(JSON.stringify({ok:!1,error:"inferno_exists",hint:"Use --force to overwrite"},null,2)),process.exit(1)),T("inferno/ already exists. Use --force to overwrite."),console.log(),process.exit(0));const R=K(o),Y="CreateTask, ReadTasks, UpdateTask, ToggleComplete, DeleteTask";let _=R,C=Y.split(",").map(a=>a.trim());if(r||m){let l=M(o,{language:F||void 0,framework:P||void 0,projectType:w||void 0});if(!t&&!x){const k=H.createInterface({input:process.stdin,output:process.stdout}),d=l.developmentProfile||{},A=d.detected||{};console.log(v(` Review inferred development stack (press Enter to accept detected values)
52
- `));const ae=await L(k,"Language",d.language||A.language||"unknown"),pe=await L(k,"Framework",d.framework||A.framework||"unknown"),fe=await L(k,"Project type",d.projectType||A.projectType||"unknown");k.close(),l=M(o,{language:ae,framework:pe,projectType:fe})}const y=l.capabilities,N=we(y);x?console.log(JSON.stringify({mode:"adopt",policyId:R,inferredCapabilities:N,components:l.components,displayFields:l.displayFields,externalLibraries:l.externalLibraries,uiLayout:l.uiLayout,styling:l.styling,developmentProfile:l.developmentProfile,apiCalls:l.apiCalls},null,2)):(console.log(),console.log(v(he(y))),console.log(),console.log(v(ke(l))),console.log(),E&&!I&&(console.log(JSON.stringify({mode:"adopt",policyId:R,inferredCapabilities:N,components:l.components,displayFields:l.displayFields,externalLibraries:l.externalLibraries,uiLayout:l.uiLayout,styling:l.styling,developmentProfile:l.developmentProfile,apiCalls:l.apiCalls},null,2)),console.log()));const D=await ye(y,t);_=R,C=D.map(k=>k.id),C.length===0&&(C=["ExampleCapability"],u||T("No capabilities detected from the code \u2014 wrote a placeholder. Edit "+f("inferno/capabilities.json")+" to describe what this project does."))}else if(!t){const a=H.createInterface({input:process.stdin,output:process.stdout});console.log(v(` Press Enter to accept defaults
53
- `)),_=await L(a,"Project / policy name",R),C=(await L(a,"Capabilities (comma-separated)",Y)).split(",").map(y=>y.trim()).filter(Boolean),a.close(),console.log()}if(i.mkdirSync(b,{recursive:!0}),r){const a=C.map(y=>({id:y,title:y.replace(/([A-Z])/g," $1").trim()})),l=M(o,{language:F||void 0,framework:P||void 0,projectType:w||void 0});ge(b,_,a,l),u||(g("Created: "+f("inferno/contract.json")),g("Created: "+f("inferno/capabilities.json")),g("Created: "+f("inferno/scenarios/adoption_baseline.json")),g("Created: "+f("inferno/adoption_profile.json")),g("Created: "+f("inferno/CHANGELOG.md")))}else Te(s.join(b,"contract.json"),_,C),u||g("Created: "+f("inferno/contract.json")),Ee(s.join(b,"capabilities.json"),C),u||g("Created: "+f("inferno/capabilities.json")),Pe(s.join(b,"scenarios"),C),u||g("Created: "+f("inferno/scenarios/happy_path.json")),oe(s.join(b,"CHANGELOG.md"),_),u||g("Created: "+f("inferno/CHANGELOG.md"));const $=Oe(),te=s.join($,"scripts","inferno-doc-gate.mjs"),ie=s.join(o,"scripts","inferno-doc-gate.mjs");U(te,ie,n,u);const se=s.join($,"scripts","inferno-install-hooks.mjs"),re=s.join(o,"scripts","inferno-install-hooks.mjs");U(se,re,n,u);const le=s.join($,"ci","github-inferno-check.yml"),ce=s.join(ne,"infernoflow-check.yml");if(U(le,ce,n,u),Ie(o,u),S&&je({cwd:o,templatesRoot:$,force:n,silent:u,logOk:a=>{u||g(a)},logWarn:a=>{u||T(a)}}),O&&be({cwd:o,templatesRoot:$,force:n,silent:u,logOk:a=>{u||g(a)},logWarn:a=>{u||T(a)}}),r){const a=s.join(b,"context-state.json");let l={};try{l=JSON.parse(i.readFileSync(a,"utf8"))}catch{}const y=M(o,{language:F||void 0,framework:P||void 0,projectType:w||void 0});l.stack=y.developmentProfile,i.writeFileSync(a,JSON.stringify(l,null,2)+`
54
- `,"utf8"),u||g("Created: "+f("inferno/context-state.json"))}if(!u){z("infernoflow initialized!");const a=s.join(b,"integrations.json");!(process.env.ANTHROPIC_API_KEY||process.env.OPENAI_API_KEY||process.env.GOOGLE_AI_API_KEY||process.env.OPENROUTER_API_KEY||process.env.GEMINI_API_KEY)&&!i.existsSync(a)&&(console.log(),console.log(` ${G("\u{1F4A1}")} ${J("Tip:")} connect an AI provider for explain, why, review, and changelog AI.`),console.log(` ${f("infernoflow ai setup")} \u2014 takes 60 seconds`)),me([f("infernoflow status")+" \u2014 see your contract at a glance",f("infernoflow check")+" \u2014 validate everything",(r?"Review inferred baseline in ":"Edit ")+G("inferno/capabilities.json")+(r?" and refine IDs/titles":" to describe each capability in detail"),"Add more "+G("inferno/scenarios/*.json")+" files for edge cases","Add "+f("inferno:check")+" to your CI pipeline",...S?["Restart Cursor \u2014 hooks write assistant text to "+G("inferno/CONTEXT.draft.md"),"Promote when ready: "+f("npm run inferno:promote-draft -- --append-notes")]:[],...O?["Restart VS Code \u2014 Copilot hooks append prompts + assistant (from transcript) to "+G("inferno/CONTEXT.draft.md"),"Promote when ready: "+f("npm run inferno:promote-draft -- --append-notes")]:[],...!S&&!O?["Optional: "+f("infernoflow install-cursor-hooks")+" or "+f("infernoflow install-vscode-copilot-hooks")]:[]])}}export{Qe as ensureGitignoreEntries,eo as initCommand};
51
+ `),c.scripts&&(z("Suggested package.json scripts for this template:"),Object.entries(c.scripts).forEach(([m,A])=>console.log(` ${J(m)}: ${v(A)}`)),console.log()),V(`Initialised from template ${J(f(u))} \u2014 ${J(String(j.length))} capabilities`),console.log(),z(`Run ${f("infernoflow vibe")} to start vibe coding mode`),console.log();return}const S=e.includes("--cursor-hooks"),x=e.includes("--vscode-copilot-hooks"),E=e.includes("--report-json"),O=e.includes("--report-json-only"),I=e.includes("--report-human-only"),N=Y(e,"--lang"),P=Y(e,"--framework"),k=Y(e,"--project-type"),g=O;O&&I&&(console.error("Error: --report-json-only and --report-human-only cannot be used together."),process.exit(1)),g||ge("init");const b=i.join(o,"inferno"),se=i.join(o,".github","workflows");n.existsSync(b)&&!t&&(g&&(console.log(JSON.stringify({ok:!1,error:"inferno_exists",hint:"Use --force to overwrite"},null,2)),process.exit(1)),T("inferno/ already exists. Use --force to overwrite."),console.log(),process.exit(0));const R=K(o),q="CreateTask, ReadTasks, UpdateTask, ToggleComplete, DeleteTask";let _=R,C=q.split(",").map(p=>p.trim());if(r||d){let c=M(o,{language:N||void 0,framework:P||void 0,projectType:k||void 0});if(!s&&!O){const j=H.createInterface({input:process.stdin,output:process.stdout}),m=c.developmentProfile||{},A=m.detected||{};console.log(v(` Review inferred development stack (press Enter to accept detected values)
52
+ `));const de=await L(j,"Language",m.language||A.language||"unknown"),ue=await L(j,"Framework",m.framework||A.framework||"unknown"),me=await L(j,"Project type",m.projectType||A.projectType||"unknown");j.close(),c=M(o,{language:de,framework:ue,projectType:me})}const h=c.capabilities,F=Se(h);O?console.log(JSON.stringify({mode:"adopt",policyId:R,inferredCapabilities:F,components:c.components,displayFields:c.displayFields,externalLibraries:c.externalLibraries,uiLayout:c.uiLayout,styling:c.styling,developmentProfile:c.developmentProfile,apiCalls:c.apiCalls},null,2)):(console.log(),console.log(v(je(h))),console.log(),console.log(v(ve(c))),console.log(),E&&!I&&(console.log(JSON.stringify({mode:"adopt",policyId:R,inferredCapabilities:F,components:c.components,displayFields:c.displayFields,externalLibraries:c.externalLibraries,uiLayout:c.uiLayout,styling:c.styling,developmentProfile:c.developmentProfile,apiCalls:c.apiCalls},null,2)),console.log()));const D=await we(h,s);_=R,C=D.map(j=>j.id),C.length===0&&(C=["ExampleCapability"],g||T("No capabilities detected from the code \u2014 wrote a placeholder. Edit "+f("inferno/capabilities.json")+" to describe what this project does."))}else if(!s){const p=H.createInterface({input:process.stdin,output:process.stdout});console.log(v(` Press Enter to accept defaults
53
+ `)),_=await L(p,"Project / policy name",R),C=(await L(p,"Capabilities (comma-separated)",q)).split(",").map(h=>h.trim()).filter(Boolean),p.close(),console.log()}if(n.mkdirSync(b,{recursive:!0}),r){const p=C.map(h=>({id:h,title:h.replace(/([A-Z])/g," $1").trim()})),c=M(o,{language:N||void 0,framework:P||void 0,projectType:k||void 0});ke(b,_,p,c),g||(w("Created: "+f("inferno/contract.json")),w("Created: "+f("inferno/capabilities.json")),w("Created: "+f("inferno/scenarios/adoption_baseline.json")),w("Created: "+f("inferno/adoption_profile.json")),w("Created: "+f("inferno/CHANGELOG.md")))}else Pe(i.join(b,"contract.json"),_,C),g||w("Created: "+f("inferno/contract.json")),Fe(i.join(b,"capabilities.json"),C),g||w("Created: "+f("inferno/capabilities.json")),Ne(i.join(b,"scenarios"),C),g||w("Created: "+f("inferno/scenarios/happy_path.json")),ie(i.join(b,"CHANGELOG.md"),_),g||w("Created: "+f("inferno/CHANGELOG.md"));const $=W(),re=i.join($,"scripts","inferno-doc-gate.mjs"),le=i.join(o,"scripts","inferno-doc-gate.mjs");U(re,le,t,g);const ce=i.join($,"scripts","inferno-install-hooks.mjs"),ae=i.join(o,"scripts","inferno-install-hooks.mjs");U(ce,ae,t,g);const pe=i.join($,"ci","github-inferno-check.yml"),fe=i.join(se,"infernoflow-check.yml");if(U(pe,fe,t,g),Te(o,g),S&&be({cwd:o,templatesRoot:$,force:t,silent:g,logOk:p=>{g||w(p)},logWarn:p=>{g||T(p)}}),x&&Oe({cwd:o,templatesRoot:$,force:t,silent:g,logOk:p=>{g||w(p)},logWarn:p=>{g||T(p)}}),r){const p=i.join(b,"context-state.json");let c={};try{c=JSON.parse(n.readFileSync(p,"utf8"))}catch{}const h=M(o,{language:N||void 0,framework:P||void 0,projectType:k||void 0});c.stack=h.developmentProfile,n.writeFileSync(p,JSON.stringify(c,null,2)+`
54
+ `,"utf8"),g||w("Created: "+f("inferno/context-state.json"))}if(!g){V("infernoflow initialized!");const p=i.join(b,"integrations.json");!(process.env.ANTHROPIC_API_KEY||process.env.OPENAI_API_KEY||process.env.GOOGLE_AI_API_KEY||process.env.OPENROUTER_API_KEY||process.env.GEMINI_API_KEY)&&!n.existsSync(p)&&(console.log(),console.log(` ${G("\u{1F4A1}")} ${J("Tip:")} connect an AI provider for explain, why, review, and changelog AI.`),console.log(` ${f("infernoflow ai setup")} \u2014 takes 60 seconds`)),he([f("infernoflow status")+" \u2014 see your contract at a glance",f("infernoflow check")+" \u2014 validate everything",(r?"Review inferred baseline in ":"Edit ")+G("inferno/capabilities.json")+(r?" and refine IDs/titles":" to describe each capability in detail"),"Add more "+G("inferno/scenarios/*.json")+" files for edge cases","Add "+f("inferno:check")+" to your CI pipeline",...S?["Restart Cursor \u2014 hooks write assistant text to "+G("inferno/CONTEXT.draft.md"),"Promote when ready: "+f("npm run inferno:promote-draft -- --append-notes")]:[],...x?["Restart VS Code \u2014 Copilot hooks append prompts + assistant (from transcript) to "+G("inferno/CONTEXT.draft.md"),"Promote when ready: "+f("npm run inferno:promote-draft -- --append-notes")]:[],...!S&&!x?["Optional: "+f("infernoflow install-cursor-hooks")+" or "+f("infernoflow install-vscode-copilot-hooks")]:[]])}}export{oo as ensureGitignoreEntries,no as initCommand};
@@ -1,5 +1,6 @@
1
- import{refreshRuleFilesFromMemory as d}from"../ruleFiles.mjs";import{updateInjectionConfig as m}from"../amp/io.mjs";import{findProjectRoot as a}from"../projectRoot.mjs";import{bold as f,cyan as p,gray as c,green as g,red as u}from"../ui/output.mjs";function s(o,r){const t=o.indexOf(r);if(t===-1)return;const n=parseInt(o[t+1],10);return Number.isFinite(n)?n:void 0}function y(o){const r={},t=s(o,"--max-memory");t!==void 0&&(r.maxEntries=t);const n=s(o,"--max-commits");n!==void 0&&(r.maxCommits=n);const l=s(o,"--max-entry-chars");return l!==void 0&&(r.maxEntryChars=l),o.includes("--no-protocol")&&(r.includeProtocol=!1),o.includes("--protocol")&&(r.includeProtocol=!0),r}async function w(o){const r=o.includes("--dry-run")||o.includes("-n"),t=o.includes("--json"),n=a(process.cwd()),l=y(o);if(!r&&Object.keys(l).length)try{m(n,l)}catch{}if(r){if(t){console.log(JSON.stringify({dryRun:!0,projectRoot:n},null,2));return}console.log(`
2
- `+f("\u{1F525} infernoflow refresh")+c(" \u2014 dry run")),console.log(" "+c("Project root: ")+p(n)),console.log(" "+c("Would rewrite: ")+".cursorrules, CLAUDE.md, .github/copilot-instructions.md"),console.log();return}let i;try{i=d(n)}catch(e){t?console.log(JSON.stringify({ok:!1,error:e.message},null,2)):console.error(u(`
1
+ import*as y from"node:path";import{refreshRuleFilesFromMemory as h}from"../ruleFiles.mjs";import{updateInjectionConfig as x}from"../amp/io.mjs";import{findProjectRoot as C}from"../projectRoot.mjs";import{detectIdeContext as E}from"../ai/ideDetection.mjs";import{bold as u,cyan as f,gray as c,green as d,red as m}from"../ui/output.mjs";const a=y.join(".github","copilot-instructions.md"),O=[".cursorrules","CLAUDE.md",a];function i(o,r){const t=o.indexOf(r);if(t===-1)return;const n=parseInt(o[t+1],10);return Number.isFinite(n)?n:void 0}function p(o,r){const t=o.indexOf(r);return t!==-1&&o[t+1]?o[t+1]:void 0}function j(){const o=process.env;if(o.CLAUDE_CODE_SESSION||o.CLAUDECODE)return["CLAUDE.md"];const{ideDetected:r}=E("auto");return r==="cursor"?[".cursorrules"]:r==="vscode"?[a]:O}function L(o){const r={},t=i(o,"--max-memory");t!==void 0&&(r.maxEntries=t);const n=i(o,"--max-commits");n!==void 0&&(r.maxCommits=n);const s=i(o,"--max-entry-chars");s!==void 0&&(r.maxEntryChars=s),o.includes("--no-protocol")&&(r.includeProtocol=!1),o.includes("--protocol")&&(r.includeProtocol=!0);const l=p(o,"--protocol-style");l&&["compact","full","off"].includes(l)&&(r.protocolStyle=l);const e=p(o,"--targets");return e&&(r.targets=e==="auto"?j():e.split(",").map(g=>g.trim()).filter(Boolean)),r}async function R(o){const r=o.includes("--dry-run")||o.includes("-n"),t=o.includes("--json"),n=C(process.cwd()),s=L(o);if(!r&&Object.keys(s).length)try{x(n,s)}catch{}if(s.targets&&!t){const e=s.targets.length<3;console.log(`
2
+ `+d("\u2714")+c(" memory block \u2192 ")+f(s.targets.join(", "))+(e?c(" (other rule files' blocks are stripped \u2014 no double-load)"):""))}if(r){if(t){console.log(JSON.stringify({dryRun:!0,projectRoot:n},null,2));return}console.log(`
3
+ `+u("\u{1F525} infernoflow refresh")+c(" \u2014 dry run")),console.log(" "+c("Project root: ")+f(n)),console.log(" "+c("Would rewrite: ")+".cursorrules, CLAUDE.md, .github/copilot-instructions.md"),console.log();return}let l;try{l=h(n)}catch(e){t?console.log(JSON.stringify({ok:!1,error:e.message},null,2)):console.error(m(`
3
4
  \u2718 refresh failed: `)+e.message+`
4
- `),process.exit(1)}if(t){console.log(JSON.stringify({ok:!0,projectRoot:n,results:i},null,2));return}console.log(`
5
- `+f("\u{1F525} infernoflow refresh")),console.log(" "+"\u2500".repeat(50));for(const e of i)"error"in e&&e.error?console.log(" "+u("\u2718 ")+e.rel+c(" \u2014 "+e.error)):"created"in e&&(e.created||e.updated)?console.log(" "+g("\u2714 ")+e.rel+c(e.created?" \u2014 created":" \u2014 updated")):console.log(" "+c("\xB7 ")+e.rel+c(" \u2014 unchanged"));console.log()}export{y as injectionPatchFromArgs,w as refreshCommand};
5
+ `),process.exit(1)}if(t){console.log(JSON.stringify({ok:!0,projectRoot:n,results:l},null,2));return}console.log(`
6
+ `+u("\u{1F525} infernoflow refresh")),console.log(" "+"\u2500".repeat(50));for(const e of l)"error"in e&&e.error?console.log(" "+m("\u2718 ")+e.rel+c(" \u2014 "+e.error)):"created"in e&&(e.created||e.updated)?console.log(" "+d("\u2714 ")+e.rel+c(e.created?" \u2014 created":" \u2014 updated")):console.log(" "+c("\xB7 ")+e.rel+c(" \u2014 unchanged"));console.log()}export{L as injectionPatchFromArgs,R as refreshCommand,j as resolveAutoTargets};
@@ -1,11 +1,11 @@
1
- import*as A from"node:fs";import*as m from"node:path";import"node:os";import{execSync as T}from"node:child_process";import{bold as z,cyan as O,gray as rt,green as ct,yellow as K,red as ut}from"../ui/output.mjs";import{ampPaths as ht,readEntries as dt,appendEntry as gt}from"../amp/io.mjs";const _="inferno";function M(){return ht(process.cwd())}const mt=m.join(_,"HANDOFF.md"),Ot=m.join(_,"sessions.jsonl"),X=m.join(_,"context-state.json"),q=m.join(_,"contract.json"),Q=m.join(_,"theme.json"),Y=m.join(_,"adoption_profile.json");function u(t){try{return JSON.parse(A.readFileSync(t,"utf8"))}catch{return null}}function lt(t){try{return A.readFileSync(t,"utf8")}catch{return null}}function Z(t){return t?new Date(t).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}):"unknown"}function tt(t){if(t<0)return"unknown";const i=Math.floor(t/36e5),a=Math.floor(t%36e5/6e4);return i>0?`${i}h ${a}m`:`${a}m`}function et(){return dt(process.cwd())}function ot(t,i,a){if(a)return new Date(0);if(i){const r=i.match(/^(\d+)h$/i),h=i.match(/^(\d+)d$/i);if(r)return new Date(Date.now()-parseInt(r[1])*36e5);if(h)return new Date(Date.now()-parseInt(h[1])*864e5);const F=new Date(i);if(!isNaN(F.getTime()))return F}const o=new Date(Date.now()-864e5),c=[];for(const r of t)if(r.type==="handoff"){const h=new Date(r.ts||0);isNaN(h.getTime())||c.push(h)}if(c.length===0)return o;const s=300*1e3,y=c[c.length-1];if(Date.now()-y.getTime()<s){if(c.length>=2){const r=c[c.length-2];return r>o?r:o}return o}return y>o?y:o}function yt(t){try{const i=process.platform;if(i==="win32")T("clip",{input:t});else if(i==="darwin")T("pbcopy",{input:t});else try{T("xclip -selection clipboard",{input:t})}catch{T("xsel --clipboard --input",{input:t})}return!0}catch{return!1}}function nt(){if(process.env.CURSOR_SESSION)return"Cursor";if(process.env.COPILOT_SESSION)return"GitHub Copilot";if(process.env.CLAUDE_CODE_SESSION)return"Claude Code";if(process.env.WINDSURF_SESSION)return"Windsurf";if(process.env.TERM_PROGRAM==="vscode")return"VS Code";const t=u(Y);return t?.ide?t.ide:null}function wt(){try{const t=T("git diff --stat HEAD 2>/dev/null || git diff --cached --stat 2>/dev/null",{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();return t||T("git log --stat -1 --pretty= 2>/dev/null",{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim()||null}catch{return null}}function at(t){try{const i=t&&t.getTime()>0?`--after="${t.toISOString()}"`:"-10",a=T(`git log ${i} --name-only --pretty=format: 2>/dev/null`,{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();if(!a)return[];const o={};for(const c of a.split(`
2
- `)){const s=c.trim();s&&(o[s]=(o[s]||0)+1)}return Object.entries(o).sort((c,s)=>s[1]-c[1]).slice(0,5).map(([c,s])=>({file:c,edits:s}))}catch{return[]}}function st(t){try{const i=t?`--after="${t.toISOString()}"`:"-5",a=T(`git log ${i} --pretty=format:"%h %s" 2>/dev/null`,{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();return a?a.split(`
3
- `).filter(Boolean):[]}catch{return[]}}function ft(t){const i=[],a=t.filter(o=>o.type==="attempt"&&(o.result==="failed"||o.result==="partial"||!o.result));for(const o of a)t.find(s=>s.type==="attempt"&&s.result==="worked"&&new Date(s.ts)>new Date(o.ts)&&s.summary.toLowerCase().includes(o.summary.split(" ")[0].toLowerCase()))||i.push({text:o.summary,ts:o.ts,kind:"unresolved-attempt"});for(const o of t)/\b(TODO|WIP|FIXME|BLOCKED|pending)\b/i.test(o.summary)&&(i.find(c=>c.text===o.summary)||i.push({text:o.summary,ts:o.ts,kind:"flagged"}));return i.slice(0,8)}function St(t,i,a){const o=u(X)||{},c=u(q)||{},s=u(Q),y=u(Y),S=et(),r=ot(S,i,a),h=S.filter(n=>new Date(n.ts||0)>r),F=S.slice(-5),E=new Date,d=E.toLocaleString("en-GB",{day:"2-digit",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit"}),b=c.policyId||m.basename(process.cwd()),N=c.policyVersion||"?",j=(c.capabilities||[]).slice(0,20),x=nt(),R=r.getTime()>0?E.getTime()-r.getTime():-1,v=tt(R),H=r.getTime()>0?r.getTime().toString(16).slice(-6).toUpperCase():"ALL",k=st(r.getTime()>0?r:null),L=wt(),G=at(r.getTime()>0?r:null),$=h.length>0?h:F,D=$.filter(n=>n.type==="gotcha"),l=$.filter(n=>n.type==="decision"),g=$.filter(n=>n.type==="attempt").filter(n=>n.result==="failed"||n.result==="partial"),W=$.filter(n=>n.type==="preference"),it=$.slice(-8),B=ft($),w=r.getTime()===0?"all time":r.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}),I=["sessions.jsonl"];(o.working||o.intent)&&I.push("context-state.json"),s&&I.push("theme.json"),c.capabilities?.length&&I.push("contract.json"),y&&I.push("adoption_profile.json"),k.length&&I.push("git log");const P=D.length,U=l.length,V=g.length;let C=Math.min(P*20,40)+Math.min(U*15,30)+Math.min(V*15,20);C=Math.min(C,100);const pt=C>=80?"A":C>=60?"B":C>=40?"C":C>=20?"D":"F",e=[`# \u{1F525} infernoflow Handoff \u2014 ${b}`,`> Generated: ${d}${t?` | Handing off to: **${t}**`:""}`,`> Session: **#${H}** \xB7 ${v} \xB7 **${h.length} entries** \xB7 Health: **${pt}** (${C}/100)`,`> Sources: ${I.join(" \xB7 ")}${x?` \xB7 IDE: ${x}`:""}`,"","---",""];if((o.working||o.intent)&&(e.push("## \u{1F3AF} Working on",""),o.working&&e.push(`**${o.working}** _(${Z(o.workingUpdated)})_`),o.intent&&e.push(`Intent: ${o.intent} _(${Z(o.intentUpdated)})_`),e.push("")),D.length&&(e.push(`## \u26A0\uFE0F STOP \u2014 Read These Before Doing Anything (${D.length} gotcha${D.length===1?"":"s"})`,""),D.forEach((n,p)=>{e.push(`${p+1}. **${n.summary}**`);const f=n.file||n.source;if(f&&/[\\/.]/.test(f)){const J=n.line?`${f}:${n.line}`:f;e.push(` \u2192 File: \`${J}\``)}}),e.push("")),l.length&&(e.push("## \u2713 Decisions In Effect \u2014 Follow These",""),l.forEach((n,p)=>{const f=n.result?` \u2192 **${n.result}**`:"";e.push(`${p+1}. ${n.summary}${f}`)}),e.push("")),g.length&&(e.push("## \u274C Already Tried \u2014 Don't Repeat",""),g.forEach((n,p)=>{const f=n.file||n.source,J=f&&/[\\/.]/.test(f)?` (\`${f}\`)`:"";e.push(`${p+1}. ${n.summary}${J} _(${Z(n.ts)})_`)}),e.push("")),G.length){e.push("## \u{1F4C1} Hot Files This Session","");for(const{file:n,edits:p}of G)e.push(`- \`${n}\` \u2014 ${p} edit${p!==1?"s":""}`);e.push("")}if(W.length){e.push("## Developer preferences","");for(const n of W)e.push(`- ${n.summary}`);e.push("")}if(k.length||L){if(e.push("## Git activity this session",""),k.length){e.push("**Commits:**");for(const n of k)e.push(`- \`${n}\``);e.push("")}L&&(e.push("**Uncommitted changes:**"),e.push("```"),e.push(L.split(`
1
+ import*as v from"node:fs";import*as m from"node:path";import"node:os";import{execSync as T}from"node:child_process";import{bold as K,cyan as O,gray as rt,green as ct,yellow as X,red as ht}from"../ui/output.mjs";import{ampPaths as dt,readEntries as gt,appendEntry as mt,readDetail as yt}from"../amp/io.mjs";const b="inferno";function R(){return dt(process.cwd())}const wt=m.join(b,"HANDOFF.md"),Ft=m.join(b,"sessions.jsonl"),q=m.join(b,"context-state.json"),Q=m.join(b,"contract.json"),Y=m.join(b,"theme.json"),Z=m.join(b,"adoption_profile.json");function u(o){try{return JSON.parse(v.readFileSync(o,"utf8"))}catch{return null}}function lt(o){try{return v.readFileSync(o,"utf8")}catch{return null}}function U(o){return o?new Date(o).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}):"unknown"}function tt(o){if(o<0)return"unknown";const i=Math.floor(o/36e5),a=Math.floor(o%36e5/6e4);return i>0?`${i}h ${a}m`:`${a}m`}function et(){return gt(process.cwd())}function ot(o,i,a){if(a)return new Date(0);if(i){const r=i.match(/^(\d+)h$/i),h=i.match(/^(\d+)d$/i);if(r)return new Date(Date.now()-parseInt(r[1])*36e5);if(h)return new Date(Date.now()-parseInt(h[1])*864e5);const E=new Date(i);if(!isNaN(E.getTime()))return E}const n=new Date(Date.now()-864e5),c=[];for(const r of o)if(r.type==="handoff"){const h=new Date(r.ts||0);isNaN(h.getTime())||c.push(h)}if(c.length===0)return n;const s=300*1e3,y=c[c.length-1];if(Date.now()-y.getTime()<s){if(c.length>=2){const r=c[c.length-2];return r>n?r:n}return n}return y>n?y:n}function St(o){try{const i=process.platform;if(i==="win32")T("clip",{input:o});else if(i==="darwin")T("pbcopy",{input:o});else try{T("xclip -selection clipboard",{input:o})}catch{T("xsel --clipboard --input",{input:o})}return!0}catch{return!1}}function nt(){if(process.env.CURSOR_SESSION)return"Cursor";if(process.env.COPILOT_SESSION)return"GitHub Copilot";if(process.env.CLAUDE_CODE_SESSION)return"Claude Code";if(process.env.WINDSURF_SESSION)return"Windsurf";if(process.env.TERM_PROGRAM==="vscode")return"VS Code";const o=u(Z);return o?.ide?o.ide:null}function $t(){try{const o=T("git diff --stat HEAD 2>/dev/null || git diff --cached --stat 2>/dev/null",{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();return o||T("git log --stat -1 --pretty= 2>/dev/null",{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim()||null}catch{return null}}function at(o){try{const i=o&&o.getTime()>0?`--after="${o.toISOString()}"`:"-10",a=T(`git log ${i} --name-only --pretty=format: 2>/dev/null`,{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();if(!a)return[];const n={};for(const c of a.split(`
2
+ `)){const s=c.trim();s&&(n[s]=(n[s]||0)+1)}return Object.entries(n).sort((c,s)=>s[1]-c[1]).slice(0,5).map(([c,s])=>({file:c,edits:s}))}catch{return[]}}function st(o){try{const i=o?`--after="${o.toISOString()}"`:"-5",a=T(`git log ${i} --pretty=format:"%h %s" 2>/dev/null`,{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();return a?a.split(`
3
+ `).filter(Boolean):[]}catch{return[]}}function ft(o){const i=[],a=o.filter(n=>n.type==="attempt"&&(n.result==="failed"||n.result==="partial"||!n.result));for(const n of a)o.find(s=>s.type==="attempt"&&s.result==="worked"&&new Date(s.ts)>new Date(n.ts)&&s.summary.toLowerCase().includes(n.summary.split(" ")[0].toLowerCase()))||i.push({text:n.summary,ts:n.ts,kind:"unresolved-attempt"});for(const n of o)/\b(TODO|WIP|FIXME|BLOCKED|pending)\b/i.test(n.summary)&&(i.find(c=>c.text===n.summary)||i.push({text:n.summary,ts:n.ts,kind:"flagged"}));return i.slice(0,8)}function Dt(o,i,a){const n=u(q)||{},c=u(Q)||{},s=u(Y),y=u(Z),$=et(),r=ot($,i,a),h=$.filter(e=>new Date(e.ts||0)>r),E=$.slice(-5),F=new Date,d=F.toLocaleString("en-GB",{day:"2-digit",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit"}),k=c.policyId||m.basename(process.cwd()),A=c.policyVersion||"?",j=(c.capabilities||[]).slice(0,20),G=nt(),B=r.getTime()>0?F.getTime()-r.getTime():-1,H=tt(B),x=r.getTime()>0?r.getTime().toString(16).slice(-6).toUpperCase():"ALL",N=st(r.getTime()>0?r:null),L=$t(),P=at(r.getTime()>0?r:null),w=h.length>0?h:E,D=w.filter(e=>e.type==="gotcha"),l=w.filter(e=>e.type==="decision"),g=w.filter(e=>e.type==="attempt").filter(e=>e.result==="failed"||e.result==="partial"),W=w.filter(e=>e.type==="preference"),_=w.filter(e=>Array.isArray(e.tags)&&e.tags.includes("bookmark")),V=w.slice(-8),S=ft(w),it=r.getTime()===0?"all time":r.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}),I=["sessions.jsonl"];(n.working||n.intent)&&I.push("context-state.json"),s&&I.push("theme.json"),c.capabilities?.length&&I.push("contract.json"),y&&I.push("adoption_profile.json"),N.length&&I.push("git log");const J=D.length,z=l.length,pt=g.length;let C=Math.min(J*20,40)+Math.min(z*15,30)+Math.min(pt*15,20);C=Math.min(C,100);const ut=C>=80?"A":C>=60?"B":C>=40?"C":C>=20?"D":"F",t=[`# \u{1F525} infernoflow Handoff \u2014 ${k}`,`> Generated: ${d}${o?` | Handing off to: **${o}**`:""}`,`> Session: **#${x}** \xB7 ${H} \xB7 **${h.length} entries** \xB7 Health: **${ut}** (${C}/100)`,`> Sources: ${I.join(" \xB7 ")}${G?` \xB7 IDE: ${G}`:""}`,"","---",""];if((n.working||n.intent)&&(t.push("## \u{1F3AF} Working on",""),n.working&&t.push(`**${n.working}** _(${U(n.workingUpdated)})_`),n.intent&&t.push(`Intent: ${n.intent} _(${U(n.intentUpdated)})_`),t.push("")),_.length){t.push(`## \u{1F516} Bookmarks \u2014 Resume Points (${_.length})`,""),_.forEach((f,M)=>t.push(`${M+1}. **${f.summary}** _(${U(f.ts)})_`));const e=_[_.length-1],p=e?yt(process.cwd(),e):null;p&&p.trim()&&t.push("",`<details><summary>\u{1F4CE} Context for "${e.summary}"</summary>`,"",p.trim(),"","</details>"),t.push("")}if(D.length&&(t.push(`## \u26A0\uFE0F STOP \u2014 Read These Before Doing Anything (${D.length} gotcha${D.length===1?"":"s"})`,""),D.forEach((e,p)=>{t.push(`${p+1}. **${e.summary}**`);const f=e.file||e.source;if(f&&/[\\/.]/.test(f)){const M=e.line?`${f}:${e.line}`:f;t.push(` \u2192 File: \`${M}\``)}}),t.push("")),l.length&&(t.push("## \u2713 Decisions In Effect \u2014 Follow These",""),l.forEach((e,p)=>{const f=e.result?` \u2192 **${e.result}**`:"";t.push(`${p+1}. ${e.summary}${f}`)}),t.push("")),g.length&&(t.push("## \u274C Already Tried \u2014 Don't Repeat",""),g.forEach((e,p)=>{const f=e.file||e.source,M=f&&/[\\/.]/.test(f)?` (\`${f}\`)`:"";t.push(`${p+1}. ${e.summary}${M} _(${U(e.ts)})_`)}),t.push("")),P.length){t.push("## \u{1F4C1} Hot Files This Session","");for(const{file:e,edits:p}of P)t.push(`- \`${e}\` \u2014 ${p} edit${p!==1?"s":""}`);t.push("")}if(W.length){t.push("## Developer preferences","");for(const e of W)t.push(`- ${e.summary}`);t.push("")}if(N.length||L){if(t.push("## Git activity this session",""),N.length){t.push("**Commits:**");for(const e of N)t.push(`- \`${e}\``);t.push("")}L&&(t.push("**Uncommitted changes:**"),t.push("```"),t.push(L.split(`
4
4
  `).slice(0,15).join(`
5
- `)),e.push("```"),e.push(""))}if(s){if(e.push("## Design system",""),s.fonts?.primary&&e.push(`- **Font:** ${s.fonts.primary}${s.fonts.mono?` \xB7 mono: ${s.fonts.mono}`:""}`),s.colors?.mode&&e.push(`- **Mode:** ${s.colors.mode}`),s.colors?.palette){const n=Object.entries(s.colors.palette).map(([p,f])=>`${p}=${f}`).join(" ");e.push(`- **Palette:** ${n}`)}if(s.cssVars&&Object.keys(s.cssVars).length){const n=Object.entries(s.cssVars).slice(0,6).map(([p,f])=>`${p}: ${f}`).join(" | ");e.push(`- **CSS vars:** ${n}`)}s.framework&&e.push(`- **Framework:** ${s.framework}`),e.push("","> \u26A0 Always match these exactly. Do not introduce new colors or fonts.","")}return j.length&&(e.push("## Capability contract",""),e.push(`Project: **${b}** v${N}`),e.push(`Capabilities: ${j.join(", ")}`),e.push("")),e.push("---"),e.push(`_Session #${H} \xB7 ${v} \xB7 Generated by infernoflow._`),e.join(`
6
- `)}async function Ft(t){const i=l=>t.includes(l),a=l=>{const g=t.indexOf(l);return g!==-1&&t[g+1]?t[g+1]:null},o=i("--show")||i("-s"),c=i("--copy")||i("-c"),s=i("--json"),y=i("--all"),S=a("--since"),r=a("--to")||t.find(l=>!l.startsWith("-")&&!["switch"].includes(l))||null;console.log(`
7
- `+z("\u{1F525} infernoflow \u2014 switch")),console.log(" "+"\u2500".repeat(50)+`
8
- `);const h=m.join(process.cwd(),".ai-memory");if(!A.existsSync(_)&&!A.existsSync(h)&&(console.error(ut(` \u2718 not initialized \u2014 run: infernoflow init
9
- `)),process.exit(1)),o){const l=lt(M().handoff)||lt(mt);if(!l){console.log(K(` \u26A0 No handoff yet \u2014 run: infernoflow switch
10
- `));return}console.log(l);return}const F=St(r,S,y);if(s){const l=u(X)||{},g=u(q)||{},W=u(Q),it=u(Y),B=et(),w=ot(B,S,y),I=B.filter(V=>new Date(V.ts||0)>w),P=st(w.getTime()>0?w:null),U=nt();console.log(JSON.stringify({state:l,contract:{policyId:g.policyId,policyVersion:g.policyVersion,capabilities:g.capabilities},theme:W,adoption:it,sessions:I,commits:P,ide:U,sessionStart:w.toISOString(),sessionId:w.getTime()>0?w.getTime().toString(16).slice(-6).toUpperCase():"ALL",sessionDuration:tt(w.getTime()>0?Date.now()-w.getTime():-1),generatedAt:new Date().toISOString()},null,2));return}A.writeFileSync(M().handoff,F,"utf8"),console.log(ct(" \u2714 Written \u2192 "+m.relative(process.cwd(),M().handoff)+`
11
- `));const E=et(),d=ot(E,S,y),b=E.filter(l=>new Date(l.ts||0)>d),N=u(X)||{},j=u(Q),x=u(q)||{},R=st(d.getTime()>0?d:null),v=at(d.getTime()>0?d:null),H=nt(),k=b.length>0?b:E.slice(-5),L=ft(k),G=tt(d.getTime()>0?Date.now()-d.getTime():-1),$=d.getTime()>0?d.getTime().toString(16).slice(-6).toUpperCase():"ALL";console.log(" "+z("Handoff ready")),console.log(" "+"\u2500".repeat(50)),console.log(" "+rt("Session #"+$+" \xB7 "+G)),N.working&&console.log(" Working on "+O(N.working)),N.intent&&console.log(" Intent "+O(N.intent)),console.log(" Memory "+b.length+" entries this session (total: "+E.length+")"),L.length&&console.log(" Open threads "+K(L.length+" unresolved")),R.length&&console.log(" Git commits "+R.length+" this session"),v.length&&console.log(" Hot files "+v.map(l=>O(l.file)).join(", ")),console.log(" Capabilities "+(x.capabilities||[]).length+" registered"),j?.fonts?.primary&&console.log(" Font "+j.fonts.primary),j?.colors?.mode&&console.log(" Color mode "+j.colors.mode),H&&console.log(" IDE "+H),r&&console.log(" Handing off \u2192 "+O(r)),console.log();const D=m.relative(process.cwd(),M().handoff)||".ai-memory/handoff.md";if(c){const l=yt(F);console.log(l?ct(" \u2714 Copied to clipboard \u2014 paste at the start of your next AI session"):K(" \u26A0 Clipboard failed \u2014 open "+D+" manually"))}else console.log(" "+z("Ready to use:")),console.log(" "+O("1.")+" Open "+O(D)),console.log(" "+O("2.")+" Copy all"),console.log(" "+O("3.")+" Paste at the start of your next AI session"),console.log(" "+rt(" tip: use --copy to skip steps 1-2 automatically"));if(console.log(),A.existsSync(M().sessions)){const l={ts:new Date().toISOString(),agent:"infernoflow",type:"handoff",summary:r?`Handed off to ${r}`:"Handoff generated"};gt(process.cwd(),l)}}export{Ft as switchCommand};
5
+ `)),t.push("```"),t.push(""))}if(s){if(t.push("## Design system",""),s.fonts?.primary&&t.push(`- **Font:** ${s.fonts.primary}${s.fonts.mono?` \xB7 mono: ${s.fonts.mono}`:""}`),s.colors?.mode&&t.push(`- **Mode:** ${s.colors.mode}`),s.colors?.palette){const e=Object.entries(s.colors.palette).map(([p,f])=>`${p}=${f}`).join(" ");t.push(`- **Palette:** ${e}`)}if(s.cssVars&&Object.keys(s.cssVars).length){const e=Object.entries(s.cssVars).slice(0,6).map(([p,f])=>`${p}: ${f}`).join(" | ");t.push(`- **CSS vars:** ${e}`)}s.framework&&t.push(`- **Framework:** ${s.framework}`),t.push("","> \u26A0 Always match these exactly. Do not introduce new colors or fonts.","")}return j.length&&(t.push("## Capability contract",""),t.push(`Project: **${k}** v${A}`),t.push(`Capabilities: ${j.join(", ")}`),t.push("")),t.push("---"),t.push(`_Session #${x} \xB7 ${H} \xB7 Generated by infernoflow._`),t.join(`
6
+ `)}async function jt(o){const i=l=>o.includes(l),a=l=>{const g=o.indexOf(l);return g!==-1&&o[g+1]?o[g+1]:null},n=i("--show")||i("-s"),c=i("--copy")||i("-c"),s=i("--json"),y=i("--all"),$=a("--since"),r=a("--to")||o.find(l=>!l.startsWith("-")&&!["switch"].includes(l))||null;console.log(`
7
+ `+K("\u{1F525} infernoflow \u2014 switch")),console.log(" "+"\u2500".repeat(50)+`
8
+ `);const h=m.join(process.cwd(),".ai-memory");if(!v.existsSync(b)&&!v.existsSync(h)&&(console.error(ht(` \u2718 not initialized \u2014 run: infernoflow init
9
+ `)),process.exit(1)),n){const l=lt(R().handoff)||lt(wt);if(!l){console.log(X(` \u26A0 No handoff yet \u2014 run: infernoflow switch
10
+ `));return}console.log(l);return}const E=Dt(r,$,y);if(s){const l=u(q)||{},g=u(Q)||{},W=u(Y),_=u(Z),V=et(),S=ot(V,$,y),it=V.filter(z=>new Date(z.ts||0)>S),I=st(S.getTime()>0?S:null),J=nt();console.log(JSON.stringify({state:l,contract:{policyId:g.policyId,policyVersion:g.policyVersion,capabilities:g.capabilities},theme:W,adoption:_,sessions:it,commits:I,ide:J,sessionStart:S.toISOString(),sessionId:S.getTime()>0?S.getTime().toString(16).slice(-6).toUpperCase():"ALL",sessionDuration:tt(S.getTime()>0?Date.now()-S.getTime():-1),generatedAt:new Date().toISOString()},null,2));return}v.writeFileSync(R().handoff,E,"utf8"),console.log(ct(" \u2714 Written \u2192 "+m.relative(process.cwd(),R().handoff)+`
11
+ `));const F=et(),d=ot(F,$,y),k=F.filter(l=>new Date(l.ts||0)>d),A=u(q)||{},j=u(Y),G=u(Q)||{},B=st(d.getTime()>0?d:null),H=at(d.getTime()>0?d:null),x=nt(),N=k.length>0?k:F.slice(-5),L=ft(N),P=tt(d.getTime()>0?Date.now()-d.getTime():-1),w=d.getTime()>0?d.getTime().toString(16).slice(-6).toUpperCase():"ALL";console.log(" "+K("Handoff ready")),console.log(" "+"\u2500".repeat(50)),console.log(" "+rt("Session #"+w+" \xB7 "+P)),A.working&&console.log(" Working on "+O(A.working)),A.intent&&console.log(" Intent "+O(A.intent)),console.log(" Memory "+k.length+" entries this session (total: "+F.length+")"),L.length&&console.log(" Open threads "+X(L.length+" unresolved")),B.length&&console.log(" Git commits "+B.length+" this session"),H.length&&console.log(" Hot files "+H.map(l=>O(l.file)).join(", ")),console.log(" Capabilities "+(G.capabilities||[]).length+" registered"),j?.fonts?.primary&&console.log(" Font "+j.fonts.primary),j?.colors?.mode&&console.log(" Color mode "+j.colors.mode),x&&console.log(" IDE "+x),r&&console.log(" Handing off \u2192 "+O(r)),console.log();const D=m.relative(process.cwd(),R().handoff)||".ai-memory/handoff.md";if(c){const l=St(E);console.log(l?ct(" \u2714 Copied to clipboard \u2014 paste at the start of your next AI session"):X(" \u26A0 Clipboard failed \u2014 open "+D+" manually"))}else console.log(" "+K("Ready to use:")),console.log(" "+O("1.")+" Open "+O(D)),console.log(" "+O("2.")+" Copy all"),console.log(" "+O("3.")+" Paste at the start of your next AI session"),console.log(" "+rt(" tip: use --copy to skip steps 1-2 automatically"));if(console.log(),v.existsSync(R().sessions)){const l={ts:new Date().toISOString(),agent:"infernoflow",type:"handoff",summary:r?`Handed off to ${r}`:"Handoff generated"};mt(process.cwd(),l)}}export{jt as switchCommand};
@@ -1 +1 @@
1
- import*as a from"node:fs";import*as t from"node:path";const d=["package.json","Cargo.toml","pyproject.toml","go.mod","Gemfile","composer.json","deno.json","deno.jsonc","build.gradle","build.gradle.kts","pom.xml","mix.exs"],j=[/\.sln$/i,/\.csproj$/i,/\.fsproj$/i,/\.vbproj$/i],f=new Map;function h(r){for(const n of d)if(a.existsSync(t.join(r,n)))return!0;return!1}function y(r){try{for(const n of a.readdirSync(r))for(const e of j)if(e.test(n))return!0}catch{}return!1}function x(r=process.cwd()){const n=t.resolve(r);if(f.has(n))return f.get(n);let e=n;for(;;){if(a.existsSync(t.join(e,".ai-memory"))||a.existsSync(t.join(e,"inferno")))return c(n,e);const o=t.dirname(e);if(o===e)break;e=o}for(e=n;;){if(a.existsSync(t.join(e,".git"))||h(e)||y(e))return c(n,e);const o=t.dirname(e);if(o===e)break;e=o}return c(n,n)}function c(r,n){return f.set(r,n),n}function S(){f.clear()}function g(r,n=6){const e=[],o=new Set,p=new Set(["node_modules",".git","dist","build","out","bin","obj",".next",".nuxt",".angular",".svelte-kit","coverage","vendor","target",".venv","venv","__pycache__",".pytest_cache"]);function u(s,m){if(m>n||o.has(s))return;o.add(s);let l;try{l=a.readdirSync(s,{withFileTypes:!0})}catch{return}for(const i of l)i.isDirectory()&&(p.has(i.name)||(i.name===".ai-memory"&&e.push({kind:"amp",path:t.join(s,i.name)}),i.name==="inferno"&&e.push({kind:"legacy",path:t.join(s,i.name)}),u(t.join(s,i.name),m+1)))}return u(t.resolve(r),0),e}export{S as _resetProjectRootCache,g as findAllMemoryDirs,x as findProjectRoot};
1
+ import*as c from"node:fs";import*as t from"node:path";const d=["package.json","Cargo.toml","pyproject.toml","go.mod","Gemfile","composer.json","deno.json","deno.jsonc","build.gradle","build.gradle.kts","pom.xml","mix.exs"],j=[/\.sln$/i,/\.csproj$/i,/\.fsproj$/i,/\.vbproj$/i],u=new Map;function h(o){for(const n of d)if(c.existsSync(t.join(o,n)))return!0;return!1}function y(o){try{for(const n of c.readdirSync(o))for(const i of j)if(i.test(n))return!0}catch{}return!1}function x(o=process.cwd()){const n=t.resolve(o);if(u.has(n))return u.get(n);const i=r=>t.dirname(r)===r;let e=n;for(;;){if(!i(e)&&(c.existsSync(t.join(e,".ai-memory"))||c.existsSync(t.join(e,"inferno"))))return f(n,e);const r=t.dirname(e);if(r===e)break;e=r}for(e=n;;){if(!i(e)&&(c.existsSync(t.join(e,".git"))||h(e)||y(e)))return f(n,e);const r=t.dirname(e);if(r===e)break;e=r}return f(n,n)}function f(o,n){return u.set(o,n),n}function S(){u.clear()}function g(o,n=6){const i=[],e=new Set,r=new Set(["node_modules",".git","dist","build","out","bin","obj",".next",".nuxt",".angular",".svelte-kit","coverage","vendor","target",".venv","venv","__pycache__",".pytest_cache"]);function m(a,l){if(l>n||e.has(a))return;e.add(a);let p;try{p=c.readdirSync(a,{withFileTypes:!0})}catch{return}for(const s of p)s.isDirectory()&&(r.has(s.name)||(s.name===".ai-memory"&&i.push({kind:"amp",path:t.join(a,s.name)}),s.name==="inferno"&&i.push({kind:"legacy",path:t.join(a,s.name)}),m(t.join(a,s.name),l+1)))}return m(t.resolve(o),0),i}export{S as _resetProjectRootCache,g as findAllMemoryDirs,x as findProjectRoot};
@@ -1,13 +1,14 @@
1
- import*as l from"node:fs";import*as d from"node:path";import{execSync as I}from"node:child_process";import{readEntries as j,readConfig as _}from"./amp/io.mjs";const y="<!-- infernoflow:start -->",f="<!-- infernoflow:end -->",x=[".cursorrules","CLAUDE.md",d.join(".github","copilot-instructions.md")],b=4,A=5,C=200;function v(e){const n=e&&e.config||{},t=n.injection&&typeof n.injection=="object"?n.injection:{},o=Array.isArray(n.inject)&&!n.inject.includes("all")?n.inject:null;return{maxEntries:Number.isInteger(t.maxEntries)&&t.maxEntries>=0?t.maxEntries:b,maxCommits:Number.isInteger(t.maxCommits)&&t.maxCommits>=0?t.maxCommits:A,maxEntryChars:Number.isInteger(t.maxEntryChars)&&t.maxEntryChars>0?t.maxEntryChars:C,targets:Array.isArray(t.targets)&&t.targets.length?t.targets:o||x,includeProtocol:t.includeProtocol!==!1}}function k(e){if(!l.existsSync(e))return{stripped:!1};const n=l.readFileSync(e,"utf8"),t=n.indexOf(y),o=n.indexOf(f);if(t===-1||o===-1||o<=t)return{stripped:!1};const r=n.slice(0,t).replace(/\s+$/,""),s=n.slice(o+f.length).replace(/^\s+/,""),i=r&&s?r+`
1
+ import*as u from"node:fs";import*as y from"node:path";import{execSync as j}from"node:child_process";import{readEntries as v,readConfig as x}from"./amp/io.mjs";const g="<!-- infernoflow:start -->",m="<!-- infernoflow:end -->",w=[".cursorrules","CLAUDE.md",y.join(".github","copilot-instructions.md")],k=4,C=5,S=200;function A(e){const o=e&&e.config||{},t=o.injection&&typeof o.injection=="object"?o.injection:{},n=Array.isArray(o.inject)&&!o.inject.includes("all")?o.inject:null,r=["full","compact","off"].includes(t.protocolStyle)?t.protocolStyle:"compact",s=t.includeProtocol===!1?"off":r;return{maxEntries:Number.isInteger(t.maxEntries)&&t.maxEntries>=0?t.maxEntries:k,maxCommits:Number.isInteger(t.maxCommits)&&t.maxCommits>=0?t.maxCommits:C,maxEntryChars:Number.isInteger(t.maxEntryChars)&&t.maxEntryChars>0?t.maxEntryChars:S,targets:Array.isArray(t.targets)&&t.targets.length?t.targets:n||w,protocolStyle:s,includeProtocol:s!=="off"}}function T(e){if(!u.existsSync(e))return{stripped:!1};const o=u.readFileSync(e,"utf8"),t=o.indexOf(g),n=o.indexOf(m);if(t===-1||n===-1||n<=t)return{stripped:!1};const r=o.slice(0,t).replace(/\s+$/,""),s=o.slice(n+m.length).replace(/^\s+/,""),i=r&&s?r+`
2
2
 
3
- `+s:r||s;return l.writeFileSync(e,i?i.replace(/\s*$/,"")+`
4
- `:"","utf8"),{stripped:!0}}function E(){const e=[];return e.push("### Memory protocol \u2014 capture as you go"),e.push(""),e.push("You have the `amp_write` MCP tool. **Use it proactively** (without the user asking) whenever you detect any of these moments. Capture the lesson in one sentence so the next AI session inherits it."),e.push(""),e.push("**If `amp_write` is not in your visible tools**, your client uses deferred MCP loading \u2014 call your tool-discovery mechanism (Claude Code: `ToolSearch` with query `infernoflow`) to load the `amp_*` tools BEFORE proceeding. Don't silently skip the protocol because the tool isn't visible yet."),e.push(""),e.push("| When you see\u2026 | Call `amp_write` with\u2026 |"),e.push("|---|---|"),e.push("| User types `!!`, `!!!`, `not working`, `still broken`, `no change`, `same error`, `retry` / `retrying`, repeated frustration | `{type: 'attempt', msg: '<one-line: what was tried and how it failed>', file: '<active file>'}` |"),e.push("| You generate a plan, numbered steps, or `Plan:` / `Approach:` block | `{type: 'note', msg: 'Plan: <one-line summary>', file: '<focus file>'}` |"),e.push("| User picks X over Y, decides an architectural approach, or you both agree on a direction | `{type: 'decision', msg: '<X> chosen over <Y> because <reason>', file: '<file>'}` |"),e.push("| Discovered non-obvious behavior, API quirk, or `aha \u2014 turns out\u2026` moment | `{type: 'gotcha', msg: '<one-line>', file: '<file>', line: <line if known>}` |"),e.push("| User says `switching` / `back to X`, or ~30 min passed without logging | `{type: 'note', msg: 'Session snapshot \u2014 worked on: <X>. Open threads: <Y>.'}` |"),e.push(""),e.push("**Do NOT log:** typos, syntax errors, obvious bugs visible in the code, anything the next AI can infer by re-reading the file."),e.push("**Do log:** anything that taught you something non-derivable from code."),e.push("**Be concise:** entries should be one sentence. Future-you will skim them."),e.push("**When you use a \u{1F525} memory entry in your reply, briefly cite it** \u2014 e.g. `\u{1F525} (from infernoflow memory) gotcha at src/api.js:42: ...` \u2014 so the user can see which guidance came from project memory vs. your own inference."),e.join(`
5
- `)}function T(){return[y,"<!-- Auto-managed by infernoflow. Don't edit between these markers. -->","## Project memory (infernoflow)","",E(),"",'_No entries yet. They\'ll appear here as you and your AI tools log them \u2014 run `infernoflow log "..."` or call `amp_write` from any MCP-aware AI._',f].join(`
6
- `)}function F(e){const n=e.indexOf("<!-- AMP:START -->"),t=e.indexOf("<!-- AMP:END -->");if(n===-1||t===-1||t<=n)return e;const o=e.slice(0,n).replace(/\s+$/,""),r=e.slice(t+16).replace(/^\s+/,"");return(o?o+(r?`
3
+ `+s:r||s;return u.writeFileSync(e,i?i.replace(/\s*$/,"")+`
4
+ `:"","utf8"),{stripped:!0}}function E(e="compact"){return e==="off"?"":e==="full"?M():F()}function F(){return["### Memory protocol","",'Use the `amp_write` and `amp_bookmark` MCP tools **proactively** (without being asked): log a one-sentence entry when the user hits frustration (`!!` / "not working" / "retry" \u2192 `attempt`), a `decision` is made, or you learn a non-obvious `gotcha`; drop a bookmark on "bookmark this". Don\'t log what\'s re-derivable from the code. The full trigger list and field shapes are in the tool descriptions \u2014 if the `amp_*` tools aren\'t visible, load them first (Claude Code: `ToolSearch` with query `infernoflow`).'].join(`
5
+ `)}function M(){const e=[];return e.push("### Memory protocol \u2014 capture as you go"),e.push(""),e.push("You have the `amp_write` MCP tool. **Use it proactively** (without the user asking) whenever you detect any of these moments. Capture the lesson in one sentence so the next AI session inherits it."),e.push(""),e.push("**If `amp_write` is not in your visible tools**, your client uses deferred MCP loading \u2014 call your tool-discovery mechanism (Claude Code: `ToolSearch` with query `infernoflow`) to load the `amp_*` tools BEFORE proceeding. Don't silently skip the protocol because the tool isn't visible yet."),e.push(""),e.push("| When you see\u2026 | Call `amp_write` with\u2026 |"),e.push("|---|---|"),e.push("| User types `!!`, `!!!`, `not working`, `still broken`, `no change`, `same error`, `retry` / `retrying`, repeated frustration | `{type: 'attempt', msg: '<one-line: what was tried and how it failed>', file: '<active file>'}` |"),e.push("| You generate a plan, numbered steps, or `Plan:` / `Approach:` block | `{type: 'note', msg: 'Plan: <one-line summary>', file: '<focus file>'}` |"),e.push("| User picks X over Y, decides an architectural approach, or you both agree on a direction | `{type: 'decision', msg: '<X> chosen over <Y> because <reason>', file: '<file>'}` |"),e.push("| Discovered non-obvious behavior, API quirk, or `aha \u2014 turns out\u2026` moment | `{type: 'gotcha', msg: '<one-line>', file: '<file>', line: <line if known>}` |"),e.push("| User says `switching` / `back to X`, or ~30 min passed without logging | `{type: 'note', msg: 'Session snapshot \u2014 worked on: <X>. Open threads: <Y>.'}` |"),e.push(""),e.push("**Do NOT log:** typos, syntax errors, obvious bugs visible in the code, anything the next AI can infer by re-reading the file."),e.push("**Do log:** anything that taught you something non-derivable from code."),e.push("**Be concise:** entries should be one sentence. Future-you will skim them."),e.push("**Bookmark resume points:** when the user says `bookmark this` / `mark this point`, or the context window is filling up while work is mid-flight, call `amp_bookmark` with a short `label` (omit `note` to auto-capture the session transcript) \u2014 so they can jump back to that exact point in the next session."),e.push("**When you use a \u{1F525} memory entry in your reply, briefly cite it** \u2014 e.g. `\u{1F525} (from infernoflow memory) gotcha at src/api.js:42: ...` \u2014 so the user can see which guidance came from project memory vs. your own inference."),e.join(`
6
+ `)}function D(e="compact"){const o=E(e);return[g,"<!-- Auto-managed by infernoflow. Don't edit between these markers. -->","## Project memory (infernoflow)","",...o?[o,""]:[],'_No entries yet. They\'ll appear here as you and your AI tools log them \u2014 run `infernoflow log "..."` or call `amp_write` from any MCP-aware AI._',m].join(`
7
+ `)}function R(e){const o=e.indexOf("<!-- AMP:START -->"),t=e.indexOf("<!-- AMP:END -->");if(o===-1||t===-1||t<=o)return e;const n=e.slice(0,o).replace(/\s+$/,""),r=e.slice(t+16).replace(/^\s+/,"");return(n?n+(r?`
7
8
 
8
- `:""):"")+r}function S(e,n){const t=d.dirname(e);if(l.existsSync(t)||l.mkdirSync(t,{recursive:!0}),!l.existsSync(e))return l.writeFileSync(e,n+`
9
- `,"utf8"),{created:!0,updated:!1};let o=l.readFileSync(e,"utf8");o=F(o);const r=o.indexOf(y),s=o.indexOf(f);if(r===-1||s===-1){const g=n+`
9
+ `:""):"")+r}function I(e,o){const t=y.dirname(e);if(u.existsSync(t)||u.mkdirSync(t,{recursive:!0}),!u.existsSync(e))return u.writeFileSync(e,o+`
10
+ `,"utf8"),{created:!0,updated:!1};let n=u.readFileSync(e,"utf8");n=R(n);const r=n.indexOf(g),s=n.indexOf(m);if(r===-1||s===-1){const d=o+`
10
11
 
11
- `+o;return l.writeFileSync(e,g,"utf8"),{created:!1,updated:!0}}const i=o.slice(0,r),p=o.slice(s+f.length),u=i+n+p;return u===o?{created:!1,updated:!1}:(l.writeFileSync(e,u,"utf8"),{created:!1,updated:!0})}function $(e){const n=T(),t=[];for(const o of x){const r=d.join(e,o);try{const s=S(r,n);t.push({rel:o,...s})}catch(s){t.push({rel:o,error:s.message})}}return t}function D(e){return j(e)}function M(e,n=10){try{return I(`git log --pretty=format:"%h%x09%ad%x09%s" --date=short -n ${n}`,{cwd:e,encoding:"utf8",stdio:["ignore","pipe","ignore"]}).split(`
12
- `).filter(Boolean).map(o=>{const[r,s,i]=o.split(" ");return{hash:(r||"").slice(0,7),date:s||"",subject:i||""}})}catch{return[]}}function R(e,n={}){const{maxEntries:t=b,maxCommits:o=A,maxEntryChars:r=C,includeProtocol:s=!0}=n,i=D(e),p=M(e,o);i.sort((c,m)=>{const h=typeof c.ts=="number"?c.ts:Date.parse(c.ts||0);return(typeof m.ts=="number"?m.ts:Date.parse(m.ts||0))-h});const u=i.slice(0,t),g={gotcha:"\u26A0",decision:"\u2713",attempt:"\u2717",note:"\xB7",detection:"\u25CB",pattern:"\u25C7"},a=[];if(a.push(y),a.push("<!-- Auto-managed by infernoflow. Don't edit between these markers. -->"),a.push("## Project memory (infernoflow)"),a.push(""),s&&(a.push(E()),a.push("")),p.length>0){a.push("### Recent commits");for(const c of p)a.push(`- \`${c.hash}\` _${c.date}_ ${c.subject}`);a.push("")}if(u.length>0){a.push("### Recent memory");for(const c of u){const m=c.file?` (\`${c.file}${c.line?":"+c.line:""}\`)`:"",h=(c.msg||c.summary||"").replace(/\n/g," "),w=r&&h.length>r?h.slice(0,r).trimEnd()+"\u2026":h;a.push(`- \u{1F525} ${g[c.type]||"\xB7"} **${c.type||"note"}**${m}: ${w}`)}a.push("")}return i.length===0&&p.length===0&&a.push('_No entries yet. They\'ll appear here as you and your AI tools log them \u2014 run `infernoflow log "..."` or call `amp_write` from any MCP-aware AI._'),a.push(f),a.join(`
13
- `)}function P(e){const n=v(_(e)),t=R(e,n),o=i=>String(i).replace(/\\/g,"/"),r=new Set(n.targets.map(o)),s=[];for(const i of x){const p=d.join(e,i);try{if(r.has(o(i))){const u=S(p,t);s.push({rel:i,...u})}else k(p).stripped&&s.push({rel:i,stripped:!0})}catch(u){s.push({rel:i,error:u.message})}}return s}export{P as refreshRuleFilesFromMemory,v as resolveInjectionSettings,$ as writeInitRuleFiles};
12
+ `+n;return u.writeFileSync(e,d,"utf8"),{created:!1,updated:!0}}const i=n.slice(0,r),p=n.slice(s+m.length),l=i+o+p;return l===n?{created:!1,updated:!1}:(u.writeFileSync(e,l,"utf8"),{created:!1,updated:!0})}function B(e){const{protocolStyle:o}=A(x(e)),t=D(o),n=[];for(const r of w){const s=y.join(e,r);try{const i=I(s,t);n.push({rel:r,...i})}catch(i){n.push({rel:r,error:i.message})}}return n}function N(e){return v(e)}function P(e,o=10){try{return j(`git log --pretty=format:"%h%x09%ad%x09%s" --date=short -n ${o}`,{cwd:e,encoding:"utf8",stdio:["ignore","pipe","ignore"]}).split(`
13
+ `).filter(Boolean).map(n=>{const[r,s,i]=n.split(" ");return{hash:(r||"").slice(0,7),date:s||"",subject:i||""}})}catch{return[]}}function O(e,o={}){const{maxEntries:t=k,maxCommits:n=C,maxEntryChars:r=S,protocolStyle:s="compact",includeProtocol:i=!0}=o,p=N(e),l=P(e,n);p.sort((a,h)=>{const f=typeof a.ts=="number"?a.ts:Date.parse(a.ts||0);return(typeof h.ts=="number"?h.ts:Date.parse(h.ts||0))-f});const d=p.slice(0,t),_={gotcha:"\u26A0",decision:"\u2713",attempt:"\u2717",note:"\xB7",detection:"\u25CB",pattern:"\u25C7"},c=[];if(c.push(g),c.push("<!-- Auto-managed by infernoflow. Don't edit between these markers. -->"),c.push("## Project memory (infernoflow)"),c.push(""),i&&s!=="off"&&(c.push(E(s)),c.push("")),l.length>0){c.push("### Recent commits");for(const a of l)c.push(`- \`${a.hash}\` _${a.date}_ ${a.subject}`);c.push("")}if(d.length>0){c.push("### Recent memory");for(const a of d){const h=a.file?` (\`${a.file}${a.line?":"+a.line:""}\`)`:"",f=(a.msg||a.summary||"").replace(/\n/g," "),b=r&&f.length>r?f.slice(0,r).trimEnd()+"\u2026":f;c.push(`- \u{1F525} ${_[a.type]||"\xB7"} **${a.type||"note"}**${h}: ${b}`)}c.push("")}return p.length===0&&l.length===0&&c.push('_No entries yet. They\'ll appear here as you and your AI tools log them \u2014 run `infernoflow log "..."` or call `amp_write` from any MCP-aware AI._'),c.push(m),c.join(`
14
+ `)}function L(e){const o=A(x(e)),t=O(e,o),n=i=>String(i).replace(/\\/g,"/"),r=new Set(o.targets.map(n)),s=[];for(const i of w){const p=y.join(e,i);try{if(r.has(n(i))){const l=I(p,t);s.push({rel:i,...l})}else T(p).stripped&&s.push({rel:i,stripped:!0})}catch(l){s.push({rel:i,error:l.message})}}return s}export{L as refreshRuleFilesFromMemory,A as resolveInjectionSettings,B as writeInitRuleFiles};
@@ -0,0 +1,4 @@
1
+ import*as f from"node:fs";import*as g from"node:os";import*as p from"node:path";function x(e){const t=p.resolve(e).replace(/[^a-zA-Z0-9]/g,"-");return p.join(g.homedir(),".claude","projects",t)}function d(e){const t=x(e);let l;try{l=f.readdirSync(t)}catch{return null}const i=l.filter(n=>n.endsWith(".jsonl")).map(n=>{const s=p.join(t,n);try{return{p:s,mtime:f.statSync(s).mtimeMs}}catch{return null}}).filter(Boolean).sort((n,s)=>s.mtime-n.mtime);return i.length?i[0].p:null}function y(e){return e?typeof e=="string"?e:Array.isArray(e)?e.map(t=>typeof t=="string"?t:t&&t.type==="text"?t.text:"").filter(Boolean).join(`
2
+ `):"":""}function j(e,t={}){const l=Number.isInteger(t.maxMessages)?t.maxMessages:40,i=Number.isInteger(t.maxCharsPerMsg)?t.maxCharsPerMsg:1500,n=d(e);if(!n)return null;let s;try{s=f.readFileSync(n,"utf8").split(`
3
+ `).filter(Boolean)}catch{return null}const c=[];for(const o of s){let r;try{r=JSON.parse(o)}catch{continue}const a=r.type||r.message&&r.message.role;if(a!=="user"&&a!=="assistant")continue;const m=y(r.message?r.message.content:r.content);m&&m.trim()&&c.push({role:a,text:m.trim()})}if(c.length===0)return null;const u=c.slice(-l),h=["# \u{1F516} Session snapshot","",`_Auto-captured ${u.length} recent turn${u.length===1?"":"s"} from the Claude Code transcript._`,""];for(const o of u){const r=o.role==="user"?"You":"AI",a=o.text.length>i?o.text.slice(0,i).trimEnd()+" \u2026":o.text;h.push(`**${r}:** ${a}`,"")}return h.join(`
4
+ `)}export{x as claudeProjectDir,d as findLatestTranscript,j as harvestSnapshot};
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: memory-keeper
3
+ description: >-
4
+ Captures durable session memory into infernoflow. Invoke at a stopping point,
5
+ before context is lost, or whenever the user says "save what we learned",
6
+ "remember this", "checkpoint", or "bookmark this". It sweeps the work for real
7
+ gotchas, decisions-with-a-because, dead ends, and durable preferences; logs the
8
+ ones worth keeping (skipping noise and duplicates); and drops a named bookmark
9
+ when asked or at a milestone. Read-and-write to the local inferno memory only.
10
+ tools: Bash, Read, Grep
11
+ ---
12
+
13
+ You are **memory-keeper**, a specialist that turns a coding session into durable,
14
+ searchable memory using the `infernoflow` CLI. You are the hands that run the
15
+ commands so the main agent doesn't have to. You never touch application code —
16
+ you only read context and write memory.
17
+
18
+ ## Operating rule: balanced
19
+
20
+ Capture what genuinely saves future time; skip the noise. A good memory entry is
21
+ something a competent developer (or the next AI session) could NOT infer from
22
+ reading the code and the diff. When in doubt about a real gotcha, log it. When in
23
+ doubt about routine work, skip it.
24
+
25
+ ## Procedure
26
+
27
+ 1. **Confirm memory is initialized.** Run `infernoflow status`. If there is no
28
+ store (`.ai-memory/`), stop and report that the project isn't initialized —
29
+ do not run `init` yourself.
30
+
31
+ 2. **Load what's already remembered** so you don't duplicate:
32
+ - `infernoflow log --show 20` — recent entries.
33
+ - `infernoflow ask "<topic>"` — for each candidate topic, check if it's known.
34
+
35
+ 3. **Identify capture-worthy items** from the context you were given (and, if
36
+ useful, from files via Read/Grep). Sort each candidate into one type:
37
+ - **gotcha** — behaved contrary to a reasonable expectation and cost time.
38
+ - **decision** — a non-obvious choice; include the *because*. Add `--result worked` (or `failed`).
39
+ - **dead end** — something tried that did NOT work → `--type gotcha --result failed`.
40
+ - **preference** — a durable thing the user wants across sessions.
41
+
42
+ 4. **Log each kept item**, one specific sentence per entry, tagged for tracing:
43
+ ```
44
+ infernoflow log "API expects multipart/form-data, rejects application/json" --type gotcha --source memory-keeper --quiet
45
+ infernoflow log "axios over fetch — needed upload progress events" --type decision --result worked --source memory-keeper --quiet
46
+ infernoflow log "tried chunked streaming upload, server rejected it" --type gotcha --result failed --source memory-keeper --quiet
47
+ infernoflow log "user prefers inline error handling over wrapper utils" --type preference --source memory-keeper --quiet
48
+ ```
49
+
50
+ 5. **Bookmark** when the user asked for one, at a milestone, or before a risky
51
+ change (on Claude Code this auto-harvests the recent transcript):
52
+ ```
53
+ infernoflow bookmark "auth flow works end to end"
54
+ infernoflow bookmark "before the state refactor" --note "current: context provider per feature"
55
+ ```
56
+
57
+ 6. **Report back** concisely to the main agent:
58
+ - What you logged (one line each, with type).
59
+ - What you deliberately skipped and why (dupe / obvious / routine).
60
+ - Any bookmark dropped.
61
+
62
+ ## Never
63
+ - Never invent entries. Capture only what actually happened in this session —
64
+ if you weren't told it and can't verify it on disk, don't log it.
65
+ - Never log secrets, tokens, credentials, or personal data.
66
+ - Never re-log something `infernoflow ask` shows is already captured.
67
+ - Never edit source code, run builds, or run destructive git/rm commands.
68
+ - Never batch several distinct insights into one entry — one insight per log.
69
+
70
+ Keep the whole pass fast and quiet. Your value is a clean, deduped memory the
71
+ next session inherits — not volume.
@@ -151,6 +151,27 @@ const TRIGGER_RES = [
151
151
  /\bdoesn['’]?t work\b/i,
152
152
  ];
153
153
 
154
+ // Deterministic BOOKMARK triggers — an explicit "bookmark this" is an intentional
155
+ // resume point (not a trouble signal), so it takes precedence and drops a real
156
+ // bookmark (which auto-captures the session transcript). No AI cooperation needed.
157
+ const BOOKMARK_RES = [
158
+ /\bbookmark (?:this|it|here)(?: point)?\b/i,
159
+ /\bmark this (?:point|spot|moment|here)\b/i,
160
+ /\bsave (?:this )?(?:point|checkpoint|resume point)\b/i,
161
+ ];
162
+
163
+ /** Strip the trigger phrase to reuse the rest of the prompt as the bookmark label. */
164
+ function deriveBookmarkLabel(prompt) {
165
+ const label = prompt
166
+ .replace(/\bbookmark (?:this|it|here)(?: point)?\b/ig, "")
167
+ .replace(/\bmark this (?:point|spot|moment|here)\b/ig, "")
168
+ .replace(/\bsave (?:this )?(?:point|checkpoint|resume point)\b/ig, "")
169
+ .replace(/[\s:.!,–—-]+/g, " ")
170
+ .replace(/\s+/g, " ")
171
+ .trim();
172
+ return (label.slice(0, 80).trim()) || "Session bookmark";
173
+ }
174
+
154
175
  function memoryRootExists() {
155
176
  return fs.existsSync(path.join(projectRoot(), ".ai-memory")) ||
156
177
  fs.existsSync(path.join(projectRoot(), "inferno"));
@@ -166,10 +187,41 @@ function cheapHash(s) {
166
187
  return String(h);
167
188
  }
168
189
 
190
+ function handleBookmarkTrigger(prompt) {
191
+ const now = Date.now();
192
+ const stateFile = triggerStatePath();
193
+ let state = {};
194
+ try { state = JSON.parse(fs.readFileSync(stateFile, "utf8")); } catch {}
195
+ const h = cheapHash(prompt.slice(0, 200));
196
+ if (state.lastBmHash === h) return; // same prompt — don't double-fire
197
+
198
+ const label = deriveBookmarkLabel(prompt);
199
+ let wrote = false;
200
+ try {
201
+ // The `bookmark` command (no --marker) auto-captures the session transcript
202
+ // as the resume point. Needs infernoflow >= 0.44.10 on PATH; if the global
203
+ // 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",
207
+ });
208
+ wrote = r.status === 0;
209
+ } catch { /* CLI unavailable — skip */ }
210
+
211
+ if (wrote) {
212
+ try { fs.writeFileSync(stateFile, JSON.stringify({ ...state, lastBmHash: h }), "utf8"); } catch {}
213
+ process.stderr.write("[inferno-session-draft] auto-bookmarked resume point\n");
214
+ }
215
+ }
216
+
169
217
  function handleUserPrompt(text) {
170
218
  const trimmed = (text || "").trim();
171
219
  if (!trimmed) return;
172
220
  if (!memoryRootExists()) return; // only inside infernoflow projects
221
+
222
+ // Bookmark trigger takes precedence — "bookmark this" is intentional, not trouble.
223
+ if (BOOKMARK_RES.some((re) => re.test(trimmed))) { handleBookmarkTrigger(trimmed); return; }
224
+
173
225
  if (!TRIGGER_RES.some((re) => re.test(trimmed))) return;
174
226
 
175
227
  const now = Date.now();
@@ -205,7 +257,7 @@ function handleUserPrompt(text) {
205
257
  }
206
258
 
207
259
  if (wrote) {
208
- try { fs.writeFileSync(stateFile, JSON.stringify({ lastTs: now, lastHash: h }), "utf8"); } catch {}
260
+ try { fs.writeFileSync(stateFile, JSON.stringify({ ...state, lastTs: now, lastHash: h }), "utf8"); } catch {}
209
261
  process.stderr.write("[inferno-session-draft] auto-captured trigger to memory\n");
210
262
  }
211
263
  }
@@ -113,6 +113,7 @@ const INFERNOFLOW_BIN = resolveInfernoflowBin();
113
113
  // Falls back to shell-out via runCmd() if the AMP layer can't be loaded.
114
114
  let ampIo = null;
115
115
  let refreshRuleFiles = null;
116
+ let harvestSnapshot = null;
116
117
  if (INFERNOFLOW_ROOT) {
117
118
  try {
118
119
  for (const c of [
@@ -127,6 +128,12 @@ if (INFERNOFLOW_ROOT) {
127
128
  ]) {
128
129
  if (fs.existsSync(c)) { refreshRuleFiles = (await import(pathToFileURL(c).href)).refreshRuleFilesFromMemory; break; }
129
130
  }
131
+ for (const c of [
132
+ path.join(INFERNOFLOW_ROOT, "lib", "transcript.mjs"),
133
+ path.join(INFERNOFLOW_ROOT, "dist", "lib", "transcript.mjs"),
134
+ ]) {
135
+ if (fs.existsSync(c)) { harvestSnapshot = (await import(pathToFileURL(c).href)).harvestSnapshot; break; }
136
+ }
130
137
  } catch { /* swallow — fallback path handles it */ }
131
138
  }
132
139
 
@@ -224,8 +231,9 @@ function isCmdError(result) {
224
231
  const TOOLS = [
225
232
  // ── AMP-spec memory tools (the product) ──────────────────────────────────
226
233
  { 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" } } } },
227
- { name: "amp_write", description: "AMP: log a new entry. Required: type + msg. Optional: file, line, tags.", 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" } } }, required: ["type","msg"] } },
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/<id>.md; NOT injected into rule files. Put the long-form context here; keep 'msg' to one summary sentence." } }, required: ["type","msg"] } },
228
235
  { 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
+ { 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"] } },
229
237
  { 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"] } } } },
230
238
  { name: "amp_health", description: "AMP: get the session health score (0-100, A-F grade).", inputSchema: { type: "object", properties: {} } },
231
239
 
@@ -392,6 +400,7 @@ function handleTool(id, name, input) {
392
400
  if (input.file) entry.file = input.file;
393
401
  if (input.line) entry.line = input.line;
394
402
  if (input.tags && input.tags.length) entry.tags = input.tags;
403
+ if (input.detail && String(input.detail).trim()) entry.detail = String(input.detail);
395
404
  try {
396
405
  const written = ampIo.appendEntry(process.cwd(), entry);
397
406
  // NOTE: rule-file refresh deliberately NOT called here — clean-tree
@@ -401,7 +410,8 @@ function handleTool(id, name, input) {
401
410
  // are for cold-start injection of the *next* session.
402
411
  text = `✔ Logged [${written.type}] ${written.id}\n msg: ${written.msg}` +
403
412
  (written.file ? `\n file: ${written.file}${written.line ? ":" + written.line : ""}` : "") +
404
- (written.tags ? `\n tags: ${written.tags.join(", ")}` : "");
413
+ (written.tags ? `\n tags: ${written.tags.join(", ")}` : "") +
414
+ (written.meta && written.meta.detailRef ? `\n detail: ${written.meta.detailRef}` : "");
405
415
  } catch (err) {
406
416
  return sendError(id, -32000, `amp_write failed (in-process): ${err.message}`);
407
417
  }
@@ -416,6 +426,40 @@ function handleTool(id, name, input) {
416
426
  if (input.tags && input.tags.length) extras.push("--tags", JSON.stringify(input.tags.join(",")));
417
427
  text = runCmd(`log ${m} --type ${t} ${extras.join(" ")}`);
418
428
  }
429
+ } else if (name === "amp_bookmark") {
430
+ // A bookmark is a `note` entry tagged "bookmark"; the optional `note`
431
+ // becomes its Tier-2 detail (a resume point the next session can recall).
432
+ if (ampIo) {
433
+ const entry = {
434
+ ts: new Date().toISOString(),
435
+ type: "note",
436
+ summary: input.label || "",
437
+ agent: process.env.INFERNOFLOW_AGENT
438
+ || (process.env.CLAUDE_CODE_SESSION ? "claude"
439
+ : process.env.CURSOR_SESSION ? "cursor"
440
+ : process.env.COPILOT_SESSION ? "copilot"
441
+ : "claude"),
442
+ tags: ["bookmark"],
443
+ };
444
+ // Context: explicit note wins; otherwise auto-capture the session
445
+ // transcript (the "save everything here" resume point).
446
+ if (input.note && String(input.note).trim()) {
447
+ entry.detail = String(input.note);
448
+ } else if (harvestSnapshot) {
449
+ try { const snap = harvestSnapshot(process.cwd()); if (snap) entry.detail = snap; } catch { /* best-effort */ }
450
+ }
451
+ try {
452
+ const written = ampIo.appendEntry(process.cwd(), entry);
453
+ text = `🔖 Bookmark saved: ${written.msg} (${written.id})` +
454
+ (written.meta && written.meta.detailRef ? `\n context: ${written.meta.detailRef}` : "");
455
+ } catch (err) {
456
+ return sendError(id, -32000, `amp_bookmark failed (in-process): ${err.message}`);
457
+ }
458
+ } else {
459
+ const l = JSON.stringify(input.label || "");
460
+ const extras = input.note ? `--note ${JSON.stringify(input.note)}` : "";
461
+ text = runCmd(`bookmark ${l} ${extras}`);
462
+ }
419
463
  } else if (name === "amp_handoff") {
420
464
  // switch writes a file; we read it back to return the content
421
465
  const switchResult = runCmd("switch");
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: infernoflow-memory
3
+ description: >-
4
+ Persistent cross-session memory for this project via the infernoflow CLI. Use
5
+ whenever you discover a gotcha, make a non-obvious decision, hit a dead end, or
6
+ learn a lasting user preference — capture it with `infernoflow log` so the next
7
+ session starts warm instead of cold. Also use to drop a `infernoflow bookmark`
8
+ at natural stopping points or whenever the user says "bookmark this" / "save
9
+ this point", and to load prior memory at the start of work. Triggers: gotcha,
10
+ "that was surprising", "turns out", dead end, "doesn't work", decision, "let's
11
+ go with", "because", preference, "I prefer", bookmark, checkpoint, resume,
12
+ "where were we", start of a work session in a repo that has an infernoflow memory store (.ai-memory/).
13
+ ---
14
+
15
+ # infernoflow memory
16
+
17
+ This project uses **infernoflow** — a local, git-tracked memory layer that stores
18
+ what you can't infer from the code: the gotchas you hit, the decisions you made
19
+ *and why*, the dead ends you already tried, and the user's durable preferences.
20
+ Memory lives in `inferno/sessions.jsonl` and is auto-injected into the rule files
21
+ this IDE already reads. Your job is to keep that memory alive so the next session
22
+ (yours or a teammate's) starts warm.
23
+
24
+ Capture is **balanced**: log the things that genuinely save future time, and skip
25
+ the noise. When unsure, prefer logging a real gotcha over staying silent — but
26
+ never log routine steps or anything obvious from reading the code.
27
+
28
+ ## Start warm
29
+
30
+ At the start of substantive work in a project that has an infernoflow memory store
31
+ (`.ai-memory/`), load
32
+ prior memory before diving in:
33
+
34
+ ```
35
+ infernoflow recap
36
+ ```
37
+
38
+ If you're looking for something specific ("did we decide on the auth approach?"),
39
+ ask memory directly:
40
+
41
+ ```
42
+ infernoflow ask "auth approach"
43
+ ```
44
+
45
+ Do this once per session, not repeatedly.
46
+
47
+ ## Capture as you work
48
+
49
+ Run `infernoflow log` the moment one of these happens — capture it right away,
50
+ while the detail is fresh, not at the end:
51
+
52
+ **Gotcha** — something behaved contrary to a reasonable expectation and cost time:
53
+ ```
54
+ infernoflow log "API expects multipart/form-data, rejects application/json" --type gotcha
55
+ ```
56
+
57
+ **Decision with a because** — a non-obvious choice a future reader would question:
58
+ ```
59
+ infernoflow log "axios over fetch — needed upload progress events" --type decision --result worked
60
+ ```
61
+
62
+ **Dead end** — something you tried that did NOT work, so nobody repeats it:
63
+ ```
64
+ infernoflow log "tried streaming upload, server rejected chunked transfer" --type gotcha --result failed
65
+ ```
66
+
67
+ **Preference** — a durable thing the user wants, stated or clearly implied:
68
+ ```
69
+ infernoflow log "user prefers inline error handling over try/catch wrappers" --type preference
70
+ ```
71
+
72
+ Keep each message to one specific sentence. Always include the *because* for a
73
+ decision. In non-interactive/automation contexts add `--quiet`.
74
+
75
+ ### Do log
76
+ - A gotcha that would waste time again (config quirk, undocumented API behavior, env-specific bug).
77
+ - A decision whose reasoning isn't visible in the diff.
78
+ - A dead end / abandoned approach.
79
+ - A user preference that should hold across sessions.
80
+
81
+ ### Do NOT log
82
+ - Routine steps or anything obvious from reading the code.
83
+ - Secrets, tokens, credentials, or personal data.
84
+ - Duplicates — if it's already in memory (`infernoflow ask`), don't repeat it.
85
+ - Vague notes ("fixed a bug") with no reusable signal.
86
+
87
+ ## Bookmark at stopping points
88
+
89
+ A bookmark is a named resume point. On Claude Code it auto-harvests the recent
90
+ transcript into the bookmark's context — deterministic, no AI calls.
91
+
92
+ Drop one:
93
+ - **Always** when the user says "bookmark this", "save this point", "checkpoint", or similar.
94
+ - At a **natural milestone** ("auth flow works end to end").
95
+ - **Before a risky change** ("before the state-management refactor") as a safety net.
96
+
97
+ ```
98
+ infernoflow bookmark "auth flow works end to end"
99
+ infernoflow bookmark "before the SP refactor" --note "current approach: context provider per feature"
100
+ ```
101
+
102
+ Recall / list / remove:
103
+ ```
104
+ infernoflow bookmark list
105
+ infernoflow bookmark show "auth flow"
106
+ infernoflow bookmark rm "auth flow"
107
+ ```
108
+
109
+ When the user returns and asks "where were we?", run `infernoflow bookmark list`
110
+ (or `infernoflow recap`) and continue from the most relevant marker.
111
+
112
+ ## Notes
113
+ - All commands are local and safe; memory is a plain JSONL file under `inferno/`.
114
+ - If a project has no infernoflow memory store (`.ai-memory/`), this skill does not
115
+ apply — memory is opt-in per repo (`infernoflow init` creates it).
116
+ - One log per distinct insight; batching many into one line loses searchability.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.44.14",
4
- "description": "Persistent memory for AI coding sessions — captures what agents can't infer from code alone. Works with Copilot, Cursor, Claude, and Windsurf.",
3
+ "version": "0.44.16",
4
+ "description": "Persistent memory for AI coding sessions \u2014 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"