infernoflow 0.38.16 → 0.40.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog — infernoflow
2
2
 
3
+ ## 0.40.0 — 2026-05-02
4
+
5
+ ### Added
6
+ - **Experimental Supabase JWT auth (`infernoflow login --browser`)** — opens your browser to Supabase's GitHub OAuth, captures the session via a one-shot localhost callback (port range 47655–47659), and stores the JWT + refresh token. After this, cloud writes are authenticated under your `auth.uid()` and the per-user RLS policy `auth.uid() = user_id` becomes enforceable. Opt-in until end-to-end verified against a real Supabase project.
7
+ - **Automatic token refresh** — `pushEntry` calls `refreshSessionIfNeeded()` before each write, hitting `/auth/v1/token?grant_type=refresh_token` within 5 minutes of expiry. Falls back silently to anon-key dev mode if refresh fails so local logging is never blocked.
8
+ - **Tagged credentials schema** — `mode: "supabase" | "device-flow"` with full backward-compatible reads of pre-v0.40 single-`access_token` files. New `getSupabaseAccessToken()` helper for synchronous JWT lookup with expiry awareness.
9
+ - **`doctor` full credential-state recognition** — distinct messages for not-logged-in, supabase-authenticated, identity-only device-flow, and legacy schema (with re-login nudge).
10
+
11
+ ### Changed
12
+ - **Default `infernoflow login` is unchanged** — still GitHub Device Flow, still works exactly as it did in v0.38–0.39. The new browser-OAuth path is opt-in via `--browser` until the Supabase project setup (allow-list URLs, GitHub provider, schema apply) is confirmed working end-to-end.
13
+ - **`scripts/supabase-schema.sql`** — `user_id` now defaults to `auth.uid()` so authenticated writes (when you opt in) auto-populate it. Schema is fully idempotent. Both RLS policies retained: "Users own their entries" enforces the authenticated path; "Anon can insert (dev mode)" keeps the device-flow path working.
14
+ - **`whoami`** prints the auth mode (Supabase JWT vs identity-only) and JWT expiry when present.
15
+
16
+ ### Required setup before `--browser` works
17
+ On your Supabase project, one-time:
18
+ 1. Authentication → Providers → enable GitHub.
19
+ 2. Authentication → URL Configuration → Redirect URLs: add `http://localhost:47655/callback` through `http://localhost:47659/callback`.
20
+ 3. SQL Editor → paste and run `scripts/supabase-schema.sql` (idempotent).
21
+
22
+ Then: `infernoflow logout && infernoflow login --browser`. If anything misbehaves, plain `infernoflow login` still works.
23
+
24
+ ## 0.39.0 — 2026-05-02
25
+
26
+ ### Added
27
+ - **Memory-mode-aware `status`** — no longer prints "✘ contract.json not found" as if something is broken. In memory mode (the default since v0.37.0) it shows entries/gotchas/decisions/attempts/last-entry plus a next-step prompt. JSON mode equivalent.
28
+ - **`doctor` router-integrity check** — scans `bin/infernoflow.mjs` for every imported command module and verifies the file exists. Catches the "vapor commands" class of regression where the CLI advertises commands whose implementation was deleted.
29
+ - **`doctor` `.gitignore` sanity check** — flags missing `node_modules/` exclusion (the kind of thing that lets 5,200+ dependency files leak into git).
30
+ - **`doctor` correct cloud-credential detection** — reads `~/.infernoflow/credentials.json` (the real path), shows logged-in user, and warns when token has expired.
31
+ - **Honest cloud documentation** — `lib/cloud/supabase.mjs` header, `login` success message, and `scripts/supabase-schema.sql` now accurately describe the anonymous-token write model rather than implying authenticated RLS that isn't enforced.
32
+
33
+ ### Changed
34
+ - **README repositioned** to lead with session memory (the actual product per `package.json` description and the strategic plan), with capability contracts as a secondary track. Added 5-command core table, badges, MCP-tools list expanded from 4 to the actual 9 tools registered by the server, cloud sync section with auth-model disclosure.
35
+ - **`doctor` memory-mode awareness** — scenarios/changelog/CONTEXT.md checks now short-circuit to "n/a in memory mode" instead of warning, since none of those exist by design in memory-mode projects.
36
+
37
+ ### Fixed
38
+ - **`doctor` crashed on launch** with `Error: The requested module '../ai/providerRouter.mjs' does not provide an export named 'detectAvailableProviders'`. Added the missing function (env-var-based provider detection).
39
+ - **`uninstall` crashed** with `hooks.every is not a function` when `.cursor/hooks.json` used the newer object-keyed-by-event format instead of a flat array. Now normalises both shapes.
40
+
3
41
  ## 0.38.16 — 2026-05-02
4
42
 
5
43
  ### Fixed
