diff --git a/README.md b/README.md index 6d03f5d..3515249 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,15 @@ 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. +### CLI commands + +```bash +preflight-dev # Interactive setup wizard +preflight-dev status # Check if preflight is configured in this project +preflight-dev --version # Print version +preflight-dev --help # Show usage help +``` + --- ## How It Works diff --git a/src/cli/init.ts b/src/cli/init.ts index dfaaa25..5a3d653 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -9,6 +9,97 @@ import { join, dirname } from "node:path"; import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; +// --------------------------------------------------------------------------- +// CLI flags: --help, --version, status +// --------------------------------------------------------------------------- + +const args = process.argv.slice(2); + +if (args.includes("--help") || args.includes("-h")) { + console.log(` +✈️ preflight-dev — Stop burning tokens on vague prompts + +USAGE + preflight-dev Interactive setup wizard (creates .mcp.json) + preflight-dev status Check if preflight is configured in this project + preflight-dev --version Print version + preflight-dev --help Show this help + +QUICK START + cd your-project + npx preflight-dev # run the setup wizard + # restart Claude Code — done! + +ONE-LINER (skip the wizard) + claude mcp add preflight -- npx -y preflight-dev-serve + +DOCS + 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 { + // Fallback when running from dist/ — package.json is one more level up + try { + const pkg2 = JSON.parse(await readFile(join(dirname(fileURLToPath(import.meta.url)), "../../../package.json"), "utf-8")); + console.log(`preflight-dev v${pkg2.version}`); + } catch { + console.log("preflight-dev (version unknown)"); + } + } + process.exit(0); +} + +if (args[0] === "status") { + const mcpPath = join(process.cwd(), ".mcp.json"); + const preflightDir = join(process.cwd(), ".preflight"); + + console.log("\n✈️ preflight status\n"); + + // Check .mcp.json + if (existsSync(mcpPath)) { + try { + const config = JSON.parse(await readFile(mcpPath, "utf-8")); + if (config.mcpServers?.preflight) { + const srv = config.mcpServers.preflight; + const profile = srv.env?.PROMPT_DISCIPLINE_PROFILE || "standard"; + const embeddings = srv.env?.EMBEDDING_PROVIDER || "local"; + console.log(` ✅ .mcp.json — preflight registered (profile: ${profile}, embeddings: ${embeddings})`); + } else { + console.log(" ❌ .mcp.json exists but no 'preflight' server configured"); + } + } catch { + console.log(" ⚠️ .mcp.json exists but failed to parse"); + } + } else { + console.log(" ❌ No .mcp.json found — run `npx preflight-dev` to set up"); + } + + // Check .preflight/ config dir + if (existsSync(preflightDir)) { + const files = ["config.yml", "triage.yml"].filter(f => existsSync(join(preflightDir, f))); + console.log(` ✅ .preflight/ directory (${files.length} config files: ${files.join(", ") || "none"})`); + } else { + console.log(" ℹ️ No .preflight/ directory (optional — sensible defaults apply)"); + } + + // Check environment + if (process.env.CLAUDE_PROJECT_DIR) { + console.log(` ✅ CLAUDE_PROJECT_DIR = ${process.env.CLAUDE_PROJECT_DIR}`); + } else { + console.log(" ℹ️ CLAUDE_PROJECT_DIR not set (some tools use cwd instead)"); + } + + console.log(""); + process.exit(0); +} + const rl = createInterface({ input: process.stdin, output: process.stdout }); function ask(question: string): Promise { diff --git a/src/index.ts b/src/index.ts index e7e9d00..1528c44 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 { registerExportReport } from "./tools/export-report.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_report", registerExportReport], ]; let registered = 0; diff --git a/src/tools/export-report.ts b/src/tools/export-report.ts new file mode 100644 index 0000000..b318c2b --- /dev/null +++ b/src/tools/export-report.ts @@ -0,0 +1,308 @@ +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 TYPE_ICONS: Record = { + prompt: "💬", + assistant: "🤖", + tool_call: "🔧", + correction: "❌", + commit: "📦", + compaction: "🗜️", + sub_agent_spawn: "🚀", + error: "⚠️", +}; + +/** Get project directories to search based on scope */ +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 EventStats { + prompts: number; + commits: number; + corrections: number; + toolCalls: number; + errors: number; + subAgentSpawns: number; + compactions: number; + assistantMessages: number; +} + +export function computeStats(events: any[]): EventStats { + const stats: EventStats = { + prompts: 0, + commits: 0, + corrections: 0, + toolCalls: 0, + errors: 0, + subAgentSpawns: 0, + compactions: 0, + assistantMessages: 0, + }; + for (const e of events) { + switch (e.type) { + case "prompt": stats.prompts++; break; + case "commit": stats.commits++; break; + case "correction": stats.corrections++; break; + case "tool_call": stats.toolCalls++; break; + case "error": stats.errors++; break; + case "sub_agent_spawn": stats.subAgentSpawns++; break; + case "compaction": stats.compactions++; break; + case "assistant": stats.assistantMessages++; break; + } + } + return stats; +} + +export function formatPeriodLabel(period: string, since?: string, until?: string): string { + if (since && until) return `${since} to ${until}`; + if (period === "7days") return "Last 7 Days"; + if (period === "30days") return "Last 30 Days"; + if (period === "24hours") return "Last 24 Hours"; + return period; +} + +export function getDateRange(period: string): { since: string; until: string } { + const now = new Date(); + const until = now.toISOString(); + let since: Date; + + switch (period) { + case "24hours": + since = new Date(now.getTime() - 24 * 60 * 60 * 1000); + break; + case "7days": + since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + break; + case "30days": + since = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + break; + default: + since = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + } + + return { since: since.toISOString(), until }; +} + +export function registerExportReport(server: McpServer) { + server.tool( + "export_report", + "Generate a markdown session report from timeline data. Includes activity summary, stats, prompt quality trends, commit log, and error/correction highlights for a given time period.", + { + scope: z.enum(["current", "related", "all"]).default("current") + .describe("Search scope: current project, related projects, or all indexed projects"), + project: z.string().optional() + .describe("Filter to a specific project name (overrides scope)"), + period: z.enum(["24hours", "7days", "30days"]).default("7days") + .describe("Time period for the report"), + since: z.string().optional() + .describe("Custom start date (ISO 8601, overrides period)"), + until: z.string().optional() + .describe("Custom end date (ISO 8601, overrides period)"), + branch: z.string().optional() + .describe("Filter to a specific branch"), + include_details: z.boolean().default(false) + .describe("Include full event details (verbose mode)"), + }, + async (params) => { + // Resolve date range + let since: string; + let until: string; + if (params.since && params.until) { + since = params.since; + until = params.until; + } else { + const range = getDateRange(params.period); + since = params.since || range.since; + until = params.until || range.until; + } + + // Resolve projects + let projectDirs: string[]; + if (params.project) { + projectDirs = [params.project]; + } else { + projectDirs = await getSearchProjects(params.scope); + } + + if (projectDirs.length === 0) { + return { + content: [{ + type: "text" as const, + text: `# Session Report\n\n_No projects found for scope "${params.scope}". Ensure CLAUDE_PROJECT_DIR is set or projects are onboarded._`, + }], + }; + } + + // Fetch all events in range (high limit for report) + const events = await getTimeline({ + project_dirs: projectDirs, + branch: params.branch, + since, + until, + limit: 2000, + offset: 0, + }); + + if (events.length === 0) { + return { + content: [{ + type: "text" as const, + text: `# Session Report\n\n_No events found for ${formatPeriodLabel(params.period, params.since, params.until)}._`, + }], + }; + } + + const stats = computeStats(events); + const periodLabel = formatPeriodLabel(params.period, params.since, params.until); + const projLabel = params.project || params.scope; + + // Group by day + 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(); + + // Build report + const lines: string[] = []; + + // Header + lines.push(`# 📋 Session Report: ${projLabel}`); + lines.push(`**Period:** ${periodLabel}`); + lines.push(`**Generated:** ${new Date().toISOString().slice(0, 16).replace("T", " ")} UTC`); + lines.push(`**Total Events:** ${events.length}`); + lines.push(""); + + // Summary stats + lines.push("## 📊 Summary"); + lines.push(""); + lines.push(`| Metric | Count |`); + lines.push(`|--------|-------|`); + lines.push(`| Prompts | ${stats.prompts} |`); + lines.push(`| Assistant responses | ${stats.assistantMessages} |`); + lines.push(`| Tool calls | ${stats.toolCalls} |`); + lines.push(`| Commits | ${stats.commits} |`); + lines.push(`| Corrections | ${stats.corrections} |`); + lines.push(`| Errors | ${stats.errors} |`); + lines.push(`| Sub-agent spawns | ${stats.subAgentSpawns} |`); + lines.push(`| Compactions | ${stats.compactions} |`); + lines.push(""); + + // Prompt quality signal + if (stats.prompts > 0) { + const correctionRate = ((stats.corrections / stats.prompts) * 100).toFixed(1); + const errorRate = ((stats.errors / stats.prompts) * 100).toFixed(1); + lines.push("## 🎯 Prompt Quality Signals"); + lines.push(""); + lines.push(`- **Correction rate:** ${correctionRate}% (${stats.corrections} corrections / ${stats.prompts} prompts)`); + lines.push(`- **Error rate:** ${errorRate}% (${stats.errors} errors / ${stats.prompts} prompts)`); + if (stats.compactions > 0) { + lines.push(`- **Compactions:** ${stats.compactions} (consider checkpointing more often if high)`); + } + lines.push(""); + } + + // Daily activity breakdown + lines.push("## 📅 Daily Activity"); + lines.push(""); + for (const day of sortedDays) { + const dayEvents = days.get(day)!; + const dayStats = computeStats(dayEvents); + const parts: string[] = []; + if (dayStats.prompts) parts.push(`${dayStats.prompts} prompts`); + if (dayStats.commits) parts.push(`${dayStats.commits} commits`); + if (dayStats.toolCalls) parts.push(`${dayStats.toolCalls} tool calls`); + if (dayStats.corrections) parts.push(`${dayStats.corrections} corrections`); + if (dayStats.errors) parts.push(`${dayStats.errors} errors`); + lines.push(`- **${day}**: ${parts.join(", ") || "no activity"} (${dayEvents.length} events)`); + } + lines.push(""); + + // Commit log + const commits = events.filter((e: any) => e.type === "commit"); + if (commits.length > 0) { + lines.push("## 📦 Commits"); + lines.push(""); + for (const c of commits) { + const time = c.timestamp ? new Date(c.timestamp).toISOString().slice(0, 16).replace("T", " ") : "??"; + let hash = "???"; + try { const m = JSON.parse(c.metadata || "{}"); hash = (m.commit_hash || "").slice(0, 7) || hash; } catch {} + const msg = (c.content || "").slice(0, 120).replace(/\n/g, " "); + lines.push(`- \`${hash}\` ${msg} _(${time})_`); + } + lines.push(""); + } + + // Errors & corrections + const issues = events.filter((e: any) => e.type === "error" || e.type === "correction"); + if (issues.length > 0) { + lines.push("## ⚠️ Errors & Corrections"); + lines.push(""); + for (const issue of issues.slice(0, 20)) { + const icon = TYPE_ICONS[issue.type] || "❓"; + const time = issue.timestamp ? new Date(issue.timestamp).toISOString().slice(0, 16).replace("T", " ") : "??"; + const content = (issue.content || "").slice(0, 150).replace(/\n/g, " "); + lines.push(`- ${icon} **${issue.type}** _(${time})_: ${content}`); + } + if (issues.length > 20) { + lines.push(`- _...and ${issues.length - 20} more_`); + } + lines.push(""); + } + + // Detailed event log (optional) + if (params.include_details) { + lines.push("## 📝 Full Event Log"); + lines.push(""); + for (const day of sortedDays) { + lines.push(`### ${day}`); + 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; + }); + for (const event of dayEvents) { + const time = event.timestamp + ? new Date(event.timestamp).toISOString().slice(11, 16) + : "??:??"; + const icon = TYPE_ICONS[event.type] || "❓"; + const content = (event.content || event.summary || "").slice(0, 200).replace(/\n/g, " "); + lines.push(`- ${time} ${icon} ${content}`); + } + lines.push(""); + } + } + + lines.push("---"); + lines.push("_Generated by preflight-dev `export_report` tool_"); + + return { + content: [{ + type: "text" as const, + text: lines.join("\n"), + }], + }; + } + ); +} diff --git a/src/tools/scope-work.ts b/src/tools/scope-work.ts index 9b5d971..cbbb685 100644 --- a/src/tools/scope-work.ts +++ b/src/tools/scope-work.ts @@ -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+/); 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/token-audit.ts b/src/tools/token-audit.ts index b7aad2c..8abb685 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; @@ -63,7 +63,7 @@ export function registerTokenAudit(server: McpServer): void { 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 wc = shell(`wc -l < '${shellEscape(f)}' 2>/dev/null`); const lines = parseInt(wc) || 0; estimatedContextTokens += lines * AVG_LINE_BYTES * AVG_TOKENS_PER_BYTE; if (lines > 500) { @@ -80,7 +80,7 @@ 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 stat = shell(`wc -c < '${shellEscape("CLAUDE.md")}' 2>/dev/null`); const bytes = parseInt(stat) || 0; if (bytes > 5120) { patterns.push(`CLAUDE.md is ${(bytes / 1024).toFixed(1)}KB — injected every session, burns tokens on paste`); @@ -139,7 +139,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/what-changed.ts b/src/tools/what-changed.ts index 913dfa2..4391f27 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,8 @@ 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`); + const diffFiles = shell(`git diff ${ref} --name-only 2>/dev/null || git diff HEAD~3 --name-only`); + const log = shell(`git log ${ref}..HEAD --oneline 2>/dev/null || git log -5 --oneline`); const branch = getBranch(); const fileList = diffFiles.split("\n").filter(Boolean); diff --git a/tests/tools/export-report.test.ts b/tests/tools/export-report.test.ts new file mode 100644 index 0000000..bbcaeb0 --- /dev/null +++ b/tests/tools/export-report.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect } from "vitest"; +import { + computeStats, + formatPeriodLabel, + getDateRange, +} from "../../src/tools/export-report.js"; + +describe("computeStats", () => { + it("counts all event types correctly", () => { + const events = [ + { type: "prompt" }, + { type: "prompt" }, + { type: "commit" }, + { type: "correction" }, + { type: "tool_call" }, + { type: "tool_call" }, + { type: "tool_call" }, + { type: "error" }, + { type: "sub_agent_spawn" }, + { type: "compaction" }, + { type: "assistant" }, + { type: "assistant" }, + ]; + const stats = computeStats(events); + expect(stats).toEqual({ + prompts: 2, + commits: 1, + corrections: 1, + toolCalls: 3, + errors: 1, + subAgentSpawns: 1, + compactions: 1, + assistantMessages: 2, + }); + }); + + it("returns all zeros for empty array", () => { + const stats = computeStats([]); + expect(stats.prompts).toBe(0); + expect(stats.commits).toBe(0); + expect(stats.errors).toBe(0); + }); + + it("ignores unknown event types", () => { + const stats = computeStats([{ type: "unknown" }, { type: "foo" }]); + expect(stats.prompts).toBe(0); + expect(stats.commits).toBe(0); + }); +}); + +describe("formatPeriodLabel", () => { + it("returns custom range when since and until provided", () => { + expect(formatPeriodLabel("7days", "2026-01-01", "2026-01-07")).toBe( + "2026-01-01 to 2026-01-07" + ); + }); + + it("returns human-readable label for known periods", () => { + expect(formatPeriodLabel("7days")).toBe("Last 7 Days"); + expect(formatPeriodLabel("30days")).toBe("Last 30 Days"); + expect(formatPeriodLabel("24hours")).toBe("Last 24 Hours"); + }); + + it("returns raw period string for unknown values", () => { + expect(formatPeriodLabel("custom")).toBe("custom"); + }); +}); + +describe("getDateRange", () => { + it("returns ISO date strings", () => { + const { since, until } = getDateRange("7days"); + expect(() => new Date(since)).not.toThrow(); + expect(() => new Date(until)).not.toThrow(); + }); + + it("24hours range is roughly 24h apart", () => { + const { since, until } = getDateRange("24hours"); + const diff = new Date(until).getTime() - new Date(since).getTime(); + const hours = diff / (1000 * 60 * 60); + expect(hours).toBeCloseTo(24, 0); + }); + + it("7days range is roughly 7 days apart", () => { + const { since, until } = getDateRange("7days"); + const diff = new Date(until).getTime() - new Date(since).getTime(); + const days = diff / (1000 * 60 * 60 * 24); + expect(days).toBeCloseTo(7, 0); + }); + + it("30days range is roughly 30 days apart", () => { + const { since, until } = getDateRange("30days"); + const diff = new Date(until).getTime() - new Date(since).getTime(); + const days = diff / (1000 * 60 * 60 * 24); + expect(days).toBeCloseTo(30, 0); + }); + + it("defaults to 7 days for unknown period", () => { + const { since, until } = getDateRange("unknown"); + const diff = new Date(until).getTime() - new Date(since).getTime(); + const days = diff / (1000 * 60 * 60 * 24); + expect(days).toBeCloseTo(7, 0); + }); +});