Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 9c081346143f5ae9f8dc17c7d2f94cc03c13ab97
parent 18e0266a1381373c8952403a498e2899102c0725
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon,  6 Jul 2026 21:19:10 -0400

Add MCP server (mcp/) exposing the archive to Claude Code / any MCP client

Layer 2 of bring-your-own-AI. A standalone local tool (not part of the static
export) that reads the site's published JSON shards and serves four MCP tools:
list_channels, search_transcripts (server-side scan with timestamped snippets),
get_transcript (markdown via the shared formatter), get_video_metadata.

- src/source.ts: ShardSource over local dir / remote origin / federated hub,
  all speaking the corpus.json + shard contract (hub fans out over members).
- src/search.ts: bounded server-side scan (the browser FlexSearch engine is
  IndexedDB/worker-coupled, so search runs directly over page shards here).
- src/server.ts + index.ts: MCP Server wiring + CLI source selection (stderr
  logs only; stdout is the JSON-RPC channel).
- Added mcp to the pnpm workspace; README with claude-mcp-add + mcp.json.

Verified: tsc clean; direct-scan smoke over real local shards; full stdio
JSON-RPC handshake (initialize -> tools/list -> list_channels/search).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Diffstat:
Mexport/CHANGELOG.md | 1+
Amcp/README.md | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amcp/package.json | 22++++++++++++++++++++++
Amcp/src/index.ts | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amcp/src/search.ts | 180+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amcp/src/server.ts | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amcp/src/source.ts | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amcp/tsconfig.json | 13+++++++++++++
Mpnpm-lock.yaml | 515+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpnpm-workspace.yaml | 1+
10 files changed, 1282 insertions(+), 0 deletions(-)

diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md @@ -2,6 +2,7 @@ ## [Unreleased] - **Bring-your-own-AI: the archive is now machine-navigable for AI tools.** Every site publishes a small fixed set of discovery files — `llms.txt` (an LLM-readable overview) and `corpus.json` (a documented index of the channels and *how to fetch any transcript* from the existing paginated JSON shards), plus `robots.txt` and a `sitemap.xml`. Nothing is generated per video (the shard scheme is documented instead), so the file count stays constant no matter how large the corpus grows. This lets Claude Code and other tools browse and answer questions about the archive by fetching a couple of URLs. The federated hub publishes an aggregate `corpus.json`/`llms.txt` spanning every member site. +- **New MCP server (`mcp/`) for Claude Code, Cursor, and other MCP clients.** A local tool that exposes the archive as MCP tools — `list_channels`, `search_transcripts` (timestamped snippets), `get_transcript`, `get_video_metadata` — reading the same static shards over disk or HTTP. It can point at one site or federate a whole hub. It never changes or hosts the site; see `mcp/README.md` for setup. ## [0.6.4] - 2026-07-06 - **Fixed: sites always opened in light mode until you re-picked a theme.** If you'd chosen dark (or left it on "system" with a dark device), the page still loaded light on every visit and only switched after you opened the theme menu again. The saved theme is now re-applied before the page paints, so your choice sticks across reloads — no flash, no re-toggling. diff --git a/mcp/README.md b/mcp/README.md @@ -0,0 +1,75 @@ +# yt-dlp-transcript-mcp + +An [MCP](https://modelcontextprotocol.io) server that exposes a transcript +archive — a single site, or a federated hub of sites — to Claude Code, Claude +Desktop, Cursor, and any other MCP client. + +It is a **local tool you run yourself**. It does not change the archive: it only +reads the site's already-published static JSON shards (`corpus.json` + +`transcripts/<slug>/…`), either from disk or over HTTP. Nothing is hosted for you. + +## Tools + +| Tool | What it does | +|------|--------------| +| `list_channels` | List channels (name, slug, video count; site in hub mode). | +| `search_transcripts` | Search captions for a term/phrase (or regex); returns matching videos with timestamped snippets. Optional `channel` filter. | +| `get_transcript` | One video's full transcript as clean markdown (metadata + timestamped captions). | +| `get_video_metadata` | One video's metadata (title, channel, date, duration, description, tags, source URL) without the transcript body. | + +## Data source (pick one) + +Resolved from flags or env — precedence hub > remote > local: + +| Mode | Flag | Env | Notes | +|------|------|-----|-------| +| Hub | `--hub <url>` | `TRANSCRIPT_HUB_URL` | Federate across every member of a hub's `corpus.json`. | +| Remote | `--remote <url>` | `TRANSCRIPT_SITE_URL` | One deployed site origin. | +| Local | `--local <dir>` | `TRANSCRIPT_LOCAL_DIR` | A composed public dir on disk (default `./export/public`). | + +## Run it + +From the monorepo (cwd is set to the package dir by `pnpm --filter … exec`, so +the TypeScript path alias resolves): + +```sh +# a deployed site +pnpm --filter yt-dlp-transcript-mcp exec tsx src/index.ts --remote https://rekietalyzer.pages.dev + +# a hub, federating every member site +pnpm --filter yt-dlp-transcript-mcp exec tsx src/index.ts --hub https://archilyzer.com + +# local shards on disk +pnpm --filter yt-dlp-transcript-mcp exec tsx src/index.ts --local ../export/public +``` + +Logs go to stderr; stdout is the MCP JSON-RPC channel. + +## Add to Claude Code + +```sh +claude mcp add rekietalyzer \ + --env TRANSCRIPT_SITE_URL=https://rekietalyzer.pages.dev \ + -- pnpm -C /ABS/PATH/TO/yt-dlp-transcript-browser --filter yt-dlp-transcript-mcp exec tsx src/index.ts +``` + +## Add to any MCP client (mcp.json) + +```json +{ + "mcpServers": { + "rekietalyzer": { + "command": "pnpm", + "args": [ + "-C", "/ABS/PATH/TO/yt-dlp-transcript-browser", + "--filter", "yt-dlp-transcript-mcp", + "exec", "tsx", "src/index.ts" + ], + "env": { "TRANSCRIPT_SITE_URL": "https://rekietalyzer.pages.dev" } + } + } +} +``` + +Swap `TRANSCRIPT_SITE_URL` for `TRANSCRIPT_HUB_URL` to federate a whole hub, or +`TRANSCRIPT_LOCAL_DIR` to read a local build. diff --git a/mcp/package.json b/mcp/package.json @@ -0,0 +1,22 @@ +{ + "name": "yt-dlp-transcript-mcp", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "MCP server exposing a yt-dlp transcript archive (or a federated hub) to Claude Code and other MCP clients — reads the site's static JSON shards.", + "bin": { + "yt-dlp-transcript-mcp": "src/index.ts" + }, + "scripts": { + "start": "tsx src/index.ts", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.12.0" + }, + "devDependencies": { + "@types/node": "^20.19.39", + "tsx": "^4.21.0", + "typescript": "^5.6.0" + } +} diff --git a/mcp/src/index.ts b/mcp/src/index.ts @@ -0,0 +1,57 @@ +#!/usr/bin/env tsx +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + LocalSource, + RemoteSource, + HubSource, + type ShardSource, +} from "./source"; +import { createServer } from "./server"; + +// Resolve the data source from flags or env. Precedence: hub > remote > local > +// positional > default. All logging goes to stderr — stdout is the MCP +// JSON-RPC channel and must not be polluted. +// +// --hub <url> | TRANSCRIPT_HUB_URL federate over a hub's member sites +// --remote <url> | TRANSCRIPT_SITE_URL one deployed site origin +// --local <dir> | TRANSCRIPT_LOCAL_DIR a composed public dir on disk +// <positional> an http(s) URL → remote, otherwise a local dir +// (default) ./export/public +export function resolveSource(argv: string[]): ShardSource { + const flag = (name: string): string | undefined => { + const i = argv.indexOf(name); + return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined; + }; + + const hub = flag("--hub") ?? process.env.TRANSCRIPT_HUB_URL; + if (hub) return new HubSource(hub); + + const remote = + flag("--remote") ?? flag("--url") ?? process.env.TRANSCRIPT_SITE_URL; + if (remote) return new RemoteSource(remote); + + const local = flag("--local") ?? process.env.TRANSCRIPT_LOCAL_DIR; + if (local) return new LocalSource(local); + + const positional = argv.find((a) => !a.startsWith("-")); + if (positional) { + return /^https?:\/\//i.test(positional) + ? new RemoteSource(positional) + : new LocalSource(positional); + } + + return new LocalSource("export/public"); +} + +async function main(): Promise<void> { + const source = resolveSource(process.argv.slice(2)); + console.error(`[yt-dlp-transcript-mcp] source: ${source.label}`); + const server = createServer(source); + await server.connect(new StdioServerTransport()); + console.error("[yt-dlp-transcript-mcp] ready on stdio"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/mcp/src/search.ts b/mcp/src/search.ts @@ -0,0 +1,180 @@ +import { formatDuration } from "yt-dlp-transcript-common/lib/format"; +import type { TranscriptDetail } from "yt-dlp-transcript-common/lib/transcripts"; +import type { ChannelRef, ShardSource } from "./source"; + +export type Snippet = { clock: string; seconds: number; text: string }; + +export type SearchHit = { + videoId: string; + channelSlug: string; + channelName: string; + siteTitle?: string; + title: string; + uploadDate: string; + webpageUrl?: string; + matches: number; + snippets: Snippet[]; +}; + +export type SearchResult = { + hits: SearchHit[]; + scanned: { channels: number; pages: number }; + truncated: boolean; +}; + +// A hard ceiling on shard pages fetched per query so a rare term over a large +// (or hub-wide) corpus can't run away. Reaching it sets `truncated`. +const MAX_PAGES = 400; + +function clock(seconds: number): string { + const s = Math.max(0, Math.floor(seconds)); + return s === 0 ? "0:00" : formatDuration(s); +} + +function makeMatcher(query: string, regex: boolean): (text: string) => boolean { + if (regex) { + const re = new RegExp(query, "i"); + return (t) => re.test(t); + } + const needle = query.toLowerCase(); + return (t) => t.toLowerCase().includes(needle); +} + +function truncate(text: string, max = 240): string { + const t = text.trim().replace(/\s+/g, " "); + return t.length > max ? t.slice(0, max - 1) + "…" : t; +} + +// Scan a source's transcript shards for `query`, returning up to `limit` matched +// videos (each with a few snippet cues + timestamps). A plain server-side scan +// with the site's own match semantics (substring by default, or a regex) — no +// browser index needed. Stops early at `limit` and at MAX_PAGES. +export async function searchTranscripts( + source: ShardSource, + opts: { + query: string; + channel?: string; + regex?: boolean; + limit?: number; + snippetsPerVideo?: number; + }, +): Promise<SearchResult> { + const limit = opts.limit ?? 20; + const snippetsPerVideo = opts.snippetsPerVideo ?? 4; + const match = makeMatcher(opts.query, opts.regex === true); + + let channels = await source.listChannels(); + if (opts.channel) { + const want = opts.channel.toLowerCase(); + channels = channels.filter( + (c) => + c.slug.toLowerCase() === want || + c.key.toLowerCase() === want || + c.name.toLowerCase() === want, + ); + } + + const hits: SearchHit[] = []; + let pagesScanned = 0; + let channelsScanned = 0; + let truncated = false; + + outer: for (const ch of channels) { + let manifest; + try { + manifest = await source.transcriptsManifest(ch); + } catch { + continue; // unreachable/missing channel — skip + } + channelsScanned++; + for (let page = 0; page < manifest.pageCount; page++) { + if (pagesScanned >= MAX_PAGES) { + truncated = true; + break outer; + } + let records: TranscriptDetail[]; + try { + records = await source.transcriptPage(ch, page); + } catch { + continue; + } + pagesScanned++; + for (const rec of records) { + const titleHit = match(rec.title ?? ""); + const snippets: Snippet[] = []; + let matches = 0; + for (const cue of rec.cues ?? []) { + if (!match(cue.text)) continue; + matches++; + if (snippets.length < snippetsPerVideo) { + snippets.push({ + clock: clock(cue.start), + seconds: cue.start, + text: truncate(cue.text), + }); + } + } + if (matches === 0 && !titleHit) continue; + hits.push({ + videoId: rec.id, + channelSlug: ch.slug, + channelName: ch.name, + ...(ch.siteTitle ? { siteTitle: ch.siteTitle } : {}), + title: rec.title, + uploadDate: rec.uploadDate, + webpageUrl: rec.webpageUrl, + matches: matches || 1, + snippets, + }); + if (hits.length >= limit) break outer; + } + } + } + + return { + hits, + scanned: { channels: channelsScanned, pages: pagesScanned }, + truncated, + }; +} + +// Locate a single video across the source's channels via each channel's +// slugToPage map, returning the full record + its channel. `channelHint` +// (slug/key/name) short-circuits the scan when the caller knows the channel. +export async function findVideo( + source: ShardSource, + videoId: string, + channelHint?: string, +): Promise<{ ch: ChannelRef; record: TranscriptDetail } | null> { + let channels = await source.listChannels(); + if (channelHint) { + const want = channelHint.toLowerCase(); + const filtered = channels.filter( + (c) => + c.slug.toLowerCase() === want || + c.key.toLowerCase() === want || + c.name.toLowerCase() === want, + ); + // Prefer the hinted channel(s), but fall back to a full scan if not found. + if (filtered.length > 0) channels = filtered; + } + for (const ch of channels) { + let manifest; + try { + manifest = await source.transcriptsManifest(ch); + } catch { + continue; + } + const page = manifest.slugToPage[videoId]; + if (page === undefined) continue; + let records: TranscriptDetail[]; + try { + records = await source.transcriptPage(ch, page); + } catch { + continue; + } + const record = records.find((r) => r.id === videoId); + if (record) return { ch, record }; + } + return null; +} diff --git a/mcp/src/server.ts b/mcp/src/server.ts @@ -0,0 +1,215 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { transcriptToMarkdown } from "yt-dlp-transcript-common/lib/transcriptToMarkdown"; +import { formatDate } from "yt-dlp-transcript-common/lib/format"; +import type { ShardSource } from "./source"; +import { searchTranscripts, findVideo } from "./search"; + +type ToolResult = { + content: { type: "text"; text: string }[]; + isError?: boolean; +}; + +function text(s: string): ToolResult { + return { content: [{ type: "text", text: s }] }; +} +function errorText(s: string): ToolResult { + return { content: [{ type: "text", text: s }], isError: true }; +} + +const TOOLS = [ + { + name: "list_channels", + description: + "List the channels in this transcript archive (in hub mode, across every " + + "federated member site). Returns each channel's display name, slug, video " + + "count, and owning site.", + inputSchema: { type: "object", properties: {}, additionalProperties: false }, + }, + { + name: "search_transcripts", + description: + "Search transcript captions for a term or phrase and return matching " + + "videos with timestamped snippets. Substring match by default; set regex " + + "to true for a case-insensitive regular expression. Optionally restrict to " + + "one channel (by slug or name).", + inputSchema: { + type: "object", + properties: { + query: { type: "string", description: "Term, phrase, or regex to find." }, + channel: { + type: "string", + description: "Optional channel slug or name to restrict the search to.", + }, + regex: { + type: "boolean", + description: "Treat query as a case-insensitive regex (default false).", + }, + limit: { + type: "number", + description: "Max matching videos to return (default 20).", + }, + }, + required: ["query"], + additionalProperties: false, + }, + }, + { + name: "get_transcript", + description: + "Fetch one video's full transcript as clean markdown (metadata header + " + + "timestamped captions). Provide the video id; optionally the channel to " + + "skip the cross-channel lookup.", + inputSchema: { + type: "object", + properties: { + video_id: { type: "string", description: "The video id." }, + channel: { type: "string", description: "Optional owning channel slug/name." }, + timestamps: { + type: "boolean", + description: "Prefix each caption line with a timestamp (default true).", + }, + }, + required: ["video_id"], + additionalProperties: false, + }, + }, + { + name: "get_video_metadata", + description: + "Fetch one video's metadata (title, channel, upload date, duration, " + + "description, tags, source URL) without the transcript body.", + inputSchema: { + type: "object", + properties: { + video_id: { type: "string", description: "The video id." }, + channel: { type: "string", description: "Optional owning channel slug/name." }, + }, + required: ["video_id"], + additionalProperties: false, + }, + }, +]; + +// Build a configured MCP server over a data source. The same four tools work +// for local / remote / hub sources — only the ShardSource differs. +export function createServer(source: ShardSource): Server { + const server = new Server( + { name: "yt-dlp-transcript-mcp", version: "0.1.0" }, + { capabilities: { tools: {} } }, + ); + + server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS })); + + server.setRequestHandler(CallToolRequestSchema, async (req) => { + const name = req.params.name; + const args = (req.params.arguments ?? {}) as Record<string, unknown>; + try { + switch (name) { + case "list_channels": + return await handleListChannels(source); + case "search_transcripts": + return await handleSearch(source, args); + case "get_transcript": + return await handleGetTranscript(source, args); + case "get_video_metadata": + return await handleGetMetadata(source, args); + default: + return errorText(`unknown tool: ${name}`); + } + } catch (e) { + return errorText(`${name} failed: ${(e as Error).message}`); + } + }); + + return server; +} + +async function handleListChannels(source: ShardSource): Promise<ToolResult> { + const channels = await source.listChannels(); + if (channels.length === 0) return text(`No channels found in ${source.label}.`); + const lines = channels.map((c) => { + const parts = [`slug: ${c.slug}`]; + if (c.videoCount != null) parts.push(`${c.videoCount} videos`); + if (c.siteTitle) parts.push(`site: ${c.siteTitle}`); + return `- ${c.name} (${parts.join(", ")})`; + }); + return text(`${channels.length} channel(s) in ${source.label}:\n${lines.join("\n")}`); +} + +async function handleSearch( + source: ShardSource, + args: Record<string, unknown>, +): Promise<ToolResult> { + const query = String(args.query ?? "").trim(); + if (!query) return errorText("query is required"); + const result = await searchTranscripts(source, { + query, + channel: typeof args.channel === "string" ? args.channel : undefined, + regex: args.regex === true, + limit: typeof args.limit === "number" ? args.limit : undefined, + }); + const footer = + `\n\n(scanned ${result.scanned.pages} page(s) across ` + + `${result.scanned.channels} channel(s)${result.truncated ? "; scan truncated at the page cap" : ""})`; + if (result.hits.length === 0) { + return text(`No matches for "${query}".${footer}`); + } + const blocks = result.hits.map((h) => { + const head = + `### ${h.title}\n` + + `- video_id: ${h.videoId} | channel: ${h.channelName}` + + (h.siteTitle ? ` | site: ${h.siteTitle}` : "") + + ` | uploaded: ${formatDate(h.uploadDate)} | matches: ${h.matches}` + + (h.webpageUrl ? `\n- source: ${h.webpageUrl}` : ""); + const snips = h.snippets.map((s) => ` - [${s.clock}] ${s.text}`).join("\n"); + return snips ? `${head}\n${snips}` : head; + }); + return text( + `${result.hits.length} video(s) matching "${query}":\n\n${blocks.join("\n\n")}${footer}`, + ); +} + +async function handleGetTranscript( + source: ShardSource, + args: Record<string, unknown>, +): Promise<ToolResult> { + const videoId = String(args.video_id ?? "").trim(); + if (!videoId) return errorText("video_id is required"); + const found = await findVideo( + source, + videoId, + typeof args.channel === "string" ? args.channel : undefined, + ); + if (!found) return errorText(`video not found: ${videoId}`); + const md = transcriptToMarkdown(found.record, { + timestamps: args.timestamps !== false, + includeTags: true, + }); + return text(md); +} + +async function handleGetMetadata( + source: ShardSource, + args: Record<string, unknown>, +): Promise<ToolResult> { + const videoId = String(args.video_id ?? "").trim(); + if (!videoId) return errorText("video_id is required"); + const found = await findVideo( + source, + videoId, + typeof args.channel === "string" ? args.channel : undefined, + ); + if (!found) return errorText(`video not found: ${videoId}`); + const { cues, ...meta } = found.record; + return text( + JSON.stringify( + { ...meta, channelName: found.ch.name, cueCount: cues?.length ?? 0 }, + null, + 2, + ), + ); +} diff --git a/mcp/src/source.ts b/mcp/src/source.ts @@ -0,0 +1,203 @@ +import { readFile, readdir } from "node:fs/promises"; +import path from "node:path"; +import { + transcriptPageFileName, + type ChannelTranscriptsManifest, +} from "yt-dlp-transcript-common/lib/manifest"; +import type { TranscriptDetail } from "yt-dlp-transcript-common/lib/transcripts"; + +// A channel the source can serve. `siteId`/`siteUrl` are only populated in hub +// mode (so results can be attributed to the owning member site); `key` is the +// stable, source-unique handle a tool passes back to fetch this channel's data. +export type ChannelRef = { + key: string; + slug: string; + name: string; + videoCount?: number; + siteId?: string; + siteTitle?: string; + siteUrl?: string; +}; + +// A read-only view over a transcript corpus's paginated JSON shards. Three +// implementations (local dir / remote origin / federated hub) all speak the +// same three-call contract, which mirrors the documented shard scheme in +// corpus.json: list channels, get a channel's manifest (slug -> page map), get +// a page of full transcript records. +export interface ShardSource { + readonly label: string; + listChannels(): Promise<ChannelRef[]>; + transcriptsManifest(ch: ChannelRef): Promise<ChannelTranscriptsManifest>; + transcriptPage(ch: ChannelRef, page: number): Promise<TranscriptDetail[]>; +} + +// Shape of the channels we read out of a site corpus.json (Layer 1). Kept loose +// — we only need slug/name/count. +type CorpusJsonChannel = { slug: string; name?: string; videoCount?: number }; +type SiteCorpusJson = { channels?: CorpusJsonChannel[] }; +type HubCorpusJson = { + kind?: string; + sites?: { siteId: string; title: string; url: string }[]; +}; + +// ─── Local: read composed shards from a directory on disk ─── +// `dir` is a composed public dir (or any dir containing transcripts/<slug>/…). +// Prefers corpus.json for the channel list (names + counts); falls back to +// listing the transcripts/ subdirectories so it works even pre-Layer-1. +export class LocalSource implements ShardSource { + readonly label: string; + constructor(private dir: string) { + this.label = `local:${dir}`; + } + + async listChannels(): Promise<ChannelRef[]> { + try { + const raw = await readFile(path.join(this.dir, "corpus.json"), "utf8"); + const corpus = JSON.parse(raw) as SiteCorpusJson; + if (Array.isArray(corpus.channels) && corpus.channels.length > 0) { + return corpus.channels.map((c) => ({ + key: c.slug, + slug: c.slug, + name: c.name ?? c.slug, + videoCount: c.videoCount, + })); + } + } catch { + // no corpus.json — fall back to a directory listing + } + const transcriptsDir = path.join(this.dir, "transcripts"); + let entries: string[] = []; + try { + entries = await readdir(transcriptsDir); + } catch { + return []; + } + const channels: ChannelRef[] = []; + for (const slug of entries.sort()) { + // A channel dir has a manifest.json; skip stray files. + try { + await readFile(path.join(transcriptsDir, slug, "manifest.json"), "utf8"); + channels.push({ key: slug, slug, name: slug }); + } catch { + // not a channel dir + } + } + return channels; + } + + async transcriptsManifest(ch: ChannelRef): Promise<ChannelTranscriptsManifest> { + const raw = await readFile( + path.join(this.dir, "transcripts", ch.slug, "manifest.json"), + "utf8", + ); + return JSON.parse(raw) as ChannelTranscriptsManifest; + } + + async transcriptPage(ch: ChannelRef, page: number): Promise<TranscriptDetail[]> { + const raw = await readFile( + path.join(this.dir, "transcripts", ch.slug, transcriptPageFileName(page)), + "utf8", + ); + return JSON.parse(raw) as TranscriptDetail[]; + } +} + +// ─── Remote: fetch shards from a deployed site origin over HTTP ─── +export class RemoteSource implements ShardSource { + readonly label: string; + private base: string; + constructor(baseUrl: string) { + this.base = baseUrl.replace(/\/+$/, ""); + this.label = `remote:${this.base}`; + } + + private async getJson<T>(p: string): Promise<T> { + const res = await fetch(`${this.base}${p}`); + if (!res.ok) { + throw new Error(`GET ${this.base}${p} -> ${res.status} ${res.statusText}`); + } + return (await res.json()) as T; + } + + async listChannels(): Promise<ChannelRef[]> { + const corpus = await this.getJson<SiteCorpusJson>("/corpus.json"); + return (corpus.channels ?? []).map((c) => ({ + key: c.slug, + slug: c.slug, + name: c.name ?? c.slug, + videoCount: c.videoCount, + siteUrl: this.base, + })); + } + + transcriptsManifest(ch: ChannelRef): Promise<ChannelTranscriptsManifest> { + return this.getJson(`/transcripts/${ch.slug}/manifest.json`); + } + + transcriptPage(ch: ChannelRef, page: number): Promise<TranscriptDetail[]> { + return this.getJson(`/transcripts/${ch.slug}/${transcriptPageFileName(page)}`); + } +} + +// ─── Hub: federate over every member site listed in the hub corpus.json ─── +// Each member is its own RemoteSource; channels are namespaced by site so keys +// stay unique, and manifest/page calls dispatch to the owning member. +export class HubSource implements ShardSource { + readonly label: string; + private hubBase: string; + private members = new Map<string, RemoteSource>(); // siteId -> source + + constructor(hubUrl: string) { + this.hubBase = hubUrl.replace(/\/+$/, ""); + this.label = `hub:${this.hubBase}`; + } + + private memberFor(siteId: string): RemoteSource { + const m = this.members.get(siteId); + if (!m) throw new Error(`unknown hub member site: ${siteId}`); + return m; + } + + async listChannels(): Promise<ChannelRef[]> { + const res = await fetch(`${this.hubBase}/corpus.json`); + if (!res.ok) { + throw new Error( + `GET ${this.hubBase}/corpus.json -> ${res.status} ${res.statusText}`, + ); + } + const hub = (await res.json()) as HubCorpusJson; + const sites = hub.sites ?? []; + const all: ChannelRef[] = []; + // Sequential member fetches keep it simple and polite; the channel count is + // small. A failing member is skipped rather than failing the whole list. + for (const site of sites) { + const remote = new RemoteSource(site.url); + this.members.set(site.siteId, remote); + try { + const channels = await remote.listChannels(); + for (const c of channels) { + all.push({ + ...c, + key: `${site.siteId}/${c.slug}`, + siteId: site.siteId, + siteTitle: site.title, + siteUrl: site.url, + }); + } + } catch { + // skip an unreachable member + } + } + return all; + } + + transcriptsManifest(ch: ChannelRef): Promise<ChannelTranscriptsManifest> { + if (!ch.siteId) throw new Error("hub channel ref missing siteId"); + return this.memberFor(ch.siteId).transcriptsManifest(ch); + } + + transcriptPage(ch: ChannelRef, page: number): Promise<TranscriptDetail[]> { + if (!ch.siteId) throw new Error("hub channel ref missing siteId"); + return this.memberFor(ch.siteId).transcriptPage(ch, page); + } +} diff --git a/mcp/tsconfig.json b/mcp/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "lib": ["esnext"], + "types": ["node"], + "baseUrl": ".", + "paths": { + "yt-dlp-transcript-common/*": ["../common/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml @@ -277,6 +277,22 @@ importers: specifier: ^5.9.3 version: 5.9.3 + mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.12.0 + version: 1.29.0(zod@4.3.6) + devDependencies: + '@types/node': + specifier: ^20.19.39 + version: 20.19.39 + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.6.0 + version: 5.9.3 + packages: '@alloc/quick-lru@5.2.0': @@ -575,6 +591,12 @@ packages: '@harperfast/extended-iterable@1.0.3': resolution: {integrity: sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw==} + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -799,6 +821,16 @@ packages: cpu: [x64] os: [win32] + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} cpu: [arm64] @@ -1952,6 +1984,10 @@ packages: '@zeit/schemas@2.36.0': resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1962,6 +1998,14 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -2067,6 +2111,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + boxen@7.0.0: resolution: {integrity: sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==} engines: {node: '>=14.16'} @@ -2176,9 +2224,33 @@ packages: resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} engines: {node: '>= 0.6'} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + 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'} + + 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'} @@ -2292,6 +2364,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -2313,6 +2389,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.344: resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==} @@ -2322,6 +2401,10 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + enhanced-resolve@5.21.0: resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} engines: {node: '>=10.13.0'} @@ -2367,6 +2450,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2495,9 +2581,21 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -2506,6 +2604,16 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2550,6 +2658,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2568,6 +2680,14 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + 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'} + fs-extra@11.3.4: resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} engines: {node: '>=14.14'} @@ -2690,6 +2810,14 @@ packages: hls.js@1.6.16: resolution: {integrity: sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==} + hono@4.12.27: + resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2698,6 +2826,10 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2714,6 +2846,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -2725,6 +2860,14 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2809,6 +2952,9 @@ packages: resolution: {integrity: sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2875,6 +3021,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2896,6 +3045,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -3048,9 +3200,17 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -3074,6 +3234,10 @@ packages: resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -3118,6 +3282,10 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + next@16.2.3: resolution: {integrity: sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==} engines: {node: '>=20.9.0'} @@ -3193,10 +3361,17 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + on-headers@1.1.0: resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} engines: {node: '>= 0.8'} + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -3232,6 +3407,10 @@ packages: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3253,6 +3432,9 @@ packages: path-to-regexp@3.3.0: resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3264,6 +3446,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + playwright-core@1.59.1: resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} engines: {node: '>=18'} @@ -3297,10 +3483,18 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -3321,6 +3515,14 @@ packages: resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} engines: {node: '>= 0.6'} + 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'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -3436,6 +3638,10 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3454,6 +3660,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -3466,9 +3675,17 @@ packages: engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + serve-handler@6.1.7: resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + serve@14.2.6: resolution: {integrity: sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==} engines: {node: '>= 14'} @@ -3486,6 +3703,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3514,6 +3734,10 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3534,6 +3758,10 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -3639,6 +3867,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -3667,6 +3899,10 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -3710,6 +3946,10 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -3788,6 +4028,9 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -3803,6 +4046,11 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -4077,6 +4325,10 @@ snapshots: '@harperfast/extended-iterable@1.0.3': {} + '@hono/node-server@1.19.14(hono@4.12.27)': + dependencies: + hono: 4.12.27 + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -4230,6 +4482,28 @@ snapshots: '@lmdb/lmdb-win32-x64@3.5.4': optional: true + '@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.27) + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.0 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.27 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 4.3.6 + zod-to-json-schema: 3.25.2(zod@4.3.6) + transitivePeerDependencies: + - supports-color + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true @@ -5356,12 +5630,21 @@ snapshots: '@zeit/schemas@2.36.0': {} + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 acorn@8.16.0: {} + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -5487,6 +5770,20 @@ snapshots: baseline-browser-mapping@2.10.23: {} + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + boxen@7.0.0: dependencies: ansi-align: 3.0.1 @@ -5611,8 +5908,23 @@ snapshots: content-disposition@0.5.2: {} + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + 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 @@ -5711,6 +6023,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + depd@2.0.0: {} + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -5732,12 +6046,16 @@ snapshots: eastasianwidth@0.2.0: {} + ee-first@1.1.1: {} + electron-to-chromium@1.5.344: {} emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + enhanced-resolve@5.21.0: dependencies: graceful-fs: 4.2.11 @@ -5875,6 +6193,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} @@ -6131,8 +6451,16 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + eventemitter3@4.0.7: {} + eventsource-parser@3.1.0: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.0 + execa@5.1.1: dependencies: cross-spawn: 7.0.6 @@ -6160,6 +6488,44 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + 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.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + 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 + fast-deep-equal@3.1.3: {} fast-equals@5.4.0: {} @@ -6198,6 +6564,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -6216,6 +6593,10 @@ snapshots: dependencies: is-callable: 1.2.7 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fs-extra@11.3.4: dependencies: graceful-fs: 4.2.11 @@ -6333,10 +6714,24 @@ snapshots: hls.js@1.6.16: {} + hono@4.12.27: {} + + 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 + human-signals@2.1.0: {} human-signals@8.0.1: {} + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -6348,6 +6743,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + ini@1.3.8: {} internal-slot@1.1.0: @@ -6358,6 +6755,10 @@ snapshots: internmap@2.0.3: {} + ip-address@10.2.0: {} + + ipaddr.js@1.9.1: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.9 @@ -6439,6 +6840,8 @@ snapshots: is-port-reachable@4.0.0: {} + is-promise@4.0.0: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -6503,6 +6906,8 @@ snapshots: jiti@2.6.1: {} + jose@6.2.3: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -6517,6 +6922,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -6651,8 +7058,12 @@ snapshots: math-intrinsics@1.1.0: {} + media-typer@1.1.0: {} + memoize-one@5.2.1: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -6670,6 +7081,10 @@ snapshots: dependencies: mime-db: 1.33.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + mimic-fn@2.1.0: {} minimatch@10.2.5: @@ -6710,6 +7125,8 @@ snapshots: negotiator@0.6.4: {} + negotiator@1.0.0: {} + next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.3 @@ -6801,8 +7218,16 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + on-headers@1.1.0: {} + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -6842,6 +7267,8 @@ snapshots: parse-ms@4.0.0: {} + parseurl@1.3.3: {} + path-exists@4.0.0: {} path-is-inside@1.0.2: {} @@ -6854,12 +7281,16 @@ snapshots: path-to-regexp@3.3.0: {} + path-to-regexp@8.4.2: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} picomatch@4.0.4: {} + pkce-challenge@5.0.1: {} + playwright-core@1.59.1: {} playwright@1.59.1: @@ -6894,8 +7325,18 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + queue-microtask@1.2.3: {} radix-ui@1.6.0(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -6963,6 +7404,15 @@ snapshots: range-parser@1.2.0: {} + 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 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -7099,6 +7549,16 @@ snapshots: reusify@1.1.0: {} + 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 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -7124,12 +7584,30 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safer-buffer@2.1.2: {} + scheduler@0.27.0: {} semver@6.3.1: {} semver@7.7.4: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + 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.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + serve-handler@6.1.7: dependencies: bytes: 3.0.0 @@ -7140,6 +7618,15 @@ snapshots: path-to-regexp: 3.3.0 range-parser: 1.2.0 + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + serve@14.2.6: dependencies: '@zeit/schemas': 2.36.0 @@ -7178,6 +7665,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setprototypeof@1.2.0: {} + sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -7244,6 +7733,14 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + 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 + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -7257,6 +7754,8 @@ snapshots: stable-hash@0.0.5: {} + statuses@2.0.2: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -7372,6 +7871,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -7400,6 +7901,12 @@ snapshots: type-fest@2.19.0: {} + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -7459,6 +7966,8 @@ snapshots: universalify@2.0.1: {} + unpipe@1.0.0: {} + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -7591,6 +8100,8 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.2.0 + wrappy@1.0.2: {} + yallist@3.1.1: {} yocto-queue@0.1.0: {} @@ -7599,6 +8110,10 @@ snapshots: yoctocolors@2.1.2: {} + zod-to-json-schema@3.25.2(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod-validation-error@4.0.2(zod@4.3.6): dependencies: zod: 4.3.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml @@ -3,6 +3,7 @@ packages: - editor - export - homepage + - mcp allowBuilds: esbuild: true