import { spawnSync } from 'node:child_process'; import path from 'node:path'; export const DEFAULT_ZVEC_QUERY_LIMIT = 8; function truthy(value, fallback = false) { const raw = String(value ?? '').trim().toLowerCase(); if (!raw) return fallback; return ['1', 'true', 'yes', 'on'].includes(raw); } export function resolveZvecWorkspaceMode(env = process.env) { const raw = String(env.MEMIND_ZVEC_WORKSPACE_MODE ?? 'off').trim().toLowerCase(); return ['off', 'shadow', 'active'].includes(raw) ? raw : 'off'; } export function resolveZvecBinary(env = process.env) { return String(env.MEMIND_ZVEC_BINARY ?? 'zg').trim() || 'zg'; } export function resolveZvecWorkspaceRoot(cwd, env = process.env) { const explicit = String(env.MEMIND_ZVEC_WORKSPACE_ROOT ?? '').trim(); if (explicit) return path.resolve(explicit); return path.resolve(String(cwd ?? process.cwd())); } function parseZvecQueryOutput(stdout) { const hits = []; const lines = String(stdout ?? '').split('\n'); for (const line of lines) { const match = line.match(/^#(\d+) matchedBy=([^\s]+)\s+(.+?):(\d+)-(\d+)/); if (!match) continue; hits.push({ rank: Number(match[1]), matchedBy: match[2], path: match[3].trim(), startLine: Number(match[4]), endLine: Number(match[5]), }); } return hits; } export function searchZvecWorkspace({ query, limit = DEFAULT_ZVEC_QUERY_LIMIT, cwd, env = process.env, spawnImpl = spawnSync, } = {}) { const mode = resolveZvecWorkspaceMode(env); const text = String(query ?? '').trim(); const workspaceRoot = resolveZvecWorkspaceRoot(cwd, env); if (mode === 'off' || !text) { return { mode, query: text, hits: [], applied: false, skipped: true, reason: mode === 'off' ? 'disabled' : 'empty_query', }; } const binary = resolveZvecBinary(env); const result = spawnImpl(binary, ['query', text, '--limit', String(limit)], { cwd: workspaceRoot, env: { ...process.env, ...env }, encoding: 'utf8', }); if (result.status !== 0) { return { mode, query: text, hits: [], applied: false, skipped: true, reason: 'zvec_query_failed', stderr: String(result.stderr ?? '').trim().slice(0, 400), }; } const hits = parseZvecQueryOutput(result.stdout); return { mode, query: text, workspaceRoot, hits, hitCount: hits.length, applied: mode === 'active', skipped: false, stdoutPreview: String(result.stdout ?? '').trim().slice(0, 1200), }; } export function formatZvecWorkspaceBlock(result) { if (!result?.hits?.length) return ''; const lines = [ '【Workspace 代码检索】', '以下片段来自本工作区索引,仅供开发/排查参考,不是用户生活记忆。', ]; for (const hit of result.hits.slice(0, 8)) { lines.push( `- ${hit.path}:${hit.startLine}-${hit.endLine} (${hit.matchedBy})`, ); } return lines.join('\n').trim(); } export function isZvecWorkspaceEnabled(env = process.env) { return resolveZvecWorkspaceMode(env) !== 'off'; } export function shouldAttachZvecWorkspaceForTask(taskType, env = process.env) { if (!isZvecWorkspaceEnabled(env)) return false; const raw = String(env.MEMIND_ZVEC_WORKSPACE_TASK_TYPES ?? 'page_data_dev_complex,repo_refactor,multi_file') .split(',') .map((item) => item.trim().toLowerCase()) .filter(Boolean); const normalizedTask = String(taskType ?? '').trim().toLowerCase(); return raw.includes(normalizedTask); }