infernoflow 0.37.0 → 0.37.3

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 +125 -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 -517
  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,402 +1,5 @@
1
- /**
2
- * infernoflow stats
3
- *
4
- * Value dashboard answers "is infernoflow actually saving me time?"
5
- *
6
- * Shows:
7
- * • Session memory — total entries + breakdown by type
8
- * • Context size — estimated tokens injected per session start
9
- * • Coverage — % of capabilities that have code analysis
10
- * • HTTP chains — resolved end-to-end call chains
11
- * • Design system — what's captured in theme.json
12
- * • Savings estimate— tokens saved by not re-discovering recorded entries
13
- *
14
- * Usage:
15
- * infernoflow stats Interactive dashboard
16
- * infernoflow stats --json Machine-readable output
17
- * infernoflow stats --brief One-line summary (for CI / scripts)
18
- */
19
-
20
- import * as fs from "node:fs";
21
- import * as path from "node:path";
22
- import { bold, cyan, gray, green, yellow, red } from "../ui/output.mjs";
23
-
24
- const INFERNO_DIR = "inferno";
25
- const SESSIONS_FILE = path.join(INFERNO_DIR, "sessions.jsonl");
26
- const CONTEXT_FILE = path.join(INFERNO_DIR, "CONTEXT.md");
27
- const THEME_FILE = path.join(INFERNO_DIR, "theme.json");
28
- const SCAN_FILE = path.join(INFERNO_DIR, "scan.json");
29
- const CONTRACT_FILE = path.join(INFERNO_DIR, "contract.json");
30
- const CAPS_FILE = path.join(INFERNO_DIR, "capabilities.json");
31
-
32
- function readJSON(f) { try { return JSON.parse(fs.readFileSync(f, "utf8")); } catch { return null; } }
33
-
34
- /**
35
- * Rough token estimator: ~1 token per 4 characters (GPT-style).
36
- * Conservative — actual savings are often higher with tool/system prompt overhead.
37
- */
38
- function estimateTokens(text) {
39
- return Math.ceil((text || "").length / 4);
40
- }
41
-
42
- /**
43
- * Per-entry-type token savings heuristic.
44
- * Each captured entry avoids N tokens of back-and-forth rediscovery.
45
- *
46
- * Gotcha: ~400 tokens (agent tries wrong thing, gets error, explains, retries)
47
- * Decision: ~200 tokens (agent asks why, developer explains)
48
- * Attempt: ~250 tokens (agent attempts failed approach, error, pivot)
49
- * Preference:~150 tokens (agent does it wrong, developer corrects)
50
- * Note: ~100 tokens (minor context the agent would have to infer)
51
- * Theme: ~300 tokens (agent uses wrong colors/fonts, developer corrects)
52
- */
53
- const SAVINGS_PER_TYPE = {
54
- gotcha: 400,
55
- decision: 200,
56
- attempt: 250,
57
- preference: 150,
58
- note: 100,
59
- theme: 300,
60
- handoff: 500, // full handoff = avoided a "catch me up" conversation
61
- error: 200,
62
- };
63
-
64
- function collectStats(cwd) {
65
- const stats = {
66
- ok: false,
67
- memory: {
68
- total: 0,
69
- byType: {},
70
- oldestEntry: null,
71
- newestEntry: null,
72
- sessionsTracked: 0,
73
- },
74
- context: {
75
- sizeBytes: 0,
76
- estimatedTokens: 0,
77
- hasIntent: false,
78
- hasWorking: false,
79
- },
80
- theme: {
81
- captured: false,
82
- fonts: 0,
83
- colors: 0,
84
- cssVars: 0,
85
- framework: null,
86
- },
87
- coverage: {
88
- total: 0,
89
- withAnalysis: 0,
90
- pct: 0,
91
- },
92
- chains: {
93
- total: 0,
94
- resolved: 0,
95
- },
96
- contract: {
97
- policyId: null,
98
- capabilities: 0,
99
- isLite: false,
100
- },
101
- savings: {
102
- estimatedTokens: 0,
103
- breakdown: {},
104
- },
105
- };
106
-
107
- // ── contract ──────────────────────────────────────────────────────────────
108
- const contract = readJSON(path.join(cwd, CONTRACT_FILE));
109
- if (contract) {
110
- stats.contract.policyId = contract.policyId;
111
- stats.contract.capabilities = (contract.capabilities || []).length;
112
- stats.contract.isLite = !!contract.lite;
113
- stats.ok = true;
114
- }
115
-
116
- // ── capabilities coverage ─────────────────────────────────────────────────
117
- const caps = readJSON(path.join(cwd, CAPS_FILE));
118
- if (caps) {
119
- const list = Array.isArray(caps) ? caps : (caps.capabilities || []);
120
- stats.coverage.total = list.length;
121
- stats.coverage.withAnalysis = list.filter(c => c.codeAnalysis).length;
122
- stats.coverage.pct = stats.coverage.total
123
- ? Math.round((stats.coverage.withAnalysis / stats.coverage.total) * 100)
124
- : 0;
125
- }
126
-
127
- // ── session memory ────────────────────────────────────────────────────────
128
- const sessPath = path.join(cwd, SESSIONS_FILE);
129
- if (fs.existsSync(sessPath)) {
130
- const lines = fs.readFileSync(sessPath, "utf8")
131
- .split("\n").filter(Boolean);
132
- const entries = lines
133
- .map(l => { try { return JSON.parse(l); } catch { return null; } })
134
- .filter(Boolean);
135
-
136
- stats.memory.total = entries.length;
137
-
138
- for (const e of entries) {
139
- const t = e.type || "note";
140
- stats.memory.byType[t] = (stats.memory.byType[t] || 0) + 1;
141
-
142
- // Savings
143
- const saved = SAVINGS_PER_TYPE[t] || 100;
144
- stats.savings.estimatedTokens += saved;
145
- stats.savings.breakdown[t] = (stats.savings.breakdown[t] || 0) + saved;
146
- }
147
-
148
- if (entries.length) {
149
- stats.memory.oldestEntry = entries[0].ts;
150
- stats.memory.newestEntry = entries[entries.length - 1].ts;
151
- }
152
-
153
- // Count unique sessions (group by day)
154
- const days = new Set(entries.map(e => (e.ts || "").slice(0, 10)));
155
- stats.memory.sessionsTracked = days.size;
156
- }
157
-
158
- // ── context size ──────────────────────────────────────────────────────────
159
- const ctxPath = path.join(cwd, CONTEXT_FILE);
160
- if (fs.existsSync(ctxPath)) {
161
- const ctxText = fs.readFileSync(ctxPath, "utf8");
162
- stats.context.sizeBytes = Buffer.byteLength(ctxText, "utf8");
163
- stats.context.estimatedTokens = estimateTokens(ctxText);
164
- stats.context.hasIntent = ctxText.includes("## Intent");
165
- stats.context.hasWorking = ctxText.includes("## Working on");
166
- }
167
-
168
- // ── theme / design system ─────────────────────────────────────────────────
169
- const theme = readJSON(path.join(cwd, THEME_FILE));
170
- if (theme) {
171
- stats.theme.captured = true;
172
- stats.theme.fonts = Object.keys(theme.fonts || {}).filter(k => theme.fonts[k]).length;
173
- stats.theme.colors = Object.keys(theme.colors?.palette || {}).length;
174
- stats.theme.cssVars = Object.keys(theme.cssVars || {}).length;
175
- stats.theme.framework = theme.framework || null;
176
- }
177
-
178
- // ── HTTP chains (from scan.json) ──────────────────────────────────────────
179
- const scan = readJSON(path.join(cwd, SCAN_FILE));
180
- if (scan?.httpChains) {
181
- const allSteps = Object.values(scan.httpChains).flat();
182
- stats.chains.total = allSteps.length;
183
- stats.chains.resolved = allSteps.filter(s => s.resolved).length;
184
- }
185
-
186
- return stats;
187
- }
188
-
189
- // ── bar chart helper ──────────────────────────────────────────────────────────
190
-
191
- function bar(value, max, width = 20) {
192
- const filled = max > 0 ? Math.round((value / max) * width) : 0;
193
- return "█".repeat(filled) + "░".repeat(width - filled);
194
- }
195
-
196
- function pctColor(pct) {
197
- if (pct >= 80) return green;
198
- if (pct >= 40) return yellow;
199
- return red;
200
- }
201
-
202
- // ── formatter ─────────────────────────────────────────────────────────────────
203
-
204
- function fmtRelDate(iso) {
205
- if (!iso) return "never";
206
- const d = new Date(iso);
207
- const now = Date.now();
208
- const diff = now - d.getTime();
209
- const days = Math.floor(diff / 86400000);
210
- if (days === 0) return "today";
211
- if (days === 1) return "yesterday";
212
- if (days < 7) return `${days}d ago`;
213
- if (days < 30) return `${Math.floor(days / 7)}w ago`;
214
- return d.toLocaleDateString("en-GB", { day: "2-digit", month: "short" });
215
- }
216
-
217
- function fmtTokens(n) {
218
- if (n >= 1000) return `~${Math.round(n / 100) / 10}k`;
219
- return `~${n}`;
220
- }
221
-
222
- // ── print dashboard ───────────────────────────────────────────────────────────
223
-
224
- function printDashboard(stats) {
225
- const SEP = gray(" " + "─".repeat(52));
226
-
227
- console.log();
228
- console.log(" " + bold("🔥 infernoflow stats"));
229
- if (stats.contract.policyId) {
230
- console.log(gray(` Project: ${stats.contract.policyId}${stats.contract.isLite ? " (lite)" : ""}`));
231
- }
232
- console.log(SEP);
233
-
234
- // ── Session memory ──────────────────────────────────────────────────────
235
- console.log();
236
- console.log(" " + bold("Session memory") + gray(" (inferno/sessions.jsonl)"));
237
- console.log();
238
-
239
- const total = stats.memory.total;
240
- if (total === 0) {
241
- console.log(gray(" No entries yet — run: infernoflow log \"<what happened>\" --type gotcha"));
242
- } else {
243
- const typeOrder = ["gotcha", "decision", "attempt", "preference", "theme", "note", "handoff", "error"];
244
- const maxCount = Math.max(...Object.values(stats.memory.byType));
245
-
246
- for (const type of typeOrder) {
247
- const count = stats.memory.byType[type] || 0;
248
- if (count === 0) continue;
249
- const b = bar(count, maxCount, 16);
250
- const label = type.padEnd(12);
251
- console.log(` ${gray(label)} ${cyan(b)} ${count}`);
252
- }
253
-
254
- console.log();
255
- console.log(gray(` Total entries: `) + bold(total));
256
- console.log(gray(` Sessions tracked: `) + stats.memory.sessionsTracked);
257
- if (stats.memory.newestEntry) {
258
- console.log(gray(` Last entry: `) + fmtRelDate(stats.memory.newestEntry));
259
- }
260
- }
261
-
262
- // ── Context injection ───────────────────────────────────────────────────
263
- console.log();
264
- console.log(SEP);
265
- console.log();
266
- console.log(" " + bold("Context injection") + gray(" (per session start)"));
267
- console.log();
268
-
269
- if (stats.context.sizeBytes === 0) {
270
- console.log(gray(" No CONTEXT.md yet — run: infernoflow context"));
271
- } else {
272
- console.log(gray(` Size: `) + `${Math.round(stats.context.sizeBytes / 1024 * 10) / 10} KB`);
273
- console.log(gray(` Tokens: `) + bold(fmtTokens(stats.context.estimatedTokens)) + gray(" injected into every session"));
274
- if (stats.context.hasIntent) console.log(gray(` `) + green("✔") + gray(" Intent captured"));
275
- if (stats.context.hasWorking) console.log(gray(` `) + green("✔") + gray(" Working state captured"));
276
- }
277
-
278
- // ── Capability coverage ─────────────────────────────────────────────────
279
- console.log();
280
- console.log(SEP);
281
- console.log();
282
- console.log(" " + bold("Capability coverage") + gray(" (code analysis via infernoflow scan)"));
283
- console.log();
284
-
285
- if (stats.coverage.total === 0) {
286
- console.log(gray(" No capabilities yet — run: infernoflow init"));
287
- } else {
288
- const colorFn = pctColor(stats.coverage.pct);
289
- const b = bar(stats.coverage.withAnalysis, stats.coverage.total, 24);
290
- console.log(` ${colorFn(b)} ${bold(stats.coverage.pct + "%")} (${stats.coverage.withAnalysis}/${stats.coverage.total})`);
291
-
292
- if (stats.coverage.pct < 100) {
293
- const uncovered = stats.coverage.total - stats.coverage.withAnalysis;
294
- console.log(gray(`\n ${uncovered} capabilities without code analysis`));
295
- console.log(gray(` Run: infernoflow scan to enrich them`));
296
- }
297
- }
298
-
299
- // ── HTTP chains ─────────────────────────────────────────────────────────
300
- if (stats.chains.total > 0) {
301
- console.log();
302
- console.log(SEP);
303
- console.log();
304
- console.log(" " + bold("HTTP call chains") + gray(" (end-to-end resolution)"));
305
- console.log();
306
- const resPct = Math.round((stats.chains.resolved / stats.chains.total) * 100);
307
- const colorFn = pctColor(resPct);
308
- const b = bar(stats.chains.resolved, stats.chains.total, 20);
309
- console.log(` ${colorFn(b)} ${bold(resPct + "%")} resolved (${stats.chains.resolved}/${stats.chains.total} call chains)`);
310
- if (stats.chains.resolved < stats.chains.total) {
311
- console.log(gray(`\n Unresolved calls may be to external services or missing route files`));
312
- }
313
- }
314
-
315
- // ── Design system ───────────────────────────────────────────────────────
316
- console.log();
317
- console.log(SEP);
318
- console.log();
319
- console.log(" " + bold("Design system") + gray(" (inferno/theme.json)"));
320
- console.log();
321
-
322
- if (!stats.theme.captured) {
323
- console.log(gray(" Not captured yet — run: infernoflow theme"));
324
- } else {
325
- const checks = [];
326
- if (stats.theme.fonts) checks.push(`${stats.theme.fonts} font${stats.theme.fonts !== 1 ? "s" : ""}`);
327
- if (stats.theme.colors) checks.push(`${stats.theme.colors} colors`);
328
- if (stats.theme.cssVars) checks.push(`${stats.theme.cssVars} CSS vars`);
329
- if (stats.theme.framework) checks.push(`${stats.theme.framework}`);
330
- console.log(gray(" ") + green("✔") + " " + checks.join(" · "));
331
- console.log(gray(" AI agents always use the correct fonts and colors for this project"));
332
- }
333
-
334
- // ── Estimated savings ───────────────────────────────────────────────────
335
- console.log();
336
- console.log(SEP);
337
- console.log();
338
- console.log(" " + bold("Estimated token savings") + gray(" (vs re-discovering from scratch)"));
339
- console.log();
340
-
341
- const saved = stats.savings.estimatedTokens;
342
- if (saved === 0) {
343
- console.log(gray(" No session entries yet — start logging to track savings"));
344
- } else {
345
- const sessions = Math.max(stats.memory.sessionsTracked, 1);
346
- const perSession = Math.round(saved / sessions);
347
-
348
- console.log(` Total saved: ` + bold(green(fmtTokens(saved) + " tokens")));
349
- console.log(` Per session: ` + bold(fmtTokens(perSession) + " tokens"));
350
- console.log();
351
- console.log(gray(" Breakdown:"));
352
-
353
- const typeOrder = ["gotcha", "handoff", "attempt", "decision", "theme", "preference", "note", "error"];
354
- for (const type of typeOrder) {
355
- const tokens = stats.savings.breakdown[type];
356
- if (!tokens) continue;
357
- const count = stats.memory.byType[type] || 0;
358
- console.log(gray(` ${type.padEnd(12)} ${count}× × ${SAVINGS_PER_TYPE[type] || 100} = `) + cyan(fmtTokens(tokens)));
359
- }
360
-
361
- console.log();
362
- console.log(gray(" * Estimates based on typical back-and-forth cost per entry type."));
363
- console.log(gray(" Actual savings vary with model, project complexity, and session length."));
364
- }
365
-
366
- console.log();
367
- console.log(SEP);
368
- console.log();
369
- }
370
-
371
- // ── entry point ───────────────────────────────────────────────────────────────
372
-
373
- export async function statsCommand(args = []) {
374
- const jsonMode = args.includes("--json");
375
- const briefMode = args.includes("--brief");
376
-
377
- const cwd = process.cwd();
378
-
379
- if (!fs.existsSync(path.join(cwd, INFERNO_DIR))) {
380
- console.error(red(" ✘ inferno/ not found — run: infernoflow init\n"));
381
- process.exit(1);
382
- }
383
-
384
- const stats = collectStats(cwd);
385
-
386
- if (jsonMode) {
387
- console.log(JSON.stringify(stats, null, 2));
388
- return;
389
- }
390
-
391
- if (briefMode) {
392
- const parts = [];
393
- if (stats.memory.total) parts.push(`${stats.memory.total} memory entries`);
394
- if (stats.context.estimatedTokens) parts.push(`${fmtTokens(stats.context.estimatedTokens)} tokens/session`);
395
- if (stats.coverage.total) parts.push(`${stats.coverage.pct}% capability coverage`);
396
- if (stats.savings.estimatedTokens) parts.push(`${fmtTokens(stats.savings.estimatedTokens)} tokens saved`);
397
- console.log(parts.join(" · ") || "No data yet — run infernoflow init + infernoflow log");
398
- return;
399
- }
400
-
401
- printDashboard(stats);
402
- }
1
+ import*as y from"node:fs";import*as f from"node:path";import{bold as g,cyan as w,gray as t,green as v,yellow as E,red as j}from"../ui/output.mjs";const m="inferno",I=f.join(m,"sessions.jsonl"),N=f.join(m,"CONTEXT.md"),O=f.join(m,"theme.json"),A=f.join(m,"scan.json"),C=f.join(m,"contract.json"),M=f.join(m,"capabilities.json");function k(o){try{return JSON.parse(y.readFileSync(o,"utf8"))}catch{return null}}function F(o){return Math.ceil((o||"").length/4)}const $={gotcha:400,decision:200,attempt:250,preference:150,note:100,theme:300,handoff:500,error:200};function L(o){const e={ok:!1,memory:{total:0,byType:{},oldestEntry:null,newestEntry:null,sessionsTracked:0},context:{sizeBytes:0,estimatedTokens:0,hasIntent:!1,hasWorking:!1},theme:{captured:!1,fonts:0,colors:0,cssVars:0,framework:null},coverage:{total:0,withAnalysis:0,pct:0},chains:{total:0,resolved:0},contract:{policyId:null,capabilities:0,isLite:!1},savings:{estimatedTokens:0,breakdown:{}}},a=k(f.join(o,C));a&&(e.contract.policyId=a.policyId,e.contract.capabilities=(a.capabilities||[]).length,e.contract.isLite=!!a.lite,e.ok=!0);const c=k(f.join(o,M));if(c){const s=Array.isArray(c)?c:c.capabilities||[];e.coverage.total=s.length,e.coverage.withAnalysis=s.filter(i=>i.codeAnalysis).length,e.coverage.pct=e.coverage.total?Math.round(e.coverage.withAnalysis/e.coverage.total*100):0}const n=f.join(o,I);if(y.existsSync(n)){const i=y.readFileSync(n,"utf8").split(`
2
+ `).filter(Boolean).map(u=>{try{return JSON.parse(u)}catch{return null}}).filter(Boolean);e.memory.total=i.length;for(const u of i){const p=u.type||"note";e.memory.byType[p]=(e.memory.byType[p]||0)+1;const b=$[p]||100;e.savings.estimatedTokens+=b,e.savings.breakdown[p]=(e.savings.breakdown[p]||0)+b}i.length&&(e.memory.oldestEntry=i[0].ts,e.memory.newestEntry=i[i.length-1].ts);const x=new Set(i.map(u=>(u.ts||"").slice(0,10)));e.memory.sessionsTracked=x.size}const l=f.join(o,N);if(y.existsSync(l)){const s=y.readFileSync(l,"utf8");e.context.sizeBytes=Buffer.byteLength(s,"utf8"),e.context.estimatedTokens=F(s),e.context.hasIntent=s.includes("## Intent"),e.context.hasWorking=s.includes("## Working on")}const r=k(f.join(o,O));r&&(e.theme.captured=!0,e.theme.fonts=Object.keys(r.fonts||{}).filter(s=>r.fonts[s]).length,e.theme.colors=Object.keys(r.colors?.palette||{}).length,e.theme.cssVars=Object.keys(r.cssVars||{}).length,e.theme.framework=r.framework||null);const h=k(f.join(o,A));if(h?.httpChains){const s=Object.values(h.httpChains).flat();e.chains.total=s.length,e.chains.resolved=s.filter(i=>i.resolved).length}return e}function T(o,e,a=20){const c=e>0?Math.round(o/e*a):0;return"\u2588".repeat(c)+"\u2591".repeat(a-c)}function S(o){return o>=80?v:o>=40?E:j}function B(o){if(!o)return"never";const e=new Date(o),c=Date.now()-e.getTime(),n=Math.floor(c/864e5);return n===0?"today":n===1?"yesterday":n<7?`${n}d ago`:n<30?`${Math.floor(n/7)}w ago`:e.toLocaleDateString("en-GB",{day:"2-digit",month:"short"})}function d(o){return o>=1e3?`~${Math.round(o/100)/10}k`:`~${o}`}function P(o){const e=t(" "+"\u2500".repeat(52));console.log(),console.log(" "+g("\u{1F525} infernoflow stats")),o.contract.policyId&&console.log(t(` Project: ${o.contract.policyId}${o.contract.isLite?" (lite)":""}`)),console.log(e),console.log(),console.log(" "+g("Session memory")+t(" (inferno/sessions.jsonl)")),console.log();const a=o.memory.total;if(a===0)console.log(t(' No entries yet \u2014 run: infernoflow log "<what happened>" --type gotcha'));else{const n=["gotcha","decision","attempt","preference","theme","note","handoff","error"],l=Math.max(...Object.values(o.memory.byType));for(const r of n){const h=o.memory.byType[r]||0;if(h===0)continue;const s=T(h,l,16),i=r.padEnd(12);console.log(` ${t(i)} ${w(s)} ${h}`)}console.log(),console.log(t(" Total entries: ")+g(a)),console.log(t(" Sessions tracked: ")+o.memory.sessionsTracked),o.memory.newestEntry&&console.log(t(" Last entry: ")+B(o.memory.newestEntry))}if(console.log(),console.log(e),console.log(),console.log(" "+g("Context injection")+t(" (per session start)")),console.log(),o.context.sizeBytes===0?console.log(t(" No CONTEXT.md yet \u2014 run: infernoflow context")):(console.log(t(" Size: ")+`${Math.round(o.context.sizeBytes/1024*10)/10} KB`),console.log(t(" Tokens: ")+g(d(o.context.estimatedTokens))+t(" injected into every session")),o.context.hasIntent&&console.log(t(" ")+v("\u2714")+t(" Intent captured")),o.context.hasWorking&&console.log(t(" ")+v("\u2714")+t(" Working state captured"))),console.log(),console.log(e),console.log(),console.log(" "+g("Capability coverage")+t(" (code analysis via infernoflow scan)")),console.log(),o.coverage.total===0)console.log(t(" No capabilities yet \u2014 run: infernoflow init"));else{const n=S(o.coverage.pct),l=T(o.coverage.withAnalysis,o.coverage.total,24);if(console.log(` ${n(l)} ${g(o.coverage.pct+"%")} (${o.coverage.withAnalysis}/${o.coverage.total})`),o.coverage.pct<100){const r=o.coverage.total-o.coverage.withAnalysis;console.log(t(`
3
+ ${r} capabilities without code analysis`)),console.log(t(" Run: infernoflow scan to enrich them"))}}if(o.chains.total>0){console.log(),console.log(e),console.log(),console.log(" "+g("HTTP call chains")+t(" (end-to-end resolution)")),console.log();const n=Math.round(o.chains.resolved/o.chains.total*100),l=S(n),r=T(o.chains.resolved,o.chains.total,20);console.log(` ${l(r)} ${g(n+"%")} resolved (${o.chains.resolved}/${o.chains.total} call chains)`),o.chains.resolved<o.chains.total&&console.log(t(`
4
+ Unresolved calls may be to external services or missing route files`))}if(console.log(),console.log(e),console.log(),console.log(" "+g("Design system")+t(" (inferno/theme.json)")),console.log(),!o.theme.captured)console.log(t(" Not captured yet \u2014 run: infernoflow theme"));else{const n=[];o.theme.fonts&&n.push(`${o.theme.fonts} font${o.theme.fonts!==1?"s":""}`),o.theme.colors&&n.push(`${o.theme.colors} colors`),o.theme.cssVars&&n.push(`${o.theme.cssVars} CSS vars`),o.theme.framework&&n.push(`${o.theme.framework}`),console.log(t(" ")+v("\u2714")+" "+n.join(" \xB7 ")),console.log(t(" AI agents always use the correct fonts and colors for this project"))}console.log(),console.log(e),console.log(),console.log(" "+g("Estimated token savings")+t(" (vs re-discovering from scratch)")),console.log();const c=o.savings.estimatedTokens;if(c===0)console.log(t(" No session entries yet \u2014 start logging to track savings"));else{const n=Math.max(o.memory.sessionsTracked,1),l=Math.round(c/n);console.log(" Total saved: "+g(v(d(c)+" tokens"))),console.log(" Per session: "+g(d(l)+" tokens")),console.log(),console.log(t(" Breakdown:"));const r=["gotcha","handoff","attempt","decision","theme","preference","note","error"];for(const h of r){const s=o.savings.breakdown[h];if(!s)continue;const i=o.memory.byType[h]||0;console.log(t(` ${h.padEnd(12)} ${i}\xD7 \xD7 ${$[h]||100} = `)+w(d(s)))}console.log(),console.log(t(" * Estimates based on typical back-and-forth cost per entry type.")),console.log(t(" Actual savings vary with model, project complexity, and session length."))}console.log(),console.log(e),console.log()}async function D(o=[]){const e=o.includes("--json"),a=o.includes("--brief"),c=process.cwd();y.existsSync(f.join(c,m))||(console.error(j(` \u2718 inferno/ not found \u2014 run: infernoflow init
5
+ `)),process.exit(1));const n=L(c);if(e){console.log(JSON.stringify(n,null,2));return}if(a){const l=[];n.memory.total&&l.push(`${n.memory.total} memory entries`),n.context.estimatedTokens&&l.push(`${d(n.context.estimatedTokens)} tokens/session`),n.coverage.total&&l.push(`${n.coverage.pct}% capability coverage`),n.savings.estimatedTokens&&l.push(`${d(n.savings.estimatedTokens)} tokens saved`),console.log(l.join(" \xB7 ")||"No data yet \u2014 run infernoflow init + infernoflow log");return}P(n)}export{D as statsCommand};
@@ -1,172 +1,4 @@
1
- import * as fs from "node:fs";
2
- import * as path from "node:path";
3
- import { header, ok, fail, warn, section, bold, cyan, yellow, gray, green, red, white } from "../ui/output.mjs";
4
-
5
- function timeAgo(ms) {
6
- const s = Math.floor((Date.now() - ms) / 1000);
7
- if (s < 60) return "just now";
8
- if (s < 3600) return `${Math.floor(s / 60)}m ago`;
9
- if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
10
- return `${Math.floor(s / 86400)}d ago`;
11
- }
12
-
13
- function getCoverage(scenariosDir, caps) {
14
- const covered = new Set();
15
- if (fs.existsSync(scenariosDir)) {
16
- for (const f of fs.readdirSync(scenariosDir).filter(f => f.endsWith(".json"))) {
17
- try {
18
- const s = JSON.parse(fs.readFileSync(path.join(scenariosDir, f), "utf8"));
19
- (s.capabilitiesCovered || []).forEach(c => covered.add(c));
20
- } catch {}
21
- }
22
- }
23
- return { covered: caps.filter(c => covered.has(c)), uncovered: caps.filter(c => !covered.has(c)) };
24
- }
25
-
26
- export async function statusCommand(args = []) {
27
- const asJson = args.includes("--json");
28
- const cwd = process.cwd();
29
- const infernoDir = path.join(cwd, "inferno");
30
- if (!asJson) {
31
- header("status");
32
- }
33
-
34
- if (!fs.existsSync(infernoDir)) {
35
- if (asJson) {
36
- console.log(JSON.stringify({ ok: false, error: "inferno_not_found", hint: "Run: infernoflow init" }, null, 2));
37
- process.exit(1);
38
- }
39
- fail("inferno/ not found", `Run: infernoflow init`);
40
- console.log();
41
- process.exit(1);
42
- }
43
-
44
- const contractPath = path.join(infernoDir, "contract.json");
45
- if (!fs.existsSync(contractPath)) {
46
- if (asJson) {
47
- console.log(JSON.stringify({ ok: false, error: "contract_not_found" }, null, 2));
48
- process.exit(1);
49
- }
50
- fail("contract.json not found");
51
- console.log();
52
- process.exit(1);
53
- }
54
-
55
- const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"));
56
- const caps = contract.capabilities || [];
57
- const stat = fs.statSync(contractPath);
58
- const scenariosDir = path.join(infernoDir, "scenarios");
59
- const changelogPath = path.join(infernoDir, "CHANGELOG.md");
60
- const capsPath = path.join(infernoDir, "capabilities.json");
61
- const { covered, uncovered } = getCoverage(scenariosDir, caps);
62
-
63
- const hasChangelog = fs.existsSync(changelogPath) && /##\s+Unreleased/i.test(fs.readFileSync(changelogPath, "utf8"));
64
- const driftReasons = [];
65
- if (uncovered.length > 0) driftReasons.push(`${uncovered.length} capabilities without scenario coverage`);
66
- if (!hasChangelog) driftReasons.push("CHANGELOG missing ## Unreleased section");
67
- const allGood = driftReasons.length === 0;
68
-
69
- if (asJson) {
70
- const payload = {
71
- ok: allGood,
72
- driftReasons,
73
- project: {
74
- policyId: contract.policyId || null,
75
- policyVersion: contract.policyVersion || null,
76
- lastChange: timeAgo(stat.mtimeMs),
77
- },
78
- capabilities: {
79
- total: caps.length,
80
- uncovered,
81
- },
82
- changelog: {
83
- hasUnreleased: hasChangelog,
84
- },
85
- };
86
- console.log(JSON.stringify(payload, null, 2));
87
- process.exit(allGood ? 0 : 1);
88
- }
89
-
90
- if (!allGood) {
91
- section("Drift");
92
- driftReasons.forEach((reason) => console.log(` ${yellow("⚠")} ${reason}`));
93
- }
94
-
95
- // ── Project ─────────────────────────────────────────────────────
96
- section("Project");
97
- console.log(` ${gray("policy")} ${bold(contract.policyId || "—")}`);
98
- console.log(` ${gray("version")} ${bold("v" + (contract.policyVersion || "?"))}`);
99
- console.log(` ${gray("last change")} ${gray(timeAgo(stat.mtimeMs))}`);
100
-
101
- // ── Capabilities ─────────────────────────────────────────────────
102
- section(`Capabilities ${gray("(" + caps.length + ")")}`);
103
-
104
- let capsRegistry = {};
105
- if (fs.existsSync(capsPath)) {
106
- try {
107
- const reg = JSON.parse(fs.readFileSync(capsPath, "utf8"));
108
- (reg.capabilities || []).forEach(c => { capsRegistry[c.id] = c; });
109
- } catch {}
110
- }
111
-
112
- caps.forEach(cap => {
113
- const reg = capsRegistry[cap];
114
- const hasCoverage = covered.includes(cap);
115
- const icon = hasCoverage ? green("✔") : red("✘");
116
- const title = reg?.title ? gray(` — ${reg.title}`) : "";
117
- const since = reg?.since ? gray(` [${reg.since}]`) : "";
118
- console.log(` ${icon} ${white(cap)}${title}${since}`);
119
- });
120
-
121
- if (uncovered.length > 0) {
122
- console.log(`\n ${yellow("⚠")} ${uncovered.length} capability(ies) lack scenario coverage`);
123
- } else {
124
- console.log(`\n ${green("✔")} All capabilities have scenario coverage`);
125
- }
126
-
127
- // ── Scenarios ─────────────────────────────────────────────────────
128
- section("Scenarios");
129
- if (fs.existsSync(scenariosDir)) {
130
- const files = fs.readdirSync(scenariosDir).filter(f => f.endsWith(".json"));
131
- if (files.length === 0) {
132
- warn("No scenario files — add .json files to inferno/scenarios/");
133
- } else {
134
- files.forEach(f => {
135
- try {
136
- const s = JSON.parse(fs.readFileSync(path.join(scenariosDir, f), "utf8"));
137
- const steps = s.steps?.length || 0;
138
- const capCount = (s.capabilitiesCovered || []).length;
139
- console.log(` ${green("✔")} ${cyan(f)} ${gray(`— ${steps} steps, ${capCount} caps covered`)}`);
140
- } catch {
141
- console.log(` ${red("✘")} ${cyan(f)} ${gray("— invalid JSON")}`);
142
- }
143
- });
144
- }
145
- } else {
146
- warn("scenarios/ directory not found");
147
- }
148
-
149
- // ── Changelog ─────────────────────────────────────────────────────
150
- section("Changelog");
151
- if (fs.existsSync(changelogPath)) {
152
- const txt = fs.readFileSync(changelogPath, "utf8");
153
- if (/##\s+Unreleased/i.test(txt)) {
154
- ok("Has ## Unreleased section");
155
- } else {
156
- fail("Missing ## Unreleased section");
157
- }
158
- const sections = txt.split("\n").filter(l => /^##\s/.test(l)).slice(0, 3);
159
- sections.forEach(l => console.log(` ${gray(l)}`));
160
- } else {
161
- fail("inferno/CHANGELOG.md not found");
162
- }
163
-
164
- // ── Health ────────────────────────────────────────────────────────
165
- console.log();
166
- if (allGood) {
167
- console.log(` ${green("●")} ${bold(green("ready"))} ${gray("— run infernoflow check for full validation")}`);
168
- } else {
169
- console.log(` ${yellow("●")} ${bold(yellow("needs attention"))} ${gray("— run infernoflow check for details")}`);
170
- }
171
- console.log();
172
- }
1
+ import*as o from"node:fs";import*as a from"node:path";import{header as A,ok as H,fail as j,warn as J,section as p,bold as m,cyan as k,yellow as x,gray as t,green as u,red as G,white as P}from"../ui/output.mjs";function M(c){const e=Math.floor((Date.now()-c)/1e3);return e<60?"just now":e<3600?`${Math.floor(e/60)}m ago`:e<86400?`${Math.floor(e/3600)}h ago`:`${Math.floor(e/86400)}d ago`}function R(c,e){const g=new Set;if(o.existsSync(c))for(const i of o.readdirSync(c).filter(f=>f.endsWith(".json")))try{(JSON.parse(o.readFileSync(a.join(c,i),"utf8")).capabilitiesCovered||[]).forEach(l=>g.add(l))}catch{}return{covered:e.filter(i=>g.has(i)),uncovered:e.filter(i=>!g.has(i))}}async function I(c=[]){const e=c.includes("--json"),g=process.cwd(),i=a.join(g,"inferno");e||A("status"),o.existsSync(i)||(e&&(console.log(JSON.stringify({ok:!1,error:"inferno_not_found",hint:"Run: infernoflow init"},null,2)),process.exit(1)),j("inferno/ not found","Run: infernoflow init"),console.log(),process.exit(1));const f=a.join(i,"contract.json");o.existsSync(f)||(e&&(console.log(JSON.stringify({ok:!1,error:"contract_not_found"},null,2)),process.exit(1)),j("contract.json not found"),console.log(),process.exit(1));const l=JSON.parse(o.readFileSync(f,"utf8")),y=l.capabilities||[],N=o.statSync(f),$=a.join(i,"scenarios"),S=a.join(i,"CHANGELOG.md"),b=a.join(i,"capabilities.json"),{covered:F,uncovered:d}=R($,y),O=o.existsSync(S)&&/##\s+Unreleased/i.test(o.readFileSync(S,"utf8")),h=[];d.length>0&&h.push(`${d.length} capabilities without scenario coverage`),O||h.push("CHANGELOG missing ## Unreleased section");const v=h.length===0;if(e){const n={ok:v,driftReasons:h,project:{policyId:l.policyId||null,policyVersion:l.policyVersion||null,lastChange:M(N.mtimeMs)},capabilities:{total:y.length,uncovered:d},changelog:{hasUnreleased:O}};console.log(JSON.stringify(n,null,2)),process.exit(v?0:1)}v||(p("Drift"),h.forEach(n=>console.log(` ${x("\u26A0")} ${n}`))),p("Project"),console.log(` ${t("policy")} ${m(l.policyId||"\u2014")}`),console.log(` ${t("version")} ${m("v"+(l.policyVersion||"?"))}`),console.log(` ${t("last change")} ${t(M(N.mtimeMs))}`),p(`Capabilities ${t("("+y.length+")")}`);let E={};if(o.existsSync(b))try{(JSON.parse(o.readFileSync(b,"utf8")).capabilities||[]).forEach(s=>{E[s.id]=s})}catch{}if(y.forEach(n=>{const s=E[n],C=F.includes(n)?u("\u2714"):G("\u2718"),w=s?.title?t(` \u2014 ${s.title}`):"",U=s?.since?t(` [${s.since}]`):"";console.log(` ${C} ${P(n)}${w}${U}`)}),d.length>0?console.log(`
2
+ ${x("\u26A0")} ${d.length} capability(ies) lack scenario coverage`):console.log(`
3
+ ${u("\u2714")} All capabilities have scenario coverage`),p("Scenarios"),o.existsSync($)){const n=o.readdirSync($).filter(s=>s.endsWith(".json"));n.length===0?J("No scenario files \u2014 add .json files to inferno/scenarios/"):n.forEach(s=>{try{const r=JSON.parse(o.readFileSync(a.join($,s),"utf8")),C=r.steps?.length||0,w=(r.capabilitiesCovered||[]).length;console.log(` ${u("\u2714")} ${k(s)} ${t(`\u2014 ${C} steps, ${w} caps covered`)}`)}catch{console.log(` ${G("\u2718")} ${k(s)} ${t("\u2014 invalid JSON")}`)}})}else J("scenarios/ directory not found");if(p("Changelog"),o.existsSync(S)){const n=o.readFileSync(S,"utf8");/##\s+Unreleased/i.test(n)?H("Has ## Unreleased section"):j("Missing ## Unreleased section"),n.split(`
4
+ `).filter(r=>/^##\s/.test(r)).slice(0,3).forEach(r=>console.log(` ${t(r)}`))}else j("inferno/CHANGELOG.md not found");console.log(),console.log(v?` ${u("\u25CF")} ${m(u("ready"))} ${t("\u2014 run infernoflow check for full validation")}`:` ${x("\u25CF")} ${m(x("needs attention"))} ${t("\u2014 run infernoflow check for details")}`),console.log()}export{I as statusCommand};