diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index bfbd9fad..3fd9ae2f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,37 +1,40 @@ --- -name: Bug report -about: Create a report to help us improve -title: '[BUG]' -labels: 'bug' -assignees: '' - +name: Bug Report Template +about: Use this template to report bugs in the line-bot-mcp-server +title: 'Bug Report' --- ## Bug Report - - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** - -Steps to reproduce the behavior: -1. use '...' function, pass the '...' parameter. -2. ... -3. ??? -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Environment (please complete the following information):** + + +## System Information - OS: [e.g. Ubuntu] - Node.js Version [e.g. Node 22] - line-bot-mcp-server version(s) [e.g. 0.0.1] - AI Agent you use [e.g. Claude for Desktop] -**Additional context** -Add any other context about the problem here. +## Expected Behavior + + +## Current Behavior + + +## Steps to Reproduce + +1. +2. +3. + +## Logs + + +## Additional Context (Optional) + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 6832110f..a2eba9a3 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,10 +1,7 @@ --- name: Feature request about: Suggest an idea for this project -title: "[Feature Request]" -labels: enhancement -assignees: '' - +title: "Feature Request" --- ## Feature Request diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..96c0d54d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,42 @@ +--- +name: "Question Template" +about: "Use this template to ask questions about usage or implementation of the line-bot-mcp-server." +title: "Question" +--- + + + +## Have You Checked the Following? +- [ ] [Developer Documentation - LINE Developers](https://developers.line.biz/en/docs/) +- [ ] [FAQ - LINE Developers](https://developers.line.biz/en/faq/tags/messaging-api/) + +## Summary of Your Question + + +## Details + + +## What You've Tried + + +## Your Environment + + +## Additional Context (Optional) + diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..fe1b7dc8 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,24 @@ +# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes +changelog: + categories: + - title: Breaking Changes + labels: + - breaking-change + - title: line-openapi updates + labels: + - line-openapi-update + - title: New Features + labels: + - new-features + - title: Bug fix + labels: + - bug-fix + - title: Dependency updates + labels: + - dependency upgrade + exclude: + labels: + - line-openapi-update + - title: Other Changes + labels: + - "*" diff --git a/.github/scripts/npm-audit.sh b/.github/scripts/npm-audit.sh new file mode 100755 index 00000000..4194204c --- /dev/null +++ b/.github/scripts/npm-audit.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +dirs=() +while IFS= read -r path; do + dirs+=("$(dirname "$path")") +done < <( + find . \( -path '*/node_modules' -o -path '*/dist' \) -prune -o \ + \( -name package.json -o -name package-lock.json \) -print +) + +IFS=$'\n' dirs=($(printf '%s\n' "${dirs[@]}" | sort -u)); unset IFS + +declare -a failed=() + +for dir in "${dirs[@]}"; do + printf '\n\n\033[1;34m==> %s\033[0m\n' "$dir" + + pushd "$dir" >/dev/null + if ! npm audit --audit-level moderate; then + failed+=("$dir") + fi + popd >/dev/null +done + +if ((${#failed[@]})); then + echo -e "\n\033[0;31mnpm audit reported vulnerabilities in:\033[0m" + printf ' - %s\n' "${failed[@]}" + echo "You can run 'npm audit fix' in these directories to resolve the issues." + echo "If running 'npm audit fix' does not resolve the issues, you may need to manually update dependencies." + exit 1 +else + echo "npm audit passed: no vulnerabilities detected" + exit 0 +fi diff --git a/.github/scripts/update-version.mjs b/.github/scripts/update-version.mjs new file mode 100644 index 00000000..6c5ad6fb --- /dev/null +++ b/.github/scripts/update-version.mjs @@ -0,0 +1,112 @@ +import fs from 'fs/promises'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +async function updateVersion(newVersion) { + // Files having version number + const files = { + packageJson: './package.json', + packageLockJson: './package-lock.json', + versionTs: './src/version.ts', + manifestJson: './manifest.json', + }; + + // package.json + const packageJsonData = JSON.parse(await fs.readFile(files.packageJson, 'utf8')); + packageJsonData.version = newVersion; + await fs.writeFile(files.packageJson, JSON.stringify(packageJsonData, null, 2) + '\n'); + + // package-lock.json + const packageLockJsonData = JSON.parse(await fs.readFile(files.packageLockJson, 'utf8')); + packageLockJsonData.version = newVersion; + if (packageLockJsonData.packages && packageLockJsonData.packages[""]) { + packageLockJsonData.packages[""].version = newVersion; + } + await fs.writeFile(files.packageLockJson, JSON.stringify(packageLockJsonData, null, 2) + '\n'); + + // src/version.ts + const versionTsData = await fs.readFile(files.versionTs, 'utf8'); + const updatedVersionTsData = versionTsData.replace( + /const LINE_BOT_MCP_SERVER_VERSION = ".*?";/, + `const LINE_BOT_MCP_SERVER_VERSION = "${newVersion}";` + ); + await fs.writeFile(files.versionTs, updatedVersionTsData); + + // manifest.json + const manifestJsonData = JSON.parse(await fs.readFile(files.manifestJson, 'utf8')); + manifestJsonData.version = newVersion; + await fs.writeFile(files.manifestJson, JSON.stringify(manifestJsonData, null, 2) + '\n'); + + console.log(`Version updated to ${newVersion} in all files.`); +} + +async function verifyVersion(expectedVersion) { + // package.json + const packageJsonData = JSON.parse(await fs.readFile('./package.json', 'utf8')); + if (packageJsonData.version !== expectedVersion) { + throw new Error(`package.json version mismatch: expected ${expectedVersion}, found ${packageJsonData.version}`); + } + + // package-lock.json + const packageLockJsonData = JSON.parse(await fs.readFile('./package-lock.json', 'utf8')); + if (packageLockJsonData.version !== expectedVersion) { + throw new Error(`package-lock.json version mismatch: expected ${expectedVersion}, found ${packageLockJsonData.version}`); + } + if (packageLockJsonData.packages && packageLockJsonData.packages[""] && packageLockJsonData.packages[""].version !== expectedVersion) { + throw new Error(`package-lock.json root package version mismatch: expected ${expectedVersion}, found ${packageLockJsonData.packages[""].version}`); + } + + // src/version.ts + const versionTsData = await fs.readFile('./src/version.ts', 'utf8'); + if (!versionTsData.includes(`const LINE_BOT_MCP_SERVER_VERSION = "${expectedVersion}";`)) { + throw new Error(`src/version.ts version mismatch: expected ${expectedVersion}`); + } + + // manifest.json + const manifestJsonData = JSON.parse(await fs.readFile('./manifest.json', 'utf8')); + if (manifestJsonData.version !== expectedVersion) { + throw new Error(`manifest.json version mismatch: expected ${expectedVersion}, found ${manifestJsonData.version}`); + } + + console.log(`All files have the correct version: ${expectedVersion}`); +} + +async function verifyGitDiff() { + try { + const { stdout: numstatOutput } = await execAsync('git diff --numstat'); + const { addedLines, deletedLines } = numstatOutput.trim().split('\n').reduce((acc, line) => { + const [added, deleted] = line.split('\t').map(Number); + acc.addedLines += added; + acc.deletedLines += deleted; + return acc; + }, { addedLines: 0, deletedLines: 0 }); + + if (addedLines !== 5 || deletedLines !== 5) { + throw new Error(`Unexpected number of changed lines: expected 5 added and 5 deleted, found ${addedLines} added and ${deletedLines} deleted`); + } + + console.log('Git diff verification passed: 5 lines added and 5 lines deleted.'); + + // Display the diff with context and color + const { stdout: diffOutput } = await execAsync('git diff -U5 --color=always'); + console.log('Git diff with context and color:\n', diffOutput); + + } catch (error) { + console.error('Error during git diff verification:', error.message); + process.exit(1); + } +} + +// Main process +const args = process.argv.slice(2); +if (args.length !== 1) { + console.error('Usage: node update-version.mjs '); + process.exit(1); +} + +const newVersion = args[0]; +await updateVersion(newVersion); +await verifyVersion(newVersion); +await verifyGitDiff(); diff --git a/.github/workflows/check-eol-newrelease.yml b/.github/workflows/check-eol-newrelease.yml index 154c9d8b..8813803f 100644 --- a/.github/workflows/check-eol-newrelease.yml +++ b/.github/workflows/check-eol-newrelease.yml @@ -15,10 +15,10 @@ jobs: if: github.repository == 'line/line-bot-mcp-server' steps: - name: Check out code - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Run EoL & NewRelease check - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const checkEolAndNewReleases = require('.github/scripts/check-eol-newrelease.cjs'); diff --git a/.github/workflows/close-issue.yml b/.github/workflows/close-issue.yml index fb57be49..f0e824df 100644 --- a/.github/workflows/close-issue.yml +++ b/.github/workflows/close-issue.yml @@ -3,8 +3,8 @@ name: Close inactive issues on: schedule: - # Every day at 1:30 UTC -> 10:30 JST - - cron: "30 1 * * *" + - cron: "30 10 * * *" + timezone: "Asia/Tokyo" jobs: close-issues: @@ -14,15 +14,15 @@ jobs: pull-requests: write if: github.repository == 'line/line-bot-mcp-server' steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: - days-before-issue-stale: 28 # 4 weeks + days-before-issue-stale: 14 days-before-issue-close: 0 stale-issue-label: "no-activity" - close-issue-message: "This issue was closed because it has been inactive for 28 days." - exempt-issue-labels: "bug,enhancement,keep" + close-issue-message: "This issue was closed because it has been inactive for 14 days." + exempt-issue-labels: "bug,enhancement,keep,untriaged" days-before-pr-stale: -1 - days-before-pr-close: 28 + days-before-pr-close: 14 stale-pr-label: "no-activity" - close-pr-message: "This pull request was closed because it has been inactive for 28 days. Please reopen if you still intend to submit this pull request." + close-pr-message: "This pull request was closed because it has been inactive for 14 days. Please reopen if you still intend to submit this pull request." repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/create-draft-release.yml b/.github/workflows/create-draft-release.yml new file mode 100644 index 00000000..59b8bc23 --- /dev/null +++ b/.github/workflows/create-draft-release.yml @@ -0,0 +1,130 @@ +name: Create Draft Release with Auto-Generated Notes + +on: + workflow_dispatch: + inputs: + version_type: + description: "Select the version type to increment (major, minor, patch)" + required: true + type: choice + options: + - patch + - minor + - major + release_title: + description: "Enter the title of the release" + required: true + type: string + acknowledge_draft: + description: "I understand that I must re-edit and finalize the draft release (Y/N)" + required: true + type: choice + options: + - "No" + - "Yes" + +jobs: + validate-input: + runs-on: ubuntu-latest + permissions: {} + steps: + - name: Validate Acknowledgement + if: ${{ github.event.inputs.acknowledge_draft != 'Yes' }} + run: | + echo "You must select 'Yes' to acknowledge your responsibility for finalizing the draft release." + exit 1 + - name: Validate title (no empty) + if: ${{ github.event.inputs.release_title == '' }} + run: | + echo "You must enter a title for the release." + exit 1 + + create-draft-release: + runs-on: ubuntu-latest + needs: validate-input + permissions: + contents: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Fetch Latest Release + id: get-latest-release + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const latestRelease = await github.rest.repos.getLatestRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + }).catch(() => null); + + if (latestRelease) { + core.setOutput('latest_tag', latestRelease.data.tag_name); + } else { + core.setOutput('latest_tag', 'v0.0.0'); // Default for first release + } + + - name: Calculate New Version + id: calculate-version + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const latestTag = '${{ steps.get-latest-release.outputs.latest_tag }}'; + const versionType = '${{ github.event.inputs.version_type }}'; + + const [major, minor, patch] = latestTag.replace('v', '').split('.').map(Number); + + let newVersion; + if (versionType === 'major') { + newVersion = `v${major + 1}.0.0`; + } else if (versionType === 'minor') { + newVersion = `v${major}.${minor + 1}.0`; + } else { + newVersion = `v${major}.${minor}.${patch + 1}`; + } + + core.setOutput('new_version', newVersion); + + - name: Generate Release Notes + id: generate-release-notes + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { data: releaseNotes } = await github.rest.repos.generateReleaseNotes({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: "${{ steps.calculate-version.outputs.new_version }}" + }); + + const actor = context.actor; + const noteToAdd = `**@${actor} 👈 TODO: Write detailed release note for this version before release**\n`; + + const footer = `---\nThis release is prepared by @${actor}`; + + const modifiedBody = releaseNotes.body.replace( + '## What\'s Changed', + `## What's Changed\n\n${noteToAdd}` + ) + .concat(`\n\n${footer}`); + + console.log(`releaseNotes (modified): ${JSON.stringify(modifiedBody, null, 2)}`); + const fs = require('fs'); + fs.writeFileSync('release-notes.txt', modifiedBody, { encoding: 'utf8' }); + + - name: Prepare Release Title + id: title + env: + # "vX.Y.Z Release Title" + RAW_TITLE: ${{ steps.calculate-version.outputs.new_version }} ${{ github.event.inputs.release_title }} + run: | + # Print RAW_TITLE safely, then escape double quotes + SANITIZED_TITLE="$(printf '%s' "$RAW_TITLE" | sed 's/"/\\"/g')" + echo "sanitized_title=$SANITIZED_TITLE" >> "$GITHUB_OUTPUT" + + - name: Create Draft Release + run: | + gh release create "${{ steps.calculate-version.outputs.new_version }}" \ + --title "${{ steps.title.outputs.sanitized_title }}" \ + --notes-file release-notes.txt \ + --draft \ + --repo "${{ github.repository }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/label-issue.yml b/.github/workflows/label-issue.yml new file mode 100644 index 00000000..6ecb8606 --- /dev/null +++ b/.github/workflows/label-issue.yml @@ -0,0 +1,32 @@ +name: Label issue + +on: + issues: + types: + - opened + - reopened + - closed + +jobs: + label-issues: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Add label on issue open + if: github.event.action == 'opened' || github.event.action == 'reopened' + run: | + gh issue edit ${{ github.event.issue.number }} \ + --add-label "untriaged" \ + env: + GH_TOKEN: ${{ github.token }} + + - name: Remove label on issue close + if: github.event.action == 'closed' + run: | + gh issue edit ${{ github.event.issue.number }} \ + --remove-label "untriaged" + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/npm-audit.yml b/.github/workflows/npm-audit.yml new file mode 100644 index 00000000..44613a59 --- /dev/null +++ b/.github/workflows/npm-audit.yml @@ -0,0 +1,96 @@ +name: "Reminder for 'run npm audit'" + +on: + schedule: + - cron: '0 22 * * *' + workflow_dispatch: + push: + branches: + - 'main' + +jobs: + run-npm-audit: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + if: github.repository == 'line/line-bot-mcp-server' + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '24' + package-manager-cache: false + + - name: Run npm audit and check diff + id: audit + run: .github/scripts/npm-audit.sh + continue-on-error: true + + - name: Close reminder issue when audit passes + if: steps.audit.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { owner, repo } = context.repo; + const title = 'Reminder: run npm audit'; + + const { data: result } = await github.rest.search.issuesAndPullRequests({ + q: `repo:${owner}/${repo} is:issue is:open in:title "${title}"` + }); + + for (const issue of result.items) { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: issue.number, + body: 'Auto-closed because npm audit is now passing.' + }); + await github.rest.issues.update({ + owner, + repo, + issue_number: issue.number, + state: 'closed', + state_reason: 'completed' + }); + } + + - name: Create or update reminder issue + if: steps.audit.outcome == 'failure' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + TZ: 'Asia/Tokyo' + with: + script: | + const { owner, repo } = context.repo; + const title = 'Reminder: run npm audit'; + const securityURL = `https://github.com/${owner}/${repo}/security`; + const baseBody = [ + 'Fix all vulnerabilities. You can check with `.github/scripts/npm-audit.sh` locally, then send a PR with the fixes.', + `After fixing, make sure the vulnerabilities count in **${securityURL}** is **0**.` + ].join('\n\n'); + + const { data: result } = await github.rest.search.issuesAndPullRequests({ + q: `repo:${owner}/${repo} is:issue is:open in:title "${title}"` + }); + + const today = new Date(); + + if (result.total_count === 0) { + await github.rest.issues.create({ + owner, + repo, + title, + body: `${baseBody}\n\n0 days have passed.` + }); + } else { + const issue = result.items[0]; + const created = new Date(issue.created_at); + const diffDays = Math.floor((today - created) / 86_400_000); + await github.rest.issues.update({ + owner, + repo, + issue_number: issue.number, + body: `${baseBody}\n\n${diffDays} days have passed.` + }); + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..2a86100c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Release Node.js Package +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: 'The version to release' + required: true + +jobs: + release-package: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + issues: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + # Setup .npmrc file to publish to GitHub Packages + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 24 + # Enable this when setup-node v5 is released, and release is broken + # package-manager-cache: false + - name: Update version in package.json, package-lock.json + run: | + if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then + VERSION=${{ github.event.inputs.version }} + else + VERSION=${{ github.event.release.tag_name }} + fi + VERSION=${VERSION#v} + echo "VERSION=$VERSION" >> $GITHUB_ENV + node .github/scripts/update-version.mjs $VERSION + - run: npm install + - run: npm run release + + - name: Create GitHub Issue on Failure + if: failure() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const { owner, repo } = context.repo; + const version = process.env.VERSION; + const issueTitle = `Release job for ${version} failed`; + const issueBody = `The release job failed. Please check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for more details.`; + const assignees = [context.actor]; + + await github.rest.issues.create({ + owner, + repo, + title: issueTitle, + body: issueBody, + assignees + }); diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fbafe357..8b9efa7c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,32 +13,52 @@ jobs: strategy: matrix: # https://nodejs.org/en/about/releases/ - node: [ '20', '20.12.2', '22' ] + node: + - '22' + - '24' + - '26' fail-fast: false name: Node.js ${{ matrix.node }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: true - name: Setup Node.js - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: ${{ matrix.node }} cache: 'npm' + # Enable this when setup-node v5 is released, and release is broken + # package-manager-cache: false - name: Install Dependency run: npm ci - name: Build run: npm run build + - name: Test + run: npm test + - name: publint + run: npm run check:publint + + docker-build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + submodules: true + - name: Build Docker image + run: docker build . pinact: runs-on: ubuntu-latest permissions: contents: read steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Run pinact - uses: suzuki-shunsuke/pinact-action@49cbd6acd0dbab6a6be2585d1dbdaa43b4410133 # v1.0.0 + uses: suzuki-shunsuke/pinact-action@896d595f299e71d65b9d28349d6956abe144390a # v3.0.0 with: skip_push: "true" diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..c0dee2a4 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +ignore-scripts=true +min-release-age=7 +registry=https://registry.npmjs.org/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de1fec99..7aa288ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,14 +13,58 @@ be fantastic if you help us by doing any of the following: You can freely fork the project, clone the forked repository, and start editing. -You may use the following npm scripts for development: +### Install dependencies -* `npm run format`: Format source code with [Prettier](https://github.com/prettier/prettier) -* `npm run format:check`: Silently run `format` and report formatting errors -* `npm run build`: Build TypeScript code into JavaScript. The built files will +Run `npm install` to install all dependencies for development. + +### Understand the project structure + +The project structure is as follows: + +- src: The main code. +- tools: The tools that can be used in the MCP server. +- common: The common code like utilities and types. + +### Add a new Tool + +To add a new Tool, you can create a new file under `src/tools/` and +implement the Tool in that file. The Tool should extend `AbstractTool` +and should be registered in `src/index.ts`. +Please remember to add the description of the tool to both README.md and README.ja.md. + +When adding a new tool, you also need to update the following tests: + +1. **Add a tool test file** — Create `test/tools/.test.ts`. +2. **Add mock methods if needed** — If your tool calls a LINE API method + not yet listed in `test/helpers/mock-line-clients.ts`, add it there. + +### Run tests + +Run `npm test` to execute the test suite: + +```bash +npm test +``` + +Tests are located in the `test/` directory and use [Vitest](https://vitest.dev/). +LINE API calls are mocked using dependency injection — each tool class accepts +a client in its constructor, so tests pass in stub objects created with `vi.fn()`. +See `test/helpers/mock-line-clients.ts` for the mock factories. + +Especially for bug fixes, please follow this flow for testing and development: +1. Write a test before making changes to the library and confirm that the test fails. +2. Modify the code of the library. +3. Run the test again and confirm that it passes thanks to your changes. + +### Run all CI tasks in your local + +- `npm run format`: Format source code with [Prettier](https://github.com/prettier/prettier) +- `npm run format:check`: Silently run `format` and report formatting errors +- `npm run build`: Build TypeScript code into JavaScript. The built files will be placed in `dist/`. +- `npm test`: Run the test suite. -We lint and build on CI, but it is always nice to check them before +We lint, build, and test on CI, but it is always nice to check them before uploading a pull request. ## Contributor license agreement diff --git a/Dockerfile b/Dockerfile index 10ae7f86..1a85f52e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,66 @@ -FROM node:22.14-alpine AS builder +FROM node:24.18-alpine AS builder COPY . /app WORKDIR /app -RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev +RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts RUN --mount=type=cache,target=/root/.npm npm run build -FROM node:22-alpine AS release +# --- Release Stage --- +FROM node:24-alpine AS release +# Install necessary tools and base fonts +RUN apk add --no-cache \ + fontconfig \ + font-noto-cjk \ + ttf-freefont \ + curl \ + unzip \ + chromium \ + nss + +# Download and install Japanese fonts from GitHub +RUN mkdir -p /usr/share/fonts/truetype/google && \ + # Noto Sans JP from GitHub + curl -L "https://github.com/googlefonts/noto-cjk/raw/main/Sans/OTF/Japanese/NotoSansJP-Regular.otf" -o /usr/share/fonts/truetype/google/NotoSansJP-Regular.otf && \ + curl -L "https://github.com/googlefonts/noto-cjk/raw/main/Sans/OTF/Japanese/NotoSansJP-Bold.otf" -o /usr/share/fonts/truetype/google/NotoSansJP-Bold.otf && \ + # IPAex Gothic font as fallback + curl -L "https://moji.or.jp/wp-content/ipafont/IPAexfont/IPAexfont00401.zip" -o ipaex.zip && \ + unzip ipaex.zip && \ + cp IPAexfont00401/*.ttf /usr/share/fonts/truetype/google/ && \ + rm -rf IPAexfont00401 ipaex.zip + +# Update font cache +RUN fc-cache -f -v + +# Set Puppeteer to use system Chromium +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + +# Set up a non-root user ('appuser'/'appgroup') to avoid running as root - good security practice! +# (-S is the Alpine option for a system user/group, suitable here) +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + +# Copy the built code and necessary package files from our builder stage COPY --from=builder /app/dist /app/dist COPY --from=builder /app/package.json /app/package.json COPY --from=builder /app/package-lock.json /app/package-lock.json +COPY --from=builder /app/richmenu-template /app/richmenu-template ENV NODE_ENV=production WORKDIR /app -RUN npm ci --ignore-scripts --omit-dev +# Give our new 'appuser' ownership of the application files inside /app +# Needs to happen after copying the files over +RUN chown -R appuser:appgroup /app + +# Install *only* the production dependencies +RUN npm ci --ignore-scripts --omit=dev + +# Now, switch to running as our non-root user for the actual app process +USER appuser +# Define how to start the application ENTRYPOINT ["node", "dist/index.js"] diff --git a/README.ja.md b/README.ja.md index a3648d00..32e27a0b 100644 --- a/README.ja.md +++ b/README.ja.md @@ -1,5 +1,7 @@ # LINE Bot MCP Server +[![npmjs](https://badge.fury.io/js/%40line%2Fline-bot-mcp-server.svg)](https://www.npmjs.com/package/@line/line-bot-mcp-server) + LINE公式アカウントとAI Agentを接続するために、LINE Messaging APIを統合する[Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) Server ![](/assets/demo.ja.png) @@ -10,67 +12,113 @@ LINE公式アカウントとAI Agentを接続するために、LINE Messaging AP ## Tools 1. **push_text_message** - - LINEでユーザーにシンプルなテキストメッセージを送信する + - LINEでユーザーにシンプルなテキストメッセージを送信する。 - **入力:** - - `user_id` (string): メッセージ受信者のユーザーID。デフォルトはDESTINATION_USER_ID。 + - `userId` (string?): メッセージ受信者のユーザーID。デフォルトはDESTINATION_USER_ID。`userId`または`DESTINATION_USER_ID`のどちらか一方は必ず設定する必要があります。 - `message.text` (string): ユーザーに送信するテキスト。 2. **push_flex_message** - - LINEでユーザーに高度にカスタマイズ可能なフレックスメッセージを送信する。バブル(単一コンテナ)とカルーセル(スワイプ可能な複数のバブル)レイアウトの両方をサポート。 + - LINEでユーザーに高度にカスタマイズ可能なフレックスメッセージを送信する。 + - **入力:** + - `userId` (string?): メッセージ受信者のユーザーID。デフォルトはDESTINATION_USER_ID。`userId`または`DESTINATION_USER_ID`のどちらか一方は必ず設定する必要があります。 + - `message.altText` (string): フレックスメッセージが表示できない場合に表示される代替テキスト。 + - `message.contents` (any): フレックスメッセージの内容。メッセージのレイアウトとコンポーネントを定義するJSONオブジェクト。 + - `message.contents.type` (enum): コンテナのタイプ。'bubble'は単一コンテナ、'carousel'は複数のスワイプ可能なバブルを示す。 +3. **broadcast_text_message** + - LINE公式アカウントと友だちになっているすべてのユーザーに、LINEでシンプルなテキストメッセージを送信する。 + - **入力:** + - `message.text` (string): ユーザーに送信するテキスト。 +4. **broadcast_flex_message** + - LINE公式アカウントと友だちになっているすべてのユーザーに、LINEで高度にカスタマイズ可能なフレックスメッセージを送信する。 - **入力:** - - `user_id` (string): メッセージ受信者のユーザーID。デフォルトはDESTINATION_USER_ID。 - `message.altText` (string): フレックスメッセージが表示できない場合に表示される代替テキスト。 - - `message.content` (any): フレックスメッセージの内容。メッセージのレイアウトとコンポーネントを定義するJSONオブジェクト。 + - `message.contents` (any): フレックスメッセージの内容。メッセージのレイアウトとコンポーネントを定義するJSONオブジェクト。 - `message.contents.type` (enum): コンテナのタイプ。'bubble'は単一コンテナ、'carousel'は複数のスワイプ可能なバブルを示す。 -3. **get_profile** +5. **get_profile** - LINEユーザーの詳細なプロフィール情報を取得する。表示名、プロフィール画像URL、ステータスメッセージ、言語を取得できる。 - - **Inputs:** - - `user_id` (string): プロフィールを取得したいユーザーのユーザーID。デフォルトはDESTINATION_USER_ID。 - -## インストール - -### Step 1: line-bot-mcp-serverをインストール + - **入力:** + - `userId` (string?): プロフィールを取得したいユーザーのユーザーID。デフォルトはDESTINATION_USER_ID。`userId`または`DESTINATION_USER_ID`のどちらか一方は必ず設定する必要があります。 +6. **get_message_quota** + - LINE公式アカウントのメッセージ容量と消費量を取得します。月間メッセージ制限と現在の使用量が表示されます。 + - **入力:** + - なし +7. **get_rich_menu_list** + - LINE公式アカウントに登録されているリッチメニューの一覧を取得する。 + - **入力:** + - なし +8. **get_rich_menu** + - IDを指定してリッチメニューを取得する。サイズ、チャットバーのテキスト、タップ領域を含む。 + - **入力:** + - `richMenuId` (string): 取得するリッチメニューのID。 +9. **delete_rich_menu** + - LINE公式アカウントからリッチメニューを削除する。 + - **入力:** + - `richMenuId` (string): 削除するリッチメニューのID。 +10. **set_rich_menu_default** + - リッチメニューをデフォルトとして設定する。 + - **入力:** + - `richMenuId` (string): デフォルトとして設定するリッチメニューのID。 +11. **cancel_rich_menu_default** + - デフォルトのリッチメニューを解除する。 + - **入力:** + - なし +12. **create_rich_menu** + - 指定されたアクションに基づいてリッチメニューを作成。画像を生成してアップロード。デフォルトとして設定する。 + - **入力:** + - `chatBarText` (string): チャットバー表示、リッチメニュー名にされるテキスト。 + - `actions` (array): リッチメニューのアクション。最小1つから最大6つのアクションを指定できる。各アクションは以下のいずれかのタイプを指定できる: + - `postback`: ポストバックアクションを送信する + - `message`: テキストメッセージを送信する + - `uri`: URLを開く + - `datetimepicker`: 日付/時間選択ツールを開く + - `camera`: カメラを開く + - `cameraRoll`: カメラロールを開く + - `location`: 現在位置を送信する + - `richmenuswitch`: 別のリッチメニューに切り替える + - `clipboard`: テキストをクリップボードにコピーする + +13. **update_rich_menu_image** + - 既存リッチメニューの画像を差し替える。LINEでは一度アップロードしたリッチメニュー画像の上書きができないため、clone-and-replace方式を採用する: 同一定義の新しいリッチメニューを作成し、新しい画像をアップロードし、旧メニューがデフォルトの場合はデフォルトを引き継ぎ、旧メニューを削除する。この結果、**新しい`richMenuId`が発行され**、旧`richMenuId`は無効になる。 + - **入力:** + - `richMenuId` (string): 画像を更新するリッチメニューのID。 + - `imagePath` (string): 新しい画像のローカルファイルパス。PNGまたはJPEG、幅800-2500px、高さ250px以上、アスペクト比1.45以上、最大1MB。寸法は既存のリッチメニューサイズ(例: 1600x910)と一致する必要がある。 + - `deleteOldRichMenu` (boolean?): 差し替え後に旧リッチメニューを削除するかどうか。デフォルトはtrue。 + +14. **get_follower_ids** + - LINE公式アカウントを友だち追加しているユーザーのユーザーIDリストを取得する。これにより、DESTINATION_USER_IDを手動で準備せずにユーザーIDを取得できる。 + - **入力:** + - `start` (string?): 次のユーザーID配列を取得するための継続トークン。前回のレスポンスの`next`プロパティから取得できる。 + - `limit` (number?): 1回のリクエストで取得するユーザーIDの最大数。 + +## インストール (npxを使用) 要件: -- Node.js v20 以上 +- Node.js v22 以上 -このリポジトリをクローンします: +### Step 1: LINE公式アカウントを作成 -``` -git clone git@github.com:line/line-bot-mcp-server.git -``` - -Node.jsを利用する場合は、必要な依存関係をインストールし、line-bot-mcp-serverをビルドします。Dockerを利用する場合は不要です。: - -``` -cd line-bot-mcp-server && npm install && npm run build -``` - -### Step 2: チャンネルアクセストークンを取得 +このMCP ServerはLINE公式アカウントを利用しています。公式アカウントをお持ちでない場合は、[こちらの手順](https://developers.line.biz/ja/docs/messaging-api/getting-started/#create-oa)に従って作成してください。 -このMCP ServerはLINE公式アカウントを利用しています。公式アカウントをお持ちでない場合は、[こちらの手順](https://www.linebiz.com/jp-en/manual/OfficialAccountManager/new_account/)に従って作成してください。 +LINE公式アカウントをお持ちであれば、[こちらの手順](https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-oa-manager)に従ってMessaging APIを有効にしてください。 -Messaging APIに接続するには、チャンネルアクセストークンが必要です。これを確認するには、[こちらの手順](https://developers.line.biz/en/docs/basics/channel-access-token/#long-lived-channel-access-token)に従ってください。 - -加えて、メッセージの受信者のユーザーIDも必要です。これを確認するには、[こちらの手順](https://developers.line.biz/en/docs/messaging-api/getting-user-ids/#get-own-user-id)に従ってください。 - - -### Step 3: AI Agentを設定 +### Step 2: AI Agentを設定 Claude DesktopやClaudeなどのAI Agentに次の設定を追加してください。 -`CHANNEL_ACCESS_TOKEN`と`DESTINATION_USER_ID`には、先ほど取得したチャンネルアクセストークンとユーザーIDをそれぞれ挿入してください。 -加えて、`mcpServers.args`にある`line-bot-mcp-server`へのパスを更新してください。 -#### Option 1: Node.jsを利用する場合 +環境変数や引数は次のように設定してください: + +- `CHANNEL_ACCESS_TOKEN`: (必須) チャネルアクセストークン。これを取得するには、[こちらの手順](https://developers.line.biz/ja/docs/basics/channel-access-token/#long-lived-channel-access-token)に従ってください。 +- `DESTINATION_USER_ID`: (オプション) デフォルトのメッセージ受信者のユーザーID。Toolの入力に`userId`が含まれていない場合、`DESTINATION_USER_ID`は必ず設定する必要があります。これを確認するには、[こちらの手順](https://developers.line.biz/ja/docs/messaging-api/getting-user-ids/#get-own-user-id)に従ってください。 ```json { "mcpServers": { "line-bot": { - "command": "node", + "command": "npx", "args": [ - "PATH/TO/line-bot-mcp-server/dist/index.js" + "@line/line-bot-mcp-server" ], "env": { + "NPM_CONFIG_IGNORE_SCRIPTS": "true", "CHANNEL_ACCESS_TOKEN" : "FILL_HERE", "DESTINATION_USER_ID" : "FILL_HERE" } @@ -79,14 +127,36 @@ Claude DesktopやClaudeなどのAI Agentに次の設定を追加してくださ } ``` -#### Option 2: Dockerを利用する場合 +## インストール (Dockerを使用) + +### Step 1: LINE公式アカウントを作成 + +このMCP ServerはLINE公式アカウントを利用しています。公式アカウントをお持ちでない場合は、[こちらの手順](https://developers.line.biz/ja/docs/messaging-api/getting-started/#create-oa)に従って作成してください。 -まずDockerイメージをビルドします: +LINE公式アカウントをお持ちであれば、[こちらの手順](https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-oa-manager)に従ってMessaging APIを有効にしてください。 + +### Step 2: line-bot-mcp-serverをインストール + +このリポジトリをクローンします: + +``` +git clone git@github.com:line/line-bot-mcp-server.git +``` + +Dockerイメージをビルドします: ``` docker build -t line/line-bot-mcp-server . ``` -次のように設定します: +### Step 3: AI Agentを設定 + +Claude DesktopやClaudeなどのAI Agentに次の設定を追加してください。 + +環境変数や引数は次のように設定してください: + +- `mcpServers.args`: (必須) `line-bot-mcp-server`へのパス。 +- `CHANNEL_ACCESS_TOKEN`: (必須) チャネルアクセストークン。これを取得するには、[こちらの手順](https://developers.line.biz/ja/docs/basics/channel-access-token/#long-lived-channel-access-token)に従ってください。 +- `DESTINATION_USER_ID`: (オプション) デフォルトのメッセージ受信者のユーザーID。Toolの入力に`userId`が含まれていない場合、`DESTINATION_USER_ID`は必ず設定する必要があります。これを確認するには、[こちらの手順](https://developers.line.biz/ja/docs/messaging-api/getting-user-ids/#get-own-user-id)に従ってください。 ```json { @@ -111,3 +181,37 @@ docker build -t line/line-bot-mcp-server . } } ``` + +## Inspector を使用したローカル開発 + +MCP Inspector を使用して、サーバーをローカルでテストおよびデバッグできます。 + +### 前提条件 + +1. リポジトリをクローンする: +```bash +git clone git@github.com:line/line-bot-mcp-server.git +cd line-bot-mcp-server +``` + +2. 依存関係をインストールする: +```bash +npm install +``` + +3. プロジェクトをビルドする: +```bash +npm run build +``` + +### Inspector の実行 + +プロジェクトをビルドした後、MCP Inspector を起動できます: + +```bash +npx @modelcontextprotocol/inspector node dist/index.js \ + -e CHANNEL_ACCESS_TOKEN="YOUR_CHANNEL_ACCESS_TOKEN" \ + -e DESTINATION_USER_ID="YOUR_DESTINATION_USER_ID" +``` + +これにより、MCP Inspector インターフェースが起動し、LINE Bot MCP Server のツールを操作して機能をテストできます。 diff --git a/README.md b/README.md index e4e5a613..696919f9 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ # LINE Bot MCP Server +[![npmjs](https://badge.fury.io/js/%40line%2Fline-bot-mcp-server.svg)](https://www.npmjs.com/package/@line/line-bot-mcp-server) + [Model Context Protocol (MCP)](https://github.com/modelcontextprotocol) server implementation that integrates the LINE Messaging API to connect an AI Agent to the LINE Official Account. ![](/assets/demo.png) @@ -12,67 +14,113 @@ ## Tools 1. **push_text_message** - - Push a simple text message to user via LINE. + - Push a simple text message to a user via LINE. - **Inputs:** - - `user_id` (string): The user ID to receive a message. Defaults to DESTINATION_USER_ID. + - `userId` (string?): The user ID to receive a message. Defaults to DESTINATION_USER_ID. Either `userId` or `DESTINATION_USER_ID` must be set. - `message.text` (string): The plain text content to send to the user. 2. **push_flex_message** - - Push a highly customizable flex message to user via LINE. Supports both bubble (single container) and carousel (multiple swipeable bubbles) layouts. + - Push a highly customizable flex message to a user via LINE. + - **Inputs:** + - `userId` (string?): The user ID to receive a message. Defaults to DESTINATION_USER_ID. Either `userId` or `DESTINATION_USER_ID` must be set. + - `message.altText` (string): Alternative text shown when flex message cannot be displayed. + - `message.contents` (any): The contents of the flex message. This is a JSON object that defines the layout and components of the message. + - `message.contents.type` (enum): Type of the container. 'bubble' for single container, 'carousel' for multiple swipeable bubbles. +3. **broadcast_text_message** + - Broadcast a simple text message via LINE to all users who have followed your LINE Official Account. + - **Inputs:** + - `message.text` (string): The plain text content to send to the users. +4. **broadcast_flex_message** + - Broadcast a highly customizable flex message via LINE to all users who have added your LINE Official Account. - **Inputs:** - - `user_id` (string): The user ID to receive a message. Defaults to DESTINATION_USER_ID. - `message.altText` (string): Alternative text shown when flex message cannot be displayed. - - `message.content` (any): The content of the flex message. This is a JSON object that defines the layout and components of the message. + - `message.contents` (any): The contents of the flex message. This is a JSON object that defines the layout and components of the message. - `message.contents.type` (enum): Type of the container. 'bubble' for single container, 'carousel' for multiple swipeable bubbles. -3. **get_profile** +5. **get_profile** - Get detailed profile information of a LINE user including display name, profile picture URL, status message and language. - **Inputs:** - - `user_id` (string): The ID of the user whose profile you want to retrieve. Defaults to DESTINATION_USER_ID. - - -## Installation - -### Step 1: Install line-bot-mcp-server + - `userId` (string?): The ID of the user whose profile you want to retrieve. Defaults to DESTINATION_USER_ID. +6. **get_message_quota** + - Get the message quota and consumption of the LINE Official Account. This shows the monthly message limit and current usage. + - **Inputs:** + - None +7. **get_rich_menu_list** + - Get the list of rich menus associated with your LINE Official Account. + - **Inputs:** + - None +8. **get_rich_menu** + - Get a rich menu by ID, including its size, chat bar text, and tap areas. + - **Inputs:** + - `richMenuId` (string): The ID of the rich menu to get. +9. **delete_rich_menu** + - Delete a rich menu from your LINE Official Account. + - **Inputs:** + - `richMenuId` (string): The ID of the rich menu to delete. +10. **set_rich_menu_default** + - Set a rich menu as the default rich menu. + - **Inputs:** + - `richMenuId` (string): The ID of the rich menu to set as default. +11. **cancel_rich_menu_default** + - Cancel the default rich menu. + - **Inputs:** + - None +12. **create_rich_menu** + - Create a rich menu based on the given actions. Generate and upload an image. Set as default. + - **Inputs:** + - `chatBarText` (string): Text displayed in chat bar, also used as rich menu name. + - `actions` (array): The actions of the rich menu. You can specify minimum 1 to maximum 6 actions. Each action can be one of the following types: + - `postback`: For sending a postback action + - `message`: For sending a text message + - `uri`: For opening a URL + - `datetimepicker`: For opening a date/time picker + - `camera`: For opening the camera + - `cameraRoll`: For opening the camera roll + - `location`: For sending the current location + - `richmenuswitch`: For switching to another rich menu + - `clipboard`: For copying text to clipboard + +13. **update_rich_menu_image** + - Replace the image of an existing rich menu. LINE does not allow overwriting an already uploaded rich menu image, so this tool uses a clone-and-replace strategy: it creates a new rich menu with the same definition, uploads the new image, takes over the default assignment if the old menu was the default, and deletes the old menu. As a result, a **new `richMenuId` is issued** and the old one is no longer valid. + - **Inputs:** + - `richMenuId` (string): The ID of the rich menu whose image to update. + - `imagePath` (string): Local file path to the new image. PNG or JPEG, width 800-2500px, height >= 250px, aspect ratio >= 1.45, max 1MB. Dimensions must match the existing rich menu size (e.g. 1600x910). + - `deleteOldRichMenu` (boolean?): Whether to delete the old rich menu after replacement. Defaults to true. + +14. **get_follower_ids** + - Get a list of user IDs of users who have added the LINE Official Account as a friend. This allows you to obtain user IDs for sending messages without manually preparing them. + - **Inputs:** + - `start` (string?): Continuation token to get the next array of user IDs. Returned in the `next` property of a previous response. + - `limit` (number?): The maximum number of user IDs to retrieve in a single request. + +## Installation (Using npx) requirements: -- Node.js v20 or later - -Clone this repository: - -``` -git clone git@github.com:line/line-bot-mcp-server.git -``` - -Install the necessary dependencies and build line-bot-mcp-server when using Node.js. This step is not required when using Docker: - -``` -cd line-bot-mcp-server && npm install && npm run build -``` +- Node.js v22 or later -### Step 2: Get a channel access token +### Step 1: Create LINE Official Account -This MCP server utilizes a LINE Official Account. If you do not have one, please create it by following [this instructions](https://www.linebiz.com/jp-en/manual/OfficialAccountManager/new_account/). +This MCP server utilizes a LINE Official Account. If you do not have one, please create it by following [this instructions](https://developers.line.biz/en/docs/messaging-api/getting-started/#create-oa). -To connect to the Messaging API, you need to have a channel access token. You can confirm this by following [this instructions](https://developers.line.biz/en/docs/basics/channel-access-token/#long-lived-channel-access-token). +If you have a LINE Official Account, enable the Messaging API for your LINE Official Account by following [this instructions](https://developers.line.biz/en/docs/messaging-api/getting-started/#using-oa-manager). -Additionally, you will need the user ID of the recipient user for messages. You can confirm this by following [this instructions](https://developers.line.biz/en/docs/messaging-api/getting-user-ids/#get-own-user-id). - -### Step 3: Configure AI Agent +### Step 2: Configure AI Agent Please add the following configuration for an AI Agent like Claude Desktop or Cline. -Insert the channel access token and user ID you obtained earlier into `CHANNEL_ACCESS_TOKEN` and `DESTINATION_USER_ID`, respectively. -Additionally, update the path to `line-bot-mcp-server` in `mcpServers.args`. -#### Option 1: Use Node +Set the environment variables or arguments as follows: + +- `CHANNEL_ACCESS_TOKEN`: (required) Channel Access Token. You can confirm this by following [this instructions](https://developers.line.biz/en/docs/basics/channel-access-token/#long-lived-channel-access-token). +- `DESTINATION_USER_ID`: (optional) The default user ID of the recipient. If the Tool's input does not include `userId`, `DESTINATION_USER_ID` is required. You can confirm this by following [this instructions](https://developers.line.biz/en/docs/messaging-api/getting-user-ids/#get-own-user-id). ```json { "mcpServers": { "line-bot": { - "command": "node", + "command": "npx", "args": [ - "PATH/TO/line-bot-mcp-server/dist/index.js" + "@line/line-bot-mcp-server" ], "env": { + "NPM_CONFIG_IGNORE_SCRIPTS": "true", "CHANNEL_ACCESS_TOKEN" : "FILL_HERE", "DESTINATION_USER_ID" : "FILL_HERE" } @@ -81,13 +129,41 @@ Additionally, update the path to `line-bot-mcp-server` in `mcpServers.args`. } ``` -#### Option 2: Use Docker +## Installation (Using Docker) + +### Step 1: Create LINE Official Account + +This MCP server utilizes a LINE Official Account. If you do not have one, please create it by following [this instructions](https://developers.line.biz/en/docs/messaging-api/getting-started/#create-oa). + +If you have a LINE Official Account, enable the Messaging API for your LINE Official Account by following [this instructions](https://developers.line.biz/en/docs/messaging-api/getting-started/#using-oa-manager). + + +### Step 2: Build line-bot-mcp-server image + +Clone this repository: + +``` +git clone git@github.com:line/line-bot-mcp-server.git +``` + +Build the Docker image: -Build the Docker image first: ``` docker build -t line/line-bot-mcp-server . ``` +### Step 3: Configure AI Agent + +Please add the following configuration for an AI Agent like Claude Desktop or Cline. + +Set the environment variables or arguments as follows: + +- `mcpServers.args`: (required) The path to `line-bot-mcp-server`. +- `CHANNEL_ACCESS_TOKEN`: (required) Channel Access Token. You can confirm this by following [this instructions](https://developers.line.biz/en/docs/basics/channel-access-token/#long-lived-channel-access-token). +- `DESTINATION_USER_ID`: (optional) The default user ID of the recipient. If the Tool's input does not include `userId`, `DESTINATION_USER_ID` is required. +You can confirm this by following [this instructions](https://developers.line.biz/en/docs/messaging-api/getting-user-ids/#get-own-user-id). + + ```json { "mcpServers": { @@ -111,3 +187,47 @@ docker build -t line/line-bot-mcp-server . } } ``` + +## Local Development with Inspector + +You can use the MCP Inspector to test and debug the server locally. + +### Prerequisites + +1. Clone the repository: +```bash +git clone git@github.com:line/line-bot-mcp-server.git +cd line-bot-mcp-server +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Build the project: +```bash +npm run build +``` + +### Run the Inspector + +After building the project, you can start the MCP Inspector: + +```bash +npx @modelcontextprotocol/inspector node dist/index.js \ + -e CHANNEL_ACCESS_TOKEN="YOUR_CHANNEL_ACCESS_TOKEN" \ + -e DESTINATION_USER_ID="YOUR_DESTINATION_USER_ID" +``` + +This will start the MCP Inspector interface where you can interact with the LINE Bot MCP Server tools and test their functionality. + +## Versioning + +This project respects semantic versioning + +See http://semver.org/ + +## Contributing + +Please check [CONTRIBUTING](./CONTRIBUTING.md) before making a contribution. diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 00000000..a076ba98 Binary files /dev/null and b/assets/icon.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 00000000..1c14e3bb --- /dev/null +++ b/manifest.json @@ -0,0 +1,109 @@ +{ + "dxt_version": "0.1", + "name": "line-bot-mcp-server", + "display_name": "LINE Official Account", + "icon": "assets/icon.png", + "version": "0.3.0", + "description": "MCP server that bridges AI agents with LINE Messaging API for automated interactions", + "long_description": "This extension enables Claude Desktop to interact with LINE Messaging API, providing tools for sending messages, managing rich menus, and retrieving user profiles through LINE Official Accounts.", + "author": { + "name": "LY Corporation", + "url": "https://line.me" + }, + "server": { + "type": "node", + "entry_point": "dist/index.js", + "mcp_config": { + "command": "node", + "args": [ + "${__dirname}/dist/index.js" + ], + "env": { + "CHANNEL_ACCESS_TOKEN": "${user_config.channel_access_token}", + "DESTINATION_USER_ID": "${user_config.destination_user_id}" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/line/line-bot-mcp-server.git" + }, + "homepage": "https://github.com/line/line-bot-mcp-server", + "documentation": "https://github.com/line/line-bot-mcp-server#readme", + "support": "https://github.com/line/line-bot-mcp-server/issues", + "tools": [ + { + "name": "push_text_message", + "description": "Send a plain text message to a specific LINE user" + }, + { + "name": "push_flex_message", + "description": "Send a rich, customizable flex message to a specific LINE user" + }, + { + "name": "broadcast_text_message", + "description": "Send a plain text message to all followers" + }, + { + "name": "broadcast_flex_message", + "description": "Send a flex message to all followers" + }, + { + "name": "get_profile", + "description": "Retrieve LINE user profile information" + }, + { + "name": "get_message_quota", + "description": "Check monthly message limits and usage" + }, + { + "name": "get_rich_menu_list", + "description": "List all rich menus for the LINE Official Account" + }, + { + "name": "delete_rich_menu", + "description": "Remove a specific rich menu" + }, + { + "name": "set_rich_menu_default", + "description": "Set a default rich menu for all users" + }, + { + "name": "cancel_rich_menu_default", + "description": "Remove the default rich menu" + }, + { + "name": "create_rich_menu", + "description": "Create a rich menu from actions, upload its image, and set it as default" + }, + { + "name": "get_follower_ids", + "description": "Get a list of user IDs of followers" + } + ], + "keywords": [ + "line", + "messaging", + "bot", + "mcp", + "ai", + "automation", + "chat" + ], + "license": "Apache-2.0", + "user_config": { + "channel_access_token": { + "type": "string", + "title": "Channel Access Token", + "description": "LINE channel access token for authentication. See https://developers.line.biz/en/docs/basics/channel-access-token/#long-lived-channel-access-token", + "required": true, + "sensitive": true + }, + "destination_user_id": { + "type": "string", + "title": "Destination User ID", + "description": "Default recipient user ID for LINE messages. See https://developers.line.biz/en/docs/messaging-api/getting-user-ids/#get-own-user-id", + "required": false + } + } +} diff --git a/package-lock.json b/package-lock.json index d337b18b..d5f6f302 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,185 +1,336 @@ { "name": "@line/line-bot-mcp-server", - "version": "0.0.1", + "version": "0.0.1-local", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@line/line-bot-mcp-server", - "version": "0.0.1", + "version": "0.0.1-local", "license": "Apache-2.0", "dependencies": { - "@line/bot-sdk": "^9.8.0", - "@modelcontextprotocol/sdk": "^1.8.0" + "@line/bot-sdk": "^11.0.1", + "@marp-team/marp-core": "^4.1.0", + "@modelcontextprotocol/sdk": "^1.8.0", + "puppeteer": "^25.0.0", + "zod": "^4.0.0" + }, + "bin": { + "line-bot-mcp-server": "dist/index.js" }, "devDependencies": { - "@types/node": "^22", - "prettier": "3.5.3", - "shx": "^0.4.0", - "tsx": "^4.19.3", - "typescript": "^5.6.2" + "@types/node": "^24.9.2", + "prettier": "^3.8.3", + "publint": "^0.3.21", + "typescript": "^6.0.2", + "vitest": "^4.0.0" }, "engines": { - "node": ">=20" + "node": ">=22" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", + "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" + "node_modules/@csstools/selector-resolve-nested": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], + "license": "MIT-0", "engines": { "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", - "cpu": [ - "arm64" + "node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", - "cpu": [ - "arm64" - ], + "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, - "os": [ - "darwin" - ], + "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" + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", - "cpu": [ - "x64" - ], + "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/@line/bot-sdk": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@line/bot-sdk/-/bot-sdk-11.0.2.tgz", + "integrity": "sha512-Y2PCEIlKw5ytu56HhrZt0Wfb8ZxBjrpKTlsmaZ4nWkbHKV2F0T7ZpwFfT7FqXryyqOrl8+RdvFYBBXE4B464SA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^24.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@marp-team/marp-core": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@marp-team/marp-core/-/marp-core-4.3.0.tgz", + "integrity": "sha512-Qwd0wsHS+7bdCBmmnxCU2OKclCSdsVu4U474zSs44Y2SMXr6d9wBYAMD5IXI4dbmaB57Ez5fdPUxPZZsFo8Tmw==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "@marp-team/marpit": "^3.2.1", + "@marp-team/marpit-svg-polyfill": "^2.1.0", + "highlight.js": "^11.11.1", + "katex": "^0.16.33", + "mathjax-full": "^3.2.2", + "postcss-selector-parser": "^7.1.1", + "xss": "^1.0.15" + }, "engines": { "node": ">=18" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@marp-team/marpit": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@marp-team/marpit/-/marpit-3.2.1.tgz", + "integrity": "sha512-MflN1movsh8PXJbM9otjJOtlcB0vSrvw5PgXs2PDguVYVzKQmGpiy9QHHKq0rryrgaSp376TdJWBP7mXHHAG8w==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "@csstools/postcss-is-pseudo-class": "^5.0.3", + "cssesc": "^3.0.0", + "js-yaml": "^4.1.1", + "lodash.kebabcase": "^4.1.1", + "markdown-it": "^14.1.1", + "markdown-it-front-matter": "^0.2.4", + "postcss": "^8.5.6", + "postcss-nesting": "^13.0.2" + }, "engines": { "node": ">=18" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@marp-team/marpit-svg-polyfill": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@marp-team/marpit-svg-polyfill/-/marpit-svg-polyfill-2.1.0.tgz", + "integrity": "sha512-VqCoAKwv1HJdzZp36dDPxznz2JZgRjkVSSPHpCzk72G2N753F0HPKXjevdjxmzN6gir9bUGBgMD1SguWJIi11A==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@marp-team/marpit": ">=0.5.0" + }, + "peerDependenciesMeta": { + "@marp-team/marpit": { + "optional": true + } + } + }, + "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/@esbuild/linux-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", - "cpu": [ - "arm" - ], + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "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/@publint/pack": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.5.tgz", + "integrity": "sha512-edgyN2pP07uXiP4tJs0s8KVmU8M8i60YPbbI0/WDeok1mIJHRXz+CgD8I0nelwDkoCh3EWL/G5kGfbuHjsdbvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyexec": "^1.2.4" + }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.5.tgz", + "integrity": "sha512-xYXNuEQmHNIPWWcbL/skf2KF7seyp7c1xmKFRk3wmdFx7VwBsKVrtOLKs8ecaezsKPsWeF1YsgwIiElAscaryA==", + "license": "Apache-2.0", + "dependencies": { + "modern-tar": "^0.7.6", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + } } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "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" ], @@ -187,69 +338,69 @@ "license": "MIT", "optional": true, "os": [ - "linux" + "android" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "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": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "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": [ - "loong64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "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": [ - "mips64el" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "freebsd" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "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": [ - "ppc64" + "arm" ], "dev": true, "license": "MIT", @@ -258,149 +409,169 @@ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "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": [ - "riscv64" + "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "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": [ - "s390x" + "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "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": [ - "x64" + "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "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": [ - "arm64" + "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "netbsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "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, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "netbsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "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": [ - "arm64" + "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ - "openbsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "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": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "openharmony" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "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": [ - "x64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "sunos" - ], + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "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" ], @@ -411,15 +582,15 @@ "win32" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "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": [ - "ia32" + "x64" ], "dev": true, "license": "MIT", @@ -428,107 +599,188 @@ "win32" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", - "cpu": [ - "x64" - ], + "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", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "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/@line/bot-sdk": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/@line/bot-sdk/-/bot-sdk-9.8.0.tgz", - "integrity": "sha512-vw4TTtkw5zwKu/FhATnm5n2sCcoKjcHEgN16Oq5NyXwGEzIcyh6RwGrYnX4SisiPO1lklAIba1DuDf9hEYZ4mg==", - "license": "Apache-2.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/node": "^22.0.0" - }, - "engines": { - "node": ">=18" + "@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/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, - "optionalDependencies": { - "axios": "^1.7.4" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.9.0.tgz", - "integrity": "sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==", + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, "license": "MIT", "dependencies": { - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.3", - "eventsource": "^3.0.2", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.23.8", - "zod-to-json-schema": "^3.24.1" + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, - "engines": { - "node": ">=18" + "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/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">= 8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 8" + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, - "engines": { - "node": ">= 8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "license": "MIT", + "engines": { + "node": ">=14.6" } }, "node_modules/accepts": { @@ -544,56 +796,114 @@ "node": ">= 0.6" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "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", - "optional": true + "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/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "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", - "optional": true, "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "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/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", - "debug": "^4.4.0", - "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, + "node_modules/body-parser/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", - "dependencies": { - "fill-range": "^7.1.1" - }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bytes": { @@ -634,29 +944,75 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "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", - "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/chromium-bidi": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz", + "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==", + "license": "Apache-2.0", "dependencies": { - "delayed-stream": "~1.0.0" + "mitt": "^3.0.1", + "zod": "^3.24.1" }, "engines": { - "node": ">= 0.8" + "node": ">=20.19.0 <22.0.0 || >=22.12.0" + }, + "peerDependencies": { + "devtools-protocol": "*" } }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", "dependencies": { - "safe-buffer": "5.2.1" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=20" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "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": { @@ -668,6 +1024,13 @@ "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", @@ -687,9 +1050,9 @@ } }, "node_modules/cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "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", @@ -697,6 +1060,10 @@ }, "engines": { "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/cross-spawn": { @@ -713,10 +1080,28 @@ "node": ">= 8" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT" + }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "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" @@ -730,16 +1115,6 @@ } } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -749,6 +1124,22 @@ "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/devtools-protocol": { + "version": "0.0.1638949", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1638949.tgz", + "integrity": "sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA==", + "license": "BSD-3-Clause" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -769,6 +1160,12 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -778,14 +1175,16 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/es-define-property": { @@ -806,10 +1205,17 @@ "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.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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" @@ -818,61 +1224,13 @@ "node": ">= 0.4" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "optional": true, - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", - "dev": true, - "hasInstallScript": true, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.2", - "@esbuild/android-arm": "0.25.2", - "@esbuild/android-arm64": "0.25.2", - "@esbuild/android-x64": "0.25.2", - "@esbuild/darwin-arm64": "0.25.2", - "@esbuild/darwin-x64": "0.25.2", - "@esbuild/freebsd-arm64": "0.25.2", - "@esbuild/freebsd-x64": "0.25.2", - "@esbuild/linux-arm": "0.25.2", - "@esbuild/linux-arm64": "0.25.2", - "@esbuild/linux-ia32": "0.25.2", - "@esbuild/linux-loong64": "0.25.2", - "@esbuild/linux-mips64el": "0.25.2", - "@esbuild/linux-ppc64": "0.25.2", - "@esbuild/linux-riscv64": "0.25.2", - "@esbuild/linux-s390x": "0.25.2", - "@esbuild/linux-x64": "0.25.2", - "@esbuild/netbsd-arm64": "0.25.2", - "@esbuild/netbsd-x64": "0.25.2", - "@esbuild/openbsd-arm64": "0.25.2", - "@esbuild/openbsd-x64": "0.25.2", - "@esbuild/sunos-x64": "0.25.2", - "@esbuild/win32-arm64": "0.25.2", - "@esbuild/win32-ia32": "0.25.2", - "@esbuild/win32-x64": "0.25.2" + "node": ">=6" } }, "node_modules/escape-html": { @@ -881,6 +1239,25 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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", @@ -891,9 +1268,9 @@ } }, "node_modules/eventsource": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", - "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", + "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" @@ -903,109 +1280,38 @@ } }, "node_modules/eventsource-parser": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", - "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "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/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "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": "MIT", - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", - "dev": true, - "license": "MIT", - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/execa/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/execa/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "node": ">=12.0.0" } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.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", @@ -1036,10 +1342,13 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", - "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "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" }, @@ -1047,53 +1356,53 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": "^4.11 || 5 || ^5.0.0-beta.1" + "express": ">= 4.11" } }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } + "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/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } + "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/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "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", - "dependencies": { - "to-regex-range": "^5.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "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", @@ -1104,67 +1413,11 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "license": "MIT", - "optional": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "optional": true, - "dependencies": { - "mime-db": "1.52.0" + "node": ">= 18.0.0" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/forwarded": { @@ -1209,6 +1462,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1246,45 +1520,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/get-tsconfig": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -1309,60 +1544,70 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "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", - "optional": true, "dependencies": { - "has-symbols": "^1.0.3" + "function-bind": "^1.1.2" }, "engines": { "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hono": { + "version": "4.12.26", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz", + "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==", "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, "engines": { - "node": ">= 0.4" + "node": ">=16.9.0" } }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "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.1", - "toidentifier": "1.0.1" + "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.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "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": { @@ -1371,14 +1616,13 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "dev": true, + "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": ">= 0.10" + "node": ">= 12" } }, "node_modules/ipaddr.js": { @@ -1390,131 +1634,485 @@ "node": ">= 0.10" } }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "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/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "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/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "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", - "engines": { - "node": ">= 0.4" + "funding": { + "url": "https://github.com/sponsors/panva" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.8" + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "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==", + "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/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "commander": "^8.3.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "katex": "cli.js" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "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, + "libc": [ + "glibc" + ], + "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, + "libc": [ + "musl" + ], + "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, + "libc": [ + "glibc" + ], + "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, + "libc": [ + "musl" + ], + "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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "license": "MIT", "engines": { - "node": ">= 8" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/linkify-it": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", + "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "license": "MIT" + }, + "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": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-it": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", + "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.1", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-front-matter": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/markdown-it-front-matter/-/markdown-it-front-matter-0.2.4.tgz", + "integrity": "sha512-25GUs0yjS2hLl8zAemVndeEzThB1p42yxuDEKbd4JlL3jiz+jsm6e56Ya8B0VREOkNxLYB4TTwaoPJ3ElMmW+w==", + "license": "MIT" + }, + "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": ">=8.6" + "node": ">= 0.4" } }, + "node_modules/mathjax-full": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz", + "integrity": "sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==", + "deprecated": "Version 4 replaces this package with the scoped package @mathjax/src", + "license": "Apache-2.0", + "dependencies": { + "esm": "^3.2.25", + "mhchemparser": "^4.1.0", + "mj-context-menu": "^0.6.1", + "speech-rule-engine": "^4.0.6" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "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/mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", + "license": "Apache-2.0" + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -1525,70 +2123,83 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "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": ">= 0.6" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", + "node": ">=18" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "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==", + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, - "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==", + "node_modules/mj-context-menu": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", + "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", + "license": "Apache-2.0" + }, + "node_modules/modern-tar": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz", + "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18.0.0" } }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "path-key": "^2.0.0" + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=4" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, + "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": ">=4" + "node": ">= 0.6" } }, "node_modules/object-assign": { @@ -1612,6 +2223,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "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", @@ -1633,15 +2258,12 @@ "wrappy": "1" } }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } + "license": "MIT" }, "node_modules/parseurl": { "version": "1.3.3", @@ -1661,48 +2283,123 @@ "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, "node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "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", - "engines": { - "node": ">=16" + "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==", + "license": "ISC" + }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "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", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/pkce-challenge": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", - "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "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==", + "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/postcss-nesting": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-resolve-nested": "^3.1.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", "dev": true, "license": "MIT", "bin": { @@ -1728,28 +2425,79 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "node_modules/publint": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.21.tgz", + "integrity": "sha512-OqejcnMV6E9zel2oCrUOJEiiFkGiAAni0A6ibfQNh1k9Gu5z4F+Yso8lllam7AzmV6Do0vp7u3UpZNRBwuXaHQ==", + "dev": true, "license": "MIT", - "optional": true + "dependencies": { + "@publint/pack": "^0.1.4", + "package-manager-detector": "^1.6.0", + "picocolors": "^1.1.1", + "sade": "^1.8.1" + }, + "bin": { + "publint": "src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "dev": true, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.2.1.tgz", + "integrity": "sha512-2D5RMkQH9FRhDU57a1/jV9xWoxqZvUjaZOYjAAPdRCEY8A01V5sxzyGOMs8XiKU9fPF91SOSwNYpHRu5SD958g==", + "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "@puppeteer/browsers": "3.0.5", + "chromium-bidi": "16.0.1", + "devtools-protocol": "0.0.1638949", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.2.1", + "typed-query-selector": "^2.12.2" + }, + "bin": { + "puppeteer": "lib/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/puppeteer-core": { + "version": "25.2.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.2.1.tgz", + "integrity": "sha512-MwEZ4FFGJ1ZLOmu/04eISxoEMKtCnHyJBRFfgpwPPSYNG6gT6Xw1laNziFSV7uwDcx3jK+ATYIo9SfOd8Uhc3w==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "3.0.5", + "chromium-bidi": "16.0.1", + "devtools-protocol": "0.0.1638949", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.2", + "ws": "^8.21.0" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "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" @@ -1761,27 +2509,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -1792,72 +2519,61 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "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.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, - "node_modules/rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", - "dev": true, - "dependencies": { - "resolve": "^1.1.6" - }, + "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" + "node": ">=0.10.0" } }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "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": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "resolve": "bin/resolve" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || >=22.12.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.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": { @@ -1876,92 +2592,55 @@ "node": ">= 18" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], "license": "MIT", "dependencies": { - "queue-microtask": "^1.2.2" + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "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": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { - "debug": "^4.3.5", + "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.0", - "mime-types": "^3.0.1", + "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.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "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", @@ -1971,6 +2650,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { @@ -1982,69 +2665,33 @@ "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/shelljs": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.9.2.tgz", - "integrity": "sha512-S3I64fEiKgTZzKCC46zT/Ib9meqofLrQVbpSswtjFfAVDW+AZ54WTnAM/3/yENoxz/V1Cy6u3kiiEbQ4DNphvw==", - "dev": true, - "license": "BSD-3-Clause", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", "dependencies": { - "execa": "^1.0.0", - "fast-glob": "^3.3.2", - "interpret": "^1.0.0", - "rechoir": "^0.6.2" - }, - "bin": { - "shjs": "bin/shjs" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/shx": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/shx/-/shx-0.4.0.tgz", - "integrity": "sha512-Z0KixSIlGPpijKgcH6oCMCbltPImvaKy0sGH8AkLRXw1KyzpKtaCTizP2xen+hNDqVF4xxgvA0KXSb9o4Q6hnA==", - "dev": true, + "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", - "dependencies": { - "minimist": "^1.2.8", - "shelljs": "^0.9.2" - }, - "bin": { - "shx": "lib/cli.js" - }, "engines": { - "node": ">=18" + "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==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -2056,13 +2703,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "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.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -2108,56 +2755,142 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speech-rule-engine": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.1.4.tgz", + "integrity": "sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==", + "license": "Apache-2.0", + "dependencies": { + "@xmldom/xmldom": "0.9.10", + "commander": "13.1.0", + "wicked-good-xpath": "1.3.0" + }, + "bin": { + "sre": "bin/sre" + } + }, + "node_modules/speech-rule-engine/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "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.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "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/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "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/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, "engines": { - "node": ">= 0.4" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "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": { - "is-number": "^7.0.0" + "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": ">=8.0" + "node": ">=14.0.0" } }, "node_modules/toidentifier": { @@ -2169,44 +2902,55 @@ "node": ">=0.6" } }, - "node_modules/tsx": { - "version": "4.19.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", - "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "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": { - "esbuild": "~0.25.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">= 18" }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "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", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "license": "MIT" + }, "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2217,10 +2961,16 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/unpipe": { @@ -2232,6 +2982,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2241,6 +2997,180 @@ "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", + "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.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "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.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "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/webdriver-bidi-protocol": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", + "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", + "license": "Apache-2.0" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2256,28 +3186,146 @@ "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/wicked-good-xpath": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", + "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "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/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "license": "MIT", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xss/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "node_modules/zod": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-to-json-schema": { - "version": "3.24.5", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", - "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "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.24.1" + "zod": "^3.25.28 || ^4" } } } diff --git a/package.json b/package.json index 7340bbea..6f87f0a4 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,30 @@ { "name": "@line/line-bot-mcp-server", - "version": "0.0.1", + "version": "0.0.1-local", "description": "MCP server for interacting with your LINE Official Account", "type": "module", "engines": { - "node": ">=20" + "node": ">=22" }, "module": "./dist/index.js", + "bin": { + "line-bot-mcp-server": "./dist/index.js" + }, "files": [ - "dist" + "dist", + "richmenu-template" ], - "main": "src/index.ts", "scripts": { - "build": "tsc && shx chmod +x dist/*.js", - "prettier": "prettier \"src/**/*.ts\"", + "build": "npm run format:check && npm run typecheck:test && npm run clean && tsc && node scripts/chmod-bin.mjs", + "prettier": "prettier \"src/**/*.ts\" \"test/**/*.ts\"", "format": "npm run prettier -- --write", "format:check": "npm run prettier -- -l", - "clean": "rm -rf dist/*", - "prebuild": "npm run format:check && npm run clean" + "clean": "node scripts/clean.mjs", + "typecheck:test": "tsc --project tsconfig.test.json", + "release": "npm run build && npm publish --provenance --access public", + "check:publint": "publint", + "test": "npm run build && vitest run", + "test:watch": "vitest" }, "repository": { "type": "git", @@ -31,15 +38,18 @@ "homepage": "https://github.com/line/line-bot-mcp-server", "bugs": "https://github.com/line/line-bot-mcp-server/issues", "dependencies": { - "@line/bot-sdk": "^9.8.0", - "@modelcontextprotocol/sdk": "^1.8.0" + "@line/bot-sdk": "^11.0.1", + "@marp-team/marp-core": "^4.1.0", + "@modelcontextprotocol/sdk": "^1.8.0", + "puppeteer": "^25.0.0", + "zod": "^4.0.0" }, "devDependencies": { - "@types/node": "^22", - "shx": "^0.4.0", - "tsx": "^4.19.3", - "typescript": "^5.6.2", - "prettier": "3.5.3" + "@types/node": "^24.9.2", + "prettier": "^3.8.3", + "publint": "^0.3.21", + "typescript": "^6.0.2", + "vitest": "^4.0.0" }, "license": "Apache-2.0" } diff --git a/renovate.json5 b/renovate.json5 index a5a9133f..2cbee925 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -1,13 +1,13 @@ { $schema: "https://docs.renovatebot.com/renovate-schema.json", - extends: [ - "config:recommended", - "helpers:pinGitHubActionDigestsToSemver" - ], + extends: ["config:recommended", "helpers:pinGitHubActionDigestsToSemver"], timezone: "Asia/Tokyo", automerge: true, platformAutomerge: true, - labels: [ - "dependency upgrade" - ] + minimumReleaseAge: "7 days", + labels: ["dependency upgrade"], + lockFileMaintenance: { + enabled: true, + schedule: ["after 1am and before 4am"], + }, } diff --git a/richmenu-template/template-01.md b/richmenu-template/template-01.md new file mode 100644 index 00000000..2bb74b2e --- /dev/null +++ b/richmenu-template/template-01.md @@ -0,0 +1,46 @@ +--- +marp: true +size: 16:9 +--- + +
+
+

