infernoflow 0.44.6 → 0.44.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  Every new AI session today starts cold. The agent re-reads your code, re-derives the obvious, and often re-makes the same wrong move someone else made yesterday. infernoflow closes that loop in four stages:
14
14
 
15
- 1. **Capture** — while you and the agent work, certain moments are worth saving: a gotcha hit, a decision made, an attempted fix that failed, a pattern noticed. The agent writes them down automatically via the `amp_write` MCP tool — no copy/paste, no `git commit -m`.
15
+ 1. **Capture** — while you and the agent work, certain moments are worth saving: a gotcha hit, a decision made, an attempted fix that failed, a pattern noticed. The agent writes them down automatically via the `amp_write` MCP tool — no copy/paste, no `git commit -m`. A protocol block injected into the rule files teaches the AI exactly when to log — e.g. when you type `!!`, `retry`, `not working`, `still broken`, or describe a `Plan:` — and a Cursor `beforeSubmitPrompt` hook backstops the AI by scanning your prompt for those triggers and writing the entry deterministically when the AI doesn't.
16
16
  2. **Link** — each captured moment becomes a structured AMP entry (`gotcha | decision | attempt | note | detection | pattern`) with timestamp, file:line, tags, and a stable AMP id. Linked into the project, not your scratchpad.
17
17
  3. **Persist** — entries land in `.ai-memory/branches/<branch>.jsonl` (git-tracked, travels with your branch — teammates inherit it) plus `.ai-memory/global.jsonl` (personal preferences, gitignored, synced across your machines via any OS-synced folder).
18
18
  4. **Restore** — when a new session starts, the agent reads `CLAUDE.md` / `.cursorrules` / `copilot-instructions.md` at boot. The most relevant entries are already there. Warm start; no cold derivation.
@@ -46,10 +46,43 @@ These cover 95% of usage:
46
46
  | `infernoflow recap` | End-of-session summary with health score + unlogged-change detection |
47
47
  | `infernoflow status` | Quick health check — entries, gotchas, decisions, last activity |
48
48
  | `infernoflow refresh` | Manually rebuild `CLAUDE.md` / `.cursorrules` / `copilot-instructions.md` from memory |
49
+ | `infernoflow forget <id|prefix>` | Delete a memory entry without hand-editing JSONL. `--last` for the newest. |
50
+ | `infernoflow prune` | Archive stale `note` / `attempt` entries older than 30 days. Gotchas/decisions never auto-pruned. Default dry-run; `--apply` to act. |
49
51
 
50
52
  In practice you barely run any of these — the MCP-aware AI does it for you. The CLI is for grep-style introspection.
51
53
 
52
- `infernoflow commands` shows the full list (17 commands, grouped by purpose).
54
+ `infernoflow commands` shows the full list (~20 commands, grouped by purpose).
55
+
56
+ ## Keeping it lean: token budget + rotation
57
+
58
+ The injected memory block is paid for on every AI turn (and twice when a tool loads both `CLAUDE.md` and `copilot-instructions.md`). infernoflow ships lean defaults — 4 entries, 5 commits, 200-char per-entry truncation — and gives you knobs to tune further. Set once in `.ai-memory/amp.json`:
59
+
60
+ ```jsonc
61
+ "config": {
62
+ "injection": {
63
+ "maxEntries": 4, // memory entries injected
64
+ "maxCommits": 5, // git commits injected
65
+ "maxEntryChars": 200, // per-entry truncation
66
+ "targets": ["CLAUDE.md", ".cursorrules"], // drop a file from the list and its stale block is stripped automatically
67
+ "includeProtocol": true // false drops the ~17-line capture protocol (advanced)
68
+ },
69
+ "rotation": {
70
+ "archiveAfterDays": 30,
71
+ "archivableTypes": ["note", "attempt", "detection"],
72
+ "auto": false // true → silent prune on every `log`
73
+ }
74
+ }
75
+ ```
76
+
77
+ Or write the same values via CLI flags:
78
+
79
+ ```bash
80
+ infernoflow setup --max-memory 3 --max-commits 5 --max-entry-chars 200 --no-protocol
81
+ infernoflow refresh --max-memory 3 # same; persists into amp.json
82
+ infernoflow prune --apply --max-age-days 14 # one-off cleanup
83
+ ```
84
+
85
+ **Rotation** archives stale `note` / `attempt` / `detection` entries to `.ai-memory/archive/sessions-YYYY-MM.jsonl` — invisible to the merged read (so the AI, sidebar, `ask`, and `refresh` stop surfacing them) but still on disk if you want them back. `gotcha`, `decision`, and `pattern` entries are **never auto-pruned** — that's the knowledge you logged infernoflow FOR.
53
86
 
54
87
  ## Branch-aware memory + cross-machine sync
55
88
 
@@ -101,7 +134,9 @@ When the MCP server is wired, your AI agent can call these directly in chat:
101
134
  | `infernoflow_context` | Generate AI-ready context for a task |
102
135
  | `infernoflow_git_drift` | Detect which capabilities recent commits affected |
103
136
 
