From 61e9cd94c6e6b71f4cd72f10b00741c748e7afca Mon Sep 17 00:00:00 2001 From: anthony lio Date: Fri, 5 Jun 2026 14:15:18 +0300 Subject: [PATCH] feat: official AI agent skills + MCP server for anime.js v4 --- .claude-plugin/marketplace.json | 18 + .claude-plugin/plugin.json | 21 + .cursor/rules/animejs.mdc | 33 + .github/copilot-instructions.md | 35 + .../instructions/anime-core.instructions.md | 14 + .../anime-frameworks.instructions.md | 12 + .../instructions/anime-react.instructions.md | 14 + .github/workflows/skills.yml | 53 + AGENTS.md | 79 + CLAUDE.md | 21 + GEMINI.md | 20 + README.md | 25 + llms.txt | 56 + mcp/.gitignore | 3 + mcp/README.md | 86 + mcp/package-lock.json | 2641 +++++++++++++++++ mcp/package.json | 42 + mcp/scripts/smoke.mjs | 37 + mcp/src/server.mjs | 127 + mcp/src/skills.mjs | 77 + mcp/src/stdio.mjs | 8 + mcp/tests/fixtures/skills.mjs | 25 + mcp/tests/helpers/fakeMcpServer.mjs | 30 + mcp/tests/unit/server.test.mjs | 108 + mcp/tests/unit/skills.test.mjs | 73 + mcp/tests/unit/v3-tokens.test.mjs | 102 + mcp/vitest.config.mjs | 15 + package.json | 3 +- scripts/v3-tokens.mjs | 79 + scripts/validate-skills.mjs | 198 ++ skills/anime-core/SKILL.md | 151 + skills/anime-engine/SKILL.md | 92 + skills/anime-frameworks/SKILL.md | 125 + skills/anime-migration-v3-v4/SKILL.md | 123 + skills/anime-react/SKILL.md | 92 + skills/anime-scroll-draggable/SKILL.md | 138 + skills/anime-svg/SKILL.md | 112 + skills/anime-text/SKILL.md | 100 + skills/anime-timeline/SKILL.md | 111 + skills/anime-utils-easings/SKILL.md | 125 + skills/anime-waapi/SKILL.md | 84 + skills/llms.txt | 81 + 42 files changed, 5388 insertions(+), 1 deletion(-) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100644 .cursor/rules/animejs.mdc create mode 100644 .github/copilot-instructions.md create mode 100644 .github/instructions/anime-core.instructions.md create mode 100644 .github/instructions/anime-frameworks.instructions.md create mode 100644 .github/instructions/anime-react.instructions.md create mode 100644 .github/workflows/skills.yml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 GEMINI.md create mode 100644 llms.txt create mode 100644 mcp/.gitignore create mode 100644 mcp/README.md create mode 100644 mcp/package-lock.json create mode 100644 mcp/package.json create mode 100644 mcp/scripts/smoke.mjs create mode 100644 mcp/src/server.mjs create mode 100644 mcp/src/skills.mjs create mode 100644 mcp/src/stdio.mjs create mode 100644 mcp/tests/fixtures/skills.mjs create mode 100644 mcp/tests/helpers/fakeMcpServer.mjs create mode 100644 mcp/tests/unit/server.test.mjs create mode 100644 mcp/tests/unit/skills.test.mjs create mode 100644 mcp/tests/unit/v3-tokens.test.mjs create mode 100644 mcp/vitest.config.mjs create mode 100644 scripts/v3-tokens.mjs create mode 100644 scripts/validate-skills.mjs create mode 100644 skills/anime-core/SKILL.md create mode 100644 skills/anime-engine/SKILL.md create mode 100644 skills/anime-frameworks/SKILL.md create mode 100644 skills/anime-migration-v3-v4/SKILL.md create mode 100644 skills/anime-react/SKILL.md create mode 100644 skills/anime-scroll-draggable/SKILL.md create mode 100644 skills/anime-svg/SKILL.md create mode 100644 skills/anime-text/SKILL.md create mode 100644 skills/anime-timeline/SKILL.md create mode 100644 skills/anime-utils-easings/SKILL.md create mode 100644 skills/anime-waapi/SKILL.md create mode 100644 skills/llms.txt diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 000000000..260a68156 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,18 @@ +{ + "name": "animejs", + "owner": { + "name": "Julian Garnier", + "url": "https://github.com/juliangarnier" + }, + "metadata": { + "description": "Official anime.js agent skills marketplace.", + "version": "1.0.0" + }, + "plugins": [ + { + "name": "animejs-skills", + "source": "./", + "description": "anime.js v4 agent skills for Claude Code, Cursor, Copilot, and the cross-agent Agent Skills standard." + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 000000000..5dfc98ec3 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,21 @@ +{ + "name": "animejs-skills", + "version": "4.0.0", + "description": "Official anime.js v4 agent skills with the correct named-import API and guidance that keeps agents off the v3 syntax, covering animation, timelines, SVG, text, scroll, draggable, easings, React, and frameworks.", + "author": { + "name": "Julian Garnier", + "url": "https://animejs.com" + }, + "homepage": "https://animejs.com", + "repository": "https://github.com/juliangarnier/anime", + "license": "MIT", + "keywords": [ + "anime.js", + "animejs", + "animation", + "svg", + "timeline", + "skills" + ], + "skills": "./skills" +} diff --git a/.cursor/rules/animejs.mdc b/.cursor/rules/animejs.mdc new file mode 100644 index 000000000..ddd290096 --- /dev/null +++ b/.cursor/rules/animejs.mdc @@ -0,0 +1,33 @@ +--- +description: anime.js v4 uses named imports and animate(targets, params), never the v3 anime({ targets }) API +globs: ["**/*.{js,ts,jsx,tsx,vue,svelte,mjs,cjs}"] +alwaysApply: false +--- + +# anime.js v4 + +This project uses anime.js **v4**. Generate the v4 API, never v3. + +v4 is an ES module with **named exports** and `animate(targets, params)`. There +is no default export and no `anime()` function. + +```js +// CORRECT (v4) +import { animate } from 'animejs'; +animate('.square', { x: 320, duration: 1000, ease: 'inOutQuint' }); + +// WRONG (deprecated v3) +import anime from 'animejs'; +anime({ targets: '.square', translateX: 320, easing: 'easeInOutQuint' }); +``` + +Rules: +- Named imports from `animejs`; `animate(targets, params)`. +- Transform shorthands `x`/`y`/`scale`/`rotate`, not `translateX`/`translateY`. +- `ease` string (`'inOutQuad'`, `'out(3)'`, `'outElastic(.3,1.4)'`); durations in ms. +- `createTimeline()` not `anime.timeline()`; `stagger()` not `anime.stagger()`; + helpers on `utils.*`. +- `alternate: true` / `reversed: true` (not `direction:`); callbacks on-prefixed. +- React/Vue/Svelte/Angular: wrap in `createScope({ root })`, `scope.revert()` on unmount. + +Full guidance: `skills/` (see `skills/anime-migration-v3-v4/SKILL.md` for fixing v3). diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..c059b51ca --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,35 @@ +# anime.js v4 Copilot instructions + +This project uses anime.js **v4**. Always generate the v4 API, never v3. + +v4 is an ES module with **named exports** and the signature +`animate(targets, params)`. There is **no default export** and **no `anime()` +function**. + +```js +// CORRECT (v4) +import { animate, stagger } from 'animejs'; +animate('.square', { x: 320, duration: 1000, ease: 'inOutQuint' }); + +// WRONG (deprecated v3, never generate) +import anime from 'animejs'; +anime({ targets: '.square', translateX: 320, easing: 'easeInOutQuint' }); +``` + +Rules: + +- **Named imports** from `animejs` (or subpaths like `animejs/timeline`). +- **`animate(targets, params)`**: targets first, params second. +- Transform shorthands `x`, `y`, `scale`, `rotate`, not `translateX`/`translateY`. +- `ease` is a string: `'inOutQuad'`, `'out(3)'`, `'outElastic(.3, 1.4)'`, + `'steps(5)'`, `'linear'`. +- Durations and delays in **milliseconds**. +- `createTimeline()` not `anime.timeline()`; `stagger()` not `anime.stagger()`; + helpers on `utils.*` (`utils.set`, `utils.random`, `utils.get`, `utils.remove`). +- `alternate: true` / `reversed: true` (not `direction:`); callbacks are + on-prefixed (`onUpdate`, `onBegin`, `onComplete`, `onLoop`, `onRender`). +- React, Vue, Svelte, or Angular: wrap in `createScope({ root })` and call + `scope.revert()` on unmount. + +Full guidance: [`skills/`](../skills/) (see `skills/anime-migration-v3-v4/SKILL.md` +to fix v3 syntax) and [`AGENTS.md`](../AGENTS.md). diff --git a/.github/instructions/anime-core.instructions.md b/.github/instructions/anime-core.instructions.md new file mode 100644 index 000000000..d9242098c --- /dev/null +++ b/.github/instructions/anime-core.instructions.md @@ -0,0 +1,14 @@ +--- +applyTo: "**/*.{js,ts,mjs,cjs}" +--- + +When writing anime.js in JavaScript/TypeScript files, follow +[`skills/anime-core/SKILL.md`](../../skills/anime-core/SKILL.md): + +- Named imports from `animejs`; `animate(targets, params)`. +- Transform shorthands `x`/`y`/`scale`/`rotate` (not `translateX`/`translateY`). +- `ease` string (`'inOutQuint'`, `'out(3)'`); durations in milliseconds. +- `createTimeline()`, `stagger()`, `utils.*`, never `anime.timeline/stagger/random`. + +If you see v3 syntax (`import anime from 'animejs'`, `anime({ targets })`), +convert it per [`skills/anime-migration-v3-v4/SKILL.md`](../../skills/anime-migration-v3-v4/SKILL.md). diff --git a/.github/instructions/anime-frameworks.instructions.md b/.github/instructions/anime-frameworks.instructions.md new file mode 100644 index 000000000..76bcc6456 --- /dev/null +++ b/.github/instructions/anime-frameworks.instructions.md @@ -0,0 +1,12 @@ +--- +applyTo: "**/*.{vue,svelte}" +--- + +When writing anime.js in Vue or Svelte files, follow +[`skills/anime-frameworks/SKILL.md`](../../skills/anime-frameworks/SKILL.md): + +- Build animations on mount (Vue `onMounted`, Svelte `onMount`) inside + `createScope({ root })` bound to a template ref. +- Call `scope.revert()` on destroy (`onUnmounted` / `onDestroy`). +- Use the v4 API: named imports, `animate(targets, params)`, `x`/`y` shorthands, + `ease` strings, `stagger()`, `utils.*`. Never the v3 `anime({ targets })`. diff --git a/.github/instructions/anime-react.instructions.md b/.github/instructions/anime-react.instructions.md new file mode 100644 index 000000000..b29614516 --- /dev/null +++ b/.github/instructions/anime-react.instructions.md @@ -0,0 +1,14 @@ +--- +applyTo: "**/*.{jsx,tsx}" +--- + +When writing anime.js in React files, follow +[`skills/anime-react/SKILL.md`](../../skills/anime-react/SKILL.md): + +- Create animations inside `useEffect` (or event handlers), never during render. +- Wrap them in `createScope({ root })` where `root` is a `useRef` to the + component container; scoped selectors resolve within that subtree. +- **Always** `return () => scope.current.revert()` from the effect (StrictMode- + and unmount-safe). +- Use the v4 API: named imports, `animate(targets, params)`, `x`/`y` shorthands, + `ease` strings, `stagger()`, `utils.*`. diff --git a/.github/workflows/skills.yml b/.github/workflows/skills.yml new file mode 100644 index 000000000..b75cac183 --- /dev/null +++ b/.github/workflows/skills.yml @@ -0,0 +1,53 @@ +name: Agent skills & MCP + +# Validates the agent skills against the built library (every documented symbol +# must be a real v4 export, no v3 syntax leaks) and runs the MCP server tests. +# Scoped to the skills/MCP files so it does not interfere with library CI. + +on: + push: + branches: [dev, master] + paths: + - "skills/**" + - "scripts/**" + - "mcp/**" + - "llms.txt" + - ".github/workflows/skills.yml" + pull_request: + paths: + - "skills/**" + - "scripts/**" + - "mcp/**" + - "llms.txt" + - ".github/workflows/skills.yml" + +permissions: + contents: read + +jobs: + validate-skills: + name: Validate skills against the built library + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: npm + - run: npm ci + - run: npm run build + - run: npm run validate:skills + + mcp-tests: + name: MCP server tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: mcp + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.x + - run: npm install + - run: npm test diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..c776e89b5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,79 @@ +# AGENTS.md: anime.js v4 + +Guidance for AI coding agents (Claude Code, Cursor, Copilot, Gemini, and others) +working with anime.js. The detailed skills for each area live in [`skills/`](skills/). +This file is the front door and the rules you must follow. + +## The one rule that matters + +**This is anime.js v4. Generate the v4 API, never v3.** + +v4 is an ES module with **named exports** and the signature +`animate(targets, params)`. There is **no default export** and **no `anime()` +function**. + +```js +// CORRECT (v4) +import { animate, stagger } from 'animejs'; +animate('.square', { x: 320, duration: 1000, ease: 'inOutQuint' }); + +// WRONG (deprecated v3, never generate this) +import anime from 'animejs'; +anime({ targets: '.square', translateX: 320, easing: 'easeInOutQuint' }); +``` + +## Core conventions + +- **Named imports** from `animejs` (or subpaths like `animejs/timeline`). +- **`animate(targets, params)`**: targets first (selector, element, array, or + object), params second. +- Transform **shorthands**: `x`, `y`, `scale`, `rotate`, `skew`. Not + `translateX` or `translateY`. +- The easing key is **`ease`**, and its value is a string with no prefix: + `'inOutQuad'`, `'out(3)'`, `'outElastic(.3, 1.4)'`, `'steps(5)'`, `'linear'`. +- **Milliseconds** for `duration` and `delay`. +- Use `createTimeline()`, not `anime.timeline()`. Use `stagger()` as a named + import, not `anime.stagger()`. Helpers live on `utils.*` (`utils.set`, + `utils.random`, `utils.get`, `utils.remove`). +- Use `alternate: true` and `reversed: true`, not `direction:`. Callbacks are + on-prefixed (`onUpdate`, `onBegin`, `onComplete`, `onLoop`, `onRender`). +- In React, Vue, Svelte, or Angular: wrap animations in + `createScope({ root })` and call `scope.revert()` on unmount. + +## Skills (read the one matching the task) + +| Skill | When | +| --- | --- | +| [anime-core](skills/anime-core/SKILL.md) | animate(), timers, transforms, keyframes, callbacks, playback | +| [anime-timeline](skills/anime-timeline/SKILL.md) | sequencing with createTimeline + position parameter | +| [anime-svg](skills/anime-svg/SKILL.md) | line drawing, motion path, morphing | +| [anime-text](skills/anime-text/SKILL.md) | splitText into lines/words/chars | +| [anime-scroll-draggable](skills/anime-scroll-draggable/SKILL.md) | onScroll, createDraggable, createAnimatable, createScope | +| [anime-utils-easings](skills/anime-utils-easings/SKILL.md) | utils.* helpers, stagger, easing | +| [anime-waapi](skills/anime-waapi/SKILL.md) | hardware-accelerated Web Animations API | +| [anime-engine](skills/anime-engine/SKILL.md) | global defaults, time unit, speed, custom loop | +| [anime-react](skills/anime-react/SKILL.md) | React (useRef + useEffect + scope.revert) | +| [anime-frameworks](skills/anime-frameworks/SKILL.md) | Vue / Svelte / Angular | +| [anime-migration-v3-v4](skills/anime-migration-v3-v4/SKILL.md) | converting or fixing v3 syntax | + +## Skills or MCP? + +Use the **skills**. They are the main thing and cover almost every case. Install +them with `npx skills add https://github.com/juliangarnier/anime` and your agent +reads them as plain files. No server to run. + +There is also an optional [MCP server](mcp/) that serves the same skills as +resources and adds a `check_anime_v4_syntax` tool. It is only worth it if your +client is MCP-native and you want those exposed as callable tools. It runs no +code and fetches nothing, so it is a convenience, not a requirement. When in +doubt, use the skills. + +## Editing these skills + +Each skill is `skills//SKILL.md` with YAML frontmatter (`name` must equal +the folder name, `description` must be 1024 chars or fewer and written in the +third person, `license: MIT`). Keep bodies focused and under about 500 lines, +with code examples in fenced ` ```js ` blocks. Every code example must be valid +v4. Running `node scripts/validate-skills.mjs` checks that every documented +symbol is a real export of the built library and that no v3 token leaks outside +the migration skill. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..a5bd402ca --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,21 @@ +# CLAUDE.md: anime.js v4 + +This is **anime.js v4**. Generate the v4 API, never v3. + +v4 uses **named ES imports** and `animate(targets, params)`. There is no default +export and no `anime()` function. + +```js +// CORRECT (v4) +import { animate } from 'animejs'; +animate('.square', { x: 320, duration: 1000, ease: 'inOutQuint' }); + +// WRONG (deprecated v3) +import anime from 'animejs'; +anime({ targets: '.square', translateX: 320, easing: 'easeInOutQuint' }); +``` + +Full conventions and skills for each area are in [AGENTS.md](AGENTS.md) and +[`skills/`](skills/). When writing or fixing anime.js code, load the matching +`skills//SKILL.md` (for example `anime-core`, `anime-timeline`, `anime-svg`, +`anime-react`, `anime-migration-v3-v4`). diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 000000000..02da2d193 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,20 @@ +# GEMINI.md: anime.js v4 + +This is **anime.js v4**. Generate the v4 API, never v3. + +v4 uses **named ES imports** and `animate(targets, params)`. There is no default +export and no `anime()` function. + +```js +// CORRECT (v4) +import { animate } from 'animejs'; +animate('.square', { x: 320, duration: 1000, ease: 'inOutQuint' }); + +// WRONG (deprecated v3) +import anime from 'animejs'; +anime({ targets: '.square', translateX: 320, easing: 'easeInOutQuint' }); +``` + +Full conventions and skills for each area are in [AGENTS.md](AGENTS.md) and +[`skills/`](skills/). When writing or fixing anime.js code, load the matching +`skills//SKILL.md`. diff --git a/README.md b/README.md index 6629f6a98..9cd237c5b 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,31 @@ animate('.square', { +## AI Agents & Skills + +Official agent skills teach AI coding assistants (Claude Code, Cursor, GitHub +Copilot, Gemini, and any tool supporting the Agent Skills standard) to write +correct anime.js **v4** code instead of the deprecated v3 API. + +```bash +# Cross-agent install (Claude Code, Cursor, Copilot, Windsurf, Codex, …) +npx skills add https://github.com/juliangarnier/anime +``` + +- **Skills:** [`skills/`](skills/) has one `SKILL.md` per area (core, timeline, + svg, text, scroll/draggable, utils/easings, waapi, engine, react, frameworks, + v3 to v4 migration). Index with triggers: [`skills/llms.txt`](skills/llms.txt). +- **Claude Code plugin:** run `/plugin marketplace add juliangarnier/anime`, then + install `animejs-skills`. +- **Cursor / Copilot:** auto-detected via [`.cursor/rules/`](.cursor/rules/) and + [`.github/`](.github/). +- **LLM guidance file:** [`llms.txt`](llms.txt) and [`AGENTS.md`](AGENTS.md). + +The skills are all you need for most setups. There is also an optional +[MCP server](mcp/) for MCP-native clients that serves the same skills as +resources and adds a `check_anime_v4_syntax` tool. It runs no code and fetches +nothing, so reach for the skills first. + ## V4 Documentation The full documentation is available [here](https://animejs.com/documentation). diff --git a/llms.txt b/llms.txt new file mode 100644 index 000000000..f7484beb0 --- /dev/null +++ b/llms.txt @@ -0,0 +1,56 @@ +# anime.js + +> anime.js is a fast, multipurpose and lightweight JavaScript animation library. +> It animates CSS properties, SVG, DOM attributes, and JavaScript objects. + +## Critical: use the v4 API, never v3 + +This repository is **anime.js v4**. v4 is an ES module with **named exports** and +the signature `animate(targets, params)`. There is **no default export** and **no +`anime()` function**. + +```js +// CORRECT (v4) +import { animate, stagger } from 'animejs'; +animate('.square', { x: 320, duration: 1000, ease: 'inOutQuint' }); + +// WRONG (deprecated v3, do not generate) +import anime from 'animejs'; +anime({ targets: '.square', translateX: 320, easing: 'easeInOutQuint' }); +``` + +Key v3 to v4 changes: `anime({ targets })` becomes `animate(targets, {})`; +`translateX` becomes `x`; `easing: 'easeInOutQuad'` becomes `ease: 'inOutQuad'`; +`anime.timeline()` becomes `createTimeline()`; `anime.stagger()` becomes +`stagger()`; `anime.random/set/get` becomes `utils.*`; `direction: 'alternate'` +becomes `alternate: true`. + +## Skills + +Detailed guidance for agents lives in `skills/`. Each skill is a `SKILL.md` +file you can use with Claude Code, Cursor, GitHub Copilot, and any tool that +supports the Agent Skills standard (`npx skills add https://github.com/juliangarnier/anime`). + +- [anime-core](skills/anime-core/SKILL.md): animate(targets, params), timers, transforms, keyframes, callbacks, playback. +- [anime-timeline](skills/anime-timeline/SKILL.md): createTimeline(), .add(targets, params, position), defaults, labels. +- [anime-svg](skills/anime-svg/SKILL.md): svg.createDrawable (line drawing), svg.createMotionPath, svg.morphTo. +- [anime-text](skills/anime-text/SKILL.md): splitText into lines/words/chars, stagger reveals, addEffect/revert. +- [anime-scroll-draggable](skills/anime-scroll-draggable/SKILL.md): onScroll autoplay, createDraggable, createAnimatable, createScope. +- [anime-utils-easings](skills/anime-utils-easings/SKILL.md): utils.* helpers, stagger, ease strings, cubicBezier, createSpring. +- [anime-waapi](skills/anime-waapi/SKILL.md): waapi.animate (hardware-accelerated), waapi.convertEase. +- [anime-engine](skills/anime-engine/SKILL.md): engine.defaults, timeUnit, speed, fps, custom loop. +- [anime-react](skills/anime-react/SKILL.md): createScope with useRef in useEffect, revert on unmount. +- [anime-frameworks](skills/anime-frameworks/SKILL.md): Vue / Svelte / Angular scope + lifecycle patterns. +- [anime-migration-v3-v4](skills/anime-migration-v3-v4/SKILL.md): full v3 to v4 mapping, fix deprecated syntax. + +The skills cover the API in this build (animejs 4.2.2). Features that are in the +online docs but not yet in this build, like `scrambleText` and Layout, are left +out on purpose. Each skill is checked against the built library, so it only +documents real exports. + +## Optional + +- [Full skills index](skills/llms.txt): the same skills with trigger keywords. +- [MCP server](mcp/README.md): exposes the skills as MCP resources plus + `search_anime_docs` and a `check_anime_v4_syntax` tool that flags v3 syntax. +- [Documentation](https://animejs.com/documentation): the full official docs. diff --git a/mcp/.gitignore b/mcp/.gitignore new file mode 100644 index 000000000..4c444c6ac --- /dev/null +++ b/mcp/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +coverage/ +*.log diff --git a/mcp/README.md b/mcp/README.md new file mode 100644 index 000000000..878231ec3 --- /dev/null +++ b/mcp/README.md @@ -0,0 +1,86 @@ +# @animejs/mcp + +An optional [MCP](https://modelcontextprotocol.io) server for **anime.js v4**. + +> **Most people don't need this.** The [skills](../skills/) are the main thing. +> Install them with `npx skills add https://github.com/juliangarnier/anime` and +> your agent gets the same guidance with no server to run. This MCP server is +> only useful if your client is MCP-native and you want the guidance and the +> v3-syntax check exposed as callable tools. + +It is a thin wrapper over [`../skills/`](../skills/): it reads the same `SKILL.md` +files at startup and serves them, so there is no separate content to maintain. It +does not fetch anything over the network, run anime.js, or execute code. The +syntax check is a set of regex rules, the same ones the `validate-skills` script +uses. + +## What it provides + +### Resources + +- `anime://skills/`: each skill's `SKILL.md` (for example `anime://skills/anime-core`). +- `anime://skills-index`: the index of all skills with trigger keywords. + +### Tools + +- `search_anime_docs({ topic })`: returns the matching skill body for a topic + (`core`, `timeline`, `svg`, `text`, `scroll`, `draggable`, `utils`, `easing`, + `react`, `vue`, `migration`, and more). With no topic, it returns the migration + and core skills. +- `check_anime_v4_syntax({ code })`: scans a snippet for deprecated v3 syntax + (default import, `anime({ targets })`, `translateX`/`translateY`, `easing:`, + `anime.timeline`/`stagger`/`random`, `direction:`) and returns the v4 fix for + each, or `OK` if the code is valid v4. + +## Install & run + +```bash +cd mcp +npm install +node src/stdio.mjs # stdio server +``` + +## Register with a client + +### Claude Code + +```bash +claude mcp add animejs -- node /absolute/path/to/anime/mcp/src/stdio.mjs +``` + +### Cursor + +`.cursor/mcp.json` (or any generic `mcpServers` config): + +```json +{ + "mcpServers": { + "animejs": { + "command": "node", + "args": ["/absolute/path/to/anime/mcp/src/stdio.mjs"] + } + } +} +``` + +## Verify it works + +```bash +cd mcp +npm install +npm run smoke # connects with the MCP client, lists tools/resources, calls both +npm test # unit tests +``` + +You can also open the official inspector to click through the tools: + +```bash +npx @modelcontextprotocol/inspector node src/stdio.mjs +``` + +## Note + +For most users the static [skills](../skills/) (installable with +`npx skills add https://github.com/juliangarnier/anime`) are enough and need no +running server. Use this MCP server when you want the `check_anime_v4_syntax` +tool, or guidance pulled on demand in an MCP-native client. diff --git a/mcp/package-lock.json b/mcp/package-lock.json new file mode 100644 index 000000000..2569defa4 --- /dev/null +++ b/mcp/package-lock.json @@ -0,0 +1,2641 @@ +{ + "name": "@animejs/mcp", + "version": "4.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@animejs/mcp", + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.4.3" + }, + "bin": { + "animejs-mcp": "src/stdio.mjs" + }, + "devDependencies": { + "@vitest/coverage-v8": "^4.1.8", + "vitest": "^4.1.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "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" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.8", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.3.tgz", + "integrity": "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.23", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", + "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/mcp/package.json b/mcp/package.json new file mode 100644 index 000000000..8bbd17137 --- /dev/null +++ b/mcp/package.json @@ -0,0 +1,42 @@ +{ + "name": "@animejs/mcp", + "version": "4.0.0", + "description": "MCP server for anime.js v4 that exposes the official agent skills as resources and a check_anime_v4_syntax tool that flags deprecated v3 syntax.", + "type": "module", + "bin": { + "animejs-mcp": "./src/stdio.mjs" + }, + "files": [ + "src" + ], + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "smoke": "node scripts/smoke.mjs" + }, + "engines": { + "node": ">=18" + }, + "homepage": "https://animejs.com", + "repository": { + "type": "git", + "url": "https://github.com/juliangarnier/anime.git", + "directory": "mcp" + }, + "license": "MIT", + "keywords": [ + "anime.js", + "animejs", + "mcp", + "model-context-protocol", + "animation" + ], + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.4.3" + }, + "devDependencies": { + "@vitest/coverage-v8": "^4.1.8", + "vitest": "^4.1.5" + } +} diff --git a/mcp/scripts/smoke.mjs b/mcp/scripts/smoke.mjs new file mode 100644 index 000000000..bd82c83ea --- /dev/null +++ b/mcp/scripts/smoke.mjs @@ -0,0 +1,37 @@ +// Quick check that the server speaks MCP: connect with the official client, +// list tools/resources, call both tools. Run from anywhere: node mcp/scripts/smoke.mjs +import { fileURLToPath } from "node:url"; +import { join, dirname } from "node:path"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +const serverPath = join(dirname(fileURLToPath(import.meta.url)), "..", "src", "stdio.mjs"); +const transport = new StdioClientTransport({ + command: "node", + args: [serverPath], +}); +const client = new Client({ name: "smoke", version: "1.0.0" }, { capabilities: {} }); +await client.connect(transport); +console.log("connected\n"); + +const { tools } = await client.listTools(); +console.log("tools:", tools.map((t) => t.name).join(", ")); + +const { resources } = await client.listResources(); +console.log("resources:", resources.length, "\n"); + +const bad = await client.callTool({ + name: "check_anime_v4_syntax", + arguments: { code: "import anime from 'animejs'; anime({ targets: '.b', translateX: 1 })" }, +}); +console.log("check_anime_v4_syntax (v3 input):"); +console.log(bad.content[0].text, "\n"); + +const guide = await client.callTool({ + name: "search_anime_docs", + arguments: { topic: "core" }, +}); +console.log("search_anime_docs (core):", guide.content[0].text.slice(0, 80).replace(/\n/g, " "), "..."); + +await client.close(); +console.log("\nclosed cleanly. server works."); diff --git a/mcp/src/server.mjs b/mcp/src/server.mjs new file mode 100644 index 000000000..9998ad701 --- /dev/null +++ b/mcp/src/server.mjs @@ -0,0 +1,127 @@ +// MCP server for anime.js v4. Serves the skills (skills/*/SKILL.md) as: +// - resources: anime://skills/ and anime://skills-index +// - tool search_anime_docs({ topic }): the matching SKILL.md body +// - tool check_anime_v4_syntax({ code }): flags v3 syntax + fixes +// +// Skills are read from ../../skills, so there's no duplicated guidance. +// registerAll() wires a server (real or test double); the stdio bootstrap +// lives in stdio.mjs so importing this for tests doesn't open a transport. + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +import { loadSkills, loadSkillsIndex, matchSkills } from "./skills.mjs"; +import { findV3Tokens } from "../../scripts/v3-tokens.mjs"; + +const DEFAULT_DOCS_SKILLS = ["anime-migration-v3-v4", "anime-core"]; + +/** + * Register all anime.js resources and tools on a server. + * @param {import("@modelcontextprotocol/sdk/server/mcp.js").McpServer} server + * @param {{ skills?: any[], skillsIndex?: string }} [deps] injectable for tests + */ +export const registerAll = (server, deps = {}) => { + const skills = deps.skills ?? loadSkills(); + const skillsIndex = deps.skillsIndex ?? loadSkillsIndex(); + + for (const skill of skills) { + server.registerResource( + skill.name, + `anime://skills/${skill.name}`, + { + title: skill.name, + description: skill.description, + mimeType: "text/markdown", + }, + async (uri) => ({ + contents: [ + { uri: uri.href, mimeType: "text/markdown", text: skill.raw }, + ], + }), + ); + } + + server.registerResource( + "skills-index", + "anime://skills-index", + { + title: "anime.js v4 skills index", + description: "Index of all anime.js v4 agent skills with trigger keywords.", + mimeType: "text/plain", + }, + async (uri) => ({ + contents: [{ uri: uri.href, mimeType: "text/plain", text: skillsIndex }], + }), + ); + + server.registerTool( + "search_anime_docs", + { + title: "Search the anime.js v4 docs", + description: + "Return the official anime.js v4 docs for a topic (e.g. 'core', " + + "'timeline', 'svg', 'text', 'scroll', 'draggable', 'utils', 'easing', " + + "'waapi', 'layout', 'engine', 'react', 'vue', 'migration'). Use before " + + "writing or fixing anime.js code so you emit the v4 named-import API " + + "instead of deprecated v3 syntax. With no topic, returns the migration " + + "and core docs.", + inputSchema: { topic: z.string().optional() }, + }, + async ({ topic }) => { + const matched = topic + ? matchSkills(skills, topic) + : skills.filter((s) => DEFAULT_DOCS_SKILLS.includes(s.name)); + const chosen = matched.length ? matched : skills; + const text = chosen + .map((s) => `# ${s.name}\n\n${s.body}`) + .join("\n\n---\n\n"); + return { content: [{ type: "text", text }] }; + }, + ); + + server.registerTool( + "check_anime_v4_syntax", + { + title: "Check anime.js code for deprecated v3 syntax", + description: + "Scan a snippet of anime.js code for deprecated v3 syntax (default import, " + + "anime({ targets }), translateX/Y, easing:, anime.timeline/stagger/random, " + + "direction:) and return the v4 fix for each. Returns OK if the code is " + + "valid v4. Run this on anime.js code before finalizing it.", + inputSchema: { code: z.string() }, + }, + async ({ code }) => { + const issues = findV3Tokens(code); + if (!issues.length) { + return { + content: [ + { + type: "text", + text: "OK, no v3 syntax detected. This looks like valid anime.js v4.", + }, + ], + }; + } + const report = issues + .map((i, n) => `${n + 1}. ${i.label}\n → ${i.fix}`) + .join("\n"); + return { + content: [ + { + type: "text", + text: + `Found ${issues.length} deprecated v3 pattern(s). Fix before using:\n\n` + + `${report}\n\n` + + `See search_anime_docs({ topic: "migration" }) for the full mapping.`, + }, + ], + }; + }, + ); + + return server; +}; + +/** Build a fully-wired McpServer instance. */ +export const createServer = (deps) => + registerAll(new McpServer({ name: "animejs", version: "4.0.0" }), deps); diff --git a/mcp/src/skills.mjs b/mcp/src/skills.mjs new file mode 100644 index 000000000..1a296cf6a --- /dev/null +++ b/mcp/src/skills.mjs @@ -0,0 +1,77 @@ +// Loads the SKILL.md files from ../../skills, so the MCP server serves the same +// content as the static skills, not a copy. + +import { readFileSync, readdirSync, existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const skillsDir = join(__dirname, "..", "..", "skills"); + +/** + * @typedef {{ name: string, description: string, body: string, raw: string }} Skill + */ + +const parseDescription = (md) => { + const fm = md.match(/^---\n([\s\S]*?)\n---/); + if (!fm) return ""; + const lines = fm[1].split("\n"); + for (let i = 0; i < lines.length; i++) { + const kv = lines[i].match(/^description:\s*(.*)$/); + if (!kv) continue; + if (kv[1] === ">") { + const folded = []; + while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1])) { + folded.push(lines[++i].trim()); + } + return folded.join(" "); + } + return kv[1]; + } + return ""; +}; + +const stripFrontmatter = (md) => md.replace(/^---\n[\s\S]*?\n---\n/, ""); + +/** @returns {Skill[]} */ +export const loadSkills = () => { + if (!existsSync(skillsDir)) return []; + return readdirSync(skillsDir, { withFileTypes: true }) + .filter((d) => d.isDirectory() && existsSync(join(skillsDir, d.name, "SKILL.md"))) + .map(({ name }) => { + const raw = readFileSync(join(skillsDir, name, "SKILL.md"), "utf8"); + return { + name, + description: parseDescription(raw), + body: stripFrontmatter(raw).trim(), + raw, + }; + }); +}; + +/** The root skills index (trigger keywords per skill). */ +export const loadSkillsIndex = () => { + const p = join(skillsDir, "llms.txt"); + return existsSync(p) ? readFileSync(p, "utf8") : ""; +}; + +/** + * Resolve a free-text topic to a skill. Matches by exact name, by the + * `anime-` convention, or by keyword contained in name/description. + * @param {Skill[]} skills + * @param {string} topic + * @returns {Skill[]} + */ +export const matchSkills = (skills, topic) => { + const q = topic.trim().toLowerCase(); + if (!q) return skills; + const exact = skills.filter( + (s) => s.name === q || s.name === `anime-${q}`, + ); + if (exact.length) return exact; + return skills.filter( + (s) => + s.name.includes(q) || + s.description.toLowerCase().includes(q), + ); +}; diff --git a/mcp/src/stdio.mjs b/mcp/src/stdio.mjs new file mode 100644 index 000000000..5b6ec82ff --- /dev/null +++ b/mcp/src/stdio.mjs @@ -0,0 +1,8 @@ +#!/usr/bin/env node +// Stdio entry point: build the anime.js MCP server and connect it over stdio. + +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { createServer } from "./server.mjs"; + +const server = createServer(); +await server.connect(new StdioServerTransport()); diff --git a/mcp/tests/fixtures/skills.mjs b/mcp/tests/fixtures/skills.mjs new file mode 100644 index 000000000..b3a33e8c7 --- /dev/null +++ b/mcp/tests/fixtures/skills.mjs @@ -0,0 +1,25 @@ +// Minimal in-memory skill fixtures, injected into registerAll() via deps so the +// tool tests don't depend on the real skills/ directory. + +export const fakeSkills = [ + { + name: "anime-core", + description: "Core v4 animation", + body: "Use animate(targets, params).", + raw: "---\nname: anime-core\n---\nUse animate(targets, params).", + }, + { + name: "anime-timeline", + description: "Sequencing with createTimeline", + body: "Use createTimeline().", + raw: "---\nname: anime-timeline\n---\nUse createTimeline().", + }, + { + name: "anime-migration-v3-v4", + description: "Convert v3 to v4", + body: "anime({targets}) -> animate(targets, {}).", + raw: "---\nname: anime-migration-v3-v4\n---\nanime({targets}) -> animate(targets, {}).", + }, +]; + +export const fakeIndex = "# anime.js v4 skills index\nanime-core\nanime-timeline\nanime-migration-v3-v4"; diff --git a/mcp/tests/helpers/fakeMcpServer.mjs b/mcp/tests/helpers/fakeMcpServer.mjs new file mode 100644 index 000000000..d53e53a3e --- /dev/null +++ b/mcp/tests/helpers/fakeMcpServer.mjs @@ -0,0 +1,30 @@ +// Captures what registerAll() registers, so tests can grab a tool or resource +// by name and call its handler directly, no transport needed. Matches the +// shapes server.mjs uses: +// registerResource(name, uri, metadata, handler) +// registerTool(name, config, handler) + +export class FakeMcpServer { + tools = []; + resources = []; + + registerResource(name, uri, metadata, handler) { + this.resources.push({ name, uri, metadata, handler }); + } + + registerTool(name, config, handler) { + this.tools.push({ name, config, handler }); + } + + getTool(name) { + const tool = this.tools.find((t) => t.name === name); + if (!tool) throw new Error(`Tool not registered: ${name}`); + return tool; + } + + getResource(name) { + const resource = this.resources.find((r) => r.name === name); + if (!resource) throw new Error(`Resource not registered: ${name}`); + return resource; + } +} diff --git a/mcp/tests/unit/server.test.mjs b/mcp/tests/unit/server.test.mjs new file mode 100644 index 000000000..538c61f8d --- /dev/null +++ b/mcp/tests/unit/server.test.mjs @@ -0,0 +1,108 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { registerAll } from "../../src/server.mjs"; +import { FakeMcpServer } from "../helpers/fakeMcpServer.mjs"; +import { fakeSkills, fakeIndex } from "../fixtures/skills.mjs"; + +const build = () => { + const server = new FakeMcpServer(); + registerAll(server, { skills: fakeSkills, skillsIndex: fakeIndex }); + return server; +}; + +describe("registerAll", () => { + let server; + beforeEach(() => { + server = build(); + }); + + it("registers both tools", () => { + expect(server.tools.map((t) => t.name).sort()).toEqual([ + "check_anime_v4_syntax", + "search_anime_docs", + ]); + }); + + it("registers one resource per skill plus the index", () => { + expect(server.resources.map((r) => r.name)).toEqual([ + "anime-core", + "anime-timeline", + "anime-migration-v3-v4", + "skills-index", + ]); + }); + + it("each tool declares an inputSchema", () => { + for (const t of server.tools) { + expect(t.config.inputSchema).toBeTypeOf("object"); + } + }); +}); + +describe("check_anime_v4_syntax tool", () => { + let tool; + beforeEach(() => { + tool = build().getTool("check_anime_v4_syntax"); + }); + + it("reports OK for valid v4 code", async () => { + const res = await tool.handler({ + code: "import { animate } from 'animejs'; animate('.b', { x: 1 })", + }); + expect(res.content[0].text).toMatch(/^OK/); + }); + + it("reports the count and fixes for v3 code", async () => { + const res = await tool.handler({ + code: "anime({ targets: '.b', translateX: 1 })", + }); + expect(res.content[0].text).toContain("deprecated v3 pattern"); + expect(res.content[0].text).toContain("→"); + }); + + it("does not throw on empty or missing code", async () => { + await expect(tool.handler({ code: "" })).resolves.toBeDefined(); + await expect(tool.handler({})).resolves.toBeDefined(); + }); +}); + +describe("search_anime_docs tool", () => { + let tool; + beforeEach(() => { + tool = build().getTool("search_anime_docs"); + }); + + it("returns the matching skill body for a topic", async () => { + const res = await tool.handler({ topic: "timeline" }); + expect(res.content[0].text).toContain("anime-timeline"); + expect(res.content[0].text).toContain("createTimeline"); + }); + + it("defaults to the migration + core skills when no topic is given", async () => { + const res = await tool.handler({}); + expect(res.content[0].text).toContain("anime-migration-v3-v4"); + expect(res.content[0].text).toContain("anime-core"); + expect(res.content[0].text).not.toContain("anime-timeline"); + }); + + it("falls back to all skills when the topic matches nothing", async () => { + const res = await tool.handler({ topic: "zzz-nope" }); + expect(res.content[0].text).toContain("anime-core"); + expect(res.content[0].text).toContain("anime-timeline"); + expect(res.content[0].text).toContain("anime-migration-v3-v4"); + }); +}); + +describe("skill resources", () => { + it("a skill resource handler returns its raw markdown", async () => { + const resource = build().getResource("anime-core"); + const res = await resource.handler(new URL("anime://skills/anime-core")); + expect(res.contents[0].text).toContain("animate(targets, params)"); + expect(res.contents[0].mimeType).toBe("text/markdown"); + }); + + it("the skills-index resource returns the index text", async () => { + const resource = build().getResource("skills-index"); + const res = await resource.handler(new URL("anime://skills-index")); + expect(res.contents[0].text).toContain("skills index"); + }); +}); diff --git a/mcp/tests/unit/skills.test.mjs b/mcp/tests/unit/skills.test.mjs new file mode 100644 index 000000000..a19a393fe --- /dev/null +++ b/mcp/tests/unit/skills.test.mjs @@ -0,0 +1,73 @@ +import { describe, it, expect } from "vitest"; +import { loadSkills, loadSkillsIndex, matchSkills } from "../../src/skills.mjs"; + +describe("loadSkills (reads the real skills/ dir)", () => { + const skills = loadSkills(); + + it("loads the core skills (and however many others exist)", () => { + const names = skills.map((s) => s.name); + expect(names).toContain("anime-core"); + expect(names).toContain("anime-migration-v3-v4"); + expect(skills.length).toBeGreaterThanOrEqual(9); + }); + + it("each skill has a name, description, body, and raw markdown", () => { + for (const s of skills) { + expect(s.name).toMatch(/^anime-/); + expect(s.description.length).toBeGreaterThan(0); + expect(s.body.length).toBeGreaterThan(0); + expect(s.raw).toContain("---"); + } + }); + + it("folds the multi-line YAML description into a single string", () => { + const core = skills.find((s) => s.name === "anime-core"); + expect(core.description).toContain("Core anime.js v4"); + expect(core.description).not.toContain("\n"); + }); + + it("strips frontmatter from the body", () => { + const core = skills.find((s) => s.name === "anime-core"); + expect(core.body.startsWith("---")).toBe(false); + }); +}); + +describe("loadSkillsIndex", () => { + it("returns the skills llms.txt with every skill mentioned", () => { + const index = loadSkillsIndex(); + for (const name of [ + "anime-core", + "anime-timeline", + "anime-migration-v3-v4", + ]) { + expect(index).toContain(name); + } + }); +}); + +describe("matchSkills", () => { + const skills = loadSkills(); + + it("matches an exact skill name", () => { + const m = matchSkills(skills, "anime-core"); + expect(m.map((s) => s.name)).toEqual(["anime-core"]); + }); + + it("matches the bare topic via the anime- prefix convention", () => { + const m = matchSkills(skills, "timeline"); + expect(m.map((s) => s.name)).toContain("anime-timeline"); + }); + + it("falls back to keyword/description substring matching", () => { + const m = matchSkills(skills, "migration"); + expect(m.map((s) => s.name)).toContain("anime-migration-v3-v4"); + }); + + it("returns all skills for an empty topic", () => { + expect(matchSkills(skills, "").length).toBe(skills.length); + }); + + it("returns an empty array for a topic that matches nothing", () => { + expect(matchSkills(skills, "zzz-no-such-topic")).toEqual([]); + }); +}); diff --git a/mcp/tests/unit/v3-tokens.test.mjs b/mcp/tests/unit/v3-tokens.test.mjs new file mode 100644 index 000000000..eafe38cf8 --- /dev/null +++ b/mcp/tests/unit/v3-tokens.test.mjs @@ -0,0 +1,102 @@ +import { describe, it, expect } from "vitest"; +import { + findV3Tokens, + stripComments, + V3_TOKENS, +} from "../../../scripts/v3-tokens.mjs"; + +describe("findV3Tokens", () => { + it("flags the canonical v3 snippet with all its deprecated patterns", () => { + const code = + "import anime from 'animejs';\n" + + "anime({ targets: '.b', translateX: 100, easing: 'easeInOutQuad' });"; + const labels = findV3Tokens(code).map((t) => t.label); + expect(labels).toContain("default import of animejs"); + expect(labels).toContain("anime() call"); + expect(labels).toContain("targets: property"); + expect(labels).toContain("translateX"); + expect(labels).toContain("easing: property"); + }); + + it("returns no issues for valid v4 code", () => { + const code = + "import { animate } from 'animejs';\n" + + "animate('.b', { x: 100, ease: 'inOutQuad' });"; + expect(findV3Tokens(code)).toEqual([]); + }); + + it("ignores v3 tokens that appear only inside comments", () => { + const code = + "import { animate } from 'animejs';\n" + + "animate('.b', { x: 100 }); // not translateX, and never anime({ targets })"; + expect(findV3Tokens(code)).toEqual([]); + }); + + it("ignores v3 tokens inside block comments", () => { + const code = "/* old: anime({ targets: '.b', translateX: 1 }) */\nanimate('.b', { x: 1 });"; + expect(findV3Tokens(code)).toEqual([]); + }); + + it("flags anime.timeline / anime.stagger / anime.random helpers", () => { + const labels = findV3Tokens( + "anime.timeline(); anime.stagger(50); anime.random(0, 1);", + ).map((t) => t.label); + expect(labels).toContain("anime.timeline()"); + expect(labels).toContain("anime.stagger()"); + expect(labels).toContain("anime.random/set/get"); + }); + + it("flags direction: as a v3 property", () => { + const labels = findV3Tokens("animate('.b', { direction: 'alternate' })").map( + (t) => t.label, + ); + expect(labels).toContain("direction: property"); + }); + + it("does not treat the v4 'ease:' key as the v3 'easing:' key", () => { + expect(findV3Tokens("animate('.b', { ease: 'inOutQuad' })")).toEqual([]); + }); + + it("every match carries a non-empty fix string", () => { + for (const { fix } of findV3Tokens("anime({ targets: 1 })")) { + expect(typeof fix).toBe("string"); + expect(fix.length).toBeGreaterThan(0); + } + }); + + it("is null-safe: non-string input yields no issues instead of throwing", () => { + expect(findV3Tokens(undefined)).toEqual([]); + expect(findV3Tokens(null)).toEqual([]); + expect(findV3Tokens(42)).toEqual([]); + }); +}); + +describe("stripComments", () => { + it("removes line comments but keeps code", () => { + expect(stripComments("const x = 1; // a comment").trim()).toBe("const x = 1;"); + }); + + it("removes block comments", () => { + expect(stripComments("a /* b */ c")).toBe("a c"); + }); + + it("does not break protocol-relative or URL double-slashes after a colon", () => { + // the (^|[^:]) guard means "https://x" is preserved + expect(stripComments("const u = 'https://x';")).toContain("https://x"); + }); + + it("coerces non-strings to empty string", () => { + expect(stripComments(undefined)).toBe(""); + expect(stripComments(null)).toBe(""); + }); +}); + +describe("V3_TOKENS", () => { + it("each token has a pattern, label, and fix", () => { + for (const t of V3_TOKENS) { + expect(t.pattern).toBeInstanceOf(RegExp); + expect(typeof t.label).toBe("string"); + expect(typeof t.fix).toBe("string"); + } + }); +}); diff --git a/mcp/vitest.config.mjs b/mcp/vitest.config.mjs new file mode 100644 index 000000000..cf771d72c --- /dev/null +++ b/mcp/vitest.config.mjs @@ -0,0 +1,15 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["tests/**/*.test.mjs"], + clearMocks: true, + restoreMocks: true, + coverage: { + provider: "v8", + reporter: ["text", "lcov"], + include: ["src/**/*.mjs"], + }, + }, +}); diff --git a/package.json b/package.json index ae59cc99b..5726858ed 100644 --- a/package.json +++ b/package.json @@ -189,6 +189,7 @@ "build": "rm -rf dist && build=true rollup -c && tsc -p tsconfig.types.json && echo '\\n\\033[32m✓ build completed\\n\\033[0m'", "test:browser": "browser-sync start --startPath tests/index.html --server --files 'dist/modules/**/*.js' 'tests/suites/**/*.js' --no-notify --directory", "test:node": "node --allow-natives-syntax \"node_modules/.bin/mocha\" -u tdd --timeout 20000 \"./tests/suites/node.test.js\"", - "open:examples": "browser-sync start --startPath examples --server --no-notify --directory --files '**/*.js'" + "open:examples": "browser-sync start --startPath examples --server --no-notify --directory --files '**/*.js'", + "validate:skills": "node scripts/validate-skills.mjs" } } diff --git a/scripts/v3-tokens.mjs b/scripts/v3-tokens.mjs new file mode 100644 index 000000000..799570ec2 --- /dev/null +++ b/scripts/v3-tokens.mjs @@ -0,0 +1,79 @@ +// v3 syntax patterns and their v4 fixes. One source of truth, shared by the +// validate-skills script and the MCP check_anime_v4_syntax tool. + +/** @typedef {{ pattern: RegExp, label: string, fix: string }} V3Token */ + +/** @type {V3Token[]} */ +export const V3_TOKENS = [ + { + pattern: /import\s+\w+\s+from\s+['"]animejs['"]/, + label: "default import of animejs", + fix: "Use named imports: import { animate } from 'animejs'", + }, + { + pattern: /\banime\s*\(/, + label: "anime() call", + fix: "Use animate(targets, params). anime() does not exist in v4", + }, + { + pattern: /\btargets\s*:/, + label: "targets: property", + fix: "Pass targets as the first argument: animate(targets, { ... })", + }, + { + pattern: /\btranslateX\b/, + label: "translateX", + fix: "Use the x shorthand", + }, + { + pattern: /\btranslateY\b/, + label: "translateY", + fix: "Use the y shorthand", + }, + { + pattern: /\beasing\s*:/, + label: "easing: property", + fix: "Use ease: and drop the 'ease' prefix from the value (easeInOutQuad → 'inOutQuad')", + }, + { + pattern: /\banime\.timeline\b/, + label: "anime.timeline()", + fix: "Use createTimeline()", + }, + { + pattern: /\banime\.stagger\b/, + label: "anime.stagger()", + fix: "Use the named import stagger()", + }, + { + pattern: /\banime\.(random|set|get)\b/, + label: "anime.random/set/get", + fix: "Use utils.random / utils.set / utils.get", + }, + { + pattern: /\bdirection\s*:/, + label: "direction: property", + fix: "Use alternate: true or reversed: true", + }, +]; + +// Drop comments before scanning. A note like "// not translateX" teaches the +// rule, it shouldn't trip the check. Non-string input returns "" instead of throwing. +export const stripComments = (code) => + typeof code !== "string" + ? "" + : code + .replace(/\/\*[\s\S]*?\*\//g, "") + .replace(/(^|[^:])\/\/.*$/gm, "$1"); + +/** + * Scan a code string for deprecated v3 tokens (comments ignored). + * @param {string} code + * @returns {{ label: string, fix: string }[]} matches (empty if clean v4) + */ +export const findV3Tokens = (code) => { + const stripped = stripComments(code); + return V3_TOKENS + .filter((t) => t.pattern.test(stripped)) + .map(({ label, fix }) => ({ label, fix })); +}; diff --git a/scripts/validate-skills.mjs b/scripts/validate-skills.mjs new file mode 100644 index 000000000..669636974 --- /dev/null +++ b/scripts/validate-skills.mjs @@ -0,0 +1,198 @@ +#!/usr/bin/env node +// Check the skills against the built library. Fails if: +// 1. a symbol a skill imports from 'animejs' isn't a real export +// 2. a utils./svg./text./easings. member doesn't exist +// 3. v3 syntax shows up outside the migration skill +// 4. frontmatter is off (name, description length, license, body length) +// +// Build first (npm run build), then: node scripts/validate-skills.mjs + +import { readFileSync, readdirSync, existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { findV3Tokens } from "./v3-tokens.mjs"; // strips comments before scanning + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); +const skillsDir = join(root, "skills"); +const distEntry = join(root, "dist", "modules", "index.js"); + +const MIGRATION_SKILL = "anime-migration-v3-v4"; +const MAX_DESCRIPTION = 1024; +const MAX_BODY_LINES = 600; +const NAMESPACES = ["utils", "svg", "text", "easings"]; + +const errors = []; +const warnings = []; +const fail = (msg) => errors.push(msg); + +if (!existsSync(distEntry)) { + console.error( + `\n✖ Built library not found at ${distEntry}\n Run \`npm run build\` first.\n`, + ); + process.exit(2); +} +const anime = await import(pathToFileURL(distEntry).href); +const topLevelExports = new Set(Object.keys(anime)); +const namespaceMembers = Object.fromEntries( + NAMESPACES.map((ns) => [ + ns, + new Set(anime[ns] ? Object.keys(anime[ns]) : []), + ]), +); + +const codeBlocks = (md) => { + const blocks = []; + const re = /```(?:js|javascript|jsx|tsx|ts|vue|svelte)?\n([\s\S]*?)```/g; + let m; + while ((m = re.exec(md))) blocks.push(m[1]); + return blocks; +}; + +const importedFromAnimejs = (code) => { + const names = new Set(); + const re = /import\s*\{([^}]*)\}\s*from\s*['"]animejs(?:\/[\w-]+)?['"]/g; + let m; + while ((m = re.exec(code))) { + for (const raw of m[1].split(",")) { + const name = raw.trim().split(/\s+as\s+/)[0].trim(); + if (name) names.add(name); + } + } + return names; +}; + +const namespaceCalls = (code) => { + const calls = []; + const re = /\b(utils|svg|text|easings)\.([a-zA-Z_$][\w$]*)/g; + let m; + while ((m = re.exec(code))) calls.push([m[1], m[2]]); + return calls; +}; + +const parseFrontmatter = (md) => { + const m = md.match(/^---\n([\s\S]*?)\n---/); + if (!m) return null; + const body = md.slice(m[0].length); + const fm = {}; + // minimal YAML: "key:" lines and ">" folded blocks (all the skills use) + const lines = m[1].split("\n"); + for (let i = 0; i < lines.length; i++) { + const kv = lines[i].match(/^(\w+):\s*(.*)$/); + if (!kv) continue; + const [, key, val] = kv; + if (val === ">") { + const folded = []; + while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1])) { + folded.push(lines[++i].trim()); + } + fm[key] = folded.join(" "); + } else { + fm[key] = val; + } + } + return { fm, body }; +}; + +const skillFolders = readdirSync(skillsDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name); + +if (skillFolders.length === 0) fail("No skill folders found under skills/"); + +for (const folder of skillFolders) { + const file = join(skillsDir, folder, "SKILL.md"); + if (!existsSync(file)) { + fail(`${folder}: missing SKILL.md`); + continue; + } + const md = readFileSync(file, "utf8"); + const parsed = parseFrontmatter(md); + if (!parsed) { + fail(`${folder}: missing or malformed YAML frontmatter`); + continue; + } + const { fm, body } = parsed; + + // Frontmatter invariants + if (fm.name !== folder) { + fail(`${folder}: frontmatter name "${fm.name}" !== folder "${folder}"`); + } + if (!fm.description) fail(`${folder}: missing description`); + else if (fm.description.length > MAX_DESCRIPTION) { + fail( + `${folder}: description ${fm.description.length} chars > ${MAX_DESCRIPTION}`, + ); + } + if (!fm.license) fail(`${folder}: missing license`); + const bodyLines = body.split("\n").length; + if (bodyLines > MAX_BODY_LINES) { + fail(`${folder}: body ${bodyLines} lines > ${MAX_BODY_LINES}`); + } + + // Code-block checks + const blocks = codeBlocks(md); + const isMigration = folder === MIGRATION_SKILL; + + for (const code of blocks) { + // (1) imported symbols must be real exports + for (const name of importedFromAnimejs(code)) { + if (!topLevelExports.has(name)) { + // even in the migration skill, named imports must be real v4 exports + fail(`${folder}: imports "${name}" which is NOT exported by animejs`); + } + } + // (2) namespace members must exist + for (const [ns, member] of namespaceCalls(code)) { + if (!namespaceMembers[ns].has(member)) { + fail(`${folder}: ${ns}.${member} is not a member of the ${ns} namespace`); + } + } + // (3) no v3 tokens outside the migration skill (comments are ignored) + if (!isMigration) { + const v3 = findV3Tokens(code); + if (v3.length) { + fail( + `${folder}: v3 syntax in a code block: ${v3 + .map((t) => t.label) + .join(", ")}`, + ); + } + } + } +} + +for (const llms of [join(skillsDir, "llms.txt"), join(root, "llms.txt")]) { + if (!existsSync(llms)) { + warnings.push(`missing ${llms}`); + continue; + } + const txt = readFileSync(llms, "utf8"); + for (const folder of skillFolders) { + if (!txt.includes(folder)) { + warnings.push(`${llms} does not mention skill "${folder}"`); + } + } +} + +const nsSummary = NAMESPACES.map( + (ns) => `${ns}(${namespaceMembers[ns].size})`, +).join(" "); +console.log( + `\nValidated ${skillFolders.length} skills against ${topLevelExports.size} ` + + `top-level exports + namespaces ${nsSummary}.`, +); + +if (warnings.length) { + console.log("\nWarnings:"); + for (const w of warnings) console.log(` ⚠ ${w}`); +} + +if (errors.length) { + console.error(`\n✖ ${errors.length} error(s):`); + for (const e of errors) console.error(` ✖ ${e}`); + console.error(""); + process.exit(1); +} + +console.log("\n✓ All skills valid: every documented symbol is a real v4 export, no v3 leaked.\n"); diff --git a/skills/anime-core/SKILL.md b/skills/anime-core/SKILL.md new file mode 100644 index 000000000..4a46482c3 --- /dev/null +++ b/skills/anime-core/SKILL.md @@ -0,0 +1,151 @@ +--- +name: anime-core +description: > + Core anime.js v4 animation API. Use when animating DOM elements, SVG, or plain + JS objects: tweening transforms (x, y, scale, rotate, opacity), CSS properties, + attributes, and numeric values; setting duration, delay, ease, loop, alternate; + keyframes; from/to and relative values; function-based values; callbacks; and + controlling playback (play, pause, restart, reverse, seek). anime.js v4 uses + NAMED ES module imports and the signature animate(targets, params). It has NO + default export and NO anime() function. If you write `import anime from + 'animejs'` or `anime({ targets })` that is the deprecated v3 API and it is + wrong for v4; load the anime-migration-v3-v4 skill instead. Triggers: anime.js, + animejs, animate, tween, animation, x, y, translate, scale, rotate, opacity, + duration, delay, ease, loop, alternate, autoplay, keyframes, from, to, + onComplete, onUpdate, onBegin, play, pause, restart, reverse, seek, timer, + createTimer. +license: MIT +--- + +# anime.js v4: Core animation + +anime.js v4 is an ES module with **named exports**. The main entry is +`animate(targets, params)`. There is **no default export** and **no `anime()` +function**. Writing `import anime from 'animejs'` or `anime({ targets, ... })` +is the deprecated **v3** API. See the `anime-migration-v3-v4` skill. + +## Import + +```js +import { animate } from 'animejs'; +// A subpath import also works: import { animate } from 'animejs/animation'; +``` + +## Basic animation + +```js +import { animate, stagger } from 'animejs'; + +animate('.square', { + x: 320, // transform shorthand, not translateX + rotate: { from: -180 }, // object form: { from, to } + scale: [0.5, 1], // array form: [from, to] + opacity: 0.5, // single value: animates to 0.5 + duration: 1250, // milliseconds + delay: stagger(65, { from: 'center' }), // per-target delay + ease: 'inOutQuint', // key is `ease`; value has no prefix + loop: true, + alternate: true, +}); +``` + +`targets` accepts a CSS selector string, a DOM element, an array/NodeList of +elements, or a plain JS object (animate its numeric properties directly). + +## Values: from/to, relative, function-based + +```js +animate('.box', { + opacity: [0, 1], // [from, to] + scale: { from: 0.5, to: 1 }, // { from, to } + x: '+=100', // relative to the current value ('+=', '-=', '*=') + rotate: () => utils.random(-90, 90), // function evaluated per target + width: (el, i) => `${100 + i * 20}px`, // receives (target, index, length) +}); +``` + +## Keyframes + +```js +// Per-property keyframes (array of { to, duration, ease, ... }) +animate('.box', { + y: [ + { to: -40, duration: 300 }, + { to: 0, duration: 300, ease: 'outBounce' }, + ], +}); + +// Shared keyframes via the `keyframes` array +animate('.box', { + keyframes: [ + { x: 100, y: 0 }, + { x: 100, y: 100 }, + { x: 0, y: 100 }, + ], + duration: 3000, + ease: 'inOut(2)', +}); +``` + +## Callbacks + +```js +animate('.box', { + x: 200, + onBegin: self => {}, + onUpdate: self => {}, // every frame + onBeforeUpdate: self => {}, + onLoop: self => {}, + onComplete: self => {}, // self is the animation instance +}); +``` + +`animate()` also returns a thenable, so `await animate(...)` resolves on complete. + +## Playback control + +```js +const animation = animate('.box', { x: 200, autoplay: false }); + +animation.play(); +animation.pause(); +animation.restart(); +animation.reverse(); +animation.alternate(); +animation.resume(); +animation.seek(500); // jump to 500ms +animation.cancel(); +animation.completed; // boolean +animation.currentTime; // ms +``` + +## Timers (a clock with no targets) + +Use `createTimer` when you need looped/timed callbacks without animating a +property. + +```js +import { createTimer } from 'animejs'; + +createTimer({ + duration: 1000, + loop: true, + onUpdate: self => { /* self.currentTime, self.iterationCurrentTime */ }, + onLoop: self => {}, +}); +``` + +## Rules + +- Durations and delays are in **milliseconds** by default. +- Use transform shorthands `x`, `y`, `scale`, `rotate`, `skew`, not + `translateX` / `translateY` (those are v3). +- `ease` is a string like `'inOutQuint'`, `'out(4)'`, `'inOutSine'`, `'linear'`, + `'steps(5)'`, or a spring or cubic-bezier. See `anime-utils-easings`. +- `loop` is a boolean or a number; `alternate: true` replaces v3 + `direction: 'alternate'`. +- To sequence multiple animations, use `createTimeline`. See `anime-timeline`. +- In React / Vue / Svelte, wrap animations in `createScope` and revert on + unmount. See `anime-react` and `anime-frameworks`. + + diff --git a/skills/anime-engine/SKILL.md b/skills/anime-engine/SKILL.md new file mode 100644 index 000000000..578f6b88e --- /dev/null +++ b/skills/anime-engine/SKILL.md @@ -0,0 +1,92 @@ +--- +name: anime-engine +description: > + anime.js v4 engine, the global clock and main loop that drives every animation. + Use when you need to set global defaults for all animations (engine.defaults), + switch the time unit between milliseconds and seconds (engine.timeUnit), change + global speed or frame rate (engine.speed, engine.fps), control how the document + hidden state pauses animations (engine.pauseOnDocumentHidden), or drive the loop + yourself (engine.useDefaultMainLoop = false; engine.update()). Triggers: engine, + global defaults, engine.defaults, timeUnit, seconds vs milliseconds, engine.speed, + global speed, slow motion, engine.fps, frame rate, pauseOnDocumentHidden, + useDefaultMainLoop, manual tick, engine.update, engine.pause, engine.resume, + precision. +license: MIT +--- + +# anime.js v4: Engine + +`engine` is the global clock that runs every animation. Import it to set global +defaults and control timing for the whole app. + +```js +import { engine } from 'animejs'; +// Subpath: import { engine } from 'animejs/engine'; +``` + +## Global defaults + +Set defaults applied to every animation unless overridden. + +```js +import { engine } from 'animejs'; + +engine.defaults.ease = 'out(2)'; +engine.defaults.duration = 800; +``` + +## Time unit: ms or seconds + +```js +engine.timeUnit = 'ms'; // default; durations/delays in milliseconds +engine.timeUnit = 's'; // switch the whole app to seconds +``` + +## Global speed and frame rate + +```js +engine.speed = 0.5; // half speed (slow motion) for all animations +engine.speed = 2; // double speed +engine.fps = 30; // cap the global frame rate +``` + +## Pause when the tab is hidden + +```js +engine.pauseOnDocumentHidden = true; // default: pause animations when the tab is hidden +engine.pauseOnDocumentHidden = false; // keep running in the background +``` + +## Drive the loop yourself + +By default anime runs its own requestAnimationFrame loop. Turn it off to tick the +engine from your own loop (for example a game loop or a canvas renderer). + +```js +import { engine } from 'animejs'; + +engine.useDefaultMainLoop = false; + +function myLoop() { + engine.update(); // advance all animations one tick + requestAnimationFrame(myLoop); +} +myLoop(); +``` + +## Pause and resume everything + +```js +engine.pause(); // pause every animation +engine.resume(); // resume +``` + +## Rules + +- `engine.defaults` sets app-wide defaults; per-animation params still override it. +- `engine.timeUnit` changes the unit for the whole app, so set it once at startup. +- Use `engine.speed` for global slow motion or fast forward, not per animation. +- For a custom render loop, set `engine.useDefaultMainLoop = false` and call + `engine.update()` yourself. This replaces the v3 `animation.tick()`. + + diff --git a/skills/anime-frameworks/SKILL.md b/skills/anime-frameworks/SKILL.md new file mode 100644 index 000000000..f74093522 --- /dev/null +++ b/skills/anime-frameworks/SKILL.md @@ -0,0 +1,125 @@ +--- +name: anime-frameworks +description: > + anime.js v4 in Vue, Svelte, and Angular. Use when animating inside components + of these frameworks: create animations on mount, scope them with createScope({ + root }) bound to a template ref (Vue ref / Svelte element binding / Angular + ElementRef), and revert on destroy. Scope's root natively understands an + Angular ref (reads nativeElement) and any element/selector, so teardown is a + single scope.revert() call. Triggers: vue, vue animation, nuxt, onMounted, + onUnmounted, template ref, svelte, svelte animation, onMount, onDestroy, + angular, angular animation, ElementRef, ngOnInit, ngOnDestroy, ViewChild, + createScope, scope, revert, framework animation. +license: MIT +--- + +# anime.js v4: Vue / Svelte / Angular + +The pattern is identical across frameworks: build animations on mount inside a +`createScope({ root })` bound to the component's root element, and call +`scope.revert()` on destroy. + +## Vue (` + + +``` + +## Svelte + +```svelte + + +
+
A
+
B
+
+``` + +## Angular + +`createScope({ root })` accepts an Angular ref directly (it reads +`root.nativeElement`). + +```ts +import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core'; +import { animate, createScope, stagger } from 'animejs'; + +@Component({ + selector: 'app-cards', + template: ` +
+
A
+
B
+
+ `, +}) +export class CardsComponent implements AfterViewInit, OnDestroy { + @ViewChild('root') root!: ElementRef; + private scope: any; + + ngAfterViewInit() { + this.scope = createScope({ root: this.root }).add(() => { + animate('.card', { y: [40, 0], opacity: [0, 1], delay: stagger(80) }); + }); + } + + ngOnDestroy() { + this.scope.revert(); + } +} +``` + +## Rules + +- Build animations in the **mount** lifecycle (Vue `onMounted`, Svelte + `onMount`, Angular `ngAfterViewInit`), never during setup/render. +- Bind `root` to the component's container ref so scoped selectors resolve inside + it; `createScope` understands Vue refs, Svelte element bindings, and Angular + `ElementRef`. +- **Always** `scope.revert()` on destroy (`onUnmounted` / `onDestroy` / + `ngOnDestroy`). +- Same animate/timeline/stagger API as vanilla. See `anime-core`, + `anime-timeline`, `anime-utils-easings`. + + diff --git a/skills/anime-migration-v3-v4/SKILL.md b/skills/anime-migration-v3-v4/SKILL.md new file mode 100644 index 000000000..d9f089c86 --- /dev/null +++ b/skills/anime-migration-v3-v4/SKILL.md @@ -0,0 +1,123 @@ +--- +name: anime-migration-v3-v4 +description: > + Convert deprecated anime.js v3 code to v4, and recognize/fix v3 syntax an AI + might emit by mistake. READ THIS whenever you see the v3 default export `import + anime from 'animejs'`, a call to `anime({ targets: ... })`, `translateX` / + `translateY`, `easing:` with `easeInOutQuad`-style values, `anime.timeline()`, + `anime.stagger()`, `anime.random/set/get`, `direction: 'alternate'`, the + `value:` property key, or callbacks named `update`/`begin`/`complete`/`change`. + v4 is ESM named imports with animate(targets, params). This skill is the + canonical before/after mapping. Triggers: v3, v4, migration, migrate, upgrade, + deprecated, anime is not a function, default export, anime(), targets, + translateX, translateY, easing, easeInOut, anime.timeline, anime.stagger, + anime.random, direction, convert anime, old anime syntax. +license: MIT +--- + +# anime.js v3 → v4 migration + +If you encounter v3 syntax (below), convert it. v4 has **no default export** and +**no `anime()` function**. + +## Imports & the main call + +```js +// v3 (WRONG for v4) +import anime from 'animejs'; +anime({ targets: 'div', translateX: 100, easing: 'easeInOutQuad' }); + +// v4 (CORRECT) +import { animate } from 'animejs'; +animate('div', { x: 100, ease: 'inOutQuad' }); +``` + +## Mapping table + +| v3 | v4 | +| --- | --- | +| `import anime from 'animejs'` | `import { animate } from 'animejs'` (named) | +| `anime({ targets: '.x', ... })` | `animate('.x', { ... })` | +| `translateX`, `translateY` | `x`, `y` | +| `easing: 'easeInOutQuad'` | `ease: 'inOutQuad'` (key `ease`; drop the `ease` prefix) | +| `opacity: { value: .5 }` | `opacity: { to: .5 }` | +| `direction: 'alternate'` | `alternate: true` | +| `direction: 'reverse'` | `reversed: true` | +| `round: 100` | `modifier: utils.round(2)` | +| `anime.timeline()` | `createTimeline()` | +| `anime.timeline({ easing, duration })` | `createTimeline({ defaults: { ease, duration } })` | +| `update`, `begin`, `complete` | `onUpdate`, `onBegin`, `onComplete` | +| `loopBegin` / `loopComplete` | `onLoop` | +| `change` | `onRender` | +| `.finished.then(...)` | `.then(...)` (the animation is thenable) | +| `anime.stagger(100)` | `stagger(100)` (named import) | +| `anime.random()` | `utils.random()` | +| `anime.set()` | `utils.set()` | +| `anime.get()` | `utils.get()` | +| `animation.remove()` | `utils.remove()` | +| `anime.path()` | `svg.createMotionPath()` | +| `anime.setDashoffset()` | `svg.createDrawable()` (animate the `draw` property) | +| motion path returns `{ x, y, angle }` | returns `{ translateX, translateY, rotate }` | +| `easing: 'spring(1, 80, 10, 0)'` | `ease: createSpring({ mass: 1, stiffness: 80, damping: 10, velocity: 0 })` | +| `anime.suspendWhenDocumentHidden` | `engine.pauseOnDocumentHidden` | +| `animation.tick()` | `engine.useDefaultMainLoop = false; engine.update()` | + +> `loop` semantics changed: in v4, `loop: 1` means **repeat once** (2 total +> iterations), not "run once". + +## Worked example + +```js +// v3 +import anime from 'animejs'; +anime({ + targets: '.box', + translateX: 250, + rotate: '1turn', + easing: 'easeInOutQuad', + direction: 'alternate', + loop: true, + update: () => {}, + complete: () => {}, +}); + +// v4 +import { animate } from 'animejs'; +animate('.box', { + x: 250, + rotate: '1turn', + ease: 'inOutQuad', + alternate: true, + loop: true, + onUpdate: () => {}, + onComplete: () => {}, +}); +``` + +## Timeline migration + +```js +// v3 +const tl = anime.timeline({ easing: 'easeOutExpo', duration: 750 }); +tl.add({ targets: '.a', translateX: 250 }) + .add({ targets: '.b', translateX: 250 }, '-=600'); + +// v4 +import { createTimeline } from 'animejs'; +const tl = createTimeline({ defaults: { ease: 'outExpo', duration: 750 } }); +tl.add('.a', { x: 250 }) + .add('.b', { x: 250 }, '-=600'); +``` + +## Quick checklist when fixing AI-written v3 + +1. Default import → named `{ animate }`. +2. `anime({ targets, ... })` → `animate(targets, { ... })`. +3. `translateX/Y` → `x/y`. +4. `easing: 'easeX'` → `ease: 'x'`. +5. `direction` → `alternate` / `reversed`. +6. `update/begin/complete/change` → `onUpdate/onBegin/onComplete/onRender`. +7. `anime.timeline/stagger/random/set/get` → `createTimeline` / `stagger` / + `utils.*`. + + diff --git a/skills/anime-react/SKILL.md b/skills/anime-react/SKILL.md new file mode 100644 index 000000000..7c1734676 --- /dev/null +++ b/skills/anime-react/SKILL.md @@ -0,0 +1,92 @@ +--- +name: anime-react +description: > + anime.js v4 in React. Use when animating inside React components: scope every + animation with createScope({ root }) where root is a useRef to the component's + container, run it inside useEffect, and call scope.revert() in the cleanup + function so animations and inline styles are torn down on unmount. Scope's root + natively understands a React ref (it reads ref.current), so selectors inside + the scope are resolved within that subtree. This avoids leaks, double-runs + under StrictMode, and SSR issues. Triggers: react, react animation, useEffect, + useRef, ref, createScope, scope, cleanup, revert, unmount, StrictMode, next.js, + nextjs, hooks, component animation, animate in react. +license: MIT +--- + +# anime.js v4: React + +Always animate through a **scope** tied to a ref, created in `useEffect`, and +reverted in the cleanup. `createScope({ root })` accepts a React ref directly +(it reads `root.current`). + +```jsx +import { useEffect, useRef } from 'react'; +import { animate, createScope, stagger } from 'animejs'; + +export const Cards = () => { + const root = useRef(null); + const scope = useRef(null); + + useEffect(() => { + scope.current = createScope({ root }).add(self => { + // selectors resolve within `root` (the ref's subtree) + animate('.card', { + y: [40, 0], + opacity: [0, 1], + delay: stagger(80), + duration: 600, + ease: 'out(3)', + }); + + // register named methods you can call later (optional) + self.add('pulse', () => { + animate('.card', { scale: [1, 1.1, 1], duration: 400 }); + }); + }); + + return () => scope.current.revert(); // cleanup: revert ALL scoped animations + }, []); + + return ( +
+
A
+
B
+
+ ); +}; +``` + +## Triggering scoped methods from handlers + +```jsx +const onPulse = () => scope.current.methods.pulse(); +// +``` + +## Animating a single ref'd element + +You can also animate a specific element ref without selectors: + +```jsx +const boxRef = useRef(null); + +useEffect(() => { + const animation = animate(boxRef.current, { x: 200, duration: 500 }); + return () => animation.cancel(); +}, []); +``` + +## Rules + +- Create animations inside `useEffect` (or an event handler), never during + render. +- Wrap them in `createScope({ root })` with `root` = a `useRef`; this scopes + selectors and makes teardown one call. +- **Always** `return () => scope.current.revert()` from the effect. This makes + the component safe under React StrictMode's double-invoke and prevents leaks. +- Use named scope methods (`self.add('name', fn)` then `scope.methods.name()`) to + drive animations from event handlers. +- Next.js / SSR: animation code only runs in `useEffect` (client), so it is + SSR-safe by construction. Mark interactive components `"use client"`. + + diff --git a/skills/anime-scroll-draggable/SKILL.md b/skills/anime-scroll-draggable/SKILL.md new file mode 100644 index 000000000..05260c2dd --- /dev/null +++ b/skills/anime-scroll-draggable/SKILL.md @@ -0,0 +1,138 @@ +--- +name: anime-scroll-draggable +description: > + anime.js v4 interactivity: scroll-driven animation, draggable elements, the + animatable cursor-follow pattern, and scopes. Use onScroll({ target, enter, + leave, sync }) as an animation's `autoplay` value to scrub/trigger it with + scroll; createDraggable(target, { container, x, y, snap }) to make elements + draggable; createAnimatable(targets, props) for high-frequency setter-style + updates (mouse/pointer follow); and createScope({ mediaQueries, root }).add(fn) + to bind responsive, revertible animations. Triggers: onScroll, scroll + animation, scroll trigger, scrub, sync scroll, scroll-driven, createDraggable, + draggable, drag, drag and drop, snap, createAnimatable, animatable, follow + cursor, pointer follow, createScope, scope, responsive animation, mediaQueries, + revert. +license: MIT +--- + +# anime.js v4: Scroll, Draggable, Animatable, Scope + +```js +import { + animate, createTimeline, onScroll, + createDraggable, createAnimatable, createScope, utils, stagger, +} from 'animejs'; +// Subpaths: animejs/events (onScroll), animejs/draggable, animejs/animatable, animejs/scope +``` + +## Scroll-driven: `onScroll` as `autoplay` + +`onScroll({ ... })` is passed as the `autoplay` value of an animation or +timeline. Use `sync` to scrub progress to scroll position. + +```js +import { animate, onScroll } from 'animejs'; + +animate('.box', { + x: 320, + rotate: 360, + autoplay: onScroll({ + target: '.box', // element whose scroll position drives playback + enter: 'bottom top', // when target bottom hits viewport top + leave: 'top bottom', + sync: true, // scrub to scroll; or a number for smoothing + }), +}); +``` + +On a timeline: + +```js +createTimeline({ + defaults: { ease: 'linear', duration: 500 }, + autoplay: onScroll({ + target: '.sticky-container', + enter: 'top top', + leave: 'bottom bottom', + sync: 0.5, + }), +}) +.add('.stack', { rotateY: [-180, 0] }, 0) +.add('.card', { rotate: 0 }); +``` + +## Draggable: `createDraggable` + +```js +import { createDraggable } from 'animejs'; + +createDraggable('.box', { + container: document.body, // bounds + containerPadding: 20, + snap: 50, // snap to a 50px grid (or [..] / function) + onDrag: self => {}, + onRelease: self => {}, +}); + +// Constrain to one axis with x/y objects: +createDraggable('.slider', { x: true, y: false, snap: 10 }); +``` + +## Animatable: `createAnimatable` (cursor follow, high-frequency setters) + +`createAnimatable` registers properties as callable setters that animate toward +the value you pass each frame. Great for pointer-follow. + +```js +import { createAnimatable, utils } from 'animejs'; + +const follower = createAnimatable('.dot', { + x: 500, // duration in ms to ease toward new values + y: 500, + ease: 'out(3)', +}); + +window.onpointermove = e => { + follower.x(e.clientX); // call the property as a function to set+animate + follower.y(e.clientY); +}; +``` + +## Scope: `createScope` (responsive + revertible) + +`createScope({ mediaQueries, defaults, root }).add(scope => {...})` groups +animations so they can react to media queries and be reverted together. Inside, +check `scope.matches.`. + +```js +import { animate, createScope, stagger } from 'animejs'; + +const scope = createScope({ + mediaQueries: { landscape: '(orientation: landscape)' }, + defaults: { ease: 'out(3)', duration: 500 }, +}).add(scope => { + if (scope.matches.landscape) { + animate('.card', { y: stagger(['-40vh', '40vh'], { from: 'center' }) }); + } else { + animate('.card', { opacity: [0, 1], delay: stagger(60) }); + } + + // optional cleanup returned from the add callback + return () => {}; +}); + +// Later (e.g. on unmount): scope.revert(); +``` + +## Rules + +- `onScroll(...)` is the **value of `autoplay`**, not a standalone call. Attach + it to an `animate()` or `createTimeline()`. +- Use `sync: true` (or a number) to scrub the animation to scroll position; + omit it for enter/leave triggering. +- `createAnimatable` properties are **functions you call each frame** + (`anim.x(value)`), not regular tweens. +- Wrap responsive/conditional animation in `createScope(...).add(fn)` and call + `scope.revert()` to clean up. This is essential in React/Vue (see those skills). + + diff --git a/skills/anime-svg/SKILL.md b/skills/anime-svg/SKILL.md new file mode 100644 index 000000000..4faf21130 --- /dev/null +++ b/skills/anime-svg/SKILL.md @@ -0,0 +1,112 @@ +--- +name: anime-svg +description: > + anime.js v4 SVG animation utilities, exposed under the svg namespace. Use when + animating SVG line drawing / stroke reveal (svg.createDrawable + the `draw` + property), moving an element along a path (svg.createMotionPath), or morphing + one shape into another (svg.morphTo). Import them as `import { svg } from + 'animejs'` and call svg.createDrawable(selector, start, end), + svg.createMotionPath(path, offset), or svg.morphTo(targetPath, precision). + Triggers: svg, svg animation, line drawing, stroke, stroke-dashoffset, draw, + createDrawable, drawable, motion path, createMotionPath, follow path, morph, + morphTo, shape morphing, path animation, animate svg. +license: MIT +--- + +# anime.js v4: SVG + +The SVG helpers live on the `svg` namespace. Import it and combine with +`animate` or `createTimeline`. + +```js +import { animate, createTimeline, svg } from 'animejs'; +// Subpath: import { svg } from 'animejs/svg'; +``` + +## Line drawing: `svg.createDrawable` + `draw` + +`svg.createDrawable(selector, start = 0, end = 0)` returns drawable proxy +target(s). Animate the special `draw` property with `'start end'` strings (0 to 1). + +```js +import { animate, svg } from 'animejs'; + +const line = svg.createDrawable('.line'); + +animate(line, { + draw: ['0 0', '0 1'], // from fully hidden to fully drawn + duration: 2000, + ease: 'inOutQuad', +}); +``` + +In a timeline: + +```js +import { createTimeline, svg } from 'animejs'; + +createTimeline() + .add(svg.createDrawable('.line-v'), { + draw: ['0 0', '0 1', '1 1'], // draw in, then retract + duration: 1000, + }) + .add(svg.createDrawable('.circle'), { + draw: ['0 0.5', '0 1'], + }); +``` + +## Motion path: `svg.createMotionPath` + +`svg.createMotionPath(path, offset = 0)` returns +`{ translateX, translateY, rotate }` property functions. Spread them into the +animation so the target follows the path (auto-rotating along it). + +```js +import { animate, svg } from 'animejs'; + +animate('.dot', { + ...svg.createMotionPath('#path'), // translateX, translateY, rotate + duration: 4000, + ease: 'inOutQuad', + loop: true, +}); +``` + +## Morphing: `svg.morphTo` + +`svg.morphTo(targetPathOrSelector, precision = 0.33)` returns a function used as +a tween value (typically as a `{ to: ... }` keyframe) to morph one `` into +another. + +```js +import { animate, svg } from 'animejs'; + +// Morph #shape's `d` toward the path of #shape-b +animate('#shape', { + d: svg.morphTo('#shape-b'), + duration: 500, + ease: 'inOutQuad', +}); + +// As keyframes between several shapes +animate('#line path', { + d: [ + { to: svg.morphTo('#line-1'), duration: 340, ease: 'inOutQuad' }, + { to: svg.morphTo('#line-2'), duration: 260 }, + ], +}); +``` + +## Rules + +- Always go through the `svg` namespace: `svg.createDrawable`, + `svg.createMotionPath`, `svg.morphTo`. +- For line drawing, animate the `draw` property (a `'start end'` string, values + 0 to 1) on a target returned by `svg.createDrawable`. Do not hand-animate + `stroke-dashoffset`. +- `svg.createMotionPath` returns properties to **spread** into the animation + params, not call directly. +- `svg.morphTo` requires both shapes to be `` elements; pass the target as + a selector or element. + + diff --git a/skills/anime-text/SKILL.md b/skills/anime-text/SKILL.md new file mode 100644 index 000000000..153a30fee --- /dev/null +++ b/skills/anime-text/SKILL.md @@ -0,0 +1,100 @@ +--- +name: anime-text +description: > + anime.js v4 text splitting and per-character / per-word / per-line text + animation, exposed via splitText and the text namespace. Use when animating + text by breaking it into lines, words, or characters and staggering an + animation across the pieces (typewriter reveals, word-by-word fades, character + bounces). The v4 API is splitText(target, { lines, words, chars }) which + returns a TextSplitter exposing .lines, .words, .chars arrays plus + .addEffect(fn) and .revert(). Animate those arrays with animate() or a + timeline, usually with stagger(). Triggers: text animation, split text, + splitText, TextSplitter, split into characters, per-character, per-word, + per-line, words, chars, lines, typewriter, text reveal, stagger text, animate + letters, text effect. +license: MIT +--- + +# anime.js v4: Text + +Split text into animatable pieces with `splitText`, then animate the resulting +`.chars` / `.words` / `.lines` arrays. + +```js +import { animate, createTimeline, stagger, splitText } from 'animejs'; +// Subpath: import { splitText } from 'animejs/text'; +``` + +> Note: this `dev` build (animejs 4.2.2) exposes `splitText` / `split` / +> `TextSplitter`. There is no `scrambleText` export here. + +## Split and animate + +`splitText(target, { lines, words, chars })` returns a `TextSplitter` with +`.lines`, `.words`, and `.chars` element arrays (whichever you enabled). + +```js +import { animate, stagger, splitText } from 'animejs'; + +const split = splitText('.headline', { chars: true }); + +animate(split.chars, { + y: [40, 0], + opacity: [0, 1], + duration: 600, + delay: stagger(30), // each character offset by 30ms + ease: 'out(3)', +}); +``` + +## Words and lines + +```js +const split = splitText('p', { words: true, lines: true }); + +animate(split.words, { + opacity: [0, 1], + delay: stagger(50), +}); +``` + +## Effects that re-run on resize: `addEffect` + +When splitting by `lines`, line boundaries change on resize. `addEffect` +re-applies your animation each time the text is re-split. Returning a timeline +syncs it with the splitter. + +```js +import { createTimeline, splitText } from 'animejs'; + +const split = splitText('p', { lines: true }); + +split.addEffect(split => { + return createTimeline({ + defaults: { duration: 1500, ease: 'inOutQuad', loop: true, alternate: true }, + }) + .add(split.lines, { + y: [20, 0], + opacity: [0, 1], + delay: stagger(100), + }); +}); +``` + +## Revert + +```js +split.revert(); // restores the original, un-split markup +``` + +## Rules + +- Use `splitText(target, { lines, words, chars })` and animate the returned + `.lines` / `.words` / `.chars` arrays. These are real DOM elements. +- Pair with `stagger()` (see `anime-utils-easings`) for sequential reveals. +- For line-based effects that must survive resize, use `.addEffect()`; return a + timeline to sync it with re-splitting. +- Call `.revert()` to restore the original text (and always revert on unmount in + React / Vue, see `anime-react` / `anime-frameworks`). + + diff --git a/skills/anime-timeline/SKILL.md b/skills/anime-timeline/SKILL.md new file mode 100644 index 000000000..9f6b04ddb --- /dev/null +++ b/skills/anime-timeline/SKILL.md @@ -0,0 +1,111 @@ +--- +name: anime-timeline +description: > + anime.js v4 timelines for sequencing and choreographing multiple animations. + Use when chaining animations in order, overlapping them with a position + parameter, sharing defaults across many tweens, adding labels, calls, or + syncing nested timers/animations. The v4 API is createTimeline(params) which + returns a Timeline you build with .add(targets, params, position). It + replaces the deprecated v3 anime.timeline(). Position accepts absolute ms, a + relative string like '+=100' / '-=250', or the '<' / '<<' previous-animation + anchors. Triggers: timeline, createTimeline, sequence, sequencing, choreograph, + multi-step animation, animation order, position parameter, .add, defaults, + labels, loop timeline, stagger timeline, nested timeline, sync. +license: MIT +--- + +# anime.js v4: Timelines + +Create a timeline with `createTimeline()` and add animations to it with +`.add(targets, params, position)`. This replaces the v3 `anime.timeline()`. + +## Import + +```js +import { createTimeline, stagger } from 'animejs'; +// Subpath: import { createTimeline } from 'animejs/timeline'; +``` + +## Basic timeline + +```js +import { createTimeline } from 'animejs'; + +const tl = createTimeline({ + defaults: { // applied to every .add() unless overridden + duration: 500, + ease: 'inOutSine', + }, + loop: true, + alternate: true, +}); + +tl.add('.a', { x: 100 }) + .add('.b', { y: 100 }) // starts after .a finishes (default: end of previous) + .add('.c', { rotate: 360 }); +``` + +## Position parameter + +The third argument to `.add()` controls when the animation starts. + +```js +const tl = createTimeline(); + +tl.add('.a', { x: 100 }, 0) // absolute: start at 0ms + .add('.b', { x: 100 }, '+=250') // relative: 250ms after the previous ends + .add('.c', { x: 100 }, '-=100') // overlap: 100ms before the previous ends + .add('.d', { x: 100 }, '<') // start at the same time as the previous animation + .add('.e', { x: 100 }, '<<'); // start at the start of the previous animation +``` + +## Labels, sets and calls + +```js +const tl = createTimeline(); + +tl.label('intro', 0) + .add('.title', { opacity: [0, 1] }, 'intro') + .set('.subtitle', { opacity: 0 }) // instant set, no tween + .add('.subtitle', { opacity: 1 }) + .call(() => console.log('done'), '+=100'); // run a function at a position +``` + +## Staggered timeline + +```js +import { createTimeline, stagger } from 'animejs'; + +createTimeline({ defaults: { duration: 750, ease: 'outElastic' } }) + .add('.item', { + y: [40, 0], + opacity: [0, 1], + delay: stagger(80), // each target offset by 80ms within this add + }); +``` + +## Playback control + +A timeline exposes the same controls as an animation. + +```js +const tl = createTimeline({ autoplay: false }); +tl.add('.box', { x: 200 }); + +tl.play(); +tl.pause(); +tl.restart(); +tl.reverse(); +tl.seek(1000); +``` + +## Rules + +- Build with `createTimeline()` then `.add()`, never `anime.timeline()` (v3). +- Put shared options in `defaults: {}`; per-`.add()` params override them. +- The position parameter is the **third** arg of `.add()`: absolute number, + relative `'+='`/`'-='` string, or `'<'`/`'<<'` anchors. +- A per-`.add()` `delay: stagger(...)` staggers the targets within that add. +- Timelines are tickable like animations: `play/pause/restart/reverse/seek`. + + diff --git a/skills/anime-utils-easings/SKILL.md b/skills/anime-utils-easings/SKILL.md new file mode 100644 index 000000000..070330d56 --- /dev/null +++ b/skills/anime-utils-easings/SKILL.md @@ -0,0 +1,125 @@ +--- +name: anime-utils-easings +description: > + anime.js v4 utility helpers (utils namespace) and easing system (easings + namespace + ease strings). Use for staggering animations with stagger(value, + { from, grid, axis, modifier }); selecting/getting/setting DOM via utils.$, + utils.get, utils.set, utils.remove; math/animation helpers like utils.clamp, + round, lerp, mapRange, snap, wrap, damp, random; and choosing easing, either + string forms ('inOutQuad', 'out(3)', 'outElastic(.3, 1.4)', 'steps(5)', + 'linear') or builders cubicBezier(x1,y1,x2,y2), spring/createSpring, steps, + irregular, and the eases object. Triggers: utils, stagger, random, utils.set, + utils.get, utils.$, remove, clamp, round, lerp, mapRange, snap, wrap, damp, + ease, easing, easings, cubicBezier, spring, createSpring, steps, irregular, + eases, inOutQuad, outElastic, easing function, stagger grid. +license: MIT +--- + +# anime.js v4: Utils & Easings + +```js +import { animate, stagger, utils, easings, createSpring, cubicBezier, eases } from 'animejs'; +// Subpaths: animejs/utils, animejs/easings +``` + +## Stagger: `stagger` + +`stagger(value, options)` returns a function that spreads a value across +targets. Use it for `delay`, or any property value. + +```js +import { animate, stagger } from 'animejs'; + +animate('.item', { + y: [40, 0], + delay: stagger(80), // 0, 80, 160, ... +}); + +animate('.item', { + x: stagger(['-20%', '20%']), // interpolate across a range + delay: stagger(60, { from: 'center' }), // 'first' | 'last' | 'center' | index +}); + +// Grid stagger (rows x cols), per-axis, with a modifier +const brightness = v => `brightness(${v})`; +animate('.cell', { + delay: stagger(50, { grid: [10, 10], axis: 'x', from: 'center' }), + filter: stagger([0.75, 1], { modifier: brightness }), +}); +``` + +Stagger options: `from` (`'first' | 'last' | 'center' | number`), `grid` +(`[cols, rows]`), `axis` (`'x' | 'y'`), `reversed`, `start`, `ease`, `modifier`, +`total`. + +## DOM utils: `$`, `get`, `set`, `remove` + +```js +import { utils } from 'animejs'; + +const [ $el ] = utils.$('#box'); // query → array of elements +utils.set('.box', { opacity: 0, x: 0 }); // instant set (no tween) +const value = utils.get($el, 'opacity'); // read an animated/computed value +utils.remove('.box'); // remove running animations from targets +utils.cleanInlineStyles($el); // strip inline styles anime added +``` + +## Math / animation helpers + +```js +import { utils } from 'animejs'; + +utils.clamp(120, 0, 100); // 100 +utils.round(3.14159, 2); // 3.14 +utils.lerp(0, 100, 0.5); // 50 +utils.mapRange(50, 0, 100, -1, 1); // 0 +utils.snap(43, 10); // 40 +utils.wrap(12, 0, 10); // wraps into [0,10) +utils.damp(current, target, 0.1); // frame-rate independent damping +utils.random(0, 100, 2); // random with optional decimal length +utils.randomPick(['a', 'b', 'c']); +utils.shuffle([1, 2, 3]); + +// Chainable: utils.round(0) returns a chainable number modifier +const r = utils.round(0).clamp(0, 100); +``` + +## Easing: strings (preferred) + +Pass `ease` as a string. Named eases and parametrized forms: + +```js +animate('.box', { x: 100, ease: 'inOutQuad' }); // named +animate('.box', { x: 100, ease: 'out(3)' }); // power: in/out/inOut(p) +animate('.box', { x: 100, ease: 'outElastic(.3, 1.4)' }); // elastic(amplitude, period) +animate('.box', { x: 100, ease: 'steps(5)' }); // stepped +animate('.box', { x: 100, ease: 'linear' }); +``` + +Built-in names include `linear`, `in/out/inOut/outIn` + `Quad`, `Cubic`, +`Quart`, `Quint`, `Sine`, `Circ`, `Expo`, `Bounce`, `Back`, `Elastic`. + +## Easing: builders + +```js +import { animate, cubicBezier, createSpring, steps, eases } from 'animejs'; + +animate('.box', { x: 100, ease: cubicBezier(0.225, 1, 0.915, 0.98) }); +animate('.box', { x: 100, ease: createSpring({ stiffness: 120, damping: 10 }) }); +animate('.box', { x: 100, ease: steps(10) }); +animate('.box', { x: 100, ease: eases.outElastic(1.1, 0.9) }); // eases.* factories +``` + +## Rules + +- Prefer **string eases** (`'inOutQuint'`, `'out(3)'`, `'outElastic(.3,1.4)'`); + reach for `cubicBezier` / `createSpring` / `steps` only when a string can't + express it. +- `stagger()` returns a function. Pass it as `delay` or as a property value, do + not call it yourself. +- Use `utils.set` for instant changes and `utils.$` to query; `utils.remove` to + stop animations on targets. +- Helpers like `clamp`, `round`, `lerp`, `mapRange`, `snap`, `wrap`, `damp`, + `random` live on `utils.*` (in v3 several were `anime.*`). + + diff --git a/skills/anime-waapi/SKILL.md b/skills/anime-waapi/SKILL.md new file mode 100644 index 000000000..0d72c6f5b --- /dev/null +++ b/skills/anime-waapi/SKILL.md @@ -0,0 +1,84 @@ +--- +name: anime-waapi +description: > + anime.js v4 Web Animations API (WAAPI) helper, exposed as waapi.animate. Use + when you want hardware-accelerated animations that run on the browser's native + compositor (transforms and opacity that keep moving smoothly even when the main + thread is busy). waapi.animate(targets, params) has the same shape as the main + animate() but compiles to a native WAAPI animation, with anime's nicer defaults: + multiple targets, default units, function-based values, individual transforms, + and spring or custom easings via waapi.convertEase. Triggers: waapi, web + animations api, hardware acceleration, hardware accelerated, compositor, native + animation, performance animation, waapi.animate, convertEase, off main thread, + GPU animation. +license: MIT +--- + +# anime.js v4: Web Animations API (WAAPI) + +`waapi.animate` compiles to a native Web Animations API animation, so transforms +and opacity run on the compositor and stay smooth even when the main thread is +busy. It takes the same kind of params as the main `animate`, plus anime's nicer +defaults. + +```js +import { waapi } from 'animejs'; +// Subpath: import { waapi } from 'animejs/waapi'; +``` + +## Basic WAAPI animation + +```js +import { waapi } from 'animejs'; + +waapi.animate('.box', { + translate: '100px', + rotate: '1turn', + opacity: [0, 1], + duration: 1000, + ease: 'out(3)', +}); +``` + +## What anime adds on top of native WAAPI + +```js +import { waapi, stagger } from 'animejs'; + +waapi.animate('.item', { + scale: [0.5, 1], // arrays work as [from, to] + x: (el, i) => i * 20, // function-based values + delay: stagger(60), // multiple targets + stagger + ease: 'outElastic', // spring/custom eases via convertEase under the hood +}); +``` + +## Custom and spring easings + +For eases the native WAAPI does not understand, convert them first. + +```js +import { waapi, createSpring } from 'animejs'; + +waapi.animate('.box', { + x: 200, + ease: waapi.convertEase(createSpring({ stiffness: 120, damping: 10 })), +}); +``` + +## When to use WAAPI vs the JS animate + +- Use `waapi.animate` for transforms and opacity you want offloaded to the + compositor (smooth under load, good for many elements). +- Use the JS `animate` (see `anime-core`) when you need to animate non-compositor + properties, fine playback control, timelines, or callbacks per frame. + +## Rules + +- Go through the `waapi` namespace: `waapi.animate`, `waapi.convertEase`. +- The params look like `animate`'s, but this is a native WAAPI animation, so + per-frame `onUpdate` and JS-only features differ. See the docs for WAAPI + differences (iterations, direction, easing, finished). +- For spring or custom eases, wrap them in `waapi.convertEase`. + + diff --git a/skills/llms.txt b/skills/llms.txt new file mode 100644 index 000000000..00c6282f0 --- /dev/null +++ b/skills/llms.txt @@ -0,0 +1,81 @@ +# anime.js v4: Agent Skills index + +anime.js is at v4. v4 is an ES module with NAMED imports and the signature +animate(targets, params). It has NO default export and NO anime() function. +The v3 default-export API (`import anime from 'animejs'; anime({ targets, +translateX, easing })`) is DEPRECATED and must not be generated. Load the +relevant skill below before writing anime.js code. + +Each skill lives in skills//SKILL.md. + +## anime-core +Core v4 animation: animate(targets, params), createTimer. Tween transforms +(x, y, scale, rotate, opacity), CSS props, attributes, and JS objects; from/to +([from,to], {from,to}), relative ('+='), and function values; keyframes; +callbacks (onBegin/onUpdate/onComplete/onLoop); playback (play/pause/restart/ +reverse/seek). Durations in milliseconds. +Triggers: anime.js, animejs, animate, tween, x, y, scale, rotate, opacity, duration, delay, ease, loop, alternate, keyframes, onComplete, play, pause, seek, createTimer + +## anime-timeline +Sequencing with createTimeline() and .add(targets, params, position). Shared +defaults, position offsets ('+=100', '-=250', '<', '<<', absolute ms), labels, +.set/.call, staggered adds. Replaces v3 anime.timeline(). +Triggers: timeline, createTimeline, sequence, sequencing, choreograph, position parameter, .add, defaults, labels, nested timeline + +## anime-svg +SVG via the svg namespace: line drawing (svg.createDrawable + the `draw` +property), motion paths (svg.createMotionPath returns {translateX,translateY, +rotate}), and shape morphing (svg.morphTo). +Triggers: svg, line drawing, stroke, draw, createDrawable, motion path, createMotionPath, follow path, morph, morphTo, shape morphing, path animation + +## anime-text +Text splitting with splitText(target, { lines, words, chars }) → TextSplitter +exposing .lines/.words/.chars arrays plus .addEffect/.revert. Animate the +arrays, usually with stagger(). (This build has no scrambleText.) +Triggers: text animation, split text, splitText, TextSplitter, per-character, per-word, per-line, words, chars, lines, typewriter, stagger text + +## anime-scroll-draggable +Interactivity: onScroll({target,enter,leave,sync}) as an autoplay value for +scroll-driven/scrubbed animation; createDraggable(target,{container,x,y,snap}); +createAnimatable for pointer-follow setters; createScope({mediaQueries,root}) +for responsive, revertible animation. +Triggers: onScroll, scroll animation, scroll trigger, scrub, sync, createDraggable, draggable, drag, snap, createAnimatable, follow cursor, createScope, scope, responsive, revert + +## anime-utils-easings +utils namespace (stagger, $, get, set, remove, clamp, round, lerp, mapRange, +snap, wrap, damp, random) and easing: string forms ('inOutQuad', 'out(3)', +'outElastic(.3,1.4)', 'steps(5)', 'linear') plus builders cubicBezier, +createSpring, steps, irregular, eases. +Triggers: utils, stagger, random, utils.set, utils.get, utils.$, remove, clamp, round, lerp, mapRange, snap, wrap, damp, ease, easing, cubicBezier, spring, createSpring, steps, eases, outElastic, grid stagger + +## anime-waapi +Hardware-accelerated animation through the Web Animations API: waapi.animate +(same params as animate, runs on the compositor) and waapi.convertEase for +spring/custom eases. +Triggers: waapi, web animations api, hardware acceleration, compositor, native animation, performance, waapi.animate, convertEase, GPU animation + +## anime-engine +The global clock and main loop. Set engine.defaults, engine.timeUnit (ms or s), +engine.speed, engine.fps, engine.pauseOnDocumentHidden, or drive the loop with +engine.useDefaultMainLoop = false and engine.update(). +Triggers: engine, global defaults, timeUnit, seconds, engine.speed, slow motion, engine.fps, frame rate, pauseOnDocumentHidden, useDefaultMainLoop, manual tick, engine.update + +## anime-react +React: scope every animation with createScope({ root }) where root is a useRef, +run it in useEffect, and return () => scope.revert() for cleanup. StrictMode- +and SSR-safe. +Triggers: react, useEffect, useRef, ref, createScope, scope, cleanup, revert, unmount, StrictMode, next.js, hooks, component animation + +## anime-frameworks +Vue / Svelte / Angular: build animations on mount inside createScope({ root }) +bound to a template ref, revert on destroy. Scope's root understands Vue refs, +Svelte element bindings, and Angular ElementRef. +Triggers: vue, nuxt, onMounted, onUnmounted, template ref, svelte, onMount, onDestroy, angular, ElementRef, ngOnDestroy, ViewChild, createScope, revert + +## anime-migration-v3-v4 +Convert v3 to v4 / fix v3 syntax. anime({targets}) → animate(targets,{}); +translateX → x; easing:'easeX' → ease:'x'; value: → to:; direction:'alternate' +→ alternate:true; update/begin/complete/change → onUpdate/onBegin/onComplete/ +onRender; anime.timeline() → createTimeline(); anime.stagger → stagger; +anime.random/set/get → utils.*. +Triggers: v3, v4, migration, migrate, upgrade, deprecated, "anime is not a function", default export, anime(), targets, translateX, translateY, easing, anime.timeline, anime.stagger, anime.random, direction, convert anime