@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.
- package/.claude/helpers/auto-memory-hook.mjs +109 -10
- package/.claude/helpers/helpers.manifest.json +4 -4
- package/.claude/helpers/intelligence.cjs +57 -4
- package/README.md +9 -3
- package/bin/cli.js +11 -1
- package/bin/mcp-server.js +11 -1
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/coverage-reader.d.ts +29 -0
- package/dist/src/commands/coverage-reader.js +247 -0
- package/dist/src/commands/doctor-native-addon.d.ts +23 -0
- package/dist/src/commands/doctor-native-addon.js +135 -0
- package/dist/src/commands/doctor.js +3 -0
- package/dist/src/commands/hooks.js +17 -181
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init.d.ts +2 -0
- package/dist/src/commands/init.js +37 -5
- package/dist/src/commands/mods.d.ts +3 -0
- package/dist/src/commands/mods.js +19 -5
- package/dist/src/commands/tui.d.ts +12 -0
- package/dist/src/commands/tui.js +98 -0
- package/dist/src/fs-secure.d.ts +25 -0
- package/dist/src/fs-secure.js +51 -2
- package/dist/src/init/executor.js +11 -14
- package/dist/src/init/helper-commands.d.ts +2 -0
- package/dist/src/init/helper-commands.js +2 -0
- package/dist/src/init/helpers-generator.js +4 -1
- package/dist/src/init/settings-generator.js +3 -3
- package/dist/src/mcp-server.js +10 -2
- package/dist/src/mcp-tools/capability-brain.d.ts +1 -1
- package/dist/src/mcp-tools/capability-brain.js +1 -1
- package/dist/src/mcp-tools/hooks-tools.js +26 -4
- package/dist/src/mcp-tools/memory-tools.js +2 -1
- package/dist/src/memory/live-memory-row.d.ts +3 -1
- package/dist/src/memory/live-memory-row.js +3 -1
- package/dist/src/memory/memory-bridge.d.ts +4 -0
- package/dist/src/memory/memory-bridge.js +3 -2
- package/dist/src/memory/memory-initializer.d.ts +4 -0
- package/dist/src/memory/memory-initializer.js +12 -11
- package/dist/src/mods/claude-installs.js +7 -3
- package/dist/src/mods/install.js +32 -12
- package/dist/src/mods/plugin-resolve.js +3 -1
- package/dist/src/mods/probe.js +92 -4
- package/dist/src/services/command-recording.d.ts +17 -0
- package/dist/src/services/command-recording.js +91 -0
- package/dist/src/services/policy-ledger-anchor.js +20 -2
- package/node_modules/@claude-flow/codex/README.md +20 -3
- package/node_modules/@claude-flow/codex/dist/cli.js +7 -0
- package/node_modules/@claude-flow/codex/dist/cli.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
- package/node_modules/@claude-flow/codex/dist/index.js +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts +8 -3
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.js +75 -13
- package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/types.d.ts +8 -0
- package/node_modules/@claude-flow/codex/dist/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/package.json +3 -3
- 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 {
|
|
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
|
-
|
|
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'),
|