@claude-flow/cli 3.56.3 → 3.56.5
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 +13 -8
- package/.claude/helpers/helpers.manifest.json +5 -5
- package/.claude/helpers/hook-handler.cjs +19 -14
- package/.claude/helpers/learning-service.mjs +5 -2
- package/.claude/helpers/session.cjs +48 -12
- package/.claude/helpers/statusline.cjs +14 -4
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/doctor.js +7 -13
- package/dist/src/commands/hooks.js +3 -6
- package/dist/src/commands/init.js +3 -10
- package/dist/src/commands/metaharness.js +9 -14
- package/dist/src/commands/plugins.js +81 -8
- package/dist/src/commands/security-code-patterns.d.ts +40 -0
- package/dist/src/commands/security-code-patterns.js +192 -0
- package/dist/src/commands/security.js +8 -11
- package/dist/src/commands/status.js +8 -10
- package/dist/src/init/executor.d.ts +1 -1
- package/dist/src/init/executor.js +25 -16
- package/dist/src/init/helper-commands.d.ts +18 -0
- package/dist/src/init/helper-commands.js +142 -0
- package/dist/src/init/helpers-generator.js +128 -29
- package/dist/src/init/settings-generator.js +36 -103
- package/dist/src/mcp-tools/metaharness-tools.js +7 -10
- package/dist/src/mcp-tools/task-tools.js +24 -3
- package/dist/src/mcp-tools/testgen-tools.js +8 -12
- package/dist/src/mcp-tools/transfer-tools.js +9 -8
- package/dist/src/mcp-tools/workflow-tools.js +10 -4
- package/dist/src/memory/graph-edge-writer.js +33 -11
- package/dist/src/memory/memory-initializer.js +2 -19
- package/dist/src/memory/shared-sqlite.d.ts +6 -0
- package/dist/src/memory/shared-sqlite.js +35 -7
- package/dist/src/missions/schemas.d.ts +125 -125
- package/dist/src/mods/command-registry/schema.d.ts +199 -199
- package/dist/src/plugins/manager.d.ts +3 -1
- package/dist/src/plugins/manager.js +31 -10
- package/dist/src/plugins/own-install.d.ts +15 -0
- package/dist/src/plugins/own-install.js +51 -0
- package/dist/src/plugins/store/discovery.d.ts +26 -2
- package/dist/src/plugins/store/discovery.js +108 -81
- package/dist/src/plugins/store/index.d.ts +1 -1
- package/dist/src/plugins/store/index.js +1 -1
- package/dist/src/plugins/store/types.d.ts +5 -0
- package/dist/src/plugins/trust-policy.d.ts +13 -0
- package/dist/src/plugins/trust-policy.js +29 -0
- package/dist/src/ruvector/graph-analyzer.js +18 -6
- package/dist/src/transfer/ipfs/client.js +3 -1
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/mcp/dist/resource-registry.d.ts +1 -0
- package/node_modules/@claude-flow/mcp/dist/resource-registry.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/resource-registry.js +10 -4
- package/node_modules/@claude-flow/mcp/dist/resource-registry.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/sampling.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/sampling.js +13 -6
- package/node_modules/@claude-flow/mcp/dist/sampling.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/task-manager.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/task-manager.js +10 -1
- package/node_modules/@claude-flow/mcp/dist/task-manager.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts +1 -0
- package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js +17 -11
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/index.d.ts +1 -1
- package/node_modules/@claude-flow/security/dist/index.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/index.js +1 -1
- package/node_modules/@claude-flow/security/dist/index.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.d.ts +9 -1
- package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.js +15 -2
- package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.js.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +5 -5
|
@@ -20,6 +20,7 @@ import { generateClaudeMd } from './claudemd-generator.js';
|
|
|
20
20
|
import { recordMemoryPackagePath } from './memory-package-resolver.js';
|
|
21
21
|
import { ensureCommonJsCompanions } from './helper-companions.js';
|
|
22
22
|
import { scanSettingsForRisk, formatRiskFindingsAsWarnings } from './settings-risk-scanner.js';
|
|
23
|
+
import { helperHookCommand, helperScopeFor, helperStatusLineCommand, pinToUserHelpers } from './helper-commands.js';
|
|
23
24
|
/**
|
|
24
25
|
* Skills to copy based on configuration
|
|
25
26
|
*/
|
|
@@ -278,12 +279,15 @@ export async function executeInit(options) {
|
|
|
278
279
|
* Preserves user customizations while adding new features like Agent Teams
|
|
279
280
|
* Uses platform-specific commands for Mac, Linux, and Windows
|
|
280
281
|
*/
|
|
281
|
-
export function mergeSettingsForUpgrade(existing) {
|
|
282
|
+
export function mergeSettingsForUpgrade(existing, targetDir) {
|
|
282
283
|
// Scan the pre-existing hooks/permissions BEFORE they get spread into
|
|
283
284
|
// `merged` below — this is the untrusted, disk-sourced content a
|
|
284
285
|
// malicious settings.json would use to smuggle a hook payload through.
|
|
285
286
|
const warnings = formatRiskFindingsAsWarnings(scanSettingsForRisk({ hooks: existing.hooks, permissions: existing.permissions }));
|
|
286
287
|
const merged = { ...existing };
|
|
288
|
+
// User-level settings (~/.claude/settings.json) apply to every opened
|
|
289
|
+
// project, so their helper commands never look inside the project.
|
|
290
|
+
const scope = helperScopeFor(targetDir);
|
|
287
291
|
const platform = detectPlatform();
|
|
288
292
|
const isWindows = platform.os === 'windows';
|
|
289
293
|
// Platform-specific command wrappers
|
|
@@ -309,8 +313,12 @@ export function mergeSettingsForUpgrade(existing) {
|
|
|
309
313
|
+ "try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}"
|
|
310
314
|
+ 'catch(e){r=process.cwd()}';
|
|
311
315
|
const autoMemoryScript = '.claude/helpers/auto-memory-hook.mjs';
|
|
312
|
-
const autoMemoryImportCmd =
|
|
313
|
-
|
|
316
|
+
const autoMemoryImportCmd = scope === 'user'
|
|
317
|
+
? helperHookCommand(autoMemoryScript, 'import', 'user')
|
|
318
|
+
: `node -e "${gitRootResolver}var f=p.join(r,'${autoMemoryScript}');import(u.pathToFileURL(f).href)" import`;
|
|
319
|
+
const autoMemorySyncCmd = scope === 'user'
|
|
320
|
+
? helperHookCommand(autoMemoryScript, 'sync', 'user')
|
|
321
|
+
: `node -e "${gitRootResolver}var f=p.join(r,'${autoMemoryScript}');import(u.pathToFileURL(f).href)" sync`;
|
|
314
322
|
// Add auto-memory import to SessionStart (if not already present)
|
|
315
323
|
const sessionStartHooks = existingHooks.SessionStart;
|
|
316
324
|
const hasAutoMemoryImport = sessionStartHooks?.some(group => group.hooks?.some(h => h.command?.includes('auto-memory-hook')));
|
|
@@ -364,7 +372,8 @@ export function mergeSettingsForUpgrade(existing) {
|
|
|
364
372
|
// / jetsam / kernel panic. Preserving the user's existing command (the
|
|
365
373
|
// old behavior here) means anyone who installed pre-#2337 and upgraded
|
|
366
374
|
// never gets the fix. Now we detect the broken form and overwrite.
|
|
367
|
-
const NEW_STATUSLINE_CMD =
|
|
375
|
+
const NEW_STATUSLINE_CMD = scope === 'user' ? helperStatusLineCommand('user') :
|
|
376
|
+
`node -e "var c=require('child_process'),p=require('path'),r;try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}catch(e){r=process.cwd()}var s=p.join(r,'.claude/helpers/statusline.cjs');process.argv.splice(1,0,s);require(s)"`;
|
|
368
377
|
// Matches any invocation of `claude-flow hooks statusline` — either via npx
|
|
369
378
|
// (`npx [--prefer-offline] [@]claude-flow[/cli][@<tag>] hooks statusline …`)
|
|
370
379
|
// or as a bare command (`claude-flow hooks statusline …`). Both forms cold-
|
|
@@ -381,9 +390,10 @@ export function mergeSettingsForUpgrade(existing) {
|
|
|
381
390
|
if (existingStatusLine) {
|
|
382
391
|
const existingCmd = typeof existingStatusLine.command === 'string' ? existingStatusLine.command : '';
|
|
383
392
|
const isBroken = BROKEN_STATUSLINE_RE.test(existingCmd) || BROKEN_NPX_LATEST_RE.test(existingCmd);
|
|
393
|
+
const pinned = scope === 'user' ? pinToUserHelpers(existingCmd) : null;
|
|
384
394
|
merged.statusLine = {
|
|
385
395
|
type: 'command',
|
|
386
|
-
command: isBroken || !existingCmd ? NEW_STATUSLINE_CMD : existingCmd,
|
|
396
|
+
command: isBroken || !existingCmd ? NEW_STATUSLINE_CMD : pinned ?? existingCmd,
|
|
387
397
|
// Remove invalid fields: refreshMs, enabled (not supported by Claude Code)
|
|
388
398
|
};
|
|
389
399
|
}
|
|
@@ -398,15 +408,8 @@ export function mergeSettingsForUpgrade(existing) {
|
|
|
398
408
|
// migration on already-correct settings is a no-op.
|
|
399
409
|
// Bounded for the same reason as BROKEN_STATUSLINE_RE above (CodeQL js/redos).
|
|
400
410
|
const BROKEN_HOOK_RE = /npx\s+(?:--?\S+\s+){0,10}@?claude-flow\/cli@latest\s+hooks\s+(\S+)/;
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
// Windows users hit a separate code path (cmd /c …) — Claude Code on
|
|
404
|
-
// Windows is rarer and the migration there is left to a follow-up.
|
|
405
|
-
if (process.platform === 'win32') {
|
|
406
|
-
return `cmd /c "IF EXIST \"%CLAUDE_PROJECT_DIR%\\.claude\\helpers\\hook-handler.cjs\" (node \"%CLAUDE_PROJECT_DIR%\\.claude\\helpers\\hook-handler.cjs\" ${sub}) ELSE (node \"%USERPROFILE%\\.claude\\helpers\\hook-handler.cjs\" ${sub})"`;
|
|
407
|
-
}
|
|
408
|
-
return `sh -c 'D="\${CLAUDE_PROJECT_DIR:-.}"; [ -f "$D/.claude/helpers/hook-handler.cjs" ] || D="\${HOME}"; exec node "$D/.claude/helpers/hook-handler.cjs" ${sub}'`;
|
|
409
|
-
};
|
|
411
|
+
// Same command settings-generator.ts emits for this settings scope.
|
|
412
|
+
const localHookCmd = (sub) => helperHookCommand('.claude/helpers/hook-handler.cjs', sub, scope);
|
|
410
413
|
const mergedHooks = merged.hooks;
|
|
411
414
|
if (mergedHooks) {
|
|
412
415
|
for (const eventName of Object.keys(mergedHooks)) {
|
|
@@ -419,6 +422,12 @@ export function mergeSettingsForUpgrade(existing) {
|
|
|
419
422
|
group.hooks = group.hooks.filter((h) => {
|
|
420
423
|
if (typeof h?.command !== 'string')
|
|
421
424
|
return true;
|
|
425
|
+
// A user-level probe ruflo generated earlier is re-pinned to HOME.
|
|
426
|
+
const pinned = scope === 'user' ? pinToUserHelpers(h.command) : null;
|
|
427
|
+
if (pinned) {
|
|
428
|
+
h.command = pinned;
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
422
431
|
const m = BROKEN_HOOK_RE.exec(h.command);
|
|
423
432
|
if (m) {
|
|
424
433
|
// Subcommand captured (e.g. "pre-bash", "post-edit", "route") — keep it.
|
|
@@ -655,7 +664,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
655
664
|
if (fs.existsSync(settingsPath)) {
|
|
656
665
|
try {
|
|
657
666
|
const existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
658
|
-
const { merged: mergedSettings, warnings: settingsRiskWarnings } = mergeSettingsForUpgrade(existingSettings);
|
|
667
|
+
const { merged: mergedSettings, warnings: settingsRiskWarnings } = mergeSettingsForUpgrade(existingSettings, targetDir);
|
|
659
668
|
fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2), 'utf-8');
|
|
660
669
|
result.updated.push('.claude/settings.json');
|
|
661
670
|
result.settingsUpdated = [
|
|
@@ -677,7 +686,7 @@ export async function executeUpgrade(targetDir, upgradeSettings = false) {
|
|
|
677
686
|
}
|
|
678
687
|
else {
|
|
679
688
|
// Create new settings.json with defaults
|
|
680
|
-
const defaultSettings = generateSettings(DEFAULT_INIT_OPTIONS);
|
|
689
|
+
const defaultSettings = generateSettings({ ...DEFAULT_INIT_OPTIONS, targetDir });
|
|
681
690
|
fs.writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2), 'utf-8');
|
|
682
691
|
result.created.push('.claude/settings.json');
|
|
683
692
|
result.settingsUpdated = ['Created new settings.json with Agent Teams'];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type HelperScope = 'project' | 'user';
|
|
2
|
+
/** Settings written into the user's home directory are user-level settings. */
|
|
3
|
+
export declare function helperScopeFor(targetDir: string | undefined): HelperScope;
|
|
4
|
+
/** Command that runs `node <script> <subcommand>` for a hook. */
|
|
5
|
+
export declare function helperHookCommand(script: string, subcommand: string, scope: HelperScope, windows?: boolean): string;
|
|
6
|
+
/** Command for the statusLine entry. */
|
|
7
|
+
export declare function helperStatusLineCommand(scope: HelperScope, windows?: boolean): string;
|
|
8
|
+
/**
|
|
9
|
+
* Rewrite a command that runs one of ruflo's helpers from the opened project
|
|
10
|
+
* into the pinned user-level form, keeping its subcommand. Covers every form
|
|
11
|
+
* ruflo has shipped: the #1943 sh/cmd probes, the earlier
|
|
12
|
+
* `${CLAUDE_PROJECT_DIR:-.}` / `%CLAUDE_PROJECT_DIR%` forms, the ADR-059
|
|
13
|
+
* `git rev-parse` node -e forms (CJS and ESM, with or without argv.splice),
|
|
14
|
+
* and plain relative paths. Returns null for anything else, so user-authored
|
|
15
|
+
* commands (other scripts, redirects, chains) are left alone.
|
|
16
|
+
*/
|
|
17
|
+
export declare function pinToUserHelpers(command: string, windows?: boolean): string | null;
|
|
18
|
+
//# sourceMappingURL=helper-commands.d.ts.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook and statusline commands that launch the `.claude/helpers` scripts.
|
|
3
|
+
*
|
|
4
|
+
* A project-level settings.json (`<project>/.claude/settings.json`) keeps the
|
|
5
|
+
* #1943 probe: run the project's helper, fall back to `$HOME/.claude/helpers`.
|
|
6
|
+
* The project controls that settings file anyway, so preferring its helpers
|
|
7
|
+
* grants it nothing new.
|
|
8
|
+
*
|
|
9
|
+
* A user-level settings.json (`~/.claude/settings.json`) applies to every
|
|
10
|
+
* project the user opens. There the probe would let any opened project supply
|
|
11
|
+
* the helper that runs, so user-level commands are pinned to the user's own
|
|
12
|
+
* `$HOME/.claude/helpers` with no project lookup.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
18
|
+
const STATUSLINE_SCRIPT = '.claude/helpers/statusline.cjs';
|
|
19
|
+
function canonical(dir) {
|
|
20
|
+
try {
|
|
21
|
+
return fs.realpathSync(dir);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return path.resolve(dir);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Settings written into the user's home directory are user-level settings. */
|
|
28
|
+
export function helperScopeFor(targetDir) {
|
|
29
|
+
if (!targetDir)
|
|
30
|
+
return 'project';
|
|
31
|
+
return canonical(targetDir) === canonical(os.homedir()) ? 'user' : 'project';
|
|
32
|
+
}
|
|
33
|
+
/** Command that runs `node <script> <subcommand>` for a hook. */
|
|
34
|
+
export function helperHookCommand(script, subcommand, scope, windows = IS_WINDOWS) {
|
|
35
|
+
const winScript = script.replace(/\//g, '\\');
|
|
36
|
+
const arg = subcommand ? ` ${subcommand}` : '';
|
|
37
|
+
if (scope === 'user') {
|
|
38
|
+
return windows
|
|
39
|
+
? `cmd /c "node "%USERPROFILE%\\${winScript}"${arg}"`
|
|
40
|
+
: `sh -c 'exec node "\${HOME}/${script}"${arg}'`;
|
|
41
|
+
}
|
|
42
|
+
if (windows) {
|
|
43
|
+
// cmd.exe equivalent of the sh probe below. `IF EXIST` checks the
|
|
44
|
+
// project-local path; falls back to %USERPROFILE% if missing.
|
|
45
|
+
return `cmd /c "IF EXIST \"%CLAUDE_PROJECT_DIR%\\${winScript}\" (node \"%CLAUDE_PROJECT_DIR%\\${winScript}\"${arg}) ELSE (node \"%USERPROFILE%\\${winScript}\"${arg})"`;
|
|
46
|
+
}
|
|
47
|
+
// POSIX sh: prefer project-local helpers, fall back to $HOME/.claude/.
|
|
48
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
49
|
+
const projVar = '${CLAUDE_PROJECT_DIR:-.}';
|
|
50
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
51
|
+
const homeVar = '${HOME}';
|
|
52
|
+
return `sh -c 'D="${projVar}"; [ -f "$D/${script}" ] || D="${homeVar}"; exec node "$D/${script}"${arg}'`;
|
|
53
|
+
}
|
|
54
|
+
/** Command for the statusLine entry. */
|
|
55
|
+
export function helperStatusLineCommand(scope, windows = IS_WINDOWS) {
|
|
56
|
+
if (windows) {
|
|
57
|
+
// The Node CLI's `-e` flag avoids all shell-quoting pitfalls (#1948,
|
|
58
|
+
// #1973): never `cmd /c` for statusline, it blocks stdin forwarding.
|
|
59
|
+
const home = "p.join(process.env.USERPROFILE||process.env.HOME||'.', '" + STATUSLINE_SCRIPT + "')";
|
|
60
|
+
const js = scope === 'user'
|
|
61
|
+
? `const p=require('path');require(${home});`
|
|
62
|
+
: "const fs=require('fs'),p=require('path');" +
|
|
63
|
+
`const d=process.env.CLAUDE_PROJECT_DIR||'.';` +
|
|
64
|
+
`const f=p.join(d,'${STATUSLINE_SCRIPT}');` +
|
|
65
|
+
`const h=${home};` +
|
|
66
|
+
'require(fs.existsSync(f)?f:h);';
|
|
67
|
+
return `node -e "${js}"`;
|
|
68
|
+
}
|
|
69
|
+
if (scope === 'user')
|
|
70
|
+
return `sh -c 'exec node "\${HOME}/${STATUSLINE_SCRIPT}"'`;
|
|
71
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
72
|
+
const projVar = '${CLAUDE_PROJECT_DIR:-.}';
|
|
73
|
+
// eslint-disable-next-line no-template-curly-in-string
|
|
74
|
+
const homeVar = '${HOME}';
|
|
75
|
+
return `sh -c 'D="${projVar}"; [ -f "$D/${STATUSLINE_SCRIPT}" ] || D="${homeVar}"; exec node "$D/${STATUSLINE_SCRIPT}"'`;
|
|
76
|
+
}
|
|
77
|
+
// Helpers ruflo installs under .claude/helpers that settings can launch.
|
|
78
|
+
const RUFLO_HELPERS = [
|
|
79
|
+
'hook-handler.cjs', 'auto-memory-hook.mjs', 'statusline.cjs', 'ruflo-hook.cjs', 'router.cjs',
|
|
80
|
+
'session.cjs', 'memory.cjs', 'intelligence.cjs', 'learning-service.mjs', 'metrics-db.mjs',
|
|
81
|
+
'context-persistence-hook.mjs',
|
|
82
|
+
];
|
|
83
|
+
const HELPER_REF = new RegExp(`\\.claude[\\\\/]helpers[\\\\/](${RUFLO_HELPERS.map((h) => h.replace('.', '\\.')).join('|')})`, 'g');
|
|
84
|
+
// How a ruflo-generated command reached the project's copy of a helper:
|
|
85
|
+
// CLAUDE_PROJECT_DIR (sh, cmd or node -e), `git rev-parse --show-toplevel`,
|
|
86
|
+
// or a path relative to the cwd.
|
|
87
|
+
const PROJECT_REACH = /CLAUDE_PROJECT_DIR|git rev-parse --show-toplevel/;
|
|
88
|
+
const RELATIVE_REF = /(?:^|[\s"'])(?:\.[\\/])?\.claude[\\/]helpers[\\/]/;
|
|
89
|
+
const SHELL_CHAINING = /&&|\|\||[;|<>`]/;
|
|
90
|
+
const ARGS = /^(?: [\w:.=-]+)*$/;
|
|
91
|
+
// The fixed shell scaffolding of the #1943 probes, which itself contains `;`
|
|
92
|
+
// and `||`; removed before checking the rest for chaining.
|
|
93
|
+
const PROBE_PREAMBLE = /D="\$\{CLAUDE_PROJECT_DIR:-\.\}"; \[ -f "\$D\/\.claude\/helpers\/[\w.-]+" \] \|\| D="\$\{HOME\}"; /;
|
|
94
|
+
const IF_EXIST_PREAMBLE = /"IF EXIST "%CLAUDE_PROJECT_DIR%\\\.claude\\helpers\\[\w.-]+" \(node "%CLAUDE_PROJECT_DIR%\\\.claude\\helpers\\[\w.-]+"(?: [\w:.=-]+)*\) ELSE \(/;
|
|
95
|
+
/**
|
|
96
|
+
* Rewrite a command that runs one of ruflo's helpers from the opened project
|
|
97
|
+
* into the pinned user-level form, keeping its subcommand. Covers every form
|
|
98
|
+
* ruflo has shipped: the #1943 sh/cmd probes, the earlier
|
|
99
|
+
* `${CLAUDE_PROJECT_DIR:-.}` / `%CLAUDE_PROJECT_DIR%` forms, the ADR-059
|
|
100
|
+
* `git rev-parse` node -e forms (CJS and ESM, with or without argv.splice),
|
|
101
|
+
* and plain relative paths. Returns null for anything else, so user-authored
|
|
102
|
+
* commands (other scripts, redirects, chains) are left alone.
|
|
103
|
+
*/
|
|
104
|
+
export function pinToUserHelpers(command, windows = IS_WINDOWS) {
|
|
105
|
+
const refs = [...command.matchAll(HELPER_REF)];
|
|
106
|
+
if (refs.length === 0)
|
|
107
|
+
return null;
|
|
108
|
+
const helper = refs[refs.length - 1][1];
|
|
109
|
+
if (refs.some((m) => m[1] !== helper))
|
|
110
|
+
return null;
|
|
111
|
+
if (!PROJECT_REACH.test(command) && !RELATIVE_REF.test(command))
|
|
112
|
+
return null;
|
|
113
|
+
let args;
|
|
114
|
+
const nodeEval = /^node -e "(.*)"((?: [\w:.=-]+)*)$/.exec(command);
|
|
115
|
+
if (nodeEval) {
|
|
116
|
+
if (!/git rev-parse --show-toplevel|CLAUDE_PROJECT_DIR/.test(nodeEval[1]))
|
|
117
|
+
return null;
|
|
118
|
+
args = nodeEval[2];
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
if (!/^(?:sh -c '|cmd \/c |node )/.test(command))
|
|
122
|
+
return null;
|
|
123
|
+
// Drop the shell wrapper's closing quote / paren, then split around the
|
|
124
|
+
// last helper reference: the head launches it, the tail is its argv.
|
|
125
|
+
const body = command.replace(/'$/, '').replace(/\)"$/, '');
|
|
126
|
+
const last = refs[refs.length - 1];
|
|
127
|
+
const head = body.slice(0, last.index ?? 0).replace(PROBE_PREAMBLE, '').replace(IF_EXIST_PREAMBLE, '');
|
|
128
|
+
let tail = body.slice((last.index ?? 0) + last[0].length);
|
|
129
|
+
if (tail.startsWith('"'))
|
|
130
|
+
tail = tail.slice(1);
|
|
131
|
+
if (SHELL_CHAINING.test(head) || SHELL_CHAINING.test(tail))
|
|
132
|
+
return null;
|
|
133
|
+
args = tail.trimEnd();
|
|
134
|
+
}
|
|
135
|
+
if (!ARGS.test(args))
|
|
136
|
+
return null;
|
|
137
|
+
const script = `.claude/helpers/${helper}`;
|
|
138
|
+
if (helper === 'statusline.cjs' && args === '')
|
|
139
|
+
return helperStatusLineCommand('user', windows);
|
|
140
|
+
return helperHookCommand(script, args.trim(), 'user', windows);
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=helper-commands.js.map
|
|
@@ -127,10 +127,52 @@ export function generateSessionManager() {
|
|
|
127
127
|
|
|
128
128
|
const fs = require('fs');
|
|
129
129
|
const path = require('path');
|
|
130
|
+
const crypto = require('crypto');
|
|
130
131
|
|
|
131
132
|
const SESSION_DIR = path.join(process.cwd(), '.claude-flow', 'sessions');
|
|
132
133
|
const SESSION_FILE = path.join(SESSION_DIR, 'current.json');
|
|
133
134
|
|
|
135
|
+
// Archive names are generated by start(); anything else in current.json is
|
|
136
|
+
// untrusted project content and must never pick a path.
|
|
137
|
+
const SESSION_ID = /^session-\\d+$/;
|
|
138
|
+
|
|
139
|
+
// current.json lives in the opened project. A symlink there must never be read
|
|
140
|
+
// or written through, so only a regular file counts as an active session.
|
|
141
|
+
function hasSessionFile() {
|
|
142
|
+
try { return fs.lstatSync(SESSION_FILE).isFile(); } catch { return false; }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function archiveId(id) {
|
|
146
|
+
return SESSION_ID.test(String(id)) ? String(id) : \`session-\${Date.now()}\`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// The session directory must resolve inside the project (no symlinked escape).
|
|
150
|
+
function assertSessionDirContained() {
|
|
151
|
+
const root = fs.realpathSync(process.cwd());
|
|
152
|
+
// Resolve the deepest part of the path that exists (lstat: a symlink counts),
|
|
153
|
+
// so a symlinked .claude-flow is refused before anything is created under it.
|
|
154
|
+
let dir = SESSION_DIR;
|
|
155
|
+
for (;;) {
|
|
156
|
+
try { fs.lstatSync(dir); break; } catch { /* not created yet */ }
|
|
157
|
+
const parent = path.dirname(dir);
|
|
158
|
+
if (parent === dir) break;
|
|
159
|
+
dir = parent;
|
|
160
|
+
}
|
|
161
|
+
const rel = path.relative(root, fs.realpathSync(dir));
|
|
162
|
+
if (rel === '..' || rel.startsWith(\`..\${path.sep}\`) || path.isAbsolute(rel)) {
|
|
163
|
+
throw new Error('Session directory resolves outside the project');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Exclusive temp file + rename: never writes through a planted symlink, and
|
|
168
|
+
// rename() replaces a symlinked target instead of following it.
|
|
169
|
+
function atomicWrite(file, data) {
|
|
170
|
+
assertSessionDirContained();
|
|
171
|
+
const tmp = \`\${file}.tmp.\${process.pid}.\${crypto.randomBytes(6).toString('hex')}\`;
|
|
172
|
+
fs.writeFileSync(tmp, data, { flag: 'wx' });
|
|
173
|
+
fs.renameSync(tmp, file);
|
|
174
|
+
}
|
|
175
|
+
|
|
134
176
|
const commands = {
|
|
135
177
|
start: () => {
|
|
136
178
|
const sessionId = \`session-\${Date.now()}\`;
|
|
@@ -147,29 +189,30 @@ const commands = {
|
|
|
147
189
|
},
|
|
148
190
|
};
|
|
149
191
|
|
|
192
|
+
assertSessionDirContained();
|
|
150
193
|
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
151
|
-
|
|
194
|
+
atomicWrite(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
152
195
|
|
|
153
196
|
console.log(\`Session started: \${sessionId}\`);
|
|
154
197
|
return session;
|
|
155
198
|
},
|
|
156
199
|
|
|
157
200
|
restore: () => {
|
|
158
|
-
if (!
|
|
201
|
+
if (!hasSessionFile()) {
|
|
159
202
|
console.log('No session to restore');
|
|
160
203
|
return null;
|
|
161
204
|
}
|
|
162
205
|
|
|
163
206
|
const session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
|
164
207
|
session.restoredAt = new Date().toISOString();
|
|
165
|
-
|
|
208
|
+
atomicWrite(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
166
209
|
|
|
167
210
|
console.log(\`Session restored: \${session.id}\`);
|
|
168
211
|
return session;
|
|
169
212
|
},
|
|
170
213
|
|
|
171
214
|
end: () => {
|
|
172
|
-
if (!
|
|
215
|
+
if (!hasSessionFile()) {
|
|
173
216
|
console.log('No active session');
|
|
174
217
|
return null;
|
|
175
218
|
}
|
|
@@ -178,12 +221,12 @@ const commands = {
|
|
|
178
221
|
session.endedAt = new Date().toISOString();
|
|
179
222
|
session.duration = Date.now() - new Date(session.startedAt).getTime();
|
|
180
223
|
|
|
181
|
-
// Archive session
|
|
182
|
-
const
|
|
183
|
-
|
|
224
|
+
// Archive session under a validated name; a crafted id cannot pick the path.
|
|
225
|
+
const id = archiveId(session.id);
|
|
226
|
+
atomicWrite(path.join(SESSION_DIR, \`\${id}.json\`), JSON.stringify(session, null, 2));
|
|
184
227
|
fs.unlinkSync(SESSION_FILE);
|
|
185
228
|
|
|
186
|
-
console.log(\`Session ended: \${
|
|
229
|
+
console.log(\`Session ended: \${id}\`);
|
|
187
230
|
console.log(\`Duration: \${Math.round(session.duration / 1000 / 60)} minutes\`);
|
|
188
231
|
console.log(\`Metrics: \${JSON.stringify(session.metrics)}\`);
|
|
189
232
|
|
|
@@ -191,7 +234,7 @@ const commands = {
|
|
|
191
234
|
},
|
|
192
235
|
|
|
193
236
|
status: () => {
|
|
194
|
-
if (!
|
|
237
|
+
if (!hasSessionFile()) {
|
|
195
238
|
console.log('No active session');
|
|
196
239
|
return null;
|
|
197
240
|
}
|
|
@@ -208,7 +251,7 @@ const commands = {
|
|
|
208
251
|
},
|
|
209
252
|
|
|
210
253
|
update: (key, value) => {
|
|
211
|
-
if (!
|
|
254
|
+
if (!hasSessionFile()) {
|
|
212
255
|
console.log('No active session');
|
|
213
256
|
return null;
|
|
214
257
|
}
|
|
@@ -216,20 +259,20 @@ const commands = {
|
|
|
216
259
|
const session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
|
217
260
|
session.context[key] = value;
|
|
218
261
|
session.updatedAt = new Date().toISOString();
|
|
219
|
-
|
|
262
|
+
atomicWrite(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
220
263
|
|
|
221
264
|
return session;
|
|
222
265
|
},
|
|
223
266
|
|
|
224
267
|
metric: (name) => {
|
|
225
|
-
if (!
|
|
268
|
+
if (!hasSessionFile()) {
|
|
226
269
|
return null;
|
|
227
270
|
}
|
|
228
271
|
|
|
229
272
|
const session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
|
230
273
|
if (session.metrics[name] !== undefined) {
|
|
231
274
|
session.metrics[name]++;
|
|
232
|
-
|
|
275
|
+
atomicWrite(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
233
276
|
}
|
|
234
277
|
|
|
235
278
|
return session;
|
|
@@ -606,7 +649,9 @@ export function generateHookHandler() {
|
|
|
606
649
|
'function spawnFunnelRefresh() {',
|
|
607
650
|
' try {',
|
|
608
651
|
" var cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';",
|
|
609
|
-
|
|
652
|
+
// --prefix: resolve the package from this helper's install root, never
|
|
653
|
+
// from the cwd (the opened project); the CLI still runs in the project.
|
|
654
|
+
" var args = ['--prefer-offline', '--prefix', path.resolve(helpersDir, '..', '..'), '@claude-flow/cli', 'hooks', 'refresh-funnel', '--quiet'];",
|
|
610
655
|
' var child = spawn(cmd, args, {',
|
|
611
656
|
" detached: true, stdio: 'ignore', env: Object.assign({}, process.env),",
|
|
612
657
|
' });',
|
|
@@ -1157,9 +1202,12 @@ import { fileURLToPath } from 'url';
|
|
|
1157
1202
|
|
|
1158
1203
|
const __filename = fileURLToPath(import.meta.url);
|
|
1159
1204
|
const __dirname = dirname(__filename);
|
|
1205
|
+
// Executable modules resolve only from the helper's own install root; a
|
|
1206
|
+
// home-level helper can serve an untrusted project. PROJECT_ROOT is data only.
|
|
1207
|
+
const MODULE_ROOT = join(__dirname, '../..');
|
|
1160
1208
|
const PROJECT_ROOT = process.env.CLAUDE_PROJECT_DIR
|
|
1161
1209
|
? resolve(process.env.CLAUDE_PROJECT_DIR)
|
|
1162
|
-
:
|
|
1210
|
+
: MODULE_ROOT;
|
|
1163
1211
|
const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
|
|
1164
1212
|
const STORE_PATH = join(DATA_DIR, 'auto-memory-store.json');
|
|
1165
1213
|
|
|
@@ -1171,7 +1219,7 @@ const dim = (msg) => console.log(\` \${DIM}\${msg}\${RESET}\`);
|
|
|
1171
1219
|
// #2545: fail LOUD instead of a silent dim skip when @claude-flow/memory is
|
|
1172
1220
|
// unresolvable — self-learning imports are a no-op and the user must be told.
|
|
1173
1221
|
function warnMemoryUnavailable() {
|
|
1174
|
-
const l1 = \`[AutoMemory] @claude-flow/memory not resolvable from \${
|
|
1222
|
+
const l1 = \`[AutoMemory] @claude-flow/memory not resolvable from \${MODULE_ROOT} — self-learning imports are DISABLED.\`;
|
|
1175
1223
|
const l2 = ' Fix: npm i -D @claude-flow/memory (or re-run: npx ruflo@latest init, then npx ruflo@latest doctor --fix)';
|
|
1176
1224
|
console.log(\`\${YELLOW}\${l1}\${RESET}\`);
|
|
1177
1225
|
console.log(\`\${YELLOW}\${l2}\${RESET}\`);
|
|
@@ -1186,7 +1234,7 @@ async function loadMemoryPackage() {
|
|
|
1186
1234
|
// path @claude-flow/memory lands in the npx cache (unreachable by walk-up), so
|
|
1187
1235
|
// init records its absolute path here — the only strategy that works there.
|
|
1188
1236
|
try {
|
|
1189
|
-
const sidecar = join(
|
|
1237
|
+
const sidecar = join(MODULE_ROOT, '.claude-flow', 'memory-package.json');
|
|
1190
1238
|
if (existsSync(sidecar)) {
|
|
1191
1239
|
const rec = JSON.parse(readFileSync(sidecar, 'utf-8'));
|
|
1192
1240
|
if (rec && rec.distPath && existsSync(rec.distPath)) {
|
|
@@ -1199,15 +1247,15 @@ async function loadMemoryPackage() {
|
|
|
1199
1247
|
// when installed as a transitive dependency via npx ruflo / npx claude-flow)
|
|
1200
1248
|
try {
|
|
1201
1249
|
const { createRequire } = await import('module');
|
|
1202
|
-
const require = createRequire(join(
|
|
1250
|
+
const require = createRequire(join(MODULE_ROOT, 'package.json'));
|
|
1203
1251
|
return require('@claude-flow/memory');
|
|
1204
1252
|
} catch { /* fall through */ }
|
|
1205
1253
|
|
|
1206
1254
|
// Strategy 2: ESM import (works when @claude-flow/memory is a direct dependency)
|
|
1207
1255
|
try { return await import('@claude-flow/memory'); } catch { /* fall through */ }
|
|
1208
1256
|
|
|
1209
|
-
// Strategy 3: Walk up from
|
|
1210
|
-
let searchDir =
|
|
1257
|
+
// Strategy 3: Walk up from MODULE_ROOT looking for the package in any node_modules
|
|
1258
|
+
let searchDir = MODULE_ROOT;
|
|
1211
1259
|
const { parse } = await import('path');
|
|
1212
1260
|
while (searchDir !== parse(searchDir).root) {
|
|
1213
1261
|
const candidate = join(searchDir, 'node_modules', '@claude-flow', 'memory', 'dist', 'index.js');
|
|
@@ -1405,6 +1453,7 @@ export function generateCrossPlatformSessionManager() {
|
|
|
1405
1453
|
|
|
1406
1454
|
const fs = require('fs');
|
|
1407
1455
|
const path = require('path');
|
|
1456
|
+
const crypto = require('crypto');
|
|
1408
1457
|
const os = require('os');
|
|
1409
1458
|
|
|
1410
1459
|
// Platform-specific paths
|
|
@@ -1431,6 +1480,49 @@ function getDataDir() {
|
|
|
1431
1480
|
const SESSION_DIR = getDataDir();
|
|
1432
1481
|
const SESSION_FILE = path.join(SESSION_DIR, 'current.json');
|
|
1433
1482
|
|
|
1483
|
+
// Archive names are generated by start(); anything else in current.json is
|
|
1484
|
+
// untrusted project content and must never pick a path.
|
|
1485
|
+
const SESSION_ID = /^session-\\d+$/;
|
|
1486
|
+
|
|
1487
|
+
// current.json lives in the opened project. A symlink there must never be read
|
|
1488
|
+
// or written through, so only a regular file counts as an active session.
|
|
1489
|
+
function hasSessionFile() {
|
|
1490
|
+
try { return fs.lstatSync(SESSION_FILE).isFile(); } catch { return false; }
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function archiveId(id) {
|
|
1494
|
+
return SESSION_ID.test(String(id)) ? String(id) : \`session-\${Date.now()}\`;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// The project session directory must resolve inside the project (no
|
|
1498
|
+
// symlinked escape). The per-user fallback directory is not project-owned.
|
|
1499
|
+
function assertSessionDirContained() {
|
|
1500
|
+
if (SESSION_DIR !== path.join(process.cwd(), '.claude-flow', 'sessions')) return;
|
|
1501
|
+
const root = fs.realpathSync(process.cwd());
|
|
1502
|
+
// Resolve the deepest part of the path that exists (lstat: a symlink counts),
|
|
1503
|
+
// so a symlinked .claude-flow is refused before anything is created under it.
|
|
1504
|
+
let dir = SESSION_DIR;
|
|
1505
|
+
for (;;) {
|
|
1506
|
+
try { fs.lstatSync(dir); break; } catch { /* not created yet */ }
|
|
1507
|
+
const parent = path.dirname(dir);
|
|
1508
|
+
if (parent === dir) break;
|
|
1509
|
+
dir = parent;
|
|
1510
|
+
}
|
|
1511
|
+
const rel = path.relative(root, fs.realpathSync(dir));
|
|
1512
|
+
if (rel === '..' || rel.startsWith(\`..\${path.sep}\`) || path.isAbsolute(rel)) {
|
|
1513
|
+
throw new Error('Session directory resolves outside the project');
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Exclusive temp file + rename: never writes through a planted symlink, and
|
|
1518
|
+
// rename() replaces a symlinked target instead of following it.
|
|
1519
|
+
function atomicWrite(file, data) {
|
|
1520
|
+
assertSessionDirContained();
|
|
1521
|
+
const tmp = \`\${file}.tmp.\${process.pid}.\${crypto.randomBytes(6).toString('hex')}\`;
|
|
1522
|
+
fs.writeFileSync(tmp, data, { flag: 'wx' });
|
|
1523
|
+
fs.renameSync(tmp, file);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1434
1526
|
// Ensure directory exists
|
|
1435
1527
|
function ensureDir(dir) {
|
|
1436
1528
|
if (!fs.existsSync(dir)) {
|
|
@@ -1440,6 +1532,7 @@ function ensureDir(dir) {
|
|
|
1440
1532
|
|
|
1441
1533
|
const commands = {
|
|
1442
1534
|
start: () => {
|
|
1535
|
+
assertSessionDirContained();
|
|
1443
1536
|
ensureDir(SESSION_DIR);
|
|
1444
1537
|
const sessionId = \`session-\${Date.now()}\`;
|
|
1445
1538
|
const session = {
|
|
@@ -1450,25 +1543,25 @@ const commands = {
|
|
|
1450
1543
|
context: {},
|
|
1451
1544
|
metrics: { edits: 0, commands: 0, tasks: 0, errors: 0 }
|
|
1452
1545
|
};
|
|
1453
|
-
|
|
1546
|
+
atomicWrite(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
1454
1547
|
console.log(\`Session started: \${sessionId}\`);
|
|
1455
1548
|
return session;
|
|
1456
1549
|
},
|
|
1457
1550
|
|
|
1458
1551
|
restore: () => {
|
|
1459
|
-
if (!
|
|
1552
|
+
if (!hasSessionFile()) {
|
|
1460
1553
|
console.log('No session to restore');
|
|
1461
1554
|
return null;
|
|
1462
1555
|
}
|
|
1463
1556
|
const session = JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
|
1464
1557
|
session.restoredAt = new Date().toISOString();
|
|
1465
|
-
|
|
1558
|
+
atomicWrite(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
1466
1559
|
console.log(\`Session restored: \${session.id}\`);
|
|
1467
1560
|
return session;
|
|
1468
1561
|
},
|
|
1469
1562
|
|
|
1470
1563
|
end: () => {
|
|
1471
|
-
if (!
|
|
1564
|
+
if (!hasSessionFile()) {
|
|
1472
1565
|
console.log('No active session');
|
|
1473
1566
|
return null;
|
|
1474
1567
|
}
|
|
@@ -1476,17 +1569,18 @@ const commands = {
|
|
|
1476
1569
|
session.endedAt = new Date().toISOString();
|
|
1477
1570
|
session.duration = Date.now() - new Date(session.startedAt).getTime();
|
|
1478
1571
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1572
|
+
// Archive under a validated name; a crafted id cannot pick the path.
|
|
1573
|
+
const id = archiveId(session.id);
|
|
1574
|
+
atomicWrite(path.join(SESSION_DIR, \`\${id}.json\`), JSON.stringify(session, null, 2));
|
|
1481
1575
|
fs.unlinkSync(SESSION_FILE);
|
|
1482
1576
|
|
|
1483
|
-
console.log(\`Session ended: \${
|
|
1577
|
+
console.log(\`Session ended: \${id}\`);
|
|
1484
1578
|
console.log(\`Duration: \${Math.round(session.duration / 1000 / 60)} minutes\`);
|
|
1485
1579
|
return session;
|
|
1486
1580
|
},
|
|
1487
1581
|
|
|
1488
1582
|
status: () => {
|
|
1489
|
-
if (!
|
|
1583
|
+
if (!hasSessionFile()) {
|
|
1490
1584
|
console.log('No active session');
|
|
1491
1585
|
return null;
|
|
1492
1586
|
}
|
|
@@ -1615,6 +1709,9 @@ function resolveCommandPath(command, env = process.env, platform = process.platf
|
|
|
1615
1709
|
? (envValue(env, 'PATHEXT') || '.COM;.EXE;.BAT;.CMD').split(';')
|
|
1616
1710
|
: [''];
|
|
1617
1711
|
for (const dir of dirs) {
|
|
1712
|
+
// A relative or empty PATH entry resolves against the cwd, i.e. the
|
|
1713
|
+
// opened project; never resolve the CLI from there.
|
|
1714
|
+
if (!hasSeparator && !(platform === 'win32' ? path.win32 : path.posix).isAbsolute(dir)) continue;
|
|
1618
1715
|
for (const ext of extensions) {
|
|
1619
1716
|
const base = path.resolve(dir || '.', command);
|
|
1620
1717
|
const candidates = ext
|
|
@@ -1767,7 +1864,9 @@ function main() {
|
|
|
1767
1864
|
// exists but cannot be resolved to an entrypoint still runs, via layer 2.
|
|
1768
1865
|
if (resolveCommandPath('ruflo')) { invokeHook('ruflo', [], hookArgs, stdinData); done(); }
|
|
1769
1866
|
if (resolveCommandPath('claude-flow')) { invokeHook('claude-flow', [], hookArgs, stdinData); done(); }
|
|
1770
|
-
|
|
1867
|
+
// --prefix: npx resolves ruflo from this helper's install root, never from
|
|
1868
|
+
// the cwd (the opened project); the CLI still runs in the project.
|
|
1869
|
+
invokeHook('npx', ['--prefer-offline', '--yes', '--prefix', path.resolve(__dirname, '..', '..'), 'ruflo@latest'], hookArgs, stdinData);
|
|
1771
1870
|
done();
|
|
1772
1871
|
}
|
|
1773
1872
|
|