item01

+
+
diff --git a/richmenu-template/template-02.md b/richmenu-template/template-02.md new file mode 100644 index 00000000..3c28b819 --- /dev/null +++ b/richmenu-template/template-02.md @@ -0,0 +1,49 @@ +--- +marp: true +size: 16:9 +--- + +
+
+

item01

+
+
+

item02

+
+
diff --git a/richmenu-template/template-03.md b/richmenu-template/template-03.md new file mode 100644 index 00000000..17114872 --- /dev/null +++ b/richmenu-template/template-03.md @@ -0,0 +1,74 @@ +--- +marp: true +size: 16:9 +--- + +
+
+
+

item01

+
+
+
+
+

item02

+
+
+

item03

+
+
+
diff --git a/richmenu-template/template-04.md b/richmenu-template/template-04.md new file mode 100644 index 00000000..17c1ca9f --- /dev/null +++ b/richmenu-template/template-04.md @@ -0,0 +1,60 @@ +--- +marp: true +size: 16:9 +--- + +
+
+

item01

+
+
+

item02

+
+
+

item03

+
+
+

item04

+
+
diff --git a/richmenu-template/template-05.md b/richmenu-template/template-05.md new file mode 100644 index 00000000..c8bb3a01 --- /dev/null +++ b/richmenu-template/template-05.md @@ -0,0 +1,67 @@ +--- +marp: true +size: 16:9 +--- + +
+
+

