diff --git a/packages/cli/package.json b/packages/cli/package.json index 61da82f..3f3483d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -60,10 +60,12 @@ }, "type": "module", "dependencies": { + "@modelcontextprotocol/sdk": "^1.32.2", "commander": "^15.0.0", "ignore": "^7.0.12", "p-limit": "^7.3.3", - "ts-morph": "^28.0.0" + "ts-morph": "^28.0.0", + "zod": "^4.6.7" }, "devDependencies": { "@types/node": "^22.20.5", diff --git a/packages/cli/src/commands/mcp.ts b/packages/cli/src/commands/mcp.ts new file mode 100644 index 0000000..cade359 --- /dev/null +++ b/packages/cli/src/commands/mcp.ts @@ -0,0 +1,37 @@ +import { resolve } from "node:path"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { createDevmapServer } from "../mcp/server.js"; + +export type McpOptions = { + root?: string; +}; + +/** + * Run DevMap as an MCP server over stdio. + * + * stdout carries the JSON-RPC stream, so anything else that would print there + * must go to stderr instead. Every console method that writes to stdout is + * redirected before the server starts. + */ +export async function mcpCommand(options: McpOptions = {}): Promise { + const redirect = (...values: unknown[]): void => { + console.error(...values); + }; + console.log = redirect; + console.info = redirect; + console.debug = redirect; + + const projectRoot = resolve(options.root ?? "."); + const { server } = createDevmapServer({ projectRoot }); + const transport = new StdioServerTransport(); + + const shutdown = (): void => { + void server.close().finally(() => process.exit(0)); + }; + process.once("SIGINT", shutdown); + process.once("SIGTERM", shutdown); + process.stdin.once("end", shutdown); + + await server.connect(transport); + console.error(`DevMap MCP server ready (root: ${projectRoot})`); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index adeaed1..97bae85 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -7,6 +7,7 @@ import { explainCommand } from "./commands/explain.js"; import { flowCommand } from "./commands/flow.js"; import { initCommand } from "./commands/init.js"; import { mapCommand } from "./commands/map.js"; +import { mcpCommand } from "./commands/mcp.js"; import { onboardingCommand } from "./commands/onboarding.js"; import { printHelp } from "./utils/help.js"; import { printWelcome } from "./utils/welcome.js"; @@ -96,6 +97,12 @@ configCommand local: options.local })); +program + .command("mcp") + .description("Run DevMap as an MCP server (stdio) so AI agents can query the project map") + .option("--root ", "project root containing .devmap/snapshot.json", ".") + .action((options) => mcpCommand({ root: options.root })); + program .command("doctor") .description("Diagnose DevMap setup") diff --git a/packages/cli/src/mcp/queries.ts b/packages/cli/src/mcp/queries.ts new file mode 100644 index 0000000..6672c89 --- /dev/null +++ b/packages/cli/src/mcp/queries.ts @@ -0,0 +1,286 @@ +import type { ProjectMap } from "../analyzers/pipeline/index.js"; +import type { RouteInfo } from "../analyzers/detectors/routeDetector.js"; +import { DevmapError } from "../utils/errors.js"; + +/** + * Pure, read-only queries over a ProjectMap snapshot. No I/O, no printing, + * so they are cheap to test and safe to call from any front-end. + */ + +export type SnapshotMeta = { generatedAt: string; stale: boolean }; + +export const DEFAULT_LIMIT = 20; +export const MAX_LIMIT = 100; + +export function clampLimit(limit: number | undefined, fallback = DEFAULT_LIMIT): number { + if (limit === undefined || !Number.isFinite(limit)) return fallback; + return Math.max(1, Math.min(MAX_LIMIT, Math.floor(limit))); +} + +// ── overview ──────────────────────────────────────────────────────────────── + +export type OverviewResult = { + project: ProjectMap["project"]; + stats: ProjectMap["stats"]; + entryPoints: string[]; + criticalFiles: Array<{ path: string; referencedBy: number; reasons: string[] }>; + features: Array<{ + name: string; + purpose: string; + confidence: string; + entryPoint?: string; + fileCount: number; + }>; + flows: Array<{ name: string; type: string; purpose: string }>; + routeCounts: { pages: number; api: number }; + database?: ProjectMap["database"]; + externalServices: string[]; + recommendedPath: string[]; + nextSteps: string[]; +}; + +export function buildOverview(snapshot: ProjectMap, limit = 10): OverviewResult { + return { + project: snapshot.project, + stats: snapshot.stats, + entryPoints: snapshot.entryPoints.slice(0, limit), + criticalFiles: snapshot.criticalFiles.slice(0, limit).map((file) => ({ + path: file.path, + referencedBy: file.referencedBy, + reasons: file.reasons + })), + features: snapshot.features.map((feature) => ({ + name: feature.name, + purpose: feature.purpose, + confidence: feature.confidence, + ...(feature.entryPoint ? { entryPoint: feature.entryPoint } : {}), + fileCount: feature.files.length + })), + flows: snapshot.flows.map((flow) => ({ + name: flow.name, + type: flow.type, + purpose: flow.purpose + })), + routeCounts: { + pages: snapshot.routes.filter((route) => route.kind === "page").length, + api: snapshot.routes.filter((route) => route.kind === "api").length + }, + ...(snapshot.database ? { database: snapshot.database } : {}), + externalServices: snapshot.externalServices, + recommendedPath: snapshot.onboarding.recommendedPath, + nextSteps: [ + "devmap_map with a feature name to see its files and dependencies", + "devmap_flow with a flow name to trace it end to end", + "devmap_impact with a file path before changing it", + "devmap_find to locate files, features or routes by keyword" + ] + }; +} + +// ── routes ────────────────────────────────────────────────────────────────── + +export type RoutesQuery = { + kind?: "page" | "api" | "all"; + prefix?: string; + method?: string; + limit?: number; +}; + +export type RoutesResult = { + total: number; + returned: number; + truncated: boolean; + routes: RouteInfo[]; +}; + +export function queryRoutes(snapshot: ProjectMap, query: RoutesQuery = {}): RoutesResult { + const kind = query.kind ?? "all"; + const prefix = query.prefix?.trim(); + const method = query.method?.trim().toUpperCase(); + const limit = clampLimit(query.limit, 50); + + const matches = snapshot.routes.filter((route) => { + if (kind !== "all" && route.kind !== kind) return false; + if (prefix && !route.path.startsWith(prefix)) return false; + if (method && !(route.methods ?? []).some((candidate) => candidate.toUpperCase() === method)) { + return false; + } + return true; + }); + + return { + total: matches.length, + returned: Math.min(matches.length, limit), + truncated: matches.length > limit, + routes: matches.slice(0, limit) + }; +} + +// ── impact ────────────────────────────────────────────────────────────────── + +export function resolveProjectFile(snapshot: ProjectMap, input: string): string { + const normalized = input.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, ""); + if (normalized === "") { + throw new DevmapError("A file path is required.", "Pass a path relative to the project root."); + } + + const paths = Object.keys(snapshot.fileIndex); + if (snapshot.fileIndex[normalized]) return normalized; + + const lower = normalized.toLowerCase(); + const insensitive = paths.filter((path) => path.toLowerCase() === lower); + if (insensitive.length === 1) return insensitive[0]!; + + const suffix = paths.filter((path) => path.toLowerCase().endsWith(`/${lower}`)); + if (suffix.length === 1) return suffix[0]!; + if (suffix.length > 1) { + throw new DevmapError( + `"${input}" matches multiple files.`, + `Be more specific — options: ${suffix.slice(0, 8).join(", ")}` + ); + } + + const similar = paths + .filter((path) => path.toLowerCase().includes(lower)) + .slice(0, 5); + throw new DevmapError( + `"${input}" isn't a file in the DevMap snapshot.`, + similar.length > 0 + ? `Did you mean: ${similar.join(", ")}?` + : "Use a path relative to the project root, or try devmap_find to locate it." + ); +} + +export type ImpactResult = { + file: string; + purpose?: string; + scope: string; + features: string[]; + importance: number; + /** Files/areas likely affected when this file changes. */ + impacts: string[]; + /** Files that import this file directly. */ + dependents: string[]; + /** Local files this file imports. */ + imports: string[]; + truncated: boolean; +}; + +export function queryImpact(snapshot: ProjectMap, input: string, limit?: number): ImpactResult { + const file = resolveProjectFile(snapshot, input); + const cap = clampLimit(limit, 50); + const entry = snapshot.fileIndex[file]!; + const impact = snapshot.changeImpact[file] ?? { impacts: [], dependents: [] }; + const imports = snapshot.fileGraph[file] ?? []; + + return { + file, + ...(entry.purpose ? { purpose: entry.purpose } : {}), + scope: entry.scope, + features: entry.featureRefs, + importance: entry.importance, + impacts: impact.impacts.slice(0, cap), + dependents: impact.dependents.slice(0, cap), + imports: imports.slice(0, cap), + truncated: + impact.impacts.length > cap || impact.dependents.length > cap || imports.length > cap + }; +} + +// ── find ──────────────────────────────────────────────────────────────────── + +export type FindHit = + | { type: "feature"; name: string; purpose: string; entryPoint?: string; score: number } + | { type: "file"; path: string; purpose?: string; scope: string; score: number } + | { type: "route"; path: string; file: string; kind: string; methods?: string[]; score: number }; + +export type FindResult = { + query: string; + total: number; + truncated: boolean; + hits: FindHit[]; +}; + +function tokenize(value: string): string[] { + return value + .replace(/([a-z0-9])([A-Z])/g, "$1 $2") + .toLowerCase() + .split(/[^a-z0-9]+/) + .filter((token) => token.length > 0); +} + +function scoreText(haystack: string | undefined, tokens: string[], weight: number): number { + if (!haystack) return 0; + const lower = haystack.toLowerCase(); + let score = 0; + for (const token of tokens) { + if (lower.includes(token)) score += weight; + } + return score; +} + +export function queryFind(snapshot: ProjectMap, query: string, limit?: number): FindResult { + const tokens = tokenize(query); + if (tokens.length === 0) { + throw new DevmapError("The search query is empty.", "Pass one or more keywords, e.g. \"auth login\"."); + } + const cap = clampLimit(limit, 15); + const hits: FindHit[] = []; + + for (const feature of snapshot.features) { + const score = + scoreText(feature.name, tokens, 5) + + scoreText(feature.searchTerms.join(" "), tokens, 3) + + scoreText(feature.purpose, tokens, 1); + if (score > 0) { + hits.push({ + type: "feature", + name: feature.name, + purpose: feature.purpose, + ...(feature.entryPoint ? { entryPoint: feature.entryPoint } : {}), + score: score + 2 + }); + } + } + + for (const [path, entry] of Object.entries(snapshot.fileIndex)) { + const score = + scoreText(path, tokens, 3) + + scoreText(entry.exportedSymbols.join(" "), tokens, 3) + + scoreText(entry.topFunctions.map((fn) => fn.name).join(" "), tokens, 2) + + scoreText(entry.searchTerms.join(" "), tokens, 2) + + scoreText(entry.purpose, tokens, 1); + if (score > 0) { + hits.push({ + type: "file", + path, + ...(entry.purpose ? { purpose: entry.purpose } : {}), + scope: entry.scope, + // importance only breaks ties between otherwise equal matches + score: score + Math.min(entry.importance, 10) / 100 + }); + } + } + + for (const route of snapshot.routes) { + const score = scoreText(route.path, tokens, 4) + scoreText(route.file, tokens, 1); + if (score > 0) { + hits.push({ + type: "route", + path: route.path, + file: route.file, + kind: route.kind, + ...(route.methods ? { methods: route.methods } : {}), + score + }); + } + } + + hits.sort((a, b) => b.score - a.score); + return { + query, + total: hits.length, + truncated: hits.length > cap, + hits: hits.slice(0, cap) + }; +} diff --git a/packages/cli/src/mcp/server.ts b/packages/cli/src/mcp/server.ts new file mode 100644 index 0000000..f15a357 --- /dev/null +++ b/packages/cli/src/mcp/server.ts @@ -0,0 +1,321 @@ +import { readdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { z } from "zod"; +import { runFlow } from "../commands/flow.js"; +import { runMap } from "../commands/map.js"; +import { DevmapError } from "../utils/errors.js"; +import { withJsonOutput } from "../utils/output.js"; +import { DEVMAP_VERSION } from "../utils/packageMetadata.js"; +import { + buildOverview, + queryFind, + queryImpact, + queryRoutes, + type SnapshotMeta +} from "./queries.js"; +import { createSnapshotProvider, type SnapshotProvider } from "./snapshotProvider.js"; + +export const MAX_TEXT_CHARS = 30_000; +const FEATURE_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/; + +const SERVER_INSTRUCTIONS = [ + "DevMap gives you a precomputed map of this codebase (features, routes, flows, dependency graph).", + "Start with devmap_overview, then drill into one feature with devmap_map or devmap_flow instead of reading many files.", + "Call devmap_impact before changing a file, and devmap_find to locate code by keyword.", + "Every result includes snapshot.stale; when it is true the code changed after the last analysis, so treat results as approximate and ask the user to run `devmap analyze --fresh`." +].join(" "); + +export type DevmapServerOptions = { + projectRoot: string; + /** Inject a provider (tests); defaults to a cached provider for `projectRoot`. */ + provider?: SnapshotProvider; +}; + +export type DevmapServer = { + server: McpServer; + provider: SnapshotProvider; +}; + +export function truncateText(value: string, max = MAX_TEXT_CHARS): { text: string; truncated: boolean } { + if (value.length <= max) return { text: value, truncated: false }; + return { + text: `${value.slice(0, max)}\n\n… [truncated ${value.length - max} characters]`, + truncated: true + }; +} + +function snapshotMeta(generatedAt: string, stale: boolean): SnapshotMeta & { note?: string } { + return { + generatedAt, + stale, + ...(stale + ? { note: "The code changed after this snapshot was generated. Results may be outdated; run `devmap analyze --fresh`." } + : {}) + }; +} + +function ok(payload: unknown): CallToolResult { + return { content: [{ type: "text", text: JSON.stringify(payload) }] }; +} + +function fail(error: unknown): CallToolResult { + const payload = error instanceof DevmapError + ? { error: error.message, ...(error.hint ? { hint: error.hint } : {}) } + : { error: error instanceof Error ? error.message : String(error) }; + return { isError: true, content: [{ type: "text", text: JSON.stringify(payload) }] }; +} + +async function respond(action: () => Promise): Promise { + try { + return ok(await action()); + } catch (error) { + return fail(error); + } +} + +const readOnly = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }; + +export function createDevmapServer(options: DevmapServerOptions): DevmapServer { + const provider = options.provider ?? createSnapshotProvider(options.projectRoot); + const projectRoot = provider.projectRoot; + + const server = new McpServer( + { name: "devmap", version: DEVMAP_VERSION }, + { instructions: SERVER_INSTRUCTIONS } + ); + + // ── tools ──────────────────────────────────────────────────────────────── + + server.registerTool( + "devmap_overview", + { + title: "Project overview", + description: + "Entry point for understanding the project: framework, stats, entry points, most critical files, " + + "detected features, flows and route counts. Call this first.", + annotations: readOnly + }, + () => respond(async () => { + const { snapshot, stale } = await provider.get(); + return { + ...buildOverview(snapshot), + snapshot: snapshotMeta(snapshot.generatedAt, stale) + }; + }) + ); + + server.registerTool( + "devmap_map", + { + title: "Dependency map", + description: + "Dependency map for the whole project (omit target), one feature (feature name) or one file (project-relative path): " + + "what it imports, what imports it, and related files. Use it to find where to look before reading code.", + inputSchema: { + target: z.string().optional().describe("Feature name or project-relative file path. Omit for the project-wide map."), + depth: z.number().int().min(1).max(6).optional().describe("Traversal depth for file/feature maps."), + includeMermaid: z.boolean().optional().describe("Also return the Mermaid source (default false).") + }, + annotations: readOnly + }, + ({ target, depth, includeMermaid }) => respond(async () => { + const result = await withJsonOutput(() => runMap(target, { projectRoot, depth, write: false })); + const markdown = truncateText(result.markdown); + return { + mode: result.mode, + target: result.target, + markdown: markdown.text, + ...(includeMermaid ? { mermaid: truncateText(result.mermaid).text } : {}), + truncated: markdown.truncated, + snapshot: snapshotMeta(result.snapshot.generatedAt, result.snapshot.stale) + }; + }) + ); + + server.registerTool( + "devmap_flow", + { + title: "Trace a flow", + description: + "Trace how a feature or API request works end to end (ordered steps with file paths). " + + "Omit target to list the curated top flows. Static analysis only, no AI.", + inputSchema: { + target: z.string().optional().describe("Flow name (case-insensitive, partial match allowed)."), + all: z.boolean().optional().describe("Include lower-confidence features and non-API routes.") + }, + annotations: readOnly + }, + ({ target, all }) => respond(async () => { + const result = await withJsonOutput(() => runFlow( + target, + { projectRoot, all, write: false, narrate: false }, + {} + )); + let truncated = false; + const flows = result.flows.slice(0, MAX_FLOWS).map((flow) => { + const markdown = truncateText(flow.markdown, 8_000); + truncated ||= markdown.truncated; + return { name: flow.name, type: flow.type, purpose: flow.purpose, markdown: markdown.text }; + }); + return { + total: result.flows.length, + truncated: truncated || result.flows.length > MAX_FLOWS, + flows, + snapshot: snapshotMeta(result.snapshot.generatedAt, result.snapshot.stale) + }; + }) + ); + + server.registerTool( + "devmap_routes", + { + title: "List routes", + description: "List detected page and API routes with their handler file and HTTP methods. Filter by kind, path prefix or method.", + inputSchema: { + kind: z.enum(["page", "api", "all"]).optional().describe("Route kind (default all)."), + prefix: z.string().optional().describe("Only routes whose path starts with this, e.g. /api/users."), + method: z.string().optional().describe("HTTP method, e.g. POST."), + limit: z.number().int().min(1).max(100).optional().describe("Max routes to return (default 50).") + }, + annotations: readOnly + }, + (input) => respond(async () => { + const { snapshot, stale } = await provider.get(); + return { + ...queryRoutes(snapshot, input), + snapshot: snapshotMeta(snapshot.generatedAt, stale) + }; + }) + ); + + server.registerTool( + "devmap_impact", + { + title: "Change impact", + description: + "What is affected if a file changes: files that import it, areas likely impacted, features it belongs to, and what it imports. " + + "Call this before editing a shared file.", + inputSchema: { + file: z.string().describe("Project-relative file path (a unique path suffix also works)."), + limit: z.number().int().min(1).max(100).optional().describe("Max entries per list (default 50).") + }, + annotations: readOnly + }, + ({ file, limit }) => respond(async () => { + const { snapshot, stale } = await provider.get(); + return { + ...queryImpact(snapshot, file, limit), + snapshot: snapshotMeta(snapshot.generatedAt, stale) + }; + }) + ); + + server.registerTool( + "devmap_find", + { + title: "Find code", + description: + "Locate features, files and routes by keyword using DevMap's index (paths, exported symbols, function names, search terms, purposes). " + + "Faster and more precise than grepping the whole repo for a concept.", + inputSchema: { + query: z.string().describe("One or more keywords, e.g. \"auth login\" or \"InvoiceService\"."), + limit: z.number().int().min(1).max(100).optional().describe("Max hits to return (default 15).") + }, + annotations: readOnly + }, + ({ query, limit }) => respond(async () => { + const { snapshot, stale } = await provider.get(); + return { + ...queryFind(snapshot, query, limit), + snapshot: snapshotMeta(snapshot.generatedAt, stale) + }; + }) + ); + + // ── resources ──────────────────────────────────────────────────────────── + + server.registerResource( + "devmap-snapshot", + "devmap://snapshot", + { + title: "DevMap snapshot", + description: "Full analysis snapshot (.devmap/snapshot.json). Can be large; prefer the tools for targeted questions.", + mimeType: "application/json" + }, + async (uri) => { + const { snapshot } = await provider.get(); + return { contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(snapshot) }] }; + } + ); + + server.registerResource( + "devmap-md", + "devmap://devmap-md", + { + title: "DEVMAP.md", + description: "The project's DEVMAP.md context file.", + mimeType: "text/markdown" + }, + async (uri) => { + try { + const text = await readFile(join(projectRoot, "DEVMAP.md"), "utf8"); + return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] }; + } catch { + throw new DevmapError("DEVMAP.md was not found in the project root.", "Run devmap init first."); + } + } + ); + + server.registerResource( + "devmap-feature", + new ResourceTemplate("devmap://feature/{id}", { + list: async () => { + const ids = await listFeatureIds(projectRoot); + return { + resources: ids.map((id) => ({ + uri: `devmap://feature/${id}`, + name: id, + mimeType: "application/json" + })) + }; + } + }), + { + title: "Feature map", + description: "Per-feature navigation data from .devmap/features/.json.", + mimeType: "application/json" + }, + async (uri, variables) => { + const raw = variables.id; + const id = Array.isArray(raw) ? raw[0] : raw; + if (typeof id !== "string" || !FEATURE_ID_PATTERN.test(id)) { + throw new DevmapError(`"${String(id)}" is not a valid feature id.`, "Feature ids are lowercase letters, digits and dashes."); + } + try { + const text = await readFile(join(projectRoot, ".devmap", "features", `${id}.json`), "utf8"); + return { contents: [{ uri: uri.href, mimeType: "application/json", text }] }; + } catch { + throw new DevmapError(`Feature "${id}" was not found.`, "Run devmap analyze, then list resources to see feature ids."); + } + } + ); + + return { server, provider }; +} + +const MAX_FLOWS = 20; + +async function listFeatureIds(projectRoot: string): Promise { + try { + const files = await readdir(join(projectRoot, ".devmap", "features")); + return files + .filter((file) => file.endsWith(".json")) + .map((file) => file.slice(0, -".json".length)) + .filter((id) => FEATURE_ID_PATTERN.test(id)) + .sort(); + } catch { + return []; + } +} diff --git a/packages/cli/src/mcp/snapshotProvider.ts b/packages/cli/src/mcp/snapshotProvider.ts new file mode 100644 index 0000000..812e1a4 --- /dev/null +++ b/packages/cli/src/mcp/snapshotProvider.ts @@ -0,0 +1,64 @@ +import { stat } from "node:fs/promises"; +import { resolve } from "node:path"; +import { + getSnapshotPath, + isSnapshotStale, + readSnapshotOrThrow, + type LoadedSnapshot +} from "../cache/snapshot.js"; + +const DEFAULT_STALE_TTL_MS = 10_000; + +export type SnapshotProvider = { + readonly projectRoot: string; + /** Read-only. Throws a DevmapError (with hint) when the snapshot is missing or unusable. */ + get(): Promise; +}; + +/** + * Serves the current snapshot to long-lived callers without re-reading and + * re-fingerprinting the whole project on every call: + * - the snapshot is re-read only when snapshot.json changes on disk + * - the staleness check (which scans project files) is memoized for a short TTL + */ +export function createSnapshotProvider( + root: string, + options: { staleTtlMs?: number; now?: () => number } = {} +): SnapshotProvider { + const projectRoot = resolve(root); + const ttl = options.staleTtlMs ?? DEFAULT_STALE_TTL_MS; + const now = options.now ?? Date.now; + + let cached: { mtimeMs: number; snapshot: LoadedSnapshot["snapshot"] } | undefined; + let staleCache: { value: boolean; at: number; mtimeMs: number } | undefined; + + return { + projectRoot, + async get(): Promise { + let mtimeMs: number | undefined; + try { + mtimeMs = (await stat(getSnapshotPath(projectRoot))).mtimeMs; + } catch { + mtimeMs = undefined; + } + + if (mtimeMs === undefined || !cached || cached.mtimeMs !== mtimeMs) { + // Throws DevmapError for missing/corrupt/unsupported snapshots. + const snapshot = await readSnapshotOrThrow(projectRoot); + cached = { mtimeMs: mtimeMs ?? 0, snapshot }; + staleCache = undefined; + } + + const currentMtime = cached.mtimeMs; + if (!staleCache || staleCache.mtimeMs !== currentMtime || now() - staleCache.at > ttl) { + staleCache = { + value: await isSnapshotStale(projectRoot, cached.snapshot), + at: now(), + mtimeMs: currentMtime + }; + } + + return { snapshot: cached.snapshot, stale: staleCache.value }; + } + }; +} diff --git a/packages/cli/test/mcp-server.test.ts b/packages/cli/test/mcp-server.test.ts new file mode 100644 index 0000000..58f4559 --- /dev/null +++ b/packages/cli/test/mcp-server.test.ts @@ -0,0 +1,386 @@ +import assert from "node:assert/strict"; +import { cp, mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { createProjectMap } from "../src/analyzers/pipeline/projectMap.js"; +import { writeAgentNavigationFiles } from "../src/cache/agentNavigation.js"; +import { saveSnapshot } from "../src/cache/snapshot.js"; +import { + createDevmapServer, + MAX_TEXT_CHARS, + truncateText +} from "../src/mcp/server.js"; +import { createSnapshotProvider } from "../src/mcp/snapshotProvider.js"; +import { + clampLimit, + queryFind, + queryImpact, + queryRoutes, + resolveProjectFile +} from "../src/mcp/queries.js"; +import { DevmapError } from "../src/utils/errors.js"; + +const testDirectory = dirname(fileURLToPath(import.meta.url)); +const packageRoot = resolve(testDirectory, ".."); +const nextFixture = join(testDirectory, "fixtures", "nextjs-project"); +const hubFixture = join(testDirectory, "fixtures", "hub-fanin"); + +/** + * A real project on disk: the fixture is copied to a temp root and analyzed there, + * so the snapshot fingerprint matches the files (stale === false until they change). + */ +async function projectFromFixture(fixtureRoot: string, withNavigation = false): Promise { + const projectRoot = await mkdtemp(join(tmpdir(), "devmap-mcp-test-")); + await cp(fixtureRoot, projectRoot, { recursive: true }); + const snapshot = await createProjectMap(projectRoot); + await saveSnapshot(projectRoot, snapshot); + if (withNavigation) { + await writeAgentNavigationFiles(projectRoot, snapshot); + } + return projectRoot; +} + +async function connect(projectRoot: string): Promise<{ client: Client; close: () => Promise }> { + const { server } = createDevmapServer({ projectRoot }); + const client = new Client({ name: "test-client", version: "0.0.0" }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); + return { + client, + close: async () => { + await client.close(); + await server.close(); + } + }; +} + +type ToolResult = { isError?: boolean; content: Array<{ type: string; text: string }> }; + +async function callTool( + client: Client, + name: string, + args: Record = {} +): Promise<{ isError: boolean; data: any }> { + const result = (await client.callTool({ name, arguments: args })) as ToolResult; + return { + isError: result.isError === true, + data: JSON.parse(result.content[0]!.text) + }; +} + +test("server lists the read-only tools with read-only annotations", async () => { + const projectRoot = await projectFromFixture(nextFixture); + const { client, close } = await connect(projectRoot); + + try { + const { tools } = await client.listTools(); + const names = tools.map((tool) => tool.name).sort(); + assert.deepEqual(names, [ + "devmap_find", + "devmap_flow", + "devmap_impact", + "devmap_map", + "devmap_overview", + "devmap_routes" + ]); + for (const tool of tools) { + assert.equal(tool.annotations?.readOnlyHint, true, `${tool.name} must be read-only`); + assert.ok(tool.description && tool.description.length > 20, `${tool.name} needs a description`); + } + assert.match(client.getInstructions() ?? "", /devmap_overview/); + } finally { + await close(); + await rm(projectRoot, { recursive: true, force: true }); + } +}); + +test("devmap_overview returns project facts, features, and snapshot freshness", async () => { + const projectRoot = await projectFromFixture(nextFixture); + const { client, close } = await connect(projectRoot); + + try { + const { isError, data } = await callTool(client, "devmap_overview"); + assert.equal(isError, false); + assert.equal(data.project.framework, "nextjs"); + assert.ok(data.features.length > 0); + assert.ok(data.stats.totalFiles > 0); + assert.equal(typeof data.snapshot.generatedAt, "string"); + assert.equal(data.snapshot.stale, false); + assert.ok(Array.isArray(data.nextSteps)); + } finally { + await close(); + await rm(projectRoot, { recursive: true, force: true }); + } +}); + +test("devmap_map and devmap_flow are silent and never write .devmap artifacts", async () => { + const projectRoot = await projectFromFixture(nextFixture); + const { client, close } = await connect(projectRoot); + + try { + const project = await callTool(client, "devmap_map"); + assert.equal(project.isError, false); + assert.equal(project.data.mode, "project"); + assert.ok(project.data.markdown.length > 0); + assert.equal(project.data.mermaid, undefined); + + const withMermaid = await callTool(client, "devmap_map", { includeMermaid: true }); + assert.match(withMermaid.data.mermaid, /graph/); + + const flows = await callTool(client, "devmap_flow"); + assert.equal(flows.isError, false); + assert.ok(flows.data.flows.length > 0); + assert.ok(flows.data.flows.every((flow: { markdown: string }) => !/How it works/.test(flow.markdown))); + + const one = await callTool(client, "devmap_flow", { target: flows.data.flows[0].name }); + assert.equal(one.data.flows.length, 1); + + const { readdir } = await import("node:fs/promises"); + const entries = await readdir(join(projectRoot, ".devmap")); + assert.deepEqual(entries.sort(), ["snapshot.json"]); + } finally { + await close(); + await rm(projectRoot, { recursive: true, force: true }); + } +}); + +test("devmap_routes filters by kind, prefix and method, and reports truncation", async () => { + const projectRoot = await projectFromFixture(nextFixture); + const { client, close } = await connect(projectRoot); + + try { + const all = await callTool(client, "devmap_routes"); + assert.ok(all.data.total > 0); + + const api = await callTool(client, "devmap_routes", { kind: "api" }); + assert.ok(api.data.routes.every((route: { kind: string }) => route.kind === "api")); + + const none = await callTool(client, "devmap_routes", { prefix: "/definitely-not-a-route" }); + assert.equal(none.data.total, 0); + + const limited = await callTool(client, "devmap_routes", { limit: 1 }); + assert.equal(limited.data.returned, 1); + assert.equal(limited.data.truncated, limited.data.total > 1); + } finally { + await close(); + await rm(projectRoot, { recursive: true, force: true }); + } +}); + +test("devmap_impact reports dependents of a hub file and accepts path suffixes", async () => { + const projectRoot = await projectFromFixture(hubFixture); + const { client, close } = await connect(projectRoot); + + try { + const snapshot = await createProjectMap(hubFixture); + const [hub] = Object.entries(snapshot.changeImpact) + .sort((a, b) => b[1].dependents.length - a[1].dependents.length)[0]!; + + const exact = await callTool(client, "devmap_impact", { file: hub }); + assert.equal(exact.isError, false); + assert.equal(exact.data.file, hub); + assert.ok(exact.data.dependents.length > 0); + + const suffix = await callTool(client, "devmap_impact", { file: hub.split("/").slice(-1)[0] }); + // A bare basename either resolves uniquely or is reported as ambiguous with options. + if (!suffix.isError) { + assert.equal(suffix.data.file, hub); + } else { + assert.match(suffix.data.hint, /options/); + } + + const missing = await callTool(client, "devmap_impact", { file: "src/does-not-exist.ts" }); + assert.equal(missing.isError, true); + assert.match(missing.data.error, /isn't a file/); + } finally { + await close(); + await rm(projectRoot, { recursive: true, force: true }); + } +}); + +test("devmap_find ranks features, files and routes and rejects empty queries", async () => { + const projectRoot = await projectFromFixture(nextFixture); + const { client, close } = await connect(projectRoot); + + try { + const found = await callTool(client, "devmap_find", { query: "auth" }); + assert.equal(found.isError, false); + assert.ok(found.data.hits.length > 0); + const scores = found.data.hits.map((hit: { score: number }) => hit.score); + assert.deepEqual([...scores].sort((a, b) => b - a), scores); + + const empty = await callTool(client, "devmap_find", { query: " " }); + assert.equal(empty.isError, true); + assert.match(empty.data.hint, /keyword/); + } finally { + await close(); + await rm(projectRoot, { recursive: true, force: true }); + } +}); + +test("tools return a structured, actionable error when there is no snapshot", async () => { + const projectRoot = await mkdtemp(join(tmpdir(), "devmap-mcp-empty-")); + const { client, close } = await connect(projectRoot); + + try { + for (const name of ["devmap_overview", "devmap_map", "devmap_flow", "devmap_routes"]) { + const { isError, data } = await callTool(client, name); + assert.equal(isError, true, name); + assert.match(data.error, /No DevMap snapshot/); + assert.match(data.hint, /devmap analyze/); + } + } finally { + await close(); + await rm(projectRoot, { recursive: true, force: true }); + } +}); + +test("results flag a stale snapshot after the project changes", async () => { + const projectRoot = await mkdtemp(join(tmpdir(), "devmap-mcp-stale-")); + await mkdir(join(projectRoot, "src"), { recursive: true }); + await writeFile(join(projectRoot, "package.json"), JSON.stringify({ name: "stale-app", version: "1.0.0" })); + await writeFile(join(projectRoot, "src", "index.ts"), "export const a = 1;\n"); + await saveSnapshot(projectRoot, await createProjectMap(projectRoot)); + + const { server } = createDevmapServer({ + projectRoot, + provider: createSnapshotProvider(projectRoot, { staleTtlMs: 0 }) + }); + const client = new Client({ name: "test-client", version: "0.0.0" }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); + + try { + const fresh = await callTool(client, "devmap_overview"); + assert.equal(fresh.data.snapshot.stale, false); + + await writeFile(join(projectRoot, "src", "extra.ts"), "export const b = 2;\n"); + + const stale = await callTool(client, "devmap_overview"); + assert.equal(stale.data.snapshot.stale, true); + assert.match(stale.data.snapshot.note, /devmap analyze --fresh/); + } finally { + await client.close(); + await server.close(); + await rm(projectRoot, { recursive: true, force: true }); + } +}); + +test("resources expose the snapshot, DEVMAP.md, and per-feature maps", async () => { + const projectRoot = await projectFromFixture(nextFixture, true); + await writeFile(join(projectRoot, "DEVMAP.md"), "# DEVMAP\nhello\n"); + const { client, close } = await connect(projectRoot); + + try { + const { resources } = await client.listResources(); + const uris = resources.map((resource) => resource.uri); + assert.ok(uris.includes("devmap://snapshot")); + assert.ok(uris.includes("devmap://devmap-md")); + const featureUris = uris.filter((uri) => uri.startsWith("devmap://feature/")); + assert.ok(featureUris.length > 0, "expected per-feature resources"); + + const snapshot = await client.readResource({ uri: "devmap://snapshot" }); + assert.equal(JSON.parse((snapshot.contents[0] as { text: string }).text).project.framework, "nextjs"); + + const doc = await client.readResource({ uri: "devmap://devmap-md" }); + assert.match((doc.contents[0] as { text: string }).text, /hello/); + + const feature = await client.readResource({ uri: featureUris[0]! }); + assert.ok(JSON.parse((feature.contents[0] as { text: string }).text)); + } finally { + await close(); + await rm(projectRoot, { recursive: true, force: true }); + } +}); + +test("feature resource rejects path traversal ids", async () => { + const projectRoot = await projectFromFixture(nextFixture, true); + const { client, close } = await connect(projectRoot); + + try { + await assert.rejects(() => client.readResource({ uri: "devmap://feature/..%2F..%2Fsnapshot" })); + await assert.rejects(() => client.readResource({ uri: "devmap://feature/nope" })); + } finally { + await close(); + await rm(projectRoot, { recursive: true, force: true }); + } +}); + +test("snapshot provider re-reads only when snapshot.json changes", async () => { + const projectRoot = await projectFromFixture(nextFixture); + const provider = createSnapshotProvider(projectRoot, { staleTtlMs: 60_000 }); + + try { + const first = await provider.get(); + const second = await provider.get(); + assert.equal(second.snapshot, first.snapshot, "unchanged file should reuse the parsed snapshot"); + + const fresh = await createProjectMap(nextFixture); + await saveSnapshot(projectRoot, { ...fresh, generatedAt: "2099-01-01T00:00:00.000Z" }); + const future = new Date(Date.now() + 5_000); + await utimes(join(projectRoot, ".devmap", "snapshot.json"), future, future); + + const third = await provider.get(); + assert.equal(third.snapshot.generatedAt, "2099-01-01T00:00:00.000Z"); + } finally { + await rm(projectRoot, { recursive: true, force: true }); + } +}); + +test("query helpers: limits, path resolution, and truncation", async () => { + assert.equal(clampLimit(undefined, 20), 20); + assert.equal(clampLimit(0), 1); + assert.equal(clampLimit(10_000), 100); + assert.equal(clampLimit(Number.NaN, 7), 7); + + const short = truncateText("abc"); + assert.deepEqual(short, { text: "abc", truncated: false }); + const long = truncateText("x".repeat(MAX_TEXT_CHARS + 50)); + assert.equal(long.truncated, true); + assert.match(long.text, /truncated 50 characters/); + + const snapshot = await createProjectMap(nextFixture); + assert.throws(() => resolveProjectFile(snapshot, ""), DevmapError); + assert.equal(queryRoutes(snapshot, { method: "ZZZ" }).total, 0); + assert.throws(() => queryImpact(snapshot, "nope/missing.ts"), DevmapError); + assert.ok(queryFind(snapshot, "route", 3).hits.length <= 3); +}); + +test("devmap mcp over stdio keeps stdout protocol-clean and answers real requests", async () => { + const projectRoot = await projectFromFixture(nextFixture); + const transport = new StdioClientTransport({ + command: process.execPath, + args: ["--import", "tsx", join(packageRoot, "src", "index.ts"), "mcp", "--root", projectRoot], + cwd: packageRoot, + stderr: "pipe" + }); + const client = new Client({ name: "stdio-test-client", version: "0.0.0" }); + + try { + // StdioClientTransport parses every stdout line as JSON-RPC; any stray log line + // (spinner, banner, console.log) surfaces as a protocol error and fails here. + const errors: unknown[] = []; + transport.onerror = (error) => errors.push(error); + + await client.connect(transport); + const { tools } = await client.listTools(); + assert.equal(tools.length, 6); + + const overview = await callTool(client, "devmap_overview"); + assert.equal(overview.isError, false); + assert.equal(overview.data.project.framework, "nextjs"); + + const flow = await callTool(client, "devmap_flow"); + assert.equal(flow.isError, false); + + assert.deepEqual(errors, []); + } finally { + await client.close(); + await rm(projectRoot, { recursive: true, force: true }); + } +}); diff --git a/packages/cli/test/package-e2e.mjs b/packages/cli/test/package-e2e.mjs index 9c73fa9..6ae7e5e 100644 --- a/packages/cli/test/package-e2e.mjs +++ b/packages/cli/test/package-e2e.mjs @@ -13,6 +13,8 @@ import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; const execute = promisify(execFile); const executeShell = promisify(exec); @@ -55,7 +57,7 @@ try { await verifyProject("express-project", "express"); await verifyProject("react-project", "react"); - console.log("Packed CLI E2E passed for Next.js, Express, and React fixtures."); + console.log("Packed CLI E2E passed for Next.js, Express, and React fixtures (CLI + MCP server)."); async function verifyProject(fixtureName, expectedFramework) { const projectRoot = join(temporaryRoot, fixtureName); @@ -142,6 +144,42 @@ try { "--json" ])); assert.equal(doctorJson.config, "missing"); + + await verifyMcpServer(projectRoot, expectedFramework); + } + + async function verifyMcpServer(projectRoot, expectedFramework) { + // The packed package must start as an MCP server, keep stdout protocol-clean + // (the SDK transport reports any non-JSON-RPC line through onerror) and answer tools. + const transport = new StdioClientTransport({ + command: npmExecutable, + args: ["exec", "--", "devmap", "mcp"], + cwd: projectRoot, + env: { + ...process.env, + HOME: isolatedHome, + USERPROFILE: isolatedHome, + NO_COLOR: "1" + }, + stderr: "pipe" + }); + const protocolErrors = []; + transport.onerror = (error) => protocolErrors.push(error); + const client = new Client({ name: "package-e2e", version: "0.0.0" }); + + try { + await client.connect(transport); + const { tools } = await client.listTools(); + assert.ok(tools.some((tool) => tool.name === "devmap_overview")); + + const overview = await client.callTool({ name: "devmap_overview", arguments: {} }); + assert.notEqual(overview.isError, true); + const payload = JSON.parse(overview.content[0].text); + assert.equal(payload.project.framework, expectedFramework); + assert.deepEqual(protocolErrors, []); + } finally { + await client.close(); + } } } finally { await rm(temporaryRoot, { recursive: true, force: true }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a91a894..ee3769e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: packages/cli: dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.32.2 + version: 1.32.2(zod@4.6.7) commander: specifier: ^15.0.0 version: 15.0.0 @@ -44,6 +47,9 @@ importers: ts-morph: specifier: ^28.0.0 version: 28.0.0 + zod: + specifier: ^4.6.7 + version: 4.6.7 devDependencies: '@types/node': specifier: ^22.20.5 @@ -449,6 +455,12 @@ packages: '@fontsource-variable/geist-mono@5.3.0': resolution: {integrity: sha512-vBbuwDEo9AkrqADMXOrlAR3DFcJi4/JxeuU43FoiQERnNwsfXNnvxvReZG02cQKmyk4DZkZdBZX3oTDvy2zBAw==} + '@hono/node-server@2.1.5': + resolution: {integrity: sha512-MgP/I06GdvsilDspMk/qmRzpk8LOUStP6jFAbSRi40W09DhoyUn+Mj7eyKICPqeYKybOjVFLVIAQzvu1bfIa7Q==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4 || ^5.0.0-0 + '@img/colour@1.1.0': resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} @@ -614,6 +626,16 @@ packages: '@jridgewell/sourcemap-codec@1.6.0': resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + '@modelcontextprotocol/sdk@1.32.2': + resolution: {integrity: sha512-SWsVtH/0vULSQStoZ6xxH2K09Li8fXYTMC64ZeogCdpZ3XSi1eCPOByV1OpuJxuI3OcmHhosDWYyii07tpPsLw==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@napi-rs/wasm-runtime@1.2.4': resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} @@ -823,6 +845,10 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + ajv-draft-04@1.0.0: resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} peerDependencies: @@ -831,6 +857,14 @@ packages: ajv: optional: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-i18n@4.2.0: resolution: {integrity: sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg==} peerDependencies: @@ -880,6 +914,10 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + body-parser@2.4.0: + resolution: {integrity: sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -887,6 +925,18 @@ packages: resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -934,13 +984,41 @@ packages: resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} engines: {node: '>= 18'} + content-disposition@2.0.1: + resolution: {integrity: sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + cookie-es@1.2.3: resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@2.0.1: resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==} engines: {node: '>=22'} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} @@ -963,9 +1041,22 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1005,19 +1096,42 @@ packages: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + emmet@2.4.11: resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + encodeurl@2.0.1: + resolution: {integrity: sha512-Ev/hIyePLZtpL98x562lvPtenrUfGr0lfaf44HFZ+hDEwso7ccV9QAZrEVLg0hxNAzygaAFxy2dYaAKKM0G06A==} + engines: {node: '>= 0.8'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@3.0.4: resolution: {integrity: sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + esbuild@0.28.2: resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} engines: {node: '>=18'} @@ -1027,9 +1141,34 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter3@5.0.4: resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.1.1: + resolution: {integrity: sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + express-rate-limit@8.7.1: + resolution: {integrity: sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.3.0: + resolution: {integrity: sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==} + engines: {node: '>= 18'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -1057,6 +1196,10 @@ packages: picomatch: optional: true + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + flattie@1.1.1: resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} engines: {node: '>=8'} @@ -1071,11 +1214,22 @@ packages: resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} engines: {node: '>=20'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1084,6 +1238,14 @@ packages: resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} engines: {node: '>=18'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-tsconfig@5.0.0-beta.6: resolution: {integrity: sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==} engines: {node: '>=20.20.0'} @@ -1091,15 +1253,31 @@ packages: github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + h3@1.15.11: resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hono@4.13.14: + resolution: {integrity: sha512-yhRLQ21U6qa9Hsu2N7Qcbh8tE+WKEcoErjIo56LeSGiGXGp9sA7UpE86Ui9uexb61KakiGwuHMn2QdV7w6i6kQ==} + engines: {node: '>=16.9.0'} + html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -1109,10 +1287,29 @@ packages: http-cache-semantics@4.3.0: resolution: {integrity: sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ignore@7.0.12: resolution: {integrity: sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==} engines: {node: '>= 4'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.7.5: + resolution: {integrity: sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -1125,13 +1322,25 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@1.21.7: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jose@6.2.13: + resolution: {integrity: sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + jsonc-parser@2.3.1: resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} @@ -1226,6 +1435,10 @@ packages: magicast@0.5.5: resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} @@ -1235,6 +1448,14 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} @@ -1250,6 +1471,14 @@ packages: micromark-util-types@2.0.3: resolution: {integrity: sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + minimatch@10.2.6: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} @@ -1258,6 +1487,9 @@ packages: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} @@ -1266,6 +1498,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} + neotraverse@1.0.1: resolution: {integrity: sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==} engines: {node: '>= 10'} @@ -1286,6 +1522,14 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + obug@3.0.0: resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} engines: {node: '>=12.20.0'} @@ -1293,6 +1537,13 @@ packages: ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + on-finished@2.4.2: + resolution: {integrity: sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + oniguruma-parser@0.12.2: resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} @@ -1314,9 +1565,20 @@ packages: package-manager-detector@1.9.0: resolution: {integrity: sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + piccolore@0.1.3: resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} @@ -1331,6 +1593,10 @@ packages: resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + postcss@8.5.30: resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==} engines: {node: ^10 || ^12 || >=14} @@ -1351,9 +1617,25 @@ packages: property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + proxy-addr@2.0.8: + resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==} + engines: {node: '>= 0.10'} + + qs@6.17.1: + resolution: {integrity: sha512-jd3COkaKVgGUW+ZHyEHCG9bH7RoaaiWuMgzsIBjUMVjLAaWGWpoUAVwljVvBHxRu/2qEHnadLGZrkutYUBPQJA==} + engines: {node: '>=0.6'} + radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -1392,6 +1674,13 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + satteri@0.10.5: resolution: {integrity: sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==} @@ -1404,6 +1693,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.35.5: resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==} engines: {node: '>=20.9.0'} @@ -1413,10 +1713,34 @@ packages: '@types/node': optional: true + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + shiki@4.5.0: resolution: {integrity: sha512-OcRW72ngQvc+MSVWBRIovC/BTW589iUE/Vxav7VeM4RcY9MUjqB+IR6ILG1k9nuW6LiG/nct9uma4/MBtnZbmQ==} engines: {node: '>=20'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -1431,6 +1755,10 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@4.3.0: resolution: {integrity: sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==} @@ -1472,6 +1800,10 @@ packages: tinyps@0.5.0: resolution: {integrity: sha512-Llu7nEtVOGHh7jok32l6+udlu+4Np5vsYVB7zeTK614coSsro2qjwte7MFt/ouLM/h4kXqbGXktSMMvMsttSDw==} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -1489,6 +1821,10 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typesafe-path@0.2.2: resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} @@ -1540,6 +1876,10 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unstorage@1.17.6: resolution: {integrity: sha512-gjKtcmn/KwZuJZO9o2sc0esAQnhmTw0LviDHwevTXbJMYs0dRTp8ZEEWXHU0vTpzbN3cA0yxlTeyXssMp3zDqw==} peerDependencies: @@ -1602,6 +1942,10 @@ packages: uploadthing: optional: true + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + verkit@0.5.0: resolution: {integrity: sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==} engines: {node: '>=18.12.0'} @@ -1765,10 +2109,18 @@ packages: vscode-uri@3.2.0: resolution: {integrity: sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg==} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xxhash-wasm@1.1.0: resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} @@ -1802,6 +2154,11 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod@4.6.7: resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} @@ -2134,6 +2491,10 @@ snapshots: '@fontsource-variable/geist-mono@5.3.0': {} + '@hono/node-server@2.1.5(hono@4.13.14)': + dependencies: + hono: 4.13.14 + '@img/colour@1.1.0': optional: true @@ -2243,6 +2604,28 @@ snapshots: '@jridgewell/sourcemap-codec@1.6.0': {} + '@modelcontextprotocol/sdk@1.32.2(zod@4.6.7)': + dependencies: + '@hono/node-server': 2.1.5(hono@4.13.14) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.1 + express: 5.3.0 + express-rate-limit: 8.7.1(express@5.3.0) + hono: 4.13.14 + jose: 6.2.13 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.6.7 + zod-to-json-schema: 3.25.2(zod@4.6.7) + transitivePeerDependencies: + - supports-color + '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 @@ -2445,10 +2828,19 @@ snapshots: '@vscode/l10n@0.0.18': {} + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.1.0 + ajv-draft-04@1.0.0(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + ajv-i18n@4.2.0(ajv@8.20.0): dependencies: ajv: 8.20.0 @@ -2570,12 +2962,38 @@ snapshots: balanced-match@4.0.4: {} + body-parser@2.4.0: + dependencies: + bytes: 3.1.2 + content-type: 2.1.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.2 + qs: 6.17.1 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + ccount@2.0.1: {} character-entities-html4@2.1.0: {} @@ -2610,10 +3028,31 @@ snapshots: common-ancestor-path@2.0.0: {} + content-disposition@2.0.1: {} + + content-type@1.0.5: {} + + content-type@2.1.0: {} + cookie-es@1.2.3: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + cookie@2.0.1: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + crossws@0.3.5: dependencies: uncrypto: 0.1.3 @@ -2642,8 +3081,14 @@ snapshots: dependencies: css-tree: 2.2.1 + debug@4.4.3: + dependencies: + ms: 2.1.3 + defu@6.1.7: {} + depd@2.0.0: {} + dequal@2.0.3: {} destr@2.0.5: {} @@ -2678,6 +3123,14 @@ snapshots: dset@3.1.4: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + emmet@2.4.11: dependencies: '@emmetio/abbreviation': 2.3.3 @@ -2685,10 +3138,20 @@ snapshots: emoji-regex@10.6.0: {} + encodeurl@2.0.1: {} + entities@4.5.0: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@3.0.4: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + esbuild@0.28.2: optionalDependencies: '@esbuild/aix-ppc64': 0.28.2 @@ -2720,8 +3183,59 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + + etag@1.8.1: {} + eventemitter3@5.0.4: {} + eventsource-parser@3.1.1: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.1 + + express-rate-limit@8.7.1(express@5.3.0): + dependencies: + debug: 4.4.3 + express: 5.3.0 + ip-address: 10.7.5 + transitivePeerDependencies: + - supports-color + + express@5.3.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.4.0 + content-disposition: 2.0.1 + content-type: 2.1.0 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.1 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.2 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.8 + qs: 6.17.1 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -2742,6 +3256,17 @@ snapshots: optionalDependencies: picomatch: 4.0.7 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.1 + escape-html: 1.0.3 + on-finished: 2.4.2 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + flattie@1.1.1: {} fnv1a-64@0.1.2: {} @@ -2754,19 +3279,45 @@ snapshots: dependencies: tiny-inflate: 1.0.3 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + get-caller-file@2.0.5: {} get-east-asian-width@1.7.0: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + get-tsconfig@5.0.0-beta.6: dependencies: resolve-pkg-maps: 1.0.0 github-slugger@2.0.0: {} + gopd@1.2.0: {} + h3@1.15.11: dependencies: cookie-es: 1.2.3 @@ -2779,6 +3330,12 @@ snapshots: ufo: 1.6.4 uncrypto: 0.1.3 + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.5 @@ -2797,25 +3354,53 @@ snapshots: dependencies: '@types/hast': 3.0.5 + hono@4.13.14: {} + html-escaper@3.0.3: {} html-void-elements@3.0.0: {} http-cache-semantics@4.3.0: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + ignore@7.0.12: {} + inherits@2.0.4: {} + + ip-address@10.7.5: {} + + ipaddr.js@1.9.1: {} + iron-webcrypto@1.2.1: {} is-docker@4.0.0: {} is-plain-obj@4.1.0: {} + is-promise@4.0.0: {} + + isexe@2.0.0: {} + jiti@1.21.7: optional: true + jose@6.2.13: {} + json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + jsonc-parser@2.3.1: {} jsonc-parser@3.3.1: {} @@ -2883,6 +3468,8 @@ snapshots: '@babel/types': 7.29.8 source-map-js: 1.2.2 + math-intrinsics@1.1.0: {} + mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.5 @@ -2899,6 +3486,10 @@ snapshots: mdn-data@2.27.1: {} + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 @@ -2916,16 +3507,28 @@ snapshots: micromark-util-types@2.0.3: {} + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + minimatch@10.2.6: dependencies: brace-expansion: 5.0.12 mrmime@2.0.1: {} + ms@2.1.3: {} + muggle-string@0.4.1: {} nanoid@3.3.20: {} + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 + neotraverse@1.0.1: {} nlcst-to-string@4.0.0: @@ -2942,6 +3545,10 @@ snapshots: dependencies: boolbase: 1.0.0 + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + obug@3.0.0: {} ofetch@1.5.1: @@ -2950,6 +3557,14 @@ snapshots: node-fetch-native: 1.6.7 ufo: 1.6.4 + on-finished@2.4.2: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + oniguruma-parser@0.12.2: {} oniguruma-to-es@4.3.6: @@ -2971,8 +3586,14 @@ snapshots: package-manager-detector@1.9.0: {} + parseurl@1.3.3: {} + path-browserify@1.0.1: {} + path-key@3.1.1: {} + + path-to-regexp@8.4.2: {} + piccolore@0.1.3: {} picocolors@1.1.1: {} @@ -2981,6 +3602,8 @@ snapshots: picomatch@4.0.7: {} + pkce-challenge@5.0.1: {} + postcss@8.5.30: dependencies: nanoid: 3.3.20 @@ -2995,8 +3618,27 @@ snapshots: property-information@7.2.0: {} + proxy-addr@2.0.8: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.17.1: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + radix3@1.1.2: {} + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + readdirp@4.1.2: {} readdirp@5.1.1: {} @@ -3046,6 +3688,18 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.14 '@rolldown/binding-win32-x64-msvc': 1.2.14 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safer-buffer@2.1.2: {} + satteri@0.10.5: dependencies: '@types/estree-jsx': 1.0.5 @@ -3067,6 +3721,33 @@ snapshots: semver@7.8.5: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.1 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.2 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.1 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + sharp@0.35.5(@types/node@25.9.1): dependencies: '@img/colour': 1.1.0 @@ -3101,6 +3782,12 @@ snapshots: '@types/node': 25.9.1 optional: true + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + shiki@4.5.0: dependencies: '@shikijs/core': 4.5.0 @@ -3112,6 +3799,34 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + sisteransi@1.0.5: {} smol-toml@1.9.1: {} @@ -3120,6 +3835,8 @@ snapshots: space-separated-tokens@2.0.2: {} + statuses@2.0.2: {} + std-env@4.3.0: {} string-width@7.2.0: @@ -3165,6 +3882,8 @@ snapshots: tinyps@0.5.0: {} + toidentifier@1.0.1: {} + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -3182,6 +3901,12 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + type-is@2.1.0: + dependencies: + content-type: 2.1.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + typesafe-path@0.2.2: {} typescript-auto-import-cache@0.3.6: @@ -3243,6 +3968,8 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 + unpipe@1.0.0: {} + unstorage@1.17.6: dependencies: anymatch: 3.1.3 @@ -3254,6 +3981,8 @@ snapshots: ofetch: 1.5.1 ufo: 1.6.4 + vary@1.1.2: {} + verkit@0.5.0: {} vfile-message@4.0.3: @@ -3391,12 +4120,18 @@ snapshots: vscode-uri@3.2.0: {} + which@2.0.2: + dependencies: + isexe: 2.0.0 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 string-width: 7.2.0 strip-ansi: 7.2.0 + wrappy@1.0.2: {} + xxhash-wasm@1.1.0: {} y18n@5.0.8: {} @@ -3433,6 +4168,10 @@ snapshots: yocto-queue@1.2.2: {} + zod-to-json-schema@3.25.2(zod@4.6.7): + dependencies: + zod: 4.6.7 + zod@4.6.7: {} zwitch@2.0.4: {}