From 914027bf809c3e457575ab3bcee0c289b0d8af09 Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Mon, 3 Aug 2026 23:10:36 -0700 Subject: [PATCH 1/3] extract shared operations layer from cli commands move the routed orchestration - device resolution, runtime-first dispatch, web api fallback, and relative seek and volume semantics - out of anonymous commander action closures into src/cli/operations, so a second frontend can drive the same code paths. commands become thin parse-and-emit adapters and emitted shapes, messages, and routing behavior are unchanged. the new operations tests exercise the routing against a real control server and a mocked web api. --- src/cli/commands/discovery.ts | 407 +--------------------- src/cli/commands/library.ts | 14 +- src/cli/commands/playback.ts | 559 +++--------------------------- src/cli/commands/playlists.ts | 48 +-- src/cli/commands/queue-devices.ts | 190 +--------- src/cli/operations/catalog.ts | 372 ++++++++++++++++++++ src/cli/operations/devices.ts | 76 ++++ src/cli/operations/playback.ts | 465 +++++++++++++++++++++++++ src/cli/operations/playlists.ts | 57 +++ src/cli/operations/queue.ts | 84 +++++ src/cli/operations/types.ts | 12 + src/cli/output.ts | 45 +++ src/cli/session.ts | 4 + src/cli/support.ts | 12 + src/cli/values.ts | 17 + test/cli-discovery.test.ts | 2 +- test/cli-queue.test.ts | 2 +- test/operations-playback.test.ts | 288 +++++++++++++++ 18 files changed, 1529 insertions(+), 1125 deletions(-) create mode 100644 src/cli/operations/catalog.ts create mode 100644 src/cli/operations/devices.ts create mode 100644 src/cli/operations/playback.ts create mode 100644 src/cli/operations/playlists.ts create mode 100644 src/cli/operations/queue.ts create mode 100644 src/cli/operations/types.ts create mode 100644 test/operations-playback.test.ts diff --git a/src/cli/commands/discovery.ts b/src/cli/commands/discovery.ts index d36aa5d..a6ce2da 100644 --- a/src/cli/commands/discovery.ts +++ b/src/cli/commands/discovery.ts @@ -1,129 +1,24 @@ import { Command, Option } from "commander"; -import { - albumTracks, - artistAlbums, - audiobookChapters, - audiobookDetails, - showDetails, - showEpisodes, -} from "../../api/catalog.ts"; -import { fetchLyrics } from "../../api/lyrics.ts"; -import { playlistItems } from "../../api/playlists.ts"; -import { search, type SearchType } from "../../api/search.ts"; +import type { SearchType } from "../../api/search.ts"; import { artistLine, - isTrack, - type Episode, type FullArtist, type Page, - type PlayableItem, - type SimpleAlbum, - type SimpleArtist, - type SimpleAudiobook, - type SimpleShow, type Track, } from "../../api/types.ts"; -import { tryRuntimeRequest } from "../../runtime/control.ts"; -import { unavailable, usageError } from "../errors.ts"; -import { formatDuration, normalizeItem, type CliIo } from "../output.ts"; -import { cliSession } from "../session.ts"; -import { integer, spotifyReference, timestampMs } from "../values.ts"; +import { usageError } from "../errors.ts"; import { - normalizePlaylistDetails, - openablePlaylistDetails, - outputFor, - playlistHeader, - table, -} from "../support.ts"; + normalizeArtist, + normalizeItem, + type CliIo, +} from "../output.ts"; +import { lyricsFor, resourceDetails, searchCatalog } from "../operations/catalog.ts"; +import { cliSession } from "../session.ts"; +import { integer, timestampMs } from "../values.ts"; +import { outputFor, table } from "../support.ts"; -/** `search --type all` covers the four music types; spoken-word types are requested by name. */ -const DEFAULT_SEARCH_TYPES: readonly SearchType[] = ["track", "artist", "album", "playlist"]; -const SEARCH_LIMIT_MAX = 50; const HISTORY_LIMIT_MAX = 50; -function normalizeEpisode(episode: Episode): Record { - return normalizeItem(episode) ?? {}; -} - -function normalizeShow(show: SimpleShow): Record { - return { - type: "show", - id: show.id, - uri: show.uri, - name: show.name, - publisher: show.publisher ?? null, - description: show.description ?? null, - totalEpisodes: show.total_episodes ?? null, - }; -} - -function normalizeAudiobook(audiobook: SimpleAudiobook): Record { - return { - type: "audiobook", - id: audiobook.id, - uri: audiobook.uri, - name: audiobook.name, - authors: (audiobook.authors ?? []).map((author) => author.name), - publisher: audiobook.publisher ?? null, - totalChapters: audiobook.total_chapters ?? null, - }; -} - -function normalizeArtist(artist: FullArtist): Record { - return { - type: "artist", - id: artist.id, - uri: artist.uri, - name: artist.name, - genres: artist.genres ?? [], - followers: artist.followers?.total ?? null, - }; -} - -/** - * Narrow a runtime status snapshot to the current track for lyric lookup. - * - * The runtime's item uses the same normalized shape the CLI emits, so it doubles as the emitted - * `track` record without another Web API read. - */ -export function runtimeLyricsTrack(value: unknown): { - name: string; - artists: string[]; - artist: string; - durationMs: number; - record: Record; -} { - const state = - value !== null && typeof value === "object" - ? (value as Record) - : {}; - const raw = state["item"]; - if (raw === null || raw === undefined || typeof raw !== "object") - throw unavailable("The current item is not a track."); - const item = raw as Record; - const name = item["name"]; - const durationMs = item["durationMs"]; - if ( - item["type"] !== "track" || - typeof name !== "string" || - typeof durationMs !== "number" - ) { - throw unavailable("The current item is not a track."); - } - const artists = Array.isArray(item["artists"]) - ? item["artists"].filter( - (artist): artist is string => typeof artist === "string", - ) - : []; - return { - name, - artists, - artist: typeof item["artist"] === "string" ? item["artist"] : artists.join(", "), - durationMs, - record: item, - }; -} - export function registerDiscovery(program: Command, io: CliIo): void { program .command("search ") @@ -153,107 +48,13 @@ export function registerDiscovery(program: Command, io: CliIo): void { options.limit === undefined ? undefined : integer(options.limit, "limit", 1); - if (limit !== undefined && limit > SEARCH_LIMIT_MAX) { - throw usageError(`Search limit cannot exceed ${SEARCH_LIMIT_MAX}.`); - } - const types: readonly SearchType[] = - options.type === "all" - ? DEFAULT_SEARCH_TYPES - : [options.type as SearchType]; - const { client } = await cliSession(); - const results = await search(client, words.join(" "), { - types, + const types: readonly SearchType[] | undefined = + options.type === "all" ? undefined : [options.type as SearchType]; + const result = await searchCatalog(words.join(" "), { + ...(types !== undefined ? { types } : {}), ...(limit !== undefined ? { limit } : {}), }); - const data = { - tracks: results.tracks.map(normalizeItem), - artists: results.artists, - albums: results.albums, - playlists: results.playlists, - shows: results.shows.map(normalizeShow), - episodes: results.episodes.map(normalizeEpisode), - audiobooks: results.audiobooks.map(normalizeAudiobook), - }; - const sections: string[] = []; - if (results.tracks.length > 0) - sections.push( - `Tracks\n${table( - ["TITLE", "ARTIST", "URI"], - results.tracks.map((item) => [ - item.name, - artistLine(item), - item.uri, - ]), - )}`, - ); - if (results.artists.length > 0) - sections.push( - `Artists\n${table( - ["NAME", "URI"], - results.artists.map((item) => [item.name, item.uri]), - )}`, - ); - if (results.albums.length > 0) - sections.push( - `Albums\n${table( - ["NAME", "YEAR", "URI"], - results.albums.map((item) => [ - item.name, - item.release_date ?? "", - item.uri, - ]), - )}`, - ); - if (results.playlists.length > 0) - sections.push( - `Playlists\n${table( - ["NAME", "OWNER", "URI"], - results.playlists.map((item) => [ - item.name, - item.owner?.display_name ?? "", - item.uri, - ]), - )}`, - ); - if (results.shows.length > 0) - sections.push( - `Shows\n${table( - ["NAME", "PUBLISHER", "URI"], - results.shows.map((item) => [ - item.name, - item.publisher ?? "", - item.uri, - ]), - )}`, - ); - if (results.episodes.length > 0) - sections.push( - `Episodes\n${table( - ["TITLE", "DATE", "TIME", "URI"], - results.episodes.map((item) => [ - item.name, - item.release_date ?? "", - formatDuration(item.duration_ms), - item.uri, - ]), - )}`, - ); - if (results.audiobooks.length > 0) - sections.push( - `Audiobooks\n${table( - ["NAME", "AUTHOR", "URI"], - results.audiobooks.map((item) => [ - item.name, - (item.authors ?? []).map((author) => author.name).join(", "), - item.uri, - ]), - )}`, - ); - outputFor(command, io).emit( - "search", - data, - sections.length === 0 ? "No results." : sections.join("\n\n"), - ); + outputFor(command, io).emit("search", result.data, result.message); }, ); @@ -261,186 +62,16 @@ export function registerDiscovery(program: Command, io: CliIo): void { .command("show ") .description("Show a Spotify resource and its playable contents") .action(async (target: string, _options, command: Command) => { - const ref = spotifyReference(target); - const { client } = await cliSession(); - let data: unknown; - let human: string; - switch (ref.kind) { - case "track": - case "episode": { - const item = await client.get( - `/${ref.kind}s/${ref.id}`, - ); - data = normalizeItem(item); - human = `${item.name} — ${artistLine(item)}\n${formatDuration(item.duration_ms)} · ${item.uri}`; - break; - } - case "album": { - const [album, tracks] = await Promise.all([ - client.get( - `/albums/${ref.id}`, - ), - albumTracks(client, ref.id), - ]); - data = { ...album, tracks }; - human = `${album.name}${album.release_date ? ` (${album.release_date})` : ""}\n\n${table( - ["#", "TITLE", "ARTIST", "TIME", "URI"], - tracks.map((item) => [ - item.track_number, - item.name, - item.artists.map((artist) => artist.name).join(", "), - formatDuration(item.duration_ms), - item.uri, - ]), - )}`; - break; - } - case "artist": { - const [artist, albums] = await Promise.all([ - client.get(`/artists/${ref.id}`), - artistAlbums(client, ref.id), - ]); - data = { ...artist, albums }; - human = `${artist.name}\n\n${table( - ["RELEASE", "DATE", "TRACKS", "URI"], - albums.map((album) => [ - album.name, - album.release_date ?? "", - album.total_tracks ?? "", - album.uri, - ]), - )}`; - break; - } - case "playlist": { - // Ownership first: the items read is permanently refused for foreign playlists, so it - // must not be spent before the metadata proves it can succeed. - const details = await openablePlaylistDetails(ref.id); - const entries = await playlistItems(client, ref.id); - data = { - playlist: normalizePlaylistDetails(details), - items: entries.map((entry) => ({ - position: entry.position, - isLocal: entry.isLocal, - item: normalizeItem(entry.item), - })), - }; - human = `${playlistHeader(details)}\n\n${table( - ["#", "TITLE", "ARTIST", "TIME", "URI"], - entries.map((entry) => [ - entry.position + 1, - entry.item.name, - artistLine(entry.item), - formatDuration(entry.item.duration_ms), - entry.item.uri, - ]), - )}`; - break; - } - case "show": { - const [show, episodes] = await Promise.all([ - showDetails(client, ref.id), - showEpisodes(client, ref.id), - ]); - data = { - ...normalizeShow(show), - episodes: episodes.map(normalizeEpisode), - }; - human = `${show.name}${show.publisher ? ` — ${show.publisher}` : ""}\n${show.uri}\n\nLatest episodes\n${table( - ["TITLE", "DATE", "TIME", "URI"], - episodes.map((episode) => [ - episode.name, - episode.release_date ?? "", - formatDuration(episode.duration_ms), - episode.uri, - ]), - )}`; - break; - } - case "audiobook": { - const [audiobook, chapters] = await Promise.all([ - audiobookDetails(client, ref.id), - audiobookChapters(client, ref.id), - ]); - data = { ...normalizeAudiobook(audiobook), chapters }; - const authors = (audiobook.authors ?? []) - .map((author) => author.name) - .join(", "); - human = `${audiobook.name}${authors === "" ? "" : ` — ${authors}`}\n${audiobook.uri}\n\nChapters\n${table( - ["#", "TITLE", "TIME", "URI"], - // Positional numbering: Spotify's chapter_number is zero-based and the page arrives in - // reading order anyway. - chapters.map((chapter, index) => [ - index + 1, - chapter.name, - formatDuration(chapter.duration_ms), - chapter.uri, - ]), - )}`; - break; - } - default: - throw usageError( - `Spotify ${ref.kind} resources are not supported by show.`, - ); - } - outputFor(command, io).emit("show", data, human); + const result = await resourceDetails(target); + outputFor(command, io).emit("show", result.data, result.message); }); program .command("lyrics [track]") .description("Show lyrics for a track or the current track") .action(async (target: string | undefined, _options, command: Command) => { - let subject: { - name: string; - artists: string[]; - artist: string; - durationMs: number; - record: Record | null; - }; - if (target !== undefined) { - const ref = spotifyReference(target, "track"); - const track = await (await cliSession()).client.get( - `/tracks/${ref.id}`, - ); - subject = { - name: track.name, - artists: track.artists.map((artist) => artist.name), - artist: artistLine(track), - durationMs: track.duration_ms, - record: normalizeItem(track), - }; - } else { - // While the local receiver is playing, native events outrun `/me/player`; asking the Web - // API here can return the previous track across a change. The runtime item is - // authoritative and already carries everything lyric lookup needs. - const runtime = await tryRuntimeRequest("status"); - if (runtime.connected) { - subject = runtimeLyricsTrack(runtime.value); - } else { - const item = (await (await cliSession()).player.state("foreground")) - ?.item; - if (item === null || item === undefined || !isTrack(item)) - throw unavailable("The current item is not a track."); - subject = { - name: item.name, - artists: item.artists.map((artist) => artist.name), - artist: artistLine(item), - durationMs: item.duration_ms, - record: normalizeItem(item), - }; - } - } - const lyrics = await fetchLyrics({ - name: subject.name, - artists: subject.artists, - durationMs: subject.durationMs, - }); - outputFor(command, io).emit( - "lyrics", - { track: subject.record, lyrics }, - `${subject.name} — ${subject.artist}\n${lyrics.source}${lyrics.synced ? " · synced" : ""}\n\n${lyrics.lines.map((line) => line.text).join("\n")}`, - ); + const result = await lyricsFor(target); + outputFor(command, io).emit("lyrics", result.data, result.message); }); const history = program diff --git a/src/cli/commands/library.ts b/src/cli/commands/library.ts index d0e7113..35a3b44 100644 --- a/src/cli/commands/library.ts +++ b/src/cli/commands/library.ts @@ -1,22 +1,10 @@ import { Command } from "commander"; import { removeLibraryItems, saveLibraryItems } from "../../api/library.ts"; -import { usageError } from "../errors.ts"; import type { CliIo } from "../output.ts"; import { cliSession } from "../session.ts"; -import { spotifyReference } from "../values.ts"; +import { libraryUri } from "../values.ts"; import { mutation } from "../support.ts"; -/** The kinds Spotify's library endpoints accept, shared by save and remove. */ -const LIBRARY_KINDS = ["track", "episode", "album", "show", "audiobook"] as const; - -function libraryUri(item: string, refusal: string): string { - const ref = spotifyReference(item); - if (!(LIBRARY_KINDS as readonly string[]).includes(ref.kind)) { - throw usageError(`Spotify ${ref.kind} resources ${refusal}.`); - } - return ref.uri; -} - export function registerLibrary(program: Command, io: CliIo): void { const library = program .command("library") diff --git a/src/cli/commands/playback.ts b/src/cli/commands/playback.ts index c0f8983..3cbb857 100644 --- a/src/cli/commands/playback.ts +++ b/src/cli/commands/playback.ts @@ -1,38 +1,29 @@ import { Command } from "commander"; -import { nextRepeatState } from "../../api/player.ts"; -import type { RepeatState } from "../../api/types.ts"; -import { runtimeRequest, tryRuntimeRequest } from "../../runtime/control.ts"; -import { ExitCode, unavailable, usageError } from "../errors.ts"; +import { runtimeRequest } from "../../runtime/control.ts"; +import { ExitCode, usageError } from "../errors.ts"; +import { normalizeRuntimePlayback, type CliIo } from "../output.ts"; import { - formatDuration, - normalizePlayback, - normalizeRuntimePlayback, - playbackText, - type CliIo, -} from "../output.ts"; -import { cliSession } from "../session.ts"; + openPlayback, + pausePlayback, + playbackStatus, + REPEAT_MODES, + seekPlayback, + setRepeat, + setShuffle, + setVolume, + skip, + startPlayback, + togglePlayback, +} from "../operations/playback.ts"; import { - booleanValue, - integer, - signedDurationMs, - signedPercent, - spotifyReference, -} from "../values.ts"; -import { - currentState, enumValue, - inactiveReceiver, mutation, outputFor, - resolveDeviceTarget, - runtimeBoolean, - runtimeNumber, runtimePlaybackText, wait, type RunState, } from "../support.ts"; - -const REPEAT_MODES: RepeatState[] = ["off", "context", "track"]; +import { booleanValue, integer, signedDurationMs, signedPercent } from "../values.ts"; export function registerPlayback( program: Command, @@ -87,21 +78,8 @@ export function registerPlayback( } return; } - const runtime = await tryRuntimeRequest("status"); - if (runtime.connected) { - outputFor(command, io).emit( - "status", - normalizeRuntimePlayback(runtime.value), - runtimePlaybackText(runtime.value), - ); - return; - } - const state = await currentState(); - outputFor(command, io).emit( - "status", - normalizePlayback(state), - playbackText(state), - ); + const status = await playbackStatus(); + outputFor(command, io).emit("status", status.data, status.message); }, ); @@ -119,106 +97,13 @@ export function registerPlayback( const index = options.index === undefined ? undefined - : integer(options.index, "index", 1) - 1; - if (index !== undefined && target === undefined) { - throw usageError( - "--index requires an album or playlist target.", - ); - } - const ref = target === undefined ? undefined : spotifyReference(target); - if ( - ref !== undefined && - index !== undefined && - ref.kind !== "album" && - ref.kind !== "playlist" - ) { - throw usageError( - "--index is only valid for album or playlist contexts.", - ); - } - if (ref?.kind === "show") { - throw usageError( - "Spotify shows cannot be played as a context. Play one of its episodes instead — `spotuify show ` lists them.", - ); - } - if ( - ref !== undefined && - !["track", "episode", "album", "artist", "playlist"].includes( - ref.kind, - ) - ) { - throw usageError(`Spotify ${ref.kind} resources cannot be played.`); - } - const runtimeParams = - ref === undefined - ? {} - : ref.kind === "track" || ref.kind === "episode" - ? { uris: [ref.uri] } - : { contextUri: ref.uri, offset: index }; - const message = - target === undefined ? "Playback resumed." : `Playing ${target}.`; - // A selector naming the embedded receiver routes through its runtime: transfer natively - // when it does not hold the session, then start playback inside the serialized stream. - const resolved = - options.device === undefined - ? undefined - : await resolveDeviceTarget(options.device); - if (resolved?.route === "local") { - if (!resolved.active) { - await runtimeRequest("device.transfer", { - selector: resolved.id, - play: false, - }); - } - const state = await runtimeRequest("play", runtimeParams); - mutation( - command, - io, - "play", - { - source: "runtime", - target: target ?? null, - deviceId: resolved.id, - state, - }, - message, - ); - return; - } - if (resolved === undefined) { - const runtime = await tryRuntimeRequest("play", runtimeParams); - if (runtime.connected) { - mutation( - command, - io, - "play", - { - source: "runtime", - target: target ?? null, - state: runtime.value, - }, - message, - ); - return; - } - } - const { player } = await cliSession(); - const deviceId = resolved?.device.id; - if (ref === undefined) await player.play({ deviceId }); - else { - if (ref.kind === "track" || ref.kind === "episode") { - await player.play({ deviceId, uris: [ref.uri] }); - } else { - await player.play({ deviceId, contextUri: ref.uri, offset: index }); - } - } - mutation( - command, - io, - "play", - { target: target ?? null, deviceId: deviceId ?? null }, - message, - ); + : integer(options.index, "index", 1); + const result = await startPlayback({ + target, + device: options.device, + index, + }); + mutation(command, io, "play", result.data, result.message); }, ); @@ -227,65 +112,16 @@ export function registerPlayback( .description("Pause playback") .option("-d, --device ", "target Spotify Connect device") .action(async (options: { device?: string }, command: Command) => { - const resolved = - options.device === undefined - ? undefined - : await resolveDeviceTarget(options.device); - if (resolved?.route === "local" && !resolved.active) { - inactiveReceiver(resolved.name); - } - if (resolved === undefined || resolved.route === "local") { - const runtime = await tryRuntimeRequest("pause"); - if (runtime.connected) { - mutation( - command, - io, - "pause", - { source: "runtime", state: runtime.value }, - "Playback paused.", - ); - return; - } - } - const { player } = await cliSession(); - const deviceId = resolved?.route === "web" ? resolved.device.id : undefined; - await player.pause(deviceId); - mutation( - command, - io, - "pause", - { deviceId: deviceId ?? null }, - "Playback paused.", - ); + const result = await pausePlayback({ device: options.device }); + mutation(command, io, "pause", result.data, result.message); }); program .command("toggle") .description("Toggle play and pause") .action(async (_options, command: Command) => { - const runtime = await tryRuntimeRequest("toggle"); - if (runtime.connected) { - const playing = runtimeBoolean(runtime.value, "isPlaying") === true; - mutation( - command, - io, - "toggle", - { source: "runtime", isPlaying: playing, state: runtime.value }, - playing ? "Playback resumed." : "Playback paused.", - ); - return; - } - const { player } = await cliSession(); - const state = await player.state("foreground"); - if (state?.is_playing === true) await player.pause(); - else await player.play(); - mutation( - command, - io, - "toggle", - { isPlaying: state?.is_playing !== true }, - state?.is_playing === true ? "Playback paused." : "Playback resumed.", - ); + const result = await togglePlayback(); + mutation(command, io, "toggle", result.data, result.message); }); for (const [name, description] of [ @@ -297,35 +133,8 @@ export function registerPlayback( .description(description) .option("-d, --device ", "target Spotify Connect device") .action(async (options: { device?: string }, command: Command) => { - const message = - name === "next" - ? "Skipped to next item." - : "Returned to previous item."; - const resolved = - options.device === undefined - ? undefined - : await resolveDeviceTarget(options.device); - if (resolved?.route === "local" && !resolved.active) { - inactiveReceiver(resolved.name); - } - if (resolved === undefined || resolved.route === "local") { - const runtime = await tryRuntimeRequest(name); - if (runtime.connected) { - mutation( - command, - io, - name, - { source: "runtime", state: runtime.value }, - message, - ); - return; - } - } - const { player } = await cliSession(); - const deviceId = - resolved?.route === "web" ? resolved.device.id : undefined; - await player[name](deviceId); - mutation(command, io, name, { deviceId: deviceId ?? null }, message); + const result = await skip(name, { device: options.device }); + mutation(command, io, name, result.data, result.message); }); } @@ -340,57 +149,13 @@ export function registerPlayback( command: Command, ) => { const parsed = signedDurationMs(position); - let target = parsed.milliseconds; - const resolved = - options.device === undefined - ? undefined - : await resolveDeviceTarget(options.device); - if (resolved?.route === "local" && !resolved.active) { - inactiveReceiver(resolved.name); - } - if (resolved === undefined || resolved.route === "local") { - // A relative seek is sent as the raw offset: the runtime applies it inside its - // serialized mutation, so two concurrent `seek +5s` commands both land. - const runtime = await tryRuntimeRequest( - "seek", - parsed.relative - ? { offsetMs: parsed.milliseconds } - : { positionMs: Math.max(0, parsed.milliseconds) }, - ); - if (runtime.connected) { - const position = - runtimeNumber(runtime.value, "progressMs") ?? - Math.max(0, parsed.milliseconds); - mutation( - command, - io, - "seek", - { source: "runtime", positionMs: position, state: runtime.value }, - `Seeked to ${formatDuration(position)}.`, - ); - return; - } - } - const { player } = await cliSession(); - const deviceId = - resolved?.route === "web" ? resolved.device.id : undefined; - if (parsed.relative) { - const state = await player.state("foreground"); - if (state === null) throw unavailable("Nothing is playing."); - if (state.progress_ms === null) { - throw unavailable("The current playback position is unavailable."); - } - target += state.progress_ms; - } - target = Math.max(0, target); - await player.seek(target, deviceId); - mutation( - command, - io, - "seek", - { positionMs: target, deviceId: deviceId ?? null }, - `Seeked to ${formatDuration(target)}.`, - ); + const result = await seekPlayback({ + ...(parsed.relative + ? { offsetMs: parsed.milliseconds } + : { positionMs: parsed.milliseconds }), + device: options.device, + }); + mutation(command, io, "seek", result.data, result.message); }, ); @@ -405,73 +170,13 @@ export function registerPlayback( command: Command, ) => { const parsed = signedPercent(level); - let percent = parsed.percent; - const resolved = - options.device === undefined - ? undefined - : await resolveDeviceTarget(options.device); - if (resolved?.route === "local" && !resolved.active) { - inactiveReceiver(resolved.name); - } - if (resolved === undefined || resolved.route === "local") { - // A relative change is sent as the raw delta and applied inside the runtime's - // serialized mutation, so two concurrent `volume +5` commands both land. - const runtime = await tryRuntimeRequest( - "volume", - parsed.relative - ? { delta: Math.round(parsed.percent) } - : { percent: Math.round(parsed.percent) }, - ); - if (runtime.connected) { - const device = - runtime.value !== null && typeof runtime.value === "object" - ? (runtime.value as Record)["device"] - : null; - const volume = - device !== null && typeof device === "object" - ? (device as Record)["volumePercent"] - : null; - mutation( - command, - io, - "volume", - { - source: "runtime", - volumePercent: typeof volume === "number" ? volume : null, - state: runtime.value, - }, - typeof volume === "number" - ? `Volume set to ${volume}%.` - : "Volume adjusted.", - ); - return; - } - } - const { player } = await cliSession(); - const device = resolved?.route === "web" ? resolved.device : undefined; - if (parsed.relative) { - // A targeted device reports its own volume; only the untargeted path needs playback state. - let current: number | null; - if (device !== undefined) current = device.volume_percent; - else { - const state = await player.state("foreground"); - if (state?.device === null || state?.device === undefined) - throw unavailable("No active playback device."); - current = state.device.volume_percent; - } - if (current === null) - throw unavailable("The device does not report its volume."); - percent += current; - } - percent = Math.max(0, Math.min(100, Math.round(percent))); - await player.setVolume(percent, device?.id); - mutation( - command, - io, - "volume", - { volumePercent: percent, deviceId: device?.id ?? null }, - `Volume set to ${percent}%.`, - ); + const result = await setVolume({ + ...(parsed.relative + ? { delta: parsed.percent } + : { percent: parsed.percent }), + device: options.device, + }); + mutation(command, io, "volume", result.data, result.message); }, ); @@ -485,49 +190,9 @@ export function registerPlayback( options: { device?: string }, command: Command, ) => { - const resolved = - options.device === undefined - ? undefined - : await resolveDeviceTarget(options.device); - if (resolved?.route === "local" && !resolved.active) { - inactiveReceiver(resolved.name); - } - if (resolved === undefined || resolved.route === "local") { - // `toggle` is sent as the toggle itself so the runtime flips its own serialized state; - // resolving it here from a status read would race a concurrent toggle. - const runtime = await tryRuntimeRequest( - "shuffle", - value === "toggle" - ? { toggle: true } - : { enabled: booleanValue(value) }, - ); - if (runtime.connected) { - const enabled = runtimeBoolean(runtime.value, "shuffle") ?? false; - mutation( - command, - io, - "shuffle", - { source: "runtime", shuffle: enabled, state: runtime.value }, - `Shuffle ${enabled ? "on" : "off"}.`, - ); - return; - } - } - const { player } = await cliSession(); - const deviceId = - resolved?.route === "web" ? resolved.device.id : undefined; - const state = - value === "toggle" - ? !(await player.state("foreground"))?.shuffle_state - : booleanValue(value); - await player.setShuffle(state, deviceId); - mutation( - command, - io, - "shuffle", - { shuffle: state, deviceId: deviceId ?? null }, - `Shuffle ${state ? "on" : "off"}.`, - ); + const state = value === "toggle" ? ("toggle" as const) : booleanValue(value); + const result = await setShuffle(state, { device: options.device }); + mutation(command, io, "shuffle", result.data, result.message); }, ); @@ -541,60 +206,12 @@ export function registerPlayback( options: { device?: string }, command: Command, ) => { - let mode: RepeatState; - const resolved = - options.device === undefined - ? undefined - : await resolveDeviceTarget(options.device); - if (resolved?.route === "local" && !resolved.active) { - inactiveReceiver(resolved.name); - } - if (resolved === undefined || resolved.route === "local") { - // `cycle` is sent as the cycle itself so the runtime advances its own serialized state; - // resolving it here from a status read would race a concurrent cycle. - const runtime = await tryRuntimeRequest( - "repeat", - { - mode: - value === "cycle" - ? "cycle" - : enumValue(REPEAT_MODES, "repeat mode")(value), - }, - ); - if (runtime.connected) { - const stateValue = - runtime.value !== null && typeof runtime.value === "object" - ? (runtime.value as Record)["repeat"] - : "off"; - const applied = REPEAT_MODES.includes(stateValue as RepeatState) - ? (stateValue as RepeatState) - : "off"; - mutation( - command, - io, - "repeat", - { source: "runtime", repeat: applied, state: runtime.value }, - `Repeat set to ${applied}.`, - ); - return; - } - } - const { player } = await cliSession(); - const deviceId = - resolved?.route === "web" ? resolved.device.id : undefined; - if (value === "cycle") - mode = nextRepeatState( - (await player.state("foreground"))?.repeat_state ?? "off", - ); - else mode = enumValue(REPEAT_MODES, "repeat mode")(value); - await player.setRepeat(mode, deviceId); - mutation( - command, - io, - "repeat", - { repeat: mode, deviceId: deviceId ?? null }, - `Repeat set to ${mode}.`, - ); + const mode = + value === "cycle" + ? ("cycle" as const) + : enumValue(REPEAT_MODES, "repeat mode")(value); + const result = await setRepeat(mode, { device: options.device }); + mutation(command, io, "repeat", result.data, result.message); }, ); @@ -608,72 +225,8 @@ export function registerPlayback( options: { device?: string }, command: Command, ) => { - const ref = spotifyReference(target); - if ( - !["track", "episode", "album", "artist", "playlist"].includes( - ref.kind, - ) - ) { - if (ref.kind === "show") { - throw usageError( - "Spotify shows cannot be played as a context. Play one of its episodes instead — `spotuify show ` lists them.", - ); - } - throw usageError(`Spotify ${ref.kind} resources cannot be played.`); - } - const params = - ref.kind === "track" || ref.kind === "episode" - ? { uris: [ref.uri] } - : { contextUri: ref.uri }; - const resolved = - options.device === undefined - ? undefined - : await resolveDeviceTarget(options.device); - if (resolved?.route === "local") { - if (!resolved.active) { - await runtimeRequest("device.transfer", { - selector: resolved.id, - play: false, - }); - } - const state = await runtimeRequest("play", params); - mutation( - command, - io, - "open", - { source: "runtime", uri: ref.uri, deviceId: resolved.id, state }, - `Playing ${ref.uri}.`, - ); - return; - } - if (resolved === undefined) { - const runtime = await tryRuntimeRequest("play", params); - if (runtime.connected) { - mutation( - command, - io, - "open", - { source: "runtime", uri: ref.uri, state: runtime.value }, - `Playing ${ref.uri}.`, - ); - return; - } - } - const { player } = await cliSession(); - const deviceId = resolved?.device.id; - if (ref.kind === "track" || ref.kind === "episode") - await player.play({ deviceId, uris: [ref.uri] }); - else if (["album", "artist", "playlist"].includes(ref.kind)) - await player.play({ deviceId, contextUri: ref.uri }); - else - throw usageError(`Spotify ${ref.kind} resources cannot be played.`); - mutation( - command, - io, - "open", - { uri: ref.uri, deviceId: deviceId ?? null }, - `Playing ${ref.uri}.`, - ); + const result = await openPlayback({ target, device: options.device }); + mutation(command, io, "open", result.data, result.message); }, ); } diff --git a/src/cli/commands/playlists.ts b/src/cli/commands/playlists.ts index 11602b1..b118bce 100644 --- a/src/cli/commands/playlists.ts +++ b/src/cli/commands/playlists.ts @@ -1,16 +1,15 @@ import { Command, Option } from "commander"; import { removeLibraryItems, saveLibraryItems } from "../../api/library.ts"; import { - addPlaylistItems, createPlaylist, movePlaylistItems, - myPlaylists, playlistDetails, playlistItems, removePlaylistItems, replacePlaylistItems, updatePlaylistDetails, } from "../../api/playlists.ts"; +import { playlistAdd, playlistList } from "../operations/playlists.ts"; import { artistLine } from "../../api/types.ts"; import { unavailable, usageError } from "../errors.ts"; import { formatDuration, normalizeItem, type CliIo } from "../output.ts"; @@ -35,25 +34,8 @@ export function registerPlaylists(program: Command, io: CliIo): void { .description("List the user's playlists") .option("--owned", "show only owned playlists") .action(async (options: { owned?: boolean }, command: Command) => { - const session = await cliSession(); - const me = await session.profile(); - const { client } = session; - const all = (await myPlaylists(client, me.id)).filter( - (item) => options.owned !== true || item.mine, - ); - outputFor(command, io).emit( - "playlist.list", - all, - table( - ["OWNED", "NAME", "OWNER", "URI"], - all.map((item) => [ - item.mine ? "yes" : "no", - item.name, - item.ownerName, - item.uri, - ]), - ), - ); + const result = await playlistList({ ownedOnly: options.owned }); + outputFor(command, io).emit("playlist.list", result.data, result.message); }); playlist .command("show ") @@ -270,28 +252,8 @@ export function registerPlaylists(program: Command, io: CliIo): void { _options, command: Command, ) => { - const uris = items.map((item) => { - const ref = spotifyReference(item); - if (ref.kind !== "track" && ref.kind !== "episode") - throw usageError("Playlists accept tracks and episodes only."); - return ref.uri; - }); - const snapshotId = await addPlaylistItems( - (await cliSession()).client, - playlistId(value), - uris, - ); - mutation( - command, - io, - "playlist.add", - { - playlist: spotifyReference(value, "playlist").uri, - uris, - snapshotId, - }, - `Added ${uris.length} item${uris.length === 1 ? "" : "s"}.`, - ); + const result = await playlistAdd(value, items); + mutation(command, io, "playlist.add", result.data, result.message); }, ); playlist diff --git a/src/cli/commands/queue-devices.ts b/src/cli/commands/queue-devices.ts index 8c28ae8..5abe213 100644 --- a/src/cli/commands/queue-devices.ts +++ b/src/cli/commands/queue-devices.ts @@ -1,42 +1,8 @@ import { Command } from "commander"; -import { runtimeRequest, tryRuntimeRequest } from "../../runtime/control.ts"; -import { unavailable, usageError } from "../errors.ts"; -import { - normalizeItem, - normalizeRuntimePlayback, - type CliIo, -} from "../output.ts"; -import { cliSession } from "../session.ts"; -import { spotifyReference } from "../values.ts"; -import { - allDevices, - currentState, - inactiveReceiver, - itemRows, - mutation, - outputFor, - resolveDeviceTarget, - table, - uniqueDevice, -} from "../support.ts"; - -/** - * The item to report as playing now. - * - * The Web queue's `currently_playing` lags native events exactly like `/me/player`, so a connected - * runtime's snapshot is authoritative for the current item — including when it says nothing is - * playing. The upcoming list exists only in the Web response and stays with it. - */ -export function queueCurrentItem( - runtime: { connected: true; value: unknown } | { connected: false }, - webCurrent: Record | null, -): Record | null { - if (!runtime.connected) return webCurrent; - const item = normalizeRuntimePlayback(runtime.value)["item"]; - return item !== null && typeof item === "object" - ? (item as Record) - : null; -} +import type { CliIo } from "../output.ts"; +import { deviceCurrent, deviceList, deviceTransfer } from "../operations/devices.ts"; +import { queueAdd, queueList } from "../operations/queue.ts"; +import { mutation, outputFor } from "../support.ts"; export function registerQueueAndDevices(program: Command, io: CliIo): void { const queue = program @@ -46,32 +12,8 @@ export function registerQueueAndDevices(program: Command, io: CliIo): void { .command("list") .description("Show the current and upcoming items") .action(async (_options, command: Command) => { - const value = await (await cliSession()).player.queue(); - const runtime = await tryRuntimeRequest("status"); - const current = queueCurrentItem( - runtime, - normalizeItem(value.currently_playing), - ); - const data = { - current, - items: value.queue.map(normalizeItem), - }; - const currentLine = - current === null - ? "Nothing is playing." - : `Now ${String(current["name"] ?? "Unknown")} — ${String(current["artist"] ?? "")}`; - const upcoming = - value.queue.length === 0 - ? "Queue is empty." - : table( - ["#", "TITLE", "ARTIST", "TIME", "URI"], - itemRows(value.queue), - ); - outputFor(command, io).emit( - "queue.list", - data, - `${currentLine}\n\n${upcoming}`, - ); + const result = await queueList(); + outputFor(command, io).emit("queue.list", result.data, result.message); }); queue .command("add ") @@ -83,50 +25,8 @@ export function registerQueueAndDevices(program: Command, io: CliIo): void { options: { device?: string }, command: Command, ) => { - const uris = items.map((item) => { - const ref = spotifyReference(item); - if (ref.kind !== "track" && ref.kind !== "episode") - throw usageError( - "Only tracks and episodes can be added to the queue.", - ); - return ref.uri; - }); - const resolved = - options.device === undefined - ? undefined - : await resolveDeviceTarget(options.device); - if (resolved?.route === "local" && !resolved.active) { - inactiveReceiver(resolved.name); - } - // A running runtime owns the queue path: additions go through its client and serialize - // with the other mutations instead of racing them from a second Web API session. - if (resolved === undefined || resolved.route === "local") { - const first = await tryRuntimeRequest("queue.add", { uri: uris[0] }); - if (first.connected) { - for (const uri of uris.slice(1)) { - await runtimeRequest("queue.add", { uri }); - } - mutation( - command, - io, - "queue.add", - { source: "runtime", uris, deviceId: resolved?.id ?? null }, - `Added ${uris.length} item${uris.length === 1 ? "" : "s"} to the queue.`, - ); - return; - } - } - const { player } = await cliSession(); - const deviceId = - resolved?.route === "web" ? resolved.device.id : undefined; - for (const uri of uris) await player.addToQueue(uri, deviceId); - mutation( - command, - io, - "queue.add", - { uris, deviceId: deviceId ?? null }, - `Added ${uris.length} item${uris.length === 1 ? "" : "s"} to the queue.`, - ); + const result = await queueAdd(items, { device: options.device }); + mutation(command, io, "queue.add", result.data, result.message); }, ); @@ -137,53 +37,15 @@ export function registerQueueAndDevices(program: Command, io: CliIo): void { .command("list") .description("List available devices") .action(async (_options, command: Command) => { - // The runtime's merged view when available: it includes the embedded receiver, which - // Spotify's device list does not always report. The listing must match what is targetable. - const devices = await allDevices(); - outputFor(command, io).emit( - "device.list", - devices, - table( - ["ACTIVE", "NAME", "TYPE", "VOLUME", "ID"], - devices.map((item) => [ - item.is_active ? "*" : "", - item.name, - item.type, - item.volume_percent === null ? "—" : `${item.volume_percent}%`, - item.id, - ]), - ), - ); + const result = await deviceList(); + outputFor(command, io).emit("device.list", result.data, result.message); }); device .command("current") .description("Show the active device") .action(async (_options, command: Command) => { - // While the receiver is active, native transfer events outrun `/me/player`; the runtime - // snapshot is the authority when one exists. - const runtime = await tryRuntimeRequest("status"); - if (runtime.connected) { - const device = normalizeRuntimePlayback(runtime.value)["device"] as { - id: string | null; - name: string; - type: string | null; - } | null; - if (device === null) throw unavailable("No active playback device."); - outputFor(command, io).emit( - "device.current", - device, - `${device.name}${device.type === null ? "" : ` (${device.type})`}\n${device.id ?? "No device ID"}`, - ); - return; - } - const state = await currentState(); - if (state?.device === null || state?.device === undefined) - throw unavailable("No active playback device."); - outputFor(command, io).emit( - "device.current", - state.device, - `${state.device.name} (${state.device.type})\n${state.device.id ?? "No device ID"}`, - ); + const result = await deviceCurrent(); + outputFor(command, io).emit("device.current", result.data, result.message); }); device .command("transfer ") @@ -195,32 +57,8 @@ export function registerQueueAndDevices(program: Command, io: CliIo): void { options: { play: boolean }, command: Command, ) => { - const runtime = await tryRuntimeRequest("device.transfer", { - selector, - play: options.play, - }); - if (runtime.connected) { - mutation( - command, - io, - "device.transfer", - { source: "runtime", result: runtime.value }, - `Transferred playback to ${selector}.`, - ); - return; - } - const { player } = await cliSession(); - const target = uniqueDevice(await player.devices(), selector); - if (target.id === null) - throw unavailable(`Device ${target.name} has no transferable ID.`); - await player.transfer(target.id, options.play); - mutation( - command, - io, - "device.transfer", - { device: target, play: options.play }, - `Transferred playback to ${target.name}.`, - ); + const result = await deviceTransfer(selector, options.play); + mutation(command, io, "device.transfer", result.data, result.message); }, ); } diff --git a/src/cli/operations/catalog.ts b/src/cli/operations/catalog.ts new file mode 100644 index 0000000..07b35e2 --- /dev/null +++ b/src/cli/operations/catalog.ts @@ -0,0 +1,372 @@ +import { + albumTracks, + artistAlbums, + audiobookChapters, + audiobookDetails, + showDetails, + showEpisodes, +} from "../../api/catalog.ts"; +import { fetchLyrics } from "../../api/lyrics.ts"; +import { playlistItems } from "../../api/playlists.ts"; +import { search, type SearchType } from "../../api/search.ts"; +import { + artistLine, + isTrack, + type FullArtist, + type PlayableItem, + type SimpleAlbum, + type SimpleArtist, + type Track, +} from "../../api/types.ts"; +import { tryRuntimeRequest } from "../../runtime/control.ts"; +import { unavailable, usageError } from "../errors.ts"; +import { + formatDuration, + normalizeArtist, + normalizeAudiobook, + normalizeEpisode, + normalizeItem, + normalizeShow, +} from "../output.ts"; +import { cliSession } from "../session.ts"; +import { + normalizePlaylistDetails, + openablePlaylistDetails, + playlistHeader, + table, +} from "../support.ts"; +import { spotifyReference } from "../values.ts"; +import type { OperationResult } from "./types.ts"; + +/** `search --type all` covers the four music types; spoken-word types are requested by name. */ +export const DEFAULT_SEARCH_TYPES: readonly SearchType[] = [ + "track", + "artist", + "album", + "playlist", +]; +export const SEARCH_LIMIT_MAX = 50; + +export async function searchCatalog( + query: string, + options: { types?: readonly SearchType[]; limit?: number } = {}, +): Promise>> { + if (options.limit !== undefined && options.limit > SEARCH_LIMIT_MAX) { + throw usageError(`Search limit cannot exceed ${SEARCH_LIMIT_MAX}.`); + } + const types = options.types ?? DEFAULT_SEARCH_TYPES; + const { client } = await cliSession(); + const results = await search(client, query, { + types, + ...(options.limit !== undefined ? { limit: options.limit } : {}), + }); + const data = { + tracks: results.tracks.map(normalizeItem), + artists: results.artists, + albums: results.albums, + playlists: results.playlists, + shows: results.shows.map(normalizeShow), + episodes: results.episodes.map(normalizeEpisode), + audiobooks: results.audiobooks.map(normalizeAudiobook), + }; + const sections: string[] = []; + if (results.tracks.length > 0) + sections.push( + `Tracks\n${table( + ["TITLE", "ARTIST", "URI"], + results.tracks.map((item) => [item.name, artistLine(item), item.uri]), + )}`, + ); + if (results.artists.length > 0) + sections.push( + `Artists\n${table( + ["NAME", "URI"], + results.artists.map((item) => [item.name, item.uri]), + )}`, + ); + if (results.albums.length > 0) + sections.push( + `Albums\n${table( + ["NAME", "YEAR", "URI"], + results.albums.map((item) => [ + item.name, + item.release_date ?? "", + item.uri, + ]), + )}`, + ); + if (results.playlists.length > 0) + sections.push( + `Playlists\n${table( + ["NAME", "OWNER", "URI"], + results.playlists.map((item) => [ + item.name, + item.owner?.display_name ?? "", + item.uri, + ]), + )}`, + ); + if (results.shows.length > 0) + sections.push( + `Shows\n${table( + ["NAME", "PUBLISHER", "URI"], + results.shows.map((item) => [item.name, item.publisher ?? "", item.uri]), + )}`, + ); + if (results.episodes.length > 0) + sections.push( + `Episodes\n${table( + ["TITLE", "DATE", "TIME", "URI"], + results.episodes.map((item) => [ + item.name, + item.release_date ?? "", + formatDuration(item.duration_ms), + item.uri, + ]), + )}`, + ); + if (results.audiobooks.length > 0) + sections.push( + `Audiobooks\n${table( + ["NAME", "AUTHOR", "URI"], + results.audiobooks.map((item) => [ + item.name, + (item.authors ?? []).map((author) => author.name).join(", "), + item.uri, + ]), + )}`, + ); + return { + data, + message: sections.length === 0 ? "No results." : sections.join("\n\n"), + }; +} + +export async function resourceDetails( + target: string, +): Promise> { + const ref = spotifyReference(target); + const { client } = await cliSession(); + switch (ref.kind) { + case "track": + case "episode": { + const item = await client.get(`/${ref.kind}s/${ref.id}`); + return { + data: normalizeItem(item), + message: `${item.name} — ${artistLine(item)}\n${formatDuration(item.duration_ms)} · ${item.uri}`, + }; + } + case "album": { + const [album, tracks] = await Promise.all([ + client.get( + `/albums/${ref.id}`, + ), + albumTracks(client, ref.id), + ]); + return { + data: { ...album, tracks }, + message: `${album.name}${album.release_date ? ` (${album.release_date})` : ""}\n\n${table( + ["#", "TITLE", "ARTIST", "TIME", "URI"], + tracks.map((item) => [ + item.track_number, + item.name, + item.artists.map((artist) => artist.name).join(", "), + formatDuration(item.duration_ms), + item.uri, + ]), + )}`, + }; + } + case "artist": { + const [artist, albums] = await Promise.all([ + client.get(`/artists/${ref.id}`), + artistAlbums(client, ref.id), + ]); + return { + data: { ...artist, albums }, + message: `${artist.name}\n\n${table( + ["RELEASE", "DATE", "TRACKS", "URI"], + albums.map((album) => [ + album.name, + album.release_date ?? "", + album.total_tracks ?? "", + album.uri, + ]), + )}`, + }; + } + case "playlist": { + // Ownership first: the items read is permanently refused for foreign playlists, so it + // must not be spent before the metadata proves it can succeed. + const details = await openablePlaylistDetails(ref.id); + const entries = await playlistItems(client, ref.id); + return { + data: { + playlist: normalizePlaylistDetails(details), + items: entries.map((entry) => ({ + position: entry.position, + isLocal: entry.isLocal, + item: normalizeItem(entry.item), + })), + }, + message: `${playlistHeader(details)}\n\n${table( + ["#", "TITLE", "ARTIST", "TIME", "URI"], + entries.map((entry) => [ + entry.position + 1, + entry.item.name, + artistLine(entry.item), + formatDuration(entry.item.duration_ms), + entry.item.uri, + ]), + )}`, + }; + } + case "show": { + const [show, episodes] = await Promise.all([ + showDetails(client, ref.id), + showEpisodes(client, ref.id), + ]); + return { + data: { + ...normalizeShow(show), + episodes: episodes.map(normalizeEpisode), + }, + message: `${show.name}${show.publisher ? ` — ${show.publisher}` : ""}\n${show.uri}\n\nLatest episodes\n${table( + ["TITLE", "DATE", "TIME", "URI"], + episodes.map((episode) => [ + episode.name, + episode.release_date ?? "", + formatDuration(episode.duration_ms), + episode.uri, + ]), + )}`, + }; + } + case "audiobook": { + const [audiobook, chapters] = await Promise.all([ + audiobookDetails(client, ref.id), + audiobookChapters(client, ref.id), + ]); + const authors = (audiobook.authors ?? []) + .map((author) => author.name) + .join(", "); + return { + data: { ...normalizeAudiobook(audiobook), chapters }, + message: `${audiobook.name}${authors === "" ? "" : ` — ${authors}`}\n${audiobook.uri}\n\nChapters\n${table( + ["#", "TITLE", "TIME", "URI"], + // Positional numbering: Spotify's chapter_number is zero-based and the page arrives in + // reading order anyway. + chapters.map((chapter, index) => [ + index + 1, + chapter.name, + formatDuration(chapter.duration_ms), + chapter.uri, + ]), + )}`, + }; + } + default: + throw usageError( + `Spotify ${ref.kind} resources are not supported by show.`, + ); + } +} + +/** + * Narrow a runtime status snapshot to the current track for lyric lookup. + * + * The runtime's item uses the same normalized shape the CLI emits, so it doubles as the emitted + * `track` record without another Web API read. + */ +export function runtimeLyricsTrack(value: unknown): { + name: string; + artists: string[]; + artist: string; + durationMs: number; + record: Record; +} { + const state = + value !== null && typeof value === "object" + ? (value as Record) + : {}; + const raw = state["item"]; + if (raw === null || raw === undefined || typeof raw !== "object") + throw unavailable("The current item is not a track."); + const item = raw as Record; + const name = item["name"]; + const durationMs = item["durationMs"]; + if ( + item["type"] !== "track" || + typeof name !== "string" || + typeof durationMs !== "number" + ) { + throw unavailable("The current item is not a track."); + } + const artists = Array.isArray(item["artists"]) + ? item["artists"].filter( + (artist): artist is string => typeof artist === "string", + ) + : []; + return { + name, + artists, + artist: + typeof item["artist"] === "string" ? item["artist"] : artists.join(", "), + durationMs, + record: item, + }; +} + +export async function lyricsFor( + target?: string, +): Promise>> { + let subject: { + name: string; + artists: string[]; + artist: string; + durationMs: number; + record: Record | null; + }; + if (target !== undefined) { + const ref = spotifyReference(target, "track"); + const track = await (await cliSession()).client.get( + `/tracks/${ref.id}`, + ); + subject = { + name: track.name, + artists: track.artists.map((artist) => artist.name), + artist: artistLine(track), + durationMs: track.duration_ms, + record: normalizeItem(track), + }; + } else { + // While the local receiver is playing, native events outrun `/me/player`; asking the Web + // API here can return the previous track across a change. The runtime item is + // authoritative and already carries everything lyric lookup needs. + const runtime = await tryRuntimeRequest("status"); + if (runtime.connected) { + subject = runtimeLyricsTrack(runtime.value); + } else { + const item = (await (await cliSession()).player.state("foreground")) + ?.item; + if (item === null || item === undefined || !isTrack(item)) + throw unavailable("The current item is not a track."); + subject = { + name: item.name, + artists: item.artists.map((artist) => artist.name), + artist: artistLine(item), + durationMs: item.duration_ms, + record: normalizeItem(item), + }; + } + } + const lyrics = await fetchLyrics({ + name: subject.name, + artists: subject.artists, + durationMs: subject.durationMs, + }); + return { + data: { track: subject.record, lyrics }, + message: `${subject.name} — ${subject.artist}\n${lyrics.source}${lyrics.synced ? " · synced" : ""}\n\n${lyrics.lines.map((line) => line.text).join("\n")}`, + }; +} diff --git a/src/cli/operations/devices.ts b/src/cli/operations/devices.ts new file mode 100644 index 0000000..505db76 --- /dev/null +++ b/src/cli/operations/devices.ts @@ -0,0 +1,76 @@ +import type { Device } from "../../api/types.ts"; +import { tryRuntimeRequest } from "../../runtime/control.ts"; +import { unavailable } from "../errors.ts"; +import { normalizeRuntimePlayback } from "../output.ts"; +import { cliSession } from "../session.ts"; +import { allDevices, currentState, table, uniqueDevice } from "../support.ts"; +import type { OperationResult } from "./types.ts"; + +export async function deviceList(): Promise> { + // The runtime's merged view when available: it includes the embedded receiver, which + // Spotify's device list does not always report. The listing must match what is targetable. + const devices = await allDevices(); + return { + data: devices, + message: table( + ["ACTIVE", "NAME", "TYPE", "VOLUME", "ID"], + devices.map((item) => [ + item.is_active ? "*" : "", + item.name, + item.type, + item.volume_percent === null ? "—" : `${item.volume_percent}%`, + item.id, + ]), + ), + }; +} + +export async function deviceCurrent(): Promise> { + // While the receiver is active, native transfer events outrun `/me/player`; the runtime + // snapshot is the authority when one exists. + const runtime = await tryRuntimeRequest("status"); + if (runtime.connected) { + const device = normalizeRuntimePlayback(runtime.value)["device"] as { + id: string | null; + name: string; + type: string | null; + } | null; + if (device === null) throw unavailable("No active playback device."); + return { + data: device, + message: `${device.name}${device.type === null ? "" : ` (${device.type})`}\n${device.id ?? "No device ID"}`, + }; + } + const state = await currentState(); + if (state?.device === null || state?.device === undefined) + throw unavailable("No active playback device."); + return { + data: state.device, + message: `${state.device.name} (${state.device.type})\n${state.device.id ?? "No device ID"}`, + }; +} + +export async function deviceTransfer( + selector: string, + play: boolean, +): Promise>> { + const runtime = await tryRuntimeRequest("device.transfer", { + selector, + play, + }); + if (runtime.connected) { + return { + data: { source: "runtime", result: runtime.value }, + message: `Transferred playback to ${selector}.`, + }; + } + const { player } = await cliSession(); + const target = uniqueDevice(await player.devices(), selector); + if (target.id === null) + throw unavailable(`Device ${target.name} has no transferable ID.`); + await player.transfer(target.id, play); + return { + data: { device: target, play }, + message: `Transferred playback to ${target.name}.`, + }; +} diff --git a/src/cli/operations/playback.ts b/src/cli/operations/playback.ts new file mode 100644 index 0000000..b055f02 --- /dev/null +++ b/src/cli/operations/playback.ts @@ -0,0 +1,465 @@ +import { nextRepeatState } from "../../api/player.ts"; +import type { RepeatState } from "../../api/types.ts"; +import { runtimeRequest, tryRuntimeRequest } from "../../runtime/control.ts"; +import { unavailable, usageError } from "../errors.ts"; +import { + formatDuration, + normalizePlayback, + normalizeRuntimePlayback, + playbackText, +} from "../output.ts"; +import { cliSession } from "../session.ts"; +import { + activeDeviceTarget, + currentState, + resolveDeviceTarget, + runtimeBoolean, + runtimeNumber, + runtimePlaybackText, +} from "../support.ts"; +import { spotifyReference, type SpotifyReference } from "../values.ts"; +import type { OperationResult } from "./types.ts"; + +export const REPEAT_MODES: readonly RepeatState[] = ["off", "context", "track"]; + +const PLAYABLE_KINDS: readonly string[] = [ + "track", + "episode", + "album", + "artist", + "playlist", +]; + +export async function playbackStatus(): Promise< + OperationResult> +> { + const runtime = await tryRuntimeRequest("status"); + if (runtime.connected) { + return { + data: normalizeRuntimePlayback(runtime.value), + message: runtimePlaybackText(runtime.value), + }; + } + const state = await currentState(); + return { data: normalizePlayback(state), message: playbackText(state) }; +} + +function assertPlayable(ref: SpotifyReference): void { + if (ref.kind === "show") { + throw usageError( + "Spotify shows cannot be played as a context. Play one of its episodes instead — `spotuify show ` lists them.", + ); + } + if (!PLAYABLE_KINDS.includes(ref.kind)) { + throw usageError(`Spotify ${ref.kind} resources cannot be played.`); + } +} + +interface PlayRequest { + uris?: string[]; + contextUri?: string; + offset?: number; +} + +type PlayOutcome = + | { via: "local"; deviceId: string; state: unknown } + | { via: "runtime"; state: unknown } + | { via: "web"; deviceId: string | undefined }; + +/** + * Dispatch a play request along the established route. + * + * A selector naming the embedded receiver routes through its runtime: transfer natively when it + * does not hold the session, then start playback inside the serialized stream. Without a selector + * an available runtime takes the command; the Web API is the fallback and the only path for a + * remote device target. + */ +async function routedPlay( + params: PlayRequest, + deviceSelector: string | undefined, +): Promise { + const resolved = + deviceSelector === undefined + ? undefined + : await resolveDeviceTarget(deviceSelector); + if (resolved?.route === "local") { + if (!resolved.active) { + await runtimeRequest("device.transfer", { + selector: resolved.id, + play: false, + }); + } + const state = await runtimeRequest("play", params); + return { via: "local", deviceId: resolved.id, state }; + } + if (resolved === undefined) { + const runtime = await tryRuntimeRequest("play", params); + if (runtime.connected) return { via: "runtime", state: runtime.value }; + } + const { player } = await cliSession(); + const deviceId = resolved?.device.id; + if (params.uris !== undefined) { + await player.play({ deviceId, uris: params.uris }); + } else if (params.contextUri !== undefined) { + await player.play({ + deviceId, + contextUri: params.contextUri, + offset: params.offset, + }); + } else { + await player.play({ deviceId }); + } + return { via: "web", deviceId }; +} + +export async function startPlayback( + options: { target?: string; device?: string; index?: number } = {}, +): Promise>> { + const { target } = options; + const offset = options.index === undefined ? undefined : options.index - 1; + if (offset !== undefined && target === undefined) { + throw usageError("--index requires an album or playlist target."); + } + const ref = target === undefined ? undefined : spotifyReference(target); + if ( + ref !== undefined && + offset !== undefined && + ref.kind !== "album" && + ref.kind !== "playlist" + ) { + throw usageError("--index is only valid for album or playlist contexts."); + } + if (ref !== undefined) assertPlayable(ref); + const params: PlayRequest = + ref === undefined + ? {} + : ref.kind === "track" || ref.kind === "episode" + ? { uris: [ref.uri] } + : { contextUri: ref.uri, offset }; + const message = + target === undefined ? "Playback resumed." : `Playing ${target}.`; + const outcome = await routedPlay(params, options.device); + switch (outcome.via) { + case "local": + return { + data: { + source: "runtime", + target: target ?? null, + deviceId: outcome.deviceId, + state: outcome.state, + }, + message, + }; + case "runtime": + return { + data: { source: "runtime", target: target ?? null, state: outcome.state }, + message, + }; + case "web": + return { + data: { target: target ?? null, deviceId: outcome.deviceId ?? null }, + message, + }; + } +} + +export async function openPlayback(options: { + target: string; + device?: string; +}): Promise>> { + const ref = spotifyReference(options.target); + assertPlayable(ref); + const params: PlayRequest = + ref.kind === "track" || ref.kind === "episode" + ? { uris: [ref.uri] } + : { contextUri: ref.uri }; + const message = `Playing ${ref.uri}.`; + const outcome = await routedPlay(params, options.device); + switch (outcome.via) { + case "local": + return { + data: { + source: "runtime", + uri: ref.uri, + deviceId: outcome.deviceId, + state: outcome.state, + }, + message, + }; + case "runtime": + return { + data: { source: "runtime", uri: ref.uri, state: outcome.state }, + message, + }; + case "web": + return { + data: { uri: ref.uri, deviceId: outcome.deviceId ?? null }, + message, + }; + } +} + +export async function pausePlayback( + options: { device?: string } = {}, +): Promise>> { + const resolved = await activeDeviceTarget(options.device); + if (resolved === undefined || resolved.route === "local") { + const runtime = await tryRuntimeRequest("pause"); + if (runtime.connected) { + return { + data: { source: "runtime", state: runtime.value }, + message: "Playback paused.", + }; + } + } + const { player } = await cliSession(); + const deviceId = resolved?.route === "web" ? resolved.device.id : undefined; + await player.pause(deviceId); + return { data: { deviceId: deviceId ?? null }, message: "Playback paused." }; +} + +export async function togglePlayback(): Promise< + OperationResult> +> { + const runtime = await tryRuntimeRequest("toggle"); + if (runtime.connected) { + const playing = runtimeBoolean(runtime.value, "isPlaying") === true; + return { + data: { source: "runtime", isPlaying: playing, state: runtime.value }, + message: playing ? "Playback resumed." : "Playback paused.", + }; + } + const { player } = await cliSession(); + const state = await player.state("foreground"); + if (state?.is_playing === true) await player.pause(); + else await player.play(); + return { + data: { isPlaying: state?.is_playing !== true }, + message: + state?.is_playing === true ? "Playback paused." : "Playback resumed.", + }; +} + +export async function skip( + direction: "next" | "previous", + options: { device?: string } = {}, +): Promise>> { + const message = + direction === "next" ? "Skipped to next item." : "Returned to previous item."; + const resolved = await activeDeviceTarget(options.device); + if (resolved === undefined || resolved.route === "local") { + const runtime = await tryRuntimeRequest(direction); + if (runtime.connected) { + return { data: { source: "runtime", state: runtime.value }, message }; + } + } + const { player } = await cliSession(); + const deviceId = resolved?.route === "web" ? resolved.device.id : undefined; + await player[direction](deviceId); + return { data: { deviceId: deviceId ?? null }, message }; +} + +export async function seekPlayback(options: { + positionMs?: number; + offsetMs?: number; + device?: string; +}): Promise>> { + if (options.positionMs !== undefined && options.offsetMs !== undefined) { + throw usageError("Seek accepts a position or an offset, not both."); + } + let relative: boolean; + let milliseconds: number; + if (options.offsetMs !== undefined) { + relative = true; + milliseconds = options.offsetMs; + } else if (options.positionMs !== undefined) { + relative = false; + milliseconds = options.positionMs; + } else { + throw usageError("Seek requires a position or an offset."); + } + let target = milliseconds; + const resolved = await activeDeviceTarget(options.device); + if (resolved === undefined || resolved.route === "local") { + // A relative seek is sent as the raw offset: the runtime applies it inside its + // serialized mutation, so two concurrent `seek +5s` commands both land. + const runtime = await tryRuntimeRequest( + "seek", + relative + ? { offsetMs: milliseconds } + : { positionMs: Math.max(0, milliseconds) }, + ); + if (runtime.connected) { + const position = + runtimeNumber(runtime.value, "progressMs") ?? + Math.max(0, milliseconds); + return { + data: { source: "runtime", positionMs: position, state: runtime.value }, + message: `Seeked to ${formatDuration(position)}.`, + }; + } + } + const { player } = await cliSession(); + const deviceId = resolved?.route === "web" ? resolved.device.id : undefined; + if (relative) { + const state = await player.state("foreground"); + if (state === null) throw unavailable("Nothing is playing."); + if (state.progress_ms === null) { + throw unavailable("The current playback position is unavailable."); + } + target += state.progress_ms; + } + target = Math.max(0, target); + await player.seek(target, deviceId); + return { + data: { positionMs: target, deviceId: deviceId ?? null }, + message: `Seeked to ${formatDuration(target)}.`, + }; +} + +export async function setVolume(options: { + percent?: number; + delta?: number; + device?: string; +}): Promise>> { + if (options.percent !== undefined && options.delta !== undefined) { + throw usageError("Volume accepts a level or a delta, not both."); + } + let relative: boolean; + let percent: number; + if (options.delta !== undefined) { + relative = true; + percent = options.delta; + } else if (options.percent !== undefined) { + relative = false; + percent = options.percent; + } else { + throw usageError("Volume requires a level or a delta."); + } + if (!relative && (percent < 0 || percent > 100)) { + throw usageError("Volume must be between 0 and 100."); + } + const resolved = await activeDeviceTarget(options.device); + if (resolved === undefined || resolved.route === "local") { + // A relative change is sent as the raw delta and applied inside the runtime's + // serialized mutation, so two concurrent `volume +5` commands both land. + const runtime = await tryRuntimeRequest( + "volume", + relative + ? { delta: Math.round(percent) } + : { percent: Math.round(percent) }, + ); + if (runtime.connected) { + const device = + runtime.value !== null && typeof runtime.value === "object" + ? (runtime.value as Record)["device"] + : null; + const volume = + device !== null && typeof device === "object" + ? (device as Record)["volumePercent"] + : null; + return { + data: { + source: "runtime", + volumePercent: typeof volume === "number" ? volume : null, + state: runtime.value, + }, + message: + typeof volume === "number" + ? `Volume set to ${volume}%.` + : "Volume adjusted.", + }; + } + } + const { player } = await cliSession(); + const device = resolved?.route === "web" ? resolved.device : undefined; + if (relative) { + // A targeted device reports its own volume; only the untargeted path needs playback state. + let current: number | null; + if (device !== undefined) current = device.volume_percent; + else { + const state = await player.state("foreground"); + if (state?.device === null || state?.device === undefined) + throw unavailable("No active playback device."); + current = state.device.volume_percent; + } + if (current === null) + throw unavailable("The device does not report its volume."); + percent += current; + } + percent = Math.max(0, Math.min(100, Math.round(percent))); + await player.setVolume(percent, device?.id); + return { + data: { volumePercent: percent, deviceId: device?.id ?? null }, + message: `Volume set to ${percent}%.`, + }; +} + +export async function setShuffle( + state: boolean | "toggle", + options: { device?: string } = {}, +): Promise>> { + const resolved = await activeDeviceTarget(options.device); + if (resolved === undefined || resolved.route === "local") { + // `toggle` is sent as the toggle itself so the runtime flips its own serialized state; + // resolving it here from a status read would race a concurrent toggle. + const runtime = await tryRuntimeRequest( + "shuffle", + state === "toggle" ? { toggle: true } : { enabled: state }, + ); + if (runtime.connected) { + const enabled = runtimeBoolean(runtime.value, "shuffle") ?? false; + return { + data: { source: "runtime", shuffle: enabled, state: runtime.value }, + message: `Shuffle ${enabled ? "on" : "off"}.`, + }; + } + } + const { player } = await cliSession(); + const deviceId = resolved?.route === "web" ? resolved.device.id : undefined; + const enabled = + state === "toggle" + ? !(await player.state("foreground"))?.shuffle_state + : state; + await player.setShuffle(enabled, deviceId); + return { + data: { shuffle: enabled, deviceId: deviceId ?? null }, + message: `Shuffle ${enabled ? "on" : "off"}.`, + }; +} + +export async function setRepeat( + mode: RepeatState | "cycle", + options: { device?: string } = {}, +): Promise>> { + const resolved = await activeDeviceTarget(options.device); + if (resolved === undefined || resolved.route === "local") { + // `cycle` is sent as the cycle itself so the runtime advances its own serialized state; + // resolving it here from a status read would race a concurrent cycle. + const runtime = await tryRuntimeRequest("repeat", { mode }); + if (runtime.connected) { + const stateValue = + runtime.value !== null && typeof runtime.value === "object" + ? (runtime.value as Record)["repeat"] + : "off"; + const applied = REPEAT_MODES.includes(stateValue as RepeatState) + ? (stateValue as RepeatState) + : "off"; + return { + data: { source: "runtime", repeat: applied, state: runtime.value }, + message: `Repeat set to ${applied}.`, + }; + } + } + const { player } = await cliSession(); + const deviceId = resolved?.route === "web" ? resolved.device.id : undefined; + const applied = + mode === "cycle" + ? nextRepeatState((await player.state("foreground"))?.repeat_state ?? "off") + : mode; + await player.setRepeat(applied, deviceId); + return { + data: { repeat: applied, deviceId: deviceId ?? null }, + message: `Repeat set to ${applied}.`, + }; +} diff --git a/src/cli/operations/playlists.ts b/src/cli/operations/playlists.ts new file mode 100644 index 0000000..1a61a4c --- /dev/null +++ b/src/cli/operations/playlists.ts @@ -0,0 +1,57 @@ +import { + addPlaylistItems, + myPlaylists, + type Playlist, +} from "../../api/playlists.ts"; +import { usageError } from "../errors.ts"; +import { cliSession } from "../session.ts"; +import { playlistId, table } from "../support.ts"; +import { spotifyReference } from "../values.ts"; +import type { OperationResult } from "./types.ts"; + +export async function playlistList( + options: { ownedOnly?: boolean } = {}, +): Promise> { + const session = await cliSession(); + const me = await session.profile(); + const all = (await myPlaylists(session.client, me.id)).filter( + (item) => options.ownedOnly !== true || item.mine, + ); + return { + data: all, + message: table( + ["OWNED", "NAME", "OWNER", "URI"], + all.map((item) => [ + item.mine ? "yes" : "no", + item.name, + item.ownerName, + item.uri, + ]), + ), + }; +} + +export async function playlistAdd( + playlist: string, + items: string[], +): Promise>> { + const uris = items.map((item) => { + const ref = spotifyReference(item); + if (ref.kind !== "track" && ref.kind !== "episode") + throw usageError("Playlists accept tracks and episodes only."); + return ref.uri; + }); + const snapshotId = await addPlaylistItems( + (await cliSession()).client, + playlistId(playlist), + uris, + ); + return { + data: { + playlist: spotifyReference(playlist, "playlist").uri, + uris, + snapshotId, + }, + message: `Added ${uris.length} item${uris.length === 1 ? "" : "s"}.`, + }; +} diff --git a/src/cli/operations/queue.ts b/src/cli/operations/queue.ts new file mode 100644 index 0000000..c668033 --- /dev/null +++ b/src/cli/operations/queue.ts @@ -0,0 +1,84 @@ +import { runtimeRequest, tryRuntimeRequest } from "../../runtime/control.ts"; +import { usageError } from "../errors.ts"; +import { normalizeItem, normalizeRuntimePlayback } from "../output.ts"; +import { cliSession } from "../session.ts"; +import { activeDeviceTarget, itemRows, table } from "../support.ts"; +import { spotifyReference } from "../values.ts"; +import type { OperationResult } from "./types.ts"; + +/** + * The item to report as playing now. + * + * The Web queue's `currently_playing` lags native events exactly like `/me/player`, so a connected + * runtime's snapshot is authoritative for the current item — including when it says nothing is + * playing. The upcoming list exists only in the Web response and stays with it. + */ +export function queueCurrentItem( + runtime: { connected: true; value: unknown } | { connected: false }, + webCurrent: Record | null, +): Record | null { + if (!runtime.connected) return webCurrent; + const item = normalizeRuntimePlayback(runtime.value)["item"]; + return item !== null && typeof item === "object" + ? (item as Record) + : null; +} + +export async function queueList(): Promise< + OperationResult> +> { + const value = await (await cliSession()).player.queue(); + const runtime = await tryRuntimeRequest("status"); + const current = queueCurrentItem( + runtime, + normalizeItem(value.currently_playing), + ); + const data = { + current, + items: value.queue.map(normalizeItem), + }; + const currentLine = + current === null + ? "Nothing is playing." + : `Now ${String(current["name"] ?? "Unknown")} — ${String(current["artist"] ?? "")}`; + const upcoming = + value.queue.length === 0 + ? "Queue is empty." + : table(["#", "TITLE", "ARTIST", "TIME", "URI"], itemRows(value.queue)); + return { data, message: `${currentLine}\n\n${upcoming}` }; +} + +export async function queueAdd( + items: string[], + options: { device?: string } = {}, +): Promise>> { + if (items.length === 0) { + throw usageError("At least one track or episode is required."); + } + const uris = items.map((item) => { + const ref = spotifyReference(item); + if (ref.kind !== "track" && ref.kind !== "episode") + throw usageError("Only tracks and episodes can be added to the queue."); + return ref.uri; + }); + const resolved = await activeDeviceTarget(options.device); + const message = `Added ${uris.length} item${uris.length === 1 ? "" : "s"} to the queue.`; + // A running runtime owns the queue path: additions go through its client and serialize + // with the other mutations instead of racing them from a second Web API session. + if (resolved === undefined || resolved.route === "local") { + const first = await tryRuntimeRequest("queue.add", { uri: uris[0] }); + if (first.connected) { + for (const uri of uris.slice(1)) { + await runtimeRequest("queue.add", { uri }); + } + return { + data: { source: "runtime", uris, deviceId: resolved?.id ?? null }, + message, + }; + } + } + const { player } = await cliSession(); + const deviceId = resolved?.route === "web" ? resolved.device.id : undefined; + for (const uri of uris) await player.addToQueue(uri, deviceId); + return { data: { uris, deviceId: deviceId ?? null }, message }; +} diff --git a/src/cli/operations/types.ts b/src/cli/operations/types.ts new file mode 100644 index 0000000..b434396 --- /dev/null +++ b/src/cli/operations/types.ts @@ -0,0 +1,12 @@ +/** + * Command orchestration shared by the CLI commands and the MCP server. + * + * An operation performs the routed work — device resolution, runtime-first dispatch, Web API + * fallback — and returns the machine data plus the human message. Frontends own argument parsing + * and emission: the CLI feeds the result to its output formatter, the MCP server to a tool result. + * Domain failures throw (`CliError` and the typed API errors); each frontend maps them itself. + */ +export interface OperationResult { + data: T; + message: string; +} diff --git a/src/cli/output.ts b/src/cli/output.ts index b07750b..1365b68 100644 --- a/src/cli/output.ts +++ b/src/cli/output.ts @@ -1,8 +1,12 @@ import { artistLine, isTrack, + type Episode, + type FullArtist, type PlayableItem, type PlaybackState, + type SimpleAudiobook, + type SimpleShow, } from "../api/types.ts"; import type { Writable } from "node:stream"; import { stripVTControlCharacters } from "node:util"; @@ -202,6 +206,47 @@ export function normalizeItem( }; } +export function normalizeEpisode(episode: Episode): Record { + return normalizeItem(episode) ?? {}; +} + +export function normalizeShow(show: SimpleShow): Record { + return { + type: "show", + id: show.id, + uri: show.uri, + name: show.name, + publisher: show.publisher ?? null, + description: show.description ?? null, + totalEpisodes: show.total_episodes ?? null, + }; +} + +export function normalizeAudiobook( + audiobook: SimpleAudiobook, +): Record { + return { + type: "audiobook", + id: audiobook.id, + uri: audiobook.uri, + name: audiobook.name, + authors: (audiobook.authors ?? []).map((author) => author.name), + publisher: audiobook.publisher ?? null, + totalChapters: audiobook.total_chapters ?? null, + }; +} + +export function normalizeArtist(artist: FullArtist): Record { + return { + type: "artist", + id: artist.id, + uri: artist.uri, + name: artist.name, + genres: artist.genres ?? [], + followers: artist.followers?.total ?? null, + }; +} + export function normalizePlayback( state: PlaybackState | null, ): Record { diff --git a/src/cli/session.ts b/src/cli/session.ts index ec7eb8b..d28a505 100644 --- a/src/cli/session.ts +++ b/src/cli/session.ts @@ -62,3 +62,7 @@ export function cliSession(): Promise { export function resetCliSessionForTests(): void { pendingSession = undefined; } + +export function primeCliSessionForTests(session: CliSession): void { + pendingSession = Promise.resolve(session); +} diff --git a/src/cli/support.ts b/src/cli/support.ts index 94b74c5..7f87803 100644 --- a/src/cli/support.ts +++ b/src/cli/support.ts @@ -185,6 +185,18 @@ export async function resolveDeviceTarget( return { route: "web", device }; } +/** Resolve a `--device` selector for a state command, refusing the idle embedded receiver up front. */ +export async function activeDeviceTarget( + selector: string | undefined, +): Promise { + if (selector === undefined) return undefined; + const resolved = await resolveDeviceTarget(selector); + if (resolved.route === "local" && !resolved.active) { + inactiveReceiver(resolved.name); + } + return resolved; +} + /** The refusal for state commands aimed at an idle receiver, where the Web API would 404 anyway. */ export function inactiveReceiver(name: string): never { throw unavailable( diff --git a/src/cli/values.ts b/src/cli/values.ts index ac04d9e..e723d5c 100644 --- a/src/cli/values.ts +++ b/src/cli/values.ts @@ -81,6 +81,23 @@ export function spotifyUri(value: string): string { return spotifyReference(value).uri; } +/** The kinds Spotify's library endpoints accept, shared by save and remove. */ +export const LIBRARY_KINDS = [ + "track", + "episode", + "album", + "show", + "audiobook", +] as const; + +export function libraryUri(item: string, refusal: string): string { + const ref = spotifyReference(item); + if (!(LIBRARY_KINDS as readonly string[]).includes(ref.kind)) { + throw usageError(`Spotify ${ref.kind} resources ${refusal}.`); + } + return ref.uri; +} + export function integer(value: string, label: string, minimum = 0): number { if (!/^-?\d+$/.test(value)) throw usageError(`${label} must be an integer.`); const parsed = Number(value); diff --git a/test/cli-discovery.test.ts b/test/cli-discovery.test.ts index ef181da..59d616e 100644 --- a/test/cli-discovery.test.ts +++ b/test/cli-discovery.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { runtimeLyricsTrack } from "../src/cli/commands/discovery.ts"; +import { runtimeLyricsTrack } from "../src/cli/operations/catalog.ts"; const runtimeItem = { type: "track", diff --git a/test/cli-queue.test.ts b/test/cli-queue.test.ts index 19c8ca6..dbda842 100644 --- a/test/cli-queue.test.ts +++ b/test/cli-queue.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { queueCurrentItem } from "../src/cli/commands/queue-devices.ts"; +import { queueCurrentItem } from "../src/cli/operations/queue.ts"; const webCurrent = { name: "Web Song", artist: "Web Artist" }; const runtimeItem = { diff --git a/test/operations-playback.test.ts b/test/operations-playback.test.ts new file mode 100644 index 0000000..eece2b2 --- /dev/null +++ b/test/operations-playback.test.ts @@ -0,0 +1,288 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import type { TokenStore } from "../src/auth/tokens.ts"; +import { + createCliSession, + primeCliSessionForTests, + resetCliSessionForTests, +} from "../src/cli/session.ts"; +import { + pausePlayback, + seekPlayback, + setVolume, + skip, + startPlayback, +} from "../src/cli/operations/playback.ts"; +import { queueAdd } from "../src/cli/operations/queue.ts"; +import { + startControlServer, + type ControlPaths, + type ControlServer, +} from "../src/runtime/control.ts"; + +const realFetch = globalThis.fetch; +const realRuntimeDir = process.env["SPOTUIFY_RUNTIME_DIR"]; + +let server: ControlServer | undefined; +let directory: string | undefined; +let sessionDirectory: string | undefined; + +async function runtimePaths(): Promise { + const socketRoot = process.platform === "darwin" ? "/private/tmp" : tmpdir(); + directory = await mkdtemp(join(socketRoot, "spotuify-operations-test-")); + return { + directory, + descriptor: join(directory, "control.json"), + endpoint: join(directory, "control.sock"), + }; +} + +/** Start a recording runtime and point the operations' default control paths at it. */ +async function startFakeRuntime( + respond: (method: string, params: unknown) => unknown, +): Promise<{ calls: { method: string; params: unknown }[] }> { + const calls: { method: string; params: unknown }[] = []; + const paths = await runtimePaths(); + server = await startControlServer( + (method, params) => { + calls.push({ method, params }); + return respond(method, params); + }, + { paths }, + ); + process.env["SPOTUIFY_RUNTIME_DIR"] = paths.directory; + return { calls }; +} + +/** Point the default control paths at an empty directory: no runtime is reachable. */ +async function withoutRuntime(): Promise { + const paths = await runtimePaths(); + process.env["SPOTUIFY_RUNTIME_DIR"] = paths.directory; +} + +/** Prime the shared session with a fake token store and a recording Web API. */ +async function primeWebSession( + respond: (path: string, init?: RequestInit) => Response, +): Promise { + const paths: string[] = []; + globalThis.fetch = (async ( + input: string | URL | Request, + init?: RequestInit, + ) => { + const path = new URL(String(input)).pathname.replace("/v1", ""); + paths.push(path); + return respond(path, init); + }) as unknown as typeof fetch; + const tokens = { + accessToken: async () => "token", + refresh: async () => { + throw new Error("unexpected refresh"); + }, + authorizationId: async () => "authorization", + } as unknown as TokenStore; + sessionDirectory = await mkdtemp( + join(tmpdir(), "spotuify-operations-session-"), + ); + primeCliSessionForTests( + await createCliSession(tokens, { + profilePath: join(sessionDirectory, "profile.json"), + }), + ); + return paths; +} + +beforeEach(() => { + resetCliSessionForTests(); +}); + +afterEach(async () => { + globalThis.fetch = realFetch; + if (realRuntimeDir === undefined) delete process.env["SPOTUIFY_RUNTIME_DIR"]; + else process.env["SPOTUIFY_RUNTIME_DIR"] = realRuntimeDir; + resetCliSessionForTests(); + await server?.close(); + server = undefined; + for (const path of [directory, sessionDirectory]) { + if (path !== undefined) await rm(path, { recursive: true, force: true }); + } + directory = undefined; + sessionDirectory = undefined; +}); + +describe("operation validation", () => { + test("startPlayback refuses an index without a context target", async () => { + await expect(startPlayback({ index: 3 })).rejects.toThrow( + "requires an album or playlist target", + ); + }); + + test("startPlayback refuses an index on a track", async () => { + await expect( + startPlayback({ target: "spotify:track:abc123", index: 2 }), + ).rejects.toThrow("only valid for album or playlist contexts"); + }); + + test("startPlayback refuses a show context with the episode hint", async () => { + await expect(startPlayback({ target: "spotify:show:abc123" })).rejects.toThrow( + "cannot be played as a context", + ); + }); + + test("seekPlayback refuses a position and an offset together", async () => { + await expect( + seekPlayback({ positionMs: 1_000, offsetMs: 1_000 }), + ).rejects.toThrow("not both"); + }); + + test("setVolume bounds an absolute level", async () => { + await expect(setVolume({ percent: 101 })).rejects.toThrow( + "between 0 and 100", + ); + }); + + test("queueAdd accepts only tracks and episodes", async () => { + await expect(queueAdd(["spotify:album:abc123"])).rejects.toThrow( + "Only tracks and episodes", + ); + }); +}); + +describe("runtime-first routing", () => { + test("startPlayback without a device hands the command to the runtime", async () => { + const { calls } = await startFakeRuntime(() => ({ isPlaying: true })); + const result = await startPlayback({ target: "spotify:track:abc123" }); + expect(calls).toEqual([ + { method: "play", params: { uris: ["spotify:track:abc123"] } }, + ]); + expect(result.data["source"]).toBe("runtime"); + expect(result.message).toBe("Playing spotify:track:abc123."); + }); + + test("startPlayback aimed at the idle receiver transfers natively first", async () => { + const { calls } = await startFakeRuntime((method) => + method === "device.list" + ? { + devices: [ + { + id: "local-id", + name: "spotuify", + type: "Computer", + is_active: false, + is_restricted: false, + volume_percent: 50, + }, + ], + localDeviceId: "local-id", + } + : {}, + ); + const result = await startPlayback({ + target: "spotify:album:abc123", + device: "spotuify", + index: 2, + }); + expect(calls.map((call) => call.method)).toEqual([ + "device.list", + "device.transfer", + "play", + ]); + expect(calls[1]?.params).toEqual({ selector: "local-id", play: false }); + expect(calls[2]?.params).toEqual({ + contextUri: "spotify:album:abc123", + offset: 1, + }); + expect(result.data["deviceId"]).toBe("local-id"); + }); + + test("a relative seek sends the raw offset for serialized application", async () => { + const { calls } = await startFakeRuntime(() => ({ progressMs: 4_000 })); + const result = await seekPlayback({ offsetMs: -2_000 }); + expect(calls).toEqual([ + { method: "seek", params: { offsetMs: -2_000 } }, + ]); + expect(result.data["positionMs"]).toBe(4_000); + }); + + test("an absolute seek clamps the position before sending it", async () => { + const { calls } = await startFakeRuntime(() => ({ progressMs: 0 })); + await seekPlayback({ positionMs: 5_000 }); + expect(calls).toEqual([ + { method: "seek", params: { positionMs: 5_000 } }, + ]); + }); + + test("a relative volume change sends the raw delta", async () => { + const { calls } = await startFakeRuntime(() => ({ + device: { volumePercent: 55 }, + })); + const result = await setVolume({ delta: 5 }); + expect(calls).toEqual([{ method: "volume", params: { delta: 5 } }]); + expect(result.data["volumePercent"]).toBe(55); + expect(result.message).toBe("Volume set to 55%."); + }); + + test("queueAdd serializes every addition through the runtime", async () => { + const { calls } = await startFakeRuntime(() => ({})); + const result = await queueAdd([ + "spotify:track:aaa111", + "spotify:track:bbb222", + ]); + expect(calls).toEqual([ + { method: "queue.add", params: { uri: "spotify:track:aaa111" } }, + { method: "queue.add", params: { uri: "spotify:track:bbb222" } }, + ]); + expect(result.data["source"]).toBe("runtime"); + }); +}); + +describe("web fallback routing", () => { + test("pausePlayback falls back to the Web API without a runtime", async () => { + await withoutRuntime(); + const paths = await primeWebSession(() => new Response(null, { status: 204 })); + const result = await pausePlayback(); + expect(paths).toEqual(["/me/player/pause"]); + expect(result.data).toEqual({ deviceId: null }); + expect(result.message).toBe("Playback paused."); + }); + + test("skip falls back to the Web API without a runtime", async () => { + await withoutRuntime(); + const paths = await primeWebSession(() => new Response(null, { status: 204 })); + const result = await skip("next"); + expect(paths).toEqual(["/me/player/next"]); + expect(result.message).toBe("Skipped to next item."); + }); + + test("a relative web seek resolves the current position first", async () => { + await withoutRuntime(); + const paths = await primeWebSession((path) => + path === "/me/player" + ? Response.json({ + is_playing: true, + progress_ms: 10_000, + item: null, + shuffle_state: false, + repeat_state: "off", + context: null, + device: null, + }) + : new Response(null, { status: 204 }), + ); + const result = await seekPlayback({ offsetMs: -4_000 }); + expect(paths).toEqual(["/me/player", "/me/player/seek"]); + expect(result.data["positionMs"]).toBe(6_000); + }); + + test("queueAdd falls back to one Web request per item", async () => { + await withoutRuntime(); + const paths = await primeWebSession(() => new Response(null, { status: 204 })); + const result = await queueAdd([ + "spotify:track:aaa111", + "spotify:episode:bbb222", + ]); + expect(paths).toEqual(["/me/player/queue", "/me/player/queue"]); + expect(result.data["deviceId"]).toBe(null); + }); +}); From 81925418dcf3c097b349caf5feceda769276c690 Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Mon, 3 Aug 2026 23:10:46 -0700 Subject: [PATCH 2/3] add mcp server behind spotuify mcp run a model context protocol server on stdio with eighteen tools covering playback, queue, devices, search, resource details, lyrics, library, and playlists, all calling the shared operations layer so structured output matches the documented --json shapes. the server is a runtime client like every one-shot command, never starts an auth flow or engine, keeps stdout protocol-only, and exits when the client closes stdin or sends a signal. domain failures are tool errors with the cli's message and hint, never protocol errors. uses the stable @modelcontextprotocol/sdk 1.x line with zod. the license walker now resolves a package's real manifest when an exports map answers with a type stub, and the javascript allowlist gains isc and bsd-2-clause for the sdk's transitive tree. --- THIRD_PARTY_NOTICES.txt | 2511 +++++++++++++++++++++++++++++++--- bun.lock | 188 +++ package.json | 2 + src/cli/commands/mcp.ts | 33 + src/cli/program.ts | 3 + src/mcp/errors.ts | 24 + src/mcp/result.ts | 32 + src/mcp/server.ts | 77 ++ src/mcp/tools/browse.ts | 98 ++ src/mcp/tools/library.ts | 124 ++ src/mcp/tools/playback.ts | 341 +++++ test/mcp-server.test.ts | 259 ++++ test/mcp-stdio.test.ts | 122 ++ tools/licenses/javascript.ts | 40 +- 14 files changed, 3620 insertions(+), 234 deletions(-) create mode 100644 src/cli/commands/mcp.ts create mode 100644 src/mcp/errors.ts create mode 100644 src/mcp/result.ts create mode 100644 src/mcp/server.ts create mode 100644 src/mcp/tools/browse.ts create mode 100644 src/mcp/tools/library.ts create mode 100644 src/mcp/tools/playback.ts create mode 100644 test/mcp-server.test.ts create mode 100644 test/mcp-stdio.test.ts diff --git a/THIRD_PARTY_NOTICES.txt b/THIRD_PARTY_NOTICES.txt index 59b5691..1c0b0ee 100644 --- a/THIRD_PARTY_NOTICES.txt +++ b/THIRD_PARTY_NOTICES.txt @@ -9068,6 +9068,70 @@ If the Work includes a "NOTICE" text file as part of its distribution, then any END OF TERMS AND CONDITIONS +=============================================================================== +BSD-2-Clause JavaScript packages + +Used by: +- json-schema-typed 8.0.2 + +BSD 2-Clause License + +Original source code is copyright (c) 2019-2025 Remy Rylan + + +All JSON Schema documentation and descriptions are copyright (c): + +2009 [draft-0] IETF Trust , Kris Zyp , +and SitePen (USA) . + +2009 [draft-1] IETF Trust , Kris Zyp , +and SitePen (USA) . + +2010 [draft-2] IETF Trust , Kris Zyp , +and SitePen (USA) . + +2010 [draft-3] IETF Trust , Kris Zyp , +Gary Court , and SitePen (USA) . + +2013 [draft-4] IETF Trust ), Francis Galiegue +, Kris Zyp , Gary Court +, and SitePen (USA) . + +2018 [draft-7] IETF Trust , Austin Wright , +Henry Andrews , Geraint Luff , and +Cloudflare, Inc. . + +2019 [draft-2019-09] IETF Trust , Austin Wright +, Henry Andrews , Ben Hutton +, and Greg Dennis . + +2020 [draft-2020-12] IETF Trust , Austin Wright +, Henry Andrews , Ben Hutton +, and Greg Dennis . + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + =============================================================================== BSD-3-Clause JavaScript packages @@ -9107,6 +9171,79 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =============================================================================== BSD-3-Clause JavaScript packages +Used by: +- qs 6.15.3 + +BSD 3-Clause License + +Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=============================================================================== +BSD-3-Clause JavaScript packages + +Used by: +- fast-uri 3.1.5 + +Copyright (c) 2011-2021, Gary Court until https://github.com/garycourt/uri-js/commit/a1acf730b4bba3f1097c9f52e7d9d3aba8cdcaae +Copyright (c) 2021-present The Fastify team +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The names of any contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * * * + +The complete list of contributors can be found at: +- https://github.com/garycourt/uri-js/graphs/contributors + +=============================================================================== +BSD-3-Clause JavaScript packages + Used by: - jpeg-js 0.4.4 @@ -9137,163 +9274,141 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. =============================================================================== -MIT JavaScript packages +ISC JavaScript packages Used by: -- commander 15.0.0 - -(The MIT License) - -Copyright (c) 2011 TJ Holowaychuk +- setprototypeof 1.2.0 -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +Copyright (c) 2015, Wes Todd -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. =============================================================================== -MIT JavaScript packages +ISC JavaScript packages Used by: -- marked 17.0.1 - -# License information - -## Contribution License Agreement - -If you contribute code to this project, you are implicitly allowing your code -to be distributed under the MIT license. You are also implicitly verifying that -all code is your original work. `` - -## Marked - -Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) -Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +- zod-to-json-schema 3.25.2 -## Markdown - -Copyright © 2004, John Gruber -http://daringfireball.net/ -All rights reserved. +ISC License -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +Copyright (c) 2020, Stefan Terdell -* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. =============================================================================== -MIT JavaScript packages +ISC JavaScript packages Used by: -- ws 8.20.1 +- inherits 2.0.4 -Copyright (c) 2011 Einar Otto Stangvik -Copyright (c) 2013 Arnout Kazemier and contributors -Copyright (c) 2016 Luigi Pinca and contributors +The ISC License -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: +Copyright (c) Isaac Z. Schlueter -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. =============================================================================== -MIT JavaScript packages +ISC JavaScript packages Used by: -- csstype 3.2.3 +- isexe 2.0.0 +- once 1.4.0 +- which 2.0.2 +- wrappy 1.0.2 -Copyright (c) 2017-2018 Fredrik Nicol +The ISC License -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Copyright (c) Isaac Z. Schlueter and Contributors -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. =============================================================================== MIT JavaScript packages Used by: -- bun-ffi-structs 0.2.4 +- express 5.2.1 -Copyright 2025 Anomaly +(The MIT License) -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Copyright (c) 2009-2014 TJ Holowaychuk +Copyright (c) 2013-2014 Roman Shtylman +Copyright (c) 2014-2015 Douglas Christopher Wilson -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =============================================================================== MIT JavaScript packages Used by: -- emoji-regex 10.6.0 +- serve-static 2.2.1 -Copyright Mathias Bynens +(The MIT License) + +Copyright (c) 2010 Sencha Inc. +Copyright (c) 2011 LearnBoost +Copyright (c) 2011 TJ Holowaychuk +Copyright (c) 2014-2016 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including +'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to @@ -9302,51 +9417,1925 @@ the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =============================================================================== MIT JavaScript packages Used by: -- @types/react 19.2.18 +- commander 15.0.0 + +(The MIT License) + +Copyright (c) 2011 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- send 1.2.1 + +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- fresh 2.0.0 + +(The MIT License) + +Copyright (c) 2012 TJ Holowaychuk +Copyright (c) 2016-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- escape-html 1.0.3 + +(The MIT License) + +Copyright (c) 2012-2013 TJ Holowaychuk +Copyright (c) 2015 Andreas Lubbe +Copyright (c) 2015 Tiancheng "Timothy" Gu + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- negotiator 1.0.0 + +(The MIT License) + +Copyright (c) 2012-2014 Federico Romero +Copyright (c) 2012-2014 Isaac Z. Schlueter +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- cookie 0.7.2 + +(The MIT License) + +Copyright (c) 2012-2014 Roman Shtylman +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- bytes 3.1.2 + +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015 Jed Watson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- range-parser 1.3.0 + +(The MIT License) + +Copyright (c) 2012-2014 TJ Holowaychuk +Copyright (c) 2015-2016 Douglas Christopher Wilson and other contributors; + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- on-finished 2.4.1 + +(The MIT License) + +Copyright (c) 2013 Jonathan Ong +Copyright (c) 2014 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- router 2.2.0 + +(The MIT License) + +Copyright (c) 2013 Roman Shtylman +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- cors 2.8.6 + +(The MIT License) + +Copyright (c) 2013 Troy Goode + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- body-parser 2.3.0 +- type-is 2.1.0 + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- parseurl 1.3.3 + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- accepts 2.0.0 +- mime-types 3.0.2 + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- mime-db 1.54.0 + +(The MIT License) + +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2015-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- etag 1.8.1 +- proxy-addr 2.0.7 + +(The MIT License) + +Copyright (c) 2014-2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- content-disposition 1.1.0 +- forwarded 0.2.0 +- media-typer 1.1.1 +- vary 1.1.2 + +(The MIT License) + +Copyright (c) 2014-2017 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- debug 4.4.3 + +(The MIT License) + +Copyright (c) 2014-2017 TJ Holowaychuk +Copyright (c) 2018-2021 Josh Junon + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software +and associated documentation files (the 'Software'), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- depd 2.0.0 + +(The MIT License) + +Copyright (c) 2014-2018 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- finalhandler 2.1.1 + +(The MIT License) + +Copyright (c) 2014-2022 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- content-type 1.0.5 +- content-type 2.0.0 + +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- unpipe 1.0.0 + +(The MIT License) + +Copyright (c) 2015 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- encodeurl 2.0.0 + +(The MIT License) + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- marked 17.0.1 + +# License information + +## Contribution License Agreement + +If you contribute code to this project, you are implicitly allowing your code +to be distributed under the MIT license. You are also implicitly verifying that +all code is your original work. `` + +## Marked + +Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) +Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## Markdown + +Copyright © 2004, John Gruber +http://daringfireball.net/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. + +=============================================================================== +MIT JavaScript packages + +Used by: +- express-rate-limit 8.6.1 + +# MIT License + +Copyright 2023 Nathan Friedly, Vedant K + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- iconv-lite 0.7.3 + +Copyright (c) 2011 Alexander Shtuchkin + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- ip-address 10.4.0 + +Copyright (C) 2011 by Beau Gunderson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- ws 8.20.1 + +Copyright (c) 2011 Einar Otto Stangvik +Copyright (c) 2013 Arnout Kazemier and contributors +Copyright (c) 2016 Luigi Pinca and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- ipaddr.js 1.9.1 + +Copyright (C) 2011-2017 whitequark + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- function-bind 1.1.2 + +Copyright (c) 2013 Raynos. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- is-promise 4.0.0 + +Copyright (c) 2014 Forbes Lindesay + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- csstype 3.2.3 + +Copyright (c) 2017-2018 Fredrik Nicol + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- bun-ffi-structs 0.2.4 + +Copyright 2025 Anomaly + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- emoji-regex 10.6.0 + +Copyright Mathias Bynens + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- @types/react 19.2.18 + +MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE + +=============================================================================== +MIT JavaScript packages + +Used by: +- object-inspect 1.13.4 + +MIT License + +Copyright (c) 2013 James Halliday + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- toidentifier 1.0.1 + +MIT License + +Copyright (c) 2016 Douglas Christopher Wilson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- has-symbols 1.1.0 + +MIT License + +Copyright (c) 2016 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- fast-deep-equal 3.1.3 +- json-schema-traverse 1.0.0 + +MIT License + +Copyright (c) 2017 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- safer-buffer 2.1.2 + +MIT License + +Copyright (c) 2018 Nikita Skovoroda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- sisteransi 1.0.5 + +MIT License + +Copyright (c) 2018 Terkel Gjervig Nielsen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- pkce-challenge 5.0.1 + +MIT License + +Copyright (c) 2019 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- side-channel-weakmap 1.0.2 +- side-channel 1.1.1 + +MIT License + +Copyright (c) 2019 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- zustand 5.0.14 + +MIT License + +Copyright (c) 2019 Paul Henschel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- ajv-formats 3.0.1 + +MIT License + +Copyright (c) 2020 Evgeny Poberezkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- get-intrinsic 1.3.0 + +MIT License + +Copyright (c) 2020 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- hono 4.12.33 + +MIT License + +Copyright (c) 2021 - present, Yusuke Wada and Hono contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- @hono/node-server 2.0.12 + +MIT License + +Copyright (c) 2022 - present, Yusuke Wada and Hono contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- gopd 1.2.0 + +MIT License + +Copyright (c) 2022 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- @modelcontextprotocol/sdk 1.30.0 + +MIT License + +Copyright (c) 2024 Anthropic, PBC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- dunder-proto 1.0.1 +- math-intrinsics 1.1.0 + +MIT License + +Copyright (c) 2024 ECMAScript Shims + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- call-bind-apply-helpers 1.0.2 +- call-bound 1.0.4 +- es-define-property 1.0.1 +- es-errors 1.3.0 +- es-object-atoms 1.1.2 +- side-channel-list 1.0.1 +- side-channel-map 1.0.1 + +MIT License + +Copyright (c) 2024 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- zod 4.4.3 + +MIT License + +Copyright (c) 2025 Colin McDonnell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- fast-wrap-ansi 0.2.2 + +MIT License + +Copyright (c) 2025 James Garbutt + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- get-proto 1.0.1 + +MIT License + +Copyright (c) 2025 Jordan Harband + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- @opentui/core platform binary packages 0.4.5 +- @opentui/core 0.4.5 +- @opentui/react 0.4.5 MIT License - Copyright (c) Microsoft Corporation. +Copyright (c) 2025 opentui - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- eventsource-parser 3.1.0 + +MIT License + +Copyright (c) 2026 Espen Hovlandsdal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- merge-descriptors 2.0.0 + +MIT License + +Copyright (c) Jonathan Ong +Copyright (c) Douglas Christopher Wilson +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- hasown 2.0.4 + +MIT License + +Copyright (c) Jordan Harband and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- shebang-command 2.0.0 + +MIT License + +Copyright (c) Kevin Mårtensson (github.com/kevva) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- react-devtools-core 7.0.1 +- react-reconciler 0.33.0 +- react 19.2.8 +- scheduler 0.27.0 + +MIT License + +Copyright (c) Meta Platforms, Inc. and affiliates. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- ansi-escapes 7.3.0 +- ansi-regex 6.2.2 +- environment 1.1.0 +- get-east-asian-width 1.6.0 +- has-flag 5.0.1 +- string-width 7.2.0 +- strip-ansi 7.1.2 +- supports-color 10.2.2 + +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- supports-hyperlinks 4.5.0 + +MIT License + +Copyright (c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) James Talmage (https://github.com/jamestalmage) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- path-key 3.1.1 +- shebang-regex 3.0.0 + +MIT License + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- @clack/core 1.4.3 +- @clack/prompts 1.7.0 + +MIT License + +MIT License Copyright (c) 2025-Present [Bombshell contributors](https://bomb.sh/team) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- shell-quote 1.8.3 + +The MIT License + +Copyright (c) 2013 James Halliday (mail@substack.net) + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and +associated documentation files (the "Software"), to +deal in the Software without restriction, including +without limitation the rights to use, copy, modify, +merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom +the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- eventsource 3.0.7 + +The MIT License + +Copyright (c) EventSource GitHub organisation + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =============================================================================== MIT JavaScript packages Used by: -- sisteransi 1.0.5 +- ws 7.5.10 -MIT License +The MIT License (MIT) -Copyright (c) 2018 Terkel Gjervig Nielsen +Copyright (c) 2011 Einar Otto Stangvik Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9370,11 +11359,12 @@ SOFTWARE. MIT JavaScript packages Used by: -- zustand 5.0.14 +- raw-body 3.0.2 -MIT License +The MIT License (MIT) -Copyright (c) 2019 Paul Henschel +Copyright (c) 2013-2014 Jonathan Ong +Copyright (c) 2014-2022 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9383,28 +11373,26 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. =============================================================================== MIT JavaScript packages Used by: -- fast-wrap-ansi 0.2.2 +- path-to-regexp 8.4.2 -MIT License - -Copyright (c) 2025 James Garbutt +The MIT License (MIT) -Copyright (c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9413,28 +11401,27 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. =============================================================================== MIT JavaScript packages Used by: -- @opentui/core platform binary packages 0.4.5 -- @opentui/core 0.4.5 -- @opentui/react 0.4.5 +- statuses 2.0.2 -MIT License +The MIT License (MIT) -Copyright (c) 2025 opentui +Copyright (c) 2014 Jonathan Ong +Copyright (c) 2016 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9443,29 +11430,26 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. =============================================================================== MIT JavaScript packages Used by: -- react-devtools-core 7.0.1 -- react-reconciler 0.33.0 -- react 19.2.8 -- scheduler 0.27.0 +- ee-first 1.1.1 -MIT License +The MIT License (MIT) -Copyright (c) Meta Platforms, Inc. and affiliates. +Copyright (c) 2014 Jonathan Ong me@jongleberry.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9474,114 +11458,139 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. =============================================================================== MIT JavaScript packages Used by: -- ansi-escapes 7.3.0 -- ansi-regex 6.2.2 -- environment 1.1.0 -- get-east-asian-width 1.6.0 -- has-flag 5.0.1 -- string-width 7.2.0 -- strip-ansi 7.1.2 -- supports-color 10.2.2 +- http-errors 2.0.1 -MIT License +The MIT License (MIT) -Copyright (c) Sindre Sorhus (https://sindresorhus.com) +Copyright (c) 2014 Jonathan Ong me@jongleberry.com +Copyright (c) 2016 Douglas Christopher Wilson doug@somethingdoug.com -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. =============================================================================== MIT JavaScript packages Used by: -- supports-hyperlinks 4.5.0 +- ajv 8.20.0 -MIT License +The MIT License (MIT) -Copyright (c) Sindre Sorhus (https://sindresorhus.com) -Copyright (c) James Talmage (https://github.com/jamestalmage) +Copyright (c) 2015-2021 Evgeny Poberezkin -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. =============================================================================== MIT JavaScript packages Used by: -- @clack/core 1.4.3 -- @clack/prompts 1.7.0 +- jose 6.2.7 -MIT License +The MIT License (MIT) -MIT License Copyright (c) 2025-Present [Bombshell contributors](https://bomb.sh/team) +Copyright (c) 2018 Filip Skokan -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. =============================================================================== MIT JavaScript packages Used by: -- shell-quote 1.8.3 +- cross-spawn 7.0.6 -The MIT License +The MIT License (MIT) -Copyright (c) 2013 James Halliday (mail@substack.net) +Copyright (c) 2018 Made With MOXY Lda -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. =============================================================================== MIT JavaScript packages Used by: -- ws 7.5.10 +- web-tree-sitter 0.25.10 The MIT License (MIT) -Copyright (c) 2011 Einar Otto Stangvik +Copyright (c) 2018-2024 Max Brunsfeld Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9605,11 +11614,11 @@ SOFTWARE. MIT JavaScript packages Used by: -- web-tree-sitter 0.25.10 +- ms 2.1.3 The MIT License (MIT) -Copyright (c) 2018-2024 Max Brunsfeld +Copyright (c) 2020 Vercel, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9657,3 +11666,59 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- object-assign 4.1.1 + +The MIT License (MIT) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +=============================================================================== +MIT JavaScript packages + +Used by: +- require-from-string 2.0.2 + +The MIT License (MIT) + +Copyright (c) Vsevolod Strukchinsky (github.com/floatdrop) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/bun.lock b/bun.lock index 0db74b3..9b47fb7 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "name": "spotuify", "dependencies": { "@clack/prompts": "1.7.0", + "@modelcontextprotocol/sdk": "1.30.0", "@opentui/core": "0.4.5", "@opentui/react": "0.4.5", "ansi-escapes": "7.3.0", @@ -13,6 +14,7 @@ "jpeg-js": "0.4.4", "react": "19.2.8", "supports-hyperlinks": "4.5.0", + "zod": "4.4.3", "zustand": "5.0.14", }, "devDependencies": { @@ -27,6 +29,10 @@ "@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="], + "@hono/node-server": ["@hono/node-server@2.0.12", "", { "peerDependencies": { "hono": "^4" } }, "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="], + "@opentui/core": ["@opentui/core@0.4.5", "", { "dependencies": { "bun-ffi-structs": "0.2.4", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.4.5", "@opentui/core-darwin-x64": "0.4.5", "@opentui/core-linux-arm64": "0.4.5", "@opentui/core-linux-arm64-musl": "0.4.5", "@opentui/core-linux-x64": "0.4.5", "@opentui/core-linux-x64-musl": "0.4.5", "@opentui/core-win32-arm64": "0.4.5", "@opentui/core-win32-x64": "0.4.5" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig=="], "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8KUG0oRidnR+oW1RSZJ72/PhZLl+qRRMk5U/mieF4c0SJ5V3tYACpBZAKzQfHNd1f7QzD8FHZct1lPpQgtmkWg=="], @@ -93,50 +99,212 @@ "@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "body-parser": ["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.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], + "bun-ffi-structs": ["bun-ffi-structs@0.2.4", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.1.0", "", {}, "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.6.1", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + "get-intrinsic": ["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.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "hono": ["hono@4.12.33", "", {}, "sha512-+SwvkaiJtxsiPjhy9LivY/1m7UsNqCJetM1BrZl9A5DkQhlbHQDU730mMiDPWjnoCYOM8Chf3WrCJw27kNTPFQ=="], + + "http-errors": ["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" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ip-address": ["ip-address@10.4.0", "", {}, "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jose": ["jose@6.2.7", "", {}, "sha512-hq1OB1bALKfydZNoViyg6hPVGV4i93ny9Op+n4zP5RSf7SCZEXa/TsG2O3IEr7+WlHRTPnpqDmHfMH6qXAD60w=="], + "jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], + + "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], "react-devtools-core": ["react-devtools-core@7.0.1", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-C3yNvRHaizlpiASzy7b9vbnBGLrhvdhl1CbdU6EnZgxPNbai60szdLtl+VL76UNOt5bOoVTOz5rNWZxgGt+Gsw=="], "react-reconciler": ["react-reconciler@0.33.0", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + "send": ["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.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], + "side-channel": ["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" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], @@ -145,16 +313,36 @@ "supports-hyperlinks": ["supports-hyperlinks@4.5.0", "", { "dependencies": { "has-flag": "^5.0.1", "supports-color": "^10.2.2" } }, "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "undici-types": ["undici-types@7.21.0", "", {}, "sha512-w9IMgQrz4O0YN1LtB7K5P63vhlIOvC7opSmouCJ+ZywlPAlO9gIkJ+otk6LvGpAs2wg4econaCz3TvQ9xPoyuQ=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "zustand": ["zustand@5.0.14", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g=="], + "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], } } diff --git a/package.json b/package.json index 71bd77d..a78acaa 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ }, "dependencies": { "@clack/prompts": "1.7.0", + "@modelcontextprotocol/sdk": "1.30.0", "@opentui/core": "0.4.5", "@opentui/react": "0.4.5", "ansi-escapes": "7.3.0", @@ -56,6 +57,7 @@ "jpeg-js": "0.4.4", "react": "19.2.8", "supports-hyperlinks": "4.5.0", + "zod": "4.4.3", "zustand": "5.0.14" } } diff --git a/src/cli/commands/mcp.ts b/src/cli/commands/mcp.ts new file mode 100644 index 0000000..fb2e3d0 --- /dev/null +++ b/src/cli/commands/mcp.ts @@ -0,0 +1,33 @@ +import { Command } from "commander"; +import { ExitCode, usageError } from "../errors.ts"; +import type { CliIo, GlobalOutputOptions } from "../output.ts"; +import type { RunState } from "../support.ts"; + +export function registerMcp( + program: Command, + io: CliIo, + state: RunState, +): void { + program + .command("mcp") + .description("Run the Model Context Protocol server on stdio") + .action(async (_options, command: Command) => { + const globals = command.optsWithGlobals() as GlobalOutputOptions; + if ( + globals.output !== undefined || + globals.json === true || + globals.plain === true || + globals.quiet === true || + globals.field !== undefined || + globals.template !== undefined + ) { + throw usageError( + "Output options do not apply to the MCP server.", + "The MCP protocol owns stdout; run `spotuify mcp` without output flags.", + ); + } + const { runMcpServer } = await import("../../mcp/server.ts"); + const { interrupted } = await runMcpServer(io); + if (interrupted) state.exitCode = ExitCode.interrupted; + }); +} diff --git a/src/cli/program.ts b/src/cli/program.ts index 9dc05ca..8c8cc97 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -3,6 +3,7 @@ import { VERSION } from "../version.ts"; import { registerDiscovery } from "./commands/discovery.ts"; import { registerFollow } from "./commands/follow.ts"; import { registerLibrary } from "./commands/library.ts"; +import { registerMcp } from "./commands/mcp.ts"; import { registerPlayback } from "./commands/playback.ts"; import { registerPlaylists } from "./commands/playlists.ts"; import { registerQueueAndDevices } from "./commands/queue-devices.ts"; @@ -56,6 +57,7 @@ const ROOT_COMMAND_GROUPS: Readonly> = { account: HELP_GROUP.system, config: HELP_GROUP.system, doctor: HELP_GROUP.system, + mcp: HELP_GROUP.system, service: HELP_GROUP.system, completion: HELP_GROUP.system, update: HELP_GROUP.system, @@ -252,6 +254,7 @@ export function createCliProgram(dependencies: CliDependencies = {}): { registerLibrary(program, io); registerFollow(program, io); registerPlaylists(program, io); + registerMcp(program, io, state); registerSystemCommands(program, io, presenter, state); groupRootCommands(program); addHelpTopics(program, presenter, io, standardHelp); diff --git a/src/mcp/errors.ts b/src/mcp/errors.ts new file mode 100644 index 0000000..50f1c55 --- /dev/null +++ b/src/mcp/errors.ts @@ -0,0 +1,24 @@ +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { asCliError } from "../cli/errors.ts"; + +/** + * Map a thrown domain error to an MCP tool result. + * + * Domain failures — missing credentials, rate limits, unavailable devices — are tool results, not + * protocol errors: the model reads them and can act on the hint. Protocol errors stay reserved for + * what the SDK raises itself (malformed arguments, unknown tools). `asCliError` already carries the + * complete error taxonomy, including the `spotuify auth` guidance and rate-limit retry times. + * + * Error results carry text only: clients validate `structuredContent` against a tool's output + * schema even on errors, so a structured error object would turn every failure on a + * schema-declaring tool into a protocol error. + */ +export function toolErrorResult(error: unknown): CallToolResult { + const cliError = asCliError(error); + const lines = [cliError.message]; + if (cliError.hint !== undefined) lines.push(`Hint: ${cliError.hint}`); + return { + isError: true, + content: [{ type: "text", text: lines.join("\n") }], + }; +} diff --git a/src/mcp/result.ts b/src/mcp/result.ts new file mode 100644 index 0000000..ea6e6ff --- /dev/null +++ b/src/mcp/result.ts @@ -0,0 +1,32 @@ +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { OperationResult } from "../cli/operations/types.ts"; +import { machineValue } from "../cli/output.ts"; +import { toolErrorResult } from "./errors.ts"; + +/** + * Wrap an operation result as an MCP tool result. + * + * `structuredContent` passes through `machineValue`, so tool output uses the same snake_case + * shapes the CLI's `--json` envelope documents in `docs/cli.md`. The human message doubles as the + * text content. Structured content must be an object, so operations whose CLI data is an array + * wrap it under a named key before reaching here. + */ +export function toolResult( + result: OperationResult>, +): CallToolResult { + return { + content: [{ type: "text", text: result.message }], + structuredContent: machineValue(result.data) as Record, + }; +} + +/** Run an operation, mapping any thrown domain error to an error tool result. */ +export async function runTool( + operation: () => Promise>>, +): Promise { + try { + return toolResult(await operation()); + } catch (error) { + return toolErrorResult(error); + } +} diff --git a/src/mcp/server.ts b/src/mcp/server.ts new file mode 100644 index 0000000..9d65aa8 --- /dev/null +++ b/src/mcp/server.ts @@ -0,0 +1,77 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import type { Writable } from "node:stream"; +import { VERSION } from "../version.ts"; +import { registerBrowseTools } from "./tools/browse.ts"; +import { registerLibraryTools } from "./tools/library.ts"; +import { registerPlaybackTools } from "./tools/playback.ts"; + +const INSTRUCTIONS = `Control the user's Spotify account: playback, queue, devices, search, lyrics, library, and playlists. + +- Wherever a target, uri, or playlist parameter appears, pass a Spotify URI (spotify:track:...) or an open.spotify.com URL. Find URIs with search, get_resource, or list_playlists. +- device parameters accept a Spotify Connect device ID or name from list_devices; omit them to use the active device. +- Relative changes use offset_ms (seek) and delta (set_volume); absolute values use position_ms and percent. +- Authentication happens outside this server. When a tool reports an authentication error, ask the user to run \`spotuify auth\` in a terminal. +- Playback control requires Spotify Premium and a reachable device. Rate-limit errors include the time to retry after. +- Structured tool results use the same snake_case shapes as the \`spotuify --json\` CLI output.`; + +/** + * Build the MCP server with every tool registered but no transport attached. + * + * Session creation is lazy: initialize and tools/list succeed without credentials, and the first + * tool call surfaces a missing login as a tool error rather than an interactive flow. The server + * is strictly a runtime client — it never starts a playback runtime of its own, so it coexists + * with a running TUI, a headless service, and other MCP server instances. + */ +export function createMcpServer(): McpServer { + const server = new McpServer( + { name: "spotuify", title: "Spotuify", version: VERSION }, + { instructions: INSTRUCTIONS }, + ); + registerPlaybackTools(server); + registerBrowseTools(server); + registerLibraryTools(server); + return server; +} + +/** + * Serve MCP over stdio until the client disconnects or a signal arrives. + * + * Stdout belongs to the protocol from the moment the transport connects; the ready line and any + * diagnostics go to stderr. Shutdown never calls `process.exit()`: closing the transport releases + * stdin and the process leaves through the CLI's normal exit path. + */ +export async function runMcpServer(io: { + stderr: Writable; +}): Promise<{ interrupted: boolean }> { + const server = createMcpServer(); + const transport = new StdioServerTransport(); + let interrupted = false; + const closed = new Promise((resolve) => { + // The protocol fires this for both peer shutdown (stdin closed) and server.close(). + server.server.onclose = resolve; + }); + const onSignal = () => { + interrupted = true; + void server.close(); + }; + // The MCP stdio shutdown sequence is the client closing our stdin; the SDK transport only + // watches for data, so end-of-input has to close the server here or the process would linger + // until the client escalates to signals. + const onStdinEnd = () => { + void server.close(); + }; + await server.connect(transport); + process.once("SIGINT", onSignal); + process.once("SIGTERM", onSignal); + process.stdin.once("end", onStdinEnd); + io.stderr.write(`spotuify ${VERSION} MCP server ready on stdio\n`); + try { + await closed; + } finally { + process.off("SIGINT", onSignal); + process.off("SIGTERM", onSignal); + process.stdin.off("end", onStdinEnd); + } + return { interrupted }; +} diff --git a/src/mcp/tools/browse.ts b/src/mcp/tools/browse.ts new file mode 100644 index 0000000..c208f0e --- /dev/null +++ b/src/mcp/tools/browse.ts @@ -0,0 +1,98 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { + lyricsFor, + resourceDetails, + searchCatalog, + SEARCH_LIMIT_MAX, +} from "../../cli/operations/catalog.ts"; +import { runTool } from "../result.ts"; + +const SEARCH_TYPES = [ + "track", + "artist", + "album", + "playlist", + "show", + "episode", + "audiobook", +] as const; + +export function registerBrowseTools(server: McpServer): void { + server.registerTool( + "search", + { + title: "Search Spotify", + description: + "Search the Spotify catalog for tracks, artists, albums, playlists, shows, episodes, or audiobooks.", + inputSchema: { + query: z.string().min(1).describe("Free-text search query."), + types: z + .array(z.enum(SEARCH_TYPES)) + .min(1) + .optional() + .describe( + "Resource types to search. Defaults to track, artist, album, and playlist.", + ), + limit: z + .number() + .int() + .min(1) + .max(SEARCH_LIMIT_MAX) + .optional() + .describe("Results per resource type, up to 50."), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + }, + (args) => + runTool(() => + searchCatalog(args.query, { + ...(args.types !== undefined ? { types: args.types } : {}), + ...(args.limit !== undefined ? { limit: args.limit } : {}), + }), + ), + ); + + server.registerTool( + "get_resource", + { + title: "Get resource", + description: + "Get details and playable contents for a Spotify track, episode, album, artist, playlist, show, or audiobook. Playlist contents are listed only for playlists the user owns.", + inputSchema: { + target: z + .string() + .describe("Spotify URI or open.spotify.com URL of the resource."), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + }, + (args) => + runTool(async () => { + // Every branch of the details switch returns an object, matching the CLI `show` data. + const result = await resourceDetails(args.target); + return { + data: result.data as Record, + message: result.message, + }; + }), + ); + + server.registerTool( + "get_lyrics", + { + title: "Get lyrics", + description: + "Get lyrics for a track, or for the currently playing track when no track is given.", + inputSchema: { + track: z + .string() + .optional() + .describe( + "Spotify track URI or URL. Omit to use the currently playing track.", + ), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true }, + }, + (args) => runTool(() => lyricsFor(args.track)), + ); +} diff --git a/src/mcp/tools/library.ts b/src/mcp/tools/library.ts new file mode 100644 index 0000000..e3b8fcb --- /dev/null +++ b/src/mcp/tools/library.ts @@ -0,0 +1,124 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { removeLibraryItems, saveLibraryItems } from "../../api/library.ts"; +import { playlistAdd, playlistList } from "../../cli/operations/playlists.ts"; +import { cliSession } from "../../cli/session.ts"; +import { libraryUri } from "../../cli/values.ts"; +import { runTool } from "../result.ts"; + +const uris = z + .array(z.string()) + .min(1) + .describe( + "Spotify URIs or open.spotify.com URLs of tracks, episodes, albums, shows, or audiobooks.", + ); + +const playlistShape = z.object({ + id: z.string(), + name: z.string(), + uri: z.string(), + owner_id: z.string(), + owner_name: z.string(), + mine: z.boolean().describe("Whether the signed-in user owns the playlist."), +}); + +export function registerLibraryTools(server: McpServer): void { + server.registerTool( + "save_to_library", + { + title: "Save to library", + description: + "Save tracks, episodes, albums, shows, or audiobooks to the user's Spotify library.", + inputSchema: { uris }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + (args) => + runTool(async () => { + const saved = args.uris.map((item) => + libraryUri(item, "cannot be saved to the library"), + ); + await saveLibraryItems((await cliSession()).client, saved); + return { + data: { uris: saved }, + message: `Saved ${saved.length} item${saved.length === 1 ? "" : "s"}.`, + }; + }), + ); + + server.registerTool( + "remove_from_library", + { + title: "Remove from library", + description: + "Remove tracks, episodes, albums, shows, or audiobooks from the user's Spotify library.", + inputSchema: { uris }, + annotations: { + readOnlyHint: false, + destructiveHint: true, + idempotentHint: true, + openWorldHint: false, + }, + }, + (args) => + runTool(async () => { + const removed = args.uris.map((item) => + libraryUri(item, "cannot be removed from the library"), + ); + await removeLibraryItems((await cliSession()).client, removed); + return { + data: { uris: removed }, + message: `Removed ${removed.length} item${removed.length === 1 ? "" : "s"}.`, + }; + }), + ); + + server.registerTool( + "list_playlists", + { + title: "List playlists", + description: "List the user's Spotify playlists.", + inputSchema: { + owned_only: z + .boolean() + .optional() + .describe("List only playlists the user owns."), + }, + outputSchema: { playlists: z.array(playlistShape) }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + }, + (args) => + runTool(async () => { + const result = await playlistList({ ownedOnly: args.owned_only }); + return { data: { playlists: result.data }, message: result.message }; + }), + ); + + server.registerTool( + "add_playlist_items", + { + title: "Add playlist items", + description: "Append tracks or episodes to a playlist the user owns.", + inputSchema: { + playlist: z + .string() + .describe("Spotify playlist URI, open.spotify.com URL, or bare playlist ID."), + uris: z + .array(z.string()) + .min(1) + .describe("Spotify track or episode URIs or open.spotify.com URLs."), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + }, + (args) => runTool(() => playlistAdd(args.playlist, args.uris)), + ); +} diff --git a/src/mcp/tools/playback.ts b/src/mcp/tools/playback.ts new file mode 100644 index 0000000..758390c --- /dev/null +++ b/src/mcp/tools/playback.ts @@ -0,0 +1,341 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { usageError } from "../../cli/errors.ts"; +import { deviceList, deviceTransfer } from "../../cli/operations/devices.ts"; +import { + pausePlayback, + playbackStatus, + seekPlayback, + setRepeat, + setShuffle, + setVolume, + skip, + startPlayback, +} from "../../cli/operations/playback.ts"; +import { queueAdd, queueList } from "../../cli/operations/queue.ts"; +import { runTool } from "../result.ts"; + +const device = z + .string() + .optional() + .describe( + "Target Spotify Connect device, by ID or name from list_devices. Omit to use the active device.", + ); + +const playbackItemShape = z + .record(z.string(), z.unknown()) + .nullable() + .describe("The current track or episode, or null when nothing is loaded."); + +const playbackDeviceShape = z + .object({ + id: z.string().nullable(), + name: z.string(), + type: z.string().nullable(), + volume_percent: z.number().nullable(), + is_restricted: z.boolean().nullable(), + }) + .nullable(); + +const PLAYBACK_STATUS_OUTPUT = { + active: z.boolean().describe("Whether any playback session exists."), + is_playing: z.boolean(), + item: playbackItemShape, + progress_ms: z.number().nullable(), + duration_ms: z.number().nullable(), + shuffle: z.boolean(), + repeat: z.enum(["off", "context", "track"]), + context_uri: z.string().nullable(), + device: playbackDeviceShape, +}; + +const queueItemShape = z.object({ + type: z.string(), + id: z.string().nullable(), + uri: z.string(), + name: z.string(), + artists: z.array(z.string()), + artist: z.string(), + album: z.string().nullable(), + show: z.string().nullable(), + duration_ms: z.number().nullable(), +}); + +const deviceShape = z.looseObject({ + id: z.string().nullable(), + name: z.string(), + type: z.string(), + is_active: z.boolean(), + is_restricted: z.boolean(), + volume_percent: z.number().nullable(), +}); + +export function registerPlaybackTools(server: McpServer): void { + server.registerTool( + "playback_status", + { + title: "Playback status", + description: + "Get the current Spotify playback state: track, device, progress, shuffle, and repeat.", + inputSchema: {}, + outputSchema: PLAYBACK_STATUS_OUTPUT, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + }, + () => runTool(() => playbackStatus()), + ); + + server.registerTool( + "play", + { + title: "Play", + description: + "Resume playback, or play a Spotify track, episode, album, artist, or playlist by URI or open.spotify.com URL.", + inputSchema: { + target: z + .string() + .optional() + .describe( + "Spotify URI or URL to play. Omit to resume the current playback.", + ), + device, + index: z + .number() + .int() + .min(1) + .optional() + .describe("One-based item position within an album or playlist target."), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + }, + (args) => runTool(() => startPlayback(args)), + ); + + server.registerTool( + "pause", + { + title: "Pause", + description: "Pause Spotify playback.", + inputSchema: { device }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + (args) => runTool(() => pausePlayback(args)), + ); + + server.registerTool( + "skip", + { + title: "Skip", + description: "Skip to the next item or return to the previous item.", + inputSchema: { + direction: z.enum(["next", "previous"]), + device, + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + }, + (args) => runTool(() => skip(args.direction, { device: args.device })), + ); + + server.registerTool( + "seek", + { + title: "Seek", + description: + "Seek within the current item: position_ms jumps to an absolute time, offset_ms moves by a signed amount. Provide exactly one.", + inputSchema: { + position_ms: z + .number() + .int() + .min(0) + .optional() + .describe("Absolute position in milliseconds."), + offset_ms: z + .number() + .int() + .optional() + .describe("Signed offset in milliseconds, e.g. -5000 to rewind five seconds."), + device, + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + }, + (args) => + runTool(() => + seekPlayback({ + positionMs: args.position_ms, + offsetMs: args.offset_ms, + device: args.device, + }), + ), + ); + + server.registerTool( + "set_volume", + { + title: "Set volume", + description: + "Set the playback volume: percent sets an absolute level 0-100, delta adjusts by a signed amount. Provide exactly one.", + inputSchema: { + percent: z + .number() + .int() + .min(0) + .max(100) + .optional() + .describe("Absolute volume level, 0-100."), + delta: z + .number() + .int() + .optional() + .describe("Signed volume change, e.g. -10 to lower by ten points."), + device, + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + }, + (args) => + runTool(() => + setVolume({ percent: args.percent, delta: args.delta, device: args.device }), + ), + ); + + server.registerTool( + "set_playback_mode", + { + title: "Set playback mode", + description: "Set shuffle and/or repeat. Provide at least one of shuffle or repeat.", + inputSchema: { + shuffle: z.boolean().optional(), + repeat: z.enum(["off", "context", "track"]).optional(), + device, + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + (args) => + runTool(async () => { + if (args.shuffle === undefined && args.repeat === undefined) { + throw usageError("Provide at least one of shuffle or repeat."); + } + const data: Record = {}; + const messages: string[] = []; + if (args.shuffle !== undefined) { + const result = await setShuffle(args.shuffle, { device: args.device }); + data["shuffle"] = result.data; + messages.push(result.message); + } + if (args.repeat !== undefined) { + const result = await setRepeat(args.repeat, { device: args.device }); + data["repeat"] = result.data; + messages.push(result.message); + } + return { data, message: messages.join(" ") }; + }), + ); + + server.registerTool( + "queue_list", + { + title: "List queue", + description: "Show the currently playing item and the upcoming queue.", + inputSchema: {}, + outputSchema: { + current: z + .record(z.string(), z.unknown()) + .nullable() + .describe("The item playing now, or null when nothing is playing."), + items: z.array(queueItemShape).describe("Upcoming items in order."), + }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + }, + () => runTool(() => queueList()), + ); + + server.registerTool( + "queue_add", + { + title: "Add to queue", + description: "Append tracks or episodes to the playback queue.", + inputSchema: { + uris: z + .array(z.string()) + .min(1) + .describe("Spotify track or episode URIs or open.spotify.com URLs."), + device, + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: false, + }, + }, + (args) => runTool(() => queueAdd(args.uris, { device: args.device })), + ); + + server.registerTool( + "list_devices", + { + title: "List devices", + description: "List the available Spotify Connect devices.", + inputSchema: {}, + outputSchema: { devices: z.array(deviceShape) }, + annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false }, + }, + () => + runTool(async () => { + const result = await deviceList(); + return { data: { devices: result.data }, message: result.message }; + }), + ); + + server.registerTool( + "transfer_playback", + { + title: "Transfer playback", + description: "Transfer playback to another Spotify Connect device.", + inputSchema: { + device: z + .string() + .describe("Target device, by ID or name from list_devices."), + play: z + .boolean() + .optional() + .describe("Start playing after the transfer. Defaults to true."), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, + }, + (args) => runTool(() => deviceTransfer(args.device, args.play ?? true)), + ); +} diff --git a/test/mcp-server.test.ts b/test/mcp-server.test.ts new file mode 100644 index 0000000..86bf06b --- /dev/null +++ b/test/mcp-server.test.ts @@ -0,0 +1,259 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { ReauthRequiredError } from "../src/auth/tokens.ts"; +import type { TokenStore } from "../src/auth/tokens.ts"; +import { + createCliSession, + primeCliSessionForTests, + resetCliSessionForTests, +} from "../src/cli/session.ts"; +import { createMcpServer } from "../src/mcp/server.ts"; +import { VERSION } from "../src/version.ts"; + +const EXPECTED_TOOLS = [ + "add_playlist_items", + "get_lyrics", + "get_resource", + "list_devices", + "list_playlists", + "pause", + "play", + "playback_status", + "queue_add", + "queue_list", + "remove_from_library", + "save_to_library", + "search", + "seek", + "set_playback_mode", + "set_volume", + "skip", + "transfer_playback", +]; + +const realFetch = globalThis.fetch; +const realRuntimeDir = process.env["SPOTUIFY_RUNTIME_DIR"]; + +let client: Client | undefined; +let directories: string[] = []; + +async function connectedClient(): Promise { + const server = createMcpServer(); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "spotuify-tests", version: "0.0.0" }); + await Promise.all([ + server.connect(serverTransport), + client.connect(clientTransport), + ]); + return client; +} + +/** Point the default control paths at an empty directory: no runtime is reachable. */ +async function withoutRuntime(): Promise { + const directory = await mkdtemp(join(tmpdir(), "spotuify-mcp-runtime-")); + directories.push(directory); + process.env["SPOTUIFY_RUNTIME_DIR"] = directory; +} + +async function primeSession( + tokens: TokenStore, + respond: (path: string) => Response, +): Promise { + const paths: string[] = []; + globalThis.fetch = (async (input: string | URL | Request) => { + const path = new URL(String(input)).pathname.replace("/v1", ""); + paths.push(path); + return respond(path); + }) as unknown as typeof fetch; + const directory = await mkdtemp(join(tmpdir(), "spotuify-mcp-session-")); + directories.push(directory); + primeCliSessionForTests( + await createCliSession(tokens, { + profilePath: join(directory, "profile.json"), + }), + ); + return paths; +} + +function workingTokens(): TokenStore { + return { + accessToken: async () => "token", + refresh: async () => { + throw new Error("unexpected refresh"); + }, + authorizationId: async () => "authorization", + } as unknown as TokenStore; +} + +function expiredTokens(): TokenStore { + return { + accessToken: async () => { + throw new ReauthRequiredError("The Spotify login has expired."); + }, + refresh: async () => { + throw new Error("unexpected refresh"); + }, + authorizationId: async () => "authorization", + } as unknown as TokenStore; +} + +beforeEach(() => { + resetCliSessionForTests(); +}); + +afterEach(async () => { + globalThis.fetch = realFetch; + if (realRuntimeDir === undefined) delete process.env["SPOTUIFY_RUNTIME_DIR"]; + else process.env["SPOTUIFY_RUNTIME_DIR"] = realRuntimeDir; + resetCliSessionForTests(); + await client?.close(); + client = undefined; + await Promise.all( + directories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +describe("mcp server surface", () => { + test("advertises the server identity and instructions", async () => { + const connected = await connectedClient(); + expect(connected.getServerVersion()).toMatchObject({ + name: "spotuify", + version: VERSION, + }); + expect(connected.getInstructions()).toContain("spotuify auth"); + }); + + test("lists the complete curated tool set", async () => { + const connected = await connectedClient(); + const { tools } = await connected.listTools(); + expect(tools.map((tool) => tool.name).sort()).toEqual(EXPECTED_TOOLS); + }); + + test("annotates reads, mutations, and the one destructive tool", async () => { + const connected = await connectedClient(); + const { tools } = await connected.listTools(); + const byName = new Map(tools.map((tool) => [tool.name, tool])); + expect(byName.get("playback_status")?.annotations).toMatchObject({ + readOnlyHint: true, + idempotentHint: true, + openWorldHint: false, + }); + expect(byName.get("search")?.annotations).toMatchObject({ + readOnlyHint: true, + openWorldHint: true, + }); + expect(byName.get("play")?.annotations).toMatchObject({ + readOnlyHint: false, + destructiveHint: false, + }); + expect(byName.get("remove_from_library")?.annotations).toMatchObject({ + destructiveHint: true, + idempotentHint: true, + }); + }); + + test("declares output schemas only for the stable read shapes", async () => { + const connected = await connectedClient(); + const { tools } = await connected.listTools(); + const withOutput = tools + .filter((tool) => tool.outputSchema !== undefined) + .map((tool) => tool.name) + .sort(); + expect(withOutput).toEqual([ + "list_devices", + "list_playlists", + "playback_status", + "queue_list", + ]); + }); +}); + +describe("mcp tool calls", () => { + test("playback_status returns the CLI's snake_case machine shape", async () => { + await withoutRuntime(); + const paths = await primeSession( + workingTokens(), + () => new Response(null, { status: 204 }), + ); + const connected = await connectedClient(); + const result = await connected.callTool({ + name: "playback_status", + arguments: {}, + }); + expect(result.isError).toBeUndefined(); + expect(result.structuredContent).toEqual({ + active: false, + is_playing: false, + item: null, + progress_ms: null, + duration_ms: null, + shuffle: false, + repeat: "off", + context_uri: null, + device: null, + }); + expect(result.content).toEqual([ + { type: "text", text: "Nothing is playing." }, + ]); + expect(paths).toEqual(["/me/player"]); + }); + + test("a mutation reports the routed result", async () => { + await withoutRuntime(); + const paths = await primeSession( + workingTokens(), + () => new Response(null, { status: 204 }), + ); + const connected = await connectedClient(); + const result = await connected.callTool({ name: "pause", arguments: {} }); + expect(result.isError).toBeUndefined(); + expect(result.structuredContent).toEqual({ device_id: null }); + expect(paths).toEqual(["/me/player/pause"]); + }); + + test("missing credentials become a tool error with the auth hint", async () => { + await withoutRuntime(); + await primeSession( + expiredTokens(), + () => new Response(null, { status: 204 }), + ); + const connected = await connectedClient(); + const result = await connected.callTool({ name: "pause", arguments: {} }); + expect(result.isError).toBe(true); + const text = (result.content as { type: string; text: string }[])[0]?.text; + expect(text).toContain("Run `spotuify auth`"); + // Error results are text-only: structured content would fail client-side output + // schema validation on tools that declare one. + expect(result.structuredContent).toBeUndefined(); + }); + + test("domain validation failures are tool errors, not protocol errors", async () => { + await withoutRuntime(); + const connected = await connectedClient(); + const result = await connected.callTool({ + name: "queue_add", + arguments: { uris: ["spotify:album:abc123"] }, + }); + expect(result.isError).toBe(true); + const text = (result.content as { type: string; text: string }[])[0]?.text; + expect(text).toContain("Only tracks and episodes"); + }); + + test("schema-invalid arguments are rejected before the handler runs", async () => { + const connected = await connectedClient(); + const result = await connected.callTool({ + name: "seek", + arguments: { position_ms: -5 }, + }); + expect(result.isError).toBe(true); + const text = (result.content as { type: string; text: string }[])[0]?.text; + expect(text).toContain("Input validation error"); + }); +}); diff --git a/test/mcp-stdio.test.ts b/test/mcp-stdio.test.ts new file mode 100644 index 0000000..9d245b5 --- /dev/null +++ b/test/mcp-stdio.test.ts @@ -0,0 +1,122 @@ +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, expect, test } from "bun:test"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { VERSION } from "../src/version.ts"; + +let directory: string | undefined; + +afterEach(async () => { + if (directory !== undefined) + await rm(directory, { recursive: true, force: true }); + directory = undefined; +}); + +test("spotuify mcp serves the protocol over stdio with a clean stderr", async () => { + directory = await mkdtemp(join(tmpdir(), "spotuify-mcp-stdio-")); + const cli = fileURLToPath(new URL("../src/cli.ts", import.meta.url)); + const transport = new StdioClientTransport({ + command: process.execPath, + args: [cli, "mcp"], + env: { + ...process.env, + XDG_CONFIG_HOME: join(directory, "config"), + XDG_CACHE_HOME: join(directory, "cache"), + SPOTUIFY_ENGINE_PATH: join(directory, "missing-engine"), + SPOTUIFY_RUNTIME_DIR: join(directory, "runtime"), + SPOTUIFY_NO_UPDATE_CHECK: "1", + }, + stderr: "pipe", + }); + let stderrText = ""; + const client = new Client({ name: "spotuify-stdio-test", version: "0.0.0" }); + try { + await client.connect(transport); + transport.stderr?.on("data", (chunk: Buffer) => { + stderrText += chunk.toString(); + }); + + // A successful initialize handshake is itself the stdout-purity check: any stray write + // would corrupt the JSON-RPC stream and fail the connection. + expect(client.getServerVersion()).toMatchObject({ + name: "spotuify", + version: VERSION, + }); + + const { tools } = await client.listTools(); + expect(tools.length).toBe(18); + expect(tools.map((tool) => tool.name)).toContain("playback_status"); + + // Unauthenticated tool calls answer with a tool error instead of hanging or crashing. + const result = await client.callTool({ name: "queue_list", arguments: {} }); + expect(result.isError).toBe(true); + const text = (result.content as { type: string; text: string }[])[0]?.text; + expect(text).toContain("spotuify auth"); + } finally { + await client.close(); + } + + // Diagnostics stay on stderr and never carry protocol frames. + expect(stderrText).not.toContain('"jsonrpc"'); + + // The stdio server must leave no application state behind. + expect( + await Bun.file(join(directory, "config", "spotuify", "token.json")).exists(), + ).toBe(false); + expect( + await Bun.file(join(directory, "cache", "spotuify", "update.json")).exists(), + ).toBe(false); +}, 15_000); + +test("closing stdin shuts the server down without signals", async () => { + directory = await mkdtemp(join(tmpdir(), "spotuify-mcp-eof-")); + const cli = fileURLToPath(new URL("../src/cli.ts", import.meta.url)); + const child = Bun.spawn([process.execPath, cli, "mcp"], { + cwd: directory, + env: { + ...process.env, + XDG_CONFIG_HOME: join(directory, "config"), + XDG_CACHE_HOME: join(directory, "cache"), + SPOTUIFY_ENGINE_PATH: join(directory, "missing-engine"), + SPOTUIFY_RUNTIME_DIR: join(directory, "runtime"), + SPOTUIFY_NO_UPDATE_CHECK: "1", + }, + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + }); + const killTimer = setTimeout(() => child.kill(), 10_000); + child.stdin.write( + `${JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { name: "eof-test", version: "0.0.0" }, + }, + })}\n`, + ); + await child.stdin.flush(); + + // Wait for the initialize response before closing stdin, like a real client. + const reader = child.stdout.getReader(); + const decoder = new TextDecoder(); + let stdout = ""; + while (!stdout.includes("\n")) { + const { value, done } = await reader.read(); + if (done) break; + stdout += decoder.decode(value); + } + expect(stdout).toContain('"serverInfo"'); + + // The MCP stdio shutdown sequence: the client closes stdin and the server exits on its own. + await child.stdin.end(); + const exitCode = await child.exited; + clearTimeout(killTimer); + expect(exitCode).toBe(0); +}, 15_000); diff --git a/tools/licenses/javascript.ts b/tools/licenses/javascript.ts index ea79d50..6f242fe 100644 --- a/tools/licenses/javascript.ts +++ b/tools/licenses/javascript.ts @@ -32,7 +32,13 @@ interface Component { const components = new Map(); const visited = new Set(); -const allowedLicenses = new Set(["Apache-2.0", "BSD-3-Clause", "MIT"]); +const allowedLicenses = new Set([ + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "MIT", +]); const platformPackageFamilies = new Map([ ["@opentui/core-", "@opentui/core platform binary packages"], ["@typescript/typescript-", "@typescript/typescript platform binary packages"], @@ -66,23 +72,35 @@ async function licenseText(packageName: string, directory: string): Promise { const require = createRequire(resolve(fromDirectory, "package.json")); + let entry: string; try { - return dirname(require.resolve(`${packageName}/package.json`)); + entry = require.resolve(`${packageName}/package.json`); } catch { try { - let directory = dirname(require.resolve(packageName)); - while (directory !== dirname(directory)) { - const metadata = Bun.file(resolve(directory, "package.json")); - if (metadata.size > 0) return directory; - directory = dirname(directory); - } - return null; + entry = require.resolve(packageName); } catch { return null; } } + // An exports map can send `/package.json` to a nested `{"type": "module"}` stub, and a + // bare resolve lands on the entry module; either way the manifest that actually names the + // package sits in a parent directory. + let directory = dirname(entry); + while (true) { + const manifest = Bun.file(resolve(directory, "package.json")); + if (manifest.size > 0) { + const metadata = (await manifest.json()) as { name?: unknown }; + if (metadata.name === packageName) return directory; + } + const parent = dirname(directory); + if (parent === directory) return null; + directory = parent; + } } async function visit( @@ -90,7 +108,7 @@ async function visit( fromDirectory: string, optional = false, ): Promise { - const directory = packageDirectory(packageName, fromDirectory); + const directory = await packageDirectory(packageName, fromDirectory); if (directory === null) { if (optional || platformPackageFamily(packageName) !== null) return; throw new Error(`production dependency ${packageName} is not installed`); From c6503e1459f5923752d00e74564aec29a61a6931 Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Mon, 3 Aug 2026 23:10:56 -0700 Subject: [PATCH 3/3] document the mcp server add docs/mcp.md with the tool table, auth prerequisite, and client registration for claude code, codex, and mcpservers-style clients; add the mcp command to the cli guide and a short readme section. --- README.md | 25 ++++++++++++ docs/cli.md | 12 ++++++ docs/mcp.md | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 docs/mcp.md diff --git a/README.md b/README.md index d392c06..a20261a 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,31 @@ spotuify status --json Run `spotuify --help` for all commands and options. See the [CLI guide](docs/cli.md) for scripting, output formats, exit codes, and the playback service. +## MCP server + +AI agents can search Spotify and control playback through the built-in +MCP server. Register `spotuify mcp` as a stdio server in any MCP client: + +```sh +claude mcp add spotuify -- spotuify mcp # Claude Code +codex mcp add spotuify -- spotuify mcp # Codex CLI +``` + +Clients that use the common `mcpServers` JSON convention (e.g., Cursor): + +```json +{ + "mcpServers": { + "spotuify": { + "command": "spotuify", + "args": ["mcp"] + } + } +} +``` + +See the [MCP guide](docs/mcp.md) for the tool list and other client configurations. + ## Development Running from source requires [Bun](https://bun.sh) and [Rust](https://rustup.rs/). diff --git a/docs/cli.md b/docs/cli.md index e27e238..e354ced 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -197,6 +197,18 @@ spotuify service stop `service run` deliberately does not daemonize itself. Use the operating system's service manager when restart policy, boot startup, and log retention are required. +## MCP server + +AI agents can drive the same command surface over the Model Context Protocol: + +```sh +spotuify mcp +``` + +The server speaks MCP on stdio, follows the same runtime-first routing as one-shot commands, and +never prompts for authentication. Output flags do not apply — the protocol owns stdout. See +[mcp.md](mcp.md) for the tool list and client registration. + ## Authentication and diagnostics Only `spotuify auth` may prompt or open a browser. Every other command is non-interactive and exits diff --git a/docs/mcp.md b/docs/mcp.md new file mode 100644 index 0000000..96b5acb --- /dev/null +++ b/docs/mcp.md @@ -0,0 +1,110 @@ +# Spotuify MCP server + +`spotuify mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio, +so AI agents can search Spotify and control playback through the same routing, sessions, and +output shapes as the CLI. + +## Setup + +The MCP server reuses the CLI's stored Spotify login and never prompts or opens a browser. +Authenticate once in a terminal: + +```sh +spotuify auth +``` + +When credentials are missing or expired, tool calls answer with an error asking the user to run +`spotuify auth`; `initialize` and tool listing work without any setup. + +## Client registration + +The server works with any MCP client: register `spotuify mcp` as a stdio server. + +Claude Code: + +```sh +claude mcp add spotuify -- spotuify mcp +``` + +Codex CLI: + +```sh +codex mcp add spotuify -- spotuify mcp +``` + +or in `~/.codex/config.toml`: + +```toml +[mcp_servers.spotuify] +command = "spotuify" +args = ["mcp"] +``` + +Clients that use the common `mcpServers` JSON convention (e.g., Cursor): + +```json +{ + "mcpServers": { + "spotuify": { + "command": "spotuify", + "args": ["mcp"] + } + } +} +``` + +## Runtime behavior + +The MCP server follows the same routing as one-shot CLI commands: while the TUI or +`spotuify service run` is open, playback commands go through that runtime's serialized command +stream; otherwise they use Spotify's Web API directly. The server never starts a renderer, +playback engine, or browser, and any number of MCP server instances can run alongside the TUI. + +Shutdown follows the MCP stdio convention: the server exits when the client closes its stdin or +sends SIGINT/SIGTERM. Diagnostics go to stderr; stdout carries only protocol messages. + +## Tools + +| Tool | Kind | Description | +| --- | --- | --- | +| `playback_status` | read | Current playback state: track, device, progress, shuffle, repeat | +| `play` | write | Resume playback, or play a track, episode, album, artist, or playlist | +| `pause` | write | Pause playback | +| `skip` | write | Skip to the next item or return to the previous one | +| `seek` | write | Jump to `position_ms` or move by a signed `offset_ms` | +| `set_volume` | write | Set an absolute `percent` or adjust by a signed `delta` | +| `set_playback_mode` | write | Set shuffle and/or repeat | +| `queue_list` | read | The current item and the upcoming queue | +| `queue_add` | write | Append tracks or episodes to the queue | +| `list_devices` | read | Available Spotify Connect devices | +| `transfer_playback` | write | Move playback to another device | +| `search` | read | Search the catalog by type, up to 50 results per type | +| `get_resource` | read | Details and playable contents for any Spotify URI or URL | +| `get_lyrics` | read | Lyrics for a track or the currently playing track | +| `save_to_library` | write | Save tracks, episodes, albums, shows, or audiobooks | +| `remove_from_library` | write, destructive | Remove items from the library | +| `list_playlists` | read | The user's playlists | +| `add_playlist_items` | write | Append tracks or episodes to an owned playlist | + +Every tool carries MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, +`openWorldHint`) so clients can gate confirmation prompts appropriately. + +## Input and output conventions + +- `target`, `uris`, and `playlist` parameters accept Spotify URIs (`spotify:track:...`) or + `open.spotify.com` URLs. +- `device` parameters accept a Spotify Connect device ID or name from `list_devices`; omitted, + commands use the active device. +- Structured tool results (`structuredContent`) use the same snake_case shapes as the CLI's + `--json` envelope `data` field, documented in [cli.md](cli.md). Two adaptations apply: results + whose CLI shape is an array are wrapped under a named key (`devices`, `playlists`), and mutation + results omit the CLI's `ok` flag because MCP conveys success through `isError`. +- Domain failures are tool errors with an actionable message and hint — rate limits include the + time to retry after — never protocol errors. + +## Constraints + +- Playback control requires Spotify Premium and a reachable device, exactly like the CLI. +- Playlist contents are listed only for playlists the user owns; Spotify permanently refuses the + items read for foreign playlists. +- The lyrics tool uses LRCLIB and Genius, not Spotify.