item01

+
+
+

item02

+
+
+

item03

+
+
+

item04

+
+
+

item05

+
+
diff --git a/richmenu-template/template-06.md b/richmenu-template/template-06.md new file mode 100644 index 00000000..3b689bf2 --- /dev/null +++ b/richmenu-template/template-06.md @@ -0,0 +1,66 @@ +--- +marp: true +size: 16:9 +--- + +
+
+

item01

+
+
+

item02

+
+
+

item03

+
+
+

item04

+
+
+

item05

+
+
+

item06

+
+
diff --git a/scripts/chmod-bin.mjs b/scripts/chmod-bin.mjs new file mode 100644 index 00000000..f10eaf39 --- /dev/null +++ b/scripts/chmod-bin.mjs @@ -0,0 +1,5 @@ +import { chmodSync, statSync } from "node:fs"; + +const file = "dist/index.js"; + +chmodSync(file, (statSync(file).mode & 0o777) | 0o111); diff --git a/scripts/clean.mjs b/scripts/clean.mjs new file mode 100644 index 00000000..920bd39b --- /dev/null +++ b/scripts/clean.mjs @@ -0,0 +1,3 @@ +import { rmSync } from "node:fs"; + +rmSync("dist", { recursive: true, force: true }); diff --git a/src/common/response.ts b/src/common/response.ts new file mode 100644 index 00000000..b743957f --- /dev/null +++ b/src/common/response.ts @@ -0,0 +1,22 @@ +export const createErrorResponse = (message: string) => { + return { + isError: true, + content: [ + { + type: "text" as const, + text: message, + }, + ], + }; +}; + +export const createSuccessResponse = (response: object) => { + return { + content: [ + { + type: "text" as const, + text: JSON.stringify(response), + }, + ], + }; +}; diff --git a/src/common/schema/actionSchema.ts b/src/common/schema/actionSchema.ts new file mode 100644 index 00000000..8490a29b --- /dev/null +++ b/src/common/schema/actionSchema.ts @@ -0,0 +1,87 @@ +import { z } from "zod"; + +// references: +// * document: +// https://developers.line.biz/ja/docs/messaging-api/actions/ +// * line-bot-sdk-nodejs: +// https://github.com/line/line-bot-sdk-nodejs/blob/master/lib/messaging-api/model/action.ts + +// 1. Postback action +const postbackActionSchema = z.object({ + type: z.literal("postback"), + label: z.string(), + data: z.string(), + displayText: z.string().optional(), + inputOption: z + .enum(["closeRichMenu", "openRichMenu", "openKeyboard", "openVoice"]) + .optional(), + fillInText: z.string().optional(), +}); +// 2. Message action +const messageActionSchema = z.object({ + type: z.literal("message"), + label: z.string(), + text: z.string(), +}); +// 3. URI action +const uriActionSchema = z.object({ + type: z.literal("uri"), + label: z.string(), + uri: z.string(), + altUri: z + .object({ + desktop: z.string().optional(), + }) + .optional(), +}); +// 4. Datetime picker action +const datetimePickerActionSchema = z.object({ + type: z.literal("datetimepicker"), + label: z.string(), + data: z.string(), + mode: z.enum(["date", "time", "datetime"]), + initial: z.string().optional(), + max: z.string().optional(), + min: z.string().optional(), +}); +// 5. Camera action +const cameraActionSchema = z.object({ + type: z.literal("camera"), + label: z.string(), +}); +// 6. Camera roll action +const cameraRollActionSchema = z.object({ + type: z.literal("cameraRoll"), + label: z.string(), +}); +// 7. Location action +const locationActionSchema = z.object({ + type: z.literal("location"), + label: z.string(), +}); +// 8. Rich menu switch action +const richMenuSwitchActionSchema = z.object({ + type: z.literal("richmenuswitch"), + label: z.string(), + richMenuAliasId: z.string(), + data: z.string(), +}); +// 9. Clipboard action +const clipboardActionSchema = z.object({ + type: z.literal("clipboard"), + label: z.string(), + clipboardText: z.string(), +}); + +// actions +export const actionSchema = z.union([ + postbackActionSchema, + messageActionSchema, + uriActionSchema, + datetimePickerActionSchema, + cameraActionSchema, + cameraRollActionSchema, + locationActionSchema, + richMenuSwitchActionSchema, + clipboardActionSchema, +]); diff --git a/src/common/schema/constants.ts b/src/common/schema/constants.ts new file mode 100644 index 00000000..93f70668 --- /dev/null +++ b/src/common/schema/constants.ts @@ -0,0 +1,2 @@ +export const NO_USER_ID_ERROR = + "Error: Specify the userId or set the DESTINATION_USER_ID in the environment variables of this MCP Server."; diff --git a/src/common/schema/flexMessage.ts b/src/common/schema/flexMessage.ts new file mode 100644 index 00000000..b606ff93 --- /dev/null +++ b/src/common/schema/flexMessage.ts @@ -0,0 +1,383 @@ +import { z } from "zod"; + +const sizeSchema = z.enum([ + "xxs", + "xs", + "sm", + "md", + "lg", + "xl", + "xxl", + "3xl", + "4xl", + "5xl", +]); +const imageSizeSchema = z.enum([ + "xxs", + "xs", + "sm", + "md", + "lg", + "xl", + "xxl", + "3xl", + "4xl", + "5xl", + "full", +]); +const marginSchema = z.enum(["none", "xs", "sm", "md", "lg", "xl", "xxl"]); +const spacingSchema = z.enum(["none", "xs", "sm", "md", "lg", "xl", "xxl"]); +const positionSchema = z.enum(["relative", "absolute"]); +const alignSchema = z.enum(["start", "end", "center"]); +const gravitySchema = z.enum(["top", "bottom", "center"]); +const offsetSchema = z.string().regex(/^\d+px$/, "Format: '10px'"); +const colorSchema = z + .string() + .regex(/^#[0-9A-Fa-f]{6}$/, "Hex format: '#FF0000'"); +const flexWeightSchema = z.number(); +const scalingSchema = z.boolean(); + +const positionFields = { + position: positionSchema.optional(), + offsetTop: offsetSchema.optional(), + offsetBottom: offsetSchema.optional(), + offsetStart: offsetSchema.optional(), + offsetEnd: offsetSchema.optional(), +}; + +const layoutFields = { + flex: flexWeightSchema.optional(), + margin: marginSchema.optional(), + ...positionFields, +}; + +const alignmentFields = { + align: alignSchema.optional(), + gravity: gravitySchema.optional(), +}; + +const textStyleFields = { + text: z.string().min(1).max(2000), + color: colorSchema.optional(), + size: sizeSchema.optional(), + weight: z.enum(["regular", "bold"]).optional(), + style: z.enum(["normal", "italic"]).optional(), + decoration: z.enum(["none", "underline", "line-through"]).optional(), +}; + +const paddingFields = { + paddingAll: z + .string() + .regex(/^\d+px$/) + .optional(), + paddingTop: z + .string() + .regex(/^\d+px$/) + .optional(), + paddingBottom: z + .string() + .regex(/^\d+px$/) + .optional(), + paddingStart: z + .string() + .regex(/^\d+px$/) + .optional(), + paddingEnd: z + .string() + .regex(/^\d+px$/) + .optional(), +}; + +const flexActionSchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("postback"), + data: z.string().min(1).max(300), + label: z.string().min(1).max(20), + displayText: z.string().min(1).max(300).optional(), + inputOption: z + .enum(["closeRichMenu", "openRichMenu", "openKeyboard", "openVoice"]) + .optional(), + fillInText: z.string().min(1).max(160).optional(), + }), + z.object({ + type: z.literal("message"), + label: z.string().min(1).max(20), + text: z.string().min(1).max(300), + }), + z.object({ + type: z.literal("uri"), + label: z.string().min(1).max(20), + uri: z + .string() + .describe( + "LINE Custom URI or URI" + + "LINE Custom URI document: https://developers.line.biz/ja/docs/messaging-api/using-line-url-scheme/", + ), + altUri: z + .object({ + desktop: z.url(), + }) + .optional(), + }), + z.object({ + type: z.literal("datetimepicker"), + label: z.string().min(1).max(20), + data: z.string().min(1).max(300), + mode: z.enum(["date", "time", "datetime"]), + initial: z + .string() + .optional() + .describe("Format: 2100-12-31, 23:59, 2100-12-31T23:59"), + max: z + .string() + .optional() + .describe("Format: 2100-12-31, 23:59, 2100-12-31T23:59"), + min: z + .string() + .optional() + .describe("Format: 2100-12-31, 23:59, 2100-12-31T23:59"), + }), + z.object({ + type: z.literal("camera"), + label: z.string().min(1).max(20), + }), + z.object({ + type: z.literal("cameraRoll"), + label: z.string().min(1).max(20), + }), + z.object({ + type: z.literal("location"), + label: z.string().min(1).max(20), + }), + z.object({ + type: z.literal("richmenuswitch"), + label: z.string().min(1).max(20), + richMenuAliasId: z.string().min(1).max(32), + data: z.string().min(1).max(300), + }), + z.object({ + type: z.literal("clipboard"), + label: z.string().min(1).max(20), + clipboardText: z.string().min(1).max(1000), + }), +]); + +const flexSpanSchema = z.object({ + type: z.literal("span"), + ...textStyleFields, +}); + +const flexComponentSchema: z.ZodType = z.lazy(() => + z.discriminatedUnion("type", [ + z.object({ + type: z.literal("separator"), + margin: marginSchema.optional(), + color: colorSchema.optional(), + }), + z.object({ + type: z.literal("text"), + contents: z.array(flexSpanSchema).optional(), + adjustMode: z.enum(["shrink-to-fit"]).optional(), + wrap: z.boolean().optional().default(true), + lineSpacing: z.enum(["xs", "sm", "md", "lg", "xl", "xxl"]).optional(), + maxLines: z.number().optional(), + action: flexActionSchema.optional(), + scaling: scalingSchema.optional(), + ...textStyleFields, + ...layoutFields, + ...alignmentFields, + }), + + z.object({ + type: z.literal("icon"), + url: z + .url() + .min(1) + .max(2000) + .refine(url => url.startsWith("https://"), "Must use HTTPS protocol"), + size: sizeSchema.optional(), + aspectRatio: z + .string() + .regex(/^\d+:\d+$/) + .describe( + "Aspect ratio in '{width}:{height}' format (e.g., '1:1', '16:9'). Width and height must be 1-100000, height cannot exceed width × 3", + ) + .optional(), + scaling: scalingSchema.optional(), + ...layoutFields, + }), + z.object({ + type: z.literal("image"), + url: z + .url() + .min(1) + .max(2000) + .default( + "https://developers-resource.landpress.line.me/fx/img/01_1_cafe.png", + ), + size: imageSizeSchema.optional(), + aspectRatio: z + .string() + .regex(/^\d+:\d+$/) + .describe( + "Aspect ratio in '{width}:{height}' format (e.g., '1:1', '16:9'). Width and height must be 1-100000, height cannot exceed width × 3", + ) + .optional(), + aspectMode: z.enum(["cover", "fit"]).optional(), + backgroundColor: colorSchema.optional(), + animated: z.boolean().optional(), + action: flexActionSchema.optional(), + scaling: scalingSchema.optional(), + ...layoutFields, + ...alignmentFields, + }), + z.object({ + type: z.literal("video"), + url: z + .url() + .min(1) + .max(2000) + .refine(url => url.startsWith("https://"), "Must use HTTPS protocol"), + previewUrl: z + .url() + .min(1) + .max(2000) + .default( + "https://developers-resource.landpress.line.me/fx/img/01_1_cafe.png", + ), + altContent: flexComponentSchema, + size: imageSizeSchema.optional(), + aspectRatio: z + .string() + .regex(/^\d+:\d+$/) + .describe( + "Aspect ratio in '{width}:{height}' format (e.g., '1:1', '16:9'). Width and height must be 1-100000, height cannot exceed width × 3", + ) + .optional(), + action: flexActionSchema.optional(), + scaling: scalingSchema.optional(), + ...layoutFields, + ...alignmentFields, + }), + + z.object({ + type: z.literal("button"), + action: flexActionSchema, + height: z.enum(["sm", "md"]).optional(), + style: z.enum(["link", "primary", "secondary"]).optional(), + color: colorSchema.optional(), + gravity: gravitySchema.optional(), + adjustMode: z.enum(["shrink-to-fit"]).optional(), + scaling: scalingSchema.optional(), + ...layoutFields, + }), + + z.object({ + type: z.literal("box"), + layout: z.enum(["horizontal", "vertical", "baseline"]), + contents: z.array(flexComponentSchema), + backgroundColor: colorSchema.optional(), + borderColor: colorSchema.optional(), + borderWidth: z + .string() + .regex(/^\d+px$/) + .optional(), + cornerRadius: z + .string() + .regex(/^\d+px$/) + .optional(), + spacing: spacingSchema.optional(), + width: z + .string() + .regex(/^\d+px$/) + .optional(), + height: z + .string() + .regex(/^\d+px$/) + .optional(), + justifyContent: z + .enum([ + "flex-start", + "center", + "flex-end", + "space-between", + "space-around", + "space-evenly", + ]) + .optional(), + alignItems: z.enum(["flex-start", "center", "flex-end"]).optional(), + background: z + .object({ + type: z.literal("linearGradient"), + angle: z.string().regex(/^\d+deg$/, "Format: '90deg'"), + startColor: colorSchema, + endColor: colorSchema, + }) + .optional(), + action: flexActionSchema.optional(), + ...layoutFields, + ...paddingFields, + }), + ]), +); + +const sectionStyleSchema = z.object({ + backgroundColor: colorSchema.optional(), + separator: z.boolean().optional(), + separatorColor: colorSchema.optional(), +}); + +const flexBubbleStylesSchema = z.object({ + header: sectionStyleSchema.optional(), + hero: sectionStyleSchema.optional(), + body: sectionStyleSchema.optional(), + footer: sectionStyleSchema.optional(), +}); + +export const flexBubbleSchema = z.object({ + type: z.literal("bubble"), + size: z + .enum(["nano", "micro", "deca", "hecto", "kilo", "mega", "giga"]) + .optional(), + direction: z.enum(["ltr", "rtl"]).optional(), + header: flexComponentSchema + .optional() + .describe("Header must be a Box") + .refine( + component => !component || component.type === "box", + "Header must be a Box", + ), + hero: flexComponentSchema.optional(), + body: flexComponentSchema + .optional() + .describe("Body must be a Box") + .refine( + component => !component || component.type === "box", + "Body must be a Box", + ), + footer: flexComponentSchema + .optional() + .describe("Footer must be a Box") + .refine( + component => !component || component.type === "box", + "Footer must be a Box", + ), + styles: flexBubbleStylesSchema.optional(), + action: flexActionSchema.optional(), +}); + +const flexCarouselSchema = z.object({ + type: z.literal("carousel"), + contents: z.array(flexBubbleSchema), +}); + +const flexContainerSchema = z.discriminatedUnion("type", [ + flexBubbleSchema, + flexCarouselSchema, +]); + +export const flexMessageSchema = z.object({ + type: z.literal("flex").default("flex"), + altText: z.string().min(1).max(400), + contents: flexContainerSchema, +}); diff --git a/src/common/schema/textMessage.ts b/src/common/schema/textMessage.ts new file mode 100644 index 00000000..c138826f --- /dev/null +++ b/src/common/schema/textMessage.ts @@ -0,0 +1,9 @@ +import { z } from "zod"; + +export const textMessageSchema = z.object({ + type: z.literal("text").default("text"), + text: z + .string() + .max(5000) + .describe("The plain text content to send to the user."), +}); diff --git a/src/index.ts b/src/index.ts index 042f5725..b24868fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,5 @@ +#!/usr/bin/env node + /** * Copyright 2025 LY Corporation * @@ -17,133 +19,53 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import * as line from "@line/bot-sdk"; -import { z } from "zod"; -import pkg from "../package.json" with { type: "json" }; +import { LINE_BOT_MCP_SERVER_VERSION, USER_AGENT } from "./version.js"; +import CancelRichMenuDefault from "./tools/cancelRichMenuDefault.js"; +import PushTextMessage from "./tools/pushTextMessage.js"; +import PushFlexMessage from "./tools/pushFlexMessage.js"; +import BroadcastTextMessage from "./tools/broadcastTextMessage.js"; +import BroadcastFlexMessage from "./tools/broadcastFlexMessage.js"; +import GetProfile from "./tools/getProfile.js"; +import GetMessageQuota from "./tools/getMessageQuota.js"; +import GetRichMenuList from "./tools/getRichMenuList.js"; +import GetRichMenu from "./tools/getRichMenu.js"; +import DeleteRichMenu from "./tools/deleteRichMenu.js"; +import SetRichMenuDefault from "./tools/setRichMenuDefault.js"; +import CreateRichMenu from "./tools/createRichMenu.js"; +import UpdateRichMenuImage from "./tools/updateRichMenuImage.js"; +import GetFollowerIds from "./tools/getFollowerIds.js"; const server = new McpServer({ name: "line-bot", - version: pkg.version, + version: LINE_BOT_MCP_SERVER_VERSION, }); const channelAccessToken = process.env.CHANNEL_ACCESS_TOKEN || ""; const destinationId = process.env.DESTINATION_USER_ID || ""; +const messagingApiBaseUrl = process.env.LINE_MESSAGING_API_BASE_URL; -const messagingApiClient = new line.messagingApi.MessagingApiClient({ +const lineBotClient = line.LineBotClient.fromChannelAccessToken({ channelAccessToken: channelAccessToken, + apiBaseURL: messagingApiBaseUrl, defaultHeaders: { - "User-Agent": `${pkg.name}/${pkg.version}`, + "User-Agent": USER_AGENT, }, }); -server.tool( - "push_text_message", - "Push a simple text message to user via LINE. Use this for sending plain text messages without formatting.", - { - userId: z - .string() - .optional() - .describe( - "The user ID to receive a message. Defaults to DESTINATION_USER_ID.", - ), - message: z.object({ - type: z.literal("text").default("text"), - text: z - .string() - .max(5000) - .describe("The plain text content to send to the user."), - }), - }, - async ({ userId, message }) => { - const response = await messagingApiClient.pushMessage({ - to: userId ?? destinationId, - messages: [message as unknown as line.messagingApi.FlexMessage], - }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response), - }, - ], - }; - }, -); - -server.tool( - "push_flex_message", - "Push a highly customizable flex message to user via LINE. Supports both bubble (single container) and carousel " + - "(multiple swipeable bubbles) layouts.", - { - userId: z - .string() - .optional() - .describe( - "The user ID to receive a message. Defaults to DESTINATION_USER_ID.", - ), - message: z.object({ - type: z.literal("flex").default("flex"), - altText: z - .string() - .describe( - "Alternative text shown when flex message cannot be displayed.", - ), - contents: z - .object({ - type: z - .enum(["bubble", "carousel"]) - .describe( - "Type of the container. 'bubble' for single container, 'carousel' for multiple swipeable bubbles.", - ), - }) - .passthrough() - .describe( - "Flexible container structure following LINE Flex Message format. For 'bubble' type, can include header, " + - "hero, body, footer, and styles sections. For 'carousel' type, includes an array of bubble containers in " + - "the 'contents' property.", - ), - }), - }, - async ({ userId, message }) => { - const response = await messagingApiClient.pushMessage({ - to: userId ?? destinationId, - messages: [message as unknown as line.messagingApi.FlexMessage], - }); - return { - content: [ - { - type: "text", - text: JSON.stringify(response), - }, - ], - }; - }, -); - -server.tool( - "get_profile", - "Get detailed profile information of a LINE user including display name, profile picture URL, status message and language.", - { - userId: z - .string() - .optional() - .describe( - "The ID of the user whose profile you want to retrieve. Defaults to DESTINATION_USER_ID.", - ), - }, - async ({ userId }) => { - const response = await messagingApiClient.getProfile( - userId ?? destinationId, - ); - return { - content: [ - { - type: "text", - text: JSON.stringify(response), - }, - ], - }; - }, -); +new PushTextMessage(lineBotClient, destinationId).register(server); +new PushFlexMessage(lineBotClient, destinationId).register(server); +new BroadcastTextMessage(lineBotClient).register(server); +new BroadcastFlexMessage(lineBotClient).register(server); +new GetProfile(lineBotClient, destinationId).register(server); +new GetMessageQuota(lineBotClient).register(server); +new GetRichMenuList(lineBotClient).register(server); +new GetRichMenu(lineBotClient).register(server); +new DeleteRichMenu(lineBotClient).register(server); +new SetRichMenuDefault(lineBotClient).register(server); +new CancelRichMenuDefault(lineBotClient).register(server); +new CreateRichMenu(lineBotClient).register(server); +new UpdateRichMenuImage(lineBotClient).register(server); +new GetFollowerIds(lineBotClient).register(server); async function main() { if (!process.env.CHANNEL_ACCESS_TOKEN) { @@ -151,11 +73,6 @@ async function main() { process.exit(1); } - if (!process.env.DESTINATION_USER_ID) { - console.error("Please set DESTINATION_USER_ID"); - process.exit(1); - } - const transport = new StdioServerTransport(); await server.connect(transport); } diff --git a/src/tools/AbstractTool.ts b/src/tools/AbstractTool.ts new file mode 100644 index 00000000..86da04f7 --- /dev/null +++ b/src/tools/AbstractTool.ts @@ -0,0 +1,9 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; + +export abstract class AbstractTool { + /** + * Registers the tool with the given MCP server. + * @param server The MCP server to register the tool with. + */ + abstract register(server: McpServer): void; +} diff --git a/src/tools/broadcastFlexMessage.ts b/src/tools/broadcastFlexMessage.ts new file mode 100644 index 00000000..d1747a36 --- /dev/null +++ b/src/tools/broadcastFlexMessage.ts @@ -0,0 +1,48 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient, messagingApi } from "@line/bot-sdk"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; +import { flexMessageSchema } from "../common/schema/flexMessage.js"; + +export default class BroadcastFlexMessage extends AbstractTool { + private client: LineBotClient; + + constructor(client: LineBotClient) { + super(); + this.client = client; + } + + register(server: McpServer) { + server.registerTool( + "broadcast_flex_message", + { + title: "Broadcast Flex Message", + description: + "Broadcast a highly customizable flex message via LINE to all users who have added your LINE Official Account. " + + "Supports both bubble (single container) and carousel (multiple swipeable bubbles) layouts. Please be aware that " + + "this message will be sent to all users.", + inputSchema: { + message: flexMessageSchema, + }, + annotations: { + destructiveHint: true, + }, + }, + async ({ message }) => { + try { + const response = await this.client.broadcast({ + messages: [message as unknown as messagingApi.Message], + }); + return createSuccessResponse(response); + } catch (error: unknown) { + return createErrorResponse( + `Failed to broadcast message: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/broadcastTextMessage.ts b/src/tools/broadcastTextMessage.ts new file mode 100644 index 00000000..994354dc --- /dev/null +++ b/src/tools/broadcastTextMessage.ts @@ -0,0 +1,47 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient, messagingApi } from "@line/bot-sdk"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; +import { textMessageSchema } from "../common/schema/textMessage.js"; + +export default class BroadcastTextMessage extends AbstractTool { + private client: LineBotClient; + + constructor(client: LineBotClient) { + super(); + this.client = client; + } + + register(server: McpServer) { + server.registerTool( + "broadcast_text_message", + { + title: "Broadcast Text Message", + description: + "Broadcast a simple text message via LINE to all users who have followed your LINE Official Account. Use this for sending " + + "plain text messages without formatting. Please be aware that this message will be sent to all users.", + inputSchema: { + message: textMessageSchema, + }, + annotations: { + destructiveHint: true, + }, + }, + async ({ message }) => { + try { + const response = await this.client.broadcast({ + messages: [message as unknown as messagingApi.Message], + }); + return createSuccessResponse(response); + } catch (error: unknown) { + return createErrorResponse( + `Failed to broadcast message: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/cancelRichMenuDefault.ts b/src/tools/cancelRichMenuDefault.ts new file mode 100644 index 00000000..033c3dc7 --- /dev/null +++ b/src/tools/cancelRichMenuDefault.ts @@ -0,0 +1,39 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient } from "@line/bot-sdk"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; + +export default class CancelRichMenuDefault extends AbstractTool { + private client: LineBotClient; + + constructor(client: LineBotClient) { + super(); + this.client = client; + } + + register(server: McpServer) { + server.registerTool( + "cancel_rich_menu_default", + { + title: "Cancel Rich Menu Default", + description: "Cancel the default rich menu.", + annotations: { + destructiveHint: true, + }, + }, + async () => { + try { + const response = await this.client.cancelDefaultRichMenu(); + return createSuccessResponse(response); + } catch (error) { + return createErrorResponse( + `Failed to cancel default rich menu: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/createRichMenu.ts b/src/tools/createRichMenu.ts new file mode 100644 index 00000000..50e85c72 --- /dev/null +++ b/src/tools/createRichMenu.ts @@ -0,0 +1,338 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient, messagingApi } from "@line/bot-sdk"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; +import { z } from "zod"; +import { Marp } from "@marp-team/marp-core"; +import puppeteer from "puppeteer"; +import fs from "fs"; +import path from "path"; +import os from "os"; +import { fileURLToPath } from "url"; +import { actionSchema } from "../common/schema/actionSchema.js"; +import { promises as fsp } from "fs"; + +const RICHMENU_HEIGHT = 910; +const RICHMENU_WIDTH = 1600; + +export default class CreateRichMenu extends AbstractTool { + private client: LineBotClient; + + constructor(client: LineBotClient) { + super(); + this.client = client; + } + + register(server: McpServer) { + server.registerTool( + "create_rich_menu", + { + title: "Create Rich Menu", + description: + "Create a rich menu based on the given actions. Generate and upload a rich menu image based on the given action. This rich menu will be registered as the default.", + inputSchema: { + chatBarText: z + .string() + .describe( + "Text displayed in the chat bar and this is also used as name of the rich menu to create", + ), + actions: z + .array(actionSchema) + .min(1) + .max(6) + .describe("The actions of the rich menu."), + }, + annotations: { + destructiveHint: true, + }, + }, + async ({ chatBarText, actions }) => { + // Flow: + // 1. Validate the rich menu image + // 2. Create a rich menu + // 3. Generate a rich menu image + // 4. Upload the rich menu image + // 5. Set the rich menu as the default rich menu + let createRichMenuResponse: any = null; + let setImageResponse: any = null; + let setDefaultResponse: any = null; + const lineActions = actions as messagingApi.Action[]; + try { + // 1. Validate the rich menu image + if (lineActions.length < 1 || lineActions.length > 6) { + return createErrorResponse("Invalid actions length"); + } + + // 2. Create a rich menu + const areas: Array = + richmenuAreas(lineActions); + const createRichMenuParams = { + name: chatBarText, + chatBarText, + selected: true, + size: { + width: RICHMENU_WIDTH, + height: RICHMENU_HEIGHT, + }, + areas, + }; + createRichMenuResponse = + await this.client.createRichMenu(createRichMenuParams); + const richMenuId = createRichMenuResponse.richMenuId; + + // 3. Generate a rich menu image + const richMenuImagePath = await generateRichMenuImage(lineActions); + + // 4. Upload the rich menu image + const imageBuffer = fs.readFileSync(richMenuImagePath); + const imageType = "image/png"; + const imageBlob = new Blob([imageBuffer], { type: imageType }); + setImageResponse = await this.client.setRichMenuImage( + richMenuId, + imageBlob, + ); + + // 5. Set the rich menu as the default rich menu + setDefaultResponse = await this.client.setDefaultRichMenu(richMenuId); + + return createSuccessResponse({ + message: "Rich menu created successfully and set as default.", + richMenuId, + createRichMenuParams, + createRichMenuResponse, + setImageResponse, + setDefaultResponse, + richMenuImagePath, + }); + } catch (error) { + console.error("Rich menu creation error:", error); + return createErrorResponse( + JSON.stringify({ + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + createRichMenuResponse, + setImageResponse, + setDefaultResponse, + }), + ); + } + }, + ); + } +} + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Function to generate a rich menu image from a Markdown template +async function generateRichMenuImage( + actions: messagingApi.Action[], +): Promise { + const templateNo = actions.length; + // Flow: + // 1. Read the Markdown template + // 2. Convert Markdown to HTML using Marp + // 3. Save the HTML as a temporary file + // 4. Use puppeteer to convert HTML to PNG + // 5. Delete the temporary HTML file + const richMenuImagePath = path.join( + os.tmpdir(), + `template-0${templateNo}-${Date.now()}.png`, + ); + const serverPath = + process.env.SERVER_PATH || path.resolve(__dirname, "..", ".."); + // 1. Read the Markdown template + const srcPath = path.join( + serverPath, + `richmenu-template/template-0${templateNo}.md`, + ); + let content = await fsp.readFile(srcPath, "utf8"); + for (let index = 0; index < actions.length; index++) { + const pattern = new RegExp(`

