infernoflow 0.44.20 → 0.46.0

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.
Files changed (37) hide show
  1. package/README.md +15 -5
  2. package/dist/bin/infernoflow.mjs +30 -26
  3. package/dist/lib/amp/io.mjs +22 -19
  4. package/dist/lib/claudeAssets.mjs +3 -1
  5. package/dist/lib/cleanTree.mjs +10 -10
  6. package/dist/lib/commands/ai.mjs +6 -2
  7. package/dist/lib/commands/ask.mjs +3 -3
  8. package/dist/lib/commands/bookmark.mjs +12 -12
  9. package/dist/lib/commands/curate.mjs +10 -0
  10. package/dist/lib/commands/doctor.mjs +3 -2
  11. package/dist/lib/commands/hook.mjs +12 -0
  12. package/dist/lib/commands/log.mjs +15 -14
  13. package/dist/lib/commands/mcp.mjs +1 -0
  14. package/dist/lib/commands/move.mjs +14 -0
  15. package/dist/lib/commands/recap.mjs +5 -5
  16. package/dist/lib/commands/resolve.mjs +5 -0
  17. package/dist/lib/commands/resume.mjs +3 -0
  18. package/dist/lib/commands/setup.mjs +82 -18
  19. package/dist/lib/commands/status.mjs +4 -4
  20. package/dist/lib/commands/sync.mjs +27 -27
  21. package/dist/lib/commands/transcript.mjs +2 -0
  22. package/dist/lib/commands/uninstall.mjs +11 -11
  23. package/dist/lib/cursorHooksInstall.mjs +1 -1
  24. package/dist/lib/mcpRegistration.mjs +6 -0
  25. package/dist/lib/memoryView.mjs +7 -0
  26. package/dist/lib/personalConfig.mjs +2 -0
  27. package/dist/lib/ruleFiles.mjs +11 -11
  28. package/dist/lib/schema.mjs +1 -0
  29. package/dist/lib/security/redact.mjs +1 -0
  30. package/dist/lib/securityRefresh.mjs +1 -1
  31. package/dist/lib/upgradeCheck.mjs +6 -4
  32. package/dist/templates/agents/memory-keeper.md +49 -54
  33. package/dist/templates/cursor/hooks/inferno-session-draft.mjs +8 -3
  34. package/dist/templates/cursor/inferno-mcp-server.mjs +78 -13
  35. package/dist/templates/hooks/infernoflow-agent-guard.mjs +32 -0
  36. package/dist/templates/skills/infernoflow-memory/SKILL.md +68 -86
  37. package/package.json +3 -3