package/README.md CHANGED
@@ -1,97 +1,142 @@
1
- # 🔥 infernoflow
2
- > The forge for liquid code — keep capabilities, contracts, and docs in sync with your codebase.
3
-
4
- ## What it does
5
- infernoflow ensures that when your code changes, your **capability contracts** and **documentation** stay in sync. It prevents semantic drift — where code evolves but no one knows what the system can actually do.
6
-
7
- ## Install
8
- ```bash
9
- npm install -g infernoflow
10
- # or:
11
- npx infernoflow init
12
- ```
13
-
14
- ## Quick Start
15
- ```bash
16
- npx infernoflow init
17
- npx infernoflow install-cursor-hooks # installs MCP server + .cursor/mcp.json
18
- # Restart Cursor → Settings → MCP → infernoflow: 4 tools enabled
19
- infernoflow status
20
- infernoflow suggest "added email notifications"
21
- infernoflow check
22
- ```
23
-
24
- ## Cursor MCP Integration (recommended)
25
-
26
- After running `install-cursor-hooks`, infernoflow registers as an MCP server inside Cursor. No copy/paste — Cursor calls infernoflow tools directly in chat.
27
-
28
- ### Setup
29
- ```bash
30
- infernoflow install-cursor-hooks
31
- # Restart Cursor
32
- # Settings → MCP → infernoflow: 4 tools enabled
33
- ```
34
-
35
- ### MCP tools
36
-
37
- | Tool | What it does |
38
- |---|---|
39
- | `infernoflow_run` | Generates a task prompt from your contract |
40
- | `infernoflow_apply` | Applies the JSON response updates contract + CHANGELOG |
41
- | `infernoflow_check` | Validates contract sync |
42
- | `infernoflow_status` | Shows contract health |
43
-
44
- ### Workflow in Cursor chat
45
- ```
46
- You: Use infernoflow_run with task "add search functionality"
47
- Cursor: [calls infernoflow_run → returns prompt]
48
- Cursor: [generates JSON]
49
- Cursor: [calls infernoflow_apply]
50
- contract.json, capabilities.json, CHANGELOG.md updated + validated
51
- ```
52
-
53
- ### Terminal fallback (without MCP)
54
- ```bash
55
- infernoflow run "add search functionality"
56
- # writes inferno/agent-prompt.md and waits
57
- # paste prompt into Cursor/Claude → save JSON to inferno/agent-response.json
58
- # infernoflow picks it up and applies automatically
59
- ```
60
-
61
- ## Commands
62
-
63
- | Command | Description |
64
- |---|---|
65
- | `infernoflow init` | Scaffold inferno/ in your project |
66
- | `infernoflow install-cursor-hooks` | MCP server + hooks + .cursor/mcp.json |
67
- | `infernoflow install-vscode-copilot-hooks` | VS Code + Copilot hooks (Preview) |
68
- | `infernoflow status` | Contract health at a glance |
69
- | `infernoflow check` | Full validation |
70
- | `infernoflow suggest` | AI-powered contract update |
71
- | `infernoflow run` | One-command flow with rollback |
72
- | `infernoflow implement` | Generate coding agent prompts |
73
- | `infernoflow context` | Build AI session context |
74
- | `infernoflow doc-gate` | Fail if docs not updated |
75
- | `infernoflow pr-impact` | Analyze PR capability drift |
76
-
77
- ## CI Integration
78
- ```yaml
79
- - name: infernoflow check
80
- run: npx infernoflow check --json
81
- - name: infernoflow doc-gate
82
- run: npx infernoflow doc-gate --json
83
- ```
84
-
85
- ## Troubleshooting
86
-
87
- - **MCP not showing in Cursor** — restart Cursor completely after install-cursor-hooks
88
- - `ide_agent_bridge_not_configured` — use MCP tools in Cursor chat instead
89
- - **infernoflow not found** use `npx infernoflow` or install globally
90
- - **PowerShell scripts disabled** — run `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass`
91
-
92
- ## Why infernoflow?
93
-
94
- AI-assisted development moves fast. Code changes daily. But what does the system *actually do*? infernoflow keeps the answer current — automatically.
95
-
96
- ## License
97
- MIT
1
+ # 🔥 infernoflow
2
+
3
+ > Persistent memory for AI coding sessions. Captures what agents can't infer from code: gotchas, decisions, dead ends. Replays it into your next AI chat so you stop re-derived context every time.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/infernoflow.svg?color=orange)](https://www.npmjs.com/package/infernoflow)
6
+ [![npm downloads](https://img.shields.io/npm/dw/infernoflow.svg?color=orange)](https://www.npmjs.com/package/infernoflow)
7
+
8
+ ## The 60-second pitch
9
+
10
+ Every new Copilot/Cursor/Claude session starts cold. The agent re-reads your code, ignores constraints that aren't expressed there, and often re-makes the same wrong move someone else made yesterday. infernoflow is a small CLI that captures *those things* — the API quirks, the failed approaches, the architectural decisions — and replays them into the next AI session as a clean handoff. One command. No paste, no copy, no manual setup.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install -g infernoflow
16
+ # or zero-install:
17
+ npx infernoflow init
18
+ ```
19
+
20
+ Zero npm dependencies. Works on Node ≥ 18. Windows, macOS, Linux.
21
+
22
+ ## Quick start (90 seconds)
23
+
24
+ ```bash
25
+ cd your-project
26
+ infernoflow init # 30-second setup, asks for your first gotcha
27
+ infernoflow log "API returns 202 not 200" --type gotcha
28
+ infernoflow log "use polling not websocket for progress" --type decision
29
+ infernoflow ask "API" # search your memory
30
+ infernoflow switch --copy # generate handoff, copy to clipboard
31
+ # paste into your next Cursor/Copilot/Claude chat — the agent picks up everything
32
+ ```
33
+
34
+ ## The 5-command core
35
+
36
+ These five cover 90% of usage:
37
+
38
+ | Command | What it does |
39
+ |---|---|
40
+ | `infernoflow log "..."` | Remember a gotcha, decision, attempt, or note. `--type gotcha\|decision\|attempt\|preference` |
41
+ | `infernoflow ask "..."` | Search your memory by keyword. Gotchas surface first. |
42
+ | `infernoflow switch` | Generate a handoff for your next AI session. `--copy` puts it on the clipboard. |
43
+ | `infernoflow recap` | End-of-session summary with health score and unlogged-change detection. |
44
+ | `infernoflow status` | Quick session-memory health check. |
45
+
46
+ Run `infernoflow commands` for the full grouped list (51 commands across Session Memory, Code Analysis, Workflow, Cloud, Setup, Advanced).
47
+
48
+ ## Auto-context for AI agents
49
+
50
+ When you run `infernoflow log`, infernoflow silently keeps these files up to date so any AI agent reading them gets your latest gotchas/decisions automatically:
51
+
52
+ - `CLAUDE.md` — picked up by Claude Code
53
+ - `.cursorrules` picked up by Cursor
54
+ - `.github/copilot-instructions.md` — picked up by GitHub Copilot
55
+
56
+ You don't have to paste anything. Set up once, every future session is better.
57
+
58
+ ## Cursor / VS Code MCP integration
59
+
60
+ ```bash
61
+ infernoflow install-cursor-hooks
62
+ # Restart Cursor → Settings → MCP → infernoflow: 4 tools enabled
63
+
64
+ # or for VS Code + Copilot (Preview):
65
+ infernoflow install-vscode-copilot-hooks
66
+ ```
67
+
68
+ After install-cursor-hooks, your AI agent can call infernoflow directly in chat:
69
+
70
+ | MCP tool | What it does |
71
+ |---|---|
72
+ | `infernoflow_run` | Generate a task prompt from your contract |
73
+ | `infernoflow_apply` | Apply the JSON response — updates contract + CHANGELOG |
74
+ | `infernoflow_check` | Validate contract sync |
75
+ | `infernoflow_status` | Show contract health |
76
+ | `infernoflow_context` | Generate AI-ready context for a new session |
77
+ | `infernoflow_implement` | Step-by-step code prompt for a specific task |
78
+ | `infernoflow_review` | Pre-merge capability drift check on the current branch |
79
+ | `infernoflow_git_drift` | Detect capabilities affected by recent commits |
80
+ | `infernoflow_scan_ui` | Detect UI / design-token changes vs contract |
81
+
82
+ ## Cloud sync (optional)
83
+
84
+ ```bash
85
+ infernoflow login # GitHub Device Flow — no PKCE, no callback server
86
+ infernoflow whoami
87
+ ```
88
+
89
+ Once logged in, every `infernoflow log` quietly mirrors the entry to a Supabase project so your memory survives across machines. Push is fire-and-forget; local always succeeds even if cloud is down.
90
+
91
+ > **Auth model (v0.38.x):** the cloud currently uses anonymous-key writes with a per-user `user_token` column. Anyone with the public anon key can write rows — fine for solo dev, not yet a security boundary. The schema is forward-compatible with authenticated mode; see `scripts/supabase-schema.sql` for the migration path.
92
+
93
+ ## Capability contracts (advanced)
94
+
95
+ The "memory" track above (Tier 1) is what most users want. infernoflow also ships a heavier "contracts" track for teams that want machine-checked guarantees about what their codebase *does*:
96
+
97
+ ```bash
98
+ infernoflow init --mode full # set up contract.json, capabilities, scenarios
99
+ infernoflow scan # AST-walk to discover capabilities
100
+ infernoflow freeze CreateItem # mark a capability as protected — AI won't modify it
101
+ infernoflow impact CreateItem # blast radius before changes
102
+ infernoflow check # CI gate
103
+ ```
104
+
105
+ Most users don't need this. If you do, run `infernoflow demo` for an interactive walkthrough.
106
+
107
+ ## CI integration
108
+
109
+ ```yaml
110
+ - name: infernoflow check
111
+ run: npx infernoflow check --json
112
+ - name: infernoflow doc-gate
113
+ run: npx infernoflow doc-gate --json
114
+ ```
115
+
116
+ Or use the GitHub Action:
117
+
118
+ ```yaml
119
+ - uses: ronmiz/infernoflow-action@v1
120
+ ```
121
+
122
+ ## Troubleshooting
123
+
124
+ - **MCP not showing in Cursor** — fully quit and relaunch Cursor after `install-cursor-hooks`.
125
+ - **`infernoflow not found`** — use `npx infernoflow` or `npm install -g infernoflow`.
126
+ - **PowerShell script execution blocked** — `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass`.
127
+ - **`infernoflow doctor`** — runs a full diagnostic if anything looks wrong.
128
+ - **Box-drawing chars look broken in PowerShell** — should auto-fall-back to ASCII; if not, you're on a non-WT_SESSION shell, please open an issue.
129
+
130
+ ## Why infernoflow?
131
+
132
+ Code changes daily. But what does the system *actually do*? What did someone try last week that didn't work? What invariants are load-bearing? infernoflow keeps the answer current — and feeds it to your AI agent so it stops re-deriving from scratch.
133
+
134
+ ## License
135
+
136
+ MIT
137
+
138
+ ## Links
139
+
140
+ - [GitHub](https://github.com/ronmiz/infernoflow)
141
+ - [npm](https://www.npmjs.com/package/infernoflow)
142
+ - [Issues](https://github.com/ronmiz/infernoflow/issues)
@@ -1 +1 @@
1
- import{detectIdeContext as r}from"./ideDetection.mjs";async function l(d="auto",i="auto"){const a=String(d||"auto").toLowerCase(),e=r(i),t=[...e.reasonCodes];return a==="local"?(t.push("LOCAL_PROVIDER_SELECTED"),{providerRequested:a,providerResolved:"local",ideDetected:e.ideDetected,agentAvailable:e.agentAvailable,reasonCodes:t}):a==="prompt"?(t.push("PROMPT_PROVIDER_SELECTED"),{providerRequested:a,providerResolved:"prompt",ideDetected:e.ideDetected,agentAvailable:e.agentAvailable,reasonCodes:t}):a==="agent"?e.agentAvailable?(t.push("IDE_AGENT_SELECTED"),{providerRequested:a,providerResolved:"agent",ideDetected:e.ideDetected,agentAvailable:e.agentAvailable,reasonCodes:t}):(t.push("EXPLICIT_AGENT_REQUIRED"),{providerRequested:a,providerResolved:"none",ideDetected:e.ideDetected,agentAvailable:e.agentAvailable,reasonCodes:t,error:"agent_unavailable"}):e.agentAvailable?(t.push("IDE_AGENT_SELECTED"),{providerRequested:"auto",providerResolved:"agent",ideDetected:e.ideDetected,agentAvailable:e.agentAvailable,reasonCodes:t}):(t.push("FALLBACK_PROMPT_MODE"),{providerRequested:"auto",providerResolved:"prompt",ideDetected:e.ideDetected,agentAvailable:e.agentAvailable,reasonCodes:t})}export{l as resolveProvider};
1
+ import{detectIdeContext as i}from"./ideDetection.mjs";async function d(o="auto",r="auto"){const a=String(o||"auto").toLowerCase(),e=i(r),t=[...e.reasonCodes];return a==="local"?(t.push("LOCAL_PROVIDER_SELECTED"),{providerRequested:a,providerResolved:"local",ideDetected:e.ideDetected,agentAvailable:e.agentAvailable,reasonCodes:t}):a==="prompt"?(t.push("PROMPT_PROVIDER_SELECTED"),{providerRequested:a,providerResolved:"prompt",ideDetected:e.ideDetected,agentAvailable:e.agentAvailable,reasonCodes:t}):a==="agent"?e.agentAvailable?(t.push("IDE_AGENT_SELECTED"),{providerRequested:a,providerResolved:"agent",ideDetected:e.ideDetected,agentAvailable:e.agentAvailable,reasonCodes:t}):(t.push("EXPLICIT_AGENT_REQUIRED"),{providerRequested:a,providerResolved:"none",ideDetected:e.ideDetected,agentAvailable:e.agentAvailable,reasonCodes:t,error:"agent_unavailable"}):e.agentAvailable?(t.push("IDE_AGENT_SELECTED"),{providerRequested:"auto",providerResolved:"agent",ideDetected:e.ideDetected,agentAvailable:e.agentAvailable,reasonCodes:t}):(t.push("FALLBACK_PROMPT_MODE"),{providerRequested:"auto",providerResolved:"prompt",ideDetected:e.ideDetected,agentAvailable:e.agentAvailable,reasonCodes:t})}function l(o){return{anthropic:!!process.env.ANTHROPIC_API_KEY,openai:!!process.env.OPENAI_API_KEY,gemini:!!(process.env.GOOGLE_AI_API_KEY||process.env.GEMINI_API_KEY),openrouter:!!process.env.OPENROUTER_API_KEY}}export{l as detectAvailableProviders,d as resolveProvider};
@@ -1,2 +1,2 @@
1
- import*as e from"node:fs";import*as i from"node:path";import*as o from"node:os";const n=i.join(o.homedir(),".infernoflow"),t=i.join(n,"credentials.json");function c(){try{return e.existsSync(t)?JSON.parse(e.readFileSync(t,"utf8")):null}catch{return null}}function a(r){e.existsSync(n)||e.mkdirSync(n,{recursive:!0}),e.writeFileSync(t,JSON.stringify(r,null,2)+`
2
- `,{mode:384})}function f(){try{return e.existsSync(t)&&e.unlinkSync(t),!0}catch{return!1}}function u(){const r=c();if(!r?.access_token)return!1;if(r.expires_at){const s=new Date(r.expires_at).getTime();if(Date.now()>s-6e4)return!1}return!0}export{f as deleteCredentials,u as isLoggedIn,c as readCredentials,a as writeCredentials};
1
+ import*as r from"node:fs";import*as i from"node:path";import*as c from"node:os";const n=i.join(c.homedir(),".infernoflow"),t=i.join(n,"credentials.json");function o(){try{return r.existsSync(t)?JSON.parse(r.readFileSync(t,"utf8")):null}catch{return null}}function u(e){r.existsSync(n)||r.mkdirSync(n,{recursive:!0}),r.writeFileSync(t,JSON.stringify(e,null,2)+`
2
+ `,{mode:384})}function a(){try{return r.existsSync(t)&&r.unlinkSync(t),!0}catch{return!1}}function f(){const e=o();return e?!!(e.mode==="supabase"&&e.access_token||e.mode==="device-flow"&&e.github_access_token||!e.mode&&e.access_token):!1}function l(){const e=o();if(!e||e.mode!=="supabase"||!e.access_token)return null;if(e.expires_at){const s=Date.parse(e.expires_at);if(!Number.isNaN(s)&&Date.now()>s-6e4)return null}return e.access_token}export{a as deleteCredentials,l as getSupabaseAccessToken,f as isLoggedIn,o as readCredentials,u as writeCredentials};
@@ -1 +1 @@
1
- import*as d from"node:https";const o=process.env.INFERNOFLOW_SUPABASE_URL||"https://vscesbbtmrsctfroigyx.supabase.co",l=process.env.INFERNOFLOW_SUPABASE_ANON_KEY||"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZzY2VzYmJ0bXJzY3Rmcm9pZ3l4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc0ODAxMjcsImV4cCI6MjA5MzA1NjEyN30.4WCXr0aGBlqC2m29DnlCSu5qKl0L-fDQoaV9AGu8-68";function u(t,e,r,n){return new Promise((h,m)=>{const c=new URL(e),a=r?JSON.stringify(r):null,y={hostname:c.hostname,port:443,path:c.pathname+c.search,method:t,headers:{"Content-Type":"application/json",Accept:"application/json","User-Agent":"infernoflow-cli",apikey:l,...n,...a?{"Content-Length":Buffer.byteLength(a)}:{}}},s=d.request(y,i=>{let p="";i.on("data",A=>p+=A),i.on("end",()=>{try{h({status:i.statusCode,body:JSON.parse(p)})}catch{h({status:i.statusCode,body:p})}})});s.on("error",m),s.setTimeout(8e3,()=>{s.destroy(new Error("timeout"))}),a&&s.write(a),s.end()})}async function f(t,e,r){try{const n={project_id:r,user_token:e,ts:t.ts,type:t.type||"note",summary:t.summary,result:t.result||null,source:t.source||null,auto:t.auto||!1,agent:t.agent||null};await u("POST",`${o}/rest/v1/entries`,n,{Authorization:`Bearer ${l}`,apikey:l,Prefer:"return=minimal"})}catch{}}async function S(t){return await u("POST",`${o}/auth/v1/token?grant_type=pkce`,{auth_code:t},{})}function I(t,e){const r=new URLSearchParams({provider:"github",redirect_to:e,state:t});return`${o}/auth/v1/authorize?${r.toString()}`}async function E(t){const e=await u("GET",`${o}/auth/v1/user`,null,{Authorization:`Bearer ${t}`});return e.status===200?e.body:null}async function O(t,e){const r=new URLSearchParams({project_id:`eq.${e}`,order:"ts.asc",limit:"10000"}),n=await u("GET",`${o}/rest/v1/entries?${r.toString()}`,null,{Authorization:`Bearer ${t}`});return n.status===200&&Array.isArray(n.body)?n.body:[]}export{l as SUPABASE_ANON_KEY,o as SUPABASE_URL,S as exchangeCodeForSession,I as getOAuthUrl,E as getUser,O as pullEntries,f as pushEntry};
1
+ import*as A from"node:https";import{readCredentials as S,writeCredentials as w,getSupabaseAccessToken as d}from"./credentials.mjs";const o=process.env.INFERNOFLOW_SUPABASE_URL||"https://vscesbbtmrsctfroigyx.supabase.co",i=process.env.INFERNOFLOW_SUPABASE_ANON_KEY||"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZzY2VzYmJ0bXJzY3Rmcm9pZ3l4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzc0ODAxMjcsImV4cCI6MjA5MzA1NjEyN30.4WCXr0aGBlqC2m29DnlCSu5qKl0L-fDQoaV9AGu8-68";function c(e,r,s,t){return new Promise((n,h)=>{const f=new URL(r),u=s?JSON.stringify(s):null,_={hostname:f.hostname,port:443,path:f.pathname+f.search,method:e,headers:{"Content-Type":"application/json",Accept:"application/json","User-Agent":"infernoflow-cli",apikey:i,...t,...u?{"Content-Length":Buffer.byteLength(u)}:{}}},a=A.request(_,p=>{let l="";p.on("data",y=>l+=y),p.on("end",()=>{try{n({status:p.statusCode,body:JSON.parse(l)})}catch{n({status:p.statusCode,body:l})}})});a.on("error",h),a.setTimeout(8e3,()=>{a.destroy(new Error("timeout"))}),u&&a.write(u),a.end()})}async function m(){const e=S();if(!e||e.mode!=="supabase"||!e.refresh_token)return;const r=e.expires_at?Date.parse(e.expires_at):0,s=300*1e3;if(!(Number.isFinite(r)&&Date.now()<r-s))try{const t=await c("POST",`${o}/auth/v1/token?grant_type=refresh_token`,{refresh_token:e.refresh_token},{Authorization:`Bearer ${i}`});if(t.status!==200||!t.body?.access_token)return;const n={...e,access_token:t.body.access_token,refresh_token:t.body.refresh_token||e.refresh_token,expires_at:t.body.expires_in?new Date(Date.now()+t.body.expires_in*1e3).toISOString():e.expires_at};w(n)}catch{}}async function x(e,r,s){try{await m();const t=d(),n=!!t,h={project_id:s,ts:e.ts,type:e.type||"note",summary:e.summary,result:e.result||null,source:e.source||null,auto:e.auto||!1,agent:e.agent||null,...n?{}:{user_token:r}};await c("POST",`${o}/rest/v1/entries`,h,{Authorization:`Bearer ${n?t:i}`,apikey:i,Prefer:"return=minimal"})}catch{}}async function I(e){const r=await c("GET",`${o}/auth/v1/user`,null,{Authorization:`Bearer ${e}`});return r.status===200?r.body:null}async function k(e){await m();const r=d(),s=new URLSearchParams({project_id:`eq.${e}`,order:"ts.asc",limit:"10000"}),t=await c("GET",`${o}/rest/v1/entries?${s.toString()}`,null,{Authorization:`Bearer ${r||i}`});return t.status===200&&Array.isArray(t.body)?t.body:[]}async function g(e){return await c("POST",`${o}/auth/v1/token?grant_type=pkce`,{auth_code:e},{})}function O(e,r){const s=new URLSearchParams({provider:"github",redirect_to:r,state:e});return`${o}/auth/v1/authorize?${s.toString()}`}export{i as SUPABASE_ANON_KEY,o as SUPABASE_URL,g as exchangeCodeForSession,O as getOAuthUrl,I as getUser,k as pullEntries,x as pushEntry,m as refreshSessionIfNeeded};
@@ -1,3 +1,4 @@
1
- import*as s from"node:fs";import*as c from"node:path";import*as w from"node:os";import*as N from"node:http";import{execSync as O,spawnSync as C}from"node:child_process";import{bold as S,cyan as $,gray as y,green as m,yellow as x,red as j}from"../ui/output.mjs";import{detectAvailableProviders as k}from"../ai/providerRouter.mjs";function l(n,t){try{const o=t();return{label:n,...o}}catch(o){return{label:n,status:"error",message:o.message,fix:null}}}function a(n,t){return{status:"pass",message:n,detail:t||null,fix:null}}function u(n,t){return{status:"warn",message:n,detail:null,fix:t||null}}function p(n,t){return{status:"fail",message:n,detail:null,fix:t||null}}function v(){const n=process.version,t=parseInt(n.slice(1).split(".")[0],10);return t>=20?a(`Node.js ${n}`,"Node 20+ recommended"):t>=18?a(`Node.js ${n}`):p(`Node.js ${n} \u2014 infernoflow requires Node 18+`,"Install Node 20 from nodejs.org")}function A(){try{const n=C("infernoflow",["--version"],{encoding:"utf8",timeout:5e3});return n.status===0?a(`infernoflow v${n.stdout.trim()} installed`):p("infernoflow CLI not found on PATH","npm install -g infernoflow")}catch{return p("infernoflow CLI not found on PATH","npm install -g infernoflow")}}function E(n){try{return O("git rev-parse --git-dir",{cwd:n,stdio:"ignore"}),a("Git repository detected")}catch{return p("Not a git repository","git init && git add . && git commit -m 'init'")}}function P(n){const t=c.join(n,"inferno");return s.existsSync(t)?a("inferno/ directory exists"):p("inferno/ not found","infernoflow init")}function I(n){const t=c.join(n,"inferno");for(const o of["contract.json","capabilities.json"]){const r=c.join(t,o);if(s.existsSync(r))try{const e=(JSON.parse(s.readFileSync(r,"utf8")).capabilities||[]).length;return a(`${o} valid \u2014 ${e} capabilities`)}catch{return p(`${o} contains invalid JSON`,`Fix the JSON syntax in inferno/${o}`)}}return p("No contract.json or capabilities.json","infernoflow init")}function b(n){const t=c.join(n,"inferno","scenarios");if(!s.existsSync(t))return u("No scenarios/ directory","infernoflow init");const o=s.readdirSync(t).filter(r=>r.endsWith(".json"));return o.length?a(`${o.length} scenario file${o.length!==1?"s":""} found`):u("scenarios/ is empty","Add scenario files or run infernoflow suggest")}function G(n){const t=c.join(n,"inferno","CHANGELOG.md");return s.existsSync(t)?a("inferno/CHANGELOG.md exists"):u("No inferno/CHANGELOG.md","infernoflow init")}function T(n){const t=c.join(n,"inferno","CONTEXT.md");if(!s.existsSync(t))return u("No CONTEXT.md generated","infernoflow context");const o=(Date.now()-s.statSync(t).mtimeMs)/(1e3*60*60*24);return o>7?u(`CONTEXT.md is ${Math.round(o)} days old \u2014 may be stale`,"infernoflow context"):a(`CONTEXT.md present (${Math.round(o)}d old)`)}function _(n){const t=c.join(n,".git","hooks"),o=c.join(t,"post-commit"),r=c.join(t,"pre-push"),i=s.existsSync(o)&&s.readFileSync(o,"utf8").includes("infernoflow"),e=s.existsSync(r)&&s.readFileSync(r,"utf8").includes("infernoflow");return i&&e?a("Git hooks installed (post-commit + pre-push)"):u(i||e?"Partial git hooks installed":"Git hooks not installed","infernoflow setup --yes")}function M(n){const t=[c.join(n,".cursor","mcp.json"),c.join(n,".mcp.json"),c.join(w.homedir(),".cursor","mcp.json"),c.join(w.homedir(),"Library","Application Support","Claude","claude_desktop_config.json"),c.join(w.homedir(),"AppData","Roaming","Claude","claude_desktop_config.json")];for(const o of t)if(s.existsSync(o))try{const r=JSON.parse(s.readFileSync(o,"utf8")),i=r.mcpServers||r.mcp_servers||{};if(Object.keys(i).some(e=>e.toLowerCase().includes("inferno")))return a(`MCP server configured in ${c.basename(o)}`)}catch{}return u("MCP server not configured","infernoflow setup --yes (adds to Cursor/Claude config)")}function F(n){const t=k(n),o=Object.entries(t).filter(([,r])=>r).map(([r])=>r);return o.length?a(`AI provider${o.length!==1?"s":""}: ${o.join(", ")}`):u("No AI provider configured",`Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY, or OPENROUTER_API_KEY
1
+ import*as s from"node:fs";import*as l from"node:path";import*as h from"node:os";import*as O from"node:http";import{execSync as b,spawnSync as k}from"node:child_process";import{fileURLToPath as v}from"node:url";import{bold as C,cyan as N,gray as w,green as y,yellow as x,red as j}from"../ui/output.mjs";import{detectAvailableProviders as A}from"../ai/providerRouter.mjs";function c(n,o){try{const e=o();return{label:n,...e}}catch(e){return{label:n,status:"error",message:e.message,fix:null}}}function a(n,o){return{status:"pass",message:n,detail:o||null,fix:null}}function f(n,o){return{status:"warn",message:n,detail:null,fix:o||null}}function m(n,o){return{status:"fail",message:n,detail:null,fix:o||null}}function _(){const n=process.version,o=parseInt(n.slice(1).split(".")[0],10);return o>=20?a(`Node.js ${n}`,"Node 20+ recommended"):o>=18?a(`Node.js ${n}`):m(`Node.js ${n} \u2014 infernoflow requires Node 18+`,"Install Node 20 from nodejs.org")}function P(){try{const n=k("infernoflow",["--version"],{encoding:"utf8",timeout:5e3});return n.status===0?a(`infernoflow v${n.stdout.trim()} installed`):m("infernoflow CLI not found on PATH","npm install -g infernoflow")}catch{return m("infernoflow CLI not found on PATH","npm install -g infernoflow")}}function E(n){try{return b("git rev-parse --git-dir",{cwd:n,stdio:"ignore"}),a("Git repository detected")}catch{return m("Not a git repository","git init && git add . && git commit -m 'init'")}}function I(n){const o=l.join(n,"inferno");return s.existsSync(o)?a("inferno/ directory exists"):m("inferno/ not found","infernoflow init")}function T(n){const o=l.join(n,"inferno");if((()=>{try{return JSON.parse(s.readFileSync(l.join(o,"config.json"),"utf8"))}catch{return{}}})().mode==="memory"){const i=l.join(o,"sessions.jsonl");if(!s.existsSync(i))return a("Memory mode \u2014 sessions.jsonl will be created on first log");let t=0;try{t=s.readFileSync(i,"utf8").split(`
2
+ `).filter(Boolean).length}catch{}return a(`Memory mode \u2014 ${t} session entr${t===1?"y":"ies"}`)}for(const i of["contract.json","capabilities.json"]){const t=l.join(o,i);if(s.existsSync(t))try{const u=(JSON.parse(s.readFileSync(t,"utf8")).capabilities||[]).length;return a(`${i} valid \u2014 ${u} capabilities`)}catch{return m(`${i} contains invalid JSON`,`Fix the JSON syntax in inferno/${i}`)}}return m("No contract.json/capabilities.json (and not in memory mode)","infernoflow init or infernoflow init --mode full")}function S(n){try{return JSON.parse(s.readFileSync(l.join(n,"inferno","config.json"),"utf8")).mode==="memory"}catch{return!1}}function G(n){if(S(n))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const o=l.join(n,"inferno","scenarios");if(!s.existsSync(o))return f("No scenarios/ directory","infernoflow init");const e=s.readdirSync(o).filter(i=>i.endsWith(".json"));return e.length?a(`${e.length} scenario file${e.length!==1?"s":""} found`):f("scenarios/ is empty","Add scenario files or run infernoflow suggest")}function F(n){if(S(n))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const o=l.join(n,"inferno","CHANGELOG.md");return s.existsSync(o)?a("inferno/CHANGELOG.md exists"):f("No inferno/CHANGELOG.md","infernoflow init")}function M(n){if(S(n))return{status:"info",message:"n/a in memory mode (CLAUDE.md is auto-maintained)",detail:null,fix:null};const o=l.join(n,"inferno","CONTEXT.md");if(!s.existsSync(o))return f("No CONTEXT.md generated","infernoflow context");const e=(Date.now()-s.statSync(o).mtimeMs)/(1e3*60*60*24);return e>7?f(`CONTEXT.md is ${Math.round(e)} days old \u2014 may be stale`,"infernoflow context"):a(`CONTEXT.md present (${Math.round(e)}d old)`)}function D(n){const o=l.join(n,".git","hooks"),e=l.join(o,"post-commit"),i=l.join(o,"pre-push"),t=s.existsSync(e)&&s.readFileSync(e,"utf8").includes("infernoflow"),r=s.existsSync(i)&&s.readFileSync(i,"utf8").includes("infernoflow");return t&&r?a("Git hooks installed (post-commit + pre-push)"):f(t||r?"Partial git hooks installed":"Git hooks not installed","infernoflow setup --yes")}function L(n){const o=[l.join(n,".cursor","mcp.json"),l.join(n,".mcp.json"),l.join(h.homedir(),".cursor","mcp.json"),l.join(h.homedir(),"Library","Application Support","Claude","claude_desktop_config.json"),l.join(h.homedir(),"AppData","Roaming","Claude","claude_desktop_config.json")];for(const e of o)if(s.existsSync(e))try{const i=JSON.parse(s.readFileSync(e,"utf8")),t=i.mcpServers||i.mcp_servers||{};if(Object.keys(t).some(r=>r.toLowerCase().includes("inferno")))return a(`MCP server configured in ${l.basename(e)}`)}catch{}return f("MCP server not configured","infernoflow setup --yes (adds to Cursor/Claude config)")}function R(n){const o=A(n),e=Object.entries(o).filter(([,i])=>i).map(([i])=>i);return e.length?a(`AI provider${e.length!==1?"s":""}: ${e.join(", ")}`):f("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 D(){return new Promise(n=>{const t=N.get({hostname:"localhost",port:11434,path:"/api/tags",timeout:1500},o=>{n(a("Ollama running on localhost:11434"))});t.on("error",()=>n({status:"info",message:"Ollama not running (optional)",fix:"ollama serve",detail:null})),t.on("timeout",()=>{t.destroy(),n({status:"info",message:"Ollama not running (optional)",fix:null,detail:null})})})}function L(n){const t=c.join(n,"inferno","integrations.json");if(!s.existsSync(t))return{status:"info",message:"Cloud sync not configured (optional)",fix:"infernoflow cloud init",detail:null};try{return JSON.parse(s.readFileSync(t,"utf8")).cloud?.token?a("Cloud sync configured"):{status:"info",message:"Cloud sync not configured (optional)",fix:"infernoflow cloud init",detail:null}}catch{return{status:"info",message:"Cloud sync not configured (optional)",fix:null,detail:null}}}function H(n,t){const o=n.filter(i=>i.status==="warn"&&i.fix),r=[];for(const i of o){const e=i.fix;if(e.startsWith("infernoflow ")){const d=e.slice(12).split(" ");C("infernoflow",d,{cwd:t,encoding:"utf8",timeout:3e4}).status===0&&r.push(i.label)}}return r}function R(n){return n==="pass"?m("\u2714"):n==="warn"?x("\u26A0"):n==="fail"?j("\u2717"):y("\xB7")}function J(n,t){const o={pass:0,warn:0,fail:0,info:0,error:0};for(const e of n)o[e.status]=(o[e.status]||0)+1;console.log(),console.log(` ${S("\u{1F525} infernoflow doctor")}`),console.log();const r=Math.max(...n.map(e=>e.label.length))+2;for(const e of n)console.log(` ${R(e.status)} ${S(e.label.padEnd(r))} ${e.message}`),e.detail&&console.log(` ${" ".repeat(r)} ${y(e.detail)}`),e.fix&&(e.status==="warn"||e.status==="fail")&&console.log(` ${" ".repeat(r)} ${$("fix:")} ${y(e.fix)}`);console.log();const i=o.fail>0?j("issues found"):o.warn>0?x("warnings"):m("all good");console.log(` ${i} \u2014 ${m(String(o.pass))} pass \xB7 ${x(String(o.warn))} warn \xB7 ${j(String(o.fail))} fail (${t}ms)`),console.log(),(o.warn>0||o.fail>0)&&(console.log(` Run ${$("infernoflow doctor --fix")} to auto-fix warnings`),console.log())}async function X(n){const t=n.slice(1),o=t.includes("--json"),r=t.includes("--fix"),i=process.cwd(),e=Date.now(),d=[l("Node.js version",()=>v()),l("infernoflow CLI",()=>A()),l("Git repository",()=>E(i)),l("inferno/ directory",()=>P(i)),l("Contract file",()=>I(i)),l("Scenarios",()=>b(i)),l("Changelog",()=>G(i)),l("CONTEXT.md",()=>T(i)),l("Git hooks",()=>_(i)),l("MCP server",()=>M(i)),l("AI providers",()=>F(i)),l("Cloud sync",()=>L(i)),await D().then(f=>({label:"Ollama (local AI)",...f}))],h=Date.now()-e;if(r){const f=H(d,i);if(f.length)return o||(console.log(),f.forEach(g=>console.log(` ${m("\u2714")} Fixed: ${g}`)),console.log()),X(["doctor","--json"])}if(o){const f={pass:0,warn:0,fail:0,info:0};d.forEach(g=>f[g.status]=(f[g.status]||0)+1),console.log(JSON.stringify({ok:f.fail===0,counts:f,results:d,elapsed:h}));return}J(d,h),d.some(f=>f.status==="fail")&&process.exit(1)}export{X as doctorCommand};
4
+ Or use VS Code with GitHub Copilot (zero config)`)}async function J(){return new Promise(n=>{const o=O.get({hostname:"localhost",port:11434,path:"/api/tags",timeout:1500},e=>{n(a("Ollama running on localhost:11434"))});o.on("error",()=>n({status:"info",message:"Ollama not running (optional)",fix:"ollama serve",detail:null})),o.on("timeout",()=>{o.destroy(),n({status:"info",message:"Ollama not running (optional)",fix:null,detail:null})})})}function H(){const n=l.join(h.homedir(),".infernoflow","credentials.json");if(!s.existsSync(n))return{status:"info",message:"Not logged in to cloud (optional)",fix:"infernoflow login",detail:null};try{const o=JSON.parse(s.readFileSync(n,"utf8")),e=o.user?.login||o.user?.name||o.user?.email||"unknown";if(o.mode==="supabase"&&o.access_token){if(o.expires_at){const i=new Date(o.expires_at).getTime();if(Date.now()>i)return f(`JWT expired for ${e} \u2014 refresh on next log will retry`,"infernoflow login")}return a(`Authenticated as ${e} (Supabase JWT \u2014 auth.uid() writes)`)}return o.mode==="device-flow"&&o.github_access_token?{status:"info",message:`Identity-only as ${e} (device flow \u2014 anon-mode writes)`,fix:"infernoflow login (without --device-flow, for full auth)",detail:null}:o.access_token?f(`Legacy login for ${e} \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 X(){try{const n=v(import.meta.url),o=l.resolve(l.dirname(n),"..","..","bin","infernoflow.mjs");if(!s.existsSync(o))return{status:"info",message:"bin/infernoflow.mjs not found from doctor location",fix:null,detail:null};const i=[...s.readFileSync(o,"utf8").matchAll(/import\("\.\.\/lib\/(commands\/[^"]+|telemetry\.mjs)"\)/g)],t=[],r=l.resolve(l.dirname(o),"..");for(const u of i){const g=u[1],$=l.join(r,"lib",g);s.existsSync($)||t.push(g)}return t.length?m(`${t.length} routed command(s) missing module files: ${t.slice(0,3).join(", ")}${t.length>3?"\u2026":""}`,"Restore the missing files or remove their entries from bin/infernoflow.mjs"):a(`All ${i.length} routed commands resolve to real files`)}catch(n){return{status:"info",message:`Router integrity check skipped: ${n.message}`,fix:null,detail:null}}}function K(n){const o=l.join(n,".gitignore");if(!s.existsSync(o))return{status:"info",message:".gitignore not found",fix:null,detail:null};const e=s.readFileSync(o,"utf8");return/^(?:\*\*\/)?node_modules\/?$/m.test(e)?a(".gitignore excludes node_modules"):f(".gitignore does not exclude node_modules","Add 'node_modules/' (and '**/node_modules/') to .gitignore")}function W(n,o){const e=n.filter(t=>t.status==="warn"&&t.fix),i=[];for(const t of e){const r=t.fix;if(r.startsWith("infernoflow ")){const u=r.slice(12).split(" ");k("infernoflow",u,{cwd:o,encoding:"utf8",timeout:3e4}).status===0&&i.push(t.label)}}return i}function Y(n){return n==="pass"?y("\u2714"):n==="warn"?x("\u26A0"):n==="fail"?j("\u2717"):w("\xB7")}function U(n,o){const e={pass:0,warn:0,fail:0,info:0,error:0};for(const r of n)e[r.status]=(e[r.status]||0)+1;console.log(),console.log(` ${C("\u{1F525} infernoflow doctor")}`),console.log();const i=Math.max(...n.map(r=>r.label.length))+2;for(const r of n)console.log(` ${Y(r.status)} ${C(r.label.padEnd(i))} ${r.message}`),r.detail&&console.log(` ${" ".repeat(i)} ${w(r.detail)}`),r.fix&&(r.status==="warn"||r.status==="fail")&&console.log(` ${" ".repeat(i)} ${N("fix:")} ${w(r.fix)}`);console.log();const t=e.fail>0?j("issues found"):e.warn>0?x("warnings"):y("all good");console.log(` ${t} \u2014 ${y(String(e.pass))} pass \xB7 ${x(String(e.warn))} warn \xB7 ${j(String(e.fail))} fail (${o}ms)`),console.log(),(e.warn>0||e.fail>0)&&(console.log(` Run ${N("infernoflow doctor --fix")} to auto-fix warnings`),console.log())}async function q(n){const o=n.slice(1),e=o.includes("--json"),i=o.includes("--fix"),t=process.cwd(),r=Date.now(),u=[c("Node.js version",()=>_()),c("infernoflow CLI",()=>P()),c("Git repository",()=>E(t)),c("inferno/ directory",()=>I(t)),c("Contract / mode",()=>T(t)),c("Scenarios",()=>G(t)),c("Changelog",()=>F(t)),c("CONTEXT.md",()=>M(t)),c("Git hooks",()=>D(t)),c("MCP server",()=>L(t)),c("AI providers",()=>R(t)),c("Cloud sync",()=>H()),c(".gitignore",()=>K(t)),c("Router integrity",()=>X()),await J().then(d=>({label:"Ollama (local AI)",...d}))],g=Date.now()-r;if(i){const d=W(u,t);if(d.length)return e||(console.log(),d.forEach(p=>console.log(` ${y("\u2714")} Fixed: ${p}`)),console.log()),q(["doctor","--json"])}if(e){const d={pass:0,warn:0,fail:0,info:0};u.forEach(p=>d[p.status]=(d[p.status]||0)+1),console.log(JSON.stringify({ok:d.fail===0,counts:d,results:u,elapsed:g}));return}U(u,g),u.some(d=>d.status==="fail")&&process.exit(1)}export{q as doctorCommand};
@@ -1 +1,35 @@
1
- import*as w from"node:https";import{bold as i,cyan as d,gray as a,green as m,red as f}from"../ui/output.mjs";import{readCredentials as $,writeCredentials as S,deleteCredentials as k,isLoggedIn as b}from"../cloud/credentials.mjs";const h="Ov23liYuUKwDRTzrywsa";function y(o,n,s){return new Promise((r,u)=>{const g=new URLSearchParams(s).toString(),c={hostname:o,port:443,path:n,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json","User-Agent":"infernoflow-cli","Content-Length":Buffer.byteLength(g)}},l=w.request(c,e=>{let t="";e.on("data",p=>t+=p),e.on("end",()=>{try{r(JSON.parse(t))}catch{r(t)}})});l.on("error",u),l.setTimeout(15e3,()=>l.destroy(new Error("timeout"))),l.write(g),l.end()})}function v(o,n,s){return new Promise((r,u)=>{const g={hostname:o,port:443,path:n,method:"GET",headers:{Accept:"application/json","User-Agent":"infernoflow-cli",Authorization:`Bearer ${s}`}},c=w.request(g,l=>{let e="";l.on("data",t=>e+=t),l.on("end",()=>{try{r(JSON.parse(e))}catch{r(e)}})});c.on("error",u),c.setTimeout(1e4,()=>c.destroy(new Error("timeout"))),c.end()})}function _(o){return new Promise(n=>setTimeout(n,o))}function A(o){try{const{execSync:n}=require("child_process"),s=process.platform==="win32"?`start "" "${o}"`:process.platform==="darwin"?`open "${o}"`:`xdg-open "${o}"`;n(s,{stdio:"ignore"})}catch{}}async function T(){if(b()){const e=$(),t=e?.user?.login||e?.user?.name||"unknown";console.log(),console.log(` ${m("\u2714")} Already logged in as ${i(t)}`),console.log(` Run ${d("infernoflow logout")} to sign out.`),console.log();return}console.log(),console.log(` ${i("\u{1F525} infernoflow login")}`),console.log();let o;try{o=await y("github.com","/login/device/code",{client_id:h,scope:"read:user user:email"})}catch(e){console.log(` ${f("\u2718")} Could not reach GitHub: ${e.message}`),console.log(),process.exit(1)}o.device_code||(console.log(` ${f("\u2718")} GitHub error: ${JSON.stringify(o)}`),console.log(),process.exit(1));const{device_code:n,user_code:s,verification_uri:r,expires_in:u,interval:g}=o,c=(g||5)*1e3;console.log(` ${i("Open this URL in your browser:")}`),console.log(` ${d(r)}`),console.log(),console.log(` ${i("Enter this code:")}`),console.log(` ${i(d(s))}`),console.log(),A(r),console.log(` ${a("Waiting for you to authorize\u2026")} ${a("(Ctrl+C to cancel)")}`),console.log();const l=Date.now()+(u||900)*1e3;for(;Date.now()<l;){await _(c);let e;try{e=await y("github.com","/login/oauth/access_token",{client_id:h,device_code:n,grant_type:"urn:ietf:params:oauth:grant-type:device_code"})}catch{continue}if(e.error!=="authorization_pending"){if(e.error==="slow_down"){await _(5e3);continue}if(e.error==="expired_token"&&(console.log(` ${f("\u2718")} Code expired. Run infernoflow login again.`),process.exit(1)),e.error==="access_denied"&&(console.log(` ${f("\u2718")} Access denied.`),process.exit(1)),e.access_token){const t=await v("api.github.com","/user",e.access_token),p=t?.login||t?.name||"unknown",L={access_token:e.access_token,refresh_token:null,expires_at:null,user:t,logged_in_at:new Date().toISOString()};S(L),console.log(` ${m("\u2714")} Logged in as ${i(p)}`),console.log(),console.log(` ${a("Session memory will now sync to the cloud on every")} ${d("infernoflow log")}`),console.log();return}}}console.log(` ${f("\u2718")} Login timed out. Run infernoflow login to try again.`),process.exit(1)}function x(){const o=k();console.log(),console.log(o?` ${m("\u2714")} Logged out. Local credentials removed.`:` ${a("Already logged out.")}`),console.log()}function C(){const o=$();if(console.log(),!o?.access_token){console.log(` ${a("Not logged in.")} Run ${d("infernoflow login")}`),console.log();return}const n=o.user?.login||o.user?.name||"unknown",s=o.user?.email||a("(no email)"),r=o.logged_in_at?new Date(o.logged_in_at).toLocaleDateString():"unknown";console.log(` ${i("\u{1F525} infernoflow")} \u2014 logged in as:`),console.log(),console.log(` User: ${i(n)}`),console.log(` Email: ${s}`),console.log(` Since: ${a(r)}`),console.log(` Status: ${m("\u2714 Active")}`),console.log()}async function R(o){const n=o[1];return n==="logout"?x():n==="whoami"?C():T()}async function U(){return x()}async function E(){return C()}export{R as loginCommand,U as logoutCommand,E as whoamiCommand};
1
+ import*as $ from"node:https";import*as k from"node:http";import*as O from"node:crypto";import{execSync as R}from"node:child_process";import{bold as p,cyan as m,gray as a,green as _,red as v,yellow as x}from"../ui/output.mjs";import{readCredentials as L,writeCredentials as A,deleteCredentials as M,isLoggedIn as G}from"../cloud/credentials.mjs";import{SUPABASE_URL as N,getUser as B}from"../cloud/supabase.mjs";const T="Ov23liYuUKwDRTzrywsa",J=[47655,47656,47657,47658,47659],z=300*1e3;function S(e,s,d){return new Promise((t,l)=>{const o=new URLSearchParams(d).toString(),n={hostname:e,port:443,path:s,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Accept:"application/json","User-Agent":"infernoflow-cli","Content-Length":Buffer.byteLength(o)}},i=$.request(n,r=>{let c="";r.on("data",u=>c+=u),r.on("end",()=>{try{t(JSON.parse(c))}catch{t(c)}})});i.on("error",l),i.setTimeout(15e3,()=>i.destroy(new Error("timeout"))),i.write(o),i.end()})}function W(e,s,d){return new Promise((t,l)=>{const o={hostname:e,port:443,path:s,method:"GET",headers:{Accept:"application/json","User-Agent":"infernoflow-cli",Authorization:`Bearer ${d}`}},n=$.request(o,i=>{let r="";i.on("data",c=>r+=c),i.on("end",()=>{try{t(JSON.parse(r))}catch{t(r)}})});n.on("error",l),n.setTimeout(1e4,()=>n.destroy(new Error("timeout"))),n.end()})}function C(e){return new Promise(s=>setTimeout(s,e))}function E(e){try{const s=process.platform==="win32"?`start "" "${e}"`:process.platform==="darwin"?`open "${e}"`:`xdg-open "${e}"`;R(s,{stdio:"ignore"})}catch{}}function F(e){return new Promise((s,d)=>{let t=0;const l=()=>{if(t>=e.length)return d(new Error("no available local port for callback"));const o=e[t++],n=k.createServer();n.on("error",()=>l()),n.listen(o,"127.0.0.1",()=>{n.close(()=>s(o))})};l()})}const Y=`<!DOCTYPE html>
2
+ <html><head><meta charset="utf-8"><title>infernoflow login</title>
3
+ <style>
4
+ body { font-family: ui-sans-serif, system-ui, -apple-system, sans-serif; max-width: 480px; margin: 60px auto; padding: 0 24px; color: #0f1117; }
5
+ .ok { color: #16a34a; }
6
+ .err { color: #dc2626; }
7
+ code { background: #f4f4f5; padding: 2px 6px; border-radius: 4px; font-size: 13px; }
8
+ </style></head>
9
+ <body>
10
+ <h2>\u{1F525} infernoflow</h2>
11
+ <p id="status">Completing login\u2026</p>
12
+ <script>
13
+ (async () => {
14
+ const status = document.getElementById('status');
15
+ const hash = window.location.hash.substring(1);
16
+ if (!hash) {
17
+ const params = new URLSearchParams(window.location.search);
18
+ const errMsg = params.get('error_description') || params.get('error') || 'No tokens received.';
19
+ status.innerHTML = '<span class="err">\u2718 Login failed: ' + errMsg + '</span>';
20
+ try { await fetch('/error?msg=' + encodeURIComponent(errMsg)); } catch (_) {}
21
+ return;
22
+ }
23
+ try {
24
+ const r = await fetch('/token', { method: 'POST', body: hash, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
25
+ if (r.ok) {
26
+ status.innerHTML = '<span class="ok">\u2714 Logged in.</span> You can close this tab and return to your terminal.';
27
+ } else {
28
+ status.innerHTML = '<span class="err">\u2718 Forwarding failed (HTTP ' + r.status + ').</span>';
29
+ }
30
+ } catch (e) {
31
+ status.innerHTML = '<span class="err">\u2718 Could not reach the local CLI: ' + e.message + '</span>';
32
+ }
33
+ })();
34
+ </script>
35
+ </body></html>`;async function K(){const e=await F(J),s=`http://localhost:${e}/callback`,d=O.randomBytes(16).toString("hex"),t=new URL(`${N}/auth/v1/authorize`);return t.searchParams.set("provider","github"),t.searchParams.set("redirect_to",s),t.searchParams.set("scopes","read:user user:email"),new Promise((l,o)=>{let n=!1,i;const r=u=>{if(!n){n=!0,clearTimeout(c);try{i?.close()}catch{}u()}},c=setTimeout(()=>{r(()=>o(new Error("login timed out \u2014 close the browser tab and try again")))},z);i=k.createServer((u,g)=>{const w=new URL(u.url,`http://localhost:${e}`);if(u.method==="GET"&&w.pathname==="/callback"){g.writeHead(200,{"Content-Type":"text/html; charset=utf-8"}),g.end(Y);return}if(u.method==="POST"&&w.pathname==="/token"){let h="";u.on("data",f=>h+=f),u.on("end",()=>{const f=new URLSearchParams(h),y=f.get("access_token"),I=f.get("refresh_token"),b=parseInt(f.get("expires_in")||"0",10),D=f.get("provider_token"),H=f.get("provider_refresh_token");if(!y){g.writeHead(400,{"Content-Type":"application/json"}),g.end(JSON.stringify({error:"no access_token in fragment"}));return}g.writeHead(204),g.end(),r(()=>l({access_token:y,refresh_token:I,expires_at:b?new Date(Date.now()+b*1e3).toISOString():null,provider_token:D,provider_refresh_token:H}))});return}if(u.method==="GET"&&w.pathname==="/error"){const h=w.searchParams.get("msg")||"unknown error";g.writeHead(204),g.end(),r(()=>o(new Error(h)));return}g.writeHead(404),g.end()}),i.on("error",u=>r(()=>o(u))),i.listen(e,"127.0.0.1",()=>{console.log(),console.log(` ${p("\u{1F525} infernoflow login")}`),console.log(),console.log(` ${a("Opening your browser to sign in with GitHub via Supabase\u2026")}`),console.log(),console.log(` ${p("If the browser doesn't open, paste this URL:")}`),console.log(` ${m(t.toString())}`),console.log(),console.log(` ${a(`Listening for the callback on http://localhost:${e}/callback`)}`),console.log(` ${a("(this prompt will close automatically when you finish)")}`),console.log(),E(t.toString())})})}async function j(){console.log(),console.log(` ${p("\u{1F525} infernoflow login")} ${a("(device-flow / identity-only)")}`),console.log(),console.log(` ${x("\u26A0")} ${a("Device flow gives us your GitHub identity but no Supabase JWT.")}`),console.log(` ${a("Cloud writes will fall back to anon-key dev mode. Run without --device-flow")}`),console.log(` ${a("for the proper authenticated flow.")}`),console.log();let e;try{e=await S("github.com","/login/device/code",{client_id:T,scope:"read:user user:email"})}catch(r){throw new Error(`could not reach GitHub: ${r.message}`)}if(!e.device_code)throw new Error(`GitHub error: ${JSON.stringify(e)}`);const{device_code:s,user_code:d,verification_uri:t,expires_in:l,interval:o}=e,n=(o||5)*1e3;console.log(` ${p("Open:")} ${m(t)}`),console.log(` ${p("Code:")} ${p(m(d))}`),console.log(),E(t),console.log(` ${a("Waiting for you to authorize\u2026")} ${a("(Ctrl+C to cancel)")}`),console.log();const i=Date.now()+(l||900)*1e3;for(;Date.now()<i;){await C(n);let r;try{r=await S("github.com","/login/oauth/access_token",{client_id:T,device_code:s,grant_type:"urn:ietf:params:oauth:grant-type:device_code"})}catch{continue}if(r.error!=="authorization_pending"){if(r.error==="slow_down"){await C(5e3);continue}if(r.error==="expired_token")throw new Error("code expired \u2014 run infernoflow login again");if(r.error==="access_denied")throw new Error("access denied");if(r.access_token){const c=await W("api.github.com","/user",r.access_token);return{mode:"device-flow",github_access_token:r.access_token,user:{provider:"github",login:c?.login||null,name:c?.name||null,email:c?.email||null,id:c?.id||null,avatar_url:c?.avatar_url||null}}}}}throw new Error("login timed out")}async function Q(e){if(G()){const o=L(),n=o?.user?.login||o?.user?.name||o?.user?.email||"unknown";console.log(),console.log(` ${_("\u2714")} Already logged in as ${p(n)}`),console.log(` Run ${m("infernoflow logout")} to sign out.`),console.log();return}const s=e.includes("--browser"),d=!s;let t;try{if(d){const o=await j();t={mode:"device-flow",github_access_token:o.github_access_token,user:o.user,logged_in_at:new Date().toISOString()}}else{const o=await K(),n=await B(o.access_token).catch(()=>null);t={mode:"supabase",access_token:o.access_token,refresh_token:o.refresh_token,expires_at:o.expires_at,provider_token:o.provider_token,provider_refresh_token:o.provider_refresh_token,user:n?{provider:"github",id:n.id||null,email:n.email||null,login:n.user_metadata?.user_name||n.user_metadata?.preferred_username||n.identities?.[0]?.identity_data?.user_name||null,name:n.user_metadata?.full_name||n.user_metadata?.name||null,avatar_url:n.user_metadata?.avatar_url||null}:{provider:"github"},logged_in_at:new Date().toISOString()}}}catch(o){console.log(),console.log(` ${v("\u2718")} Login failed: ${o.message}`),console.log(s?` ${a("If --browser fails, fall back to the default flow:")} ${m("infernoflow login")}`:` ${a("To try the experimental authenticated browser flow:")} ${m("infernoflow login --browser")}`),console.log(),process.exit(1)}A(t);const l=t.user?.login||t.user?.name||t.user?.email||"unknown";console.log(),console.log(` ${_("\u2714")} Logged in as ${p(l)}`),console.log(),t.mode==="supabase"?console.log(` ${a("Cloud sync is now authenticated. Every")} ${m("infernoflow log")} ${a("writes under your auth.uid().")}`):console.log(` ${a("Identity-only login (device flow). Cloud writes still use the anon-key dev mode.")}`),console.log()}function P(){const e=M();console.log(),console.log(e?` ${_("\u2714")} Logged out. Local credentials removed.`:` ${a("Already logged out.")}`),console.log()}function U(){const e=L();if(console.log(),!e?.access_token&&!e?.github_access_token){console.log(` ${a("Not logged in.")} Run ${m("infernoflow login")}`),console.log();return}const s=e.user?.login||e.user?.name||e.user?.email||"unknown",d=e.user?.email||a("(no email)"),t=e.logged_in_at?new Date(e.logged_in_at).toLocaleDateString():"unknown",l=e.mode==="supabase"?_("\u2714 authenticated (Supabase JWT)"):e.mode==="device-flow"?x("\u26A0 identity-only (device flow)"):a("legacy");if(console.log(` ${p("\u{1F525} infernoflow")} \u2014 logged in as:`),console.log(),console.log(` User: ${p(s)}`),console.log(` Email: ${d}`),console.log(` Since: ${a(t)}`),console.log(` Mode: ${l}`),e.expires_at){const o=new Date(e.expires_at),n=Date.now()>o.getTime();console.log(` Expires: ${a(o.toLocaleString())}${n?" "+v("(expired \u2014 run login again)"):""}`)}console.log()}async function ee(e){const s=e[1];return s==="logout"?P():s==="whoami"?U():Q(e)}async function oe(){return P()}async function ne(){return U()}export{ee as loginCommand,oe as logoutCommand,ne as whoamiCommand};
@@ -1,4 +1,7 @@
1
- import*as o from"node:fs";import*as a from"node:path";import{header as A,ok as H,fail as j,warn as J,section as p,bold as m,cyan as k,yellow as x,gray as t,green as u,red as G,white as P}from"../ui/output.mjs";function M(c){const e=Math.floor((Date.now()-c)/1e3);return e<60?"just now":e<3600?`${Math.floor(e/60)}m ago`:e<86400?`${Math.floor(e/3600)}h ago`:`${Math.floor(e/86400)}d ago`}function R(c,e){const g=new Set;if(o.existsSync(c))for(const i of o.readdirSync(c).filter(f=>f.endsWith(".json")))try{(JSON.parse(o.readFileSync(a.join(c,i),"utf8")).capabilitiesCovered||[]).forEach(l=>g.add(l))}catch{}return{covered:e.filter(i=>g.has(i)),uncovered:e.filter(i=>!g.has(i))}}async function I(c=[]){const e=c.includes("--json"),g=process.cwd(),i=a.join(g,"inferno");e||A("status"),o.existsSync(i)||(e&&(console.log(JSON.stringify({ok:!1,error:"inferno_not_found",hint:"Run: infernoflow init"},null,2)),process.exit(1)),j("inferno/ not found","Run: infernoflow init"),console.log(),process.exit(1));const f=a.join(i,"contract.json");o.existsSync(f)||(e&&(console.log(JSON.stringify({ok:!1,error:"contract_not_found"},null,2)),process.exit(1)),j("contract.json not found"),console.log(),process.exit(1));const l=JSON.parse(o.readFileSync(f,"utf8")),y=l.capabilities||[],N=o.statSync(f),$=a.join(i,"scenarios"),S=a.join(i,"CHANGELOG.md"),b=a.join(i,"capabilities.json"),{covered:F,uncovered:d}=R($,y),O=o.existsSync(S)&&/##\s+Unreleased/i.test(o.readFileSync(S,"utf8")),h=[];d.length>0&&h.push(`${d.length} capabilities without scenario coverage`),O||h.push("CHANGELOG missing ## Unreleased section");const v=h.length===0;if(e){const n={ok:v,driftReasons:h,project:{policyId:l.policyId||null,policyVersion:l.policyVersion||null,lastChange:M(N.mtimeMs)},capabilities:{total:y.length,uncovered:d},changelog:{hasUnreleased:O}};console.log(JSON.stringify(n,null,2)),process.exit(v?0:1)}v||(p("Drift"),h.forEach(n=>console.log(` ${x("\u26A0")} ${n}`))),p("Project"),console.log(` ${t("policy")} ${m(l.policyId||"\u2014")}`),console.log(` ${t("version")} ${m("v"+(l.policyVersion||"?"))}`),console.log(` ${t("last change")} ${t(M(N.mtimeMs))}`),p(`Capabilities ${t("("+y.length+")")}`);let E={};if(o.existsSync(b))try{(JSON.parse(o.readFileSync(b,"utf8")).capabilities||[]).forEach(s=>{E[s.id]=s})}catch{}if(y.forEach(n=>{const s=E[n],C=F.includes(n)?u("\u2714"):G("\u2718"),w=s?.title?t(` \u2014 ${s.title}`):"",U=s?.since?t(` [${s.since}]`):"";console.log(` ${C} ${P(n)}${w}${U}`)}),d.length>0?console.log(`
2
- ${x("\u26A0")} ${d.length} capability(ies) lack scenario coverage`):console.log(`
3
- ${u("\u2714")} All capabilities have scenario coverage`),p("Scenarios"),o.existsSync($)){const n=o.readdirSync($).filter(s=>s.endsWith(".json"));n.length===0?J("No scenario files \u2014 add .json files to inferno/scenarios/"):n.forEach(s=>{try{const r=JSON.parse(o.readFileSync(a.join($,s),"utf8")),C=r.steps?.length||0,w=(r.capabilitiesCovered||[]).length;console.log(` ${u("\u2714")} ${k(s)} ${t(`\u2014 ${C} steps, ${w} caps covered`)}`)}catch{console.log(` ${G("\u2718")} ${k(s)} ${t("\u2014 invalid JSON")}`)}})}else J("scenarios/ directory not found");if(p("Changelog"),o.existsSync(S)){const n=o.readFileSync(S,"utf8");/##\s+Unreleased/i.test(n)?H("Has ## Unreleased section"):j("Missing ## Unreleased section"),n.split(`
4
- `).filter(r=>/^##\s/.test(r)).slice(0,3).forEach(r=>console.log(` ${t(r)}`))}else j("inferno/CHANGELOG.md not found");console.log(),console.log(v?` ${u("\u25CF")} ${m(u("ready"))} ${t("\u2014 run infernoflow check for full validation")}`:` ${x("\u25CF")} ${m(x("needs attention"))} ${t("\u2014 run infernoflow check for details")}`),console.log()}export{I as statusCommand};
1
+ import*as n from"node:fs";import*as r from"node:path";import{header as R,ok as H,fail as J,warn as P,section as $,bold as c,cyan as x,yellow as p,gray as e,green as g,red as U,white as I}from"../ui/output.mjs";function E(a){const s=Math.floor((Date.now()-a)/1e3);return s<60?"just now":s<3600?`${Math.floor(s/60)}m ago`:s<86400?`${Math.floor(s/3600)}h ago`:`${Math.floor(s/86400)}d ago`}function _(a,s){const S=new Set;if(n.existsSync(a))for(const i of n.readdirSync(a).filter(m=>m.endsWith(".json")))try{(JSON.parse(n.readFileSync(r.join(a,i),"utf8")).capabilitiesCovered||[]).forEach(d=>S.add(d))}catch{}return{covered:s.filter(i=>S.has(i)),uncovered:s.filter(i=>!S.has(i))}}async function W(a=[]){const s=a.includes("--json"),S=process.cwd(),i=r.join(S,"inferno");s||R("status"),n.existsSync(i)||(s&&(console.log(JSON.stringify({ok:!1,error:"inferno_not_found",hint:"Run: infernoflow init"},null,2)),process.exit(1)),J("inferno/ not found","Run: infernoflow init"),console.log(),process.exit(1));const m=r.join(i,"config.json"),d=r.join(i,"contract.json");if((()=>{try{return JSON.parse(n.readFileSync(m,"utf8")).mode||null}catch{return null}})()==="memory"||!n.existsSync(d)&&n.existsSync(m)){const t=r.join(i,"sessions.jsonl"),o=n.existsSync(t)?n.readFileSync(t,"utf8").split(`
2
+ `).filter(Boolean).map(f=>{try{return JSON.parse(f)}catch{return null}}).filter(Boolean):[],l=o.filter(f=>f.type==="gotcha").length,h=o.filter(f=>f.type==="decision").length,u=o.filter(f=>f.type==="attempt").length,y=o[o.length-1];if(s){console.log(JSON.stringify({ok:!0,mode:"memory",entries:o.length,gotchas:l,decisions:h,attempts:u,lastEntry:y?y.ts:null},null,2));return}$("Session memory"),console.log(` ${e("entries")} ${c(String(o.length))}`),console.log(` ${e("gotchas")} ${c(String(l))}`),console.log(` ${e("decisions")} ${c(String(h))}`),console.log(` ${e("attempts")} ${c(String(u))}`),y&&console.log(` ${e("last entry")} ${e(E(new Date(y.ts).getTime()))}`),console.log(),o.length===0?console.log(` ${p("\u25CF")} ${c(p("empty"))} ${e("\u2014 log your first gotcha:")} ${x('infernoflow log "..." --type gotcha')}`):console.log(` ${g("\u25CF")} ${c(g("ready"))} ${e("\u2014 run")} ${x("infernoflow recap")} ${e("for the full session summary")}`),console.log(`
3
+ ${e("Want capability contracts + CI gates? Run:")} ${x("infernoflow init --mode full")}
4
+ `);return}n.existsSync(d)||(s&&(console.log(JSON.stringify({ok:!1,error:"contract_not_found"},null,2)),process.exit(1)),J("contract.json not found"),console.log(),process.exit(1));const j=JSON.parse(n.readFileSync(d,"utf8")),C=j.capabilities||[],M=n.statSync(d),N=r.join(i,"scenarios"),O=r.join(i,"CHANGELOG.md"),k=r.join(i,"capabilities.json"),{covered:A,uncovered:v}=_(N,C),F=n.existsSync(O)&&/##\s+Unreleased/i.test(n.readFileSync(O,"utf8")),w=[];v.length>0&&w.push(`${v.length} capabilities without scenario coverage`),F||w.push("CHANGELOG missing ## Unreleased section");const b=w.length===0;if(s){const t={ok:b,driftReasons:w,project:{policyId:j.policyId||null,policyVersion:j.policyVersion||null,lastChange:E(M.mtimeMs)},capabilities:{total:C.length,uncovered:v},changelog:{hasUnreleased:F}};console.log(JSON.stringify(t,null,2)),process.exit(b?0:1)}b||($("Drift"),w.forEach(t=>console.log(` ${p("\u26A0")} ${t}`))),$("Project"),console.log(` ${e("policy")} ${c(j.policyId||"\u2014")}`),console.log(` ${e("version")} ${c("v"+(j.policyVersion||"?"))}`),console.log(` ${e("last change")} ${e(E(M.mtimeMs))}`),$(`Capabilities ${e("("+C.length+")")}`);let G={};if(n.existsSync(k))try{(JSON.parse(n.readFileSync(k,"utf8")).capabilities||[]).forEach(o=>{G[o.id]=o})}catch{}if(C.forEach(t=>{const o=G[t],h=A.includes(t)?g("\u2714"):U("\u2718"),u=o?.title?e(` \u2014 ${o.title}`):"",y=o?.since?e(` [${o.since}]`):"";console.log(` ${h} ${I(t)}${u}${y}`)}),v.length>0?console.log(`
5
+ ${p("\u26A0")} ${v.length} capability(ies) lack scenario coverage`):console.log(`
6
+ ${g("\u2714")} All capabilities have scenario coverage`),$("Scenarios"),n.existsSync(N)){const t=n.readdirSync(N).filter(o=>o.endsWith(".json"));t.length===0?P("No scenario files \u2014 add .json files to inferno/scenarios/"):t.forEach(o=>{try{const l=JSON.parse(n.readFileSync(r.join(N,o),"utf8")),h=l.steps?.length||0,u=(l.capabilitiesCovered||[]).length;console.log(` ${g("\u2714")} ${x(o)} ${e(`\u2014 ${h} steps, ${u} caps covered`)}`)}catch{console.log(` ${U("\u2718")} ${x(o)} ${e("\u2014 invalid JSON")}`)}})}else P("scenarios/ directory not found");if($("Changelog"),n.existsSync(O)){const t=n.readFileSync(O,"utf8");/##\s+Unreleased/i.test(t)?H("Has ## Unreleased section"):J("Missing ## Unreleased section"),t.split(`
7
+ `).filter(l=>/^##\s/.test(l)).slice(0,3).forEach(l=>console.log(` ${e(l)}`))}else J("inferno/CHANGELOG.md not found");console.log(),console.log(b?` ${g("\u25CF")} ${c(g("ready"))} ${e("\u2014 run infernoflow check for full validation")}`:` ${p("\u25CF")} ${c(p("needs attention"))} ${e("\u2014 run infernoflow check for details")}`),console.log()}export{W as statusCommand};
@@ -1,13 +1,13 @@
1
- import*as u from"node:fs";import*as r from"node:path";import*as A from"node:os";import*as K from"node:readline";import{bold as I,gray as a,green as D,yellow as J,red as N}from"../ui/output.mjs";const y="inferno",O="CLAUDE.md",k=".claude",S=".cursor",E=r.join(S,"inferno-mcp-server.mjs"),P="inferno-mcp-server.mjs",d=r.join(S,"hooks.json"),b=r.join(S,"hooks","inferno-session-draft.mjs"),v=r.join(S,"mcp.json"),w=r.join(A.homedir(),".claude.json"),L=[".git/hooks/post-commit",".git/hooks/pre-push"],C="# infernoflow";function h(n){return u.existsSync(n)}function m(n){try{return JSON.parse(u.readFileSync(n,"utf8"))}catch{return null}}function F(n){try{return u.readFileSync(n,"utf8")}catch{return null}}function W(n){return new Promise(e=>{const s=K.createInterface({input:process.stdin,output:process.stdout});s.question(n,o=>{s.close(),e(o)})})}function $(n,e,s){const o=[],t=r.join(n,y);if(!h(t))return o;if(s)return o.push({type:"skip",path:y,reason:"--keep-inferno"}),o;if(e){const i=u.readdirSync(t);for(const c of i)if(c==="sessions.jsonl")o.push({type:"skip",path:r.join(y,c),reason:"--keep-memory"});else{const l=r.join(t,c);u.statSync(l).isDirectory()?o.push({type:"rmdir",path:r.join(y,c)}):o.push({type:"rm",path:r.join(y,c)})}}else o.push({type:"rmdir",path:y});return o}function G(n){const e=r.join(n,O);return h(e)?[{type:"rm",path:O}]:[]}function T(n){const e=[],s=r.join(n,k,"settings.json");if(!h(s))return e;const o=m(s),t=o?.tools?.some?.(c=>c.startsWith?.("mcp__infernoflow")),i=o&&Object.keys(o).some(c=>c==="tools"?(o.tools||[]).some(l=>!l.startsWith("mcp__infernoflow")):c!=="tools");return t&&!i?e.push({type:"rm",path:r.join(k,"settings.json"),desc:"auto-approved tools"}):t&&e.push({type:"edit",path:r.join(k,"settings.json"),desc:"remove infernoflow tools (preserve other content)"}),e}function V(n){const e=[];h(r.join(n,E))&&e.push({type:"rm",path:E}),h(r.join(n,P))&&e.push({type:"rm",path:P}),h(r.join(n,b))&&e.push({type:"rm",path:b});const s=r.join(n,d);return h(s)&&((m(s)?.hooks||[]).every(c=>(c.name||c.command||"").includes("inferno"))?e.push({type:"rm",path:d,desc:"infernoflow-only hooks config"}):e.push({type:"edit",path:d,desc:"remove infernoflow hook entry (preserve others)"})),e}function q(n){const e=r.join(n,v);if(!h(e))return[];const s=m(e);return s?.mcpServers?.infernoflow?Object.keys(s.mcpServers||{}).filter(t=>t!=="infernoflow").length===0&&Object.keys(s).length===1?[{type:"rm",path:v,desc:"infernoflow-only file"}]:[{type:"edit",path:v,desc:'remove "infernoflow" key (preserve other servers)'}]:[]}function Y(){return h(w)?m(w)?.mcpServers?.infernoflow?[{type:"edit",path:"~/.claude.json",desc:'remove "infernoflow" MCP entry (preserve other entries)',_realPath:w}]:[]:[]}function z(n){const e=[];for(const s of L){const o=r.join(n,s);if(!h(o))continue;const t=F(o);if(!t?.includes(C))continue;const i=t.split(`
2
- `),c=i.findIndex(f=>f.includes(C)),l=i.slice(0,c).join(`
3
- `).trim();!l||l==="#!/bin/sh"||l==="#!/bin/bash"?e.push({type:"rm",path:s,desc:"infernoflow-only hook"}):e.push({type:"edit",path:s,desc:"remove infernoflow section (preserve existing hooks)"})}return e}function B(n,e,s){for(const o of e){if(o.type==="skip")continue;const t=r.join(n,o.path);if(!s)try{o.type==="rmdir"?u.rmSync(t,{recursive:!0,force:!0}):u.unlinkSync(t)}catch{}}}function Q(n,e){if(!e)try{u.unlinkSync(r.join(n,O))}catch{}}function X(n,e,s){const o=r.join(n,k,"settings.json");for(const t of e)if(!s){if(t.type==="rm")try{u.unlinkSync(r.join(n,t.path))}catch{}else if(t.type==="edit")try{const i=m(o);i?.tools&&(i.tools=i.tools.filter(c=>!c.startsWith("mcp__infernoflow"))),u.writeFileSync(o,JSON.stringify(i,null,2)+`
4
- `,"utf8")}catch{}}}function Z(n,e,s){if(!s){for(const o of e)if(o.type==="rm")try{u.unlinkSync(r.join(n,o.path))}catch{}else if(o.type==="edit"&&o.path===d)try{const t=m(r.join(n,d));t?.hooks&&(t.hooks=t.hooks.filter(i=>!(i.name||i.command||"").includes("inferno"))),u.writeFileSync(r.join(n,d),JSON.stringify(t,null,2)+`
5
- `,"utf8")}catch{}}}function oo(n,e,s){const o=r.join(n,v);for(const t of e)if(!s){if(t.type==="rm")try{u.unlinkSync(o)}catch{}else if(t.type==="edit")try{const i=m(o);i?.mcpServers?.infernoflow&&delete i.mcpServers.infernoflow,u.writeFileSync(o,JSON.stringify(i,null,2)+`
6
- `,"utf8")}catch{}}}function eo(n,e){for(const s of n){if(e)continue;const o=s._realPath||w;if(s.type==="edit")try{const t=m(o);t?.mcpServers?.infernoflow&&delete t.mcpServers.infernoflow,u.writeFileSync(o,JSON.stringify(t,null,2)+`
7
- `,"utf8")}catch{}}}function no(n,e,s){for(const o of e){const t=r.join(n,o.path);if(!s){if(o.type==="rm")try{u.unlinkSync(t)}catch{}else if(o.type==="edit")try{const c=F(t).split(`
8
- `),l=c.findIndex(g=>g.includes(C)),f=c.slice(0,l).join(`
9
- `).trimEnd();u.writeFileSync(t,f+`
10
- `,"utf8")}catch{}}}}async function ro(n=[]){const e=p=>n.includes(p),s=e("--dry-run")||e("--dry"),o=e("--keep-memory"),t=e("--keep-inferno"),i=e("--yes")||e("-y"),c=e("--json"),l=process.cwd(),f={infernoDir:$(l,o,t),claudeMd:G(l),claudeDir:T(l),cursorMcpServer:V(l),cursorMcpJson:q(l),claudeJson:Y(),gitHooks:z(l)},g=Object.values(f).flat(),j=g.filter(p=>p.type!=="skip");if(c){console.log(JSON.stringify({dryRun:s,keepMemory:o,keepInferno:t,plan:f,actionCount:j.length},null,2));return}const R=a(" "+"\u2500".repeat(52));if(console.log(),console.log(" "+I("\u{1F525} infernoflow uninstall")),s&&console.log(J(" DRY RUN \u2014 nothing will be changed")),console.log(R),j.length===0){console.log(),console.log(D(" \u2714 Nothing to remove \u2014 infernoflow is not installed in this project")),console.log();return}console.log(),console.log(" "+I("Will remove:")),console.log();const U={rm:N(" \u2716"),rmdir:N(" \u2716"),edit:J(" ~"),skip:a(" \xB7")};for(const p of g){const x=U[p.type]||" ?",H=p.desc?a(` (${p.desc})`):"";console.log(`${x} ${p.path}${H}`)}if(o&&(console.log(),console.log(a(" \u2139 inferno/sessions.jsonl will be preserved (--keep-memory)"))),console.log(),!s&&!i){if(!(await W(" Continue? "+a("[y/N] "))).trim().toLowerCase().startsWith("y")){console.log(a(`
1
+ import*as a from"node:fs";import*as i from"node:path";import*as x from"node:os";import*as K from"node:readline";import{bold as I,gray as u,green as D,yellow as J,red as N}from"../ui/output.mjs";const y="inferno",O="CLAUDE.md",k=".claude",S=".cursor",b=i.join(S,"inferno-mcp-server.mjs"),E="inferno-mcp-server.mjs",d=i.join(S,"hooks.json"),P=i.join(S,"hooks","inferno-session-draft.mjs"),v=i.join(S,"mcp.json"),w=i.join(x.homedir(),".claude.json"),L=[".git/hooks/post-commit",".git/hooks/pre-push"],C="# infernoflow";function h(t){return a.existsSync(t)}function m(t){try{return JSON.parse(a.readFileSync(t,"utf8"))}catch{return null}}function F(t){try{return a.readFileSync(t,"utf8")}catch{return null}}function W(t){return new Promise(e=>{const s=K.createInterface({input:process.stdin,output:process.stdout});s.question(t,o=>{s.close(),e(o)})})}function $(t,e,s){const o=[],n=i.join(t,y);if(!h(n))return o;if(s)return o.push({type:"skip",path:y,reason:"--keep-inferno"}),o;if(e){const c=a.readdirSync(n);for(const l of c)if(l==="sessions.jsonl")o.push({type:"skip",path:i.join(y,l),reason:"--keep-memory"});else{const r=i.join(n,l);a.statSync(r).isDirectory()?o.push({type:"rmdir",path:i.join(y,l)}):o.push({type:"rm",path:i.join(y,l)})}}else o.push({type:"rmdir",path:y});return o}function G(t){const e=i.join(t,O);return h(e)?[{type:"rm",path:O}]:[]}function T(t){const e=[],s=i.join(t,k,"settings.json");if(!h(s))return e;const o=m(s),n=o?.tools?.some?.(l=>l.startsWith?.("mcp__infernoflow")),c=o&&Object.keys(o).some(l=>l==="tools"?(o.tools||[]).some(r=>!r.startsWith("mcp__infernoflow")):l!=="tools");return n&&!c?e.push({type:"rm",path:i.join(k,"settings.json"),desc:"auto-approved tools"}):n&&e.push({type:"edit",path:i.join(k,"settings.json"),desc:"remove infernoflow tools (preserve other content)"}),e}function V(t){const e=[];h(i.join(t,b))&&e.push({type:"rm",path:b}),h(i.join(t,E))&&e.push({type:"rm",path:E}),h(i.join(t,P))&&e.push({type:"rm",path:P});const s=i.join(t,d);if(h(s)){const n=m(s)?.hooks,c=Array.isArray(n)?n:n&&typeof n=="object"?Object.values(n).flatMap(r=>Array.isArray(r)?r:[r]).filter(Boolean):[];c.length>0&&c.every(r=>(r?.name||r?.command||"").includes("inferno"))?e.push({type:"rm",path:d,desc:"infernoflow-only hooks config"}):e.push({type:"edit",path:d,desc:"remove infernoflow hook entry (preserve others)"})}return e}function q(t){const e=i.join(t,v);if(!h(e))return[];const s=m(e);return s?.mcpServers?.infernoflow?Object.keys(s.mcpServers||{}).filter(n=>n!=="infernoflow").length===0&&Object.keys(s).length===1?[{type:"rm",path:v,desc:"infernoflow-only file"}]:[{type:"edit",path:v,desc:'remove "infernoflow" key (preserve other servers)'}]:[]}function B(){return h(w)?m(w)?.mcpServers?.infernoflow?[{type:"edit",path:"~/.claude.json",desc:'remove "infernoflow" MCP entry (preserve other entries)',_realPath:w}]:[]:[]}function Y(t){const e=[];for(const s of L){const o=i.join(t,s);if(!h(o))continue;const n=F(o);if(!n?.includes(C))continue;const c=n.split(`
2
+ `),l=c.findIndex(f=>f.includes(C)),r=c.slice(0,l).join(`
3
+ `).trim();!r||r==="#!/bin/sh"||r==="#!/bin/bash"?e.push({type:"rm",path:s,desc:"infernoflow-only hook"}):e.push({type:"edit",path:s,desc:"remove infernoflow section (preserve existing hooks)"})}return e}function z(t,e,s){for(const o of e){if(o.type==="skip")continue;const n=i.join(t,o.path);if(!s)try{o.type==="rmdir"?a.rmSync(n,{recursive:!0,force:!0}):a.unlinkSync(n)}catch{}}}function Q(t,e){if(!e)try{a.unlinkSync(i.join(t,O))}catch{}}function X(t,e,s){const o=i.join(t,k,"settings.json");for(const n of e)if(!s){if(n.type==="rm")try{a.unlinkSync(i.join(t,n.path))}catch{}else if(n.type==="edit")try{const c=m(o);c?.tools&&(c.tools=c.tools.filter(l=>!l.startsWith("mcp__infernoflow"))),a.writeFileSync(o,JSON.stringify(c,null,2)+`
4
+ `,"utf8")}catch{}}}function Z(t,e,s){if(!s){for(const o of e)if(o.type==="rm")try{a.unlinkSync(i.join(t,o.path))}catch{}else if(o.type==="edit"&&o.path===d)try{const n=m(i.join(t,d));n?.hooks&&(n.hooks=n.hooks.filter(c=>!(c.name||c.command||"").includes("inferno"))),a.writeFileSync(i.join(t,d),JSON.stringify(n,null,2)+`
5
+ `,"utf8")}catch{}}}function oo(t,e,s){const o=i.join(t,v);for(const n of e)if(!s){if(n.type==="rm")try{a.unlinkSync(o)}catch{}else if(n.type==="edit")try{const c=m(o);c?.mcpServers?.infernoflow&&delete c.mcpServers.infernoflow,a.writeFileSync(o,JSON.stringify(c,null,2)+`
6
+ `,"utf8")}catch{}}}function eo(t,e){for(const s of t){if(e)continue;const o=s._realPath||w;if(s.type==="edit")try{const n=m(o);n?.mcpServers?.infernoflow&&delete n.mcpServers.infernoflow,a.writeFileSync(o,JSON.stringify(n,null,2)+`
7
+ `,"utf8")}catch{}}}function no(t,e,s){for(const o of e){const n=i.join(t,o.path);if(!s){if(o.type==="rm")try{a.unlinkSync(n)}catch{}else if(o.type==="edit")try{const l=F(n).split(`
8
+ `),r=l.findIndex(g=>g.includes(C)),f=l.slice(0,r).join(`
9
+ `).trimEnd();a.writeFileSync(n,f+`
10
+ `,"utf8")}catch{}}}}async function ro(t=[]){const e=p=>t.includes(p),s=e("--dry-run")||e("--dry"),o=e("--keep-memory"),n=e("--keep-inferno"),c=e("--yes")||e("-y"),l=e("--json"),r=process.cwd(),f={infernoDir:$(r,o,n),claudeMd:G(r),claudeDir:T(r),cursorMcpServer:V(r),cursorMcpJson:q(r),claudeJson:B(),gitHooks:Y(r)},g=Object.values(f).flat(),j=g.filter(p=>p.type!=="skip");if(l){console.log(JSON.stringify({dryRun:s,keepMemory:o,keepInferno:n,plan:f,actionCount:j.length},null,2));return}const M=u(" "+"\u2500".repeat(52));if(console.log(),console.log(" "+I("\u{1F525} infernoflow uninstall")),s&&console.log(J(" DRY RUN \u2014 nothing will be changed")),console.log(M),j.length===0){console.log(),console.log(D(" \u2714 Nothing to remove \u2014 infernoflow is not installed in this project")),console.log();return}console.log(),console.log(" "+I("Will remove:")),console.log();const A={rm:N(" \u2716"),rmdir:N(" \u2716"),edit:J(" ~"),skip:u(" \xB7")};for(const p of g){const H=A[p.type]||" ?",U=p.desc?u(` (${p.desc})`):"";console.log(`${H} ${p.path}${U}`)}if(o&&(console.log(),console.log(u(" \u2139 inferno/sessions.jsonl will be preserved (--keep-memory)"))),console.log(),!s&&!c){if(!(await W(" Continue? "+u("[y/N] "))).trim().toLowerCase().startsWith("y")){console.log(u(`
11
11
  Aborted \u2014 nothing changed.
12
- `));return}console.log()}if(s){console.log(a(` \u2191 Dry run complete \u2014 run without --dry-run to apply
13
- `));return}B(l,f.infernoDir,!1),f.claudeMd.length&&Q(l,!1),f.claudeDir.length&&X(l,f.claudeDir,!1),f.cursorMcpServer.length&&Z(l,f.cursorMcpServer,!1),f.cursorMcpJson.length&&oo(l,f.cursorMcpJson,!1),f.claudeJson.length&&eo(f.claudeJson,!1),f.gitHooks.length&&no(l,f.gitHooks,!1),console.log(R),console.log(),console.log(D(" \u2714 infernoflow removed from this project")),console.log();const M=j.filter(p=>p.type==="edit"),_=j.filter(p=>p.type==="rm"||p.type==="rmdir");_.length&&console.log(a(" Deleted: ")+_.map(p=>p.path).join(", ")),M.length&&console.log(a(" Edited: ")+M.map(p=>p.path).join(", ")),o&&(console.log(),console.log(a(" Session memory kept \u2192 inferno/sessions.jsonl")),console.log(a(" Re-run infernoflow init to restore the rest."))),console.log()}export{ro as uninstallCommand};
12
+ `));return}console.log()}if(s){console.log(u(` \u2191 Dry run complete \u2014 run without --dry-run to apply
13
+ `));return}z(r,f.infernoDir,!1),f.claudeMd.length&&Q(r,!1),f.claudeDir.length&&X(r,f.claudeDir,!1),f.cursorMcpServer.length&&Z(r,f.cursorMcpServer,!1),f.cursorMcpJson.length&&oo(r,f.cursorMcpJson,!1),f.claudeJson.length&&eo(f.claudeJson,!1),f.gitHooks.length&&no(r,f.gitHooks,!1),console.log(M),console.log(),console.log(D(" \u2714 infernoflow removed from this project")),console.log();const R=j.filter(p=>p.type==="edit"),_=j.filter(p=>p.type==="rm"||p.type==="rmdir");_.length&&console.log(u(" Deleted: ")+_.map(p=>p.path).join(", ")),R.length&&console.log(u(" Edited: ")+R.map(p=>p.path).join(", ")),o&&(console.log(),console.log(u(" Session memory kept \u2192 inferno/sessions.jsonl")),console.log(u(" Re-run infernoflow init to restore the rest."))),console.log()}export{ro as uninstallCommand};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infernoflow",
3
- "version": "0.38.16",
3
+ "version": "0.40.0",
4
4
  "description": "Persistent memory for AI coding sessions \u2014 captures what agents can't infer from code alone. Works with Copilot, Cursor, Claude, and Windsurf.",
5
5
  "type": "module",
6
6
  "bin": {