├── .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 | [](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 |
--------------------------------------------------------------------------------