claude-flow 3.5.3 → 3.5.4

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.
@@ -139,17 +139,30 @@ async function loadMemoryPackage() {
139
139
  } catch { /* fall through */ }
140
140
  }
141
141
 
142
- // Strategy 2: npm installed @claude-flow/memory
142
+ // Strategy 2: Use createRequire for CJS-style resolution (handles nested node_modules
143
+ // when installed as a transitive dependency via npx ruflo / npx claude-flow)
144
+ try {
145
+ const { createRequire } = await import('module');
146
+ const require = createRequire(join(PROJECT_ROOT, 'package.json'));
147
+ return require('@claude-flow/memory');
148
+ } catch { /* fall through */ }
149
+
150
+ // Strategy 3: ESM import (works when @claude-flow/memory is a direct dependency)
143
151
  try {
144
152
  return await import('@claude-flow/memory');
145
153
  } catch { /* fall through */ }
146
154
 
147
- // Strategy 3: Installed via @claude-flow/cli which includes memory
148
- const cliMemory = join(PROJECT_ROOT, 'node_modules/@claude-flow/memory/dist/index.js');
149
- if (existsSync(cliMemory)) {
150
- try {
151
- return await import(`file://${cliMemory}`);
152
- } catch { /* fall through */ }
155
+ // Strategy 4: Walk up from PROJECT_ROOT looking for @claude-flow/memory in any node_modules
156
+ let searchDir = PROJECT_ROOT;
157
+ const { parse } = await import('path');
158
+ while (searchDir !== parse(searchDir).root) {
159
+ const candidate = join(searchDir, 'node_modules', '@claude-flow', 'memory', 'dist', 'index.js');
160
+ if (existsSync(candidate)) {
161
+ try {
162
+ return await import(`file://${candidate}`);
163
+ } catch { /* fall through */ }
164
+ }
165
+ searchDir = dirname(searchDir);
153
166
  }
154
167
 
155
168
  return null;
@@ -36,7 +36,30 @@ const memory = safeRequire(path.join(helpersDir, 'memory.cjs'));
36
36
  const intelligence = safeRequire(path.join(helpersDir, 'intelligence.cjs'));
37
37
 
38
38
  const [,, command, ...args] = process.argv;
39
- const prompt = process.env.PROMPT || process.env.TOOL_INPUT_command || args.join(' ') || '';
39
+
40
+ // Read stdin — Claude Code sends hook data as JSON via stdin
41
+ async function readStdin() {
42
+ if (process.stdin.isTTY) return '';
43
+ let data = '';
44
+ process.stdin.setEncoding('utf8');
45
+ for await (const chunk of process.stdin) {
46
+ data += chunk;
47
+ }
48
+ return data;
49
+ }
50
+
51
+ async function main() {
52
+ let stdinData = '';
53
+ try { stdinData = await readStdin(); } catch (e) { /* ignore stdin errors */ }
54
+
55
+ let hookInput = {};
56
+ if (stdinData.trim()) {
57
+ try { hookInput = JSON.parse(stdinData); } catch (e) { /* ignore parse errors */ }
58
+ }
59
+
60
+ // Merge stdin data into prompt resolution: prefer stdin fields, then env, then argv
61
+ const prompt = hookInput.prompt || hookInput.command || hookInput.toolInput
62
+ || process.env.PROMPT || process.env.TOOL_INPUT_command || args.join(' ') || '';
40
63
 
41
64
  const handlers = {
42
65
  'route': () => {
@@ -63,7 +86,7 @@ const handlers = {
63
86
  },
64
87
 
65
88
  'pre-bash': () => {
66
- var cmd = prompt.toLowerCase();
89
+ var cmd = (hookInput.command || prompt).toLowerCase();
67
90
  var dangerous = ['rm -rf /', 'format c:', 'del /s /q c:\\', ':(){:|:&};:'];
68
91
  for (var i = 0; i < dangerous.length; i++) {
69
92
  if (cmd.includes(dangerous[i])) {
@@ -80,7 +103,8 @@ const handlers = {
80
103
  }
81
104
  if (intelligence && intelligence.recordEdit) {
82
105
  try {
83
- var file = process.env.TOOL_INPUT_file_path || args[0] || '';
106
+ var file = hookInput.file_path || (hookInput.toolInput && hookInput.toolInput.file_path)
107
+ || process.env.TOOL_INPUT_file_path || args[0] || '';
84
108
  intelligence.recordEdit(file);
85
109
  } catch (e) { /* non-fatal */ }
86
110
  }
@@ -179,13 +203,18 @@ const handlers = {
179
203
  };
180
204
 
181
205
  if (command && handlers[command]) {
182
- try {
183
- handlers[command]();
184
- } catch (e) {
185
- console.log('[WARN] Hook ' + command + ' encountered an error: ' + e.message);
206
+ try {
207
+ handlers[command]();
208
+ } catch (e) {
209
+ console.log('[WARN] Hook ' + command + ' encountered an error: ' + e.message);
210
+ }
211
+ } else if (command) {
212
+ console.log('[OK] Hook: ' + command);
213
+ } else {
214
+ console.log('Usage: hook-handler.cjs <route|pre-bash|post-edit|session-restore|session-end|pre-task|post-task|compact-manual|compact-auto|status|stats>');
186
215
  }
187
- } else if (command) {
188
- console.log('[OK] Hook: ' + command);
189
- } else {
190
- console.log('Usage: hook-handler.cjs <route|pre-bash|post-edit|session-restore|session-end|pre-task|post-task|compact-manual|compact-auto|status|stats>');
191
216
  }
217
+
218
+ main().catch(function(e) {
219
+ console.log('[WARN] Hook handler error: ' + e.message);
220
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.5.3",
3
+ "version": "3.5.4",
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",
@@ -192,10 +192,15 @@ async function checkDiskSpace() {
192
192
  if (process.platform === 'win32') {
193
193
  return { name: 'Disk Space', status: 'pass', message: 'Check skipped on Windows' };
194
194
  }
195
- const output_str = await runCommand('df -h . | tail -1');
195
+ // Use df -Ph for POSIX mode (guarantees single-line output even with long device names)
196
+ const output_str = await runCommand('df -Ph . | tail -1');
196
197
  const parts = output_str.split(/\s+/);
198
+ // POSIX format: Filesystem Size Used Avail Capacity Mounted
197
199
  const available = parts[3];
198
200
  const usePercent = parseInt(parts[4]?.replace('%', '') || '0', 10);
201
+ if (isNaN(usePercent)) {
202
+ return { name: 'Disk Space', status: 'warn', message: `${available || 'unknown'} available (unable to parse usage)` };
203
+ }
199
204
  if (usePercent > 90) {
200
205
  return { name: 'Disk Space', status: 'fail', message: `${available} available (${usePercent}% used)`, fix: 'Free up disk space' };
201
206
  }
@@ -247,7 +247,19 @@ const statusCommand = {
247
247
  description: 'Show MCP server status',
248
248
  action: async (ctx) => {
249
249
  try {
250
- const status = await getMCPServerStatus();
250
+ let status = await getMCPServerStatus();
251
+ // If PID-based check says not running, detect stdio mode
252
+ if (!status.running) {
253
+ const isStdio = !process.stdin.isTTY;
254
+ const envTransport = process.env.CLAUDE_FLOW_MCP_TRANSPORT;
255
+ if (isStdio || envTransport === 'stdio') {
256
+ status = {
257
+ running: true,
258
+ pid: process.pid,
259
+ transport: 'stdio',
260
+ };
261
+ }
262
+ }
251
263
  if (ctx.flags.format === 'json') {
252
264
  output.printJson(status);
253
265
  return { success: true, data: status };
@@ -273,9 +285,12 @@ const statusCommand = {
273
285
  { metric: 'Status', value: output.success('Running') },
274
286
  { metric: 'PID', value: status.pid },
275
287
  { metric: 'Transport', value: status.transport },
276
- { metric: 'Host', value: status.host },
277
- { metric: 'Port', value: status.port },
278
288
  ];
289
+ // Only show host/port for non-stdio transports
290
+ if (status.transport !== 'stdio') {
291
+ displayData.push({ metric: 'Host', value: status.host });
292
+ displayData.push({ metric: 'Port', value: status.port });
293
+ }
279
294
  if (status.uptime !== undefined) {
280
295
  displayData.push({ metric: 'Uptime', value: formatUptime(status.uptime) });
281
296
  }
@@ -223,7 +223,12 @@ function displayStatus(status) {
223
223
  // MCP section
224
224
  output.writeln(output.bold('MCP Server'));
225
225
  if (status.mcp.running) {
226
- output.printInfo(` Running on port ${status.mcp.port} (${status.mcp.transport})`);
226
+ if (status.mcp.transport === 'stdio') {
227
+ output.printInfo(' Running (stdio mode)');
228
+ }
229
+ else {
230
+ output.printInfo(` Running on port ${status.mcp.port} (${status.mcp.transport})`);
231
+ }
227
232
  }
228
233
  else {
229
234
  output.printInfo(' Not running');
@@ -316,7 +321,9 @@ async function performHealthCheck(status) {
316
321
  checks.push({
317
322
  name: 'MCP Server',
318
323
  status: status.mcp.running ? 'pass' : 'warn',
319
- message: status.mcp.running ? `Running on port ${status.mcp.port}` : 'Not running'
324
+ message: status.mcp.running
325
+ ? (status.mcp.transport === 'stdio' ? 'Running (stdio mode)' : `Running on port ${status.mcp.port}`)
326
+ : 'Not running'
320
327
  });
321
328
  // Check memory backend
322
329
  checks.push({
@@ -794,10 +794,34 @@ const dim = (msg) => console.log(\` \${DIM}\${msg}\${RESET}\`);
794
794
  // Ensure data dir
795
795
  if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
796
796
 
797
+ async function loadMemoryPackage() {
798
+ // Strategy 1: Use createRequire for CJS-style resolution (handles nested node_modules
799
+ // when installed as a transitive dependency via npx ruflo / npx claude-flow)
800
+ try {
801
+ const { createRequire } = await import('module');
802
+ const require = createRequire(join(PROJECT_ROOT, 'package.json'));
803
+ return require('@claude-flow/memory');
804
+ } catch { /* fall through */ }
805
+
806
+ // Strategy 2: ESM import (works when @claude-flow/memory is a direct dependency)
807
+ try { return await import('@claude-flow/memory'); } catch { /* fall through */ }
808
+
809
+ // Strategy 3: Walk up from PROJECT_ROOT looking for the package in any node_modules
810
+ let searchDir = PROJECT_ROOT;
811
+ const { parse } = await import('path');
812
+ while (searchDir !== parse(searchDir).root) {
813
+ const candidate = join(searchDir, 'node_modules', '@claude-flow', 'memory', 'dist', 'index.js');
814
+ if (existsSync(candidate)) {
815
+ try { return await import(\`file://\${candidate}\`); } catch { /* fall through */ }
816
+ }
817
+ searchDir = dirname(searchDir);
818
+ }
819
+
820
+ return null;
821
+ }
822
+
797
823
  async function doImport() {
798
- // Try loading @claude-flow/memory for full functionality
799
- let memPkg = null;
800
- try { memPkg = await import('@claude-flow/memory'); } catch {}
824
+ const memPkg = await loadMemoryPackage();
801
825
 
802
826
  if (!memPkg || !memPkg.AutoMemoryBridge) {
803
827
  dim('Memory package not available — auto memory import skipped (non-critical)');
@@ -814,8 +838,7 @@ async function doSync() {
814
838
  return;
815
839
  }
816
840
 
817
- let memPkg = null;
818
- try { memPkg = await import('@claude-flow/memory'); } catch {}
841
+ const memPkg = await loadMemoryPackage();
819
842
 
820
843
  if (!memPkg || !memPkg.AutoMemoryBridge) {
821
844
  dim('Memory package not available — sync skipped (non-critical)');
@@ -142,6 +142,21 @@ export class MCPServerManager extends EventEmitter {
142
142
  // Check PID file
143
143
  const pid = await this.readPidFile();
144
144
  if (!pid) {
145
+ // No PID file found. Detect if we are running in stdio mode
146
+ // (e.g., launched by Claude Code via `claude mcp add`).
147
+ const isStdio = !process.stdin.isTTY;
148
+ const envTransport = process.env.CLAUDE_FLOW_MCP_TRANSPORT;
149
+ if (isStdio || envTransport === 'stdio' || this.options.transport === 'stdio') {
150
+ return {
151
+ running: true,
152
+ pid: process.pid,
153
+ transport: 'stdio',
154
+ startedAt: this.startTime?.toISOString(),
155
+ uptime: this.startTime
156
+ ? Math.floor((Date.now() - this.startTime.getTime()) / 1000)
157
+ : undefined,
158
+ };
159
+ }
145
160
  return { running: false };
146
161
  }
147
162
  // Check if process is running
@@ -459,6 +474,7 @@ export class MCPServerManager extends EventEmitter {
459
474
  this.emit('health-error', error);
460
475
  }
461
476
  }, 30000);
477
+ this.healthCheckInterval.unref();
462
478
  }
463
479
  /**
464
480
  * Write PID file
@@ -512,12 +512,32 @@ export const hooksPostEdit = {
512
512
  handler: async (params) => {
513
513
  const filePath = params.filePath;
514
514
  const success = params.success !== false;
515
+ const agent = params.agent;
516
+ // Wire recordFeedback through bridge (issue #1209)
517
+ let feedbackResult = null;
518
+ try {
519
+ const bridge = await import('../memory/memory-bridge.js');
520
+ feedbackResult = await bridge.bridgeRecordFeedback({
521
+ taskId: `edit-${filePath}-${Date.now()}`,
522
+ success,
523
+ quality: success ? 0.85 : 0.3,
524
+ agent,
525
+ });
526
+ }
527
+ catch {
528
+ // Bridge not available — continue with basic response
529
+ }
515
530
  return {
516
531
  recorded: true,
517
532
  filePath,
518
533
  success,
519
534
  timestamp: new Date().toISOString(),
520
535
  learningUpdate: success ? 'pattern_reinforced' : 'pattern_adjusted',
536
+ feedback: feedbackResult ? {
537
+ recorded: feedbackResult.success,
538
+ controller: feedbackResult.controller,
539
+ updates: feedbackResult.updated,
540
+ } : { recorded: false, controller: 'unavailable', updates: 0 },
521
541
  };
522
542
  },
523
543
  };
@@ -325,5 +325,93 @@ export const systemTools = [
325
325
  };
326
326
  },
327
327
  },
328
+ {
329
+ name: 'mcp_status',
330
+ description: 'Get MCP server status, including stdio mode detection',
331
+ category: 'system',
332
+ inputSchema: {
333
+ type: 'object',
334
+ properties: {},
335
+ },
336
+ handler: async () => {
337
+ // Detect if we are running inside an MCP stdio session.
338
+ // When Claude Code launches us via `claude mcp add`, stdin is piped (not a TTY)
339
+ // and the process IS the MCP server, so it is running.
340
+ const isStdio = !process.stdin.isTTY;
341
+ const transport = process.env.CLAUDE_FLOW_MCP_TRANSPORT || (isStdio ? 'stdio' : 'http');
342
+ const port = parseInt(process.env.CLAUDE_FLOW_MCP_PORT || '3000', 10);
343
+ if (transport === 'stdio' || isStdio) {
344
+ // In stdio mode the MCP server is this process itself
345
+ return {
346
+ running: true,
347
+ pid: process.pid,
348
+ transport: 'stdio',
349
+ port: null,
350
+ host: null,
351
+ };
352
+ }
353
+ // For HTTP/WebSocket, try to check if the server is listening
354
+ const host = process.env.CLAUDE_FLOW_MCP_HOST || 'localhost';
355
+ try {
356
+ const { createConnection } = await import('node:net');
357
+ const connected = await new Promise((resolve) => {
358
+ const socket = createConnection({ host, port }, () => {
359
+ socket.destroy();
360
+ resolve(true);
361
+ });
362
+ socket.on('error', () => resolve(false));
363
+ socket.setTimeout(2000, () => {
364
+ socket.destroy();
365
+ resolve(false);
366
+ });
367
+ });
368
+ return {
369
+ running: connected,
370
+ transport,
371
+ port,
372
+ host,
373
+ };
374
+ }
375
+ catch {
376
+ return {
377
+ running: false,
378
+ transport,
379
+ port,
380
+ host,
381
+ };
382
+ }
383
+ },
384
+ },
385
+ {
386
+ name: 'task_summary',
387
+ description: 'Get a summary of all tasks by status',
388
+ category: 'task',
389
+ inputSchema: {
390
+ type: 'object',
391
+ properties: {},
392
+ },
393
+ handler: async () => {
394
+ // Read from the task store file
395
+ const storePath = join(process.cwd(), '.claude-flow', 'tasks', 'store.json');
396
+ let tasks = [];
397
+ try {
398
+ if (existsSync(storePath)) {
399
+ const data = readFileSync(storePath, 'utf-8');
400
+ const store = JSON.parse(data);
401
+ tasks = Object.values(store.tasks || {});
402
+ }
403
+ }
404
+ catch {
405
+ // empty store
406
+ }
407
+ return {
408
+ total: tasks.length,
409
+ pending: tasks.filter(t => t.status === 'pending').length,
410
+ running: tasks.filter(t => t.status === 'in_progress').length,
411
+ completed: tasks.filter(t => t.status === 'completed').length,
412
+ failed: tasks.filter(t => t.status === 'failed').length,
413
+ };
414
+ },
415
+ },
328
416
  ];
329
417
  //# sourceMappingURL=system-tools.js.map
@@ -263,6 +263,42 @@ export const taskTools = [
263
263
  };
264
264
  },
265
265
  },
266
+ {
267
+ name: 'task_assign',
268
+ description: 'Assign a task to one or more agents',
269
+ category: 'task',
270
+ inputSchema: {
271
+ type: 'object',
272
+ properties: {
273
+ taskId: { type: 'string', description: 'Task ID to assign' },
274
+ agentIds: { type: 'array', items: { type: 'string' }, description: 'Agent IDs to assign' },
275
+ unassign: { type: 'boolean', description: 'Unassign all agents from task' },
276
+ },
277
+ required: ['taskId'],
278
+ },
279
+ handler: async (input) => {
280
+ const store = loadTaskStore();
281
+ const taskId = input.taskId;
282
+ const task = store.tasks[taskId];
283
+ if (!task) {
284
+ return { taskId, error: 'Task not found' };
285
+ }
286
+ const previouslyAssigned = [...task.assignedTo];
287
+ if (input.unassign) {
288
+ task.assignedTo = [];
289
+ }
290
+ else {
291
+ const agentIds = input.agentIds || [];
292
+ task.assignedTo = agentIds;
293
+ }
294
+ saveTaskStore(store);
295
+ return {
296
+ taskId: task.taskId,
297
+ assignedTo: task.assignedTo,
298
+ previouslyAssigned,
299
+ };
300
+ },
301
+ },
266
302
  {
267
303
  name: 'task_cancel',
268
304
  description: 'Cancel a task',
@@ -39,6 +39,97 @@ function saveWorkflowStore(store) {
39
39
  writeFileSync(getWorkflowPath(), JSON.stringify(store, null, 2), 'utf-8');
40
40
  }
41
41
  export const workflowTools = [
42
+ {
43
+ name: 'workflow_run',
44
+ description: 'Run a workflow from a template or file',
45
+ category: 'workflow',
46
+ inputSchema: {
47
+ type: 'object',
48
+ properties: {
49
+ template: { type: 'string', description: 'Template name to run' },
50
+ file: { type: 'string', description: 'Workflow file path' },
51
+ task: { type: 'string', description: 'Task description' },
52
+ options: {
53
+ type: 'object',
54
+ description: 'Workflow options',
55
+ properties: {
56
+ parallel: { type: 'boolean', description: 'Run stages in parallel' },
57
+ maxAgents: { type: 'number', description: 'Maximum agents to use' },
58
+ timeout: { type: 'number', description: 'Timeout in seconds' },
59
+ dryRun: { type: 'boolean', description: 'Validate without executing' },
60
+ },
61
+ },
62
+ },
63
+ },
64
+ handler: async (input) => {
65
+ const store = loadWorkflowStore();
66
+ const template = input.template;
67
+ const task = input.task;
68
+ const options = input.options || {};
69
+ const dryRun = options.dryRun;
70
+ // Build workflow from template or inline
71
+ const workflowId = `workflow-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
72
+ const stages = [];
73
+ // Generate stages based on template
74
+ const templateName = template || 'custom';
75
+ const stageNames = (() => {
76
+ switch (templateName) {
77
+ case 'feature':
78
+ return ['Research', 'Design', 'Implement', 'Test', 'Review'];
79
+ case 'bugfix':
80
+ return ['Investigate', 'Fix', 'Test', 'Review'];
81
+ case 'refactor':
82
+ return ['Analyze', 'Refactor', 'Test', 'Review'];
83
+ case 'security':
84
+ return ['Scan', 'Analyze', 'Report'];
85
+ default:
86
+ return ['Execute'];
87
+ }
88
+ })();
89
+ for (const name of stageNames) {
90
+ stages.push({
91
+ name,
92
+ status: dryRun ? 'validated' : 'pending',
93
+ agents: [],
94
+ });
95
+ }
96
+ if (!dryRun) {
97
+ // Create and save the workflow
98
+ const steps = stageNames.map((name, i) => ({
99
+ stepId: `step-${i + 1}`,
100
+ name,
101
+ type: 'task',
102
+ config: { task: task || name },
103
+ status: 'pending',
104
+ }));
105
+ const workflow = {
106
+ workflowId,
107
+ name: task || `${templateName} workflow`,
108
+ description: task,
109
+ steps,
110
+ status: 'running',
111
+ currentStep: 0,
112
+ variables: { template: templateName, ...options },
113
+ createdAt: new Date().toISOString(),
114
+ startedAt: new Date().toISOString(),
115
+ };
116
+ store.workflows[workflowId] = workflow;
117
+ saveWorkflowStore(store);
118
+ }
119
+ return {
120
+ workflowId,
121
+ template: templateName,
122
+ status: dryRun ? 'validated' : 'running',
123
+ stages,
124
+ metrics: {
125
+ totalStages: stages.length,
126
+ completedStages: 0,
127
+ agentsSpawned: 0,
128
+ estimatedDuration: `${stages.length * 30}s`,
129
+ },
130
+ };
131
+ },
132
+ },
42
133
  {
43
134
  name: 'workflow_create',
44
135
  description: 'Create a new workflow',
@@ -83,6 +83,7 @@ async function getRegistry(dbPath) {
83
83
  tieredCache: true,
84
84
  hierarchicalMemory: true,
85
85
  memoryConsolidation: true,
86
+ memoryGraph: true, // issue #1214: enable MemoryGraph for graph-aware ranking
86
87
  },
87
88
  });
88
89
  }
@@ -481,6 +481,7 @@ export class Spinner {
481
481
  this.render();
482
482
  this.frameIndex = (this.frameIndex + 1) % this.frames.length;
483
483
  }, 100);
484
+ this.interval.unref();
484
485
  this.render();
485
486
  }
486
487
  stop(message) {
@@ -481,6 +481,7 @@ export class ContainerWorkerPool extends EventEmitter {
481
481
  this.healthCheckTimer = setInterval(async () => {
482
482
  await this.runHealthChecks();
483
483
  }, this.config.healthCheckIntervalMs);
484
+ this.healthCheckTimer.unref();
484
485
  }
485
486
  /**
486
487
  * Run health checks on all containers
@@ -522,6 +523,7 @@ export class ContainerWorkerPool extends EventEmitter {
522
523
  this.idleCheckTimer = setInterval(async () => {
523
524
  await this.runIdleChecks();
524
525
  }, 60000); // Check every minute
526
+ this.idleCheckTimer.unref();
525
527
  }
526
528
  /**
527
529
  * Terminate idle containers above minimum
@@ -57,6 +57,7 @@ class InMemoryStore {
57
57
  if (this.cleanupTimer)
58
58
  return;
59
59
  this.cleanupTimer = setInterval(() => this.cleanupExpired(), 60000);
60
+ this.cleanupTimer.unref();
60
61
  }
61
62
  /**
62
63
  * Stop cleanup timer
@@ -495,6 +496,7 @@ export class WorkerQueue extends EventEmitter {
495
496
  this.store.setWorker(this.workerId, registration);
496
497
  }
497
498
  }, this.config.heartbeatIntervalMs);
499
+ this.heartbeatTimer.unref();
498
500
  }
499
501
  /**
500
502
  * Stop heartbeat timer
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.5.3",
3
+ "version": "3.5.4",
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",