@claude-flow/cli 3.56.6 → 3.56.8

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 (59) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +109 -10
  2. package/.claude/helpers/helpers.manifest.json +4 -4
  3. package/.claude/helpers/intelligence.cjs +57 -4
  4. package/README.md +9 -3
  5. package/bin/cli.js +11 -1
  6. package/bin/mcp-server.js +11 -1
  7. package/catalog-manifest.json +2 -2
  8. package/dist/src/commands/coverage-reader.d.ts +29 -0
  9. package/dist/src/commands/coverage-reader.js +247 -0
  10. package/dist/src/commands/doctor-native-addon.d.ts +23 -0
  11. package/dist/src/commands/doctor-native-addon.js +135 -0
  12. package/dist/src/commands/doctor.js +3 -0
  13. package/dist/src/commands/hooks.js +17 -181
  14. package/dist/src/commands/index.js +2 -0
  15. package/dist/src/commands/init.d.ts +2 -0
  16. package/dist/src/commands/init.js +37 -5
  17. package/dist/src/commands/mods.d.ts +3 -0
  18. package/dist/src/commands/mods.js +19 -5
  19. package/dist/src/commands/tui.d.ts +12 -0
  20. package/dist/src/commands/tui.js +98 -0
  21. package/dist/src/fs-secure.d.ts +25 -0
  22. package/dist/src/fs-secure.js +51 -2
  23. package/dist/src/init/executor.js +11 -14
  24. package/dist/src/init/helper-commands.d.ts +2 -0
  25. package/dist/src/init/helper-commands.js +2 -0
  26. package/dist/src/init/helpers-generator.js +4 -1
  27. package/dist/src/init/settings-generator.js +3 -3
  28. package/dist/src/mcp-server.js +10 -2
  29. package/dist/src/mcp-tools/capability-brain.d.ts +1 -1
  30. package/dist/src/mcp-tools/capability-brain.js +1 -1
  31. package/dist/src/mcp-tools/hooks-tools.js +26 -4
  32. package/dist/src/mcp-tools/memory-tools.js +2 -1
  33. package/dist/src/memory/live-memory-row.d.ts +3 -1
  34. package/dist/src/memory/live-memory-row.js +3 -1
  35. package/dist/src/memory/memory-bridge.d.ts +4 -0
  36. package/dist/src/memory/memory-bridge.js +3 -2
  37. package/dist/src/memory/memory-initializer.d.ts +4 -0
  38. package/dist/src/memory/memory-initializer.js +12 -11
  39. package/dist/src/mods/claude-installs.js +7 -3
  40. package/dist/src/mods/install.js +32 -12
  41. package/dist/src/mods/plugin-resolve.js +3 -1
  42. package/dist/src/mods/probe.js +92 -4
  43. package/dist/src/services/command-recording.d.ts +17 -0
  44. package/dist/src/services/command-recording.js +91 -0
  45. package/dist/src/services/policy-ledger-anchor.js +20 -2
  46. package/node_modules/@claude-flow/codex/README.md +20 -3
  47. package/node_modules/@claude-flow/codex/dist/cli.js +7 -0
  48. package/node_modules/@claude-flow/codex/dist/cli.js.map +1 -1
  49. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  50. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  51. package/node_modules/@claude-flow/codex/dist/initializer.d.ts +8 -3
  52. package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
  53. package/node_modules/@claude-flow/codex/dist/initializer.js +75 -13
  54. package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
  55. package/node_modules/@claude-flow/codex/dist/types.d.ts +8 -0
  56. package/node_modules/@claude-flow/codex/dist/types.d.ts.map +1 -1
  57. package/node_modules/@claude-flow/codex/package.json +1 -1
  58. package/package.json +3 -3
  59. package/plugins/ruflo-console/dist/tui.mjs +279 -0
