claude-flow 3.31.2 → 3.32.0
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/package.json +1 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +80 -1
- package/v3/@claude-flow/cli/dist/src/index.js +16 -1
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.d.ts +49 -20
- package/v3/@claude-flow/cli/dist/src/init/helper-refresh.js +102 -37
- package/v3/@claude-flow/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-flow",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.32.0",
|
|
4
4
|
"description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -1404,6 +1404,20 @@ export const doctorCommand = {
|
|
|
1404
1404
|
description: 'Verbose output',
|
|
1405
1405
|
type: 'boolean',
|
|
1406
1406
|
default: false
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
name: 'fix-handles',
|
|
1410
|
+
// Windows-only mitigation for anthropics/claude-code#67888 — Claude Code's
|
|
1411
|
+
// Bash tool spawns cmd.exe/bash.exe without cleaning up child conhost.exe
|
|
1412
|
+
// handles, so a long session accumulates dozens (observed live: 26+
|
|
1413
|
+
// orphaned conhost.exe after a ~4h session). Each holds a kernel object
|
|
1414
|
+
// + ~1MB, and combined with memory pressure this measurably slows the
|
|
1415
|
+
// machine. This flag kills orphan conhost.exe (safe — Windows respawns
|
|
1416
|
+
// on demand). Deliberately does NOT touch cmd.exe/bash.exe — those can
|
|
1417
|
+
// be the invoking shell, and killing them 255's the caller.
|
|
1418
|
+
description: 'Windows only: kill orphaned conhost.exe processes leaked by Claude Code (mitigation for anthropics/claude-code#67888)',
|
|
1419
|
+
type: 'boolean',
|
|
1420
|
+
default: false
|
|
1407
1421
|
}
|
|
1408
1422
|
],
|
|
1409
1423
|
examples: [
|
|
@@ -1411,13 +1425,78 @@ export const doctorCommand = {
|
|
|
1411
1425
|
{ command: 'claude-flow doctor --fix', description: 'Print suggested fix commands (does not auto-apply)' },
|
|
1412
1426
|
{ command: 'claude-flow doctor --install', description: 'Auto-install missing dependencies' },
|
|
1413
1427
|
{ command: 'claude-flow doctor -c version', description: 'Check for stale npx cache' },
|
|
1414
|
-
{ command: 'claude-flow doctor -c claude', description: 'Check Claude Code CLI only' }
|
|
1428
|
+
{ command: 'claude-flow doctor -c claude', description: 'Check Claude Code CLI only' },
|
|
1429
|
+
{ command: 'claude-flow doctor --fix-handles', description: 'Windows: kill leaked conhost.exe from Claude Code sessions' }
|
|
1415
1430
|
],
|
|
1416
1431
|
action: async (ctx) => {
|
|
1417
1432
|
const showFix = ctx.flags.fix;
|
|
1418
1433
|
const autoInstall = ctx.flags.install;
|
|
1419
1434
|
const component = ctx.flags.component;
|
|
1420
1435
|
const verbose = ctx.flags.verbose;
|
|
1436
|
+
// Parser camelCases kebab-case flag names — read via `fixHandles`, not `['fix-handles']`.
|
|
1437
|
+
const fixHandles = ctx.flags.fixHandles;
|
|
1438
|
+
// Early-return short-circuit: `--fix-handles` is a targeted mitigation, not
|
|
1439
|
+
// part of the health-check flow. Runs, reports, exits.
|
|
1440
|
+
if (fixHandles) {
|
|
1441
|
+
output.writeln();
|
|
1442
|
+
output.writeln(output.bold('RuFlo Doctor — fix-handles'));
|
|
1443
|
+
output.writeln(output.dim('─'.repeat(50)));
|
|
1444
|
+
output.writeln();
|
|
1445
|
+
if (process.platform !== 'win32') {
|
|
1446
|
+
output.printInfo('--fix-handles is a Windows-only mitigation. On this platform (' + process.platform + '), no action taken.');
|
|
1447
|
+
return { success: true };
|
|
1448
|
+
}
|
|
1449
|
+
const { spawnSync } = await import('child_process');
|
|
1450
|
+
// PowerShell one-liner: read before-count, kill conhost, read after-count, report delta.
|
|
1451
|
+
const psScript = [
|
|
1452
|
+
"$before = (Get-Process conhost -EA SilentlyContinue).Count",
|
|
1453
|
+
"$mem_before = [math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1MB, 2)",
|
|
1454
|
+
"$killed = 0",
|
|
1455
|
+
"Get-Process conhost -EA SilentlyContinue | ForEach-Object {",
|
|
1456
|
+
" try { Stop-Process -Id $_.Id -Force -EA Stop; $killed++ } catch {}",
|
|
1457
|
+
"}",
|
|
1458
|
+
"Start-Sleep -Seconds 1",
|
|
1459
|
+
"$after = (Get-Process conhost -EA SilentlyContinue).Count",
|
|
1460
|
+
"$mem_after = [math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1MB, 2)",
|
|
1461
|
+
"Write-Output ('BEFORE_COUNT=' + $before)",
|
|
1462
|
+
"Write-Output ('KILLED=' + $killed)",
|
|
1463
|
+
"Write-Output ('AFTER_COUNT=' + $after)",
|
|
1464
|
+
"Write-Output ('MEM_BEFORE_GB=' + $mem_before)",
|
|
1465
|
+
"Write-Output ('MEM_AFTER_GB=' + $mem_after)",
|
|
1466
|
+
].join('; ');
|
|
1467
|
+
const res = spawnSync('powershell', ['-NoProfile', '-Command', psScript], {
|
|
1468
|
+
encoding: 'utf-8', timeout: 30000, windowsHide: true,
|
|
1469
|
+
});
|
|
1470
|
+
if (res.status !== 0) {
|
|
1471
|
+
output.printError('PowerShell exited with code ' + res.status);
|
|
1472
|
+
if (res.stderr)
|
|
1473
|
+
output.writeln(output.dim(res.stderr));
|
|
1474
|
+
return { success: false, exitCode: 1 };
|
|
1475
|
+
}
|
|
1476
|
+
const parse = (key) => {
|
|
1477
|
+
const line = (res.stdout || '').split('\n').find((l) => l.startsWith(key + '='));
|
|
1478
|
+
return line ? line.slice(key.length + 1).trim() : '?';
|
|
1479
|
+
};
|
|
1480
|
+
const before = parse('BEFORE_COUNT');
|
|
1481
|
+
const killed = parse('KILLED');
|
|
1482
|
+
const after = parse('AFTER_COUNT');
|
|
1483
|
+
const memBefore = parse('MEM_BEFORE_GB');
|
|
1484
|
+
const memAfter = parse('MEM_AFTER_GB');
|
|
1485
|
+
output.writeln('conhost.exe processes:');
|
|
1486
|
+
output.writeln(' before: ' + before);
|
|
1487
|
+
output.writeln(' killed: ' + output.success(killed));
|
|
1488
|
+
output.writeln(' after: ' + after);
|
|
1489
|
+
output.writeln('');
|
|
1490
|
+
output.writeln('Free RAM:');
|
|
1491
|
+
output.writeln(' before: ' + memBefore + ' GB');
|
|
1492
|
+
output.writeln(' after: ' + memAfter + ' GB');
|
|
1493
|
+
output.writeln('');
|
|
1494
|
+
output.writeln(output.dim('Note: does NOT touch cmd.exe/bash.exe/node.exe — those may be the invoking shell'));
|
|
1495
|
+
output.writeln(output.dim(' or an active MCP server. Kill them manually if you need to.'));
|
|
1496
|
+
output.writeln('');
|
|
1497
|
+
output.writeln(output.dim('Upstream tracking: https://github.com/anthropics/claude-code/issues/67888'));
|
|
1498
|
+
return { success: true };
|
|
1499
|
+
}
|
|
1421
1500
|
output.writeln();
|
|
1422
1501
|
output.writeln(output.bold('RuFlo Doctor'));
|
|
1423
1502
|
output.writeln(output.dim('System diagnostics and health check'));
|
|
@@ -122,7 +122,16 @@ export class CLI {
|
|
|
122
122
|
if (commandPath[0] !== 'init' && commandPath[0] !== 'update') {
|
|
123
123
|
try {
|
|
124
124
|
const { autoRefreshHelpersIfStale } = await import('./init/helper-refresh.js');
|
|
125
|
-
|
|
125
|
+
// alsoRefreshGlobal:true — refresh ~/.claude/helpers too, not just
|
|
126
|
+
// <cwd>/.claude/helpers. Fixes the "promo row missing on remote
|
|
127
|
+
// installs" bug where Claude Code's global settings.json falls back
|
|
128
|
+
// to ~/.claude/helpers/statusline.cjs (executor.ts:460-462) and that
|
|
129
|
+
// file was frozen at whatever version was current when the user
|
|
130
|
+
// last ran `ruflo init` — pre-3.31.3 nothing refreshed it, so any
|
|
131
|
+
// helpers change (e.g. the 2026-07-13 Line-3 funnel row addition)
|
|
132
|
+
// never reached existing installs. Same forward-only semver.gte
|
|
133
|
+
// guard applies to the global pass.
|
|
134
|
+
const r = await autoRefreshHelpersIfStale(process.cwd(), { alsoRefreshGlobal: true });
|
|
126
135
|
if (r.blocked) {
|
|
127
136
|
// Integrity failure = potential on-disk tampering of hook code. Warn
|
|
128
137
|
// loudly (not silent) — the existing project helpers were left intact.
|
|
@@ -131,6 +140,12 @@ export class CLI {
|
|
|
131
140
|
else if (r.refreshed && this.output.isVerbose()) {
|
|
132
141
|
this.output.printDebug(`Refreshed .claude/helpers (${r.from} → ${r.to})`);
|
|
133
142
|
}
|
|
143
|
+
if (r.global?.refreshed && this.output.isVerbose()) {
|
|
144
|
+
this.output.printDebug(`Refreshed ~/.claude/helpers (${r.global.from} → ${r.global.to})`);
|
|
145
|
+
}
|
|
146
|
+
else if (r.global?.blocked && r.global.blocked !== r.blocked) {
|
|
147
|
+
this.output.printWarning(`Skipped ~/.claude/helpers auto-refresh — ${r.global.blocked}.`);
|
|
148
|
+
}
|
|
134
149
|
}
|
|
135
150
|
catch { /* silent */ }
|
|
136
151
|
// ADR-177: adopt a signed proven-configuration champion if the package
|
|
@@ -9,42 +9,71 @@ export declare const CRITICAL_HELPERS: string[];
|
|
|
9
9
|
/** Installed @claude-flow/cli version — the value the helpers are stamped with. */
|
|
10
10
|
export declare function getInstalledCliVersion(): string;
|
|
11
11
|
/**
|
|
12
|
-
* On CLI startup
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* On CLI startup, refresh critical helpers if their stamp is older than the
|
|
13
|
+
* installed CLI version. Two passes:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Project pass** — `<cwd>/.claude/helpers/`. Always attempted. The
|
|
16
|
+
* original behavior; project statuslines pin to a stamp per repo.
|
|
17
|
+
*
|
|
18
|
+
* 2. **Global pass** — `~/.claude/helpers/`. Opt-in via `alsoRefreshGlobal`.
|
|
19
|
+
* Fixes the "promo row missing on remote installs" bug: `ruflo init`
|
|
20
|
+
* writes helpers to `~/.claude/helpers/` too so Claude Code's global
|
|
21
|
+
* settings.json statusLine can fall back to them (executor.ts:460-462),
|
|
22
|
+
* but nothing ever REFRESHED that global copy — so any install predating
|
|
23
|
+
* a helpers change (e.g. the 2026-07-13 funnel/promo Line-3 addition)
|
|
24
|
+
* stayed frozen at the pre-feature statusline forever, even after `npm
|
|
25
|
+
* i -g @claude-flow/cli@latest`. The global pass fixes that on the next
|
|
26
|
+
* `ruflo <anything>` invocation. Same forward-only `semver.gte` guard
|
|
27
|
+
* protects against downgrade by a stale cached CLI.
|
|
28
|
+
*
|
|
29
|
+
* `alsoRefreshGlobal` defaults FALSE so tests don't touch the developer's
|
|
30
|
+
* real `~/.claude/helpers/`. The real CLI entry (src/index.ts) passes
|
|
31
|
+
* `true` to activate the global pass in production.
|
|
32
|
+
*
|
|
33
|
+
* Best-effort, never throws. No-op for a helpers dir that doesn't already
|
|
34
|
+
* contain a `hook-handler.cjs` — never creates files in an unrelated dir.
|
|
17
35
|
*
|
|
18
36
|
* FORWARD-ONLY (never downgrades): refreshing on any mere INEQUALITY, rather
|
|
19
37
|
* than only when the installed version is semver-NEWER, is a real corruption
|
|
20
38
|
* vector — confirmed live: a stray/older installed binary (a stale `npx`
|
|
21
39
|
* cache, a marketplace install lagging behind an unpublished dev-tree fix)
|
|
22
|
-
* running `daemon start` (or any command)
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
40
|
+
* running `daemon start` (or any command) would see its own older version !=
|
|
41
|
+
* the project's newer stamp and silently overwrite hand-fixed
|
|
42
|
+
* `hook-handler.cjs`/`intelligence.cjs` with its own older, already-superseded
|
|
43
|
+
* bundled copies. Comparing with `semver.gt` instead of `!==` makes that
|
|
44
|
+
* impossible: an older or equal installed version is always a no-op,
|
|
45
|
+
* regardless of how it got invoked.
|
|
28
46
|
*
|
|
29
47
|
* `opts` exists for tests ONLY (mirrors daemon-autostart.ts's injectable
|
|
30
|
-
* `SpawnDaemonFn` pattern): the real signed-copy path is otherwise coupled
|
|
31
|
-
* THIS repo's actual current `.claude/helpers` + its real Ed25519
|
|
32
|
-
* fine for production (that coupling to the real source IS the
|
|
33
|
-
* it means a test exercising that path for real would only pass
|
|
34
|
-
* repo's manifest happens to be currently re-signed, which is a
|
|
35
|
-
* gated, occasionally-stale publish-time step. `sourceDirOverride`
|
|
36
|
-
* `pubkeyPemOverride` let a test build its own tiny, throwaway-keypair-
|
|
37
|
-
* signed fixture and get real, deterministic coverage of the verify → hash
|
|
38
|
-
* copy logic without depending on that.
|
|
48
|
+
* `SpawnDaemonFn` pattern): the real signed-copy path is otherwise coupled
|
|
49
|
+
* to THIS repo's actual current `.claude/helpers` + its real Ed25519
|
|
50
|
+
* signature — fine for production (that coupling to the real source IS the
|
|
51
|
+
* point), but it means a test exercising that path for real would only pass
|
|
52
|
+
* when this repo's manifest happens to be currently re-signed, which is a
|
|
53
|
+
* separately-gated, occasionally-stale publish-time step. `sourceDirOverride`
|
|
54
|
+
* + `pubkeyPemOverride` let a test build its own tiny, throwaway-keypair-
|
|
55
|
+
* signed fixture and get real, deterministic coverage of the verify → hash
|
|
56
|
+
* → copy logic without depending on that.
|
|
57
|
+
*
|
|
58
|
+
* Return shape: the project-pass result is the top-level object (backwards-
|
|
59
|
+
* compat with pre-3.31.3 callers). If the global pass ran, its own result is
|
|
60
|
+
* carried in the optional `global` field.
|
|
39
61
|
*/
|
|
40
62
|
export declare function autoRefreshHelpersIfStale(cwd: string, opts?: {
|
|
41
63
|
sourceDirOverride?: string;
|
|
42
64
|
pubkeyPemOverride?: string;
|
|
43
65
|
versionOverride?: string;
|
|
66
|
+
alsoRefreshGlobal?: boolean;
|
|
44
67
|
}): Promise<{
|
|
45
68
|
refreshed: boolean;
|
|
46
69
|
from?: string;
|
|
47
70
|
to?: string;
|
|
48
71
|
blocked?: string;
|
|
72
|
+
global?: {
|
|
73
|
+
refreshed: boolean;
|
|
74
|
+
from?: string;
|
|
75
|
+
to?: string;
|
|
76
|
+
blocked?: string;
|
|
77
|
+
};
|
|
49
78
|
}>;
|
|
50
79
|
//# sourceMappingURL=helper-refresh.d.ts.map
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* heavy generators only on the rare fallback path (source dir unresolvable).
|
|
15
15
|
*/
|
|
16
16
|
import * as fs from 'fs';
|
|
17
|
+
import * as os from 'os';
|
|
17
18
|
import * as path from 'path';
|
|
18
19
|
import { fileURLToPath } from 'url';
|
|
19
20
|
import { createRequire } from 'module';
|
|
@@ -230,50 +231,114 @@ async function writeCriticalHelpers(helpersDir, version, opts = {}) {
|
|
|
230
231
|
* signed fixture and get real, deterministic coverage of the verify → hash →
|
|
231
232
|
* copy logic without depending on that.
|
|
232
233
|
*/
|
|
234
|
+
async function refreshOneHelpersDir(helpersDir, version, opts) {
|
|
235
|
+
if (!fs.existsSync(path.join(helpersDir, 'hook-handler.cjs')))
|
|
236
|
+
return { refreshed: false };
|
|
237
|
+
// .LOCKED marker: users developing ruflo itself (or any project with
|
|
238
|
+
// hand-maintained helpers) can place a `.LOCKED` file at
|
|
239
|
+
// `.claude/helpers/.LOCKED` to opt out of auto-refresh entirely. Fixes the
|
|
240
|
+
// observed-live concurrent-session clobber where a sibling Claude Code
|
|
241
|
+
// session running a stale cached CLI would overwrite hand-edited helpers
|
|
242
|
+
// on this repo (CLAUDE.md "Concurrent-session helper corruption"). The
|
|
243
|
+
// existing semver.gte guard below still fires for normal installs — this
|
|
244
|
+
// is the escape hatch for the small set of users editing helpers directly.
|
|
245
|
+
// Applies to whichever dir this call is refreshing (project or global).
|
|
246
|
+
if (fs.existsSync(path.join(helpersDir, '.LOCKED'))) {
|
|
247
|
+
return { refreshed: false, blocked: '.LOCKED marker present — refresh skipped (delete to re-enable)' };
|
|
248
|
+
}
|
|
249
|
+
let stamped = '';
|
|
250
|
+
try {
|
|
251
|
+
stamped = fs.readFileSync(path.join(helpersDir, HELPERS_STAMP_FILE), 'utf-8').trim();
|
|
252
|
+
}
|
|
253
|
+
catch { /* pre-feature: unstamped */ }
|
|
254
|
+
if (stamped === version)
|
|
255
|
+
return { refreshed: false }; // up to date — fast path
|
|
256
|
+
if (stamped && semver.valid(stamped) && semver.valid(version) && semver.gte(stamped, version)) {
|
|
257
|
+
// Stamped version is already >= what this binary reports — refreshing
|
|
258
|
+
// would silently DOWNGRADE the helpers. Skip, untouched.
|
|
259
|
+
return { refreshed: false };
|
|
260
|
+
}
|
|
261
|
+
const res = await writeCriticalHelpers(helpersDir, version, {
|
|
262
|
+
sourceDirOverride: opts.sourceDirOverride,
|
|
263
|
+
pubkeyPemOverride: opts.pubkeyPemOverride,
|
|
264
|
+
});
|
|
265
|
+
if (res.blocked)
|
|
266
|
+
return { refreshed: false, blocked: res.blocked };
|
|
267
|
+
return res.wrote ? { refreshed: true, from: stamped || '(unstamped)', to: version } : { refreshed: false };
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* On CLI startup, refresh critical helpers if their stamp is older than the
|
|
271
|
+
* installed CLI version. Two passes:
|
|
272
|
+
*
|
|
273
|
+
* 1. **Project pass** — `<cwd>/.claude/helpers/`. Always attempted. The
|
|
274
|
+
* original behavior; project statuslines pin to a stamp per repo.
|
|
275
|
+
*
|
|
276
|
+
* 2. **Global pass** — `~/.claude/helpers/`. Opt-in via `alsoRefreshGlobal`.
|
|
277
|
+
* Fixes the "promo row missing on remote installs" bug: `ruflo init`
|
|
278
|
+
* writes helpers to `~/.claude/helpers/` too so Claude Code's global
|
|
279
|
+
* settings.json statusLine can fall back to them (executor.ts:460-462),
|
|
280
|
+
* but nothing ever REFRESHED that global copy — so any install predating
|
|
281
|
+
* a helpers change (e.g. the 2026-07-13 funnel/promo Line-3 addition)
|
|
282
|
+
* stayed frozen at the pre-feature statusline forever, even after `npm
|
|
283
|
+
* i -g @claude-flow/cli@latest`. The global pass fixes that on the next
|
|
284
|
+
* `ruflo <anything>` invocation. Same forward-only `semver.gte` guard
|
|
285
|
+
* protects against downgrade by a stale cached CLI.
|
|
286
|
+
*
|
|
287
|
+
* `alsoRefreshGlobal` defaults FALSE so tests don't touch the developer's
|
|
288
|
+
* real `~/.claude/helpers/`. The real CLI entry (src/index.ts) passes
|
|
289
|
+
* `true` to activate the global pass in production.
|
|
290
|
+
*
|
|
291
|
+
* Best-effort, never throws. No-op for a helpers dir that doesn't already
|
|
292
|
+
* contain a `hook-handler.cjs` — never creates files in an unrelated dir.
|
|
293
|
+
*
|
|
294
|
+
* FORWARD-ONLY (never downgrades): refreshing on any mere INEQUALITY, rather
|
|
295
|
+
* than only when the installed version is semver-NEWER, is a real corruption
|
|
296
|
+
* vector — confirmed live: a stray/older installed binary (a stale `npx`
|
|
297
|
+
* cache, a marketplace install lagging behind an unpublished dev-tree fix)
|
|
298
|
+
* running `daemon start` (or any command) would see its own older version !=
|
|
299
|
+
* the project's newer stamp and silently overwrite hand-fixed
|
|
300
|
+
* `hook-handler.cjs`/`intelligence.cjs` with its own older, already-superseded
|
|
301
|
+
* bundled copies. Comparing with `semver.gt` instead of `!==` makes that
|
|
302
|
+
* impossible: an older or equal installed version is always a no-op,
|
|
303
|
+
* regardless of how it got invoked.
|
|
304
|
+
*
|
|
305
|
+
* `opts` exists for tests ONLY (mirrors daemon-autostart.ts's injectable
|
|
306
|
+
* `SpawnDaemonFn` pattern): the real signed-copy path is otherwise coupled
|
|
307
|
+
* to THIS repo's actual current `.claude/helpers` + its real Ed25519
|
|
308
|
+
* signature — fine for production (that coupling to the real source IS the
|
|
309
|
+
* point), but it means a test exercising that path for real would only pass
|
|
310
|
+
* when this repo's manifest happens to be currently re-signed, which is a
|
|
311
|
+
* separately-gated, occasionally-stale publish-time step. `sourceDirOverride`
|
|
312
|
+
* + `pubkeyPemOverride` let a test build its own tiny, throwaway-keypair-
|
|
313
|
+
* signed fixture and get real, deterministic coverage of the verify → hash
|
|
314
|
+
* → copy logic without depending on that.
|
|
315
|
+
*
|
|
316
|
+
* Return shape: the project-pass result is the top-level object (backwards-
|
|
317
|
+
* compat with pre-3.31.3 callers). If the global pass ran, its own result is
|
|
318
|
+
* carried in the optional `global` field.
|
|
319
|
+
*/
|
|
233
320
|
export async function autoRefreshHelpersIfStale(cwd, opts = {}) {
|
|
234
321
|
try {
|
|
235
|
-
|
|
236
|
-
if (!fs.existsSync(path.join(helpersDir, 'hook-handler.cjs')))
|
|
237
|
-
return { refreshed: false };
|
|
238
|
-
// .LOCKED marker: users developing ruflo itself (or any project with
|
|
239
|
-
// hand-maintained helpers) can place a `.LOCKED` file at
|
|
240
|
-
// `.claude/helpers/.LOCKED` to opt this project out of auto-refresh
|
|
241
|
-
// entirely. Fixes the observed-live concurrent-session clobber where a
|
|
242
|
-
// sibling Claude Code session running a stale cached CLI would overwrite
|
|
243
|
-
// hand-edited helpers on this repo (CLAUDE.md "Concurrent-session helper
|
|
244
|
-
// corruption"). Existing semver.gte guard below still fires for normal
|
|
245
|
-
// installs — this is the escape hatch for the small set of users editing
|
|
246
|
-
// helpers directly. Delete the file to re-enable refresh.
|
|
247
|
-
if (fs.existsSync(path.join(helpersDir, '.LOCKED'))) {
|
|
248
|
-
return { refreshed: false, blocked: '.LOCKED marker present — refresh skipped (delete to re-enable)' };
|
|
249
|
-
}
|
|
250
|
-
// Also honor an env-level opt-out for CI / release-time scripting that
|
|
251
|
-
// knows it doesn't want any writes to helpers this run.
|
|
322
|
+
// Env-level opt-out — applies to BOTH project and global passes.
|
|
252
323
|
if (/^(1|true|on|yes)$/i.test(String(process.env.RUFLO_HELPERS_LOCKED || ''))) {
|
|
253
324
|
return { refreshed: false, blocked: 'RUFLO_HELPERS_LOCKED env — refresh skipped' };
|
|
254
325
|
}
|
|
255
326
|
const version = opts.versionOverride ?? getInstalledCliVersion();
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
327
|
+
const projectDir = path.join(cwd, '.claude', 'helpers');
|
|
328
|
+
const projectResult = await refreshOneHelpersDir(projectDir, version, opts);
|
|
329
|
+
// Global pass — opt-in only; test callers omit alsoRefreshGlobal to avoid
|
|
330
|
+
// touching the developer's real ~/.claude/helpers.
|
|
331
|
+
if (opts.alsoRefreshGlobal) {
|
|
332
|
+
const globalDir = path.join(os.homedir(), '.claude', 'helpers');
|
|
333
|
+
// Skip if project === global (e.g. someone invoked ruflo from $HOME
|
|
334
|
+
// and $HOME happens to be a ruflo project — refreshing twice is
|
|
335
|
+
// redundant AND could second-guess the first pass's result).
|
|
336
|
+
if (path.resolve(globalDir) !== path.resolve(projectDir)) {
|
|
337
|
+
const globalResult = await refreshOneHelpersDir(globalDir, version, opts);
|
|
338
|
+
return { ...projectResult, global: globalResult };
|
|
339
|
+
}
|
|
267
340
|
}
|
|
268
|
-
|
|
269
|
-
sourceDirOverride: opts.sourceDirOverride,
|
|
270
|
-
pubkeyPemOverride: opts.pubkeyPemOverride,
|
|
271
|
-
});
|
|
272
|
-
// A blocked refresh is a SECURITY signal (tampered source/manifest) — surface
|
|
273
|
-
// it, don't advance the stamp, and leave the project's existing helpers intact.
|
|
274
|
-
if (res.blocked)
|
|
275
|
-
return { refreshed: false, blocked: res.blocked };
|
|
276
|
-
return res.wrote ? { refreshed: true, from: stamped || '(unstamped)', to: version } : { refreshed: false };
|
|
341
|
+
return projectResult;
|
|
277
342
|
}
|
|
278
343
|
catch {
|
|
279
344
|
return { refreshed: false };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-flow/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.32.0",
|
|
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",
|