infernoflow 0.42.5 → 0.42.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog — infernoflow
2
2
 
3
+ ## 0.42.6 — 2026-05-06
4
+
5
+ ### Fixed
6
+ - **`infernoflow contract graph` no longer crashes with cryptic "Cannot read properties of undefined (reading 'add')"**. Two issues addressed:
7
+ 1. Friendly error when `inferno/scan.json` is missing — tells the user to run `infernoflow scan` first instead of crashing.
8
+ 2. Defensive guard around the dependency-edge build — stale or duplicate scan entries can no longer trigger the undefined-Set crash.
9
+ - **MCP server: `infernoflow setup` no longer registers a phantom `infernoflow_suggest` tool**. The MCP_TOOLS pre-approval list in `lib/commands/setup.mjs` was out of sync with the actual MCP server (`templates/cursor/inferno-mcp-server.mjs`) — it included one tool that was never implemented and was missing several real ones. Now lists all 14 actual tools: 9 `infernoflow_*` (added `infernoflow_apply`) + 5 `amp_*` aliases (`amp_read`, `amp_write`, `amp_search`, `amp_handoff`, `amp_health`).
10
+ - **MCP server: CLI failures are surfaced as proper JSON-RPC errors**. Previously `runCmd()` swallowed all command failures and returned the raw error text as if it were successful output, so AI agents got garbled stderr mixed into their tool replies. `runCmd()` now returns a structured `{__error, message, stderr, stdout, status}` object on failure; a central check at the dispatcher converts these into `sendError()` calls. `amp_health` and `amp_handoff` defensively handle CLI failures instead of throwing on `.trim()`.
11
+
12
+ ### Notes
13
+ - VS Code extension `infernoflow.infernoflow@0.7.2` shipped on the same day. To get the matching auto-capture popup, CodeLens, bulk-delete, and orphan-handling features, install the extension from the VS Code Marketplace.
14
+ - Existing users running `infernoflow setup` after upgrading to 0.42.6 will see all 14 MCP tools pre-approved correctly. Old `.claude/settings.json` files with the phantom `infernoflow_suggest` entry won't cause harm — that tool just isn't available — but re-running `setup` will clean it up.
15
+
3
16
  ## 0.42.5 — 2026-05-05
4
17
 
5
18
  ### Added
@@ -68,6 +81,11 @@
68
81
 
69
82
 
70
83
 
84
+ - VS Code Marketplace badge + extension install section
85
+
86
+ - extension v0.7.2 + CLI hotfixes: auto-capture, CodeLens, bulk + orphan delete, MCP setup tools fix, graph crash guard
87
+ - VS Code Marketplace badge + extension install section
88
+
71
89
  ## 0.42.1 — 2026-05-03
72
90
 
73
91
  ### Added
