fix: 5 critical bugs — cwd, intelligence hang, memory init, ruvector v3.5.52

- #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 <cohen@ruv-mac-mini.local>
This commit is contained in:
rUv
2026-04-06 11:47:30 -04:00
committed by GitHub
co-authored by Reuven
parent 322b2ae521
commit 5c5ede94b6
29 changed files with 362 additions and 150 deletions
+43 -18
View File
@@ -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);
}
+6
View File
@@ -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; }
}
+6 -5
View File
@@ -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 ""
}
@@ -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}`);
@@ -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, {
@@ -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(`
@@ -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)',
@@ -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)
`);
}
@@ -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');
@@ -88,6 +88,7 @@ export const optimizeCommand: Command = {
},
{
name: 'database',
short: 'd',
description: 'Database name',
type: 'string',
},
@@ -180,25 +180,43 @@ export const statusCommand: Command = {
const pgVersion = versionResult.rows[0].version;
(statusData.connection as Record<string, unknown>).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}`);
}
+34 -1
View File
@@ -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);
}
@@ -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 {
@@ -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<string, unknown> = {
};
function getConfigDir(): string {
return join(process.cwd(), STORAGE_DIR);
return join(getProjectCwd(), STORAGE_DIR);
}
function getConfigPath(): string {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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();
@@ -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<string, unknown> } {
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<string, unknown> } {
}
function saveAgentStore(store: { agents: Record<string, unknown> }): 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;
@@ -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,
@@ -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 {
@@ -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 {
@@ -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');
}
@@ -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 {
@@ -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)) {
@@ -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<string, Record<string, unknown>> } = { 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<string, Record<string, unknown>> } = { 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 });
}
@@ -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<string, string>) || {},
};
@@ -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;
@@ -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;
@@ -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 {