item0${index + 1}

`, "g"); + content = content.replace(pattern, `

${actions[index].label}

`); + } + + // 2. Convert Markdown to HTML using Marp + const marp = new Marp(); + const { html, css } = marp.render(content); + + // 3. Save the HTML as a temporary file with Japanese font support + const htmlContent = ` + + + + + + + ${html} + + `; + const tempHtmlPath = path.join( + os.tmpdir(), + `temp_marp_slide_${Date.now()}.html`, + ); + await fsp.writeFile(tempHtmlPath, htmlContent, "utf8"); + + // 4. Use puppeteer to convert HTML to PNG with Docker-compatible settings + const browser = await puppeteer.launch({ + headless: true, + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || undefined, + args: [ + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + "--disable-gpu", + "--disable-web-security", + "--disable-features=VizDisplayCompositor", + "--no-first-run", + "--no-default-browser-check", + "--disable-default-apps", + "--disable-extensions", + ], + }); + const page = await browser.newPage(); + await page.setViewport({ width: RICHMENU_WIDTH, height: RICHMENU_HEIGHT }); + await page.goto(`file://${tempHtmlPath}`, { + waitUntil: "networkidle0", + }); + + // Wait for fonts to load + await page.evaluate(() => document.fonts.ready); + await new Promise(resolve => setTimeout(resolve, 2000)); + + await page.screenshot({ + path: richMenuImagePath as `${string}.png`, + clip: { x: 0, y: 0, width: RICHMENU_WIDTH, height: RICHMENU_HEIGHT }, + }); + await browser.close(); + + // Save image to output directory + const outputPath = path.join("/tmp", path.basename(richMenuImagePath)); + + try { + await fsp.copyFile(richMenuImagePath, outputPath); + } catch (error) { + console.warn(`Failed to save image to output directory: ${error}`); + } finally { + // 5. Delete the temporary HTML file + await fsp.unlink(tempHtmlPath); + } + + return richMenuImagePath; +} + +const richmenuAreas = ( + actions: messagingApi.Action[], +): messagingApi.RichMenuArea[] => { + const bounds = richmenuBounds(actions.length); + return actions.map((action, index) => { + return { + bounds: bounds[index], + action: action, + }; + }); +}; + +const richmenuBounds = (actionCount: number) => { + const boundsMap: { x: number; y: number; width: number; height: number }[][] = + [ + [], + // template-01 + [ + { + x: 0, + y: 0, + width: RICHMENU_WIDTH, + height: RICHMENU_HEIGHT, + }, + ], + // template-02 + [0, 1].map(i => ({ + x: (RICHMENU_WIDTH / 2) * i, + y: 0, + width: RICHMENU_WIDTH / 2, + height: RICHMENU_HEIGHT, + })), + // template-03 + [ + { + x: 0, + y: 0, + width: (RICHMENU_WIDTH / 3) * 2, + height: RICHMENU_HEIGHT, + }, + ...[0, 1].map(i => ({ + x: (RICHMENU_WIDTH / 3) * 2, + y: (RICHMENU_HEIGHT / 2) * i, + width: RICHMENU_WIDTH / 3, + height: RICHMENU_HEIGHT / 2, + })), + ], + // template-04 + [0, 1] + .map(i => + [0, 1].map(j => ({ + x: (RICHMENU_WIDTH / 2) * j, + y: (RICHMENU_HEIGHT / 2) * i, + width: RICHMENU_WIDTH / 2, + height: RICHMENU_HEIGHT / 2, + })), + ) + .flat(), + // template-05 + [ + { + x: 0, + y: 0, + width: (RICHMENU_WIDTH / 3) * 2, + height: RICHMENU_HEIGHT / 2, + }, + { + x: (RICHMENU_WIDTH / 3) * 2, + y: 0, + width: RICHMENU_WIDTH / 3, + height: RICHMENU_HEIGHT / 2, + }, + ...[0, 1, 2].map(i => ({ + x: (RICHMENU_WIDTH / 3) * i, + y: RICHMENU_HEIGHT / 2, + width: RICHMENU_WIDTH / 3, + height: RICHMENU_HEIGHT / 2, + })), + ], + // template-06 + [0, 1] + .map(i => + [0, 1, 2].map(j => ({ + x: (RICHMENU_WIDTH / 3) * j, + y: (RICHMENU_HEIGHT / 2) * i, + width: RICHMENU_WIDTH / 3, + height: RICHMENU_HEIGHT / 2, + })), + ) + .flat(), + ]; + + return boundsMap[actionCount]; +}; diff --git a/src/tools/deleteRichMenu.ts b/src/tools/deleteRichMenu.ts new file mode 100644 index 00000000..38df0406 --- /dev/null +++ b/src/tools/deleteRichMenu.ts @@ -0,0 +1,49 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient } from "@line/bot-sdk"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; +import { z } from "zod"; + +export default class DeleteRichMenu extends AbstractTool { + private client: LineBotClient; + + constructor(client: LineBotClient) { + super(); + this.client = client; + } + + register(server: McpServer) { + const richMenuIdSchema = z + .string() + .describe("The ID of the rich menu to delete."); + + server.registerTool( + "delete_rich_menu", + { + title: "Delete Rich Menu", + description: "Delete a rich menu from your LINE Official Account.", + inputSchema: { + richMenuId: richMenuIdSchema.describe( + "The ID of the rich menu to delete.", + ), + }, + annotations: { + destructiveHint: true, + }, + }, + async ({ richMenuId }) => { + try { + const response = await this.client.deleteRichMenu(richMenuId); + return createSuccessResponse(response); + } catch (error: unknown) { + return createErrorResponse( + `Failed to delete rich menu: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/getFollowerIds.ts b/src/tools/getFollowerIds.ts new file mode 100644 index 00000000..da49b9bc --- /dev/null +++ b/src/tools/getFollowerIds.ts @@ -0,0 +1,54 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient } from "@line/bot-sdk"; +import { z } from "zod"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; + +export default class GetFollowerIds extends AbstractTool { + private client: LineBotClient; + + constructor(client: LineBotClient) { + super(); + this.client = client; + } + + register(server: McpServer) { + server.registerTool( + "get_follower_ids", + { + description: + "Get a list of user IDs of users who have added the LINE Official Account as a friend. This allows you to obtain user IDs for sending messages without manually preparing them.", + inputSchema: { + start: z + .string() + .optional() + .describe( + "Continuation token to get the next array of user IDs. Returned in the 'next' property of a previous response.", + ), + limit: z + .number() + .optional() + .describe( + "The maximum number of user IDs to retrieve in a single request.", + ), + }, + annotations: { + readOnlyHint: true, + }, + }, + async ({ start, limit }) => { + try { + const response = await this.client.getFollowers(start, limit); + return createSuccessResponse(response); + } catch (error: unknown) { + return createErrorResponse( + `Failed to get follower IDs: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/getMessageQuota.ts b/src/tools/getMessageQuota.ts new file mode 100644 index 00000000..a41ba055 --- /dev/null +++ b/src/tools/getMessageQuota.ts @@ -0,0 +1,46 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient } from "@line/bot-sdk"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; + +export default class GetMessageQuota extends AbstractTool { + private client: LineBotClient; + + constructor(client: LineBotClient) { + super(); + this.client = client; + } + + register(server: McpServer) { + server.registerTool( + "get_message_quota", + { + title: "Get Message Quota", + description: + "Get the message quota and consumption of the LINE Official Account. This shows the monthly message limit and current usage.", + annotations: { + readOnlyHint: true, + }, + }, + async () => { + try { + const messageQuotaResponse = await this.client.getMessageQuota(); + const messageQuotaConsumptionResponse = + await this.client.getMessageQuotaConsumption(); + const response = { + limited: messageQuotaResponse.value, + totalUsage: messageQuotaConsumptionResponse.totalUsage, + }; + return createSuccessResponse(response); + } catch (error) { + return createErrorResponse( + `Failed to get message quota: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/getProfile.ts b/src/tools/getProfile.ts new file mode 100644 index 00000000..7dd6f52c --- /dev/null +++ b/src/tools/getProfile.ts @@ -0,0 +1,58 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient } from "@line/bot-sdk"; +import { z } from "zod"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; +import { NO_USER_ID_ERROR } from "../common/schema/constants.js"; + +export default class GetProfile extends AbstractTool { + private client: LineBotClient; + private destinationId: string; + + constructor(client: LineBotClient, destinationId: string) { + super(); + this.client = client; + this.destinationId = destinationId; + } + + register(server: McpServer) { + const userIdSchema = z + .string() + .default(this.destinationId) + .describe( + "The user ID to get a profile. Defaults to DESTINATION_USER_ID.", + ); + + server.registerTool( + "get_profile", + { + title: "Get Profile", + description: + "Get detailed profile information of a LINE user including display name, profile picture URL, status message and language.", + inputSchema: { + userId: userIdSchema, + }, + annotations: { + readOnlyHint: true, + }, + }, + async ({ userId }) => { + if (!userId) { + return createErrorResponse(NO_USER_ID_ERROR); + } + + try { + const response = await this.client.getProfile(userId); + return createSuccessResponse(response); + } catch (error: unknown) { + return createErrorResponse( + `Failed to get profile: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/getRichMenu.ts b/src/tools/getRichMenu.ts new file mode 100644 index 00000000..2127c3a5 --- /dev/null +++ b/src/tools/getRichMenu.ts @@ -0,0 +1,44 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient } from "@line/bot-sdk"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; +import { z } from "zod"; + +export default class GetRichMenu extends AbstractTool { + private client: LineBotClient; + + constructor(client: LineBotClient) { + super(); + this.client = client; + } + + register(server: McpServer) { + server.registerTool( + "get_rich_menu", + { + title: "Get Rich Menu", + description: + "Get a rich menu by ID, including its size, chat bar text, and tap areas.", + inputSchema: { + richMenuId: z.string().describe("The ID of the rich menu to get."), + }, + annotations: { + readOnlyHint: true, + }, + }, + async ({ richMenuId }) => { + try { + const response = await this.client.getRichMenu(richMenuId); + return createSuccessResponse(response); + } catch (error: unknown) { + return createErrorResponse( + `Failed to get rich menu: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/getRichMenuList.ts b/src/tools/getRichMenuList.ts new file mode 100644 index 00000000..b3a4ab8e --- /dev/null +++ b/src/tools/getRichMenuList.ts @@ -0,0 +1,40 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient } from "@line/bot-sdk"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; + +export default class GetRichMenuList extends AbstractTool { + private client: LineBotClient; + + constructor(client: LineBotClient) { + super(); + this.client = client; + } + + register(server: McpServer) { + server.registerTool( + "get_rich_menu_list", + { + title: "Get Rich Menu List", + description: + "Get the list of rich menus associated with your LINE Official Account.", + annotations: { + readOnlyHint: true, + }, + }, + async () => { + try { + const response = await this.client.getRichMenuList(); + return createSuccessResponse(response); + } catch (error: unknown) { + return createErrorResponse( + `Failed to get rich menu list: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/pushFlexMessage.ts b/src/tools/pushFlexMessage.ts new file mode 100644 index 00000000..062a16fc --- /dev/null +++ b/src/tools/pushFlexMessage.ts @@ -0,0 +1,64 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient, messagingApi } from "@line/bot-sdk"; +import { z } from "zod"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; +import { NO_USER_ID_ERROR } from "../common/schema/constants.js"; +import { flexMessageSchema } from "../common/schema/flexMessage.js"; + +export default class PushFlexMessage extends AbstractTool { + private client: LineBotClient; + private destinationId: string; + + constructor(client: LineBotClient, destinationId: string) { + super(); + this.client = client; + this.destinationId = destinationId; + } + + register(server: McpServer) { + const userIdSchema = z + .string() + .default(this.destinationId) + .describe( + "The user ID to receive a message. Defaults to DESTINATION_USER_ID.", + ); + + server.registerTool( + "push_flex_message", + { + title: "Push Flex Message", + description: + "Push a highly customizable flex message to a user via LINE. Supports both bubble (single container) and carousel " + + "(multiple swipeable bubbles) layouts.", + inputSchema: { + userId: userIdSchema, + message: flexMessageSchema, + }, + annotations: { + destructiveHint: true, + }, + }, + async ({ userId, message }) => { + if (!userId) { + return createErrorResponse(NO_USER_ID_ERROR); + } + + try { + const response = await this.client.pushMessage({ + to: userId, + messages: [message as unknown as messagingApi.Message], + }); + return createSuccessResponse(response); + } catch (error: unknown) { + return createErrorResponse( + `Failed to push flex message: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/pushTextMessage.ts b/src/tools/pushTextMessage.ts new file mode 100644 index 00000000..7b837996 --- /dev/null +++ b/src/tools/pushTextMessage.ts @@ -0,0 +1,63 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient, messagingApi } from "@line/bot-sdk"; +import { z } from "zod"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; +import { NO_USER_ID_ERROR } from "../common/schema/constants.js"; +import { textMessageSchema } from "../common/schema/textMessage.js"; + +export default class PushTextMessage extends AbstractTool { + private client: LineBotClient; + private destinationId: string; + + constructor(client: LineBotClient, destinationId: string) { + super(); + this.client = client; + this.destinationId = destinationId; + } + + register(server: McpServer) { + const userIdSchema = z + .string() + .default(this.destinationId) + .describe( + "The user ID to receive a message. Defaults to DESTINATION_USER_ID.", + ); + + server.registerTool( + "push_text_message", + { + title: "Push Text Message", + description: + "Push a simple text message to a user via LINE. Use this for sending plain text messages without formatting.", + inputSchema: { + userId: userIdSchema, + message: textMessageSchema, + }, + annotations: { + destructiveHint: true, + }, + }, + async ({ userId, message }) => { + if (!userId) { + return createErrorResponse(NO_USER_ID_ERROR); + } + + try { + const response = await this.client.pushMessage({ + to: userId, + messages: [message as unknown as messagingApi.Message], + }); + return createSuccessResponse(response); + } catch (error: unknown) { + return createErrorResponse( + `Failed to push message: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/setRichMenuDefault.ts b/src/tools/setRichMenuDefault.ts new file mode 100644 index 00000000..6caffa65 --- /dev/null +++ b/src/tools/setRichMenuDefault.ts @@ -0,0 +1,49 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient } from "@line/bot-sdk"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; +import { z } from "zod"; + +export default class SetRichMenuDefault extends AbstractTool { + private client: LineBotClient; + + constructor(client: LineBotClient) { + super(); + this.client = client; + } + + register(server: McpServer) { + const richMenuIdSchema = z + .string() + .describe("The ID of the rich menu to set as default."); + + server.registerTool( + "set_rich_menu_default", + { + title: "Set Rich Menu Default", + description: "Set a rich menu as the default rich menu.", + inputSchema: { + richMenuId: richMenuIdSchema.describe( + "The ID of the rich menu to set as default.", + ), + }, + annotations: { + destructiveHint: true, + }, + }, + async ({ richMenuId }) => { + try { + const response = await this.client.setDefaultRichMenu(richMenuId); + return createSuccessResponse(response); + } catch (error) { + return createErrorResponse( + `Failed to set default rich menu: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/updateRichMenuImage.ts b/src/tools/updateRichMenuImage.ts new file mode 100644 index 00000000..55b659d5 --- /dev/null +++ b/src/tools/updateRichMenuImage.ts @@ -0,0 +1,290 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { LineBotClient, messagingApi } from "@line/bot-sdk"; +import { + createErrorResponse, + createSuccessResponse, +} from "../common/response.js"; +import { AbstractTool } from "./AbstractTool.js"; +import { z } from "zod"; +import fs from "fs"; +import path from "path"; + +const MIN_WIDTH = 800; +const MAX_WIDTH = 2500; +const MIN_HEIGHT = 250; +const MIN_ASPECT_RATIO = 1.45; +const MAX_FILE_SIZE = 1_048_576; // 1MB + +export type ImageDimensions = { width: number; height: number }; + +/** + * Parse the width/height of a PNG or JPEG file by reading its binary header only. + * No image decoding is performed. + */ +export function readImageDimensions( + buffer: Buffer, + ext: string, +): ImageDimensions { + if (ext === ".png") { + // PNG signature (8 bytes) + IHDR chunk. width at offset 16, height at offset 20. + if (buffer.length < 24) { + throw new Error("Invalid PNG file: header is too short."); + } + const width = buffer.readUInt32BE(16); + const height = buffer.readUInt32BE(20); + return { width, height }; + } + + // JPEG: scan markers until a Start Of Frame (SOF) marker is found. + // SOF markers are 0xC0-0xCF, excluding DHT (0xC4), DAC (0xCC) and RSTn (0xD0-0xD7). + let offset = 2; // skip the 0xFFD8 SOI marker + while (offset < buffer.length) { + if (buffer[offset] !== 0xff) { + offset++; + continue; + } + const marker = buffer[offset + 1]; + if ( + marker >= 0xc0 && + marker <= 0xcf && + marker !== 0xc4 && + marker !== 0xc8 && + marker !== 0xcc + ) { + // SOF marker: height at marker+5 (u16 BE), width at marker+7 (u16 BE). + const height = buffer.readUInt16BE(offset + 5); + const width = buffer.readUInt16BE(offset + 7); + return { width, height }; + } + // Skip this segment using its length field. + const segmentLength = buffer.readUInt16BE(offset + 2); + offset += 2 + segmentLength; + } + throw new Error( + "Invalid JPEG file: no Start Of Frame marker found; cannot read image dimensions.", + ); +} + +/** + * Validate the new image against LINE rich menu image constraints and against + * the existing rich menu size. Throws an actionable Error on any violation. + */ +export function validateRichMenuImage( + imagePath: string, + richMenuSize: messagingApi.RichMenuSize, +): { dimensions: ImageDimensions; contentType: string } { + if (!fs.existsSync(imagePath)) { + throw new Error( + `Image file not found at "${imagePath}". Provide a valid local file path to an existing PNG or JPEG image.`, + ); + } + + const ext = path.extname(imagePath).toLowerCase(); + if (ext !== ".png" && ext !== ".jpg" && ext !== ".jpeg") { + throw new Error( + `Unsupported image extension "${ext || "(none)"}". Provide a PNG or JPEG file with a .png, .jpg or .jpeg extension.`, + ); + } + const contentType = ext === ".png" ? "image/png" : "image/jpeg"; + + const stats = fs.statSync(imagePath); + if (stats.size > MAX_FILE_SIZE) { + throw new Error( + `Image file is ${stats.size} bytes but the maximum allowed is ${MAX_FILE_SIZE} bytes (1MB). Compress or re-export the image to be at most 1MB.`, + ); + } + + const buffer = fs.readFileSync(imagePath); + const dimensions = readImageDimensions(buffer, ext); + const { width, height } = dimensions; + + if (width < MIN_WIDTH || width > MAX_WIDTH) { + throw new Error( + `Image width is ${width}px but must be between ${MIN_WIDTH} and ${MAX_WIDTH}px. Regenerate the image with a width in that range (ideally ${richMenuSize.width}px to match the rich menu).`, + ); + } + if (height < MIN_HEIGHT) { + throw new Error( + `Image height is ${height}px but must be at least ${MIN_HEIGHT}px. Regenerate the image taller (ideally ${richMenuSize.height}px to match the rich menu).`, + ); + } + const aspectRatio = width / height; + if (aspectRatio < MIN_ASPECT_RATIO) { + throw new Error( + `Image aspect ratio (width/height) is ${aspectRatio.toFixed(3)} but must be at least ${MIN_ASPECT_RATIO}. Regenerate the image wider relative to its height (e.g. ${richMenuSize.width}x${richMenuSize.height}).`, + ); + } + + if (width !== richMenuSize.width || height !== richMenuSize.height) { + throw new Error( + `Image is ${width}x${height} but the rich menu size is ${richMenuSize.width}x${richMenuSize.height}. Regenerate the image at exactly ${richMenuSize.width}x${richMenuSize.height} px.`, + ); + } + + return { dimensions, contentType }; +} + +export default class UpdateRichMenuImage extends AbstractTool { + private client: LineBotClient; + + constructor(client: LineBotClient) { + super(); + this.client = client; + } + + register(server: McpServer) { + server.registerTool( + "update_rich_menu_image", + { + title: "Update Rich Menu Image", + description: + "Replace the image of an existing rich menu. LINE does not allow overwriting an already uploaded rich menu image, so this tool uses a clone-and-replace strategy: it creates a new rich menu with the same definition as the old one, uploads the new image, takes over the default assignment if the old menu was the default, and deletes the old menu. As a result, a NEW richMenuId is issued and returned; the old richMenuId will no longer be valid. Image constraints: PNG or JPEG, width 800-2500px, height >= 250px, aspect ratio (width/height) >= 1.45, max 1MB. The image dimensions must match the size of the existing rich menu.", + inputSchema: { + richMenuId: z + .string() + .describe("The ID of the rich menu whose image to update."), + imagePath: z + .string() + .describe( + "Local file path to the new image. PNG or JPEG, width 800-2500px, height >= 250px, aspect ratio >= 1.45, max 1MB. Dimensions must match the existing rich menu size (e.g. 1600x910).", + ), + deleteOldRichMenu: z + .boolean() + .default(true) + .describe( + "Whether to delete the old rich menu after replacement. Defaults to true.", + ), + }, + annotations: { + destructiveHint: true, + }, + }, + async ({ richMenuId, imagePath, deleteOldRichMenu }) => { + try { + // 1. Fetch the old rich menu definition. + let oldRichMenu: messagingApi.RichMenuResponse; + try { + oldRichMenu = await this.client.getRichMenu(richMenuId); + } catch (error: unknown) { + return createErrorResponse( + `Failed to get the rich menu "${richMenuId}": ${error instanceof Error ? error.message : String(error)}`, + ); + } + + // 2. Validate the new image (before any API calls that mutate state). + let contentType: string; + try { + ({ contentType } = validateRichMenuImage( + imagePath, + oldRichMenu.size, + )); + } catch (error: unknown) { + return createErrorResponse( + error instanceof Error ? error.message : String(error), + ); + } + + // 3. Create a new rich menu with the same definition as the old one. + const richMenuRequest: messagingApi.RichMenuRequest = { + size: oldRichMenu.size, + selected: oldRichMenu.selected, + name: oldRichMenu.name, + chatBarText: oldRichMenu.chatBarText, + areas: oldRichMenu.areas, + }; + const createResponse = + await this.client.createRichMenu(richMenuRequest); + const newRichMenuId = createResponse.richMenuId; + + // 4. Upload the new image. Roll back the new menu on failure. + try { + const imageBuffer = fs.readFileSync(imagePath); + const imageBlob = new Blob([imageBuffer], { type: contentType }); + await this.client.setRichMenuImage(newRichMenuId, imageBlob); + } catch (uploadError: unknown) { + let rollbackError: unknown = null; + try { + await this.client.deleteRichMenu(newRichMenuId); + } catch (error: unknown) { + rollbackError = error; + } + return createErrorResponse( + JSON.stringify({ + error: `Failed to upload the new rich menu image: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`, + newRichMenuId, + rolledBack: rollbackError === null, + rollbackError: + rollbackError === null + ? undefined + : `Failed to delete the newly created rich menu "${newRichMenuId}" during rollback: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, + }), + ); + } + + // 5. Determine whether the old menu was the default and take it over. + let oldWasDefault = false; + try { + const defaultResponse = await this.client.getDefaultRichMenuId(); + oldWasDefault = defaultResponse.richMenuId === richMenuId; + } catch { + // No default rich menu is set (LINE returns 404). Treat as "no default". + oldWasDefault = false; + } + + let defaultSwitched = false; + let defaultSwitchError: string | undefined; + if (oldWasDefault) { + try { + await this.client.setDefaultRichMenu(newRichMenuId); + defaultSwitched = true; + } catch (error: unknown) { + defaultSwitchError = `Failed to switch the default rich menu to "${newRichMenuId}": ${error instanceof Error ? error.message : String(error)}`; + } + } + + // 6. Delete the old rich menu (unless the default switch failed). + let oldRichMenuDeleted = false; + let deleteError: string | undefined; + if (oldWasDefault && !defaultSwitched) { + // Do not delete the old menu; it is still the active default. + return createErrorResponse( + JSON.stringify({ + error: defaultSwitchError, + oldRichMenuId: richMenuId, + newRichMenuId, + defaultSwitched, + oldRichMenuDeleted, + message: + "The new rich menu and image were created, but switching the default failed. The old rich menu was kept as the active default. Retry setting the default to the new rich menu manually, then delete the old one.", + }), + ); + } + + if (deleteOldRichMenu) { + try { + await this.client.deleteRichMenu(richMenuId); + oldRichMenuDeleted = true; + } catch (error: unknown) { + deleteError = `Failed to delete the old rich menu "${richMenuId}": ${error instanceof Error ? error.message : String(error)}`; + } + } + + return createSuccessResponse({ + message: + "Rich menu image updated via clone-and-replace. A new richMenuId was issued; the old richMenuId is no longer valid." + + (deleteError ? ` Warning: ${deleteError}` : ""), + oldRichMenuId: richMenuId, + newRichMenuId, + defaultSwitched, + oldRichMenuDeleted, + ...(deleteError ? { deleteError } : {}), + }); + } catch (error: unknown) { + return createErrorResponse( + `Failed to update rich menu image: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }, + ); + } +} diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 00000000..86987733 --- /dev/null +++ b/src/version.ts @@ -0,0 +1,2 @@ +export const LINE_BOT_MCP_SERVER_VERSION = "0.1.0-local"; +export const USER_AGENT = `@line/line-bot-mcp-server/${LINE_BOT_MCP_SERVER_VERSION}`; diff --git a/test/common/schema/flexMessage.test.ts b/test/common/schema/flexMessage.test.ts new file mode 100644 index 00000000..47b49a93 --- /dev/null +++ b/test/common/schema/flexMessage.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "vitest"; +import { flexMessageSchema } from "../../../src/common/schema/flexMessage.js"; + +const toFlexMessage = (component: unknown) => ({ + type: "flex" as const, + altText: "Test message", + contents: { + type: "bubble" as const, + body: { + type: "box" as const, + layout: "vertical" as const, + contents: [component], + }, + }, +}); + +describe("flexMessageSchema URL validation", () => { + it("accepts valid URL values for image and altUri.desktop", () => { + const imageResult = flexMessageSchema.safeParse( + toFlexMessage({ + type: "image", + url: "https://example.com/image.png", + }), + ); + expect(imageResult.success).toBe(true); + + const altUriResult = flexMessageSchema.safeParse( + toFlexMessage({ + type: "button", + action: { + type: "uri", + label: "Open", + uri: "https://example.com/page", + altUri: { + desktop: "https://example.com/desktop", + }, + }, + }), + ); + expect(altUriResult.success).toBe(true); + }); + + it("rejects malformed URL in image.url", () => { + const result = flexMessageSchema.safeParse( + toFlexMessage({ + type: "image", + url: "not-a-url", + }), + ); + + expect(result.success).toBe(false); + if (!result.success) { + expect( + result.error.issues.some(issue => issue.path.includes("url")), + ).toBe(true); + } + }); + + it("rejects non-https URL in icon.url", () => { + const result = flexMessageSchema.safeParse( + toFlexMessage({ + type: "icon", + url: "http://example.com/icon.png", + }), + ); + + expect(result.success).toBe(false); + if (!result.success) { + expect( + result.error.issues.some( + issue => + issue.path.includes("url") && + issue.message.includes("Must use HTTPS protocol"), + ), + ).toBe(true); + } + }); + + it("rejects malformed URL in uri action altUri.desktop", () => { + const result = flexMessageSchema.safeParse( + toFlexMessage({ + type: "button", + action: { + type: "uri", + label: "Open", + uri: "https://example.com/page", + altUri: { + desktop: "invalid-desktop-url", + }, + }, + }), + ); + + expect(result.success).toBe(false); + if (!result.success) { + expect( + result.error.issues.some(issue => issue.path.includes("desktop")), + ).toBe(true); + } + }); +}); diff --git a/test/e2e/pushTextMessage.e2e.test.ts b/test/e2e/pushTextMessage.e2e.test.ts new file mode 100644 index 00000000..d9d1ce2e --- /dev/null +++ b/test/e2e/pushTextMessage.e2e.test.ts @@ -0,0 +1,147 @@ +import { createServer, type IncomingHttpHeaders, type Server } from "node:http"; +import { fileURLToPath } from "node:url"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { describe, expect, it } from "vitest"; +import { USER_AGENT } from "../../src/version.js"; + +type RecordedRequest = { + method: string; + url: string; + headers: IncomingHttpHeaders; + body: string; +}; + +const DIST_SERVER_ENTRY = fileURLToPath( + new URL("../../dist/index.js", import.meta.url), +); + +async function closeServer(server: Server): Promise { + if (!server.listening) { + return; + } + + await new Promise((resolve, reject) => { + server.close(error => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); +} + +async function startMockLineApiServer(): Promise<{ + baseUrl: string; + recordedRequests: RecordedRequest[]; + close: () => Promise; +}> { + const recordedRequests: RecordedRequest[] = []; + + const server = createServer((request, response) => { + const chunks: Buffer[] = []; + + request.on("data", chunk => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + + request.on("end", () => { + const body = Buffer.concat(chunks).toString("utf8"); + + recordedRequests.push({ + method: request.method ?? "", + url: request.url ?? "", + headers: request.headers, + body, + }); + + if (request.method === "POST" && request.url === "/v2/bot/message/push") { + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify({})); + return; + } + + response.writeHead(404, { "content-type": "application/json" }); + response.end(JSON.stringify({ message: "Not found" })); + }); + }); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.off("error", reject); + resolve(); + }); + }); + + const address = server.address(); + if (!address || typeof address === "string") { + await closeServer(server); + throw new Error("Failed to determine mock LINE API server address"); + } + + return { + baseUrl: `http://127.0.0.1:${address.port}`, + recordedRequests, + close: () => closeServer(server), + }; +} + +describe("push_text_message E2E with localhost mock LINE API", () => { + it( + "routes the tool call through MCP stdio and sends the HTTP request to the mock server", + { timeout: 30_000 }, + async () => { + const mockApi = await startMockLineApiServer(); + const client = new Client({ name: "test-client", version: "0.0.1" }); + + try { + const transport = new StdioClientTransport({ + command: process.execPath, + args: [DIST_SERVER_ENTRY], + env: { + CHANNEL_ACCESS_TOKEN: "test-channel-access-token", + DESTINATION_USER_ID: "U_TEST_DESTINATION", + LINE_MESSAGING_API_BASE_URL: mockApi.baseUrl, + }, + }); + + await client.connect(transport); + + const result = await client.callTool({ + name: "push_text_message", + arguments: { + message: { type: "text", text: "hello from e2e" }, + }, + }); + + expect(result.isError).toBeFalsy(); + + const text = ( + result.content as Array<{ type: string; text: string }> + )[0].text; + expect(JSON.parse(text)).toEqual({}); + + expect(mockApi.recordedRequests).toHaveLength(1); + expect(mockApi.recordedRequests[0]).toMatchObject({ + method: "POST", + url: "/v2/bot/message/push", + }); + expect(mockApi.recordedRequests[0].headers.authorization).toBe( + "Bearer test-channel-access-token", + ); + expect(mockApi.recordedRequests[0].headers["user-agent"]).toBe( + USER_AGENT, + ); + expect(JSON.parse(mockApi.recordedRequests[0].body)).toEqual({ + to: "U_TEST_DESTINATION", + messages: [{ type: "text", text: "hello from e2e" }], + }); + } finally { + await client.close().catch(() => undefined); + await mockApi.close(); + } + }, + ); +}); diff --git a/test/helpers/mock-line-clients.ts b/test/helpers/mock-line-clients.ts new file mode 100644 index 00000000..5b18a29e --- /dev/null +++ b/test/helpers/mock-line-clients.ts @@ -0,0 +1,21 @@ +import { vi } from "vitest"; +import type { LineBotClient } from "@line/bot-sdk"; + +export function createMockLineBotClient() { + return { + pushMessage: vi.fn(), + broadcast: vi.fn(), + getProfile: vi.fn(), + getMessageQuota: vi.fn(), + getMessageQuotaConsumption: vi.fn(), + getRichMenuList: vi.fn(), + getRichMenu: vi.fn(), + deleteRichMenu: vi.fn(), + setDefaultRichMenu: vi.fn(), + cancelDefaultRichMenu: vi.fn(), + createRichMenu: vi.fn(), + getFollowers: vi.fn(), + setRichMenuImage: vi.fn(), + getDefaultRichMenuId: vi.fn(), + } as unknown as LineBotClient; +} diff --git a/test/tools/broadcastFlexMessage.test.ts b/test/tools/broadcastFlexMessage.test.ts new file mode 100644 index 00000000..a2b2d679 --- /dev/null +++ b/test/tools/broadcastFlexMessage.test.ts @@ -0,0 +1,92 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import BroadcastFlexMessage from "../../src/tools/broadcastFlexMessage.js"; + +const SAMPLE_FLEX_MESSAGE = { + type: "flex", + altText: "Test flex message", + contents: { + type: "bubble", + body: { + type: "box", + layout: "vertical", + contents: [{ type: "text", text: "Hello" }], + }, + }, +}; + +// Zod schema applies default values (e.g. wrap: true for text elements) +const EXPECTED_FLEX_MESSAGE = { + type: "flex", + altText: "Test flex message", + contents: { + type: "bubble", + body: { + type: "box", + layout: "vertical", + contents: [{ type: "text", text: "Hello", wrap: true }], + }, + }, +}; + +describe("broadcast_flex_message tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new BroadcastFlexMessage(mockLineClient).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it("calls broadcast with the correct arguments", async () => { + vi.mocked(mockLineClient.broadcast).mockResolvedValue({} as never); + + const result = await client.callTool({ + name: "broadcast_flex_message", + arguments: { + message: SAMPLE_FLEX_MESSAGE, + }, + }); + + expect(mockLineClient.broadcast).toHaveBeenCalledWith({ + messages: [EXPECTED_FLEX_MESSAGE], + }); + expect(result.isError).toBeFalsy(); + }); + + it("returns an error response when LINE API fails", async () => { + vi.mocked(mockLineClient.broadcast).mockRejectedValue( + new Error("API error"), + ); + + const result = await client.callTool({ + name: "broadcast_flex_message", + arguments: { + message: SAMPLE_FLEX_MESSAGE, + }, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("Failed to broadcast message"); + }); +}); diff --git a/test/tools/broadcastTextMessage.test.ts b/test/tools/broadcastTextMessage.test.ts new file mode 100644 index 00000000..ae202492 --- /dev/null +++ b/test/tools/broadcastTextMessage.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import BroadcastTextMessage from "../../src/tools/broadcastTextMessage.js"; + +describe("broadcast_text_message tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new BroadcastTextMessage(mockLineClient).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it("calls broadcast with the correct arguments", async () => { + vi.mocked(mockLineClient.broadcast).mockResolvedValue({} as never); + + const result = await client.callTool({ + name: "broadcast_text_message", + arguments: { + message: { type: "text", text: "hello everyone" }, + }, + }); + + expect(mockLineClient.broadcast).toHaveBeenCalledWith({ + messages: [{ type: "text", text: "hello everyone" }], + }); + expect(result.isError).toBeFalsy(); + }); + + it("returns an error response when LINE API fails", async () => { + vi.mocked(mockLineClient.broadcast).mockRejectedValue( + new Error("API error"), + ); + + const result = await client.callTool({ + name: "broadcast_text_message", + arguments: { + message: { type: "text", text: "hello" }, + }, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("Failed to broadcast message"); + }); +}); diff --git a/test/tools/cancelRichMenuDefault.test.ts b/test/tools/cancelRichMenuDefault.test.ts new file mode 100644 index 00000000..c3ea701f --- /dev/null +++ b/test/tools/cancelRichMenuDefault.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import CancelRichMenuDefault from "../../src/tools/cancelRichMenuDefault.js"; + +describe("cancel_rich_menu_default tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new CancelRichMenuDefault(mockLineClient).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it("calls cancelDefaultRichMenu", async () => { + vi.mocked(mockLineClient.cancelDefaultRichMenu).mockResolvedValue( + {} as never, + ); + + const result = await client.callTool({ + name: "cancel_rich_menu_default", + arguments: {}, + }); + + expect(mockLineClient.cancelDefaultRichMenu).toHaveBeenCalled(); + expect(result.isError).toBeFalsy(); + }); + + it("returns an error when LINE API fails", async () => { + vi.mocked(mockLineClient.cancelDefaultRichMenu).mockRejectedValue( + new Error("API error"), + ); + + const result = await client.callTool({ + name: "cancel_rich_menu_default", + arguments: {}, + }); + + expect(result.isError).toBe(true); + }); +}); diff --git a/test/tools/createRichMenu.test.ts b/test/tools/createRichMenu.test.ts new file mode 100644 index 00000000..1a28559d --- /dev/null +++ b/test/tools/createRichMenu.test.ts @@ -0,0 +1,277 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; + +// Mock external dependencies before importing the tool. +// vi.mock factories are hoisted, so all values must be created inline. +vi.mock("puppeteer", () => ({ + default: { + launch: vi.fn().mockResolvedValue({ + newPage: vi.fn().mockResolvedValue({ + setViewport: vi.fn().mockResolvedValue(undefined), + goto: vi.fn().mockResolvedValue(undefined), + evaluate: vi.fn().mockResolvedValue(undefined), + screenshot: vi.fn().mockResolvedValue(undefined), + }), + close: vi.fn().mockResolvedValue(undefined), + }), + }, +})); + +vi.mock("@marp-team/marp-core", () => ({ + Marp: class { + render() { + return { html: "
mock
", css: "body{}" }; + } + }, +})); + +vi.mock("fs", async importOriginal => { + const actual: any = await importOriginal(); + return { + ...actual, + default: { + ...actual.default, + readFileSync: vi.fn().mockReturnValue(Buffer.from("fake-image-data")), + }, + promises: { + ...actual.promises, + readFile: vi + .fn() + .mockResolvedValue("# Template\n

