infernoflow 0.44.9 → 0.44.11
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 +118 -26
- package/dist/bin/infernoflow.mjs +8 -8
- package/dist/lib/amp/io.mjs +13 -13
- package/dist/lib/commands/bookmark.mjs +28 -0
- package/dist/lib/commands/log.mjs +13 -12
- package/dist/lib/commands/switch.mjs +10 -10
- package/dist/lib/projectRoot.mjs +1 -1
- package/dist/lib/ruleFiles.mjs +8 -8
- package/dist/lib/transcript.mjs +4 -0
- package/dist/templates/cursor/hooks/inferno-session-draft.mjs +53 -1
- package/dist/templates/cursor/inferno-mcp-server.mjs +46 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# 🔥 infernoflow
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> **Every new AI session starts cold — the gotchas you found, the decisions you made, don't survive. infernoflow makes them stick.**
|
|
4
|
+
>
|
|
5
|
+
> Persistent memory for AI coding sessions. Captures the gotchas, decisions and dead ends your code can't tell an agent — and replays them into your next Cursor / Claude Code / Copilot chat so the same wrong turn never happens twice.
|
|
4
6
|
|
|
5
7
|
[](https://www.npmjs.com/package/infernoflow)
|
|
6
8
|
[](https://www.npmjs.com/package/infernoflow)
|
|
@@ -8,16 +10,45 @@
|
|
|
8
10
|
[](https://marketplace.visualstudio.com/items?itemName=infernoflow.infernoflow)
|
|
9
11
|
[](./LICENSE)
|
|
10
12
|
|
|
13
|
+
You spent 90 minutes yesterday teaching Claude that your auth API returns `200` even on errors — check the response body, not the status code. Today you ask the same kind of question and watch it write `if (response.ok) { return data }` all over again. The AI didn't get worse overnight. **It just doesn't have memory.**
|
|
14
|
+
|
|
15
|
+
infernoflow is a local-first CLI + VS Code extension + open protocol (AMP) that gives your AI persistent context across sessions. No SaaS. JSONL on disk. Three rule files your IDE already reads.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## What's new — 🆕 v0.44.10: session bookmarks
|
|
20
|
+
|
|
21
|
+
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
|
+
|
|
23
|
+
```bash
|
|
24
|
+
infernoflow bookmark "before the SP refactor"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or just say it in chat — the Cursor `beforeSubmitPrompt` hook catches phrases like `"bookmark this"` / `"mark this point"` / `"save this checkpoint"` and drops the bookmark itself, no AI cooperation required. Or let the AI do it via the `amp_bookmark` MCP tool when it notices the session is getting long.
|
|
28
|
+
|
|
29
|
+
**No `--note`?** infernoflow reads Claude Code's own on-disk transcript (`~/.claude/projects/…/*.jsonl`), distills the last 40 turns to markdown, and stores it as the bookmark's context. Fully deterministic — no model call. Recall any time:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
infernoflow bookmark list # ● has context, ○ marker only
|
|
33
|
+
infernoflow bookmark show "SP refactor" # jump back — see the whole snapshot
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Bookmarks are **never auto-pruned**, and they surface in `infernoflow switch` under a `## 🔖 Bookmarks — Resume Points` section so the next session opens right where work paused.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
11
40
|
## The loop
|
|
12
41
|
|
|
13
|
-
Every new AI session today starts cold. The agent re-reads your code, re-derives the obvious, and
|
|
42
|
+
Every new AI session today starts cold. The agent re-reads your code, re-derives the obvious, and re-makes the same wrong move someone else made yesterday. infernoflow closes that loop in four stages:
|
|
14
43
|
|
|
15
|
-
1. **Capture** — while you and the agent work,
|
|
16
|
-
2. **Link** — each captured moment becomes a structured AMP entry (`gotcha | decision | attempt | note | detection | pattern`) with timestamp, file:line
|
|
44
|
+
1. **Capture** — while you and the agent work, moments worth saving get logged automatically: a gotcha hit, a decision made, an attempted fix that failed, a pattern noticed, a resume point marked. The AI writes them via the `amp_write` MCP tool. A protocol block injected into the rule files teaches it exactly when. A Cursor `beforeSubmitPrompt` hook backstops the AI by scanning your prompt for triggers (`!!`, `retry`, `not working`, `still broken`, `bookmark this`) and writing the entry deterministically when the AI doesn't.
|
|
45
|
+
2. **Link** — each captured moment becomes a structured AMP entry (`gotcha | decision | attempt | note | detection | pattern | bookmark`) with timestamp, `file:line`, tags, and a stable AMP id.
|
|
17
46
|
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
|
|
47
|
+
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
48
|
|
|
20
|
-
|
|
49
|
+
No service to log into. No SaaS. JSONL on disk, an MCP server, and three rule files your IDE already reads.
|
|
50
|
+
|
|
51
|
+
---
|
|
21
52
|
|
|
22
53
|
## Install
|
|
23
54
|
|
|
@@ -28,13 +59,15 @@ infernoflow init --yes
|
|
|
28
59
|
|
|
29
60
|
Zero runtime dependencies. Works on Node ≥ 18 — macOS, Linux, Windows.
|
|
30
61
|
|
|
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
|
|
62
|
+
`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:
|
|
32
63
|
|
|
33
64
|
```bash
|
|
34
65
|
infernoflow status
|
|
35
66
|
```
|
|
36
67
|
|
|
37
|
-
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## The 5-command core + 4
|
|
38
71
|
|
|
39
72
|
These cover 95% of usage:
|
|
40
73
|
|
|
@@ -45,13 +78,26 @@ These cover 95% of usage:
|
|
|
45
78
|
| `infernoflow switch` | Generate a handoff for the next session. `--copy` puts it on your clipboard |
|
|
46
79
|
| `infernoflow recap` | End-of-session summary with health score + unlogged-change detection |
|
|
47
80
|
| `infernoflow status` | Quick health check — entries, gotchas, decisions, last activity |
|
|
81
|
+
| `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. |
|
|
48
82
|
| `infernoflow refresh` | Manually rebuild `CLAUDE.md` / `.cursorrules` / `copilot-instructions.md` from memory |
|
|
49
|
-
| `infernoflow forget <id
|
|
50
|
-
| `infernoflow prune` | Archive stale `note` / `attempt` entries older than 30 days. Gotchas/decisions never auto-pruned. Default dry-run; `--apply` to act
|
|
83
|
+
| `infernoflow forget <id\|prefix>` | Delete a memory entry without hand-editing JSONL. `--last` for the newest |
|
|
84
|
+
| `infernoflow prune` | Archive stale `note` / `attempt` entries older than 30 days. Gotchas/decisions/bookmarks never auto-pruned. Default dry-run; `--apply` to act |
|
|
51
85
|
|
|
52
86
|
In practice you barely run any of these — the MCP-aware AI does it for you. The CLI is for grep-style introspection.
|
|
53
87
|
|
|
54
|
-
`infernoflow commands` shows the full list (~
|
|
88
|
+
`infernoflow commands` shows the full list (~23 commands, grouped by purpose).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Works with GitHub Copilot Chat (via VS Code LMT — not MCP)
|
|
93
|
+
|
|
94
|
+
**GitHub Copilot Chat doesn't support MCP.** Every other AI memory tool assumes MCP is the transport, so none of them work with Copilot Chat.
|
|
95
|
+
|
|
96
|
+
The infernoflow **VS Code extension** registers `amp_write` and `amp_read` as native **VS Code Language Model Tools** — Copilot's supported extension surface. So Copilot can log a gotcha or recall a past decision **on its own**, mid-conversation, the moment it notices something worth remembering. Nothing to wire up: install the extension and Copilot's tool picker shows `🔥 amp_write`. Call by hand with `#amp_write` / `#amp_read` in the chat box.
|
|
97
|
+
|
|
98
|
+
Cursor and Claude Code get the same capability through the MCP server the CLI installs. **Same protocol, same disk file, three transports** — MCP for Cursor/Claude, LMT for Copilot, native Node in the extension.
|
|
99
|
+
|
|
100
|
+
---
|
|
55
101
|
|
|
56
102
|
## Keeping it lean: token budget + rotation
|
|
57
103
|
|
|
@@ -82,7 +128,11 @@ infernoflow refresh --max-memory 3 # same; persists into amp.j
|
|
|
82
128
|
infernoflow prune --apply --max-age-days 14 # one-off cleanup
|
|
83
129
|
```
|
|
84
130
|
|
|
85
|
-
**Rotation** archives stale `note` / `attempt` / `detection` entries to `.ai-memory/archive/sessions-YYYY-MM.jsonl` — invisible to the merged read (so the AI, sidebar, `ask`, and `refresh` stop surfacing them) but still on disk if you want them back. `gotcha`, `decision`, and `
|
|
131
|
+
**Rotation** archives stale `note` / `attempt` / `detection` entries to `.ai-memory/archive/sessions-YYYY-MM.jsonl` — invisible to the merged read (so the AI, sidebar, `ask`, and `refresh` stop surfacing them) but still on disk if you want them back. `gotcha`, `decision`, `pattern`, and `bookmark` entries are **never auto-pruned** — that's the knowledge you logged infernoflow FOR.
|
|
132
|
+
|
|
133
|
+
**Two-tier bodies (new in 0.44.10):** any entry can carry a rich `detail` — stored in `.ai-memory/details/<id>.md`, loaded on demand via `readDetail()`, and **never injected into rule files**. The lean index stays lean; you pay for the body only when you open it. `log --detail`, `--detail-file`, MCP `amp_write` `detail`, and the new `amp_bookmark` tool all feed it.
|
|
134
|
+
|
|
135
|
+
---
|
|
86
136
|
|
|
87
137
|
## Branch-aware memory + cross-machine sync
|
|
88
138
|
|
|
@@ -93,6 +143,8 @@ infernoflow prune --apply --max-age-days 14 # one-off cleanup
|
|
|
93
143
|
├── branches/
|
|
94
144
|
│ ├── main.jsonl ← project-wide truths (git-tracked)
|
|
95
145
|
│ └── feature-auth.jsonl ← your current branch's work (git-tracked)
|
|
146
|
+
├── details/ ← Tier-2 rich bodies (loaded on demand)
|
|
147
|
+
│ └── amp_01HXYZ....md
|
|
96
148
|
├── global.jsonl ← your personal preferences (gitignored)
|
|
97
149
|
└── sessions.jsonl ← legacy flat file (still read)
|
|
98
150
|
```
|
|
@@ -106,17 +158,21 @@ infernoflow prune --apply --max-age-days 14 # one-off cleanup
|
|
|
106
158
|
- **`merge=union` on branch JSONLs** means concurrent commits from different machines merge cleanly — no manual conflict resolution.
|
|
107
159
|
- **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.
|
|
108
160
|
|
|
161
|
+
---
|
|
162
|
+
|
|
109
163
|
## Cross-IDE — same memory, every tool
|
|
110
164
|
|
|
111
|
-
|
|
165
|
+
| Tool | Reads from | Writes via |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| Claude Code | `CLAUDE.md` | MCP (`amp_write`) |
|
|
168
|
+
| Cursor | `.cursorrules` | MCP (`amp_write`) + `beforeSubmitPrompt` hook |
|
|
169
|
+
| GitHub Copilot Chat (VS Code) | `.github/copilot-instructions.md` | **VS Code LMT** (`amp_write`) — extension only, no MCP |
|
|
170
|
+
| GitHub Copilot (JetBrains) | `.github/copilot-instructions.md` | rule files only (read-only surface) |
|
|
171
|
+
| Windsurf | `.windsurfrules` | MCP (planned) |
|
|
112
172
|
|
|
113
|
-
|
|
114
|
-
|---|---|
|
|
115
|
-
| Claude Code | `CLAUDE.md` |
|
|
116
|
-
| Cursor | `.cursorrules` |
|
|
117
|
-
| GitHub Copilot (VS Code, JetBrains) | `.github/copilot-instructions.md` |
|
|
173
|
+
The MCP server is wired by `infernoflow setup` / `init` into each tool's config file. No per-tool setup.
|
|
118
174
|
|
|
119
|
-
|
|
175
|
+
---
|
|
120
176
|
|
|
121
177
|
## MCP tools (for AI agents)
|
|
122
178
|
|
|
@@ -124,9 +180,10 @@ When the MCP server is wired, your AI agent can call these directly in chat:
|
|
|
124
180
|
|
|
125
181
|
| Tool | What it does |
|
|
126
182
|
|---|---|
|
|
127
|
-
| `amp_write` | Log an entry (`type`, `msg`, optional `file` / `line` / `tags`) |
|
|
183
|
+
| `amp_write` | Log an entry (`type`, `msg`, optional `file` / `line` / `tags` / `detail`) |
|
|
128
184
|
| `amp_read` | Read entries with optional filters |
|
|
129
185
|
| `amp_search` | Keyword search across entries |
|
|
186
|
+
| `amp_bookmark` | **🆕** Drop a named resume point — auto-captures the current session transcript when no `note` is given |
|
|
130
187
|
| `amp_handoff` | Generate the handoff document for the next AI session |
|
|
131
188
|
| `amp_health` | Session health score (A–F) |
|
|
132
189
|
| `infernoflow_status` | Memory + project health at a glance |
|
|
@@ -134,9 +191,11 @@ When the MCP server is wired, your AI agent can call these directly in chat:
|
|
|
134
191
|
| `infernoflow_context` | Generate AI-ready context for a task |
|
|
135
192
|
| `infernoflow_git_drift` | Detect which capabilities recent commits affected |
|
|
136
193
|
|
|
137
|
-
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
|
|
194
|
+
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 six names. The same six are also available as CLI aliases (`infernoflow amp read | write | search | bookmark | handoff | health`) so the CLI and MCP surfaces match name-for-name.
|
|
195
|
+
|
|
196
|
+
Every memory line injected into the rule files is prefixed with `🔥` so the AI (and you) can tell at a glance that a line came from infernoflow even when it's quoted out of the managed block. When the AI uses one, the protocol tells it to briefly cite the source — e.g. *🔥 (from infernoflow memory) gotcha at src/api.js:42: API returns 202 not 200*.
|
|
138
197
|
|
|
139
|
-
|
|
198
|
+
---
|
|
140
199
|
|
|
141
200
|
## What it has caught (real dogfood)
|
|
142
201
|
|
|
@@ -151,9 +210,17 @@ infernoflow was developed by building a multi-tenant kanban (`infernotest_01`) a
|
|
|
151
210
|
|
|
152
211
|
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.
|
|
153
212
|
|
|
213
|
+
---
|
|
214
|
+
|
|
154
215
|
## VS Code extension
|
|
155
216
|
|
|
156
|
-
The companion extension
|
|
217
|
+
The companion extension is the visual surface over your memory:
|
|
218
|
+
|
|
219
|
+
- **Live sidebar** — ranked-by-relevance gotchas / decisions / attempts for whatever file you're editing.
|
|
220
|
+
- **Gotchas as Problems** — logged with a `file:line`? They appear as yellow squigglies in the editor and rows in the **Problems panel**, right next to your TypeScript errors. Both *you* and *Copilot* see the warning before making the same mistake again.
|
|
221
|
+
- **Status bar health score** — always visible: `🔥 B 65 · ⚠3 · ✓2 · ❌1 · 📋 Switch`. Click `Switch` to copy the handoff.
|
|
222
|
+
- **Copilot Chat integration** — `#amp_write` / `#amp_read` in the chat box; Copilot picks them up automatically via VS Code LMT (see above).
|
|
223
|
+
- **Keyboard-first logging** — `Ctrl+Alt+G` (gotcha) / `Ctrl+Alt+D` (decision) / `Ctrl+Alt+A` (ask) / `Ctrl+Alt+S` (switch) / `Ctrl+Alt+R` (recap). Right-click in the editor to log a gotcha for the current line.
|
|
157
224
|
|
|
158
225
|
```
|
|
159
226
|
ext install infernoflow.infernoflow
|
|
@@ -163,19 +230,43 @@ Or in the Marketplace: [infernoflow.infernoflow](https://marketplace.visualstudi
|
|
|
163
230
|
|
|
164
231
|
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.
|
|
165
232
|
|
|
233
|
+
---
|
|
234
|
+
|
|
166
235
|
## Troubleshooting
|
|
167
236
|
|
|
168
237
|
- **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.
|
|
169
238
|
- **`infernoflow` not found.** Use `npx infernoflow` until the global install resolves on your PATH.
|
|
170
239
|
- **PowerShell script execution blocked.** `Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass`.
|
|
171
|
-
- **Box-drawing chars look broken.** Should auto-fall-back to ASCII on legacy PowerShell. If not, open an issue.
|
|
240
|
+
- **Box-drawing chars look broken.** Force UTF-8 first: `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8`. Should auto-fall-back to ASCII on legacy PowerShell. If not, open an issue.
|
|
172
241
|
- **`infernoflow doctor`** — full diagnostic if anything looks wrong. Includes the MCP runtime stamp check + AI provider detection + git hooks status.
|
|
173
242
|
|
|
243
|
+
---
|
|
244
|
+
|
|
174
245
|
## Why this matters
|
|
175
246
|
|
|
176
247
|
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.
|
|
177
248
|
|
|
178
|
-
That's the whole product. No vendor lock-in (it's JSONL on disk). No SaaS. One CLI, one
|
|
249
|
+
That's the whole product. No vendor lock-in (it's JSONL on disk). No SaaS. One CLI, one VS Code extension, one open protocol, three rule files your IDE was reading anyway.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Security & privacy
|
|
254
|
+
|
|
255
|
+
Local-first by design:
|
|
256
|
+
|
|
257
|
+
- 🚫 **No telemetry.** No analytics, no error reporting, no install pings.
|
|
258
|
+
- 🚫 **No `postinstall` script.** `npm install -g infernoflow` runs no code — it only copies files.
|
|
259
|
+
- 🚫 **No network calls in any default command path.** Everything runs on your machine.
|
|
260
|
+
- 🚫 **No auto-updates, no background processes, no cloud sync.**
|
|
261
|
+
- ✅ **Reads and writes only inside your project directory** (`.ai-memory/`, plus the three rule files at repo root).
|
|
262
|
+
- ✅ **Auto-injected content is wrapped in markers** (`<!-- infernoflow:start -->` / `<!-- infernoflow:end -->`) — your manual edits outside the block are never touched.
|
|
263
|
+
- ✅ **Secret patterns rejected on capture** — entries matching `sk-`, `ghp_`, `-----BEGIN` are refused at the AMP writer.
|
|
264
|
+
|
|
265
|
+
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.
|
|
266
|
+
|
|
267
|
+
Full policy: [SECURITY.md](./SECURITY.md). Vulnerability reports: `hello@infernoflow.dev` or [GitHub Security Advisory](https://github.com/ronmiz/infernoflow/security/advisories/new).
|
|
268
|
+
|
|
269
|
+
---
|
|
179
270
|
|
|
180
271
|
## License
|
|
181
272
|
|
|
@@ -183,6 +274,7 @@ MIT
|
|
|
183
274
|
|
|
184
275
|
## Links
|
|
185
276
|
|
|
186
|
-
- [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)
|
|
277
|
+
- [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) · [infernoflow.dev](https://www.infernoflow.dev)
|
|
187
278
|
- [AMP protocol spec](docs/protocol/PROTOCOL.md) — vendor-neutral memory format
|
|
188
279
|
- [Dogfood: what infernoflow caught while building infernotest_01](docs/dogfood-infernotest_01.md)
|
|
280
|
+
- [Why your AI coding assistant has amnesia](docs/BLOG_POST.md) — the case for the protocol
|
package/dist/bin/infernoflow.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
(function(){if(process.platform!=="win32"||process.env.WT_SESSION||process.env.ConEmuPID||process.env.TERM_PROGRAM==="vscode")return;const
|
|
3
|
-
`)}const
|
|
4
|
-
${
|
|
2
|
+
(function(){if(process.platform!=="win32"||process.env.WT_SESSION||process.env.ConEmuPID||process.env.TERM_PROGRAM==="vscode")return;const n={"\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(n).join("|"),"g"),l=/[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{200D}]\u{FE0F}? ?/gu,f=c=>c.replace(r,p=>n[p]).replace(l,"");function u(c){const p=c.write.bind(c);c.write=function(a,...k){return typeof a=="string"?a=f(a):Buffer.isBuffer(a)&&(a=Buffer.from(f(a.toString("utf8")),"utf8")),p(a,...k)}}u(process.stdout),u(process.stderr)})();import{readFileSync as C}from"node:fs";import{dirname as $,join as y}from"node:path";import{fileURLToPath as v}from"node:url";import{bold as i,gray as e,cyan as t,red as g}from"../lib/ui/output.mjs";const b=$(v(import.meta.url));function A(o){for(const n of[y(o,"..","..","package.json"),y(o,"..","package.json")])try{return JSON.parse(C(n,"utf8"))}catch{}return{version:"0.0.0-source"}}const I=A(b),m=I.version||"0.0.0",h={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)"},d={log:async o=>(await import("../lib/commands/log.mjs")).logCommand(o),ask:async o=>(await import("../lib/commands/ask.mjs")).askCommand(o),switch:async o=>(await import("../lib/commands/switch.mjs")).switchCommand(o),recap:async o=>(await import("../lib/commands/recap.mjs")).recapCommand(o),status:async o=>(await import("../lib/commands/status.mjs")).statusCommand(o),refresh:async o=>(await import("../lib/commands/refresh.mjs")).refreshCommand(o),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),"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 G(){const o=Object.keys(h),n=Math.max(...o.map(r=>r.length),8)+1;return Object.entries(h).map(([r,l])=>` ${r.padEnd(n," ")}${l}`).join(`
|
|
3
|
+
`)}const M={"Memory (the 5-command core)":["log","ask","switch","recap","status","refresh","forget","prune","bookmark"],Setup:["init","setup","doctor","context"],"IDE wiring":["install-cursor-hooks","install-vscode-copilot-hooks","generate-skills"],Configuration:["ai","telemetry","sync","uninstall"],Contract:["check"],"AMP (use: infernoflow amp <verb>)":["status","migrate","validate","version"]};function O(){return Object.entries(M).map(([o,n])=>` ${i(o+":")}
|
|
4
|
+
${n.join(" ")}`).join(`
|
|
5
5
|
|
|
6
6
|
`)}const w=Object.keys(d).length,E=`
|
|
7
7
|
${i("\u{1F525} infernoflow")} ${e("v"+m)}
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
|
|
29
29
|
${e("Run")} ${t("infernoflow commands")} ${e("to see all "+w+" commands grouped.")}
|
|
30
30
|
${e("Run")} ${t("infernoflow <command> --help")} ${e("for command-specific options.")}
|
|
31
|
-
`;import*as R from"node:fs";import*as S from"node:path";try{const o=S.join(process.cwd(),"inferno");if(R.existsSync(o)){const{observeCommandStart:
|
|
31
|
+
`;import*as R from"node:fs";import*as S from"node:path";try{const o=S.join(process.cwd(),"inferno");if(R.existsSync(o)){const{observeCommandStart:n}=await import("../lib/learning/observe.mjs"),r=process.argv[2];r&&!r.startsWith("-")&&n(o,r)}}catch{}const[,,s,...x]=process.argv;(!s||s==="--help"||s==="-h")&&(console.log(E),process.exit(0)),(s==="--version"||s==="-v")&&(console.log(m),process.exit(0)),s==="commands"&&(console.log(`
|
|
32
32
|
${i("\u{1F525} infernoflow")} ${e("v"+m)} ${e("\u2014 all "+w+" commands")}
|
|
33
|
-
`),console.log(
|
|
33
|
+
`),console.log(O()),console.log(`
|
|
34
34
|
${e("Run")} ${t("infernoflow <command> --help")} ${e("for options.")}
|
|
35
|
-
`),process.exit(0));const D=Object.keys(d);D.includes(
|
|
36
|
-
Unknown command: ${
|
|
37
|
-
`)),process.exit(1));const P=[
|
|
35
|
+
`),process.exit(0));const D=Object.keys(d);D.includes(s)||(console.error(g(`
|
|
36
|
+
Unknown command: ${s}`)),console.error(e("Run: infernoflow commands (see all commands)")),console.error(e(`Run: infernoflow --help (quick start)
|
|
37
|
+
`)),process.exit(1));const P=[s,...x];try{const{runUpgradeBackfillIfNeeded:o}=await import("../lib/upgradeCheck.mjs");await o(m,s)}catch{}d[s](P).catch(o=>{console.error(g(`
|
|
38
38
|
Error: `)+o.message),process.exit(1)});
|
package/dist/lib/amp/io.mjs
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import*as
|
|
2
|
-
`).filter(Boolean).map(t=>{try{return JSON.parse(t)}catch{return null}}).filter(Boolean).map(
|
|
3
|
-
`;if(
|
|
4
|
-
`)}catch{continue}const f=[];let
|
|
1
|
+
import*as s from"node:fs";import*as J from"node:os";import*as r from"node:path";import{findProjectRoot as O}from"../projectRoot.mjs";import{getBranchInfo as M}from"../git/branch.mjs";const T="1.0",_=new Set(["gotcha","decision","attempt","note","detection","pattern"]),P=new Set(["copilot","cursor","claude","windsurf","other"]);function L(e){let t=r.basename(r.resolve(e));try{const o=r.join(e,".ai-memory","amp.json");if(s.existsSync(o)){const n=JSON.parse(s.readFileSync(o,"utf8"));n&&typeof n.project=="string"&&n.project.trim()&&(t=n.project.trim())}}catch{}return t.toLowerCase().replace(/[^a-z0-9_.\-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,64)||"unnamed-project"}function W(e,t){let o=process.env.INFERNOFLOW_GLOBAL_DIR;if(!o)try{const l=JSON.parse(s.readFileSync(r.join(t,"amp.json"),"utf8"));l&&typeof l.globalDir=="string"&&l.globalDir.trim()&&(o=l.globalDir.trim())}catch{}if(!o)return r.join(t,"global.jsonl");let n=o;n.startsWith("~")&&(n=r.join(J.homedir(),n.slice(1).replace(/^[\/\\]/,""))),r.isAbsolute(n)||(n=r.resolve(e,n));const c=L(e);return r.join(n,c,"global.jsonl")}function m(e,t={}){const o=t.literal?r.resolve(e):O(e),n=r.join(o,".ai-memory"),c=r.join(o,"inferno"),l=t.forWrite||s.existsSync(n)||!s.existsSync(c),a=l?n:c,i=l,u=r.join(a,"branches"),f=M(o),h=r.join(u,`${f.currentSlug}.jsonl`),g=f.defaultSlug?r.join(u,`${f.defaultSlug}.jsonl`):null,S=Y(o,a);return{root:a,projectRoot:o,isAmp:i,sessions:r.join(a,"sessions.jsonl"),config:r.join(a,i?"amp.json":"config.json"),handoff:r.join(a,i?"handoff.md":"HANDOFF.md"),globalFile:S,branchesDir:u,currentBranchFile:h,defaultBranchFile:g,branch:f}}function Y(e,t){try{return W(e,t)}catch{return r.join(t,"global.jsonl")}}function x(e){const t=O(e),o=r.join(t,".ai-memory");return s.existsSync(o)||s.mkdirSync(o,{recursive:!0}),o}const B="0123456789ABCDEFGHJKMNPQRSTVWXYZ";function E(){let e=Date.now(),t="";for(let n=0;n<10;n++)t=B[e%32]+t,e=Math.floor(e/32);let o="";for(let n=0;n<16;n++)o+=B[Math.floor(Math.random()*32)];return t+o}function N(e){const t={...e.meta||{}};let o=e.type||"note";_.has(o)||(t.subtype=o,o="note"),e.result&&(t.result=e.result),e.detailRef&&(t.detailRef=e.detailRef);let n;const c=e.agent;c&&P.has(c)?n=c:c&&(t.agent=c);const l=typeof e.ts=="number"?e.ts:e.ts?Date.parse(e.ts):Date.now(),a=e.confidence!=null?e.confidence:e.auto?.7:void 0,i={type:o,msg:e.summary||e.msg||"",ts:l,id:e.id||`amp_${E()}`};return e.file&&(i.file=e.file),e.line&&(i.line=e.line),e.function&&(i.function=e.function),e.tags&&e.tags.length&&(i.tags=e.tags),e.source&&(i.source=e.source),n&&(i.tool=n),e.session&&(i.session=e.session),a!=null&&(i.confidence=a),Object.keys(t).length&&(i.meta=t),i}function G(e){if(e.summary&&!e.msg)return e;const t=e.meta||{},o=t.subtype||e.type||"note",n={ts:e.ts,type:o,summary:e.msg||""};e.id&&(n.id=e.id),e.file&&(n.file=e.file),e.line&&(n.line=e.line),e.function&&(n.function=e.function),e.tags&&(n.tags=e.tags),e.source&&(n.source=e.source),e.tool&&(n.agent=e.tool),t.agent&&(n.agent=t.agent),t.result&&(n.result=t.result),t.detailRef&&(n.detailRef=t.detailRef),e.confidence!=null&&(n.confidence=e.confidence,e.confidence<1&&(n.auto=!0));const{subtype:c,agent:l,result:a,detailRef:i,...u}=t;return Object.keys(u).length&&(n.meta=u),n}function U(e){if(!e||!s.existsSync(e))return[];try{return s.readFileSync(e,"utf8").split(`
|
|
2
|
+
`).filter(Boolean).map(t=>{try{return JSON.parse(t)}catch{return null}}).filter(Boolean).map(G)}catch{return[]}}function ne(e){const t=m(e),o=new Set,n=[],c=[t.sessions,t.globalFile,t.defaultBranchFile,t.currentBranchFile],l=[...new Set(c.filter(Boolean))];for(const a of l)for(const i of U(a)){const u=i.id||`${i.ts}|${i.summary}`;o.has(u)||(o.add(u),n.push(i))}return n.sort((a,i)=>{const u=typeof a.ts=="number"?a.ts:Date.parse(a.ts||0),f=typeof i.ts=="number"?i.ts:Date.parse(i.ts||0);return u-f})}function H(e,t){return t.target==="global"?e.globalFile:t.target==="legacy"?e.sessions:t.target==="branch"?e.currentBranchFile:t.type==="preference"?e.globalFile:e.currentBranchFile}function oe(e,t){x(e);const o=m(e,{forWrite:!0}),n=t.id||`amp_${E()}`,c={...t,id:n};if(typeof t.detail=="string"&&t.detail.trim()){const u=`details/${n}.md`,f=r.join(o.root,"details",`${n}.md`);try{s.mkdirSync(r.dirname(f),{recursive:!0}),s.writeFileSync(f,t.detail,"utf8"),c.detailRef=u}catch{}}delete c.detail;const l=H(o,c),a=N(c),i=JSON.stringify(a)+`
|
|
3
|
+
`;if(s.mkdirSync(r.dirname(l),{recursive:!0}),s.appendFileSync(l,i,"utf8"),l!==o.sessions)try{s.mkdirSync(r.dirname(o.sessions),{recursive:!0}),s.appendFileSync(o.sessions,i,"utf8")}catch{}return a}function ie(e,t){const o=m(e);return r.join(o.root,"details",`${t}.md`)}function se(e,t){if(!t)return null;const o=t.detailRef||t.meta&&t.meta.detailRef;if(!o||typeof o!="string")return null;const n=m(e),c=r.isAbsolute(o)?o:r.join(n.root,o);try{return s.readFileSync(c,"utf8")}catch{return null}}function re(e,t){if(!t)return{removed:0,files:[]};const o=m(e),n=[o.sessions,o.globalFile,o.currentBranchFile,o.defaultBranchFile].filter(Boolean);try{if(s.existsSync(o.branchesDir))for(const i of s.readdirSync(o.branchesDir))i.endsWith(".jsonl")&&n.push(r.join(o.branchesDir,i))}catch{}const c=[...new Set(n)];let l=0;const a=[];for(const i of c){if(!s.existsSync(i))continue;let u;try{u=s.readFileSync(i,"utf8").split(`
|
|
4
|
+
`)}catch{continue}const f=[];let h=0;for(const g of u){if(!g.trim())continue;let S;try{S=JSON.parse(g).id}catch{f.push(g);continue}if(S===t){h++,l++;continue}f.push(g)}h>0&&(s.writeFileSync(i,f.length?f.join(`
|
|
5
5
|
`)+`
|
|
6
|
-
`:"","utf8"),a.push(s))}return{removed:l,files:a}}const
|
|
7
|
-
`)}catch{continue}const F=[];let
|
|
6
|
+
`:"","utf8"),a.push(i))}try{const i=r.join(o.root,"details",`${t}.md`);s.existsSync(i)&&(s.unlinkSync(i),a.push(i))}catch{}return{removed:l,files:a}}const V=30,z=["note","attempt","detection"],w=new Set(["gotcha","decision","pattern"]);function q(e){const t=e&&e.config&&e.config.rotation&&typeof e.config.rotation=="object"?e.config.rotation:{};return{archiveAfterDays:Number.isInteger(t.archiveAfterDays)&&t.archiveAfterDays>0?t.archiveAfterDays:V,archivableTypes:Array.isArray(t.archivableTypes)&&t.archivableTypes.length?t.archivableTypes:z,auto:t.auto===!0}}function K(e){const t=[e.sessions,e.currentBranchFile,e.defaultBranchFile].filter(Boolean);try{if(s.existsSync(e.branchesDir))for(const o of s.readdirSync(e.branchesDir))o.endsWith(".jsonl")&&t.push(r.join(e.branchesDir,o))}catch{}return[...new Set(t)]}function Q(e,t){const o=new Date(t),n=o.getUTCFullYear(),c=String(o.getUTCMonth()+1).padStart(2,"0");return r.join(e,"archive",`sessions-${n}-${c}.jsonl`)}function ce(e,t={}){const o=m(e,{forWrite:!t.dryRun}),n=q(X(e)),c=Number.isFinite(t.maxAgeDays)?t.maxAgeDays:n.archiveAfterDays,l=t.archive!==!1,a=new Set((Array.isArray(t.types)&&t.types.length?t.types:n.archivableTypes).filter(p=>!w.has(p))),i=Date.now()-c*24*60*60*1e3,u=K(o),f={scanned:0,archived:0,kept:0,files:[],byType:{},dryRun:!!t.dryRun},h=new Map,g=new Set,S=new Set,R=new Set;for(const p of u){if(!s.existsSync(p))continue;let b;try{b=s.readFileSync(p,"utf8").split(`
|
|
7
|
+
`)}catch{continue}const F=[];let v=0;for(const j of b){if(!j.trim())continue;let y;try{y=JSON.parse(j)}catch{F.push(j);continue}const d=y&&y.id,D=y&&typeof y.type=="string"?y.type:"note",A=typeof y.ts=="number"?y.ts:Date.parse(y.ts||0),I=Number.isFinite(A)&&A<i,$=Array.isArray(y.tags)&&y.tags.includes("bookmark"),C=a.has(D)&&!w.has(D)&&!$;if(d&&!S.has(d)&&(S.add(d),f.scanned++),d||f.scanned++,I&&C){if(v++,(!d||!g.has(d))&&(d&&g.add(d),f.archived++,f.byType[D]=(f.byType[D]||0)+1,l)){const k=Q(o.root,A);h.has(k)||h.set(k,[]),h.get(k).push(j)}}else d&&!R.has(d)&&(R.add(d),f.kept++),d||f.kept++,F.push(j)}v>0&&!t.dryRun?(s.writeFileSync(p,F.length?F.join(`
|
|
8
8
|
`)+`
|
|
9
|
-
`:"","utf8"),f.files.push(
|
|
9
|
+
`:"","utf8"),f.files.push(p)):v>0&&f.files.push(p)}if(!t.dryRun&&l&&h.size>0)for(const[p,b]of h)try{s.mkdirSync(r.dirname(p),{recursive:!0}),s.appendFileSync(p,b.join(`
|
|
10
10
|
`)+`
|
|
11
|
-
`,"utf8")}catch{}return f}function
|
|
12
|
-
`,"utf8"),!0}function
|
|
13
|
-
`,"utf8"),n.config.injection}function
|
|
14
|
-
`).filter(Boolean);let a=0;for(const
|
|
15
|
-
`,"utf8"),a++}catch{}return
|
|
11
|
+
`,"utf8")}catch{}return f}function X(e){const{config:t}=m(e);try{return JSON.parse(s.readFileSync(t,"utf8"))}catch{return null}}function ae(e,t={}){x(e);const{config:o}=m(e,{forWrite:!0});if(s.existsSync(o))return!1;const n={amp:T,project:t.project||r.basename(e),stack:t.stack||{},config:{autoCapture:!0,maxEntries:1e3,rotationStrategy:"archive",inject:["all"],injection:{maxEntries:4,maxCommits:5,maxEntryChars:200},rotation:{archiveAfterDays:30,archivableTypes:["note","attempt","detection"],auto:!1},...t.config||{}}};return s.writeFileSync(o,JSON.stringify(n,null,2)+`
|
|
12
|
+
`,"utf8"),!0}function fe(e,t={}){x(e);const{config:o}=m(e,{forWrite:!0});let n={};try{n=JSON.parse(s.readFileSync(o,"utf8"))}catch{n={}}(!n||typeof n!="object")&&(n={}),n.amp||(n.amp=T),n.project||(n.project=r.basename(e)),(!n.config||typeof n.config!="object")&&(n.config={});const c=n.config.injection&&typeof n.config.injection=="object"?n.config.injection:{};return n.config.injection={...c,...t},s.writeFileSync(o,JSON.stringify(n,null,2)+`
|
|
13
|
+
`,"utf8"),n.config.injection}function le(e){const t=r.join(e,"inferno"),o=r.join(t,"sessions.jsonl");if(!s.existsSync(o))return{migrated:0,reason:"no legacy sessions.jsonl"};const n=r.join(e,".ai-memory"),c=r.join(n,"sessions.jsonl");if(s.existsSync(c))return{migrated:0,reason:".ai-memory/sessions.jsonl already exists"};x(e);const l=s.readFileSync(o,"utf8").split(`
|
|
14
|
+
`).filter(Boolean);let a=0;for(const i of l)try{const u=JSON.parse(i),f=N(u);s.appendFileSync(c,JSON.stringify(f)+`
|
|
15
|
+
`,"utf8"),a++}catch{}return s.writeFileSync(r.join(n,"MIGRATED.md"),`# Migrated from inferno/
|
|
16
16
|
|
|
17
17
|
Copied ${a} entries from inferno/sessions.jsonl on ${new Date().toISOString()}.
|
|
18
18
|
|
|
19
19
|
The original inferno/sessions.jsonl is untouched. You can delete it once you're confident the new layout works.
|
|
20
|
-
`,"utf8"),{migrated:a,reason:"ok"}}export{
|
|
20
|
+
`,"utf8"),{migrated:a,reason:"ok"}}export{T as AMP_VERSION,m as ampPaths,oe as appendEntry,re as deleteEntry,ie as detailPath,x as ensureAmpDir,G as fromAmp,E as generateULID,le as migrateLegacy,L as projectSlug,ce as pruneEntries,X as readConfig,se as readDetail,ne as readEntries,W as resolveGlobalFile,q as resolveRotationSettings,N as toAmp,fe as updateInjectionConfig,ae as writeDefaultConfig};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import*as k from"node:fs";import*as x from"node:path";import{bold as u,cyan as f,gray as t,green as h,yellow as d,red as p}from"../ui/output.mjs";import{readEntries as L,appendEntry as R,deleteEntry as D,readDetail as I}from"../amp/io.mjs";import{harvestSnapshot as B}from"../transcript.mjs";const b="bookmark",F=e=>Array.isArray(e.tags)&&e.tags.includes(b);function y(e){return L(e).filter(F)}function $(e){const n=new Date(e);return isNaN(n.getTime())?"":n.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"})}function W(){return process.env.CURSOR_SESSION?"cursor":process.env.COPILOT_SESSION?"copilot":process.env.CLAUDE_CODE_SESSION?"claude":process.env.WINDSURF_SESSION?"windsurf":process.env.INFERNOFLOW_AGENT?process.env.INFERNOFLOW_AGENT:"human"}const w=(e,n)=>{const r=e.indexOf(n);return r!==-1&&e[r+1]?e[r+1]:null};function N(e,n){if(!n)return{error:"no-token"};const r=e.filter(s=>s.id&&(s.id===n||s.id.startsWith(n)));if(r.length===1)return{bookmark:r[0]};if(r.length>1)return{error:"ambiguous",matches:r};const o=n.toLowerCase(),l=e.filter(s=>(s.msg||s.summary||"").toLowerCase().includes(o));return l.length===0?{error:"none"}:l.length===1?{bookmark:l[0]}:{bookmark:l[l.length-1],ambiguousLabel:l}}function O(){console.log(`
|
|
2
|
+
`+u("\u{1F516} infernoflow bookmark")+t(` \u2014 named session resume points
|
|
3
|
+
`)),console.log(t(" Drop: ")+f('infernoflow bookmark "<label>"')+t(" auto-captures the session transcript as context")),console.log(t(" ")+t('add --note "..." / --detail-file <path|-> for explicit context \xB7 --marker for label only')),console.log(t(" List: ")+f("infernoflow bookmark list")),console.log(t(" Recall: ")+f("infernoflow bookmark show <id|label>")),console.log(t(" Remove: ")+f("infernoflow bookmark rm <id|label>")),console.log()}async function M(e=[]){const n=process.cwd(),r=e[0]==="bookmark"?e.slice(1):e;if(r.length===0){O();return}const o=r[0];return o==="list"||o==="ls"?j(n,r.slice(1)):o==="show"||o==="jump"||o==="recall"?U(n,r.slice(1)):o==="rm"||o==="remove"||o==="delete"?T(n,r.slice(1)):_(n,r)}function _(e,n){const r=w(n,"--note"),o=w(n,"--detail-file"),l=w(n,"--tags"),s=new Set([r,o,l].filter(Boolean)),c=n.filter(m=>!m.startsWith("--")&&!s.has(m)).join(" ").trim();c||(O(),process.exit(1));const a=n.includes("--marker");let i=null,g=null;if(o)try{i=o==="-"?k.readFileSync(0,"utf8"):k.readFileSync(o,"utf8"),g="file"}catch(m){console.error(p(`
|
|
4
|
+
\u2718 Could not read --detail-file ${o}: ${m.message}
|
|
5
|
+
`)),process.exit(1)}else if(r)i=r,g="note";else if(!a)try{i=B(e),i&&(g="transcript")}catch{}const v=x.join(e,".ai-memory"),E=x.join(e,"inferno");!k.existsSync(v)&&!k.existsSync(E)&&(console.error(p(`
|
|
6
|
+
\u2718 no .ai-memory/ or inferno/ \u2014 run: infernoflow init
|
|
7
|
+
`)),process.exit(1));const A=l?l.split(",").map(m=>m.trim()).filter(Boolean):[],C=[b,...A.filter(m=>m!==b)],S=R(e,{ts:new Date().toISOString(),agent:W(),type:"note",summary:c,tags:C,...i&&i.trim()?{detail:i}:{}});console.log(),console.log(" "+h("\u{1F516} Bookmark saved: ")+u(c)+t(` ${S.id}`)),i&&i.trim()?console.log(" "+t(`${g==="transcript"?"auto-captured session transcript":"captured context"} (${i.length} chars) \u2014 recall with `)+f(`infernoflow bookmark show ${S.id.slice(0,12)}`)):console.log(" "+t(`${a?"marker only (--marker)":"marker only \u2014 no session transcript found here"} \u2014 recall by label, or add context with --note / --detail-file`)),console.log()}function j(e,n){const r=y(e);if(n.includes("--json")){console.log(JSON.stringify(r.map(o=>({id:o.id,label:o.msg||o.summary,ts:o.ts,hasContext:!!o.detailRef})),null,2));return}if(console.log(`
|
|
8
|
+
`+u("\u{1F516} infernoflow bookmarks")),console.log(" "+"\u2500".repeat(50)),r.length===0){console.log(t(`
|
|
9
|
+
No bookmarks yet. Drop one: `)+f('infernoflow bookmark "<label>"')+`
|
|
10
|
+
`);return}[...r].reverse().forEach((o,l)=>{const s=o.detailRef?h(" \u25CF"):t(" \u25CB");console.log(` ${t(String(l+1).padStart(3))}${s} ${t($(o.ts))} ${o.msg||o.summary} ${t(o.id.slice(0,12))}`)}),console.log(t(`
|
|
11
|
+
\u25CF has saved context \xB7 \u25CB marker only \u2014 recall with: `)+f("infernoflow bookmark show <id|label>")+`
|
|
12
|
+
`)}function U(e,n){const r=n.includes("--json"),o=n.find(i=>i&&!i.startsWith("-")),l=y(e);l.length===0&&(console.error(d(`
|
|
13
|
+
No bookmarks yet.
|
|
14
|
+
`)),process.exit(1));const s=N(l,o);if(s.error==="no-token"&&(console.error(t(`
|
|
15
|
+
Usage: `)+f("infernoflow bookmark show <id|label>")+`
|
|
16
|
+
`),process.exit(1)),s.error==="none"&&(console.error(p(`
|
|
17
|
+
No bookmark matches: ${o}
|
|
18
|
+
`)),process.exit(1)),s.error==="ambiguous"){console.error(d(`
|
|
19
|
+
Ambiguous \u2014 ${s.matches.length} bookmarks match "${o}":`));for(const i of s.matches.slice(0,8))console.error(t(` ${i.id.slice(0,14)} `)+(i.msg||i.summary||""));console.error(""),process.exit(1)}const c=s.bookmark,a=I(e,c);if(r){console.log(JSON.stringify({id:c.id,label:c.msg||c.summary,ts:c.ts,detail:a||null},null,2));return}console.log(),console.log(" "+u("\u{1F516} "+(c.msg||c.summary))),console.log(" "+t(`${$(c.ts)} \xB7 ${c.id}`)),s.ambiguousLabel&&console.log(" "+t(`(newest of ${s.ambiguousLabel.length} matching "${o}")`)),console.log(" "+"\u2500".repeat(50)),a&&a.trim()?(console.log(),console.log(a.trim()),console.log()):console.log(t(`
|
|
20
|
+
(marker only \u2014 no context was captured for this bookmark)
|
|
21
|
+
`))}function T(e,n){const r=n.find(a=>a&&!a.startsWith("-")),o=y(e);o.length===0&&(console.error(d(`
|
|
22
|
+
No bookmarks to remove.
|
|
23
|
+
`)),process.exit(1));const l=N(o,r);if(l.error){if(l.error==="ambiguous"){console.error(d(`
|
|
24
|
+
Ambiguous \u2014 be specific:`));for(const a of l.matches.slice(0,8))console.error(t(` ${a.id.slice(0,14)} `)+(a.msg||a.summary||""));console.error("")}else l.error==="none"?console.error(p(`
|
|
25
|
+
No bookmark matches: ${r}
|
|
26
|
+
`)):console.error(t(`
|
|
27
|
+
Usage: `)+f("infernoflow bookmark rm <id|label>")+`
|
|
28
|
+
`);process.exit(1)}const s=l.bookmark,{removed:c}=D(e,s.id);console.log(),console.log(c>0?" "+h("\u2714")+" Removed bookmark "+u(s.msg||s.summary)+t(` ${s.id}`):" "+d("Nothing removed.")),console.log()}export{b as BOOKMARK_TAG,M as bookmarkCommand};
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import*as
|
|
2
|
-
`)),process.exit(1)),
|
|
3
|
-
`+
|
|
1
|
+
import*as c from"node:fs";import*as O from"node:path";import"node:os";import{bold as v,cyan as P,gray as t,green as A,red as S}from"../ui/output.mjs";import{ampPaths as U,appendEntry as V,readEntries as q,readConfig as W,resolveRotationSettings as B,pruneEntries as G}from"../amp/io.mjs";import{refreshRuleFilesFromMemory as L}from"../ruleFiles.mjs";function J(){return U(process.cwd())}const j=["gotcha","decision","attempt","note","detection","pattern","preference","theme","handoff","error"],D=["worked","failed","partial","unknown"];function Y(){return q(process.cwd())}function M(n,{auto:l=!1,quiet:e=!1}={}){const s=process.cwd(),p=O.join(s,".ai-memory"),a=O.join(s,"inferno");if(l&&!c.existsSync(p)&&!c.existsSync(a))return!1;!c.existsSync(p)&&!c.existsSync(a)&&(e||console.error(S(` \u2718 no .ai-memory/ or inferno/ \u2014 run: infernoflow init
|
|
2
|
+
`)),process.exit(1)),V(s,n);try{B(W(s)).auto&&G(s)}catch{}try{L(s)}catch{}return!0}function X(){return process.env.CURSOR_SESSION?"cursor":process.env.COPILOT_SESSION?"copilot":process.env.CLAUDE_CODE_SESSION?"claude":process.env.WINDSURF_SESSION?"windsurf":process.env.INFERNOFLOW_AGENT?process.env.INFERNOFLOW_AGENT:"human"}function z(n,l){const e=new Date(n.ts).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}),s=n.type||"note",p=s==="gotcha"?"\x1B[33m":s==="decision"?"\x1B[36m":s==="theme"?"\x1B[35m":s==="preference"?"\x1B[34m":s==="attempt"?"\x1B[90m":s==="error"?"\x1B[31m":"\x1B[0m",a="\x1B[0m",m=n.result?` [${n.result}]`:"",f=n.agent&&n.agent!=="human"?t(` (${n.agent})`):"";return` ${t(String(l+1).padStart(3))} ${t(e)} ${p}${s}${a}${m} ${n.summary}${f}`}async function te(n){const l=o=>n.includes(o),e=(o,r)=>{const g=n.indexOf(o);return g!==-1&&n[g+1]?n[g+1]:r},s=l("--show"),p=l("--clear"),a=l("--json"),m=l("--auto"),f=l("--quiet"),y=e("--source",null);if(s||a){const o=Y(),r=n[n.indexOf("--show")+1],g=r&&/^\d+$/.test(r)?parseInt(r):20,d=o.slice(-g);if(a){console.log(JSON.stringify(d,null,2));return}if(console.log(`
|
|
3
|
+
`+v("\u{1F525} infernoflow \u2014 session memory")),console.log(" "+"\u2500".repeat(50)),!d.length){console.log(t(`
|
|
4
4
|
No entries yet. Start logging with: infernoflow log "<what happened>"
|
|
5
|
-
`));return}console.log(t(` Showing last ${d.length} of ${
|
|
6
|
-
`)),d.forEach((
|
|
7
|
-
`));return}const r=
|
|
8
|
-
`));return}const
|
|
9
|
-
`+
|
|
5
|
+
`));return}console.log(t(` Showing last ${d.length} of ${o.length} entries
|
|
6
|
+
`)),d.forEach((T,C)=>console.log(z(T,o.length-d.length+C))),console.log();return}if(p){const{sessions:o}=J();if(!c.existsSync(o)){console.log(t(` Nothing to clear.
|
|
7
|
+
`));return}const r=o.replace(".jsonl",`-archive-${Date.now()}.jsonl`);c.renameSync(o,r),console.log(A(` \u2714 Session log archived \u2192 ${O.basename(r)}
|
|
8
|
+
`));return}const R=new Set([e("--type",""),e("--result",""),e("--agent",""),e("--source",""),e("--file",""),e("--line",""),e("--tags",""),e("--detail",""),e("--detail-file","")].filter(Boolean)),$=n.slice(1).filter(o=>!o.startsWith("--")&&!R.has(o)).join(" ").trim();if(!$){console.log(`
|
|
9
|
+
`+v("\u{1F525} infernoflow log")+` \u2014 append to session memory
|
|
10
10
|
`),console.log(t(" Usage:")),console.log(t(' infernoflow log "what happened"')),console.log(t(' infernoflow log "tried X, failed because Y" --type attempt --result failed')),console.log(t(' infernoflow log "always use multipart/form-data" --type gotcha')),console.log(t(' infernoflow log "switched to dark mode" --type theme')),console.log(t(" infernoflow log --show Print last 20 entries")),console.log(t(" infernoflow log --json Print as JSON")),console.log(),console.log(t(" Types: note \xB7 attempt \xB7 decision \xB7 gotcha \xB7 preference \xB7 theme \xB7 handoff \xB7 error")),console.log(t(" Results: worked \xB7 failed \xB7 partial \xB7 unknown")),console.log(t(` Auto-capture: --auto (silent skip if no inferno/) \xB7 --quiet \xB7 --source <name>
|
|
11
|
-
`));return}const
|
|
12
|
-
`)),process.exit(1)),
|
|
13
|
-
`)),process.exit(1));const I=
|
|
14
|
-
`)}}}
|
|
11
|
+
`));return}const h=e("--type","note"),u=e("--result",null),_=e("--agent",X()),b=e("--file",null),x=e("--line",null),F=e("--tags",null);j.includes(h)||(f||console.error(S(` \u2718 Invalid type: ${h}. Valid: ${j.join(", ")}
|
|
12
|
+
`)),process.exit(1)),u&&!D.includes(u)&&(f||console.error(S(` \u2718 Invalid result: ${u}. Valid: ${D.join(", ")}
|
|
13
|
+
`)),process.exit(1));const I=x&&/^\d+$/.test(x)?parseInt(x,10):null,E=F?F.split(",").map(o=>o.trim()).filter(Boolean):null,N=e("--detail",null),w=e("--detail-file",null);let i=null;if(w)try{i=w==="-"?c.readFileSync(0,"utf8"):c.readFileSync(w,"utf8")}catch(o){f||console.error(S(` \u2718 Could not read --detail-file ${w}: ${o.message}
|
|
14
|
+
`)),process.exit(1)}else N&&(i=N);const k={ts:new Date().toISOString(),agent:_,type:h,summary:$,...u?{result:u}:{},...y?{source:y}:{},...b?{file:b}:{},...I?{line:I}:{},...E&&E.length?{tags:E}:{},...i&&i.trim()?{detail:i}:{},...m?{auto:!0}:{}};if(M(k,{auto:m,quiet:f})){if(n.includes("--refresh-rules"))try{L(process.cwd())}catch{}if(!f){const o=h!=="note"?P(` [${h}]`):"",r=u?t(` \u2192 ${u}`):"",g=y?t(` (via ${y})`):"",d=i&&i.trim()?t(` (+${i.length}-char detail)`):"";console.log(A(` \u2714 Logged${o}${r}${g}: `)+$+d+`
|
|
15
|
+
`)}}}export{te as logCommand};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import*as
|
|
2
|
-
`)){const s=c.trim();s&&(
|
|
3
|
-
`).filter(Boolean):[]}catch{return[]}}function ft(
|
|
1
|
+
import*as v from"node:fs";import*as m from"node:path";import"node:os";import{execSync as T}from"node:child_process";import{bold as K,cyan as O,gray as rt,green as ct,yellow as X,red as ht}from"../ui/output.mjs";import{ampPaths as dt,readEntries as gt,appendEntry as mt,readDetail as yt}from"../amp/io.mjs";const b="inferno";function R(){return dt(process.cwd())}const wt=m.join(b,"HANDOFF.md"),Ft=m.join(b,"sessions.jsonl"),q=m.join(b,"context-state.json"),Q=m.join(b,"contract.json"),Y=m.join(b,"theme.json"),Z=m.join(b,"adoption_profile.json");function u(o){try{return JSON.parse(v.readFileSync(o,"utf8"))}catch{return null}}function lt(o){try{return v.readFileSync(o,"utf8")}catch{return null}}function U(o){return o?new Date(o).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}):"unknown"}function tt(o){if(o<0)return"unknown";const i=Math.floor(o/36e5),a=Math.floor(o%36e5/6e4);return i>0?`${i}h ${a}m`:`${a}m`}function et(){return gt(process.cwd())}function ot(o,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 E=new Date(i);if(!isNaN(E.getTime()))return E}const n=new Date(Date.now()-864e5),c=[];for(const r of o)if(r.type==="handoff"){const h=new Date(r.ts||0);isNaN(h.getTime())||c.push(h)}if(c.length===0)return n;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>n?r:n}return n}return y>n?y:n}function St(o){try{const i=process.platform;if(i==="win32")T("clip",{input:o});else if(i==="darwin")T("pbcopy",{input:o});else try{T("xclip -selection clipboard",{input:o})}catch{T("xsel --clipboard --input",{input:o})}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 o=u(Z);return o?.ide?o.ide:null}function $t(){try{const o=T("git diff --stat HEAD 2>/dev/null || git diff --cached --stat 2>/dev/null",{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim();return o||T("git log --stat -1 --pretty= 2>/dev/null",{encoding:"utf8",stdio:["pipe","pipe","pipe"]}).trim()||null}catch{return null}}function at(o){try{const i=o&&o.getTime()>0?`--after="${o.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 n={};for(const c of a.split(`
|
|
2
|
+
`)){const s=c.trim();s&&(n[s]=(n[s]||0)+1)}return Object.entries(n).sort((c,s)=>s[1]-c[1]).slice(0,5).map(([c,s])=>({file:c,edits:s}))}catch{return[]}}function st(o){try{const i=o?`--after="${o.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(o){const i=[],a=o.filter(n=>n.type==="attempt"&&(n.result==="failed"||n.result==="partial"||!n.result));for(const n of a)o.find(s=>s.type==="attempt"&&s.result==="worked"&&new Date(s.ts)>new Date(n.ts)&&s.summary.toLowerCase().includes(n.summary.split(" ")[0].toLowerCase()))||i.push({text:n.summary,ts:n.ts,kind:"unresolved-attempt"});for(const n of o)/\b(TODO|WIP|FIXME|BLOCKED|pending)\b/i.test(n.summary)&&(i.find(c=>c.text===n.summary)||i.push({text:n.summary,ts:n.ts,kind:"flagged"}));return i.slice(0,8)}function Dt(o,i,a){const n=u(q)||{},c=u(Q)||{},s=u(Y),y=u(Z),$=et(),r=ot($,i,a),h=$.filter(e=>new Date(e.ts||0)>r),E=$.slice(-5),F=new Date,d=F.toLocaleString("en-GB",{day:"2-digit",month:"short",year:"numeric",hour:"2-digit",minute:"2-digit"}),k=c.policyId||m.basename(process.cwd()),A=c.policyVersion||"?",j=(c.capabilities||[]).slice(0,20),G=nt(),B=r.getTime()>0?F.getTime()-r.getTime():-1,H=tt(B),x=r.getTime()>0?r.getTime().toString(16).slice(-6).toUpperCase():"ALL",N=st(r.getTime()>0?r:null),L=$t(),P=at(r.getTime()>0?r:null),w=h.length>0?h:E,D=w.filter(e=>e.type==="gotcha"),l=w.filter(e=>e.type==="decision"),g=w.filter(e=>e.type==="attempt").filter(e=>e.result==="failed"||e.result==="partial"),W=w.filter(e=>e.type==="preference"),_=w.filter(e=>Array.isArray(e.tags)&&e.tags.includes("bookmark")),V=w.slice(-8),S=ft(w),it=r.getTime()===0?"all time":r.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"}),I=["sessions.jsonl"];(n.working||n.intent)&&I.push("context-state.json"),s&&I.push("theme.json"),c.capabilities?.length&&I.push("contract.json"),y&&I.push("adoption_profile.json"),N.length&&I.push("git log");const J=D.length,z=l.length,pt=g.length;let C=Math.min(J*20,40)+Math.min(z*15,30)+Math.min(pt*15,20);C=Math.min(C,100);const ut=C>=80?"A":C>=60?"B":C>=40?"C":C>=20?"D":"F",t=[`# \u{1F525} infernoflow Handoff \u2014 ${k}`,`> Generated: ${d}${o?` | Handing off to: **${o}**`:""}`,`> Session: **#${x}** \xB7 ${H} \xB7 **${h.length} entries** \xB7 Health: **${ut}** (${C}/100)`,`> Sources: ${I.join(" \xB7 ")}${G?` \xB7 IDE: ${G}`:""}`,"","---",""];if((n.working||n.intent)&&(t.push("## \u{1F3AF} Working on",""),n.working&&t.push(`**${n.working}** _(${U(n.workingUpdated)})_`),n.intent&&t.push(`Intent: ${n.intent} _(${U(n.intentUpdated)})_`),t.push("")),_.length){t.push(`## \u{1F516} Bookmarks \u2014 Resume Points (${_.length})`,""),_.forEach((f,M)=>t.push(`${M+1}. **${f.summary}** _(${U(f.ts)})_`));const e=_[_.length-1],p=e?yt(process.cwd(),e):null;p&&p.trim()&&t.push("",`<details><summary>\u{1F4CE} Context for "${e.summary}"</summary>`,"",p.trim(),"","</details>"),t.push("")}if(D.length&&(t.push(`## \u26A0\uFE0F STOP \u2014 Read These Before Doing Anything (${D.length} gotcha${D.length===1?"":"s"})`,""),D.forEach((e,p)=>{t.push(`${p+1}. **${e.summary}**`);const f=e.file||e.source;if(f&&/[\\/.]/.test(f)){const M=e.line?`${f}:${e.line}`:f;t.push(` \u2192 File: \`${M}\``)}}),t.push("")),l.length&&(t.push("## \u2713 Decisions In Effect \u2014 Follow These",""),l.forEach((e,p)=>{const f=e.result?` \u2192 **${e.result}**`:"";t.push(`${p+1}. ${e.summary}${f}`)}),t.push("")),g.length&&(t.push("## \u274C Already Tried \u2014 Don't Repeat",""),g.forEach((e,p)=>{const f=e.file||e.source,M=f&&/[\\/.]/.test(f)?` (\`${f}\`)`:"";t.push(`${p+1}. ${e.summary}${M} _(${U(e.ts)})_`)}),t.push("")),P.length){t.push("## \u{1F4C1} Hot Files This Session","");for(const{file:e,edits:p}of P)t.push(`- \`${e}\` \u2014 ${p} edit${p!==1?"s":""}`);t.push("")}if(W.length){t.push("## Developer preferences","");for(const e of W)t.push(`- ${e.summary}`);t.push("")}if(N.length||L){if(t.push("## Git activity this session",""),N.length){t.push("**Commits:**");for(const e of N)t.push(`- \`${e}\``);t.push("")}L&&(t.push("**Uncommitted changes:**"),t.push("```"),t.push(L.split(`
|
|
4
4
|
`).slice(0,15).join(`
|
|
5
|
-
`)),
|
|
6
|
-
`)}async function
|
|
7
|
-
`+
|
|
8
|
-
`);const h=m.join(process.cwd(),".ai-memory");if(!
|
|
9
|
-
`)),process.exit(1)),
|
|
10
|
-
`));return}console.log(l);return}const
|
|
11
|
-
`));const
|
|
5
|
+
`)),t.push("```"),t.push(""))}if(s){if(t.push("## Design system",""),s.fonts?.primary&&t.push(`- **Font:** ${s.fonts.primary}${s.fonts.mono?` \xB7 mono: ${s.fonts.mono}`:""}`),s.colors?.mode&&t.push(`- **Mode:** ${s.colors.mode}`),s.colors?.palette){const e=Object.entries(s.colors.palette).map(([p,f])=>`${p}=${f}`).join(" ");t.push(`- **Palette:** ${e}`)}if(s.cssVars&&Object.keys(s.cssVars).length){const e=Object.entries(s.cssVars).slice(0,6).map(([p,f])=>`${p}: ${f}`).join(" | ");t.push(`- **CSS vars:** ${e}`)}s.framework&&t.push(`- **Framework:** ${s.framework}`),t.push("","> \u26A0 Always match these exactly. Do not introduce new colors or fonts.","")}return j.length&&(t.push("## Capability contract",""),t.push(`Project: **${k}** v${A}`),t.push(`Capabilities: ${j.join(", ")}`),t.push("")),t.push("---"),t.push(`_Session #${x} \xB7 ${H} \xB7 Generated by infernoflow._`),t.join(`
|
|
6
|
+
`)}async function jt(o){const i=l=>o.includes(l),a=l=>{const g=o.indexOf(l);return g!==-1&&o[g+1]?o[g+1]:null},n=i("--show")||i("-s"),c=i("--copy")||i("-c"),s=i("--json"),y=i("--all"),$=a("--since"),r=a("--to")||o.find(l=>!l.startsWith("-")&&!["switch"].includes(l))||null;console.log(`
|
|
7
|
+
`+K("\u{1F525} infernoflow \u2014 switch")),console.log(" "+"\u2500".repeat(50)+`
|
|
8
|
+
`);const h=m.join(process.cwd(),".ai-memory");if(!v.existsSync(b)&&!v.existsSync(h)&&(console.error(ht(` \u2718 not initialized \u2014 run: infernoflow init
|
|
9
|
+
`)),process.exit(1)),n){const l=lt(R().handoff)||lt(wt);if(!l){console.log(X(` \u26A0 No handoff yet \u2014 run: infernoflow switch
|
|
10
|
+
`));return}console.log(l);return}const E=Dt(r,$,y);if(s){const l=u(q)||{},g=u(Q)||{},W=u(Y),_=u(Z),V=et(),S=ot(V,$,y),it=V.filter(z=>new Date(z.ts||0)>S),I=st(S.getTime()>0?S:null),J=nt();console.log(JSON.stringify({state:l,contract:{policyId:g.policyId,policyVersion:g.policyVersion,capabilities:g.capabilities},theme:W,adoption:_,sessions:it,commits:I,ide:J,sessionStart:S.toISOString(),sessionId:S.getTime()>0?S.getTime().toString(16).slice(-6).toUpperCase():"ALL",sessionDuration:tt(S.getTime()>0?Date.now()-S.getTime():-1),generatedAt:new Date().toISOString()},null,2));return}v.writeFileSync(R().handoff,E,"utf8"),console.log(ct(" \u2714 Written \u2192 "+m.relative(process.cwd(),R().handoff)+`
|
|
11
|
+
`));const F=et(),d=ot(F,$,y),k=F.filter(l=>new Date(l.ts||0)>d),A=u(q)||{},j=u(Y),G=u(Q)||{},B=st(d.getTime()>0?d:null),H=at(d.getTime()>0?d:null),x=nt(),N=k.length>0?k:F.slice(-5),L=ft(N),P=tt(d.getTime()>0?Date.now()-d.getTime():-1),w=d.getTime()>0?d.getTime().toString(16).slice(-6).toUpperCase():"ALL";console.log(" "+K("Handoff ready")),console.log(" "+"\u2500".repeat(50)),console.log(" "+rt("Session #"+w+" \xB7 "+P)),A.working&&console.log(" Working on "+O(A.working)),A.intent&&console.log(" Intent "+O(A.intent)),console.log(" Memory "+k.length+" entries this session (total: "+F.length+")"),L.length&&console.log(" Open threads "+X(L.length+" unresolved")),B.length&&console.log(" Git commits "+B.length+" this session"),H.length&&console.log(" Hot files "+H.map(l=>O(l.file)).join(", ")),console.log(" Capabilities "+(G.capabilities||[]).length+" registered"),j?.fonts?.primary&&console.log(" Font "+j.fonts.primary),j?.colors?.mode&&console.log(" Color mode "+j.colors.mode),x&&console.log(" IDE "+x),r&&console.log(" Handing off \u2192 "+O(r)),console.log();const D=m.relative(process.cwd(),R().handoff)||".ai-memory/handoff.md";if(c){const l=St(E);console.log(l?ct(" \u2714 Copied to clipboard \u2014 paste at the start of your next AI session"):X(" \u26A0 Clipboard failed \u2014 open "+D+" manually"))}else console.log(" "+K("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(),v.existsSync(R().sessions)){const l={ts:new Date().toISOString(),agent:"infernoflow",type:"handoff",summary:r?`Handed off to ${r}`:"Handoff generated"};mt(process.cwd(),l)}}export{jt as switchCommand};
|
package/dist/lib/projectRoot.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as
|
|
1
|
+
import*as c from"node:fs";import*as t from"node:path";const d=["package.json","Cargo.toml","pyproject.toml","go.mod","Gemfile","composer.json","deno.json","deno.jsonc","build.gradle","build.gradle.kts","pom.xml","mix.exs"],j=[/\.sln$/i,/\.csproj$/i,/\.fsproj$/i,/\.vbproj$/i],u=new Map;function h(o){for(const n of d)if(c.existsSync(t.join(o,n)))return!0;return!1}function y(o){try{for(const n of c.readdirSync(o))for(const i of j)if(i.test(n))return!0}catch{}return!1}function x(o=process.cwd()){const n=t.resolve(o);if(u.has(n))return u.get(n);const i=r=>t.dirname(r)===r;let e=n;for(;;){if(!i(e)&&(c.existsSync(t.join(e,".ai-memory"))||c.existsSync(t.join(e,"inferno"))))return f(n,e);const r=t.dirname(e);if(r===e)break;e=r}for(e=n;;){if(!i(e)&&(c.existsSync(t.join(e,".git"))||h(e)||y(e)))return f(n,e);const r=t.dirname(e);if(r===e)break;e=r}return f(n,n)}function f(o,n){return u.set(o,n),n}function S(){u.clear()}function g(o,n=6){const i=[],e=new Set,r=new Set(["node_modules",".git","dist","build","out","bin","obj",".next",".nuxt",".angular",".svelte-kit","coverage","vendor","target",".venv","venv","__pycache__",".pytest_cache"]);function m(a,l){if(l>n||e.has(a))return;e.add(a);let p;try{p=c.readdirSync(a,{withFileTypes:!0})}catch{return}for(const s of p)s.isDirectory()&&(r.has(s.name)||(s.name===".ai-memory"&&i.push({kind:"amp",path:t.join(a,s.name)}),s.name==="inferno"&&i.push({kind:"legacy",path:t.join(a,s.name)}),m(t.join(a,s.name),l+1)))}return m(t.resolve(o),0),i}export{S as _resetProjectRootCache,g as findAllMemoryDirs,x as findProjectRoot};
|
package/dist/lib/ruleFiles.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import*as l from"node:fs";import*as d from"node:path";import{execSync as
|
|
1
|
+
import*as l from"node:fs";import*as d from"node:path";import{execSync as S}from"node:child_process";import{readEntries as I,readConfig as j}from"./amp/io.mjs";const y="<!-- infernoflow:start -->",h="<!-- infernoflow:end -->",w=[".cursorrules","CLAUDE.md",d.join(".github","copilot-instructions.md")],b=4,A=5,C=200;function _(e){const n=e&&e.config||{},t=n.injection&&typeof n.injection=="object"?n.injection:{},o=Array.isArray(n.inject)&&!n.inject.includes("all")?n.inject:null;return{maxEntries:Number.isInteger(t.maxEntries)&&t.maxEntries>=0?t.maxEntries:b,maxCommits:Number.isInteger(t.maxCommits)&&t.maxCommits>=0?t.maxCommits:A,maxEntryChars:Number.isInteger(t.maxEntryChars)&&t.maxEntryChars>0?t.maxEntryChars:C,targets:Array.isArray(t.targets)&&t.targets.length?t.targets:o||w,includeProtocol:t.includeProtocol!==!1}}function v(e){if(!l.existsSync(e))return{stripped:!1};const n=l.readFileSync(e,"utf8"),t=n.indexOf(y),o=n.indexOf(h);if(t===-1||o===-1||o<=t)return{stripped:!1};const r=n.slice(0,t).replace(/\s+$/,""),s=n.slice(o+h.length).replace(/^\s+/,""),i=r&&s?r+`
|
|
2
2
|
|
|
3
3
|
`+s:r||s;return l.writeFileSync(e,i?i.replace(/\s*$/,"")+`
|
|
4
|
-
`:"","utf8"),{stripped:!0}}function E(){const e=[];return e.push("### Memory protocol \u2014 capture as you go"),e.push(""),e.push("You have the `amp_write` MCP tool. **Use it proactively** (without the user asking) whenever you detect any of these moments. Capture the lesson in one sentence so the next AI session inherits it."),e.push(""),e.push("**If `amp_write` is not in your visible tools**, your client uses deferred MCP loading \u2014 call your tool-discovery mechanism (Claude Code: `ToolSearch` with query `infernoflow`) to load the `amp_*` tools BEFORE proceeding. Don't silently skip the protocol because the tool isn't visible yet."),e.push(""),e.push("| When you see\u2026 | Call `amp_write` with\u2026 |"),e.push("|---|---|"),e.push("| User types `!!`, `!!!`, `not working`, `still broken`, `no change`, `same error`, `retry` / `retrying`, repeated frustration | `{type: 'attempt', msg: '<one-line: what was tried and how it failed>', file: '<active file>'}` |"),e.push("| You generate a plan, numbered steps, or `Plan:` / `Approach:` block | `{type: 'note', msg: 'Plan: <one-line summary>', file: '<focus file>'}` |"),e.push("| User picks X over Y, decides an architectural approach, or you both agree on a direction | `{type: 'decision', msg: '<X> chosen over <Y> because <reason>', file: '<file>'}` |"),e.push("| Discovered non-obvious behavior, API quirk, or `aha \u2014 turns out\u2026` moment | `{type: 'gotcha', msg: '<one-line>', file: '<file>', line: <line if known>}` |"),e.push("| User says `switching` / `back to X`, or ~30 min passed without logging | `{type: 'note', msg: 'Session snapshot \u2014 worked on: <X>. Open threads: <Y>.'}` |"),e.push(""),e.push("**Do NOT log:** typos, syntax errors, obvious bugs visible in the code, anything the next AI can infer by re-reading the file."),e.push("**Do log:** anything that taught you something non-derivable from code."),e.push("**Be concise:** entries should be one sentence. Future-you will skim them."),e.push("**When you use a \u{1F525} memory entry in your reply, briefly cite it** \u2014 e.g. `\u{1F525} (from infernoflow memory) gotcha at src/api.js:42: ...` \u2014 so the user can see which guidance came from project memory vs. your own inference."),e.join(`
|
|
5
|
-
`)}function T(){return[y,"<!-- Auto-managed by infernoflow. Don't edit between these markers. -->","## Project memory (infernoflow)","",E(),"",'_No entries yet. They\'ll appear here as you and your AI tools log them \u2014 run `infernoflow log "..."` or call `amp_write` from any MCP-aware AI._',
|
|
4
|
+
`:"","utf8"),{stripped:!0}}function E(){const e=[];return e.push("### Memory protocol \u2014 capture as you go"),e.push(""),e.push("You have the `amp_write` MCP tool. **Use it proactively** (without the user asking) whenever you detect any of these moments. Capture the lesson in one sentence so the next AI session inherits it."),e.push(""),e.push("**If `amp_write` is not in your visible tools**, your client uses deferred MCP loading \u2014 call your tool-discovery mechanism (Claude Code: `ToolSearch` with query `infernoflow`) to load the `amp_*` tools BEFORE proceeding. Don't silently skip the protocol because the tool isn't visible yet."),e.push(""),e.push("| When you see\u2026 | Call `amp_write` with\u2026 |"),e.push("|---|---|"),e.push("| User types `!!`, `!!!`, `not working`, `still broken`, `no change`, `same error`, `retry` / `retrying`, repeated frustration | `{type: 'attempt', msg: '<one-line: what was tried and how it failed>', file: '<active file>'}` |"),e.push("| You generate a plan, numbered steps, or `Plan:` / `Approach:` block | `{type: 'note', msg: 'Plan: <one-line summary>', file: '<focus file>'}` |"),e.push("| User picks X over Y, decides an architectural approach, or you both agree on a direction | `{type: 'decision', msg: '<X> chosen over <Y> because <reason>', file: '<file>'}` |"),e.push("| Discovered non-obvious behavior, API quirk, or `aha \u2014 turns out\u2026` moment | `{type: 'gotcha', msg: '<one-line>', file: '<file>', line: <line if known>}` |"),e.push("| User says `switching` / `back to X`, or ~30 min passed without logging | `{type: 'note', msg: 'Session snapshot \u2014 worked on: <X>. Open threads: <Y>.'}` |"),e.push(""),e.push("**Do NOT log:** typos, syntax errors, obvious bugs visible in the code, anything the next AI can infer by re-reading the file."),e.push("**Do log:** anything that taught you something non-derivable from code."),e.push("**Be concise:** entries should be one sentence. Future-you will skim them."),e.push("**Bookmark resume points:** when the user says `bookmark this` / `mark this point`, or the context window is filling up while work is mid-flight, call `amp_bookmark` with a short `label` (omit `note` to auto-capture the session transcript) \u2014 so they can jump back to that exact point in the next session."),e.push("**When you use a \u{1F525} memory entry in your reply, briefly cite it** \u2014 e.g. `\u{1F525} (from infernoflow memory) gotcha at src/api.js:42: ...` \u2014 so the user can see which guidance came from project memory vs. your own inference."),e.join(`
|
|
5
|
+
`)}function T(){return[y,"<!-- Auto-managed by infernoflow. Don't edit between these markers. -->","## Project memory (infernoflow)","",E(),"",'_No entries yet. They\'ll appear here as you and your AI tools log them \u2014 run `infernoflow log "..."` or call `amp_write` from any MCP-aware AI._',h].join(`
|
|
6
6
|
`)}function F(e){const n=e.indexOf("<!-- AMP:START -->"),t=e.indexOf("<!-- AMP:END -->");if(n===-1||t===-1||t<=n)return e;const o=e.slice(0,n).replace(/\s+$/,""),r=e.slice(t+16).replace(/^\s+/,"");return(o?o+(r?`
|
|
7
7
|
|
|
8
|
-
`:""):"")+r}function
|
|
9
|
-
`,"utf8"),{created:!0,updated:!1};let o=l.readFileSync(e,"utf8");o=F(o);const r=o.indexOf(y),s=o.indexOf(
|
|
8
|
+
`:""):"")+r}function k(e,n){const t=d.dirname(e);if(l.existsSync(t)||l.mkdirSync(t,{recursive:!0}),!l.existsSync(e))return l.writeFileSync(e,n+`
|
|
9
|
+
`,"utf8"),{created:!0,updated:!1};let o=l.readFileSync(e,"utf8");o=F(o);const r=o.indexOf(y),s=o.indexOf(h);if(r===-1||s===-1){const g=n+`
|
|
10
10
|
|
|
11
|
-
`+o;return l.writeFileSync(e,g,"utf8"),{created:!1,updated:!0}}const i=o.slice(0,r),p=o.slice(s+
|
|
12
|
-
`).filter(Boolean).map(o=>{const[r,s,i]=o.split(" ");return{hash:(r||"").slice(0,7),date:s||"",subject:i||""}})}catch{return[]}}function R(e,n={}){const{maxEntries:t=b,maxCommits:o=A,maxEntryChars:r=C,includeProtocol:s=!0}=n,i=D(e),p=M(e,o);i.sort((
|
|
13
|
-
`)}function P(e){const n=
|
|
11
|
+
`+o;return l.writeFileSync(e,g,"utf8"),{created:!1,updated:!0}}const i=o.slice(0,r),p=o.slice(s+h.length),u=i+n+p;return u===o?{created:!1,updated:!1}:(l.writeFileSync(e,u,"utf8"),{created:!1,updated:!0})}function $(e){const n=T(),t=[];for(const o of w){const r=d.join(e,o);try{const s=k(r,n);t.push({rel:o,...s})}catch(s){t.push({rel:o,error:s.message})}}return t}function D(e){return I(e)}function M(e,n=10){try{return S(`git log --pretty=format:"%h%x09%ad%x09%s" --date=short -n ${n}`,{cwd:e,encoding:"utf8",stdio:["ignore","pipe","ignore"]}).split(`
|
|
12
|
+
`).filter(Boolean).map(o=>{const[r,s,i]=o.split(" ");return{hash:(r||"").slice(0,7),date:s||"",subject:i||""}})}catch{return[]}}function R(e,n={}){const{maxEntries:t=b,maxCommits:o=A,maxEntryChars:r=C,includeProtocol:s=!0}=n,i=D(e),p=M(e,o);i.sort((a,m)=>{const f=typeof a.ts=="number"?a.ts:Date.parse(a.ts||0);return(typeof m.ts=="number"?m.ts:Date.parse(m.ts||0))-f});const u=i.slice(0,t),g={gotcha:"\u26A0",decision:"\u2713",attempt:"\u2717",note:"\xB7",detection:"\u25CB",pattern:"\u25C7"},c=[];if(c.push(y),c.push("<!-- Auto-managed by infernoflow. Don't edit between these markers. -->"),c.push("## Project memory (infernoflow)"),c.push(""),s&&(c.push(E()),c.push("")),p.length>0){c.push("### Recent commits");for(const a of p)c.push(`- \`${a.hash}\` _${a.date}_ ${a.subject}`);c.push("")}if(u.length>0){c.push("### Recent memory");for(const a of u){const m=a.file?` (\`${a.file}${a.line?":"+a.line:""}\`)`:"",f=(a.msg||a.summary||"").replace(/\n/g," "),x=r&&f.length>r?f.slice(0,r).trimEnd()+"\u2026":f;c.push(`- \u{1F525} ${g[a.type]||"\xB7"} **${a.type||"note"}**${m}: ${x}`)}c.push("")}return i.length===0&&p.length===0&&c.push('_No entries yet. They\'ll appear here as you and your AI tools log them \u2014 run `infernoflow log "..."` or call `amp_write` from any MCP-aware AI._'),c.push(h),c.join(`
|
|
13
|
+
`)}function P(e){const n=_(j(e)),t=R(e,n),o=i=>String(i).replace(/\\/g,"/"),r=new Set(n.targets.map(o)),s=[];for(const i of w){const p=d.join(e,i);try{if(r.has(o(i))){const u=k(p,t);s.push({rel:i,...u})}else v(p).stripped&&s.push({rel:i,stripped:!0})}catch(u){s.push({rel:i,error:u.message})}}return s}export{P as refreshRuleFilesFromMemory,_ as resolveInjectionSettings,$ as writeInitRuleFiles};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import*as f from"node:fs";import*as g from"node:os";import*as p from"node:path";function x(e){const t=p.resolve(e).replace(/[^a-zA-Z0-9]/g,"-");return p.join(g.homedir(),".claude","projects",t)}function d(e){const t=x(e);let l;try{l=f.readdirSync(t)}catch{return null}const i=l.filter(n=>n.endsWith(".jsonl")).map(n=>{const s=p.join(t,n);try{return{p:s,mtime:f.statSync(s).mtimeMs}}catch{return null}}).filter(Boolean).sort((n,s)=>s.mtime-n.mtime);return i.length?i[0].p:null}function y(e){return e?typeof e=="string"?e:Array.isArray(e)?e.map(t=>typeof t=="string"?t:t&&t.type==="text"?t.text:"").filter(Boolean).join(`
|
|
2
|
+
`):"":""}function j(e,t={}){const l=Number.isInteger(t.maxMessages)?t.maxMessages:40,i=Number.isInteger(t.maxCharsPerMsg)?t.maxCharsPerMsg:1500,n=d(e);if(!n)return null;let s;try{s=f.readFileSync(n,"utf8").split(`
|
|
3
|
+
`).filter(Boolean)}catch{return null}const c=[];for(const o of s){let r;try{r=JSON.parse(o)}catch{continue}const a=r.type||r.message&&r.message.role;if(a!=="user"&&a!=="assistant")continue;const m=y(r.message?r.message.content:r.content);m&&m.trim()&&c.push({role:a,text:m.trim()})}if(c.length===0)return null;const u=c.slice(-l),h=["# \u{1F516} Session snapshot","",`_Auto-captured ${u.length} recent turn${u.length===1?"":"s"} from the Claude Code transcript._`,""];for(const o of u){const r=o.role==="user"?"You":"AI",a=o.text.length>i?o.text.slice(0,i).trimEnd()+" \u2026":o.text;h.push(`**${r}:** ${a}`,"")}return h.join(`
|
|
4
|
+
`)}export{x as claudeProjectDir,d as findLatestTranscript,j as harvestSnapshot};
|
|
@@ -151,6 +151,27 @@ const TRIGGER_RES = [
|
|
|
151
151
|
/\bdoesn['’]?t work\b/i,
|
|
152
152
|
];
|
|
153
153
|
|
|
154
|
+
// Deterministic BOOKMARK triggers — an explicit "bookmark this" is an intentional
|
|
155
|
+
// resume point (not a trouble signal), so it takes precedence and drops a real
|
|
156
|
+
// bookmark (which auto-captures the session transcript). No AI cooperation needed.
|
|
157
|
+
const BOOKMARK_RES = [
|
|
158
|
+
/\bbookmark (?:this|it|here)(?: point)?\b/i,
|
|
159
|
+
/\bmark this (?:point|spot|moment|here)\b/i,
|
|
160
|
+
/\bsave (?:this )?(?:point|checkpoint|resume point)\b/i,
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
/** Strip the trigger phrase to reuse the rest of the prompt as the bookmark label. */
|
|
164
|
+
function deriveBookmarkLabel(prompt) {
|
|
165
|
+
const label = prompt
|
|
166
|
+
.replace(/\bbookmark (?:this|it|here)(?: point)?\b/ig, "")
|
|
167
|
+
.replace(/\bmark this (?:point|spot|moment|here)\b/ig, "")
|
|
168
|
+
.replace(/\bsave (?:this )?(?:point|checkpoint|resume point)\b/ig, "")
|
|
169
|
+
.replace(/[\s:.!,–—-]+/g, " ")
|
|
170
|
+
.replace(/\s+/g, " ")
|
|
171
|
+
.trim();
|
|
172
|
+
return (label.slice(0, 80).trim()) || "Session bookmark";
|
|
173
|
+
}
|
|
174
|
+
|
|
154
175
|
function memoryRootExists() {
|
|
155
176
|
return fs.existsSync(path.join(projectRoot(), ".ai-memory")) ||
|
|
156
177
|
fs.existsSync(path.join(projectRoot(), "inferno"));
|
|
@@ -166,10 +187,41 @@ function cheapHash(s) {
|
|
|
166
187
|
return String(h);
|
|
167
188
|
}
|
|
168
189
|
|
|
190
|
+
function handleBookmarkTrigger(prompt) {
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
const stateFile = triggerStatePath();
|
|
193
|
+
let state = {};
|
|
194
|
+
try { state = JSON.parse(fs.readFileSync(stateFile, "utf8")); } catch {}
|
|
195
|
+
const h = cheapHash(prompt.slice(0, 200));
|
|
196
|
+
if (state.lastBmHash === h) return; // same prompt — don't double-fire
|
|
197
|
+
|
|
198
|
+
const label = deriveBookmarkLabel(prompt);
|
|
199
|
+
let wrote = false;
|
|
200
|
+
try {
|
|
201
|
+
// The `bookmark` command (no --marker) auto-captures the session transcript
|
|
202
|
+
// as the resume point. Needs infernoflow >= 0.44.10 on PATH; if the global
|
|
203
|
+
// CLI is older / missing, this no-ops and the AI's amp_bookmark path covers it.
|
|
204
|
+
const bin = process.platform === "win32" ? "infernoflow.cmd" : "infernoflow";
|
|
205
|
+
const r = spawnSync(bin, ["bookmark", label], {
|
|
206
|
+
cwd: projectRoot(), encoding: "utf8", timeout: 12000, shell: process.platform === "win32",
|
|
207
|
+
});
|
|
208
|
+
wrote = r.status === 0;
|
|
209
|
+
} catch { /* CLI unavailable — skip */ }
|
|
210
|
+
|
|
211
|
+
if (wrote) {
|
|
212
|
+
try { fs.writeFileSync(stateFile, JSON.stringify({ ...state, lastBmHash: h }), "utf8"); } catch {}
|
|
213
|
+
process.stderr.write("[inferno-session-draft] auto-bookmarked resume point\n");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
169
217
|
function handleUserPrompt(text) {
|
|
170
218
|
const trimmed = (text || "").trim();
|
|
171
219
|
if (!trimmed) return;
|
|
172
220
|
if (!memoryRootExists()) return; // only inside infernoflow projects
|
|
221
|
+
|
|
222
|
+
// Bookmark trigger takes precedence — "bookmark this" is intentional, not trouble.
|
|
223
|
+
if (BOOKMARK_RES.some((re) => re.test(trimmed))) { handleBookmarkTrigger(trimmed); return; }
|
|
224
|
+
|
|
173
225
|
if (!TRIGGER_RES.some((re) => re.test(trimmed))) return;
|
|
174
226
|
|
|
175
227
|
const now = Date.now();
|
|
@@ -205,7 +257,7 @@ function handleUserPrompt(text) {
|
|
|
205
257
|
}
|
|
206
258
|
|
|
207
259
|
if (wrote) {
|
|
208
|
-
try { fs.writeFileSync(stateFile, JSON.stringify({ lastTs: now, lastHash: h }), "utf8"); } catch {}
|
|
260
|
+
try { fs.writeFileSync(stateFile, JSON.stringify({ ...state, lastTs: now, lastHash: h }), "utf8"); } catch {}
|
|
209
261
|
process.stderr.write("[inferno-session-draft] auto-captured trigger to memory\n");
|
|
210
262
|
}
|
|
211
263
|
}
|
|
@@ -113,6 +113,7 @@ const INFERNOFLOW_BIN = resolveInfernoflowBin();
|
|
|
113
113
|
// Falls back to shell-out via runCmd() if the AMP layer can't be loaded.
|
|
114
114
|
let ampIo = null;
|
|
115
115
|
let refreshRuleFiles = null;
|
|
116
|
+
let harvestSnapshot = null;
|
|
116
117
|
if (INFERNOFLOW_ROOT) {
|
|
117
118
|
try {
|
|
118
119
|
for (const c of [
|
|
@@ -127,6 +128,12 @@ if (INFERNOFLOW_ROOT) {
|
|
|
127
128
|
]) {
|
|
128
129
|
if (fs.existsSync(c)) { refreshRuleFiles = (await import(pathToFileURL(c).href)).refreshRuleFilesFromMemory; break; }
|
|
129
130
|
}
|
|
131
|
+
for (const c of [
|
|
132
|
+
path.join(INFERNOFLOW_ROOT, "lib", "transcript.mjs"),
|
|
133
|
+
path.join(INFERNOFLOW_ROOT, "dist", "lib", "transcript.mjs"),
|
|
134
|
+
]) {
|
|
135
|
+
if (fs.existsSync(c)) { harvestSnapshot = (await import(pathToFileURL(c).href)).harvestSnapshot; break; }
|
|
136
|
+
}
|
|
130
137
|
} catch { /* swallow — fallback path handles it */ }
|
|
131
138
|
}
|
|
132
139
|
|
|
@@ -224,8 +231,9 @@ function isCmdError(result) {
|
|
|
224
231
|
const TOOLS = [
|
|
225
232
|
// ── AMP-spec memory tools (the product) ──────────────────────────────────
|
|
226
233
|
{ name: "amp_read", description: "AMP: read session memory entries with optional filters.", inputSchema: { type: "object", properties: { file: { type: "string" }, type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] }, query: { type: "string" }, limit: { type: "number" } } } },
|
|
227
|
-
{ name: "amp_write", description: "AMP: log a new entry. Required: type + msg. Optional: file, line, tags.", inputSchema: { type: "object", properties: { type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] }, msg: { type: "string" }, file: { type: "string" }, line: { type: "number" }, tags: { type: "array", items: { type: "string" } } }, required: ["type","msg"] } },
|
|
234
|
+
{ name: "amp_write", description: "AMP: log a new entry. Required: type + msg (one sentence). Optional: file, line, tags, detail. Use 'detail' for a rich multi-paragraph body (repro steps, code, full reasoning, or a session snapshot) — it's stored in a sidecar and loaded on demand, so it never bloats the always-on memory index.", inputSchema: { type: "object", properties: { type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] }, msg: { type: "string" }, file: { type: "string" }, line: { type: "number" }, tags: { type: "array", items: { type: "string" } }, detail: { type: "string", description: "Optional rich body (Tier-2). Stored in details/<id>.md; NOT injected into rule files. Put the long-form context here; keep 'msg' to one summary sentence." } }, required: ["type","msg"] } },
|
|
228
235
|
{ name: "amp_search", description: "AMP: search entries by keyword. Optional type filter.", inputSchema: { type: "object", properties: { query: { type: "string" }, type: { type: "string", enum: ["gotcha","decision","attempt","note","detection","pattern"] } }, required: ["query"] } },
|
|
236
|
+
{ name: "amp_bookmark", description: "AMP: drop a named session bookmark — a resume point. Required: label (short name). Optional: note. If note is OMITTED, the current session transcript is auto-captured as the bookmark's context (the 'save everything here' resume point). Use when the user says 'bookmark this' / 'mark this point', or before a risky change / when the context window is filling up, so the exact state can be recalled later and appears in the next session's handoff. Bookmarks are never auto-pruned.", inputSchema: { type: "object", properties: { label: { type: "string" }, note: { type: "string", description: "Optional explicit context. Omit to auto-capture the session transcript instead. Stored in a sidecar; not injected into rule files." } }, required: ["label"] } },
|
|
229
237
|
{ name: "amp_handoff", description: "AMP: generate the handoff document for the next AI session. format=markdown|json (default: markdown).", inputSchema: { type: "object", properties: { format: { type: "string", enum: ["markdown","json"] } } } },
|
|
230
238
|
{ name: "amp_health", description: "AMP: get the session health score (0-100, A-F grade).", inputSchema: { type: "object", properties: {} } },
|
|
231
239
|
|
|
@@ -392,6 +400,7 @@ function handleTool(id, name, input) {
|
|
|
392
400
|
if (input.file) entry.file = input.file;
|
|
393
401
|
if (input.line) entry.line = input.line;
|
|
394
402
|
if (input.tags && input.tags.length) entry.tags = input.tags;
|
|
403
|
+
if (input.detail && String(input.detail).trim()) entry.detail = String(input.detail);
|
|
395
404
|
try {
|
|
396
405
|
const written = ampIo.appendEntry(process.cwd(), entry);
|
|
397
406
|
// NOTE: rule-file refresh deliberately NOT called here — clean-tree
|
|
@@ -401,7 +410,8 @@ function handleTool(id, name, input) {
|
|
|
401
410
|
// are for cold-start injection of the *next* session.
|
|
402
411
|
text = `✔ Logged [${written.type}] ${written.id}\n msg: ${written.msg}` +
|
|
403
412
|
(written.file ? `\n file: ${written.file}${written.line ? ":" + written.line : ""}` : "") +
|
|
404
|
-
(written.tags ? `\n tags: ${written.tags.join(", ")}` : "")
|
|
413
|
+
(written.tags ? `\n tags: ${written.tags.join(", ")}` : "") +
|
|
414
|
+
(written.meta && written.meta.detailRef ? `\n detail: ${written.meta.detailRef}` : "");
|
|
405
415
|
} catch (err) {
|
|
406
416
|
return sendError(id, -32000, `amp_write failed (in-process): ${err.message}`);
|
|
407
417
|
}
|
|
@@ -416,6 +426,40 @@ function handleTool(id, name, input) {
|
|
|
416
426
|
if (input.tags && input.tags.length) extras.push("--tags", JSON.stringify(input.tags.join(",")));
|
|
417
427
|
text = runCmd(`log ${m} --type ${t} ${extras.join(" ")}`);
|
|
418
428
|
}
|
|
429
|
+
} else if (name === "amp_bookmark") {
|
|
430
|
+
// A bookmark is a `note` entry tagged "bookmark"; the optional `note`
|
|
431
|
+
// becomes its Tier-2 detail (a resume point the next session can recall).
|
|
432
|
+
if (ampIo) {
|
|
433
|
+
const entry = {
|
|
434
|
+
ts: new Date().toISOString(),
|
|
435
|
+
type: "note",
|
|
436
|
+
summary: input.label || "",
|
|
437
|
+
agent: process.env.INFERNOFLOW_AGENT
|
|
438
|
+
|| (process.env.CLAUDE_CODE_SESSION ? "claude"
|
|
439
|
+
: process.env.CURSOR_SESSION ? "cursor"
|
|
440
|
+
: process.env.COPILOT_SESSION ? "copilot"
|
|
441
|
+
: "claude"),
|
|
442
|
+
tags: ["bookmark"],
|
|
443
|
+
};
|
|
444
|
+
// Context: explicit note wins; otherwise auto-capture the session
|
|
445
|
+
// transcript (the "save everything here" resume point).
|
|
446
|
+
if (input.note && String(input.note).trim()) {
|
|
447
|
+
entry.detail = String(input.note);
|
|
448
|
+
} else if (harvestSnapshot) {
|
|
449
|
+
try { const snap = harvestSnapshot(process.cwd()); if (snap) entry.detail = snap; } catch { /* best-effort */ }
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const written = ampIo.appendEntry(process.cwd(), entry);
|
|
453
|
+
text = `🔖 Bookmark saved: ${written.msg} (${written.id})` +
|
|
454
|
+
(written.meta && written.meta.detailRef ? `\n context: ${written.meta.detailRef}` : "");
|
|
455
|
+
} catch (err) {
|
|
456
|
+
return sendError(id, -32000, `amp_bookmark failed (in-process): ${err.message}`);
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
const l = JSON.stringify(input.label || "");
|
|
460
|
+
const extras = input.note ? `--note ${JSON.stringify(input.note)}` : "";
|
|
461
|
+
text = runCmd(`bookmark ${l} ${extras}`);
|
|
462
|
+
}
|
|
419
463
|
} else if (name === "amp_handoff") {
|
|
420
464
|
// switch writes a file; we read it back to return the content
|
|
421
465
|
const switchResult = runCmd("switch");
|
package/package.json
CHANGED