diff --git a/README.md b/README.md index f60fefa..c1a8584 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A 24-tool MCP server for Claude Code that catches ambiguous instructions before [![npm](https://img.shields.io/npm/v/preflight-dev)](https://www.npmjs.com/package/preflight-dev) [![Node 18+](https://img.shields.io/badge/node-18%2B-brightgreen?logo=node.js&logoColor=white)](https://nodejs.org/) -[Quick Start](#quick-start) · [How It Works](#how-it-works) · [Tool Reference](#tool-reference) · [Configuration](#configuration) · [Scoring](#the-12-category-scorecard) +[Quick Start](#quick-start) · [How It Works](#how-it-works) · [Tool Reference](#tool-reference) · [Configuration](#configuration) · [Scoring](#the-12-category-scorecard) · [Troubleshooting](TROUBLESHOOTING.md) diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..8e8ba66 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,131 @@ +# Troubleshooting + +Common issues and fixes for preflight. + +--- + +## Installation + +### `npm install` fails with LanceDB native binary errors + +LanceDB uses native binaries. If you see errors like `prebuild-install WARN` or `node-gyp` failures: + +``` +npm ERR! @lancedb/lancedb@0.26.2: The platform "linux" is incompatible +``` + +**Fix:** Make sure you're on a supported platform (macOS arm64/x64, Linux x64, Windows x64) and Node >= 20: + +```bash +node -v # must be >= 20 +npm cache clean --force +rm -rf node_modules package-lock.json +npm install +``` + +If you're on an unsupported platform (e.g., Linux arm64), LanceDB won't work. The timeline/vector search tools will be unavailable, but the core preflight tools still function. + +### `npx tsx` not found + +```bash +npm install -g tsx +# or use npx (comes with npm 7+): +npx tsx src/index.ts +``` + +--- + +## Configuration + +### Tools load but `CLAUDE_PROJECT_DIR` warnings appear + +The timeline and contract search tools need `CLAUDE_PROJECT_DIR` to know which project to index. Without it, those tools will error on use. + +**Fix — Claude Code CLI:** +```bash +claude mcp add preflight \ + -e CLAUDE_PROJECT_DIR=/absolute/path/to/your/project \ + -- npx tsx /path/to/preflight/src/index.ts +``` + +**Fix — `.mcp.json`:** +```json +{ + "mcpServers": { + "preflight": { + "command": "npx", + "args": ["tsx", "/path/to/preflight/src/index.ts"], + "env": { + "CLAUDE_PROJECT_DIR": "/absolute/path/to/your/project" + } + } + } +} +``` + +Use absolute paths — relative paths resolve from the MCP server's cwd, which may not be your project. + +### `.preflight/` config not being picked up + +Preflight looks for `.preflight/config.yml` in `CLAUDE_PROJECT_DIR`. If it's not found, defaults are used silently. + +**Check:** +1. File is named exactly `config.yml` (not `config.yaml`) +2. It's inside `.preflight/` at your project root +3. `CLAUDE_PROJECT_DIR` points to the right directory + +--- + +## Runtime + +### "Server started" but no tools appear in Claude Code + +The MCP handshake might be failing silently. + +**Debug steps:** +1. Test the server standalone: `npx tsx src/index.ts` — should print `preflight: server started` +2. Check Claude Code's MCP logs: `claude mcp list` to see registered servers +3. Remove and re-add: `claude mcp remove preflight && claude mcp add preflight -- npx tsx /path/to/preflight/src/index.ts` + +### Vector search returns no results + +Timeline search uses LanceDB to index your Claude Code session history (JSONL files). + +**Common causes:** +- No session history exists yet — use Claude Code for a few sessions first +- `CLAUDE_PROJECT_DIR` not set (search doesn't know where to look) +- Session files are in a non-standard location + +**Where Claude Code stores sessions:** `~/.claude/projects/` with JSONL files per session. + +### `preflight_check` returns generic advice + +The triage system works best with specific prompts. If you're testing with something like "do stuff", the response will be generic by design — that's it telling you the prompt is too vague. + +Try a real prompt: `"add rate limiting to the /api/users endpoint"` — you'll see it route through scope analysis, contract search, and produce actionable guidance. + +--- + +## Performance + +### Slow startup (>5 seconds) + +LanceDB initialization can be slow on first run as it builds the vector index. + +**Fix:** Subsequent runs are faster. If consistently slow, check that `node_modules/@lancedb` isn't corrupted: +```bash +rm -rf node_modules/@lancedb +npm install +``` + +### High memory usage + +Each indexed project maintains an in-memory vector index. If you're indexing many large projects, memory can grow. + +**Fix:** Only set `CLAUDE_PROJECT_DIR` to the project you're actively working on. + +--- + +## Still stuck? + +Open an issue: https://github.com/TerminalGravity/preflight/issues diff --git a/src/lib/git.ts b/src/lib/git.ts index a32ee3c..d2bcded 100644 --- a/src/lib/git.ts +++ b/src/lib/git.ts @@ -1,4 +1,4 @@ -import { execFileSync } from "child_process"; +import { execFileSync, execSync } from "child_process"; import { PROJECT_DIR } from "./files.js"; import type { RunError } from "../types.js"; @@ -30,6 +30,31 @@ export function run(argsOrCmd: string | string[], opts: { timeout?: number } = { } } +/** + * Run a shell command string via execSync (with shell). + * Use for commands that genuinely need shell features: pipes, redirects, ||, &&. + * Prefer run() with array args for simple git commands. + */ +export function shell(cmd: string, opts: { timeout?: number } = {}): string { + try { + return execSync(cmd, { + cwd: PROJECT_DIR, + encoding: "utf-8", + timeout: opts.timeout || 10000, + maxBuffer: 1024 * 1024, + stdio: ["pipe", "pipe", "pipe"], + }).trim(); + } catch (e: any) { + const timedOut = e.killed === true || e.signal === "SIGTERM"; + if (timedOut) { + return `[timed out after ${opts.timeout || 10000}ms]`; + } + const output = e.stdout?.trim() || e.stderr?.trim(); + if (output) return output; + return `[command failed: ${cmd} (exit ${e.status ?? "?"})]`; + } +} + /** Convenience: run a raw command string (split on spaces). Only for simple, known-safe commands. */ function gitCmd(cmdStr: string, opts?: { timeout?: number }): string { return run(cmdStr.split(/\s+/), opts); diff --git a/src/tools/audit-workspace.ts b/src/tools/audit-workspace.ts index d4306bd..6f49a46 100644 --- a/src/tools/audit-workspace.ts +++ b/src/tools/audit-workspace.ts @@ -1,5 +1,5 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { run } from "../lib/git.js"; +import { run, shell } from "../lib/git.js"; import { readIfExists, findWorkspaceDocs } from "../lib/files.js"; /** Extract top-level work areas from file paths generically */ @@ -36,7 +36,7 @@ export function registerAuditWorkspace(server: McpServer): void { {}, async () => { const docs = findWorkspaceDocs(); - const recentFiles = run("git diff --name-only HEAD~10 2>/dev/null || echo ''").split("\n").filter(Boolean); + const recentFiles = run(["diff", "--name-only", "HEAD~10"]).split("\n").filter(Boolean); const sections: string[] = []; // Doc freshness @@ -75,7 +75,7 @@ export function registerAuditWorkspace(server: McpServer): void { // Check for gap trackers or similar tracking docs const trackingDocs = Object.entries(docs).filter(([n]) => /gap|track|progress/i.test(n)); if (trackingDocs.length > 0) { - const testFilesCount = parseInt(run("find tests -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | wc -l").trim()) || 0; + const testFilesCount = parseInt(shell("find tests -name '*.spec.ts' -o -name '*.test.ts' 2>/dev/null | wc -l").trim()) || 0; sections.push(`## Tracking Docs\n${trackingDocs.map(([n]) => { const age = docStatus.find(d => d.name === n)?.ageHours ?? "?"; return `- .claude/${n} — last updated ${age}h ago`; diff --git a/src/tools/checkpoint.ts b/src/tools/checkpoint.ts index e086f01..a5ab559 100644 --- a/src/tools/checkpoint.ts +++ b/src/tools/checkpoint.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { writeFileSync, existsSync, mkdirSync } from "fs"; import { join, dirname } from "path"; -import { run, getBranch, getStatus, getLastCommit, getStagedFiles } from "../lib/git.js"; +import { run, shell, getBranch, getStatus, getLastCommit, getStagedFiles } from "../lib/git.js"; import { PROJECT_DIR } from "../lib/files.js"; import { appendLog, now } from "../lib/state.js"; @@ -84,11 +84,12 @@ ${dirty || "clean"} if (commitResult === "no uncommitted changes") { // Stage the checkpoint file too - run(`git add "${checkpointFile}"`); - const result = run(`${addCmd} && git commit -m "${commitMsg.replace(/"/g, '\\"')}" 2>&1`); + run(["add", checkpointFile]); + shell(addCmd); + const result = run(["commit", "-m", commitMsg]); if (result.includes("commit failed") || result.includes("nothing to commit")) { // Rollback: unstage if commit failed - run("git reset HEAD 2>/dev/null"); + run(["reset", "HEAD"]); commitResult = `commit failed: ${result}`; } else { commitResult = result; diff --git a/src/tools/clarify-intent.ts b/src/tools/clarify-intent.ts index 32efa3a..f141269 100644 --- a/src/tools/clarify-intent.ts +++ b/src/tools/clarify-intent.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { run, getBranch, getStatus, getRecentCommits, getDiffFiles, getStagedFiles } from "../lib/git.js"; +import { run, shell, getBranch, getStatus, getRecentCommits, getDiffFiles, getStagedFiles } from "../lib/git.js"; import { findWorkspaceDocs, PROJECT_DIR } from "../lib/files.js"; import { searchSemantic } from "../lib/timeline-db.js"; import { getRelatedProjects } from "../lib/config.js"; @@ -152,10 +152,10 @@ export function registerClarifyIntent(server: McpServer): void { let hasTestFailures = false; if (!area || area.includes("test") || area.includes("fix") || area.includes("ui") || area.includes("api")) { - const typeErrors = run("pnpm tsc --noEmit 2>&1 | grep -c 'error TS' || echo '0'"); + const typeErrors = shell("pnpm tsc --noEmit 2>&1 | grep -c 'error TS' || echo '0'"); hasTypeErrors = parseInt(typeErrors, 10) > 0; - const testFiles = run("find tests -name '*.spec.ts' -maxdepth 4 2>/dev/null | head -20"); + const testFiles = shell("find tests -name '*.spec.ts' -maxdepth 4 2>/dev/null | head -20"); const failingTests = getTestFailures(); hasTestFailures = failingTests !== "all passing" && failingTests !== "no test report found"; diff --git a/src/tools/enrich-agent-task.ts b/src/tools/enrich-agent-task.ts index 236edfa..e0b75e0 100644 --- a/src/tools/enrich-agent-task.ts +++ b/src/tools/enrich-agent-task.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { run, getDiffFiles } from "../lib/git.js"; +import { run, shell, getDiffFiles } from "../lib/git.js"; import { PROJECT_DIR } from "../lib/files.js"; import { getConfig, type RelatedProject } from "../lib/config.js"; import { existsSync, readFileSync } from "fs"; @@ -29,12 +29,15 @@ function findAreaFiles(area: string): string { // If area looks like a path, search directly if (area.includes("/")) { - return run(`git ls-files -- '${safeArea}*' 2>/dev/null | head -20`); + const allFiles = run(["ls-files", "--", `${safeArea}*`]); + return allFiles.split("\n").filter(Boolean).slice(0, 20).join("\n"); } // Search for area keyword in git-tracked file paths - const files = run(`git ls-files 2>/dev/null | grep -i '${safeArea}' | head -20`); - if (files && !files.startsWith("[command failed")) return files; + const allFiles = run(["ls-files"]); + if (allFiles.startsWith("[")) return getDiffFiles("HEAD~3"); + const matched = allFiles.split("\n").filter(f => f.toLowerCase().includes(safeArea.toLowerCase())).slice(0, 20); + if (matched.length > 0) return matched.join("\n"); // Fallback to recently changed files return getDiffFiles("HEAD~3"); @@ -42,18 +45,28 @@ function findAreaFiles(area: string): string { /** Find related test files for an area */ function findRelatedTests(area: string): string { - if (!area) return run("git ls-files 2>/dev/null | grep -E '\\.(spec|test)\\.(ts|tsx|js|jsx)$' | head -10"); + const allFiles = run(["ls-files"]); + if (allFiles.startsWith("[")) return ""; + const testPattern = /\.(spec|test)\.(ts|tsx|js|jsx)$/; + const allTests = allFiles.split("\n").filter(f => testPattern.test(f)); - const safeArea = shellEscape(area.split(/\s+/)[0]); - const tests = run(`git ls-files 2>/dev/null | grep -E '\\.(spec|test)\\.(ts|tsx|js|jsx)$' | grep -i '${safeArea}' | head -10`); - return tests || run("git ls-files 2>/dev/null | grep -E '\\.(spec|test)\\.(ts|tsx|js|jsx)$' | head -10"); + if (!area) return allTests.slice(0, 10).join("\n"); + + const safeArea = shellEscape(area.split(/\s+/)[0]).toLowerCase(); + const areaTests = allTests.filter(f => f.toLowerCase().includes(safeArea)).slice(0, 10); + return areaTests.length > 0 ? areaTests.join("\n") : allTests.slice(0, 10).join("\n"); } /** Get an example pattern from the first matching file */ function getExamplePattern(files: string): string { const firstFile = files.split("\n").filter(Boolean)[0]; if (!firstFile) return "no pattern available"; - return run(`head -30 '${shellEscape(firstFile)}' 2>/dev/null || echo 'could not read file'`); + try { + const content = readFileSync(join(PROJECT_DIR, firstFile), "utf-8"); + return content.split("\n").slice(0, 30).join("\n"); + } catch { + return "could not read file"; + } } // --------------------------------------------------------------------------- diff --git a/src/tools/scope-work.ts b/src/tools/scope-work.ts index 9b5d971..96b7708 100644 --- a/src/tools/scope-work.ts +++ b/src/tools/scope-work.ts @@ -1,7 +1,7 @@ // CATEGORY 1: scope_work — Plans import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { run, getBranch, getRecentCommits, getStatus } from "../lib/git.js"; +import { run, shell, getBranch, getRecentCommits, getStatus } from "../lib/git.js"; import { readIfExists, findWorkspaceDocs, PROJECT_DIR } from "../lib/files.js"; import { searchSemantic } from "../lib/timeline-db.js"; import { getRelatedProjects } from "../lib/config.js"; @@ -93,9 +93,9 @@ export function registerScopeWork(server: McpServer): void { const timestamp = now(); const currentBranch = branch ?? getBranch(); const recentCommits = getRecentCommits(10); - const porcelain = run("git status --porcelain"); + const porcelain = run(["status", "--porcelain"]); const dirtyFiles = parsePortelainFiles(porcelain); - const diffStat = dirtyFiles.length > 0 ? run("git diff --stat") : "(clean working tree)"; + const diffStat = dirtyFiles.length > 0 ? run(["diff", "--stat"]) : "(clean working tree)"; // Scan for relevant files based on task keywords const keywords = task.toLowerCase().split(/\s+/); @@ -128,7 +128,7 @@ export function registerScopeWork(server: McpServer): void { .slice(0, 5); if (grepTerms.length > 0) { const pattern = shellEscape(grepTerms.join("|")); - matchedFiles = run(`git ls-files | head -500 | grep -iE '${pattern}' | head -30`); + matchedFiles = shell(`git ls-files | head -500 | grep -iE '${pattern}' | head -30`); } // Check which relevant dirs actually exist (with path traversal protection) diff --git a/src/tools/sequence-tasks.ts b/src/tools/sequence-tasks.ts index 22dea23..139356d 100644 --- a/src/tools/sequence-tasks.ts +++ b/src/tools/sequence-tasks.ts @@ -1,7 +1,7 @@ // CATEGORY 6: sequence_tasks — Sequencing import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { run } from "../lib/git.js"; +import { run, shell } from "../lib/git.js"; import { now } from "../lib/state.js"; import { PROJECT_DIR } from "../lib/files.js"; import { existsSync } from "fs"; @@ -90,7 +90,8 @@ export function registerSequenceTasks(server: McpServer): void { // For locality: infer directories from path-like tokens in task text if (strategy === "locality") { // Use git ls-files with a depth limit instead of find for performance - const gitFiles = run("git ls-files 2>/dev/null | head -1000"); + const allFiles = run(["ls-files"]); + const gitFiles = allFiles.startsWith("[") ? "" : allFiles.split("\n").slice(0, 1000).join("\n"); const knownDirs = new Set(); for (const f of gitFiles.split("\n").filter(Boolean)) { const parts = f.split("/"); diff --git a/src/tools/session-handoff.ts b/src/tools/session-handoff.ts index d199462..6d01428 100644 --- a/src/tools/session-handoff.ts +++ b/src/tools/session-handoff.ts @@ -2,13 +2,13 @@ import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { existsSync, readFileSync } from "fs"; import { join } from "path"; -import { run, getBranch, getRecentCommits, getStatus } from "../lib/git.js"; +import { run, shell, getBranch, getRecentCommits, getStatus } from "../lib/git.js"; import { readIfExists, findWorkspaceDocs } from "../lib/files.js"; import { STATE_DIR, now } from "../lib/state.js"; /** Check if a CLI tool is available */ function hasCommand(cmd: string): boolean { - const result = run(`command -v ${cmd} 2>/dev/null`); + const result = shell(`command -v ${cmd} 2>/dev/null`); return !!result && !result.startsWith("[command failed"); } @@ -44,7 +44,7 @@ export function registerSessionHandoff(server: McpServer): void { // Only try gh if it exists if (hasCommand("gh")) { - const openPRs = run("gh pr list --state open --json number,title,headRefName 2>/dev/null || echo '[]'"); + const openPRs = shell("gh pr list --state open --json number,title,headRefName 2>/dev/null || echo '[]'"); if (openPRs && openPRs !== "[]") { sections.push(`## Open PRs\n\`\`\`json\n${openPRs}\n\`\`\``); } diff --git a/src/tools/session-health.ts b/src/tools/session-health.ts index bd6a819..7f86ba1 100644 --- a/src/tools/session-health.ts +++ b/src/tools/session-health.ts @@ -1,6 +1,6 @@ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; -import { getBranch, getStatus, getLastCommit, getLastCommitTime, run } from "../lib/git.js"; +import { getBranch, getStatus, getLastCommit, getLastCommitTime, run, shell } from "../lib/git.js"; import { readIfExists, findWorkspaceDocs } from "../lib/files.js"; import { loadState, saveState } from "../lib/state.js"; import { getConfig } from "../lib/config.js"; @@ -27,7 +27,7 @@ export function registerSessionHealth(server: McpServer): void { const dirtyCount = dirty ? dirty.split("\n").filter(Boolean).length : 0; const lastCommit = getLastCommit(); const lastCommitTimeStr = getLastCommitTime(); - const uncommittedDiff = run("git diff --stat | tail -1"); + const uncommittedDiff = shell("git diff --stat | tail -1"); // Parse commit time safely const commitDate = parseGitDate(lastCommitTimeStr); diff --git a/src/tools/sharpen-followup.ts b/src/tools/sharpen-followup.ts index db5acaa..0065458 100644 --- a/src/tools/sharpen-followup.ts +++ b/src/tools/sharpen-followup.ts @@ -87,7 +87,7 @@ export function registerSharpenFollowup(server: McpServer): void { // Gather context to resolve ambiguity const contextFiles: string[] = [...(previous_files ?? [])]; const recentChanged = getRecentChangedFiles(); - const porcelainOutput = run("git status --porcelain 2>/dev/null"); + const porcelainOutput = run(["status", "--porcelain"]); const untrackedOrModified = parsePortelainFiles(porcelainOutput); const allKnownFiles = [...new Set([...contextFiles, ...recentChanged, ...untrackedOrModified])].filter(Boolean); diff --git a/src/tools/token-audit.ts b/src/tools/token-audit.ts index b7aad2c..a7f1bbb 100644 --- a/src/tools/token-audit.ts +++ b/src/tools/token-audit.ts @@ -1,7 +1,7 @@ // CATEGORY 5: token_audit — Token Efficiency import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { run } from "../lib/git.js"; +import { run, shell } from "../lib/git.js"; import { readIfExists, findWorkspaceDocs, PROJECT_DIR } from "../lib/files.js"; import { loadState, saveState, now, STATE_DIR } from "../lib/state.js"; import { readFileSync, existsSync, statSync } from "fs"; @@ -39,8 +39,8 @@ export function registerTokenAudit(server: McpServer): void { let wasteScore = 0; // 1. Git diff size & dirty file count - const diffStat = run("git diff --stat --no-color 2>/dev/null"); - const dirtyFiles = run("git diff --name-only 2>/dev/null"); + const diffStat = run(["diff", "--stat", "--no-color"]); + const dirtyFiles = run(["diff", "--name-only"]); const dirtyList = dirtyFiles.split("\n").filter(Boolean); const dirtyCount = dirtyList.length; @@ -62,9 +62,12 @@ export function registerTokenAudit(server: McpServer): void { const largeFiles: string[] = []; for (const f of dirtyList.slice(0, 30)) { - // Use shell-safe quoting instead of interpolation - const wc = run(`wc -l < '${shellEscape(f)}' 2>/dev/null`); - const lines = parseInt(wc) || 0; + let lines = 0; + try { + const filePath = join(PROJECT_DIR, f); + const content = readFileSync(filePath, "utf-8"); + lines = content.split("\n").length; + } catch { /* file may not exist or be binary */ } estimatedContextTokens += lines * AVG_LINE_BYTES * AVG_TOKENS_PER_BYTE; if (lines > 500) { largeFiles.push(`${f} (${lines} lines)`); @@ -80,8 +83,10 @@ export function registerTokenAudit(server: McpServer): void { // 3. CLAUDE.md bloat check const claudeMd = readIfExists("CLAUDE.md", 1); if (claudeMd !== null) { - const stat = run(`wc -c < '${shellEscape("CLAUDE.md")}' 2>/dev/null`); - const bytes = parseInt(stat) || 0; + let bytes = 0; + try { + bytes = statSync(join(PROJECT_DIR, "CLAUDE.md")).size; + } catch { /* ignore */ } if (bytes > 5120) { patterns.push(`CLAUDE.md is ${(bytes / 1024).toFixed(1)}KB — injected every session, burns tokens on paste`); recommendations.push("Trim CLAUDE.md to essentials (<5KB). Move reference docs to files read on-demand"); @@ -139,7 +144,7 @@ export function registerTokenAudit(server: McpServer): void { // Read with size cap: take the tail if too large const raw = stat.size <= MAX_TOOL_LOG_BYTES ? readFileSync(toolLogPath, "utf-8") - : run(`tail -c ${MAX_TOOL_LOG_BYTES} '${shellEscape(toolLogPath)}'`); + : shell(`tail -c ${MAX_TOOL_LOG_BYTES} '${shellEscape(toolLogPath)}'`); const lines = raw.trim().split("\n").filter(Boolean); totalToolCalls = lines.length; diff --git a/src/tools/verify-completion.ts b/src/tools/verify-completion.ts index 732532f..2b010b7 100644 --- a/src/tools/verify-completion.ts +++ b/src/tools/verify-completion.ts @@ -1,8 +1,8 @@ import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { run, getStatus } from "../lib/git.js"; +import { run, shell, getStatus } from "../lib/git.js"; import { PROJECT_DIR } from "../lib/files.js"; -import { existsSync } from "fs"; +import { existsSync, readFileSync } from "fs"; import { join } from "path"; /** Detect package manager from lockfiles */ @@ -34,7 +34,8 @@ function detectTestRunner(): string | null { /** Check if a build script exists in package.json */ function hasBuildScript(): boolean { try { - const pkg = JSON.parse(run("cat package.json 2>/dev/null")); + const pkgPath = join(PROJECT_DIR, "package.json"); + const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); return !!pkg?.scripts?.build; } catch { return false; } } @@ -55,7 +56,7 @@ export function registerVerifyCompletion(server: McpServer): void { const checks: { name: string; passed: boolean; detail: string }[] = []; // 1. Type check (single invocation, extract both result and count) - const tscOutput = run(`${pm === "npx" ? "npx" : pm} tsc --noEmit 2>&1 | tail -20`); + const tscOutput = shell(`${pm === "npx" ? "npx" : pm} tsc --noEmit 2>&1 | tail -20`); const errorLines = tscOutput.split("\n").filter(l => /error TS\d+/.test(l)); const typePassed = errorLines.length === 0; checks.push({ @@ -80,7 +81,7 @@ export function registerVerifyCompletion(server: McpServer): void { // 3. Tests if (!skip_tests) { const runner = detectTestRunner(); - const changedFiles = run("git diff --name-only HEAD~1 2>/dev/null").split("\n").filter(Boolean); + const changedFiles = run(["diff", "--name-only", "HEAD~1"]).split("\n").filter(Boolean); let testCmd = ""; if (runner === "playwright") { @@ -112,7 +113,7 @@ export function registerVerifyCompletion(server: McpServer): void { } if (testCmd) { - const testResult = run(testCmd, { timeout: 120000 }); + const testResult = shell(testCmd, { timeout: 120000 }); const testPassed = /pass/i.test(testResult) && !/fail/i.test(testResult); checks.push({ name: "Tests", @@ -130,7 +131,7 @@ export function registerVerifyCompletion(server: McpServer): void { // 4. Build check (only if build script exists and not skipped) if (!skip_build && hasBuildScript()) { - const buildCheck = run(`${pm === "npx" ? "npm run" : pm} build 2>&1 | tail -10`, { timeout: 60000 }); + const buildCheck = shell(`${pm === "npx" ? "npm run" : pm} build 2>&1 | tail -10`, { timeout: 60000 }); const buildPassed = !/\b[Ee]rror\b/.test(buildCheck) || /Successfully compiled/.test(buildCheck); checks.push({ name: "Build", diff --git a/src/tools/what-changed.ts b/src/tools/what-changed.ts index 913dfa2..ea9f96e 100644 --- a/src/tools/what-changed.ts +++ b/src/tools/what-changed.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { run, getBranch, getDiffStat } from "../lib/git.js"; +import { run, shell, getBranch, getDiffStat } from "../lib/git.js"; export function registerWhatChanged(server: McpServer): void { server.tool( @@ -12,8 +12,10 @@ export function registerWhatChanged(server: McpServer): void { async ({ since }) => { const ref = since || "HEAD~5"; const diffStat = getDiffStat(ref); - const diffFiles = run(`git diff ${ref} --name-only 2>/dev/null || git diff HEAD~3 --name-only`); - const log = run(`git log ${ref}..HEAD --oneline 2>/dev/null || git log -5 --oneline`); + let diffFiles = run(["diff", ref, "--name-only"]); + if (diffFiles.startsWith("[")) diffFiles = run(["diff", "HEAD~3", "--name-only"]); + let log = run(["log", `${ref}..HEAD`, "--oneline"]); + if (log.startsWith("[")) log = run(["log", "-5", "--oneline"]); const branch = getBranch(); const fileList = diffFiles.split("\n").filter(Boolean);