From d5f0d016aff065ead0401b10d35b29cd29378168 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Wed, 18 Mar 2026 13:41:13 -0700 Subject: [PATCH 1/3] docs: add example CLAUDE.md for automatic preflight integration Adds a ready-to-use CLAUDE.md template that makes Claude Code automatically run preflight_check on prompts. Users can copy it into their project to get preflight working without manual tool calls. Referenced from Quick Start in README and examples/README. --- README.md | 10 ++++++++++ examples/CLAUDE.md | 30 ++++++++++++++++++++++++++++++ examples/README.md | 16 ++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 examples/CLAUDE.md diff --git a/README.md b/README.md index 6d03f5d..7969d58 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,16 @@ claude mcp add preflight -- preflight-dev-serve > **Note:** `preflight-dev` runs the interactive setup wizard. `preflight-dev-serve` starts the MCP server — that's what you want in your Claude Code config. +### Make Claude use preflight automatically + +Add preflight rules to your project's `CLAUDE.md` so Claude runs `preflight_check` on every prompt without you asking: + +```bash +cp /path/to/preflight/examples/CLAUDE.md your-project/CLAUDE.md +``` + +See [`examples/CLAUDE.md`](examples/CLAUDE.md) for a ready-to-use template with recommended rules for when to preflight, session hygiene, and skip-lists. + --- ## How It Works diff --git a/examples/CLAUDE.md b/examples/CLAUDE.md new file mode 100644 index 0000000..da55f40 --- /dev/null +++ b/examples/CLAUDE.md @@ -0,0 +1,30 @@ +# CLAUDE.md — Preflight Integration Example +# +# Drop this into your project's CLAUDE.md (or .claude/CLAUDE.md) to make +# Claude Code automatically use preflight tools during your sessions. +# Customize the rules below to match your workflow. + +## Preflight Rules + +Before starting any non-trivial task, run `preflight_check` with my prompt. This catches vague instructions before they waste tokens on wrong→fix cycles. + +### When to use preflight tools: + +- **Every prompt**: `preflight_check` triages automatically — let it decide what's needed +- **Before multi-file changes**: Run `scope_work` to get a phased plan +- **Before sub-agent tasks**: Use `enrich_agent_task` to add context +- **After making a mistake**: Use `log_correction` so preflight learns the pattern +- **Before ending a session**: Run `checkpoint` to save state for next time +- **When I say "fix it" or "do the others"**: Use `sharpen_followup` to resolve what I actually mean + +### Session hygiene: + +- Run `check_session_health` if we've been going for a while without committing +- If I ask about something we did before, use `search_history` to find it +- Before declaring a task done, run `verify_completion` (type check + tests) + +### Don't preflight these: + +- Simple git commands (commit, push, status) +- Formatting / linting +- Reading files I explicitly named diff --git a/examples/README.md b/examples/README.md index 778f15d..f2fafc1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,6 +12,22 @@ The `.preflight/` directory contains example configuration files you can copy in └── api.yml # Manual contract definitions for cross-service types ``` +## `CLAUDE.md` Integration + +The `CLAUDE.md` file tells Claude Code how to behave in your project. Adding preflight rules here makes Claude automatically use preflight tools without you having to ask. + +```bash +# Copy the example into your project: +cp /path/to/preflight/examples/CLAUDE.md my-project/CLAUDE.md + +# Or append to your existing CLAUDE.md: +cat /path/to/preflight/examples/CLAUDE.md >> my-project/CLAUDE.md +``` + +This is the **recommended way** to integrate preflight — once it's in your `CLAUDE.md`, every session automatically runs `preflight_check` on your prompts. + +--- + ### Quick setup ```bash From c17f46344bf005464e2bd65b1f1631852a51e069 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Thu, 19 Mar 2026 08:20:24 -0700 Subject: [PATCH 2/3] feat(cli): add --help and --version flags, fix Node badge to 20+ - CLI now responds to --help/-h with usage info, profiles, and links - CLI now responds to --version/-v with package version - Previously, any flag just launched the interactive wizard - Fixed README badge from Node 18+ to Node 20+ (matches engines field) --- README.md | 2 +- src/cli/init.ts | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7969d58..e7a385f 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A 24-tool MCP server for Claude Code that catches ambiguous instructions before [![MCP](https://img.shields.io/badge/MCP-Compatible-blueviolet)](https://modelcontextprotocol.io/) [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) [![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/) +[![Node 20+](https://img.shields.io/badge/node-20%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) diff --git a/src/cli/init.ts b/src/cli/init.ts index dfaaa25..d1b0021 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -9,6 +9,46 @@ import { join, dirname } from "node:path"; import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; +// Handle --help and --version before launching interactive wizard +const args = process.argv.slice(2); + +if (args.includes("--help") || args.includes("-h")) { + console.log(` +✈️ preflight-dev — MCP server for Claude Code prompt discipline + +Usage: + preflight-dev Interactive setup wizard (creates .mcp.json) + preflight-dev --help Show this help message + preflight-dev --version Show version + +The wizard will: + 1. Ask you to choose a profile (minimal / standard / full) + 2. Optionally create a .preflight/ config directory + 3. Write an .mcp.json so Claude Code auto-connects to preflight + +After setup, restart Claude Code and preflight tools will appear. + +Profiles: + minimal 4 tools — clarify_intent, check_session_health, session_stats, prompt_score + standard 16 tools — all prompt discipline + session_stats + prompt_score + full 20 tools — everything + timeline/vector search (needs LanceDB) + +More info: https://github.com/TerminalGravity/preflight +`); + process.exit(0); +} + +if (args.includes("--version") || args.includes("-v")) { + const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "../../package.json"); + try { + const pkg = JSON.parse(await readFile(pkgPath, "utf-8")); + console.log(`preflight-dev v${pkg.version}`); + } catch { + console.log("preflight-dev (version unknown)"); + } + process.exit(0); +} + const rl = createInterface({ input: process.stdin, output: process.stdout }); function ask(question: string): Promise { From 52c3b1d5334f7ec7b89fc974187c3ba6f5a8d8a8 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Thu, 19 Mar 2026 13:17:44 -0700 Subject: [PATCH 3/3] test: add 29 unit tests for session-parser helpers Export and test 5 pure helper functions from session-parser.ts: - extractText: content block extraction (6 tests) - extractToolUseBlocks: tool_use filtering (3 tests) - normalizeTimestamp: ISO/epoch/fallback handling (7 tests) - preview: text truncation (5 tests) - isCorrection: correction pattern detection (8 tests) Increases total test count from 43 to 72. --- src/lib/session-parser.ts | 10 +- tests/lib/session-parser.test.ts | 182 +++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 tests/lib/session-parser.test.ts diff --git a/src/lib/session-parser.ts b/src/lib/session-parser.ts index 76bc02f..9a19c7b 100644 --- a/src/lib/session-parser.ts +++ b/src/lib/session-parser.ts @@ -36,7 +36,7 @@ const CORRECTION_PATTERNS = [ // ── Helpers ──────────────────────────────────────────────────────────────── -function extractText(content: unknown): string { +export function extractText(content: unknown): string { if (typeof content === "string") return content; if (Array.isArray(content)) { return content @@ -47,12 +47,12 @@ function extractText(content: unknown): string { return ""; } -function extractToolUseBlocks(content: unknown): any[] { +export function extractToolUseBlocks(content: unknown): any[] { if (!Array.isArray(content)) return []; return content.filter((b: any) => b.type === "tool_use"); } -function normalizeTimestamp(ts: unknown, fallback: string): string { +export function normalizeTimestamp(ts: unknown, fallback: string): string { if (!ts) return fallback; if (typeof ts === "string") { const d = new Date(ts); @@ -66,12 +66,12 @@ function normalizeTimestamp(ts: unknown, fallback: string): string { return fallback; } -function preview(text: string, max = 120): string { +export function preview(text: string, max = 120): string { const line = text.split("\n")[0] ?? ""; return line.length > max ? line.slice(0, max) + "…" : line; } -function isCorrection(text: string): boolean { +export function isCorrection(text: string): boolean { return CORRECTION_PATTERNS.some((p) => p.test(text)); } diff --git a/tests/lib/session-parser.test.ts b/tests/lib/session-parser.test.ts new file mode 100644 index 0000000..f142c10 --- /dev/null +++ b/tests/lib/session-parser.test.ts @@ -0,0 +1,182 @@ +import { describe, it, expect } from "vitest"; +import { + extractText, + extractToolUseBlocks, + normalizeTimestamp, + preview, + isCorrection, +} from "../../src/lib/session-parser.js"; + +// ── extractText ──────────────────────────────────────────────────────────── + +describe("extractText", () => { + it("returns string content as-is", () => { + expect(extractText("hello world")).toBe("hello world"); + }); + + it("extracts text from content blocks array", () => { + const blocks = [ + { type: "text", text: "first" }, + { type: "tool_use", name: "bash", input: {} }, + { type: "text", text: "second" }, + ]; + expect(extractText(blocks)).toBe("first\nsecond"); + }); + + it("returns empty string for null/undefined", () => { + expect(extractText(null)).toBe(""); + expect(extractText(undefined)).toBe(""); + }); + + it("returns empty string for non-text arrays", () => { + expect(extractText([{ type: "tool_use", name: "bash" }])).toBe(""); + }); + + it("returns empty string for numbers/objects", () => { + expect(extractText(42)).toBe(""); + expect(extractText({ foo: "bar" })).toBe(""); + }); + + it("skips blocks with non-string text field", () => { + const blocks = [ + { type: "text", text: 123 }, + { type: "text", text: "valid" }, + ]; + expect(extractText(blocks)).toBe("valid"); + }); +}); + +// ── extractToolUseBlocks ─────────────────────────────────────────────────── + +describe("extractToolUseBlocks", () => { + it("returns tool_use blocks from content array", () => { + const blocks = [ + { type: "text", text: "hello" }, + { type: "tool_use", name: "bash", input: { cmd: "ls" } }, + { type: "tool_use", name: "read", input: { path: "." } }, + ]; + const result = extractToolUseBlocks(blocks); + expect(result).toHaveLength(2); + expect(result[0].name).toBe("bash"); + expect(result[1].name).toBe("read"); + }); + + it("returns empty array for non-array input", () => { + expect(extractToolUseBlocks("hello")).toEqual([]); + expect(extractToolUseBlocks(null)).toEqual([]); + expect(extractToolUseBlocks(undefined)).toEqual([]); + expect(extractToolUseBlocks(42)).toEqual([]); + }); + + it("returns empty array when no tool_use blocks", () => { + expect(extractToolUseBlocks([{ type: "text", text: "hi" }])).toEqual([]); + }); +}); + +// ── normalizeTimestamp ───────────────────────────────────────────────────── + +describe("normalizeTimestamp", () => { + const fallback = "2025-01-01T00:00:00.000Z"; + + it("returns fallback for null/undefined/empty", () => { + expect(normalizeTimestamp(null, fallback)).toBe(fallback); + expect(normalizeTimestamp(undefined, fallback)).toBe(fallback); + expect(normalizeTimestamp("", fallback)).toBe(fallback); + expect(normalizeTimestamp(0, fallback)).toBe(fallback); + }); + + it("parses valid ISO string", () => { + const ts = "2025-06-15T10:30:00.000Z"; + expect(normalizeTimestamp(ts, fallback)).toBe(ts); + }); + + it("returns fallback for invalid string", () => { + expect(normalizeTimestamp("not-a-date", fallback)).toBe(fallback); + }); + + it("handles epoch seconds", () => { + // 1700000000 = 2023-11-14T22:13:20.000Z + const result = normalizeTimestamp(1700000000, fallback); + expect(result).toBe("2023-11-14T22:13:20.000Z"); + }); + + it("handles epoch milliseconds", () => { + const result = normalizeTimestamp(1700000000000, fallback); + expect(result).toBe("2023-11-14T22:13:20.000Z"); + }); + + it("returns fallback for non-string/non-number types", () => { + expect(normalizeTimestamp({ time: 123 }, fallback)).toBe(fallback); + expect(normalizeTimestamp(true, fallback)).toBe(fallback); + }); +}); + +// ── preview ──────────────────────────────────────────────────────────────── + +describe("preview", () => { + it("returns short text unchanged", () => { + expect(preview("hello")).toBe("hello"); + }); + + it("truncates long first line with ellipsis", () => { + const long = "x".repeat(200); + const result = preview(long); + expect(result).toHaveLength(121); // 120 + "…" + expect(result.endsWith("…")).toBe(true); + }); + + it("only uses the first line", () => { + expect(preview("first line\nsecond line\nthird")).toBe("first line"); + }); + + it("respects custom max length", () => { + const result = preview("abcdefghij", 5); + expect(result).toBe("abcde…"); + }); + + it("handles empty string", () => { + expect(preview("")).toBe(""); + }); +}); + +// ── isCorrection ─────────────────────────────────────────────────────────── + +describe("isCorrection", () => { + it("detects 'no' as correction", () => { + expect(isCorrection("No, that's wrong")).toBe(true); + }); + + it("detects 'wrong'", () => { + expect(isCorrection("That's wrong")).toBe(true); + }); + + it("detects 'not that'", () => { + expect(isCorrection("not that file")).toBe(true); + }); + + it("detects 'i meant'", () => { + expect(isCorrection("I meant the other one")).toBe(true); + }); + + it("detects 'actually'", () => { + expect(isCorrection("Actually, use TypeScript")).toBe(true); + }); + + it("detects 'instead'", () => { + expect(isCorrection("Use vitest instead")).toBe(true); + }); + + it("detects 'undo'", () => { + expect(isCorrection("Please undo that change")).toBe(true); + }); + + it("returns false for normal prompts", () => { + expect(isCorrection("Add a new function to parse dates")).toBe(false); + expect(isCorrection("How does the config system work?")).toBe(false); + }); + + it("is case insensitive", () => { + expect(isCorrection("WRONG approach")).toBe(true); + expect(isCorrection("ACTUALLY nevermind")).toBe(true); + }); +});