104
- The `amp_*` tools follow the [AMP MCP spec §7.3](docs/protocol/PROTOCOL.md#73-mcp-tool-interface) — vendor-neutral. Any AMP-Full client only needs to know those five names.
137
+ The `amp_*` tools follow the [AMP MCP spec §7.3](docs/protocol/PROTOCOL.md#73-mcp-tool-interface) — vendor-neutral. Any AMP-Full client only needs to know those five names. The same five are also available as CLI aliases (`infernoflow amp read | write | search | handoff | health`) so the CLI and MCP surfaces match name-for-name.
138
+
139
+ Every memory line injected into the rule files is prefixed with `🔥` so the AI (and you) can tell at a glance that a line came from infernoflow even when it's quoted out of the managed block.
105
140
 
106
141
  ## What it has caught (real dogfood)
107
142
 
@@ -1,13 +1,13 @@
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 k(e){const n=e&&e.config||{},t=n.injection&&typeof n.injection=="object"?n.injection:{},r=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:r||x,includeProtocol:t.includeProtocol!==!1}}function T(e){if(!l.existsSync(e))return{stripped:!1};const n=l.readFileSync(e,"utf8"),t=n.indexOf(y),r=n.indexOf(f);if(t===-1||r===-1||r<=t)return{stripped:!1};const s=n.slice(0,t).replace(/\s+$/,""),o=n.slice(r+f.length).replace(/^\s+/,""),i=s&&o?s+`
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 k(e){const n=e&&e.config||{},t=n.injection&&typeof n.injection=="object"?n.injection:{},r=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:r||x,includeProtocol:t.includeProtocol!==!1}}function T(e){if(!l.existsSync(e))return{stripped:!1};const n=l.readFileSync(e,"utf8"),t=n.indexOf(y),r=n.indexOf(f);if(t===-1||r===-1||r<=t)return{stripped:!1};const o=n.slice(0,t).replace(/\s+$/,""),s=n.slice(r+f.length).replace(/^\s+/,""),i=o&&s?o+`
2
2
 
3
- `+o:s||o;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("| 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.join(`
5
- `)}function F(){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 v(e){const n=e.indexOf("<!-- AMP:START -->"),t=e.indexOf("<!-- AMP:END -->");if(n===-1||t===-1||t<=n)return e;const r=e.slice(0,n).replace(/\s+$/,""),s=e.slice(t+16).replace(/^\s+/,"");return(r?r+(s?`
3
+ `+s:o||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("| 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 v(){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 r=e.slice(0,n).replace(/\s+$/,""),o=e.slice(t+16).replace(/^\s+/,"");return(r?r+(o?`
7
7
 
8
- `:""):"")+s}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 r=l.readFileSync(e,"utf8");r=v(r);const s=r.indexOf(y),o=r.indexOf(f);if(s===-1||o===-1){const g=n+`
8
+ `:""):"")+o}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 r=l.readFileSync(e,"utf8");r=F(r);const o=r.indexOf(y),s=r.indexOf(f);if(o===-1||s===-1){const g=n+`
10
10
 
11
- `+r;return l.writeFileSync(e,g,"utf8"),{created:!1,updated:!0}}const i=r.slice(0,s),p=r.slice(o+f.length),u=i+n+p;return u===r?{created:!1,updated:!1}:(l.writeFileSync(e,u,"utf8"),{created:!1,updated:!0})}function O(e){const n=F(),t=[];for(const r of x){const s=d.join(e,r);try{const o=S(s,n);t.push({rel:r,...o})}catch(o){t.push({rel:r,error:o.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(r=>{const[s,o,i]=r.split(" ");return{hash:(s||"").slice(0,7),date:o||"",subject:i||""}})}catch{return[]}}function N(e,n={}){const{maxEntries:t=b,maxCommits:r=A,maxEntryChars:s=C,includeProtocol:o=!0}=n,i=D(e),p=M(e,r);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(""),o&&(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=s&&h.length>s?h.slice(0,s).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=k(_(e)),t=N(e,n),r=i=>String(i).replace(/\\/g,"/"),s=new Set(n.targets.map(r)),o=[];for(const i of x){const p=d.join(e,i);try{if(s.has(r(i))){const u=S(p,t);o.push({rel:i,...u})}else T(p).stripped&&o.push({rel:i,stripped:!0})}catch(u){o.push({rel:i,error:u.message})}}return o}export{P as refreshRuleFilesFromMemory,k as resolveInjectionSettings,O as writeInitRuleFiles};
11
+ `+r;return l.writeFileSync(e,g,"utf8"),{created:!1,updated:!0}}const i=r.slice(0,o),p=r.slice(s+f.length),u=i+n+p;return u===r?{created:!1,updated:!1}:(l.writeFileSync(e,u,"utf8"),{created:!1,updated:!0})}function O(e){const n=v(),t=[];for(const r of x){const o=d.join(e,r);try{const s=S(o,n);t.push({rel:r,...s})}catch(s){t.push({rel:r,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(r=>{const[o,s,i]=r.split(" ");return{hash:(o||"").slice(0,7),date:s||"",subject:i||""}})}catch{return[]}}function N(e,n={}){const{maxEntries:t=b,maxCommits:r=A,maxEntryChars:o=C,includeProtocol:s=!0}=n,i=D(e),p=M(e,r);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=o&&h.length>o?h.slice(0,o).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=k(_(e)),t=N(e,n),r=i=>String(i).replace(/\\/g,"/"),o=new Set(n.targets.map(r)),s=[];for(const i of x){const p=d.join(e,i);try{if(o.has(r(i))){const u=S(p,t);s.push({rel:i,...u})}else T(p).stripped&&s.push({rel:i,stripped:!0})}catch(u){s.push({rel:i,error:u.message})}}return s}export{P as refreshRuleFilesFromMemory,k as resolveInjectionSettings,O as writeInitRuleFiles};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.44.6",
3
+ "version": "0.44.8",
4
4
  "description": "Persistent memory for AI coding sessions — captures what agents can't infer from code alone. Works with Copilot, Cursor, Claude, and Windsurf.",
5
5
  "type": "module",
6
6
  "bin": {