├── .dockerignore ├── .env.example ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── smithery.config.json ├── smithery.json ├── smithery.yaml └── src ├── index.js ├── modules ├── api-key-manager.js ├── post-generator.js ├── transcript-extractor.js └── transcript-summarizer.js └── server.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .env 4 | .git 5 | .gitignore 6 | README.md 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # OpenAI API Key 2 | OPENAI_API_KEY=your_openai_api_key_here 3 | 4 | # YouTube API Key (optional, used as fallback) 5 | YOUTUBE_API_KEY=your_youtube_api_key_here 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | jspm_packages/ 4 | 5 | # Environment variables 6 | .env 7 | .env.local 8 | .env.development.local 9 | .env.test.local 10 | .env.production.local 11 | 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | lerna-debug.log* 19 | .pnpm-debug.log* 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # Build outputs 26 | dist/ 27 | build/ 28 | out/ 29 | 30 | # Cache directories 31 | .npm 32 | .eslintcache 33 | .stylelintcache 34 | .rpt2_cache/ 35 | .rts2_cache_cjs/ 36 | .rts2_cache_es/ 37 | .rts2_cache_umd/ 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | 42 | # Output of 'npm pack' 43 | *.tgz 44 | 45 | # Yarn Integrity file 46 | .yarn-integrity 47 | 48 | # dotenv environment variable files 49 | .env 50 | .env.development.local 51 | .env.test.local 52 | .env.production.local 53 | .env.local 54 | 55 | # parcel-bundler cache 56 | .cache 57 | .parcel-cache 58 | 59 | # Next.js build output 60 | .next 61 | out 62 | 63 | # Nuxt.js build / generate output 64 | .nuxt 65 | dist 66 | 67 | # Gatsby files 68 | .cache/ 69 | public 70 | 71 | # vuepress build output 72 | .vuepress/dist 73 | 74 | # vuepress v2.x temp and cache directory 75 | .temp 76 | .cache 77 | 78 | # Docusaurus cache and generated files 79 | .docusaurus 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | 90 | # TernJS port file 91 | .tern-port 92 | 93 | # Stores VSCode versions used for testing VSCode extensions 94 | .vscode-test 95 | 96 | # yarn v2 97 | .yarn/cache 98 | .yarn/unplugged 99 | .yarn/build-state.yml 100 | .yarn/install-state.gz 101 | .pnp.* 102 | 103 | # IDE specific files 104 | .idea/ 105 | .vscode/ 106 | *.swp 107 | *.swo 108 | .DS_Store 109 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-slim 2 | 3 | WORKDIR /app 4 | 5 | # Copy package files and install dependencies 6 | COPY package*.json ./ 7 | RUN npm install 8 | 9 | # Copy application code 10 | COPY . . 11 | 12 | # Set executable permissions for the entry point 13 | RUN chmod +x src/index.js 14 | 15 | # Expose the port the app runs on 16 | EXPOSE 8000 17 | 18 | # Set the entry point 19 | ENTRYPOINT ["node", "src/index.js"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Anirudh Nuti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LinkedIn Post Generator 2 | 3 | [![smithery badge](https://smithery.ai/badge/@NvkAnirudh/linkedin-post-generator)](https://smithery.ai/server/@NvkAnirudh/linkedin-post-generator) 4 | 5 | A Model Context Protocol (MCP) server that automates generating professional LinkedIn post drafts from YouTube videos. This tool streamlines content repurposing by extracting transcripts from YouTube videos, summarizing the content, and generating engaging LinkedIn posts tailored to your preferences. 6 | 7 | 8 | 9 | 10 | 11 | ## Table of Contents 12 | - [Features](#features) 13 | - [Installation](#installation) 14 | - [Local Development](#local-development) 15 | - [Using with Claude Desktop](#using-with-claude-desktop) 16 | - [Configuration](#configuration) 17 | - [Usage](#usage) 18 | - [Available Tools](#available-tools) 19 | - [Workflow Example](#workflow-example) 20 | - [Deployment](#deployment) 21 | - [License](#license) 22 | 23 | ## Features 24 | 25 | - **YouTube Transcript Extraction**: Automatically extract transcripts from any YouTube video 26 | - **Content Summarization**: Generate concise summaries with customizable tone and target audience 27 | - **LinkedIn Post Generation**: Create professional LinkedIn posts with customizable style and tone 28 | - **All-in-One Workflow**: Go from YouTube URL to LinkedIn post in a single operation 29 | - **Customization Options**: Adjust tone, audience, word count, and more to match your personal brand 30 | - **MCP Integration**: Works seamlessly with AI assistants that support the Model Context Protocol 31 | 32 | ## Installation 33 | 34 | ### Local Development 35 | 36 | 1. Clone the repository: 37 | ```bash 38 | git clone https://github.com/NvkAnirudh/LinkedIn-Post-Generator.git 39 | cd LinkedIn-Post-Generator 40 | ``` 41 | 42 | 2. Install dependencies: 43 | ```bash 44 | npm install 45 | ``` 46 | 47 | 3. Create a `.env` file based on the example: 48 | ```bash 49 | cp .env.example .env 50 | ``` 51 | 52 | 4. Add your API keys to the `.env` file: 53 | ``` 54 | OPENAI_API_KEY=your_openai_api_key 55 | YOUTUBE_API_KEY=your_youtube_api_key 56 | ``` 57 | 58 | 5. Run the server: 59 | ```bash 60 | npm run dev 61 | ``` 62 | 63 | 6. Test with MCP Inspector: 64 | ```bash 65 | npm run inspect 66 | ``` 67 | 68 | ### Using with Claude Desktop 69 | 70 | This MCP server is designed to work with Claude Desktop and other AI assistants that support the Model Context Protocol. To use it with Claude Desktop: 71 | 72 | 1. Configure Claude Desktop by editing the configuration file at `~/Library/Application Support/Claude/claude_desktop_config.json` (Mac) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): 73 | 74 | ```json 75 | { 76 | "mcpServers": { 77 | "linkedin-post-generator": { 78 | "command": "npx", 79 | "args": [ 80 | "-y", 81 | "@smithery/cli@latest", 82 | "run", 83 | "@NvkAnirudh/linkedin-post-generator", 84 | "--key", 85 | "YOUR_SMITHERY_API_KEY", 86 | "--config", 87 | "{\"OPENAI_API_KEY\":\"YOUR_OPENAI_API_KEY\",\"YOUTUBE_API_KEY\":\"YOUR_YOUTUBE_API_KEY\"}", 88 | "--transport", 89 | "stdio" 90 | ] 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | Replace: 97 | - `YOUR_SMITHERY_API_KEY` with your Smithery API key 98 | - `YOUR_OPENAI_API_KEY` with your OpenAI API key 99 | - `YOUR_YOUTUBE_API_KEY` with your YouTube API key (optional) 100 | 101 | 2. Restart Claude Desktop 102 | 103 | 3. In Claude Desktop, you can now access the LinkedIn Post Generator tools without needing to set API keys again 104 | 105 | ## Configuration 106 | 107 | The application requires API keys to function properly: 108 | 109 | 1. **OpenAI API Key** (required): Used for content summarization and post generation 110 | 2. **YouTube API Key** (optional): Enhances YouTube metadata retrieval 111 | 112 | You can provide these keys in three ways: 113 | 114 | ### 1. Via Claude Desktop Configuration (Recommended) 115 | 116 | When using with Claude Desktop and Smithery, the best approach is to include your API keys in the Claude Desktop configuration file as shown in the [Using with Claude Desktop](#using-with-claude-desktop) section. This way, the keys are automatically passed to the MCP server, and you don't need to set them again. 117 | 118 | ### 2. As Environment Variables 119 | 120 | When running locally, you can set API keys as environment variables in a `.env` file: 121 | ``` 122 | OPENAI_API_KEY=your_openai_api_key 123 | YOUTUBE_API_KEY=your_youtube_api_key 124 | ``` 125 | 126 | ### 3. Using the Set API Keys Tool 127 | 128 | If you haven't provided API keys through the configuration or environment variables, you can set them directly through the MCP interface using the `set_api_keys` tool. 129 | 130 | ## Usage 131 | 132 | ### Available Tools 133 | 134 | #### Set API Keys 135 | - Tool: `set_api_keys` 136 | - Purpose: Configure your API keys 137 | - Parameters: 138 | - `openaiApiKey`: Your OpenAI API key (required) 139 | - `youtubeApiKey`: Your YouTube API key (optional) 140 | 141 | #### Check API Keys 142 | - Tool: `check_api_keys` 143 | - Purpose: Verify your API key configuration status 144 | 145 | #### Extract Transcript 146 | - Tool: `extract_transcript` 147 | - Purpose: Get the transcript from a YouTube video 148 | - Parameters: 149 | - `youtubeUrl`: URL of the YouTube video 150 | 151 | #### Summarize Transcript 152 | - Tool: `summarize_transcript` 153 | - Purpose: Create a concise summary of the video content 154 | - Parameters: 155 | - `transcript`: The video transcript text 156 | - `tone`: Educational, inspirational, professional, or conversational 157 | - `audience`: General, technical, business, or academic 158 | - `wordCount`: Approximate word count for the summary (100-300) 159 | 160 | #### Generate LinkedIn Post 161 | - Tool: `generate_linkedin_post` 162 | - Purpose: Create a LinkedIn post from a summary 163 | - Parameters: 164 | - `summary`: Summary of the video content 165 | - `videoTitle`: Title of the YouTube video 166 | - `speakerName`: Name of the speaker (optional) 167 | - `hashtags`: Relevant hashtags (optional) 168 | - `tone`: First-person, third-person, or thought-leader 169 | - `includeCallToAction`: Whether to include a call to action 170 | 171 | #### All-in-One: YouTube to LinkedIn Post 172 | - Tool: `youtube_to_linkedin_post` 173 | - Purpose: Complete workflow from YouTube URL to LinkedIn post 174 | - Parameters: 175 | - `youtubeUrl`: YouTube video URL 176 | - `tone`: Desired tone for the post 177 | - Plus additional customization options 178 | 179 | ### Workflow Example 180 | 181 | 1. Set your API keys using the `set_api_keys` tool 182 | 2. Use the `youtube_to_linkedin_post` tool with a YouTube URL 183 | 3. Receive a complete LinkedIn post draft ready to publish 184 | 185 | ## Deployment 186 | 187 | This server is deployed on [Smithery](https://smithery.ai), a platform for hosting and sharing MCP servers. The deployment configuration is defined in the `smithery.yaml` file. 188 | 189 | To deploy your own instance: 190 | 191 | 1. Create an account on Smithery 192 | 2. Install the Smithery CLI: 193 | ```bash 194 | npm install -g @smithery/cli 195 | ``` 196 | 3. Deploy the server: 197 | ```bash 198 | smithery deploy 199 | ``` 200 | 201 | ## Contributing 202 | 203 | Contributions are welcome and appreciated! Here's how you can contribute to the LinkedIn Post Generator: 204 | 205 | ### Reporting Issues 206 | 207 | - Use the [GitHub issue tracker](https://github.com/NvkAnirudh/LinkedIn-Post-Generator/issues) to report bugs or suggest features 208 | - Please provide detailed information about the issue, including steps to reproduce, expected behavior, and actual behavior 209 | - Include your environment details (OS, Node.js version, etc.) when reporting bugs 210 | 211 | ### Pull Requests 212 | 213 | 1. Fork the repository 214 | 2. Create a new branch (`git checkout -b feature/your-feature-name`) 215 | 3. Make your changes 216 | 4. Run tests to ensure your changes don't break existing functionality 217 | 5. Commit your changes (`git commit -m 'Add some feature'`) 218 | 6. Push to the branch (`git push origin feature/your-feature-name`) 219 | 7. Open a Pull Request 220 | 221 | ### Development Guidelines 222 | 223 | - Follow the existing code style and conventions 224 | - Write clear, commented code 225 | - Include tests for new features 226 | - Update documentation to reflect your changes 227 | 228 | ### Feature Suggestions 229 | 230 | If you have ideas for new features or improvements: 231 | 232 | 1. Check existing issues to see if your suggestion has already been proposed 233 | 2. If not, open a new issue with the label 'enhancement' 234 | 3. Clearly describe the feature and its potential benefits 235 | 236 | ### Documentation 237 | 238 | Improvements to documentation are always welcome: 239 | 240 | - Fix typos or clarify existing documentation 241 | - Add examples or use cases 242 | - Improve the structure or organization of the documentation 243 | 244 | By contributing to this project, you agree that your contributions will be licensed under the project's MIT License. 245 | 246 | ## License 247 | 248 | [MIT](https://github.com/NvkAnirudh/LinkedIn-Post-Generator/blob/main/LICENSE) 249 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yt-to-linkedin-mcp", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "yt-to-linkedin-mcp", 9 | "version": "1.0.0", 10 | "dependencies": { 11 | "@modelcontextprotocol/sdk": "^1.0.0", 12 | "dotenv": "^16.3.1", 13 | "node-fetch": "^3.3.2", 14 | "openai": "^4.20.1", 15 | "youtube-transcript": "^1.0.6", 16 | "zod": "^3.22.4" 17 | }, 18 | "bin": { 19 | "yt-to-linkedin-mcp": "src/index.js" 20 | } 21 | }, 22 | "node_modules/@modelcontextprotocol/sdk": { 23 | "version": "1.8.0", 24 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.8.0.tgz", 25 | "integrity": "sha512-e06W7SwrontJDHwCawNO5SGxG+nU9AAx+jpHHZqGl/WrDBdWOpvirC+s58VpJTB5QemI4jTRcjWT4Pt3Q1NPQQ==", 26 | "dependencies": { 27 | "content-type": "^1.0.5", 28 | "cors": "^2.8.5", 29 | "cross-spawn": "^7.0.3", 30 | "eventsource": "^3.0.2", 31 | "express": "^5.0.1", 32 | "express-rate-limit": "^7.5.0", 33 | "pkce-challenge": "^4.1.0", 34 | "raw-body": "^3.0.0", 35 | "zod": "^3.23.8", 36 | "zod-to-json-schema": "^3.24.1" 37 | }, 38 | "engines": { 39 | "node": ">=18" 40 | } 41 | }, 42 | "node_modules/@types/node": { 43 | "version": "18.19.86", 44 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.86.tgz", 45 | "integrity": "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ==", 46 | "dependencies": { 47 | "undici-types": "~5.26.4" 48 | } 49 | }, 50 | "node_modules/@types/node-fetch": { 51 | "version": "2.6.12", 52 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", 53 | "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", 54 | "dependencies": { 55 | "@types/node": "*", 56 | "form-data": "^4.0.0" 57 | } 58 | }, 59 | "node_modules/abort-controller": { 60 | "version": "3.0.0", 61 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 62 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 63 | "dependencies": { 64 | "event-target-shim": "^5.0.0" 65 | }, 66 | "engines": { 67 | "node": ">=6.5" 68 | } 69 | }, 70 | "node_modules/accepts": { 71 | "version": "2.0.0", 72 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", 73 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 74 | "dependencies": { 75 | "mime-types": "^3.0.0", 76 | "negotiator": "^1.0.0" 77 | }, 78 | "engines": { 79 | "node": ">= 0.6" 80 | } 81 | }, 82 | "node_modules/agentkeepalive": { 83 | "version": "4.6.0", 84 | "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", 85 | "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", 86 | "dependencies": { 87 | "humanize-ms": "^1.2.1" 88 | }, 89 | "engines": { 90 | "node": ">= 8.0.0" 91 | } 92 | }, 93 | "node_modules/asynckit": { 94 | "version": "0.4.0", 95 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 96 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 97 | }, 98 | "node_modules/body-parser": { 99 | "version": "2.2.0", 100 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", 101 | "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", 102 | "dependencies": { 103 | "bytes": "^3.1.2", 104 | "content-type": "^1.0.5", 105 | "debug": "^4.4.0", 106 | "http-errors": "^2.0.0", 107 | "iconv-lite": "^0.6.3", 108 | "on-finished": "^2.4.1", 109 | "qs": "^6.14.0", 110 | "raw-body": "^3.0.0", 111 | "type-is": "^2.0.0" 112 | }, 113 | "engines": { 114 | "node": ">=18" 115 | } 116 | }, 117 | "node_modules/bytes": { 118 | "version": "3.1.2", 119 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 120 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 121 | "engines": { 122 | "node": ">= 0.8" 123 | } 124 | }, 125 | "node_modules/call-bind-apply-helpers": { 126 | "version": "1.0.2", 127 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 128 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 129 | "dependencies": { 130 | "es-errors": "^1.3.0", 131 | "function-bind": "^1.1.2" 132 | }, 133 | "engines": { 134 | "node": ">= 0.4" 135 | } 136 | }, 137 | "node_modules/call-bound": { 138 | "version": "1.0.4", 139 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 140 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 141 | "dependencies": { 142 | "call-bind-apply-helpers": "^1.0.2", 143 | "get-intrinsic": "^1.3.0" 144 | }, 145 | "engines": { 146 | "node": ">= 0.4" 147 | }, 148 | "funding": { 149 | "url": "https://github.com/sponsors/ljharb" 150 | } 151 | }, 152 | "node_modules/combined-stream": { 153 | "version": "1.0.8", 154 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 155 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 156 | "dependencies": { 157 | "delayed-stream": "~1.0.0" 158 | }, 159 | "engines": { 160 | "node": ">= 0.8" 161 | } 162 | }, 163 | "node_modules/content-disposition": { 164 | "version": "1.0.0", 165 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", 166 | "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 167 | "dependencies": { 168 | "safe-buffer": "5.2.1" 169 | }, 170 | "engines": { 171 | "node": ">= 0.6" 172 | } 173 | }, 174 | "node_modules/content-type": { 175 | "version": "1.0.5", 176 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 177 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 178 | "engines": { 179 | "node": ">= 0.6" 180 | } 181 | }, 182 | "node_modules/cookie": { 183 | "version": "0.7.2", 184 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 185 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 186 | "engines": { 187 | "node": ">= 0.6" 188 | } 189 | }, 190 | "node_modules/cookie-signature": { 191 | "version": "1.2.2", 192 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", 193 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 194 | "engines": { 195 | "node": ">=6.6.0" 196 | } 197 | }, 198 | "node_modules/cors": { 199 | "version": "2.8.5", 200 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 201 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 202 | "dependencies": { 203 | "object-assign": "^4", 204 | "vary": "^1" 205 | }, 206 | "engines": { 207 | "node": ">= 0.10" 208 | } 209 | }, 210 | "node_modules/cross-spawn": { 211 | "version": "7.0.6", 212 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 213 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 214 | "dependencies": { 215 | "path-key": "^3.1.0", 216 | "shebang-command": "^2.0.0", 217 | "which": "^2.0.1" 218 | }, 219 | "engines": { 220 | "node": ">= 8" 221 | } 222 | }, 223 | "node_modules/data-uri-to-buffer": { 224 | "version": "4.0.1", 225 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", 226 | "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", 227 | "engines": { 228 | "node": ">= 12" 229 | } 230 | }, 231 | "node_modules/debug": { 232 | "version": "4.4.0", 233 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 234 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 235 | "dependencies": { 236 | "ms": "^2.1.3" 237 | }, 238 | "engines": { 239 | "node": ">=6.0" 240 | }, 241 | "peerDependenciesMeta": { 242 | "supports-color": { 243 | "optional": true 244 | } 245 | } 246 | }, 247 | "node_modules/delayed-stream": { 248 | "version": "1.0.0", 249 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 250 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 251 | "engines": { 252 | "node": ">=0.4.0" 253 | } 254 | }, 255 | "node_modules/depd": { 256 | "version": "2.0.0", 257 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 258 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 259 | "engines": { 260 | "node": ">= 0.8" 261 | } 262 | }, 263 | "node_modules/dotenv": { 264 | "version": "16.4.7", 265 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", 266 | "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", 267 | "engines": { 268 | "node": ">=12" 269 | }, 270 | "funding": { 271 | "url": "https://dotenvx.com" 272 | } 273 | }, 274 | "node_modules/dunder-proto": { 275 | "version": "1.0.1", 276 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 277 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 278 | "dependencies": { 279 | "call-bind-apply-helpers": "^1.0.1", 280 | "es-errors": "^1.3.0", 281 | "gopd": "^1.2.0" 282 | }, 283 | "engines": { 284 | "node": ">= 0.4" 285 | } 286 | }, 287 | "node_modules/ee-first": { 288 | "version": "1.1.1", 289 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 290 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 291 | }, 292 | "node_modules/encodeurl": { 293 | "version": "2.0.0", 294 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 295 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 296 | "engines": { 297 | "node": ">= 0.8" 298 | } 299 | }, 300 | "node_modules/es-define-property": { 301 | "version": "1.0.1", 302 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 303 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 304 | "engines": { 305 | "node": ">= 0.4" 306 | } 307 | }, 308 | "node_modules/es-errors": { 309 | "version": "1.3.0", 310 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 311 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 312 | "engines": { 313 | "node": ">= 0.4" 314 | } 315 | }, 316 | "node_modules/es-object-atoms": { 317 | "version": "1.1.1", 318 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 319 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 320 | "dependencies": { 321 | "es-errors": "^1.3.0" 322 | }, 323 | "engines": { 324 | "node": ">= 0.4" 325 | } 326 | }, 327 | "node_modules/es-set-tostringtag": { 328 | "version": "2.1.0", 329 | "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 330 | "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 331 | "dependencies": { 332 | "es-errors": "^1.3.0", 333 | "get-intrinsic": "^1.2.6", 334 | "has-tostringtag": "^1.0.2", 335 | "hasown": "^2.0.2" 336 | }, 337 | "engines": { 338 | "node": ">= 0.4" 339 | } 340 | }, 341 | "node_modules/escape-html": { 342 | "version": "1.0.3", 343 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 344 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 345 | }, 346 | "node_modules/etag": { 347 | "version": "1.8.1", 348 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 349 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 350 | "engines": { 351 | "node": ">= 0.6" 352 | } 353 | }, 354 | "node_modules/event-target-shim": { 355 | "version": "5.0.1", 356 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 357 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 358 | "engines": { 359 | "node": ">=6" 360 | } 361 | }, 362 | "node_modules/eventsource": { 363 | "version": "3.0.6", 364 | "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz", 365 | "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==", 366 | "dependencies": { 367 | "eventsource-parser": "^3.0.1" 368 | }, 369 | "engines": { 370 | "node": ">=18.0.0" 371 | } 372 | }, 373 | "node_modules/eventsource-parser": { 374 | "version": "3.0.1", 375 | "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", 376 | "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", 377 | "engines": { 378 | "node": ">=18.0.0" 379 | } 380 | }, 381 | "node_modules/express": { 382 | "version": "5.1.0", 383 | "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", 384 | "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", 385 | "dependencies": { 386 | "accepts": "^2.0.0", 387 | "body-parser": "^2.2.0", 388 | "content-disposition": "^1.0.0", 389 | "content-type": "^1.0.5", 390 | "cookie": "^0.7.1", 391 | "cookie-signature": "^1.2.1", 392 | "debug": "^4.4.0", 393 | "encodeurl": "^2.0.0", 394 | "escape-html": "^1.0.3", 395 | "etag": "^1.8.1", 396 | "finalhandler": "^2.1.0", 397 | "fresh": "^2.0.0", 398 | "http-errors": "^2.0.0", 399 | "merge-descriptors": "^2.0.0", 400 | "mime-types": "^3.0.0", 401 | "on-finished": "^2.4.1", 402 | "once": "^1.4.0", 403 | "parseurl": "^1.3.3", 404 | "proxy-addr": "^2.0.7", 405 | "qs": "^6.14.0", 406 | "range-parser": "^1.2.1", 407 | "router": "^2.2.0", 408 | "send": "^1.1.0", 409 | "serve-static": "^2.2.0", 410 | "statuses": "^2.0.1", 411 | "type-is": "^2.0.1", 412 | "vary": "^1.1.2" 413 | }, 414 | "engines": { 415 | "node": ">= 18" 416 | }, 417 | "funding": { 418 | "type": "opencollective", 419 | "url": "https://opencollective.com/express" 420 | } 421 | }, 422 | "node_modules/express-rate-limit": { 423 | "version": "7.5.0", 424 | "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", 425 | "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", 426 | "engines": { 427 | "node": ">= 16" 428 | }, 429 | "funding": { 430 | "url": "https://github.com/sponsors/express-rate-limit" 431 | }, 432 | "peerDependencies": { 433 | "express": "^4.11 || 5 || ^5.0.0-beta.1" 434 | } 435 | }, 436 | "node_modules/fetch-blob": { 437 | "version": "3.2.0", 438 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", 439 | "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", 440 | "funding": [ 441 | { 442 | "type": "github", 443 | "url": "https://github.com/sponsors/jimmywarting" 444 | }, 445 | { 446 | "type": "paypal", 447 | "url": "https://paypal.me/jimmywarting" 448 | } 449 | ], 450 | "dependencies": { 451 | "node-domexception": "^1.0.0", 452 | "web-streams-polyfill": "^3.0.3" 453 | }, 454 | "engines": { 455 | "node": "^12.20 || >= 14.13" 456 | } 457 | }, 458 | "node_modules/finalhandler": { 459 | "version": "2.1.0", 460 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", 461 | "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 462 | "dependencies": { 463 | "debug": "^4.4.0", 464 | "encodeurl": "^2.0.0", 465 | "escape-html": "^1.0.3", 466 | "on-finished": "^2.4.1", 467 | "parseurl": "^1.3.3", 468 | "statuses": "^2.0.1" 469 | }, 470 | "engines": { 471 | "node": ">= 0.8" 472 | } 473 | }, 474 | "node_modules/form-data": { 475 | "version": "4.0.2", 476 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", 477 | "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", 478 | "dependencies": { 479 | "asynckit": "^0.4.0", 480 | "combined-stream": "^1.0.8", 481 | "es-set-tostringtag": "^2.1.0", 482 | "mime-types": "^2.1.12" 483 | }, 484 | "engines": { 485 | "node": ">= 6" 486 | } 487 | }, 488 | "node_modules/form-data-encoder": { 489 | "version": "1.7.2", 490 | "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", 491 | "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" 492 | }, 493 | "node_modules/form-data/node_modules/mime-db": { 494 | "version": "1.52.0", 495 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 496 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 497 | "engines": { 498 | "node": ">= 0.6" 499 | } 500 | }, 501 | "node_modules/form-data/node_modules/mime-types": { 502 | "version": "2.1.35", 503 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 504 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 505 | "dependencies": { 506 | "mime-db": "1.52.0" 507 | }, 508 | "engines": { 509 | "node": ">= 0.6" 510 | } 511 | }, 512 | "node_modules/formdata-node": { 513 | "version": "4.4.1", 514 | "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", 515 | "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", 516 | "dependencies": { 517 | "node-domexception": "1.0.0", 518 | "web-streams-polyfill": "4.0.0-beta.3" 519 | }, 520 | "engines": { 521 | "node": ">= 12.20" 522 | } 523 | }, 524 | "node_modules/formdata-node/node_modules/web-streams-polyfill": { 525 | "version": "4.0.0-beta.3", 526 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", 527 | "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", 528 | "engines": { 529 | "node": ">= 14" 530 | } 531 | }, 532 | "node_modules/formdata-polyfill": { 533 | "version": "4.0.10", 534 | "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", 535 | "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", 536 | "dependencies": { 537 | "fetch-blob": "^3.1.2" 538 | }, 539 | "engines": { 540 | "node": ">=12.20.0" 541 | } 542 | }, 543 | "node_modules/forwarded": { 544 | "version": "0.2.0", 545 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 546 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 547 | "engines": { 548 | "node": ">= 0.6" 549 | } 550 | }, 551 | "node_modules/fresh": { 552 | "version": "2.0.0", 553 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", 554 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 555 | "engines": { 556 | "node": ">= 0.8" 557 | } 558 | }, 559 | "node_modules/function-bind": { 560 | "version": "1.1.2", 561 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 562 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 563 | "funding": { 564 | "url": "https://github.com/sponsors/ljharb" 565 | } 566 | }, 567 | "node_modules/get-intrinsic": { 568 | "version": "1.3.0", 569 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 570 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 571 | "dependencies": { 572 | "call-bind-apply-helpers": "^1.0.2", 573 | "es-define-property": "^1.0.1", 574 | "es-errors": "^1.3.0", 575 | "es-object-atoms": "^1.1.1", 576 | "function-bind": "^1.1.2", 577 | "get-proto": "^1.0.1", 578 | "gopd": "^1.2.0", 579 | "has-symbols": "^1.1.0", 580 | "hasown": "^2.0.2", 581 | "math-intrinsics": "^1.1.0" 582 | }, 583 | "engines": { 584 | "node": ">= 0.4" 585 | }, 586 | "funding": { 587 | "url": "https://github.com/sponsors/ljharb" 588 | } 589 | }, 590 | "node_modules/get-proto": { 591 | "version": "1.0.1", 592 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 593 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 594 | "dependencies": { 595 | "dunder-proto": "^1.0.1", 596 | "es-object-atoms": "^1.0.0" 597 | }, 598 | "engines": { 599 | "node": ">= 0.4" 600 | } 601 | }, 602 | "node_modules/gopd": { 603 | "version": "1.2.0", 604 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 605 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 606 | "engines": { 607 | "node": ">= 0.4" 608 | }, 609 | "funding": { 610 | "url": "https://github.com/sponsors/ljharb" 611 | } 612 | }, 613 | "node_modules/has-symbols": { 614 | "version": "1.1.0", 615 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 616 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 617 | "engines": { 618 | "node": ">= 0.4" 619 | }, 620 | "funding": { 621 | "url": "https://github.com/sponsors/ljharb" 622 | } 623 | }, 624 | "node_modules/has-tostringtag": { 625 | "version": "1.0.2", 626 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 627 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 628 | "dependencies": { 629 | "has-symbols": "^1.0.3" 630 | }, 631 | "engines": { 632 | "node": ">= 0.4" 633 | }, 634 | "funding": { 635 | "url": "https://github.com/sponsors/ljharb" 636 | } 637 | }, 638 | "node_modules/hasown": { 639 | "version": "2.0.2", 640 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 641 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 642 | "dependencies": { 643 | "function-bind": "^1.1.2" 644 | }, 645 | "engines": { 646 | "node": ">= 0.4" 647 | } 648 | }, 649 | "node_modules/http-errors": { 650 | "version": "2.0.0", 651 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 652 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 653 | "dependencies": { 654 | "depd": "2.0.0", 655 | "inherits": "2.0.4", 656 | "setprototypeof": "1.2.0", 657 | "statuses": "2.0.1", 658 | "toidentifier": "1.0.1" 659 | }, 660 | "engines": { 661 | "node": ">= 0.8" 662 | } 663 | }, 664 | "node_modules/humanize-ms": { 665 | "version": "1.2.1", 666 | "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", 667 | "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", 668 | "dependencies": { 669 | "ms": "^2.0.0" 670 | } 671 | }, 672 | "node_modules/iconv-lite": { 673 | "version": "0.6.3", 674 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 675 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 676 | "dependencies": { 677 | "safer-buffer": ">= 2.1.2 < 3.0.0" 678 | }, 679 | "engines": { 680 | "node": ">=0.10.0" 681 | } 682 | }, 683 | "node_modules/inherits": { 684 | "version": "2.0.4", 685 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 686 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 687 | }, 688 | "node_modules/ipaddr.js": { 689 | "version": "1.9.1", 690 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 691 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 692 | "engines": { 693 | "node": ">= 0.10" 694 | } 695 | }, 696 | "node_modules/is-promise": { 697 | "version": "4.0.0", 698 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", 699 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" 700 | }, 701 | "node_modules/isexe": { 702 | "version": "2.0.0", 703 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 704 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 705 | }, 706 | "node_modules/math-intrinsics": { 707 | "version": "1.1.0", 708 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 709 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 710 | "engines": { 711 | "node": ">= 0.4" 712 | } 713 | }, 714 | "node_modules/media-typer": { 715 | "version": "1.1.0", 716 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", 717 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 718 | "engines": { 719 | "node": ">= 0.8" 720 | } 721 | }, 722 | "node_modules/merge-descriptors": { 723 | "version": "2.0.0", 724 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 725 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 726 | "engines": { 727 | "node": ">=18" 728 | }, 729 | "funding": { 730 | "url": "https://github.com/sponsors/sindresorhus" 731 | } 732 | }, 733 | "node_modules/mime-db": { 734 | "version": "1.54.0", 735 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 736 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 737 | "engines": { 738 | "node": ">= 0.6" 739 | } 740 | }, 741 | "node_modules/mime-types": { 742 | "version": "3.0.1", 743 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", 744 | "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", 745 | "dependencies": { 746 | "mime-db": "^1.54.0" 747 | }, 748 | "engines": { 749 | "node": ">= 0.6" 750 | } 751 | }, 752 | "node_modules/ms": { 753 | "version": "2.1.3", 754 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 755 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 756 | }, 757 | "node_modules/negotiator": { 758 | "version": "1.0.0", 759 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 760 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 761 | "engines": { 762 | "node": ">= 0.6" 763 | } 764 | }, 765 | "node_modules/node-domexception": { 766 | "version": "1.0.0", 767 | "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", 768 | "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", 769 | "funding": [ 770 | { 771 | "type": "github", 772 | "url": "https://github.com/sponsors/jimmywarting" 773 | }, 774 | { 775 | "type": "github", 776 | "url": "https://paypal.me/jimmywarting" 777 | } 778 | ], 779 | "engines": { 780 | "node": ">=10.5.0" 781 | } 782 | }, 783 | "node_modules/node-fetch": { 784 | "version": "3.3.2", 785 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", 786 | "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", 787 | "dependencies": { 788 | "data-uri-to-buffer": "^4.0.0", 789 | "fetch-blob": "^3.1.4", 790 | "formdata-polyfill": "^4.0.10" 791 | }, 792 | "engines": { 793 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 794 | }, 795 | "funding": { 796 | "type": "opencollective", 797 | "url": "https://opencollective.com/node-fetch" 798 | } 799 | }, 800 | "node_modules/object-assign": { 801 | "version": "4.1.1", 802 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 803 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 804 | "engines": { 805 | "node": ">=0.10.0" 806 | } 807 | }, 808 | "node_modules/object-inspect": { 809 | "version": "1.13.4", 810 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 811 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 812 | "engines": { 813 | "node": ">= 0.4" 814 | }, 815 | "funding": { 816 | "url": "https://github.com/sponsors/ljharb" 817 | } 818 | }, 819 | "node_modules/on-finished": { 820 | "version": "2.4.1", 821 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 822 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 823 | "dependencies": { 824 | "ee-first": "1.1.1" 825 | }, 826 | "engines": { 827 | "node": ">= 0.8" 828 | } 829 | }, 830 | "node_modules/once": { 831 | "version": "1.4.0", 832 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 833 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 834 | "dependencies": { 835 | "wrappy": "1" 836 | } 837 | }, 838 | "node_modules/openai": { 839 | "version": "4.91.0", 840 | "resolved": "https://registry.npmjs.org/openai/-/openai-4.91.0.tgz", 841 | "integrity": "sha512-zdDg6eyvUmCP58QAW7/aPb+XdeavJ51pK6AcwZOWG5QNSLIovVz0XonRL9vARGJRmw8iImmvf2A31Q7hoh544w==", 842 | "dependencies": { 843 | "@types/node": "^18.11.18", 844 | "@types/node-fetch": "^2.6.4", 845 | "abort-controller": "^3.0.0", 846 | "agentkeepalive": "^4.2.1", 847 | "form-data-encoder": "1.7.2", 848 | "formdata-node": "^4.3.2", 849 | "node-fetch": "^2.6.7" 850 | }, 851 | "bin": { 852 | "openai": "bin/cli" 853 | }, 854 | "peerDependencies": { 855 | "ws": "^8.18.0", 856 | "zod": "^3.23.8" 857 | }, 858 | "peerDependenciesMeta": { 859 | "ws": { 860 | "optional": true 861 | }, 862 | "zod": { 863 | "optional": true 864 | } 865 | } 866 | }, 867 | "node_modules/openai/node_modules/node-fetch": { 868 | "version": "2.7.0", 869 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", 870 | "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", 871 | "dependencies": { 872 | "whatwg-url": "^5.0.0" 873 | }, 874 | "engines": { 875 | "node": "4.x || >=6.0.0" 876 | }, 877 | "peerDependencies": { 878 | "encoding": "^0.1.0" 879 | }, 880 | "peerDependenciesMeta": { 881 | "encoding": { 882 | "optional": true 883 | } 884 | } 885 | }, 886 | "node_modules/parseurl": { 887 | "version": "1.3.3", 888 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 889 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 890 | "engines": { 891 | "node": ">= 0.8" 892 | } 893 | }, 894 | "node_modules/path-key": { 895 | "version": "3.1.1", 896 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 897 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 898 | "engines": { 899 | "node": ">=8" 900 | } 901 | }, 902 | "node_modules/path-to-regexp": { 903 | "version": "8.2.0", 904 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", 905 | "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", 906 | "engines": { 907 | "node": ">=16" 908 | } 909 | }, 910 | "node_modules/pkce-challenge": { 911 | "version": "4.1.0", 912 | "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", 913 | "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", 914 | "engines": { 915 | "node": ">=16.20.0" 916 | } 917 | }, 918 | "node_modules/proxy-addr": { 919 | "version": "2.0.7", 920 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 921 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 922 | "dependencies": { 923 | "forwarded": "0.2.0", 924 | "ipaddr.js": "1.9.1" 925 | }, 926 | "engines": { 927 | "node": ">= 0.10" 928 | } 929 | }, 930 | "node_modules/qs": { 931 | "version": "6.14.0", 932 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 933 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 934 | "dependencies": { 935 | "side-channel": "^1.1.0" 936 | }, 937 | "engines": { 938 | "node": ">=0.6" 939 | }, 940 | "funding": { 941 | "url": "https://github.com/sponsors/ljharb" 942 | } 943 | }, 944 | "node_modules/range-parser": { 945 | "version": "1.2.1", 946 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 947 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 948 | "engines": { 949 | "node": ">= 0.6" 950 | } 951 | }, 952 | "node_modules/raw-body": { 953 | "version": "3.0.0", 954 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 955 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 956 | "dependencies": { 957 | "bytes": "3.1.2", 958 | "http-errors": "2.0.0", 959 | "iconv-lite": "0.6.3", 960 | "unpipe": "1.0.0" 961 | }, 962 | "engines": { 963 | "node": ">= 0.8" 964 | } 965 | }, 966 | "node_modules/router": { 967 | "version": "2.2.0", 968 | "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", 969 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 970 | "dependencies": { 971 | "debug": "^4.4.0", 972 | "depd": "^2.0.0", 973 | "is-promise": "^4.0.0", 974 | "parseurl": "^1.3.3", 975 | "path-to-regexp": "^8.0.0" 976 | }, 977 | "engines": { 978 | "node": ">= 18" 979 | } 980 | }, 981 | "node_modules/safe-buffer": { 982 | "version": "5.2.1", 983 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 984 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 985 | "funding": [ 986 | { 987 | "type": "github", 988 | "url": "https://github.com/sponsors/feross" 989 | }, 990 | { 991 | "type": "patreon", 992 | "url": "https://www.patreon.com/feross" 993 | }, 994 | { 995 | "type": "consulting", 996 | "url": "https://feross.org/support" 997 | } 998 | ] 999 | }, 1000 | "node_modules/safer-buffer": { 1001 | "version": "2.1.2", 1002 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1003 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1004 | }, 1005 | "node_modules/send": { 1006 | "version": "1.2.0", 1007 | "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", 1008 | "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", 1009 | "dependencies": { 1010 | "debug": "^4.3.5", 1011 | "encodeurl": "^2.0.0", 1012 | "escape-html": "^1.0.3", 1013 | "etag": "^1.8.1", 1014 | "fresh": "^2.0.0", 1015 | "http-errors": "^2.0.0", 1016 | "mime-types": "^3.0.1", 1017 | "ms": "^2.1.3", 1018 | "on-finished": "^2.4.1", 1019 | "range-parser": "^1.2.1", 1020 | "statuses": "^2.0.1" 1021 | }, 1022 | "engines": { 1023 | "node": ">= 18" 1024 | } 1025 | }, 1026 | "node_modules/serve-static": { 1027 | "version": "2.2.0", 1028 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", 1029 | "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", 1030 | "dependencies": { 1031 | "encodeurl": "^2.0.0", 1032 | "escape-html": "^1.0.3", 1033 | "parseurl": "^1.3.3", 1034 | "send": "^1.2.0" 1035 | }, 1036 | "engines": { 1037 | "node": ">= 18" 1038 | } 1039 | }, 1040 | "node_modules/setprototypeof": { 1041 | "version": "1.2.0", 1042 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1043 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 1044 | }, 1045 | "node_modules/shebang-command": { 1046 | "version": "2.0.0", 1047 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1048 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1049 | "dependencies": { 1050 | "shebang-regex": "^3.0.0" 1051 | }, 1052 | "engines": { 1053 | "node": ">=8" 1054 | } 1055 | }, 1056 | "node_modules/shebang-regex": { 1057 | "version": "3.0.0", 1058 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1059 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1060 | "engines": { 1061 | "node": ">=8" 1062 | } 1063 | }, 1064 | "node_modules/side-channel": { 1065 | "version": "1.1.0", 1066 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 1067 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 1068 | "dependencies": { 1069 | "es-errors": "^1.3.0", 1070 | "object-inspect": "^1.13.3", 1071 | "side-channel-list": "^1.0.0", 1072 | "side-channel-map": "^1.0.1", 1073 | "side-channel-weakmap": "^1.0.2" 1074 | }, 1075 | "engines": { 1076 | "node": ">= 0.4" 1077 | }, 1078 | "funding": { 1079 | "url": "https://github.com/sponsors/ljharb" 1080 | } 1081 | }, 1082 | "node_modules/side-channel-list": { 1083 | "version": "1.0.0", 1084 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 1085 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 1086 | "dependencies": { 1087 | "es-errors": "^1.3.0", 1088 | "object-inspect": "^1.13.3" 1089 | }, 1090 | "engines": { 1091 | "node": ">= 0.4" 1092 | }, 1093 | "funding": { 1094 | "url": "https://github.com/sponsors/ljharb" 1095 | } 1096 | }, 1097 | "node_modules/side-channel-map": { 1098 | "version": "1.0.1", 1099 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 1100 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 1101 | "dependencies": { 1102 | "call-bound": "^1.0.2", 1103 | "es-errors": "^1.3.0", 1104 | "get-intrinsic": "^1.2.5", 1105 | "object-inspect": "^1.13.3" 1106 | }, 1107 | "engines": { 1108 | "node": ">= 0.4" 1109 | }, 1110 | "funding": { 1111 | "url": "https://github.com/sponsors/ljharb" 1112 | } 1113 | }, 1114 | "node_modules/side-channel-weakmap": { 1115 | "version": "1.0.2", 1116 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 1117 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 1118 | "dependencies": { 1119 | "call-bound": "^1.0.2", 1120 | "es-errors": "^1.3.0", 1121 | "get-intrinsic": "^1.2.5", 1122 | "object-inspect": "^1.13.3", 1123 | "side-channel-map": "^1.0.1" 1124 | }, 1125 | "engines": { 1126 | "node": ">= 0.4" 1127 | }, 1128 | "funding": { 1129 | "url": "https://github.com/sponsors/ljharb" 1130 | } 1131 | }, 1132 | "node_modules/statuses": { 1133 | "version": "2.0.1", 1134 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1135 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1136 | "engines": { 1137 | "node": ">= 0.8" 1138 | } 1139 | }, 1140 | "node_modules/toidentifier": { 1141 | "version": "1.0.1", 1142 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1143 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1144 | "engines": { 1145 | "node": ">=0.6" 1146 | } 1147 | }, 1148 | "node_modules/tr46": { 1149 | "version": "0.0.3", 1150 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 1151 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 1152 | }, 1153 | "node_modules/type-is": { 1154 | "version": "2.0.1", 1155 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", 1156 | "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 1157 | "dependencies": { 1158 | "content-type": "^1.0.5", 1159 | "media-typer": "^1.1.0", 1160 | "mime-types": "^3.0.0" 1161 | }, 1162 | "engines": { 1163 | "node": ">= 0.6" 1164 | } 1165 | }, 1166 | "node_modules/undici-types": { 1167 | "version": "5.26.5", 1168 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 1169 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" 1170 | }, 1171 | "node_modules/unpipe": { 1172 | "version": "1.0.0", 1173 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1174 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1175 | "engines": { 1176 | "node": ">= 0.8" 1177 | } 1178 | }, 1179 | "node_modules/vary": { 1180 | "version": "1.1.2", 1181 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1182 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1183 | "engines": { 1184 | "node": ">= 0.8" 1185 | } 1186 | }, 1187 | "node_modules/web-streams-polyfill": { 1188 | "version": "3.3.3", 1189 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", 1190 | "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", 1191 | "engines": { 1192 | "node": ">= 8" 1193 | } 1194 | }, 1195 | "node_modules/webidl-conversions": { 1196 | "version": "3.0.1", 1197 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 1198 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 1199 | }, 1200 | "node_modules/whatwg-url": { 1201 | "version": "5.0.0", 1202 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 1203 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 1204 | "dependencies": { 1205 | "tr46": "~0.0.3", 1206 | "webidl-conversions": "^3.0.0" 1207 | } 1208 | }, 1209 | "node_modules/which": { 1210 | "version": "2.0.2", 1211 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1212 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1213 | "dependencies": { 1214 | "isexe": "^2.0.0" 1215 | }, 1216 | "bin": { 1217 | "node-which": "bin/node-which" 1218 | }, 1219 | "engines": { 1220 | "node": ">= 8" 1221 | } 1222 | }, 1223 | "node_modules/wrappy": { 1224 | "version": "1.0.2", 1225 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1226 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 1227 | }, 1228 | "node_modules/youtube-transcript": { 1229 | "version": "1.2.1", 1230 | "resolved": "https://registry.npmjs.org/youtube-transcript/-/youtube-transcript-1.2.1.tgz", 1231 | "integrity": "sha512-TvEGkBaajKw+B6y91ziLuBLsa5cawgowou+Bk0ciGpjELDfAzSzTGXaZmeSSkUeknCPpEr/WGApOHDwV7V+Y9Q==", 1232 | "engines": { 1233 | "node": ">=18.0.0" 1234 | } 1235 | }, 1236 | "node_modules/zod": { 1237 | "version": "3.24.2", 1238 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", 1239 | "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", 1240 | "funding": { 1241 | "url": "https://github.com/sponsors/colinhacks" 1242 | } 1243 | }, 1244 | "node_modules/zod-to-json-schema": { 1245 | "version": "3.24.5", 1246 | "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", 1247 | "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", 1248 | "peerDependencies": { 1249 | "zod": "^3.24.1" 1250 | } 1251 | } 1252 | } 1253 | } 1254 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yt-to-linkedin-mcp", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "bin": { 6 | "yt-to-linkedin-mcp": "src/index.js" 7 | }, 8 | "scripts": { 9 | "start": "node src/index.js", 10 | "dev": "node --watch src/index.js", 11 | "inspect": "npx -y @modelcontextprotocol/inspector node src/index.js" 12 | }, 13 | "dependencies": { 14 | "@modelcontextprotocol/sdk": "^1.0.0", 15 | "dotenv": "^16.3.1", 16 | "node-fetch": "^3.3.2", 17 | "openai": "^4.20.1", 18 | "youtube-transcript": "^1.0.6", 19 | "zod": "^3.22.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /smithery.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkedin-post-generator", 3 | "description": "Generate LinkedIn post drafts from YouTube videos", 4 | "version": "1.0.0", 5 | "type": "mcp", 6 | "configSchema": { 7 | "type": "object", 8 | "required": ["OPENAI_API_KEY"], 9 | "properties": { 10 | "OPENAI_API_KEY": { 11 | "type": "string", 12 | "description": "Your OpenAI API key (required)" 13 | }, 14 | "YOUTUBE_API_KEY": { 15 | "type": "string", 16 | "description": "Your YouTube API key (optional)" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /smithery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linkedin-post-generator", 3 | "description": "Generate LinkedIn post drafts from YouTube videos", 4 | "version": "1.0.0", 5 | "type": "mcp", 6 | "configSchema": { 7 | "type": "object", 8 | "required": ["OPENAI_API_KEY"], 9 | "properties": { 10 | "OPENAI_API_KEY": { 11 | "type": "string", 12 | "description": "Your OpenAI API key (required)" 13 | }, 14 | "YOUTUBE_API_KEY": { 15 | "type": "string", 16 | "description": "Your YouTube API key (optional)" 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery.ai configuration 2 | name: yt-to-linkedin-mcp 3 | description: Model Context Protocol server that automates generating LinkedIn post drafts from YouTube videos 4 | version: 1.0.0 5 | tags: 6 | - youtube 7 | - linkedin 8 | - content-generation 9 | - mcp 10 | 11 | startCommand: 12 | type: stdio 13 | configSchema: 14 | type: object 15 | properties: 16 | OPENAI_API_KEY: 17 | type: string 18 | description: OpenAI API key for summarization and post generation (optional, can be provided in requests) 19 | YOUTUBE_API_KEY: 20 | type: string 21 | description: YouTube Data API key for fetching video metadata (optional, can be provided in requests) 22 | # PORT: 23 | # type: string 24 | # description: Port to run the server on 25 | # default: "8000" 26 | required: [] 27 | commandFunction: |- 28 | (config) => ({ 29 | "command": "node", 30 | "args": ["src/index.js"], 31 | "env": { 32 | "OPENAI_API_KEY": config.OPENAI_API_KEY || "", 33 | "YOUTUBE_API_KEY": config.YOUTUBE_API_KEY || "", 34 | "PORT": config.PORT || "8000" 35 | } 36 | }) 37 | 38 | # Specify the MCP configuration 39 | mcp: 40 | type: "stdio" 41 | 42 | resources: 43 | cpu: 1 44 | memory: 1Gi 45 | 46 | ports: 47 | - name: http 48 | port: 8000 49 | protocol: TCP 50 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; 3 | import { server } from './server.js'; 4 | import dotenv from 'dotenv'; 5 | import fs from 'fs'; 6 | import path from 'path'; 7 | 8 | /** 9 | * LinkedIn Post Generator MCP Server 10 | * 11 | * Configuration Requirements: 12 | * - OPENAI_API_KEY: Required for generating content 13 | * - YOUTUBE_API_KEY: Optional for enhanced YouTube data fetching 14 | */ 15 | 16 | // Load environment variables (but they're now optional) 17 | dotenv.config(); 18 | 19 | // Check for Smithery configuration 20 | const args = process.argv; 21 | let smitheryConfig = null; 22 | 23 | // Look for --config argument 24 | for (let i = 0; i < args.length; i++) { 25 | if (args[i] === '--config' && i + 1 < args.length) { 26 | try { 27 | smitheryConfig = JSON.parse(args[i + 1]); 28 | console.log('Smithery configuration detected'); 29 | 30 | // Set environment variables from Smithery config 31 | if (smitheryConfig.OPENAI_API_KEY) { 32 | process.env.OPENAI_API_KEY = smitheryConfig.OPENAI_API_KEY; 33 | console.log('OpenAI API key set from Smithery config'); 34 | } 35 | 36 | if (smitheryConfig.YOUTUBE_API_KEY) { 37 | process.env.YOUTUBE_API_KEY = smitheryConfig.YOUTUBE_API_KEY; 38 | console.log('YouTube API key set from Smithery config'); 39 | } 40 | 41 | break; 42 | } catch (error) { 43 | console.error('Error parsing Smithery config:', error.message); 44 | } 45 | } 46 | } 47 | 48 | console.log('Starting LinkedIn Post Generator MCP server...'); 49 | if (!process.env.OPENAI_API_KEY) { 50 | console.log('Note: You will need to set your API keys using the set_api_keys tool before using other functionality.'); 51 | } 52 | 53 | // Start receiving messages on stdin and sending messages on stdout 54 | const transport = new StdioServerTransport(); 55 | await server.connect(transport); 56 | 57 | // Keep the process alive 58 | process.on('SIGINT', () => { 59 | console.log('Shutting down server...'); 60 | process.exit(0); 61 | }); 62 | -------------------------------------------------------------------------------- /src/modules/api-key-manager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Manages API keys for the application 3 | */ 4 | export class ApiKeyManager { 5 | constructor() { 6 | // Initialize API keys from environment variables 7 | this.openaiApiKey = process.env.OPENAI_API_KEY || null; 8 | this.youtubeApiKey = process.env.YOUTUBE_API_KEY || null; 9 | 10 | // Log initialization status 11 | if (this.openaiApiKey) { 12 | console.log('OpenAI API key initialized from environment'); 13 | } 14 | if (this.youtubeApiKey) { 15 | console.log('YouTube API key initialized from environment'); 16 | } 17 | } 18 | 19 | /** 20 | * Set the OpenAI API key 21 | * @param {string} key - The OpenAI API key 22 | */ 23 | setOpenAIKey(key) { 24 | if (!key || typeof key !== 'string' || key.trim() === '') { 25 | throw new Error("Invalid OpenAI API key"); 26 | } 27 | this.openaiApiKey = key.trim(); 28 | console.log("OpenAI API key set successfully"); 29 | } 30 | 31 | /** 32 | * Set the YouTube API key 33 | * @param {string} key - The YouTube API key 34 | */ 35 | setYouTubeKey(key) { 36 | if (!key || typeof key !== 'string' || key.trim() === '') { 37 | throw new Error("Invalid YouTube API key"); 38 | } 39 | this.youtubeApiKey = key.trim(); 40 | console.log("YouTube API key set successfully"); 41 | } 42 | 43 | /** 44 | * Get the OpenAI API key 45 | * @returns {string|null} - The OpenAI API key or null if not set 46 | */ 47 | getOpenAIKey() { 48 | return this.openaiApiKey; 49 | } 50 | 51 | /** 52 | * Get the YouTube API key 53 | * @returns {string|null} - The YouTube API key or null if not set 54 | */ 55 | getYouTubeKey() { 56 | return this.youtubeApiKey; 57 | } 58 | 59 | /** 60 | * Check if OpenAI API key is set 61 | * @returns {boolean} - True if the key is set 62 | */ 63 | hasOpenAIKey() { 64 | return !!this.openaiApiKey; 65 | } 66 | 67 | /** 68 | * Check if YouTube API key is set 69 | * @returns {boolean} - True if the key is set 70 | */ 71 | hasYouTubeKey() { 72 | return !!this.youtubeApiKey; 73 | } 74 | 75 | /** 76 | * Get the status of API keys 77 | * @returns {Object} - Status object with key information 78 | */ 79 | getStatus() { 80 | return { 81 | openai: { 82 | set: this.hasOpenAIKey(), 83 | key: this.hasOpenAIKey() ? "********" + this.openaiApiKey.slice(-4) : null 84 | }, 85 | youtube: { 86 | set: this.hasYouTubeKey(), 87 | key: this.hasYouTubeKey() ? "********" + this.youtubeApiKey.slice(-4) : null 88 | } 89 | }; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/modules/post-generator.js: -------------------------------------------------------------------------------- 1 | import { OpenAI } from 'openai'; 2 | 3 | /** 4 | * Generate a LinkedIn post from a video summary 5 | * @param {string} summary - Summary of the video content 6 | * @param {string} videoTitle - Title of the YouTube video 7 | * @param {string} speakerName - Name of the speaker (optional) 8 | * @param {string[]} hashtags - Relevant hashtags (optional) 9 | * @param {string} tone - Tone for the post (first-person, third-person, thought-leader) 10 | * @param {boolean} includeCallToAction - Whether to include a call to action 11 | * @param {string} apiKey - OpenAI API key 12 | * @returns {Promise} - The generated LinkedIn post 13 | */ 14 | export async function generateLinkedInPost( 15 | summary, 16 | videoTitle, 17 | speakerName = null, 18 | hashtags = [], 19 | tone = "first-person", 20 | includeCallToAction = true, 21 | apiKey 22 | ) { 23 | if (!apiKey) { 24 | throw new Error("OpenAI API key not provided"); 25 | } 26 | 27 | if (!summary || summary.trim().length === 0) { 28 | throw new Error("Empty summary provided"); 29 | } 30 | 31 | console.log(`Generating LinkedIn post with tone: ${tone}`); 32 | 33 | try { 34 | // Initialize OpenAI client with provided API key 35 | const openai = new OpenAI({ 36 | apiKey: apiKey, 37 | }); 38 | 39 | // Prepare hashtag string 40 | const hashtagString = hashtags && hashtags.length > 0 41 | ? hashtags.map(tag => tag.startsWith('#') ? tag : `#${tag}`).join(' ') 42 | : ''; 43 | 44 | // Prepare speaker reference 45 | const speakerReference = speakerName ? `by ${speakerName}` : ''; 46 | 47 | const response = await openai.chat.completions.create({ 48 | model: "gpt-3.5-turbo", 49 | messages: [ 50 | { 51 | role: "system", 52 | content: `You are a professional LinkedIn content creator. 53 | Create a compelling LinkedIn post in a ${tone} tone based on the provided video summary. 54 | The post should be between 500-1200 characters (not including hashtags). 55 | 56 | Structure the post with: 57 | 1. An attention-grabbing hook 58 | 2. 2-3 key insights from the video 59 | 3. A personal reflection or takeaway 60 | ${includeCallToAction ? '4. A soft call to action (e.g., asking a question, inviting comments)' : ''} 61 | 62 | The post should feel authentic, professional, and valuable to LinkedIn readers. 63 | Avoid clickbait or overly promotional language.` 64 | }, 65 | { 66 | role: "user", 67 | content: `Create a LinkedIn post based on this YouTube video: 68 | 69 | Title: ${videoTitle} ${speakerReference} 70 | 71 | Summary: 72 | ${summary} 73 | 74 | ${hashtagString ? `Suggested hashtags: ${hashtagString}` : ''} 75 | 76 | Please format the post ready to copy and paste to LinkedIn.` 77 | } 78 | ], 79 | temperature: 0.7, 80 | max_tokens: 700 81 | }); 82 | 83 | if (response.choices && response.choices.length > 0) { 84 | let post = response.choices[0].message.content.trim(); 85 | 86 | // Ensure hashtags are at the end if they weren't included 87 | if (hashtagString && !post.includes(hashtagString)) { 88 | post += `\n\n${hashtagString}`; 89 | } 90 | 91 | return post; 92 | } else { 93 | throw new Error("No post generated"); 94 | } 95 | } catch (error) { 96 | console.error("Post generation error:", error); 97 | throw new Error(`Failed to generate LinkedIn post: ${error.message}`); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/modules/transcript-extractor.js: -------------------------------------------------------------------------------- 1 | import { YoutubeTranscript } from 'youtube-transcript'; 2 | import fetch from 'node-fetch'; 3 | 4 | /** 5 | * Extract transcript from a YouTube video 6 | * @param {string} youtubeUrl - The YouTube video URL 7 | * @param {string} youtubeApiKey - Optional YouTube API key 8 | * @returns {Promise} - The extracted transcript text 9 | */ 10 | export async function extractTranscript(youtubeUrl, youtubeApiKey = null) { 11 | try { 12 | console.log(`Extracting transcript from: ${youtubeUrl}`); 13 | 14 | // Extract video ID from URL 15 | const videoId = extractVideoId(youtubeUrl); 16 | if (!videoId) { 17 | throw new Error("Invalid YouTube URL. Could not extract video ID."); 18 | } 19 | 20 | // Try to get transcript using youtube-transcript package 21 | try { 22 | const transcriptItems = await YoutubeTranscript.fetchTranscript(videoId); 23 | if (!transcriptItems || transcriptItems.length === 0) { 24 | throw new Error("No transcript available"); 25 | } 26 | 27 | // Combine transcript segments into a single text 28 | const fullTranscript = transcriptItems 29 | .map(item => item.text) 30 | .join(' ') 31 | .replace(/\s+/g, ' '); // Clean up extra spaces 32 | 33 | return fullTranscript; 34 | } catch (error) { 35 | console.error("Error with primary transcript method:", error); 36 | 37 | // Fallback to YouTube API if available 38 | if (youtubeApiKey) { 39 | return await fetchTranscriptWithYouTubeAPI(videoId, youtubeApiKey); 40 | } else { 41 | throw new Error("Failed to extract transcript: " + error.message); 42 | } 43 | } 44 | } catch (error) { 45 | console.error("Transcript extraction error:", error); 46 | throw new Error(`Failed to extract transcript: ${error.message}`); 47 | } 48 | } 49 | 50 | /** 51 | * Extract video ID from YouTube URL 52 | * @param {string} url - YouTube URL 53 | * @returns {string|null} - Video ID or null if not found 54 | */ 55 | function extractVideoId(url) { 56 | try { 57 | const urlObj = new URL(url); 58 | 59 | // Standard YouTube URL (youtube.com/watch?v=VIDEO_ID) 60 | if (urlObj.hostname.includes('youtube.com')) { 61 | return urlObj.searchParams.get('v'); 62 | } 63 | 64 | // Short YouTube URL (youtu.be/VIDEO_ID) 65 | if (urlObj.hostname === 'youtu.be') { 66 | return urlObj.pathname.substring(1); 67 | } 68 | 69 | return null; 70 | } catch (error) { 71 | console.error("Error extracting video ID:", error); 72 | return null; 73 | } 74 | } 75 | 76 | /** 77 | * Fallback method to fetch transcript using YouTube API 78 | * @param {string} videoId - YouTube video ID 79 | * @param {string} apiKey - YouTube API key 80 | * @returns {Promise} - Transcript text 81 | */ 82 | async function fetchTranscriptWithYouTubeAPI(videoId, apiKey) { 83 | if (!apiKey) { 84 | throw new Error("YouTube API key not provided"); 85 | } 86 | 87 | // First, get the caption track 88 | const captionUrl = `https://www.googleapis.com/youtube/v3/captions?part=snippet&videoId=${videoId}&key=${apiKey}`; 89 | 90 | const response = await fetch(captionUrl); 91 | if (!response.ok) { 92 | throw new Error(`YouTube API error: ${response.statusText}`); 93 | } 94 | 95 | const data = await response.json(); 96 | 97 | if (!data.items || data.items.length === 0) { 98 | throw new Error("No captions available for this video"); 99 | } 100 | 101 | // Note: Actually downloading the caption track requires OAuth2 authentication 102 | // which is beyond the scope of this example 103 | throw new Error("YouTube API fallback requires OAuth2 authentication"); 104 | } 105 | -------------------------------------------------------------------------------- /src/modules/transcript-summarizer.js: -------------------------------------------------------------------------------- 1 | import { OpenAI } from 'openai'; 2 | 3 | /** 4 | * Summarize a transcript using OpenAI 5 | * @param {string} transcript - The transcript text to summarize 6 | * @param {string} tone - Desired tone (educational, inspirational, etc.) 7 | * @param {string} audience - Target audience (general, technical, etc.) 8 | * @param {number} wordCount - Approximate word count for summary 9 | * @param {string} apiKey - OpenAI API key 10 | * @returns {Promise} - The summarized text 11 | */ 12 | export async function summarizeTranscript(transcript, tone, audience, wordCount, apiKey) { 13 | if (!apiKey) { 14 | throw new Error("OpenAI API key not provided"); 15 | } 16 | 17 | if (!transcript || transcript.trim().length === 0) { 18 | throw new Error("Empty transcript provided"); 19 | } 20 | 21 | console.log(`Summarizing transcript (${transcript.length} chars) with tone: ${tone}, audience: ${audience}`); 22 | 23 | try { 24 | // Initialize OpenAI client with provided API key 25 | const openai = new OpenAI({ 26 | apiKey: apiKey, 27 | }); 28 | 29 | // Truncate transcript if it's too long (to fit within token limits) 30 | const truncatedTranscript = truncateText(transcript, 15000); 31 | 32 | const response = await openai.chat.completions.create({ 33 | model: "gpt-3.5-turbo", 34 | messages: [ 35 | { 36 | role: "system", 37 | content: `You are a professional content summarizer. Summarize the provided transcript in a ${tone} tone for a ${audience} audience. 38 | The summary should be approximately ${wordCount} words and capture the key points, insights, and valuable information from the transcript. 39 | Focus on making the summary concise, informative, and engaging.` 40 | }, 41 | { 42 | role: "user", 43 | content: `Please summarize the following video transcript:\n\n${truncatedTranscript}` 44 | } 45 | ], 46 | temperature: 0.7, 47 | max_tokens: 500 48 | }); 49 | 50 | if (response.choices && response.choices.length > 0) { 51 | return response.choices[0].message.content.trim(); 52 | } else { 53 | throw new Error("No summary generated"); 54 | } 55 | } catch (error) { 56 | console.error("Summarization error:", error); 57 | throw new Error(`Failed to summarize transcript: ${error.message}`); 58 | } 59 | } 60 | 61 | /** 62 | * Truncate text to a maximum character length 63 | * @param {string} text - Text to truncate 64 | * @param {number} maxLength - Maximum length in characters 65 | * @returns {string} - Truncated text 66 | */ 67 | function truncateText(text, maxLength) { 68 | if (text.length <= maxLength) return text; 69 | 70 | // Try to truncate at a sentence boundary 71 | const truncated = text.substring(0, maxLength); 72 | const lastPeriod = truncated.lastIndexOf('.'); 73 | 74 | if (lastPeriod > maxLength * 0.8) { 75 | return truncated.substring(0, lastPeriod + 1); 76 | } 77 | 78 | return truncated + "..."; 79 | } 80 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 2 | import { z } from 'zod'; 3 | import { extractTranscript } from './modules/transcript-extractor.js'; 4 | import { summarizeTranscript } from './modules/transcript-summarizer.js'; 5 | import { generateLinkedInPost } from './modules/post-generator.js'; 6 | import { ApiKeyManager } from './modules/api-key-manager.js'; 7 | 8 | // Create API key manager 9 | const apiKeyManager = new ApiKeyManager(); 10 | 11 | // Create an MCP server for LinkedIn post generation 12 | const server = new McpServer({ 13 | name: "LinkedIn Post Generator", 14 | version: "1.0.0", 15 | description: "Generate LinkedIn post drafts from YouTube videos" 16 | }); 17 | 18 | // Set API keys tool 19 | server.tool( 20 | "set_api_keys", 21 | { 22 | openaiApiKey: z.string().min(1).describe("Your OpenAI API key"), 23 | youtubeApiKey: z.string().optional().describe("Your YouTube API key (optional)") 24 | }, 25 | async ({ openaiApiKey, youtubeApiKey }) => { 26 | try { 27 | apiKeyManager.setOpenAIKey(openaiApiKey); 28 | if (youtubeApiKey) { 29 | apiKeyManager.setYouTubeKey(youtubeApiKey); 30 | } 31 | 32 | return { 33 | content: [{ 34 | type: "text", 35 | text: JSON.stringify({ 36 | success: true, 37 | message: "API keys set successfully. You can now use the other tools." 38 | }, null, 2) 39 | }] 40 | }; 41 | } catch (error) { 42 | return { 43 | content: [{ 44 | type: "text", 45 | text: JSON.stringify({ 46 | success: false, 47 | error: error.message 48 | }, null, 2) 49 | }], 50 | isError: true 51 | }; 52 | } 53 | }, 54 | { description: "Set your API keys for OpenAI and YouTube (optional)" } 55 | ); 56 | 57 | // Check API keys status 58 | server.tool( 59 | "check_api_keys", 60 | {}, 61 | async () => { 62 | const status = apiKeyManager.getStatus(); 63 | return { 64 | content: [{ 65 | type: "text", 66 | text: JSON.stringify(status, null, 2) 67 | }] 68 | }; 69 | }, 70 | { description: "Check the status of your API keys" } 71 | ); 72 | 73 | // Extract transcript tool 74 | server.tool( 75 | "extract_transcript", 76 | { 77 | youtubeUrl: z.string().url().describe("YouTube video URL") 78 | }, 79 | async ({ youtubeUrl }) => { 80 | try { 81 | // Check if YouTube API key is set (if needed) 82 | if (!apiKeyManager.hasYouTubeKey()) { 83 | console.log("No YouTube API key set, will try without it"); 84 | } 85 | 86 | const transcript = await extractTranscript(youtubeUrl, apiKeyManager.getYouTubeKey()); 87 | return { 88 | content: [{ 89 | type: "text", 90 | text: JSON.stringify({ 91 | success: true, 92 | transcript 93 | }, null, 2) 94 | }] 95 | }; 96 | } catch (error) { 97 | return { 98 | content: [{ 99 | type: "text", 100 | text: JSON.stringify({ 101 | success: false, 102 | error: error.message 103 | }, null, 2) 104 | }], 105 | isError: true 106 | }; 107 | } 108 | }, 109 | { description: "Extract transcript from a YouTube video" } 110 | ); 111 | 112 | // Summarize transcript tool 113 | server.tool( 114 | "summarize_transcript", 115 | { 116 | transcript: z.string().describe("Video transcript text"), 117 | tone: z.enum(["educational", "inspirational", "professional", "conversational"]) 118 | .default("professional") 119 | .describe("Tone of the summary"), 120 | audience: z.enum(["general", "technical", "business", "academic"]) 121 | .default("general") 122 | .describe("Target audience for the summary"), 123 | wordCount: z.number().min(100).max(300).default(200) 124 | .describe("Approximate word count for the summary") 125 | }, 126 | async ({ transcript, tone, audience, wordCount }) => { 127 | try { 128 | // Check if OpenAI API key is set 129 | if (!apiKeyManager.hasOpenAIKey()) { 130 | throw new Error("OpenAI API key not set. Please use the set_api_keys tool first."); 131 | } 132 | 133 | const summary = await summarizeTranscript( 134 | transcript, 135 | tone, 136 | audience, 137 | wordCount, 138 | apiKeyManager.getOpenAIKey() 139 | ); 140 | 141 | return { 142 | content: [{ 143 | type: "text", 144 | text: JSON.stringify({ 145 | success: true, 146 | summary 147 | }, null, 2) 148 | }] 149 | }; 150 | } catch (error) { 151 | return { 152 | content: [{ 153 | type: "text", 154 | text: JSON.stringify({ 155 | success: false, 156 | error: error.message 157 | }, null, 2) 158 | }], 159 | isError: true 160 | }; 161 | } 162 | }, 163 | { description: "Summarize a video transcript" } 164 | ); 165 | 166 | // Generate LinkedIn post tool 167 | server.tool( 168 | "generate_linkedin_post", 169 | { 170 | summary: z.string().describe("Summary of the video content"), 171 | videoTitle: z.string().describe("Title of the YouTube video"), 172 | speakerName: z.string().optional().describe("Name of the speaker in the video (optional)"), 173 | hashtags: z.array(z.string()).optional().describe("Relevant hashtags (optional)"), 174 | tone: z.enum(["first-person", "third-person", "thought-leader"]) 175 | .default("first-person") 176 | .describe("Tone of the LinkedIn post"), 177 | includeCallToAction: z.boolean().default(true) 178 | .describe("Whether to include a call to action") 179 | }, 180 | async ({ summary, videoTitle, speakerName, hashtags, tone, includeCallToAction }) => { 181 | try { 182 | // Check if OpenAI API key is set 183 | if (!apiKeyManager.hasOpenAIKey()) { 184 | throw new Error("OpenAI API key not set. Please use the set_api_keys tool first."); 185 | } 186 | 187 | const post = await generateLinkedInPost( 188 | summary, 189 | videoTitle, 190 | speakerName, 191 | hashtags, 192 | tone, 193 | includeCallToAction, 194 | apiKeyManager.getOpenAIKey() 195 | ); 196 | 197 | return { 198 | content: [{ 199 | type: "text", 200 | text: JSON.stringify({ 201 | success: true, 202 | post 203 | }, null, 2) 204 | }] 205 | }; 206 | } catch (error) { 207 | return { 208 | content: [{ 209 | type: "text", 210 | text: JSON.stringify({ 211 | success: false, 212 | error: error.message 213 | }, null, 2) 214 | }], 215 | isError: true 216 | }; 217 | } 218 | }, 219 | { description: "Generate a LinkedIn post draft from a video summary" } 220 | ); 221 | 222 | // All-in-one tool: YouTube URL to LinkedIn post 223 | server.tool( 224 | "youtube_to_linkedin_post", 225 | { 226 | youtubeUrl: z.string().url().describe("YouTube video URL"), 227 | tone: z.enum(["first-person", "third-person", "thought-leader"]) 228 | .default("first-person") 229 | .describe("Tone of the LinkedIn post"), 230 | summaryTone: z.enum(["educational", "inspirational", "professional", "conversational"]) 231 | .default("professional") 232 | .describe("Tone of the summary"), 233 | audience: z.enum(["general", "technical", "business", "academic"]) 234 | .default("general") 235 | .describe("Target audience"), 236 | hashtags: z.array(z.string()).optional().describe("Relevant hashtags (optional)"), 237 | includeCallToAction: z.boolean().default(true) 238 | .describe("Whether to include a call to action") 239 | }, 240 | async ({ youtubeUrl, tone, summaryTone, audience, hashtags, includeCallToAction }) => { 241 | try { 242 | // Check if API keys are set 243 | if (!apiKeyManager.hasOpenAIKey()) { 244 | throw new Error("OpenAI API key not set. Please use the set_api_keys tool first."); 245 | } 246 | 247 | // Step 1: Extract transcript 248 | const transcript = await extractTranscript(youtubeUrl, apiKeyManager.getYouTubeKey()); 249 | 250 | // Step 2: Get video metadata (title, etc.) 251 | const videoTitle = await getVideoTitle(youtubeUrl); 252 | 253 | // Step 3: Summarize transcript 254 | const summary = await summarizeTranscript( 255 | transcript, 256 | summaryTone, 257 | audience, 258 | 200, 259 | apiKeyManager.getOpenAIKey() 260 | ); 261 | 262 | // Step 4: Generate LinkedIn post 263 | const post = await generateLinkedInPost( 264 | summary, 265 | videoTitle, 266 | undefined, // speaker name not available without additional API calls 267 | hashtags, 268 | tone, 269 | includeCallToAction, 270 | apiKeyManager.getOpenAIKey() 271 | ); 272 | 273 | return { 274 | content: [{ 275 | type: "text", 276 | text: JSON.stringify({ 277 | success: true, 278 | videoTitle, 279 | transcript: transcript.substring(0, 300) + "...", // Preview only 280 | summary, 281 | post 282 | }, null, 2) 283 | }] 284 | }; 285 | } catch (error) { 286 | return { 287 | content: [{ 288 | type: "text", 289 | text: JSON.stringify({ 290 | success: false, 291 | error: error.message 292 | }, null, 2) 293 | }], 294 | isError: true 295 | }; 296 | } 297 | }, 298 | { description: "Generate a LinkedIn post draft directly from a YouTube video URL" } 299 | ); 300 | 301 | // Helper function to extract video title from URL 302 | async function getVideoTitle(youtubeUrl) { 303 | try { 304 | // Extract video ID from URL 305 | const videoId = new URL(youtubeUrl).searchParams.get('v'); 306 | if (!videoId) { 307 | throw new Error("Could not extract video ID from URL"); 308 | } 309 | 310 | // For now, return a placeholder. In a production environment, 311 | // you would use the YouTube API to get the actual title 312 | return `YouTube Video (${videoId})`; 313 | } catch (error) { 314 | console.error("Error getting video title:", error); 315 | return "YouTube Video"; 316 | } 317 | } 318 | 319 | export { server }; 320 | --------------------------------------------------------------------------------