infernoflow 0.43.12 → 0.44.1

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 (52) hide show
  1. package/README.md +87 -149
  2. package/dist/bin/infernoflow.mjs +30 -33
  3. package/dist/lib/amp/io.mjs +8 -8
  4. package/dist/lib/cleanTree.mjs +12 -0
  5. package/dist/lib/commands/ai.mjs +2 -2
  6. package/dist/lib/commands/amp.mjs +4 -4
  7. package/dist/lib/commands/ask.mjs +2 -2
  8. package/dist/lib/commands/context.mjs +18 -18
  9. package/dist/lib/commands/doctor.mjs +2 -3
  10. package/dist/lib/commands/init.mjs +31 -32
  11. package/dist/lib/commands/log.mjs +13 -19
  12. package/dist/lib/commands/recap.mjs +3 -3
  13. package/dist/lib/commands/refresh.mjs +5 -0
  14. package/dist/lib/commands/setup.mjs +6 -6
  15. package/dist/lib/commands/status.mjs +6 -7
  16. package/dist/lib/commands/switch.mjs +5 -5
  17. package/dist/lib/commands/sync.mjs +41 -0
  18. package/dist/lib/git/branch.mjs +2 -0
  19. package/dist/lib/mcpRuntime.mjs +1 -0
  20. package/dist/lib/projectRoot.mjs +1 -0
  21. package/dist/lib/ruleFiles.mjs +9 -8
  22. package/dist/lib/upgradeCheck.mjs +1 -1
  23. package/dist/templates/cursor/inferno-mcp-server.mjs +200 -325
  24. package/package.json +13 -5
  25. package/dist/lib/commands/changelog.mjs +0 -21
  26. package/dist/lib/commands/ci.mjs +0 -3
  27. package/dist/lib/commands/claudeMd.mjs +0 -116
  28. package/dist/lib/commands/coverage.mjs +0 -2
  29. package/dist/lib/commands/demo.mjs +0 -113
  30. package/dist/lib/commands/diff.mjs +0 -5
  31. package/dist/lib/commands/explain.mjs +0 -8
  32. package/dist/lib/commands/feedback.mjs +0 -12
  33. package/dist/lib/commands/graph.mjs +0 -76
  34. package/dist/lib/commands/impact.mjs +0 -2
  35. package/dist/lib/commands/implement.mjs +0 -7
  36. package/dist/lib/commands/monorepo.mjs +0 -4
  37. package/dist/lib/commands/notify.mjs +0 -4
  38. package/dist/lib/commands/prImpact.mjs +0 -2
  39. package/dist/lib/commands/publish.mjs +0 -21
  40. package/dist/lib/commands/review.mjs +0 -24
  41. package/dist/lib/commands/run.mjs +0 -10
  42. package/dist/lib/commands/scaffold.mjs +0 -124
  43. package/dist/lib/commands/scan.mjs +0 -42
  44. package/dist/lib/commands/stability.mjs +0 -2
  45. package/dist/lib/commands/stats.mjs +0 -4
  46. package/dist/lib/commands/suggest.mjs +0 -62
  47. package/dist/lib/commands/syncAuto.mjs +0 -1
  48. package/dist/lib/commands/test.mjs +0 -6
  49. package/dist/lib/commands/theme.mjs +0 -18
  50. package/dist/lib/commands/upgrade.mjs +0 -20
  51. package/dist/lib/commands/watch.mjs +0 -7
  52. package/dist/lib/commands/why.mjs +0 -4
package/README.md CHANGED
@@ -1,208 +1,146 @@
1
1
  # 🔥 infernoflow
2
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-deriving context every time.
4
- >
5
- > infernoflow is the reference CLI for [**AMP — the AI Memory Protocol**](docs/protocol/PROTOCOL.md). Any AMP-compatible tool can read your `.ai-memory/sessions.jsonl` — Cursor, Copilot, Claude, Windsurf, future agents. Vendor-neutral, file-based, zero deps.
3
+ > Persistent memory for AI coding sessions. Capture what agents can't infer from code — the gotchas, the decisions, the failed approaches — and replay it into your next chat so you stop re-deriving context every time you open Cursor / Claude Code / Copilot.
6
4
 
7
5
  [![npm version](https://img.shields.io/npm/v/infernoflow.svg?color=orange)](https://www.npmjs.com/package/infernoflow)
8
6
  [![npm downloads](https://img.shields.io/npm/dw/infernoflow.svg?color=orange)](https://www.npmjs.com/package/infernoflow)
9
- [![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](./package.json)
10
- [![npm audit](https://img.shields.io/badge/npm%20audit-0%20vulnerabilities-brightgreen)](https://docs.npmjs.com/cli/v10/commands/npm-audit)
7
+ [![zero runtime dependencies](https://img.shields.io/badge/runtime%20deps-0-brightgreen)](./package.json)
11
8
  [![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/infernoflow.infernoflow?label=VS%20Code&color=orange)](https://marketplace.visualstudio.com/items?itemName=infernoflow.infernoflow)
12
- [![status: alpha](https://img.shields.io/badge/status-alpha-yellow)](#)
13
9
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
14
10
 
15
- > **⚠ Alpha — actively iterating.** Local memory operations (logging, search, handoff, AI rule-file injection) are stable. The product does ONE thing: persistent memory that any AI tool can read. No network calls in default command paths. See [SECURITY.md](./SECURITY.md) for the full data-flow disclosure.
11
+ ## The loop
16
12
 
17
- ## The 60-second pitch
13
+ Every new AI session today starts cold. The agent re-reads your code, re-derives the obvious, and often re-makes the same wrong move someone else made yesterday. infernoflow closes that loop in four stages:
18
14
 
19
- 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.
15
+ 1. **Capture** while you and the agent work, certain moments are worth saving: a gotcha hit, a decision made, an attempted fix that failed, a pattern noticed. The agent writes them down automatically via the `amp_write` MCP tool no copy/paste, no `git commit -m`.
16
+ 2. **Link** — each captured moment becomes a structured AMP entry (`gotcha | decision | attempt | note | detection | pattern`) with timestamp, file:line, tags, and a stable AMP id. Linked into the project, not your scratchpad.
17
+ 3. **Persist** — entries land in `.ai-memory/branches/<branch>.jsonl` (git-tracked, travels with your branch — teammates inherit it) plus `.ai-memory/global.jsonl` (personal preferences, gitignored, synced across your machines via any OS-synced folder).
18
+ 4. **Restore** — when a new session starts, the agent reads `CLAUDE.md` / `.cursorrules` / `copilot-instructions.md` at boot. The most relevant entries are already there. Warm start; no cold derivation.
19
+
20
+ That's it. No service to log into. No SaaS. JSONL on disk, an MCP server, and three rule files your IDE already reads.
20
21
 
21
22
  ## Install
22
23
 
23
24
  ```bash
24
25
  npm install -g infernoflow
25
- # or zero-install:
26
- npx infernoflow init
26
+ infernoflow init --yes
27
27
  ```
28
28
 
29
- Zero npm dependencies. Works on Node ≥ 18. Windows, macOS, Linux.
29
+ Zero runtime dependencies. Works on Node ≥ 18 macOS, Linux, Windows.
30
30
 
31
- ## Quick start (90 seconds)
31
+ `init --yes` does the whole setup: creates `.ai-memory/`, writes rule files for every supported IDE, wires the MCP server for Cursor / VS Code Copilot / Claude Code in one shot, applies the clean-tree git policy, and drops a visible demo entry so you can confirm the loop is alive with one command:
32
32
 
33
33
  ```bash
34
- cd your-project
35
- infernoflow init # 30-second setup, asks for your first gotcha
36
- infernoflow log "API returns 202 not 200" --type gotcha
37
- infernoflow log "use polling not websocket for progress" --type decision
38
- infernoflow ask "API" # search your memory
39
- infernoflow switch --copy # generate handoff, copy to clipboard
40
- # paste into your next Cursor/Copilot/Claude chat — the agent picks up everything
34
+ infernoflow status
41
35
  ```
42
36
 
43
- ## The 5-command core
37
+ ## The 5-command core + 1
44
38
 
45
- These five cover 90% of usage:
39
+ These cover 95% of usage:
46
40
 
47
41
  | Command | What it does |
48
42
  |---|---|
49
- | `infernoflow log "..."` | Remember a gotcha, decision, attempt, or note. `--type gotcha\|decision\|attempt\|preference` |
50
- | `infernoflow ask "..."` | Search your memory by keyword. Gotchas surface first. |
51
- | `infernoflow switch` | Generate a handoff for your next AI session. `--copy` puts it on the clipboard. |
52
- | `infernoflow recap` | End-of-session summary with health score and unlogged-change detection. |
53
- | `infernoflow status` | Quick session-memory health check. |
43
+ | `infernoflow log "..."` | Remember a gotcha / decision / attempt / note. `--type gotcha\|decision\|attempt\|preference` |
44
+ | `infernoflow ask "..."` | Search your memory by keyword gotchas surface first |
45
+ | `infernoflow switch` | Generate a handoff for the next session. `--copy` puts it on your clipboard |
46
+ | `infernoflow recap` | End-of-session summary with health score + unlogged-change detection |
47
+ | `infernoflow status` | Quick health check — entries, gotchas, decisions, last activity |
48
+ | `infernoflow refresh` | Manually rebuild `CLAUDE.md` / `.cursorrules` / `copilot-instructions.md` from memory |
49
+
50
+ In practice you barely run any of these — the MCP-aware AI does it for you. The CLI is for grep-style introspection.
54
51
 
55
- Run `infernoflow commands` for the full grouped list (12 visible commands across Session Memory, Code Analysis, Workflow, Cloud, Setup; ~40 more available as aliases for backward compat).
52
+ `infernoflow commands` shows the full list (17 commands, grouped by purpose).
56
53
 
57
- ## The AI Memory Protocol (AMP)
54
+ ## Branch-aware memory + cross-machine sync
58
55
 
59
- infernoflow stores memory in the AMP-canonical layout:
56
+ **Your teammate takes your branch they inherit your memory.**
60
57
 
61
58
  ```
62
59
  .ai-memory/
63
- ├── sessions.jsonl # one AMP entry per line (gotchas, decisions, attempts, notes…)
64
- ├── amp.json # project metadata
65
- └── handoff.md # generated handoff for AI agents
60
+ ├── branches/
61
+ ├── main.jsonl ← project-wide truths (git-tracked)
62
+ └── feature-auth.jsonl ← your current branch's work (git-tracked)
63
+ ├── global.jsonl ← your personal preferences (gitignored)
64
+ └── sessions.jsonl ← legacy flat file (still read)
66
65
  ```
67
66
 
68
- Each entry on disk is AMP wire format:
67
+ - **Captures on a feature branch travel with that branch via git.** When a teammate runs `git checkout feature-auth`, the JSONL is there. Their MCP server boots, reads it, regenerates their rule files — their AI is warm-started on *your* findings without you sending a message.
68
+ - **Personal preferences travel between your own machines.** Point at any OS-synced folder once:
69
+ ```
70
+ infernoflow sync set ~/Dropbox/infernoflow-memory
71
+ ```
72
+ Home → work → home. No infra to stand up; the OS does the sync.
73
+ - **`merge=union` on branch JSONLs** means concurrent commits from different machines merge cleanly — no manual conflict resolution.
74
+ - **Branch switching never blocked.** Rule files refresh only at MCP server boot or via explicit `infernoflow refresh`, not on every entry — your working tree stays clean while you log.
69
75
 
70
- ```json
71
- {"type":"gotcha","msg":"API returns 202 not 200","ts":1714704000000,"id":"amp_01HXYZ...","file":"src/api.js","line":42}
72
- ```
76
+ ## Cross-IDE — same memory, every tool
73
77
 
74
- The full spec is in [docs/protocol/PROTOCOL.md](docs/protocol/PROTOCOL.md). Any tool that can parse JSONL can read your memory that's the whole point. infernoflow is currently the **AMP Full** reference implementation: read + write + handoff + injection across CLAUDE.md / .cursorrules / copilot-instructions.md.
78
+ The rule files at the top level of your project are what every AI agent reads on boot. infernoflow writes the same canonical block to all three:
75
79
 
76
- Building your own AMP-compatible tool? Use the reference TS library:
80
+ | Tool | Reads from |
81
+ |---|---|
82
+ | Claude Code | `CLAUDE.md` |
83
+ | Cursor | `.cursorrules` |
84
+ | GitHub Copilot (VS Code, JetBrains) | `.github/copilot-instructions.md` |
77
85
 
78
- ```bash
79
- npm install infernoflow-amp
80
- ```
86
+ Plus MCP for tools that speak it — Cursor, Claude Code, VS Code Copilot Chat. The MCP server is wired by `infernoflow setup` / `init` into each tool's config file. No per-tool setup.
81
87
 
82
- If you have a project on the legacy `inferno/sessions.jsonl` layout, migrate with one command:
88
+ ## MCP tools (for AI agents)
83
89
 
84
- ```bash
85
- infernoflow amp migrate
86
- ```
90
+ When the MCP server is wired, your AI agent can call these directly in chat:
91
+
92
+ | Tool | What it does |
93
+ |---|---|
94
+ | `amp_write` | Log an entry (`type`, `msg`, optional `file` / `line` / `tags`) |
95
+ | `amp_read` | Read entries with optional filters |
96
+ | `amp_search` | Keyword search across entries |
97
+ | `amp_handoff` | Generate the handoff document for the next AI session |
98
+ | `amp_health` | Session health score (A–F) |
99
+ | `infernoflow_status` | Memory + project health at a glance |
100
+ | `infernoflow_check` | Validate the capability contract (read-only) |
101
+ | `infernoflow_context` | Generate AI-ready context for a task |
102
+ | `infernoflow_git_drift` | Detect which capabilities recent commits affected |
87
103
 
88
- The original `inferno/sessions.jsonl` is left in place nothing is overwritten.
104
+ The `amp_*` tools follow the [AMP MCP spec §7.3](docs/protocol/PROTOCOL.md#73-mcp-tool-interface) vendor-neutral. Any AMP-Full client only needs to know those five names.
89
105
 
90
- ## Auto-context for AI agents
106
+ ## What it has caught (real dogfood)
91
107
 
92
- 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:
108
+ infernoflow was developed by building a multi-tenant kanban (`infernotest_01`) and capturing what it surfaced. A sample of real entries from that dogfood:
93
109
 
94
- - `CLAUDE.md` — picked up by Claude Code
95
- - `.cursorrules` picked up by Cursor
96
- - `.github/copilot-instructions.md` — picked up by GitHub Copilot
110
+ - **gotcha** (`vite.config.ts`): *"Vite proxy with `changeOrigin: true` rewrites the Host header server-side URL construction produces URLs pointing at the BACKEND port. Build user-facing URLs client-side via `window.location.origin`."*
111
+ - **gotcha** (`server/prisma/schema.prisma`): *"Prisma 6 `query_engine.dll.node` is locked while tsx watch is running; `prisma migrate dev` fails with EPERM on rename. Stop the dev server before migrating."*
112
+ - **gotcha** (`server/src/routes/members.ts`): *"Invite accept must NOT burn the token when the caller is already a member of the workspace return early with the existing membership before marking acceptedAt."*
113
+ - **pattern** (`server/src/routes/columns.ts`): *"Position assignment for ordered children: next position = max(existing) + 1024. The 1024 step leaves room for ~10 inserts between two siblings without renumbering."*
114
+ - **pattern** (`server/src/access.ts`): *"Cross-entity auth helpers do `where: { memberships: { where: { userId } } }` via Prisma nested-select — one DB hop per assertion. Return 404 not 403 when not a member to avoid leaking existence."*
115
+ - **decision** (`server/src/auth.ts`): *"Opaque session tokens in a Session table (not JWTs) — chosen so we can revoke per-session (`deleteMany` on Session). bcryptjs over native bcrypt to avoid platform-specific binaries."*
97
116
 
98
- You don't have to paste anything. Set up once, every future session is better.
117
+ These are the things you'd otherwise forget by next Tuesday and re-derive at 11pm on a Friday. They live in `.ai-memory/branches/*.jsonl` forever.
99
118
 
100
119
  ## VS Code extension
101
120
 
102
- The companion extension turns the CLI into a live visual surface inside VS Code. Install it from the Marketplace:
121
+ The companion extension renders your memory as a live sidebar ranked-by-relevance gotchas/decisions for whatever file you're editing, status bar health score, inline CodeLens annotations at gotcha locations.
103
122
 
104
123
  ```
105
124
  ext install infernoflow.infernoflow
106
125
  ```
107
126
 
108
- Or browse it [on the Marketplace](https://marketplace.visualstudio.com/items?itemName=infernoflow.infernoflow). Activates automatically on any project containing `.ai-memory/sessions.jsonl` or `inferno/`.
109
-
110
- **What you get (v0.7.4):**
111
-
112
- The extension closes a real loop: **capture** the right thing → **rank** it for the file you're editing → **inject** it into the AI's context automatically.
113
-
114
- #### Capture (zero-typing memory)
115
-
116
- - **Auto-capture popup** — when you edit the same file 5+ times in 10 minutes, a popup asks "Stuck on something?" with [Log Gotcha] [Log Attempt] [Dismiss] buttons. Click once → entry is auto-written with timestamp, file:line, the enclosing function name, a 5-line code-context window, nearby TypeScript/ESLint diagnostics, AND failure/success-keyword excerpts from the recent agent conversation.
117
- - **Agent conversation harvest** — if you've installed Cursor / Copilot hooks (`infernoflow install-cursor-hooks`), every agent exchange writes to `CONTEXT.draft.md`. Auto-capture pulls failure-signal lines (`error`, `cannot`, `still failing`) AND resolution-signal lines (`got it`, `the fix was`, `turns out`) so the gotcha tells the full arc — breakdown and breakthrough.
118
- - **AI session summarize** — new "Summarize session with AI" action runs the recent conversation through Copilot (via VS Code LM API) or your configured AI provider, proposes 1–6 structured memory entries, shows them in a multi-select picker. Tick which to keep.
119
- - **Bulk delete + orphan cleanup** — "Manage entries…" picker grouped by date for cleanups. "Cleanup orphaned entries…" finds entries whose source files were deleted (e.g., after refactor) for bulk removal.
120
-
121
- #### Inject (closing the loop)
122
-
123
- - **AI Context for [current file]** sidebar section — shows top 5 most-relevant entries for whatever file you're editing, scored by: same file (+100), same directory (+40), same extension (+10), recent (+20), type-weighted. Updates as you switch editors.
124
- - **Auto-sync rule files** (default on, debounced 1.5s) — on every memory change OR editor switch, the extension rewrites `.cursorrules` / `CLAUDE.md` / `.github/copilot-instructions.md` with the current ranking. Recent git commits go at the top, top-5 most-relevant memory entries next, older context collapsed under `<details>`. Idempotent — uses delimiter comments, doesn't trample your manual edits. **Result: when you open a NEW AI chat in the same VS Code session, the AI tool reads the updated rule files at chat start and sees the right gotchas first — no manual "rebuild" step required.**
125
- - **Inline CodeLens** — `🔥 ⚠ 2 gotchas · 1 failed · 1 decision` above any file with logged entries. Per-line annotations at gotcha locations. Click for full entry detail.
126
- - **Editor diagnostics** — gotchas as yellow Warnings, failed attempts as blue Information squiggles. Copilot reads them.
127
-
128
- #### Surface
129
-
130
- - **Sidebar memory panel** — Session Health (A–F), AI Context for [current file], Gotchas, Decisions, Failed Attempts, Quick Actions, CLI Tools (one-click access to 11 CLI commands).
131
- - **Status bar** — `🔥 B 65 · ⚠3 · ✓2 · ❌1 · 📋 Switch` with grade-based color coding.
132
- - **Help tooltips everywhere** — hover any sidebar item for a description of what it does, when to use it, what happens when clicked.
133
- - **Keyboard shortcuts** — `Ctrl+Alt+G/D/A/S/R` for log gotcha / log decision / ask memory / generate handoff / show recap.
134
-
135
- ## Cursor / VS Code MCP integration
136
-
137
- ```bash
138
- infernoflow install-cursor-hooks
139
- # Restart Cursor → Settings → MCP → infernoflow: 4 tools enabled
140
-
141
- # or for VS Code + Copilot (Preview):
142
- infernoflow install-vscode-copilot-hooks
143
- ```
144
-
145
- After install-cursor-hooks, your AI agent can call infernoflow directly in chat:
146
-
147
- | MCP tool | What it does |
148
- |---|---|
149
- | `infernoflow_run` | Generate a task prompt from your contract |
150
- | `infernoflow_apply` | Apply the JSON response — updates contract + CHANGELOG |
151
- | `infernoflow_check` | Validate contract sync |
152
- | `infernoflow_status` | Show contract health |
153
- | `infernoflow_context` | Generate AI-ready context for a new session |
154
- | `infernoflow_implement` | Step-by-step code prompt for a specific task |
155
- | `infernoflow_review` | Pre-merge capability drift check on the current branch |
156
- | `infernoflow_git_drift` | Detect capabilities affected by recent commits |
157
- | `infernoflow_scan_ui` | Detect UI / design-token changes vs contract |
158
- | `amp_read` | **AMP-spec** alias — read entries with optional filters |
159
- | `amp_write` | **AMP-spec** alias — log a new entry |
160
- | `amp_handoff` | **AMP-spec** alias — generate the handoff document |
161
- | `amp_search` | **AMP-spec** alias — search entries by keyword |
162
- | `amp_health` | **AMP-spec** alias — session health score |
163
-
164
- The `amp_*` tools are vendor-neutral aliases following the [AMP MCP spec §7.3](docs/protocol/PROTOCOL.md#73-mcp-tool-interface). Any AMP-Full client only needs to know these names — the `infernoflow_*` set stays for backward compat.
165
-
166
- ## Capability contracts (advanced)
167
-
168
- 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*:
169
-
170
- ```bash
171
- infernoflow init --mode full # set up contract.json, capabilities, scenarios
172
- infernoflow scan # AST-walk to discover capabilities
173
- infernoflow freeze CreateItem # mark a capability as protected — AI won't modify it
174
- infernoflow impact CreateItem # blast radius before changes
175
- infernoflow check # CI gate
176
- ```
177
-
178
- Most users don't need this. If you do, run `infernoflow demo` for an interactive walkthrough.
127
+ Or in the Marketplace: [infernoflow.infernoflow](https://marketplace.visualstudio.com/items?itemName=infernoflow.infernoflow). Activates on any project with `.ai-memory/` (or legacy `inferno/`).
179
128
 
180
- ## CI integration
181
-
182
- ```yaml
183
- - name: infernoflow check
184
- run: npx infernoflow check --json
185
- - name: infernoflow doc-gate
186
- run: npx infernoflow doc-gate --json
187
- ```
188
-
189
- Or use the GitHub Action:
190
-
191
- ```yaml
192
- - uses: ronmiz/infernoflow-action@v1
193
- ```
129
+ The extension is **window only** in v0.7.9+ — the CLI is the single canonical writer of rule files. No race between extension and CLI; the extension watches `.ai-memory/**/*.jsonl` and renders.
194
130
 
195
131
  ## Troubleshooting
196
132
 
197
- - **MCP not showing in Cursor** fully quit and relaunch Cursor after `install-cursor-hooks`.
198
- - **`infernoflow not found`** use `npx infernoflow` or `npm install -g infernoflow`.
199
- - **PowerShell script execution blocked** `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass`.
200
- - **`infernoflow doctor`** runs a full diagnostic if anything looks wrong.
201
- - **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.
133
+ - **I upgraded infernoflow but `amp_write` entries still look wrong.** Your IDE's MCP server is loaded into memory at session start and doesn't reload from disk. **Quit and reopen Cursor / Claude Code / VS Code.** `infernoflow doctor` will flag this with a "MCP runtime v… but CLI v…" warning.
134
+ - **`infernoflow` not found.** Use `npx infernoflow` until the global install resolves on your PATH.
135
+ - **PowerShell script execution blocked.** `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass`.
136
+ - **Box-drawing chars look broken.** Should auto-fall-back to ASCII on legacy PowerShell. If not, open an issue.
137
+ - **`infernoflow doctor`** full diagnostic if anything looks wrong. Includes the MCP runtime stamp check + AI provider detection + git hooks status.
138
+
139
+ ## Why this matters
202
140
 
203
- ## Why infernoflow?
141
+ Code changes daily. What the system *actually does* under all those edits — the invariants, the constraints, the things that bit you last week — code can't tell you. infernoflow keeps that current and feeds it to the agent so the agent stops re-deriving from scratch.
204
142
 
205
- 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.
143
+ That's the whole product. No vendor lock-in (it's JSONL on disk). No SaaS. One CLI, one MCP server, three rule files your IDE was reading anyway.
206
144
 
207
145
  ## License
208
146
 
@@ -210,6 +148,6 @@ MIT
210
148
 
211
149
  ## Links
212
150
 
213
- - [GitHub](https://github.com/ronmiz/infernoflow)
214
- - [npm](https://www.npmjs.com/package/infernoflow)
215
- - [Issues](https://github.com/ronmiz/infernoflow/issues)
151
+ - [GitHub](https://github.com/ronmiz/infernoflow) · [npm](https://www.npmjs.com/package/infernoflow) · [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=infernoflow.infernoflow) · [Issues](https://github.com/ronmiz/infernoflow/issues)
152
+ - [AMP protocol spec](docs/protocol/PROTOCOL.md) — vendor-neutral memory format
153
+ - [Dogfood: what infernoflow caught while building infernotest_01](docs/dogfood-infernotest_01.md)
@@ -1,41 +1,38 @@
1
1
  #!/usr/bin/env node
2
- (function(){if(process.platform!=="win32"||process.env.WT_SESSION||process.env.ConEmuPID||process.env.TERM_PROGRAM==="vscode")return;const t={"\u2500":"-","\u2501":"-","\u2550":"=","\u2502":"|","\u2503":"|","\u2551":"|","\u250C":"+","\u2510":"+","\u2514":"+","\u2518":"+","\u251C":"+","\u2524":"+","\u252C":"+","\u2534":"+","\u253C":"+","\xB7":"*","\u2192":"->","\u2190":"<-","\u2714":"[OK]","\u2713":"[OK]","\u2718":"[X]","\u2717":"[X]","\u26A0":"[!]",\u2139:"[i]"},n=new RegExp(Object.keys(t).join("|"),"g");function r(c){const d=c.write.bind(c);c.write=function(s,...f){if(typeof s=="string")s=s.replace(n,u=>t[u]);else if(Buffer.isBuffer(s)){const u=s.toString("utf8").replace(n,k=>t[k]);s=Buffer.from(u,"utf8")}return d(s,...f)}}r(process.stdout),r(process.stderr)})();import{readFileSync as $}from"node:fs";import{dirname as S,join as g}from"node:path";import{fileURLToPath as x}from"node:url";import{bold as m,gray as o,cyan as a,red as h}from"../lib/ui/output.mjs";const A=S(x(import.meta.url));function O(e){for(const t of[g(e,"..","..","package.json"),g(e,"..","package.json")])try{return JSON.parse($(t,"utf8"))}catch{}return{version:"0.0.0-source"}}const I=O(A),p=I.version||"0.0.0",y={publish:"Bump version, update changelog, build, npm publish, git commit + push in one shot",diff:"Show what capabilities changed since the last git tag (or any ref)",changelog:"Draft a changelog entry from commits since the last tag",setup:"One command to get fully operational \u2014 detects IDE, inits, installs hooks + MCP",init:"Scaffold inferno/ in your project (or adopt existing project)","install-cursor-hooks":"Install Cursor hooks: draft agent replies to inferno/CONTEXT.draft.md","install-vscode-copilot-hooks":"Install VS Code + Copilot agent hooks (Preview): draft to inferno/CONTEXT.draft.md",check:"Validate contract, capabilities, scenarios, changelog",status:"Show contract health at a glance","pr-impact":"Summarize PR impact on capabilities and docs",sync:"Run deterministic inferno sync flow",run:"One-command detect/propose/apply/validate flow","doc-gate":"Fail if code changed but docs were not updated",suggest:"Generate AI prompt + apply capability updates",implement:"Generate code-agent implementation prompt(s)",context:"Generate AI-ready context for new sessions","generate-skills":"Generate personalised Cursor rules + skill files from your developer profile",watch:"Watch source files and run suggest automatically on save",ci:"CI-native check: GitHub Actions annotations, GitLab code quality, exit codes",notify:"Post capability drift summary to Slack or Discord",monorepo:"Manage infernoflow across monorepo packages (init | list | status | diff | sync)",doctor:"Diagnose your infernoflow setup \u2014 checks Node, git, contract, AI providers, MCP, hooks",coverage:"Map test files to capabilities \u2014 show which caps have test coverage and which don't",review:"AI-powered capability impact review for staged or recent git changes",scan:"Deep AST scan \u2014 route discovery, entry point detection, HTTP URL extraction, capability suggestions",graph:"Build capability dependency graph \u2014 shows which caps call which, detects breaking changes",stability:"Show solid/liquid stability level for every capability (frozen/stable/experimental)",freeze:"Mark a capability as frozen (solid) \u2014 AI will not modify it without explicit instruction",thaw:"Reset a capability to experimental (liquid) \u2014 free to evolve",why:"Given a file or function name \u2014 show which capability it serves, scenarios, stability, and git history",impact:"Blast radius analysis \u2014 see every cap, scenario, and risk level affected before you change anything",scaffold:"Generate a new capability \u2014 source skeleton, contract registration, and placeholder scenario in one command",explain:"AI narrative about a capability \u2014 what it does, why it exists, what's risky, and what to test",test:"Run registered scenarios for a capability \u2014 auto-generates a smoke harness if no test runner is configured",ai:"Manage AI providers \u2014 setup, status, test connection (subcommands: setup | status | test | clear)",demo:"Interactive walkthrough \u2014 scaffolds a sample project and runs the full capability chain end-to-end",feedback:"60-second CLI survey about how you use infernoflow (--form to open web form)",telemetry:"Manage anonymous usage telemetry (on | off | status) \u2014 opt-in, command names only",log:"Append to session memory (decisions, gotchas, failed attempts, theme changes) \u2014 what AI can't infer from code",theme:"Scan fonts, colors, and CSS variables \u2014 write inferno/theme.json so AI always matches the design system",switch:"Generate a handoff summary when switching AI agents \u2014 paste into the next session so nothing is lost",upgrade:"Upgrade a lite infernoflow setup to the full structure (scenarios, changelog, scripts)",stats:"Value dashboard \u2014 session memory, tokens injected per session, coverage %, estimated savings",ask:"Query session memory \u2014 search gotchas, decisions, and failed attempts by keyword or type",recap:"End-of-session summary \u2014 what was captured, what git changes weren't logged, session health score",uninstall:"Remove infernoflow from a project \u2014 inferno/, CLAUDE.md, MCP server, git hooks (--dry-run to preview)",contract:"Capability contracts \u2014 scan, freeze, impact, graph, scaffold, etc. (run: infernoflow contract)",dev:"Maintenance & integration \u2014 publish, changelog, dashboard, ai, ci, sync, etc. (run: infernoflow dev)",amp:"AI Memory Protocol \u2014 status, migrate from legacy, validate (run: infernoflow amp)"},l={publish:async e=>(await import("../lib/commands/publish.mjs")).publishCommand(e),diff:async e=>(await import("../lib/commands/diff.mjs")).diffCommand(e),changelog:async e=>(await import("../lib/commands/changelog.mjs")).changelogCommand(e),setup:async e=>(await import("../lib/commands/setup.mjs")).setupCommand(e),init:async e=>(await import("../lib/commands/init.mjs")).initCommand(e),"install-cursor-hooks":async e=>(await import("../lib/commands/installCursorHooks.mjs")).installCursorHooksCommand(e),"install-vscode-copilot-hooks":async e=>(await import("../lib/commands/installVsCodeCopilotHooks.mjs")).installVsCodeCopilotHooksCommand(e),check:async e=>(await import("../lib/commands/check.mjs")).checkCommand(e),status:async e=>(await import("../lib/commands/status.mjs")).statusCommand(e),"pr-impact":async e=>(await import("../lib/commands/prImpact.mjs")).prImpactCommand(e),sync:async e=>(await import("../lib/commands/syncAuto.mjs")).syncCommand(e),run:async e=>(await import("../lib/commands/run.mjs")).runCommand(e),suggest:async e=>(await import("../lib/commands/suggest.mjs")).suggestCommand(e),implement:async e=>(await import("../lib/commands/implement.mjs")).implementCommand(e),context:async e=>(await import("../lib/commands/context.mjs")).contextCommand(e),"doc-gate":async e=>(await import("../lib/commands/docGate.mjs")).docGateCommand(e),"generate-skills":async e=>(await import("../lib/commands/generateSkills.mjs")).generateSkillsCommand(e),watch:async e=>(await import("../lib/commands/watch.mjs")).watchCommand(e),ci:async e=>(await import("../lib/commands/ci.mjs")).ciCommand(e),notify:async e=>(await import("../lib/commands/notify.mjs")).notifyCommand(e),monorepo:async e=>(await import("../lib/commands/monorepo.mjs")).monorepoCommand(e),doctor:async e=>(await import("../lib/commands/doctor.mjs")).doctorCommand(e),coverage:async e=>(await import("../lib/commands/coverage.mjs")).coverageCommand(e),review:async e=>(await import("../lib/commands/review.mjs")).reviewCommand(e),scan:async e=>(await import("../lib/commands/scan.mjs")).scanCommand(e),graph:async e=>(await import("../lib/commands/graph.mjs")).graphCommand(e),stability:async e=>(await import("../lib/commands/stability.mjs")).stabilityCommand(e),freeze:async e=>(await import("../lib/commands/stability.mjs")).freezeCommand(e),thaw:async e=>(await import("../lib/commands/stability.mjs")).thawCommand(e),why:async e=>(await import("../lib/commands/why.mjs")).whyCommand(e),impact:async e=>(await import("../lib/commands/impact.mjs")).impactCommand(e),scaffold:async e=>(await import("../lib/commands/scaffold.mjs")).scaffoldCommand(e),explain:async e=>(await import("../lib/commands/explain.mjs")).explainCommand(e),test:async e=>(await import("../lib/commands/test.mjs")).testCommand(e),ai:async e=>(await import("../lib/commands/ai.mjs")).aiCommand(e),demo:async e=>(await import("../lib/commands/demo.mjs")).demoCommand(e),log:async e=>(await import("../lib/commands/log.mjs")).logCommand(e),theme:async e=>(await import("../lib/commands/theme.mjs")).themeCommand(e),switch:async e=>(await import("../lib/commands/switch.mjs")).switchCommand(e),upgrade:async e=>(await import("../lib/commands/upgrade.mjs")).upgradeCommand(e),stats:async e=>(await import("../lib/commands/stats.mjs")).statsCommand(e),ask:async e=>(await import("../lib/commands/ask.mjs")).askCommand(e),recap:async e=>(await import("../lib/commands/recap.mjs")).recapCommand(e),uninstall:async e=>(await import("../lib/commands/uninstall.mjs")).uninstallCommand(e),feedback:async e=>(await import("../lib/commands/feedback.mjs")).feedbackCommand(e),telemetry:async e=>(await import("../lib/telemetry.mjs")).telemetryCommand(e),contract:async e=>w("contract",v,e),dev:async e=>w("dev",b,e),amp:async e=>(await import("../lib/commands/amp.mjs")).ampCommand(e)};async function w(e,t,n){const r=n[1];if(!r||r==="--help"||r==="-h"){console.log(),console.log(` ${m("\u{1F525} infernoflow "+e)} ${o("\u2014 available verbs:")}`),console.log();const d=Math.max(...Object.keys(t).map(s=>s.length))+2;for(const s of Object.keys(t)){const f=y[t[s]]||"";console.log(` ${a(s.padEnd(d))} ${o(f)}`)}console.log(),console.log(` ${o("Run")} ${a(`infernoflow ${e} <verb> --help`)} ${o("for verb-specific options.")}`),console.log();return}const c=t[r];return(!c||!l[c])&&(console.error(h(`
3
- Unknown ${e} verb: ${r}`)),console.error(o(` Run: infernoflow ${e} (see all verbs)
4
- `)),process.exit(1)),l[c]([c,...n.slice(2)])}function B(){const e=Object.keys(y),t=Math.max(...e.map(n=>n.length),8)+1;return Object.entries(y).map(([n,r])=>` ${n.padEnd(t," ")}${r}`).join(`
5
- `)}const v={scan:"scan",check:"check",status:"status",freeze:"freeze",thaw:"thaw",why:"why",impact:"impact",graph:"graph",stability:"stability",scaffold:"scaffold",explain:"explain",test:"test",coverage:"coverage",suggest:"suggest",run:"run",implement:"implement","doc-gate":"doc-gate","pr-impact":"pr-impact",review:"review",demo:"demo",upgrade:"upgrade",context:"context",sync:"sync"},b={publish:"publish",changelog:"changelog",diff:"diff",monorepo:"monorepo",ci:"ci",ai:"ai",theme:"theme",stats:"stats",feedback:"feedback",telemetry:"telemetry",uninstall:"uninstall","generate-skills":"generate-skills","install-cursor-hooks":"install-cursor-hooks","install-vscode-copilot-hooks":"install-vscode-copilot-hooks",setup:"setup"},M={"Memory (top-level)":["log","ask","switch","recap","status"],"Watch (top-level)":["watch"],"Setup (top-level)":["init","doctor"],"AMP (use: infernoflow amp <verb>)":["status","migrate","validate","version"],"Contract (use: infernoflow contract <verb>)":Object.keys(v),"Dev (use: infernoflow dev <verb>)":Object.keys(b)};function R(){return Object.entries(M).map(([t,n])=>` ${m(t+":")}
6
- ${n.join(" ")}`).join(`
2
+ (function(){if(process.platform!=="win32"||process.env.WT_SESSION||process.env.ConEmuPID||process.env.TERM_PROGRAM==="vscode")return;const s={"\u2500":"-","\u2501":"-","\u2550":"=","\u2502":"|","\u2503":"|","\u2551":"|","\u250C":"+","\u2510":"+","\u2514":"+","\u2518":"+","\u251C":"+","\u2524":"+","\u252C":"+","\u2534":"+","\u253C":"+","\xB7":"*","\u2192":"->","\u2190":"<-","\u2714":"[OK]","\u2713":"[OK]","\u2718":"[X]","\u2717":"[X]","\u26A0":"[!]",\u2139:"[i]"},r=new RegExp(Object.keys(s).join("|"),"g");function c(l){const h=l.write.bind(l);l.write=function(a,...w){if(typeof a=="string")a=a.replace(r,p=>s[p]);else if(Buffer.isBuffer(a)){const p=a.toString("utf8").replace(r,$=>s[$]);a=Buffer.from(p,"utf8")}return h(a,...w)}}c(process.stdout),c(process.stderr)})();import{readFileSync as C}from"node:fs";import{dirname as k,join as f}from"node:path";import{fileURLToPath as v}from"node:url";import{bold as i,gray as e,cyan as t,red as u}from"../lib/ui/output.mjs";const A=k(v(import.meta.url));function I(o){for(const s of[f(o,"..","..","package.json"),f(o,"..","package.json")])try{return JSON.parse(C(s,"utf8"))}catch{}return{version:"0.0.0-source"}}const M=I(A),m=M.version||"0.0.0",y={log:"Append to session memory (decisions, gotchas, failed attempts)",ask:"Query memory by keyword (gotchas surface first)",switch:"Generate a handoff doc for the next AI agent / session",recap:"End-of-session summary + health score + unlogged-change surfacing",status:"Quick health check \u2014 entries, gotchas, decisions, last activity",refresh:"Rebuild CLAUDE.md / .cursorrules / copilot-instructions.md from memory",init:"Scaffold .ai-memory/ and wire the current IDE in one command",setup:"Re-run wiring (idempotent) \u2014 detects IDE, installs MCP + hooks",doctor:"Diagnose your setup \u2014 Node, git, contract, AI provider, MCP, hooks",context:"Generate AI-ready context for new sessions","install-cursor-hooks":"Install Cursor hooks (afterAgentResponse + stop)","install-vscode-copilot-hooks":"Install VS Code + Copilot agent hooks (Preview)","generate-skills":"Generate Cursor rules + skill files from your developer profile",ai:"Manage AI providers \u2014 setup, status, test, clear",telemetry:"Opt-in anonymous telemetry (on | off | status)",uninstall:"Remove infernoflow from a project (--dry-run to preview)",check:"Validate contract, capabilities, scenarios, changelog",sync:"Cross-machine sync for personal memory \u2014 status/set/clear/migrate",amp:"AI Memory Protocol \u2014 status, migrate, validate (run: infernoflow amp)"},d={log:async o=>(await import("../lib/commands/log.mjs")).logCommand(o),ask:async o=>(await import("../lib/commands/ask.mjs")).askCommand(o),switch:async o=>(await import("../lib/commands/switch.mjs")).switchCommand(o),recap:async o=>(await import("../lib/commands/recap.mjs")).recapCommand(o),status:async o=>(await import("../lib/commands/status.mjs")).statusCommand(o),refresh:async o=>(await import("../lib/commands/refresh.mjs")).refreshCommand(o),init:async o=>(await import("../lib/commands/init.mjs")).initCommand(o),setup:async o=>(await import("../lib/commands/setup.mjs")).setupCommand(o),doctor:async o=>(await import("../lib/commands/doctor.mjs")).doctorCommand(o),context:async o=>(await import("../lib/commands/context.mjs")).contextCommand(o),"install-cursor-hooks":async o=>(await import("../lib/commands/installCursorHooks.mjs")).installCursorHooksCommand(o),"install-vscode-copilot-hooks":async o=>(await import("../lib/commands/installVsCodeCopilotHooks.mjs")).installVsCodeCopilotHooksCommand(o),"generate-skills":async o=>(await import("../lib/commands/generateSkills.mjs")).generateSkillsCommand(o),ai:async o=>(await import("../lib/commands/ai.mjs")).aiCommand(o),telemetry:async o=>(await import("../lib/telemetry.mjs")).telemetryCommand(o),uninstall:async o=>(await import("../lib/commands/uninstall.mjs")).uninstallCommand(o),check:async o=>(await import("../lib/commands/check.mjs")).checkCommand(o),sync:async o=>(await import("../lib/commands/sync.mjs")).syncCommand(o),amp:async o=>(await import("../lib/commands/amp.mjs")).ampCommand(o)};function U(){const o=Object.keys(y),s=Math.max(...o.map(r=>r.length),8)+1;return Object.entries(y).map(([r,c])=>` ${r.padEnd(s," ")}${c}`).join(`
3
+ `)}const O={"Memory (the 5-command core)":["log","ask","switch","recap","status","refresh"],Setup:["init","setup","doctor","context"],"IDE wiring":["install-cursor-hooks","install-vscode-copilot-hooks","generate-skills"],Configuration:["ai","telemetry","sync","uninstall"],Contract:["check"],"AMP (use: infernoflow amp <verb>)":["status","migrate","validate","version"]};function S(){return Object.entries(O).map(([o,s])=>` ${i(o+":")}
4
+ ${s.join(" ")}`).join(`
7
5
 
8
- `)}const C=Object.keys(l).length,j=`
9
- ${m("\u{1F525} infernoflow")} ${o("v"+p)}
10
- ${o("Persistent memory for AI coding sessions")}
6
+ `)}const g=Object.keys(d).length,R=`
7
+ ${i("\u{1F525} infernoflow")} ${e("v"+m)}
8
+ ${e("Persistent memory for AI coding sessions")}
11
9
 
12
- ${m("Usage:")}
10
+ ${i("Usage:")}
13
11
  infernoflow [command] [options]
14
12
 
15
- ${m("Memory")} ${o("\u2014 the 5-command core")}
16
- ${a("log")} ${o('"..."')} Add to session memory ${o("(--type gotcha|decision|attempt)")}
17
- ${a("ask")} ${o('"..."')} Search your memory by keyword ${o("(gotchas surface first)")}
18
- ${a("switch")} Generate handoff for next AI agent
19
- ${a("recap")} End-of-session health score + unlogged changes
20
- ${a("status")} Quick health check
13
+ ${i("Memory")} ${e("\u2014 the 5-command core")}
14
+ ${t("log")} ${e('"..."')} Add to session memory ${e("(--type gotcha|decision|attempt)")}
15
+ ${t("ask")} ${e('"..."')} Search your memory by keyword ${e("(gotchas surface first)")}
16
+ ${t("switch")} Generate handoff for next AI agent
17
+ ${t("recap")} End-of-session health score + unlogged changes
18
+ ${t("status")} Quick health check
21
19
 
22
- ${m("Setup")}
23
- ${a("init")} 60-second setup ${o("(memory mode by default)")}
24
- ${a("watch")} Auto-capture mode ${o("(stuck-loops, dep changes, test removals)")}
25
- ${a("doctor")} Diagnose your setup
20
+ ${i("Setup")}
21
+ ${t("init")} 60-second setup ${e("(memory mode by default)")}
22
+ ${t("setup")} Re-run IDE wiring ${e("(idempotent \u2014 MCP + hooks)")}
23
+ ${t("doctor")} Diagnose your setup
24
+ ${t("context")} Generate AI-ready context for new sessions
26
25
 
27
- ${m("Subsystems")} ${o("\u2014 grouped, run for verbs:")}
28
- ${a("amp")} AI Memory Protocol ${o("(status, migrate, validate)")}
29
- ${a("contract")} Capability contracts ${o("(scan, freeze, impact, scaffold, \u2026)")}
30
- ${a("dev")} Publishing, AI providers, hooks ${o("(publish, ai, ci, \u2026)")}
26
+ ${i("Subsystems")} ${e("\u2014 grouped, run for verbs:")}
27
+ ${t("amp")} AI Memory Protocol ${e("(status, migrate, validate)")}
31
28
 
32
- ${o("Run")} ${a("infernoflow commands")} ${o("to see all "+C+" commands grouped.")}
33
- ${o("Run")} ${a("infernoflow <command> --help")} ${o("for command-specific options.")}
34
- `;import*as E from"node:fs";import*as P from"node:path";try{const e=P.join(process.cwd(),"inferno");if(E.existsSync(e)){const{observeCommandStart:t}=await import("../lib/learning/observe.mjs"),n=process.argv[2];n&&!n.startsWith("-")&&t(e,n)}}catch{}const[,,i,...D]=process.argv;(!i||i==="--help"||i==="-h")&&(console.log(j),process.exit(0)),(i==="--version"||i==="-v")&&(console.log(p),process.exit(0)),i==="commands"&&(console.log(`
35
- ${m("\u{1F525} infernoflow")} ${o("v"+p)} ${o("\u2014 all "+C+" commands")}
36
- `),console.log(R()),console.log(`
37
- ${o("Run")} ${a("infernoflow <command> --help")} ${o("for options.")}
38
- `),process.exit(0));const N=Object.keys(l);N.includes(i)||(console.error(h(`
39
- Unknown command: ${i}`)),console.error(o("Run: infernoflow commands (see all commands)")),console.error(o(`Run: infernoflow --help (quick start)
40
- `)),process.exit(1));const T=[i,...D];try{const{runUpgradeBackfillIfNeeded:e}=await import("../lib/upgradeCheck.mjs");await e(p,i)}catch{}l[i](T).catch(e=>{console.error(h(`
41
- Error: `)+e.message),process.exit(1)});
29
+ ${e("Run")} ${t("infernoflow commands")} ${e("to see all "+g+" commands grouped.")}
30
+ ${e("Run")} ${t("infernoflow <command> --help")} ${e("for command-specific options.")}
31
+ `;import*as b from"node:fs";import*as E from"node:path";try{const o=E.join(process.cwd(),"inferno");if(b.existsSync(o)){const{observeCommandStart:s}=await import("../lib/learning/observe.mjs"),r=process.argv[2];r&&!r.startsWith("-")&&s(o,r)}}catch{}const[,,n,...x]=process.argv;(!n||n==="--help"||n==="-h")&&(console.log(R),process.exit(0)),(n==="--version"||n==="-v")&&(console.log(m),process.exit(0)),n==="commands"&&(console.log(`
32
+ ${i("\u{1F525} infernoflow")} ${e("v"+m)} ${e("\u2014 all "+g+" commands")}
33
+ `),console.log(S()),console.log(`
34
+ ${e("Run")} ${t("infernoflow <command> --help")} ${e("for options.")}
35
+ `),process.exit(0));const P=Object.keys(d);P.includes(n)||(console.error(u(`
36
+ Unknown command: ${n}`)),console.error(e("Run: infernoflow commands (see all commands)")),console.error(e(`Run: infernoflow --help (quick start)
37
+ `)),process.exit(1));const j=[n,...x];try{const{runUpgradeBackfillIfNeeded:o}=await import("../lib/upgradeCheck.mjs");await o(m,n)}catch{}d[n](j).catch(o=>{console.error(u(`
38
+ Error: `)+o.message),process.exit(1)});
@@ -1,11 +1,11 @@
1
- import*as i from"node:fs";import*as c from"node:path";const p="1.0",A={start:"<!-- AMP:START -->",end:"<!-- AMP:END -->"},S=new Set(["gotcha","decision","attempt","note","detection","pattern"]),j=new Set(["copilot","cursor","claude","windsurf","other"]);function a(e,t={}){const o=c.join(e,".ai-memory"),n=c.join(e,"inferno"),r=t.forWrite||i.existsSync(o)||!i.existsSync(n),u=r?o:n,f=r;return{root:u,isAmp:f,sessions:c.join(u,"sessions.jsonl"),config:c.join(u,f?"amp.json":"config.json"),handoff:c.join(u,f?"handoff.md":"HANDOFF.md")}}function l(e){const t=c.join(e,".ai-memory");return i.existsSync(t)||i.mkdirSync(t,{recursive:!0}),t}const y="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function h(){let e=Date.now(),t="";for(let n=0;n<10;n++)t=y[e%32]+t,e=Math.floor(e/32);let o="";for(let n=0;n<16;n++)o+=y[Math.floor(Math.random()*32)];return t+o}function g(e){const t={...e.meta||{}};let o=e.type||"note";S.has(o)||(t.subtype=o,o="note"),e.result&&(t.result=e.result);let n;const r=e.agent;r&&j.has(r)?n=r:r&&(t.agent=r);const u=typeof e.ts=="number"?e.ts:e.ts?Date.parse(e.ts):Date.now(),f=e.confidence!=null?e.confidence:e.auto?.7:void 0,s={type:o,msg:e.summary||e.msg||"",ts:u,id:e.id||`amp_${h()}`};return e.file&&(s.file=e.file),e.line&&(s.line=e.line),e.function&&(s.function=e.function),e.tags&&e.tags.length&&(s.tags=e.tags),e.source&&(s.source=e.source),n&&(s.tool=n),e.session&&(s.session=e.session),f!=null&&(s.confidence=f),Object.keys(t).length&&(s.meta=t),s}function x(e){if(e.summary&&!e.msg)return e;const t=e.meta||{},o=t.subtype||e.type||"note",n={ts:e.ts,type:o,summary:e.msg||""};e.id&&(n.id=e.id),e.file&&(n.file=e.file),e.line&&(n.line=e.line),e.function&&(n.function=e.function),e.tags&&(n.tags=e.tags),e.source&&(n.source=e.source),e.tool&&(n.agent=e.tool),t.agent&&(n.agent=t.agent),t.result&&(n.result=t.result),e.confidence!=null&&(n.confidence=e.confidence,e.confidence<1&&(n.auto=!0));const{subtype:r,agent:u,result:f,...s}=t;return Object.keys(s).length&&(n.meta=s),n}function D(e){const{sessions:t}=a(e);return i.existsSync(t)?i.readFileSync(t,"utf8").split(`
2
- `).filter(Boolean).map(o=>{try{return JSON.parse(o)}catch{return null}}).filter(Boolean).map(x):[]}function M(e,t){l(e);const{sessions:o}=a(e,{forWrite:!0}),n=g(t);return i.appendFileSync(o,JSON.stringify(n)+`
3
- `,"utf8"),n}function O(e){const{config:t}=a(e);try{return JSON.parse(i.readFileSync(t,"utf8"))}catch{return null}}function N(e,t={}){l(e);const{config:o}=a(e,{forWrite:!0});if(i.existsSync(o))return!1;const n={amp:p,project:t.project||c.basename(e),stack:t.stack||{},config:{autoCapture:!0,maxEntries:1e3,rotationStrategy:"archive",inject:["all"],...t.config||{}}};return i.writeFileSync(o,JSON.stringify(n,null,2)+`
4
- `,"utf8"),!0}function E(e){const t=c.join(e,"inferno"),o=c.join(t,"sessions.jsonl");if(!i.existsSync(o))return{migrated:0,reason:"no legacy sessions.jsonl"};const n=c.join(e,".ai-memory"),r=c.join(n,"sessions.jsonl");if(i.existsSync(r))return{migrated:0,reason:".ai-memory/sessions.jsonl already exists"};l(e);const u=i.readFileSync(o,"utf8").split(`
5
- `).filter(Boolean);let f=0;for(const s of u)try{const d=JSON.parse(s),m=g(d);i.appendFileSync(r,JSON.stringify(m)+`
6
- `,"utf8"),f++}catch{}return i.writeFileSync(c.join(n,"MIGRATED.md"),`# Migrated from inferno/
1
+ import*as i from"node:fs";import*as F from"node:os";import*as r from"node:path";import{findProjectRoot as m}from"../projectRoot.mjs";import{getBranchInfo as b}from"../git/branch.mjs";const x="1.0",D=new Set(["gotcha","decision","attempt","note","detection","pattern"]),O=new Set(["copilot","cursor","claude","windsurf","other"]);function B(e){let t=r.basename(r.resolve(e));try{const o=r.join(e,".ai-memory","amp.json");if(i.existsSync(o)){const n=JSON.parse(i.readFileSync(o,"utf8"));n&&typeof n.project=="string"&&n.project.trim()&&(t=n.project.trim())}}catch{}return t.toLowerCase().replace(/[^a-z0-9_.\-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,64)||"unnamed-project"}function N(e,t){let o=process.env.INFERNOFLOW_GLOBAL_DIR;if(!o)try{const u=JSON.parse(i.readFileSync(r.join(t,"amp.json"),"utf8"));u&&typeof u.globalDir=="string"&&u.globalDir.trim()&&(o=u.globalDir.trim())}catch{}if(!o)return r.join(t,"global.jsonl");let n=o;n.startsWith("~")&&(n=r.join(F.homedir(),n.slice(1).replace(/^[\/\\]/,""))),r.isAbsolute(n)||(n=r.resolve(e,n));const f=B(e);return r.join(n,f,"global.jsonl")}function g(e,t={}){const o=t.literal?r.resolve(e):m(e),n=r.join(o,".ai-memory"),f=r.join(o,"inferno"),u=t.forWrite||i.existsSync(n)||!i.existsSync(f),c=u?n:f,s=u,l=r.join(c,"branches"),a=b(o),S=r.join(l,`${a.currentSlug}.jsonl`),j=a.defaultSlug?r.join(l,`${a.defaultSlug}.jsonl`):null,h=A(o,c);return{root:c,projectRoot:o,isAmp:s,sessions:r.join(c,"sessions.jsonl"),config:r.join(c,s?"amp.json":"config.json"),handoff:r.join(c,s?"handoff.md":"HANDOFF.md"),globalFile:h,branchesDir:l,currentBranchFile:S,defaultBranchFile:j,branch:a}}function A(e,t){try{return N(e,t)}catch{return r.join(t,"global.jsonl")}}function y(e){const t=m(e),o=r.join(t,".ai-memory");return i.existsSync(o)||i.mkdirSync(o,{recursive:!0}),o}const d="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function k(){let e=Date.now(),t="";for(let n=0;n<10;n++)t=d[e%32]+t,e=Math.floor(e/32);let o="";for(let n=0;n<16;n++)o+=d[Math.floor(Math.random()*32)];return t+o}function p(e){const t={...e.meta||{}};let o=e.type||"note";D.has(o)||(t.subtype=o,o="note"),e.result&&(t.result=e.result);let n;const f=e.agent;f&&O.has(f)?n=f:f&&(t.agent=f);const u=typeof e.ts=="number"?e.ts:e.ts?Date.parse(e.ts):Date.now(),c=e.confidence!=null?e.confidence:e.auto?.7:void 0,s={type:o,msg:e.summary||e.msg||"",ts:u,id:e.id||`amp_${k()}`};return e.file&&(s.file=e.file),e.line&&(s.line=e.line),e.function&&(s.function=e.function),e.tags&&e.tags.length&&(s.tags=e.tags),e.source&&(s.source=e.source),n&&(s.tool=n),e.session&&(s.session=e.session),c!=null&&(s.confidence=c),Object.keys(t).length&&(s.meta=t),s}function J(e){if(e.summary&&!e.msg)return e;const t=e.meta||{},o=t.subtype||e.type||"note",n={ts:e.ts,type:o,summary:e.msg||""};e.id&&(n.id=e.id),e.file&&(n.file=e.file),e.line&&(n.line=e.line),e.function&&(n.function=e.function),e.tags&&(n.tags=e.tags),e.source&&(n.source=e.source),e.tool&&(n.agent=e.tool),t.agent&&(n.agent=t.agent),t.result&&(n.result=t.result),e.confidence!=null&&(n.confidence=e.confidence,e.confidence<1&&(n.auto=!0));const{subtype:f,agent:u,result:c,...s}=t;return Object.keys(s).length&&(n.meta=s),n}function M(e){if(!e||!i.existsSync(e))return[];try{return i.readFileSync(e,"utf8").split(`
2
+ `).filter(Boolean).map(t=>{try{return JSON.parse(t)}catch{return null}}).filter(Boolean).map(J)}catch{return[]}}function P(e){const t=g(e),o=new Set,n=[],f=[t.sessions,t.globalFile,t.defaultBranchFile,t.currentBranchFile],u=[...new Set(f.filter(Boolean))];for(const c of u)for(const s of M(c)){const l=s.id||`${s.ts}|${s.summary}`;o.has(l)||(o.add(l),n.push(s))}return n.sort((c,s)=>{const l=typeof c.ts=="number"?c.ts:Date.parse(c.ts||0),a=typeof s.ts=="number"?s.ts:Date.parse(s.ts||0);return l-a})}function w(e,t){return t.target==="global"?e.globalFile:t.target==="legacy"?e.sessions:t.target==="branch"?e.currentBranchFile:t.type==="preference"?e.globalFile:e.currentBranchFile}function $(e,t){y(e);const o=g(e,{forWrite:!0}),n=w(o,t),f=p(t),u=JSON.stringify(f)+`
3
+ `;if(i.mkdirSync(r.dirname(n),{recursive:!0}),i.appendFileSync(n,u,"utf8"),n!==o.sessions)try{i.mkdirSync(r.dirname(o.sessions),{recursive:!0}),i.appendFileSync(o.sessions,u,"utf8")}catch{}return f}function C(e){const{config:t}=g(e);try{return JSON.parse(i.readFileSync(t,"utf8"))}catch{return null}}function L(e,t={}){y(e);const{config:o}=g(e,{forWrite:!0});if(i.existsSync(o))return!1;const n={amp:x,project:t.project||r.basename(e),stack:t.stack||{},config:{autoCapture:!0,maxEntries:1e3,rotationStrategy:"archive",inject:["all"],...t.config||{}}};return i.writeFileSync(o,JSON.stringify(n,null,2)+`
4
+ `,"utf8"),!0}function _(e){const t=r.join(e,"inferno"),o=r.join(t,"sessions.jsonl");if(!i.existsSync(o))return{migrated:0,reason:"no legacy sessions.jsonl"};const n=r.join(e,".ai-memory"),f=r.join(n,"sessions.jsonl");if(i.existsSync(f))return{migrated:0,reason:".ai-memory/sessions.jsonl already exists"};y(e);const u=i.readFileSync(o,"utf8").split(`
5
+ `).filter(Boolean);let c=0;for(const s of u)try{const l=JSON.parse(s),a=p(l);i.appendFileSync(f,JSON.stringify(a)+`
6
+ `,"utf8"),c++}catch{}return i.writeFileSync(r.join(n,"MIGRATED.md"),`# Migrated from inferno/
7
7
 
8
- Copied ${f} entries from inferno/sessions.jsonl on ${new Date().toISOString()}.
8
+ Copied ${c} entries from inferno/sessions.jsonl on ${new Date().toISOString()}.
9
9
 
10
10
  The original inferno/sessions.jsonl is untouched. You can delete it once you're confident the new layout works.
11
- `,"utf8"),{migrated:f,reason:"ok"}}export{A as AMP_MARKERS,p as AMP_VERSION,a as ampPaths,M as appendEntry,l as ensureAmpDir,x as fromAmp,h as generateULID,E as migrateLegacy,O as readConfig,D as readEntries,g as toAmp,N as writeDefaultConfig};
11
+ `,"utf8"),{migrated:c,reason:"ok"}}export{x as AMP_VERSION,g as ampPaths,$ as appendEntry,y as ensureAmpDir,J as fromAmp,k as generateULID,_ as migrateLegacy,B as projectSlug,C as readConfig,P as readEntries,N as resolveGlobalFile,p as toAmp,L as writeDefaultConfig};
@@ -0,0 +1,12 @@
1
+ import*as m from"node:fs";import*as p from"node:path";const d="# >>> infernoflow:start",s="# <<< infernoflow:end",g=["",d,"# Personal memory (per-developer, per-machine). Sync via cloud folder","# or `infernoflow sync`, not git.",".ai-memory/global.jsonl",".ai-memory/sessions.jsonl","# Regenerated artifacts \u2014 never commit these.",".ai-memory/handoff.md",".ai-memory/CONTEXT.draft.md",".ai-memory/HANDOFF.md",".ai-memory/.last-cli-version","# Build/publish hygiene \u2014 don't ship memory in published .NET / monorepo bundles.","**/publish/.ai-memory/","**/publish/inferno/","**/dist/.ai-memory/","**/dist/inferno/",s,""].join(`
2
+ `),I=["",d,"# Branch-local memory: append-only JSONL files. Auto-merge concurrent","# additions from different machines/branches as union of lines so","# `home \u2192 work \u2192 home` syncs don't produce conflicts.",".ai-memory/branches/*.jsonl merge=union",s,""].join(`
3
+ `),T="# --- infernoflow (developer-local AI memory; do not commit) ---",y="# --- /infernoflow ---";function A(n){const r=n.indexOf(T),i=n.indexOf(y);if(r===-1||i===-1||i<=r)return n;const o=n.slice(0,r).replace(/\s+$/,""),e=n.slice(i+y.length).replace(/^\s+/,"");return(o?o+`
4
+ `:"")+(e||"")}function h(n,r,i){const o=p.join(n,r);let e="";try{e=m.readFileSync(o,"utf8")}catch{}e=A(e);const c=e.indexOf(d),a=e.indexOf(s),l=i.trim();let t;if(c!==-1&&a!==-1&&a>c){const f=e.slice(0,c).replace(/\s+$/,""),u=e.slice(a+s.length).replace(/^\s+/,"");t=(f?f+`
5
+
6
+ `:"")+l+`
7
+ `+(u?`
8
+ `+u:"")}else e?t=e.replace(/\s+$/,"")+`
9
+
10
+ `+l+`
11
+ `:t=l+`
12
+ `;return t===e?"unchanged":(m.mkdirSync(p.dirname(o),{recursive:!0}),m.writeFileSync(o,t,"utf8"),e?"updated":"created")}function E(n){return{gitignore:h(n,".gitignore",g),gitattributes:h(n,".gitattributes",I)}}export{s as GITIGNORE_END,d as GITIGNORE_START,E as applyCleanTreePolicy,h as ensureManagedBlock};
@@ -1,2 +1,2 @@
1
- import*as g from"node:fs";import*as k from"node:path";import*as x from"node:https";import*as C from"node:http";import*as R from"node:readline";import{bold as d,cyan as v,gray as i,green as r,yellow as y,red as h}from"../ui/output.mjs";function O(n){return k.join(n,"inferno")}function A(n){const o=k.join(O(n),"integrations.json");if(!g.existsSync(o))return{};try{return JSON.parse(g.readFileSync(o,"utf8"))}catch{return{}}}function E(n,o){const t=O(n);g.existsSync(t)||g.mkdirSync(t,{recursive:!0}),g.writeFileSync(k.join(t,"integrations.json"),JSON.stringify(o,null,2)+`
2
- `)}const m=[{id:"anthropic",name:"Anthropic (Claude)",envKey:"ANTHROPIC_API_KEY",models:["claude-sonnet-4-6","claude-opus-4-6","claude-haiku-4-5-20251001"],default:"claude-sonnet-4-6",keyHint:"sk-ant-api03-\u2026",docsUrl:"https://console.anthropic.com/settings/keys"},{id:"openai",name:"OpenAI (GPT)",envKey:"OPENAI_API_KEY",models:["gpt-4o","gpt-4o-mini","gpt-4-turbo"],default:"gpt-4o",keyHint:"sk-\u2026",docsUrl:"https://platform.openai.com/api-keys"},{id:"gemini",name:"Google Gemini",envKey:"GOOGLE_AI_API_KEY",models:["gemini-2.0-flash","gemini-1.5-pro","gemini-1.5-flash"],default:"gemini-2.0-flash",keyHint:"AIza\u2026",docsUrl:"https://aistudio.google.com/app/apikey"},{id:"openrouter",name:"OpenRouter",envKey:"OPENROUTER_API_KEY",models:["anthropic/claude-sonnet-4-6","openai/gpt-4o","meta-llama/llama-3.1-8b-instruct:free"],default:"anthropic/claude-sonnet-4-6",keyHint:"sk-or-\u2026",docsUrl:"https://openrouter.ai/keys"},{id:"ollama",name:"Ollama (local)",envKey:null,models:["llama3.2","mistral","codellama","phi3"],default:"llama3.2",keyHint:null,docsUrl:"https://ollama.com"}];function _(n){return new Promise(o=>{const t=new URL(n),e=(t.protocol==="https:"?x:C).request({hostname:t.hostname,port:t.port||(t.protocol==="https:"?443:80),path:t.pathname+(t.search||""),method:"GET",timeout:5e3},s=>{let a="";s.on("data",c=>a+=c),s.on("end",()=>{try{o({status:s.statusCode,body:JSON.parse(a)})}catch{o({status:s.statusCode,body:a})}})});e.on("error",()=>o(null)),e.on("timeout",()=>{e.destroy(),o(null)}),e.end()})}async function b(n,o){const l={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}[n],e=o[n]?.apiKey,s=l||e,a=l?"env":e?"integrations.json":null,c=o[n]?.model||m.find(u=>u.id===n)?.default;if(n==="ollama"){const u=await _("http://localhost:11434/api/tags").catch(()=>null);if(u?.status===200){const p=u.body?.models?.map(f=>f.name)||[];return{configured:!0,source:"local",model:o.ollama?.model||"llama3.2",available:!0,models:p}}return{configured:!1,source:null,model:null,available:!1}}return{configured:!!s,source:a,model:c,available:null,masked:s?s.slice(0,8)+"\u2026":null}}async function N(n,o,t){try{const{callAI:l}=await import("../ai/providerRouter.mjs"),e=`Reply with exactly: "infernoflow AI test OK \u2014 ${n}"`;return await l(e,t,n)}catch{return null}}function $(n,o){return new Promise(t=>n.question(o,t))}async function U(n){const o=A(n);console.log(),console.log(` ${d("infernoflow ai")} ${i("\u2014 provider status")}`),console.log();let t=!1;for(const l of m){const e=await b(l.id,o);e.configured&&(t=!0);const s=e.configured?r("\u2713"):i("\u25CB"),a=d(l.name.padEnd(22)),c=e.configured?`${r("configured")} ${i(e.source)} ${i("model: "+e.model)}${e.masked?" "+i(e.masked):""}`:i("not configured");console.log(` ${s} ${a} ${c}`)}console.log(),t?console.log(` ${i("Run")} ${v("infernoflow ai test")} ${i("to verify the active provider.")}`):(console.log(` ${y("No AI providers configured.")} Run: ${v("infernoflow ai setup")}`),console.log(` ${i("Without a provider, explain/why/review use structural fallbacks.")}`)),console.log()}async function G(n){const o=A(n),t={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};console.log(),console.log(` ${d("\u{1F525} infernoflow ai setup")}`),console.log(` ${i("Connect an AI provider for explain, why, review, and changelog.")}`),console.log(),m.forEach((e,s)=>{const a=t[e.id],c=o[e.id]?.apiKey,u=a?r(" \u2713 key detected in environment"):c?r(" \u2713 key already saved"):"",p=d(String(s+1)),f=e.id==="ollama"?i(" (local, no key needed)"):"";console.log(` ${p}) ${d(e.name.padEnd(22))}${f}${u}`)}),console.log();const l=R.createInterface({input:process.stdin,output:process.stdout});try{const e=await $(l," Select provider [1]: "),s=(parseInt(e.trim())||1)-1;if(s<0||s>=m.length){console.log(h(` Invalid choice. Enter a number 1\u2013${m.length}.`));return}const a=m[s],c=a.id;if(console.log(),console.log(` ${d(a.name)}`),c==="ollama"){const p=await $(l," Ollama host [http://localhost:11434]: "),f=await $(l,` Model [${a.default}]: `);o.ollama={host:p.trim()||"http://localhost:11434",model:f.trim()||a.default},E(n,o),console.log(),process.stdout.write(` ${r("\u2713")} Saved. Testing connection\u2026 `),(await _(`${o.ollama.host}/api/tags`).catch(()=>null))?.status===200?console.log(r("OK")):(console.log(y("not reachable")),console.log(` ${y("\u26A0")} Start Ollama first: ${v("ollama serve")}`))}else{const p=t[c],f=o[c]?.apiKey,I=p||f;if(I){const w=p?"environment variable":"saved config";if(console.log(` ${r("\u2713")} API key detected from ${w}: ${i(I.slice(0,12)+"\u2026")}`),(await $(l," Use this key? [Y/n]: ")).trim().toLowerCase()==="n"){console.log(),a.docsUrl&&console.log(` ${i("Get a key at:")} ${v(a.docsUrl)}`);const K=await $(l," Paste new API key: ");if(!K.trim()){console.log(h(" No key provided. Exiting."));return}o[c]={apiKey:K.trim(),model:o[c]?.model||a.default}}else o[c]={apiKey:I,model:o[c]?.model||a.default}}else{console.log(` ${i("Get your API key at:")} ${v(a.docsUrl)}`),console.log(` ${i("Tip: paste the key below \u2014 it starts with")} ${i(a.keyHint)}`),console.log();const w=await $(l," Paste API key: ");if(!w.trim()){console.log(h(" No key provided. Exiting."));return}o[c]={apiKey:w.trim(),model:a.default}}const P=o[c].model;console.log(),console.log(` ${i("Available models:")} ${a.models.join(" ")}`);const S=await $(l,` Model [${P}]: `);o[c].model=S.trim()||P,E(n,o),console.log(),process.stdout.write(` ${r("\u2713")} Saved. Testing connection\u2026 `),(await N(c,o,n))?.text?console.log(r("OK")+i(` (${o[c].model})`)):(console.log(y("no response")),console.log(` ${y("\u26A0")} Connection failed \u2014 double-check your API key.`))}console.log(),console.log(` ${r("\u2713")} ${d(a.name)} is ready.`),console.log(` ${i("AI-powered commands:")} explain why review changelog`),console.log();const u=k.join(n,".gitignore");g.existsSync(u)&&(g.readFileSync(u,"utf8").includes("integrations.json")||(console.log(` ${y("\u26A0")} Add ${v("inferno/integrations.json")} to your .gitignore to avoid committing your API key.`),console.log()))}finally{l.close()}}async function Y(n,o){const t=A(o),l=n.find(s=>!s.startsWith("--"))||null;console.log(),console.log(` ${d("infernoflow ai test")}`),console.log();const e=l?m.filter(s=>s.id===l):m;for(const s of e){if(!(await b(s.id,t)).configured){console.log(` ${i("\u25CB")} ${d(s.name.padEnd(22))} ${i("not configured \u2014 skipping")}`);continue}process.stdout.write(` ${y("\u2026")} ${d(s.name.padEnd(22))} testing\u2026 `);const c=await N(s.id,t,o);c?.text?(console.log(r("OK")+i(` (${c.model||s.id})`)),console.log(` ${i(c.text.trim().slice(0,80))}`)):(console.log(h("FAIL")),console.log(` ${h("No response \u2014 check API key or model name")}`))}console.log()}async function T(n,o){const t=A(o),l=n.find(e=>!e.startsWith("--"));if(l||(console.error(h("\u2717 Usage: infernoflow ai clear <provider>")),console.error(i(" Example: infernoflow ai clear openai")),process.exit(1)),!t[l]){console.log(i(` No config found for "${l}"`));return}delete t[l],E(o,t),console.log(r(` \u2713 Cleared config for ${l}`))}async function L(n){const o=(n||[]).slice(1),t=o.find(s=>!s.startsWith("--"))||"status",l=o.filter(s=>s!==t),e=process.cwd();switch(t){case"setup":return G(e);case"status":return U(e);case"test":return Y(l,e);case"clear":return T(l,e);default:console.error(h(`\u2717 Unknown subcommand: "${t}"`)),console.error(i(" Usage: infernoflow ai <setup|status|test|clear>")),process.exit(1)}}export{L as aiCommand};
1
+ import*as g from"node:fs";import*as A from"node:path";import*as x from"node:https";import*as C from"node:http";import*as R from"node:readline";import{bold as d,cyan as v,gray as i,green as r,yellow as y,red as h}from"../ui/output.mjs";function O(n){return A.join(n,"inferno")}function k(n){const o=A.join(O(n),"integrations.json");if(!g.existsSync(o))return{};try{return JSON.parse(g.readFileSync(o,"utf8"))}catch{return{}}}function E(n,o){const t=O(n);g.existsSync(t)||g.mkdirSync(t,{recursive:!0}),g.writeFileSync(A.join(t,"integrations.json"),JSON.stringify(o,null,2)+`
2
+ `)}const m=[{id:"anthropic",name:"Anthropic (Claude)",envKey:"ANTHROPIC_API_KEY",models:["claude-sonnet-4-6","claude-opus-4-6","claude-haiku-4-5-20251001"],default:"claude-sonnet-4-6",keyHint:"sk-ant-api03-\u2026",docsUrl:"https://console.anthropic.com/settings/keys"},{id:"openai",name:"OpenAI (GPT)",envKey:"OPENAI_API_KEY",models:["gpt-4o","gpt-4o-mini","gpt-4-turbo"],default:"gpt-4o",keyHint:"sk-\u2026",docsUrl:"https://platform.openai.com/api-keys"},{id:"gemini",name:"Google Gemini",envKey:"GOOGLE_AI_API_KEY",models:["gemini-2.0-flash","gemini-1.5-pro","gemini-1.5-flash"],default:"gemini-2.0-flash",keyHint:"AIza\u2026",docsUrl:"https://aistudio.google.com/app/apikey"},{id:"openrouter",name:"OpenRouter",envKey:"OPENROUTER_API_KEY",models:["anthropic/claude-sonnet-4-6","openai/gpt-4o","meta-llama/llama-3.1-8b-instruct:free"],default:"anthropic/claude-sonnet-4-6",keyHint:"sk-or-\u2026",docsUrl:"https://openrouter.ai/keys"},{id:"ollama",name:"Ollama (local)",envKey:null,models:["llama3.2","mistral","codellama","phi3"],default:"llama3.2",keyHint:null,docsUrl:"https://ollama.com"}];function _(n){return new Promise(o=>{const t=new URL(n),e=(t.protocol==="https:"?x:C).request({hostname:t.hostname,port:t.port||(t.protocol==="https:"?443:80),path:t.pathname+(t.search||""),method:"GET",timeout:5e3},s=>{let a="";s.on("data",c=>a+=c),s.on("end",()=>{try{o({status:s.statusCode,body:JSON.parse(a)})}catch{o({status:s.statusCode,body:a})}})});e.on("error",()=>o(null)),e.on("timeout",()=>{e.destroy(),o(null)}),e.end()})}async function b(n,o){const l={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}[n],e=o[n]?.apiKey,s=l||e,a=l?"env":e?"integrations.json":null,c=o[n]?.model||m.find(u=>u.id===n)?.default;if(n==="ollama"){const u=await _("http://localhost:11434/api/tags").catch(()=>null);if(u?.status===200){const p=u.body?.models?.map(f=>f.name)||[];return{configured:!0,source:"local",model:o.ollama?.model||"llama3.2",available:!0,models:p}}return{configured:!1,source:null,model:null,available:!1}}return{configured:!!s,source:a,model:c,available:null,masked:s?s.slice(0,8)+"\u2026":null}}async function N(n,o,t){try{const l=await import("../ai/providerRouter.mjs");if(typeof l.callAI!="function")return null;const e=`Reply with exactly: "infernoflow AI test OK \u2014 ${n}"`;return await l.callAI(e,t,n)}catch{return null}}function $(n,o){return new Promise(t=>n.question(o,t))}async function U(n){const o=k(n);console.log(),console.log(` ${d("infernoflow ai")} ${i("\u2014 provider status")}`),console.log();let t=!1;for(const l of m){const e=await b(l.id,o);e.configured&&(t=!0);const s=e.configured?r("\u2713"):i("\u25CB"),a=d(l.name.padEnd(22)),c=e.configured?`${r("configured")} ${i(e.source)} ${i("model: "+e.model)}${e.masked?" "+i(e.masked):""}`:i("not configured");console.log(` ${s} ${a} ${c}`)}console.log(),t?console.log(` ${i("Run")} ${v("infernoflow ai test")} ${i("to verify the active provider.")}`):(console.log(` ${y("No AI providers configured.")} Run: ${v("infernoflow ai setup")}`),console.log(` ${i("Without a provider, explain/why/review use structural fallbacks.")}`)),console.log()}async function G(n){const o=k(n),t={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};console.log(),console.log(` ${d("\u{1F525} infernoflow ai setup")}`),console.log(` ${i("Connect an AI provider for explain, why, review, and changelog.")}`),console.log(),m.forEach((e,s)=>{const a=t[e.id],c=o[e.id]?.apiKey,u=a?r(" \u2713 key detected in environment"):c?r(" \u2713 key already saved"):"",p=d(String(s+1)),f=e.id==="ollama"?i(" (local, no key needed)"):"";console.log(` ${p}) ${d(e.name.padEnd(22))}${f}${u}`)}),console.log();const l=R.createInterface({input:process.stdin,output:process.stdout});try{const e=await $(l," Select provider [1]: "),s=(parseInt(e.trim())||1)-1;if(s<0||s>=m.length){console.log(h(` Invalid choice. Enter a number 1\u2013${m.length}.`));return}const a=m[s],c=a.id;if(console.log(),console.log(` ${d(a.name)}`),c==="ollama"){const p=await $(l," Ollama host [http://localhost:11434]: "),f=await $(l,` Model [${a.default}]: `);o.ollama={host:p.trim()||"http://localhost:11434",model:f.trim()||a.default},E(n,o),console.log(),process.stdout.write(` ${r("\u2713")} Saved. Testing connection\u2026 `),(await _(`${o.ollama.host}/api/tags`).catch(()=>null))?.status===200?console.log(r("OK")):(console.log(y("not reachable")),console.log(` ${y("\u26A0")} Start Ollama first: ${v("ollama serve")}`))}else{const p=t[c],f=o[c]?.apiKey,I=p||f;if(I){const w=p?"environment variable":"saved config";if(console.log(` ${r("\u2713")} API key detected from ${w}: ${i(I.slice(0,12)+"\u2026")}`),(await $(l," Use this key? [Y/n]: ")).trim().toLowerCase()==="n"){console.log(),a.docsUrl&&console.log(` ${i("Get a key at:")} ${v(a.docsUrl)}`);const K=await $(l," Paste new API key: ");if(!K.trim()){console.log(h(" No key provided. Exiting."));return}o[c]={apiKey:K.trim(),model:o[c]?.model||a.default}}else o[c]={apiKey:I,model:o[c]?.model||a.default}}else{console.log(` ${i("Get your API key at:")} ${v(a.docsUrl)}`),console.log(` ${i("Tip: paste the key below \u2014 it starts with")} ${i(a.keyHint)}`),console.log();const w=await $(l," Paste API key: ");if(!w.trim()){console.log(h(" No key provided. Exiting."));return}o[c]={apiKey:w.trim(),model:a.default}}const P=o[c].model;console.log(),console.log(` ${i("Available models:")} ${a.models.join(" ")}`);const S=await $(l,` Model [${P}]: `);o[c].model=S.trim()||P,E(n,o),console.log(),process.stdout.write(` ${r("\u2713")} Saved. Testing connection\u2026 `),(await N(c,o,n))?.text?console.log(r("OK")+i(` (${o[c].model})`)):(console.log(y("no response")),console.log(` ${y("\u26A0")} Connection failed \u2014 double-check your API key.`))}console.log(),console.log(` ${r("\u2713")} ${d(a.name)} is ready.`),console.log(` ${i("AI-powered commands:")} explain why review changelog`),console.log();const u=A.join(n,".gitignore");g.existsSync(u)&&(g.readFileSync(u,"utf8").includes("integrations.json")||(console.log(` ${y("\u26A0")} Add ${v("inferno/integrations.json")} to your .gitignore to avoid committing your API key.`),console.log()))}finally{l.close()}}async function Y(n,o){const t=k(o),l=n.find(s=>!s.startsWith("--"))||null;console.log(),console.log(` ${d("infernoflow ai test")}`),console.log();const e=l?m.filter(s=>s.id===l):m;for(const s of e){if(!(await b(s.id,t)).configured){console.log(` ${i("\u25CB")} ${d(s.name.padEnd(22))} ${i("not configured \u2014 skipping")}`);continue}process.stdout.write(` ${y("\u2026")} ${d(s.name.padEnd(22))} testing\u2026 `);const c=await N(s.id,t,o);c?.text?(console.log(r("OK")+i(` (${c.model||s.id})`)),console.log(` ${i(c.text.trim().slice(0,80))}`)):(console.log(h("FAIL")),console.log(` ${h("No response \u2014 check API key or model name")}`))}console.log()}async function T(n,o){const t=k(o),l=n.find(e=>!e.startsWith("--"));if(l||(console.error(h("\u2717 Usage: infernoflow ai clear <provider>")),console.error(i(" Example: infernoflow ai clear openai")),process.exit(1)),!t[l]){console.log(i(` No config found for "${l}"`));return}delete t[l],E(o,t),console.log(r(` \u2713 Cleared config for ${l}`))}async function L(n){const o=(n||[]).slice(1),t=o.find(s=>!s.startsWith("--"))||"status",l=o.filter(s=>s!==t),e=process.cwd();switch(t){case"setup":return G(e);case"status":return U(e);case"test":return Y(l,e);case"clear":return T(l,e);default:console.error(h(`\u2717 Unknown subcommand: "${t}"`)),console.error(i(" Usage: infernoflow ai <setup|status|test|clear>")),process.exit(1)}}export{L as aiCommand};