infernoflow 0.37.1 → 0.37.4

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.
Files changed (88) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/bin/infernoflow.mjs +29 -277
  3. package/dist/lib/adopters/angular.mjs +1 -128
  4. package/dist/lib/adopters/css.mjs +1 -111
  5. package/dist/lib/adopters/react.mjs +1 -104
  6. package/dist/lib/ai/ideDetection.mjs +1 -31
  7. package/dist/lib/ai/localProvider.mjs +1 -88
  8. package/dist/lib/ai/providerRouter.mjs +2 -295
  9. package/dist/lib/commands/adopt.mjs +20 -869
  10. package/dist/lib/commands/adoptWizard.mjs +9 -320
  11. package/dist/lib/commands/agent.mjs +5 -191
  12. package/dist/lib/commands/ai.mjs +2 -407
  13. package/dist/lib/commands/ask.mjs +4 -299
  14. package/dist/lib/commands/audit.mjs +13 -300
  15. package/dist/lib/commands/changelog.mjs +26 -594
  16. package/dist/lib/commands/check.mjs +3 -184
  17. package/dist/lib/commands/ci.mjs +3 -208
  18. package/dist/lib/commands/claudeMd.mjs +30 -135
  19. package/dist/lib/commands/cloud.mjs +10 -773
  20. package/dist/lib/commands/context.mjs +34 -346
  21. package/dist/lib/commands/coverage.mjs +2 -282
  22. package/dist/lib/commands/dashboard.mjs +123 -635
  23. package/dist/lib/commands/demo.mjs +8 -465
  24. package/dist/lib/commands/diff.mjs +5 -274
  25. package/dist/lib/commands/docGate.mjs +2 -81
  26. package/dist/lib/commands/doctor.mjs +3 -321
  27. package/dist/lib/commands/explain.mjs +8 -438
  28. package/dist/lib/commands/export.mjs +10 -239
  29. package/dist/lib/commands/feedback.mjs +12 -216
  30. package/dist/lib/commands/generateSkills.mjs +38 -163
  31. package/dist/lib/commands/graph.mjs +11 -378
  32. package/dist/lib/commands/health.mjs +2 -309
  33. package/dist/lib/commands/impact.mjs +2 -325
  34. package/dist/lib/commands/implement.mjs +7 -103
  35. package/dist/lib/commands/init.mjs +45 -631
  36. package/dist/lib/commands/installCursorHooks.mjs +1 -36
  37. package/dist/lib/commands/installVsCodeCopilotHooks.mjs +1 -37
  38. package/dist/lib/commands/link.mjs +2 -342
  39. package/dist/lib/commands/log.mjs +18 -248
  40. package/dist/lib/commands/monorepo.mjs +4 -428
  41. package/dist/lib/commands/notify.mjs +4 -258
  42. package/dist/lib/commands/onboard.mjs +4 -296
  43. package/dist/lib/commands/prComment.mjs +2 -361
  44. package/dist/lib/commands/prImpact.mjs +2 -157
  45. package/dist/lib/commands/publish.mjs +15 -316
  46. package/dist/lib/commands/recap.mjs +6 -380
  47. package/dist/lib/commands/report.mjs +28 -272
  48. package/dist/lib/commands/review.mjs +9 -223
  49. package/dist/lib/commands/run.mjs +8 -336
  50. package/dist/lib/commands/scaffold.mjs +54 -419
  51. package/dist/lib/commands/scan.mjs +11 -1118
  52. package/dist/lib/commands/scout.mjs +2 -291
  53. package/dist/lib/commands/setup.mjs +5 -310
  54. package/dist/lib/commands/share.mjs +13 -196
  55. package/dist/lib/commands/snapshot.mjs +3 -383
  56. package/dist/lib/commands/stability.mjs +2 -293
  57. package/dist/lib/commands/stats.mjs +5 -402
  58. package/dist/lib/commands/status.mjs +4 -172
  59. package/dist/lib/commands/suggest.mjs +21 -563
  60. package/dist/lib/commands/switch.mjs +13 -520
  61. package/dist/lib/commands/syncAuto.mjs +1 -96
  62. package/dist/lib/commands/synthesize.mjs +10 -228
  63. package/dist/lib/commands/teamSync.mjs +2 -388
  64. package/dist/lib/commands/test.mjs +6 -363
  65. package/dist/lib/commands/theme.mjs +18 -195
  66. package/dist/lib/commands/uninstall.mjs +13 -406
  67. package/dist/lib/commands/upgrade.mjs +20 -153
  68. package/dist/lib/commands/version.mjs +2 -282
  69. package/dist/lib/commands/vibe.mjs +7 -357
  70. package/dist/lib/commands/watch.mjs +4 -203
  71. package/dist/lib/commands/why.mjs +4 -358
  72. package/dist/lib/cursorHooksInstall.mjs +1 -60
  73. package/dist/lib/draftToolingInstall.mjs +7 -68
  74. package/dist/lib/git/detect-drift.mjs +4 -208
  75. package/dist/lib/learning/adapt.mjs +6 -101
  76. package/dist/lib/learning/observe.mjs +1 -119
  77. package/dist/lib/learning/patternDetector.mjs +1 -298
  78. package/dist/lib/learning/profile.mjs +2 -279
  79. package/dist/lib/learning/skillSynthesizer.mjs +24 -145
  80. package/dist/lib/telemetry.mjs +19 -269
  81. package/dist/lib/templates/index.mjs +1 -131
  82. package/dist/lib/theme/scanner.mjs +4 -343
  83. package/dist/lib/ui/errors.mjs +1 -142
  84. package/dist/lib/ui/output.mjs +6 -95
  85. package/dist/lib/ui/prompts.mjs +6 -147
  86. package/dist/lib/vsCodeCopilotHooksInstall.mjs +1 -42
  87. package/package.json +2 -4
  88. package/scripts/postinstall.js +2 -2