package/README.md CHANGED
@@ -8,6 +8,7 @@
8
8
  [![npm downloads](https://img.shields.io/npm/dw/infernoflow.svg?color=orange)](https://www.npmjs.com/package/infernoflow)
9
9
  [![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](./package.json)
10
10
  [![npm audit](https://img.shields.io/badge/npm%20audit-0%20vulnerabilities-brightgreen)](https://docs.npmjs.com/cli/v10/commands/npm-audit)
11
+ [![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/infernoflow.infernoflow?label=VS%20Code&color=orange)](https://marketplace.visualstudio.com/items?itemName=infernoflow.infernoflow)
11
12
 
12
13
  ## The 60-second pitch
13
14
 
@@ -92,6 +93,16 @@ When you run `infernoflow log`, infernoflow silently keeps these files up to dat
92
93
 
93
94
  You don't have to paste anything. Set up once, every future session is better.
94
95
 
96
+ ## VS Code extension
97
+
98
+ For the visual experience inside VS Code (sidebar memory panel, gotchas as Problems-panel warnings, status-bar health score, one-click handoff), install the companion extension:
99
+
100
+ ```
101
+ ext install infernoflow.infernoflow
102
+ ```
103
+
104
+ Or browse it on the [Marketplace](https://marketplace.visualstudio.com/items?itemName=infernoflow.infernoflow). Activates automatically on any project containing `.ai-memory/sessions.jsonl` or `inferno/`.
105
+
95
106
  ## Cursor / VS Code MCP integration
96
107
 
97
108
  ```bash
@@ -1 +1,3 @@
1
- import*as A from"node:fs";import*as C from"node:path";import{bold as h,cyan as N,gray as g,green as m,yellow as v,red as w}from"../ui/output.mjs";function O(t){try{return JSON.parse(A.readFileSync(t,"utf8"))}catch{return null}}function P(t){return t?.stability||"experimental"}const u={frozen:"\u{1F9CA}",stable:"\u3030\uFE0F ",experimental:"\u{1F30A}"},x={frozen:w,stable:v,experimental:m};function z(t,i){const c={},r={},f={},s={};for(const e of t){const l=i.find(o=>o.id===e.id)||{};c[e.id]={id:e.id,name:e.name||l.name||l.title||e.id,stability:l.stability||"experimental",functions:e.codeAnalysis?.functions||[],calls:e.codeAnalysis?.calls||[],services:e.codeAnalysis?.services||[],dbCalls:e.codeAnalysis?.dbCalls||[],httpCalls:e.codeAnalysis?.httpCalls||[]},r[e.id]=new Set,f[e.id]=new Set;for(const o of e.codeAnalysis?.functions||[]){const n=o.replace(/\(\)$/,"");s[n]=e.id,s[n.toLowerCase()]=e.id}}for(const[e,l]of Object.entries(c))for(const o of l.calls){const n=o.replace(/\(\)$/,""),d=s[n]||s[n.toLowerCase()];d&&d!==e&&(r[e].add(d),f[d].add(e))}const b={},a={};for(const e of Object.keys(c))b[e]=[...r[e]],a[e]=[...f[e]];return{nodes:c,edges:b,reverse:a}}function I(t){const{nodes:i,edges:c,reverse:r}=t,f=Object.keys(i).sort();console.log(),console.log(h(" Capability Dependency Graph")),console.log(g(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log();let s=!1;for(const a of f){const e=i[a],l=c[a]||[],o=r[a]||[],n=u[e.stability]||"\u{1F30A}",d=x[e.stability]||m;if(!(l.length===0&&o.length===0)){if(s=!0,console.log(` ${n} ${h(d(a))}`),l.length>0){console.log(g(" calls \u2192"));for(const p of l){const $=i[p],y=u[$?.stability]||"\u{1F30A}";console.log(g(` ${y} ${p}`))}}if(o.length>0){console.log(g(" called by \u2190"));for(const p of o){const $=u[i[p]?.stability]||"\u{1F30A}";console.log(g(` ${$} ${p}`))}}console.log()}}s||(console.log(g(" No inter-capability dependencies detected.")),console.log(g(" Run `infernoflow scan` first to populate call data.")),console.log());const b=Object.values(t.edges).reduce((a,e)=>a+e.length,0);console.log(g(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(g(` ${f.length} capabilities \xB7 ${b} dependency edge(s)`)),console.log()}function E(t,i){const{nodes:c,edges:r,reverse:f}=i,s=c[t];s||(console.error(w(`\u2717 Capability "${t}" not found in graph.`)),process.exit(1));const b=u[s.stability]||"\u{1F30A}",a=x[s.stability]||m;console.log(),console.log(h(` ${b} ${a(t)}`)+g(` (${s.stability})`)),s.services?.length&&console.log(g(" external: ")+N(s.services.join(", "))),console.log();const e=r[t]||[],l=f[t]||[];if(e.length>0){console.log(h(" Calls (downstream dependencies):"));for(const o of e){const n=c[o],d=x[n?.stability]||m,p=u[n?.stability]||"\u{1F30A}";console.log(` ${p} ${d(o)}`+g(n?.services?.length?` [${n.services.join(", ")}]`:""))}console.log()}else console.log(g(" No downstream dependencies.")),console.log();if(l.length>0){console.log(h(" Called by (upstream dependents):"));for(const o of l){const n=c[o],d=x[n?.stability]||m,p=u[n?.stability]||"\u{1F30A}";console.log(` ${p} ${d(o)}`)}console.log()}else console.log(g(" No capabilities call this one.")),console.log();if((s.stability==="frozen"||s.stability==="stable")&&l.length>0){const o=s.stability==="frozen"?w:v;console.log(o(` \u26A0 This capability is ${s.stability}. Changing it may break:`));for(const n of l)console.log(o(` \u2022 ${n}`));console.log()}}function F(t,i){const c=[];if(!t||!i)return c;for(const[r,f]of Object.entries(i.nodes)){if(f.stability==="experimental")continue;const s=new Set(t.reverse?.[r]||[]),a=[...new Set(i.reverse[r]||[])].filter(e=>!s.has(e));if(a.length>0&&c.push({type:"new-dependents",capId:r,stability:f.stability,detail:`${a.join(", ")} now depend on this`}),f.stability==="frozen"){const e=new Set(t.edges?.[r]||[]),l=new Set(i.edges[r]||[]),o=[...l].filter(d=>!e.has(d)),n=[...e].filter(d=>!l.has(d));(o.length>0||n.length>0)&&c.push({type:"frozen-internals-changed",capId:r,stability:f.stability,detail:[o.length?`added calls: ${o.join(", ")}`:"",n.length?`removed calls: ${n.join(", ")}`:""].filter(Boolean).join("; ")})}}return c}async function R(t){const i=(t||[]).slice(1),c=i.includes("--json"),r=i.includes("--check"),f=i.indexOf("--cap"),s=f!==-1?i[f+1]:null,b=process.cwd(),a=C.join(b,"inferno"),e=C.join(a,"scan.json"),l=C.join(a,"graph.json"),o=C.join(a,"capabilities.json"),n=O(e);n||(console.error(w("\u2717 inferno/scan.json not found \u2014 run `infernoflow scan` first.")),process.exit(1));let d=[];const p=O(o);p&&(d=Array.isArray(p)?p:p.capabilities||[]);const $=n.capabilities||[],y=z($,d),L=O(l),D=F(L,y),k={builtAt:new Date().toISOString(),capabilities:Object.keys(y.nodes).length,edges:Object.values(y.edges).reduce((j,S)=>j+S.length,0),nodes:y.nodes,deps:y.edges,dependents:y.reverse};if(c||A.writeFileSync(l,JSON.stringify(k,null,2)),c){console.log(JSON.stringify(k,null,2));return}if(s?E(s,y):I(y),D.length>0){console.log(v(" \u26A0 Dependency changes detected:"));for(const j of D){const S=j.stability==="frozen"?w("\u{1F9CA}"):v("\u3030\uFE0F ");console.log(` ${S} ${h(j.capId)} \u2014 ${j.detail}`)}console.log(),r&&process.exit(1)}c||console.log(g(" Graph saved \u2192 inferno/graph.json"))}function B(t){return O(C.join(t,"graph.json"))}export{R as graphCommand,B as loadGraph};
1
+ import*as k from"node:fs";import*as m from"node:path";import{bold as h,cyan as L,gray as f,green as w,yellow as v,red as u}from"../ui/output.mjs";function x(t){try{return JSON.parse(k.readFileSync(t,"utf8"))}catch{return null}}function J(t){return t?.stability||"experimental"}const j={frozen:"\u{1F9CA}",stable:"\u3030\uFE0F ",experimental:"\u{1F30A}"},O={frozen:u,stable:v,experimental:w};function z(t,i){const c={},a={},g={},s={};for(const e of t){const l=i.find(n=>n.id===e.id)||{};c[e.id]={id:e.id,name:e.name||l.name||l.title||e.id,stability:l.stability||"experimental",functions:e.codeAnalysis?.functions||[],calls:e.codeAnalysis?.calls||[],services:e.codeAnalysis?.services||[],dbCalls:e.codeAnalysis?.dbCalls||[],httpCalls:e.codeAnalysis?.httpCalls||[]},a[e.id]=new Set,g[e.id]=new Set;for(const n of e.codeAnalysis?.functions||[]){const o=n.replace(/\(\)$/,"");s[o]=e.id,s[o.toLowerCase()]=e.id}}for(const[e,l]of Object.entries(c))for(const n of l.calls){const o=n.replace(/\(\)$/,""),d=s[o]||s[o.toLowerCase()];d&&d!==e&&a[e]&&g[d]&&(a[e].add(d),g[d].add(e))}const y={},r={};for(const e of Object.keys(c))y[e]=[...a[e]],r[e]=[...g[e]];return{nodes:c,edges:y,reverse:r}}function I(t){const{nodes:i,edges:c,reverse:a}=t,g=Object.keys(i).sort();console.log(),console.log(h(" Capability Dependency Graph")),console.log(f(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log();let s=!1;for(const r of g){const e=i[r],l=c[r]||[],n=a[r]||[],o=j[e.stability]||"\u{1F30A}",d=O[e.stability]||w;if(!(l.length===0&&n.length===0)){if(s=!0,console.log(` ${o} ${h(d(r))}`),l.length>0){console.log(f(" calls \u2192"));for(const p of l){const $=i[p],b=j[$?.stability]||"\u{1F30A}";console.log(f(` ${b} ${p}`))}}if(n.length>0){console.log(f(" called by \u2190"));for(const p of n){const $=j[i[p]?.stability]||"\u{1F30A}";console.log(f(` ${$} ${p}`))}}console.log()}}s||(console.log(f(" No inter-capability dependencies detected.")),console.log(f(" Run `infernoflow scan` first to populate call data.")),console.log());const y=Object.values(t.edges).reduce((r,e)=>r+e.length,0);console.log(f(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")),console.log(f(` ${g.length} capabilities \xB7 ${y} dependency edge(s)`)),console.log()}function E(t,i){const{nodes:c,edges:a,reverse:g}=i,s=c[t];s||(console.error(u(`\u2717 Capability "${t}" not found in graph.`)),process.exit(1));const y=j[s.stability]||"\u{1F30A}",r=O[s.stability]||w;console.log(),console.log(h(` ${y} ${r(t)}`)+f(` (${s.stability})`)),s.services?.length&&console.log(f(" external: ")+L(s.services.join(", "))),console.log();const e=a[t]||[],l=g[t]||[];if(e.length>0){console.log(h(" Calls (downstream dependencies):"));for(const n of e){const o=c[n],d=O[o?.stability]||w,p=j[o?.stability]||"\u{1F30A}";console.log(` ${p} ${d(n)}`+f(o?.services?.length?` [${o.services.join(", ")}]`:""))}console.log()}else console.log(f(" No downstream dependencies.")),console.log();if(l.length>0){console.log(h(" Called by (upstream dependents):"));for(const n of l){const o=c[n],d=O[o?.stability]||w,p=j[o?.stability]||"\u{1F30A}";console.log(` ${p} ${d(n)}`)}console.log()}else console.log(f(" No capabilities call this one.")),console.log();if((s.stability==="frozen"||s.stability==="stable")&&l.length>0){const n=s.stability==="frozen"?u:v;console.log(n(` \u26A0 This capability is ${s.stability}. Changing it may break:`));for(const o of l)console.log(n(` \u2022 ${o}`));console.log()}}function F(t,i){const c=[];if(!t||!i)return c;for(const[a,g]of Object.entries(i.nodes)){if(g.stability==="experimental")continue;const s=new Set(t.reverse?.[a]||[]),r=[...new Set(i.reverse[a]||[])].filter(e=>!s.has(e));if(r.length>0&&c.push({type:"new-dependents",capId:a,stability:g.stability,detail:`${r.join(", ")} now depend on this`}),g.stability==="frozen"){const e=new Set(t.edges?.[a]||[]),l=new Set(i.edges[a]||[]),n=[...l].filter(d=>!e.has(d)),o=[...e].filter(d=>!l.has(d));(n.length>0||o.length>0)&&c.push({type:"frozen-internals-changed",capId:a,stability:g.stability,detail:[n.length?`added calls: ${n.join(", ")}`:"",o.length?`removed calls: ${o.join(", ")}`:""].filter(Boolean).join("; ")})}}return c}async function P(t){const i=(t||[]).slice(1),c=i.includes("--json"),a=i.includes("--check"),g=i.indexOf("--cap"),s=g!==-1?i[g+1]:null,y=process.cwd(),r=m.join(y,"inferno"),e=m.join(r,"scan.json"),l=m.join(r,"graph.json"),n=m.join(r,"capabilities.json"),o=x(e);o||(console.error(u("\u2717 inferno/scan.json not found.")),console.error(f(" The graph is built from a deep AST scan of your codebase.")),console.error(f(" Run this first to generate it:")),console.error(L(`
2
+ infernoflow scan
3
+ `)),process.exit(1)),(!Array.isArray(o.capabilities)||o.capabilities.length===0)&&(console.error(u("\u2717 inferno/scan.json has no capabilities.")),console.error(f(" Re-run `infernoflow scan` to refresh the data.")),process.exit(1));let d=[];const p=x(n);p&&(d=Array.isArray(p)?p:p.capabilities||[]);const $=o.capabilities||[],b=z($,d),N=x(l),A=F(N,b),D={builtAt:new Date().toISOString(),capabilities:Object.keys(b.nodes).length,edges:Object.values(b.edges).reduce((C,S)=>C+S.length,0),nodes:b.nodes,deps:b.edges,dependents:b.reverse};if(c||k.writeFileSync(l,JSON.stringify(D,null,2)),c){console.log(JSON.stringify(D,null,2));return}if(s?E(s,b):I(b),A.length>0){console.log(v(" \u26A0 Dependency changes detected:"));for(const C of A){const S=C.stability==="frozen"?u("\u{1F9CA}"):v("\u3030\uFE0F ");console.log(` ${S} ${h(C.capId)} \u2014 ${C.detail}`)}console.log(),a&&process.exit(1)}c||console.log(f(" Graph saved \u2192 inferno/graph.json"))}function T(t){return x(m.join(t,"graph.json"))}export{P as graphCommand,T as loadGraph};
@@ -1,4 +1,4 @@
1
- import*as t from"node:fs";import*as c from"node:path";import*as x from"node:os";import{fileURLToPath as $}from"node:url";import{execSync as k}from"node:child_process";import{detectIdeContext as P}from"../ai/ideDetection.mjs";import{header as O,ok as a,warn as d,info as j,done as J,cyan as _,yellow as F,bold as m,green as g}from"../ui/output.mjs";import{installCursorHooksArtifacts as M}from"../cursorHooksInstall.mjs";import{installVsCodeCopilotHooksArtifacts as N}from"../vsCodeCopilotHooksInstall.mjs";const b=c.dirname($(import.meta.url));function A(){return c.resolve(b,"../../templates")}function T(s){try{return k(`npx infernoflow ${s}`,{encoding:"utf8",cwd:process.cwd(),timeout:6e4,stdio:["inherit","pipe","pipe"]})}catch(e){return e.stdout||e.stderr||e.message}}const D=["infernoflow_status","infernoflow_run","infernoflow_suggest","infernoflow_check","infernoflow_context","infernoflow_implement","infernoflow_git_drift","infernoflow_scan_ui","infernoflow_review"];function L(s){const e=c.join(x.homedir(),".claude.json");let o={};if(t.existsSync(e))try{o=JSON.parse(t.readFileSync(e,"utf8"))}catch{o={}}o.mcpServers||(o.mcpServers={});const r=o.mcpServers.infernoflow;if(r&&r.args&&r.args[0]===s)return{updated:!1};o.mcpServers.infernoflow={command:"node",args:[s]};const i=JSON.stringify(o,null,2).replace(/\u0000+/g,"");return t.writeFileSync(e,i,"utf8"),{updated:!0,path:e}}function R(s,e){const o=c.join(s,".claude"),r=c.join(o,"settings.json");let i={};if(t.existsSync(r))try{i=JSON.parse(t.readFileSync(r,"utf8"))}catch{i={}}const l=new Set(i.allowedTools||[]);for(const u of D)l.add(`mcp__infernoflow__${u}`);const w={...i,allowedTools:[...l]};return t.mkdirSync(o,{recursive:!0}),t.writeFileSync(r,JSON.stringify(w,null,2),"utf8"),r}async function E(s){const e=process.cwd(),o=s.includes("--force")||s.includes("-f"),r=s.includes("--yes")||s.includes("-y"),i=A();O("infernoflow setup");const{ideDetected:l}=P("auto");j(`IDE detected: ${m(l==="cursor"?"Cursor":l==="vscode"?"VS Code":l==="windsurf"?"Windsurf":"unknown")}`);const u=c.join(e,"inferno"),y=c.join(u,"contract.json");if(t.existsSync(y))a("inferno/contract.json already exists \u2014 skipping init");else{console.log(`
1
+ import*as t from"node:fs";import*as c from"node:path";import*as x from"node:os";import{fileURLToPath as $}from"node:url";import{execSync as k}from"node:child_process";import{detectIdeContext as P}from"../ai/ideDetection.mjs";import{header as O,ok as a,warn as d,info as j,done as J,cyan as _,yellow as F,bold as m,green as g}from"../ui/output.mjs";import{installCursorHooksArtifacts as M}from"../cursorHooksInstall.mjs";import{installVsCodeCopilotHooksArtifacts as N}from"../vsCodeCopilotHooksInstall.mjs";const b=c.dirname($(import.meta.url));function A(){return c.resolve(b,"../../templates")}function T(r){try{return k(`npx infernoflow ${r}`,{encoding:"utf8",cwd:process.cwd(),timeout:6e4,stdio:["inherit","pipe","pipe"]})}catch(e){return e.stdout||e.stderr||e.message}}const D=["infernoflow_status","infernoflow_run","infernoflow_apply","infernoflow_check","infernoflow_context","infernoflow_implement","infernoflow_git_drift","infernoflow_scan_ui","infernoflow_review","amp_read","amp_write","amp_search","amp_handoff","amp_health"];function L(r){const e=c.join(x.homedir(),".claude.json");let o={};if(t.existsSync(e))try{o=JSON.parse(t.readFileSync(e,"utf8"))}catch{o={}}o.mcpServers||(o.mcpServers={});const s=o.mcpServers.infernoflow;if(s&&s.args&&s.args[0]===r)return{updated:!1};o.mcpServers.infernoflow={command:"node",args:[r]};const i=JSON.stringify(o,null,2).replace(/\u0000+/g,"");return t.writeFileSync(e,i,"utf8"),{updated:!0,path:e}}function R(r,e){const o=c.join(r,".claude"),s=c.join(o,"settings.json");let i={};if(t.existsSync(s))try{i=JSON.parse(t.readFileSync(s,"utf8"))}catch{i={}}const l=new Set(i.allowedTools||[]);for(const u of D)l.add(`mcp__infernoflow__${u}`);const w={...i,allowedTools:[...l]};return t.mkdirSync(o,{recursive:!0}),t.writeFileSync(s,JSON.stringify(w,null,2),"utf8"),s}async function E(r){const e=process.cwd(),o=r.includes("--force")||r.includes("-f"),s=r.includes("--yes")||r.includes("-y"),i=A();O("infernoflow setup");const{ideDetected:l}=P("auto");j(`IDE detected: ${m(l==="cursor"?"Cursor":l==="vscode"?"VS Code":l==="windsurf"?"Windsurf":"unknown")}`);const u=c.join(e,"inferno"),y=c.join(u,"contract.json");if(t.existsSync(y))a("inferno/contract.json already exists \u2014 skipping init");else{console.log(`
2
2
  ${F("inferno/")} not found \u2014 running init --adopt ...
3
- `);const n=["--adopt",r?"--yes":""].filter(Boolean).join(" ");T(`init ${n}`)}const S=n=>a(n),h=n=>d(n);M({cwd:e,templatesRoot:i,force:o,silent:!1,logOk:S,logWarn:h});const v=c.join(i,"cursor","inferno-mcp-server.mjs"),f=c.join(e,".cursor","inferno-mcp-server.mjs");(!t.existsSync(f)||o)&&(t.mkdirSync(c.dirname(f),{recursive:!0}),t.copyFileSync(v,f),a("Copied MCP server \u2192 .cursor/inferno-mcp-server.mjs")),l==="vscode"&&N({cwd:e,templatesRoot:i,force:o,silent:!1,logOk:S,logWarn:h}),console.log(),j("Configuring Claude Code (VS Code extension)...");const C=f;try{L(C).updated?a("Updated ~/.claude.json \u2192 infernoflow MCP server registered"):a("~/.claude.json already has infernoflow \u2014 no changes needed")}catch(n){d(`Could not update ~/.claude.json: ${n.message}`),d(`Add manually: ${_('"mcpServers": { "infernoflow": { "command": "node", "args": ["'+C+'"] } }')}`)}try{const n=R(e,o);a("Written .claude/settings.json \u2014 infernoflow tools pre-approved (no more prompts)")}catch(n){d(`Could not write .claude/settings.json: ${n.message}`)}let p=0;try{p=(JSON.parse(t.readFileSync(y,"utf8")).capabilities||[]).length}catch{}console.log(),J(p>0?`infernoflow ready \u2014 ${p} capabilities tracked`:"infernoflow ready"),console.log(`
3
+ `);const n=["--adopt",s?"--yes":""].filter(Boolean).join(" ");T(`init ${n}`)}const h=n=>a(n),S=n=>d(n);M({cwd:e,templatesRoot:i,force:o,silent:!1,logOk:h,logWarn:S});const v=c.join(i,"cursor","inferno-mcp-server.mjs"),f=c.join(e,".cursor","inferno-mcp-server.mjs");(!t.existsSync(f)||o)&&(t.mkdirSync(c.dirname(f),{recursive:!0}),t.copyFileSync(v,f),a("Copied MCP server \u2192 .cursor/inferno-mcp-server.mjs")),l==="vscode"&&N({cwd:e,templatesRoot:i,force:o,silent:!1,logOk:h,logWarn:S}),console.log(),j("Configuring Claude Code (VS Code extension)...");const C=f;try{L(C).updated?a("Updated ~/.claude.json \u2192 infernoflow MCP server registered"):a("~/.claude.json already has infernoflow \u2014 no changes needed")}catch(n){d(`Could not update ~/.claude.json: ${n.message}`),d(`Add manually: ${_('"mcpServers": { "infernoflow": { "command": "node", "args": ["'+C+'"] } }')}`)}try{const n=R(e,o);a("Written .claude/settings.json \u2014 infernoflow tools pre-approved (no more prompts)")}catch(n){d(`Could not write .claude/settings.json: ${n.message}`)}let p=0;try{p=(JSON.parse(t.readFileSync(y,"utf8")).capabilities||[]).length}catch{}console.log(),J(p>0?`infernoflow ready \u2014 ${p} capabilities tracked`:"infernoflow ready"),console.log(`
4
4
  ${m("What was set up:")}`),console.log(` ${g("\u2714")} MCP server installed \u2192 .cursor/inferno-mcp-server.mjs`),console.log(` ${g("\u2714")} ~/.claude.json updated \u2192 Claude Code will find infernoflow`),console.log(` ${g("\u2714")} .claude/settings.json \u2192 no permission prompts`),console.log(),console.log(` ${m("Next step:")} Restart VS Code, then ask Claude:`),console.log(` ${_('"show me the infernoflow status of this project"')}`),console.log()}export{E as setupCommand};
@@ -7,9 +7,33 @@ function send(obj) { process.stdout.write(JSON.stringify(obj) + "\n"); }
7
7
  function sendResult(id, result) { send({ jsonrpc: "2.0", id, result }); }
8
8
  function sendError(id, code, message) { send({ jsonrpc: "2.0", id, error: { code, message } }); }
9
9
 
10
+ /**
11
+ * Run the infernoflow CLI. Returns either the stdout string OR a structured
12
+ * error object so call sites can decide whether to surface it via JSON-RPC
13
+ * sendError() instead of returning gibberish text to the agent.
14
+ */
10
15
  function runCmd(args, env = {}) {
11
- try { return execSync(`npx infernoflow ${args}`, { encoding: "utf8", cwd: process.cwd(), timeout: 30000, env: { ...process.env, ...env } }); }
12
- catch (err) { return err.stdout || err.message; }
16
+ try {
17
+ return execSync(`npx infernoflow ${args}`, {
18
+ encoding: "utf8",
19
+ cwd: process.cwd(),
20
+ timeout: 30000,
21
+ env: { ...process.env, ...env },
22
+ });
23
+ } catch (err) {
24
+ return {
25
+ __error: true,
26
+ message: err.message || "command failed",
27
+ stderr: err.stderr || "",
28
+ stdout: err.stdout || "",
29
+ status: err.status ?? 1,
30
+ };
31
+ }
32
+ }
33
+
34
+ /** True if a runCmd() result is actually a structured error. */
35
+ function isCmdError(result) {
36
+ return typeof result === "object" && result !== null && result.__error === true;
13
37
  }
14
38
 
15
39
  const TOOLS = [
@@ -480,7 +504,10 @@ function handleTool(id, name, input) {
480
504
  text = runCmd(`log ${m} --type ${t} ${extras.join(" ")}`);
481
505
  } else if (name === "amp_handoff") {
482
506
  // switch writes a file; we read it back to return the content
483
- runCmd("switch");
507
+ const switchResult = runCmd("switch");
508
+ if (isCmdError(switchResult)) {
509
+ return sendError(id, -32000, `infernoflow switch failed: ${switchResult.message}\n${switchResult.stderr || switchResult.stdout || ""}`.trim());
510
+ }
484
511
  try {
485
512
  const ampPath = path.join(process.cwd(), ".ai-memory", "handoff.md");
486
513
  const legacyPath = path.join(process.cwd(), "inferno", "HANDOFF.md");
@@ -498,9 +525,25 @@ function handleTool(id, name, input) {
498
525
  if (input.type) args.push("--type", input.type);
499
526
  text = runCmd("ask " + args.join(" "));
500
527
  } else if (name === "amp_health") {
501
- text = runCmd("recap --json").trim() || runCmd("status");
528
+ const recap = runCmd("recap --json");
529
+ if (isCmdError(recap)) {
530
+ text = runCmd("status");
531
+ } else {
532
+ text = recap.trim() || runCmd("status");
533
+ }
502
534
 
503
535
  } else { return sendError(id, -32601, `Unknown tool: ${name}`); }
536
+
537
+ // Central error check — if any runCmd() call produced a structured error,
538
+ // surface it as a real JSON-RPC error so the calling AI sees a proper
539
+ // failure instead of garbled stderr text mixed into a "successful" reply.
540
+ if (isCmdError(text)) {
541
+ const detail = (text.stderr || text.stdout || "").trim();
542
+ const fullMsg = detail
543
+ ? `infernoflow CLI failed: ${text.message}\n${detail}`
544
+ : `infernoflow CLI failed: ${text.message}`;
545
+ return sendError(id, -32000, fullMsg);
546
+ }
504
547
  sendResult(id, { content: [{ type: "text", text: text || "(no output)" }] });
505
548
  } catch (err) { sendError(id, -32000, err.message); }
506
549
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.42.5",
3
+ "version": "0.42.6",
4
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": {