infernoflow 0.44.0 β 0.44.2
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/lib/commands/doctor.mjs +2 -2
- package/dist/lib/commands/recap.mjs +5 -5
- package/dist/lib/commands/setup.mjs +6 -6
- package/dist/lib/commands/switch.mjs +10 -10
- package/dist/lib/mcpRuntime.mjs +1 -0
- package/dist/templates/cursor/inferno-mcp-server.mjs +71 -2
- package/package.json +1 -1
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)
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import*as s from"node:fs";import*as a from"node:path";import*as y from"node:os";import*as
|
|
1
|
+
import*as s from"node:fs";import*as a from"node:path";import*as y from"node:os";import*as O from"node:http";import{execSync as P,spawnSync as v}from"node:child_process";import{fileURLToPath as A}from"node:url";import{bold as C,cyan as b,gray as S,green as w,yellow as k,red as $}from"../ui/output.mjs";import{detectAvailableProviders as _}from"../ai/providerRouter.mjs";import{readEntries as I}from"../amp/io.mjs";function u(e,n){try{const o=n();return{label:e,...o}}catch(o){return{label:e,status:"error",message:o.message,fix:null}}}function c(e,n){return{status:"pass",message:e,detail:n||null,fix:null}}function l(e,n){return{status:"warn",message:e,detail:null,fix:n||null}}function h(e,n){return{status:"fail",message:e,detail:null,fix:n||null}}function E(){const e=process.version,n=parseInt(e.slice(1).split(".")[0],10);return n>=20?c(`Node.js ${e}`,"Node 20+ recommended"):n>=18?c(`Node.js ${e}`):h(`Node.js ${e} \u2014 infernoflow requires Node 18+`,"Install Node 20 from nodejs.org")}function M(){try{const e=v("infernoflow",["--version"],{encoding:"utf8",timeout:5e3,shell:process.platform==="win32"});return e.status===0?c(`infernoflow v${e.stdout.trim()} installed`):c("infernoflow CLI on PATH (version probe failed but doctor itself ran)")}catch{return c("infernoflow CLI on PATH (version probe threw but doctor itself ran)")}}function T(e){try{return P("git rev-parse --git-dir",{cwd:e,stdio:"ignore"}),c("Git repository detected")}catch{return h("Not a git repository","git init && git add . && git commit -m 'init'")}}function D(e){const n=a.join(e,".ai-memory"),o=a.join(e,"inferno");return s.existsSync(n)&&s.existsSync(o)?c(".ai-memory/ + inferno/ both present"):s.existsSync(n)?c(".ai-memory/ directory exists (memory mode)"):s.existsSync(o)?c("inferno/ directory exists"):h("No memory directory found (.ai-memory/ or inferno/)","infernoflow init")}function G(e){if(x(e)){let o=0;try{o=I(e).length}catch{}return c(o===0?"Memory mode \u2014 sessions.jsonl will be created on first log":`Memory mode \u2014 ${o} session entr${o===1?"y":"ies"}`)}const n=a.join(e,"inferno");for(const o of["contract.json","capabilities.json"]){const t=a.join(n,o);if(s.existsSync(t))try{const i=(JSON.parse(s.readFileSync(t,"utf8")).capabilities||[]).length;return c(`${o} valid \u2014 ${i} capabilities`)}catch{return h(`${o} contains invalid JSON`,`Fix the JSON syntax in inferno/${o}`)}}return h("No contract.json/capabilities.json (and not in memory mode)","infernoflow init or infernoflow init --mode full")}function x(e){const n=a.join(e,".ai-memory"),o=a.join(e,"inferno","contract.json");if(s.existsSync(n)&&!s.existsSync(o))return!0;try{return JSON.parse(s.readFileSync(a.join(e,"inferno","config.json"),"utf8")).mode==="memory"}catch{return!1}}function F(e){if(x(e))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const n=a.join(e,"inferno","scenarios");if(!s.existsSync(n))return l("No scenarios/ directory","infernoflow init");const o=s.readdirSync(n).filter(t=>t.endsWith(".json"));return o.length?c(`${o.length} scenario file${o.length!==1?"s":""} found`):l("scenarios/ is empty","Add scenario files or run infernoflow suggest")}function L(e){if(x(e))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const n=a.join(e,"inferno","CHANGELOG.md");return s.existsSync(n)?c("inferno/CHANGELOG.md exists"):l("No inferno/CHANGELOG.md","infernoflow init")}function R(e){if(x(e))return{status:"info",message:"n/a in memory mode (CLAUDE.md is auto-maintained)",detail:null,fix:null};const n=a.join(e,"inferno","CONTEXT.md");if(!s.existsSync(n))return l("No CONTEXT.md generated","infernoflow context");const o=(Date.now()-s.statSync(n).mtimeMs)/(1e3*60*60*24);return o>7?l(`CONTEXT.md is ${Math.round(o)} days old \u2014 may be stale`,"infernoflow context"):c(`CONTEXT.md present (${Math.round(o)}d old)`)}function J(e){const n=a.join(e,".git","hooks"),o=a.join(n,"post-commit"),t=a.join(n,"pre-push"),r=s.existsSync(o)&&s.readFileSync(o,"utf8").includes("infernoflow"),i=s.existsSync(t)&&s.readFileSync(t,"utf8").includes("infernoflow");return r&&i?c("Git hooks installed (post-commit + pre-push)"):l(r||i?"Partial git hooks installed":"Git hooks not installed","infernoflow setup --yes")}function H(e){const n=[a.join(e,".cursor","mcp.json"),a.join(e,".mcp.json"),a.join(y.homedir(),".cursor","mcp.json"),a.join(y.homedir(),"Library","Application Support","Claude","claude_desktop_config.json"),a.join(y.homedir(),"AppData","Roaming","Claude","claude_desktop_config.json")];for(const o of n)if(s.existsSync(o))try{const t=JSON.parse(s.readFileSync(o,"utf8")),r=t.mcpServers||t.mcp_servers||{};if(Object.keys(r).some(i=>i.toLowerCase().includes("inferno")))return c(`MCP server configured in ${a.basename(o)}`)}catch{}return l("MCP server not configured","infernoflow setup --yes (adds to Cursor/Claude config)")}function W(e,n){const o=a.join(e,".ai-memory",".mcp-runtime.json");if(!s.existsSync(o))return c("MCP runtime stamp not present yet \u2014 start your AI tool to write one");let t;try{t=JSON.parse(s.readFileSync(o,"utf8"))}catch{return l(".mcp-runtime.json present but unreadable","Delete .ai-memory/.mcp-runtime.json and restart your AI tool")}return!t||typeof t.version!="string"?l(".mcp-runtime.json malformed","Delete it; the next MCP boot will rewrite it"):t.version===n?c(`MCP runtime v${t.version} matches CLI`):n.startsWith("0.0.0")||t.version.startsWith("0.0.0")?c(`MCP runtime v${t.version} (dev/source) \u2014 skipping version-skew check`):l(`MCP server is running v${t.version} but CLI is v${n} \u2014 restart your AI tool to load the new code.`,"Quit and reopen Cursor / Claude Code / VS Code (the long-running MCP process keeps the old code in memory until restart)")}function X(e){const n=_(e),o=Object.entries(n).filter(([,t])=>t).map(([t])=>t);return o.length?c(`AI provider${o.length!==1?"s":""}: ${o.join(", ")}`):l("No AI provider configured",`Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY, or OPENROUTER_API_KEY
|
|
2
2
|
Or install Ollama (ollama.com) for free local AI
|
|
3
|
-
Or use VS Code with GitHub Copilot (zero config)`)}async function
|
|
3
|
+
Or use VS Code with GitHub Copilot (zero config)`)}async function K(){return new Promise(e=>{const n=O.get({hostname:"localhost",port:11434,path:"/api/tags",timeout:1500},o=>{e(c("Ollama running on localhost:11434"))});n.on("error",()=>e({status:"info",message:"Ollama not running (optional)",fix:"ollama serve",detail:null})),n.on("timeout",()=>{n.destroy(),e({status:"info",message:"Ollama not running (optional)",fix:null,detail:null})})})}function U(){const e=a.join(y.homedir(),".infernoflow","credentials.json");if(!s.existsSync(e))return{status:"info",message:"Not logged in to cloud (optional)",fix:"infernoflow login",detail:null};try{const n=JSON.parse(s.readFileSync(e,"utf8")),o=n.user?.login||n.user?.name||n.user?.email||"unknown";if(n.mode==="supabase"&&n.access_token){if(n.expires_at){const t=new Date(n.expires_at).getTime();if(Date.now()>t)return l(`JWT expired for ${o} \u2014 refresh on next log will retry`,"infernoflow login")}return c(`Authenticated as ${o} (Supabase JWT \u2014 auth.uid() writes)`)}return n.mode==="device-flow"&&n.github_access_token?{status:"info",message:`Identity-only as ${o} (device flow \u2014 anon-mode writes)`,fix:"infernoflow login (without --device-flow, for full auth)",detail:null}:n.access_token?l(`Legacy login for ${o} \u2014 re-run for authenticated cloud writes`,"infernoflow logout && infernoflow login"):{status:"info",message:"Credentials file present but no recognised token",fix:"infernoflow logout && infernoflow login",detail:null}}catch{return l("Credentials file unreadable","infernoflow logout && infernoflow login")}}function V(){try{const e=A(import.meta.url),n=a.resolve(a.dirname(e),"..","..","bin","infernoflow.mjs");if(!s.existsSync(n))return{status:"info",message:"bin/infernoflow.mjs not found from doctor location",fix:null,detail:null};const t=[...s.readFileSync(n,"utf8").matchAll(/import\("\.\.\/lib\/(commands\/[^"]+|telemetry\.mjs)"\)/g)],r=[],i=a.resolve(a.dirname(n),"..");for(const m of t){const d=m[1],p=a.join(i,"lib",d);s.existsSync(p)||r.push(d)}return r.length?h(`${r.length} routed command(s) missing module files: ${r.slice(0,3).join(", ")}${r.length>3?"\u2026":""}`,"Restore the missing files or remove their entries from bin/infernoflow.mjs"):c(`All ${t.length} routed commands resolve to real files`)}catch(e){return{status:"info",message:`Router integrity check skipped: ${e.message}`,fix:null,detail:null}}}function Y(e){const n=a.join(e,"package.json");if(!s.existsSync(n))return c("No package.json to audit");let o;try{o=JSON.parse(s.readFileSync(n,"utf8"))}catch{return{status:"info",message:"package.json unreadable; skipping audit",detail:null,fix:null}}const t=o.scripts||{},r=new Set(["log","ask","switch","recap","status","init","doctor","graph","watch","amp","contract","dev","demo","setup","log-decision","log-attempt","context","stats","test"]),i=[];for(const[d,p]of Object.entries(t)){const j=/\binfernoflow\s+([a-z][a-z0-9-]*)/.exec(String(p));if(!j)continue;const f=j[1];r.has(f)||i.push({scriptName:d,verb:f})}if(i.length===0)return c("npm scripts use current command surface");const m=i.map(d=>`${d.scriptName} \u2192 infernoflow ${d.verb}`).join(", ");return l(`package.json references ${i.length} deprecated command(s): ${m}`,"Edit package.json scripts to use the current surface (run `infernoflow --help` to list verbs)")}function z(e){const n=a.join(e,".gitignore");if(!s.existsSync(n))return{status:"info",message:".gitignore not found",fix:null,detail:null};const o=s.readFileSync(n,"utf8");return/^(?:\*\*\/)?node_modules\/?$/m.test(o)?c(".gitignore excludes node_modules"):l(".gitignore does not exclude node_modules","Add 'node_modules/' (and '**/node_modules/') to .gitignore")}function q(e,n){const o=e.filter(r=>r.status==="warn"&&r.fix),t=[];for(const r of o){const i=r.fix;if(i.startsWith("infernoflow ")){const m=i.slice(12).split(" ");v("infernoflow",m,{cwd:n,encoding:"utf8",timeout:3e4}).status===0&&t.push(r.label)}}return t}function B(e){return e==="pass"?w("\u2714"):e==="warn"?k("\u26A0"):e==="fail"?$("\u2717"):S("\xB7")}function Q(e,n){const o={pass:0,warn:0,fail:0,info:0,error:0};for(const i of e)o[i.status]=(o[i.status]||0)+1;console.log(),console.log(` ${C("\u{1F525} infernoflow doctor")}`),console.log();const t=Math.max(...e.map(i=>i.label.length))+2;for(const i of e)console.log(` ${B(i.status)} ${C(i.label.padEnd(t))} ${i.message}`),i.detail&&console.log(` ${" ".repeat(t)} ${S(i.detail)}`),i.fix&&(i.status==="warn"||i.status==="fail")&&console.log(` ${" ".repeat(t)} ${b("fix:")} ${S(i.fix)}`);console.log();const r=o.fail>0?$("issues found"):o.warn>0?k("warnings"):w("all good");console.log(` ${r} \u2014 ${w(String(o.pass))} pass \xB7 ${k(String(o.warn))} warn \xB7 ${$(String(o.fail))} fail (${n}ms)`),console.log(),(o.warn>0||o.fail>0)&&(console.log(` Run ${b("infernoflow doctor --fix")} to auto-fix warnings`),console.log())}async function Z(e){const n=e.slice(1),o=n.includes("--json"),t=n.includes("--fix"),r=process.cwd(),i=Date.now();let m="0.0.0-unknown";try{const{fileURLToPath:f}=await import("node:url"),g=a.dirname(f(import.meta.url)),N=a.join(g,"..","..","package.json");m=JSON.parse(s.readFileSync(N,"utf8")).version||m}catch{}const d=[u("Node.js version",()=>E()),u("infernoflow CLI",()=>M()),u("Git repository",()=>T(r)),u("inferno/ directory",()=>D(r)),u("Contract / mode",()=>G(r)),u("Scenarios",()=>F(r)),u("Changelog",()=>L(r)),u("CONTEXT.md",()=>R(r)),u("Git hooks",()=>J(r)),u("MCP server",()=>H(r)),u("MCP runtime version",()=>W(r,m)),u("AI providers",()=>X(r)),u("Cloud sync",()=>U()),u(".gitignore",()=>z(r)),u("Router integrity",()=>V()),u("npm scripts",()=>Y(r)),await K().then(f=>({label:"Ollama (local AI)",...f}))],p=Date.now()-i;if(t){const f=q(d,r);if(f.length)return o||(console.log(),f.forEach(g=>console.log(` ${w("\u2714")} Fixed: ${g}`)),console.log()),Z(["doctor","--json"])}if(o){const f={pass:0,warn:0,fail:0,info:0};d.forEach(g=>f[g.status]=(f[g.status]||0)+1),console.log(JSON.stringify({ok:f.fail===0,counts:f,results:d,elapsed:p}));return}Q(d,p),d.some(f=>f.status==="fail")&&process.exit(1)}export{Z as doctorCommand};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import*as C from"node:fs";import*as
|
|
2
|
-
`),...
|
|
3
|
-
`),...
|
|
4
|
-
`)].map(
|
|
5
|
-
`)),process.exit(1));const
|
|
1
|
+
import*as C from"node:fs";import*as I from"node:path";import{execSync as M}from"node:child_process";import{bold as b,cyan as y,gray as t,green as k,yellow as $,red as D}from"../ui/output.mjs";import{readEntries as R}from"../amp/io.mjs";const N="inferno",v=I.join(N,"contract.json");function L(n){try{return JSON.parse(C.readFileSync(n,"utf8"))}catch{return null}}function A(n,s){if(s){const g=s.match(/^(\d+)h$/i),f=s.match(/^(\d+)d$/i);if(g)return new Date(Date.now()-parseInt(g[1])*36e5);if(f)return new Date(Date.now()-parseInt(f[1])*864e5);const r=new Date(s);if(!isNaN(r.getTime()))return r}const o=new Date(Date.now()-864e5),e=[];for(const g of n)if(g.type==="handoff"){const f=new Date(g.ts||0);isNaN(f.getTime())||e.push(f)}if(e.length===0)return o;const l=300*1e3,u=e[e.length-1];if(Date.now()-u.getTime()<l){if(e.length>=2){const g=e[e.length-2];return g>o?g:o}return o}return u>o?u:o}function P(n){const s=process.cwd(),o=f=>{try{return M(f,{cwd:s,encoding:"utf8",timeout:5e3,stdio:["pipe","pipe","pipe"]}).trim()}catch{return""}},e=n.toISOString().slice(0,19),l=o("git diff --cached --name-only"),u=o("git diff --name-only"),a=o(`git log --since="${e}" --name-only --pretty=format:""`);return[...new Set([...l.split(`
|
|
2
|
+
`),...u.split(`
|
|
3
|
+
`),...a.split(`
|
|
4
|
+
`)].map(f=>f.trim()).filter(Boolean))]}function _(n,s){const o=[{keywords:["auth","login","logout","session","jwt","token","password"],topic:"authentication"},{keywords:["stripe","payment","checkout","billing","subscription"],topic:"payments"},{keywords:["upload","file","s3","storage","bucket","cdn"],topic:"file handling"},{keywords:["email","sendgrid","ses","smtp","nodemailer","twilio"],topic:"notifications"},{keywords:["db","database","prisma","mongoose","postgres","mysql","migration"],topic:"database"},{keywords:["deploy","docker","ci","workflow","action","kubernetes"],topic:"deployment"},{keywords:["cache","redis","memcache"],topic:"caching"},{keywords:["test","spec","jest","vitest","cypress","playwright"],topic:"testing"},{keywords:["config","env",".env","environment","secret"],topic:"configuration"},{keywords:["api","route","endpoint","controller","handler"],topic:"API routes"},{keywords:["ui","component","style","css","tailwind","theme"],topic:"UI/styles"}],e=s.map(a=>(a.summary||"").toLowerCase()).join(" "),l=[],u=new Set;for(const a of o){if(u.has(a.topic))continue;const g=n.filter(r=>a.keywords.some(w=>r.toLowerCase().includes(w)));!g.length||a.keywords.some(r=>e.includes(r))||(u.add(a.topic),l.push({topic:a.topic,files:g.slice(0,3),suggestedType:"gotcha"}))}return l}function B(n){const s=new Set(n.map(l=>l.type));let o=0;const e=[];return n.length>0?(o+=20,e.push({ok:!0,label:`${n.length} entr${n.length!==1?"ies":"y"} logged`})):e.push({ok:!1,label:"nothing logged this session"}),s.has("gotcha")?(o+=35,e.push({ok:!0,label:"gotchas captured"})):e.push({ok:!1,label:"no gotchas (most valuable \u2014 log landmines!)"}),s.has("decision")?(o+=25,e.push({ok:!0,label:"decisions recorded"})):e.push({ok:!1,label:"no decisions recorded"}),s.has("attempt")&&(o+=10,e.push({ok:!0,label:"attempts tracked"})),s.has("preference")&&(o+=10,e.push({ok:!0,label:"preferences noted"})),{score:Math.min(o,100),checks:e}}function H(n){if(!n)return"";const s=new Date(n),o=Date.now()-s.getTime(),e=Math.floor(o/6e4);if(e<60)return`${e}m ago`;const l=Math.floor(o/36e5);return l<24?`${l}h ago`:`${Math.floor(o/864e5)}d ago`}const J={gotcha:"\u26A0",decision:"\u2713",attempt:"\u21BA",preference:"\u2666",theme:"\u25C8",note:"\xB7",error:"\u2717",handoff:"\u2192"},U={gotcha:$,decision:k,attempt:y,preference:y,theme:y,note:t,error:D,handoff:t};function G(n){const s=U[n.type]||t,o=J[n.type]||"\xB7",e=n.result?t(` [${n.result}]`):"",l=t(` (${H(n.ts)})`);console.log(` ${s(o+" "+(n.type||"note").padEnd(11))}${e}${l}`),console.log(` ${n.summary}`)}async function Q(n=[]){const s=n,o=s.includes("--json"),e=s.includes("--brief"),l=s.indexOf("--since"),u=l!==-1?s[l+1]:null,a=process.cwd();!C.existsSync(I.join(a,N))&&!C.existsSync(I.join(a,".ai-memory"))&&(o||console.error(D(` \u2718 not initialized \u2014 run: infernoflow init
|
|
5
|
+
`)),process.exit(1));const g=R(a),f=A(g,u),r=g.filter(d=>new Date(d.ts||0)>f),w=P(f),m=_(w,r),{score:i,checks:O}=B(r),F=L(I.join(a,v));if(o){console.log(JSON.stringify({sessionStart:f.toISOString(),entries:r,changedFiles:w,unloggedTopics:m,health:{score:i,checks:O}},null,2));return}if(e){const d=i>=80?"A":i>=60?"B":i>=40?"C":"D",p=i>=60?k:i>=40?$:D;console.log(p(`Session health: ${d} (${i}/100)`)+t(` \u2014 ${r.length} entries logged`)),m.length&&console.log($(` ${m.length} topic${m.length!==1?"s":""} changed but not logged: `)+m.map(c=>c.topic).join(", "));return}const S=t(" "+"\u2500".repeat(52));console.log(),console.log(" "+b("\u{1F525} infernoflow recap")),F?.policyId&&console.log(t(` Project: ${F.policyId}`));const j=f.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"});if(console.log(t(` Session since: ${j}`)),console.log(S),console.log(),console.log(" "+b("Captured this session")),console.log(),r.length===0)console.log(t(" Nothing logged yet this session."));else{const d=["gotcha","decision","attempt","preference","theme","note","error"],p=new Map;for(const c of r){const h=c.type||"note";p.has(h)||p.set(h,[]),p.get(h).push(c)}for(const c of d){const h=p.get(c);if(h?.length)for(const x of h)console.log(),G(x)}}if(m.length>0){console.log(),console.log(S),console.log(),console.log(" "+b("Changed but not logged")+t(" (git diff since session start)")),console.log();for(const{topic:d,files:p}of m){console.log($(` ? ${d}`));for(const c of p)console.log(t(` ${c}`))}console.log(),console.log(t(" Any gotchas or decisions from these areas worth capturing?")),console.log(t(" Run: ")+y('infernoflow log "<what happened>" --type gotcha'))}else w.length>0&&(console.log(),console.log(S),console.log(),console.log(k(" \u2714 ")+t(`${w.length} changed files \u2014 all topics appear to be logged`)));console.log(),console.log(S),console.log(),console.log(" "+b("Session health")),console.log();const E=i>=80?"A":i>=60?"B":i>=40?"C":"D",T=i>=60?k:i>=40?$:D;console.log(` ${T(b(`${E}`))} ${T(`${i}/100`)}`),console.log();for(const{ok:d,label:p}of O){const c=d?k(" \u2714"):$(" \xB7");console.log(`${c} ${d?p:t(p)}`)}{const d=r.filter(h=>h.type==="gotcha").length,p=r.filter(h=>h.type==="decision").length,c=[];if(d===0?c.push(y('infernoflow log "..." --type gotcha')+t(" \u2014 adds 35 pts")):d<3&&i<80&&c.push(t(` ${3-d} more gotcha(s) would push you higher`)),p===0&&c.push(y('infernoflow log "..." --type decision')+t(" \u2014 adds 25 pts")),i>=60&&i<80&&c.push(t(" Almost B! One more entry gets you there.")),i>=80&&c.push(k(" Great session \u2014 your handoff will be excellent.")),c.length){console.log(),console.log(t(" How to improve:"));for(const h of c)console.log(" "+h)}}(r.length>0||m.length>0)&&(console.log(),console.log(S),console.log(),console.log(t(" Before your next session:")),console.log(t(" ")+y("infernoflow switch")+t(" \u2014 generate a handoff summary for the next AI agent")),console.log(t(" ")+y("infernoflow ask --recent")+t(" \u2014 review what's in memory before starting"))),console.log()}export{Q as recapCommand};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import*as n from"node:fs";import*as
|
|
2
|
-
`,"utf8"),{updated:!0,path:o})}function
|
|
3
|
-
`,"utf8"),i.cursorMcp=!0,o("Registered MCP server in "+f(".cursor/mcp.json")))}catch(e){r(".cursor/mcp.json update skipped: "+e.message)}try{
|
|
4
|
-
${
|
|
5
|
-
`),
|
|
6
|
-
${g("What was set up:")}`),console.log(` ${d("\u2714")} MCP server installed \u2192 ${f(".cursor/inferno-mcp-server.mjs")}`),e.cursorMcp&&console.log(` ${d("\u2714")} Cursor MCP config \u2192 ${f(".cursor/mcp.json")}`),e.vscodeMcp&&console.log(` ${d("\u2714")} VS Code Copilot MCP config \u2192 ${f(".vscode/mcp.json")}`),e.claudeJson&&console.log(` ${d("\u2714")} Claude Code MCP config \u2192 ${f("~/.claude.json")}`),e.claudeSettings&&console.log(` ${d("\u2714")} Auto-approved tools \u2192 ${f(".claude/settings.json")}`),console.log(),console.log(` ${g("Next step:")} Restart your AI tool. Test by asking:`),console.log(` ${f('"call the amp_write tool with a test note"')}`),console.log()}export{
|
|
1
|
+
import*as n from"node:fs";import*as a from"node:path";import*as k from"node:os";import{fileURLToPath as P}from"node:url";import{execSync as J}from"node:child_process";import{detectIdeContext as O}from"../ai/ideDetection.mjs";import{header as R,ok as S,warn as F,info as w,done as N,cyan as f,yellow as v,bold as g,green as d,gray as T}from"../ui/output.mjs";import"../cursorHooksInstall.mjs";import"../vsCodeCopilotHooksInstall.mjs";const V=a.dirname(P(import.meta.url));function C(){return a.resolve(V,"../../templates")}function L(s){try{return J(`npx infernoflow ${s}`,{encoding:"utf8",cwd:process.cwd(),timeout:6e4,stdio:["inherit","pipe","pipe"]})}catch(t){return t.stdout||t.stderr||t.message}}const D=["infernoflow_status","infernoflow_run","infernoflow_apply","infernoflow_check","infernoflow_context","infernoflow_implement","infernoflow_git_drift","infernoflow_scan_ui","infernoflow_review","amp_read","amp_write","amp_search","amp_handoff","amp_health"];function I(s){const t=a.join(k.homedir(),".claude.json");let c={};if(n.existsSync(t))try{c=JSON.parse(n.readFileSync(t,"utf8"))}catch{c={}}c.mcpServers||(c.mcpServers={});const o=c.mcpServers.infernoflow;if(o&&o.args&&o.args[0]===s)return{updated:!1};c.mcpServers.infernoflow={command:"node",args:[s]};const r=JSON.stringify(c,null,2).replace(/\u0000+/g,"");return n.writeFileSync(t,r,"utf8"),{updated:!0,path:t}}function W(s,t){const c=a.join(s,".vscode"),o=a.join(c,"mcp.json");let r={};if(n.existsSync(o))try{r=JSON.parse(n.readFileSync(o,"utf8"))}catch{r={}}r.servers||(r.servers={});const i=r.servers.infernoflow;return i&&i.args&&i.args[0]===t?{updated:!1}:(r.servers.infernoflow={type:"stdio",command:"node",args:[t]},n.mkdirSync(c,{recursive:!0}),n.writeFileSync(o,JSON.stringify(r,null,2)+`
|
|
2
|
+
`,"utf8"),{updated:!0,path:o})}function A(s,t){const c=a.join(s,".claude"),o=a.join(c,"settings.json");let r={};if(n.existsSync(o))try{r=JSON.parse(n.readFileSync(o,"utf8"))}catch{r={}}const i=new Set(r.allowedTools||[]);for(const l of D)i.add(`mcp__infernoflow__${l}`);const m={...r,allowedTools:[...i]};return n.mkdirSync(c,{recursive:!0}),n.writeFileSync(o,JSON.stringify(m,null,2),"utf8"),o}function b(s,{silent:t=!1}={}){const c=C(),o=t?()=>{}:e=>S(e),r=t?()=>{}:e=>F(e),i={mcpServer:!1,claudeJson:!1,claudeSettings:!1},m=a.join(c,"cursor","inferno-mcp-server.mjs"),l=a.join(s,".cursor","inferno-mcp-server.mjs");try{n.existsSync(l)||(n.mkdirSync(a.dirname(l),{recursive:!0}),n.copyFileSync(m,l),i.mcpServer=!0,o("Copied MCP server \u2192 "+f(".cursor/inferno-mcp-server.mjs")))}catch(e){r("MCP server copy skipped: "+e.message)}try{I(l).updated&&(i.claudeJson=!0,o("Registered MCP server in "+f("~/.claude.json")))}catch(e){r("~/.claude.json update skipped: "+e.message)}try{W(s,l).updated&&(i.vscodeMcp=!0,o("Registered MCP server in "+f(".vscode/mcp.json")+T(" (Copilot Chat)")))}catch(e){r(".vscode/mcp.json update skipped: "+e.message)}try{const e=a.join(s,".cursor","mcp.json");let p={};if(n.existsSync(e))try{p=JSON.parse(n.readFileSync(e,"utf8"))}catch{p={}}p.mcpServers||(p.mcpServers={});const u=p.mcpServers.infernoflow;(!u||!u.args||u.args[0]!==l)&&(p.mcpServers.infernoflow={command:"node",args:[l],env:{}},n.mkdirSync(a.dirname(e),{recursive:!0}),n.writeFileSync(e,JSON.stringify(p,null,2)+`
|
|
3
|
+
`,"utf8"),i.cursorMcp=!0,o("Registered MCP server in "+f(".cursor/mcp.json")))}catch(e){r(".cursor/mcp.json update skipped: "+e.message)}try{A(s,!1),i.claudeSettings=!0,o("Pre-approved infernoflow tools in "+f(".claude/settings.json"))}catch(e){r(".claude/settings.json skipped: "+e.message)}return i}async function Q(s){const t=process.cwd(),c=s.includes("--force")||s.includes("-f"),o=s.includes("--yes")||s.includes("-y"),r=C();R("infernoflow setup");const{ideDetected:i}=O("auto");w(`IDE detected: ${g(i==="cursor"?"Cursor":i==="vscode"?"VS Code":i==="windsurf"?"Windsurf":"unknown")}`);const l=a.join(t,".ai-memory");n.existsSync(l)?S(".ai-memory/ already exists \u2014 skipping init"):(console.log(`
|
|
4
|
+
${v(".ai-memory/")} not found \u2014 running init ...
|
|
5
|
+
`),L(o?"init --yes":"init")),console.log(),w("Wiring up MCP servers for Cursor / VS Code Copilot / Claude Code ...");const e=b(t,{silent:!1});console.log(),N("infernoflow ready"),console.log(`
|
|
6
|
+
${g("What was set up:")}`),console.log(` ${d("\u2714")} MCP server installed \u2192 ${f(".cursor/inferno-mcp-server.mjs")}`),e.cursorMcp&&console.log(` ${d("\u2714")} Cursor MCP config \u2192 ${f(".cursor/mcp.json")}`),e.vscodeMcp&&console.log(` ${d("\u2714")} VS Code Copilot MCP config \u2192 ${f(".vscode/mcp.json")}`),e.claudeJson&&console.log(` ${d("\u2714")} Claude Code MCP config \u2192 ${f("~/.claude.json")}`),e.claudeSettings&&console.log(` ${d("\u2714")} Auto-approved tools \u2192 ${f(".claude/settings.json")}`);try{const{detectStaleMcpRuntime:p}=await import("../mcpRuntime.mjs"),{readFileSync:u}=await import("node:fs"),{dirname:j,join:h}=await import("node:path"),{fileURLToPath:_}=await import("node:url"),x=j(_(import.meta.url)),M=h(x,"..","..","package.json"),$=JSON.parse(u(M,"utf8")).version,y=p(t,$);y&&(console.log(),console.log(` ${v("\u26A0")} ${g("Restart required:")} ${y.message}`))}catch{}console.log(),console.log(` ${g("Next step:")} Restart your AI tool. Test by asking:`),console.log(` ${f('"call the amp_write tool with a test note"')}`),console.log()}export{D as MCP_TOOLS,b as autoSetupMcp,Q as setupCommand,I as updateClaudeJson,W as updateVscodeMcpJson,A as writeClaudeSettings};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import*as
|
|
2
|
-
`)){const s=c.trim();s&&(o[s]=(o[s]||0)+1)}return Object.entries(o).sort((c,s)=>s[1]-c[1]).slice(0,5).map(([c,s])=>({file:c,edits:s}))}catch{return[]}}function
|
|
3
|
-
`).filter(Boolean):[]}catch{return[]}}function
|
|
1
|
+
import*as A from"node:fs";import*as m from"node:path";import"node:os";import{execSync as T}from"node:child_process";import{bold as z,cyan as O,gray as rt,green as ct,yellow as K,red as ut}from"../ui/output.mjs";import{ampPaths as ht,readEntries as dt,appendEntry as gt}from"../amp/io.mjs";const _="inferno";function M(){return ht(process.cwd())}const mt=m.join(_,"HANDOFF.md"),Ot=m.join(_,"sessions.jsonl"),X=m.join(_,"context-state.json"),q=m.join(_,"contract.json"),Q=m.join(_,"theme.json"),Y=m.join(_,"adoption_profile.json");function u(t){try{return JSON.parse(A.readFileSync(t,"utf8"))}catch{return null}}function lt(t){try{return A.readFileSync(t,"utf8")}catch{return null}}function Z(t){return t?new Date(t).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}):"unknown"}function tt(t){if(t<0)return"unknown";const i=Math.floor(t/36e5),a=Math.floor(t%36e5/6e4);return i>0?`${i}h ${a}m`:`${a}m`}function et(){return dt(process.cwd())}function ot(t,i,a){if(a)return new Date(0);if(i){const r=i.match(/^(\d+)h$/i),h=i.match(/^(\d+)d$/i);if(r)return new Date(Date.now()-parseInt(r[1])*36e5);if(h)return new Date(Date.now()-parseInt(h[1])*864e5);const F=new Date(i);if(!isNaN(F.getTime()))return F}const o=new Date(Date.now()-864e5),c=[];for(const r of t)if(r.type==="handoff"){const h=new Date(r.ts||0);isNaN(h.getTime())||c.push(h)}if(c.length===0)return o;const s=300*1e3,y=c[c.length-1];if(Date.now()-y.getTime()<s){if(c.length>=2){const r=c[c.length-2];return r>o?r:o}return o}return y>o?y:o}function yt(t){try{const i=process.platform;if(i==="win32")T("clip",{input:t});else if(i==="darwin")T("pbcopy",{input:t});else try{T("xclip -selection clipboard",{input:t})}catch{T("xsel --clipboard --input",{input:t})}return!0}catch{return!1}}function nt(){if(process.env.CURSOR_SESSION)return"Cursor";if(process.env.COPILOT_SESSION)return"GitHub Copilot";if(process.env.CLAUDE_CODE_SESSION)return"Claude Code";if(process.env.WINDSURF_SESSION)return"Windsurf";if(process.env.TERM_PROGRAM==="vscode")return"VS Code";const t=u(Y);return t?.ide?t.ide:null}function wt(){try{const t=T("git diff --stat HEAD 2>/dev/null || git diff --cached --stat 2>/dev/null",{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();return t||T("git log --stat -1 --pretty= 2>/dev/null",{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim()||null}catch{return null}}function at(t){try{const i=t&&t.getTime()>0?`--after="${t.toISOString()}"`:"-10",a=T(`git log ${i} --name-only --pretty=format: 2>/dev/null`,{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();if(!a)return[];const o={};for(const c of a.split(`
|
|
2
|
+
`)){const s=c.trim();s&&(o[s]=(o[s]||0)+1)}return Object.entries(o).sort((c,s)=>s[1]-c[1]).slice(0,5).map(([c,s])=>({file:c,edits:s}))}catch{return[]}}function st(t){try{const i=t?`--after="${t.toISOString()}"`:"-5",a=T(`git log ${i} --pretty=format:"%h %s" 2>/dev/null`,{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();return a?a.split(`
|
|
3
|
+
`).filter(Boolean):[]}catch{return[]}}function ft(t){const i=[],a=t.filter(o=>o.type==="attempt"&&(o.result==="failed"||o.result==="partial"||!o.result));for(const o of a)t.find(s=>s.type==="attempt"&&s.result==="worked"&&new Date(s.ts)>new Date(o.ts)&&s.summary.toLowerCase().includes(o.summary.split(" ")[0].toLowerCase()))||i.push({text:o.summary,ts:o.ts,kind:"unresolved-attempt"});for(const o of t)/\b(TODO|WIP|FIXME|BLOCKED|pending)\b/i.test(o.summary)&&(i.find(c=>c.text===o.summary)||i.push({text:o.summary,ts:o.ts,kind:"flagged"}));return i.slice(0,8)}function St(t,i,a){const o=u(X)||{},c=u(q)||{},s=u(Q),y=u(Y),S=et(),r=ot(S,i,a),h=S.filter(n=>new Date(n.ts||0)>r),F=S.slice(-5),E=new Date,d=E.toLocaleString("en-GB",{day:"2-digit",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit"}),b=c.policyId||m.basename(process.cwd()),N=c.policyVersion||"?",j=(c.capabilities||[]).slice(0,20),x=nt(),R=r.getTime()>0?E.getTime()-r.getTime():-1,v=tt(R),H=r.getTime()>0?r.getTime().toString(16).slice(-6).toUpperCase():"ALL",k=st(r.getTime()>0?r:null),L=wt(),G=at(r.getTime()>0?r:null),$=h.length>0?h:F,D=$.filter(n=>n.type==="gotcha"),l=$.filter(n=>n.type==="decision"),g=$.filter(n=>n.type==="attempt").filter(n=>n.result==="failed"||n.result==="partial"),W=$.filter(n=>n.type==="preference"),it=$.slice(-8),B=ft($),w=r.getTime()===0?"all time":r.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}),I=["sessions.jsonl"];(o.working||o.intent)&&I.push("context-state.json"),s&&I.push("theme.json"),c.capabilities?.length&&I.push("contract.json"),y&&I.push("adoption_profile.json"),k.length&&I.push("git log");const P=D.length,U=l.length,V=g.length;let C=Math.min(P*20,40)+Math.min(U*15,30)+Math.min(V*15,20);C=Math.min(C,100);const pt=C>=80?"A":C>=60?"B":C>=40?"C":C>=20?"D":"F",e=[`# \u{1F525} infernoflow Handoff \u2014 ${b}`,`> Generated: ${d}${t?` | Handing off to: **${t}**`:""}`,`> Session: **#${H}** \xB7 ${v} \xB7 **${h.length} entries** \xB7 Health: **${pt}** (${C}/100)`,`> Sources: ${I.join(" \xB7 ")}${x?` \xB7 IDE: ${x}`:""}`,"","---",""];if((o.working||o.intent)&&(e.push("## \u{1F3AF} Working on",""),o.working&&e.push(`**${o.working}** _(${Z(o.workingUpdated)})_`),o.intent&&e.push(`Intent: ${o.intent} _(${Z(o.intentUpdated)})_`),e.push("")),D.length&&(e.push(`## \u26A0\uFE0F STOP \u2014 Read These Before Doing Anything (${D.length} gotcha${D.length===1?"":"s"})`,""),D.forEach((n,p)=>{e.push(`${p+1}. **${n.summary}**`);const f=n.file||n.source;if(f&&/[\\/.]/.test(f)){const J=n.line?`${f}:${n.line}`:f;e.push(` \u2192 File: \`${J}\``)}}),e.push("")),l.length&&(e.push("## \u2713 Decisions In Effect \u2014 Follow These",""),l.forEach((n,p)=>{const f=n.result?` \u2192 **${n.result}**`:"";e.push(`${p+1}. ${n.summary}${f}`)}),e.push("")),g.length&&(e.push("## \u274C Already Tried \u2014 Don't Repeat",""),g.forEach((n,p)=>{const f=n.file||n.source,J=f&&/[\\/.]/.test(f)?` (\`${f}\`)`:"";e.push(`${p+1}. ${n.summary}${J} _(${Z(n.ts)})_`)}),e.push("")),G.length){e.push("## \u{1F4C1} Hot Files This Session","");for(const{file:n,edits:p}of G)e.push(`- \`${n}\` \u2014 ${p} edit${p!==1?"s":""}`);e.push("")}if(W.length){e.push("## Developer preferences","");for(const n of W)e.push(`- ${n.summary}`);e.push("")}if(k.length||L){if(e.push("## Git activity this session",""),k.length){e.push("**Commits:**");for(const n of k)e.push(`- \`${n}\``);e.push("")}L&&(e.push("**Uncommitted changes:**"),e.push("```"),e.push(L.split(`
|
|
4
4
|
`).slice(0,15).join(`
|
|
5
|
-
`)),e.push("```"),e.push(""))}if(s){if(e.push("## Design system",""),s.fonts?.primary&&e.push(`- **Font:** ${s.fonts.primary}${s.fonts.mono?` \xB7 mono: ${s.fonts.mono}`:""}`),s.colors?.mode&&e.push(`- **Mode:** ${s.colors.mode}`),s.colors?.palette){const n=Object.entries(s.colors.palette).map(([p,f])=>`${p}=${f}`).join(" ");e.push(`- **Palette:** ${n}`)}if(s.cssVars&&Object.keys(s.cssVars).length){const n=Object.entries(s.cssVars).slice(0,6).map(([p,f])=>`${p}: ${f}`).join(" | ");e.push(`- **CSS vars:** ${n}`)}s.framework&&e.push(`- **Framework:** ${s.framework}`),e.push("","> \u26A0 Always match these exactly. Do not introduce new colors or fonts.","")}return
|
|
6
|
-
`)}async function Ft(t){const i=
|
|
7
|
-
`+
|
|
8
|
-
`);const
|
|
9
|
-
`)),process.exit(1)),o){const
|
|
10
|
-
`));return}console.log(
|
|
11
|
-
`));const
|
|
5
|
+
`)),e.push("```"),e.push(""))}if(s){if(e.push("## Design system",""),s.fonts?.primary&&e.push(`- **Font:** ${s.fonts.primary}${s.fonts.mono?` \xB7 mono: ${s.fonts.mono}`:""}`),s.colors?.mode&&e.push(`- **Mode:** ${s.colors.mode}`),s.colors?.palette){const n=Object.entries(s.colors.palette).map(([p,f])=>`${p}=${f}`).join(" ");e.push(`- **Palette:** ${n}`)}if(s.cssVars&&Object.keys(s.cssVars).length){const n=Object.entries(s.cssVars).slice(0,6).map(([p,f])=>`${p}: ${f}`).join(" | ");e.push(`- **CSS vars:** ${n}`)}s.framework&&e.push(`- **Framework:** ${s.framework}`),e.push("","> \u26A0 Always match these exactly. Do not introduce new colors or fonts.","")}return j.length&&(e.push("## Capability contract",""),e.push(`Project: **${b}** v${N}`),e.push(`Capabilities: ${j.join(", ")}`),e.push("")),e.push("---"),e.push(`_Session #${H} \xB7 ${v} \xB7 Generated by infernoflow._`),e.join(`
|
|
6
|
+
`)}async function Ft(t){const i=l=>t.includes(l),a=l=>{const g=t.indexOf(l);return g!==-1&&t[g+1]?t[g+1]:null},o=i("--show")||i("-s"),c=i("--copy")||i("-c"),s=i("--json"),y=i("--all"),S=a("--since"),r=a("--to")||t.find(l=>!l.startsWith("-")&&!["switch"].includes(l))||null;console.log(`
|
|
7
|
+
`+z("\u{1F525} infernoflow \u2014 switch")),console.log(" "+"\u2500".repeat(50)+`
|
|
8
|
+
`);const h=m.join(process.cwd(),".ai-memory");if(!A.existsSync(_)&&!A.existsSync(h)&&(console.error(ut(` \u2718 not initialized \u2014 run: infernoflow init
|
|
9
|
+
`)),process.exit(1)),o){const l=lt(M().handoff)||lt(mt);if(!l){console.log(K(` \u26A0 No handoff yet \u2014 run: infernoflow switch
|
|
10
|
+
`));return}console.log(l);return}const F=St(r,S,y);if(s){const l=u(X)||{},g=u(q)||{},W=u(Q),it=u(Y),B=et(),w=ot(B,S,y),I=B.filter(V=>new Date(V.ts||0)>w),P=st(w.getTime()>0?w:null),U=nt();console.log(JSON.stringify({state:l,contract:{policyId:g.policyId,policyVersion:g.policyVersion,capabilities:g.capabilities},theme:W,adoption:it,sessions:I,commits:P,ide:U,sessionStart:w.toISOString(),sessionId:w.getTime()>0?w.getTime().toString(16).slice(-6).toUpperCase():"ALL",sessionDuration:tt(w.getTime()>0?Date.now()-w.getTime():-1),generatedAt:new Date().toISOString()},null,2));return}A.writeFileSync(M().handoff,F,"utf8"),console.log(ct(" \u2714 Written \u2192 "+m.relative(process.cwd(),M().handoff)+`
|
|
11
|
+
`));const E=et(),d=ot(E,S,y),b=E.filter(l=>new Date(l.ts||0)>d),N=u(X)||{},j=u(Q),x=u(q)||{},R=st(d.getTime()>0?d:null),v=at(d.getTime()>0?d:null),H=nt(),k=b.length>0?b:E.slice(-5),L=ft(k),G=tt(d.getTime()>0?Date.now()-d.getTime():-1),$=d.getTime()>0?d.getTime().toString(16).slice(-6).toUpperCase():"ALL";console.log(" "+z("Handoff ready")),console.log(" "+"\u2500".repeat(50)),console.log(" "+rt("Session #"+$+" \xB7 "+G)),N.working&&console.log(" Working on "+O(N.working)),N.intent&&console.log(" Intent "+O(N.intent)),console.log(" Memory "+b.length+" entries this session (total: "+E.length+")"),L.length&&console.log(" Open threads "+K(L.length+" unresolved")),R.length&&console.log(" Git commits "+R.length+" this session"),v.length&&console.log(" Hot files "+v.map(l=>O(l.file)).join(", ")),console.log(" Capabilities "+(x.capabilities||[]).length+" registered"),j?.fonts?.primary&&console.log(" Font "+j.fonts.primary),j?.colors?.mode&&console.log(" Color mode "+j.colors.mode),H&&console.log(" IDE "+H),r&&console.log(" Handing off \u2192 "+O(r)),console.log();const D=m.relative(process.cwd(),M().handoff)||".ai-memory/handoff.md";if(c){const l=yt(F);console.log(l?ct(" \u2714 Copied to clipboard \u2014 paste at the start of your next AI session"):K(" \u26A0 Clipboard failed \u2014 open "+D+" manually"))}else console.log(" "+z("Ready to use:")),console.log(" "+O("1.")+" Open "+O(D)),console.log(" "+O("2.")+" Copy all"),console.log(" "+O("3.")+" Paste at the start of your next AI session"),console.log(" "+rt(" tip: use --copy to skip steps 1-2 automatically"));if(console.log(),A.existsSync(M().sessions)){const l={ts:new Date().toISOString(),agent:"infernoflow",type:"handoff",summary:r?`Handed off to ${r}`:"Handoff generated"};gt(process.cwd(),l)}}export{Ft as switchCommand};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import*as o from"node:fs";import*as i from"node:path";function u(e){const r=i.join(e,".ai-memory",".mcp-runtime.json");try{const t=o.readFileSync(r,"utf8"),n=JSON.parse(t);return!n||typeof n.version!="string"?null:n}catch{return null}}function l(e,r){return!e||!e.version||e.version===r||r.startsWith("0.0.0")||e.version.startsWith("0.0.0")?null:{severity:"warn",message:`Your AI tool is running an older infernoflow MCP server: MCP=${e.version} (booted ${e.bootedAt}) vs CLI=${r}. Quit and reopen Cursor / Claude Code / VS Code to pick up the new code. Until you do, amp_write calls will use the cached old wrapper.`}}function s(e,r){return l(u(e),r)}export{l as compareMcpRuntime,s as detectStaleMcpRuntime,u as readMcpRuntimeStamp};
|
|
@@ -18,8 +18,10 @@ const require = createRequire(import.meta.url);
|
|
|
18
18
|
* in node_modules of the CWD or one of its parents.
|
|
19
19
|
* Returns null if neither finds infernoflow.
|
|
20
20
|
*/
|
|
21
|
-
function
|
|
22
|
-
let dir
|
|
21
|
+
function walkUpForInfernoflow(startFile) {
|
|
22
|
+
let dir;
|
|
23
|
+
try { dir = path.dirname(fs.realpathSync(startFile)); }
|
|
24
|
+
catch { dir = path.dirname(startFile); }
|
|
23
25
|
while (true) {
|
|
24
26
|
const pj = path.join(dir, "package.json");
|
|
25
27
|
if (fs.existsSync(pj)) {
|
|
@@ -32,9 +34,46 @@ function findInfernoflowRoot() {
|
|
|
32
34
|
if (parent === dir) break;
|
|
33
35
|
dir = parent;
|
|
34
36
|
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findInfernoflowRoot() {
|
|
41
|
+
// 1. Walk up from this template's own location.
|
|
42
|
+
// Works when the template runs from inside infernoflow-pkg/ or from a
|
|
43
|
+
// project's .cursor/ copy that has node_modules/infernoflow/ in scope.
|
|
44
|
+
const fromHere = walkUpForInfernoflow(fileURLToPath(import.meta.url));
|
|
45
|
+
if (fromHere) return fromHere;
|
|
46
|
+
|
|
47
|
+
// 2. require.resolve β works when infernoflow is in CWD's node_modules.
|
|
35
48
|
try {
|
|
36
49
|
return path.dirname(require.resolve("infernoflow/package.json"));
|
|
37
50
|
} catch {}
|
|
51
|
+
|
|
52
|
+
// 3. Resolve via the global install on PATH.
|
|
53
|
+
// When the user runs `npm install -g infernoflow` and `init` copies this
|
|
54
|
+
// template into their .cursor/, neither (1) nor (2) can find the package
|
|
55
|
+
// β there's no parent package.json above .cursor/ with name=infernoflow,
|
|
56
|
+
// and the user's project doesn't depend on infernoflow locally. Without
|
|
57
|
+
// this branch the MCP server boots with v0.0.0-unknown.
|
|
58
|
+
try {
|
|
59
|
+
const lookup = process.platform === "win32" ? "where infernoflow" : "which infernoflow";
|
|
60
|
+
const out = execSync(lookup, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
61
|
+
for (const candidate of out.split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
|
|
62
|
+
if (!fs.existsSync(candidate)) continue;
|
|
63
|
+
const binDir = path.dirname(candidate);
|
|
64
|
+
// Windows layout: <npm-prefix>/infernoflow.cmd + <npm-prefix>/node_modules/infernoflow/
|
|
65
|
+
// Unix layout: <npm-prefix>/bin/infernoflow + <npm-prefix>/lib/node_modules/infernoflow/
|
|
66
|
+
for (const layout of [
|
|
67
|
+
path.join(binDir, "node_modules", "infernoflow"),
|
|
68
|
+
path.join(binDir, "..", "lib", "node_modules", "infernoflow"),
|
|
69
|
+
]) {
|
|
70
|
+
if (fs.existsSync(path.join(layout, "package.json"))) {
|
|
71
|
+
try { return fs.realpathSync(layout); } catch { return layout; }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch {}
|
|
76
|
+
|
|
38
77
|
return null;
|
|
39
78
|
}
|
|
40
79
|
|
|
@@ -102,6 +141,36 @@ if (refreshRuleFiles) {
|
|
|
102
141
|
try { refreshRuleFiles(process.cwd()); } catch { /* non-fatal */ }
|
|
103
142
|
}
|
|
104
143
|
|
|
144
|
+
// ββ Boot stamp: record which MCP version is running ββββββββββββββββββββββ
|
|
145
|
+
// IDE-loaded MCP servers stay in memory until session restart. After
|
|
146
|
+
// `npm install -g infernoflow@<new>` the on-disk wrapper updates but the
|
|
147
|
+
// running process is still the old code β silent version skew that
|
|
148
|
+
// shipped 0.43β0.44 bugs (fileβsource field misroute, etc.). We write a
|
|
149
|
+
// boot stamp every time the server starts so `infernoflow setup` and
|
|
150
|
+
// `infernoflow doctor` can compare against the installed CLI version and
|
|
151
|
+
// tell the user when to restart their AI tool.
|
|
152
|
+
try {
|
|
153
|
+
const root = (() => { try { return findInfernoflowRoot(); } catch { return null; } })();
|
|
154
|
+
let runtimeVersion = "0.0.0-unknown";
|
|
155
|
+
if (root) {
|
|
156
|
+
try { runtimeVersion = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version || runtimeVersion; }
|
|
157
|
+
catch {}
|
|
158
|
+
}
|
|
159
|
+
const memDir = path.join(process.cwd(), ".ai-memory");
|
|
160
|
+
if (fs.existsSync(memDir)) {
|
|
161
|
+
fs.writeFileSync(path.join(memDir, ".mcp-runtime.json"), JSON.stringify({
|
|
162
|
+
version: runtimeVersion,
|
|
163
|
+
pid: process.pid,
|
|
164
|
+
bootedAt: new Date().toISOString(),
|
|
165
|
+
source: "inferno-mcp-server.mjs",
|
|
166
|
+
}, null, 2) + "\n", "utf8");
|
|
167
|
+
}
|
|
168
|
+
// Also surface the version on stderr so users can see it in their IDE's
|
|
169
|
+
// MCP-server log panel β that's the easiest way to verify "the new code
|
|
170
|
+
// is running" without running another command.
|
|
171
|
+
process.stderr.write(`[infernoflow MCP] active β v${runtimeVersion}, pid ${process.pid}\n`);
|
|
172
|
+
} catch { /* boot stamp is best-effort; never block the server */ }
|
|
173
|
+
|
|
105
174
|
/**
|
|
106
175
|
* Run the infernoflow CLI. Returns either the stdout string OR a structured
|
|
107
176
|
* error object so call sites can decide whether to surface it via JSON-RPC
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infernoflow",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.2",
|
|
4
4
|
"description": "Persistent memory for AI coding sessions β captures what agents can't infer from code alone. Works with Copilot, Cursor, Claude, and Windsurf.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|