From d5f0d016aff065ead0401b10d35b29cd29378168 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Wed, 18 Mar 2026 13:41:13 -0700 Subject: [PATCH 1/4] 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/4] 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 b28d77c0dee19e6845fcbc84d2f4f72c1ce1c9b2 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Thu, 19 Mar 2026 14:21:08 -0700 Subject: [PATCH 3/4] docs: add TROUBLESHOOTING.md with common setup and config fixes --- README.md | 10 +++ TROUBLESHOOTING.md | 165 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 TROUBLESHOOTING.md diff --git a/README.md b/README.md index e7a385f..cdd5a58 100644 --- a/README.md +++ b/README.md @@ -744,6 +744,16 @@ curl http://localhost:11434/api/embed -d '{"model":"all-minilm","input":"test"}' --- +## Troubleshooting + +Having issues? Check **[TROUBLESHOOTING.md](TROUBLESHOOTING.md)** for solutions to common problems including: +- LanceDB setup and platform issues +- Embedding provider configuration +- `.preflight/` config parsing errors +- Profile selection guide + +--- + ## Contributing This project is young and there's plenty to do. Check the [issues](https://github.com/TerminalGravity/preflight/issues) — several are tagged `good first issue`. diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..77171fc --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,165 @@ +# Troubleshooting + +Common issues and fixes for preflight. + +--- + +## Installation & Setup + +### `npx preflight-dev-serve` fails with module errors + +**Symptoms:** `ERR_MODULE_NOT_FOUND` or `Cannot find module` when running via npx. + +**Fix:** Preflight requires **Node 20+**. Check your version: +```bash +node --version +``` +If you're on Node 18 or below, upgrade via [nvm](https://github.com/nvm-sh/nvm): +```bash +nvm install 20 +nvm use 20 +``` + +### Tools don't appear in Claude Code after `claude mcp add` + +**Fix:** Restart Claude Code completely after adding the MCP server. The tool list is loaded at startup. + +If tools still don't appear, verify the server starts without errors: +```bash +npx preflight-dev-serve +``` +You should see MCP protocol output (JSON on stdout). If you see errors, check the sections below. + +--- + +## LanceDB & Timeline Search + +### `Error: Failed to open LanceDB` or LanceDB crashes on startup + +**Symptoms:** Timeline tools (`search_timeline`, `index_sessions`, etc.) fail. Other tools work fine. + +**Cause:** LanceDB uses native binaries that may not be available for your platform, or the database directory has permission issues. + +**Fixes:** +1. Make sure `~/.preflight/projects/` is writable: + ```bash + mkdir -p ~/.preflight/projects + ls -la ~/.preflight/ + ``` +2. If on an unsupported platform, use the **minimal** or **standard** profile (no LanceDB required): + ```bash + npx preflight-dev + # Choose "minimal" or "standard" when prompted + ``` +3. Clear corrupted LanceDB data: + ```bash + rm -rf ~/.preflight/projects/*/timeline.lance + ``` + Then re-index with `index_sessions`. + +### Timeline search returns no results + +**Cause:** Sessions haven't been indexed yet. Preflight doesn't auto-index — you need to run `index_sessions` first. + +**Fix:** In Claude Code, run: +``` +index my sessions +``` +Or use the `index_sessions` tool directly. Indexing reads your `~/.claude/projects/` session files. + +--- + +## Embeddings + +### `OpenAI API key required for openai embedding provider` + +**Cause:** You selected OpenAI embeddings but didn't set the API key. + +**Fixes:** + +Option A — Set the environment variable when adding the MCP server: +```bash +claude mcp add preflight \ + -e OPENAI_API_KEY=sk-... \ + -- npx -y preflight-dev-serve +``` + +Option B — Switch to local embeddings (no API key needed). Create or edit `~/.preflight/config.json`: +```json +{ + "embedding_provider": "local", + "embedding_model": "Xenova/all-MiniLM-L6-v2" +} +``` + +### Local embeddings are slow on first run + +**Expected.** The model (~80MB) downloads on first use and is cached afterward. Subsequent runs are fast. + +--- + +## `.preflight/` Config + +### `warning - failed to parse .preflight/config.yml` + +**Cause:** YAML syntax error in your project's `.preflight/config.yml`. + +**Fix:** Validate your YAML: +```bash +npx yaml-lint .preflight/config.yml +``` +Or check for common issues: wrong indentation, tabs instead of spaces, missing colons. + +### Config changes not taking effect + +**Cause:** Preflight reads config at MCP server startup, not on every tool call. + +**Fix:** Restart Claude Code after editing `.preflight/config.yml` or `.preflight/triage.yml`. + +--- + +## Profiles + +### Which profile should I use? + +| Profile | Tools | Best for | +|---------|-------|----------| +| **minimal** | 4 | Try it out, low overhead | +| **standard** | 16 | Daily use, no vector search needed | +| **full** | 20 | Power users who want timeline search | + +You can change profiles by re-running the setup wizard: +```bash +npx preflight-dev +``` + +--- + +## Platform-Specific + +### Apple Silicon (M1/M2/M3/M4): LanceDB build fails + +LanceDB ships prebuilt binaries for Apple Silicon. If `npm install` fails on the native module: +```bash +# Ensure you're using the ARM64 version of Node +node -p process.arch # should print "arm64" + +# If it prints "x64", reinstall Node natively (not via Rosetta) +``` + +### Linux: Permission denied on `~/.preflight/` + +```bash +chmod -R u+rwX ~/.preflight/ +``` + +--- + +## Still stuck? + +1. Check [open issues](https://github.com/TerminalGravity/preflight/issues) — someone may have hit the same problem +2. [Open a new issue](https://github.com/TerminalGravity/preflight/issues/new) with: + - Your Node version (`node --version`) + - Your OS and architecture (`uname -a`) + - The full error message + - Which profile you selected From f5c77b71bfe1ff35e9a5388c9602dbf9c0ce2ed3 Mon Sep 17 00:00:00 2001 From: Jack Felke Date: Thu, 19 Mar 2026 15:35:54 -0700 Subject: [PATCH 4/4] feat: add export_timeline tool for markdown session reports (#5) Adds a new export_timeline MCP tool that generates markdown reports from timeline data. Features: - Summary stats table (prompts, commits, tool calls, corrections, errors) - Prompt quality rating based on correction rate - Two formats: detailed (full event log) or summary (daily counts table) - Relative date support (7days, 1week, etc.) - Scope/project/branch/author filtering Closes #5 --- src/index.ts | 2 + src/tools/export-timeline.ts | 335 +++++++++++++++++++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 src/tools/export-timeline.ts diff --git a/src/index.ts b/src/index.ts index e7e9d00..c2a525a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,6 +49,7 @@ import { registerScanSessions } from "./tools/scan-sessions.js"; import { registerGenerateScorecard } from "./tools/generate-scorecard.js"; import { registerSearchContracts } from "./tools/search-contracts.js"; import { registerEstimateCost } from "./tools/estimate-cost.js"; +import { registerExportTimeline } from "./tools/export-timeline.js"; // Validate related projects from config function validateRelatedProjects(): void { @@ -110,6 +111,7 @@ const toolRegistry: Array<[string, RegisterFn]> = [ ["generate_scorecard", registerGenerateScorecard], ["estimate_cost", registerEstimateCost], ["search_contracts", registerSearchContracts], + ["export_timeline", registerExportTimeline], ]; let registered = 0; diff --git a/src/tools/export-timeline.ts b/src/tools/export-timeline.ts new file mode 100644 index 0000000..dba2ef3 --- /dev/null +++ b/src/tools/export-timeline.ts @@ -0,0 +1,335 @@ +import { z } from "zod"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { getTimeline, listIndexedProjects } from "../lib/timeline-db.js"; +import { getRelatedProjects } from "../lib/config.js"; +import type { SearchScope } from "../types.js"; + +const RELATIVE_DATE_RE = /^(\d+)(days?|weeks?|months?|years?)$/; + +function parseRelativeDate(input: string): string { + const match = input.match(RELATIVE_DATE_RE); + if (!match) return input; + const [, numStr, unit] = match; + const num = parseInt(numStr, 10); + const d = new Date(); + if (unit.startsWith("day")) d.setDate(d.getDate() - num); + else if (unit.startsWith("week")) d.setDate(d.getDate() - num * 7); + else if (unit.startsWith("month")) d.setMonth(d.getMonth() - num); + else if (unit.startsWith("year")) d.setFullYear(d.getFullYear() - num); + return d.toISOString(); +} + +const TYPE_ICONS: Record = { + prompt: "💬", + assistant: "🤖", + tool_call: "🔧", + correction: "❌", + commit: "📦", + compaction: "🗜️", + sub_agent_spawn: "🚀", + error: "⚠️", +}; + +async function getSearchProjects(scope: SearchScope): Promise { + const currentProject = process.env.CLAUDE_PROJECT_DIR; + switch (scope) { + case "current": + return currentProject ? [currentProject] : []; + case "related": { + const related = getRelatedProjects(); + return currentProject ? [currentProject, ...related] : related; + } + case "all": { + const projects = await listIndexedProjects(); + return projects.map((p) => p.project); + } + default: + return currentProject ? [currentProject] : []; + } +} + +interface GroupedEvents { + days: Map; + totalEvents: number; + dateRange: string; +} + +async function fetchAndGroup(params: { + scope: "current" | "related" | "all"; + project?: string; + branch?: string; + author?: string; + since?: string; + until?: string; + type?: string; +}): Promise { + const since = params.since ? parseRelativeDate(params.since) : undefined; + const until = params.until ? parseRelativeDate(params.until) : undefined; + + let projectDirs: string[]; + if (params.project) { + projectDirs = [params.project]; + } else { + projectDirs = await getSearchProjects(params.scope); + } + + let events = await getTimeline({ + project_dirs: projectDirs, + project: undefined, + branch: params.branch, + since, + until, + type: params.type === "all" || !params.type ? undefined : (params.type as any), + limit: 10000, + offset: 0, + }); + + if (params.author) { + const authorLower = params.author.toLowerCase(); + events = events.filter((e: any) => { + if (e.type !== "commit") return true; + try { + const meta = JSON.parse(e.metadata || "{}"); + return (meta.author || "").toLowerCase().includes(authorLower); + } catch { + return true; + } + }); + } + + const days = new Map(); + for (const event of events) { + const day = event.timestamp + ? new Date(event.timestamp).toISOString().slice(0, 10) + : "unknown"; + if (!days.has(day)) days.set(day, []); + days.get(day)!.push(event); + } + + const sortedDays = [...days.keys()].sort().reverse(); + const dateRange = + sortedDays.length > 1 + ? `${sortedDays[sortedDays.length - 1]} to ${sortedDays[0]}` + : sortedDays[0] || "no data"; + + return { days, totalEvents: events.length, dateRange }; +} + +function computeStats(days: Map) { + let totalPrompts = 0; + let totalAssistant = 0; + let totalToolCalls = 0; + let totalCommits = 0; + let totalCorrections = 0; + let totalErrors = 0; + let totalCompactions = 0; + let totalSubAgents = 0; + + for (const events of days.values()) { + for (const e of events) { + switch (e.type) { + case "prompt": totalPrompts++; break; + case "assistant": totalAssistant++; break; + case "tool_call": totalToolCalls++; break; + case "commit": totalCommits++; break; + case "correction": totalCorrections++; break; + case "error": totalErrors++; break; + case "compaction": totalCompactions++; break; + case "sub_agent_spawn": totalSubAgents++; break; + } + } + } + + return { + totalPrompts, + totalAssistant, + totalToolCalls, + totalCommits, + totalCorrections, + totalErrors, + totalCompactions, + totalSubAgents, + }; +} + +function renderMarkdown( + grouped: GroupedEvents, + projectLabel: string, + branch?: string, + format: "detailed" | "summary" = "detailed" +): string { + const { days, totalEvents, dateRange } = grouped; + const stats = computeStats(days); + const sortedDays = [...days.keys()].sort().reverse(); + const branchStr = branch ? ` (${branch})` : ""; + const generated = new Date().toISOString().slice(0, 19).replace("T", " "); + + const lines: string[] = [ + `# Session Report: ${projectLabel}${branchStr}`, + "", + `**Period:** ${dateRange} `, + `**Generated:** ${generated} `, + `**Total Events:** ${totalEvents}`, + "", + "## Summary", + "", + `| Metric | Count |`, + `|--------|-------|`, + `| Prompts | ${stats.totalPrompts} |`, + `| Responses | ${stats.totalAssistant} |`, + `| Tool Calls | ${stats.totalToolCalls} |`, + `| Commits | ${stats.totalCommits} |`, + `| Corrections | ${stats.totalCorrections} |`, + `| Errors | ${stats.totalErrors} |`, + `| Compactions | ${stats.totalCompactions} |`, + `| Sub-agents | ${stats.totalSubAgents} |`, + "", + ]; + + // Prompt quality indicator + if (stats.totalPrompts > 0) { + const correctionRate = stats.totalCorrections / stats.totalPrompts; + const quality = + correctionRate < 0.05 ? "🟢 Excellent" : + correctionRate < 0.15 ? "🟡 Good" : + correctionRate < 0.3 ? "🟠 Needs Improvement" : + "🔴 Poor"; + lines.push( + "## Prompt Quality", + "", + `- **Correction Rate:** ${(correctionRate * 100).toFixed(1)}% (${stats.totalCorrections}/${stats.totalPrompts})`, + `- **Quality Rating:** ${quality}`, + "", + ); + } + + // Daily breakdown + if (format === "detailed") { + lines.push("## Daily Breakdown", ""); + + for (const day of sortedDays) { + const dayEvents = days.get(day)!; + dayEvents.sort((a: any, b: any) => { + const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0; + const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0; + return ta - tb; + }); + + const dayStats = computeStats(new Map([["day", dayEvents]])); + lines.push( + `### ${day} (${dayEvents.length} events)`, + "", + ); + + // Day summary line + const parts: string[] = []; + if (dayStats.totalPrompts) parts.push(`${dayStats.totalPrompts} prompts`); + if (dayStats.totalCommits) parts.push(`${dayStats.totalCommits} commits`); + if (dayStats.totalToolCalls) parts.push(`${dayStats.totalToolCalls} tool calls`); + if (dayStats.totalCorrections) parts.push(`${dayStats.totalCorrections} corrections`); + if (dayStats.totalErrors) parts.push(`${dayStats.totalErrors} errors`); + if (parts.length) lines.push(`> ${parts.join(" · ")}`, ""); + + for (const event of dayEvents) { + const time = event.timestamp + ? new Date(event.timestamp).toISOString().slice(11, 16) + : "??:??"; + const icon = TYPE_ICONS[event.type] || "❓"; + let content = (event.content || event.summary || "") + .slice(0, 200) + .replace(/\n/g, " "); + + if (event.type === "commit") { + const hash = event.commit_hash ? event.commit_hash.slice(0, 7) : ""; + content = `\`${hash}\` ${content}`; + } else if (event.type === "tool_call") { + const tool = event.tool_name || ""; + const target = content ? ` → ${content}` : ""; + content = `\`${tool}\`${target}`; + } + + lines.push(`- ${time} ${icon} ${content}`); + } + lines.push(""); + } + } else { + // Summary mode: just daily counts + lines.push("## Daily Activity", ""); + lines.push("| Date | Prompts | Commits | Tools | Corrections | Errors |"); + lines.push("|------|---------|---------|-------|-------------|--------|"); + + for (const day of sortedDays) { + const dayEvents = days.get(day)!; + const ds = computeStats(new Map([["day", dayEvents]])); + lines.push( + `| ${day} | ${ds.totalPrompts} | ${ds.totalCommits} | ${ds.totalToolCalls} | ${ds.totalCorrections} | ${ds.totalErrors} |` + ); + } + lines.push(""); + } + + lines.push("---", `_Generated by preflight export_timeline_`); + return lines.join("\n"); +} + +export function registerExportTimeline(server: McpServer) { + server.tool( + "export_timeline", + "Export timeline data as a markdown report with summary stats, prompt quality trends, and daily breakdowns. Use for weekly summaries, retrospectives, and session analysis.", + { + scope: z + .enum(["current", "related", "all"]) + .default("current") + .describe("Search scope"), + project: z.string().optional().describe("Filter to a specific project"), + branch: z.string().optional(), + author: z.string().optional().describe("Filter commits by author"), + since: z + .string() + .optional() + .describe("Start date (ISO or relative like '7days', '1week')"), + until: z.string().optional().describe("End date"), + format: z + .enum(["detailed", "summary"]) + .default("detailed") + .describe("detailed = full event log; summary = daily counts table"), + }, + async (params) => { + const grouped = await fetchAndGroup({ + scope: params.scope, + project: params.project, + branch: params.branch, + author: params.author, + since: params.since, + until: params.until, + }); + + if (grouped.totalEvents === 0) { + return { + content: [ + { + type: "text", + text: `# Session Report\n\n_No events found for the given filters. Make sure projects are onboarded with \`onboard_project\`._`, + }, + ], + }; + } + + const projectLabel = + params.project || params.scope === "current" + ? process.env.CLAUDE_PROJECT_DIR || "current project" + : params.scope; + + const markdown = renderMarkdown( + grouped, + projectLabel, + params.branch, + params.format + ); + + return { + content: [{ type: "text", text: markdown }], + }; + } + ); +}