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.
- package/.claude/helpers/auto-memory-hook.mjs +20 -7
- package/.claude/helpers/hook-handler.cjs +40 -11
- package/package.json +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +6 -1
- package/v3/@claude-flow/cli/dist/src/commands/mcp.js +18 -3
- package/v3/@claude-flow/cli/dist/src/commands/status.js +9 -2
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +28 -5
- package/v3/@claude-flow/cli/dist/src/mcp-server.js +16 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +20 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/system-tools.js +88 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/task-tools.js +36 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/workflow-tools.js +91 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +1 -0
- package/v3/@claude-flow/cli/dist/src/output.js +1 -0
- package/v3/@claude-flow/cli/dist/src/services/container-worker-pool.js +2 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-queue.js +2 -0
- package/v3/@claude-flow/cli/package.json +1 -1
|
@@ -139,17 +139,30 @@ async function loadMemoryPackage() {
|
|
|
139
139
|
} catch { /* fall through */ }
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
// Strategy 2:
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
"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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
@@ -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
|
+
"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",
|