@@ -1,380 +1,6 @@
1
- /**
2
- * infernoflow recap
3
- *
4
- * End-of-session summary. Answers "what did infernoflow capture today?"
5
- * and "what git changes might be worth logging?"
6
- *
7
- * The feedback loop that makes the habit stick:
8
- * - You see what was captured this session
9
- * - You see what git changes happened but weren't logged
10
- * - You get a session health score
11
- * - You get one-line nudges to log what's missing
12
- *
13
- * "Session" = entries since the last `handoff` entry, or the last 24h,
14
- * whichever is more recent. Use --since to override.
15
- *
16
- * Usage:
17
- * infernoflow recap Full session summary
18
- * infernoflow recap --since 48h Look back 48 hours
19
- * infernoflow recap --since 2026-04-20 Since a specific date
20
- * infernoflow recap --json Machine-readable output
21
- * infernoflow recap --brief One-line health score only
22
- */
23
-
24
- import * as fs from "node:fs";
25
- import * as path from "node:path";
26
- import { execSync } from "node:child_process";
27
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
28
-
29
- const INFERNO_DIR = "inferno";
30
- const SESSIONS_FILE = path.join(INFERNO_DIR, "sessions.jsonl");
31
- const CONTRACT_FILE = path.join(INFERNO_DIR, "contract.json");
32
-
33
- function readJSON(f) { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } }
34
-
35
- // ── Session boundary detection ────────────────────────────────────────────────
36
-
37
- /**
38
- * Find the start of the "current session":
39
- * - The timestamp of the last `handoff` entry (switching to a new agent = new session)
40
- * - OR 24 hours ago — whichever is more recent
41
- * - --since flag overrides both
42
- */
43
- function findSessionStart(entries, sinceArg) {
44
- if (sinceArg) {
45
- // e.g. "48h", "7d", "2026-04-20"
46
- const hoursMatch = sinceArg.match(/^(\d+)h$/i);
47
- const daysMatch = sinceArg.match(/^(\d+)d$/i);
48
- if (hoursMatch) return new Date(Date.now() - parseInt(hoursMatch[1]) * 3600000);
49
- if (daysMatch) return new Date(Date.now() - parseInt(daysMatch[1]) * 86400000);
50
- const parsed = new Date(sinceArg);
51
- if (!isNaN(parsed)) return parsed;
52
- }
53
-
54
- // Find last handoff entry
55
- for (let i = entries.length - 1; i >= 0; i--) {
56
- if (entries[i].type === "handoff") {
57
- const ts = new Date(entries[i].ts || 0);
58
- const dayAgo = new Date(Date.now() - 86400000);
59
- // Use whichever is more recent
60
- return ts > dayAgo ? ts : dayAgo;
61
- }
62
- }
63
-
64
- // Default: last 24h
65
- return new Date(Date.now() - 86400000);
66
- }
67
-
68
- // ── Git helpers ───────────────────────────────────────────────────────────────
69
-
70
- function gitChangedFiles(since) {
71
- const cwd = process.cwd();
72
- const run = (cmd) => {
73
- try { return execSync(cmd, { cwd, encoding: "utf8", timeout: 5000, stdio: ["pipe","pipe","pipe"] }).trim(); }
74
- catch { return ""; }
75
- };
76
-
77
- const sinceIso = since.toISOString().slice(0, 19);
78
- const staged = run("git diff --cached --name-only");
79
- const unstaged = run("git diff --name-only");
80
- const committed = run(`git log --since="${sinceIso}" --name-only --pretty=format:""`);
81
-
82
- const all = new Set([
83
- ...staged.split("\n"),
84
- ...unstaged.split("\n"),
85
- ...committed.split("\n"),
86
- ].map(f => f.trim()).filter(Boolean));
87
-
88
- return [...all];
89
- }
90
-
91
- /**
92
- * From a list of changed files, infer what topics might need logging.
93
- * Returns: [{ topic, files, suggestedType }]
94
- */
95
- function inferUnloggedTopics(changedFiles, sessionEntries) {
96
- const TOPIC_RULES = [
97
- { keywords: ["auth", "login", "logout", "session", "jwt", "token", "password"], topic: "authentication" },
98
- { keywords: ["stripe", "payment", "checkout", "billing", "subscription"], topic: "payments" },
99
- { keywords: ["upload", "file", "s3", "storage", "bucket", "cdn"], topic: "file handling" },
100
- { keywords: ["email", "sendgrid", "ses", "smtp", "nodemailer", "twilio"], topic: "notifications" },
101
- { keywords: ["db", "database", "prisma", "mongoose", "postgres", "mysql", "migration"], topic: "database" },
102
- { keywords: ["deploy", "docker", "ci", "workflow", "action", "kubernetes"], topic: "deployment" },
103
- { keywords: ["cache", "redis", "memcache"], topic: "caching" },
104
- { keywords: ["test", "spec", "jest", "vitest", "cypress", "playwright"], topic: "testing" },
105
- { keywords: ["config", "env", ".env", "environment", "secret"], topic: "configuration" },
106
- { keywords: ["api", "route", "endpoint", "controller", "handler"], topic: "API routes" },
107
- { keywords: ["ui", "component", "style", "css", "tailwind", "theme"], topic: "UI/styles" },
108
- ];
109
-
110
- // Build set of topics already mentioned in session entries
111
- const loggedText = sessionEntries.map(e => (e.summary || "").toLowerCase()).join(" ");
112
-
113
- const candidates = [];
114
- const seen = new Set();
115
-
116
- for (const rule of TOPIC_RULES) {
117
- if (seen.has(rule.topic)) continue;
118
-
119
- const matchingFiles = changedFiles.filter(f =>
120
- rule.keywords.some(kw => f.toLowerCase().includes(kw))
121
- );
122
- if (!matchingFiles.length) continue;
123
-
124
- // Check if session already has entries about this topic
125
- const alreadyLogged = rule.keywords.some(kw => loggedText.includes(kw));
126
- if (alreadyLogged) continue;
127
-
128
- seen.add(rule.topic);
129
- candidates.push({
130
- topic: rule.topic,
131
- files: matchingFiles.slice(0, 3),
132
- suggestedType: "gotcha", // prompt for the most valuable type
133
- });
134
- }
135
-
136
- return candidates;
137
- }
138
-
139
- // ── Session health score ──────────────────────────────────────────────────────
140
-
141
- /**
142
- * Score 0-100 based on what types were logged this session.
143
- * A "healthy" session has at least one gotcha or decision.
144
- */
145
- function sessionHealth(entries) {
146
- const types = new Set(entries.map(e => e.type));
147
- let score = 0;
148
- const checks = [];
149
-
150
- if (entries.length > 0) {
151
- score += 20;
152
- checks.push({ ok: true, label: `${entries.length} entr${entries.length !== 1 ? "ies" : "y"} logged` });
153
- } else {
154
- checks.push({ ok: false, label: "nothing logged this session" });
155
- }
156
-
157
- if (types.has("gotcha")) {
158
- score += 35;
159
- checks.push({ ok: true, label: "gotchas captured" });
160
- } else {
161
- checks.push({ ok: false, label: "no gotchas (most valuable — log landmines!)" });
162
- }
163
-
164
- if (types.has("decision")) {
165
- score += 25;
166
- checks.push({ ok: true, label: "decisions recorded" });
167
- } else {
168
- checks.push({ ok: false, label: "no decisions recorded" });
169
- }
170
-
171
- if (types.has("attempt")) {
172
- score += 10;
173
- checks.push({ ok: true, label: "attempts tracked" });
174
- }
175
-
176
- if (types.has("preference")) {
177
- score += 10;
178
- checks.push({ ok: true, label: "preferences noted" });
179
- }
180
-
181
- return { score: Math.min(score, 100), checks };
182
- }
183
-
184
- // ── formatters ────────────────────────────────────────────────────────────────
185
-
186
- function fmtRelDate(iso) {
187
- if (!iso) return "";
188
- const d = new Date(iso);
189
- const diff = Date.now() - d.getTime();
190
- const mins = Math.floor(diff / 60000);
191
- if (mins < 60) return `${mins}m ago`;
192
- const hours = Math.floor(diff / 3600000);
193
- if (hours < 24) return `${hours}h ago`;
194
- return `${Math.floor(diff / 86400000)}d ago`;
195
- }
196
-
197
- const TYPE_ICONS = { gotcha:"⚠", decision:"✓", attempt:"↺", preference:"♦", theme:"◈", note:"·", error:"✗", handoff:"→" };
198
- const TYPE_COLORS = { gotcha: yellow, decision: green, attempt: cyan, preference: cyan, theme: cyan, note: gray, error: red, handoff: gray };
199
-
200
- function printEntry(e) {
201
- const colorFn = TYPE_COLORS[e.type] || gray;
202
- const icon = TYPE_ICONS[e.type] || "·";
203
- const result = e.result ? gray(` [${e.result}]`) : "";
204
- const date = gray(` (${fmtRelDate(e.ts)})`);
205
- console.log(` ${colorFn(icon + " " + (e.type || "note").padEnd(11))}${result}${date}`);
206
- console.log(` ${e.summary}`);
207
- }
208
-
209
- // ── entry point ───────────────────────────────────────────────────────────────
210
-
211
- export async function recapCommand(rawArgs = []) {
212
- const args = rawArgs;
213
- const jsonMode = args.includes("--json");
214
- const briefMode = args.includes("--brief");
215
- const sinceIdx = args.indexOf("--since");
216
- const sinceArg = sinceIdx !== -1 ? args[sinceIdx + 1] : null;
217
-
218
- const cwd = process.cwd();
219
-
220
- if (!fs.existsSync(path.join(cwd, INFERNO_DIR))) {
221
- if (!jsonMode) console.error(red(" ✘ inferno/ not found — run: infernoflow init\n"));
222
- process.exit(1);
223
- }
224
-
225
- // Load all sessions
226
- const allEntries = [];
227
- const sessPath = path.join(cwd, SESSIONS_FILE);
228
- if (fs.existsSync(sessPath)) {
229
- fs.readFileSync(sessPath, "utf8").split("\n").filter(Boolean).forEach(l => {
230
- try { allEntries.push(JSON.parse(l)); } catch {}
231
- });
232
- }
233
-
234
- // Find session start
235
- const sessionStart = findSessionStart(allEntries, sinceArg);
236
- const sessionEntries = allEntries.filter(e => new Date(e.ts || 0) > sessionStart);
237
-
238
- // Git changes in this window
239
- const changedFiles = gitChangedFiles(sessionStart);
240
- const unloggedTopics = inferUnloggedTopics(changedFiles, sessionEntries);
241
-
242
- // Health score
243
- const { score, checks } = sessionHealth(sessionEntries);
244
-
245
- const contract = readJSON(path.join(cwd, CONTRACT_FILE));
246
-
247
- if (jsonMode) {
248
- console.log(JSON.stringify({
249
- sessionStart: sessionStart.toISOString(),
250
- entries: sessionEntries,
251
- changedFiles,
252
- unloggedTopics,
253
- health: { score, checks },
254
- }, null, 2));
255
- return;
256
- }
257
-
258
- if (briefMode) {
259
- const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : "D";
260
- const colorFn = score >= 60 ? green : score >= 40 ? yellow : red;
261
- console.log(colorFn(`Session health: ${grade} (${score}/100)`) + gray(` — ${sessionEntries.length} entries logged`));
262
- if (unloggedTopics.length) {
263
- console.log(yellow(` ${unloggedTopics.length} topic${unloggedTopics.length !== 1 ? "s" : ""} changed but not logged: `) + unloggedTopics.map(t => t.topic).join(", "));
264
- }
265
- return;
266
- }
267
-
268
- // ── Full dashboard ─────────────────────────────────────────────────────────
269
- const SEP = gray(" " + "─".repeat(52));
270
-
271
- console.log();
272
- console.log(" " + bold("🔥 infernoflow recap"));
273
- if (contract?.policyId) console.log(gray(` Project: ${contract.policyId}`));
274
- const sinceStr = sessionStart.toLocaleString("en-GB", { day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit" });
275
- console.log(gray(` Session since: ${sinceStr}`));
276
- console.log(SEP);
277
-
278
- // ── This session's entries ────────────────────────────────────────────────
279
- console.log();
280
- console.log(" " + bold("Captured this session"));
281
- console.log();
282
-
283
- if (sessionEntries.length === 0) {
284
- console.log(gray(" Nothing logged yet this session."));
285
- } else {
286
- // Group by type, priority order
287
- const typeOrder = ["gotcha", "decision", "attempt", "preference", "theme", "note", "error"];
288
- const byType = new Map();
289
- for (const e of sessionEntries) {
290
- const t = e.type || "note";
291
- if (!byType.has(t)) byType.set(t, []);
292
- byType.get(t).push(e);
293
- }
294
- for (const t of typeOrder) {
295
- const group = byType.get(t);
296
- if (!group?.length) continue;
297
- for (const e of group) { console.log(); printEntry(e); }
298
- }
299
- }
300
-
301
- // ── Unlogged changes ──────────────────────────────────────────────────────
302
- if (unloggedTopics.length > 0) {
303
- console.log();
304
- console.log(SEP);
305
- console.log();
306
- console.log(" " + bold("Changed but not logged") + gray(" (git diff since session start)"));
307
- console.log();
308
-
309
- for (const { topic, files } of unloggedTopics) {
310
- console.log(yellow(` ? ${topic}`));
311
- for (const f of files) console.log(gray(` ${f}`));
312
- }
313
-
314
- console.log();
315
- console.log(gray(" Any gotchas or decisions from these areas worth capturing?"));
316
- console.log(gray(" Run: ") + cyan(`infernoflow log "<what happened>" --type gotcha`));
317
- } else if (changedFiles.length > 0) {
318
- console.log();
319
- console.log(SEP);
320
- console.log();
321
- console.log(green(" ✔ ") + gray(`${changedFiles.length} changed files — all topics appear to be logged`));
322
- }
323
-
324
- // ── Session health score ──────────────────────────────────────────────────
325
- console.log();
326
- console.log(SEP);
327
- console.log();
328
- console.log(" " + bold("Session health"));
329
- console.log();
330
-
331
- const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : "D";
332
- const colorFn = score >= 60 ? green : score >= 40 ? yellow : red;
333
- console.log(` ${colorFn(bold(`${grade}`))} ${colorFn(`${score}/100`)}`);
334
- console.log();
335
-
336
- for (const { ok, label } of checks) {
337
- const icon = ok ? green(" ✔") : yellow(" ·");
338
- console.log(`${icon} ${ok ? label : gray(label)}`);
339
- }
340
-
341
- // Actionable improvement tips
342
- {
343
- const gotchaCount = sessionEntries.filter(e => e.type === "gotcha").length;
344
- const decisionCount = sessionEntries.filter(e => e.type === "decision").length;
345
- const tips = [];
346
-
347
- if (gotchaCount === 0) {
348
- tips.push(cyan("infernoflow log \"...\" --type gotcha") + gray(" — adds 35 pts"));
349
- } else if (gotchaCount < 3 && score < 80) {
350
- tips.push(gray(` ${3 - gotchaCount} more gotcha(s) would push you higher`));
351
- }
352
- if (decisionCount === 0) {
353
- tips.push(cyan("infernoflow log \"...\" --type decision") + gray(" — adds 25 pts"));
354
- }
355
- if (score >= 60 && score < 80) {
356
- tips.push(gray(" Almost B! One more entry gets you there."));
357
- }
358
- if (score >= 80) {
359
- tips.push(green(" Great session — your handoff will be excellent."));
360
- }
361
-
362
- if (tips.length) {
363
- console.log();
364
- console.log(gray(" How to improve:"));
365
- for (const t of tips) console.log(" " + t);
366
- }
367
- }
368
-
369
- // ── Next session tip ──────────────────────────────────────────────────────
370
- if (sessionEntries.length > 0 || unloggedTopics.length > 0) {
371
- console.log();
372
- console.log(SEP);
373
- console.log();
374
- console.log(gray(" Before your next session:"));
375
- console.log(gray(" ") + cyan("infernoflow switch") + gray(" — generate a handoff summary for the next AI agent"));
376
- console.log(gray(" ") + cyan("infernoflow ask --recent") + gray(" — review what's in memory before starting"));
377
- }
378
-
379
- console.log();
380
- }
1
+ import*as O from"node:fs";import*as b from"node:path";import{execSync as M}from"node:child_process";import{bold as I,cyan as u,gray as n,green as S,yellow as k,red as C}from"../ui/output.mjs";const j="inferno",v=b.join(j,"sessions.jsonl"),B=b.join(j,"contract.json");function P(e){try{return JSON.parse(O.readFileSync(e,"utf8"))}catch{return null}}function R(e,s){if(s){const o=s.match(/^(\d+)h$/i),t=s.match(/^(\d+)d$/i);if(o)return new Date(Date.now()-parseInt(o[1])*36e5);if(t)return new Date(Date.now()-parseInt(t[1])*864e5);const c=new Date(s);if(!isNaN(c))return c}for(let o=e.length-1;o>=0;o--)if(e[o].type==="handoff"){const t=new Date(e[o].ts||0),c=new Date(Date.now()-864e5);return t>c?t:c}return new Date(Date.now()-864e5)}function A(e){const s=process.cwd(),o=m=>{try{return M(m,{cwd:s,encoding:"utf8",timeout:5e3,stdio:["pipe","pipe","pipe"]}).trim()}catch{return""}},t=e.toISOString().slice(0,19),c=o("git diff --cached --name-only"),w=o("git diff --name-only"),a=o(`git log --since="${t}" --name-only --pretty=format:""`);return[...new Set([...c.split(`
2
+ `),...w.split(`
3
+ `),...a.split(`
4
+ `)].map(m=>m.trim()).filter(Boolean))]}function _(e,s){const o=[{keywords:["auth","login","logout","session","jwt","token","password"],topic:"authentication"},{keywords:["stripe","payment","checkout","billing","subscription"],topic:"payments"},{keywords:["upload","file","s3","storage","bucket","cdn"],topic:"file handling"},{keywords:["email","sendgrid","ses","smtp","nodemailer","twilio"],topic:"notifications"},{keywords:["db","database","prisma","mongoose","postgres","mysql","migration"],topic:"database"},{keywords:["deploy","docker","ci","workflow","action","kubernetes"],topic:"deployment"},{keywords:["cache","redis","memcache"],topic:"caching"},{keywords:["test","spec","jest","vitest","cypress","playwright"],topic:"testing"},{keywords:["config","env",".env","environment","secret"],topic:"configuration"},{keywords:["api","route","endpoint","controller","handler"],topic:"API routes"},{keywords:["ui","component","style","css","tailwind","theme"],topic:"UI/styles"}],t=s.map(a=>(a.summary||"").toLowerCase()).join(" "),c=[],w=new Set;for(const a of o){if(w.has(a.topic))continue;const y=e.filter(p=>a.keywords.some(d=>p.toLowerCase().includes(d)));!y.length||a.keywords.some(p=>t.includes(p))||(w.add(a.topic),c.push({topic:a.topic,files:y.slice(0,3),suggestedType:"gotcha"}))}return c}function J(e){const s=new Set(e.map(c=>c.type));let o=0;const t=[];return e.length>0?(o+=20,t.push({ok:!0,label:`${e.length} entr${e.length!==1?"ies":"y"} logged`})):t.push({ok:!1,label:"nothing logged this session"}),s.has("gotcha")?(o+=35,t.push({ok:!0,label:"gotchas captured"})):t.push({ok:!1,label:"no gotchas (most valuable \u2014 log landmines!)"}),s.has("decision")?(o+=25,t.push({ok:!0,label:"decisions recorded"})):t.push({ok:!1,label:"no decisions recorded"}),s.has("attempt")&&(o+=10,t.push({ok:!0,label:"attempts tracked"})),s.has("preference")&&(o+=10,t.push({ok:!0,label:"preferences noted"})),{score:Math.min(o,100),checks:t}}function U(e){if(!e)return"";const s=new Date(e),o=Date.now()-s.getTime(),t=Math.floor(o/6e4);if(t<60)return`${t}m ago`;const c=Math.floor(o/36e5);return c<24?`${c}h ago`:`${Math.floor(o/864e5)}d ago`}const G={gotcha:"\u26A0",decision:"\u2713",attempt:"\u21BA",preference:"\u2666",theme:"\u25C8",note:"\xB7",error:"\u2717",handoff:"\u2192"},H={gotcha:k,decision:S,attempt:u,preference:u,theme:u,note:n,error:C,handoff:n};function Y(e){const s=H[e.type]||n,o=G[e.type]||"\xB7",t=e.result?n(` [${e.result}]`):"",c=n(` (${U(e.ts)})`);console.log(` ${s(o+" "+(e.type||"note").padEnd(11))}${t}${c}`),console.log(` ${e.summary}`)}async function K(e=[]){const s=e,o=s.includes("--json"),t=s.includes("--brief"),c=s.indexOf("--since"),w=c!==-1?s[c+1]:null,a=process.cwd();O.existsSync(b.join(a,j))||(o||console.error(C(` \u2718 inferno/ not found \u2014 run: infernoflow init
5
+ `)),process.exit(1));const y=[],m=b.join(a,v);O.existsSync(m)&&O.readFileSync(m,"utf8").split(`
6
+ `).filter(Boolean).forEach(r=>{try{y.push(JSON.parse(r))}catch{}});const p=R(y,w),d=y.filter(r=>new Date(r.ts||0)>p),D=A(p),h=_(D,d),{score:i,checks:E}=J(d),N=P(b.join(a,B));if(o){console.log(JSON.stringify({sessionStart:p.toISOString(),entries:d,changedFiles:D,unloggedTopics:h,health:{score:i,checks:E}},null,2));return}if(t){const r=i>=80?"A":i>=60?"B":i>=40?"C":"D",g=i>=60?S:i>=40?k:C;console.log(g(`Session health: ${r} (${i}/100)`)+n(` \u2014 ${d.length} entries logged`)),h.length&&console.log(k(` ${h.length} topic${h.length!==1?"s":""} changed but not logged: `)+h.map(l=>l.topic).join(", "));return}const $=n(" "+"\u2500".repeat(52));console.log(),console.log(" "+I("\u{1F525} infernoflow recap")),N?.policyId&&console.log(n(` Project: ${N.policyId}`));const F=p.toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"});if(console.log(n(` Session since: ${F}`)),console.log($),console.log(),console.log(" "+I("Captured this session")),console.log(),d.length===0)console.log(n(" Nothing logged yet this session."));else{const r=["gotcha","decision","attempt","preference","theme","note","error"],g=new Map;for(const l of d){const f=l.type||"note";g.has(f)||g.set(f,[]),g.get(f).push(l)}for(const l of r){const f=g.get(l);if(f?.length)for(const L of f)console.log(),Y(L)}}if(h.length>0){console.log(),console.log($),console.log(),console.log(" "+I("Changed but not logged")+n(" (git diff since session start)")),console.log();for(const{topic:r,files:g}of h){console.log(k(` ? ${r}`));for(const l of g)console.log(n(` ${l}`))}console.log(),console.log(n(" Any gotchas or decisions from these areas worth capturing?")),console.log(n(" Run: ")+u('infernoflow log "<what happened>" --type gotcha'))}else D.length>0&&(console.log(),console.log($),console.log(),console.log(S(" \u2714 ")+n(`${D.length} changed files \u2014 all topics appear to be logged`)));console.log(),console.log($),console.log(),console.log(" "+I("Session health")),console.log();const T=i>=80?"A":i>=60?"B":i>=40?"C":"D",x=i>=60?S:i>=40?k:C;console.log(` ${x(I(`${T}`))} ${x(`${i}/100`)}`),console.log();for(const{ok:r,label:g}of E){const l=r?S(" \u2714"):k(" \xB7");console.log(`${l} ${r?g:n(g)}`)}{const r=d.filter(f=>f.type==="gotcha").length,g=d.filter(f=>f.type==="decision").length,l=[];if(r===0?l.push(u('infernoflow log "..." --type gotcha')+n(" \u2014 adds 35 pts")):r<3&&i<80&&l.push(n(` ${3-r} more gotcha(s) would push you higher`)),g===0&&l.push(u('infernoflow log "..." --type decision')+n(" \u2014 adds 25 pts")),i>=60&&i<80&&l.push(n(" Almost B! One more entry gets you there.")),i>=80&&l.push(S(" Great session \u2014 your handoff will be excellent.")),l.length){console.log(),console.log(n(" How to improve:"));for(const f of l)console.log(" "+f)}}(d.length>0||h.length>0)&&(console.log(),console.log($),console.log(),console.log(n(" Before your next session:")),console.log(n(" ")+u("infernoflow switch")+n(" \u2014 generate a handoff summary for the next AI agent")),console.log(n(" ")+u("infernoflow ask --recent")+n(" \u2014 review what's in memory before starting"))),console.log()}export{K as recapCommand};