@sethdouglasford/claude-flow
Version:
Claude Code Flow - Advanced AI-powered development workflows with SPARC methodology
709 lines (692 loc) ⢠26.9 kB
JavaScript
/**
* Simple orchestrator implementation for Node.js compatibility
*/
import { EventEmitter } from "events";
import express from "express";
import { WebSocketServer } from "ws";
import { createServer } from "http";
import { spawn } from "child_process";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Simple in-memory stores
const agents = new Map();
const tasks = new Map();
const memory = new Map();
// Event bus
const eventBus = new EventEmitter();
// Component status
const componentStatus = {
eventBus: false,
orchestrator: false,
memoryManager: false,
terminalPool: false,
mcpServer: false,
coordinationManager: false,
webUI: false,
};
// Simple MCP server
function startMCPServer(port) {
console.log(`š Starting MCP server on port ${port}...`);
// In a real implementation, this would start the actual MCP server
componentStatus.mcpServer = true;
return true;
}
// Enhanced web UI with console interface
function startWebUI(host, port) {
const app = express();
const server = createServer(app);
const wss = new WebSocketServer({ server });
// Store CLI output history and active connections
const outputHistory = [];
const activeConnections = new Set();
// CLI output capture system
const _cliProcess = null;
const consoleHTML = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Claude-Flow Console</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
background: #0d1117;
color: #c9d1d9;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header {
background: #161b22;
border-bottom: 1px solid #21262d;
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-weight: bold;
color: #58a6ff;
}
.connection-status {
font-size: 12px;
color: #7c3aed;
}
.console-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.console-output {
flex: 1;
overflow-y: auto;
padding: 10px;
background: #0d1117;
font-size: 13px;
line-height: 1.4;
white-space: pre-wrap;
word-wrap: break-word;
}
.console-input {
background: #161b22;
border: none;
border-top: 1px solid #21262d;
padding: 10px;
color: #c9d1d9;
font-family: inherit;
font-size: 13px;
outline: none;
}
.console-input:focus {
background: #21262d;
}
.prompt {
color: #58a6ff;
font-weight: bold;
}
.error {
color: #ff7b72;
}
.success {
color: #3fb950;
}
.warning {
color: #ffa657;
}
.info {
color: #79c0ff;
}
.dim {
color: #8b949e;
}
.scrollbar {
scrollbar-width: thin;
scrollbar-color: #21262d #0d1117;
}
.scrollbar::-webkit-scrollbar {
width: 8px;
}
.scrollbar::-webkit-scrollbar-track {
background: #0d1117;
}
.scrollbar::-webkit-scrollbar-thumb {
background: #21262d;
border-radius: 4px;
}
.scrollbar::-webkit-scrollbar-thumb:hover {
background: #30363d;
}
.system-status {
display: flex;
gap: 15px;
font-size: 11px;
}
.status-item {
display: flex;
align-items: center;
gap: 5px;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #3fb950;
}
.status-dot.inactive {
background: #f85149;
}
</style>
</head>
<body>
<div class="header">
<div class="title">š§ Claude-Flow Console</div>
<div class="system-status">
<div class="status-item">
<div class="status-dot" id="ws-status"></div>
<span id="ws-text">Connecting...</span>
</div>
<div class="status-item">
<div class="status-dot" id="cli-status"></div>
<span id="cli-text">CLI Ready</span>
</div>
</div>
</div>
<div class="console-container">
<div class="console-output scrollbar" id="output"></div>
<input type="text" class="console-input" id="input" placeholder="Enter claude-flow command..." autocomplete="off">
</div>
<script>
const output = document.getElementById('output');
const input = document.getElementById('input');
const wsStatus = document.getElementById('ws-status');
const wsText = document.getElementById('ws-text');
const cliStatus = document.getElementById('cli-status');
const cliText = document.getElementById('cli-text');
let ws = null;
let commandHistory = [];
let historyIndex = -1;
function connect() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(\`\${protocol}//\${window.location.host}\`);
ws.onopen = () => {
wsStatus.classList.remove('inactive');
wsText.textContent = 'Connected';
appendOutput('\n<span class="success">š Connected to Claude-Flow Console</span>\n');
appendOutput('<span class="info">Type "help" for available commands or use any claude-flow command</span>\n\n');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleMessage(data);
};
ws.onclose = () => {
wsStatus.classList.add('inactive');
wsText.textContent = 'Disconnected';
appendOutput('\n<span class="error">š Connection lost. Reconnecting...</span>\n');
setTimeout(connect, 3000);
};
ws.onerror = (error) => {
appendOutput('\n<span class="error">ā WebSocket error</span>\n');
};
}
function handleMessage(data) {
switch (data.type) {
case 'output':
appendOutput(data.data);
break;
case 'error':
appendOutput('<span class="error">' + data.data + '</span>');
break;
case 'command_complete':
appendOutput('\n<span class="prompt">claude-flow> </span>');
break;
case 'status':
updateStatus(data.data);
break;
}
}
function appendOutput(text) {
output.innerHTML += text;
output.scrollTop = output.scrollHeight;
}
function updateStatus(status) {
// Update CLI status based on server response
if (status.cliActive) {
cliStatus.classList.remove('inactive');
cliText.textContent = 'CLI Active';
} else {
cliStatus.classList.add('inactive');
cliText.textContent = 'CLI Inactive';
}
}
function sendCommand(command) {
if (ws && ws.readyState === WebSocket.OPEN) {
appendOutput('<span class="prompt">claude-flow> </span>' + command + '\n');
ws.send(JSON.stringify({
type: 'command',
data: command
}));
// Add to history
if (command.trim() && commandHistory[commandHistory.length - 1] !== command) {
commandHistory.push(command);
if (commandHistory.length > 100) {
commandHistory.shift();
}
}
historyIndex = commandHistory.length;
}
}
// Input handling
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const command = input.value.trim();
if (command) {
sendCommand(command);
input.value = '';
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (historyIndex > 0) {
historyIndex--;
input.value = commandHistory[historyIndex] || '';
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
input.value = commandHistory[historyIndex] || '';
} else {
historyIndex = commandHistory.length;
input.value = '';
}
} else if (e.key === 'Tab') {
e.preventDefault();
// Basic tab completion for common commands
const value = input.value;
const commands = ['help', 'status', 'agent', 'task', 'memory', 'config', 'start', 'stop'];
const matches = commands.filter(cmd => cmd.startsWith(value));
if (matches.length === 1) {
input.value = matches[0] + ' ';
}
}
});
// Focus input on page load
window.addEventListener('load', () => {
input.focus();
connect();
});
// Keep input focused
document.addEventListener('click', () => {
input.focus();
});
</script>
</body>
</html>
`;
app.get("/", (req, res) => {
res.send(consoleHTML);
});
// API endpoints
app.get("/api/status", (req, res) => {
res.json({
components: componentStatus,
metrics: {
agents: agents.size,
tasks: tasks.size,
memory: memory.size,
connectedClients: activeConnections.size,
},
});
});
app.get("/api/history", (req, res) => {
const limit = parseInt(req.query.limit) || 100;
res.json({
history: outputHistory.slice(-limit),
total: outputHistory.length,
});
});
app.post("/api/command", express.json(), (req, res) => {
const { command } = req.body;
if (!command) {
return res.status(400).json({ error: "Command is required" });
}
// Execute command and return immediately
// Output will be sent via WebSocket
try {
broadcastToClients({
type: "output",
data: `<span class="prompt">API> </span>${command}\\n`,
});
executeCliCommand(command, null);
return res.json({ success: true, message: "Command executed" });
}
catch (error) {
return res.status(500).json({ error: error.message });
}
});
app.get("/api/agents", (req, res) => {
const agentList = Array.from(agents.entries()).map(([id, agent]) => ({
id,
...agent,
}));
res.json(agentList);
});
app.get("/api/tasks", (req, res) => {
const taskList = Array.from(tasks.entries()).map(([id, task]) => ({
id,
...task,
}));
res.json(taskList);
});
app.get("/api/memory", (req, res) => {
const memoryList = Array.from(memory.entries()).map(([key, value]) => ({
key,
value,
type: typeof value,
size: JSON.stringify(value).length,
}));
res.json(memoryList);
});
// Health check endpoint
app.get("/health", (req, res) => {
res.json({
status: "healthy",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
components: componentStatus,
});
});
// WebSocket for real-time CLI interaction
wss.on("connection", (ws) => {
console.log("š WebSocket client connected");
activeConnections.add(ws);
// Send initial status and history
ws.send(JSON.stringify({
type: "status",
data: { ...componentStatus, cliActive: true },
}));
// Send recent output history
outputHistory.slice(-50).forEach(line => {
ws.send(JSON.stringify({
type: "output",
data: line,
}));
});
// Handle incoming commands
ws.on("message", (message) => {
try {
const data = JSON.parse(String(message));
if (data.type === "command") {
handleCliCommand(data.data, ws);
}
}
catch (error) {
ws.send(JSON.stringify({
type: "error",
data: `Invalid message format: ${error.message}`,
}));
}
});
ws.on("close", () => {
console.log("š WebSocket client disconnected");
activeConnections.delete(ws);
});
ws.on("error", (error) => {
console.error("WebSocket error:", error);
activeConnections.delete(ws);
});
});
// Helper function to send response to specific client or broadcast
function sendResponse(ws, data) {
if (ws) {
ws.send(JSON.stringify(data));
}
else {
broadcastToClients(data);
}
}
// CLI command execution handler
function handleCliCommand(command, ws) {
try {
// Add timestamp and format output
const timestamp = new Date().toLocaleTimeString();
const logEntry = `[${timestamp}] Executing: ${command}`;
outputHistory.push(logEntry);
// Broadcast to all connected clients
broadcastToClients({
type: "output",
data: `<span class="dim">[${timestamp}]</span> <span class="info">Executing:</span> ${command}\\n`,
});
// Execute the command
executeCliCommand(command, ws);
}
catch (error) {
const errorMsg = `Error executing command: ${error.message}`;
outputHistory.push(errorMsg);
sendResponse(ws, {
type: "error",
data: errorMsg,
});
}
}
// Execute CLI commands and capture output
function executeCliCommand(command, ws) {
// Handle built-in commands first
if (command === "help") {
const helpText = `<span class="success">Available Commands:</span>
⢠<span class="info">help</span> - Show this help message
⢠<span class="info">status</span> - Show system status
⢠<span class="info">agent list</span> - List all agents
⢠<span class="info">agent spawn [type]</span> - Spawn a new agent
⢠<span class="info">task list</span> - List all tasks
⢠<span class="info">memory list</span> - List memory entries
⢠<span class="info">config show</span> - Show configuration
⢠<span class="info">clear</span> - Clear console
⢠<span class="info">version</span> - Show version information
<span class="warning">Note:</span> This is a web console interface for claude-flow CLI commands.
`;
sendResponse(ws, {
type: "output",
data: helpText,
});
sendResponse(ws, { type: "command_complete" });
return;
}
if (command === "clear") {
sendResponse(ws, {
type: "output",
data: "\\x1b[2J\\x1b[H", // ANSI clear screen
});
sendResponse(ws, { type: "command_complete" });
return;
}
if (command === "status") {
const statusText = `<span class="success">System Status:</span>
⢠Event Bus: <span class="${componentStatus.eventBus ? "success" : "error"}">${componentStatus.eventBus ? "Active" : "Inactive"}</span>
⢠Orchestrator: <span class="${componentStatus.orchestrator ? "success" : "error"}">${componentStatus.orchestrator ? "Active" : "Inactive"}</span>
⢠Memory Manager: <span class="${componentStatus.memoryManager ? "success" : "error"}">${componentStatus.memoryManager ? "Active" : "Inactive"}</span>
⢠Terminal Pool: <span class="${componentStatus.terminalPool ? "success" : "error"}">${componentStatus.terminalPool ? "Active" : "Inactive"}</span>
⢠MCP Server: <span class="${componentStatus.mcpServer ? "success" : "error"}">${componentStatus.mcpServer ? "Active" : "Inactive"}</span>
⢠Coordination Manager: <span class="${componentStatus.coordinationManager ? "success" : "error"}">${componentStatus.coordinationManager ? "Active" : "Inactive"}</span>
⢠Web UI: <span class="${componentStatus.webUI ? "success" : "error"}">${componentStatus.webUI ? "Active" : "Inactive"}</span>
<span class="info">Metrics:</span>
⢠Active Agents: ${agents.size}
⢠Pending Tasks: ${tasks.size}
⢠Memory Entries: ${memory.size}
`;
sendResponse(ws, {
type: "output",
data: statusText,
});
sendResponse(ws, { type: "command_complete" });
return;
}
// For other commands, spawn a subprocess
const args = command.split(" ");
const _cmd = args[0];
const cmdArgs = args.slice(1);
// Determine the correct claude-flow executable path
const rootDir = path.resolve(__dirname, "../..");
const _cliPath = path.join(rootDir, "bin", "claude-flow");
// Spawn the command using the main TypeScript CLI
const child = spawn("node", [path.join(rootDir, "dist/cli/main.js"), ...cmdArgs], {
stdio: ["pipe", "pipe", "pipe"],
env: { ...process.env, CLAUDE_FLOW_WEB_MODE: "true" },
});
// Handle stdout
child.stdout.on("data", (data) => {
const output = data.toString();
outputHistory.push(output);
// Convert ANSI colors to HTML spans
const htmlOutput = convertAnsiToHtml(output);
broadcastToClients({
type: "output",
data: htmlOutput,
});
});
// Handle stderr
child.stderr.on("data", (data) => {
const error = data.toString();
outputHistory.push(error);
broadcastToClients({
type: "error",
data: convertAnsiToHtml(error),
});
});
// Handle process exit
child.on("close", (code) => {
const exitMsg = code === 0 ?
"<span class=\"success\">Command completed successfully</span>" :
`<span class="error">Command failed with exit code ${code}</span>`;
broadcastToClients({
type: "output",
data: `\\n${exitMsg}\\n`,
});
sendResponse(ws, { type: "command_complete" });
});
child.on("error", (error) => {
const errorMsg = `<span class="error">Failed to execute command: ${error.message}</span>`;
outputHistory.push(errorMsg);
sendResponse(ws, {
type: "error",
data: errorMsg,
});
sendResponse(ws, { type: "command_complete" });
});
}
// Broadcast message to all connected clients
function broadcastToClients(message) {
const messageStr = JSON.stringify(message);
activeConnections.forEach(client => {
if (client.readyState === 1) { // WebSocket.OPEN
client.send(messageStr);
}
});
}
// Convert ANSI escape codes to HTML
function convertAnsiToHtml(text) {
return text
.replace(/\x1b\[0m/g, "</span>")
.replace(/\x1b\[1m/g, "<span style=\"font-weight: bold;\">")
.replace(/\x1b\[31m/g, "<span class=\"error\">")
.replace(/\x1b\[32m/g, "<span class=\"success\">")
.replace(/\x1b\[33m/g, "<span class=\"warning\">")
.replace(/\x1b\[34m/g, "<span class=\"info\">")
.replace(/\x1b\[35m/g, "<span style=\"color: #d946ef;\">")
.replace(/\x1b\[36m/g, "<span style=\"color: #06b6d4;\">")
.replace(/\x1b\[37m/g, "<span class=\"dim\">")
.replace(/\x1b\[90m/g, "<span class=\"dim\">")
.replace(/\x1b\[[0-9;]*m/g, "") // Remove any remaining ANSI codes
.replace(/\n/g, "\\n")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/<span/g, "<span")
.replace(/span>/g, "span>");
}
return new Promise((resolve, reject) => {
server.on("error", (err) => {
if (err.code === "EADDRINUSE") {
console.error(`\nā Port ${port} is already in use`);
console.log(`š” Try a different port: claude-flow start --ui --port ${port + 1}`);
console.log(`š” Or stop the process using port ${port}: lsof -ti:${port} | xargs kill -9`);
componentStatus.webUI = false;
reject(err);
}
else {
console.error("ā Web UI server error:", err.message);
reject(err);
}
});
server.listen(port, host, () => {
console.log(`š Web UI available at http://${host}:${port}`);
componentStatus.webUI = true;
resolve(server);
});
});
}
// Start all components
export async function startOrchestrator(options) {
console.log("\nš Starting orchestration components...\n");
// Start Event Bus
console.log("ā” Starting Event Bus...");
componentStatus.eventBus = true;
eventBus.emit("system:start");
console.log("ā
Event Bus started");
// Start Orchestrator Engine
console.log("š§ Starting Orchestrator Engine...");
componentStatus.orchestrator = true;
console.log("ā
Orchestrator Engine started");
// Start Memory Manager
console.log("š¾ Starting Memory Manager...");
componentStatus.memoryManager = true;
console.log("ā
Memory Manager started");
// Start Terminal Pool
console.log("š„ļø Starting Terminal Pool...");
componentStatus.terminalPool = true;
console.log("ā
Terminal Pool started");
// Start MCP Server
const mcpPort = options.mcpPort || 3001;
startMCPServer(mcpPort);
console.log("ā
MCP Server started");
// Start Coordination Manager
console.log("š Starting Coordination Manager...");
componentStatus.coordinationManager = true;
console.log("ā
Coordination Manager started");
// Start Web UI if requested
if (options.ui && !options.noUi) {
const host = options.host || "localhost";
const port = options.port || 3000;
try {
await startWebUI(host, port);
}
catch (err) {
if (err.code === "EADDRINUSE") {
console.log("\nā ļø Web UI could not start due to port conflict");
console.log(" Orchestrator is running without Web UI");
}
else {
console.error("\nā ļø Web UI failed to start:", err.message);
}
}
}
console.log("\nā
All components started successfully!");
console.log("\nš System Status:");
console.log(" ⢠Event Bus: Active");
console.log(" ⢠Orchestrator: Active");
console.log(" ⢠Memory Manager: Active");
console.log(" ⢠Terminal Pool: Active");
console.log(" ⢠MCP Server: Active");
console.log(" ⢠Coordination Manager: Active");
if (options.ui && !options.noUi) {
console.log(` ⢠Web UI: Active at http://${options.host || "localhost"}:${options.port || 3000}`);
}
console.log("\nš” Use \"claude-flow status\" to check system status");
console.log("š” Use \"claude-flow stop\" to stop the orchestrator");
// Keep the process running
if (!options.daemon) {
console.log("\nš Press Ctrl+C to stop the orchestrator...\n");
// Handle graceful shutdown
process.on("SIGINT", () => {
console.log("\n\nš Shutting down orchestrator...");
process.exit(0);
});
}
}
// Export component status for other commands
export function getComponentStatus() {
return componentStatus;
}
// Export stores for other commands
export function getStores() {
return { agents, tasks, memory };
}
//# sourceMappingURL=simple-orchestrator.js.map