@@ -1,3 +1,4 @@
1
- import*as c from"node:fs";import*as s from"node:path";import*as y from"node:os";import*as A from"node:http";import{execSync as O,spawnSync as v}from"node:child_process";import{fileURLToPath as P}from"node:url";import{bold as C,cyan as b,gray as k,green as w,yellow as S,red as $}from"../ui/output.mjs";import{detectAvailableProviders as I}from"../ai/providerRouter.mjs";import{readEntries as _,readConfig as M}from"../amp/io.mjs";import{resolveInjectionSettings as E}from"../ruleFiles.mjs";function l(e,n){try{const o=n();return{label:e,...o}}catch(o){return{label:e,status:"error",message:o.message,fix:null}}}function a(e,n){return{status:"pass",message:e,detail:n||null,fix:null}}function f(e,n){return{status:"warn",message:e,detail:null,fix:n||null}}function h(e,n){return{status:"fail",message:e,detail:null,fix:n||null}}function T(){const e=process.version,n=parseInt(e.slice(1).split(".")[0],10);return n>=20?a(`Node.js ${e}`,"Node 20+ recommended"):n>=18?a(`Node.js ${e}`):h(`Node.js ${e} \u2014 infernoflow requires Node 18+`,"Install Node 20 from nodejs.org")}function D(){try{const e=v("infernoflow",["--version"],{encoding:"utf8",timeout:5e3,shell:process.platform==="win32"});return e.status===0?a(`infernoflow v${e.stdout.trim()} installed`):a("infernoflow CLI on PATH (version probe failed but doctor itself ran)")}catch{return a("infernoflow CLI on PATH (version probe threw but doctor itself ran)")}}function G(e){try{return O("git rev-parse --git-dir",{cwd:e,stdio:"ignore"}),a("Git repository detected")}catch{return h("Not a git repository","git init && git add . && git commit -m 'init'")}}function F(e){const n=s.join(e,".ai-memory"),o=s.join(e,"inferno");return c.existsSync(n)&&c.existsSync(o)?a(".ai-memory/ + inferno/ both present"):c.existsSync(n)?a(".ai-memory/ directory exists (memory mode)"):c.existsSync(o)?a("inferno/ directory exists"):h("No memory directory found (.ai-memory/ or inferno/)","infernoflow init")}function L(e){if(j(e)){let o=0;try{o=_(e).length}catch{}return a(o===0?"Memory mode \u2014 sessions.jsonl will be created on first log":`Memory mode \u2014 ${o} session entr${o===1?"y":"ies"}`)}const n=s.join(e,"inferno");for(const o of["contract.json","capabilities.json"]){const t=s.join(n,o);if(c.existsSync(t))try{const i=(JSON.parse(c.readFileSync(t,"utf8")).capabilities||[]).length;return a(`${o} valid \u2014 ${i} capabilities`)}catch{return h(`${o} contains invalid JSON`,`Fix the JSON syntax in inferno/${o}`)}}return h("No contract.json/capabilities.json (and not in memory mode)","infernoflow init or infernoflow init --mode full")}function j(e){const n=s.join(e,".ai-memory"),o=s.join(e,"inferno","contract.json");if(c.existsSync(n)&&!c.existsSync(o))return!0;try{return JSON.parse(c.readFileSync(s.join(e,"inferno","config.json"),"utf8")).mode==="memory"}catch{return!1}}function R(e){if(j(e))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const n=s.join(e,"inferno","scenarios");if(!c.existsSync(n))return f("No scenarios/ directory","infernoflow init");const o=c.readdirSync(n).filter(t=>t.endsWith(".json"));return o.length?a(`${o.length} scenario file${o.length!==1?"s":""} found`):f("scenarios/ is empty","Add scenario files or run infernoflow suggest")}function J(e){if(j(e))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const n=s.join(e,"inferno","CHANGELOG.md");return c.existsSync(n)?a("inferno/CHANGELOG.md exists"):f("No inferno/CHANGELOG.md","infernoflow init")}function H(e){if(j(e))return{status:"info",message:"n/a in memory mode (CLAUDE.md is auto-maintained)",detail:null,fix:null};const n=s.join(e,"inferno","CONTEXT.md");if(!c.existsSync(n))return f("No CONTEXT.md generated","infernoflow context");const o=(Date.now()-c.statSync(n).mtimeMs)/(1e3*60*60*24);return o>7?f(`CONTEXT.md is ${Math.round(o)} days old \u2014 may be stale`,"infernoflow context"):a(`CONTEXT.md present (${Math.round(o)}d old)`)}function W(e){const n=s.join(e,".git","hooks"),o=s.join(n,"post-commit");return c.existsSync(o)&&c.readFileSync(o,"utf8").includes("infernoflow")?a("Git hooks installed (post-commit)"):f("Git hooks not installed","infernoflow setup --yes")}function X(e){const n=[s.join(e,".cursor","mcp.json"),s.join(e,".mcp.json"),s.join(e,".vscode","mcp.json"),s.join(y.homedir(),".claude.json"),s.join(y.homedir(),".cursor","mcp.json"),s.join(y.homedir(),"Library","Application Support","Claude","claude_desktop_config.json"),s.join(y.homedir(),"AppData","Roaming","Claude","claude_desktop_config.json")];for(const o of n)if(c.existsSync(o))try{const t=JSON.parse(c.readFileSync(o,"utf8")),r=t.mcpServers||t.mcp_servers||t.servers||{};if(Object.keys(r).some(i=>i.toLowerCase().includes("inferno")))return a(`MCP server configured in ${s.basename(o)}`)}catch{}return f("MCP server not configured","infernoflow setup --yes (adds to Cursor/Claude/Copilot config)")}function K(e,n){const o=s.join(e,".ai-memory",".mcp-runtime.json");if(!c.existsSync(o))return a("MCP runtime stamp not present yet \u2014 start your AI tool to write one");let t;try{t=JSON.parse(c.readFileSync(o,"utf8"))}catch{return f(".mcp-runtime.json present but unreadable","Delete .ai-memory/.mcp-runtime.json and restart your AI tool")}return!t||typeof t.version!="string"?f(".mcp-runtime.json malformed","Delete it; the next MCP boot will rewrite it"):t.version===n?a(`MCP runtime v${t.version} matches CLI`):n.startsWith("0.0.0")||t.version.startsWith("0.0.0")?a(`MCP runtime v${t.version} (dev/source) \u2014 skipping version-skew check`):f(`MCP server is running v${t.version} but CLI is v${n} \u2014 restart your AI tool to load the new code.`,"Quit and reopen Cursor / Claude Code / VS Code (the long-running MCP process keeps the old code in memory until restart)")}function U(e){const n=E(M(e));if(!n.includeProtocol)return a("Memory-protocol injection disabled in config (injection.includeProtocol=false)");const o=n.targets,t=[];for(const r of o)try{const i=c.readFileSync(s.join(e,String(r)),"utf8");i.includes("amp_write")&&/Memory protocol/i.test(i)&&t.push(r)}catch{}return t.length?a(`Memory protocol in ${t.length}/${o.length} configured rule file(s) \u2014 AI knows when to auto-capture`):f("Memory protocol missing from rule files \u2014 the AI won't auto-capture gotchas","infernoflow refresh (writes the amp_write capture protocol into your rule files)")}function V(e){const n=I(e),o=Object.entries(n).filter(([,t])=>t).map(([t])=>t);return o.length?a(`AI provider${o.length!==1?"s":""}: ${o.join(", ")}`):f("No AI provider configured",`Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY, or OPENROUTER_API_KEY
1
+ import{claudeJsonPath as A,claudeDesktopConfigPath as I,isInfernoflowEntry as M}from"../mcpRegistration.mjs";import{HOOK_VERSION_MARKER as _}from"../securityRefresh.mjs";import{redactSecrets as E}from"../security/redact.mjs";import*as c from"node:fs";import*as s from"node:path";import*as y from"node:os";import*as D from"node:http";import{execSync as $,spawnSync as b}from"node:child_process";import{fileURLToPath as C}from"node:url";import{bold as N,cyan as P,gray as S,green as j,yellow as x,red as v}from"../ui/output.mjs";import{detectAvailableProviders as F}from"../ai/providerRouter.mjs";import{readEntries as R,readConfig as T}from"../amp/io.mjs";import{resolveInjectionSettings as G}from"../ruleFiles.mjs";function f(e,n){try{const o=n();return{label:e,...o}}catch(o){return{label:e,status:"error",message:o.message,fix:null}}}function a(e,n){return{status:"pass",message:e,detail:n||null,fix:null}}function l(e,n){return{status:"warn",message:e,detail:null,fix:n||null}}function h(e,n){return{status:"fail",message:e,detail:null,fix:n||null}}function L(){const e=process.version,n=parseInt(e.slice(1).split(".")[0],10);return n>=20?a(`Node.js ${e}`,"Node 20+ recommended"):n>=18?a(`Node.js ${e}`):h(`Node.js ${e} \u2014 infernoflow requires Node 18+`,"Install Node 20 from nodejs.org")}function J(){try{const e=b("infernoflow",["--version"],{encoding:"utf8",timeout:5e3,shell:process.platform==="win32"});return e.status===0?a(`infernoflow v${e.stdout.trim()} installed`):a("infernoflow CLI on PATH (version probe failed but doctor itself ran)")}catch{return a("infernoflow CLI on PATH (version probe threw but doctor itself ran)")}}function H(e){try{return $("git rev-parse --git-dir",{cwd:e,stdio:"ignore"}),a("Git repository detected")}catch{return h("Not a git repository","git init && git add . && git commit -m 'init'")}}function K(e){const n=s.join(e,".ai-memory"),o=s.join(e,"inferno");return c.existsSync(n)&&c.existsSync(o)?a(".ai-memory/ + inferno/ both present"):c.existsSync(n)?a(".ai-memory/ directory exists (memory mode)"):c.existsSync(o)?a("inferno/ directory exists"):h("No memory directory found (.ai-memory/ or inferno/)","infernoflow init")}function U(e){if(w(e)){let o=0;try{o=R(e).length}catch{}return a(o===0?"Memory mode \u2014 sessions.jsonl will be created on first log":`Memory mode \u2014 ${o} session entr${o===1?"y":"ies"}`)}const n=s.join(e,"inferno");for(const o of["contract.json","capabilities.json"]){const t=s.join(n,o);if(c.existsSync(t))try{const i=(JSON.parse(c.readFileSync(t,"utf8")).capabilities||[]).length;return a(`${o} valid \u2014 ${i} capabilities`)}catch{return h(`${o} contains invalid JSON`,`Fix the JSON syntax in inferno/${o}`)}}return h("No contract.json/capabilities.json (and not in memory mode)","infernoflow init or infernoflow init --mode full")}function w(e){const n=s.join(e,".ai-memory"),o=s.join(e,"inferno","contract.json");if(c.existsSync(n)&&!c.existsSync(o))return!0;try{return JSON.parse(c.readFileSync(s.join(e,"inferno","config.json"),"utf8")).mode==="memory"}catch{return!1}}function W(e){if(w(e))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const n=s.join(e,"inferno","scenarios");if(!c.existsSync(n))return l("No scenarios/ directory","infernoflow init");const o=c.readdirSync(n).filter(t=>t.endsWith(".json"));return o.length?a(`${o.length} scenario file${o.length!==1?"s":""} found`):l("scenarios/ is empty","Add scenario files or run infernoflow suggest")}function V(e){if(w(e))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const n=s.join(e,"inferno","CHANGELOG.md");return c.existsSync(n)?a("inferno/CHANGELOG.md exists"):l("No inferno/CHANGELOG.md","infernoflow init")}function X(e){if(w(e))return{status:"info",message:"n/a in memory mode (CLAUDE.md is auto-maintained)",detail:null,fix:null};const n=s.join(e,"inferno","CONTEXT.md");if(!c.existsSync(n))return l("No CONTEXT.md generated","infernoflow context");const o=(Date.now()-c.statSync(n).mtimeMs)/(1e3*60*60*24);return o>7?l(`CONTEXT.md is ${Math.round(o)} days old \u2014 may be stale`,"infernoflow context"):a(`CONTEXT.md present (${Math.round(o)}d old)`)}function Y(e){const n=s.join(e,".git","hooks","post-commit");return c.existsSync(n)&&c.readFileSync(n,"utf8").includes('infernoflow log "commit:')?l("Old commit-logging git hook still installed","infernoflow setup --yes"):a("No commit-logging git hook (git keeps commit history)")}function z(e){const n=[s.join(e,".cursor","mcp.json"),s.join(e,".mcp.json"),s.join(e,".vscode","mcp.json"),s.join(y.homedir(),".claude.json"),s.join(y.homedir(),".cursor","mcp.json"),s.join(y.homedir(),"Library","Application Support","Claude","claude_desktop_config.json"),s.join(y.homedir(),"AppData","Roaming","Claude","claude_desktop_config.json")];for(const o of n)if(c.existsSync(o))try{const t=JSON.parse(c.readFileSync(o,"utf8")),r=t.mcpServers||t.mcp_servers||t.servers||{};if(Object.keys(r).some(i=>i.toLowerCase().includes("inferno")))return a(`MCP server configured in ${s.basename(o)}`)}catch{}return l("MCP server not configured","infernoflow setup --yes (adds to Cursor/Claude/Copilot config)")}function q(){const e=[];for(const[n,o]of[["~/.claude.json",A()],["claude_desktop_config.json",I()]])try{const t=JSON.parse(c.readFileSync(o,"utf8"))?.mcpServers?.infernoflow;t&&M(t)&&e.push(`${n} \u2192 ${(t.env||{}).INFERNOFLOW_PROJECT_DIR||"?"}`)}catch{}return e.length?l(`User-level MCP entry pins every project to one repo: ${e.join("; ")}`,"infernoflow setup --yes (registers per project and removes the pinned entry)"):a("No user-level infernoflow MCP entry pinning all projects")}function B(e){const n=[],o=(()=>{try{return c.readFileSync(C(new URL("../../templates/cursor/inferno-mcp-server.mjs",import.meta.url)),"utf8")}catch{return null}})(),t=r=>r.replace(/\r\n/g,`
2
+ `);for(const r of[s.join(".cursor","inferno-mcp-server.mjs"),"inferno-mcp-server.mjs"])try{const i=c.readFileSync(s.join(e,r),"utf8");/serverInfo: \{ name: "infernoflow"|\[infernoflow MCP\]/.test(i)&&o&&t(i)!==t(o)&&n.push(r)}catch{}for(const r of[s.join(".claude","hooks","log-frustration.mjs"),s.join(".cursor","hooks","inferno-session-draft.mjs")])try{c.readFileSync(s.join(e,r),"utf8").includes(_)||n.push(r)}catch{}return n.length?l(`Outdated (vulnerable) copies: ${n.map(r=>r.split(s.sep).join("/")).join(", ")}`,"infernoflow setup --yes"):a("Generated MCP server / hook copies are current")}function Q(e){const n=s.join(e,".ai-memory");if(!c.existsSync(n))return a("No memory store \u2014 nothing to scan");const o=[],t=r=>{for(const i of c.readdirSync(r,{withFileTypes:!0})){const d=s.join(r,i.name);if(i.isDirectory()){t(d);continue}if(/\.(jsonl|json|md)$/.test(i.name)&&!(i.name==="details.local.jsonl"||i.name==="global.jsonl"))try{const u=E(c.readFileSync(d,"utf8")).kinds;u.length&&o.push(`${s.relative(e,d).split(s.sep).join("/")} (${u.join(", ")})`)}catch{}}};try{t(n)}catch{}return o.length?l(`Possible secrets in memory files: ${o.join("; ")}`,"Remove them (infernoflow forget <id>); if already pushed, rotate the secret \u2014 it is in git history"):a("No secrets found in memory files")}function Z(e){try{const n=JSON.parse(c.readFileSync(s.join(e,".ai-memory","amp.json"),"utf8"));if(n&&n.globalDir)return l(`.ai-memory/amp.json sets globalDir (${n.globalDir}) \u2014 ignored: a committed file can't decide where personal memory goes`,`infernoflow sync set ${n.globalDir} then remove globalDir from amp.json`)}catch{}return a("Personal memory location is set only in your personal config")}function ee(e){const n=s.join(e,"inferno","integrations.json");if(!c.existsSync(n))return a("No API keys stored in the project");let o=!1;try{o=Object.values(JSON.parse(c.readFileSync(n,"utf8"))||{}).some(r=>r&&typeof r=="object"&&r.apiKey)}catch{}let t=!1;try{$("git ls-files --error-unmatch -- inferno/integrations.json",{cwd:e,stdio:"ignore"}),t=!0}catch{}return o&&t?l("inferno/integrations.json holds an API key AND is tracked by git","Rotate the key with the provider, then run: infernoflow ai status (moves keys out of the project)"):o?l("inferno/integrations.json holds an API key","infernoflow ai status (moves it to ~/.infernoflow/ai-credentials.json)"):t?l("inferno/integrations.json is tracked by git","git rm --cached inferno/integrations.json"):a("No API keys stored in the project")}function ne(e,n){const o=s.join(e,".ai-memory",".mcp-runtime.json");if(!c.existsSync(o))return a("MCP runtime stamp not present yet \u2014 start your AI tool to write one");let t;try{t=JSON.parse(c.readFileSync(o,"utf8"))}catch{return l(".mcp-runtime.json present but unreadable","Delete .ai-memory/.mcp-runtime.json and restart your AI tool")}return!t||typeof t.version!="string"?l(".mcp-runtime.json malformed","Delete it; the next MCP boot will rewrite it"):t.version===n?a(`MCP runtime v${t.version} matches CLI`):n.startsWith("0.0.0")||t.version.startsWith("0.0.0")?a(`MCP runtime v${t.version} (dev/source) \u2014 skipping version-skew check`):l(`MCP server is running v${t.version} but CLI is v${n} \u2014 restart your AI tool to load the new code.`,"Quit and reopen Cursor / Claude Code / VS Code (the long-running MCP process keeps the old code in memory until restart)")}function oe(e){const n=G(T(e));if(!n.includeProtocol)return a("Memory-protocol injection disabled in config (injection.includeProtocol=false)");const o=n.targets,t=[];for(const r of o)try{const i=c.readFileSync(s.join(e,String(r)),"utf8");i.includes("amp_write")&&/Memory protocol/i.test(i)&&t.push(r)}catch{}return t.length?a(`Memory protocol in ${t.length}/${o.length} configured rule file(s) \u2014 AI knows when to auto-capture`):l("Memory protocol missing from rule files \u2014 the AI won't auto-capture gotchas","infernoflow refresh (writes the amp_write capture protocol into your rule files)")}function te(e){const n=F(e),o=Object.entries(n).filter(([,t])=>t).map(([t])=>t);return o.length?a(`AI provider${o.length!==1?"s":""}: ${o.join(", ")}`):l("No AI provider configured",`Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY, or OPENROUTER_API_KEY
2
3
  Or install Ollama (ollama.com) for free local AI
3
- Or use VS Code with GitHub Copilot (zero config)`)}async function Y(){return new Promise(e=>{const n=A.get({hostname:"localhost",port:11434,path:"/api/tags",timeout:1500},o=>{e(a("Ollama running on localhost:11434"))});n.on("error",()=>e({status:"info",message:"Ollama not running (optional)",fix:"ollama serve",detail:null})),n.on("timeout",()=>{n.destroy(),e({status:"info",message:"Ollama not running (optional)",fix:null,detail:null})})})}function z(){const e=s.join(y.homedir(),".infernoflow","credentials.json");if(!c.existsSync(e))return{status:"info",message:"Not logged in to cloud (optional)",fix:"infernoflow login",detail:null};try{const n=JSON.parse(c.readFileSync(e,"utf8")),o=n.user?.login||n.user?.name||n.user?.email||"unknown";if(n.mode==="supabase"&&n.access_token){if(n.expires_at){const t=new Date(n.expires_at).getTime();if(Date.now()>t)return f(`JWT expired for ${o} \u2014 refresh on next log will retry`,"infernoflow login")}return a(`Authenticated as ${o} (Supabase JWT \u2014 auth.uid() writes)`)}return n.mode==="device-flow"&&n.github_access_token?{status:"info",message:`Identity-only as ${o} (device flow \u2014 anon-mode writes)`,fix:"infernoflow login (without --device-flow, for full auth)",detail:null}:n.access_token?f(`Legacy login for ${o} \u2014 re-run for authenticated cloud writes`,"infernoflow logout && infernoflow login"):{status:"info",message:"Credentials file present but no recognised token",fix:"infernoflow logout && infernoflow login",detail:null}}catch{return f("Credentials file unreadable","infernoflow logout && infernoflow login")}}function q(){try{const e=P(import.meta.url),n=s.resolve(s.dirname(e),"..","..","bin","infernoflow.mjs");if(!c.existsSync(n))return{status:"info",message:"bin/infernoflow.mjs not found from doctor location",fix:null,detail:null};const t=[...c.readFileSync(n,"utf8").matchAll(/import\("\.\.\/lib\/(commands\/[^"]+|telemetry\.mjs)"\)/g)],r=[],i=s.resolve(s.dirname(n),"..");for(const d of t){const m=d[1],p=s.join(i,"lib",m);c.existsSync(p)||r.push(m)}return r.length?h(`${r.length} routed command(s) missing module files: ${r.slice(0,3).join(", ")}${r.length>3?"\u2026":""}`,"Restore the missing files or remove their entries from bin/infernoflow.mjs"):a(`All ${t.length} routed commands resolve to real files`)}catch(e){return{status:"info",message:`Router integrity check skipped: ${e.message}`,fix:null,detail:null}}}function B(e){const n=s.join(e,"package.json");if(!c.existsSync(n))return a("No package.json to audit");let o;try{o=JSON.parse(c.readFileSync(n,"utf8"))}catch{return{status:"info",message:"package.json unreadable; skipping audit",detail:null,fix:null}}const t=o.scripts||{},r=new Set(["log","ask","switch","recap","status","refresh","forget","prune","bookmark","init","setup","doctor","context","install-cursor-hooks","install-vscode-copilot-hooks","generate-skills","ai","telemetry","uninstall","check","sync","amp","commands"]),i=[];for(const[m,p]of Object.entries(t)){const x=/\binfernoflow\s+([a-z][a-z0-9-]*)/.exec(String(p));if(!x)continue;const u=x[1];r.has(u)||i.push({scriptName:m,verb:u})}if(i.length===0)return a("npm scripts use current command surface");const d=i.map(m=>`${m.scriptName} \u2192 infernoflow ${m.verb}`).join(", ");return f(`package.json references ${i.length} deprecated command(s): ${d}`,"Edit package.json scripts to use the current surface (run `infernoflow --help` to list verbs)")}function Q(e){const n=s.join(e,".gitignore");if(!c.existsSync(n))return{status:"info",message:".gitignore not found",fix:null,detail:null};const o=c.readFileSync(n,"utf8");return/^(?:\*\*\/)?node_modules\/?$/m.test(o)?a(".gitignore excludes node_modules"):f(".gitignore does not exclude node_modules","Add 'node_modules/' (and '**/node_modules/') to .gitignore")}function Z(e,n){const o=e.filter(r=>r.status==="warn"&&r.fix),t=[];for(const r of o){const i=r.fix;if(i.startsWith("infernoflow ")){const d=i.slice(12).split(" ");v("infernoflow",d,{cwd:n,encoding:"utf8",timeout:3e4}).status===0&&t.push(r.label)}}return t}function ee(e){return e==="pass"?w("\u2714"):e==="warn"?S("\u26A0"):e==="fail"?$("\u2717"):k("\xB7")}function oe(e,n){const o={pass:0,warn:0,fail:0,info:0,error:0};for(const i of e)o[i.status]=(o[i.status]||0)+1;console.log(),console.log(` ${C("\u{1F525} infernoflow doctor")}`),console.log();const t=Math.max(...e.map(i=>i.label.length))+2;for(const i of e)console.log(` ${ee(i.status)} ${C(i.label.padEnd(t))} ${i.message}`),i.detail&&console.log(` ${" ".repeat(t)} ${k(i.detail)}`),i.fix&&(i.status==="warn"||i.status==="fail")&&console.log(` ${" ".repeat(t)} ${b("fix:")} ${k(i.fix)}`);console.log();const r=o.fail>0?$("issues found"):o.warn>0?S("warnings"):w("all good");console.log(` ${r} \u2014 ${w(String(o.pass))} pass \xB7 ${S(String(o.warn))} warn \xB7 ${$(String(o.fail))} fail (${n}ms)`),console.log(),(o.warn>0||o.fail>0)&&(console.log(` Run ${b("infernoflow doctor --fix")} to auto-fix warnings`),console.log())}async function ne(e){const n=e.slice(1),o=n.includes("--json"),t=n.includes("--fix"),r=process.cwd(),i=Date.now();let d="0.0.0-unknown";try{const{fileURLToPath:u}=await import("node:url"),g=s.dirname(u(import.meta.url)),N=s.join(g,"..","..","package.json");d=JSON.parse(c.readFileSync(N,"utf8")).version||d}catch{}const m=[l("Node.js version",()=>T()),l("infernoflow CLI",()=>D()),l("Git repository",()=>G(r)),l("inferno/ directory",()=>F(r)),l("Contract / mode",()=>L(r)),l("Scenarios",()=>R(r)),l("Changelog",()=>J(r)),l("CONTEXT.md",()=>H(r)),l("Git hooks",()=>W(r)),l("Auto-capture protocol",()=>U(r)),l("MCP server",()=>X(r)),l("MCP runtime version",()=>K(r,d)),l("AI providers",()=>V(r)),l("Cloud sync",()=>z()),l(".gitignore",()=>Q(r)),l("Router integrity",()=>q()),l("npm scripts",()=>B(r)),await Y().then(u=>({label:"Ollama (local AI)",...u}))],p=Date.now()-i;if(t){const u=Z(m,r);if(u.length)return o||(console.log(),u.forEach(g=>console.log(` ${w("\u2714")} Fixed: ${g}`)),console.log()),ne(o?["doctor","--json"]:["doctor"])}if(o){const u={pass:0,warn:0,fail:0,info:0};m.forEach(g=>u[g.status]=(u[g.status]||0)+1),console.log(JSON.stringify({ok:u.fail===0,counts:u,results:m,elapsed:p}));return}oe(m,p),m.some(u=>u.status==="fail")&&process.exit(1)}export{ne as doctorCommand};
4
+ Or use VS Code with GitHub Copilot (zero config)`)}async function re(){return new Promise(e=>{const n=D.get({hostname:"localhost",port:11434,path:"/api/tags",timeout:1500},o=>{e(a("Ollama running on localhost:11434"))});n.on("error",()=>e({status:"info",message:"Ollama not running (optional)",fix:"ollama serve",detail:null})),n.on("timeout",()=>{n.destroy(),e({status:"info",message:"Ollama not running (optional)",fix:null,detail:null})})})}function ie(){const e=s.join(y.homedir(),".infernoflow","credentials.json");if(!c.existsSync(e))return{status:"info",message:"Not logged in to cloud (optional)",fix:"infernoflow login",detail:null};try{const n=JSON.parse(c.readFileSync(e,"utf8")),o=n.user?.login||n.user?.name||n.user?.email||"unknown";if(n.mode==="supabase"&&n.access_token){if(n.expires_at){const t=new Date(n.expires_at).getTime();if(Date.now()>t)return l(`JWT expired for ${o} \u2014 refresh on next log will retry`,"infernoflow login")}return a(`Authenticated as ${o} (Supabase JWT \u2014 auth.uid() writes)`)}return n.mode==="device-flow"&&n.github_access_token?{status:"info",message:`Identity-only as ${o} (device flow \u2014 anon-mode writes)`,fix:"infernoflow login (without --device-flow, for full auth)",detail:null}:n.access_token?l(`Legacy login for ${o} \u2014 re-run for authenticated cloud writes`,"infernoflow logout && infernoflow login"):{status:"info",message:"Credentials file present but no recognised token",fix:"infernoflow logout && infernoflow login",detail:null}}catch{return l("Credentials file unreadable","infernoflow logout && infernoflow login")}}function se(){try{const e=C(import.meta.url),n=s.resolve(s.dirname(e),"..","..","bin","infernoflow.mjs");if(!c.existsSync(n))return{status:"info",message:"bin/infernoflow.mjs not found from doctor location",fix:null,detail:null};const t=[...c.readFileSync(n,"utf8").matchAll(/import\("\.\.\/lib\/(commands\/[^"]+|telemetry\.mjs)"\)/g)],r=[],i=s.resolve(s.dirname(n),"..");for(const d of t){const u=d[1],p=s.join(i,"lib",u);c.existsSync(p)||r.push(u)}return r.length?h(`${r.length} routed command(s) missing module files: ${r.slice(0,3).join(", ")}${r.length>3?"\u2026":""}`,"Restore the missing files or remove their entries from bin/infernoflow.mjs"):a(`All ${t.length} routed commands resolve to real files`)}catch(e){return{status:"info",message:`Router integrity check skipped: ${e.message}`,fix:null,detail:null}}}function ce(e){const n=s.join(e,"package.json");if(!c.existsSync(n))return a("No package.json to audit");let o;try{o=JSON.parse(c.readFileSync(n,"utf8"))}catch{return{status:"info",message:"package.json unreadable; skipping audit",detail:null,fix:null}}const t=o.scripts||{},r=new Set(["log","ask","switch","recap","status","refresh","forget","prune","bookmark","init","setup","doctor","context","mcp","hook","resume","resolve","move","curate","transcript","install-cursor-hooks","install-vscode-copilot-hooks","generate-skills","ai","telemetry","uninstall","check","sync","amp","commands"]),i=[];for(const[u,p]of Object.entries(t)){const k=/\binfernoflow\s+([a-z][a-z0-9-]*)/.exec(String(p));if(!k)continue;const m=k[1];r.has(m)||i.push({scriptName:u,verb:m})}if(i.length===0)return a("npm scripts use current command surface");const d=i.map(u=>`${u.scriptName} \u2192 infernoflow ${u.verb}`).join(", ");return l(`package.json references ${i.length} deprecated command(s): ${d}`,"Edit package.json scripts to use the current surface (run `infernoflow --help` to list verbs)")}function ae(e){const n=s.join(e,".gitignore");if(!c.existsSync(n))return{status:"info",message:".gitignore not found",fix:null,detail:null};const o=c.readFileSync(n,"utf8");return/^(?:\*\*\/)?node_modules\/?$/m.test(o)?a(".gitignore excludes node_modules"):l(".gitignore does not exclude node_modules","Add 'node_modules/' (and '**/node_modules/') to .gitignore")}function le(e,n){const o=e.filter(r=>r.status==="warn"&&r.fix),t=[];for(const r of o){const i=r.fix;if(i.startsWith("infernoflow ")){const d=i.slice(12).split(" ");b("infernoflow",d,{cwd:n,encoding:"utf8",timeout:3e4}).status===0&&t.push(r.label)}}return t}function fe(e){return e==="pass"?j("\u2714"):e==="warn"?x("\u26A0"):e==="fail"?v("\u2717"):S("\xB7")}function ue(e,n){const o={pass:0,warn:0,fail:0,info:0,error:0};for(const i of e)o[i.status]=(o[i.status]||0)+1;console.log(),console.log(` ${N("\u{1F525} infernoflow doctor")}`),console.log();const t=Math.max(...e.map(i=>i.label.length))+2;for(const i of e)console.log(` ${fe(i.status)} ${N(i.label.padEnd(t))} ${i.message}`),i.detail&&console.log(` ${" ".repeat(t)} ${S(i.detail)}`),i.fix&&(i.status==="warn"||i.status==="fail")&&console.log(` ${" ".repeat(t)} ${P("fix:")} ${S(i.fix)}`);console.log();const r=o.fail>0?v("issues found"):o.warn>0?x("warnings"):j("all good");console.log(` ${r} \u2014 ${j(String(o.pass))} pass \xB7 ${x(String(o.warn))} warn \xB7 ${v(String(o.fail))} fail (${n}ms)`),console.log(),(o.warn>0||o.fail>0)&&(console.log(` Run ${P("infernoflow doctor --fix")} to auto-fix warnings`),console.log())}async function me(e){const n=e.slice(1),o=n.includes("--json"),t=n.includes("--fix"),r=process.cwd(),i=Date.now();let d="0.0.0-unknown";try{const{fileURLToPath:m}=await import("node:url"),g=s.dirname(m(import.meta.url)),O=s.join(g,"..","..","package.json");d=JSON.parse(c.readFileSync(O,"utf8")).version||d}catch{}const u=[f("Node.js version",()=>L()),f("infernoflow CLI",()=>J()),f("Git repository",()=>H(r)),f("inferno/ directory",()=>K(r)),f("Contract / mode",()=>U(r)),f("Scenarios",()=>W(r)),f("Changelog",()=>V(r)),f("CONTEXT.md",()=>X(r)),f("Git hooks",()=>Y(r)),f("Auto-capture protocol",()=>oe(r)),f("MCP server",()=>z(r)),f("MCP runtime version",()=>ne(r,d)),f("MCP registration",()=>q()),f("Generated copies",()=>B(r)),f("Secrets in memory",()=>Q(r)),f("API keys",()=>ee(r)),f("Personal memory path",()=>Z(r)),f("AI providers",()=>te(r)),f("Cloud sync",()=>ie()),f(".gitignore",()=>ae(r)),f("Router integrity",()=>se()),f("npm scripts",()=>ce(r)),await re().then(m=>({label:"Ollama (local AI)",...m}))],p=Date.now()-i;if(t){const m=le(u,r);if(m.length)return o||(console.log(),m.forEach(g=>console.log(` ${j("\u2714")} Fixed: ${g}`)),console.log()),me(o?["doctor","--json"]:["doctor"])}if(o){const m={pass:0,warn:0,fail:0,info:0};u.forEach(g=>m[g.status]=(m[g.status]||0)+1),console.log(JSON.stringify({ok:m.fail===0,counts:m,results:u,elapsed:p}));return}ue(u,p),u.some(m=>m.status==="fail")&&process.exit(1)}export{me as doctorCommand};
@@ -0,0 +1,12 @@
1
+ import*as c from"node:fs";import*as f from"node:path";import{execFileSync as S}from"node:child_process";import{appendEntry as w,readEntries as x,deleteEntry as A,ampPaths as O}from"../amp/io.mjs";import{buildSessionContext as _,openAttempts as E,visibleEntries as j}from"../memoryView.mjs";import{findProjectRoot as D}from"../projectRoot.mjs";const p="auto-handoff",$=5;function m(){try{if(process.stdin.isTTY)return{};const t=c.readFileSync(0,"utf8");return t.trim()?JSON.parse(t):{}}catch{return{}}}function y(){const t=process.env.CLAUDE_PROJECT_DIR||process.cwd();try{return D(t)}catch{return t}}function g(t){if(!t||typeof t!="string")return null;let e;try{const o=c.statSync(t),r=c.openSync(t,"r"),s=Math.min(o.size,2e6),i=Buffer.alloc(s);c.readSync(r,i,0,s,o.size-s),c.closeSync(r),e=i.toString("utf8")}catch{return null}const a=e.split(`
2
+ `).reverse();for(const o of a){let r;try{r=JSON.parse(o)}catch{continue}if(r.type!=="user"||!r.message)continue;const s=r.message.content,i=typeof s=="string"?s:Array.isArray(s)?s.filter(u=>u&&u.type==="text").map(u=>u.text).join(" "):"",l=String(i||"").replace(/\s+/g," ").trim();if(l&&!l.startsWith("<"))return l.slice(0,400)}return null}function b(t){try{return S("git",["status","--porcelain"],{cwd:t,encoding:"utf8",stdio:["ignore","pipe","ignore"],windowsHide:!0,timeout:5e3}).split(`
3
+ `).map(e=>e.trimEnd()).filter(Boolean)}catch{return[]}}function T(){const t=y();if(c.existsSync(f.join(t,".ai-memory"))){m();try{process.stdout.write(_(t)+`
4
+ `)}catch{}}}function k(t=[]){const e=y();if(!c.existsSync(f.join(e,".ai-memory")))return;const a=n=>{const d=t.indexOf(n);return d!==-1?t[d+1]:void 0},o=a("--transcript")||a("--reason")?{transcript_path:a("--transcript"),reason:a("--reason")}:m(),r=g(o.transcript_path),s=b(e);if(!r&&!s.length)return;const i=E(j(e)).slice(-5),l=new Date().toISOString().slice(0,16).replace("T"," "),u=[`# Session ended \u2014 ${l}${o.reason?` (${o.reason})`:""}`,"",r?`**Last request:** ${r}`:null,s.length?`
5
+ **Uncommitted changes (${s.length}):**
6
+ ${s.slice(0,25).map(n=>"- `"+n+"`").join(`
7
+ `)}`:null,i.length?`
8
+ **Open dead ends:**
9
+ ${i.map(n=>"- "+(n.summary||n.msg)).join(`
10
+ `)}`:null].filter(Boolean).join(`
11
+ `);w(e,{ts:new Date().toISOString(),type:"note",summary:`Session ended \u2014 ${l}`,tags:["bookmark",p],agent:"hook",source:"session-end",detail:u,detailLocal:!0,localOnly:!0});const h=x(e).filter(n=>Array.isArray(n.tags)&&n.tags.includes(p)).sort((n,d)=>(Number(d.ts)||Date.parse(d.ts)||0)-(Number(n.ts)||Date.parse(n.ts)||0));for(const n of h.slice($))try{A(e,n.id)}catch{}}async function q(t){const e=(t||[])[1];try{if(e==="session-start")return T();if(e==="session-end")return k(t)}catch{}["session-start","session-end"].includes(e)||(process.stderr.write(`usage: infernoflow hook session-start|session-end (called by AI-tool hooks)
12
+ `),process.exitCode=1)}export{O as _ampPaths,g as _lastUserRequest,q as hookCommand};
@@ -1,15 +1,16 @@
1
- import*as c from"node:fs";import*as E from"node:path";import"node:os";import{bold as j,cyan as I,gray as o,green as b,yellow as P,red as S,orange as U}from"../ui/output.mjs";import{ampPaths as V,appendEntry as W,readEntries as q,readConfig as G,resolveRotationSettings as J,pruneEntries as Y}from"../amp/io.mjs";import{refreshRuleFilesFromMemory as R}from"../ruleFiles.mjs";function M(){return V(process.cwd())}const _=["gotcha","decision","attempt","note","detection","pattern","preference","theme","handoff","error"],T=["worked","failed","partial","unknown"];function X(){return q(process.cwd())}function z(t,{auto:l=!1,quiet:n=!1}={}){const r=process.cwd(),h=E.join(r,".ai-memory"),g=E.join(r,"inferno");if(l&&!c.existsSync(h)&&!c.existsSync(g))return!1;!c.existsSync(h)&&!c.existsSync(g)&&(n||console.error(S(` \u2718 no .ai-memory/ or inferno/ \u2014 run: infernoflow init
2
- `)),process.exit(1)),W(r,t);try{J(G(r)).auto&&Y(r)}catch{}try{R(r)}catch{}return!0}function H(){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"}function K(t,l){const n=new Date(t.ts).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}),r=t.type||"note",h=r==="gotcha"?P:r==="decision"?I:r==="theme"?U:r==="preference"?I:r==="attempt"?o:r==="error"?S:d=>d,g=t.result?` [${t.result}]`:"",m=t.agent&&t.agent!=="human"?o(` (${t.agent})`):"";return` ${o(String(l+1).padStart(3))} ${o(n)} ${h(r)}${g} ${t.summary}${m}`}async function re(t){const l=e=>t.includes(e),n=(e,a)=>{const i=t.indexOf(e);return i!==-1&&t[i+1]?t[i+1]:a};l("--no-color")&&(process.env.NO_COLOR="1");const r=l("--show"),h=l("--clear"),g=l("--json"),m=l("--auto"),d=l("--quiet"),w=n("--source",null);if(r||g){const e=X(),a=t[t.indexOf("--show")+1],i=a&&/^\d+$/.test(a)?parseInt(a):20,s=e.slice(-i);if(g){console.log(JSON.stringify(s,null,2));return}if(console.log(`
3
- `+j("\u{1F525} infernoflow \u2014 session memory")),console.log(" "+"\u2500".repeat(50)),!s.length){console.log(o(`
1
+ import{CLI_TYPES as G,RESULTS as T,AGENT_TYPES as V,LEGACY_CLI_TYPES as W}from"../schema.mjs";import{redactSecrets as I}from"../security/redact.mjs";import{describeStore as q}from"../amp/io.mjs";import*as f from"node:fs";import*as O from"node:path";import"node:os";import{bold as _,cyan as N,gray as o,green as b,yellow as R,red as S,orange as J}from"../ui/output.mjs";import{ampPaths as M,appendEntry as X,readEntries as z,readConfig as H,resolveRotationSettings as K,pruneEntries as Q}from"../amp/io.mjs";import{refreshRuleFilesFromMemory as C}from"../ruleFiles.mjs";function Z(){return M(process.cwd())}const P=G,k=T;function ee(){return z(process.cwd())}function ne(t,{auto:c=!1,quiet:n=!1}={}){const s=process.cwd(),m=O.join(s,".ai-memory"),g=O.join(s,"inferno");if(c&&!f.existsSync(m)&&!f.existsSync(g))return!1;!f.existsSync(m)&&!f.existsSync(g)&&(n||console.error(S(` \u2718 no .ai-memory/ or inferno/ \u2014 run: infernoflow init
2
+ `)),process.exit(1)),X(s,t);try{K(H(s)).auto&&Q(s)}catch{}try{C(s)}catch{}return!0}function oe(){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"}function te(t,c){const n=new Date(t.ts).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}),s=t.type||"note",m=s==="gotcha"?R:s==="decision"?N:s==="theme"?J:s==="preference"?N:s==="attempt"?o:s==="error"?S:d=>d,g=t.result?` [${t.result}]`:"",h=t.agent&&t.agent!=="human"?o(` (${t.agent})`):"";return` ${o(String(c+1).padStart(3))} ${o(n)} ${m(s)}${g} ${t.summary}${h}`}async function de(t){const c=e=>t.includes(e),n=(e,u)=>{const a=t.indexOf(e);return a!==-1&&t[a+1]?t[a+1]:u};c("--no-color")&&(process.env.NO_COLOR="1");const s=c("--show"),m=c("--clear"),g=c("--json"),h=c("--auto"),d=c("--quiet"),w=n("--source",null);if(s||g){const e=ee(),u=t[t.indexOf("--show")+1],a=u&&/^\d+$/.test(u)?parseInt(u):20,l=e.slice(-a);if(g){console.log(JSON.stringify(l,null,2));return}if(console.log(`
3
+ `+_("\u{1F525} infernoflow \u2014 session memory")),console.log(" "+o(q(process.cwd()))),console.log(" "+"\u2500".repeat(50)),!l.length){console.log(o(`
4
4
  No entries yet. Start logging with: infernoflow log "<what happened>"
5
- `));return}console.log(o(` Showing last ${s.length} of ${e.length} entries
6
- `)),s.forEach((u,O)=>console.log(K(u,e.length-s.length+O))),console.log();return}if(h){const e=M(),a=Date.now(),i=new Set([e.sessions,e.currentBranchFile,e.defaultBranchFile].filter(Boolean));try{if(e.branchesDir&&c.existsSync(e.branchesDir))for(const u of c.readdirSync(e.branchesDir))u.endsWith(".jsonl")&&i.add(E.join(e.branchesDir,u))}catch{}const s=[];for(const u of i){if(!c.existsSync(u))continue;const O=u.replace(/\.jsonl$/,`-archive-${a}.jsonl`);try{c.renameSync(u,O),s.push(E.basename(O))}catch{}}if(!s.length){console.log(o(` Nothing to clear.
7
- `));return}console.log(b(` \u2714 Session log archived (${s.length} file${s.length===1?"":"s"}) \u2014 entries cleared from active memory.
8
- `));return}const k=new Set([n("--type",""),n("--result",""),n("--agent",""),n("--source",""),n("--file",""),n("--line",""),n("--tags",""),n("--detail",""),n("--detail-file","")].filter(Boolean)),v=t.slice(1).filter(e=>!e.startsWith("--")&&!k.has(e)).join(" ").trim();if(!v){console.log(`
9
- `+j("\u{1F525} infernoflow log")+` \u2014 append to session memory
10
- `),console.log(o(" Usage:")),console.log(o(' infernoflow log "what happened"')),console.log(o(' infernoflow log "tried X, failed because Y" --type attempt --result failed')),console.log(o(' infernoflow log "always use multipart/form-data" --type gotcha')),console.log(o(' infernoflow log "switched to dark mode" --type theme')),console.log(o(" infernoflow log --show Print last 20 entries")),console.log(o(" infernoflow log --json Print as JSON")),console.log(),console.log(o(" Types: note \xB7 attempt \xB7 decision \xB7 gotcha \xB7 preference \xB7 theme \xB7 handoff \xB7 error")),console.log(o(" Results: worked \xB7 failed \xB7 partial \xB7 unknown")),console.log(o(` Auto-capture: --auto (silent skip if no inferno/) \xB7 --quiet \xB7 --source <name>
11
- `));return}const y=n("--type","note"),p=n("--result",null),C=n("--agent",H()),A=n("--file",null),F=n("--line",null),D=n("--tags",null);_.includes(y)||(d||console.error(S(` \u2718 Invalid type: ${y}. Valid: ${_.join(", ")}
12
- `)),process.exit(1)),p&&!T.includes(p)&&(d||console.error(S(` \u2718 Invalid result: ${p}. Valid: ${T.join(", ")}
13
- `)),process.exit(1));const L=F&&/^\d+$/.test(F)?parseInt(F,10):null,N=D?D.split(",").map(e=>e.trim()).filter(Boolean):null,x=n("--detail",null),$=n("--detail-file",null);let f=null;if($)try{f=$==="-"?c.readFileSync(0,"utf8"):c.readFileSync($,"utf8")}catch(e){d||console.error(S(` \u2718 Could not read --detail-file ${$}: ${e.message}
14
- `)),process.exit(1)}else x&&(f=x);const B={ts:new Date().toISOString(),agent:C,type:y,summary:v,...p?{result:p}:{},...w?{source:w}:{},...A?{file:A}:{},...L?{line:L}:{},...N&&N.length?{tags:N}:{},...f&&f.trim()?{detail:f}:{},...m?{auto:!0}:{}};if(z(B,{auto:m,quiet:d})){if(t.includes("--refresh-rules"))try{R(process.cwd())}catch{}if(!d){const e=y!=="note"?I(` [${y}]`):"",a=p?o(` \u2192 ${p}`):"",i=w?o(` (via ${w})`):"",s=f&&f.trim()?o(` (+${f.length}-char detail)`):"";console.log(b(` \u2714 Logged${e}${a}${i}: `)+v+s+`
15
- `)}}}export{re as logCommand};
5
+ `));return}console.log(o(` Showing last ${l.length} of ${e.length} entries
6
+ `)),l.forEach((i,E)=>console.log(te(i,e.length-l.length+E))),console.log();return}if(m){const e=Z(),u=Date.now(),a=new Set([e.sessions,e.currentBranchFile,e.defaultBranchFile].filter(Boolean));try{if(e.branchesDir&&f.existsSync(e.branchesDir))for(const i of f.readdirSync(e.branchesDir))i.endsWith(".jsonl")&&a.add(O.join(e.branchesDir,i))}catch{}const l=[];for(const i of a){if(!f.existsSync(i))continue;const E=i.replace(/\.jsonl$/,`-archive-${u}.jsonl`);try{f.renameSync(i,E),l.push(O.basename(E))}catch{}}if(!l.length){console.log(o(` Nothing to clear.
7
+ `));return}console.log(b(` \u2714 Session log archived (${l.length} file${l.length===1?"":"s"}) \u2014 entries cleared from active memory.
8
+ `));return}const B=new Set([n("--type",""),n("--result",""),n("--agent",""),n("--source",""),n("--file",""),n("--line",""),n("--tags",""),n("--detail",""),n("--detail-file","")].filter(Boolean)),v=t.slice(1).filter(e=>!e.startsWith("--")&&!B.has(e)).join(" ").trim();if(!v){console.log(`
9
+ `+_("\u{1F525} infernoflow log")+` \u2014 append to session memory
10
+ `),console.log(o(" Usage:")),console.log(o(' infernoflow log "what happened"')),console.log(o(' infernoflow log "tried X, failed because Y" --type attempt --result failed')),console.log(o(' infernoflow log "always use multipart/form-data" --type gotcha')),console.log(o(' infernoflow log "switched to dark mode" --type theme')),console.log(o(" infernoflow log --show Print last 20 entries")),console.log(o(" infernoflow log --json Print as JSON")),console.log(),console.log(o(" Types: "+V.join(" \xB7 ")+" (legacy: "+W.join(" \xB7 ")+")")),console.log(o(" Results: "+T.join(" \xB7 "))),console.log(o(` Auto-capture: --auto (silent skip if no .ai-memory/) \xB7 --quiet \xB7 --source <name>
11
+ `));return}const y=n("--type","note"),p=n("--result",null),U=n("--agent",oe()),j=n("--file",null),L=n("--line",null),A=n("--tags",null);P.includes(y)||(d||console.error(S(` \u2718 Invalid type: ${y}. Valid: ${P.join(", ")}
12
+ `)),process.exit(1)),p&&!k.includes(p)&&(d||console.error(S(` \u2718 Invalid result: ${p}. Valid: ${k.join(", ")}
13
+ `)),process.exit(1));const x=L&&/^\d+$/.test(L)?parseInt(L,10):null,F=A?A.split(",").map(e=>e.trim()).filter(Boolean):null,D=n("--detail",null),$=n("--detail-file",null);let r=null;if($)try{r=$==="-"?f.readFileSync(0,"utf8"):f.readFileSync($,"utf8")}catch(e){d||console.error(S(` \u2718 Could not read --detail-file ${$}: ${e.message}
14
+ `)),process.exit(1)}else D&&(r=D);const Y={ts:new Date().toISOString(),agent:U,type:y,summary:v,...p?{result:p}:{},...w?{source:w}:{},...j?{file:j}:{},...x?{line:x}:{},...F&&F.length?{tags:F}:{},...r&&r.trim()?{detail:r}:{},...h?{auto:!0}:{}};if(ne(Y,{auto:h,quiet:d})){if(t.includes("--refresh-rules"))try{C(process.cwd())}catch{}if(!d){const e=y!=="note"?N(` [${y}]`):"",u=p?o(` \u2192 ${p}`):"",a=w?o(` (via ${w})`):"",l=r&&r.trim()?o(` (+${r.length}-char detail)`):"",i=I(v);console.log(b(` \u2714 Logged${e}${u}${a}: `)+i.text+l+`
15
+ `),(i.kinds.length||r&&I(r).kinds.length)&&console.log(R(` \u26A0 secrets were redacted before saving (${[...new Set([...i.kinds,...r?I(r).kinds:[]])].join(", ")})`)+`
16
+ `)}}}export{de as logCommand};
@@ -0,0 +1 @@
1
+ import*as r from"node:path";import{fileURLToPath as o,pathToFileURL as m}from"node:url";const t=r.dirname(o(import.meta.url));async function s(a){const e=r.resolve(t,"../../templates/cursor/inferno-mcp-server.mjs");await import(m(e).href)}export{s as mcpCommand};
@@ -0,0 +1,14 @@
1
+ import*as E from"node:fs";import*as n from"node:path";import{readEntries as v,readDetail as S,appendEntry as b,deleteEntry as L}from"../amp/io.mjs";import{findProjectRoot as w}from"../projectRoot.mjs";import{bold as R,cyan as W,gray as s,green as A,red as d,yellow as D}from"../ui/output.mjs";async function N(i=[]){const l=process.cwd(),m=e=>{const o=i.indexOf(e);return o!==-1?i[o+1]:null},c=m("--to"),a=m("--query"),f=i.includes("--apply"),x=new Set([c,a].filter(Boolean)),p=i.slice(1).filter(e=>!e.startsWith("--")&&!x.has(e));if(!c||!a&&!p.length){console.error(d(`
2
+ \u2718 usage: infernoflow move <id-prefix\u2026> | --query "text" --to <project-dir> [--apply]
3
+ `)),process.exitCode=1;return}let t;try{t=w(n.resolve(c))}catch{t=n.resolve(c)}const y=w(l);if(!E.existsSync(n.join(t,".ai-memory"))){console.error(d(`
4
+ \u2718 ${t} has no .ai-memory/ \u2014 run infernoflow init there first
5
+ `)),process.exitCode=1;return}if(n.resolve(t)===n.resolve(y)){console.error(d(`
6
+ \u2718 --to is this project
7
+ `)),process.exitCode=1;return}const $=v(l),u=a?a.toLowerCase():null,r=$.filter(e=>e.id&&(p.some(o=>o.length>=6&&e.id.startsWith(o))||u&&`${e.summary||""} ${e.file||""} ${(e.tags||[]).join(" ")}`.toLowerCase().includes(u)));if(!r.length){console.log(s(`
8
+ Nothing matched.
9
+ `));return}const h=new Set(v(t).map(e=>e.id));console.log(`
10
+ `+R(`${f?"Moving":"Would move"} ${r.length} entr${r.length===1?"y":"ies"}`)+s(` \u2192 ${t}`));for(const e of r)console.log(" "+W(e.id.slice(0,14))+" "+(e.type||"note").padEnd(9)+" "+String(e.summary||"").slice(0,80)+(h.has(e.id)?s(" (already there)"):""));if(!f){console.log(`
11
+ `+D("Dry run.")+s(` Add --apply to move them.
12
+ `));return}let g=0;for(const e of r){if(!h.has(e.id)){let o=null;try{o=S(l,e)}catch{}const j=e.detailRef==="details.local.jsonl",{detailRef:k,...C}=e;b(t,{...C,id:e.id,ts:e.ts,meta:(({commit:B,...q})=>({...q,movedFrom:n.basename(y)}))(e.meta||{}),...o?{detail:o,detailLocal:j}:{}})}L(l,e.id),g++}console.log(`
13
+ `+A(`\u2714 moved ${g}`)+s(` \u2014 commit both repos' .ai-memory/ changes.
14
+ `))}export{N as moveCommand};
@@ -1,5 +1,5 @@
1
- import*as C from"node:fs";import*as I from"node:path";import{execSync as M}from"node:child_process";import{bold as b,cyan as y,gray as t,green as k,yellow as $,red as D}from"../ui/output.mjs";import{readEntries as R}from"../amp/io.mjs";const N="inferno",v=I.join(N,"contract.json");function L(n){try{return JSON.parse(C.readFileSync(n,"utf8"))}catch{return null}}function A(n,s){if(s){const g=s.match(/^(\d+)h$/i),f=s.match(/^(\d+)d$/i);if(g)return new Date(Date.now()-parseInt(g[1])*36e5);if(f)return new Date(Date.now()-parseInt(f[1])*864e5);const r=new Date(s);if(!isNaN(r.getTime()))return r}const o=new Date(Date.now()-864e5),e=[];for(const g of n)if(g.type==="handoff"){const f=new Date(g.ts||0);isNaN(f.getTime())||e.push(f)}if(e.length===0)return o;const l=300*1e3,u=e[e.length-1];if(Date.now()-u.getTime()<l){if(e.length>=2){const g=e[e.length-2];return g>o?g:o}return o}return u>o?u:o}function P(n){const s=process.cwd(),o=f=>{try{return M(f,{cwd:s,encoding:"utf8",timeout:5e3,stdio:["pipe","pipe","pipe"]}).trim()}catch{return""}},e=n.toISOString().slice(0,19),l=o("git diff --cached --name-only"),u=o("git diff --name-only"),a=o(`git log --since="${e}" --name-only --pretty=format:""`);return[...new Set([...l.split(`
2
- `),...u.split(`
3
- `),...a.split(`
4
- `)].map(f=>f.trim()).filter(Boolean))]}function _(n,s){const o=[{keywords:["auth","login","logout","session","jwt","token","password"],topic:"authentication"},{keywords:["stripe","payment","checkout","billing","subscription"],topic:"payments"},{keywords:["upload","file","s3","storage","bucket","cdn"],topic:"file handling"},{keywords:["email","sendgrid","ses","smtp","nodemailer","twilio"],topic:"notifications"},{keywords:["db","database","prisma","mongoose","postgres","mysql","migration"],topic:"database"},{keywords:["deploy","docker","ci","workflow","action","kubernetes"],topic:"deployment"},{keywords:["cache","redis","memcache"],topic:"caching"},{keywords:["test","spec","jest","vitest","cypress","playwright"],topic:"testing"},{keywords:["config","env",".env","environment","secret"],topic:"configuration"},{keywords:["api","route","endpoint","controller","handler"],topic:"API routes"},{keywords:["ui","component","style","css","tailwind","theme"],topic:"UI/styles"}],e=s.map(a=>(a.summary||"").toLowerCase()).join(" "),l=[],u=new Set;for(const a of o){if(u.has(a.topic))continue;const g=n.filter(r=>a.keywords.some(w=>r.toLowerCase().includes(w)));!g.length||a.keywords.some(r=>e.includes(r))||(u.add(a.topic),l.push({topic:a.topic,files:g.slice(0,3),suggestedType:"gotcha"}))}return l}function B(n){const s=new Set(n.map(l=>l.type));let o=0;const e=[];return n.length>0?(o+=20,e.push({ok:!0,label:`${n.length} entr${n.length!==1?"ies":"y"} logged`})):e.push({ok:!1,label:"nothing logged this session"}),s.has("gotcha")?(o+=35,e.push({ok:!0,label:"gotchas captured"})):e.push({ok:!1,label:"no gotchas (most valuable \u2014 log landmines!)"}),s.has("decision")?(o+=25,e.push({ok:!0,label:"decisions recorded"})):e.push({ok:!1,label:"no decisions recorded"}),s.has("attempt")&&(o+=10,e.push({ok:!0,label:"attempts tracked"})),s.has("preference")&&(o+=10,e.push({ok:!0,label:"preferences noted"})),{score:Math.min(o,100),checks:e}}function H(n){if(!n)return"";const s=new Date(n),o=Date.now()-s.getTime(),e=Math.floor(o/6e4);if(e<60)return`${e}m ago`;const l=Math.floor(o/36e5);return l<24?`${l}h ago`:`${Math.floor(o/864e5)}d ago`}const J={gotcha:"\u26A0",decision:"\u2713",attempt:"\u21BA",preference:"\u2666",theme:"\u25C8",note:"\xB7",error:"\u2717",handoff:"\u2192"},U={gotcha:$,decision:k,attempt:y,preference:y,theme:y,note:t,error:D,handoff:t};function G(n){const s=U[n.type]||t,o=J[n.type]||"\xB7",e=n.result?t(` [${n.result}]`):"",l=t(` (${H(n.ts)})`);console.log(` ${s(o+" "+(n.type||"note").padEnd(11))}${e}${l}`),console.log(` ${n.summary}`)}async function Q(n=[]){const s=n,o=s.includes("--json"),e=s.includes("--brief"),l=s.indexOf("--since"),u=l!==-1?s[l+1]:null,a=process.cwd();!C.existsSync(I.join(a,N))&&!C.existsSync(I.join(a,".ai-memory"))&&(o||console.error(D(` \u2718 not initialized \u2014 run: infernoflow init
5
- `)),process.exit(1));const g=R(a),f=A(g,u),r=g.filter(d=>new Date(d.ts||0)>f),w=P(f),m=_(w,r),{score:i,checks:O}=B(r),F=L(I.join(a,v));if(o){console.log(JSON.stringify({sessionStart:f.toISOString(),entries:r,changedFiles:w,unloggedTopics:m,health:{score:i,checks:O}},null,2));return}if(e){const d=i>=80?"A":i>=60?"B":i>=40?"C":"D",p=i>=60?k:i>=40?$:D;console.log(p(`Session health: ${d} (${i}/100)`)+t(` \u2014 ${r.length} entries logged`)),m.length&&console.log($(` ${m.length} topic${m.length!==1?"s":""} changed but not logged: `)+m.map(c=>c.topic).join(", "));return}const S=t(" "+"\u2500".repeat(52));console.log(),console.log(" "+b("\u{1F525} infernoflow recap")),F?.policyId&&console.log(t(` Project: ${F.policyId}`));const j=f.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"});if(console.log(t(` Session since: ${j}`)),console.log(S),console.log(),console.log(" "+b("Captured this session")),console.log(),r.length===0)console.log(t(" Nothing logged yet this session."));else{const d=["gotcha","decision","attempt","preference","theme","note","error"],p=new Map;for(const c of r){const h=c.type||"note";p.has(h)||p.set(h,[]),p.get(h).push(c)}for(const c of d){const h=p.get(c);if(h?.length)for(const x of h)console.log(),G(x)}}if(m.length>0){console.log(),console.log(S),console.log(),console.log(" "+b("Changed but not logged")+t(" (git diff since session start)")),console.log();for(const{topic:d,files:p}of m){console.log($(` ? ${d}`));for(const c of p)console.log(t(` ${c}`))}console.log(),console.log(t(" Any gotchas or decisions from these areas worth capturing?")),console.log(t(" Run: ")+y('infernoflow log "<what happened>" --type gotcha'))}else w.length>0&&(console.log(),console.log(S),console.log(),console.log(k(" \u2714 ")+t(`${w.length} changed files \u2014 all topics appear to be logged`)));console.log(),console.log(S),console.log(),console.log(" "+b("Session health")),console.log();const E=i>=80?"A":i>=60?"B":i>=40?"C":"D",T=i>=60?k:i>=40?$:D;console.log(` ${T(b(`${E}`))} ${T(`${i}/100`)}`),console.log();for(const{ok:d,label:p}of O){const c=d?k(" \u2714"):$(" \xB7");console.log(`${c} ${d?p:t(p)}`)}{const d=r.filter(h=>h.type==="gotcha").length,p=r.filter(h=>h.type==="decision").length,c=[];if(d===0?c.push(y('infernoflow log "..." --type gotcha')+t(" \u2014 adds 35 pts")):d<3&&i<80&&c.push(t(` ${3-d} more gotcha(s) would push you higher`)),p===0&&c.push(y('infernoflow log "..." --type decision')+t(" \u2014 adds 25 pts")),i>=60&&i<80&&c.push(t(" Almost B! One more entry gets you there.")),i>=80&&c.push(k(" Great session \u2014 your handoff will be excellent.")),c.length){console.log(),console.log(t(" How to improve:"));for(const h of c)console.log(" "+h)}}(r.length>0||m.length>0)&&(console.log(),console.log(S),console.log(),console.log(t(" Before your next session:")),console.log(t(" ")+y("infernoflow switch")+t(" \u2014 generate a handoff summary for the next AI agent")),console.log(t(" ")+y("infernoflow ask --recent")+t(" \u2014 review what's in memory before starting"))),console.log()}export{Q as recapCommand};
1
+ import{describeStore as T}from"../amp/io.mjs";import*as C from"node:fs";import*as I from"node:path";import{execSync as R}from"node:child_process";import{bold as b,cyan as y,gray as t,green as $,yellow as k,red as D}from"../ui/output.mjs";import{readEntries as v}from"../amp/io.mjs";const x="inferno",A=I.join(x,"contract.json");function L(o){try{return JSON.parse(C.readFileSync(o,"utf8"))}catch{return null}}function P(o,i){if(i){const l=i.match(/^(\d+)h$/i),s=i.match(/^(\d+)d$/i);if(l)return new Date(Date.now()-parseInt(l[1])*36e5);if(s)return new Date(Date.now()-parseInt(s[1])*864e5);const f=new Date(i);if(!isNaN(f.getTime()))return f}const n=new Date(Date.now()-864e5),e=[];for(const l of o)if(l.type==="handoff"){const s=new Date(l.ts||0);isNaN(s.getTime())||e.push(s)}if(e.length===0)return n;const a=300*1e3,h=e[e.length-1];if(Date.now()-h.getTime()<a){if(e.length>=2){const l=e[e.length-2];return l>n?l:n}return n}return h>n?h:n}function _(o){const i=process.cwd(),n=s=>{try{return R(s,{cwd:i,encoding:"utf8",timeout:5e3,stdio:["pipe","pipe","pipe"]}).trim()}catch{return""}},e=o.toISOString().slice(0,19),a=n("git diff --cached --name-only"),h=n("git diff --name-only"),c=n(`git log --since="${e}" --name-only --pretty=format:""`);return[...new Set([...a.split(`
2
+ `),...h.split(`
3
+ `),...c.split(`
4
+ `)].map(s=>s.trim()).filter(Boolean))]}function B(o,i){const n=[{keywords:["auth","login","logout","session","jwt","token","password"],topic:"authentication"},{keywords:["stripe","payment","checkout","billing","subscription"],topic:"payments"},{keywords:["upload","file","s3","storage","bucket","cdn"],topic:"file handling"},{keywords:["email","sendgrid","ses","smtp","nodemailer","twilio"],topic:"notifications"},{keywords:["db","database","prisma","mongoose","postgres","mysql","migration"],topic:"database"},{keywords:["deploy","docker","ci","workflow","action","kubernetes"],topic:"deployment"},{keywords:["cache","redis","memcache"],topic:"caching"},{keywords:["test","spec","jest","vitest","cypress","playwright"],topic:"testing"},{keywords:["config","env",".env","environment","secret"],topic:"configuration"},{keywords:["api","route","endpoint","controller","handler"],topic:"API routes"},{keywords:["ui","component","style","css","tailwind","theme"],topic:"UI/styles"}],e=i.map(c=>(c.summary||"").toLowerCase()).join(" "),a=[],h=new Set;for(const c of n){if(h.has(c.topic))continue;const l=o.filter(f=>c.keywords.some(w=>f.toLowerCase().includes(w)));!l.length||c.keywords.some(f=>e.includes(f))||(h.add(c.topic),a.push({topic:c.topic,files:l.slice(0,3),suggestedType:"gotcha"}))}return a}function H(o,i=[],n=[]){const e=[],a=new Set(o.map(s=>s.type)),h=o.filter(s=>s.source==="hook"&&/^User frustration:/.test(s.summary||"")),c=o.filter(s=>s.type==="attempt"&&s.source!=="hook");let l=100;return!i.length&&!h.length?e.push({ok:!0,label:o.length?`${o.length} entr${o.length!==1?"ies":"y"} logged`:"no code changes or failures this session \u2014 nothing needed logging"}):(i.length&&(n.length?(l-=Math.min(45,15*n.length),e.push({ok:!1,label:`${n.length} changed topic${n.length!==1?"s":""} not logged`})):e.push({ok:!0,label:"changed areas are covered by memory"}),o.length||(l-=25,e.push({ok:!1,label:`${i.length} file${i.length!==1?"s":""} changed but nothing logged`}))),h.length&&!c.length&&(l-=20,e.push({ok:!1,label:"frustration signals with no distilled dead end (log what was tried and why it failed)"}))),a.has("gotcha")&&e.push({ok:!0,label:"gotchas captured"}),a.has("decision")&&e.push({ok:!0,label:"decisions recorded"}),c.length&&e.push({ok:!0,label:"dead ends recorded"}),{score:Math.max(0,Math.min(l,100)),checks:e}}function U(o){if(!o)return"";const i=new Date(o),n=Date.now()-i.getTime(),e=Math.floor(n/6e4);if(e<60)return`${e}m ago`;const a=Math.floor(n/36e5);return a<24?`${a}h ago`:`${Math.floor(n/864e5)}d ago`}const J={gotcha:"\u26A0",decision:"\u2713",attempt:"\u21BA",preference:"\u2666",theme:"\u25C8",note:"\xB7",error:"\u2717",handoff:"\u2192"},G={gotcha:k,decision:$,attempt:y,preference:y,theme:y,note:t,error:D,handoff:t};function Y(o){const i=G[o.type]||t,n=J[o.type]||"\xB7",e=o.result?t(` [${o.result}]`):"",a=t(` (${U(o.ts)})`);console.log(` ${i(n+" "+(o.type||"note").padEnd(11))}${e}${a}`),console.log(` ${o.summary}`)}async function W(o=[]){const i=o,n=i.includes("--json"),e=i.includes("--brief"),a=i.indexOf("--since"),h=a!==-1?i[a+1]:null,c=process.cwd();!C.existsSync(I.join(c,x))&&!C.existsSync(I.join(c,".ai-memory"))&&(n||console.error(D(` \u2718 not initialized \u2014 run: infernoflow init
5
+ `)),process.exit(1));const l=v(c),s=P(l,h),f=l.filter(d=>new Date(d.ts||0)>s),w=_(s),m=B(w,f),{score:g,checks:O}=H(f,w,m),N=L(I.join(c,A));if(n){console.log(JSON.stringify({sessionStart:s.toISOString(),entries:f,changedFiles:w,unloggedTopics:m,health:{score:g,checks:O}},null,2));return}if(e){const d=g>=80?"A":g>=60?"B":g>=40?"C":"D",p=g>=60?$:g>=40?k:D;console.log(p(`Session health: ${d} (${g}/100)`)+t(` \u2014 ${f.length} entries logged`)),m.length&&console.log(k(` ${m.length} topic${m.length!==1?"s":""} changed but not logged: `)+m.map(r=>r.topic).join(", "));return}const S=t(" "+"\u2500".repeat(52));console.log(),console.log(" "+b("\u{1F525} infernoflow recap")),console.log(" "+t(T(c))),N?.policyId&&console.log(t(` Project: ${N.policyId}`));const E=s.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"});if(console.log(t(` Session since: ${E}`)),console.log(S),console.log(),console.log(" "+b("Captured this session")),console.log(),f.length===0)console.log(t(" Nothing logged yet this session."));else{const d=["gotcha","decision","attempt","preference","theme","note","error"],p=new Map;for(const r of f){const u=r.type||"note";p.has(u)||p.set(u,[]),p.get(u).push(r)}for(const r of d){const u=p.get(r);if(u?.length)for(const M of u)console.log(),Y(M)}}if(m.length>0){console.log(),console.log(S),console.log(),console.log(" "+b("Changed but not logged")+t(" (git diff since session start)")),console.log();for(const{topic:d,files:p}of m){console.log(k(` ? ${d}`));for(const r of p)console.log(t(` ${r}`))}console.log(),console.log(t(" Any gotchas or decisions from these areas worth capturing?")),console.log(t(" Run: ")+y('infernoflow log "<what happened>" --type gotcha'))}else w.length>0&&(console.log(),console.log(S),console.log(),console.log($(" \u2714 ")+t(`${w.length} changed files \u2014 all topics appear to be logged`)));console.log(),console.log(S),console.log(),console.log(" "+b("Session health")),console.log();const F=g>=80?"A":g>=60?"B":g>=40?"C":"D",j=g>=60?$:g>=40?k:D;console.log(` ${j(b(`${F}`))} ${j(`${g}/100`)}`),console.log();for(const{ok:d,label:p}of O){const r=d?$(" \u2714"):k(" \xB7");console.log(`${r} ${d?p:t(p)}`)}{const d=f.filter(u=>u.type==="gotcha").length,p=f.filter(u=>u.type==="decision").length,r=[];if(d===0?r.push(y('infernoflow log "..." --type gotcha')+t(" \u2014 adds 35 pts")):d<3&&g<80&&r.push(t(` ${3-d} more gotcha(s) would push you higher`)),p===0&&r.push(y('infernoflow log "..." --type decision')+t(" \u2014 adds 25 pts")),g>=60&&g<80&&r.push(t(" Almost B! One more entry gets you there.")),g>=80&&r.push($(" Great session \u2014 your handoff will be excellent.")),r.length){console.log(),console.log(t(" How to improve:"));for(const u of r)console.log(" "+u)}}(f.length>0||m.length>0)&&(console.log(),console.log(S),console.log(),console.log(t(" Before your next session:")),console.log(t(" ")+y("infernoflow switch")+t(" \u2014 generate a handoff summary for the next AI agent")),console.log(t(" ")+y("infernoflow ask --recent")+t(" \u2014 review what's in memory before starting"))),console.log()}export{W as recapCommand};
@@ -0,0 +1,5 @@
1
+ import{readEntries as u,updateEntry as f}from"../amp/io.mjs";import{gray as a,green as m,red as h,cyan as y}from"../ui/output.mjs";function g(e,t){if(!t||t.length<6)return{error:"give at least 6 characters of the entry id (see: infernoflow ask)"};const r=u(e).filter(o=>o.id&&o.id.startsWith(t));return r.length?r.length>1?{error:`${r.length} entries match ${t} \u2014 use more characters`}:{entry:r[0]}:{error:`no entry with id starting ${t}`}}async function w(e=[]){const t=process.cwd(),r=e.slice(1).find(n=>!n.startsWith("--")&&e[e.indexOf(n)-1]!=="--note"),o=e.indexOf("--note"),i=o!==-1?String(e[o+1]||"").slice(0,500):null,d=e.includes("--undo"),{entry:s,error:c}=g(t,r);if(c){console.error(h(`
2
+ \u2718 `+c+`
3
+ `)),process.exitCode=1;return}const l=f(t,s.id,n=>(d?delete n.meta.resolved:n.meta.resolved={ts:Date.now(),...i?{note:i}:{}},Object.keys(n.meta).length||delete n.meta,n));console.log(`
4
+ `+m("\u2714 ")+(d?"Re-opened ":"Resolved ")+y(s.id)+a(` (${l.updated} cop${l.updated===1?"y":"ies"})`)),console.log(" "+a(s.summary||s.msg||"")+`
5
+ `)}export{g as findByIdPrefix,w as resolveCommand};
@@ -0,0 +1,3 @@
1
+ import{buildResume as s,renderResume as i}from"../memoryView.mjs";async function r(n=[]){const e=n.indexOf("--file"),l=e!==-1?n[e+1]:null,o=s(process.cwd(),{file:l});if(n.includes("--json")){console.log(JSON.stringify(o,null,2));return}console.log(`
2
+ `+i(o)+`
3
+ `)}export{r as resumeCommand};
@@ -1,15 +1,12 @@
1
- import*as a from"node:fs";import*as u from"node:path";import*as w from"node:os";import{fileURLToPath as N}from"node:url";import{execSync as I}from"node:child_process";import{detectIdeContext as M}from"../ai/ideDetection.mjs";import{header as J,ok as k,warn as E,info as v,done as D,cyan as p,yellow as C,bold as h,green as g,gray as j}from"../ui/output.mjs";import"../cursorHooksInstall.mjs";import{refreshSecuritySensitiveCopies as L}from"../securityRefresh.mjs";import"../vsCodeCopilotHooksInstall.mjs";import{updateInjectionConfig as T}from"../amp/io.mjs";import{findProjectRoot as A}from"../projectRoot.mjs";import{injectionPatchFromArgs as H}from"./refresh.mjs";const W=u.dirname(N(import.meta.url));function _(){return u.resolve(W,"../../templates")}function U(t){try{return I(`npx infernoflow ${t}`,{encoding:"utf8",cwd:process.cwd(),timeout:6e4,stdio:["inherit","pipe","pipe"]})}catch(i){return i.stdout||i.stderr||i.message}}function y(t){if(!a.existsSync(t))return{data:{},existed:!1,corrupt:!1,backup:null};let i;try{i=a.readFileSync(t,"utf8")}catch{return{data:{},existed:!0,corrupt:!1,backup:null}}try{const s=JSON.parse(i);return!s||typeof s!="object"||Array.isArray(s)?{data:{},existed:!0,corrupt:!1,backup:null}:{data:s,existed:!0,corrupt:!1,backup:null}}catch{let s=null;try{s=`${t}.corrupt-${Date.now()}.bak`,a.writeFileSync(s,i,"utf8")}catch{s=null}return{data:{},existed:!0,corrupt:!0,backup:s}}}function V(){const t=w.homedir();return process.platform==="win32"?u.join(t,"AppData","Roaming","Claude","claude_desktop_config.json"):process.platform==="darwin"?u.join(t,"Library","Application Support","Claude","claude_desktop_config.json"):u.join(t,".config","Claude","claude_desktop_config.json")}function K(t,i){const s=V(),n=u.dirname(s);if(!a.existsSync(n)&&!a.existsSync(s))return{updated:!1,skipped:"not-installed"};const{data:r,backup:o}=y(s);r.mcpServers||(r.mcpServers={});const l=r.mcpServers.infernoflow;if(l&&Array.isArray(l.args)&&l.args[0]===t&&l.env&&l.env.INFERNOFLOW_PROJECT_DIR===i)return{updated:!1,backup:o};const c={...l&&l.env?l.env:{}};i&&(c.INFERNOFLOW_PROJECT_DIR=i),r.mcpServers.infernoflow={command:"node",args:[t],env:c};const d=JSON.stringify(r,null,2);return a.mkdirSync(n,{recursive:!0}),a.writeFileSync(s,d,"utf8"),{updated:!0,path:s,backup:o}}function q(t){const i=u.join(t,".git");if(!a.existsSync(i))return{installed:!1,skipped:"not-a-git-repo"};const s=u.join(i,"hooks"),n=u.join(s,"post-commit"),r='infernoflow log "commit: $(git log -1 --pretty=%s)" --type note --source git-hook --auto --quiet >/dev/null 2>&1 || true';try{if(a.mkdirSync(s,{recursive:!0}),a.existsSync(n)){const o=a.readFileSync(n,"utf8");if(o.includes("infernoflow"))return{installed:!1,already:!0};const l=o.replace(/\s*$/,"")+`
2
-
3
- # infernoflow auto-capture
4
- `+r+`
5
- `;a.writeFileSync(n,l,"utf8")}else{const o=["#!/bin/sh","# infernoflow: auto-capture the commit subject into session memory.","# Best-effort and non-blocking \u2014 never fails a commit.",r,""].join(`
6
- `);a.writeFileSync(n,o,"utf8")}try{a.chmodSync(n,493)}catch{}return{installed:!0,path:n}}catch(o){return{installed:!1,error:o.message}}}const B=2,b=`#!/usr/bin/env node
1
+ import*as d from"node:fs";import*as f from"node:path";import"node:os";import{fileURLToPath as O}from"node:url";import{execSync as J}from"node:child_process";import{detectIdeContext as H}from"../ai/ideDetection.mjs";import{header as T,ok as k,warn as E,info as S,done as L,cyan as c,yellow as C,bold as y,green as u,gray as m}from"../ui/output.mjs";import"../cursorHooksInstall.mjs";import{refreshSecuritySensitiveCopies as I}from"../securityRefresh.mjs";import{updateProjectMcpJson as N,updateIdeMcpJson as U,updateClaudeDesktopPerProject as W,removeLegacyUserLevelEntry as v,claudeJsonPath as V,claudeDesktopConfigPath as b,runningFromNpxCache as K}from"../mcpRegistration.mjs";import"../vsCodeCopilotHooksInstall.mjs";import{updateInjectionConfig as B}from"../amp/io.mjs";import{findProjectRoot as q}from"../projectRoot.mjs";import{refreshRuleFilesFromMemory as G}from"../ruleFiles.mjs";import{injectionPatchFromArgs as z}from"./refresh.mjs";const Q=f.dirname(O(import.meta.url));function _(){return f.resolve(Q,"../../templates")}function X(t){try{return J(`npx infernoflow ${t}`,{encoding:"utf8",cwd:process.cwd(),timeout:6e4,stdio:["inherit","pipe","pipe"]})}catch(s){return s.stdout||s.stderr||s.message}}function w(t){if(!d.existsSync(t))return{data:{},existed:!1,corrupt:!1,backup:null};let s;try{s=d.readFileSync(t,"utf8")}catch{return{data:{},existed:!0,corrupt:!1,backup:null}}try{const i=JSON.parse(s);return!i||typeof i!="object"||Array.isArray(i)?{data:{},existed:!0,corrupt:!1,backup:null}:{data:i,existed:!0,corrupt:!1,backup:null}}catch{let i=null;try{i=`${t}.corrupt-${Date.now()}.bak`,d.writeFileSync(i,s,"utf8")}catch{i=null}return{data:{},existed:!0,corrupt:!0,backup:i}}}function Y(t){const s=f.join(t,".git","hooks","post-commit");if(!d.existsSync(s))return{installed:!1,removed:!1};try{const i=d.readFileSync(s,"utf8");if(!i.includes('infernoflow log "commit:'))return{installed:!1,removed:!1};const n=i.split(/\r?\n/).filter(r=>!r.includes('infernoflow log "commit:')&&!/^#\s*infernoflow(:| auto-capture)/.test(r.trim())&&!/^# Best-effort and non-blocking — never fails a commit\.$/.test(r.trim()));return n.filter(r=>r.trim()&&!r.startsWith("#!")).length?d.writeFileSync(s,n.join(`
2
+ `).replace(/\n*$/,`
3
+ `),"utf8"):d.unlinkSync(s),{installed:!1,removed:!0}}catch(i){return{installed:!1,removed:!1,error:i.message}}}const x=3,P=`#!/usr/bin/env node
7
4
  // infernoflow UserPromptSubmit hook (Claude Code / Cursor).
8
- // infernoflow-hook-version: ${B}
5
+ // infernoflow-hook-version: ${x}
9
6
  // Logs a best-effort 'attempt' entry when the user's prompt shows frustration,
10
7
  // so the highest-value capture signal doesn't depend on the model remembering.
11
8
  // Never blocks the prompt. Never uses a shell.
12
- import { readFileSync, existsSync, realpathSync } from "node:fs";
9
+ import { readFileSync, writeFileSync, existsSync, realpathSync } from "node:fs";
13
10
  import { execFileSync, spawnSync } from "node:child_process";
14
11
  import { dirname, join } from "node:path";
15
12
 
@@ -24,11 +21,16 @@ function findCliMjs() {
24
21
  let hits = [];
25
22
  try {
26
23
  const finder = process.platform === "win32" ? "where" : "which";
27
- hits = execFileSync(finder, ["infernoflow"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true, timeout: 3000 })
24
+ hits = execFileSync(finder, ["infernoflow"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true, timeout: 10_000 })
28
25
  .split(/\\r?\\n/).map((s) => s.trim()).filter(Boolean);
29
26
  } catch {}
27
+ // where (Windows) searches the current folder first: never use a launcher
28
+ // that lives inside the project (a cloned repo could plant one).
29
+ const proj = (process.env.CLAUDE_PROJECT_DIR || process.cwd()).toLowerCase();
30
+ const inProject = (p) => { const r = p.toLowerCase(); return r === proj || r.startsWith(proj + "/") || r.startsWith(proj + "\\\\"); };
30
31
  for (const c of hits) {
31
- try { const real = realpathSync(c); if (/\\.m?js$/i.test(real)) return real; } catch {}
32
+ if (inProject(c)) continue;
33
+ try { const real = realpathSync(c); if (/\\.m?js$/i.test(real) && !inProject(real)) return real; } catch {}
32
34
  const d = dirname(c);
33
35
  for (const pkg of [join(d, "node_modules", "infernoflow"), join(d, "..", "lib", "node_modules", "infernoflow")]) {
34
36
  for (const f of [join(pkg, "dist", "bin", "infernoflow.mjs"), join(pkg, "bin", "infernoflow.mjs")]) {
@@ -40,22 +42,84 @@ function findCliMjs() {
40
42
  }
41
43
 
42
44
  const MARKERS = [/!!+/, /not working/i, /still (broken|failing|not)/i, /does ?n'?t work/i, /\\bbroken\\b/i, /\\bretry(ing)?\\b/i, /same error/i, /no change/i];
43
- if (prompt && MARKERS.some((re) => re.test(prompt))) {
45
+ // D14 (0.46.0): one entry per burst \u2014 a run of frustrated prompts within 10
46
+ // minutes is the same problem; the agent / memory-keeper writes the real lesson.
47
+ const STATE = join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), ".ai-memory", ".hook-state.json");
48
+ function recentlyLogged() {
49
+ try { const s = JSON.parse(readFileSync(STATE, "utf8")); return Date.now() - (s.lastFrustration || 0) < 10 * 60_000; } catch { return false; }
50
+ }
51
+ if (prompt && MARKERS.some((re) => re.test(prompt)) && !recentlyLogged()) {
44
52
  // Leading dashes are stripped so the text can never be read as a CLI flag.
45
53
  const msg = "User frustration: " + prompt.replace(/\\s+/g, " ").trim().replace(/^[\\s-]+/, "").slice(0, 120);
46
54
  const cli = findCliMjs();
47
55
  if (cli) {
48
56
  try {
49
- spawnSync(process.execPath, [cli, "log", msg, "--type", "attempt", "--result", "failed", "--auto", "--quiet", "--source", "hook"], {
50
- stdio: "ignore", timeout: 5000, windowsHide: true, shell: false,
57
+ const r = spawnSync(process.execPath, [cli, "log", msg, "--type", "attempt", "--result", "failed", "--auto", "--quiet", "--source", "hook", "--tags", "needs-summary"], {
58
+ stdio: "ignore", timeout: 15_000, windowsHide: true, shell: false,
51
59
  });
60
+ // Start the cooldown only if the entry was actually written.
61
+ if (r.status === 0) { try { writeFileSync(STATE, JSON.stringify({ lastFrustration: Date.now() })); } catch {} }
52
62
  } catch {}
53
63
  }
54
64
  }
55
65
  process.exit(0);
56
- `;function G(t){const i=u.join(t,".claude","hooks"),s=u.join(i,"log-frustration.mjs");try{a.mkdirSync(i,{recursive:!0}),a.writeFileSync(s,b,"utf8");try{a.chmodSync(s,493)}catch{}}catch(c){return{installed:!1,error:c.message}}const n=u.join(t,".claude","settings.json"),{data:r}=y(n);(!r.hooks||typeof r.hooks!="object")&&(r.hooks={}),Array.isArray(r.hooks.UserPromptSubmit)||(r.hooks.UserPromptSubmit=[]);const o="node .claude/hooks/log-frustration.mjs",l=r.hooks.UserPromptSubmit.some(c=>Array.isArray(c?.hooks)&&c.hooks.some(d=>typeof d?.command=="string"&&d.command.includes("log-frustration.mjs")));l||r.hooks.UserPromptSubmit.push({hooks:[{type:"command",command:o}]});try{a.mkdirSync(u.dirname(n),{recursive:!0}),a.writeFileSync(n,JSON.stringify(r,null,2),"utf8")}catch(c){return{installed:!0,registered:!1,error:c.message}}return{installed:!0,registered:!l}}const z=["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 Q(t,i){const s=u.join(w.homedir(),".claude.json"),{data:n,backup:r}=y(s);n.mcpServers||(n.mcpServers={});const o=n.mcpServers.infernoflow;if(o&&o.args&&o.args[0]===t&&o.env&&o.env.INFERNOFLOW_PROJECT_DIR===i)return{updated:!1,backup:r};const l={...o&&o.env?o.env:{}};i&&(l.INFERNOFLOW_PROJECT_DIR=i),n.mcpServers.infernoflow={command:"node",args:[t],env:l};const c=JSON.stringify(n,null,2).replace(/\u0000+/g,"");return a.writeFileSync(s,c,"utf8"),{updated:!0,path:s,backup:r}}function X(t,i,s){const n=u.join(t,".vscode"),r=u.join(n,"mcp.json"),{data:o,backup:l}=y(r);o.servers||(o.servers={});const c=o.servers.infernoflow;if(c&&c.args&&c.args[0]===i&&c.env&&c.env.INFERNOFLOW_PROJECT_DIR===s)return{updated:!1,backup:l};const d={...c&&c.env?c.env:{}};return s&&(d.INFERNOFLOW_PROJECT_DIR=s),o.servers.infernoflow={type:"stdio",command:"node",args:[i],env:d},a.mkdirSync(n,{recursive:!0}),a.writeFileSync(r,JSON.stringify(o,null,2)+`
57
- `,"utf8"),{updated:!0,path:r,backup:l}}function Y(t,i){const s=u.join(t,".claude"),n=u.join(s,"settings.json"),{data:r}=y(n),o=new Set(r.allowedTools||[]);for(const c of z)o.add(`mcp__infernoflow__${c}`);const l={...r,allowedTools:[...o]};return a.mkdirSync(s,{recursive:!0}),a.writeFileSync(n,JSON.stringify(l,null,2),"utf8"),n}function Z(t,{silent:i=!1}={}){const s=_(),n=i?()=>{}:e=>k(e),r=i?()=>{}:e=>E(e),o={mcpServer:!1,claudeJson:!1,claudeSettings:!1,claudeDesktop:!1,gitHooks:!1,captureHook:!1,backups:[]},l=u.join(s,"cursor","inferno-mcp-server.mjs"),c=u.join(t,".cursor","inferno-mcp-server.mjs"),d=(()=>{try{return A(t)}catch{return t}})();try{a.existsSync(c)||(a.mkdirSync(u.dirname(c),{recursive:!0}),a.copyFileSync(l,c),o.mcpServer=!0,n("Copied MCP server \u2192 "+p(".cursor/inferno-mcp-server.mjs")))}catch(e){r("MCP server copy skipped: "+e.message)}try{const e=L(t,{captureHookScript:b});e.length&&(o.securityRefreshed=e,e.some(f=>f.endsWith("inferno-mcp-server.mjs"))&&(o.mcpServer=!0),n("Security update: replaced outdated "+e.map(f=>p(f)).join(", ")))}catch{}try{const e=Q(c,d);e.updated&&(o.claudeJson=!0,n("Registered MCP server in "+p("~/.claude.json"))),e.backup&&(o.backups.push(e.backup),r("~/.claude.json was unreadable \u2014 backed up to "+e.backup+" before rewrite"))}catch(e){r("~/.claude.json update skipped: "+e.message)}try{X(t,c,d).updated&&(o.vscodeMcp=!0,n("Registered MCP server in "+p(".vscode/mcp.json")+j(" (Copilot Chat)")))}catch(e){r(".vscode/mcp.json update skipped: "+e.message)}try{const e=u.join(t,".cursor","mcp.json"),{data:f}=y(e);f.mcpServers||(f.mcpServers={});const m=f.mcpServers.infernoflow;(!m||!m.args||m.args[0]!==c||!m.env||m.env.INFERNOFLOW_PROJECT_DIR!==d)&&(f.mcpServers.infernoflow={command:"node",args:[c],env:{...m&&m.env?m.env:{},INFERNOFLOW_PROJECT_DIR:d}},a.mkdirSync(u.dirname(e),{recursive:!0}),a.writeFileSync(e,JSON.stringify(f,null,2)+`
58
- `,"utf8"),o.cursorMcp=!0,n("Registered MCP server in "+p(".cursor/mcp.json")))}catch(e){r(".cursor/mcp.json update skipped: "+e.message)}try{Y(t,!1),o.claudeSettings=!0,n("Pre-approved infernoflow tools in "+p(".claude/settings.json"))}catch(e){r(".claude/settings.json skipped: "+e.message)}try{const e=K(c,d);e.updated&&(o.claudeDesktop=!0,n("Registered MCP server in "+p("claude_desktop_config.json")+j(" (Claude Desktop app)"))),e.backup&&(o.backups.push(e.backup),r("Claude Desktop config was unreadable \u2014 backed up to "+e.backup))}catch(e){r("Claude Desktop config skipped: "+e.message)}try{q(t).installed&&(o.gitHooks=!0,n("Installed git post-commit hook \u2192 "+p(".git/hooks/post-commit")))}catch(e){r("git hook install skipped: "+e.message)}try{G(t).installed&&(o.captureHook=!0,n("Installed capture hook \u2192 "+p(".claude/hooks/log-frustration.mjs")))}catch(e){r("capture hook install skipped: "+e.message)}return o}async function de(t){const i=process.cwd(),s=t.includes("--force")||t.includes("-f"),n=t.includes("--yes")||t.includes("-y"),r=_();J("infernoflow setup");const{ideDetected:o}=M("auto");v(`IDE detected: ${h(o==="cursor"?"Cursor":o==="vscode"?"VS Code":o==="windsurf"?"Windsurf":"unknown")}`);const c=u.join(i,".ai-memory");a.existsSync(c)?k(".ai-memory/ already exists \u2014 skipping init"):(console.log(`
66
+ `;function Z(t){const s=f.join(t,".claude","hooks"),i=f.join(s,"log-frustration.mjs");try{d.mkdirSync(s,{recursive:!0}),d.writeFileSync(i,P,"utf8");try{d.chmodSync(i,493)}catch{}}catch(a){return{installed:!1,error:a.message}}const n=f.join(t,".claude","settings.json"),{data:o}=w(n);(!o.hooks||typeof o.hooks!="object")&&(o.hooks={}),Array.isArray(o.hooks.UserPromptSubmit)||(o.hooks.UserPromptSubmit=[]);const r="node .claude/hooks/log-frustration.mjs",p=o.hooks.UserPromptSubmit.some(a=>Array.isArray(a?.hooks)&&a.hooks.some(e=>typeof e?.command=="string"&&e.command.includes("log-frustration.mjs")));p||o.hooks.UserPromptSubmit.push({hooks:[{type:"command",command:r}]});try{d.mkdirSync(f.dirname(n),{recursive:!0}),d.writeFileSync(n,JSON.stringify(o,null,2),"utf8")}catch(a){return{installed:!0,registered:!1,error:a.message}}return{installed:!0,registered:!p}}const ee=`#!/usr/bin/env node
67
+ // infernoflow Claude Code session hook \u2014 usage: node infernoflow-session.mjs start|end
68
+ // infernoflow-hook-version: ${x}
69
+ import { readFileSync, existsSync, realpathSync } from "node:fs";
70
+ import { execFileSync, spawnSync, spawn } from "node:child_process";
71
+ import { dirname, join } from "node:path";
72
+
73
+ let raw = "";
74
+ try { raw = readFileSync(0, "utf8"); } catch {}
75
+ const cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
76
+ if (!existsSync(join(cwd, ".ai-memory"))) process.exit(0);
77
+
78
+ function findCliMjs() {
79
+ let hits = [];
80
+ try {
81
+ const finder = process.platform === "win32" ? "where" : "which";
82
+ hits = execFileSync(finder, ["infernoflow"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], windowsHide: true, timeout: 10_000 })
83
+ .split(/\\r?\\n/).map((s) => s.trim()).filter(Boolean);
84
+ } catch {}
85
+ // where (Windows) searches the current folder first: never use a launcher
86
+ // that lives inside the project (a cloned repo could plant one).
87
+ const proj = (process.env.CLAUDE_PROJECT_DIR || process.cwd()).toLowerCase();
88
+ const inProject = (p) => { const r = p.toLowerCase(); return r === proj || r.startsWith(proj + "/") || r.startsWith(proj + "\\\\"); };
89
+ for (const c of hits) {
90
+ if (inProject(c)) continue;
91
+ try { const real = realpathSync(c); if (/\\.m?js$/i.test(real) && !inProject(real)) return real; } catch {}
92
+ const d = dirname(c);
93
+ for (const pkg of [join(d, "node_modules", "infernoflow"), join(d, "..", "lib", "node_modules", "infernoflow")]) {
94
+ for (const f of [join(pkg, "dist", "bin", "infernoflow.mjs"), join(pkg, "bin", "infernoflow.mjs")]) {
95
+ if (existsSync(f)) return f;
96
+ }
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ const cli = findCliMjs();
103
+ if (!cli) process.exit(0);
104
+ if (process.argv[2] === "end") {
105
+ try {
106
+ // Payload fields go as arguments (not stdin): this process exits at once
107
+ // (SessionEnd's ~1.5 s budget) and a pipe could be cut before it is read.
108
+ let p = {}; try { p = JSON.parse(raw || "{}"); } catch {}
109
+ const extra = [];
110
+ if (typeof p.transcript_path === "string") extra.push("--transcript", p.transcript_path);
111
+ if (typeof p.reason === "string") extra.push("--reason", p.reason.slice(0, 40));
112
+ const child = spawn(process.execPath, [cli, "hook", "session-end", ...extra], { cwd, detached: true, stdio: "ignore", windowsHide: true, shell: false });
113
+ child.unref();
114
+ } catch {}
115
+ process.exit(0);
116
+ }
117
+ try {
118
+ const r = spawnSync(process.execPath, [cli, "hook", "session-start"], { cwd, input: raw, encoding: "utf8", timeout: 15000, windowsHide: true, shell: false });
119
+ if (r.stdout) process.stdout.write(r.stdout);
120
+ } catch {}
121
+ process.exit(0);
122
+ `;function oe(t){const s=f.join(t,".claude","hooks"),i=f.join(s,"infernoflow-session.mjs");try{d.mkdirSync(s,{recursive:!0}),d.writeFileSync(i,ee,"utf8");try{d.chmodSync(i,493)}catch{}}catch(a){return{installed:!1,error:a.message}}const n=f.join(t,".claude","settings.json"),{data:o,corrupt:r}=w(n);if(r)return{installed:!0,registered:!1,error:"settings.json unreadable"};(!o.hooks||typeof o.hooks!="object")&&(o.hooks={});let p=!1;for(const[a,e]of[["SessionStart","start"],["SessionEnd","end"]])Array.isArray(o.hooks[a])||(o.hooks[a]=[]),o.hooks[a].some(h=>Array.isArray(h?.hooks)&&h.hooks.some(g=>typeof g?.command=="string"&&g.command.includes("infernoflow-session.mjs")))||(o.hooks[a].push({hooks:[{type:"command",command:`node .claude/hooks/infernoflow-session.mjs ${e}`}]}),p=!0);if(p)try{d.writeFileSync(n,JSON.stringify(o,null,2),"utf8")}catch(a){return{installed:!0,registered:!1,error:a.message}}return{installed:!0,registered:p}}const te=["infernoflow_status","infernoflow_check","infernoflow_context","infernoflow_git_drift","amp_read","amp_write","amp_search","amp_bookmark","amp_handoff","amp_health","amp_resume"];function re(t,s){const i=f.join(t,".claude"),n=f.join(i,"settings.json"),{data:o}=w(n),r=new Set(o.allowedTools||[]);for(const a of te)r.add(`mcp__infernoflow__${a}`);const p={...o,allowedTools:[...r]};return d.mkdirSync(i,{recursive:!0}),d.writeFileSync(n,JSON.stringify(p,null,2),"utf8"),n}function se(t,{silent:s=!1}={}){const i=_(),n=s?()=>{}:e=>k(e),o=s?()=>{}:e=>E(e),r={mcpServer:!1,projectMcpJson:!1,claudeJson:!1,claudeSettings:!1,claudeDesktop:!1,gitHooks:!1,captureHook:!1,backups:[]},p=(()=>{try{return q(t)}catch{return t}})();try{const e=I(t,{captureHookScript:P});e.length&&(r.securityRefreshed=e,e.some(l=>l.endsWith("inferno-mcp-server.mjs"))&&(r.mcpServer=!0),n("Security update: replaced outdated "+e.map(l=>c(l)).join(", ")))}catch{}K()&&o("infernoflow is running from the npx cache \u2014 MCP configs will point there and break when the cache is cleared. Install it: npm i -g infernoflow, then run: infernoflow setup --yes");let a=!1;try{const e=N(p);e.skipped==="tracked"?o(".mcp.json is tracked by git \u2014 not adding this machine's paths to it. Add the infernoflow server with: claude mcp add infernoflow -s local -- node <infernoflow>/bin/infernoflow.mjs mcp"):e.skipped?o(".mcp.json is unreadable \u2014 left unchanged"):(a=!0,e.updated&&(r.projectMcpJson=!0,n("Registered MCP server in "+c(".mcp.json")+m(" (Claude Code, this project only)"))),e.gitignored&&n("Added "+c(".mcp.json")+" to .gitignore (it holds this machine's paths)"))}catch(e){o(".mcp.json update skipped: "+e.message)}if(a)try{const e=v(V());e.removed&&(r.legacyClaudeJsonRemoved=e,n("Removed the old user-level infernoflow entry from "+c("~/.claude.json")+m(` (was pinned to ${e.pinnedTo||"one project"}; backup: ${e.backup})`)))}catch(e){o("~/.claude.json cleanup skipped: "+e.message)}for(const[e,l]of[["vscode",".vscode/mcp.json"],["cursor",".cursor/mcp.json"]])try{U(p,e).updated&&(r[e==="vscode"?"vscodeMcp":"cursorMcp"]=!0,n("Registered MCP server in "+c(l)))}catch(h){o(l+" update skipped: "+h.message)}try{const e=W(p);if(e.updated&&(r.claudeDesktop=!0,n("Registered MCP server "+c(e.name)+" in "+c("claude_desktop_config.json")+m(" (Claude Desktop app)"))),!e.skipped){const l=v(b());l.removed&&(r.legacyDesktopRemoved=l,n("Removed the old pinned infernoflow entry from "+c("claude_desktop_config.json")+m(` (backup: ${l.backup})`)))}}catch(e){o("Claude Desktop config skipped: "+e.message)}try{re(t,!1),r.claudeSettings=!0,n("Pre-approved infernoflow tools in "+c(".claude/settings.json"))}catch(e){o(".claude/settings.json skipped: "+e.message)}try{Y(t).removed&&(r.gitHookRemoved=!0,n("Removed the old commit-logging line from "+c(".git/hooks/post-commit")+m(" (git already keeps commit history)")))}catch(e){o("git hook install skipped: "+e.message)}try{Z(t).installed&&(r.captureHook=!0,n("Installed capture hook \u2192 "+c(".claude/hooks/log-frustration.mjs")))}catch(e){o("capture hook install skipped: "+e.message)}try{oe(t).registered&&(r.sessionHooks=!0,n("Installed session hooks \u2192 "+c(".claude/hooks/infernoflow-session.mjs")+m(" (fresh memory at start, resume point at end)")))}catch(e){o("session hooks skipped: "+e.message)}try{G(t)}catch{}return r}async function je(t){const s=process.cwd(),i=t.includes("--force")||t.includes("-f"),n=t.includes("--yes")||t.includes("-y"),o=_();T("infernoflow setup");const{ideDetected:r}=H("auto");S(`IDE detected: ${y(r==="cursor"?"Cursor":r==="vscode"?"VS Code":r==="windsurf"?"Windsurf":"unknown")}`);const a=f.join(s,".ai-memory");d.existsSync(a)?k(".ai-memory/ already exists \u2014 skipping init"):(console.log(`
59
123
  ${C(".ai-memory/")} not found \u2014 running init ...
60
- `),U(n?"init --yes":"init"));const d=H(t);if(Object.keys(d).length)try{T(i,d),k("Injection config updated \u2192 "+JSON.stringify(d))}catch{}console.log(),v("Wiring up MCP servers for Cursor / VS Code Copilot / Claude Code ...");const e=Z(i,{silent:!1});console.log(),D("infernoflow ready"),console.log(`
61
- ${h("What was set up:")}`),console.log(` ${g("\u2714")} MCP server installed \u2192 ${p(".cursor/inferno-mcp-server.mjs")}`),e.cursorMcp&&console.log(` ${g("\u2714")} Cursor MCP config \u2192 ${p(".cursor/mcp.json")}`),e.vscodeMcp&&console.log(` ${g("\u2714")} VS Code Copilot MCP config \u2192 ${p(".vscode/mcp.json")}`),e.claudeJson&&console.log(` ${g("\u2714")} Claude Code MCP config \u2192 ${p("~/.claude.json")}`),e.claudeSettings&&console.log(` ${g("\u2714")} Auto-approved tools \u2192 ${p(".claude/settings.json")}`),e.claudeDesktop&&console.log(` ${g("\u2714")} Claude Desktop MCP config \u2192 ${p("claude_desktop_config.json")}`),e.gitHooks&&console.log(` ${g("\u2714")} Git post-commit hook \u2192 ${p(".git/hooks/post-commit")}`),e.captureHook&&console.log(` ${g("\u2714")} Capture hook (Claude Code/Cursor) \u2192 ${p(".claude/hooks/log-frustration.mjs")}`);try{const{detectStaleMcpRuntime:f}=await import("../mcpRuntime.mjs"),{readFileSync:m}=await import("node:fs"),{dirname:R,join:x}=await import("node:path"),{fileURLToPath:O}=await import("node:url"),P=R(O(import.meta.url)),F=x(P,"..","..","package.json"),$=JSON.parse(m(F,"utf8")).version,S=f(i,$);S&&(console.log(),console.log(` ${C("\u26A0")} ${h("Restart required:")} ${S.message}`))}catch{}console.log(),console.log(` ${h("Next step:")} Restart your AI tool. Test by asking:`),console.log(` ${p('"call the amp_write tool with a test note"')}`),console.log()}export{b as CAPTURE_HOOK_SCRIPT,B as CAPTURE_HOOK_VERSION,z as MCP_TOOLS,Z as autoSetupMcp,V as claudeDesktopConfigPath,G as installClaudeCodeCaptureHook,q as installGitHooks,de as setupCommand,K as updateClaudeDesktopConfig,Q as updateClaudeJson,X as updateVscodeMcpJson,Y as writeClaudeSettings};
124
+ `),X(n?"init --yes":"init"));const e=z(t);if(Object.keys(e).length)try{B(s,e),k("Injection config updated \u2192 "+JSON.stringify(e))}catch{}console.log(),S("Wiring up MCP servers for Cursor / VS Code Copilot / Claude Code ...");const l=se(s,{silent:!1});console.log(),L("infernoflow ready"),console.log(`
125
+ ${y("What was set up:")}`),console.log(` ${u("\u2714")} MCP server \u2192 ${c("infernoflow mcp")} ${m("(runs from the installed package)")}`),l.projectMcpJson&&console.log(` ${u("\u2714")} Claude Code MCP config \u2192 ${c(".mcp.json")} ${m("(this project)")}`),l.cursorMcp&&console.log(` ${u("\u2714")} Cursor MCP config \u2192 ${c(".cursor/mcp.json")}`),l.vscodeMcp&&console.log(` ${u("\u2714")} VS Code Copilot MCP config \u2192 ${c(".vscode/mcp.json")}`),l.legacyClaudeJsonRemoved&&console.log(` ${u("\u2714")} Removed old pinned entry from ${c("~/.claude.json")} ${m("(backup saved)")}`),l.claudeSettings&&console.log(` ${u("\u2714")} Auto-approved tools \u2192 ${c(".claude/settings.json")}`),l.claudeDesktop&&console.log(` ${u("\u2714")} Claude Desktop MCP config \u2192 ${c("claude_desktop_config.json")}`),l.gitHookRemoved&&console.log(` ${u("\u2714")} Old commit-logging hook removed \u2192 ${c(".git/hooks/post-commit")}`),l.sessionHooks&&console.log(` ${u("\u2714")} Session hooks (fresh memory at start, resume point at end) \u2192 ${c(".claude/hooks/infernoflow-session.mjs")}`),l.captureHook&&console.log(` ${u("\u2714")} Capture hook (Claude Code/Cursor) \u2192 ${c(".claude/hooks/log-frustration.mjs")}`);try{const{detectStaleMcpRuntime:h}=await import("../mcpRuntime.mjs"),{readFileSync:g}=await import("node:fs"),{dirname:$,join:R}=await import("node:path"),{fileURLToPath:F}=await import("node:url"),M=$(F(import.meta.url)),A=R(M,"..","..","package.json"),D=JSON.parse(g(A,"utf8")).version,j=h(s,D);j&&(console.log(),console.log(` ${C("\u26A0")} ${y("Restart required:")} ${j.message}`))}catch{}console.log(),console.log(` ${y("Next step:")} Restart your AI tool. Test by asking:`),console.log(` ${c('"call the amp_write tool with a test note"')}`),console.log()}export{P as CAPTURE_HOOK_SCRIPT,x as CAPTURE_HOOK_VERSION,te as MCP_TOOLS,ee as SESSION_HOOK_SCRIPT,se as autoSetupMcp,b as claudeDesktopConfigPath,Z as installClaudeCodeCaptureHook,oe as installClaudeSessionHooks,Y as installGitHooks,je as setupCommand,re as writeClaudeSettings};
@@ -1,6 +1,6 @@
1
- import*as e from"node:fs";import*as l from"node:path";import{header as I,ok as L,fail as b,warn as A,section as $,bold as r,cyan as v,yellow as u,gray as n,green as h,red as R,white as V}from"../ui/output.mjs";import{readEntries as W}from"../amp/io.mjs";function M(f){const t=Math.floor((Date.now()-f)/1e3);return t<60?"just now":t<3600?`${Math.floor(t/60)}m ago`:t<86400?`${Math.floor(t/3600)}h ago`:`${Math.floor(t/86400)}d ago`}function _(f,t){const g=new Set;if(e.existsSync(f))for(const i of e.readdirSync(f).filter(y=>y.endsWith(".json")))try{(JSON.parse(e.readFileSync(l.join(f,i),"utf8")).capabilitiesCovered||[]).forEach(E=>g.add(E))}catch{}return{covered:t.filter(i=>g.has(i)),uncovered:t.filter(i=>!g.has(i))}}async function K(f=[]){const t=f.includes("--json"),g=process.cwd(),i=l.join(g,"inferno"),y=l.join(g,".ai-memory");t||I("status"),!e.existsSync(i)&&!e.existsSync(y)&&(t&&(console.log(JSON.stringify({ok:!1,error:"not_initialized",hint:"Run: infernoflow init"},null,2)),process.exit(1)),b("not initialized \u2014 neither .ai-memory/ nor inferno/ found","Run: infernoflow init"),console.log(),process.exit(1));const E=l.join(y,"amp.json"),J=l.join(i,"config.json"),m=l.join(i,"contract.json"),U=e.existsSync(E)||e.existsSync(y),D=(()=>{try{return JSON.parse(e.readFileSync(J,"utf8")).mode||null}catch{return null}})();if(U||D==="memory"||!e.existsSync(m)&&e.existsSync(J)){const o=W(g),s=o.filter(d=>d.type==="gotcha").length,c=o.filter(d=>d.type==="decision").length,p=o.filter(d=>d.type==="attempt").length,a=o[o.length-1];if(t){console.log(JSON.stringify({ok:!0,mode:"memory",entries:o.length,gotchas:s,decisions:c,attempts:p,lastEntry:a?a.ts:null},null,2));return}$("Session memory"),console.log(` ${n("entries")} ${r(String(o.length))}`),console.log(` ${n("gotchas")} ${r(String(s))}`),console.log(` ${n("decisions")} ${r(String(c))}`),console.log(` ${n("attempts")} ${r(String(p))}`),a&&console.log(` ${n("last entry")} ${n(M(new Date(a.ts).getTime()))}`),console.log(),o.length===0?console.log(` ${u("\u25CF")} ${r(u("empty"))} ${n("\u2014 log your first gotcha:")} ${v('infernoflow log "..." --type gotcha')}`):console.log(` ${h("\u25CF")} ${r(h("ready"))} ${n("\u2014 run")} ${v("infernoflow recap")} ${n("for the full session summary")}`),e.existsSync(m)?console.log():console.log(`
1
+ import*as e from"node:fs";import{describeStore as I}from"../amp/io.mjs";import*as l from"node:path";import{header as L,ok as V,fail as O,warn as A,section as $,bold as r,cyan as v,yellow as u,gray as n,green as h,red as R,white as W}from"../ui/output.mjs";import{readEntries as _}from"../amp/io.mjs";function M(g){const t=Math.floor((Date.now()-g)/1e3);return t<60?"just now":t<3600?`${Math.floor(t/60)}m ago`:t<86400?`${Math.floor(t/3600)}h ago`:`${Math.floor(t/86400)}d ago`}function z(g,t){const a=new Set;if(e.existsSync(g))for(const i of e.readdirSync(g).filter(y=>y.endsWith(".json")))try{(JSON.parse(e.readFileSync(l.join(g,i),"utf8")).capabilitiesCovered||[]).forEach(E=>a.add(E))}catch{}return{covered:t.filter(i=>a.has(i)),uncovered:t.filter(i=>!a.has(i))}}async function X(g=[]){const t=g.includes("--json"),a=process.cwd(),i=l.join(a,"inferno"),y=l.join(a,".ai-memory");t||L("status"),!e.existsSync(i)&&!e.existsSync(y)&&(t&&(console.log(JSON.stringify({ok:!1,error:"not_initialized",hint:"Run: infernoflow init"},null,2)),process.exit(1)),O("not initialized \u2014 neither .ai-memory/ nor inferno/ found","Run: infernoflow init"),console.log(),process.exit(1));const E=l.join(y,"amp.json"),J=l.join(i,"config.json"),m=l.join(i,"contract.json"),U=e.existsSync(E)||e.existsSync(y),D=(()=>{try{return JSON.parse(e.readFileSync(J,"utf8")).mode||null}catch{return null}})();if(U||D==="memory"||!e.existsSync(m)&&e.existsSync(J)){const o=_(a),s=o.filter(d=>d.type==="gotcha").length,c=o.filter(d=>d.type==="decision").length,p=o.filter(d=>d.type==="attempt").length,f=o[o.length-1];if(t){console.log(JSON.stringify({ok:!0,mode:"memory",entries:o.length,gotchas:s,decisions:c,attempts:p,lastEntry:f?f.ts:null},null,2));return}$("Session memory"),console.log(` ${n(I(a))}`),console.log(` ${n("entries")} ${r(String(o.length))}`),console.log(` ${n("gotchas")} ${r(String(s))}`),console.log(` ${n("decisions")} ${r(String(c))}`),console.log(` ${n("attempts")} ${r(String(p))}`),f&&console.log(` ${n("last entry")} ${n(M(new Date(f.ts).getTime()))}`),console.log(),o.length===0?console.log(` ${u("\u25CF")} ${r(u("empty"))} ${n("\u2014 log your first gotcha:")} ${v('infernoflow log "..." --type gotcha')}`):console.log(` ${h("\u25CF")} ${r(h("ready"))} ${n("\u2014 run")} ${v("infernoflow recap")} ${n("for the full session summary")}`),e.existsSync(m)?console.log():console.log(`
2
2
  ${n("Want capability contracts + CI gates? Run:")} ${v("infernoflow init --mode full")}
3
- `);return}e.existsSync(m)||(t&&(console.log(JSON.stringify({ok:!1,error:"contract_not_found"},null,2)),process.exit(1)),b("contract.json not found"),console.log(),process.exit(1));const S=JSON.parse(e.readFileSync(m,"utf8")),w=S.capabilities||[],k=e.statSync(m),C=l.join(i,"scenarios"),N=l.join(i,"CHANGELOG.md"),F=l.join(i,"capabilities.json"),{covered:H,uncovered:j}=_(C,w),G=e.existsSync(N)&&/##\s+Unreleased/i.test(e.readFileSync(N,"utf8")),x=[];j.length>0&&x.push(`${j.length} capabilities without scenario coverage`),G||x.push("CHANGELOG missing ## Unreleased section");const O=x.length===0;if(t){const o={ok:O,driftReasons:x,project:{policyId:S.policyId||null,policyVersion:S.policyVersion||null,lastChange:M(k.mtimeMs)},capabilities:{total:w.length,uncovered:j},changelog:{hasUnreleased:G}};console.log(JSON.stringify(o,null,2)),process.exit(O?0:1)}O||($("Drift"),x.forEach(o=>console.log(` ${u("\u26A0")} ${o}`))),$("Project"),console.log(` ${n("policy")} ${r(S.policyId||"\u2014")}`),console.log(` ${n("version")} ${r("v"+(S.policyVersion||"?"))}`),console.log(` ${n("last change")} ${n(M(k.mtimeMs))}`),$(`Capabilities ${n("("+w.length+")")}`);let P={};if(e.existsSync(F))try{(JSON.parse(e.readFileSync(F,"utf8")).capabilities||[]).forEach(s=>{P[s.id]=s})}catch{}if(w.forEach(o=>{const s=P[o],p=H.includes(o)?h("\u2714"):R("\u2718"),a=s?.title?n(` \u2014 ${s.title}`):"",d=s?.since?n(` [${s.since}]`):"";console.log(` ${p} ${V(o)}${a}${d}`)}),j.length>0?console.log(`
3
+ `);return}e.existsSync(m)||(t&&(console.log(JSON.stringify({ok:!1,error:"contract_not_found"},null,2)),process.exit(1)),O("contract.json not found"),console.log(),process.exit(1));const S=JSON.parse(e.readFileSync(m,"utf8")),w=S.capabilities||[],k=e.statSync(m),C=l.join(i,"scenarios"),N=l.join(i,"CHANGELOG.md"),F=l.join(i,"capabilities.json"),{covered:H,uncovered:j}=z(C,w),G=e.existsSync(N)&&/##\s+Unreleased/i.test(e.readFileSync(N,"utf8")),x=[];j.length>0&&x.push(`${j.length} capabilities without scenario coverage`),G||x.push("CHANGELOG missing ## Unreleased section");const b=x.length===0;if(t){const o={ok:b,driftReasons:x,project:{policyId:S.policyId||null,policyVersion:S.policyVersion||null,lastChange:M(k.mtimeMs)},capabilities:{total:w.length,uncovered:j},changelog:{hasUnreleased:G}};console.log(JSON.stringify(o,null,2)),process.exit(b?0:1)}b||($("Drift"),x.forEach(o=>console.log(` ${u("\u26A0")} ${o}`))),$("Project"),console.log(` ${n("policy")} ${r(S.policyId||"\u2014")}`),console.log(` ${n("version")} ${r("v"+(S.policyVersion||"?"))}`),console.log(` ${n("last change")} ${n(M(k.mtimeMs))}`),$(`Capabilities ${n("("+w.length+")")}`);let P={};if(e.existsSync(F))try{(JSON.parse(e.readFileSync(F,"utf8")).capabilities||[]).forEach(s=>{P[s.id]=s})}catch{}if(w.forEach(o=>{const s=P[o],p=H.includes(o)?h("\u2714"):R("\u2718"),f=s?.title?n(` \u2014 ${s.title}`):"",d=s?.since?n(` [${s.since}]`):"";console.log(` ${p} ${W(o)}${f}${d}`)}),j.length>0?console.log(`
4
4
  ${u("\u26A0")} ${j.length} capability(ies) lack scenario coverage`):console.log(`
5
- ${h("\u2714")} All capabilities have scenario coverage`),$("Scenarios"),e.existsSync(C)){const o=e.readdirSync(C).filter(s=>s.endsWith(".json"));o.length===0?A("No scenario files \u2014 add .json files to inferno/scenarios/"):o.forEach(s=>{try{const c=JSON.parse(e.readFileSync(l.join(C,s),"utf8")),p=c.steps?.length||0,a=(c.capabilitiesCovered||[]).length;console.log(` ${h("\u2714")} ${v(s)} ${n(`\u2014 ${p} steps, ${a} caps covered`)}`)}catch{console.log(` ${R("\u2718")} ${v(s)} ${n("\u2014 invalid JSON")}`)}})}else A("scenarios/ directory not found");if($("Changelog"),e.existsSync(N)){const o=e.readFileSync(N,"utf8");/##\s+Unreleased/i.test(o)?L("Has ## Unreleased section"):b("Missing ## Unreleased section"),o.split(`
6
- `).filter(c=>/^##\s/.test(c)).slice(0,3).forEach(c=>console.log(` ${n(c)}`))}else b("inferno/CHANGELOG.md not found");console.log(),console.log(O?` ${h("\u25CF")} ${r(h("ready"))} ${n("\u2014 run infernoflow check for full validation")}`:` ${u("\u25CF")} ${r(u("needs attention"))} ${n("\u2014 run infernoflow check for details")}`),console.log()}export{K as statusCommand};
5
+ ${h("\u2714")} All capabilities have scenario coverage`),$("Scenarios"),e.existsSync(C)){const o=e.readdirSync(C).filter(s=>s.endsWith(".json"));o.length===0?A("No scenario files \u2014 add .json files to inferno/scenarios/"):o.forEach(s=>{try{const c=JSON.parse(e.readFileSync(l.join(C,s),"utf8")),p=c.steps?.length||0,f=(c.capabilitiesCovered||[]).length;console.log(` ${h("\u2714")} ${v(s)} ${n(`\u2014 ${p} steps, ${f} caps covered`)}`)}catch{console.log(` ${R("\u2718")} ${v(s)} ${n("\u2014 invalid JSON")}`)}})}else A("scenarios/ directory not found");if($("Changelog"),e.existsSync(N)){const o=e.readFileSync(N,"utf8");/##\s+Unreleased/i.test(o)?V("Has ## Unreleased section"):O("Missing ## Unreleased section"),o.split(`
6
+ `).filter(c=>/^##\s/.test(c)).slice(0,3).forEach(c=>console.log(` ${n(c)}`))}else O("inferno/CHANGELOG.md not found");console.log(),console.log(b?` ${h("\u25CF")} ${r(h("ready"))} ${n("\u2014 run infernoflow check for full validation")}`:` ${u("\u25CF")} ${r(u("needs attention"))} ${n("\u2014 run infernoflow check for details")}`),console.log()}export{X as statusCommand};