infernoflow 0.44.3 → 0.44.4

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,10 +1,10 @@
1
- import*as a from"node:fs";import*as p from"node:path";import{execSync as w}from"node:child_process";import{readEntries as b}from"./amp/io.mjs";const m="<!-- infernoflow:start -->",h="<!-- infernoflow:end -->",d=[".cursorrules","CLAUDE.md",p.join(".github","copilot-instructions.md")];function y(){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`, 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(`
1
+ import*as a from"node:fs";import*as p from"node:path";import{execSync as w}from"node:child_process";import{readEntries as b}from"./amp/io.mjs";const m="<!-- infernoflow:start -->",h="<!-- infernoflow:end -->",d=[".cursorrules","CLAUDE.md",p.join(".github","copilot-instructions.md")];function y(){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(`
2
2
  `)}function A(){return[m,"<!-- Auto-managed by infernoflow. Don't edit between these markers. -->","## Project memory (infernoflow)","",y(),"",'_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._',h].join(`
3
- `)}function x(e){const r=e.indexOf("<!-- AMP:START -->"),o=e.indexOf("<!-- AMP:END -->");if(r===-1||o===-1||o<=r)return e;const t=e.slice(0,r).replace(/\s+$/,""),i=e.slice(o+16).replace(/^\s+/,"");return(t?t+(i?`
3
+ `)}function x(e){const s=e.indexOf("<!-- AMP:START -->"),o=e.indexOf("<!-- AMP:END -->");if(s===-1||o===-1||o<=s)return e;const t=e.slice(0,s).replace(/\s+$/,""),i=e.slice(o+16).replace(/^\s+/,"");return(t?t+(i?`
4
4
 
5
- `:""):"")+i}function g(e,r){const o=p.dirname(e);if(a.existsSync(o)||a.mkdirSync(o,{recursive:!0}),!a.existsSync(e))return a.writeFileSync(e,r+`
6
- `,"utf8"),{created:!0,updated:!1};let t=a.readFileSync(e,"utf8");t=x(t);const i=t.indexOf(m),c=t.indexOf(h);if(i===-1||c===-1){const u=r+`
5
+ `:""):"")+i}function g(e,s){const o=p.dirname(e);if(a.existsSync(o)||a.mkdirSync(o,{recursive:!0}),!a.existsSync(e))return a.writeFileSync(e,s+`
6
+ `,"utf8"),{created:!0,updated:!1};let t=a.readFileSync(e,"utf8");t=x(t);const i=t.indexOf(m),c=t.indexOf(h);if(i===-1||c===-1){const u=s+`
7
7
 
8
- `+t;return a.writeFileSync(e,u,"utf8"),{created:!1,updated:!0}}const l=t.slice(0,i),s=t.slice(c+h.length),n=l+r+s;return n===t?{created:!1,updated:!1}:(a.writeFileSync(e,n,"utf8"),{created:!1,updated:!0})}function D(e){const r=A(),o=[];for(const t of d){const i=p.join(e,t);try{const c=g(i,r);o.push({rel:t,...c})}catch(c){o.push({rel:t,error:c.message})}}return o}function I(e){return b(e)}function S(e,r=10){try{return w(`git log --pretty=format:"%h%x09%ad%x09%s" --date=short -n ${r}`,{cwd:e,encoding:"utf8",stdio:["ignore","pipe","ignore"]}).split(`
9
- `).filter(Boolean).map(t=>{const[i,c,l]=t.split(" ");return{hash:(i||"").slice(0,7),date:c||"",subject:l||""}})}catch{return[]}}function k(e,r=10,o=10){const t=I(e),i=S(e,o);t.sort((n,u)=>{const f=typeof n.ts=="number"?n.ts:Date.parse(n.ts||0);return(typeof u.ts=="number"?u.ts:Date.parse(u.ts||0))-f});const c=t.slice(0,r),l={gotcha:"\u26A0",decision:"\u2713",attempt:"\u2717",note:"\xB7",detection:"\u25CB",pattern:"\u25C7"},s=[];if(s.push(m),s.push("<!-- Auto-managed by infernoflow. Don't edit between these markers. -->"),s.push("## Project memory (infernoflow)"),s.push(""),s.push(y()),s.push(""),i.length>0){s.push("### Recent commits");for(const n of i)s.push(`- \`${n.hash}\` _${n.date}_ ${n.subject}`);s.push("")}if(c.length>0){s.push("### Recent memory");for(const n of c){const u=n.file?` (\`${n.file}${n.line?":"+n.line:""}\`)`:"",f=(n.msg||n.summary||"").replace(/\n/g," ");s.push(`- ${l[n.type]||"\xB7"} **${n.type||"note"}**${u}: ${f}`)}s.push("")}return t.length===0&&i.length===0&&s.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._'),s.push(h),s.join(`
10
- `)}function R(e){const r=k(e),o=[];for(const t of d){const i=p.join(e,t);try{const c=g(i,r);o.push({rel:t,...c})}catch(c){o.push({rel:t,error:c.message})}}return o}export{R as refreshRuleFilesFromMemory,D as writeInitRuleFiles};
8
+ `+t;return a.writeFileSync(e,u,"utf8"),{created:!1,updated:!0}}const l=t.slice(0,i),r=t.slice(c+h.length),n=l+s+r;return n===t?{created:!1,updated:!1}:(a.writeFileSync(e,n,"utf8"),{created:!1,updated:!0})}function D(e){const s=A(),o=[];for(const t of d){const i=p.join(e,t);try{const c=g(i,s);o.push({rel:t,...c})}catch(c){o.push({rel:t,error:c.message})}}return o}function I(e){return b(e)}function S(e,s=10){try{return w(`git log --pretty=format:"%h%x09%ad%x09%s" --date=short -n ${s}`,{cwd:e,encoding:"utf8",stdio:["ignore","pipe","ignore"]}).split(`
9
+ `).filter(Boolean).map(t=>{const[i,c,l]=t.split(" ");return{hash:(i||"").slice(0,7),date:c||"",subject:l||""}})}catch{return[]}}function k(e,s=10,o=10){const t=I(e),i=S(e,o);t.sort((n,u)=>{const f=typeof n.ts=="number"?n.ts:Date.parse(n.ts||0);return(typeof u.ts=="number"?u.ts:Date.parse(u.ts||0))-f});const c=t.slice(0,s),l={gotcha:"\u26A0",decision:"\u2713",attempt:"\u2717",note:"\xB7",detection:"\u25CB",pattern:"\u25C7"},r=[];if(r.push(m),r.push("<!-- Auto-managed by infernoflow. Don't edit between these markers. -->"),r.push("## Project memory (infernoflow)"),r.push(""),r.push(y()),r.push(""),i.length>0){r.push("### Recent commits");for(const n of i)r.push(`- \`${n.hash}\` _${n.date}_ ${n.subject}`);r.push("")}if(c.length>0){r.push("### Recent memory");for(const n of c){const u=n.file?` (\`${n.file}${n.line?":"+n.line:""}\`)`:"",f=(n.msg||n.summary||"").replace(/\n/g," ");r.push(`- \u{1F525} ${l[n.type]||"\xB7"} **${n.type||"note"}**${u}: ${f}`)}r.push("")}return t.length===0&&i.length===0&&r.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._'),r.push(h),r.join(`
10
+ `)}function R(e){const s=k(e),o=[];for(const t of d){const i=p.join(e,t);try{const c=g(i,s);o.push({rel:t,...c})}catch(c){o.push({rel:t,error:c.message})}}return o}export{R as refreshRuleFilesFromMemory,D as writeInitRuleFiles};
@@ -16,6 +16,7 @@
16
16
  */
17
17
  import * as fs from "node:fs";
18
18
  import * as path from "node:path";
19
+ import { spawnSync } from "node:child_process";
19
20
 
20
21
  /** Keep in sync with templates/scripts/inferno-promote-draft.mjs */
21
22
  const DRAFT_HEADER = `# CONTEXT draft (gitignored)
@@ -132,6 +133,83 @@ async function readStdin() {
132
133
  return Buffer.concat(chunks).toString("utf8");
133
134
  }
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
+
135
213
  // ── main ───────────────────────────────────────────────────────────────────
136
214
 
137
215
  function main() {
@@ -149,6 +227,16 @@ function main() {
149
227
  return;
150
228
  }
151
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
+
152
240
  const file = draftPath();
153
241
 
154
242
  if (agentStop) {
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "version": 1,
3
3
  "hooks": {
4
+ "beforeSubmitPrompt": [
5
+ {
6
+ "command": "node .cursor/hooks/inferno-session-draft.mjs --user-prompt"
7
+ }
8
+ ],
4
9
  "afterAgentResponse": [
5
10
  {
6
11
  "command": "node .cursor/hooks/inferno-session-draft.mjs"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.44.3",
3
+ "version": "0.44.4",
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": {