UNPKG

@claude-flow/embeddings

Version:

V3 Embedding Service - OpenAI, Transformers.js, Agentic-Flow (ONNX), Mock providers with hyperbolic embeddings, normalization, and chunking

186 lines • 7.62 kB
/** * ADR-121 Phase 5 (lightweight) — ruvector MCP sidecar availability probe. * * The `ruvector` CLI ships its own MCP server with a rich tool surface * (`hooks_*`, vector memory, trajectory tracking, etc.). The full * Phase 5 plan is to wire a sidecar MCP server alongside `@claude-flow/cli` * so embedding-heavy MCP tools can delegate to the optimized Rust * backend. That's a multi-package integration (touches the CLI's MCP * registry, the doctor command, the cost-tracker) and lands in a * follow-up. * * This module ships the *foundation* for that work: a structured * availability probe that detects whether `ruvector` is installed, * what version, and which MCP tools it exposes. The probe is * shellout-based (spawns `ruvector --version` + `ruvector mcp tools`), * times out cleanly, and degrades to a `null` report when the CLI * isn't on the PATH. * * Consumers: * - `ruflo doctor` integration (follow-up PR) — reports the sidecar * availability + version in the system health table * - New `embeddings_check_ruvector_sidecar` MCP tool — exposes the * report to LLM agents that want to know whether the optimized * backend is reachable before issuing embedding-heavy calls * - CI guards — assert the probe handles both installed/not-installed * paths cleanly */ import { spawn } from 'node:child_process'; /** * Run a child process to completion or timeout. Returns null on * any failure (spawn error, timeout, non-zero exit). The caller * surfaces the failure as a structured `reason` string instead of * propagating the raw error — the probe is meant to be friendly, * not throw. */ async function runShort(bin, args, timeoutMs) { return new Promise((resolve) => { // Use the literal `['ignore', 'pipe', 'pipe']` stdio shape so // TypeScript narrows to ChildProcessByStdio<null, Readable, Readable> // (no stdin handle, both stdout + stderr pipeable). let proc; try { proc = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'] }); } catch { resolve(null); return; } let stdout = ''; let stderr = ''; let done = false; const finish = (val) => { if (!done) { done = true; resolve(val); } }; proc.stdout.on('data', (b) => { stdout += b.toString(); }); proc.stderr.on('data', (b) => { stderr += b.toString(); }); const timer = setTimeout(() => { try { proc.kill('SIGTERM'); } catch { /* ignore */ } finish(null); }, timeoutMs); proc.on('error', () => { clearTimeout(timer); finish(null); }); proc.on('close', (code) => { clearTimeout(timer); // For `ruvector --version` and `ruvector mcp tools`, both succeed // with exit 0 and print to stdout. A non-zero exit means the CLI // surface changed — treat as "not available" for probe purposes. finish(code === 0 ? stdout : (stderr ? null : stdout || null)); }); }); } /** * Parse the columnar text output of `ruvector mcp tools` into a * structured tool list. The CLI's output shape (as of ruvector * 0.2.x): * * RuVector MCP Tools * * <group> (<count>): * <name> <params> <description> * ... * * Parser is intentionally tolerant of column-alignment variation * (the CLI right-pads to a column width that depends on the * longest tool name) — we split on runs of 2+ spaces, not exact * column positions. */ function parseMcpTools(stdout) { const tools = []; let currentGroup = null; for (const rawLine of stdout.split('\n')) { const line = rawLine.replace(/\[[0-9;]*m/g, ''); // strip ANSI color const trimmed = line.trim(); if (!trimmed) continue; // Group header looks like " hooks-core (10):" const groupMatch = /^\s+([a-z0-9_-]+)\s*\((\d+)\):\s*$/i.exec(line); if (groupMatch) { currentGroup = groupMatch[1]; continue; } // Tool row: indented further, has at least 3 columns. if (currentGroup && /^\s{4,}\S/.test(line)) { // Split on runs of 2+ spaces to get [name, params, ...description]. const parts = trimmed.split(/\s{2,}/); if (parts.length < 2) continue; const name = parts[0].trim(); const params = (parts[1] ?? '').trim(); const description = parts.slice(2).join(' ').trim() || params; // Re-derive params if description ate them (description was // missing → parts had 2 entries, second was actually the desc). const finalParams = parts.length >= 3 ? params : '(none)'; const finalDesc = parts.length >= 3 ? description : params; // Skip header-shaped rows that slip past the regex (e.g. an // extra divider line in a future CLI release). if (!name || name.startsWith('-') || name === 'RuVector') continue; tools.push({ group: currentGroup, name, params: finalParams, description: finalDesc }); } } return tools; } /** * Probe ruvector CLI availability + MCP tool surface. Never throws; * returns a `RuvectorAvailability` report with `available: false` and * a `reason` string when the CLI isn't reachable. */ export async function probeRuvectorSidecar(options = {}) { const t0 = Date.now(); const bin = options.binary ?? 'ruvector'; const timeoutMs = options.timeoutMs ?? 5_000; // Step 1 — version probe. If this fails the CLI isn't there. const versionOut = await runShort(bin, ['--version'], timeoutMs); if (!versionOut) { return { available: false, reason: `'${bin} --version' did not return cleanly within ${timeoutMs}ms; CLI likely not installed`, probeMs: Date.now() - t0, }; } const version = versionOut.trim().split('\n')[0]?.trim(); // Step 2 — locate the binary via `which` (best-effort, non-fatal). let binary; const whichOut = await runShort('which', [bin], 2_000); if (whichOut) binary = whichOut.trim().split('\n')[0]?.trim(); // Step 3 — tool list. `ruvector mcp tools` is the documented surface. const toolsOut = await runShort(bin, ['mcp', 'tools'], timeoutMs); if (!toolsOut) { return { available: true, version, binary, reason: `'${bin} mcp tools' did not return cleanly within ${timeoutMs}ms; tool surface unknown`, probeMs: Date.now() - t0, }; } const tools = parseMcpTools(toolsOut); const groups = {}; for (const t of tools) groups[t.group] = (groups[t.group] ?? 0) + 1; return { available: true, version, binary, tools, toolCount: tools.length, groups, probeMs: Date.now() - t0, }; } /** * Compact summary for dashboards / `ruflo doctor` output. Returns * a single line suitable for a table row. */ export function formatRuvectorAvailability(r) { if (!r.available) { return `ruvector: not available (${r.reason ?? 'unknown reason'}) [${r.probeMs}ms]`; } const groupSummary = r.groups ? Object.entries(r.groups).map(([g, n]) => `${g}=${n}`).join(', ') : 'tools unknown'; return `ruvector: ${r.version ?? '?'} (${r.toolCount ?? 0} MCP tools — ${groupSummary}) [${r.probeMs}ms]`; } //# sourceMappingURL=ruvector-mcp-probe.js.map