infernoflow 0.45.0 → 0.46.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 +32 -10
- package/dist/bin/infernoflow.mjs +30 -26
- package/dist/lib/amp/io.mjs +28 -19
- package/dist/lib/claudeAssets.mjs +3 -1
- package/dist/lib/cleanTree.mjs +6 -6
- package/dist/lib/commands/ask.mjs +3 -3
- package/dist/lib/commands/curate.mjs +10 -0
- package/dist/lib/commands/doctor.mjs +3 -3
- package/dist/lib/commands/hook.mjs +12 -0
- package/dist/lib/commands/log.mjs +14 -14
- package/dist/lib/commands/move.mjs +14 -0
- package/dist/lib/commands/recap.mjs +5 -5
- package/dist/lib/commands/resolve.mjs +5 -0
- package/dist/lib/commands/resume.mjs +3 -0
- package/dist/lib/commands/setup.mjs +84 -18
- package/dist/lib/commands/sync.mjs +27 -27
- package/dist/lib/commands/transcript.mjs +2 -0
- package/dist/lib/commands/uninstall.mjs +12 -12
- package/dist/lib/memoryView.mjs +7 -0
- package/dist/lib/personalConfig.mjs +2 -0
- package/dist/lib/ruleFiles.mjs +11 -11
- package/dist/lib/schema.mjs +1 -0
- package/dist/lib/securityRefresh.mjs +1 -1
- package/dist/templates/agents/memory-keeper.md +49 -54
- package/dist/templates/cursor/hooks/inferno-session-draft.mjs +10 -5
- package/dist/templates/cursor/inferno-mcp-server.mjs +70 -11
- package/dist/templates/hooks/infernoflow-agent-guard.mjs +32 -0
- package/dist/templates/skills/infernoflow-memory/SKILL.md +68 -86
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -16,7 +16,17 @@ infernoflow is a local-first CLI + VS Code extension + open protocol (AMP) that
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
## What's new
|
|
19
|
+
## What's new in 0.46
|
|
20
|
+
|
|
21
|
+
- **Fresh memory every Claude Code session** — a SessionStart hook injects current memory (framed as information to verify, not instructions); `CLAUDE.md` no longer carries a copy that goes stale.
|
|
22
|
+
- **`infernoflow resume`** — "where were we?" in one call. Claude Code also leaves an automatic local resume point when a session ends.
|
|
23
|
+
- **Stale and resolved entries** — entries about a file that changed since are marked *may be stale*; `infernoflow resolve <id>` retires one from AI context.
|
|
24
|
+
- **Review what arrives through git** (0.46.1) — memory added by teammates is listed until you review it, and the GitHub Action shows memory changes in each PR.
|
|
25
|
+
- **Safer by default** — only read-only MCP tools are pre-approved; the memory-keeper agent is limited to single `infernoflow` commands; secrets are redacted; MCP is registered per project.
|
|
26
|
+
|
|
27
|
+
Full list: [CHANGELOG.md](./CHANGELOG.md).
|
|
28
|
+
|
|
29
|
+
## Session bookmarks
|
|
20
30
|
|
|
21
31
|
Drop a **named resume point** mid-session. When the context window fills up or you're about to make a risky change, one command captures the current session's transcript and stores it as a jumpable checkpoint:
|
|
22
32
|
|
|
@@ -41,8 +51,8 @@ Bookmarks are **never auto-pruned**, and they surface in `infernoflow switch` un
|
|
|
41
51
|
|
|
42
52
|
`init` now installs two things into `.claude/` so the loop runs itself on Claude Code:
|
|
43
53
|
|
|
44
|
-
- **`infernoflow-memory` skill** (`.claude/skills/`) — teaches the agent to start warm (`
|
|
45
|
-
- **`memory-keeper` subagent** (`.claude/agents/`) — a specialist you can delegate to. It sweeps the session, dedupes against what's already stored, logs what's worth keeping, drops bookmarks, and reports back. It never edits code or logs secrets.
|
|
54
|
+
- **`infernoflow-memory` skill** (`.claude/skills/`) — teaches the agent to start warm (`resume`), then proactively `log` real gotchas, decisions-with-a-*because*, dead ends, and durable preferences, and drop a `bookmark` at stopping points or when you say *"bookmark this"*. Capture is **balanced** — it skips routine steps, anything obvious from the code, secrets, and duplicates.
|
|
55
|
+
- **`memory-keeper` subagent** (`.claude/agents/`) — a specialist you can delegate to. It sweeps the session, dedupes against what's already stored, logs what's worth keeping, drops bookmarks, and reports back. It never edits code or logs secrets, and a guard limits its shell to single `infernoflow` read/log commands (blocked if the guard can't run).
|
|
46
56
|
|
|
47
57
|
Both are installed automatically by `infernoflow init` (fresh **and** on re-run of an existing project) and ship in the package — nothing to set up by hand.
|
|
48
58
|
|
|
@@ -85,14 +95,20 @@ These cover 95% of usage:
|
|
|
85
95
|
| Command | What it does |
|
|
86
96
|
|---|---|
|
|
87
97
|
| `infernoflow log "..."` | Remember a gotcha / decision / attempt / note. `--type gotcha\|decision\|attempt\|preference` |
|
|
88
|
-
| `infernoflow
|
|
98
|
+
| `infernoflow resume` | **🆕 0.46** "Where were we?" in one call — last resume point, open dead ends, recent decisions, uncommitted work. `--file <path>` ranks by file |
|
|
99
|
+
| `infernoflow ask "..."` | Search your memory by keyword — gotchas surface first. `--file <path>` ranks entries about that file first |
|
|
89
100
|
| `infernoflow switch` | Generate a handoff for the next session. `--copy` puts it on your clipboard |
|
|
90
|
-
| `infernoflow recap` | End-of-session summary with health score + unlogged-change detection |
|
|
101
|
+
| `infernoflow recap` | End-of-session summary with health score + unlogged-change detection; lists memory that arrived through git and marks it reviewed |
|
|
91
102
|
| `infernoflow status` | Quick health check — entries, gotchas, decisions, last activity |
|
|
92
103
|
| `infernoflow bookmark "..."` | **🆕** Drop a named resume point — auto-captures the session transcript as its context. `list` / `show <id\|label>` / `rm` round it out. Surfaces in `switch`. Never auto-pruned. |
|
|
93
104
|
| `infernoflow refresh` | Manually rebuild `CLAUDE.md` / `.cursorrules` / `copilot-instructions.md` from memory |
|
|
94
105
|
| `infernoflow forget <id\|prefix>` | Delete a memory entry without hand-editing JSONL. `--last` for the newest |
|
|
95
106
|
| `infernoflow prune` | Archive stale `note` / `attempt` entries older than 30 days. Gotchas/decisions/bookmarks never auto-pruned. Default dry-run; `--apply` to act |
|
|
107
|
+
| `infernoflow resolve <id>` | **🆕 0.46** Mark an entry fixed/outdated — stays searchable, no longer injected. Entries whose file changed since they were written show **"may be stale"** |
|
|
108
|
+
| `infernoflow curate` | **🆕 0.46** Remove noise: old commit notes, duplicates, raw frustration prompts. Dry-run; `--apply` |
|
|
109
|
+
| `infernoflow move <id…> --to <dir>` | **🆕 0.46** Move misfiled entries to another project's memory. Dry-run; `--apply` |
|
|
110
|
+
|
|
111
|
+
Any command takes `--project <dir>` to work on another project's memory (multi-folder workspaces, hooks, agents).
|
|
96
112
|
|
|
97
113
|
In practice you barely run any of these — the MCP-aware AI does it for you. The CLI is for grep-style introspection.
|
|
98
114
|
|
|
@@ -156,13 +172,15 @@ infernoflow prune --apply --max-age-days 14 # one-off cleanup
|
|
|
156
172
|
├── branches/
|
|
157
173
|
│ ├── main.jsonl ← project-wide truths (git-tracked)
|
|
158
174
|
│ └── feature-auth.jsonl ← your current branch's work (git-tracked)
|
|
159
|
-
├── details
|
|
160
|
-
|
|
175
|
+
├── details.jsonl ← long-form entry bodies (git-tracked, loaded on demand)
|
|
176
|
+
├── details.local.jsonl ← bookmark transcript snapshots (gitignored, this machine only)
|
|
161
177
|
├── global.jsonl ← your personal preferences (gitignored)
|
|
162
|
-
└── sessions.jsonl ←
|
|
178
|
+
└── sessions.jsonl ← everything this machine wrote (gitignored mirror)
|
|
163
179
|
```
|
|
164
180
|
|
|
165
181
|
- **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.
|
|
182
|
+
- **You see what arrives.** Entries that reach your machine through git and that you haven't reviewed are listed by `infernoflow resume` (and in Claude Code's session-start context); `infernoflow recap` shows them and marks them reviewed. Tracked locally per machine, from the entry's content — not from the author it claims. Retire a wrong one with `infernoflow resolve <id>`.
|
|
183
|
+
- **Review memory in pull requests.** The GitHub Action ([`action/`](./action)) comments on each PR with the memory entries it adds, edits or deletes, so they are reviewed like code before they reach everyone's AI.
|
|
166
184
|
- **Personal preferences travel between your own machines.** Point at any OS-synced folder once:
|
|
167
185
|
```
|
|
168
186
|
infernoflow sync set ~/Dropbox/infernoflow-memory
|
|
@@ -193,8 +211,9 @@ When the MCP server is wired, your AI agent can call these directly in chat:
|
|
|
193
211
|
|
|
194
212
|
| Tool | What it does |
|
|
195
213
|
|---|---|
|
|
196
|
-
| `amp_write` | Log an entry (`type`, `msg`, optional `file` / `line` / `tags` / `detail`) |
|
|
197
|
-
| `
|
|
214
|
+
| `amp_write` | Log an entry (`type`, `msg`, optional `file` / `line` / `tags` / `detail`). In a multi-folder workspace an entry about another open folder's file goes to that folder's memory |
|
|
215
|
+
| `amp_resume` | **🆕 0.46** "Where were we?" — last resume point, open dead ends, recent decisions, uncommitted changes |
|
|
216
|
+
| `amp_read` | Read entries with optional filters (`type`, `query`, `file` — entries about that file rank first) |
|
|
198
217
|
| `amp_search` | Keyword search across entries |
|
|
199
218
|
| `amp_bookmark` | **🆕** Drop a named resume point — auto-captures the current session transcript when no `note` is given |
|
|
200
219
|
| `amp_handoff` | Generate the handoff document for the next AI session |
|
|
@@ -278,6 +297,9 @@ Local-first by design:
|
|
|
278
297
|
- ✅ **Bookmark transcript snapshots stay on your machine** (`.ai-memory/details.local.jsonl`, gitignored). Only context you write explicitly (`--note`, `detail`) is shared with the team.
|
|
279
298
|
- ✅ **API keys never live in the project** — environment variables are used as-is; pasted keys go to `~/.infernoflow/ai-credentials.json` (owner-only).
|
|
280
299
|
- ✅ **No shell is used to run commands** from the MCP server or the prompt hooks (since 0.44.20), and MCP tool arguments are validated against their schema.
|
|
300
|
+
- ✅ **Only read-only tools are pre-approved** in Claude Code (`permissions.allow` in `.claude/settings.json`). Tools that write memory ask for your permission like any other tool, so text the AI reads can't silently write to shared memory.
|
|
301
|
+
- ✅ **Memory is data, not instructions.** Injected memory is framed that way, each entry records its author, entries about a changed file are marked *may be stale*, and memory that arrived through git is listed until you review it (`resume` / `recap`).
|
|
302
|
+
- ✅ **The frustration hook keeps only a 60-character prefix** of the prompt that triggered it.
|
|
281
303
|
|
|
282
304
|
The optional `infernoflow ai setup` command wires an AI provider (Anthropic / OpenAI / Google / Ollama) for a few enrichment commands — same trust model as using that provider directly. Off by default.
|
|
283
305
|
|
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -1,40 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
(function(){if(process.platform!=="win32"||process.argv[2]==="mcp"||process.env.WT_SESSION||process.env.ConEmuPID||process.env.TERM_PROGRAM==="vscode")return;const
|
|
3
|
-
`)}const
|
|
4
|
-
${
|
|
2
|
+
(function(){if(process.platform!=="win32"||process.argv[2]==="mcp"||process.env.WT_SESSION||process.env.ConEmuPID||process.env.TERM_PROGRAM==="vscode")return;const t={"\u2500":"-","\u2501":"-","\u2550":"=","\u2502":"|","\u2503":"|","\u2551":"|","\u250C":"+","\u2510":"+","\u2514":"+","\u2518":"+","\u251C":"+","\u2524":"+","\u252C":"+","\u2534":"+","\u253C":"+","\xB7":"*","\u2192":"->","\u2190":"<-","\u2714":"[OK]","\u2713":"[OK]","\u2718":"[X]","\u2717":"[X]","\u26A0":"[!]",\u2139:"[i]"},r=new RegExp(Object.keys(t).join("|"),"g"),f=/[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{200D}]\u{FE0F}? ?/gu,w=c=>c.replace(r,y=>t[y]).replace(f,"");function k(c){const y=c.write.bind(c);c.write=function(a,...b){return typeof a=="string"?a=w(a):Buffer.isBuffer(a)&&(a=Buffer.from(w(a.toString("utf8")),"utf8")),y(a,...b)}}k(process.stdout),k(process.stderr)})();import{readFileSync as x}from"node:fs";import{dirname as A,join as C}from"node:path";import{fileURLToPath as I}from"node:url";import{bold as i,gray as e,cyan as s,red as p}from"../lib/ui/output.mjs";const M=A(I(import.meta.url));function S(o){for(const t of[C(o,"..","..","package.json"),C(o,"..","package.json")])try{return JSON.parse(x(t,"utf8"))}catch{}return{version:"0.0.0-source"}}const j=S(M),l=j.version||"0.0.0",v={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",forget:"Delete a memory entry by id or unique prefix (--last for the newest)",prune:"Archive stale notes/attempts (gotchas/decisions never touched) \u2014 see --apply",bookmark:"Drop / list / recall named session resume points (bookmarks with context)",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)",mcp:"Run the MCP server (used by AI tool configs \u2014 not run by hand)",resume:"Where were we? Latest resume point, open dead ends, recent decisions, uncommitted work",resolve:"Mark an entry resolved (kept searchable, no longer injected) \u2014 resolve <id> [--note]",move:"Move misfiled entries to another project \u2014 move <id\u2026>|--query q --to <dir> [--apply]",curate:"Remove noise: commit notes, duplicates, old raw frustration prompts [--apply]",transcript:"Print this project's current Claude Code session transcript (plain text)",hook:"Entry point for AI-tool hooks (session-start / session-end) \u2014 not run by hand"},h={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),forget:async o=>(await import("../lib/commands/forget.mjs")).forgetCommand(o),prune:async o=>(await import("../lib/commands/prune.mjs")).pruneCommand(o),bookmark:async o=>(await import("../lib/commands/bookmark.mjs")).bookmarkCommand(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),mcp:async o=>(await import("../lib/commands/mcp.mjs")).mcpCommand(o),hook:async o=>(await import("../lib/commands/hook.mjs")).hookCommand(o),resume:async o=>(await import("../lib/commands/resume.mjs")).resumeCommand(o),resolve:async o=>(await import("../lib/commands/resolve.mjs")).resolveCommand(o),move:async o=>(await import("../lib/commands/move.mjs")).moveCommand(o),curate:async o=>(await import("../lib/commands/curate.mjs")).curateCommand(o),transcript:async o=>(await import("../lib/commands/transcript.mjs")).transcriptCommand(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 B(){const o=Object.keys(v),t=Math.max(...o.map(r=>r.length),8)+1;return Object.entries(v).map(([r,f])=>` ${r.padEnd(t," ")}${f}`).join(`
|
|
3
|
+
`)}const O={Memory:["log","ask","resume","switch","recap","status","bookmark","resolve","refresh","forget","prune","curate","move","transcript"],Setup:["init","setup","doctor","context","mcp","hook"],"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 R(){return Object.entries(O).map(([o,t])=>` ${i(o+":")}
|
|
4
|
+
${t.join(" ")}`).join(`
|
|
5
5
|
|
|
6
|
-
`)}const
|
|
7
|
-
${i("\u{1F525} infernoflow")} ${e("v"+
|
|
6
|
+
`)}const $=Object.keys(h).length,E=`
|
|
7
|
+
${i("\u{1F525} infernoflow")} ${e("v"+l)}
|
|
8
8
|
${e("Persistent memory for AI coding sessions")}
|
|
9
9
|
|
|
10
10
|
${i("Usage:")}
|
|
11
11
|
infernoflow [command] [options]
|
|
12
12
|
|
|
13
13
|
${i("Memory")} ${e("\u2014 capture, recall & curate")}
|
|
14
|
-
${
|
|
15
|
-
${
|
|
16
|
-
${
|
|
17
|
-
${
|
|
18
|
-
${
|
|
19
|
-
${
|
|
20
|
-
${
|
|
14
|
+
${s("log")} ${e('"..."')} Add to session memory ${e("(--type gotcha|decision|attempt)")}
|
|
15
|
+
${s("ask")} ${e('"..."')} Search your memory by keyword ${e("(gotchas surface first)")}
|
|
16
|
+
${s("switch")} Generate handoff for next AI agent
|
|
17
|
+
${s("recap")} End-of-session health score + unlogged changes
|
|
18
|
+
${s("status")} Quick health check
|
|
19
|
+
${s("forget")} Delete a bad entry ${e("(--last for the newest, or by id/prefix)")}
|
|
20
|
+
${s("prune")} Archive stale notes/attempts ${e("(dry-run; --apply to commit)")}
|
|
21
21
|
|
|
22
22
|
${i("Setup")}
|
|
23
|
-
${
|
|
24
|
-
${
|
|
25
|
-
${
|
|
26
|
-
${
|
|
23
|
+
${s("init")} 60-second setup ${e("(memory mode by default)")}
|
|
24
|
+
${s("setup")} Re-run IDE wiring ${e("(idempotent \u2014 MCP + hooks)")}
|
|
25
|
+
${s("doctor")} Diagnose your setup
|
|
26
|
+
${s("context")} Generate AI-ready context for new sessions
|
|
27
27
|
|
|
28
28
|
${i("Subsystems")} ${e("\u2014 grouped, run for verbs:")}
|
|
29
|
-
${
|
|
29
|
+
${s("amp")} AI Memory Protocol ${e("(status, migrate, validate)")}
|
|
30
30
|
|
|
31
|
-
${e("Run")} ${
|
|
32
|
-
${e("Run")} ${
|
|
33
|
-
`;import*as
|
|
34
|
-
${i("\u{1F525} infernoflow")} ${e("v"+
|
|
35
|
-
`),console.log(
|
|
36
|
-
${e("Run")} ${
|
|
37
|
-
`),process.exit(0));const D=Object.keys(
|
|
38
|
-
Unknown command: ${
|
|
39
|
-
`)),process.exit(1));const
|
|
31
|
+
${e("Run")} ${s("infernoflow commands")} ${e("to see all "+$+" commands grouped.")}
|
|
32
|
+
${e("Run")} ${s("infernoflow <command> --help")} ${e("for command-specific options.")}
|
|
33
|
+
`;import*as m from"node:fs";import*as d from"node:path";try{const o=d.join(process.cwd(),"inferno");if(m.existsSync(o)){const{observeCommandStart:t}=await import("../lib/learning/observe.mjs"),r=process.argv[2];r&&!r.startsWith("-")&&t(o,r)}}catch{}const[,,n,...u]=process.argv;(!n||n==="--help"||n==="-h")&&(console.log(E),process.exit(0)),(n==="--version"||n==="-v")&&(console.log(l),process.exit(0)),n==="commands"&&(console.log(`
|
|
34
|
+
${i("\u{1F525} infernoflow")} ${e("v"+l)} ${e("\u2014 all "+$+" commands")}
|
|
35
|
+
`),console.log(R()),console.log(`
|
|
36
|
+
${e("Run")} ${s("infernoflow <command> --help")} ${e("for options.")}
|
|
37
|
+
`),process.exit(0));const D=Object.keys(h);D.includes(n)||(console.error(p(`
|
|
38
|
+
Unknown command: ${n}`)),console.error(e("Run: infernoflow commands (see all commands)")),console.error(e(`Run: infernoflow --help (quick start)
|
|
39
|
+
`)),process.exit(1));const g=u.indexOf("--project");if(g!==-1){const o=u[g+1],t=o?d.resolve(o):null;(!t||!m.existsSync(t)||!m.statSync(t).isDirectory())&&(console.error(p(`
|
|
40
|
+
\u2718 --project: not a folder: ${o||"(missing)"}
|
|
41
|
+
`)),process.exit(1)),!["init","setup"].includes(n)&&!m.existsSync(d.join(t,".ai-memory"))&&!m.existsSync(d.join(t,"inferno"))&&(console.error(p(`
|
|
42
|
+
\u2718 --project: ${t} has no .ai-memory/ \u2014 run infernoflow init there
|
|
43
|
+
`)),process.exit(1)),u.splice(g,2),process.chdir(t)}const P=[n,...u];try{const{runUpgradeBackfillIfNeeded:o}=await import("../lib/upgradeCheck.mjs");await o(l,n)}catch{}h[n](P).catch(o=>{console.error(p(`
|
|
40
44
|
Error: `)+o.message),process.exit(1)});
|
package/dist/lib/amp/io.mjs
CHANGED
|
@@ -1,27 +1,36 @@
|
|
|
1
|
-
import*as
|
|
2
|
-
`).filter(Boolean).map(e=>{try{return JSON.parse(e)}catch{return null}}).filter(Boolean).map(
|
|
3
|
-
`,"utf8")}function
|
|
4
|
-
`))if(
|
|
5
|
-
`))if(
|
|
1
|
+
import{execFileSync as K}from"node:child_process";import{readPersonalConfig as q,expandUserPath as X}from"../personalConfig.mjs";import*as Z from"node:crypto";import*as o from"node:fs";import{redactSecrets as Q}from"../security/redact.mjs";import"node:os";import*as a from"node:path";import{findProjectRoot as T}from"../projectRoot.mjs";import{getBranchInfo as tt}from"../git/branch.mjs";const _="1.0",et=new Set(["gotcha","decision","attempt","note","detection","pattern"]),nt=new Set(["copilot","cursor","claude","windsurf","other"]);function rt(t){let e=a.basename(a.resolve(t));try{const n=a.join(t,".ai-memory","amp.json");if(o.existsSync(n)){const r=JSON.parse(o.readFileSync(n,"utf8"));r&&typeof r.project=="string"&&r.project.trim()&&(e=r.project.trim())}}catch{}return e.toLowerCase().replace(/[^a-z0-9_.\-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,64)||"unnamed-project"}function it(t,e){let n=process.env.INFERNOFLOW_GLOBAL_DIR;if(!n){const l=q();typeof l.globalDir=="string"&&l.globalDir.trim()&&(n=l.globalDir.trim())}if(!n)return a.join(e,"global.jsonl");const r=X(n),i=rt(t);return a.join(r,i,"global.jsonl")}function m(t,e={}){const n=e.literal?a.resolve(t):T(t),r=a.join(n,".ai-memory"),i=a.join(n,"inferno"),l=e.forWrite||o.existsSync(r)||!o.existsSync(i),c=l?r:i,s=l,u=a.join(c,"branches"),f=tt(n),d=a.join(u,`${f.currentSlug}.jsonl`),y=f.defaultSlug?a.join(u,`${f.defaultSlug}.jsonl`):null,S=ot(n,c);return{root:c,projectRoot:n,isAmp:s,sessions:a.join(c,"sessions.jsonl"),config:a.join(c,s?"amp.json":"config.json"),handoff:a.join(c,s?"handoff.md":"HANDOFF.md"),globalFile:S,branchesDir:u,currentBranchFile:d,defaultBranchFile:y,branch:f}}function Rt(t){try{const e=m(t),n=e.branch&&e.branch.current;return`store: ${e.root}${n&&!e.branch.isSynthetic?` (branch ${n})`:""}`}catch{return"store: (unresolved)"}}function ot(t,e){try{return it(t,e)}catch{return a.join(e,"global.jsonl")}}function A(t){const e=T(t),n=a.join(e,".ai-memory");return o.existsSync(n)||o.mkdirSync(n,{recursive:!0}),n}const L="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function I(){let t=Date.now(),e="";for(let r=0;r<10;r++)e=L[t%32]+e,t=Math.floor(t/32);let n="";for(let r=0;r<16;r++)n+=L[Math.floor(Math.random()*32)];return e+n}function J(t){const e={...t.meta||{}};let n=t.type||"note";et.has(n)||(e.subtype=n,n="note"),t.result&&(e.result=t.result),t.detailRef&&(e.detailRef=t.detailRef);let r;const i=t.agent;i&&nt.has(i)?r=i:i&&(e.agent=i);const l=typeof t.ts=="number"?t.ts:t.ts?Date.parse(t.ts):Date.now(),c=t.confidence!=null?t.confidence:t.auto?.7:void 0,s={type:n,msg:t.summary||t.msg||"",ts:l,id:t.id||`amp_${I()}`};return t.file&&(s.file=t.file),t.line&&(s.line=t.line),t.function&&(s.function=t.function),t.tags&&t.tags.length&&(s.tags=t.tags),t.source&&(s.source=t.source),r&&(s.tool=r),t.session&&(s.session=t.session),c!=null&&(s.confidence=c),Object.keys(e).length&&(s.meta=e),s}function $(t){if(t.summary&&!t.msg)return t;const e=t.meta||{},n=e.subtype||t.type||"note",r={ts:t.ts,type:n,summary:t.msg||""};t.id&&(r.id=t.id),t.file&&(r.file=t.file),t.line&&(r.line=t.line),t.function&&(r.function=t.function),t.tags&&(r.tags=t.tags),t.source&&(r.source=t.source),t.tool&&(r.agent=t.tool),e.agent&&(r.agent=e.agent),e.result&&(r.result=e.result),e.detailRef&&(r.detailRef=e.detailRef),t.confidence!=null&&(r.confidence=t.confidence,t.confidence<1&&(r.auto=!0));const{subtype:i,agent:l,result:c,detailRef:s,...u}=e;return Object.keys(u).length&&(r.meta=u),r}function st(t){if(!t||!o.existsSync(t))return[];try{return o.readFileSync(t,"utf8").split(`
|
|
2
|
+
`).filter(Boolean).map(e=>{try{return JSON.parse(e)}catch{return null}}).filter(Boolean).map($)}catch{return[]}}function wt(t){const e=m(t),n=new Set,r=[],i=[e.sessions,e.globalFile,e.defaultBranchFile,e.currentBranchFile],l=[...new Set(i.filter(Boolean))];for(const c of l)for(const s of st(c)){const u=s.id||`${s.ts}|${s.summary}`;n.has(u)||(n.add(u),r.push(s))}return r.sort((c,s)=>{const u=typeof c.ts=="number"?c.ts:Date.parse(c.ts||0),f=typeof s.ts=="number"?s.ts:Date.parse(s.ts||0);return u-f})}function ct(t,e){return e.target==="global"?t.globalFile:e.target==="legacy"?t.sessions:e.target==="branch"?t.currentBranchFile:e.type==="preference"?t.globalFile:t.currentBranchFile}const C=new Set;function x(t){return a.join(t,"details.jsonl")}const W="details.local.jsonl";function k(t){return a.join(t,W)}function at(t,e,n,r=!1){const i=r?k(t):x(t);o.mkdirSync(a.dirname(i),{recursive:!0}),o.appendFileSync(i,JSON.stringify({id:e,body:n})+`
|
|
3
|
+
`,"utf8")}function P(t,e,n=!1){if(!e)return null;let r;try{r=o.readFileSync(n?k(t):x(t),"utf8")}catch{return null}let i=null;for(const l of r.split(`
|
|
4
|
+
`))if(l.trim())try{const c=JSON.parse(l);c.id===e&&typeof c.body=="string"&&(i=c.body)}catch{}return i}function M(t,e,n=!1){const r=n?k(t):x(t);let i;try{i=o.readFileSync(r,"utf8")}catch{return!1}const l=[];let c=!1;for(const s of i.split(`
|
|
5
|
+
`))if(s.trim()){try{if(JSON.parse(s).id===e){c=!0;continue}}catch{}l.push(s)}return c&&o.writeFileSync(r,l.length?l.join(`
|
|
6
6
|
`)+`
|
|
7
|
-
`:"","utf8"),c}function
|
|
8
|
-
`))if(
|
|
9
|
-
`,
|
|
10
|
-
`;if(
|
|
11
|
-
`)
|
|
7
|
+
`:"","utf8"),c}function lt(t){const e=a.join(t,"details");if(!o.existsSync(e))return 0;let n;try{n=o.readdirSync(e).filter(c=>c.endsWith(".md"))}catch{return 0}if(!n.length)return 0;const r=new Set;try{const c=o.readFileSync(x(t),"utf8");for(const s of c.split(`
|
|
8
|
+
`))if(s.trim())try{r.add(JSON.parse(s).id)}catch{}}catch{}let i="",l=0;for(const c of n){const s=c.replace(/\.md$/,"");if(r.has(s))continue;let u;try{u=o.readFileSync(a.join(e,c),"utf8")}catch{continue}i+=JSON.stringify({id:s,body:u})+`
|
|
9
|
+
`,l++}if(i)try{o.appendFileSync(x(t),i,"utf8")}catch{return 0}for(const c of n)try{o.unlinkSync(a.join(e,c))}catch{}try{o.rmdirSync(e)}catch{}return l}const O=new Map;function N(t,e){const n=t+"\0"+e.join("\0");if(O.has(n))return O.get(n);let r=null;try{r=K("git",e,{cwd:t,encoding:"utf8",stdio:["ignore","pipe","ignore"],windowsHide:!0,timeout:1e4}).trim()||null}catch{r=null}return O.set(n,r),r}function Et(){O.clear()}function ft(t,e,n){const r=t.projectRoot,i={},l=process.env.INFERNOFLOW_AUTHOR||N(r,["config","user.name"])||(N(r,["config","user.email"])||"").split("@")[0]||null;l&&(i.author=l.slice(0,80)),i.repo=a.basename(r),t.branch&&t.branch.current&&!t.branch.isSynthetic&&(i.branch=t.branch.current);const c=a.relative(r,a.resolve(n));if(c&&!c.startsWith("..")&&!a.isAbsolute(c)&&(i.cwd=c.split(a.sep).join("/")),e.file){const s=N(r,["rev-parse","HEAD"]);s&&/^[0-9a-f]{40}$/.test(s)&&(i.commit=s)}return i}function kt(t,e){A(t);const n=m(t,{forWrite:!0});if(!C.has(n.root)){C.add(n.root);try{lt(n.root)}catch{}}const r=e.id||`amp_${I()}`,i={...e,id:r},l=new Set,c=j=>{if(typeof j!="string")return j;const p=Q(j);return p.kinds.forEach(b=>l.add(b)),p.text};i.msg!==void 0&&(i.msg=c(i.msg)),i.summary!==void 0&&(i.summary=c(i.summary)),Array.isArray(i.tags)&&(i.tags=i.tags.map(c)),typeof i.result=="string"&&(i.result=c(i.result));const s=i.detailLocal===!0;delete i.detailLocal;const u=typeof e.detail=="string"?c(e.detail):e.detail;l.size&&(i.meta={...i.meta||{},redacted:[...l]});try{i.meta={...ft(n,i,t),...i.meta||{}}}catch{}const f=i.localOnly===!0;if(delete i.localOnly,typeof u=="string"&&u.trim())try{at(n.root,r,u,s),i.detailRef=s?W:"details.jsonl"}catch{}delete i.detail;const d=f?n.sessions:ct(n,i),y=J(i),S=JSON.stringify(y)+`
|
|
10
|
+
`;if(o.mkdirSync(a.dirname(d),{recursive:!0}),o.appendFileSync(d,S,"utf8"),d!==n.sessions)try{o.mkdirSync(a.dirname(n.sessions),{recursive:!0}),o.appendFileSync(n.sessions,S,"utf8")}catch{}try{H(n.root,y)}catch{}return y}const B=".review-seen.log",G=5e4;function ut(t){const e=JSON.stringify([t&&t.type,t&&(t.summary??t.msg),t&&t.file,t&&t.line,t&&Array.isArray(t.tags)?t.tags:null,t&&(t.detailRef||t.meta&&t.meta.detailRef)||null,!!(t&&t.meta&&t.meta.resolved)]),n=Z.createHash("sha1").update(e).digest("hex").slice(0,16);return(t&&typeof t.id=="string"&&t.id?t.id.replace(/\s/g,"").slice(0,64):"-")+":"+n}function Nt(t){let e;try{e=o.readFileSync(a.join(t,B),"utf8")}catch{return null}return new Set(e.split(`
|
|
11
|
+
`).map(n=>n.trim()).filter(n=>n&&!n.startsWith("#")))}function dt(t,e){const n=e.filter(Boolean);o.mkdirSync(t,{recursive:!0});const r=a.join(t,B);o.appendFileSync(r,n.length?n.join(`
|
|
12
12
|
`)+`
|
|
13
|
-
`:"","utf8")
|
|
14
|
-
`)
|
|
13
|
+
`:"","utf8");try{if(o.statSync(r).size>G*40){const i=[...new Set(o.readFileSync(r,"utf8").split(`
|
|
14
|
+
`).filter(Boolean))].slice(-G),l=r+"."+process.pid+".tmp";o.writeFileSync(l,i.join(`
|
|
15
15
|
`)+`
|
|
16
|
-
|
|
16
|
+
`,"utf8"),o.renameSync(l,r)}}catch{}}function H(t,e){o.existsSync(a.join(t,B))&&dt(t,[ut($(e))])}function Bt(t,e){const n=m(t);return a.join(n.root,"details",`${e}.md`)}function Tt(t,e){if(!e)return null;const n=e.detailRef||e.meta&&e.meta.detailRef;if(!n)return null;const r=m(t),i=e.id||e.meta&&e.meta.id,l=P(r.root,i,!0);if(l!=null)return l;const c=P(r.root,i);if(c!=null)return c;if(typeof n=="string"&&/^details[\\/][A-Za-z0-9_.-]+\.md$/.test(n)&&!n.includes("..")){const s=a.join(r.root,"details"),u=a.join(r.root,n);try{const f=a.join(o.realpathSync(r.root),"details");if(o.lstatSync(s).isSymbolicLink()||o.realpathSync(s)!==f)return null;const d=o.realpathSync(u);return a.dirname(d)!==f?null:o.readFileSync(d,"utf8")}catch{return null}}return null}function yt(t){const e=m(t),n=[e.sessions,e.globalFile,e.currentBranchFile,e.defaultBranchFile].filter(Boolean);try{if(o.existsSync(e.branchesDir))for(const r of o.readdirSync(e.branchesDir))r.endsWith(".jsonl")&&n.push(a.join(e.branchesDir,r))}catch{}return[...new Set(n)]}function _t(t,e,n){if(!e||typeof n!="function")return{updated:0,files:[]};let r=0;const i=[];for(const l of yt(t)){if(!o.existsSync(l))continue;let c;try{c=o.readFileSync(l,"utf8").split(`
|
|
17
|
+
`)}catch{continue}let s=!1;const u=c.map(f=>{if(!f.trim())return f;let d;try{d=JSON.parse(f)}catch{return f}if(d.id!==e)return f;const y=n({...d,meta:{...d.meta||{}}})||d;s=!0,r++;try{H(m(t).root,y)}catch{}return JSON.stringify(y)});s&&(o.writeFileSync(l,u.filter((f,d)=>f.trim()||d<u.length-1).join(`
|
|
18
|
+
`).replace(/\n*$/,`
|
|
19
|
+
`),"utf8"),i.push(l))}return{updated:r,files:i}}function Lt(t,e){if(!e)return{removed:0,files:[]};const n=m(t),r=[n.sessions,n.globalFile,n.currentBranchFile,n.defaultBranchFile].filter(Boolean);try{if(o.existsSync(n.branchesDir))for(const s of o.readdirSync(n.branchesDir))s.endsWith(".jsonl")&&r.push(a.join(n.branchesDir,s))}catch{}const i=[...new Set(r)];let l=0;const c=[];for(const s of i){if(!o.existsSync(s))continue;let u;try{u=o.readFileSync(s,"utf8").split(`
|
|
20
|
+
`)}catch{continue}const f=[];let d=0;for(const y of u){if(!y.trim())continue;let S;try{S=JSON.parse(y).id}catch{f.push(y);continue}if(S===e){d++,l++;continue}f.push(y)}d>0&&(o.writeFileSync(s,f.length?f.join(`
|
|
17
21
|
`)+`
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
`:"","utf8"),c.push(s))}try{M(n.root,e)}catch{}try{M(n.root,e,!0)}catch{}try{const s=a.join(n.root,"details",`${e}.md`);o.existsSync(s)&&(o.unlinkSync(s),c.push(s))}catch{}return{removed:l,files:c}}const pt=30,ht=["note","attempt","detection"],U=new Set(["gotcha","decision","pattern"]);function mt(t){const e=t&&t.config&&t.config.rotation&&typeof t.config.rotation=="object"?t.config.rotation:{};return{archiveAfterDays:Number.isInteger(e.archiveAfterDays)&&e.archiveAfterDays>0?e.archiveAfterDays:pt,archivableTypes:Array.isArray(e.archivableTypes)&&e.archivableTypes.length?e.archivableTypes:ht,auto:e.auto===!0}}function gt(t){const e=[t.sessions,t.currentBranchFile,t.defaultBranchFile].filter(Boolean);try{if(o.existsSync(t.branchesDir))for(const n of o.readdirSync(t.branchesDir))n.endsWith(".jsonl")&&e.push(a.join(t.branchesDir,n))}catch{}return[...new Set(e)]}function St(t,e){const n=new Date(e),r=n.getUTCFullYear(),i=String(n.getUTCMonth()+1).padStart(2,"0");return a.join(t,"archive",`sessions-${r}-${i}.jsonl`)}function It(t,e={}){const n=m(t,{forWrite:!e.dryRun}),r=mt(jt(t)),i=Number.isFinite(e.maxAgeDays)?e.maxAgeDays:r.archiveAfterDays,l=e.archive!==!1,c=new Set((Array.isArray(e.types)&&e.types.length?e.types:r.archivableTypes).filter(p=>!U.has(p))),s=Date.now()-i*24*60*60*1e3,u=gt(n),f={scanned:0,archived:0,kept:0,files:[],byType:{},dryRun:!!e.dryRun},d=new Map,y=new Set,S=new Set,j=new Set;for(const p of u){if(!o.existsSync(p))continue;let b;try{b=o.readFileSync(p,"utf8").split(`
|
|
23
|
+
`)}catch{continue}const D=[];let R=0;for(const F of b){if(!F.trim())continue;let g;try{g=JSON.parse(F)}catch{D.push(F);continue}const h=g&&g.id,v=g&&typeof g.type=="string"?g.type:"note",w=typeof g.ts=="number"?g.ts:Date.parse(g.ts||0),Y=Number.isFinite(w)&&w<s,z=Array.isArray(g.tags)&&g.tags.includes("bookmark"),V=c.has(v)&&!U.has(v)&&!z;if(h&&!S.has(h)&&(S.add(h),f.scanned++),h||f.scanned++,Y&&V){if(R++,(!h||!y.has(h))&&(h&&y.add(h),f.archived++,f.byType[v]=(f.byType[v]||0)+1,l)){const E=St(n.root,w);d.has(E)||d.set(E,[]),d.get(E).push(F)}}else h&&!j.has(h)&&(j.add(h),f.kept++),h||f.kept++,D.push(F)}R>0&&!e.dryRun?(o.writeFileSync(p,D.length?D.join(`
|
|
24
|
+
`)+`
|
|
25
|
+
`:"","utf8"),f.files.push(p)):R>0&&f.files.push(p)}if(!e.dryRun&&l&&d.size>0)for(const[p,b]of d)try{o.mkdirSync(a.dirname(p),{recursive:!0}),o.appendFileSync(p,b.join(`
|
|
26
|
+
`)+`
|
|
27
|
+
`,"utf8")}catch{}return f}function jt(t){const{config:e}=m(t);try{return JSON.parse(o.readFileSync(e,"utf8"))}catch{return null}}function Jt(t,e={}){A(t);const{config:n}=m(t,{forWrite:!0});if(o.existsSync(n))return!1;const r={amp:_,project:e.project||a.basename(t),stack:e.stack||{},config:{autoCapture:!0,maxEntries:1e3,rotationStrategy:"archive",inject:["all"],injection:{maxEntries:4,maxCommits:5,maxEntryChars:200,protocolStyle:"compact"},rotation:{archiveAfterDays:30,archivableTypes:["note","attempt","detection"],auto:!1},...e.config||{}}};return o.writeFileSync(n,JSON.stringify(r,null,2)+`
|
|
28
|
+
`,"utf8"),!0}function $t(t,e={}){A(t);const{config:n}=m(t,{forWrite:!0});let r={};try{r=JSON.parse(o.readFileSync(n,"utf8"))}catch{r={}}(!r||typeof r!="object")&&(r={}),r.amp||(r.amp=_),r.project||(r.project=a.basename(t)),(!r.config||typeof r.config!="object")&&(r.config={});const i=r.config.injection&&typeof r.config.injection=="object"?r.config.injection:{};return r.config.injection={...i,...e},o.writeFileSync(n,JSON.stringify(r,null,2)+`
|
|
29
|
+
`,"utf8"),r.config.injection}function Ct(t){const e=a.join(t,"inferno"),n=a.join(e,"sessions.jsonl");if(!o.existsSync(n))return{migrated:0,reason:"no legacy sessions.jsonl"};const r=a.join(t,".ai-memory"),i=a.join(r,"sessions.jsonl");if(o.existsSync(i))return{migrated:0,reason:".ai-memory/sessions.jsonl already exists"};A(t);const l=o.readFileSync(n,"utf8").split(`
|
|
30
|
+
`).filter(Boolean);let c=0;for(const s of l)try{const u=JSON.parse(s),f=J(u);o.appendFileSync(i,JSON.stringify(f)+`
|
|
31
|
+
`,"utf8"),c++}catch{}return o.writeFileSync(a.join(r,"MIGRATED.md"),`# Migrated from inferno/
|
|
23
32
|
|
|
24
33
|
Copied ${c} entries from inferno/sessions.jsonl on ${new Date().toISOString()}.
|
|
25
34
|
|
|
26
35
|
The original inferno/sessions.jsonl is untouched. You can delete it once you're confident the new layout works.
|
|
27
|
-
`,"utf8"),{migrated:c,reason:"ok"}}export{
|
|
36
|
+
`,"utf8"),{migrated:c,reason:"ok"}}export{_ as AMP_VERSION,W as LOCAL_DETAIL_STORE,B as REVIEW_LOG_FILE,Et as _resetGitStampCache,m as ampPaths,kt as appendEntry,dt as appendReviewSeen,Lt as deleteEntry,Rt as describeStore,Bt as detailPath,A as ensureAmpDir,ut as entryKey,$ as fromAmp,I as generateULID,yt as memoryFiles,Ct as migrateLegacy,rt as projectSlug,It as pruneEntries,jt as readConfig,Tt as readDetail,wt as readEntries,Nt as readReviewSeen,it as resolveGlobalFile,mt as resolveRotationSettings,J as toAmp,_t as updateEntry,$t as updateInjectionConfig,Jt as writeDefaultConfig};
|
|
@@ -1 +1,3 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as n from"node:fs";import*as r from"node:path";import*as b from"node:crypto";import{fileURLToPath as j}from"node:url";const w=r.dirname(j(import.meta.url));function h(){return r.resolve(w,"../templates")}const i=e=>b.createHash("sha256").update(String(e).replace(/\r\n/g,`
|
|
2
|
+
`)).digest("hex"),p=new Set(["59ca1d6150462664c9677a112bc8cf57412b048788b8af3aef826acfe9a90fde","bb30ab374eace187685923ab2efb476703a9e0f13e0c99d6961104b04baa5953","ca8cee44c41ae45e4f3f4953c3875b7d4bd70fa3374a8ff5a702f69819e3d8dd"]),g=[{src:["skills","infernoflow-memory","SKILL.md"],dst:["skills","infernoflow-memory","SKILL.md"],generated:!1},{src:["agents","memory-keeper.md"],dst:["agents","memory-keeper.md"],generated:!1},{src:["hooks","infernoflow-agent-guard.mjs"],dst:["hooks","infernoflow-agent-guard.mjs"],generated:!0}],y=new Set;function v(e){return n.existsSync(r.join(e,".ai-memory"))||n.existsSync(r.join(e,"inferno"))}function k(e){const t={created:[],updated:[],pending:[]};if(!e||!v(e))return t;const u=h();for(const c of g){const m=r.join(u,...c.src),l=r.join(e,".claude",...c.dst),s=[".claude",...c.dst].join("/");let a;try{a=n.readFileSync(m,"utf8")}catch{continue}let o=null;try{o=n.readFileSync(l,"utf8")}catch{}try{if(o==null)n.mkdirSync(r.dirname(l),{recursive:!0}),n.writeFileSync(l,a,"utf8"),t.created.push(s);else if(i(o)!==i(a))if(c.generated||p.has(i(o)))n.writeFileSync(l,a,"utf8"),t.updated.push(s);else{const d=l+".new";let f=null;try{f=n.readFileSync(d,"utf8")}catch{}(f==null||i(f)!==i(a))&&(n.writeFileSync(d,a,"utf8"),t.pending.push(s))}}catch{}}return t}function F(e){const t=h(),u=[];let c=!1,m=!1;for(const s of g){if(s.generated)continue;const a=r.join(e,".claude",...s.dst);let o;try{o=n.readFileSync(a,"utf8")}catch{continue}let d=null;try{d=n.readFileSync(r.join(t,...s.src),"utf8")}catch{}const f=d!=null&&i(o)===i(d)||p.has(i(o)),S=[".claude",...s.dst].join("/");s.dst[0]==="agents"&&(m=!0,c=f),f&&u.push(S)}const l=r.join(e,".claude","hooks","infernoflow-agent-guard.mjs");return n.existsSync(l)&&(!m||c)&&u.push(".claude/hooks/infernoflow-agent-guard.mjs"),u}function L(e){try{if(!e||y.has(e))return 0;y.add(e);const t=k(e);return t.pending.length&&process.stderr.write(` infernoflow: a newer version of ${t.pending.join(", ")} is available \u2014 you edited yours, so it was saved as <name>.new; merge when convenient.
|
|
3
|
+
`),t.created.length+t.updated.length}catch{return 0}}export{L as ensureClaudeAssets,F as removableClaudeAssets,k as syncClaudeAssets};
|
package/dist/lib/cleanTree.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as l from"node:fs";import*as u 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","# Automatic bookmark transcript snapshots \u2014 local only, never committed.",".ai-memory/details.local.jsonl","# AI provider settings \u2014 older versions stored API keys here.","inferno/integrations.json","# Hook state (machine-specific).",".ai-memory/.hook-state.json",".ai-memory/.trigger-state.json",".ai-memory/.review-seen.log","# MCP runtime stamp (machine-specific).",".ai-memory/.mcp-runtime.json","# 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
2
|
`),A=["",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
3
|
`),I="# --- infernoflow (developer-local AI memory; do not commit) ---",y="# --- /infernoflow ---";function T(n){const r=n.indexOf(I),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=u.join(n,r);let e="";try{e=
|
|
4
|
+
`:"")+(e||"")}function h(n,r,i){const o=u.join(n,r);let e="";try{e=l.readFileSync(o,"utf8")}catch{}e=T(e);const a=e.indexOf(d),c=e.indexOf(s),m=i.trim();let t;if(a!==-1&&c!==-1&&c>a){const f=e.slice(0,a).replace(/\s+$/,""),p=e.slice(c+s.length).replace(/^\s+/,"");t=(f?f+`
|
|
5
5
|
|
|
6
|
-
`:"")+
|
|
6
|
+
`:"")+m+`
|
|
7
7
|
`+(p?`
|
|
8
8
|
`+p:"")}else e?t=e.replace(/\s+$/,"")+`
|
|
9
9
|
|
|
10
|
-
`+
|
|
11
|
-
`:t=
|
|
12
|
-
`;return t===e?"unchanged":(
|
|
10
|
+
`+m+`
|
|
11
|
+
`:t=m+`
|
|
12
|
+
`;return t===e?"unchanged":(l.mkdirSync(u.dirname(o),{recursive:!0}),l.writeFileSync(o,t,"utf8"),e?"updated":"created")}function E(n){return{gitignore:h(n,".gitignore",g),gitattributes:h(n,".gitattributes",A)}}export{s as GITIGNORE_END,d as GITIGNORE_START,E as applyCleanTreePolicy,h as ensureManagedBlock};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{describeStore as
|
|
2
|
-
No session memory yet.`)),console.log(
|
|
3
|
-
`));return}const
|
|
1
|
+
import{describeStore as L}from"../amp/io.mjs";import"node:fs";import"node:path";import{bold as O,cyan as T,gray as s,green as M,yellow as j,red as q}from"../ui/output.mjs";import{readEntries as Y}from"../amp/io.mjs";const p={gotcha:0,decision:1,attempt:2,preference:3,theme:4,note:5,error:5,handoff:6},I={gotcha:"\u26A0",decision:"\u2713",attempt:"\u21BA",preference:"\u2666",theme:"\u{1F3A8}",note:"\xB7",error:"\u2717",handoff:"\u2192"},N={gotcha:j,decision:M,attempt:T,preference:T,theme:T,note:s,error:q,handoff:s};function E(n){return n.toLowerCase().replace(/[^a-z0-9\s]/g," ").split(/\s+/).filter(t=>t.length>1)}function P(n,t){const l=[n.summary||"",n.type||""].join(" ").toLowerCase(),o=E(l);let r=0;for(const c of t)(n.summary||"").toLowerCase().includes(c)&&(r+=3),o.includes(c)&&(r+=1),o.some(a=>a.startsWith(c)||c.startsWith(a))&&(r+=.5);return r}function v(n){if(n==null||n==="")return"";const t=new Date(n);if(isNaN(t.getTime()))return"";const l=r=>new Date(r.getFullYear(),r.getMonth(),r.getDate()).getTime(),o=Math.round((l(new Date)-l(t))/864e5);return o<=0?`today ${t.toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"})}`:o===1?"yesterday":o<7?`${o}d ago`:o<30?`${Math.floor(o/7)}w ago`:t.toLocaleDateString("en-GB",{day:"2-digit",month:"short"})}function F(n,t){const l=n.type||"note",o=I[l]||"\xB7",r=N[l]||s,c=n.result?s(` [${n.result}]`):"",a=n.agent?s(` \u2014 ${n.agent}`):"",e=s(` (${v(n.ts)})`);let i=n.summary||"";if(t)for(const d of t){const u=new RegExp(`(${d})`,"gi");i=i.replace(u,D=>O(D))}console.log(` ${r(o+" "+l.padEnd(11))}${c}${a}${e}`),console.log(` ${i}`)}function _(n){return Y(n)}function W(n,t){if(!t||!n.file)return 0;const l=e=>String(e).replace(/\\/g,"/").replace(/^\.\//,""),o=l(n.file),r=l(t);if(o===r||o.endsWith("/"+r)||r.endsWith("/"+o))return 2;const c=o.includes("/")?o.slice(0,o.lastIndexOf("/")):"",a=r.includes("/")?r.slice(0,r.lastIndexOf("/")):"";return c&&c===a?1:0}function b(n,t,l,o,r=null){let c=n;l&&(c=c.filter(e=>(e.type||"note")===l));let a;return t.length>0?a=c.map(e=>({entry:e,score:P(e,t)})).filter(({score:e})=>e>0).map(e=>({...e,score:e.score+3*W(e.entry,r)})).sort((e,i)=>{if(i.score!==e.score)return i.score-e.score;const d=p[e.entry.type]??9,u=p[i.entry.type]??9;return d!==u?d-u:new Date(i.entry.ts||0).getTime()-new Date(e.entry.ts||0).getTime()}):a=c.map(e=>({entry:e,score:1+W(e,r)})).sort((e,i)=>{if(i.score!==e.score)return i.score-e.score;const d=p[e.entry.type]??9,u=p[i.entry.type]??9;return d!==u?d-u:new Date(i.entry.ts||0).getTime()-new Date(e.entry.ts||0).getTime()}),a.slice(0,o||20)}async function z(n=[]){const t=n,l=t.indexOf("--type"),o=l!==-1?t[l+1]:null,r=t.indexOf("--limit")!==-1?t.indexOf("--limit"):t.indexOf("-n"),c=r!==-1?parseInt(t[r+1]||"20",10):15,a=t.includes("--json"),e=t.includes("--recent")||t.includes("-r"),i=t.indexOf("--file"),d=i!==-1?t[i+1]:null,u=t.slice(1),m=u.filter((f,g)=>!(f.startsWith("--")||g>0&&u[g-1].startsWith("--"))).join(" ").trim(),x=e?[]:E(m),k=process.cwd(),y=_(k);if(y.length===0){if(a){console.log(JSON.stringify({results:[],total:0}));return}console.log(s(`
|
|
2
|
+
No session memory yet.`)),console.log(s(` Run: infernoflow log "<what happened>" --type gotcha
|
|
3
|
+
`));return}const h=e?y.slice(-c).reverse().map(f=>({entry:f,score:1})):b(y,x,o,c,d);if(a){console.log(JSON.stringify({query:m,type:o,total:y.length,matched:h.length,results:h.map(({entry:f,score:g})=>({...f,relevanceScore:g}))},null,2));return}if(console.log(),console.log(m?` ${O("\u{1F525} infernoflow ask")} ${T(`"${m}"`)}${o?s(` [${o}]`):""}`:e?` ${O("\u{1F525} infernoflow ask")} ${s("\u2014 recent entries")}`:` ${O("\u{1F525} infernoflow ask")} ${s("\u2014 all entries")}${o?s(` [${o}]`):""}`),console.log(s(` ${L(process.cwd())}`)),console.log(s(` ${"\u2500".repeat(52)}`)),h.length===0){console.log(),m?(console.log(s(` No entries found for "${m}"`)),o&&console.log(s(` Try removing --type ${o} to widen the search`))):console.log(s(" No entries found.")),console.log();return}const w=new Map;for(const{entry:f,score:g}of h){const $=f.type||"note";w.has($)||w.set($,[]),w.get($).push({entry:f,score:g})}const C=Object.keys(p).sort((f,g)=>p[f]-p[g]);let S=0;for(const f of C){const g=w.get(f);if(!g?.length)continue;console.log();const $=N[f]||s;console.log($(` ${I[f]} ${f.toUpperCase()}S (${g.length})`)),console.log(s(" "+"\u2500".repeat(50)));for(const{entry:R}of g)console.log(),F(R,x),S++}console.log(),console.log(s(` ${S} result${S!==1?"s":""} from ${y.length} total entries`)),h.length===c&&y.length>c&&console.log(s(" Use --limit N to see more")),console.log()}export{z as askCommand};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import*as d from"node:fs";import*as g from"node:path";import{readEntries as h,deleteEntry as w,toAmp as S,projectSlug as $}from"../amp/io.mjs";import{isNoise as j}from"../memoryView.mjs";import{findProjectRoot as k}from"../projectRoot.mjs";import{infernoflowHome as v}from"../personalConfig.mjs";import{bold as N,gray as l,green as b,yellow as A}from"../ui/output.mjs";const m=r=>(typeof r.ts=="number"?r.ts:Date.parse(r.ts||0))||0;function D(r){const i=h(r).sort((o,s)=>m(o)-m(s)),e={commitNotes:[],duplicates:[],oldFrustration:[]},n=new Map,c=Date.now()-14*864e5;for(const o of i){if(Array.isArray(o.tags)&&o.tags.includes("bookmark"))continue;if(j(o)){e.commitNotes.push(o);continue}if(o.source==="hook"&&/^User frustration:/.test(o.summary||"")&&m(o)<c){e.oldFrustration.push(o);continue}const s=`${o.type}\0${o.file||""}\0${String(o.summary||"").trim().toLowerCase()}`;n.has(s)?e.duplicates.push(o):n.set(s,o)}return e}async function M(r=[]){const i=process.cwd(),e=r.includes("--apply"),n=D(i),c=[["commit notes (duplicate git history)",n.commitNotes],["duplicates",n.duplicates],["raw frustration prompts > 14 days",n.oldFrustration]],o=c.reduce((t,[,a])=>t+a.length,0);console.log(`
|
|
2
|
+
`+N("\u{1F525} infernoflow curate"));for(const[t,a]of c){console.log(` ${String(a.length).padStart(4)} ${t}`);for(const y of a.slice(0,3))console.log(l(" "+String(y.summary||"").slice(0,90)))}if(!o){console.log(l(`
|
|
3
|
+
Nothing to curate.
|
|
4
|
+
`));return}if(!e){console.log(`
|
|
5
|
+
`+A("Dry run.")+l(` Add --apply to remove them (archived first).
|
|
6
|
+
`));return}const s=k(i),f=g.join(v(),"backups");d.mkdirSync(f,{recursive:!0});const p=g.join(f,`${$(s)}-curated-${new Date().toISOString().replace(/[:.]/g,"-")}.jsonl`),u=c.flatMap(([,t])=>t);d.writeFileSync(p,u.map(t=>JSON.stringify(S(t))).join(`
|
|
7
|
+
`)+`
|
|
8
|
+
`,"utf8");for(const t of u)w(i,t.id);console.log(`
|
|
9
|
+
`+b(`\u2714 removed ${u.length}`)+l(` \u2014 archived to ${p}
|
|
10
|
+
`))}export{D as curateCandidates,M as curateCommand};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{claudeJsonPath as A,claudeDesktopConfigPath as I,isInfernoflowEntry as M}from"../mcpRegistration.mjs";import{HOOK_VERSION_MARKER as _}from"../securityRefresh.mjs";import{redactSecrets as E}from"../security/redact.mjs";import*as c from"node:fs";import*as s from"node:path";import*as y from"node:os";import*as
|
|
2
|
-
`);for(const r of[s.join(".cursor","inferno-mcp-server.mjs"),"inferno-mcp-server.mjs"])try{const i=c.readFileSync(s.join(e,r),"utf8");/serverInfo: \{ name: "infernoflow"|\[infernoflow MCP\]/.test(i)&&
|
|
1
|
+
import{claudeJsonPath as A,claudeDesktopConfigPath as I,isInfernoflowEntry as M}from"../mcpRegistration.mjs";import{HOOK_VERSION_MARKER as _}from"../securityRefresh.mjs";import{redactSecrets as E}from"../security/redact.mjs";import*as c from"node:fs";import*as s from"node:path";import*as y from"node:os";import*as D from"node:http";import{execSync as $,spawnSync as b}from"node:child_process";import{fileURLToPath as C}from"node:url";import{bold as N,cyan as P,gray as S,green as j,yellow as x,red as v}from"../ui/output.mjs";import{detectAvailableProviders as F}from"../ai/providerRouter.mjs";import{readEntries as R,readConfig as T}from"../amp/io.mjs";import{resolveInjectionSettings as G}from"../ruleFiles.mjs";function f(e,n){try{const o=n();return{label:e,...o}}catch(o){return{label:e,status:"error",message:o.message,fix:null}}}function a(e,n){return{status:"pass",message:e,detail:n||null,fix:null}}function l(e,n){return{status:"warn",message:e,detail:null,fix:n||null}}function h(e,n){return{status:"fail",message:e,detail:null,fix:n||null}}function L(){const e=process.version,n=parseInt(e.slice(1).split(".")[0],10);return n>=20?a(`Node.js ${e}`,"Node 20+ recommended"):n>=18?a(`Node.js ${e}`):h(`Node.js ${e} \u2014 infernoflow requires Node 18+`,"Install Node 20 from nodejs.org")}function J(){try{const e=b("infernoflow",["--version"],{encoding:"utf8",timeout:5e3,shell:process.platform==="win32"});return e.status===0?a(`infernoflow v${e.stdout.trim()} installed`):a("infernoflow CLI on PATH (version probe failed but doctor itself ran)")}catch{return a("infernoflow CLI on PATH (version probe threw but doctor itself ran)")}}function H(e){try{return $("git rev-parse --git-dir",{cwd:e,stdio:"ignore"}),a("Git repository detected")}catch{return h("Not a git repository","git init && git add . && git commit -m 'init'")}}function K(e){const n=s.join(e,".ai-memory"),o=s.join(e,"inferno");return c.existsSync(n)&&c.existsSync(o)?a(".ai-memory/ + inferno/ both present"):c.existsSync(n)?a(".ai-memory/ directory exists (memory mode)"):c.existsSync(o)?a("inferno/ directory exists"):h("No memory directory found (.ai-memory/ or inferno/)","infernoflow init")}function U(e){if(w(e)){let o=0;try{o=R(e).length}catch{}return a(o===0?"Memory mode \u2014 sessions.jsonl will be created on first log":`Memory mode \u2014 ${o} session entr${o===1?"y":"ies"}`)}const n=s.join(e,"inferno");for(const o of["contract.json","capabilities.json"]){const t=s.join(n,o);if(c.existsSync(t))try{const i=(JSON.parse(c.readFileSync(t,"utf8")).capabilities||[]).length;return a(`${o} valid \u2014 ${i} capabilities`)}catch{return h(`${o} contains invalid JSON`,`Fix the JSON syntax in inferno/${o}`)}}return h("No contract.json/capabilities.json (and not in memory mode)","infernoflow init or infernoflow init --mode full")}function w(e){const n=s.join(e,".ai-memory"),o=s.join(e,"inferno","contract.json");if(c.existsSync(n)&&!c.existsSync(o))return!0;try{return JSON.parse(c.readFileSync(s.join(e,"inferno","config.json"),"utf8")).mode==="memory"}catch{return!1}}function W(e){if(w(e))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const n=s.join(e,"inferno","scenarios");if(!c.existsSync(n))return l("No scenarios/ directory","infernoflow init");const o=c.readdirSync(n).filter(t=>t.endsWith(".json"));return o.length?a(`${o.length} scenario file${o.length!==1?"s":""} found`):l("scenarios/ is empty","Add scenario files or run infernoflow suggest")}function V(e){if(w(e))return{status:"info",message:"n/a in memory mode",detail:null,fix:null};const n=s.join(e,"inferno","CHANGELOG.md");return c.existsSync(n)?a("inferno/CHANGELOG.md exists"):l("No inferno/CHANGELOG.md","infernoflow init")}function X(e){if(w(e))return{status:"info",message:"n/a in memory mode (CLAUDE.md is auto-maintained)",detail:null,fix:null};const n=s.join(e,"inferno","CONTEXT.md");if(!c.existsSync(n))return l("No CONTEXT.md generated","infernoflow context");const o=(Date.now()-c.statSync(n).mtimeMs)/(1e3*60*60*24);return o>7?l(`CONTEXT.md is ${Math.round(o)} days old \u2014 may be stale`,"infernoflow context"):a(`CONTEXT.md present (${Math.round(o)}d old)`)}function Y(e){const n=s.join(e,".git","hooks","post-commit");return c.existsSync(n)&&c.readFileSync(n,"utf8").includes('infernoflow log "commit:')?l("Old commit-logging git hook still installed","infernoflow setup --yes"):a("No commit-logging git hook (git keeps commit history)")}function z(e){const n=[s.join(e,".cursor","mcp.json"),s.join(e,".mcp.json"),s.join(e,".vscode","mcp.json"),s.join(y.homedir(),".claude.json"),s.join(y.homedir(),".cursor","mcp.json"),s.join(y.homedir(),"Library","Application Support","Claude","claude_desktop_config.json"),s.join(y.homedir(),"AppData","Roaming","Claude","claude_desktop_config.json")];for(const o of n)if(c.existsSync(o))try{const t=JSON.parse(c.readFileSync(o,"utf8")),r=t.mcpServers||t.mcp_servers||t.servers||{};if(Object.keys(r).some(i=>i.toLowerCase().includes("inferno")))return a(`MCP server configured in ${s.basename(o)}`)}catch{}return l("MCP server not configured","infernoflow setup --yes (adds to Cursor/Claude/Copilot config)")}function q(){const e=[];for(const[n,o]of[["~/.claude.json",A()],["claude_desktop_config.json",I()]])try{const t=JSON.parse(c.readFileSync(o,"utf8"))?.mcpServers?.infernoflow;t&&M(t)&&e.push(`${n} \u2192 ${(t.env||{}).INFERNOFLOW_PROJECT_DIR||"?"}`)}catch{}return e.length?l(`User-level MCP entry pins every project to one repo: ${e.join("; ")}`,"infernoflow setup --yes (registers per project and removes the pinned entry)"):a("No user-level infernoflow MCP entry pinning all projects")}function B(e){const n=[],o=(()=>{try{return c.readFileSync(C(new URL("../../templates/cursor/inferno-mcp-server.mjs",import.meta.url)),"utf8")}catch{return null}})(),t=r=>r.replace(/\r\n/g,`
|
|
2
|
+
`);for(const r of[s.join(".cursor","inferno-mcp-server.mjs"),"inferno-mcp-server.mjs"])try{const i=c.readFileSync(s.join(e,r),"utf8");/serverInfo: \{ name: "infernoflow"|\[infernoflow MCP\]/.test(i)&&o&&t(i)!==t(o)&&n.push(r)}catch{}for(const r of[s.join(".claude","hooks","log-frustration.mjs"),s.join(".cursor","hooks","inferno-session-draft.mjs")])try{c.readFileSync(s.join(e,r),"utf8").includes(_)||n.push(r)}catch{}return n.length?l(`Outdated (vulnerable) copies: ${n.map(r=>r.split(s.sep).join("/")).join(", ")}`,"infernoflow setup --yes"):a("Generated MCP server / hook copies are current")}function Q(e){const n=s.join(e,".ai-memory");if(!c.existsSync(n))return a("No memory store \u2014 nothing to scan");const o=[],t=r=>{for(const i of c.readdirSync(r,{withFileTypes:!0})){const d=s.join(r,i.name);if(i.isDirectory()){t(d);continue}if(/\.(jsonl|json|md)$/.test(i.name)&&!(i.name==="details.local.jsonl"||i.name==="global.jsonl"))try{const u=E(c.readFileSync(d,"utf8")).kinds;u.length&&o.push(`${s.relative(e,d).split(s.sep).join("/")} (${u.join(", ")})`)}catch{}}};try{t(n)}catch{}return o.length?l(`Possible secrets in memory files: ${o.join("; ")}`,"Remove them (infernoflow forget <id>); if already pushed, rotate the secret \u2014 it is in git history"):a("No secrets found in memory files")}function Z(e){try{const n=JSON.parse(c.readFileSync(s.join(e,".ai-memory","amp.json"),"utf8"));if(n&&n.globalDir)return l(`.ai-memory/amp.json sets globalDir (${n.globalDir}) \u2014 ignored: a committed file can't decide where personal memory goes`,`infernoflow sync set ${n.globalDir} then remove globalDir from amp.json`)}catch{}return a("Personal memory location is set only in your personal config")}function ee(e){const n=s.join(e,"inferno","integrations.json");if(!c.existsSync(n))return a("No API keys stored in the project");let o=!1;try{o=Object.values(JSON.parse(c.readFileSync(n,"utf8"))||{}).some(r=>r&&typeof r=="object"&&r.apiKey)}catch{}let t=!1;try{$("git ls-files --error-unmatch -- inferno/integrations.json",{cwd:e,stdio:"ignore"}),t=!0}catch{}return o&&t?l("inferno/integrations.json holds an API key AND is tracked by git","Rotate the key with the provider, then run: infernoflow ai status (moves keys out of the project)"):o?l("inferno/integrations.json holds an API key","infernoflow ai status (moves it to ~/.infernoflow/ai-credentials.json)"):t?l("inferno/integrations.json is tracked by git","git rm --cached inferno/integrations.json"):a("No API keys stored in the project")}function ne(e,n){const o=s.join(e,".ai-memory",".mcp-runtime.json");if(!c.existsSync(o))return a("MCP runtime stamp not present yet \u2014 start your AI tool to write one");let t;try{t=JSON.parse(c.readFileSync(o,"utf8"))}catch{return l(".mcp-runtime.json present but unreadable","Delete .ai-memory/.mcp-runtime.json and restart your AI tool")}return!t||typeof t.version!="string"?l(".mcp-runtime.json malformed","Delete it; the next MCP boot will rewrite it"):t.version===n?a(`MCP runtime v${t.version} matches CLI`):n.startsWith("0.0.0")||t.version.startsWith("0.0.0")?a(`MCP runtime v${t.version} (dev/source) \u2014 skipping version-skew check`):l(`MCP server is running v${t.version} but CLI is v${n} \u2014 restart your AI tool to load the new code.`,"Quit and reopen Cursor / Claude Code / VS Code (the long-running MCP process keeps the old code in memory until restart)")}function oe(e){const n=G(T(e));if(!n.includeProtocol)return a("Memory-protocol injection disabled in config (injection.includeProtocol=false)");const o=n.targets,t=[];for(const r of o)try{const i=c.readFileSync(s.join(e,String(r)),"utf8");i.includes("amp_write")&&/Memory protocol/i.test(i)&&t.push(r)}catch{}return t.length?a(`Memory protocol in ${t.length}/${o.length} configured rule file(s) \u2014 AI knows when to auto-capture`):l("Memory protocol missing from rule files \u2014 the AI won't auto-capture gotchas","infernoflow refresh (writes the amp_write capture protocol into your rule files)")}function te(e){const n=F(e),o=Object.entries(n).filter(([,t])=>t).map(([t])=>t);return o.length?a(`AI provider${o.length!==1?"s":""}: ${o.join(", ")}`):l("No AI provider configured",`Set ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_AI_API_KEY, or OPENROUTER_API_KEY
|
|
3
3
|
Or install Ollama (ollama.com) for free local AI
|
|
4
|
-
Or use VS Code with GitHub Copilot (zero config)`)}async function
|
|
4
|
+
Or use VS Code with GitHub Copilot (zero config)`)}async function re(){return new Promise(e=>{const n=D.get({hostname:"localhost",port:11434,path:"/api/tags",timeout:1500},o=>{e(a("Ollama running on localhost:11434"))});n.on("error",()=>e({status:"info",message:"Ollama not running (optional)",fix:"ollama serve",detail:null})),n.on("timeout",()=>{n.destroy(),e({status:"info",message:"Ollama not running (optional)",fix:null,detail:null})})})}function ie(){const e=s.join(y.homedir(),".infernoflow","credentials.json");if(!c.existsSync(e))return{status:"info",message:"Not logged in to cloud (optional)",fix:"infernoflow login",detail:null};try{const n=JSON.parse(c.readFileSync(e,"utf8")),o=n.user?.login||n.user?.name||n.user?.email||"unknown";if(n.mode==="supabase"&&n.access_token){if(n.expires_at){const t=new Date(n.expires_at).getTime();if(Date.now()>t)return l(`JWT expired for ${o} \u2014 refresh on next log will retry`,"infernoflow login")}return a(`Authenticated as ${o} (Supabase JWT \u2014 auth.uid() writes)`)}return n.mode==="device-flow"&&n.github_access_token?{status:"info",message:`Identity-only as ${o} (device flow \u2014 anon-mode writes)`,fix:"infernoflow login (without --device-flow, for full auth)",detail:null}:n.access_token?l(`Legacy login for ${o} \u2014 re-run for authenticated cloud writes`,"infernoflow logout && infernoflow login"):{status:"info",message:"Credentials file present but no recognised token",fix:"infernoflow logout && infernoflow login",detail:null}}catch{return l("Credentials file unreadable","infernoflow logout && infernoflow login")}}function se(){try{const e=C(import.meta.url),n=s.resolve(s.dirname(e),"..","..","bin","infernoflow.mjs");if(!c.existsSync(n))return{status:"info",message:"bin/infernoflow.mjs not found from doctor location",fix:null,detail:null};const t=[...c.readFileSync(n,"utf8").matchAll(/import\("\.\.\/lib\/(commands\/[^"]+|telemetry\.mjs)"\)/g)],r=[],i=s.resolve(s.dirname(n),"..");for(const d of t){const u=d[1],p=s.join(i,"lib",u);c.existsSync(p)||r.push(u)}return r.length?h(`${r.length} routed command(s) missing module files: ${r.slice(0,3).join(", ")}${r.length>3?"\u2026":""}`,"Restore the missing files or remove their entries from bin/infernoflow.mjs"):a(`All ${t.length} routed commands resolve to real files`)}catch(e){return{status:"info",message:`Router integrity check skipped: ${e.message}`,fix:null,detail:null}}}function ce(e){const n=s.join(e,"package.json");if(!c.existsSync(n))return a("No package.json to audit");let o;try{o=JSON.parse(c.readFileSync(n,"utf8"))}catch{return{status:"info",message:"package.json unreadable; skipping audit",detail:null,fix:null}}const t=o.scripts||{},r=new Set(["log","ask","switch","recap","status","refresh","forget","prune","bookmark","init","setup","doctor","context","mcp","hook","resume","resolve","move","curate","transcript","install-cursor-hooks","install-vscode-copilot-hooks","generate-skills","ai","telemetry","uninstall","check","sync","amp","commands"]),i=[];for(const[u,p]of Object.entries(t)){const k=/\binfernoflow\s+([a-z][a-z0-9-]*)/.exec(String(p));if(!k)continue;const m=k[1];r.has(m)||i.push({scriptName:u,verb:m})}if(i.length===0)return a("npm scripts use current command surface");const d=i.map(u=>`${u.scriptName} \u2192 infernoflow ${u.verb}`).join(", ");return l(`package.json references ${i.length} deprecated command(s): ${d}`,"Edit package.json scripts to use the current surface (run `infernoflow --help` to list verbs)")}function ae(e){const n=s.join(e,".gitignore");if(!c.existsSync(n))return{status:"info",message:".gitignore not found",fix:null,detail:null};const o=c.readFileSync(n,"utf8");return/^(?:\*\*\/)?node_modules\/?$/m.test(o)?a(".gitignore excludes node_modules"):l(".gitignore does not exclude node_modules","Add 'node_modules/' (and '**/node_modules/') to .gitignore")}function le(e,n){const o=e.filter(r=>r.status==="warn"&&r.fix),t=[];for(const r of o){const i=r.fix;if(i.startsWith("infernoflow ")){const d=i.slice(12).split(" ");b("infernoflow",d,{cwd:n,encoding:"utf8",timeout:3e4}).status===0&&t.push(r.label)}}return t}function fe(e){return e==="pass"?j("\u2714"):e==="warn"?x("\u26A0"):e==="fail"?v("\u2717"):S("\xB7")}function ue(e,n){const o={pass:0,warn:0,fail:0,info:0,error:0};for(const i of e)o[i.status]=(o[i.status]||0)+1;console.log(),console.log(` ${N("\u{1F525} infernoflow doctor")}`),console.log();const t=Math.max(...e.map(i=>i.label.length))+2;for(const i of e)console.log(` ${fe(i.status)} ${N(i.label.padEnd(t))} ${i.message}`),i.detail&&console.log(` ${" ".repeat(t)} ${S(i.detail)}`),i.fix&&(i.status==="warn"||i.status==="fail")&&console.log(` ${" ".repeat(t)} ${P("fix:")} ${S(i.fix)}`);console.log();const r=o.fail>0?v("issues found"):o.warn>0?x("warnings"):j("all good");console.log(` ${r} \u2014 ${j(String(o.pass))} pass \xB7 ${x(String(o.warn))} warn \xB7 ${v(String(o.fail))} fail (${n}ms)`),console.log(),(o.warn>0||o.fail>0)&&(console.log(` Run ${P("infernoflow doctor --fix")} to auto-fix warnings`),console.log())}async function me(e){const n=e.slice(1),o=n.includes("--json"),t=n.includes("--fix"),r=process.cwd(),i=Date.now();let d="0.0.0-unknown";try{const{fileURLToPath:m}=await import("node:url"),g=s.dirname(m(import.meta.url)),O=s.join(g,"..","..","package.json");d=JSON.parse(c.readFileSync(O,"utf8")).version||d}catch{}const u=[f("Node.js version",()=>L()),f("infernoflow CLI",()=>J()),f("Git repository",()=>H(r)),f("inferno/ directory",()=>K(r)),f("Contract / mode",()=>U(r)),f("Scenarios",()=>W(r)),f("Changelog",()=>V(r)),f("CONTEXT.md",()=>X(r)),f("Git hooks",()=>Y(r)),f("Auto-capture protocol",()=>oe(r)),f("MCP server",()=>z(r)),f("MCP runtime version",()=>ne(r,d)),f("MCP registration",()=>q()),f("Generated copies",()=>B(r)),f("Secrets in memory",()=>Q(r)),f("API keys",()=>ee(r)),f("Personal memory path",()=>Z(r)),f("AI providers",()=>te(r)),f("Cloud sync",()=>ie()),f(".gitignore",()=>ae(r)),f("Router integrity",()=>se()),f("npm scripts",()=>ce(r)),await re().then(m=>({label:"Ollama (local AI)",...m}))],p=Date.now()-i;if(t){const m=le(u,r);if(m.length)return o||(console.log(),m.forEach(g=>console.log(` ${j("\u2714")} Fixed: ${g}`)),console.log()),me(o?["doctor","--json"]:["doctor"])}if(o){const m={pass:0,warn:0,fail:0,info:0};u.forEach(g=>m[g.status]=(m[g.status]||0)+1),console.log(JSON.stringify({ok:m.fail===0,counts:m,results:u,elapsed:p}));return}ue(u,p),u.some(m=>m.status==="fail")&&process.exit(1)}export{me as doctorCommand};
|