@claude-flow/cli 3.38.14 → 3.38.15
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/.helpers-version +1 -1
- package/.claude/helpers/helpers.manifest.json +2 -2
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/hive-mind.js +12 -10
- package/dist/src/commands/init.js +54 -0
- package/dist/src/commands/mcp.js +5 -4
- package/dist/src/init/executor.d.ts +11 -1
- package/dist/src/init/executor.js +17 -2
- package/dist/src/init/settings-risk-scanner.d.ts +47 -0
- package/dist/src/init/settings-risk-scanner.js +161 -0
- package/dist/src/init/types.d.ts +7 -0
- package/dist/src/mcp-tools/hooks-tools.js +93 -17
- package/dist/src/runtime/claude-command.d.ts +21 -0
- package/dist/src/runtime/claude-command.js +49 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
3.38.
|
|
1
|
+
3.38.15
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version": "3.38.
|
|
3
|
+
"version": "3.38.15",
|
|
4
4
|
"files": {
|
|
5
5
|
"auto-memory-hook.mjs": "85fe05c757421c52137c0bc8545a0896bab6b4714538c2a11d1d0835bfcc8c1c",
|
|
6
6
|
"hook-handler.cjs": "dae295fb9ae2626b89899c19a20cc911541af82b52d2eeb9b214d618b96e9a86",
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
"statusline.cjs": "0457fe53f8cd2c56458ff178392536a5868efd1a573665fa43bc01d2d95ca677"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
|
-
"signature": "
|
|
11
|
+
"signature": "BPJ/dA1o5KjvB5Ic9PGq/9BHvQnXADaTmGXZW3UUCfDKDKmDFKMzQZowG3HF54zTek1SapxJb3irPiIWMPAFDA==",
|
|
12
12
|
"algorithm": "ed25519"
|
|
13
13
|
}
|
package/catalog-manifest.json
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
import { output } from '../output.js';
|
|
9
9
|
import { select, confirm, input } from '../prompt.js';
|
|
10
10
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
11
|
-
import { spawn as childSpawn
|
|
11
|
+
import { spawn as childSpawn } from 'child_process';
|
|
12
12
|
import { mkdir, writeFile } from 'fs/promises';
|
|
13
13
|
import { existsSync } from 'fs';
|
|
14
14
|
import { join } from 'path';
|
|
15
|
+
import { resolveClaudeLaunchCommand } from '../runtime/claude-command.js';
|
|
15
16
|
// Hive topologies
|
|
16
17
|
const TOPOLOGIES = [
|
|
17
18
|
{ value: 'hierarchical', label: 'Hierarchical', hint: 'Queen-led with worker agents' },
|
|
@@ -185,20 +186,18 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
185
186
|
await writeFile(promptFile, hiveMindPrompt, 'utf8');
|
|
186
187
|
output.writeln();
|
|
187
188
|
output.printSuccess(`Hive Mind prompt saved to: ${promptFile}`);
|
|
188
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
189
|
+
// Resolve a directly spawnable command. On Windows, npm exposes shell
|
|
190
|
+
// shims that Node cannot launch with shell:false, so the resolver follows
|
|
191
|
+
// the shim to Claude Code's executable or JavaScript entry point.
|
|
192
|
+
const claudeLaunch = resolveClaudeLaunchCommand();
|
|
193
|
+
if (!claudeLaunch) {
|
|
195
194
|
output.writeln();
|
|
196
195
|
output.printWarning('Claude Code CLI not found in PATH');
|
|
197
196
|
output.writeln(output.dim('Install it with: npm install -g @anthropic-ai/claude-code'));
|
|
198
197
|
output.writeln(output.dim('Falling back to displaying instructions...'));
|
|
199
198
|
}
|
|
200
199
|
const dryRun = flags.dryRun || flags['dry-run'];
|
|
201
|
-
if (
|
|
200
|
+
if (claudeLaunch && !dryRun) {
|
|
202
201
|
// Build arguments - flags first, then prompt
|
|
203
202
|
const claudeArgs = [];
|
|
204
203
|
// #1748 Issue 2 — pass --mcp-config so the spawned worker actually has
|
|
@@ -273,7 +272,10 @@ async function spawnClaudeCodeInstance(swarmId, swarmName, objective, workers, f
|
|
|
273
272
|
output.printInfo('Launching Claude Code...');
|
|
274
273
|
output.writeln(output.dim('Press Ctrl+C to pause the session'));
|
|
275
274
|
// Spawn claude with properly ordered arguments
|
|
276
|
-
const claudeProcess = childSpawn(
|
|
275
|
+
const claudeProcess = childSpawn(claudeLaunch.command, [
|
|
276
|
+
...claudeLaunch.argsPrefix,
|
|
277
|
+
...claudeArgs,
|
|
278
|
+
], {
|
|
277
279
|
stdio: 'inherit',
|
|
278
280
|
shell: false,
|
|
279
281
|
});
|
|
@@ -613,6 +613,18 @@ const initClaudeAction = async (ctx) => {
|
|
|
613
613
|
}
|
|
614
614
|
output.printBox(summary.join('\n'), 'Summary');
|
|
615
615
|
output.writeln();
|
|
616
|
+
// Security: surface anything settings-risk-scanner.ts flagged in a
|
|
617
|
+
// pre-existing settings.json this init carried forward unexamined.
|
|
618
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
619
|
+
output.printWarning('Settings review recommended:');
|
|
620
|
+
for (const warning of result.warnings.slice(0, 5)) {
|
|
621
|
+
output.printInfo(` • ${warning}`);
|
|
622
|
+
}
|
|
623
|
+
if (result.warnings.length > 5) {
|
|
624
|
+
output.printInfo(` ... and ${result.warnings.length - 5} more`);
|
|
625
|
+
}
|
|
626
|
+
output.writeln();
|
|
627
|
+
}
|
|
616
628
|
// Show what was created
|
|
617
629
|
if (options.components.claudeMd || options.components.settings || options.components.skills || options.components.commands || options.components.agents) {
|
|
618
630
|
output.printBox([
|
|
@@ -1022,6 +1034,18 @@ const wizardCommand = {
|
|
|
1022
1034
|
return { success: false, exitCode: 1 };
|
|
1023
1035
|
}
|
|
1024
1036
|
spinner.succeed('Setup complete!');
|
|
1037
|
+
// Security: surface anything settings-risk-scanner.ts flagged in a
|
|
1038
|
+
// pre-existing settings.json this wizard run carried forward unexamined.
|
|
1039
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
1040
|
+
output.writeln();
|
|
1041
|
+
output.printWarning('Settings review recommended:');
|
|
1042
|
+
for (const warning of result.warnings.slice(0, 5)) {
|
|
1043
|
+
output.printInfo(` • ${warning}`);
|
|
1044
|
+
}
|
|
1045
|
+
if (result.warnings.length > 5) {
|
|
1046
|
+
output.printInfo(` ... and ${result.warnings.length - 5} more`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1025
1049
|
// Initialize embeddings if enabled
|
|
1026
1050
|
let embeddingsInitialized = false;
|
|
1027
1051
|
if (enableEmbeddings) {
|
|
@@ -1170,6 +1194,15 @@ const skillsCommand = {
|
|
|
1170
1194
|
const result = await executeInit(options);
|
|
1171
1195
|
if (result.success) {
|
|
1172
1196
|
spinner.succeed(`Installed ${result.summary.skillsCount} skills`);
|
|
1197
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
1198
|
+
output.printWarning('Settings review recommended:');
|
|
1199
|
+
for (const warning of result.warnings.slice(0, 5)) {
|
|
1200
|
+
output.printInfo(` • ${warning}`);
|
|
1201
|
+
}
|
|
1202
|
+
if (result.warnings.length > 5) {
|
|
1203
|
+
output.printInfo(` ... and ${result.warnings.length - 5} more`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1173
1206
|
}
|
|
1174
1207
|
else {
|
|
1175
1208
|
spinner.fail('Failed to install skills');
|
|
@@ -1232,6 +1265,15 @@ const hooksCommand = {
|
|
|
1232
1265
|
const result = await executeInit(options);
|
|
1233
1266
|
if (result.success) {
|
|
1234
1267
|
spinner.succeed(`Created settings.json with ${result.summary.hooksEnabled} hooks enabled`);
|
|
1268
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
1269
|
+
output.printWarning('Settings review recommended:');
|
|
1270
|
+
for (const warning of result.warnings.slice(0, 5)) {
|
|
1271
|
+
output.printInfo(` • ${warning}`);
|
|
1272
|
+
}
|
|
1273
|
+
if (result.warnings.length > 5) {
|
|
1274
|
+
output.printInfo(` ... and ${result.warnings.length - 5} more`);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1235
1277
|
}
|
|
1236
1278
|
else {
|
|
1237
1279
|
spinner.fail('Failed to create hooks configuration');
|
|
@@ -1342,6 +1384,18 @@ const upgradeCommand = {
|
|
|
1342
1384
|
output.printBox(result.settingsUpdated.map(s => `+ ${s}`).join('\n'), 'Settings Updated');
|
|
1343
1385
|
output.writeln();
|
|
1344
1386
|
}
|
|
1387
|
+
// Security: surface anything settings-risk-scanner.ts flagged in the
|
|
1388
|
+
// pre-existing settings.json this upgrade carried forward unexamined.
|
|
1389
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
1390
|
+
output.printWarning('Settings review recommended:');
|
|
1391
|
+
for (const warning of result.warnings.slice(0, 5)) {
|
|
1392
|
+
output.printInfo(` • ${warning}`);
|
|
1393
|
+
}
|
|
1394
|
+
if (result.warnings.length > 5) {
|
|
1395
|
+
output.printInfo(` ... and ${result.warnings.length - 5} more`);
|
|
1396
|
+
}
|
|
1397
|
+
output.writeln();
|
|
1398
|
+
}
|
|
1345
1399
|
output.printSuccess('Your statusline helper has been updated to the latest version');
|
|
1346
1400
|
output.printInfo('Existing metrics and learning data were preserved');
|
|
1347
1401
|
// Show settings summary
|
package/dist/src/commands/mcp.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { output } from '../output.js';
|
|
9
9
|
import { confirm } from '../prompt.js';
|
|
10
10
|
import { installParentDeathWatchdog } from '../runtime/parent-death-watchdog.js';
|
|
11
|
-
import { getServerManager, getMCPServerStatus, } from '../mcp-server.js';
|
|
11
|
+
import { getServerManager, getMCPServerStatus, filterAdvertisedMcpTools, parseMcpToolSelection, } from '../mcp-server.js';
|
|
12
12
|
import { listMCPTools, callMCPTool, hasTool } from '../mcp-client.js';
|
|
13
13
|
// MCP tools categories
|
|
14
14
|
const TOOL_CATEGORIES = [
|
|
@@ -406,7 +406,8 @@ const toolsCommand = {
|
|
|
406
406
|
// Use local tool registry
|
|
407
407
|
let tools;
|
|
408
408
|
// Get tools from local registry
|
|
409
|
-
const
|
|
409
|
+
const selection = parseMcpToolSelection(process.env.CLAUDE_FLOW_MCP_TOOLS);
|
|
410
|
+
const registeredTools = filterAdvertisedMcpTools(listMCPTools(category), selection);
|
|
410
411
|
if (registeredTools.length > 0) {
|
|
411
412
|
tools = registeredTools.map(tool => ({
|
|
412
413
|
name: tool.name,
|
|
@@ -417,7 +418,7 @@ const toolsCommand = {
|
|
|
417
418
|
}
|
|
418
419
|
else {
|
|
419
420
|
// Fallback to static tool list
|
|
420
|
-
tools = [
|
|
421
|
+
tools = filterAdvertisedMcpTools([
|
|
421
422
|
// Agent tools
|
|
422
423
|
{ name: 'agent_spawn', category: 'agent', description: 'Spawn a new agent', enabled: true },
|
|
423
424
|
{ name: 'agent_list', category: 'agent', description: 'List all agents', enabled: true },
|
|
@@ -449,7 +450,7 @@ const toolsCommand = {
|
|
|
449
450
|
{ name: 'system_info', category: 'system', description: 'System information', enabled: true },
|
|
450
451
|
{ name: 'system_health', category: 'system', description: 'Health status', enabled: true },
|
|
451
452
|
{ name: 'system_metrics', category: 'system', description: 'Server metrics', enabled: true },
|
|
452
|
-
].filter(t => !category || t.category === category);
|
|
453
|
+
].filter(t => !category || t.category === category), selection);
|
|
453
454
|
}
|
|
454
455
|
if (ctx.flags.format === 'json') {
|
|
455
456
|
output.printJson(tools);
|
|
@@ -22,13 +22,23 @@ export interface UpgradeResult {
|
|
|
22
22
|
addedCommands?: string[];
|
|
23
23
|
/** Added by --settings flag */
|
|
24
24
|
settingsUpdated?: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Advisory-only findings from settings-risk-scanner.ts: dangerous-looking
|
|
27
|
+
* hook commands or Bash allow-rules found in the *pre-existing*
|
|
28
|
+
* settings.json this upgrade carried forward unexamined. Never blocks
|
|
29
|
+
* the upgrade — surfaced so a user can review before trusting it.
|
|
30
|
+
*/
|
|
31
|
+
warnings?: string[];
|
|
25
32
|
}
|
|
26
33
|
/**
|
|
27
34
|
* Merge new settings into existing settings.json
|
|
28
35
|
* Preserves user customizations while adding new features like Agent Teams
|
|
29
36
|
* Uses platform-specific commands for Mac, Linux, and Windows
|
|
30
37
|
*/
|
|
31
|
-
export declare function mergeSettingsForUpgrade(existing: Record<string, unknown>):
|
|
38
|
+
export declare function mergeSettingsForUpgrade(existing: Record<string, unknown>): {
|
|
39
|
+
merged: Record<string, unknown>;
|
|
40
|
+
warnings: string[];
|
|
41
|
+
};
|
|
32
42
|
/**
|
|
33
43
|
* Execute upgrade - updates helpers and creates missing metrics without losing data
|
|
34
44
|
* This is safe for existing users who want the latest statusline fixes
|
|
@@ -18,6 +18,7 @@ import { generatePreCommitHook, generatePostCommitHook, generateSessionManager,
|
|
|
18
18
|
import { getInstalledCliVersion, HELPERS_STAMP_FILE } from './helper-refresh.js';
|
|
19
19
|
import { generateClaudeMd } from './claudemd-generator.js';
|
|
20
20
|
import { recordMemoryPackagePath } from './memory-package-resolver.js';
|
|
21
|
+
import { scanSettingsForRisk, formatRiskFindingsAsWarnings } from './settings-risk-scanner.js';
|
|
21
22
|
/**
|
|
22
23
|
* Skills to copy based on configuration
|
|
23
24
|
*/
|
|
@@ -277,6 +278,10 @@ export async function executeInit(options) {
|
|
|
277
278
|
* Uses platform-specific commands for Mac, Linux, and Windows
|
|
278
279
|
*/
|
|
279
280
|
export function mergeSettingsForUpgrade(existing) {
|
|
281
|
+
// Scan the pre-existing hooks/permissions BEFORE they get spread into
|
|
282
|
+
// `merged` below — this is the untrusted, disk-sourced content a
|
|
283
|
+
// malicious settings.json would use to smuggle a hook payload through.
|
|
284
|
+
const warnings = formatRiskFindingsAsWarnings(scanSettingsForRisk({ hooks: existing.hooks, permissions: existing.permissions }));
|
|
280
285
|
const merged = { ...existing };
|
|
281
286
|
const platform = detectPlatform();
|
|
282
287
|
const isWindows = platform.os === 'windows';
|
|
@@ -459,7 +464,7 @@ export function mergeSettingsForUpgrade(existing) {
|
|
|
459
464
|
agentScopes: existingMemory.agentScopes ?? { enabled: true },
|
|
460
465
|
},
|
|
461
466
|
};
|
|
462
|
-
return merged;
|
|
467
|
+
return { merged, warnings };
|
|
463
468
|
}
|
|
464
469
|
/**
|
|
465
470
|
* Execute upgrade - updates helpers and creates missing metrics without losing data
|
|
@@ -643,7 +648,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
643
648
|
if (fs.existsSync(settingsPath)) {
|
|
644
649
|
try {
|
|
645
650
|
const existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
646
|
-
const mergedSettings = mergeSettingsForUpgrade(existingSettings);
|
|
651
|
+
const { merged: mergedSettings, warnings: settingsRiskWarnings } = mergeSettingsForUpgrade(existingSettings);
|
|
647
652
|
fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2), 'utf-8');
|
|
648
653
|
result.updated.push('.claude/settings.json');
|
|
649
654
|
result.settingsUpdated = [
|
|
@@ -655,6 +660,9 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
655
660
|
'claudeFlow.agentTeams',
|
|
656
661
|
'claudeFlow.memory (learningBridge, memoryGraph, agentScopes)',
|
|
657
662
|
];
|
|
663
|
+
if (settingsRiskWarnings.length > 0) {
|
|
664
|
+
result.warnings = [...(result.warnings ?? []), ...settingsRiskWarnings];
|
|
665
|
+
}
|
|
658
666
|
}
|
|
659
667
|
catch (settingsError) {
|
|
660
668
|
result.errors.push(`Settings merge failed: ${settingsError instanceof Error ? settingsError.message : String(settingsError)}`);
|
|
@@ -797,6 +805,13 @@ async function writeSettings(targetDir, options, result) {
|
|
|
797
805
|
// Merge hooks/env/permissions into existing settings instead of skipping
|
|
798
806
|
try {
|
|
799
807
|
const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
808
|
+
// Scan the pre-existing hooks/permissions BEFORE any merge decision
|
|
809
|
+
// below — same untrusted, disk-sourced content mergeSettingsForUpgrade()
|
|
810
|
+
// scans; see settings-risk-scanner.ts for why.
|
|
811
|
+
const riskWarnings = formatRiskFindingsAsWarnings(scanSettingsForRisk({ hooks: existing.hooks, permissions: existing.permissions }));
|
|
812
|
+
if (riskWarnings.length > 0) {
|
|
813
|
+
result.warnings = [...(result.warnings ?? []), ...riskWarnings];
|
|
814
|
+
}
|
|
800
815
|
let merged = false;
|
|
801
816
|
// Merge hooks (the critical missing piece — #1484)
|
|
802
817
|
if (generated.hooks && !existing.hooks) {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static, non-blocking risk scanner for pre-existing .claude/settings.json
|
|
3
|
+
* content that `ruflo init`/`ruflo init --upgrade` carry forward unexamined.
|
|
4
|
+
*
|
|
5
|
+
* `mergeSettingsForUpgrade()` and `writeSettings()` both read a target
|
|
6
|
+
* project's *existing* settings.json off disk and spread its `hooks` /
|
|
7
|
+
* `permissions.allow` entries into the merged output verbatim (see
|
|
8
|
+
* executor.ts:352-353 and :901-912) — the same trust shape as the publicly
|
|
9
|
+
* disclosed CVE-2025-59536 class (a settings.json hook payload achieving
|
|
10
|
+
* command execution with no review step). Ruflo's own hook dispatch is a
|
|
11
|
+
* closed set of internal handlers (hook-handler.cjs), so this isn't
|
|
12
|
+
* remotely exploitable through Ruflo itself — but a malicious fork, PR, or
|
|
13
|
+
* compromised dependency that plants a bad settings.json before a user
|
|
14
|
+
* runs `ruflo init`/`--upgrade` in that directory would have its payload
|
|
15
|
+
* silently preserved and reported as a normal "merged"/"updated" result,
|
|
16
|
+
* with zero visibility.
|
|
17
|
+
*
|
|
18
|
+
* This scanner is advisory-only: it never mutates its input and never
|
|
19
|
+
* blocks a merge/write. Callers decide what to do with the findings
|
|
20
|
+
* (surfaced as a CLI warning today).
|
|
21
|
+
*
|
|
22
|
+
* KNOWN LIMITATION (found by an independent adversarial review the same
|
|
23
|
+
* night this was written — see docs/dream-cycle/dream-gist-2026-08-16.md
|
|
24
|
+
* §5): this is a best-effort, static, blocklist-style heuristic, NOT a
|
|
25
|
+
* security boundary. It catches copy-pasted proof-of-concept payload
|
|
26
|
+
* shapes; it does not and cannot catch every obfuscation a motivated
|
|
27
|
+
* adversary could construct (string-built commands, uncommon interpreters,
|
|
28
|
+
* novel exfil channels, etc). Treat findings as "worth a second look,"
|
|
29
|
+
* never as a guarantee of safety.
|
|
30
|
+
*/
|
|
31
|
+
export interface SettingsRiskFinding {
|
|
32
|
+
location: string;
|
|
33
|
+
snippet: string;
|
|
34
|
+
reason: string;
|
|
35
|
+
}
|
|
36
|
+
/** Reasons a single hook `command` string looks risky (empty = clean). */
|
|
37
|
+
export declare function scanCommandStringForRisk(command: string): string[];
|
|
38
|
+
/** Reasons a single `permissions.allow` rule string looks risky (empty = clean). */
|
|
39
|
+
export declare function scanAllowRuleForRisk(rule: string): string[];
|
|
40
|
+
/**
|
|
41
|
+
* Scans a settings.json-shaped object's `hooks` and `permissions.allow`
|
|
42
|
+
* for content matching known-dangerous patterns.
|
|
43
|
+
*/
|
|
44
|
+
export declare function scanSettingsForRisk(settings: Record<string, unknown>): SettingsRiskFinding[];
|
|
45
|
+
/** Formats findings as short, human-readable CLI warning lines. */
|
|
46
|
+
export declare function formatRiskFindingsAsWarnings(findings: SettingsRiskFinding[]): string[];
|
|
47
|
+
//# sourceMappingURL=settings-risk-scanner.d.ts.map
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static, non-blocking risk scanner for pre-existing .claude/settings.json
|
|
3
|
+
* content that `ruflo init`/`ruflo init --upgrade` carry forward unexamined.
|
|
4
|
+
*
|
|
5
|
+
* `mergeSettingsForUpgrade()` and `writeSettings()` both read a target
|
|
6
|
+
* project's *existing* settings.json off disk and spread its `hooks` /
|
|
7
|
+
* `permissions.allow` entries into the merged output verbatim (see
|
|
8
|
+
* executor.ts:352-353 and :901-912) — the same trust shape as the publicly
|
|
9
|
+
* disclosed CVE-2025-59536 class (a settings.json hook payload achieving
|
|
10
|
+
* command execution with no review step). Ruflo's own hook dispatch is a
|
|
11
|
+
* closed set of internal handlers (hook-handler.cjs), so this isn't
|
|
12
|
+
* remotely exploitable through Ruflo itself — but a malicious fork, PR, or
|
|
13
|
+
* compromised dependency that plants a bad settings.json before a user
|
|
14
|
+
* runs `ruflo init`/`--upgrade` in that directory would have its payload
|
|
15
|
+
* silently preserved and reported as a normal "merged"/"updated" result,
|
|
16
|
+
* with zero visibility.
|
|
17
|
+
*
|
|
18
|
+
* This scanner is advisory-only: it never mutates its input and never
|
|
19
|
+
* blocks a merge/write. Callers decide what to do with the findings
|
|
20
|
+
* (surfaced as a CLI warning today).
|
|
21
|
+
*
|
|
22
|
+
* KNOWN LIMITATION (found by an independent adversarial review the same
|
|
23
|
+
* night this was written — see docs/dream-cycle/dream-gist-2026-08-16.md
|
|
24
|
+
* §5): this is a best-effort, static, blocklist-style heuristic, NOT a
|
|
25
|
+
* security boundary. It catches copy-pasted proof-of-concept payload
|
|
26
|
+
* shapes; it does not and cannot catch every obfuscation a motivated
|
|
27
|
+
* adversary could construct (string-built commands, uncommon interpreters,
|
|
28
|
+
* novel exfil channels, etc). Treat findings as "worth a second look,"
|
|
29
|
+
* never as a guarantee of safety.
|
|
30
|
+
*/
|
|
31
|
+
const DANGEROUS_COMMAND_WORDS = [
|
|
32
|
+
'rm', 'rmdir', 'del', 'format', 'mkfs', 'dd', 'chmod', 'chown',
|
|
33
|
+
'kill', 'killall', 'pkill', 'reboot', 'shutdown', 'poweroff', 'halt',
|
|
34
|
+
'sudo', 'eval',
|
|
35
|
+
];
|
|
36
|
+
// Words that make a *hook command string* look risky when combined with a
|
|
37
|
+
// downloader (curl/wget) or a decode step (base64) anywhere in the same
|
|
38
|
+
// string — deliberately co-occurrence-based, not strict adjacency/piping,
|
|
39
|
+
// since `curl … -o f && sh f` and `curl … | tee x | bash` both reach the
|
|
40
|
+
// same outcome as the textbook `curl … | bash` and a strict pipe-adjacency
|
|
41
|
+
// regex misses both (found by adversarial review).
|
|
42
|
+
const SHELL_EXEC_WORDS = ['bash', 'sh', 'zsh', 'python', 'python3', 'perl', 'ruby', 'eval'];
|
|
43
|
+
const DOWNLOADER_WORDS = ['curl', 'wget'];
|
|
44
|
+
const STANDALONE_RISK_PATTERNS = [
|
|
45
|
+
/\bInvoke-Expression\b/i,
|
|
46
|
+
/\biex\s*\(/i,
|
|
47
|
+
/powershell(\.exe)?\s+.*-e(nc(odedcommand)?)?\s+\S/i,
|
|
48
|
+
/\/dev\/tcp\//i,
|
|
49
|
+
/\bnc\s+-e\b/i,
|
|
50
|
+
/base64\s+(-d|--decode)\b/i,
|
|
51
|
+
];
|
|
52
|
+
// Words a Bash allow-rule should never blanket-preapprove without a
|
|
53
|
+
// specific argument scope, even though they aren't "dangerous" on their
|
|
54
|
+
// own the way `rm`/`chmod` are — pre-approving them removes the human
|
|
55
|
+
// review step for exactly the commands download-and-execute attacks use.
|
|
56
|
+
const RISKY_PREAPPROVE_WORDS = [...DANGEROUS_COMMAND_WORDS, ...SHELL_EXEC_WORDS, ...DOWNLOADER_WORDS];
|
|
57
|
+
function wordBoundaryMatches(text, word) {
|
|
58
|
+
return new RegExp(`\\b${word}\\b`, 'i').test(text);
|
|
59
|
+
}
|
|
60
|
+
/** Reasons a single hook `command` string looks risky (empty = clean). */
|
|
61
|
+
export function scanCommandStringForRisk(command) {
|
|
62
|
+
const reasons = [];
|
|
63
|
+
for (const pattern of STANDALONE_RISK_PATTERNS) {
|
|
64
|
+
if (pattern.test(command)) {
|
|
65
|
+
reasons.push('matches a known-dangerous execution pattern (encoded/remote command execution)');
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const hasDownloader = DOWNLOADER_WORDS.some((w) => wordBoundaryMatches(command, w));
|
|
70
|
+
const hasShellExec = SHELL_EXEC_WORDS.some((w) => wordBoundaryMatches(command, w));
|
|
71
|
+
if (hasDownloader && hasShellExec) {
|
|
72
|
+
reasons.push('combines a downloader (curl/wget) with a shell/interpreter invocation in the same command');
|
|
73
|
+
}
|
|
74
|
+
for (const word of DANGEROUS_COMMAND_WORDS) {
|
|
75
|
+
if (wordBoundaryMatches(command, word)) {
|
|
76
|
+
reasons.push(`invokes dangerous command "${word}"`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return reasons;
|
|
80
|
+
}
|
|
81
|
+
/** Reasons a single `permissions.allow` rule string looks risky (empty = clean). */
|
|
82
|
+
export function scanAllowRuleForRisk(rule) {
|
|
83
|
+
const trimmed = rule.trim();
|
|
84
|
+
const reasons = [];
|
|
85
|
+
if (/^Bash\(\s*\*\s*(:\s*\*\s*)?\)$/i.test(trimmed)) {
|
|
86
|
+
reasons.push('grants unrestricted Bash execution (wildcard allow rule)');
|
|
87
|
+
}
|
|
88
|
+
for (const word of RISKY_PREAPPROVE_WORDS) {
|
|
89
|
+
if (new RegExp(`^Bash\\(\\s*${word}\\s*(:| |\\))`, 'i').test(trimmed)) {
|
|
90
|
+
reasons.push(`pre-allows "${word}" with no per-call confirmation`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return reasons;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Strips ANSI/control characters from untrusted text before it's ever
|
|
97
|
+
* embedded in a CLI warning string. Without this, a malicious hook command
|
|
98
|
+
* could use escape sequences to spoof or corrupt the terminal output of
|
|
99
|
+
* the very warning meant to flag it (found during this candidate's own
|
|
100
|
+
* security review — not part of the CVE-2025-59536 class itself, but the
|
|
101
|
+
* same "untrusted settings.json content reaches the user's terminal"
|
|
102
|
+
* shape, so it gets the same treatment here rather than being left open).
|
|
103
|
+
*/
|
|
104
|
+
function sanitizeForDisplay(text) {
|
|
105
|
+
// eslint-disable-next-line no-control-regex
|
|
106
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
107
|
+
}
|
|
108
|
+
function buildSnippet(text) {
|
|
109
|
+
const clean = sanitizeForDisplay(text);
|
|
110
|
+
return clean.length > 120 ? `${clean.slice(0, 120)}…` : clean;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Scans a settings.json-shaped object's `hooks` and `permissions.allow`
|
|
114
|
+
* for content matching known-dangerous patterns.
|
|
115
|
+
*/
|
|
116
|
+
export function scanSettingsForRisk(settings) {
|
|
117
|
+
const findings = [];
|
|
118
|
+
const hooks = settings.hooks;
|
|
119
|
+
if (hooks && typeof hooks === 'object') {
|
|
120
|
+
for (const [eventName, groups] of Object.entries(hooks)) {
|
|
121
|
+
if (!Array.isArray(groups))
|
|
122
|
+
continue;
|
|
123
|
+
groups.forEach((group, groupIdx) => {
|
|
124
|
+
const hookList = group?.hooks;
|
|
125
|
+
if (!Array.isArray(hookList))
|
|
126
|
+
return;
|
|
127
|
+
hookList.forEach((h, hookIdx) => {
|
|
128
|
+
if (typeof h?.command !== 'string')
|
|
129
|
+
return;
|
|
130
|
+
for (const reason of scanCommandStringForRisk(h.command)) {
|
|
131
|
+
findings.push({
|
|
132
|
+
location: `hooks.${eventName}[${groupIdx}].hooks[${hookIdx}]`,
|
|
133
|
+
snippet: buildSnippet(h.command),
|
|
134
|
+
reason,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const allow = settings.permissions?.allow;
|
|
142
|
+
if (Array.isArray(allow)) {
|
|
143
|
+
allow.forEach((rule, idx) => {
|
|
144
|
+
if (typeof rule !== 'string')
|
|
145
|
+
return;
|
|
146
|
+
for (const reason of scanAllowRuleForRisk(rule)) {
|
|
147
|
+
findings.push({
|
|
148
|
+
location: `permissions.allow[${idx}]`,
|
|
149
|
+
snippet: buildSnippet(rule),
|
|
150
|
+
reason,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return findings;
|
|
156
|
+
}
|
|
157
|
+
/** Formats findings as short, human-readable CLI warning lines. */
|
|
158
|
+
export function formatRiskFindingsAsWarnings(findings) {
|
|
159
|
+
return findings.map((f) => `${f.location}: ${f.reason} — "${f.snippet}"`);
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=settings-risk-scanner.js.map
|
package/dist/src/init/types.d.ts
CHANGED
|
@@ -327,5 +327,12 @@ export interface InitResult {
|
|
|
327
327
|
agentsCount: number;
|
|
328
328
|
hooksEnabled: number;
|
|
329
329
|
};
|
|
330
|
+
/**
|
|
331
|
+
* Advisory-only findings from settings-risk-scanner.ts: dangerous-looking
|
|
332
|
+
* hook commands or Bash allow-rules found in a *pre-existing*
|
|
333
|
+
* settings.json this init carried forward unexamined. Never blocks
|
|
334
|
+
* init — surfaced so a user can review before trusting it.
|
|
335
|
+
*/
|
|
336
|
+
warnings?: string[];
|
|
330
337
|
}
|
|
331
338
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -392,7 +392,7 @@ const activeTrajectories = new Map();
|
|
|
392
392
|
const MEMORY_DIR = '.claude-flow/memory';
|
|
393
393
|
const MEMORY_FILE = 'store.json';
|
|
394
394
|
function getMemoryPath() {
|
|
395
|
-
return resolve(join(MEMORY_DIR, MEMORY_FILE));
|
|
395
|
+
return resolve(join(getProjectCwd(), MEMORY_DIR, MEMORY_FILE));
|
|
396
396
|
}
|
|
397
397
|
function loadMemoryStore() {
|
|
398
398
|
try {
|
|
@@ -407,6 +407,80 @@ function loadMemoryStore() {
|
|
|
407
407
|
}
|
|
408
408
|
return { entries: {}, version: '3.0.0' };
|
|
409
409
|
}
|
|
410
|
+
function isLearnedPatternEntry(entry) {
|
|
411
|
+
return entry.key.includes('pattern') ||
|
|
412
|
+
entry.metadata?.type === 'pattern' ||
|
|
413
|
+
entry.key.startsWith('learned-') ||
|
|
414
|
+
entry.namespace === 'patterns' ||
|
|
415
|
+
entry.metadata?.type === 'routing-decision';
|
|
416
|
+
}
|
|
417
|
+
function timestampInRange(value, start, end) {
|
|
418
|
+
if (typeof value !== 'string' && typeof value !== 'number')
|
|
419
|
+
return false;
|
|
420
|
+
const timestamp = typeof value === 'number' ? value : Date.parse(value);
|
|
421
|
+
return Number.isFinite(timestamp) && timestamp >= start && timestamp <= end;
|
|
422
|
+
}
|
|
423
|
+
function nonNegativeInteger(value) {
|
|
424
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
425
|
+
? Math.max(0, Math.floor(value))
|
|
426
|
+
: 0;
|
|
427
|
+
}
|
|
428
|
+
function loadActiveSessionState() {
|
|
429
|
+
try {
|
|
430
|
+
const sessionPath = join(getProjectCwd(), '.claude-flow', 'sessions', 'current.json');
|
|
431
|
+
if (!existsSync(sessionPath))
|
|
432
|
+
return null;
|
|
433
|
+
const session = JSON.parse(readFileSync(sessionPath, 'utf-8'));
|
|
434
|
+
if (typeof session.id !== 'string' || session.id.trim().length === 0)
|
|
435
|
+
return null;
|
|
436
|
+
if (typeof session.startedAt !== 'string' || !Number.isFinite(Date.parse(session.startedAt)))
|
|
437
|
+
return null;
|
|
438
|
+
return session;
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function loadSessionActivity(session, endedAt) {
|
|
445
|
+
const startedAt = Date.parse(session.startedAt);
|
|
446
|
+
let tasksCompleted = 0;
|
|
447
|
+
try {
|
|
448
|
+
const taskPath = join(getProjectCwd(), '.claude-flow', 'tasks', 'store.json');
|
|
449
|
+
if (existsSync(taskPath)) {
|
|
450
|
+
const store = JSON.parse(readFileSync(taskPath, 'utf-8'));
|
|
451
|
+
for (const task of Object.values(store.tasks ?? {})) {
|
|
452
|
+
if (task.status === 'completed' && timestampInRange(task.completedAt, startedAt, endedAt)) {
|
|
453
|
+
tasksCompleted++;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// Missing or malformed task state contributes no completed tasks.
|
|
460
|
+
}
|
|
461
|
+
const patternsLearned = Object.values(loadMemoryStore().entries)
|
|
462
|
+
.filter(entry => isLearnedPatternEntry(entry) && timestampInRange(entry.storedAt, startedAt, endedAt))
|
|
463
|
+
.length;
|
|
464
|
+
return {
|
|
465
|
+
tasksCompleted,
|
|
466
|
+
patternsLearned,
|
|
467
|
+
editsRecorded: nonNegativeInteger(session.metrics?.edits),
|
|
468
|
+
commandsRecorded: nonNegativeInteger(session.metrics?.commands),
|
|
469
|
+
errorsRecorded: nonNegativeInteger(session.metrics?.errors),
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function buildSessionSummary(activity, duration) {
|
|
473
|
+
const durationMinutes = Math.max(0, Math.round(duration / 60000));
|
|
474
|
+
const count = (value, singular, plural = `${singular}s`) => `${value} ${value === 1 ? singular : plural}`;
|
|
475
|
+
return [
|
|
476
|
+
`${count(activity.tasksCompleted, 'task')} completed`,
|
|
477
|
+
`${count(activity.patternsLearned, 'pattern')} learned`,
|
|
478
|
+
`${count(activity.editsRecorded, 'edit')} recorded`,
|
|
479
|
+
`${count(activity.commandsRecorded, 'command')} recorded`,
|
|
480
|
+
`${count(activity.errorsRecorded, 'error')} recorded`,
|
|
481
|
+
`duration ${count(durationMinutes, 'minute')}`,
|
|
482
|
+
].join('; ');
|
|
483
|
+
}
|
|
410
484
|
/**
|
|
411
485
|
* Get real intelligence statistics from memory store
|
|
412
486
|
*/
|
|
@@ -428,11 +502,7 @@ function getIntelligenceStatsFromMemory() {
|
|
|
428
502
|
// patterns the metric is meant to count, so include them: any entry
|
|
429
503
|
// whose namespace is `patterns`, plus the original shapes for
|
|
430
504
|
// forward-compatibility with a future explicit `pattern` writer.
|
|
431
|
-
const patternEntries = entries.filter(
|
|
432
|
-
e.metadata?.type === 'pattern' ||
|
|
433
|
-
e.key.startsWith('learned-') ||
|
|
434
|
-
e.namespace === 'patterns' ||
|
|
435
|
-
e.metadata?.type === 'routing-decision');
|
|
505
|
+
const patternEntries = entries.filter(isLearnedPatternEntry);
|
|
436
506
|
// Categorize patterns
|
|
437
507
|
const categories = {};
|
|
438
508
|
patternEntries.forEach(e => {
|
|
@@ -2144,7 +2214,15 @@ export const hooksSessionEnd = {
|
|
|
2144
2214
|
handler: async (params) => {
|
|
2145
2215
|
const saveState = params.saveState !== false;
|
|
2146
2216
|
const shouldStopDaemon = params.stopDaemon !== false;
|
|
2147
|
-
const
|
|
2217
|
+
const session = loadActiveSessionState();
|
|
2218
|
+
if (!session) {
|
|
2219
|
+
throw new Error('No active session state found at .claude-flow/sessions/current.json');
|
|
2220
|
+
}
|
|
2221
|
+
const sessionId = session.id;
|
|
2222
|
+
const endedAt = Date.now();
|
|
2223
|
+
const duration = Math.max(0, endedAt - Date.parse(session.startedAt));
|
|
2224
|
+
const activity = loadSessionActivity(session, endedAt);
|
|
2225
|
+
const summary = buildSessionSummary(activity, duration);
|
|
2148
2226
|
// Stop daemon if enabled
|
|
2149
2227
|
let daemonStopped = false;
|
|
2150
2228
|
if (shouldStopDaemon) {
|
|
@@ -2157,12 +2235,10 @@ export const hooksSessionEnd = {
|
|
|
2157
2235
|
// Daemon may not be running
|
|
2158
2236
|
}
|
|
2159
2237
|
}
|
|
2160
|
-
// Read
|
|
2238
|
+
// Read aggregate store data for the remaining compatibility metrics.
|
|
2161
2239
|
const store = loadMemoryStore();
|
|
2162
2240
|
const allEntries = Object.values(store.entries);
|
|
2163
|
-
const taskCount = allEntries.filter(e => e.key.includes('task')).length;
|
|
2164
2241
|
const agentCount = allEntries.filter(e => e.key.includes('agent')).length;
|
|
2165
|
-
const patternCount = allEntries.filter(e => e.key.includes('pattern')).length;
|
|
2166
2242
|
const trajectoryCount = activeTrajectories.size;
|
|
2167
2243
|
// Check for pending-insights.jsonl
|
|
2168
2244
|
let insightCount = 0;
|
|
@@ -2183,9 +2259,9 @@ export const hooksSessionEnd = {
|
|
|
2183
2259
|
bridge = await import('../memory/memory-bridge.js');
|
|
2184
2260
|
const result = await bridge.bridgeSessionEnd({
|
|
2185
2261
|
sessionId,
|
|
2186
|
-
summary
|
|
2187
|
-
tasksCompleted:
|
|
2188
|
-
patternsLearned:
|
|
2262
|
+
summary,
|
|
2263
|
+
tasksCompleted: activity.tasksCompleted,
|
|
2264
|
+
patternsLearned: activity.patternsLearned,
|
|
2189
2265
|
});
|
|
2190
2266
|
if (result) {
|
|
2191
2267
|
sessionPersistence = {
|
|
@@ -2210,19 +2286,19 @@ export const hooksSessionEnd = {
|
|
|
2210
2286
|
}
|
|
2211
2287
|
return {
|
|
2212
2288
|
sessionId,
|
|
2213
|
-
duration
|
|
2289
|
+
duration,
|
|
2214
2290
|
statePath: saveState ? `.claude/sessions/${sessionId}.json` : undefined,
|
|
2215
2291
|
daemon: { stopped: daemonStopped },
|
|
2216
2292
|
sessionPersistence: sessionPersistence || { controller: 'none', persisted: false },
|
|
2217
2293
|
summary: {
|
|
2218
|
-
tasksExecuted:
|
|
2219
|
-
filesModified:
|
|
2294
|
+
tasksExecuted: activity.tasksCompleted,
|
|
2295
|
+
filesModified: activity.editsRecorded,
|
|
2220
2296
|
agentsSpawned: agentCount,
|
|
2221
2297
|
pendingInsights: insightCount,
|
|
2222
2298
|
memoryEntries: allEntries.length,
|
|
2223
2299
|
},
|
|
2224
2300
|
learningUpdates: {
|
|
2225
|
-
patternsLearned:
|
|
2301
|
+
patternsLearned: activity.patternsLearned,
|
|
2226
2302
|
trajectoriesRecorded: trajectoryCount,
|
|
2227
2303
|
},
|
|
2228
2304
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface ClaudeLaunchCommand {
|
|
2
|
+
command: string;
|
|
3
|
+
argsPrefix: string[];
|
|
4
|
+
}
|
|
5
|
+
interface ClaudeCommandResolverOptions {
|
|
6
|
+
platform?: NodeJS.Platform;
|
|
7
|
+
nodeExecutable?: string;
|
|
8
|
+
lookup?: (command: string, args: string[]) => string;
|
|
9
|
+
fileExists?: (path: string) => boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Resolve Claude Code to a target that Node can spawn without a shell.
|
|
13
|
+
*
|
|
14
|
+
* Windows npm shims (`claude.cmd` and `claude.ps1`) are shell scripts and
|
|
15
|
+
* cannot be passed directly to spawn with `shell: false`. Resolve the native
|
|
16
|
+
* executable used by current Claude Code releases, or the JavaScript entry
|
|
17
|
+
* used by older releases, while keeping user-provided prompts out of a shell.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveClaudeLaunchCommand(options?: ClaudeCommandResolverOptions): ClaudeLaunchCommand | null;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=claude-command.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { posix, win32 } from 'node:path';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve Claude Code to a target that Node can spawn without a shell.
|
|
6
|
+
*
|
|
7
|
+
* Windows npm shims (`claude.cmd` and `claude.ps1`) are shell scripts and
|
|
8
|
+
* cannot be passed directly to spawn with `shell: false`. Resolve the native
|
|
9
|
+
* executable used by current Claude Code releases, or the JavaScript entry
|
|
10
|
+
* used by older releases, while keeping user-provided prompts out of a shell.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveClaudeLaunchCommand(options = {}) {
|
|
13
|
+
const platform = options.platform ?? process.platform;
|
|
14
|
+
const path = platform === 'win32' ? win32 : posix;
|
|
15
|
+
const nodeExecutable = options.nodeExecutable ?? process.execPath;
|
|
16
|
+
const lookup = options.lookup ?? ((command, args) => execFileSync(command, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }));
|
|
17
|
+
const fileExists = options.fileExists ?? existsSync;
|
|
18
|
+
let matches;
|
|
19
|
+
try {
|
|
20
|
+
const output = platform === 'win32'
|
|
21
|
+
? lookup('where.exe', ['claude'])
|
|
22
|
+
: lookup('which', ['claude']);
|
|
23
|
+
matches = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (platform !== 'win32') {
|
|
29
|
+
const command = matches.find(fileExists);
|
|
30
|
+
return command ? { command, argsPrefix: [] } : null;
|
|
31
|
+
}
|
|
32
|
+
const executable = matches.find((candidate) => path.extname(candidate).toLowerCase() === '.exe' && fileExists(candidate));
|
|
33
|
+
if (executable)
|
|
34
|
+
return { command: executable, argsPrefix: [] };
|
|
35
|
+
for (const shim of matches) {
|
|
36
|
+
const npmBin = path.dirname(shim);
|
|
37
|
+
const packageRoot = path.join(npmBin, 'node_modules', '@anthropic-ai', 'claude-code');
|
|
38
|
+
const nativeExecutable = path.join(packageRoot, 'bin', 'claude.exe');
|
|
39
|
+
if (fileExists(nativeExecutable)) {
|
|
40
|
+
return { command: nativeExecutable, argsPrefix: [] };
|
|
41
|
+
}
|
|
42
|
+
const javascriptEntry = path.join(packageRoot, 'cli.js');
|
|
43
|
+
if (fileExists(javascriptEntry)) {
|
|
44
|
+
return { command: nodeExecutable, argsPrefix: [javascriptEntry] };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=claude-command.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.38.
|
|
3
|
+
"version": "3.38.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
|
|
6
6
|
"main": "dist/src/index.js",
|