infernoflow 0.44.14 → 0.44.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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,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};
@@ -1,285 +1,337 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Cursor hook: capture agent output for infernoflow.
4
- *
5
- * Two jobs in one:
6
- * 1. Always append agent text to inferno/CONTEXT.draft.md (existing behaviour)
7
- * 2. When infernoflow is waiting (inferno/agent-prompt.md exists),
8
- * extract the JSON block from the agent reply and write it to
9
- * inferno/agent-response.json so infernoflow picks it up automatically.
10
- *
11
- * Trigger in .cursor/hooks.json:
12
- * afterAgentResponse → { text }
13
- * stop → { status, loop_count, ... } (--agent-stop flag)
14
- *
15
- * Never fail closed: errors go to stderr; stdout is {} for Cursor.
16
- */
17
- import * as fs from "node:fs";
18
- import * as path from "node:path";
19
- import { spawnSync } from "node:child_process";
20
-
21
- /** Keep in sync with templates/scripts/inferno-promote-draft.mjs */
22
- const DRAFT_HEADER = `# CONTEXT draft (gitignored)
23
- Auto-captured by Cursor hooks (\`.cursor/hooks/inferno-session-draft.mjs\`). **Not product truth** — review, then run \`npm run inferno:promote-draft\` or \`infernoflow context\`.
24
- ---
25
- `;
26
-
27
- const MAX_MESSAGE_CHARS = 120_000;
28
- const MAX_FILE_BYTES = 280_000;
29
-
30
- // ── paths ──────────────────────────────────────────────────────────────────
31
-
32
- function projectRoot() {
33
- return process.cwd();
34
- }
35
-
36
- function draftPath() {
37
- return path.join(projectRoot(), "inferno", "CONTEXT.draft.md");
38
- }
39
-
40
- function agentPromptPath() {
41
- return path.join(projectRoot(), "inferno", "agent-prompt.md");
42
- }
43
-
44
- function agentResponsePath() {
45
- return path.join(projectRoot(), "inferno", "agent-response.json");
46
- }
47
-
48
- // ── CONTEXT.draft.md helpers ───────────────────────────────────────────────
49
-
50
- function ensureDraftFile(file) {
51
- if (!fs.existsSync(file)) {
52
- fs.mkdirSync(path.dirname(file), { recursive: true });
53
- fs.writeFileSync(file, DRAFT_HEADER, "utf8");
54
- }
55
- }
56
-
57
- function trimFile(file) {
58
- const raw = fs.readFileSync(file, "utf8");
59
- if (Buffer.byteLength(raw, "utf8") <= MAX_FILE_BYTES) return;
60
- const keep = raw.slice(-Math.floor(MAX_FILE_BYTES * 0.85));
61
- const idx = keep.indexOf("\n### ");
62
- const body = idx === -1 ? keep : keep.slice(idx);
63
- fs.writeFileSync(
64
- file,
65
- `${DRAFT_HEADER}\n_(older capture trimmed for size)_\n\n${body}`,
66
- "utf8",
67
- );
68
- }
69
-
70
- function appendBlock(file, block) {
71
- ensureDraftFile(file);
72
- fs.appendFileSync(file, block, "utf8");
73
- trimFile(file);
74
- }
75
-
76
- // ── JSON extraction ────────────────────────────────────────────────────────
77
-
78
- function extractJsonFromText(text) {
79
- // 1. fenced code block
80
- const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
81
- if (fenceMatch) {
82
- const candidate = fenceMatch[1].trim();
83
- try {
84
- JSON.parse(candidate);
85
- return candidate;
86
- } catch {}
87
- }
88
-
89
- // 2. largest bare JSON object in the text
90
- const jsonMatch = text.match(/\{[\s\S]*\}/);
91
- if (jsonMatch) {
92
- try {
93
- JSON.parse(jsonMatch[0]);
94
- return jsonMatch[0];
95
- } catch {}
96
- }
97
-
98
- return null;
99
- }
100
-
101
- // ── bridge: write agent-response.json if infernoflow is waiting ────────────
102
-
103
- function maybeWriteAgentResponse(text) {
104
- const promptFile = agentPromptPath();
105
- const responseFile = agentResponsePath();
106
-
107
- if (!fs.existsSync(promptFile)) return false;
108
-
109
- const json = extractJsonFromText(text);
110
- if (!json) {
111
- process.stderr.write(
112
- "[inferno-session-draft] infernoflow waiting but no JSON found in agent reply\n",
113
- );
114
- return false;
115
- }
116
-
117
- fs.writeFileSync(responseFile, json, "utf8");
118
- try {
119
- fs.unlinkSync(promptFile);
120
- } catch {}
121
-
122
- process.stderr.write(
123
- "[inferno-session-draft] ✔ agent-response.json written — infernoflow will continue\n",
124
- );
125
- return true;
126
- }
127
-
128
- // ── stdin ──────────────────────────────────────────────────────────────────
129
-
130
- async function readStdin() {
131
- const chunks = [];
132
- for await (const c of process.stdin) chunks.push(c);
133
- return Buffer.concat(chunks).toString("utf8");
134
- }
135
-
136
- // ── Deterministic trigger capture (beforeSubmitPrompt) ──────────────────────
137
- // Cursor's beforeSubmitPrompt hook hands us the USER's prompt text before it
138
- // goes to the model. The Memory-protocol block already asks the AI to log on
139
- // these signals, but the AI doesn't always obey — so this is a deterministic
140
- // backstop: if the prompt itself contains a trouble signal (!!, retry, "not
141
- // working", …) we write an `attempt` entry ourselves. Bounded hard against
142
- // noise: a 90s cooldown + identical-prompt dedupe, so a frustrated burst of
143
- // "still broken!! retry!!" produces ONE entry, not ten.
144
- const TRIGGER_RES = [
145
- /(?:^|\s)!!+/,
146
- /\bretry(?:ing)?\b/i,
147
- /\bnot working\b/i,
148
- /\bstill (?:broken|failing|not working|doesn['’]?t)\b/i,
149
- /\bsame (?:error|issue|problem)\b/i,
150
- /\bno change\b/i,
151
- /\bdoesn['’]?t work\b/i,
152
- ];
153
-
154
- function memoryRootExists() {
155
- return fs.existsSync(path.join(projectRoot(), ".ai-memory")) ||
156
- fs.existsSync(path.join(projectRoot(), "inferno"));
157
- }
158
-
159
- function triggerStatePath() {
160
- return path.join(projectRoot(), ".ai-memory", ".trigger-state.json");
161
- }
162
-
163
- function cheapHash(s) {
164
- let h = 0;
165
- for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
166
- return String(h);
167
- }
168
-
169
- function handleUserPrompt(text) {
170
- const trimmed = (text || "").trim();
171
- if (!trimmed) return;
172
- if (!memoryRootExists()) return; // only inside infernoflow projects
173
- if (!TRIGGER_RES.some((re) => re.test(trimmed))) return;
174
-
175
- const now = Date.now();
176
- const stateFile = triggerStatePath();
177
- let state = {};
178
- try { state = JSON.parse(fs.readFileSync(stateFile, "utf8")); } catch {}
179
- const h = cheapHash(trimmed.slice(0, 200));
180
- const COOLDOWN_MS = 90_000;
181
- if (state.lastHash === h) return; // exact same prompt — skip
182
- if (state.lastTs && now - state.lastTs < COOLDOWN_MS) return; // rate-limit
183
-
184
- const msg = "Auto-trigger — user signalled trouble: " +
185
- trimmed.replace(/\s+/g, " ").slice(0, 180);
186
-
187
- // Prefer the CLI (correct id / branch routing / AMP shape); fall back to a
188
- // direct sessions.jsonl append so capture still works without a global CLI.
189
- let wrote = false;
190
- try {
191
- const bin = process.platform === "win32" ? "infernoflow.cmd" : "infernoflow";
192
- const r = spawnSync(bin, ["log", msg, "--type", "attempt", "--source", "cursor-trigger", "--tags", "auto-trigger"], {
193
- cwd: projectRoot(), encoding: "utf8", timeout: 8000, shell: process.platform === "win32",
194
- });
195
- wrote = r.status === 0;
196
- } catch { /* fall through to direct write */ }
197
- if (!wrote) {
198
- try {
199
- const sess = path.join(projectRoot(), ".ai-memory", "sessions.jsonl");
200
- fs.mkdirSync(path.dirname(sess), { recursive: true });
201
- const entry = { type: "attempt", msg, ts: now, id: "amp_hook_" + now.toString(36), source: "cursor-trigger", tags: ["auto-trigger"], meta: { agent: "cursor-hook" } };
202
- fs.appendFileSync(sess, JSON.stringify(entry) + "\n", "utf8");
203
- wrote = true;
204
- } catch { /* best effort */ }
205
- }
206
-
207
- if (wrote) {
208
- try { fs.writeFileSync(stateFile, JSON.stringify({ lastTs: now, lastHash: h }), "utf8"); } catch {}
209
- process.stderr.write("[inferno-session-draft] auto-captured trigger to memory\n");
210
- }
211
- }
212
-
213
- // ── main ───────────────────────────────────────────────────────────────────
214
-
215
- function main() {
216
- const agentStop = process.argv.includes("--agent-stop");
217
-
218
- readStdin()
219
- .then((raw) => {
220
- let data = {};
221
- try {
222
- data = raw.trim() ? JSON.parse(raw) : {};
223
- } catch (e) {
224
- console.error("[inferno-session-draft] stdin JSON parse:", e.message);
225
- console.log("{}");
226
- process.exit(0);
227
- return;
228
- }
229
-
230
- // beforeSubmitPrompt: deterministic trigger capture on the USER's prompt.
231
- if (process.argv.includes("--user-prompt")) {
232
- const t = typeof data.prompt === "string" ? data.prompt
233
- : typeof data.text === "string" ? data.text : "";
234
- try { handleUserPrompt(t); } catch (e) { console.error("[inferno-session-draft] trigger:", e?.message); }
235
- console.log("{}");
236
- process.exit(0);
237
- return;
238
- }
239
-
240
- const file = draftPath();
241
-
242
- if (agentStop) {
243
- const status = data.status ?? "unknown";
244
- const loop = data.loop_count ?? 0;
245
- appendBlock(
246
- file,
247
- `\n### _agent stop_ (${new Date().toISOString()})\n\nstatus: \`${status}\` · loop_count: ${loop}\n\n---\n`,
248
- );
249
- console.log("{}");
250
- process.exit(0);
251
- return;
252
- }
253
-
254
- const text = typeof data.text === "string" ? data.text : "";
255
- if (!text.trim()) {
256
- console.log("{}");
257
- process.exit(0);
258
- return;
259
- }
260
-
261
- // Job 2: feed infernoflow's file-based bridge if it is waiting
262
- maybeWriteAgentResponse(text);
263
-
264
- // Job 1: always append to CONTEXT.draft.md
265
- const clipped =
266
- text.length > MAX_MESSAGE_CHARS
267
- ? `${text.slice(0, MAX_MESSAGE_CHARS)}\n\n_…trimmed (${text.length - MAX_MESSAGE_CHARS} chars omitted)_\n`
268
- : text;
269
-
270
- appendBlock(
271
- file,
272
- `\n### Assistant message (${new Date().toISOString()})\n\n${clipped}\n\n---\n`,
273
- );
274
-
275
- console.log("{}");
276
- process.exit(0);
277
- })
278
- .catch((e) => {
279
- console.error("[inferno-session-draft]", e);
280
- console.log("{}");
281
- process.exit(0);
282
- });
283
- }
284
-
285
- main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cursor hook: capture agent output for infernoflow.
4
+ *
5
+ * Two jobs in one:
6
+ * 1. Always append agent text to inferno/CONTEXT.draft.md (existing behaviour)
7
+ * 2. When infernoflow is waiting (inferno/agent-prompt.md exists),
8
+ * extract the JSON block from the agent reply and write it to
9
+ * inferno/agent-response.json so infernoflow picks it up automatically.
10
+ *
11
+ * Trigger in .cursor/hooks.json:
12
+ * afterAgentResponse → { text }
13
+ * stop → { status, loop_count, ... } (--agent-stop flag)
14
+ *
15
+ * Never fail closed: errors go to stderr; stdout is {} for Cursor.
16
+ */
17
+ import * as fs from "node:fs";
18
+ import * as path from "node:path";
19
+ import { spawnSync } from "node:child_process";
20
+
21
+ /** Keep in sync with templates/scripts/inferno-promote-draft.mjs */
22
+ const DRAFT_HEADER = `# CONTEXT draft (gitignored)
23
+ Auto-captured by Cursor hooks (\`.cursor/hooks/inferno-session-draft.mjs\`). **Not product truth** — review, then run \`npm run inferno:promote-draft\` or \`infernoflow context\`.
24
+ ---
25
+ `;
26
+
27
+ const MAX_MESSAGE_CHARS = 120_000;
28
+ const MAX_FILE_BYTES = 280_000;
29
+
30
+ // ── paths ──────────────────────────────────────────────────────────────────
31
+
32
+ function projectRoot() {
33
+ return process.cwd();
34
+ }
35
+
36
+ function draftPath() {
37
+ return path.join(projectRoot(), "inferno", "CONTEXT.draft.md");
38
+ }
39
+
40
+ function agentPromptPath() {
41
+ return path.join(projectRoot(), "inferno", "agent-prompt.md");
42
+ }
43
+
44
+ function agentResponsePath() {
45
+ return path.join(projectRoot(), "inferno", "agent-response.json");
46
+ }
47
+
48
+ // ── CONTEXT.draft.md helpers ───────────────────────────────────────────────
49
+
50
+ function ensureDraftFile(file) {
51
+ if (!fs.existsSync(file)) {
52
+ fs.mkdirSync(path.dirname(file), { recursive: true });
53
+ fs.writeFileSync(file, DRAFT_HEADER, "utf8");
54
+ }
55
+ }
56
+
57
+ function trimFile(file) {
58
+ const raw = fs.readFileSync(file, "utf8");
59
+ if (Buffer.byteLength(raw, "utf8") <= MAX_FILE_BYTES) return;
60
+ const keep = raw.slice(-Math.floor(MAX_FILE_BYTES * 0.85));
61
+ const idx = keep.indexOf("\n### ");
62
+ const body = idx === -1 ? keep : keep.slice(idx);
63
+ fs.writeFileSync(
64
+ file,
65
+ `${DRAFT_HEADER}\n_(older capture trimmed for size)_\n\n${body}`,
66
+ "utf8",
67
+ );
68
+ }
69
+
70
+ function appendBlock(file, block) {
71
+ ensureDraftFile(file);
72
+ fs.appendFileSync(file, block, "utf8");
73
+ trimFile(file);
74
+ }
75
+
76
+ // ── JSON extraction ────────────────────────────────────────────────────────
77
+
78
+ function extractJsonFromText(text) {
79
+ // 1. fenced code block
80
+ const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
81
+ if (fenceMatch) {
82
+ const candidate = fenceMatch[1].trim();
83
+ try {
84
+ JSON.parse(candidate);
85
+ return candidate;
86
+ } catch {}
87
+ }
88
+
89
+ // 2. largest bare JSON object in the text
90
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
91
+ if (jsonMatch) {
92
+ try {
93
+ JSON.parse(jsonMatch[0]);
94
+ return jsonMatch[0];
95
+ } catch {}
96
+ }
97
+
98
+ return null;
99
+ }
100
+
101
+ // ── bridge: write agent-response.json if infernoflow is waiting ────────────
102
+
103
+ function maybeWriteAgentResponse(text) {
104
+ const promptFile = agentPromptPath();
105
+ const responseFile = agentResponsePath();
106
+
107
+ if (!fs.existsSync(promptFile)) return false;
108
+
109
+ const json = extractJsonFromText(text);
110
+ if (!json) {
111
+ process.stderr.write(
112
+ "[inferno-session-draft] infernoflow waiting but no JSON found in agent reply\n",
113
+ );
114
+ return false;
115
+ }
116
+
117
+ fs.writeFileSync(responseFile, json, "utf8");
118
+ try {
119
+ fs.unlinkSync(promptFile);
120
+ } catch {}
121
+
122
+ process.stderr.write(
123
+ "[inferno-session-draft] ✔ agent-response.json written — infernoflow will continue\n",
124
+ );
125
+ return true;
126
+ }
127
+
128
+ // ── stdin ──────────────────────────────────────────────────────────────────
129
+
130
+ async function readStdin() {
131
+ const chunks = [];
132
+ for await (const c of process.stdin) chunks.push(c);
133
+ return Buffer.concat(chunks).toString("utf8");
134
+ }
135
+
136
+ // ── Deterministic trigger capture (beforeSubmitPrompt) ──────────────────────
137
+ // Cursor's beforeSubmitPrompt hook hands us the USER's prompt text before it
138
+ // goes to the model. The Memory-protocol block already asks the AI to log on
139
+ // these signals, but the AI doesn't always obey — so this is a deterministic
140
+ // backstop: if the prompt itself contains a trouble signal (!!, retry, "not
141
+ // working", …) we write an `attempt` entry ourselves. Bounded hard against
142
+ // noise: a 90s cooldown + identical-prompt dedupe, so a frustrated burst of
143
+ // "still broken!! retry!!" produces ONE entry, not ten.
144
+ const TRIGGER_RES = [
145
+ /(?:^|\s)!!+/,
146
+ /\bretry(?:ing)?\b/i,
147
+ /\bnot working\b/i,
148
+ /\bstill (?:broken|failing|not working|doesn['’]?t)\b/i,
149
+ /\bsame (?:error|issue|problem)\b/i,
150
+ /\bno change\b/i,
151
+ /\bdoesn['’]?t work\b/i,
152
+ ];
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
+
175
+ function memoryRootExists() {
176
+ return fs.existsSync(path.join(projectRoot(), ".ai-memory")) ||
177
+ fs.existsSync(path.join(projectRoot(), "inferno"));
178
+ }
179
+
180
+ function triggerStatePath() {
181
+ return path.join(projectRoot(), ".ai-memory", ".trigger-state.json");
182
+ }
183
+
184
+ function cheapHash(s) {
185
+ let h = 0;
186
+ for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
187
+ return String(h);
188
+ }
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
+
217
+ function handleUserPrompt(text) {
218
+ const trimmed = (text || "").trim();
219
+ if (!trimmed) return;
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
+
225
+ if (!TRIGGER_RES.some((re) => re.test(trimmed))) return;
226
+
227
+ const now = Date.now();
228
+ const stateFile = triggerStatePath();
229
+ let state = {};
230
+ try { state = JSON.parse(fs.readFileSync(stateFile, "utf8")); } catch {}
231
+ const h = cheapHash(trimmed.slice(0, 200));
232
+ const COOLDOWN_MS = 90_000;
233
+ if (state.lastHash === h) return; // exact same prompt — skip
234
+ if (state.lastTs && now - state.lastTs < COOLDOWN_MS) return; // rate-limit
235
+
236
+ const msg = "Auto-trigger — user signalled trouble: " +
237
+ trimmed.replace(/\s+/g, " ").slice(0, 180);
238
+
239
+ // Prefer the CLI (correct id / branch routing / AMP shape); fall back to a
240
+ // direct sessions.jsonl append so capture still works without a global CLI.
241
+ let wrote = false;
242
+ try {
243
+ const bin = process.platform === "win32" ? "infernoflow.cmd" : "infernoflow";
244
+ const r = spawnSync(bin, ["log", msg, "--type", "attempt", "--source", "cursor-trigger", "--tags", "auto-trigger"], {
245
+ cwd: projectRoot(), encoding: "utf8", timeout: 8000, shell: process.platform === "win32",
246
+ });
247
+ wrote = r.status === 0;
248
+ } catch { /* fall through to direct write */ }
249
+ if (!wrote) {
250
+ try {
251
+ const sess = path.join(projectRoot(), ".ai-memory", "sessions.jsonl");
252
+ fs.mkdirSync(path.dirname(sess), { recursive: true });
253
+ const entry = { type: "attempt", msg, ts: now, id: "amp_hook_" + now.toString(36), source: "cursor-trigger", tags: ["auto-trigger"], meta: { agent: "cursor-hook" } };
254
+ fs.appendFileSync(sess, JSON.stringify(entry) + "\n", "utf8");
255
+ wrote = true;
256
+ } catch { /* best effort */ }
257
+ }
258
+
259
+ if (wrote) {
260
+ try { fs.writeFileSync(stateFile, JSON.stringify({ ...state, lastTs: now, lastHash: h }), "utf8"); } catch {}
261
+ process.stderr.write("[inferno-session-draft] auto-captured trigger to memory\n");
262
+ }
263
+ }
264
+
265
+ // ── main ───────────────────────────────────────────────────────────────────
266
+
267
+ function main() {
268
+ const agentStop = process.argv.includes("--agent-stop");
269
+
270
+ readStdin()
271
+ .then((raw) => {
272
+ let data = {};
273
+ try {
274
+ data = raw.trim() ? JSON.parse(raw) : {};
275
+ } catch (e) {
276
+ console.error("[inferno-session-draft] stdin JSON parse:", e.message);
277
+ console.log("{}");
278
+ process.exit(0);
279
+ return;
280
+ }
281
+
282
+ // beforeSubmitPrompt: deterministic trigger capture on the USER's prompt.
283
+ if (process.argv.includes("--user-prompt")) {
284
+ const t = typeof data.prompt === "string" ? data.prompt
285
+ : typeof data.text === "string" ? data.text : "";
286
+ try { handleUserPrompt(t); } catch (e) { console.error("[inferno-session-draft] trigger:", e?.message); }
287
+ console.log("{}");
288
+ process.exit(0);
289
+ return;
290
+ }
291
+
292
+ const file = draftPath();
293
+
294
+ if (agentStop) {
295
+ const status = data.status ?? "unknown";
296
+ const loop = data.loop_count ?? 0;
297
+ appendBlock(
298
+ file,
299
+ `\n### _agent stop_ (${new Date().toISOString()})\n\nstatus: \`${status}\` · loop_count: ${loop}\n\n---\n`,
300
+ );
301
+ console.log("{}");
302
+ process.exit(0);
303
+ return;
304
+ }
305
+
306
+ const text = typeof data.text === "string" ? data.text : "";
307
+ if (!text.trim()) {
308
+ console.log("{}");
309
+ process.exit(0);
310
+ return;
311
+ }
312
+
313
+ // Job 2: feed infernoflow's file-based bridge if it is waiting
314
+ maybeWriteAgentResponse(text);
315
+
316
+ // Job 1: always append to CONTEXT.draft.md
317
+ const clipped =
318
+ text.length > MAX_MESSAGE_CHARS
319
+ ? `${text.slice(0, MAX_MESSAGE_CHARS)}\n\n_…trimmed (${text.length - MAX_MESSAGE_CHARS} chars omitted)_\n`
320
+ : text;
321
+
322
+ appendBlock(
323
+ file,
324
+ `\n### Assistant message (${new Date().toISOString()})\n\n${clipped}\n\n---\n`,
325
+ );
326
+
327
+ console.log("{}");
328
+ process.exit(0);
329
+ })
330
+ .catch((e) => {
331
+ console.error("[inferno-session-draft]", e);
332
+ console.log("{}");
333
+ process.exit(0);
334
+ });
335
+ }
336
+
337
+ main();
@@ -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");
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.15",
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"