From 5c5ede94b6b1b8ec3c8a3b7d346f241f2bfa6810 Mon Sep 17 00:00:00 2001 From: rUv Date: Mon, 6 Apr 2026 11:47:30 -0400 Subject: [PATCH] =?UTF-8?q?fix:=205=20critical=20bugs=20=E2=80=94=20cwd,?= =?UTF-8?q?=20intelligence=20hang,=20memory=20init,=20ruvector=20v3.5.52?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #1532: Add getProjectCwd() to all 16 MCP tool files, respecting CLAUDE_FLOW_CWD env var instead of bare process.cwd(). Fixes macOS global install where cwd='/'. Install script now passes -e CLAUDE_FLOW_CWD="$HOME" to claude mcp add. - #1530/#1531: Add 10MB file size guard and 5000-node PageRank cap to intelligence.cjs. Add 3s timeout wrapper on intelligence.init()/consolidate() in hook-handler.cjs. Prevents indefinite hang on large graph-state.json. - #1524: Auto-initialize memory database on MCP server startup in mcp-server.ts with graceful degradation. Fixes "Database not initialized" for memory_store. - #1522/#1520/#1523: Detect ruvector extension before falling back to pgvector in all ruvector CLI commands. Dynamic vector type names in DDL/indexes. Fix -d flag mapping to --database in benchmark/optimize. Closes #1532, #1531, #1530, #1524, #1522, #1520, #1523 Co-authored-by: Reuven --- .claude/helpers/hook-handler.cjs | 61 +++++++++---- .claude/helpers/intelligence.cjs | 6 ++ scripts/install.sh | 11 +-- .../cli/.claude/helpers/hook-handler.cjs | 60 +++++++++---- .../cli/.claude/helpers/intelligence.cjs | 34 ++++++- .../cli/src/commands/ruvector/benchmark.ts | 18 +++- .../cli/src/commands/ruvector/index.ts | 4 +- .../cli/src/commands/ruvector/init.ts | 88 +++++++++++++------ .../cli/src/commands/ruvector/migrate.ts | 29 ++++-- .../cli/src/commands/ruvector/optimize.ts | 1 + .../cli/src/commands/ruvector/status.ts | 52 +++++++---- v3/@claude-flow/cli/src/mcp-server.ts | 35 +++++++- .../cli/src/mcp-tools/agent-tools.ts | 4 +- .../cli/src/mcp-tools/config-tools.ts | 4 +- .../cli/src/mcp-tools/coordination-tools.ts | 4 +- .../cli/src/mcp-tools/daa-tools.ts | 4 +- .../cli/src/mcp-tools/github-tools.ts | 4 +- .../cli/src/mcp-tools/guidance-tools.ts | 10 +-- .../cli/src/mcp-tools/hive-mind-tools.ts | 10 +-- .../cli/src/mcp-tools/hooks-tools.ts | 4 +- .../cli/src/mcp-tools/neural-tools.ts | 4 +- .../cli/src/mcp-tools/performance-tools.ts | 4 +- .../cli/src/mcp-tools/session-tools.ts | 16 ++-- .../cli/src/mcp-tools/swarm-tools.ts | 4 +- .../cli/src/mcp-tools/system-tools.ts | 8 +- .../cli/src/mcp-tools/task-tools.ts | 10 +-- .../cli/src/mcp-tools/terminal-tools.ts | 10 +-- v3/@claude-flow/cli/src/mcp-tools/types.ts | 9 ++ .../cli/src/mcp-tools/workflow-tools.ts | 4 +- 29 files changed, 362 insertions(+), 150 deletions(-) diff --git a/.claude/helpers/hook-handler.cjs b/.claude/helpers/hook-handler.cjs index 7effd9533..2968e5ba2 100644 --- a/.claude/helpers/hook-handler.cjs +++ b/.claude/helpers/hook-handler.cjs @@ -35,6 +35,26 @@ const session = safeRequire(path.join(helpersDir, 'session.cjs')); const memory = safeRequire(path.join(helpersDir, 'memory.cjs')); const intelligence = safeRequire(path.join(helpersDir, 'intelligence.cjs')); +// ── Intelligence timeout protection (fixes #1530, #1531) ─────────────────── +var INTELLIGENCE_TIMEOUT_MS = 3000; +function runWithTimeout(fn, label) { + return new Promise(function(resolve) { + var timer = setTimeout(function() { + process.stderr.write("[WARN] " + label + " timed out after " + INTELLIGENCE_TIMEOUT_MS + "ms, skipping\n"); + resolve(null); + }, INTELLIGENCE_TIMEOUT_MS); + try { + var result = fn(); + clearTimeout(timer); + resolve(result); + } catch (e) { + clearTimeout(timer); + resolve(null); + } + }); +} + + const [,, command, ...args] = process.argv; // Read stdin — Claude Code sends hook data as JSON via stdin @@ -58,6 +78,13 @@ async function readStdin() { } async function main() { + // Global safety timeout: hooks must NEVER hang (#1530, #1531) + var safetyTimer = setTimeout(function() { + process.stderr.write("[WARN] Hook handler global timeout (5s), forcing exit\n"); + process.exit(0); + }, 5000); + safetyTimer.unref(); + let stdinData = ''; try { stdinData = await readStdin(); } catch (e) { /* ignore stdin errors */ } @@ -122,7 +149,7 @@ const handlers = { console.log('[OK] Edit recorded'); }, - 'session-restore': () => { + 'session-restore': async () => { if (session) { var existing = session.restore && session.restore(); if (!existing) { @@ -131,27 +158,25 @@ const handlers = { } else { console.log('[OK] Session restored: session-' + Date.now()); } + // Initialize intelligence (with timeout — #1530) if (intelligence && intelligence.init) { - try { - var result = intelligence.init(); - if (result && result.nodes > 0) { - console.log('[INTELLIGENCE] Loaded ' + result.nodes + ' patterns, ' + result.edges + ' edges'); - } - } catch (e) { /* non-fatal */ } + var initResult = await runWithTimeout(function() { return intelligence.init(); }, 'intelligence.init()'); + if (initResult && initResult.nodes > 0) { + console.log('[INTELLIGENCE] Loaded ' + initResult.nodes + ' patterns, ' + initResult.edges + ' edges'); + } } }, - 'session-end': () => { + 'session-end': async () => { + // Consolidate intelligence (with timeout — #1530) if (intelligence && intelligence.consolidate) { - try { - var result = intelligence.consolidate(); - if (result && result.entries > 0) { - var msg = '[INTELLIGENCE] Consolidated: ' + result.entries + ' entries, ' + result.edges + ' edges'; - if (result.newEntries > 0) msg += ', ' + result.newEntries + ' new'; - msg += ', PageRank recomputed'; - console.log(msg); - } - } catch (e) { /* non-fatal */ } + var consResult = await runWithTimeout(function() { return intelligence.consolidate(); }, 'intelligence.consolidate()'); + if (consResult && consResult.entries > 0) { + var msg = '[INTELLIGENCE] Consolidated: ' + consResult.entries + ' entries, ' + consResult.edges + ' edges'; + if (consResult.newEntries > 0) msg += ', ' + consResult.newEntries + ' new'; + msg += ', PageRank recomputed'; + console.log(msg); + } } if (session && session.end) { session.end(); @@ -215,7 +240,7 @@ const handlers = { if (command && handlers[command]) { try { - handlers[command](); + await Promise.resolve(handlers[command]()); } catch (e) { console.log('[WARN] Hook ' + command + ' encountered an error: ' + e.message); } diff --git a/.claude/helpers/intelligence.cjs b/.claude/helpers/intelligence.cjs index beffad94d..1f9fb4af6 100644 --- a/.claude/helpers/intelligence.cjs +++ b/.claude/helpers/intelligence.cjs @@ -17,11 +17,17 @@ const PENDING_PATH = path.join(DATA_DIR, 'pending-insights.jsonl'); const SESSION_DIR = path.join(process.cwd(), '.claude-flow', 'sessions'); const SESSION_FILE = path.join(SESSION_DIR, 'current.json'); +// ── Safety limits (fixes #1530, #1531) ───────────────────────────────────── +var MAX_DATA_FILE_SIZE = 10 * 1024 * 1024; // 10 MB — skip files larger than this +var MAX_GRAPH_NODES = 5000; // skip PageRank if graph exceeds this + function ensureDir(dir) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } function readJSON(p) { + // Safety: skip files exceeding MAX_DATA_FILE_SIZE (#1531) + try { var stat = fs.statSync(p); if (stat.size > MAX_DATA_FILE_SIZE) { process.stderr.write("[INTELLIGENCE] WARN: Skipping " + path.basename(p) + " (" + Math.round(stat.size / 1048576) + "MB exceeds 10MB limit)\n"); return null; } } catch(e) { /* file may not exist */ } try { return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf-8")) : null; } catch { return null; } } diff --git a/scripts/install.sh b/scripts/install.sh index 6a65db78a..301c5d2ba 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -327,15 +327,16 @@ setup_mcp_server() { return 0 fi - # Add MCP server + # Add MCP server (pass CLAUDE_FLOW_CWD so tools resolve paths correctly + # even when the MCP server is spawned with cwd='/') if [ "$GLOBAL" = "1" ]; then - claude mcp add ruflo -- ruflo mcp start 2>/dev/null && \ + claude mcp add ruflo -e CLAUDE_FLOW_CWD="$HOME" -- ruflo mcp start 2>/dev/null && \ print_substep "MCP server configured ✓" || \ - print_warning "MCP setup failed - run manually: claude mcp add ruflo -- ruflo mcp start" + print_warning "MCP setup failed - run manually: claude mcp add ruflo -e CLAUDE_FLOW_CWD=\"\$HOME\" -- ruflo mcp start" else - claude mcp add ruflo -- npx -y ruflo@${VERSION} mcp start 2>/dev/null && \ + claude mcp add ruflo -e CLAUDE_FLOW_CWD="$HOME" -- npx -y ruflo@${VERSION} mcp start 2>/dev/null && \ print_substep "MCP server configured ✓" || \ - print_warning "MCP setup failed - run manually: claude mcp add ruflo -- npx -y ruflo@latest mcp start" + print_warning "MCP setup failed - run manually: claude mcp add ruflo -e CLAUDE_FLOW_CWD=\"\$HOME\" -- npx -y ruflo@latest mcp start" fi echo "" } diff --git a/v3/@claude-flow/cli/.claude/helpers/hook-handler.cjs b/v3/@claude-flow/cli/.claude/helpers/hook-handler.cjs index ed13a38d8..c0f185d2f 100644 --- a/v3/@claude-flow/cli/.claude/helpers/hook-handler.cjs +++ b/v3/@claude-flow/cli/.claude/helpers/hook-handler.cjs @@ -47,6 +47,29 @@ const session = safeRequire(path.join(helpersDir, 'session.js')); const memory = safeRequire(path.join(helpersDir, 'memory.js')); const intelligence = safeRequire(path.join(helpersDir, 'intelligence.cjs')); +// ── Intelligence timeout protection (fixes #1530, #1531) ─────────────────── +const INTELLIGENCE_TIMEOUT_MS = 3000; +function runWithTimeout(fn, label) { + // For synchronous blocking calls, we use a global safety timer. + // The readJSON file-size guard prevents loading huge files, but this + // is an additional safety net. + return new Promise((resolve) => { + const timer = setTimeout(() => { + process.stderr.write("[WARN] " + label + " timed out after " + INTELLIGENCE_TIMEOUT_MS + "ms, skipping\n"); + resolve(null); + }, INTELLIGENCE_TIMEOUT_MS); + try { + const result = fn(); + clearTimeout(timer); + resolve(result); + } catch (e) { + clearTimeout(timer); + resolve(null); + } + }); +} + + // Get the command from argv const [,, command, ...args] = process.argv; @@ -70,6 +93,13 @@ async function readStdin() { } async function main() { + // Global safety timeout: hooks must NEVER hang (#1530, #1531) + const safetyTimer = setTimeout(() => { + process.stderr.write("[WARN] Hook handler global timeout (5s), forcing exit\n"); + process.exit(0); + }, 5000); + safetyTimer.unref(); // don't keep process alive just for this timer + let stdinData = ''; try { stdinData = await readStdin(); } catch (e) { /* ignore stdin errors */ } @@ -162,7 +192,7 @@ const handlers = { console.log('[OK] Edit recorded'); }, - 'session-restore': () => { + 'session-restore': async () => { if (session) { // Try restore first, fall back to start const existing = session.restore && session.restore(); @@ -186,26 +216,22 @@ const handlers = { console.log('| Memory Entries | 0 |'); console.log('+----------------+-------+'); } - // Initialize intelligence graph after session restore + // Initialize intelligence graph after session restore (with timeout — #1530) if (intelligence && intelligence.init) { - try { - const result = intelligence.init(); - if (result && result.nodes > 0) { - console.log(`[INTELLIGENCE] Loaded ${result.nodes} patterns, ${result.edges} edges`); - } - } catch (e) { /* non-fatal */ } + const initResult = await runWithTimeout(() => intelligence.init(), 'intelligence.init()'); + if (initResult && initResult.nodes > 0) { + console.log(`[INTELLIGENCE] Loaded ${initResult.nodes} patterns, ${initResult.edges} edges`); + } } }, - 'session-end': () => { - // Consolidate intelligence before ending session + 'session-end': async () => { + // Consolidate intelligence before ending session (with timeout — #1530) if (intelligence && intelligence.consolidate) { - try { - const result = intelligence.consolidate(); - if (result && result.entries > 0) { - console.log(`[INTELLIGENCE] Consolidated: ${result.entries} entries, ${result.edges} edges${result.newEntries > 0 ? `, ${result.newEntries} new` : ''}, PageRank recomputed`); - } - } catch (e) { /* non-fatal */ } + const consResult = await runWithTimeout(() => intelligence.consolidate(), 'intelligence.consolidate()'); + if (consResult && consResult.entries > 0) { + console.log(`[INTELLIGENCE] Consolidated: ${consResult.entries} entries, ${consResult.edges} edges${consResult.newEntries > 0 ? `, ${consResult.newEntries} new` : ''}, PageRank recomputed`); + } } if (session && session.end) { session.end(); @@ -249,7 +275,7 @@ const handlers = { // Execute the handler if (command && handlers[command]) { try { - handlers[command](); + await Promise.resolve(handlers[command]()); } catch (e) { // Hooks should never crash Claude Code - fail silently console.log(`[WARN] Hook ${command} encountered an error: ${e.message}`); diff --git a/v3/@claude-flow/cli/.claude/helpers/intelligence.cjs b/v3/@claude-flow/cli/.claude/helpers/intelligence.cjs index e4cc63138..b9caa2be6 100644 --- a/v3/@claude-flow/cli/.claude/helpers/intelligence.cjs +++ b/v3/@claude-flow/cli/.claude/helpers/intelligence.cjs @@ -25,6 +25,10 @@ const PENDING_PATH = path.join(DATA_DIR, 'pending-insights.jsonl'); const SESSION_DIR = path.join(process.cwd(), '.claude-flow', 'sessions'); const SESSION_FILE = path.join(SESSION_DIR, 'current.json'); +// ── Safety limits (fixes #1530, #1531) ───────────────────────────────────── +const MAX_DATA_FILE_SIZE = 10 * 1024 * 1024; // 10 MB — skip files larger than this +const MAX_GRAPH_NODES = 5000; // skip PageRank if graph exceeds this + // ── Stop words for trigram matching ────────────────────────────────────────── const STOP_WORDS = new Set([ @@ -46,6 +50,14 @@ function ensureDataDir() { } function readJSON(filePath) { + // Safety: skip files exceeding MAX_DATA_FILE_SIZE (#1531) + try { + const stat = fs.statSync(filePath); + if (stat.size > MAX_DATA_FILE_SIZE) { + process.stderr.write("[INTELLIGENCE] WARN: Skipping " + path.basename(filePath) + " (" + Math.round(stat.size / 1048576) + "MB exceeds 10MB limit)\n"); + return null; + } + } catch { /* file may not exist yet */ } try { if (fs.existsSync(filePath)) return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { /* corrupt file — start fresh */ } @@ -342,8 +354,15 @@ function init() { // Build edges const edges = buildEdges(store); - // Compute PageRank - const pageRanks = computePageRank(nodes, edges, 0.85, 30); + // Compute PageRank (skip if graph too large — #1531) + const nodeCount = Object.keys(nodes).length; + let pageRanks = {}; + if (nodeCount > MAX_GRAPH_NODES) { + process.stderr.write("[INTELLIGENCE] WARN: Graph has " + nodeCount + " nodes (>" + MAX_GRAPH_NODES + "), skipping PageRank\n"); + for (const id of Object.keys(nodes)) pageRanks[id] = 1 / nodeCount; + } else { + pageRanks = computePageRank(nodes, edges, 0.85, 30); + } // Write graph state const graph = { @@ -595,8 +614,15 @@ function consolidate() { }; } - // 5. Recompute PageRank - const pageRanks = computePageRank(nodes, edges, 0.85, 30); + // 5. Recompute PageRank (skip if graph too large — #1531) + const nodeCount = Object.keys(nodes).length; + let pageRanks = {}; + if (nodeCount > MAX_GRAPH_NODES) { + process.stderr.write("[INTELLIGENCE] WARN: Graph has " + nodeCount + " nodes (>" + MAX_GRAPH_NODES + "), skipping PageRank in consolidate\n"); + for (const id of Object.keys(nodes)) pageRanks[id] = 1 / nodeCount; + } else { + pageRanks = computePageRank(nodes, edges, 0.85, 30); + } // 6. Write updated graph writeJSON(GRAPH_PATH, { diff --git a/v3/@claude-flow/cli/src/commands/ruvector/benchmark.ts b/v3/@claude-flow/cli/src/commands/ruvector/benchmark.ts index 051153ef7..3b6b9f203 100644 --- a/v3/@claude-flow/cli/src/commands/ruvector/benchmark.ts +++ b/v3/@claude-flow/cli/src/commands/ruvector/benchmark.ts @@ -59,7 +59,6 @@ export const benchmarkCommand: Command = { }, { name: 'dimensions', - short: 'd', description: 'Vector dimensions', type: 'number', default: 1536, @@ -121,6 +120,7 @@ export const benchmarkCommand: Command = { }, { name: 'database', + short: 'd', description: 'Database name', type: 'string', }, @@ -240,6 +240,15 @@ export const benchmarkCommand: Command = { await client.connect(); spinner.succeed('Connected to PostgreSQL'); + // Detect vector extension type: prefer ruvector, fall back to pgvector + let vectorTypeName = 'vector'; + const ruvectorCheck = await client.query(` + SELECT extname FROM pg_extension WHERE extname = 'ruvector' + `); + if (ruvectorCheck.rows.length > 0) { + vectorTypeName = 'ruvector'; + } + // Create benchmark table const benchmarkTable = `${config.schema}.benchmark_${Date.now()}`; spinner.setText('Creating benchmark table...'); spinner.start(); @@ -247,7 +256,7 @@ export const benchmarkCommand: Command = { await client.query(` CREATE TABLE ${benchmarkTable} ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - embedding vector(${dimensions}), + embedding ${vectorTypeName}(${dimensions}), created_at TIMESTAMPTZ DEFAULT NOW() ) `); @@ -293,8 +302,9 @@ export const benchmarkCommand: Command = { spinner.setText(`Creating ${indexType.toUpperCase()} index...`); spinner.start(); const indexStart = Date.now(); - const metricOp = metric === 'cosine' ? 'vector_cosine_ops' : - metric === 'l2' ? 'vector_l2_ops' : 'vector_ip_ops'; + const opsPrefix = vectorTypeName === 'ruvector' ? 'ruvector' : 'vector'; + const metricOp = metric === 'cosine' ? `${opsPrefix}_cosine_ops` : + metric === 'l2' ? `${opsPrefix}_l2_ops` : `${opsPrefix}_ip_ops`; if (indexType === 'hnsw') { await client.query(` diff --git a/v3/@claude-flow/cli/src/commands/ruvector/index.ts b/v3/@claude-flow/cli/src/commands/ruvector/index.ts index 404e59b4a..e14de79ae 100644 --- a/v3/@claude-flow/cli/src/commands/ruvector/index.ts +++ b/v3/@claude-flow/cli/src/commands/ruvector/index.ts @@ -3,7 +3,7 @@ * Management commands for RuVector PostgreSQL integration * * Features: - * - pgvector integration for vector operations + * - ruvector/pgvector integration for vector operations * - Attention mechanism embeddings * - Graph Neural Network support * - Hyperbolic embeddings (Poincare ball) @@ -98,7 +98,7 @@ export const ruvectorCommand: Command = { output.printBox([ 'RuVector provides PostgreSQL integration for Claude Flow with:', '', - ' - pgvector extension for vector operations', + ' - ruvector/pgvector extension for vector operations', ' - Attention mechanism embeddings', ' - Graph Neural Network (GNN) support', ' - Hyperbolic embeddings (Poincare ball model)', diff --git a/v3/@claude-flow/cli/src/commands/ruvector/init.ts b/v3/@claude-flow/cli/src/commands/ruvector/init.ts index 784a29044..8eca1c593 100644 --- a/v3/@claude-flow/cli/src/commands/ruvector/init.ts +++ b/v3/@claude-flow/cli/src/commands/ruvector/init.ts @@ -228,27 +228,63 @@ export const initCommand: Command = { await client.connect(); spinner.succeed('Connected to PostgreSQL'); - // Check pgvector extension - spinner.setText('Checking pgvector extension...'); spinner.start(); - const extensionResult = await client.query(` - SELECT extversion FROM pg_extension WHERE extname = 'vector' + // Detect vector extension: prefer ruvector, fall back to pgvector + spinner.setText('Detecting vector extension...'); spinner.start(); + let vectorExtName = 'vector'; // default pgvector type name + let vectorTypeName = 'vector'; // SQL type used in column definitions + + // Check for ruvector extension first (ships with ruvector-postgres image) + const ruvectorResult = await client.query(` + SELECT extname, extversion FROM pg_extension WHERE extname = 'ruvector' `); - if (extensionResult.rows.length === 0) { - spinner.succeed('pgvector not installed, attempting to create...'); - try { - await client.query('CREATE EXTENSION IF NOT EXISTS vector'); - spinner.succeed('pgvector extension created'); - } catch (error) { - spinner.fail('Failed to create pgvector extension'); - output.printError('Please install pgvector manually: https://github.com/pgvector/pgvector'); - await client.end(); - return { success: false, exitCode: 1 }; - } + if (ruvectorResult.rows.length > 0) { + vectorExtName = 'ruvector'; + vectorTypeName = 'ruvector'; + spinner.succeed(`ruvector v${ruvectorResult.rows[0].extversion} found`); } else { - spinner.succeed(`pgvector v${extensionResult.rows[0].extversion} found`); + // Fall back to pgvector + const pgvectorResult = await client.query(` + SELECT extname, extversion FROM pg_extension WHERE extname = 'vector' + `); + + if (pgvectorResult.rows.length > 0) { + vectorExtName = 'vector'; + vectorTypeName = 'vector'; + spinner.succeed(`pgvector v${pgvectorResult.rows[0].extversion} found`); + } else { + // Neither installed -- try to create ruvector first, then pgvector + spinner.succeed('No vector extension found, attempting to create...'); + let created = false; + try { + await client.query("CREATE EXTENSION IF NOT EXISTS ruvector"); + vectorExtName = 'ruvector'; + vectorTypeName = 'ruvector'; + spinner.succeed('ruvector extension created'); + created = true; + } catch { + // ruvector not available, try pgvector + } + if (!created) { + try { + await client.query("CREATE EXTENSION IF NOT EXISTS vector"); + vectorExtName = 'vector'; + vectorTypeName = 'vector'; + spinner.succeed('pgvector extension created'); + } catch { + spinner.fail('Failed to create vector extension'); + output.printError('Please install ruvector or pgvector manually.'); + output.printError(' ruvector: https://hub.docker.com/r/ruvnet/ruvector-postgres'); + output.printError(' pgvector: https://github.com/pgvector/pgvector'); + await client.end(); + return { success: false, exitCode: 1 }; + } + } + } } + const cosineOps = vectorExtName === 'ruvector' ? 'ruvector_cosine_ops' : 'vector_cosine_ops'; + // Drop schema if force mode if (force) { spinner.setText(`Dropping schema "${config.schema}"...`); spinner.start(); @@ -271,7 +307,7 @@ export const initCommand: Command = { key VARCHAR(512) NOT NULL, namespace VARCHAR(128) NOT NULL DEFAULT 'default', content TEXT, - embedding vector(${dimensions}), + embedding ${vectorTypeName}(${dimensions}), metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), @@ -284,9 +320,9 @@ export const initCommand: Command = { CREATE TABLE IF NOT EXISTS ${config.schema}.attention_patterns ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), pattern_name VARCHAR(256) NOT NULL, - query_embedding vector(${dimensions}), - key_embedding vector(${dimensions}), - value_embedding vector(${dimensions}), + query_embedding ${vectorTypeName}(${dimensions}), + key_embedding ${vectorTypeName}(${dimensions}), + value_embedding ${vectorTypeName}(${dimensions}), attention_weights JSONB, context TEXT, created_at TIMESTAMPTZ DEFAULT NOW() @@ -312,7 +348,7 @@ export const initCommand: Command = { CREATE TABLE IF NOT EXISTS ${config.schema}.hyperbolic_embeddings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), entity_id UUID NOT NULL, - embedding vector(${dimensions}), + embedding ${vectorTypeName}(${dimensions}), curvature FLOAT DEFAULT -1.0, hierarchy_level INTEGER DEFAULT 0, parent_id UUID, @@ -368,21 +404,21 @@ export const initCommand: Command = { await client.query(` CREATE INDEX IF NOT EXISTS idx_embeddings_vector_hnsw ON ${config.schema}.embeddings - USING hnsw (embedding vector_cosine_ops) + USING hnsw (embedding ${cosineOps}) WITH (m = 16, ef_construction = 64) `); await client.query(` CREATE INDEX IF NOT EXISTS idx_attention_query_hnsw ON ${config.schema}.attention_patterns - USING hnsw (query_embedding vector_cosine_ops) + USING hnsw (query_embedding ${cosineOps}) WITH (m = 16, ef_construction = 64) `); await client.query(` CREATE INDEX IF NOT EXISTS idx_hyperbolic_embedding_hnsw ON ${config.schema}.hyperbolic_embeddings - USING hnsw (embedding vector_cosine_ops) + USING hnsw (embedding ${cosineOps}) WITH (m = 16, ef_construction = 64) `); } else { @@ -390,14 +426,14 @@ export const initCommand: Command = { await client.query(` CREATE INDEX IF NOT EXISTS idx_embeddings_vector_ivfflat ON ${config.schema}.embeddings - USING ivfflat (embedding vector_cosine_ops) + USING ivfflat (embedding ${cosineOps}) WITH (lists = 100) `); await client.query(` CREATE INDEX IF NOT EXISTS idx_attention_query_ivfflat ON ${config.schema}.attention_patterns - USING ivfflat (query_embedding vector_cosine_ops) + USING ivfflat (query_embedding ${cosineOps}) WITH (lists = 100) `); } diff --git a/v3/@claude-flow/cli/src/commands/ruvector/migrate.ts b/v3/@claude-flow/cli/src/commands/ruvector/migrate.ts index 8279d72f3..9c3957dac 100644 --- a/v3/@claude-flow/cli/src/commands/ruvector/migrate.ts +++ b/v3/@claude-flow/cli/src/commands/ruvector/migrate.ts @@ -72,7 +72,7 @@ const MIGRATIONS: Migration[] = [ CREATE TABLE IF NOT EXISTS {{schema}}.query_cache ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), query_hash VARCHAR(64) NOT NULL UNIQUE, - query_embedding vector(1536), + query_embedding {{vector_type}}(1536), result_ids UUID[], result_scores FLOAT[], hit_count INTEGER DEFAULT 1, @@ -121,8 +121,8 @@ const MIGRATIONS: Migration[] = [ CREATE TABLE IF NOT EXISTS {{schema}}.neural_patterns ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), pattern_type VARCHAR(64) NOT NULL, - input_embedding vector(1536), - output_embedding vector(1536), + input_embedding {{vector_type}}(1536), + output_embedding {{vector_type}}(1536), weight_matrix JSONB, activation VARCHAR(32) DEFAULT 'relu', accuracy FLOAT, @@ -136,7 +136,7 @@ const MIGRATIONS: Migration[] = [ CREATE INDEX IF NOT EXISTS idx_neural_patterns_input_hnsw ON {{schema}}.neural_patterns - USING hnsw (input_embedding vector_cosine_ops) + USING hnsw (input_embedding {{cosine_ops}}) WITH (m = 16, ef_construction = 64); `, down: ` @@ -308,6 +308,17 @@ export const migrateCommand: Command = { await client.connect(); spinner.succeed('Connected to PostgreSQL'); + // Detect vector extension type: prefer ruvector, fall back to pgvector + let vectorTypeName = 'vector'; + let cosineOps = 'vector_cosine_ops'; + const ruvectorCheck = await client.query(` + SELECT extname FROM pg_extension WHERE extname = 'ruvector' + `); + if (ruvectorCheck.rows.length > 0) { + vectorTypeName = 'ruvector'; + cosineOps = 'ruvector_cosine_ops'; + } + // Check if schema and migrations table exist spinner.setText('Checking migration status...'); spinner.start(); @@ -423,7 +434,10 @@ export const migrateCommand: Command = { if (dryRun) { for (const migration of migrationsToRun) { const sql = direction === 'up' ? migration.up : migration.down; - const resolvedSql = sql.replace(/\{\{schema\}\}/g, config.schema); + const resolvedSql = sql + .replace(/\{\{schema\}\}/g, config.schema) + .replace(/\{\{vector_type\}\}/g, vectorTypeName) + .replace(/\{\{cosine_ops\}\}/g, cosineOps); output.writeln(output.bold(`-- Migration ${migration.version}: ${migration.name}`)); output.writeln(output.dim('-- Direction: ' + direction.toUpperCase())); @@ -456,7 +470,10 @@ export const migrateCommand: Command = { try { const sql = direction === 'up' ? migration.up : migration.down; - const resolvedSql = sql.replace(/\{\{schema\}\}/g, config.schema); + const resolvedSql = sql + .replace(/\{\{schema\}\}/g, config.schema) + .replace(/\{\{vector_type\}\}/g, vectorTypeName) + .replace(/\{\{cosine_ops\}\}/g, cosineOps); await client.query('BEGIN'); diff --git a/v3/@claude-flow/cli/src/commands/ruvector/optimize.ts b/v3/@claude-flow/cli/src/commands/ruvector/optimize.ts index b18abca94..1a477ea78 100644 --- a/v3/@claude-flow/cli/src/commands/ruvector/optimize.ts +++ b/v3/@claude-flow/cli/src/commands/ruvector/optimize.ts @@ -88,6 +88,7 @@ export const optimizeCommand: Command = { }, { name: 'database', + short: 'd', description: 'Database name', type: 'string', }, diff --git a/v3/@claude-flow/cli/src/commands/ruvector/status.ts b/v3/@claude-flow/cli/src/commands/ruvector/status.ts index 5779c328a..ad894925c 100644 --- a/v3/@claude-flow/cli/src/commands/ruvector/status.ts +++ b/v3/@claude-flow/cli/src/commands/ruvector/status.ts @@ -180,25 +180,43 @@ export const statusCommand: Command = { const pgVersion = versionResult.rows[0].version; (statusData.connection as Record).pgVersion = pgVersion; - // Check pgvector extension - if (!jsonOutput) spinner.setText('Checking pgvector extension...'); spinner.start(); + // Check vector extension: prefer ruvector, fall back to pgvector + if (!jsonOutput) spinner.setText('Checking vector extension...'); spinner.start(); - const extensionResult = await client.query(` - SELECT extversion FROM pg_extension WHERE extname = 'vector' + // Check for ruvector first + const ruvectorResult = await client.query(` + SELECT extname, extversion FROM pg_extension WHERE extname = 'ruvector' `); - if (extensionResult.rows.length === 0) { - statusData.pgvector = { installed: false }; - if (!jsonOutput) { - spinner.succeed(output.warning('pgvector extension not installed')); - } - } else { + if (ruvectorResult.rows.length > 0) { statusData.pgvector = { installed: true, - version: extensionResult.rows[0].extversion, + extensionName: 'ruvector', + version: ruvectorResult.rows[0].extversion, }; if (!jsonOutput) { - spinner.succeed(`pgvector v${extensionResult.rows[0].extversion} installed`); + spinner.succeed(`ruvector v${ruvectorResult.rows[0].extversion} installed`); + } + } else { + // Fall back to pgvector + const pgvectorResult = await client.query(` + SELECT extname, extversion FROM pg_extension WHERE extname = 'vector' + `); + + if (pgvectorResult.rows.length > 0) { + statusData.pgvector = { + installed: true, + extensionName: 'vector', + version: pgvectorResult.rows[0].extversion, + }; + if (!jsonOutput) { + spinner.succeed(`pgvector v${pgvectorResult.rows[0].extversion} installed`); + } + } else { + statusData.pgvector = { installed: false }; + if (!jsonOutput) { + spinner.succeed(output.warning('No vector extension installed (ruvector or pgvector)')); + } } } @@ -372,10 +390,14 @@ export const statusCommand: Command = { }); output.writeln(); - // pgvector info - output.writeln(output.highlight('pgvector Extension:')); - const pgvectorData = statusData.pgvector as { installed: boolean; version?: string }; + // Vector extension info + const pgvectorData = statusData.pgvector as { installed: boolean; extensionName?: string; version?: string }; + const extDisplayName = pgvectorData.extensionName === 'ruvector' ? 'RuVector' : 'pgvector'; + output.writeln(output.highlight(`Vector Extension (${extDisplayName}):`)); output.writeln(` Status: ${pgvectorData.installed ? output.success('Installed') : output.error('Not Installed')}`); + if (pgvectorData.extensionName) { + output.writeln(` Extension: ${pgvectorData.extensionName}`); + } if (pgvectorData.version) { output.writeln(` Version: ${pgvectorData.version}`); } diff --git a/v3/@claude-flow/cli/src/mcp-server.ts b/v3/@claude-flow/cli/src/mcp-server.ts index cc7d0adf2..4102ba062 100644 --- a/v3/@claude-flow/cli/src/mcp-server.ts +++ b/v3/@claude-flow/cli/src/mcp-server.ts @@ -318,6 +318,39 @@ export class MCPServerManager extends EventEmitter { console.error( `[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Starting in stdio mode` ); + + // Auto-initialize memory database before tools are registered (#1524) + // This ensures memory_store and other memory tools work immediately + // without waiting for the first tool call to trigger lazy init. + try { + const { initializeMemoryDatabase, checkMemoryInitialization } = await import('./memory/memory-initializer.js'); + const status = await checkMemoryInitialization(); + if (!status.initialized) { + console.error( + `[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Auto-initializing memory database...` + ); + const result = await initializeMemoryDatabase({ force: false, verbose: false }); + if (result.success) { + console.error( + `[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Memory database initialized at ${result.dbPath}` + ); + } else if (result.error && !result.error.includes('already exists')) { + console.error( + `[${new Date().toISOString()}] WARN [claude-flow-mcp] (${sessionId}) Memory database init returned: ${result.error}` + ); + } + } else { + console.error( + `[${new Date().toISOString()}] INFO [claude-flow-mcp] (${sessionId}) Memory database already initialized (v${status.version || 'unknown'})` + ); + } + } catch (memInitError) { + // Graceful degradation: server continues even if memory init fails. + // Memory tools will attempt lazy init on first call via ensureInitialized(). + console.error( + `[${new Date().toISOString()}] WARN [claude-flow-mcp] (${sessionId}) Memory auto-init failed (tools will retry on first call): ${memInitError instanceof Error ? memInitError.message : String(memInitError)}` + ); + } console.error(JSON.stringify({ arch: process.arch, mode: 'mcp-stdio', @@ -653,7 +686,7 @@ export class MCPServerManager extends EventEmitter { } // Also clean up legacy PID file location from older versions try { - const legacyPath = path.join(process.cwd(), '.claude-flow', 'mcp-server.pid'); + const legacyPath = path.join(process.env.CLAUDE_FLOW_CWD || process.cwd(), '.claude-flow', 'mcp-server.pid'); if (legacyPath !== this.options.pidFile) { await fs.promises.unlink(legacyPath); } diff --git a/v3/@claude-flow/cli/src/mcp-tools/agent-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/agent-tools.ts index 36682881a..4c763357a 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/agent-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/agent-tools.ts @@ -7,7 +7,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; // Storage paths const STORAGE_DIR = '.claude-flow'; @@ -37,7 +37,7 @@ interface AgentStore { } function getAgentDir(): string { - return join(process.cwd(), STORAGE_DIR, AGENT_DIR); + return join(getProjectCwd(), STORAGE_DIR, AGENT_DIR); } function getAgentPath(): string { diff --git a/v3/@claude-flow/cli/src/mcp-tools/config-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/config-tools.ts index 6db4d5fc0..411c2028e 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/config-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/config-tools.ts @@ -6,7 +6,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; // Storage paths const STORAGE_DIR = '.claude-flow'; @@ -34,7 +34,7 @@ const DEFAULT_CONFIG: Record = { }; function getConfigDir(): string { - return join(process.cwd(), STORAGE_DIR); + return join(getProjectCwd(), STORAGE_DIR); } function getConfigPath(): string { diff --git a/v3/@claude-flow/cli/src/mcp-tools/coordination-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/coordination-tools.ts index 1f1c894c2..31f9acf59 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/coordination-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/coordination-tools.ts @@ -9,7 +9,7 @@ * - Useful for single-machine workflow orchestration */ -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; @@ -77,7 +77,7 @@ interface CoordinationStore { } function getCoordDir(): string { - return join(process.cwd(), STORAGE_DIR, COORD_DIR); + return join(getProjectCwd(), STORAGE_DIR, COORD_DIR); } function getCoordPath(): string { diff --git a/v3/@claude-flow/cli/src/mcp-tools/daa-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/daa-tools.ts index 65392c9fa..ab6a0b612 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/daa-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/daa-tools.ts @@ -9,7 +9,7 @@ * - Useful for workflow orchestration and state tracking */ -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; @@ -53,7 +53,7 @@ interface DAAStore { } function getDAADir(): string { - return join(process.cwd(), STORAGE_DIR, DAA_DIR); + return join(getProjectCwd(), STORAGE_DIR, DAA_DIR); } function getDAAPath(): string { diff --git a/v3/@claude-flow/cli/src/mcp-tools/github-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/github-tools.ts index 152c22223..9b5ce522f 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/github-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/github-tools.ts @@ -9,7 +9,7 @@ * - For real GitHub operations, use `gh` CLI or GitHub MCP server */ -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; @@ -40,7 +40,7 @@ interface GitHubStore { } function getGitHubDir(): string { - return join(process.cwd(), STORAGE_DIR, GITHUB_DIR); + return join(getProjectCwd(), STORAGE_DIR, GITHUB_DIR); } function getGitHubPath(): string { diff --git a/v3/@claude-flow/cli/src/mcp-tools/guidance-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/guidance-tools.ts index 3213ea9dd..14d3d60b0 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/guidance-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/guidance-tools.ts @@ -7,7 +7,7 @@ * @module @claude-flow/cli/mcp-tools/guidance */ -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -22,8 +22,8 @@ const CLI_ROOT = join(__dirname, '../../..'); */ function findProjectRoot(): string { // Strategy 1: CWD (most reliable when invoked by user) - if (existsSync(join(process.cwd(), '.claude'))) { - return process.cwd(); + if (existsSync(join(getProjectCwd(), '.claude'))) { + return getProjectCwd(); } // Strategy 2: Walk up from CLI package location @@ -34,7 +34,7 @@ function findProjectRoot(): string { } // Strategy 3: Walk up from CWD - let dir = process.cwd(); + let dir = getProjectCwd(); for (let i = 0; i < 10; i++) { if (existsSync(join(dir, '.claude'))) return dir; const parent = dirname(dir); @@ -43,7 +43,7 @@ function findProjectRoot(): string { } // Fallback: CWD - return process.cwd(); + return getProjectCwd(); } const PROJECT_ROOT = findProjectRoot(); diff --git a/v3/@claude-flow/cli/src/mcp-tools/hive-mind-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/hive-mind-tools.ts index 2ac1b1409..23e7400e5 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/hive-mind-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/hive-mind-tools.ts @@ -6,7 +6,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; // Storage paths const STORAGE_DIR = '.claude-flow'; @@ -151,7 +151,7 @@ function tryResolveProposal( } function getHiveDir(): string { - return join(process.cwd(), STORAGE_DIR, HIVE_DIR); + return join(getProjectCwd(), STORAGE_DIR, HIVE_DIR); } function getHivePath(): string { @@ -196,7 +196,7 @@ function saveHiveState(state: HiveState): void { import { existsSync as agentStoreExists, readFileSync as readAgentStore, writeFileSync as writeAgentStore, mkdirSync as mkdirAgentStore } from 'node:fs'; function loadAgentStore(): { agents: Record } { - const storePath = join(process.cwd(), '.claude-flow', 'agents.json'); + const storePath = join(getProjectCwd(), '.claude-flow', 'agents.json'); try { if (agentStoreExists(storePath)) { return JSON.parse(readAgentStore(storePath, 'utf-8')); @@ -206,7 +206,7 @@ function loadAgentStore(): { agents: Record } { } function saveAgentStore(store: { agents: Record }): void { - const storeDir = join(process.cwd(), '.claude-flow'); + const storeDir = join(getProjectCwd(), '.claude-flow'); if (!agentStoreExists(storeDir)) { mkdirAgentStore(storeDir, { recursive: true }); } @@ -346,7 +346,7 @@ export const hiveMindTools: MCPTool[] = [ const agentStore = loadAgentStore(); // Compute real task metrics from task store - const taskStorePath = join(process.cwd(), '.claude-flow', 'tasks', 'store.json'); + const taskStorePath = join(getProjectCwd(), '.claude-flow', 'tasks', 'store.json'); let pendingTaskCount = 0; let activeTaskCount = 0; let completedTaskCount = 0; diff --git a/v3/@claude-flow/cli/src/mcp-tools/hooks-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/hooks-tools.ts index 4870e4f0f..66fdd97e6 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/hooks-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/hooks-tools.ts @@ -5,7 +5,7 @@ import { mkdirSync, writeFileSync, existsSync, readFileSync, statSync } from 'fs'; import { dirname, join, resolve } from 'path'; -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; // Real vector search functions - lazy loaded to avoid circular imports let searchEntriesFn: ((options: { @@ -1570,7 +1570,7 @@ export const hooksSessionStart: MCPTool = { try { // Dynamic import to avoid circular dependencies const { startDaemon } = await import('../services/worker-daemon.js'); - const daemon = await startDaemon(process.cwd()); + const daemon = await startDaemon(getProjectCwd()); const status = daemon.getStatus(); daemonStatus = { started: true, diff --git a/v3/@claude-flow/cli/src/mcp-tools/neural-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/neural-tools.ts index 3b2c2d18a..00298bced 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/neural-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/neural-tools.ts @@ -12,7 +12,7 @@ * Note: For production neural features, use @claude-flow/neural module */ -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; @@ -90,7 +90,7 @@ interface NeuralStore { } function getNeuralDir(): string { - return join(process.cwd(), STORAGE_DIR, NEURAL_DIR); + return join(getProjectCwd(), STORAGE_DIR, NEURAL_DIR); } function getNeuralPath(): string { diff --git a/v3/@claude-flow/cli/src/mcp-tools/performance-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/performance-tools.ts index dbc13ff2a..4cb91d5a1 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/performance-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/performance-tools.ts @@ -12,7 +12,7 @@ * Note: Some optimization suggestions are illustrative */ -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; import * as os from 'node:os'; @@ -52,7 +52,7 @@ interface PerfStore { } function getPerfDir(): string { - return join(process.cwd(), STORAGE_DIR, PERF_DIR); + return join(getProjectCwd(), STORAGE_DIR, PERF_DIR); } function getPerfPath(): string { diff --git a/v3/@claude-flow/cli/src/mcp-tools/session-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/session-tools.ts index 4972e1a5c..bfc26d9c4 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/session-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/session-tools.ts @@ -6,7 +6,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync, statSync } from 'node:fs'; import { join } from 'node:path'; -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; // Storage paths const STORAGE_DIR = '.claude-flow'; @@ -31,7 +31,7 @@ interface SessionRecord { } function getSessionDir(): string { - return join(process.cwd(), STORAGE_DIR, SESSION_DIR); + return join(getProjectCwd(), STORAGE_DIR, SESSION_DIR); } function getSessionPath(sessionId: string): string { @@ -89,7 +89,7 @@ function loadRelatedStores(options: { includeMemory?: boolean; includeTasks?: bo if (options.includeMemory) { try { - const memoryPath = join(process.cwd(), STORAGE_DIR, 'memory', 'store.json'); + const memoryPath = join(getProjectCwd(), STORAGE_DIR, 'memory', 'store.json'); if (existsSync(memoryPath)) { data.memory = JSON.parse(readFileSync(memoryPath, 'utf-8')); } @@ -98,7 +98,7 @@ function loadRelatedStores(options: { includeMemory?: boolean; includeTasks?: bo if (options.includeTasks) { try { - const taskPath = join(process.cwd(), STORAGE_DIR, 'tasks', 'store.json'); + const taskPath = join(getProjectCwd(), STORAGE_DIR, 'tasks', 'store.json'); if (existsSync(taskPath)) { data.tasks = JSON.parse(readFileSync(taskPath, 'utf-8')); } @@ -107,7 +107,7 @@ function loadRelatedStores(options: { includeMemory?: boolean; includeTasks?: bo if (options.includeAgents) { try { - const agentPath = join(process.cwd(), STORAGE_DIR, 'agents', 'store.json'); + const agentPath = join(getProjectCwd(), STORAGE_DIR, 'agents', 'store.json'); if (existsSync(agentPath)) { data.agents = JSON.parse(readFileSync(agentPath, 'utf-8')); } @@ -212,17 +212,17 @@ export const sessionTools: MCPTool[] = [ if (session) { // Restore data to respective stores if (session.data?.memory) { - const memoryDir = join(process.cwd(), STORAGE_DIR, 'memory'); + const memoryDir = join(getProjectCwd(), STORAGE_DIR, 'memory'); if (!existsSync(memoryDir)) mkdirSync(memoryDir, { recursive: true }); writeFileSync(join(memoryDir, 'store.json'), JSON.stringify(session.data.memory, null, 2), 'utf-8'); } if (session.data?.tasks) { - const taskDir = join(process.cwd(), STORAGE_DIR, 'tasks'); + const taskDir = join(getProjectCwd(), STORAGE_DIR, 'tasks'); if (!existsSync(taskDir)) mkdirSync(taskDir, { recursive: true }); writeFileSync(join(taskDir, 'store.json'), JSON.stringify(session.data.tasks, null, 2), 'utf-8'); } if (session.data?.agents) { - const agentDir = join(process.cwd(), STORAGE_DIR, 'agents'); + const agentDir = join(getProjectCwd(), STORAGE_DIR, 'agents'); if (!existsSync(agentDir)) mkdirSync(agentDir, { recursive: true }); writeFileSync(join(agentDir, 'store.json'), JSON.stringify(session.data.agents, null, 2), 'utf-8'); } diff --git a/v3/@claude-flow/cli/src/mcp-tools/swarm-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/swarm-tools.ts index 3fe2cbb03..25c066f67 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/swarm-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/swarm-tools.ts @@ -7,7 +7,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; // Swarm state persistence const SWARM_DIR = '.claude-flow/swarm'; @@ -31,7 +31,7 @@ interface SwarmStore { } function getSwarmDir(): string { - return join(process.cwd(), SWARM_DIR); + return join(getProjectCwd(), SWARM_DIR); } function getSwarmStatePath(): string { diff --git a/v3/@claude-flow/cli/src/mcp-tools/system-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/system-tools.ts index 8867db003..c695bfec8 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/system-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/system-tools.ts @@ -9,7 +9,7 @@ * - os module for system information */ -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -47,7 +47,7 @@ interface SystemMetrics { } function getSystemDir(): string { - return join(process.cwd(), STORAGE_DIR, SYSTEM_DIR); + return join(getProjectCwd(), STORAGE_DIR, SYSTEM_DIR); } function getMetricsPath(): string { @@ -308,7 +308,7 @@ export const systemTools: MCPTool[] = [ platform: process.platform, arch: process.arch, pid: process.pid, - cwd: process.cwd(), + cwd: getProjectCwd(), env: process.env.NODE_ENV || 'development', features: { swarm: true, @@ -437,7 +437,7 @@ export const systemTools: MCPTool[] = [ }, handler: async () => { // Read from the task store file - const storePath = join(process.cwd(), '.claude-flow', 'tasks', 'store.json'); + const storePath = join(getProjectCwd(), '.claude-flow', 'tasks', 'store.json'); let tasks: Array<{ status: string }> = []; try { if (existsSync(storePath)) { diff --git a/v3/@claude-flow/cli/src/mcp-tools/task-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/task-tools.ts index 0a32240b0..14bba51d2 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/task-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/task-tools.ts @@ -6,7 +6,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; // Storage paths const STORAGE_DIR = '.claude-flow'; @@ -34,7 +34,7 @@ interface TaskStore { } function getTaskDir(): string { - return join(process.cwd(), STORAGE_DIR, TASK_DIR); + return join(getProjectCwd(), STORAGE_DIR, TASK_DIR); } function getTaskPath(): string { @@ -243,7 +243,7 @@ export const taskTools: MCPTool[] = [ // Sync assigned agents back to idle and increment taskCount if (task.assignedTo.length > 0) { - const agentStorePath = join(process.cwd(), STORAGE_DIR, 'agents.json'); + const agentStorePath = join(getProjectCwd(), STORAGE_DIR, 'agents.json'); try { let agentStore: { agents: Record> } = { agents: {} }; if (existsSync(agentStorePath)) { @@ -354,7 +354,7 @@ export const taskTools: MCPTool[] = [ const previouslyAssigned = [...task.assignedTo]; // Load agent store to sync worker state - const agentStorePath = join(process.cwd(), STORAGE_DIR, 'agents.json'); + const agentStorePath = join(getProjectCwd(), STORAGE_DIR, 'agents.json'); let agentStore: { agents: Record> } = { agents: {} }; try { if (existsSync(agentStorePath)) { @@ -399,7 +399,7 @@ export const taskTools: MCPTool[] = [ saveTaskStore(store); // Save agent store - const agentDir = join(process.cwd(), STORAGE_DIR); + const agentDir = join(getProjectCwd(), STORAGE_DIR); if (!existsSync(agentDir)) { mkdirSync(agentDir, { recursive: true }); } diff --git a/v3/@claude-flow/cli/src/mcp-tools/terminal-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/terminal-tools.ts index b3a9a9dc9..23b457079 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/terminal-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/terminal-tools.ts @@ -4,7 +4,7 @@ * Terminal session management with real command execution. */ -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; import { execSync } from 'node:child_process'; @@ -31,7 +31,7 @@ interface TerminalStore { } function getTerminalDir(): string { - return join(process.cwd(), STORAGE_DIR, TERMINAL_DIR); + return join(getProjectCwd(), STORAGE_DIR, TERMINAL_DIR); } function getTerminalPath(): string { @@ -85,7 +85,7 @@ export const terminalTools: MCPTool[] = [ status: 'active', createdAt: new Date().toISOString(), lastActivity: new Date().toISOString(), - workingDir: (input.workingDir as string) || process.cwd(), + workingDir: (input.workingDir as string) || getProjectCwd(), history: [], env: (input.env as Record) || {}, }; @@ -134,7 +134,7 @@ export const terminalTools: MCPTool[] = [ status: 'active', createdAt: new Date().toISOString(), lastActivity: new Date().toISOString(), - workingDir: process.cwd(), + workingDir: getProjectCwd(), history: [], env: {}, }; @@ -142,7 +142,7 @@ export const terminalTools: MCPTool[] = [ } const timeout = (input.timeout as number) || 30_000; - const cwd = session.workingDir || process.cwd(); + const cwd = session.workingDir || getProjectCwd(); const startTime = Date.now(); let output: string; let exitCode: number; diff --git a/v3/@claude-flow/cli/src/mcp-tools/types.ts b/v3/@claude-flow/cli/src/mcp-tools/types.ts index 1e1384bcf..4e1b1f2dd 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/types.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/types.ts @@ -20,6 +20,15 @@ export interface MCPToolResult { isError?: boolean; } +/** + * Returns the effective project working directory. + * Prefers CLAUDE_FLOW_CWD (set by the install script for global/MCP installs + * where process.cwd() may resolve to '/') over the real process.cwd(). + */ +export function getProjectCwd(): string { + return process.env.CLAUDE_FLOW_CWD || process.cwd(); +} + export interface MCPTool { name: string; description: string; diff --git a/v3/@claude-flow/cli/src/mcp-tools/workflow-tools.ts b/v3/@claude-flow/cli/src/mcp-tools/workflow-tools.ts index 3aee34009..5eb5b5a44 100644 --- a/v3/@claude-flow/cli/src/mcp-tools/workflow-tools.ts +++ b/v3/@claude-flow/cli/src/mcp-tools/workflow-tools.ts @@ -6,7 +6,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { join } from 'node:path'; -import type { MCPTool } from './types.js'; +import { type MCPTool, getProjectCwd } from './types.js'; // Storage paths const STORAGE_DIR = '.claude-flow'; @@ -45,7 +45,7 @@ interface WorkflowStore { } function getWorkflowDir(): string { - return join(process.cwd(), STORAGE_DIR, WORKFLOW_DIR); + return join(getProjectCwd(), STORAGE_DIR, WORKFLOW_DIR); } function getWorkflowPath(): string {