claude-flow 3.56.9 → 3.56.10
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/.proven-config-version +1 -0
- package/.claude/helpers/intelligence.cjs +51 -9
- package/.claude/proven-config.json +42 -0
- package/.claude/skills/verification-quality/SKILL.md +14 -11
- package/README.md +1 -1
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
- package/package.json +2 -2
- package/v3/@claude-flow/cli/dist/src/autopilot-state.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/autopilot-state.js +36 -8
- package/v3/@claude-flow/cli/dist/src/command-resolution.d.ts +15 -0
- package/v3/@claude-flow/cli/dist/src/command-resolution.js +25 -0
- package/v3/@claude-flow/cli/dist/src/commands/agent.js +17 -17
- package/v3/@claude-flow/cli/dist/src/commands/claims.js +6 -8
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +86 -31
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +14 -2
- package/v3/@claude-flow/cli/dist/src/commands/security-code-patterns.d.ts +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/security-code-patterns.js +61 -3
- package/v3/@claude-flow/cli/dist/src/commands/security.d.ts +24 -0
- package/v3/@claude-flow/cli/dist/src/commands/security.js +81 -35
- package/v3/@claude-flow/cli/dist/src/commands/tui.d.ts +9 -0
- package/v3/@claude-flow/cli/dist/src/commands/tui.js +29 -3
- package/v3/@claude-flow/cli/dist/src/config/proven-config-refresh.d.ts +9 -0
- package/v3/@claude-flow/cli/dist/src/config/proven-config-refresh.js +24 -3
- package/v3/@claude-flow/cli/dist/src/funnel/local-signals.js +2 -1
- package/v3/@claude-flow/cli/dist/src/index.js +6 -42
- package/v3/@claude-flow/cli/dist/src/init/types.js +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +15 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-health-metrics.d.ts +22 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-health-metrics.js +33 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +45 -22
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-federation.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-federation.js +7 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +25 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/aidefence-known-misses.d.ts +22 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/aidefence-known-misses.js +57 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/analyze-tools.js +10 -11
- package/v3/@claude-flow/cli/dist/src/mcp-tools/autopilot-tools.js +2 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +6 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/embeddings-tools.js +29 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +4 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +70 -20
- package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +28 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/ruvllm-tools.js +6 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/security-tools.js +55 -17
- package/v3/@claude-flow/cli/dist/src/mcp-tools/workflow-tools.js +66 -17
- package/v3/@claude-flow/cli/dist/src/memory/intelligence.d.ts +5 -1
- package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +105 -29
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +10 -3
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +2 -1
- package/v3/@claude-flow/cli/dist/src/memory/search-defaults.d.ts +54 -0
- package/v3/@claude-flow/cli/dist/src/memory/search-defaults.js +37 -0
- package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.d.ts +28 -0
- package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.js +59 -2
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +24 -24
- package/v3/@claude-flow/cli/dist/src/ruvector/agent-wasm.d.ts +10 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/agent-wasm.js +27 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/diff-classifier.d.ts +2 -4
- package/v3/@claude-flow/cli/dist/src/ruvector/diff-classifier.js +202 -69
- package/v3/@claude-flow/cli/dist/src/ruvector/ruvllm-wasm.d.ts +4 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/ruvllm-wasm.js +12 -1
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +7 -1
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +11 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +37 -0
- package/v3/@claude-flow/cli/dist/src/transfer/store/discovery.js +16 -17
- package/v3/@claude-flow/cli/dist/src/transfer/store/types.d.ts +4 -0
- package/v3/@claude-flow/cli/package.json +4 -4
|
@@ -164,37 +164,44 @@ const startCommand = {
|
|
|
164
164
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
165
165
|
}
|
|
166
166
|
catch { /* exists */ }
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
return { success: true };
|
|
167
|
+
lockFd = tryAcquireDaemonLock(lockFile);
|
|
168
|
+
if (lockFd === null) {
|
|
169
|
+
// Another `daemon start` is mid-spawn. Wait for it while its holder
|
|
170
|
+
// is alive; only a lock whose holder is DEAD is stale and may be
|
|
171
|
+
// retaken — atomically, via O_EXCL again, never by proceeding
|
|
172
|
+
// lockless. The old fixed 5s timeout + unlink let every waiter
|
|
173
|
+
// through on Windows, where the holder's killStaleDaemons
|
|
174
|
+
// (`tasklist /v`) routinely takes longer than that, and each waiter
|
|
175
|
+
// then forked its own daemon (3 per workspace observed).
|
|
176
|
+
const waitMs = Number(process.env.RUFLO_DAEMON_LOCK_WAIT_MS) > 0
|
|
177
|
+
? Number(process.env.RUFLO_DAEMON_LOCK_WAIT_MS)
|
|
178
|
+
: 60_000;
|
|
179
|
+
const deadline = Date.now() + waitMs;
|
|
180
|
+
while (lockFd === null) {
|
|
181
|
+
const winnerPid = getBackgroundDaemonPid(projectRoot);
|
|
182
|
+
if (winnerPid && isProcessRunning(winnerPid)) {
|
|
183
|
+
if (!quiet) {
|
|
184
|
+
output.printWarning(`Daemon already running in background (PID: ${winnerPid}). Stop it first with: daemon stop`);
|
|
186
185
|
}
|
|
186
|
+
return { success: true };
|
|
187
187
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
if (daemonLockIsStale(lockFile)) {
|
|
189
|
+
try {
|
|
190
|
+
fs.unlinkSync(lockFile);
|
|
191
|
+
}
|
|
192
|
+
catch { /* raced with another taker */ }
|
|
193
|
+
lockFd = tryAcquireDaemonLock(lockFile);
|
|
194
|
+
continue;
|
|
193
195
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
if (Date.now() >= deadline) {
|
|
197
|
+
// A live process is still starting the daemon. Spawning another
|
|
198
|
+
// would be the duplicate this lock exists to prevent.
|
|
199
|
+
if (!quiet) {
|
|
200
|
+
output.printWarning('Another `daemon start` is still in progress for this workspace; not starting a second daemon.');
|
|
201
|
+
}
|
|
202
|
+
return { success: true };
|
|
203
|
+
}
|
|
204
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
198
205
|
}
|
|
199
206
|
}
|
|
200
207
|
// Dedup check while holding the lock.
|
|
@@ -298,15 +305,22 @@ const startCommand = {
|
|
|
298
305
|
// NOTE: Do NOT write PID file here — startDaemon() writes it internally.
|
|
299
306
|
// Writing it before startDaemon() causes checkExistingDaemon() to detect
|
|
300
307
|
// our own PID and return early, leaving no workers scheduled (#1478 Bug 1).
|
|
301
|
-
// Clean up PID file on exit
|
|
308
|
+
// Clean up PID file on exit — only if it is OURS. A process that lost
|
|
309
|
+
// the singleton race must never delete the winner's PID file, or the
|
|
310
|
+
// next auto-start sees "no daemon" and forks yet another one.
|
|
302
311
|
const cleanup = () => {
|
|
303
312
|
try {
|
|
304
|
-
if (fs.existsSync(pidFile)) {
|
|
313
|
+
if (fs.existsSync(pidFile) && fs.readFileSync(pidFile, 'utf-8').trim() === String(process.pid)) {
|
|
305
314
|
fs.unlinkSync(pidFile);
|
|
306
315
|
}
|
|
307
316
|
}
|
|
308
317
|
catch { /* ignore */ }
|
|
309
318
|
};
|
|
319
|
+
// WorkerDaemon.start() declines (returns without running) when another
|
|
320
|
+
// live daemon already owns daemon.pid. Such a process must exit rather
|
|
321
|
+
// than park on the keep-alive below: it has no workers and no TTL, so
|
|
322
|
+
// it would linger as an idle duplicate forever.
|
|
323
|
+
const declined = (d) => !d.getStatus().running;
|
|
310
324
|
process.on('exit', cleanup);
|
|
311
325
|
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
312
326
|
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
@@ -319,6 +333,11 @@ const startCommand = {
|
|
|
319
333
|
spinner.start();
|
|
320
334
|
const daemon = await startDaemon(projectRoot, config);
|
|
321
335
|
const status = daemon.getStatus();
|
|
336
|
+
if (declined(daemon)) {
|
|
337
|
+
spinner.stop();
|
|
338
|
+
output.printWarning(`Daemon already running (PID: ${getBackgroundDaemonPid(projectRoot) ?? 'unknown'}); not starting a second one.`);
|
|
339
|
+
return { success: true };
|
|
340
|
+
}
|
|
322
341
|
spinner.succeed('Worker daemon started (foreground mode)');
|
|
323
342
|
output.writeln();
|
|
324
343
|
output.printBox([
|
|
@@ -371,7 +390,9 @@ const startCommand = {
|
|
|
371
390
|
await new Promise(() => { }); // Never resolves - daemon runs until killed
|
|
372
391
|
}
|
|
373
392
|
else {
|
|
374
|
-
await startDaemon(projectRoot, config);
|
|
393
|
+
const daemon = await startDaemon(projectRoot, config);
|
|
394
|
+
if (declined(daemon))
|
|
395
|
+
return { success: true };
|
|
375
396
|
setInterval(() => { }, 60_000); // Keep alive with ref'd handle (#1478)
|
|
376
397
|
await new Promise(() => { }); // Keep alive
|
|
377
398
|
}
|
|
@@ -910,6 +931,40 @@ function getBackgroundDaemonPid(projectRoot) {
|
|
|
910
931
|
return null;
|
|
911
932
|
}
|
|
912
933
|
}
|
|
934
|
+
/**
|
|
935
|
+
* Atomically create the launcher lock and stamp it with our PID.
|
|
936
|
+
* Returns the fd, or null when another process holds the lock.
|
|
937
|
+
*/
|
|
938
|
+
function tryAcquireDaemonLock(lockFile) {
|
|
939
|
+
try {
|
|
940
|
+
const fd = fs.openSync(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|
941
|
+
fs.writeSync(fd, String(process.pid));
|
|
942
|
+
return fd;
|
|
943
|
+
}
|
|
944
|
+
catch (e) {
|
|
945
|
+
if (e.code === 'EEXIST')
|
|
946
|
+
return null;
|
|
947
|
+
throw e;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* A lock is stale only when its holder is gone. An empty lock is the brief
|
|
952
|
+
* window between O_EXCL create and the PID write, so it counts as held
|
|
953
|
+
* unless it has been empty for a while (holder crashed in that window).
|
|
954
|
+
*/
|
|
955
|
+
function daemonLockIsStale(lockFile) {
|
|
956
|
+
try {
|
|
957
|
+
const raw = fs.readFileSync(lockFile, 'utf-8').trim();
|
|
958
|
+
const pid = parseInt(raw, 10);
|
|
959
|
+
if (!raw || Number.isNaN(pid)) {
|
|
960
|
+
return Date.now() - fs.statSync(lockFile).mtimeMs > 10_000;
|
|
961
|
+
}
|
|
962
|
+
return pid !== process.pid && !isProcessRunning(pid);
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
return false; // vanished: the next O_EXCL attempt decides
|
|
966
|
+
}
|
|
967
|
+
}
|
|
913
968
|
/**
|
|
914
969
|
* Check if a process is running
|
|
915
970
|
*/
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Memory operations for AgentDB integration
|
|
4
4
|
*/
|
|
5
5
|
import { validateAppendConditions } from '../memory/append-conditions.js';
|
|
6
|
+
import { DEFAULT_SEARCH_THRESHOLD, nearMisses } from '../memory/search-defaults.js';
|
|
6
7
|
import { output } from '../output.js';
|
|
7
8
|
import { select, confirm, input } from '../prompt.js';
|
|
8
9
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
@@ -430,7 +431,7 @@ const searchCommand = {
|
|
|
430
431
|
// default to 0.7, which silently re-broke #2558: `memory search` matched
|
|
431
432
|
// content word-for-word and still returned nothing. Regression guard:
|
|
432
433
|
// __tests__/memory-search-recall-2558.test.ts.
|
|
433
|
-
default: 0.
|
|
434
|
+
default: 0.2
|
|
434
435
|
},
|
|
435
436
|
{
|
|
436
437
|
name: 'type',
|
|
@@ -499,7 +500,9 @@ const searchCommand = {
|
|
|
499
500
|
// coalescing preserves an explicit zero. Fallback aligned with the
|
|
500
501
|
// option's declared `default: 0.7` (was `0.3` — the two disagreed
|
|
501
502
|
// and --help advertised a default the code did not honor).
|
|
502
|
-
const threshold = ctx.flags.threshold ??
|
|
503
|
+
const threshold = ctx.flags.threshold ?? DEFAULT_SEARCH_THRESHOLD;
|
|
504
|
+
// The parser fills the option's declared default, so an untouched flag arrives as the default value.
|
|
505
|
+
const thresholdWasDefault = ctx.flags.threshold === undefined || ctx.flags.threshold === DEFAULT_SEARCH_THRESHOLD;
|
|
503
506
|
const searchType = ctx.flags.type || 'semantic';
|
|
504
507
|
const buildHnsw = (ctx.flags['build-hnsw'] || ctx.flags.buildHnsw);
|
|
505
508
|
const requestedIntent = ctx.flags.intent || 'mixed';
|
|
@@ -738,6 +741,15 @@ const searchCommand = {
|
|
|
738
741
|
output.writeln();
|
|
739
742
|
if (results.length === 0) {
|
|
740
743
|
output.printWarning('No results found');
|
|
744
|
+
if (thresholdWasDefault) {
|
|
745
|
+
const near = await nearMisses(searchEntries, { query, namespace, provenanceFilter, dbPath: dbPathSearch });
|
|
746
|
+
if (near) {
|
|
747
|
+
output.writeln(output.dim(` ${near.hint.replace(' in nearMisses', ' below').replace('Pass threshold: 0', 'Pass --threshold 0')}`));
|
|
748
|
+
for (const n of near.nearMisses) {
|
|
749
|
+
output.writeln(output.dim(` ${n.similarity.toFixed(2)} ${n.namespace}/${n.key}`));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
741
753
|
await warnIfSiblingHasRows(ctx.flags.path);
|
|
742
754
|
output.writeln(output.dim('Try: claude-flow memory store -k "key" --value "data"'));
|
|
743
755
|
return { success: true, data: [] };
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* - every rule is tested after comment text is blanked, so a comment naming `child_process.execFile` or `eval(` is
|
|
7
7
|
* not a finding;
|
|
8
8
|
* - eval needs a word start: "retrieval (" in prose is not a call; `eval(` and `window.eval(` still are;
|
|
9
|
-
* - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and
|
|
10
|
-
* execSync
|
|
9
|
+
* - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and also catches
|
|
10
|
+
* dynamic exec/execSync command strings when the import was on another line;
|
|
11
11
|
* - a pg placeholder such as `$${paramCount + 1}` is a parameter number, not a value in the SQL text. It is exempt only
|
|
12
12
|
* when its expression is a number, a loop letter, `params.length`-style, or a name containing `param` or `placeholder`, it
|
|
13
13
|
* stands where SQL takes a value, and it is not inside a quoted SQL string.
|
|
@@ -6,14 +6,72 @@
|
|
|
6
6
|
* - every rule is tested after comment text is blanked, so a comment naming `child_process.execFile` or `eval(` is
|
|
7
7
|
* not a finding;
|
|
8
8
|
* - eval needs a word start: "retrieval (" in prose is not a call; `eval(` and `window.eval(` still are;
|
|
9
|
-
* - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and
|
|
10
|
-
* execSync
|
|
9
|
+
* - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and also catches
|
|
10
|
+
* dynamic exec/execSync command strings when the import was on another line;
|
|
11
11
|
* - a pg placeholder such as `$${paramCount + 1}` is a parameter number, not a value in the SQL text. It is exempt only
|
|
12
12
|
* when its expression is a number, a loop letter, `params.length`-style, or a name containing `param` or `placeholder`, it
|
|
13
13
|
* stands where SQL takes a value, and it is not inside a quoted SQL string.
|
|
14
14
|
*/
|
|
15
15
|
/** No `g` flag, so `test` keeps no lastIndex between lines. */
|
|
16
16
|
const re = (pattern) => (line) => pattern.test(line);
|
|
17
|
+
/** Detect a dynamic shell command at an exec/execSync call site, independent of its import. */
|
|
18
|
+
const interpolatedExec = (line) => {
|
|
19
|
+
let quote = null;
|
|
20
|
+
for (let i = 0; i < line.length; i++) {
|
|
21
|
+
const ch = line[i];
|
|
22
|
+
if (quote !== null) {
|
|
23
|
+
if (ch === '\\') {
|
|
24
|
+
i++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (ch === quote)
|
|
28
|
+
quote = null;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
32
|
+
quote = ch;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (i > 0 && /[\w$]/.test(line[i - 1]))
|
|
36
|
+
continue;
|
|
37
|
+
const name = line.startsWith('execSync', i) ? 'execSync' : line.startsWith('exec', i) ? 'exec' : '';
|
|
38
|
+
if (!name)
|
|
39
|
+
continue;
|
|
40
|
+
let arg = i + name.length;
|
|
41
|
+
while (/\s/.test(line[arg] ?? ''))
|
|
42
|
+
arg++;
|
|
43
|
+
if (line[arg++] !== '(')
|
|
44
|
+
continue;
|
|
45
|
+
while (/\s/.test(line[arg] ?? ''))
|
|
46
|
+
arg++;
|
|
47
|
+
const delimiter = line[arg];
|
|
48
|
+
if (delimiter !== '`' && delimiter !== "'" && delimiter !== '"')
|
|
49
|
+
continue;
|
|
50
|
+
for (let end = arg + 1; end < line.length; end++) {
|
|
51
|
+
if (line[end] === '\\') {
|
|
52
|
+
end++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (delimiter === '`' && line[end] === '$' && line[end + 1] === '{')
|
|
56
|
+
return true;
|
|
57
|
+
if (line[end] !== delimiter)
|
|
58
|
+
continue;
|
|
59
|
+
if (delimiter !== '`') {
|
|
60
|
+
let next = end + 1;
|
|
61
|
+
while (/\s/.test(line[next] ?? ''))
|
|
62
|
+
next++;
|
|
63
|
+
if (line[next++] === '+') {
|
|
64
|
+
while (/\s/.test(line[next] ?? ''))
|
|
65
|
+
next++;
|
|
66
|
+
if (/[A-Za-z_$]/.test(line[next] ?? ''))
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
17
75
|
// A `.length` counts only on a param-, value- or arg-named array: `req.body.length` is whatever the client sent.
|
|
18
76
|
const COUNTER = /^\s*(?:\d+|[ijkn]|(?:[A-Za-z_$][\w$]*(?:Params?|Placeholders?|Values?|Args?)|params?|placeholders?|values?|args?)\.length|[A-Za-z_$]*(?:[Pp]aram|[Pp]laceholder)[\w$]*)\s*(?:[+-]\s*\d+\s*)?$/;
|
|
19
77
|
// Where a pg parameter stands in SQL: after a comparison, a list or call opener, or a clause keyword.
|
|
@@ -70,7 +128,7 @@ export const CODE_PATTERNS = [
|
|
|
70
128
|
{ match: re(/(?<![\w$])eval\s*\(/), type: 'Eval Usage', severity: 'medium', desc: 'eval() can execute arbitrary code' },
|
|
71
129
|
{ match: re(/innerHTML\s*=/), type: 'innerHTML', severity: 'medium', desc: 'XSS risk with innerHTML' },
|
|
72
130
|
{ match: re(/dangerouslySetInnerHTML/), type: 'React XSS', severity: 'medium', desc: 'React XSS risk' },
|
|
73
|
-
{ match:
|
|
131
|
+
{ match: line => /child_process.*exec/.test(line) || interpolatedExec(line), type: 'Command Injection', severity: 'high', desc: 'Possible command injection' },
|
|
74
132
|
{ match: line => sqlInterpolation(line) && sqlInterpolation(withoutPlaceholders(line)), type: 'SQL Injection', severity: 'high', desc: 'Possible SQL injection' },
|
|
75
133
|
];
|
|
76
134
|
/**
|
|
@@ -5,6 +5,30 @@
|
|
|
5
5
|
* Created with ❤️ by ruv.io
|
|
6
6
|
*/
|
|
7
7
|
import type { Command } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* #4124: `npm audit` can fail without exiting through the vulnerability path.
|
|
10
|
+
* In a pnpm/yarn/bun project there is no package-lock.json, so it prints
|
|
11
|
+
* `{"error":{"code":"ENOLOCK",...}}`. That has no `vulnerabilities` key, and
|
|
12
|
+
* both `scan` and `cve` used to read it as a clean tree. Only a report with a
|
|
13
|
+
* `vulnerabilities` object counts as a scan; anything else is "could not scan".
|
|
14
|
+
*/
|
|
15
|
+
type NpmAuditVuln = {
|
|
16
|
+
severity: string;
|
|
17
|
+
via: Array<{
|
|
18
|
+
title?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
source?: number;
|
|
21
|
+
name?: string;
|
|
22
|
+
} | string>;
|
|
23
|
+
};
|
|
24
|
+
export type NpmAuditResult = {
|
|
25
|
+
ok: true;
|
|
26
|
+
vulnerabilities: Record<string, NpmAuditVuln>;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
reason: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function interpretNpmAudit(raw: string, dir?: string): NpmAuditResult;
|
|
8
32
|
export declare const securityCommand: Command;
|
|
9
33
|
export default securityCommand;
|
|
10
34
|
//# sourceMappingURL=security.d.ts.map
|
|
@@ -155,6 +155,7 @@ const scanCommand = {
|
|
|
155
155
|
spinner.start();
|
|
156
156
|
const findings = [];
|
|
157
157
|
let criticalCount = 0, highCount = 0, mediumCount = 0, lowCount = 0;
|
|
158
|
+
let depsSkipped;
|
|
158
159
|
try {
|
|
159
160
|
const fs = await import('fs');
|
|
160
161
|
const path = await import('path');
|
|
@@ -178,35 +179,41 @@ const scanCommand = {
|
|
|
178
179
|
// npm audit exits non-zero when vulnerabilities found — stdout still has JSON
|
|
179
180
|
auditResult = (auditErr instanceof Error && 'stdout' in auditErr ? auditErr.stdout : undefined) || '{}';
|
|
180
181
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
182
|
+
const audit = interpretNpmAudit(auditResult, path.resolve(target));
|
|
183
|
+
if (!audit.ok) {
|
|
184
|
+
depsSkipped = audit.reason;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
for (const [pkg, vuln] of Object.entries(audit.vulnerabilities)) {
|
|
188
|
+
const sev = vuln.severity || 'low';
|
|
189
|
+
const first = Array.isArray(vuln.via) ? vuln.via[0] : undefined;
|
|
190
|
+
const title = typeof first === 'object' && first?.title ? first.title : 'Vulnerability';
|
|
191
|
+
if (sev === 'critical')
|
|
192
|
+
criticalCount++;
|
|
193
|
+
else if (sev === 'high')
|
|
194
|
+
highCount++;
|
|
195
|
+
else if (sev === 'moderate' || sev === 'medium')
|
|
196
|
+
mediumCount++;
|
|
197
|
+
else
|
|
198
|
+
lowCount++;
|
|
199
|
+
findings.push({
|
|
200
|
+
severity: sev === 'critical' ? 'critical' :
|
|
201
|
+
sev === 'high' ? 'high' :
|
|
202
|
+
sev === 'moderate' || sev === 'medium' ? 'medium' : 'low',
|
|
203
|
+
type: 'Dependency CVE',
|
|
204
|
+
location: `package.json:${pkg}`,
|
|
205
|
+
description: title.substring(0, 35),
|
|
206
|
+
});
|
|
204
207
|
}
|
|
205
208
|
}
|
|
206
|
-
catch { /* JSON parse failed, no vulns */ }
|
|
207
209
|
}
|
|
210
|
+
else {
|
|
211
|
+
depsSkipped = 'No package.json found in the scan target; dependency scan could not run.';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
depsSkipped = `npm audit could not run: ${err instanceof Error ? err.message : String(err)}`;
|
|
208
216
|
}
|
|
209
|
-
catch { /* npm audit failed */ }
|
|
210
217
|
}
|
|
211
218
|
// Phase 2: Scan for hardcoded secrets
|
|
212
219
|
if (scanType === 'all' || scanType === 'code') {
|
|
@@ -342,9 +349,14 @@ const scanCommand = {
|
|
|
342
349
|
output.writeln(output.dim(`... and ${findings.length - 20} more issues`));
|
|
343
350
|
}
|
|
344
351
|
}
|
|
352
|
+
else if (depsSkipped) {
|
|
353
|
+
output.writeln(output.warning('No issues found by the checks that ran, but the dependency scan did not run.'));
|
|
354
|
+
}
|
|
345
355
|
else {
|
|
346
356
|
output.writeln(output.success('No security issues found!'));
|
|
347
357
|
}
|
|
358
|
+
if (depsSkipped)
|
|
359
|
+
output.writeln(output.warning(`Dependency scan skipped: ${depsSkipped}`));
|
|
348
360
|
output.writeln();
|
|
349
361
|
output.printBox([
|
|
350
362
|
`Target: ${target}`,
|
|
@@ -367,6 +379,7 @@ const scanCommand = {
|
|
|
367
379
|
low: lowCount,
|
|
368
380
|
total: findings.length,
|
|
369
381
|
},
|
|
382
|
+
...(depsSkipped ? { incomplete: true, skipped: { deps: depsSkipped } } : {}),
|
|
370
383
|
findings,
|
|
371
384
|
};
|
|
372
385
|
// Persist the scan result so downstream consumers (the statusline's
|
|
@@ -408,6 +421,11 @@ const scanCommand = {
|
|
|
408
421
|
output.printJson(record);
|
|
409
422
|
else if (outputFormat === 'sarif')
|
|
410
423
|
output.printJson(scanSarif(findings));
|
|
424
|
+
if (depsSkipped) {
|
|
425
|
+
if (outputFormat !== 'text')
|
|
426
|
+
output.printError(`Dependency scan skipped: ${depsSkipped}`);
|
|
427
|
+
return { success: false, exitCode: 2 };
|
|
428
|
+
}
|
|
411
429
|
return { success: findings.length === 0 || (criticalCount === 0 && highCount === 0) };
|
|
412
430
|
}
|
|
413
431
|
catch (error) {
|
|
@@ -418,6 +436,36 @@ const scanCommand = {
|
|
|
418
436
|
}
|
|
419
437
|
},
|
|
420
438
|
};
|
|
439
|
+
export function interpretNpmAudit(raw, dir) {
|
|
440
|
+
let audit;
|
|
441
|
+
try {
|
|
442
|
+
audit = JSON.parse(raw);
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return { ok: false, reason: 'npm audit did not return a JSON report.' };
|
|
446
|
+
}
|
|
447
|
+
if (audit && typeof audit === 'object' && audit.error) {
|
|
448
|
+
const code = typeof audit.error.code === 'string' ? audit.error.code : 'error';
|
|
449
|
+
const summary = typeof audit.error.summary === 'string' ? audit.error.summary : 'npm audit failed.';
|
|
450
|
+
return { ok: false, reason: `npm audit failed (${code}): ${summary}${code === 'ENOLOCK' ? auditHint(dir) : ''}` };
|
|
451
|
+
}
|
|
452
|
+
if (!audit || typeof audit !== 'object' || !audit.vulnerabilities || typeof audit.vulnerabilities !== 'object' || Array.isArray(audit.vulnerabilities)) {
|
|
453
|
+
return { ok: false, reason: 'npm audit returned no vulnerability report.' };
|
|
454
|
+
}
|
|
455
|
+
return { ok: true, vulnerabilities: audit.vulnerabilities };
|
|
456
|
+
}
|
|
457
|
+
function auditHint(dir) {
|
|
458
|
+
if (!dir)
|
|
459
|
+
return '';
|
|
460
|
+
const has = (f) => existsSync(resolvePath(dir, f));
|
|
461
|
+
if (has('pnpm-lock.yaml'))
|
|
462
|
+
return ' This is a pnpm project: run `pnpm audit` instead.';
|
|
463
|
+
if (has('yarn.lock'))
|
|
464
|
+
return ' This is a yarn project: run `yarn npm audit` (Berry) or `yarn audit` (v1) instead.';
|
|
465
|
+
if (has('bun.lock') || has('bun.lockb'))
|
|
466
|
+
return ' This is a bun project: run `bun audit` instead.';
|
|
467
|
+
return ' Create one with `npm i --package-lock-only`, or use your package manager\'s audit.';
|
|
468
|
+
}
|
|
421
469
|
// CVE subcommand
|
|
422
470
|
const cveCommand = {
|
|
423
471
|
name: 'cve',
|
|
@@ -451,22 +499,20 @@ const cveCommand = {
|
|
|
451
499
|
// npm audit exits non-zero when vulnerabilities found — stdout still has JSON
|
|
452
500
|
auditJson = (e instanceof Error && 'stdout' in e ? e.stdout : '') || '{}';
|
|
453
501
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
catch {
|
|
459
|
-
output.writeln(output.warning('⚠ Could not parse `npm audit --json` output.'));
|
|
460
|
-
output.writeln(output.dim('Make sure you are inside a project with a package.json.'));
|
|
502
|
+
const audit = interpretNpmAudit(auditJson, process.cwd());
|
|
503
|
+
if (!audit.ok) {
|
|
504
|
+
output.writeln(output.warning(`⚠ Could not check dependencies: ${audit.reason}`));
|
|
505
|
+
output.writeln(output.dim('No result is reported rather than a false all-clear.'));
|
|
461
506
|
return { success: false, exitCode: 2 };
|
|
462
507
|
}
|
|
463
|
-
const vulns = audit.vulnerabilities
|
|
508
|
+
const vulns = audit.vulnerabilities;
|
|
464
509
|
const cveRows = [];
|
|
465
510
|
const CVE_RE = /CVE-\d{4}-\d{4,7}/g;
|
|
466
511
|
for (const [pkg, v] of Object.entries(vulns)) {
|
|
467
512
|
const sev = v.severity || 'low';
|
|
468
|
-
const
|
|
469
|
-
const
|
|
513
|
+
const via = (v.via || []).filter((x) => typeof x === 'object' && x !== null);
|
|
514
|
+
const titles = via.filter((x) => x.title).map((x) => x.title);
|
|
515
|
+
const urls = via.filter((x) => x.url).map((x) => x.url);
|
|
470
516
|
const allText = `${titles.join(' ')} ${urls.join(' ')}`;
|
|
471
517
|
const cveIds = Array.from(new Set(allText.match(CVE_RE) || []));
|
|
472
518
|
cveRows.push({ pkg, severity: sev, cveIds, title: titles[0] || 'Vulnerability', url: urls[0] });
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { Command } from '../types.js';
|
|
2
2
|
/** `<own install>/plugins/ruflo-console/dist/tui.mjs`, the first that exists. */
|
|
3
3
|
export declare function locateTuiBundle(startDir?: string): string | null;
|
|
4
|
+
/** The look and motion flags: `--look bbs|orbital|plain` (default bbs, as in the console), `--fps N` (0-30, default 8; 0 draws pictures still), `--no-anim`, `--no-pictures`. A bad value is ignored, never passed on. */
|
|
5
|
+
export declare function lookFlags(flags: Record<string, unknown> | undefined): {
|
|
6
|
+
look?: string;
|
|
7
|
+
fps?: number;
|
|
8
|
+
anim?: boolean;
|
|
9
|
+
pictures?: boolean;
|
|
10
|
+
boot?: boolean;
|
|
11
|
+
mouse?: boolean;
|
|
12
|
+
};
|
|
4
13
|
/** The AI page's settings from the flags (and one environment variable for the endpoint list). Absent flags leave the defaults; a bad number is ignored, never passed on. */
|
|
5
14
|
export declare function aiPage(flags: Record<string, unknown> | undefined): {
|
|
6
15
|
model?: string;
|
|
@@ -34,6 +34,20 @@ function cliEntry() {
|
|
|
34
34
|
const entry = join(__dirname, '..', '..', '..', 'bin', 'cli.js');
|
|
35
35
|
return existsSync(entry) ? entry : undefined;
|
|
36
36
|
}
|
|
37
|
+
/** The look and motion flags: `--look bbs|orbital|plain` (default bbs, as in the console), `--fps N` (0-30, default 8; 0 draws pictures still), `--no-anim`, `--no-pictures`. A bad value is ignored, never passed on. */
|
|
38
|
+
export function lookFlags(flags) {
|
|
39
|
+
const look = flags?.look;
|
|
40
|
+
const fps = flags?.fps;
|
|
41
|
+
const number = typeof fps === 'number' ? fps : typeof fps === 'string' && /^\d{1,2}$/.test(fps) ? Number(fps) : undefined;
|
|
42
|
+
return {
|
|
43
|
+
...(look === 'bbs' || look === 'orbital' || look === 'plain' ? { look } : {}),
|
|
44
|
+
...(number !== undefined && Number.isFinite(number) && number >= 0 && number <= 30 ? { fps: Math.floor(number) } : {}),
|
|
45
|
+
...(flags?.anim === false ? { anim: false } : {}),
|
|
46
|
+
...(flags?.pictures === false ? { pictures: false } : {}),
|
|
47
|
+
...(flags?.boot === true ? { boot: true } : {}),
|
|
48
|
+
...(flags?.mouse === true ? { mouse: true } : {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
37
51
|
/** The AI page's settings from the flags (and one environment variable for the endpoint list). Absent flags leave the defaults; a bad number is ignored, never passed on. */
|
|
38
52
|
export function aiPage(flags) {
|
|
39
53
|
const model = typeof flags?.model === 'string' && flags.model !== '' ? flags.model : undefined;
|
|
@@ -46,11 +60,17 @@ export const tuiCommand = {
|
|
|
46
60
|
name: 'tui',
|
|
47
61
|
description: 'The ruflo console in your terminal: Overview, Cost, Swarm, Events, Missions (read-only) and an AI page that asks Codex or OpenRouter, each question after a card.',
|
|
48
62
|
options: [
|
|
49
|
-
{ name: 'view', description: `Page to open: ${VIEWS.join(' | ')}`, type: 'string', default: 'overview' },
|
|
63
|
+
{ name: 'view', description: `Page to open: ${VIEWS.join(' | ')}, or any console view (menu, federation, settings, ...): 0 opens the menu and : goes to a page by name`, type: 'string', default: 'overview' },
|
|
50
64
|
{ name: 'ai', description: 'The AI page (ask Codex read-only or OpenRouter, each question after a confirm card). --no-ai turns it off', type: 'boolean', default: true },
|
|
51
65
|
{ name: 'model', description: 'OpenRouter model for this session (default openrouter/auto)', type: 'string' },
|
|
52
66
|
{ name: 'max-spend-usd', description: 'Stop sending to OpenRouter once the provider-reported spend reaches this (default 0.5)', type: 'number' },
|
|
53
67
|
{ name: 'convo-targets', description: 'Extra OpenAI-compatible endpoints, the console convoTargets syntax: endpoint:<name>=<https-base-url>|<KEY_ENV>|<model>;... (a key is named by its environment variable). Also RUFLO_TUI_CONVO_TARGETS', type: 'string' },
|
|
68
|
+
{ name: 'look', description: 'The console look: bbs (default, as in Claude Code) | orbital | plain. Key t cycles them', type: 'string' },
|
|
69
|
+
{ name: 'fps', description: 'Frames per second for moving pictures, 0-30 (default 8; 0 draws every picture still)', type: 'number' },
|
|
70
|
+
{ name: 'anim', description: 'Animate the pictures. --no-anim draws them still', type: 'boolean', default: true },
|
|
71
|
+
{ name: 'pictures', description: 'Draw the pictures of the console (title art, swarm graph, gauges). --no-pictures shows their words only', type: 'boolean', default: true },
|
|
72
|
+
{ name: 'mouse', description: 'Read mouse reports: click a page in the bar, scroll with the wheel (every action also has a key)', type: 'boolean', default: false },
|
|
73
|
+
{ name: 'boot', description: 'Play the look\'s start-up screen first (as the console\'s boot setting)', type: 'boolean', default: false },
|
|
54
74
|
{ name: 'plain', description: 'Print one frame as plain text and exit 0 (scripts and CI). Without it, a non-terminal gets a hint and exit 2', type: 'boolean', default: false },
|
|
55
75
|
],
|
|
56
76
|
examples: [
|
|
@@ -62,7 +82,9 @@ export const tuiCommand = {
|
|
|
62
82
|
async action(context) {
|
|
63
83
|
const ctx = context;
|
|
64
84
|
const wanted = ctx.flags?.view ?? ctx.args?.find(arg => !arg.startsWith('-')) ?? 'overview';
|
|
65
|
-
|
|
85
|
+
// The six pages are known here; any other console view (menu, federation, settings, …) is checked against the bundle's own list once it is loaded.
|
|
86
|
+
const isKnown = VIEWS.includes(wanted);
|
|
87
|
+
if (!isKnown && !/^[a-z]{2,16}$/.test(wanted)) {
|
|
66
88
|
output.writeln(output.error(`tui: unknown view "${String(wanted).slice(0, 40)}". Use one of: ${VIEWS.join(', ')}`));
|
|
67
89
|
return { success: false, exitCode: 2, data: { error: 'unknown-view' } };
|
|
68
90
|
}
|
|
@@ -80,12 +102,16 @@ export const tuiCommand = {
|
|
|
80
102
|
output.writeln(output.error(`tui: could not load the console bundle: ${error instanceof Error ? error.message.slice(0, 160) : 'unknown error'}`));
|
|
81
103
|
return { success: false, exitCode: 1, data: { error: 'bundle-load-failed' } };
|
|
82
104
|
}
|
|
105
|
+
if (!isKnown && mod.isPage?.(wanted) !== true) {
|
|
106
|
+
output.writeln(output.error(`tui: unknown view "${String(wanted).slice(0, 40)}". Use one of: ${VIEWS.join(', ')}, or any console view such as menu, federation, settings`));
|
|
107
|
+
return { success: false, exitCode: 2, data: { error: 'unknown-view' } };
|
|
108
|
+
}
|
|
83
109
|
const stop = new AbortController();
|
|
84
110
|
const onSignal = () => stop.abort();
|
|
85
111
|
process.once('SIGTERM', onSignal);
|
|
86
112
|
process.once('SIGHUP', onSignal);
|
|
87
113
|
try {
|
|
88
|
-
const code = await mod.runTui({ cwd: ctx.cwd ?? process.cwd(), view: wanted, ...(ctx.flags?.plain === true && { plain: true }), ...(ctx.flags?.ai !== false && { aiPage: aiPage(ctx.flags) }), ...(cliEntry() !== undefined && { cliEntry: cliEntry() }), ...(process.env.CLAUDE_CONFIG_DIR !== undefined && { configDir: process.env.CLAUDE_CONFIG_DIR }) }, { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr, env: process.env, signal: stop.signal });
|
|
114
|
+
const code = await mod.runTui({ cwd: ctx.cwd ?? process.cwd(), view: wanted, ...lookFlags(ctx.flags), ...(ctx.flags?.plain === true && { plain: true }), ...(ctx.flags?.ai !== false && { aiPage: aiPage(ctx.flags) }), ...(cliEntry() !== undefined && { cliEntry: cliEntry() }), ...(process.env.CLAUDE_CONFIG_DIR !== undefined && { configDir: process.env.CLAUDE_CONFIG_DIR }) }, { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr, env: process.env, signal: stop.signal });
|
|
89
115
|
return { success: code === 0, exitCode: code };
|
|
90
116
|
}
|
|
91
117
|
finally {
|
|
@@ -15,6 +15,15 @@ export declare const ADOPTED_CONFIG_FILE = "proven-config.json";
|
|
|
15
15
|
export declare function loadShippedChampion(srcPath: string): SignedProvenConfig | null;
|
|
16
16
|
/** Build the local environment the champion's constraints are checked against. */
|
|
17
17
|
export declare function currentInstallEnv(cwd?: string): InstallEnv;
|
|
18
|
+
/**
|
|
19
|
+
* #4176: is `cwd` a ruflo project? A bare `.claude/` is not enough: Claude Code
|
|
20
|
+
* itself creates `.claude/.cc-writes/` in whatever directory a Bash command ran
|
|
21
|
+
* in, so every subdirectory a session cd-ed into used to qualify and received a
|
|
22
|
+
* proven-config.json. A ruflo project carries `ruflo init`'s own markers: a
|
|
23
|
+
* `.claude/settings.json`, the `.claude/helpers/` directory, or an earlier
|
|
24
|
+
* adoption stamp.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isRufloProject(cwd: string): boolean;
|
|
18
27
|
export interface AdoptResult {
|
|
19
28
|
adopted: boolean;
|
|
20
29
|
from?: string;
|