infernoflow 0.44.13 → 0.44.14

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.
@@ -113,7 +113,6 @@ 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;
117
116
  if (INFERNOFLOW_ROOT) {
118
117
  try {
119
118
  for (const c of [
@@ -128,12 +127,6 @@ if (INFERNOFLOW_ROOT) {
128
127
  ]) {
129
128
  if (fs.existsSync(c)) { refreshRuleFiles = (await import(pathToFileURL(c).href)).refreshRuleFilesFromMemory; break; }
130
129
  }
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
- }
137
130
  } catch { /* swallow — fallback path handles it */ }
138
131
  }
139
132
 
@@ -231,9 +224,8 @@ function isCmdError(result) {
231
224
  const TOOLS = [
232
225
  // ── AMP-spec memory tools (the product) ──────────────────────────────────
233
226
  { 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" } } } },
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"] } },
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"] } },
235
228
  { 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"] } },
237
229
  { 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"] } } } },
238
230
  { name: "amp_health", description: "AMP: get the session health score (0-100, A-F grade).", inputSchema: { type: "object", properties: {} } },
239
231
 
@@ -400,7 +392,6 @@ function handleTool(id, name, input) {
400
392
  if (input.file) entry.file = input.file;
401
393
  if (input.line) entry.line = input.line;
402
394
  if (input.tags && input.tags.length) entry.tags = input.tags;
403
- if (input.detail && String(input.detail).trim()) entry.detail = String(input.detail);
404
395
  try {
405
396
  const written = ampIo.appendEntry(process.cwd(), entry);
406
397
  // NOTE: rule-file refresh deliberately NOT called here — clean-tree
@@ -410,8 +401,7 @@ function handleTool(id, name, input) {
410
401
  // are for cold-start injection of the *next* session.
411
402
  text = `✔ Logged [${written.type}] ${written.id}\n msg: ${written.msg}` +
412
403
  (written.file ? `\n file: ${written.file}${written.line ? ":" + written.line : ""}` : "") +
413
- (written.tags ? `\n tags: ${written.tags.join(", ")}` : "") +
414
- (written.meta && written.meta.detailRef ? `\n detail: ${written.meta.detailRef}` : "");
404
+ (written.tags ? `\n tags: ${written.tags.join(", ")}` : "");
415
405
  } catch (err) {
416
406
  return sendError(id, -32000, `amp_write failed (in-process): ${err.message}`);
417
407
  }
@@ -426,40 +416,6 @@ function handleTool(id, name, input) {
426
416
  if (input.tags && input.tags.length) extras.push("--tags", JSON.stringify(input.tags.join(",")));
427
417
  text = runCmd(`log ${m} --type ${t} ${extras.join(" ")}`);
428
418
  }
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
- }
463
419
  } else if (name === "amp_handoff") {
464
420
  // switch writes a file; we read it back to return the content
465
421
  const switchResult = runCmd("switch");
package/package.json CHANGED
@@ -1,69 +1,69 @@
1
- {
2
- "name": "infernoflow",
3
- "version": "0.44.13",
4
- "description": "Persistent memory for AI coding sessions — captures what agents can't infer from code alone. Works with Copilot, Cursor, Claude, and Windsurf.",
5
- "type": "module",
6
- "bin": {
7
- "infernoflow": "dist/bin/infernoflow.mjs"
8
- },
9
- "engines": {
10
- "node": ">=18"
11
- },
12
- "files": [
13
- "dist/bin",
14
- "dist/lib",
15
- "dist/templates",
16
- "README.md"
17
- ],
18
- "scripts": {
19
- "test": "tsc --noEmit && vitest run",
20
- "test:watch": "vitest",
21
- "test:vitest": "vitest run",
22
- "test:smoke": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs",
23
- "test:install": "cross-env RUN_INSTALL_TESTS=1 vitest run tests/tarball-install.test.mjs",
24
- "test:help": "node bin/infernoflow.mjs --help",
25
- "typecheck": "tsc --noEmit",
26
- "build": "node build.mjs",
27
- "prepublishOnly": "echo skipping build",
28
- "inferno:promote-draft": "node scripts/inferno-promote-draft.mjs"
29
- },
30
- "keywords": [
31
- "ai",
32
- "ai-memory",
33
- "ai-context",
34
- "ai-coding",
35
- "session-memory",
36
- "persistent-memory",
37
- "agent-memory",
38
- "agent-handoff",
39
- "copilot",
40
- "cursor",
41
- "claude",
42
- "windsurf",
43
- "mcp",
44
- "mcp-server",
45
- "context-switching",
46
- "developer-tools",
47
- "cli",
48
- "infernoflow",
49
- "gotchas",
50
- "coding-assistant"
51
- ],
52
- "author": "infernoflow",
53
- "license": "MIT",
54
- "repository": {
55
- "type": "git",
56
- "url": "git+https://github.com/ronmiz/infernoflow.git"
57
- },
58
- "homepage": "https://infernoflow.dev",
59
- "bugs": {
60
- "url": "https://github.com/ronmiz/infernoflow/issues"
61
- },
62
- "devDependencies": {
63
- "@types/node": "^25.9.0",
64
- "cross-env": "^10.1.0",
65
- "esbuild": "^0.28.0",
66
- "typescript": "^6.0.3",
67
- "vitest": "^4.1.6"
68
- }
69
- }
1
+ {
2
+ "name": "infernoflow",
3
+ "version": "0.44.14",
4
+ "description": "Persistent memory for AI coding sessions — captures what agents can't infer from code alone. Works with Copilot, Cursor, Claude, and Windsurf.",
5
+ "type": "module",
6
+ "bin": {
7
+ "infernoflow": "dist/bin/infernoflow.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "files": [
13
+ "dist/bin",
14
+ "dist/lib",
15
+ "dist/templates",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "test": "tsc --noEmit && vitest run",
20
+ "test:watch": "vitest",
21
+ "test:vitest": "vitest run",
22
+ "test:smoke": "node scripts/smoke.mjs && node scripts/json-smoke.mjs && node scripts/json-negative-smoke.mjs",
23
+ "test:install": "cross-env RUN_INSTALL_TESTS=1 vitest run tests/tarball-install.test.mjs",
24
+ "test:help": "node bin/infernoflow.mjs --help",
25
+ "typecheck": "tsc --noEmit",
26
+ "build": "node build.mjs",
27
+ "prepublishOnly": "echo skipping build",
28
+ "inferno:promote-draft": "node scripts/inferno-promote-draft.mjs"
29
+ },
30
+ "keywords": [
31
+ "ai",
32
+ "ai-memory",
33
+ "ai-context",
34
+ "ai-coding",
35
+ "session-memory",
36
+ "persistent-memory",
37
+ "agent-memory",
38
+ "agent-handoff",
39
+ "copilot",
40
+ "cursor",
41
+ "claude",
42
+ "windsurf",
43
+ "mcp",
44
+ "mcp-server",
45
+ "context-switching",
46
+ "developer-tools",
47
+ "cli",
48
+ "infernoflow",
49
+ "gotchas",
50
+ "coding-assistant"
51
+ ],
52
+ "author": "infernoflow",
53
+ "license": "MIT",
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/ronmiz/infernoflow.git"
57
+ },
58
+ "homepage": "https://infernoflow.dev",
59
+ "bugs": {
60
+ "url": "https://github.com/ronmiz/infernoflow/issues"
61
+ },
62
+ "devDependencies": {
63
+ "@types/node": "^25.9.0",
64
+ "cross-env": "^10.1.0",
65
+ "esbuild": "^0.28.0",
66
+ "typescript": "^6.0.3",
67
+ "vitest": "^4.1.6"
68
+ }
69
+ }
@@ -1,28 +0,0 @@
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,4 +0,0 @@
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};