From d223b89d0517284558ddc16494d5b705756502ba Mon Sep 17 00:00:00 2001 From: David Dias Date: Mon, 3 Mar 2025 21:03:16 -0500 Subject: [PATCH] chore: first commit --- .github/CODEOWNERS | 1 + .github/CODE_OF_CONDUCT.md | 128 ++++++ .github/CONTRIBUTING.md | 63 +++ .github/FUNDING.yml | 2 + .github/ISSUE_TEMPLATE/BUG_REPORT.yml | 48 ++ .github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml | 48 ++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/SECURITY.md | 7 + .github/actions/install/action.yml | 35 ++ .github/labeler.yml | 0 .github/workflows/release.yml | 43 ++ .gitignore | 4 + .nvmrc | 1 + README.md | 94 ++++ package.json | 45 ++ pnpm-lock.yaml | 357 +++++++++++++++ src/index.ts | 506 +++++++++++++++++++++ tsconfig.json | 15 + 18 files changed, 1402 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/CONTRIBUTING.md create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/BUG_REPORT.yml create mode 100644 .github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/SECURITY.md create mode 100644 .github/actions/install/action.yml create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100755 .nvmrc create mode 100644 README.md create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 src/index.ts create mode 100644 tsconfig.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..caae33e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @thedaviddias diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..18aeab3 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +haydenbleasel.com/contact. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..9fca847 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,63 @@ +# Contributing to This Project + +Thank you for your interest in contributing! This document outlines the process for contributing to our project. + +## Getting Started + +There are two ways to contribute a new website to our list: + +### Option 1: Web Interface (Recommended) + +The easiest way to contribute is through our web interface: + +1. Visit our website +2. Log in with your GitHub account +3. Submit your website through our user-friendly form +4. Your submission will be automatically validated and processed + +### Option 2: Manual Pull Request + +If you prefer to contribute directly through GitHub: + +1. Fork the repository +2. Create a new branch for your addition: `git checkout -b add/your-website-name` +3. Create a new MDX file in the content/websites directory +4. Add your website information following our template format +5. Test your changes thoroughly +6. Commit your changes with clear, descriptive commit messages +7. Push to your fork +8. Submit a Pull Request + +## Pull Request Guidelines + +- Ensure your PR addresses a specific issue or adds value to the project +- Include a clear description of the changes +- Keep changes focused and atomic +- Follow existing code style and conventions +- Include tests if applicable +- Update documentation as needed + +## Code Style + +- Follow the existing code formatting in the project (ensure you have Biome installed) +- Write clear, self-documenting code +- Add comments only when necessary to explain complex logic +- Use meaningful variable and function names + +## Reporting Issues + +- Use the GitHub issue tracker +- Check if the issue already exists before creating a new one +- Provide a clear description of the issue +- Include steps to reproduce if applicable +- Add relevant labels + +## Questions or Need Help? + +Feel free to open an issue for questions or join our discussions. We're here to help! + +## Code of Conduct + +Please note that this project follows a Code of Conduct. By participating, you are expected to uphold this code. + +Thank you for contributing! diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..53ac1a9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: thedaviddias +custom: ["https://thanks.dev/u/thedaviddias"] diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml new file mode 100644 index 0000000..0df980f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -0,0 +1,48 @@ +name: Bug Report +description: Report a bug to help us improve +title: "[BUG] " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report this bug! + + - type: textarea + id: description + attributes: + label: What happened? + description: A clear and concise description of the bug + placeholder: When I click X, Y happens instead of Z + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: How can we reproduce this issue? + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. See error + validations: + required: true + + - type: dropdown + id: environment + attributes: + label: Environment + description: Where does this occur? + options: + - Development + - Staging + - Production + validations: + required: true + + - type: textarea + id: additional + attributes: + label: Additional context + description: Add any other context, screenshots, or error messages diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml new file mode 100644 index 0000000..1573aab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml @@ -0,0 +1,48 @@ +name: Feature Request +description: Suggest an idea for this project +title: "[FEATURE] " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to suggest a new feature! + + - type: textarea + id: problem + attributes: + label: Problem Statement + description: What problem are you trying to solve? + placeholder: I'm always frustrated when... + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: What solution would you like to see? + placeholder: It would be great if... + validations: + required: true + + - type: dropdown + id: scope + attributes: + label: Scope + description: Which part of the project does this affect? + options: + - Frontend + - Backend + - Infrastructure + - Documentation + - Other + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: What alternative solutions have you considered? + placeholder: I also thought about... diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..dd4254b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Questions & Discussions + url: https://github.com/thedaviddias/ai-templates/discussions + about: Please ask and answer questions here diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..a8dc9a2 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +## Reporting a Vulnerability + +If you believe you have found a security vulnerability in Links Base, we encourage you to responsibly disclose this and not open a public issue. Please report it using [GitHub Security Advisory](https://github.com/thedaviddias/llms-txt-hub/security/advisories/new) tool, to ensure confidentiality and security. + +We'll review it as soon as possible and publish a fix accordingly. diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml new file mode 100644 index 0000000..df53ce5 --- /dev/null +++ b/.github/actions/install/action.yml @@ -0,0 +1,35 @@ +name: Setup project dependencies + +description: "Setup project dependencies" + +runs: + using: "composite" + steps: + - name: Install pnpm + uses: pnpm/action-setup@v4.0.0 + id: pnpm-install + with: + run_install: false + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + + - name: Cache dependencies + id: cache_dependencies + uses: actions/cache@v4 + with: + path: | + ~/.pnpm + ${{ github.workspace }}/.next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} + restore-keys: | + ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- + + - name: Install dependencies + shell: bash + # if: steps.cache_dependencies.outputs.cache-hit != 'true' + run: pnpm install diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..e69de29 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d675129 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,43 @@ +name: Release to npm + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install + uses: ./.github/actions/install + with: + registry-url: 'https://registry.npmjs.org' + scope: '@thedaviddias' + + - name: Setup npm auth + run: | + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc + echo "@thedaviddias:registry=https://registry.npmjs.org/" >> ~/.npmrc + npm whoami || echo "Not logged in to npm" + + - name: Build + run: pnpm build + + - name: Publish + run: | + echo "Attempting to publish with npm..." + npm publish --access public || { + echo "npm publish failed, trying with pnpm..." + pnpm publish --no-git-checks --access public + } + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_REGISTRY: 'https://registry.npmjs.org' + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f106e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +build/ +*.log +.env* \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100755 index 0000000..7d41c73 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.14.0 diff --git a/README.md b/README.md new file mode 100644 index 0000000..4f4b4ae --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# MCP LLMS.txt Explorer + +A Model Context Protocol server for exploring websites with llms.txt files. This server helps you discover and analyze websites that implement the llms.txt standard. + +## Features + +### Resources +- Check websites for llms.txt and llms-full.txt files +- Parse and validate llms.txt file contents +- Access structured data about compliant websites + +### Tools +- `check_website` - Check if a website has llms.txt files + - Takes domain URL as input + - Returns file locations and validation status +- `list_websites` - List known websites with llms.txt files + - Returns structured data about compliant websites + - Supports filtering by file type (llms.txt/llms-full.txt) + +## Development + +Install dependencies: +```bash +pnpm install +``` + +Build the server: +```bash +pnpm run build +``` + +For development with auto-rebuild: +```bash +pnpm run watch +``` + +## Installation + +To use this server: + +```bash +# Clone the repository +git clone https://github.com/thedaviddias/mcp-llms-txt-explorer.git +cd mcp-llms-txt-explorer + +# Install dependencies +pnpm install + +# Build the server +pnpm run build +``` + +### Configuration with Claude Desktop + +To use with Claude Desktop, add the server config: + +On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` +On Windows: `%APPDATA%/Claude/claude_desktop_config.json` + +```json +{ + "mcpServers": { + "llms-txt-explorer": { + "command": "/path/to/llms-txt-explorer/build/index.js" + } + } +} +``` + +For npx usage, you can use: +```json +{ + "mcpServers": { + "llms-txt-explorer": { + "command": "npx", + "args": ["-y", "@thedaviddias/mcp-llms-txt-explorer"] + } + } +} +``` + +### Debugging + +Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script: + +```bash +pnpm run inspector +``` + +The Inspector will provide a URL to access debugging tools in your browser. + +## License + +This project is licensed under the MIT License—see the LICENSE file for details. diff --git a/package.json b/package.json new file mode 100644 index 0000000..9b478ee --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "@thedaviddias/mcp-llms-txt-explorer", + "version": "0.1.0", + "description": "A Model Context Protocol server for exploring websites with llms.txt files", + "type": "module", + "bin": { + "mcp-llms-txt-explorer": "./build/index.js" + }, + "files": [ + "build" + ], + "scripts": { + "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"", + "prepare": "npm run build", + "watch": "tsc --watch", + "inspector": "npx @modelcontextprotocol/inspector build/index.js", + "test": "tsc --noEmit", + "prepublishOnly": "pnpm run build" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "0.6.0", + "@types/node-fetch": "^2.6.12", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "@types/node": "^20.11.24", + "typescript": "^5.3.3" + }, + "keywords": [ + "mcp", + "llms-txt", + "model-context-protocol", + "claude" + ], + "packageManager": "pnpm@9.15.0", + "engines": { + "node": ">=20" + }, + "author": "David Dias", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/thedaviddias/mcp-llms-txt-explorer.git" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..f67f785 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,357 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@modelcontextprotocol/sdk': + specifier: 0.6.0 + version: 0.6.0 + '@types/node-fetch': + specifier: ^2.6.12 + version: 2.6.12 + node-fetch: + specifier: ^3.3.2 + version: 3.3.2 + devDependencies: + '@types/node': + specifier: ^20.11.24 + version: 20.17.23 + typescript: + specifier: ^5.3.3 + version: 5.8.2 + +packages: + + '@modelcontextprotocol/sdk@0.6.0': + resolution: {integrity: sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==} + + '@types/node-fetch@2.6.12': + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + + '@types/node@20.17.23': + resolution: {integrity: sha512-8PCGZ1ZJbEZuYNTMqywO+Sj4vSKjSjT6Ua+6RFOYlEvIvKQABPtrNkoVSLSKDb4obYcMhspVKmsw8Cm10NFRUg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.19.8: + resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + zod@3.24.2: + resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + +snapshots: + + '@modelcontextprotocol/sdk@0.6.0': + dependencies: + content-type: 1.0.5 + raw-body: 3.0.0 + zod: 3.24.2 + + '@types/node-fetch@2.6.12': + dependencies: + '@types/node': 20.17.23 + form-data: 4.0.2 + + '@types/node@20.17.23': + dependencies: + undici-types: 6.19.8 + + asynckit@0.4.0: {} + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + content-type@1.0.5: {} + + data-uri-to-buffer@4.0.1: {} + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + + safer-buffer@2.1.2: {} + + setprototypeof@1.2.0: {} + + statuses@2.0.1: {} + + toidentifier@1.0.1: {} + + typescript@5.8.2: {} + + undici-types@6.19.8: {} + + unpipe@1.0.0: {} + + web-streams-polyfill@3.3.3: {} + + zod@3.24.2: {} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..da6a1fc --- /dev/null +++ b/src/index.ts @@ -0,0 +1,506 @@ +#!/opt/homebrew/bin/node + +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListResourcesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import fetch from "node-fetch"; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); +const { version } = require('../package.json'); + +const websites = 'https://raw.githubusercontent.com/thedaviddias/llms-txt-hub/main/data/websites.json' + +/** + * Type for a website with llms.txt information + */ +interface Website { + name: string; + domain: string; + description: string; + llmsTxtUrl?: string; + llmsFullTxtUrl?: string; + category?: string; + favicon?: string; +} + +/** + * Type for a linked content from llms.txt + */ +interface LinkedContent { + url: string; + content?: string; + error?: string; +} + +/** + * Type for the check website result + */ +interface WebsiteCheckResult { + hasLlmsTxt: boolean; + hasLlmsFullTxt: boolean; + llmsTxtUrl?: string; + llmsFullTxtUrl?: string; + llmsTxtContent?: string; + llmsFullTxtContent?: string; + linkedContents?: LinkedContent[]; + error?: string; +} + +/** + * Known websites with llms.txt files + * Initial data from llms-txt-hub + */ +let knownWebsites: Website[] = []; + +/** + * Cache for website check results + */ +const websiteCheckCache: { [domain: string]: WebsiteCheckResult } = {}; + +/** + * Create an MCP server for exploring llms.txt files + */ +const server = new Server( + { + name: "LLMS.txt Explorer", + version, + }, + { + capabilities: { + resources: {}, + tools: {}, + }, + } +); + +/** + * Validate website data + */ +function isValidWebsite(website: unknown): website is Website { + if (!website || typeof website !== 'object') return false; + const w = website as Record; + return ( + typeof w.name === 'string' && + typeof w.domain === 'string' && + typeof w.description === 'string' && + (w.llmsTxtUrl === undefined || typeof w.llmsTxtUrl === 'string') && + (w.llmsFullTxtUrl === undefined || typeof w.llmsFullTxtUrl === 'string') && + (w.category === undefined || typeof w.category === 'string') && + (w.favicon === undefined || typeof w.favicon === 'string') + ); +} + +/** + * Fetch websites list from GitHub + */ +async function fetchWebsitesList() { + try { + console.error('Fetching websites list from GitHub...'); + const response = await fetch(websites); + + if (!response.ok) { + throw new Error(`Failed to fetch websites list: ${response.status}`); + } + + const data = await response.json(); + + if (!Array.isArray(data)) { + throw new Error('Invalid data format: expected an array'); + } + + const validWebsites = data.filter(isValidWebsite); + console.error(`Fetched ${validWebsites.length} valid websites`); + knownWebsites = validWebsites; + } catch (error) { + console.error('Error fetching websites list:', error); + // Fallback to default website if fetch fails + knownWebsites = [{ + name: "Supabase", + domain: "https://supabase.com", + description: "Build production-grade applications with Postgres", + llmsTxtUrl: "https://supabase.com/llms.txt", + category: "developer-tools" + }]; + } +} + +/** + * Extract linked URLs from llms.txt content + */ +function extractLinkedUrls(content: string): string[] { + const urls: string[] = []; + const lines = content.split('\n'); + + for (const line of lines) { + const trimmedLine = line.trim(); + if (trimmedLine.startsWith('@')) { + const url = trimmedLine.slice(1).trim(); + if (url) { + urls.push(url); + } + } + } + + return urls; +} + +/** + * Check if a website has llms.txt files + */ +async function checkWebsite(domain: string): Promise { + console.error('Starting website check for:', domain); + + // Return cached result if available + if (websiteCheckCache[domain]) { + console.error('Returning cached result for:', domain); + return websiteCheckCache[domain]; + } + + const result: WebsiteCheckResult = { + hasLlmsTxt: false, + hasLlmsFullTxt: false + }; + + // Create an overall timeout for the entire operation + const globalTimeout = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Global timeout exceeded')); + }, 15000); // 15 second global timeout + }); + + try { + // Normalize domain and add protocol if missing + let normalizedDomain = domain; + if (!domain.startsWith('http://') && !domain.startsWith('https://')) { + normalizedDomain = `https://${domain}`; + } + console.error('Normalized domain:', normalizedDomain); + + // Validate URL format + let url: URL; + try { + url = new URL(normalizedDomain); + } catch (e) { + console.error('Invalid URL:', domain); + throw new Error(`Invalid URL format: ${domain}`); + } + + // Use the normalized URL + const baseUrl = url.origin; + console.error('Base URL:', baseUrl); + + // Helper function to fetch with timeout + async function fetchWithTimeout(url: string, timeout = 5000) { // Reduced to 5 seconds + console.error(`Fetching ${url} with ${timeout}ms timeout`); + const controller = new AbortController(); + const timeoutId = setTimeout(() => { + controller.abort(); + console.error(`Timeout after ${timeout}ms for ${url}`); + }, timeout); + + try { + const startTime = Date.now(); + const response = await fetch(url, { + signal: controller.signal, + headers: { + 'User-Agent': 'llms-txt-explorer/0.1.0' + } + }); + const endTime = Date.now(); + console.error(`Fetch completed in ${endTime - startTime}ms for ${url}`); + clearTimeout(timeoutId); + return response; + } catch (error) { + clearTimeout(timeoutId); + console.error(`Fetch error for ${url}:`, error); + throw error; + } + } + + const checkPromise = (async () => { + // Check for llms.txt + try { + const llmsTxtUrl = `${baseUrl}/llms.txt`; + console.error('Fetching llms.txt from:', llmsTxtUrl); + const llmsTxtRes = await fetchWithTimeout(llmsTxtUrl); + console.error('llms.txt response status:', llmsTxtRes.status); + + if (llmsTxtRes.ok) { + result.hasLlmsTxt = true; + result.llmsTxtUrl = llmsTxtUrl; + const content = await llmsTxtRes.text(); + console.error(`llms.txt content length: ${content.length} bytes`); + result.llmsTxtContent = content; + console.error('Successfully fetched llms.txt'); + + // Extract and fetch linked contents in parallel with timeout + const linkedUrls = extractLinkedUrls(content).slice(0, 3); // Reduced to 3 linked contents + if (linkedUrls.length > 0) { + console.error(`Found ${linkedUrls.length} linked URLs in llms.txt (limited to 3)`); + result.linkedContents = []; + + const fetchPromises = linkedUrls.map(async (url) => { + console.error(`Fetching linked content from: ${url}`); + try { + const linkedRes = await fetchWithTimeout(url); + if (!linkedRes.ok) { + throw new Error(`Failed to fetch content: ${linkedRes.status}`); + } + const linkedContent = await linkedRes.text(); + console.error(`Linked content length: ${linkedContent.length} bytes`); + return { + url, + content: linkedContent + }; + } catch (error) { + console.error(`Error fetching linked content from ${url}:`, error); + return { + url, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } + }); + + // Wait for all fetches to complete with a 10 second timeout + const linkedContentTimeout = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Linked content fetch timeout')); + }, 10000); + }); + + try { + result.linkedContents = await Promise.race([ + Promise.all(fetchPromises), + linkedContentTimeout + ]); + } catch (error) { + console.error('Error fetching linked contents:', error); + result.linkedContents = linkedUrls.map(url => ({ + url, + error: 'Timeout fetching linked contents' + })); + } + } + } + } catch (error: unknown) { + console.error('Error in main llms.txt fetch:', error); + if (error instanceof Error) { + result.error = error.message; + } else { + result.error = 'Unknown error fetching llms.txt'; + } + } + + // Only check llms-full.txt if llms.txt was successful + if (result.hasLlmsTxt && !result.error) { + try { + const llmsFullTxtUrl = `${baseUrl}/llms-full.txt`; + console.error('Fetching llms-full.txt from:', llmsFullTxtUrl); + const llmsFullTxtRes = await fetchWithTimeout(llmsFullTxtUrl); + console.error('llms-full.txt response status:', llmsFullTxtRes.status); + + if (llmsFullTxtRes.ok) { + result.hasLlmsFullTxt = true; + result.llmsFullTxtUrl = llmsFullTxtUrl; + const content = await llmsFullTxtRes.text(); + console.error(`llms-full.txt content length: ${content.length} bytes`); + result.llmsFullTxtContent = content; + console.error('Successfully fetched llms-full.txt'); + } + } catch (error) { + console.error('Error fetching llms-full.txt:', error); + // Don't fail the whole operation for llms-full.txt errors + } + } + + return result; + })(); + + // Race between the check operation and the global timeout + const finalResult = await Promise.race([checkPromise, globalTimeout]); + + // Cache successful results only + if (!finalResult.error) { + websiteCheckCache[domain] = finalResult; + } + + console.error('Final result:', JSON.stringify(finalResult, null, 2)); + return finalResult; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + console.error('Error checking website:', errorMessage); + return { + hasLlmsTxt: false, + hasLlmsFullTxt: false, + error: errorMessage + }; + } +} + +/** + * Handler for listing available websites as resources + */ +server.setRequestHandler(ListResourcesRequestSchema, async () => { + return { + resources: knownWebsites.map(site => ({ + uri: `website://${site.domain}`, + mimeType: "application/json", + name: site.name, + description: site.description + })) + }; +}); + +/** + * Handler for reading website information + */ +server.setRequestHandler(ReadResourceRequestSchema, async (request) => { + const url = new URL(request.params.uri); + const domain = url.hostname; + + const website = knownWebsites.find(site => new URL(site.domain).hostname === domain); + if (!website) { + throw new Error(`Website ${domain} not found in known websites`); + } + + const checkResult = await checkWebsite(website.domain); + + return { + contents: [{ + uri: request.params.uri, + mimeType: "application/json", + text: JSON.stringify({ ...website, ...checkResult }, null, 2) + }] + }; +}); + +/** + * Handler that lists available tools + */ +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "check_website", + description: "Check if a website has llms.txt files", + inputSchema: { + type: "object", + properties: { + url: { + type: "string", + description: "URL of the website to check" + } + }, + required: ["url"] + } + }, + { + name: "list_websites", + description: "List known websites with llms.txt files", + inputSchema: { + type: "object", + properties: { + filter_llms_txt: { + type: "boolean", + description: "Only show websites with llms.txt" + }, + filter_llms_full_txt: { + type: "boolean", + description: "Only show websites with llms-full.txt" + } + } + } + } + ] + }; +}); + +/** + * Handler for tool calls + */ +server.setRequestHandler(CallToolRequestSchema, async (request) => { + console.error('Received tool request:', request.params.name); + + switch (request.params.name) { + case "check_website": { + const url = String(request.params.arguments?.url); + console.error('Checking website:', url); + + if (!url) { + console.error('URL is required'); + return { + content: [{ + type: "text", + text: JSON.stringify({ error: "URL is required" }, null, 2) + }] + }; + } + + try { + const result = await checkWebsite(url); + console.error('Tool returning result:', JSON.stringify(result, null, 2)); + return { + content: [{ + type: "text", + text: JSON.stringify(result, null, 2) + }] + }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + console.error('Tool returning error:', errorMessage); + return { + content: [{ + type: "text", + text: JSON.stringify({ error: errorMessage }, null, 2) + }] + }; + } + } + + case "list_websites": { + const filterLlmsTxt = Boolean(request.params.arguments?.filter_llms_txt); + const filterLlmsFullTxt = Boolean(request.params.arguments?.filter_llms_full_txt); + + let websites = knownWebsites; + + if (filterLlmsTxt) { + websites = websites.filter(site => site.llmsTxtUrl); + } + if (filterLlmsFullTxt) { + websites = websites.filter(site => site.llmsFullTxtUrl); + } + + return { + content: [{ + type: "text", + text: JSON.stringify(websites, null, 2) + }] + }; + } + + default: + throw new Error("Unknown tool"); + } +}); + +/** + * Start the server using stdio transport + */ +async function main() { + // Fetch websites list before starting the server + await fetchWebsitesList(); + + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +main().catch((error) => { + console.error("Server error:", error); + process.exit(1); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a14bee0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./build", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +}