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.
- package/README.md +87 -149
- package/dist/bin/infernoflow.mjs +30 -33
- package/dist/lib/amp/io.mjs +8 -8
- package/dist/lib/cleanTree.mjs +12 -0
- package/dist/lib/commands/ai.mjs +2 -2
- package/dist/lib/commands/amp.mjs +4 -4
- package/dist/lib/commands/ask.mjs +2 -2
- package/dist/lib/commands/context.mjs +18 -18
- package/dist/lib/commands/doctor.mjs +2 -3
- package/dist/lib/commands/init.mjs +31 -32
- package/dist/lib/commands/log.mjs +13 -19
- package/dist/lib/commands/recap.mjs +3 -3
- package/dist/lib/commands/refresh.mjs +5 -0
- package/dist/lib/commands/setup.mjs +6 -6
- package/dist/lib/commands/status.mjs +6 -7
- package/dist/lib/commands/switch.mjs +5 -5
- package/dist/lib/commands/sync.mjs +41 -0
- package/dist/lib/git/branch.mjs +2 -0
- package/dist/lib/mcpRuntime.mjs +1 -0
- package/dist/lib/projectRoot.mjs +1 -0
- package/dist/lib/ruleFiles.mjs +9 -8
- package/dist/lib/upgradeCheck.mjs +1 -1
- package/dist/templates/cursor/inferno-mcp-server.mjs +200 -325
- package/package.json +13 -5
- package/dist/lib/commands/changelog.mjs +0 -21
- package/dist/lib/commands/ci.mjs +0 -3
- package/dist/lib/commands/claudeMd.mjs +0 -116
- package/dist/lib/commands/coverage.mjs +0 -2
- package/dist/lib/commands/demo.mjs +0 -113
- package/dist/lib/commands/diff.mjs +0 -5
- package/dist/lib/commands/explain.mjs +0 -8
- package/dist/lib/commands/feedback.mjs +0 -12
- package/dist/lib/commands/graph.mjs +0 -76
- package/dist/lib/commands/impact.mjs +0 -2
- package/dist/lib/commands/implement.mjs +0 -7
- package/dist/lib/commands/monorepo.mjs +0 -4
- package/dist/lib/commands/notify.mjs +0 -4
- package/dist/lib/commands/prImpact.mjs +0 -2
- package/dist/lib/commands/publish.mjs +0 -21
- package/dist/lib/commands/review.mjs +0 -24
- package/dist/lib/commands/run.mjs +0 -10
- package/dist/lib/commands/scaffold.mjs +0 -124
- package/dist/lib/commands/scan.mjs +0 -42
- package/dist/lib/commands/stability.mjs +0 -2
- package/dist/lib/commands/stats.mjs +0 -4
- package/dist/lib/commands/suggest.mjs +0 -62
- package/dist/lib/commands/syncAuto.mjs +0 -1
- package/dist/lib/commands/test.mjs +0 -6
- package/dist/lib/commands/theme.mjs +0 -18
- package/dist/lib/commands/upgrade.mjs +0 -20
- package/dist/lib/commands/watch.mjs +0 -7
- 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.
|
|
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
|
[](https://www.npmjs.com/package/infernoflow)
|
|
8
6
|
[](https://www.npmjs.com/package/infernoflow)
|
|
9
|
-
[](https://docs.npmjs.com/cli/v10/commands/npm-audit)
|
|
7
|
+
[](./package.json)
|
|
11
8
|
[](https://marketplace.visualstudio.com/items?itemName=infernoflow.infernoflow)
|
|
12
|
-
[](#)
|
|
13
9
|
[](./LICENSE)
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
## The loop
|
|
16
12
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
npx infernoflow init
|
|
26
|
+
infernoflow init --yes
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Zero
|
|
29
|
+
Zero runtime dependencies. Works on Node ≥ 18 — macOS, Linux, Windows.
|
|
30
30
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
39
|
+
These cover 95% of usage:
|
|
46
40
|
|
|
47
41
|
| Command | What it does |
|
|
48
42
|
|---|---|
|
|
49
|
-
| `infernoflow log "..."` | Remember a gotcha
|
|
50
|
-
| `infernoflow ask "..."` | Search your memory by keyword
|
|
51
|
-
| `infernoflow switch` | Generate a handoff for
|
|
52
|
-
| `infernoflow recap` | End-of-session summary with health score
|
|
53
|
-
| `infernoflow status` | Quick
|
|
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
|
-
|
|
52
|
+
`infernoflow commands` shows the full list (17 commands, grouped by purpose).
|
|
56
53
|
|
|
57
|
-
##
|
|
54
|
+
## Branch-aware memory + cross-machine sync
|
|
58
55
|
|
|
59
|
-
|
|
56
|
+
**Your teammate takes your branch — they inherit your memory.**
|
|
60
57
|
|
|
61
58
|
```
|
|
62
59
|
.ai-memory/
|
|
63
|
-
├──
|
|
64
|
-
├──
|
|
65
|
-
└──
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
+
## MCP tools (for AI agents)
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
-
##
|
|
106
|
+
## What it has caught (real dogfood)
|
|
91
107
|
|
|
92
|
-
|
|
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
|
-
- `
|
|
95
|
-
-
|
|
96
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
198
|
-
- **`infernoflow not found
|
|
199
|
-
- **PowerShell script execution blocked
|
|
200
|
-
-
|
|
201
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- [
|
|
215
|
-
- [
|
|
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)
|
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -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
|
|
3
|
-
|
|
4
|
-
|
|
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
|
|
9
|
-
${
|
|
10
|
-
${
|
|
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
|
-
${
|
|
10
|
+
${i("Usage:")}
|
|
13
11
|
infernoflow [command] [options]
|
|
14
12
|
|
|
15
|
-
${
|
|
16
|
-
${
|
|
17
|
-
${
|
|
18
|
-
${
|
|
19
|
-
${
|
|
20
|
-
${
|
|
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
|
-
${
|
|
23
|
-
${
|
|
24
|
-
${
|
|
25
|
-
${
|
|
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
|
-
${
|
|
28
|
-
${
|
|
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
|
-
${
|
|
33
|
-
${
|
|
34
|
-
`;import*as
|
|
35
|
-
${
|
|
36
|
-
`),console.log(
|
|
37
|
-
${
|
|
38
|
-
`),process.exit(0));const
|
|
39
|
-
Unknown command: ${
|
|
40
|
-
`)),process.exit(1));const
|
|
41
|
-
Error: `)+
|
|
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)});
|
package/dist/lib/amp/io.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import*as i from"node:fs";import*as
|
|
2
|
-
`).filter(Boolean).map(
|
|
3
|
-
|
|
4
|
-
`,"utf8"),!0}function
|
|
5
|
-
`).filter(Boolean);let
|
|
6
|
-
`,"utf8"),
|
|
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 ${
|
|
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:
|
|
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};
|
package/dist/lib/commands/ai.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import*as g from"node:fs";import*as
|
|
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
|
|
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};
|