item01

\n

item02

"), + writeFile: vi.fn().mockResolvedValue(undefined), + copyFile: vi.fn().mockResolvedValue(undefined), + unlink: vi.fn().mockResolvedValue(undefined), + }, + }; +}); + +// Import after mocks are set up +import CreateRichMenu from "../../src/tools/createRichMenu.js"; + +describe("create_rich_menu tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new CreateRichMenu(mockLineClient).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it( + "creates a rich menu, uploads image, and sets as default", + { timeout: 10000 }, + async () => { + vi.mocked(mockLineClient.createRichMenu).mockResolvedValue({ + richMenuId: "richmenu-new-123", + } as never); + vi.mocked(mockLineClient.setRichMenuImage).mockResolvedValue({} as never); + vi.mocked(mockLineClient.setDefaultRichMenu).mockResolvedValue( + {} as never, + ); + + const result = await client.callTool({ + name: "create_rich_menu", + arguments: { + chatBarText: "My Menu", + actions: [ + { type: "message", label: "Action 1", text: "action1" }, + { type: "message", label: "Action 2", text: "action2" }, + ], + }, + }); + + expect(mockLineClient.createRichMenu).toHaveBeenCalledWith( + expect.objectContaining({ + name: "My Menu", + chatBarText: "My Menu", + selected: true, + size: { width: 1600, height: 910 }, + }), + ); + expect(mockLineClient.setRichMenuImage).toHaveBeenCalledWith( + "richmenu-new-123", + expect.any(Blob), + ); + expect(mockLineClient.setDefaultRichMenu).toHaveBeenCalledWith( + "richmenu-new-123", + ); + expect(result.isError).toBeFalsy(); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + const parsed = JSON.parse(text); + expect(parsed.richMenuId).toBe("richmenu-new-123"); + expect(parsed.message).toContain("Rich menu created successfully"); + }, + ); + + it.each([ + { + // +-----------+ + // | | + // | 1 | + // | | + // +-----------+ + templateName: "template-01", + actionCount: 1, + expectedBounds: [{ x: 0, y: 0, width: 1600, height: 910 }], + }, + { + // +-----+-----+ + // | | | + // | 1 | 2 | + // | | | + // +-----+-----+ + templateName: "template-02", + actionCount: 2, + expectedBounds: [ + { x: 0, y: 0, width: 800, height: 910 }, + { x: 800, y: 0, width: 800, height: 910 }, + ], + }, + { + // +-------+---+ + // | | 2 | + // | 1 +---+ + // | | 3 | + // +-------+---+ + templateName: "template-03", + actionCount: 3, + expectedBounds: [ + { x: 0, y: 0, width: (1600 / 3) * 2, height: 910 }, + { x: (1600 / 3) * 2, y: 0, width: 1600 / 3, height: 455 }, + { x: (1600 / 3) * 2, y: 455, width: 1600 / 3, height: 455 }, + ], + }, + { + // +-----+-----+ + // | 1 | 2 | + // +-----+-----+ + // | 3 | 4 | + // +-----+-----+ + templateName: "template-04", + actionCount: 4, + expectedBounds: [ + { x: 0, y: 0, width: 800, height: 455 }, + { x: 800, y: 0, width: 800, height: 455 }, + { x: 0, y: 455, width: 800, height: 455 }, + { x: 800, y: 455, width: 800, height: 455 }, + ], + }, + { + // +-------+---+ + // | 1 | 2 | + // +---+---+---+ + // | 3 | 4 | 5 | + // +---+---+---+ + templateName: "template-05", + actionCount: 5, + expectedBounds: [ + { x: 0, y: 0, width: (1600 / 3) * 2, height: 455 }, + { x: (1600 / 3) * 2, y: 0, width: 1600 / 3, height: 455 }, + { x: 0, y: 455, width: 1600 / 3, height: 455 }, + { x: 1600 / 3, y: 455, width: 1600 / 3, height: 455 }, + { x: (1600 / 3) * 2, y: 455, width: 1600 / 3, height: 455 }, + ], + }, + { + // +---+---+---+ + // | 1 | 2 | 3 | + // +---+---+---+ + // | 4 | 5 | 6 | + // +---+---+---+ + templateName: "template-06", + actionCount: 6, + expectedBounds: [ + { x: 0, y: 0, width: 1600 / 3, height: 455 }, + { x: 1600 / 3, y: 0, width: 1600 / 3, height: 455 }, + { x: (1600 / 3) * 2, y: 0, width: 1600 / 3, height: 455 }, + { x: 0, y: 455, width: 1600 / 3, height: 455 }, + { x: 1600 / 3, y: 455, width: 1600 / 3, height: 455 }, + { x: (1600 / 3) * 2, y: 455, width: 1600 / 3, height: 455 }, + ], + }, + ])( + "$templateName: creates a rich menu with correct areas for $actionCount actions", + { timeout: 10000 }, + async ({ actionCount, expectedBounds }) => { + vi.mocked(mockLineClient.createRichMenu).mockResolvedValue({ + richMenuId: "richmenu-new-123", + } as never); + vi.mocked(mockLineClient.setRichMenuImage).mockResolvedValue({} as never); + vi.mocked(mockLineClient.setDefaultRichMenu).mockResolvedValue( + {} as never, + ); + + const actions = Array.from({ length: actionCount }, (_, i) => ({ + type: "message" as const, + label: `Action ${i + 1}`, + text: `action${i + 1}`, + })); + + await client.callTool({ + name: "create_rich_menu", + arguments: { chatBarText: "My Menu", actions }, + }); + + const callArgs = vi.mocked(mockLineClient.createRichMenu).mock + .calls[0][0]; + const actualBounds = callArgs.areas?.map(area => area.bounds); + expect(actualBounds).toEqual(expectedBounds); + + // Verify no areas overlap + for (let i = 0; i < expectedBounds.length; i++) { + for (let j = i + 1; j < expectedBounds.length; j++) { + const a = expectedBounds[i]; + const b = expectedBounds[j]; + const overlaps = + a.x < b.x + b.width && + b.x < a.x + a.width && + a.y < b.y + b.height && + b.y < a.y + a.height; + expect(overlaps, `areas[${i}] and areas[${j}] overlap`).toBe(false); + } + } + }, + ); + + it( + "returns an error when createRichMenu API fails", + { timeout: 10000 }, + async () => { + vi.mocked(mockLineClient.createRichMenu).mockRejectedValue( + new Error("API error"), + ); + + const result = await client.callTool({ + name: "create_rich_menu", + arguments: { + chatBarText: "My Menu", + actions: [{ type: "message", label: "Action 1", text: "action1" }], + }, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("API error"); + }, + ); +}); diff --git a/test/tools/deleteRichMenu.test.ts b/test/tools/deleteRichMenu.test.ts new file mode 100644 index 00000000..71c56775 --- /dev/null +++ b/test/tools/deleteRichMenu.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import DeleteRichMenu from "../../src/tools/deleteRichMenu.js"; + +describe("delete_rich_menu tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new DeleteRichMenu(mockLineClient).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it("calls deleteRichMenu with the correct richMenuId", async () => { + vi.mocked(mockLineClient.deleteRichMenu).mockResolvedValue({} as never); + + const result = await client.callTool({ + name: "delete_rich_menu", + arguments: { richMenuId: "richmenu-123" }, + }); + + expect(mockLineClient.deleteRichMenu).toHaveBeenCalledWith("richmenu-123"); + expect(result.isError).toBeFalsy(); + }); + + it("returns an error response when LINE API fails", async () => { + vi.mocked(mockLineClient.deleteRichMenu).mockRejectedValue( + new Error("Not found"), + ); + + const result = await client.callTool({ + name: "delete_rich_menu", + arguments: { richMenuId: "richmenu-unknown" }, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("Failed to delete rich menu"); + }); +}); diff --git a/test/tools/getFollowerIds.test.ts b/test/tools/getFollowerIds.test.ts new file mode 100644 index 00000000..cde960d2 --- /dev/null +++ b/test/tools/getFollowerIds.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import GetFollowerIds from "../../src/tools/getFollowerIds.js"; + +describe("get_follower_ids tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new GetFollowerIds(mockLineClient).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it("calls getFollowers with the correct arguments", async () => { + const followerData = { + userIds: ["U_USER_1", "U_USER_2"], + next: "continuation_token", + }; + vi.mocked(mockLineClient.getFollowers).mockResolvedValue( + followerData as never, + ); + + const result = await client.callTool({ + name: "get_follower_ids", + arguments: { start: "token_abc", limit: 100 }, + }); + + expect(mockLineClient.getFollowers).toHaveBeenCalledWith("token_abc", 100); + expect(result.isError).toBeFalsy(); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(JSON.parse(text)).toEqual(followerData); + }); + + it("calls getFollowers without optional parameters", async () => { + vi.mocked(mockLineClient.getFollowers).mockResolvedValue({ + userIds: [], + } as never); + + const result = await client.callTool({ + name: "get_follower_ids", + arguments: {}, + }); + + expect(mockLineClient.getFollowers).toHaveBeenCalledWith( + undefined, + undefined, + ); + expect(result.isError).toBeFalsy(); + }); + + it("returns an error response when LINE API fails", async () => { + vi.mocked(mockLineClient.getFollowers).mockRejectedValue( + new Error("Forbidden"), + ); + + const result = await client.callTool({ + name: "get_follower_ids", + arguments: {}, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("Failed to get follower IDs"); + }); +}); diff --git a/test/tools/getMessageQuota.test.ts b/test/tools/getMessageQuota.test.ts new file mode 100644 index 00000000..d4d36abd --- /dev/null +++ b/test/tools/getMessageQuota.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import GetMessageQuota from "../../src/tools/getMessageQuota.js"; + +describe("get_message_quota tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new GetMessageQuota(mockLineClient).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it("returns quota and consumption data", async () => { + vi.mocked(mockLineClient.getMessageQuota).mockResolvedValue({ + type: "limited", + value: 1000, + } as never); + vi.mocked(mockLineClient.getMessageQuotaConsumption).mockResolvedValue({ + totalUsage: 250, + } as never); + + const result = await client.callTool({ + name: "get_message_quota", + arguments: {}, + }); + + expect(mockLineClient.getMessageQuota).toHaveBeenCalled(); + expect(mockLineClient.getMessageQuotaConsumption).toHaveBeenCalled(); + expect(result.isError).toBeFalsy(); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(JSON.parse(text)).toEqual({ limited: 1000, totalUsage: 250 }); + }); + + it("returns an error when LINE API fails", async () => { + vi.mocked(mockLineClient.getMessageQuota).mockRejectedValue( + new Error("API error"), + ); + + const result = await client.callTool({ + name: "get_message_quota", + arguments: {}, + }); + + expect(result.isError).toBe(true); + }); +}); diff --git a/test/tools/getProfile.test.ts b/test/tools/getProfile.test.ts new file mode 100644 index 00000000..9bdda998 --- /dev/null +++ b/test/tools/getProfile.test.ts @@ -0,0 +1,105 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import GetProfile from "../../src/tools/getProfile.js"; + +const DESTINATION_ID = "U_DEFAULT_USER"; + +describe("get_profile tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new GetProfile(mockLineClient, DESTINATION_ID).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it("calls getProfile with the correct userId", async () => { + const profileData = { + displayName: "Test User", + userId: "U_EXPLICIT_USER", + pictureUrl: "https://example.com/pic.jpg", + statusMessage: "Hello", + }; + vi.mocked(mockLineClient.getProfile).mockResolvedValue( + profileData as never, + ); + + const result = await client.callTool({ + name: "get_profile", + arguments: { userId: "U_EXPLICIT_USER" }, + }); + + expect(mockLineClient.getProfile).toHaveBeenCalledWith("U_EXPLICIT_USER"); + expect(result.isError).toBeFalsy(); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(JSON.parse(text)).toEqual(profileData); + }); + + it("uses default destinationId when userId is omitted", async () => { + vi.mocked(mockLineClient.getProfile).mockResolvedValue({} as never); + + await client.callTool({ + name: "get_profile", + arguments: {}, + }); + + expect(mockLineClient.getProfile).toHaveBeenCalledWith(DESTINATION_ID); + }); + + it("returns an error response when LINE API fails", async () => { + vi.mocked(mockLineClient.getProfile).mockRejectedValue( + new Error("Not found"), + ); + + const result = await client.callTool({ + name: "get_profile", + arguments: { userId: "U_UNKNOWN" }, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("Failed to get profile"); + }); + + it("returns an error when userId is empty and no default is set", async () => { + const emptyServer = new McpServer({ name: "test", version: "0.0.1" }); + new GetProfile(mockLineClient, "").register(emptyServer); + + const [ct, st] = InMemoryTransport.createLinkedPair(); + const emptyClient = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([emptyClient.connect(ct), emptyServer.connect(st)]); + + const result = await emptyClient.callTool({ + name: "get_profile", + arguments: {}, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("userId"); + + await emptyClient.close(); + await emptyServer.close(); + }); +}); diff --git a/test/tools/getRichMenu.test.ts b/test/tools/getRichMenu.test.ts new file mode 100644 index 00000000..7d9baf0f --- /dev/null +++ b/test/tools/getRichMenu.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import GetRichMenu from "../../src/tools/getRichMenu.js"; + +describe("get_rich_menu tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new GetRichMenu(mockLineClient).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it("calls getRichMenu with the correct richMenuId and returns the rich menu", async () => { + const richMenu = { + richMenuId: "richmenu-123", + size: { width: 2500, height: 1686 }, + chatBarText: "Menu", + areas: [], + }; + vi.mocked(mockLineClient.getRichMenu).mockResolvedValue(richMenu as never); + + const result = await client.callTool({ + name: "get_rich_menu", + arguments: { richMenuId: "richmenu-123" }, + }); + + expect(mockLineClient.getRichMenu).toHaveBeenCalledWith("richmenu-123"); + expect(result.isError).toBeFalsy(); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(JSON.parse(text)).toEqual(richMenu); + }); + + it("returns an error response when LINE API fails", async () => { + vi.mocked(mockLineClient.getRichMenu).mockRejectedValue( + new Error("Not found"), + ); + + const result = await client.callTool({ + name: "get_rich_menu", + arguments: { richMenuId: "richmenu-unknown" }, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("Failed to get rich menu"); + }); +}); diff --git a/test/tools/getRichMenuList.test.ts b/test/tools/getRichMenuList.test.ts new file mode 100644 index 00000000..4154c9de --- /dev/null +++ b/test/tools/getRichMenuList.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import GetRichMenuList from "../../src/tools/getRichMenuList.js"; + +describe("get_rich_menu_list tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new GetRichMenuList(mockLineClient).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it("returns the list of rich menus", async () => { + const richMenus = { + richmenus: [ + { richMenuId: "rm-1", name: "Menu 1" }, + { richMenuId: "rm-2", name: "Menu 2" }, + ], + }; + vi.mocked(mockLineClient.getRichMenuList).mockResolvedValue( + richMenus as never, + ); + + const result = await client.callTool({ + name: "get_rich_menu_list", + arguments: {}, + }); + + expect(mockLineClient.getRichMenuList).toHaveBeenCalled(); + expect(result.isError).toBeFalsy(); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(JSON.parse(text)).toEqual(richMenus); + }); + + it("returns an error response when LINE API fails", async () => { + vi.mocked(mockLineClient.getRichMenuList).mockRejectedValue( + new Error("API error"), + ); + + const result = await client.callTool({ + name: "get_rich_menu_list", + arguments: {}, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("Failed to get rich menu list"); + }); +}); diff --git a/test/tools/pushFlexMessage.test.ts b/test/tools/pushFlexMessage.test.ts new file mode 100644 index 00000000..e0ecd4e5 --- /dev/null +++ b/test/tools/pushFlexMessage.test.ts @@ -0,0 +1,137 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import PushFlexMessage from "../../src/tools/pushFlexMessage.js"; + +const DESTINATION_ID = "U_DEFAULT_USER"; + +const SAMPLE_FLEX_MESSAGE = { + type: "flex", + altText: "Test flex message", + contents: { + type: "bubble", + body: { + type: "box", + layout: "vertical", + contents: [{ type: "text", text: "Hello" }], + }, + }, +}; + +// Zod schema applies default values (e.g. wrap: true for text elements) +const EXPECTED_FLEX_MESSAGE = { + type: "flex", + altText: "Test flex message", + contents: { + type: "bubble", + body: { + type: "box", + layout: "vertical", + contents: [{ type: "text", text: "Hello", wrap: true }], + }, + }, +}; + +describe("push_flex_message tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new PushFlexMessage(mockLineClient, DESTINATION_ID).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it("calls pushMessage with the correct arguments", async () => { + vi.mocked(mockLineClient.pushMessage).mockResolvedValue({} as never); + + const result = await client.callTool({ + name: "push_flex_message", + arguments: { + userId: "U_EXPLICIT_USER", + message: SAMPLE_FLEX_MESSAGE, + }, + }); + + expect(mockLineClient.pushMessage).toHaveBeenCalledWith({ + to: "U_EXPLICIT_USER", + messages: [EXPECTED_FLEX_MESSAGE], + }); + expect(result.isError).toBeFalsy(); + }); + + it("uses default destinationId when userId is omitted", async () => { + vi.mocked(mockLineClient.pushMessage).mockResolvedValue({} as never); + + await client.callTool({ + name: "push_flex_message", + arguments: { + message: SAMPLE_FLEX_MESSAGE, + }, + }); + + expect(mockLineClient.pushMessage).toHaveBeenCalledWith({ + to: DESTINATION_ID, + messages: [EXPECTED_FLEX_MESSAGE], + }); + }); + + it("returns an error response when LINE API fails", async () => { + vi.mocked(mockLineClient.pushMessage).mockRejectedValue( + new Error("API error"), + ); + + const result = await client.callTool({ + name: "push_flex_message", + arguments: { + userId: "U_USER", + message: SAMPLE_FLEX_MESSAGE, + }, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("Failed to push flex message"); + }); + + it("returns an error when userId is empty and no default is set", async () => { + const emptyServer = new McpServer({ name: "test", version: "0.0.1" }); + new PushFlexMessage(mockLineClient, "").register(emptyServer); + + const [ct, st] = InMemoryTransport.createLinkedPair(); + const emptyClient = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([emptyClient.connect(ct), emptyServer.connect(st)]); + + const result = await emptyClient.callTool({ + name: "push_flex_message", + arguments: { + message: SAMPLE_FLEX_MESSAGE, + }, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("userId"); + + await emptyClient.close(); + await emptyServer.close(); + }); +}); diff --git a/test/tools/pushTextMessage.test.ts b/test/tools/pushTextMessage.test.ts new file mode 100644 index 00000000..54685e84 --- /dev/null +++ b/test/tools/pushTextMessage.test.ts @@ -0,0 +1,110 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import PushTextMessage from "../../src/tools/pushTextMessage.js"; + +const DESTINATION_ID = "U_DEFAULT_USER"; + +describe("push_text_message tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new PushTextMessage(mockLineClient, DESTINATION_ID).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it("calls pushMessage with the correct arguments", async () => { + vi.mocked(mockLineClient.pushMessage).mockResolvedValue({} as never); + + const result = await client.callTool({ + name: "push_text_message", + arguments: { + userId: "U_EXPLICIT_USER", + message: { type: "text", text: "hello" }, + }, + }); + + expect(mockLineClient.pushMessage).toHaveBeenCalledWith({ + to: "U_EXPLICIT_USER", + messages: [{ type: "text", text: "hello" }], + }); + expect(result.isError).toBeFalsy(); + }); + + it("uses default destinationId when userId is omitted", async () => { + vi.mocked(mockLineClient.pushMessage).mockResolvedValue({} as never); + + await client.callTool({ + name: "push_text_message", + arguments: { + message: { type: "text", text: "hello" }, + }, + }); + + expect(mockLineClient.pushMessage).toHaveBeenCalledWith( + expect.objectContaining({ to: DESTINATION_ID }), + ); + }); + + it("returns an error response when LINE API fails", async () => { + vi.mocked(mockLineClient.pushMessage).mockRejectedValue( + new Error("API error"), + ); + + const result = await client.callTool({ + name: "push_text_message", + arguments: { + userId: "U_USER", + message: { type: "text", text: "hello" }, + }, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("Failed to push message"); + }); + + it("returns an error when userId is empty and no default is set", async () => { + // Create a new server with empty destinationId + const emptyServer = new McpServer({ name: "test", version: "0.0.1" }); + new PushTextMessage(mockLineClient, "").register(emptyServer); + + const [ct, st] = InMemoryTransport.createLinkedPair(); + const emptyClient = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([emptyClient.connect(ct), emptyServer.connect(st)]); + + const result = await emptyClient.callTool({ + name: "push_text_message", + arguments: { + message: { type: "text", text: "hello" }, + }, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("userId"); + + await emptyClient.close(); + await emptyServer.close(); + }); +}); diff --git a/test/tools/setRichMenuDefault.test.ts b/test/tools/setRichMenuDefault.test.ts new file mode 100644 index 00000000..8e153f8f --- /dev/null +++ b/test/tools/setRichMenuDefault.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import SetRichMenuDefault from "../../src/tools/setRichMenuDefault.js"; + +describe("set_rich_menu_default tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new SetRichMenuDefault(mockLineClient).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + }); + + it("calls setDefaultRichMenu with the correct richMenuId", async () => { + vi.mocked(mockLineClient.setDefaultRichMenu).mockResolvedValue({} as never); + + const result = await client.callTool({ + name: "set_rich_menu_default", + arguments: { richMenuId: "richmenu-123" }, + }); + + expect(mockLineClient.setDefaultRichMenu).toHaveBeenCalledWith( + "richmenu-123", + ); + expect(result.isError).toBeFalsy(); + }); + + it("returns an error when LINE API fails", async () => { + vi.mocked(mockLineClient.setDefaultRichMenu).mockRejectedValue( + new Error("API error"), + ); + + const result = await client.callTool({ + name: "set_rich_menu_default", + arguments: { richMenuId: "richmenu-unknown" }, + }); + + expect(result.isError).toBe(true); + }); +}); diff --git a/test/tools/updateRichMenuImage.test.ts b/test/tools/updateRichMenuImage.test.ts new file mode 100644 index 00000000..5cb87ff7 --- /dev/null +++ b/test/tools/updateRichMenuImage.test.ts @@ -0,0 +1,266 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { createMockLineBotClient } from "../helpers/mock-line-clients.js"; +import fs from "fs"; +import os from "os"; +import path from "path"; +import UpdateRichMenuImage from "../../src/tools/updateRichMenuImage.js"; + +// Build a minimal PNG buffer whose IHDR header advertises the given dimensions. +// The pixel data is not real; only the header bytes are read by the tool. +function makePngBuffer(width: number, height: number): Buffer { + const buffer = Buffer.alloc(33); + // PNG signature + Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(buffer, 0); + // IHDR length + type (not strictly parsed, kept for realism) + buffer.writeUInt32BE(13, 8); + buffer.write("IHDR", 12, "ascii"); + buffer.writeUInt32BE(width, 16); + buffer.writeUInt32BE(height, 20); + return buffer; +} + +function writeTempPng(width: number, height: number): string { + const filePath = path.join( + os.tmpdir(), + `update-rich-menu-image-test-${Date.now()}-${Math.random().toString(36).slice(2)}.png`, + ); + fs.writeFileSync(filePath, makePngBuffer(width, height)); + return filePath; +} + +const OLD_RICH_MENU = { + richMenuId: "richmenu-old-123", + size: { width: 1600, height: 910 }, + selected: true, + name: "My Menu", + chatBarText: "Menu", + areas: [ + { + bounds: { x: 0, y: 0, width: 1600, height: 910 }, + action: { type: "message", text: "hi" }, + }, + ], +}; + +describe("update_rich_menu_image tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + const tempFiles: string[] = []; + + function tempPng(width: number, height: number): string { + const p = writeTempPng(width, height); + tempFiles.push(p); + return p; + } + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new UpdateRichMenuImage(mockLineClient).register(server); + + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + client = new Client({ name: "test-client", version: "0.0.1" }); + await Promise.all([ + client.connect(clientTransport), + server.connect(serverTransport), + ]); + }); + + afterEach(async () => { + await client?.close(); + await server?.close(); + for (const f of tempFiles.splice(0)) { + try { + fs.unlinkSync(f); + } catch { + // ignore + } + } + }); + + function parseResult(result: Awaited>) { + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + return JSON.parse(text); + } + + it("clones the menu, uploads the image, switches default and deletes the old menu", async () => { + vi.mocked(mockLineClient.getRichMenu).mockResolvedValue( + OLD_RICH_MENU as never, + ); + vi.mocked(mockLineClient.createRichMenu).mockResolvedValue({ + richMenuId: "richmenu-new-456", + } as never); + vi.mocked(mockLineClient.setRichMenuImage).mockResolvedValue({} as never); + vi.mocked(mockLineClient.getDefaultRichMenuId).mockResolvedValue({ + richMenuId: "richmenu-old-123", + } as never); + vi.mocked(mockLineClient.setDefaultRichMenu).mockResolvedValue({} as never); + vi.mocked(mockLineClient.deleteRichMenu).mockResolvedValue({} as never); + + const imagePath = tempPng(1600, 910); + const result = await client.callTool({ + name: "update_rich_menu_image", + arguments: { richMenuId: "richmenu-old-123", imagePath }, + }); + + expect(result.isError).toBeFalsy(); + expect(mockLineClient.createRichMenu).toHaveBeenCalledWith( + expect.objectContaining({ + name: "My Menu", + chatBarText: "Menu", + selected: true, + size: { width: 1600, height: 910 }, + }), + ); + expect(mockLineClient.setRichMenuImage).toHaveBeenCalledWith( + "richmenu-new-456", + expect.any(Blob), + ); + expect(mockLineClient.setDefaultRichMenu).toHaveBeenCalledWith( + "richmenu-new-456", + ); + expect(mockLineClient.deleteRichMenu).toHaveBeenCalledWith( + "richmenu-old-123", + ); + + const parsed = parseResult(result); + expect(parsed.oldRichMenuId).toBe("richmenu-old-123"); + expect(parsed.newRichMenuId).toBe("richmenu-new-456"); + expect(parsed.defaultSwitched).toBe(true); + expect(parsed.oldRichMenuDeleted).toBe(true); + }); + + it("does not switch the default when the old menu was not the default", async () => { + vi.mocked(mockLineClient.getRichMenu).mockResolvedValue( + OLD_RICH_MENU as never, + ); + vi.mocked(mockLineClient.createRichMenu).mockResolvedValue({ + richMenuId: "richmenu-new-456", + } as never); + vi.mocked(mockLineClient.setRichMenuImage).mockResolvedValue({} as never); + vi.mocked(mockLineClient.getDefaultRichMenuId).mockResolvedValue({ + richMenuId: "richmenu-other-999", + } as never); + vi.mocked(mockLineClient.deleteRichMenu).mockResolvedValue({} as never); + + const imagePath = tempPng(1600, 910); + const result = await client.callTool({ + name: "update_rich_menu_image", + arguments: { richMenuId: "richmenu-old-123", imagePath }, + }); + + expect(result.isError).toBeFalsy(); + expect(mockLineClient.setDefaultRichMenu).not.toHaveBeenCalled(); + const parsed = parseResult(result); + expect(parsed.defaultSwitched).toBe(false); + expect(parsed.oldRichMenuDeleted).toBe(true); + }); + + it("does not delete the old menu when deleteOldRichMenu is false", async () => { + vi.mocked(mockLineClient.getRichMenu).mockResolvedValue( + OLD_RICH_MENU as never, + ); + vi.mocked(mockLineClient.createRichMenu).mockResolvedValue({ + richMenuId: "richmenu-new-456", + } as never); + vi.mocked(mockLineClient.setRichMenuImage).mockResolvedValue({} as never); + vi.mocked(mockLineClient.getDefaultRichMenuId).mockResolvedValue({ + richMenuId: "richmenu-other-999", + } as never); + + const imagePath = tempPng(1600, 910); + const result = await client.callTool({ + name: "update_rich_menu_image", + arguments: { + richMenuId: "richmenu-old-123", + imagePath, + deleteOldRichMenu: false, + }, + }); + + expect(result.isError).toBeFalsy(); + expect(mockLineClient.deleteRichMenu).not.toHaveBeenCalled(); + const parsed = parseResult(result); + expect(parsed.oldRichMenuDeleted).toBe(false); + }); + + it("returns an actionable error and does not create a menu when image size mismatches", async () => { + vi.mocked(mockLineClient.getRichMenu).mockResolvedValue( + OLD_RICH_MENU as never, + ); + + // Passes width/height/aspect-ratio checks but does not match the 1600x910 menu. + const imagePath = tempPng(2000, 910); + const result = await client.callTool({ + name: "update_rich_menu_image", + arguments: { richMenuId: "richmenu-old-123", imagePath }, + }); + + expect(result.isError).toBe(true); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("2000x910"); + expect(text).toContain("1600x910"); + expect(mockLineClient.createRichMenu).not.toHaveBeenCalled(); + }); + + it("rolls back the newly created menu when the image upload fails", async () => { + vi.mocked(mockLineClient.getRichMenu).mockResolvedValue( + OLD_RICH_MENU as never, + ); + vi.mocked(mockLineClient.createRichMenu).mockResolvedValue({ + richMenuId: "richmenu-new-456", + } as never); + vi.mocked(mockLineClient.setRichMenuImage).mockRejectedValue( + new Error("upload failed"), + ); + vi.mocked(mockLineClient.deleteRichMenu).mockResolvedValue({} as never); + + const imagePath = tempPng(1600, 910); + const result = await client.callTool({ + name: "update_rich_menu_image", + arguments: { richMenuId: "richmenu-old-123", imagePath }, + }); + + expect(result.isError).toBe(true); + expect(mockLineClient.deleteRichMenu).toHaveBeenCalledWith( + "richmenu-new-456", + ); + const text = (result.content as Array<{ type: string; text: string }>)[0] + .text; + expect(text).toContain("upload failed"); + expect(JSON.parse(text).rolledBack).toBe(true); + }); + + it("continues when getDefaultRichMenuId throws (no default set)", async () => { + vi.mocked(mockLineClient.getRichMenu).mockResolvedValue( + OLD_RICH_MENU as never, + ); + vi.mocked(mockLineClient.createRichMenu).mockResolvedValue({ + richMenuId: "richmenu-new-456", + } as never); + vi.mocked(mockLineClient.setRichMenuImage).mockResolvedValue({} as never); + vi.mocked(mockLineClient.getDefaultRichMenuId).mockRejectedValue( + new Error("404 not found"), + ); + vi.mocked(mockLineClient.deleteRichMenu).mockResolvedValue({} as never); + + const imagePath = tempPng(1600, 910); + const result = await client.callTool({ + name: "update_rich_menu_image", + arguments: { richMenuId: "richmenu-old-123", imagePath }, + }); + + expect(result.isError).toBeFalsy(); + expect(mockLineClient.setDefaultRichMenu).not.toHaveBeenCalled(); + const parsed = parseResult(result); + expect(parsed.defaultSwitched).toBe(false); + expect(parsed.oldRichMenuDeleted).toBe(true); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 0be3fba6..3ba0ec6d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,8 @@ "exclude": [ "node_modules", "dist", - ".git" + ".git", + "test", + "vitest.config.ts" ], } diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 00000000..ed1df42e --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + }, + "include": ["src", "test"], + "exclude": ["node_modules", "dist", ".git"] +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..5fab2394 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/**/*.test.ts"], + testTimeout: 30_000, + }, +});