@@ -0,0 +1,247 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ /** Coverage reports are build output the project controls: refuse anything large enough to stall or exhaust the process. */
4
+ const MAX_REPORT_BYTES = 64 * 1024 * 1024;
5
+ /** The file's text, or undefined when it is missing, not a regular file, unreadable, or over the size cap. */
6
+ function readReport(path) {
7
+ try {
8
+ const stat = statSync(path);
9
+ if (!stat.isFile() || stat.size > MAX_REPORT_BYTES)
10
+ return undefined;
11
+ return readFileSync(path, 'utf-8');
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ /**
18
+ * Read coverage data from disk. Checks these locations in order:
19
+ * 1. coverage/coverage-summary.json (Jest/Istanbul)
20
+ * 2. coverage/lcov.info (lcov format)
21
+ * 3. coverage/coverage-final.json (Istanbul counters)
22
+ * 4. .nyc_output/out.json (nyc counters or legacy summary)
23
+ */
24
+ export function readCoverageFromDisk() {
25
+ const cwd = process.cwd();
26
+ const noData = {
27
+ found: false,
28
+ source: 'none',
29
+ entries: [],
30
+ summary: { totalFiles: 0, overallLineCoverage: 0, overallBranchCoverage: 0, overallFunctionCoverage: 0, overallStatementCoverage: 0 },
31
+ };
32
+ // 1. Try coverage-summary.json (Jest/Istanbul)
33
+ for (const relPath of ['coverage/coverage-summary.json', 'coverage-summary.json']) {
34
+ const text = readReport(join(cwd, relPath));
35
+ if (text !== undefined) {
36
+ try {
37
+ const raw = JSON.parse(text);
38
+ const report = parseCoverageSummaryJson(raw, relPath);
39
+ if (report.found)
40
+ return report;
41
+ }
42
+ catch {
43
+ // malformed, try next
44
+ }
45
+ }
46
+ }
47
+ // 2. Try lcov.info
48
+ for (const relPath of ['coverage/lcov.info', 'lcov.info']) {
49
+ const raw = readReport(join(cwd, relPath));
50
+ if (raw !== undefined) {
51
+ try {
52
+ const report = parseLcovInfo(raw, relPath);
53
+ if (report.found)
54
+ return report;
55
+ }
56
+ catch {
57
+ // malformed, try next
58
+ }
59
+ }
60
+ }
61
+ // Istanbul's raw counter map (Jest, Vitest, nyc).
62
+ for (const relPath of ['coverage/coverage-final.json', 'coverage-final.json', '.nyc_output/out.json']) {
63
+ const text = readReport(join(cwd, relPath));
64
+ if (text === undefined)
65
+ continue;
66
+ try {
67
+ const raw = JSON.parse(text);
68
+ const report = parseIstanbulCoverage(raw, relPath);
69
+ if (report.found)
70
+ return report;
71
+ const legacy = parseCoverageSummaryJson(raw, relPath);
72
+ if (legacy.found)
73
+ return legacy;
74
+ }
75
+ catch {
76
+ // Missing or malformed, try the next supported report.
77
+ }
78
+ }
79
+ return noData;
80
+ }
81
+ function parseCoverageSummaryJson(data, source) {
82
+ const entries = [];
83
+ const metricsByFile = [];
84
+ for (const [filePath, metrics] of Object.entries(data)) {
85
+ if (filePath === 'total')
86
+ continue;
87
+ const m = metrics;
88
+ if (!m || typeof m !== 'object')
89
+ continue;
90
+ if (!['lines', 'branches', 'functions', 'statements'].some(key => m[key]))
91
+ continue;
92
+ metricsByFile.push(m);
93
+ const linePct = percentage(m.lines) ?? 0;
94
+ const branchPct = (percentage(m.branches) ?? 0);
95
+ const funcPct = (percentage(m.functions) ?? 0);
96
+ const stmtPct = (percentage(m.statements) ?? 0);
97
+ entries.push({ filePath, lines: linePct, branches: branchPct, functions: funcPct, statements: stmtPct });
98
+ }
99
+ // Only aggregate complete counts. A missing denominator is unknown, not empty.
100
+ const total = data['total'];
101
+ const aggregate = (key) => {
102
+ const supplied = percentage(total?.[key]);
103
+ if (supplied !== undefined)
104
+ return supplied;
105
+ const metrics = metricsByFile.map(file => file[key]);
106
+ if (metrics.length === 0 || !metrics.every(hasCounts))
107
+ return 0;
108
+ return ratio(metrics.reduce((sum, m) => sum + m.covered, 0), metrics.reduce((sum, m) => sum + m.total, 0));
109
+ };
110
+ // Sort by lowest line coverage
111
+ entries.sort((a, b) => a.lines - b.lines);
112
+ return {
113
+ found: entries.length > 0,
114
+ source,
115
+ entries,
116
+ summary: {
117
+ totalFiles: entries.length,
118
+ overallLineCoverage: aggregate('lines'),
119
+ overallBranchCoverage: aggregate('branches'),
120
+ overallFunctionCoverage: aggregate('functions'),
121
+ overallStatementCoverage: aggregate('statements'),
122
+ },
123
+ };
124
+ }
125
+ function parseLcovInfo(raw, source) {
126
+ const entries = [];
127
+ let currentFile = '';
128
+ let linesHit = 0, linesFound = 0;
129
+ let branchesHit = 0, branchesFound = 0;
130
+ let functionsHit = 0, functionsFound = 0;
131
+ let totalLH = 0, totalLF = 0, totalBH = 0, totalBF = 0, totalFH = 0, totalFF = 0;
132
+ const flushRecord = () => {
133
+ if (currentFile) {
134
+ // A corrupt record cannot report more hits than lines, or a negative count.
135
+ linesHit = Math.min(linesHit, linesFound);
136
+ branchesHit = Math.min(branchesHit, branchesFound);
137
+ functionsHit = Math.min(functionsHit, functionsFound);
138
+ totalLH += linesHit;
139
+ totalLF += linesFound;
140
+ totalBH += branchesHit;
141
+ totalBF += branchesFound;
142
+ totalFH += functionsHit;
143
+ totalFF += functionsFound;
144
+ entries.push({
145
+ filePath: currentFile,
146
+ lines: linesFound > 0 ? (linesHit / linesFound) * 100 : 100,
147
+ branches: branchesFound > 0 ? (branchesHit / branchesFound) * 100 : 100,
148
+ functions: functionsFound > 0 ? (functionsHit / functionsFound) * 100 : 100,
149
+ statements: linesFound > 0 ? (linesHit / linesFound) * 100 : 100,
150
+ });
151
+ }
152
+ };
153
+ for (const line of raw.split('\n')) {
154
+ const trimmed = line.trim();
155
+ if (trimmed.startsWith('SF:')) {
156
+ flushRecord();
157
+ currentFile = trimmed.slice(3);
158
+ linesHit = 0;
159
+ linesFound = 0;
160
+ branchesHit = 0;
161
+ branchesFound = 0;
162
+ functionsHit = 0;
163
+ functionsFound = 0;
164
+ }
165
+ else if (trimmed.startsWith('LH:')) {
166
+ linesHit = count(trimmed.slice(3));
167
+ }
168
+ else if (trimmed.startsWith('LF:')) {
169
+ linesFound = count(trimmed.slice(3));
170
+ }
171
+ else if (trimmed.startsWith('BRH:')) {
172
+ branchesHit = count(trimmed.slice(4));
173
+ }
174
+ else if (trimmed.startsWith('BRF:')) {
175
+ branchesFound = count(trimmed.slice(4));
176
+ }
177
+ else if (trimmed.startsWith('FNH:')) {
178
+ functionsHit = count(trimmed.slice(4));
179
+ }
180
+ else if (trimmed.startsWith('FNF:')) {
181
+ functionsFound = count(trimmed.slice(4));
182
+ }
183
+ else if (trimmed === 'end_of_record') {
184
+ flushRecord();
185
+ currentFile = '';
186
+ }
187
+ }
188
+ flushRecord();
189
+ entries.sort((a, b) => a.lines - b.lines);
190
+ return {
191
+ found: entries.length > 0,
192
+ source,
193
+ entries,
194
+ summary: {
195
+ totalFiles: entries.length,
196
+ overallLineCoverage: ratio(totalLH, totalLF),
197
+ overallBranchCoverage: ratio(totalBH, totalBF),
198
+ overallFunctionCoverage: ratio(totalFH, totalFF),
199
+ overallStatementCoverage: ratio(totalLH, totalLF),
200
+ },
201
+ };
202
+ }
203
+ /** A non-negative integer count; anything else in a report line is treated as 0. */
204
+ function count(text) {
205
+ const n = parseInt(text, 10);
206
+ return Number.isFinite(n) && n > 0 ? n : 0;
207
+ }
208
+ function ratio(covered, total) {
209
+ return total > 0 ? covered / total * 100 : 100;
210
+ }
211
+ function hasCounts(metric) {
212
+ return typeof metric?.total === 'number' && Number.isFinite(metric.total) && metric.total >= 0
213
+ && typeof metric.covered === 'number' && Number.isFinite(metric.covered)
214
+ && metric.covered >= 0 && metric.covered <= metric.total;
215
+ }
216
+ function percentage(metric) {
217
+ if (typeof metric?.pct === 'number' && Number.isFinite(metric.pct)
218
+ && metric.pct >= 0 && metric.pct <= 100)
219
+ return metric.pct;
220
+ return hasCounts(metric) ? ratio(metric.covered, metric.total) : undefined;
221
+ }
222
+ /** Istanbul line coverage uses the maximum statement hit count on each start line. */
223
+ function parseIstanbulCoverage(data, source) {
224
+ const summary = {};
225
+ for (const [filePath, value] of Object.entries(data)) {
226
+ const cov = value;
227
+ if (!cov || !cov.s || !cov.f || !cov.b || !cov.statementMap)
228
+ continue;
229
+ const lines = new Map();
230
+ for (const [id, hits] of Object.entries(cov.s)) {
231
+ const line = cov.statementMap[id]?.start?.line;
232
+ if (!Number.isInteger(line) || line < 1)
233
+ throw new Error('Invalid Istanbul statement location');
234
+ lines.set(line, Math.max(lines.get(line) ?? 0, hits));
235
+ }
236
+ const metric = (hits) => {
237
+ if (hits.some(n => !Number.isFinite(n) || n < 0))
238
+ throw new Error('Invalid Istanbul counter');
239
+ const covered = hits.filter(n => n > 0).length;
240
+ return { total: hits.length, covered, pct: ratio(covered, hits.length) };
241
+ };
242
+ summary[filePath] = { lines: metric([...lines.values()]), statements: metric(Object.values(cov.s)),
243
+ functions: metric(Object.values(cov.f)), branches: metric(Object.values(cov.b).flat()) };
244
+ }
245
+ return parseCoverageSummaryJson(summary, source);
246
+ }
247
+ //# sourceMappingURL=coverage-reader.js.map
@@ -0,0 +1,23 @@
1
+ export interface NativeAddonCheck {
2
+ name: string;
3
+ status: 'pass' | 'warn' | 'fail';
4
+ message: string;
5
+ fix?: string;
6
+ }
7
+ /** Node 24.19.0 up to, but not including, 24.22.0. Re-check this bound when a release ships the fix. */
8
+ export declare function isAffectedNodeVersion(version: string): boolean;
9
+ /** The Node version of the headers a copy was built against, from its config.gypi. */
10
+ export declare function headersVersionFromGypi(gypi: string): string | null;
11
+ /** `better-sqlite3` copies in node_modules next to, or one level under, each ancestor of the start dirs. */
12
+ export declare function findBetterSqlite3Copies(startDirs: string[], maxDepth?: number): string[];
13
+ export interface NativeAddonCheckDeps {
14
+ /** Runtime version, default process.versions.node. */
15
+ nodeVersion?: string;
16
+ /** Exact better-sqlite3 package directories to inspect (skips discovery). */
17
+ copies?: string[];
18
+ /** Directories to walk up from when discovering copies. */
19
+ startDirs?: string[];
20
+ }
21
+ /** Read-only: warn when a locally compiled better-sqlite3 was built against Node >= 24.19 headers on an affected runtime. */
22
+ export declare function checkNativeAddonHeaders(deps?: NativeAddonCheckDeps): Promise<NativeAddonCheck>;
23
+ //# sourceMappingURL=doctor-native-addon.d.ts.map
@@ -0,0 +1,135 @@
1
+ /**
2
+ * #4055: native addons compiled against Node >= 24.19 headers abort when a
3
+ * garbage-collected ObjectWrap (a better-sqlite3 Statement) calls
4
+ * RemoveEnvironmentCleanupHook() with no Environment (nodejs/node#65446).
5
+ * The fix (abe1b673e9b, PR #65943) was in no release through v24.21.0.
6
+ *
7
+ * Only copies that were COMPILED LOCALLY carry a build/config.gypi naming the
8
+ * headers they were built against; prebuilt binaries (prebuild-install) do not,
9
+ * so they are never flagged. The check is read-only and never loads an addon.
10
+ */
11
+ import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
12
+ import { dirname, join, resolve } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ const NAME = 'Native addon headers (Node 24)';
15
+ function parse(v) {
16
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v);
17
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
18
+ }
19
+ /** Node 24.19.0 up to, but not including, 24.22.0. Re-check this bound when a release ships the fix. */
20
+ export function isAffectedNodeVersion(version) {
21
+ const v = parse(version);
22
+ if (!v || v[0] !== 24)
23
+ return false;
24
+ return v[1] >= 19 && v[1] <= 21;
25
+ }
26
+ /** The Node version of the headers a copy was built against, from its config.gypi. */
27
+ export function headersVersionFromGypi(gypi) {
28
+ const field = /["']node_version["']\s*:\s*["']v?(\d+\.\d+\.\d+)["']/.exec(gypi);
29
+ if (field)
30
+ return field[1];
31
+ const dir = /["']nodedir["']\s*:\s*["']([^"']*)["']/.exec(gypi);
32
+ if (!dir)
33
+ return null;
34
+ const all = [...dir[1].matchAll(/(\d+)\.(\d+)\.(\d+)/g)];
35
+ const last = all[all.length - 1];
36
+ return last ? `${last[1]}.${last[2]}.${last[3]}` : null;
37
+ }
38
+ function readJson(path) {
39
+ try {
40
+ return JSON.parse(readFileSync(path, 'utf8'));
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ function listDir(path) {
47
+ try {
48
+ return readdirSync(path);
49
+ }
50
+ catch {
51
+ return [];
52
+ }
53
+ }
54
+ /** `better-sqlite3` copies in node_modules next to, or one level under, each ancestor of the start dirs. */
55
+ export function findBetterSqlite3Copies(startDirs, maxDepth = 12) {
56
+ const found = new Set();
57
+ const add = (dir) => {
58
+ if (!existsSync(join(dir, 'package.json')))
59
+ return;
60
+ try {
61
+ found.add(realpathSync(dir));
62
+ }
63
+ catch { /* unreadable */ }
64
+ };
65
+ const scanNodeModules = (nm) => {
66
+ add(join(nm, 'better-sqlite3'));
67
+ for (const entry of listDir(nm)) {
68
+ if (entry.startsWith('.') || entry === 'better-sqlite3')
69
+ continue;
70
+ if (entry.startsWith('@')) {
71
+ for (const sub of listDir(join(nm, entry)))
72
+ add(join(nm, entry, sub, 'node_modules', 'better-sqlite3'));
73
+ }
74
+ else {
75
+ add(join(nm, entry, 'node_modules', 'better-sqlite3'));
76
+ }
77
+ }
78
+ };
79
+ for (const start of startDirs) {
80
+ let dir = resolve(start);
81
+ for (let i = 0; i < maxDepth; i++) {
82
+ scanNodeModules(join(dir, 'node_modules'));
83
+ const parent = dirname(dir);
84
+ if (parent === dir)
85
+ break;
86
+ dir = parent;
87
+ }
88
+ }
89
+ return [...found].sort();
90
+ }
91
+ const FIX = [
92
+ 'Use a Node release that contains nodejs/node#65943 (not in any release through 24.21.0), or Node 22 / 24.18.1;',
93
+ 'or rebuild each listed copy against 24.18.1 headers:',
94
+ 'cd <copy> && npx node-gyp rebuild --release --nodedir=<path to node 24.18.1 headers> ;',
95
+ 'or install the 12.x prebuilt (npm install better-sqlite3@12 in the project).',
96
+ 'ruflo itself opens AgentDB through its own prebuilt 12.x copy, so a flagged nested copy matters only to code that loads it directly.',
97
+ ].join(' ');
98
+ /** Read-only: warn when a locally compiled better-sqlite3 was built against Node >= 24.19 headers on an affected runtime. */
99
+ export async function checkNativeAddonHeaders(deps = {}) {
100
+ const nodeVersion = deps.nodeVersion ?? process.versions.node;
101
+ if (!isAffectedNodeVersion(nodeVersion)) {
102
+ return { name: NAME, status: 'pass', message: `Node ${nodeVersion} is not in the affected range (24.19.0-24.21.x)` };
103
+ }
104
+ const here = dirname(fileURLToPath(import.meta.url));
105
+ const copies = deps.copies ?? findBetterSqlite3Copies(deps.startDirs ?? [here, process.cwd()]);
106
+ if (copies.length === 0) {
107
+ return { name: NAME, status: 'pass', message: `Node ${nodeVersion}: no better-sqlite3 copy found to inspect` };
108
+ }
109
+ const bad = [];
110
+ for (const copy of copies) {
111
+ let gypi;
112
+ try {
113
+ gypi = readFileSync(join(copy, 'build', 'config.gypi'), 'utf8');
114
+ }
115
+ catch {
116
+ continue;
117
+ }
118
+ const headers = headersVersionFromGypi(gypi);
119
+ const parsed = headers ? parse(headers) : null;
120
+ if (!parsed || parsed[0] !== 24 || parsed[1] < 19)
121
+ continue;
122
+ const version = readJson(join(copy, 'package.json'))?.version;
123
+ bad.push(`${copy} (better-sqlite3 ${typeof version === 'string' ? version : '?'}, built against Node ${headers} headers)`);
124
+ }
125
+ if (bad.length === 0) {
126
+ return { name: NAME, status: 'pass', message: `Node ${nodeVersion}: ${copies.length} better-sqlite3 cop${copies.length === 1 ? 'y' : 'ies'} checked, none built against 24.19+ headers` };
127
+ }
128
+ return {
129
+ name: NAME,
130
+ status: 'warn',
131
+ message: `Node ${nodeVersion} can abort ("Assertion failed: (env) != nullptr", nodejs/node#65446) in: ${bad.join('; ')}`,
132
+ fix: FIX,
133
+ };
134
+ }
135
+ //# sourceMappingURL=doctor-native-addon.js.map
@@ -10,6 +10,7 @@ import { join, dirname } from 'path';
10
10
  import { fileURLToPath, pathToFileURL } from 'url';
11
11
  import { createHash } from 'crypto';
12
12
  import { ownInstallAncestors } from '../plugins/own-install.js';
13
+ import { checkNativeAddonHeaders } from './doctor-native-addon.js';
13
14
  import { execSync, exec, execFile } from 'child_process';
14
15
  import { promisify } from 'util';
15
16
  import { decodeKey, isEncryptionEnabled } from '../encryption/vault.js';
@@ -2607,6 +2608,7 @@ export const doctorCommand = {
2607
2608
  ];
2608
2609
  },
2609
2610
  'memory-package': checkMemoryPackageVersion, // #3392
2611
+ 'native-addon': () => checkNativeAddonHeaders(), // #4055
2610
2612
  'learning': checkLearningBridge, // #2545
2611
2613
  'learning-bridge': checkLearningBridge, // #2545
2612
2614
  'api': checkApiKeys,
@@ -2722,6 +2724,7 @@ export const doctorCommand = {
2722
2724
  checkMemoryPersistenceDriver, // #2968/#3321 — read-only native capability probe
2723
2725
  checkLearningBridge, // #2545 — can the auto-memory hook actually load @claude-flow/memory?
2724
2726
  checkMemoryPackageVersion, // #3392 — loaded @claude-flow/memory must satisfy the CLI's declared range
2727
+ () => checkNativeAddonHeaders(), // #4055 — better-sqlite3 built against Node 24.19+ headers on 24.19-24.21
2725
2728
  checkApiKeys,
2726
2729
  checkMcpServers,
2727
2730
  checkMcpSchemaOverhead, // #2726 — fixed tools/list prompt cost
@@ -1,10 +1,14 @@
1
+ /**
2
+ * V3 CLI Hooks Command
3
+ * Self-learning hooks system for intelligent workflow automation
4
+ */
1
5
  import { validateFeedbackPatterns } from '../memory/feedback-patterns.js';
6
+ import { readCoverageFromDisk } from './coverage-reader.js';
2
7
  import { output } from '../output.js';
3
8
  import { confirm } from '../prompt.js';
4
9
  import { callMCPTool, MCPClientError } from '../mcp-client.js';
5
10
  import { storeCommand } from './transfer-store.js';
6
- import { existsSync, readFileSync, statSync } from 'node:fs';
7
- import { join } from 'node:path';
11
+ import { readFileSync, statSync } from 'node:fs';
8
12
  import { getSecurityStatus as sharedGetSecurityStatus, getSwarmStatus as sharedGetSwarmStatus, getGitUncommittedCount as sharedGetGitUncommittedCount, } from '../funnel/local-signals.js';
9
13
  /**
10
14
  * #1686 — `?? 0` only defaults null/undefined; NaN slips through and
@@ -16,181 +20,6 @@ function safeNum(value, fallback = 0) {
16
20
  const n = typeof value === 'number' ? value : Number(value);
17
21
  return Number.isFinite(n) ? n : fallback;
18
22
  }
19
- /**
20
- * Read coverage data from disk. Checks these locations in order:
21
- * 1. coverage/coverage-summary.json (Jest/Istanbul)
22
- * 2. coverage/lcov.info (lcov format)
23
- * 3. .nyc_output/out.json (nyc)
24
- */
25
- function readCoverageFromDisk() {
26
- const cwd = process.cwd();
27
- const noData = {
28
- found: false,
29
- source: 'none',
30
- entries: [],
31
- summary: { totalFiles: 0, overallLineCoverage: 0, overallBranchCoverage: 0, overallFunctionCoverage: 0, overallStatementCoverage: 0 },
32
- };
33
- // 1. Try coverage-summary.json (Jest/Istanbul)
34
- for (const relPath of ['coverage/coverage-summary.json', 'coverage-summary.json']) {
35
- const summaryPath = join(cwd, relPath);
36
- if (existsSync(summaryPath)) {
37
- try {
38
- const raw = JSON.parse(readFileSync(summaryPath, 'utf-8'));
39
- return parseCoverageSummaryJson(raw, relPath);
40
- }
41
- catch {
42
- // malformed, try next
43
- }
44
- }
45
- }
46
- // 2. Try lcov.info
47
- for (const relPath of ['coverage/lcov.info', 'lcov.info']) {
48
- const lcovPath = join(cwd, relPath);
49
- if (existsSync(lcovPath)) {
50
- try {
51
- const raw = readFileSync(lcovPath, 'utf-8');
52
- return parseLcovInfo(raw, relPath);
53
- }
54
- catch {
55
- // malformed, try next
56
- }
57
- }
58
- }
59
- // 3. Try .nyc_output/out.json
60
- const nycPath = join(cwd, '.nyc_output', 'out.json');
61
- if (existsSync(nycPath)) {
62
- try {
63
- const raw = JSON.parse(readFileSync(nycPath, 'utf-8'));
64
- return parseCoverageSummaryJson(raw, '.nyc_output/out.json');
65
- }
66
- catch {
67
- // malformed
68
- }
69
- }
70
- return noData;
71
- }
72
- function parseCoverageSummaryJson(data, source) {
73
- const entries = [];
74
- let totalLines = 0, coveredLines = 0;
75
- let totalBranches = 0, coveredBranches = 0;
76
- let totalFunctions = 0, coveredFunctions = 0;
77
- let totalStatements = 0, coveredStatements = 0;
78
- for (const [filePath, metrics] of Object.entries(data)) {
79
- if (filePath === 'total')
80
- continue;
81
- const m = metrics;
82
- if (!m || typeof m !== 'object')
83
- continue;
84
- const linePct = m.lines?.pct ?? m.lines?.covered != null ? ((m.lines?.covered ?? 0) / Math.max(m.lines?.total ?? 1, 1)) * 100 : 0;
85
- const branchPct = m.branches?.pct ?? (m.branches?.total ? ((m.branches?.covered ?? 0) / m.branches.total) * 100 : 100);
86
- const funcPct = m.functions?.pct ?? (m.functions?.total ? ((m.functions?.covered ?? 0) / m.functions.total) * 100 : 100);
87
- const stmtPct = m.statements?.pct ?? (m.statements?.total ? ((m.statements?.covered ?? 0) / m.statements.total) * 100 : 100);
88
- entries.push({ filePath, lines: linePct, branches: branchPct, functions: funcPct, statements: stmtPct });
89
- totalLines += m.lines?.total ?? 0;
90
- coveredLines += m.lines?.covered ?? 0;
91
- totalBranches += m.branches?.total ?? 0;
92
- coveredBranches += m.branches?.covered ?? 0;
93
- totalFunctions += m.functions?.total ?? 0;
94
- coveredFunctions += m.functions?.covered ?? 0;
95
- totalStatements += m.statements?.total ?? 0;
96
- coveredStatements += m.statements?.covered ?? 0;
97
- }
98
- // Also read the total key if present
99
- const total = data['total'];
100
- const overallLine = total?.lines?.pct ?? (totalLines > 0 ? (coveredLines / totalLines) * 100 : 0);
101
- const overallBranch = total?.branches?.pct ?? (totalBranches > 0 ? (coveredBranches / totalBranches) * 100 : 0);
102
- const overallFunction = total?.functions?.pct ?? (totalFunctions > 0 ? (coveredFunctions / totalFunctions) * 100 : 0);
103
- const overallStatement = total?.statements?.pct ?? (totalStatements > 0 ? (coveredStatements / totalStatements) * 100 : 0);
104
- // Sort by lowest line coverage
105
- entries.sort((a, b) => a.lines - b.lines);
106
- return {
107
- found: true,
108
- source,
109
- entries,
110
- summary: {
111
- totalFiles: entries.length,
112
- overallLineCoverage: overallLine,
113
- overallBranchCoverage: overallBranch,
114
- overallFunctionCoverage: overallFunction,
115
- overallStatementCoverage: overallStatement,
116
- },
117
- };
118
- }
119
- function parseLcovInfo(raw, source) {
120
- const entries = [];
121
- let currentFile = '';
122
- let linesHit = 0, linesFound = 0;
123
- let branchesHit = 0, branchesFound = 0;
124
- let functionsHit = 0, functionsFound = 0;
125
- const flushRecord = () => {
126
- if (currentFile) {
127
- entries.push({
128
- filePath: currentFile,
129
- lines: linesFound > 0 ? (linesHit / linesFound) * 100 : 0,
130
- branches: branchesFound > 0 ? (branchesHit / branchesFound) * 100 : 100,
131
- functions: functionsFound > 0 ? (functionsHit / functionsFound) * 100 : 100,
132
- statements: linesFound > 0 ? (linesHit / linesFound) * 100 : 0,
133
- });
134
- }
135
- };
136
- for (const line of raw.split('\n')) {
137
- const trimmed = line.trim();
138
- if (trimmed.startsWith('SF:')) {
139
- currentFile = trimmed.slice(3);
140
- linesHit = 0;
141
- linesFound = 0;
142
- branchesHit = 0;
143
- branchesFound = 0;
144
- functionsHit = 0;
145
- functionsFound = 0;
146
- }
147
- else if (trimmed.startsWith('LH:')) {
148
- linesHit = parseInt(trimmed.slice(3), 10) || 0;
149
- }
150
- else if (trimmed.startsWith('LF:')) {
151
- linesFound = parseInt(trimmed.slice(3), 10) || 0;
152
- }
153
- else if (trimmed.startsWith('BRH:')) {
154
- branchesHit = parseInt(trimmed.slice(4), 10) || 0;
155
- }
156
- else if (trimmed.startsWith('BRF:')) {
157
- branchesFound = parseInt(trimmed.slice(4), 10) || 0;
158
- }
159
- else if (trimmed.startsWith('FNH:')) {
160
- functionsHit = parseInt(trimmed.slice(4), 10) || 0;
161
- }
162
- else if (trimmed.startsWith('FNF:')) {
163
- functionsFound = parseInt(trimmed.slice(4), 10) || 0;
164
- }
165
- else if (trimmed === 'end_of_record') {
166
- flushRecord();
167
- currentFile = '';
168
- }
169
- }
170
- flushRecord();
171
- entries.sort((a, b) => a.lines - b.lines);
172
- let totalLH = 0, totalLF = 0, totalBH = 0, totalBF = 0;
173
- for (const e of entries) {
174
- // Approximate from percentages (we lost exact counts after flush, but summaries are okay)
175
- totalLH += e.lines;
176
- totalLF += 100;
177
- totalBH += e.branches;
178
- totalBF += 100;
179
- }
180
- const n = entries.length || 1;
181
- return {
182
- found: true,
183
- source,
184
- entries,
185
- summary: {
186
- totalFiles: entries.length,
187
- overallLineCoverage: totalLH / n,
188
- overallBranchCoverage: totalBH / n,
189
- overallFunctionCoverage: 0,
190
- overallStatementCoverage: totalLH / n,
191
- },
192
- };
193
- }
194
23
  /**
195
24
  * Classify a coverage gap by priority type based on coverage percentage and threshold
196
25
  */
@@ -599,9 +428,16 @@ const postCommandCommand = {
599
428
  ttl: ctx.flags.ttl,
600
429
  timestamp: Date.now(),
601
430
  });
431
+ // #4056: recording switched off on purpose is not a failure.
432
+ const recordingOff = result.disabled === true;
602
433
  if (ctx.flags.format === 'json') {
603
434
  output.printJson(result);
604
- return { success: result.recorded, exitCode: result.recorded ? 0 : 1, data: result };
435
+ const ok = result.recorded || recordingOff;
436
+ return { success: ok, exitCode: ok ? 0 : 1, data: result };
437
+ }
438
+ if (recordingOff) {
439
+ output.printInfo('Command recording is switched off (CLAUDE_FLOW_HOOKS_RECORD_COMMANDS / CLAUDE_FLOW_HOOKS_ENABLED / hooks.recordCommands); nothing was stored.');
440
+ return { success: true, data: result };
605
441
  }
606
442
  if (!result.recorded) {
607
443
  output.printError('Command outcome could not be recorded');
@@ -2989,7 +2825,7 @@ const coverageRouteCommand = {
2989
2825
  'c8: npx c8 npm test',
2990
2826
  ]);
2991
2827
  output.writeln();
2992
- output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, or .nyc_output/out.json'));
2828
+ output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, coverage/coverage-final.json, or .nyc_output/out.json'));
2993
2829
  return { success: false, exitCode: 1 };
2994
2830
  }
2995
2831
  }
@@ -3180,7 +3016,7 @@ const coverageSuggestCommand = {
3180
3016
  'c8: npx c8 npm test',
3181
3017
  ]);
3182
3018
  output.writeln();
3183
- output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, or .nyc_output/out.json'));
3019
+ output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, coverage/coverage-final.json, or .nyc_output/out.json'));
3184
3020
  return { success: false, exitCode: 1 };
3185
3021
  }
3186
3022
  }
@@ -3400,7 +3236,7 @@ const coverageGapsCommand = {
3400
3236
  'c8: npx c8 npm test',
3401
3237
  ]);
3402
3238
  output.writeln();
3403
- output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, or .nyc_output/out.json'));
3239
+ output.writeln(output.dim('Expected files: coverage/coverage-summary.json, coverage/lcov.info, coverage/coverage-final.json, or .nyc_output/out.json'));
3404
3240
  return { success: false, exitCode: 1 };
3405
3241
  }
3406
3242
  }
@@ -90,6 +90,8 @@ const commandLoaders = {
90
90
  spinner: () => import('./spinner.js'),
91
91
  // ruflo as a Claude Code mod: function hooks, early access (ADR-404)
92
92
  mods: () => import('./mods.js'),
93
+ // ADR-489: the console in a terminal of its own (read-only Overview and Cost); loads a bundle from this CLI's own install.
94
+ tui: () => import('./tui.js'),
93
95
  // ADR-406 command catalog and mission control (interim homes until mods.ts follow-up)
94
96
  catalog: () => import('./catalog.js'),
95
97
  mission: () => import('./mission.js'),