mcp.txt
This commit is contained in:
commit
92e7732ab4
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
data/
|
||||||
|
.toes
|
||||||
|
|
||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
.sandlot/
|
||||||
38
README.md
Normal file
38
README.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# mcp.txt
|
||||||
|
|
||||||
|
A minimal MCP server for managing text files from Claude (web, desktop, and iOS).
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DATA_DIR=~/Documents/claude-files PORT=3100 bun start
|
||||||
|
```
|
||||||
|
|
||||||
|
- `DATA_DIR` — directory the server reads/writes from (default: `~/Documents/claude-files`)
|
||||||
|
- `PORT` — HTTP port (default: `3001`)
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `list_files` | List files and directories in a given path |
|
||||||
|
| `read_file` | Read the contents of a text file |
|
||||||
|
| `create_file` | Create a new file (fails if it already exists) |
|
||||||
|
| `edit_file` | Replace the contents of an existing file |
|
||||||
|
|
||||||
|
All paths are relative to `DATA_DIR`. Path traversal is rejected. There is no delete — by design.
|
||||||
|
|
||||||
|
## Connect to Claude
|
||||||
|
|
||||||
|
1. Run the server
|
||||||
|
2. Expose via HTTPS tunnel (e.g. Sneakers, ngrok, Cloudflare Tunnel)
|
||||||
|
3. Claude.ai → Settings → Integrations → Add Custom Integration
|
||||||
|
4. Set the URL to `https://your-tunnel/mcp`
|
||||||
|
|
||||||
|
Works on web, desktop, and iOS.
|
||||||
222
bun.lock
Normal file
222
bun.lock
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "mcp.txt",
|
||||||
|
"dependencies": {
|
||||||
|
"@because/forge": "^0.0.3",
|
||||||
|
"@because/hype": "^0.0.6",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
|
"marked": "^17.0.3",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@because/forge": ["@because/forge@0.0.3", "https://npm.nose.space/@because/forge/-/forge-0.0.3.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-3V2be2vkkW1qZH6WSurfYBgyVjot/GcyOt5LfIVyEYQ5cAq6bWbtxsXs5CK/sT8OqbvCu3VHc2k2OXOC6Q3feg=="],
|
||||||
|
|
||||||
|
"@because/hype": ["@because/hype@0.0.6", "https://npm.nose.space/@because/hype/-/hype-0.0.6.tgz", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "sha512-WSRPNoeTBR3nRcPTqfbu6+FUaNenCo/sN/CB2Ism7oiJwTap1i+1AlWPa+MF1eMQlNd2AYRlA3AAu6F52j6/fA=="],
|
||||||
|
|
||||||
|
"@hono/node-server": ["@hono/node-server@1.19.9", "https://npm.nose.space/@hono/node-server/-/node-server-1.19.9.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
||||||
|
|
||||||
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "https://npm.nose.space/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/bun@1.3.9", "https://npm.nose.space/@types/bun/-/bun-1.3.9.tgz", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@25.2.3", "https://npm.nose.space/@types/node/-/node-25.2.3.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
|
||||||
|
|
||||||
|
"accepts": ["accepts@2.0.0", "https://npm.nose.space/accepts/-/accepts-2.0.0.tgz", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||||
|
|
||||||
|
"ajv": ["ajv@8.18.0", "https://npm.nose.space/ajv/-/ajv-8.18.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
|
"ajv-formats": ["ajv-formats@3.0.1", "https://npm.nose.space/ajv-formats/-/ajv-formats-3.0.1.tgz", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||||
|
|
||||||
|
"body-parser": ["body-parser@2.2.2", "https://npm.nose.space/body-parser/-/body-parser-2.2.2.tgz", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.9", "https://npm.nose.space/bun-types/-/bun-types-1.3.9.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
|
||||||
|
|
||||||
|
"bytes": ["bytes@3.1.2", "https://npm.nose.space/bytes/-/bytes-3.1.2.tgz", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://npm.nose.space/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"call-bound": ["call-bound@1.0.4", "https://npm.nose.space/call-bound/-/call-bound-1.0.4.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||||
|
|
||||||
|
"content-disposition": ["content-disposition@1.0.1", "https://npm.nose.space/content-disposition/-/content-disposition-1.0.1.tgz", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||||
|
|
||||||
|
"content-type": ["content-type@1.0.5", "https://npm.nose.space/content-type/-/content-type-1.0.5.tgz", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@0.7.2", "https://npm.nose.space/cookie/-/cookie-0.7.2.tgz", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
|
"cookie-signature": ["cookie-signature@1.2.2", "https://npm.nose.space/cookie-signature/-/cookie-signature-1.2.2.tgz", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
|
|
||||||
|
"cors": ["cors@2.8.6", "https://npm.nose.space/cors/-/cors-2.8.6.tgz", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "https://npm.nose.space/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "https://npm.nose.space/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"depd": ["depd@2.0.0", "https://npm.nose.space/depd/-/depd-2.0.0.tgz", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "https://npm.nose.space/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"ee-first": ["ee-first@1.1.1", "https://npm.nose.space/ee-first/-/ee-first-1.1.1.tgz", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||||
|
|
||||||
|
"encodeurl": ["encodeurl@2.0.0", "https://npm.nose.space/encodeurl/-/encodeurl-2.0.0.tgz", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "https://npm.nose.space/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "https://npm.nose.space/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "https://npm.nose.space/es-object-atoms/-/es-object-atoms-1.1.1.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"escape-html": ["escape-html@1.0.3", "https://npm.nose.space/escape-html/-/escape-html-1.0.3.tgz", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
|
|
||||||
|
"etag": ["etag@1.8.1", "https://npm.nose.space/etag/-/etag-1.8.1.tgz", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||||
|
|
||||||
|
"eventsource": ["eventsource@3.0.7", "https://npm.nose.space/eventsource/-/eventsource-3.0.7.tgz", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||||
|
|
||||||
|
"eventsource-parser": ["eventsource-parser@3.0.6", "https://npm.nose.space/eventsource-parser/-/eventsource-parser-3.0.6.tgz", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||||
|
|
||||||
|
"express": ["express@5.2.1", "https://npm.nose.space/express/-/express-5.2.1.tgz", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||||
|
|
||||||
|
"express-rate-limit": ["express-rate-limit@8.2.1", "https://npm.nose.space/express-rate-limit/-/express-rate-limit-8.2.1.tgz", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="],
|
||||||
|
|
||||||
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "https://npm.nose.space/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
|
"fast-uri": ["fast-uri@3.1.0", "https://npm.nose.space/fast-uri/-/fast-uri-3.1.0.tgz", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||||
|
|
||||||
|
"finalhandler": ["finalhandler@2.1.1", "https://npm.nose.space/finalhandler/-/finalhandler-2.1.1.tgz", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||||
|
|
||||||
|
"forwarded": ["forwarded@0.2.0", "https://npm.nose.space/forwarded/-/forwarded-0.2.0.tgz", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||||
|
|
||||||
|
"fresh": ["fresh@2.0.0", "https://npm.nose.space/fresh/-/fresh-2.0.0.tgz", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "https://npm.nose.space/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "https://npm.nose.space/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "https://npm.nose.space/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "https://npm.nose.space/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "https://npm.nose.space/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.2", "https://npm.nose.space/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.11.10", "https://npm.nose.space/hono/-/hono-4.11.10.tgz", {}, "sha512-kyWP5PAiMooEvGrA9jcD3IXF7ATu8+o7B3KCbPXid5se52NPqnOpM/r9qeW2heMnOekF4kqR1fXJqCYeCLKrZg=="],
|
||||||
|
|
||||||
|
"http-errors": ["http-errors@2.0.1", "https://npm.nose.space/http-errors/-/http-errors-2.0.1.tgz", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.7.2", "https://npm.nose.space/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||||
|
|
||||||
|
"inherits": ["inherits@2.0.4", "https://npm.nose.space/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"ip-address": ["ip-address@10.0.1", "https://npm.nose.space/ip-address/-/ip-address-10.0.1.tgz", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
|
||||||
|
|
||||||
|
"ipaddr.js": ["ipaddr.js@1.9.1", "https://npm.nose.space/ipaddr.js/-/ipaddr.js-1.9.1.tgz", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||||
|
|
||||||
|
"is-promise": ["is-promise@4.0.0", "https://npm.nose.space/is-promise/-/is-promise-4.0.0.tgz", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@2.0.0", "https://npm.nose.space/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"jose": ["jose@6.1.3", "https://npm.nose.space/jose/-/jose-6.1.3.tgz", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "https://npm.nose.space/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
|
"json-schema-typed": ["json-schema-typed@8.0.2", "https://npm.nose.space/json-schema-typed/-/json-schema-typed-8.0.2.tgz", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||||
|
|
||||||
|
"kleur": ["kleur@4.1.5", "https://npm.nose.space/kleur/-/kleur-4.1.5.tgz", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|
||||||
|
"marked": ["marked@17.0.3", "https://npm.nose.space/marked/-/marked-17.0.3.tgz", { "bin": { "marked": "bin/marked.js" } }, "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "https://npm.nose.space/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"media-typer": ["media-typer@1.1.0", "https://npm.nose.space/media-typer/-/media-typer-1.1.0.tgz", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||||
|
|
||||||
|
"merge-descriptors": ["merge-descriptors@2.0.0", "https://npm.nose.space/merge-descriptors/-/merge-descriptors-2.0.0.tgz", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||||
|
|
||||||
|
"mime-db": ["mime-db@1.54.0", "https://npm.nose.space/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
|
|
||||||
|
"mime-types": ["mime-types@3.0.2", "https://npm.nose.space/mime-types/-/mime-types-3.0.2.tgz", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "https://npm.nose.space/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"negotiator": ["negotiator@1.0.0", "https://npm.nose.space/negotiator/-/negotiator-1.0.0.tgz", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "https://npm.nose.space/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"object-inspect": ["object-inspect@1.13.4", "https://npm.nose.space/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
|
"on-finished": ["on-finished@2.4.1", "https://npm.nose.space/on-finished/-/on-finished-2.4.1.tgz", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||||
|
|
||||||
|
"once": ["once@1.4.0", "https://npm.nose.space/once/-/once-1.4.0.tgz", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
|
"parseurl": ["parseurl@1.3.3", "https://npm.nose.space/parseurl/-/parseurl-1.3.3.tgz", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "https://npm.nose.space/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"path-to-regexp": ["path-to-regexp@8.3.0", "https://npm.nose.space/path-to-regexp/-/path-to-regexp-8.3.0.tgz", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="],
|
||||||
|
|
||||||
|
"pkce-challenge": ["pkce-challenge@5.0.1", "https://npm.nose.space/pkce-challenge/-/pkce-challenge-5.0.1.tgz", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||||
|
|
||||||
|
"proxy-addr": ["proxy-addr@2.0.7", "https://npm.nose.space/proxy-addr/-/proxy-addr-2.0.7.tgz", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||||
|
|
||||||
|
"qs": ["qs@6.15.0", "https://npm.nose.space/qs/-/qs-6.15.0.tgz", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||||
|
|
||||||
|
"range-parser": ["range-parser@1.2.1", "https://npm.nose.space/range-parser/-/range-parser-1.2.1.tgz", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||||
|
|
||||||
|
"raw-body": ["raw-body@3.0.2", "https://npm.nose.space/raw-body/-/raw-body-3.0.2.tgz", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||||
|
|
||||||
|
"require-from-string": ["require-from-string@2.0.2", "https://npm.nose.space/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
|
"router": ["router@2.2.0", "https://npm.nose.space/router/-/router-2.2.0.tgz", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "https://npm.nose.space/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"send": ["send@1.2.1", "https://npm.nose.space/send/-/send-1.2.1.tgz", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||||
|
|
||||||
|
"serve-static": ["serve-static@2.2.1", "https://npm.nose.space/serve-static/-/serve-static-2.2.1.tgz", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||||
|
|
||||||
|
"setprototypeof": ["setprototypeof@1.2.0", "https://npm.nose.space/setprototypeof/-/setprototypeof-1.2.0.tgz", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "https://npm.nose.space/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "https://npm.nose.space/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"side-channel": ["side-channel@1.1.0", "https://npm.nose.space/side-channel/-/side-channel-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||||
|
|
||||||
|
"side-channel-list": ["side-channel-list@1.0.0", "https://npm.nose.space/side-channel-list/-/side-channel-list-1.0.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||||
|
|
||||||
|
"side-channel-map": ["side-channel-map@1.0.1", "https://npm.nose.space/side-channel-map/-/side-channel-map-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||||
|
|
||||||
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "https://npm.nose.space/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
|
"statuses": ["statuses@2.0.2", "https://npm.nose.space/statuses/-/statuses-2.0.2.tgz", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
||||||
|
"toidentifier": ["toidentifier@1.0.1", "https://npm.nose.space/toidentifier/-/toidentifier-1.0.1.tgz", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
|
"type-is": ["type-is@2.0.1", "https://npm.nose.space/type-is/-/type-is-2.0.1.tgz", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "https://npm.nose.space/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@7.16.0", "https://npm.nose.space/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
|
"unpipe": ["unpipe@1.0.0", "https://npm.nose.space/unpipe/-/unpipe-1.0.0.tgz", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||||
|
|
||||||
|
"vary": ["vary@1.1.2", "https://npm.nose.space/vary/-/vary-1.1.2.tgz", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||||
|
|
||||||
|
"which": ["which@2.0.2", "https://npm.nose.space/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"wrappy": ["wrappy@1.0.2", "https://npm.nose.space/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.3.6", "https://npm.nose.space/zod/-/zod-4.3.6.tgz", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||||
|
|
||||||
|
"zod-to-json-schema": ["zod-to-json-schema@3.25.1", "https://npm.nose.space/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
89
index.html
Normal file
89
index.html
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>mcp.txt file browser</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>mcp.txt file browser</h1>
|
||||||
|
<p>Path: <code id="current-path">/</code></p>
|
||||||
|
<div id="listing"></div>
|
||||||
|
<hr>
|
||||||
|
<pre id="file-content" hidden></pre>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentPath = ".";
|
||||||
|
|
||||||
|
async function navigate(path) {
|
||||||
|
currentPath = path;
|
||||||
|
document.getElementById("current-path").textContent = path === "." ? "/" : "/" + path;
|
||||||
|
document.getElementById("file-content").hidden = true;
|
||||||
|
|
||||||
|
const res = await fetch("/api/list?path=" + encodeURIComponent(path));
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
document.getElementById("listing").textContent = "Error: " + data.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listing = document.getElementById("listing");
|
||||||
|
listing.innerHTML = "";
|
||||||
|
const ul = document.createElement("ul");
|
||||||
|
|
||||||
|
// parent directory link
|
||||||
|
if (path !== ".") {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = "#";
|
||||||
|
a.textContent = "..";
|
||||||
|
const parent = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : ".";
|
||||||
|
a.onclick = (e) => { e.preventDefault(); navigate(parent); };
|
||||||
|
li.appendChild(a);
|
||||||
|
ul.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of data.entries) {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = "#";
|
||||||
|
a.textContent = entry.name + (entry.type === "directory" ? "/" : "");
|
||||||
|
const entryPath = path === "." ? entry.name : path + "/" + entry.name;
|
||||||
|
|
||||||
|
if (entry.type === "directory") {
|
||||||
|
a.onclick = (e) => { e.preventDefault(); navigate(entryPath); };
|
||||||
|
} else {
|
||||||
|
a.onclick = (e) => { e.preventDefault(); readFile(entryPath); };
|
||||||
|
li.append(" (" + entry.size + " bytes)");
|
||||||
|
}
|
||||||
|
|
||||||
|
li.prepend(a);
|
||||||
|
ul.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.entries.length === 0) {
|
||||||
|
listing.textContent = "(empty directory)";
|
||||||
|
} else {
|
||||||
|
listing.appendChild(ul);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readFile(path) {
|
||||||
|
const pre = document.getElementById("file-content");
|
||||||
|
pre.hidden = false;
|
||||||
|
pre.textContent = "Loading...";
|
||||||
|
|
||||||
|
const res = await fetch("/api/read?path=" + encodeURIComponent(path));
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
pre.textContent = "Error: " + data.error;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.textContent = "=== " + path + " ===\n\n" + data.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(".");
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
package.json
Normal file
26
package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "mcp.txt",
|
||||||
|
"module": "src/server/index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"start": "bun run src/server/index.ts",
|
||||||
|
"dev": "bun run --hot src/server/index.ts",
|
||||||
|
"toes": "bun run --hot src/server/index.ts"
|
||||||
|
},
|
||||||
|
"toes": {
|
||||||
|
"icon": "📝"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@because/forge": "^0.0.3",
|
||||||
|
"@because/hype": "^0.0.6",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
|
"marked": "^17.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/client/edit.ts
Normal file
82
src/client/edit.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
const editor = document.getElementById("editor") as HTMLTextAreaElement;
|
||||||
|
const backLink = document.getElementById("back-link") as HTMLAnchorElement;
|
||||||
|
const saveBtn = document.getElementById("save-btn") as HTMLButtonElement;
|
||||||
|
const status = document.getElementById("status")!;
|
||||||
|
|
||||||
|
const path = decodeURIComponent(window.location.pathname.replace(/^\/edit\//, "")) || null;
|
||||||
|
|
||||||
|
// Set back link to return to the file view
|
||||||
|
if (path) {
|
||||||
|
backLink.href = "/" + path.split("/").map(encodeURIComponent).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
let savedContent = "";
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
editor.value = "Error: no file path specified";
|
||||||
|
editor.disabled = true;
|
||||||
|
} else {
|
||||||
|
const res = await fetch("/api/read?path=" + encodeURIComponent(path));
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
editor.value = "Error: " + data.error;
|
||||||
|
editor.disabled = true;
|
||||||
|
} else {
|
||||||
|
savedContent = data.content;
|
||||||
|
editor.value = data.content;
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDirty = () => editor.value !== savedContent;
|
||||||
|
|
||||||
|
editor.addEventListener("input", () => {
|
||||||
|
const dirty = isDirty();
|
||||||
|
saveBtn.disabled = !dirty;
|
||||||
|
status.textContent = dirty ? "Unsaved changes" : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("beforeunload", (e) => {
|
||||||
|
if (isDirty()) e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function save() {
|
||||||
|
if (!path) return;
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
status.textContent = "Saving...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/edit", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ path, content: editor.value }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
status.textContent = "Error: " + data.error;
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
} else {
|
||||||
|
savedContent = editor.value;
|
||||||
|
window.location.href = "/" + path.split("/").map(encodeURIComponent).join("/");
|
||||||
|
}
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const message = err instanceof Error ? err.message : "Failed to save";
|
||||||
|
status.textContent = "Error: " + message;
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBtn.addEventListener("click", save);
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!saveBtn.disabled) save();
|
||||||
|
}
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!saveBtn.disabled) save();
|
||||||
|
}
|
||||||
|
});
|
||||||
51
src/client/file.ts
Normal file
51
src/client/file.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { marked } from "marked";
|
||||||
|
|
||||||
|
const content = document.getElementById("file-content")!;
|
||||||
|
const markdownContent = document.getElementById("markdown-content")!;
|
||||||
|
const backLink = document.getElementById("back-link") as HTMLAnchorElement;
|
||||||
|
const deleteBtn = document.getElementById("delete-btn") as HTMLButtonElement;
|
||||||
|
|
||||||
|
const path = decodeURIComponent(window.location.pathname.slice(1)) || null;
|
||||||
|
|
||||||
|
const isMarkdown = path?.endsWith(".md") || path?.endsWith(".markdown");
|
||||||
|
|
||||||
|
// Set back link to return to the parent directory
|
||||||
|
if (path && path.includes("/")) {
|
||||||
|
const dir = path.substring(0, path.lastIndexOf("/"));
|
||||||
|
backLink.href = "/?dir=" + encodeURIComponent(dir);
|
||||||
|
} else {
|
||||||
|
backLink.href = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
content.textContent = "Error: no file path specified";
|
||||||
|
} else {
|
||||||
|
const res = await fetch("/api/read?path=" + encodeURIComponent(path));
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
content.textContent = "Error: " + data.error;
|
||||||
|
} else if (isMarkdown) {
|
||||||
|
content.style.display = "none";
|
||||||
|
markdownContent.style.display = "block";
|
||||||
|
markdownContent.innerHTML = await marked(data.content);
|
||||||
|
} else {
|
||||||
|
content.textContent = data.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteBtn.addEventListener("click", async () => {
|
||||||
|
if (!path) return;
|
||||||
|
if (!confirm(`Delete "${path}"? This cannot be undone.`)) return;
|
||||||
|
|
||||||
|
const res = await fetch("/api/files/" + encodeURIComponent(path), {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
alert("Delete failed: " + data.error);
|
||||||
|
} else {
|
||||||
|
window.location.href = backLink.href;
|
||||||
|
}
|
||||||
|
});
|
||||||
173
src/client/main.ts
Normal file
173
src/client/main.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
let currentPath = ".";
|
||||||
|
|
||||||
|
const listingEl = document.getElementById("listing")!;
|
||||||
|
const newFileBtn = document.getElementById("new-file-btn")!;
|
||||||
|
const newFileDialog = document.getElementById(
|
||||||
|
"new-file-dialog",
|
||||||
|
) as HTMLDialogElement;
|
||||||
|
const closeNewBtn = document.getElementById("close-new-dialog")!;
|
||||||
|
const newFileForm = document.getElementById(
|
||||||
|
"new-file-form",
|
||||||
|
) as HTMLFormElement;
|
||||||
|
const newFileName = document.getElementById(
|
||||||
|
"new-file-name",
|
||||||
|
) as HTMLInputElement;
|
||||||
|
const newFileContentInput = document.getElementById(
|
||||||
|
"new-file-content",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
const newFileError = document.getElementById("new-file-error")!;
|
||||||
|
|
||||||
|
newFileBtn.addEventListener("click", () => {
|
||||||
|
newFileName.value = "";
|
||||||
|
newFileContentInput.value = "";
|
||||||
|
newFileError.style.display = "none";
|
||||||
|
newFileDialog.showModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
closeNewBtn.addEventListener("click", () => newFileDialog.close());
|
||||||
|
|
||||||
|
newFileDialog.addEventListener("keydown", (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
newFileForm.requestSubmit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
newFileForm.addEventListener("submit", async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
newFileError.style.display = "none";
|
||||||
|
|
||||||
|
const name = newFileName.value.trim();
|
||||||
|
if (!name) {
|
||||||
|
newFileError.textContent = "Filename is required.";
|
||||||
|
newFileError.style.display = "block";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = currentPath === "." ? name : currentPath + "/" + name;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/create", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ path, content: newFileContentInput.value }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
newFileError.textContent = data.error;
|
||||||
|
newFileError.style.display = "block";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newFileDialog.close();
|
||||||
|
window.location.href = "/" + path.split("/").map(encodeURIComponent).join("/");
|
||||||
|
} catch (err: any) {
|
||||||
|
newFileError.textContent = err.message || "Failed to create file.";
|
||||||
|
newFileError.style.display = "block";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function navigate(path: string) {
|
||||||
|
currentPath = path;
|
||||||
|
|
||||||
|
const res = await fetch("/api/list?path=" + encodeURIComponent(path));
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
listingEl.innerHTML = "<p>Error: " + data.error + "</p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.entries.length === 0) {
|
||||||
|
listingEl.innerHTML = "<p><em>Empty directory</em></p>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort: directories first, then files
|
||||||
|
const sorted = data.entries.sort((a: any, b: any) => {
|
||||||
|
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = document.createElement("table");
|
||||||
|
table.setAttribute("role", "grid");
|
||||||
|
table.innerHTML =
|
||||||
|
"<thead><tr><th>Name</th><th>Size</th><th>Modified</th></tr></thead>";
|
||||||
|
const tbody = document.createElement("tbody");
|
||||||
|
|
||||||
|
// Parent directory row
|
||||||
|
if (path !== ".") {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
const td = document.createElement("td");
|
||||||
|
td.setAttribute("colspan", "3");
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = "#";
|
||||||
|
a.textContent = "..";
|
||||||
|
const parent = path.includes("/")
|
||||||
|
? path.substring(0, path.lastIndexOf("/"))
|
||||||
|
: ".";
|
||||||
|
a.addEventListener("click", (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(parent);
|
||||||
|
});
|
||||||
|
td.appendChild(a);
|
||||||
|
tr.appendChild(td);
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of sorted) {
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
const entryPath = path === "." ? entry.name : path + "/" + entry.name;
|
||||||
|
|
||||||
|
const nameTd = document.createElement("td");
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = "#";
|
||||||
|
a.textContent =
|
||||||
|
entry.type === "directory" ? entry.name + "/" : entry.name;
|
||||||
|
|
||||||
|
if (entry.type === "directory") {
|
||||||
|
a.addEventListener("click", (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(entryPath);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
a.addEventListener("click", (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
readFile(entryPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
nameTd.appendChild(a);
|
||||||
|
tr.appendChild(nameTd);
|
||||||
|
|
||||||
|
const sizeTd = document.createElement("td");
|
||||||
|
sizeTd.textContent =
|
||||||
|
entry.type === "file" ? formatSize(entry.size) : "";
|
||||||
|
tr.appendChild(sizeTd);
|
||||||
|
|
||||||
|
const modTd = document.createElement("td");
|
||||||
|
modTd.textContent = entry.modified
|
||||||
|
? new Date(entry.modified).toLocaleString()
|
||||||
|
: "";
|
||||||
|
tr.appendChild(modTd);
|
||||||
|
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
table.appendChild(tbody);
|
||||||
|
listingEl.innerHTML = "";
|
||||||
|
listingEl.appendChild(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSize(bytes: number): string {
|
||||||
|
if (bytes < 1024) return bytes + " B";
|
||||||
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
||||||
|
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFile(path: string) {
|
||||||
|
window.location.href = "/" + path.split("/").map(encodeURIComponent).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
navigate(params.get("dir") || ".");
|
||||||
71
src/pages/_edit.tsx
Normal file
71
src/pages/_edit.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { define, Styles } from "@because/forge";
|
||||||
|
|
||||||
|
const BackLink = define("EditBackLink", {
|
||||||
|
base: "a",
|
||||||
|
display: "inline-block",
|
||||||
|
marginBottom: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Editor = define("Editor", {
|
||||||
|
base: "textarea",
|
||||||
|
width: "100%",
|
||||||
|
minHeight: 400,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: 14,
|
||||||
|
resize: "vertical",
|
||||||
|
});
|
||||||
|
|
||||||
|
const Toolbar = define("EditToolbar", {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
marginBottom: 12,
|
||||||
|
});
|
||||||
|
|
||||||
|
const SaveBtn = define("SaveBtn", {
|
||||||
|
base: "button",
|
||||||
|
padding: "8px 16px",
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "inherit",
|
||||||
|
lineHeight: "inherit",
|
||||||
|
background: "var(--pico-primary-background)",
|
||||||
|
border: "1px solid var(--pico-primary-background)",
|
||||||
|
color: "var(--pico-primary-inverse)",
|
||||||
|
|
||||||
|
states: {
|
||||||
|
hover: { background: "var(--pico-primary-hover-background)" },
|
||||||
|
disabled: { opacity: "0.5", cursor: "default" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const Status = define("EditStatus", {
|
||||||
|
base: "span",
|
||||||
|
fontSize: 14,
|
||||||
|
color: "var(--pico-muted-color)",
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ({ req }: any) => {
|
||||||
|
const path = req.param("path") || "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<Styles />
|
||||||
|
<BackLink id="back-link" href={`/${path.split("/").map(encodeURIComponent).join("/")}`}>
|
||||||
|
← Back to file
|
||||||
|
</BackLink>
|
||||||
|
<h3 id="file-title">Editing: {path}</h3>
|
||||||
|
<Editor id="editor" spellcheck={false}>Loading...</Editor>
|
||||||
|
<Toolbar>
|
||||||
|
<SaveBtn id="save-btn" disabled>Save</SaveBtn>
|
||||||
|
<Status id="status"></Status>
|
||||||
|
</Toolbar>
|
||||||
|
<script
|
||||||
|
type="module"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `import "/client/edit.ts"`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
102
src/pages/_file.tsx
Normal file
102
src/pages/_file.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import { define, Styles } from "@because/forge";
|
||||||
|
|
||||||
|
const TopBar = define("FileTopBar", {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
marginBottom: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
const BackLink = define("FileBackLink", {
|
||||||
|
base: "a",
|
||||||
|
});
|
||||||
|
|
||||||
|
const Title = define("FileTitle", {
|
||||||
|
base: "h3",
|
||||||
|
margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Actions = define("FileActions", {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionButton = define("ActionButton", {
|
||||||
|
base: "a",
|
||||||
|
padding: "6px 14px",
|
||||||
|
borderRadius: 4,
|
||||||
|
fontSize: 14,
|
||||||
|
textDecoration: "none",
|
||||||
|
cursor: "pointer",
|
||||||
|
background: "var(--pico-primary-background)",
|
||||||
|
color: "var(--pico-primary-inverse)",
|
||||||
|
|
||||||
|
states: {
|
||||||
|
hover: { background: "var(--pico-primary-hover-background)" },
|
||||||
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
danger: {
|
||||||
|
true: {
|
||||||
|
background: "var(--pico-del-color)",
|
||||||
|
states: {
|
||||||
|
hover: { background: "var(--pico-del-color)", opacity: 0.85 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const CodeBlock = define("CodeBlock", {
|
||||||
|
base: "pre",
|
||||||
|
width: "100%",
|
||||||
|
minHeight: 200,
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: 14,
|
||||||
|
padding: 16,
|
||||||
|
overflow: "auto",
|
||||||
|
border: "1px solid var(--pico-muted-border-color)",
|
||||||
|
borderRadius: 4,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordWrap: "break-word",
|
||||||
|
});
|
||||||
|
|
||||||
|
const MarkdownContent = define("MarkdownContent", {
|
||||||
|
base: "div",
|
||||||
|
width: "100%",
|
||||||
|
minHeight: 200,
|
||||||
|
padding: 16,
|
||||||
|
overflow: "auto",
|
||||||
|
border: "1px solid var(--pico-muted-border-color)",
|
||||||
|
borderRadius: 4,
|
||||||
|
lineHeight: 1.6,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ({ req }: any) => {
|
||||||
|
const path = req.param("path") || "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<Styles />
|
||||||
|
<TopBar>
|
||||||
|
<Title id="file-title">{path}</Title>
|
||||||
|
<Actions>
|
||||||
|
<BackLink id="back-link" href="/">
|
||||||
|
← Back
|
||||||
|
</BackLink>
|
||||||
|
<ActionButton href={`/edit/${path.split("/").map(encodeURIComponent).join("/")}`}>Edit</ActionButton>
|
||||||
|
<ActionButton as="button" id="delete-btn" danger>Delete</ActionButton>
|
||||||
|
</Actions>
|
||||||
|
</TopBar>
|
||||||
|
<CodeBlock id="file-content">Loading...</CodeBlock>
|
||||||
|
<MarkdownContent id="markdown-content" style="display:none" />
|
||||||
|
<script
|
||||||
|
type="module"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `import "/client/file.ts"`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
23
src/pages/_layout.tsx
Normal file
23
src/pages/_layout.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { type FC } from 'hono/jsx'
|
||||||
|
|
||||||
|
const Layout: FC = ({ children, title, props }) =>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{title ?? 'hype'}</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="color-scheme" content="light dark" />
|
||||||
|
|
||||||
|
{props.reset && <link href="/css/reset.css" rel="stylesheet" />}
|
||||||
|
{props.pico && <link href="/css/pico.css" rel="stylesheet" />}
|
||||||
|
<link href="/css/main.css" rel="stylesheet" />
|
||||||
|
<script src="/client/main.ts" type="module"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
export default Layout
|
||||||
97
src/pages/index.tsx
Normal file
97
src/pages/index.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
import { define, Styles } from "@because/forge";
|
||||||
|
|
||||||
|
// --- Forge Components ---
|
||||||
|
|
||||||
|
const Toolbar = define("IndexToolbar", {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
marginBottom: 16,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Btn = define("Btn", {
|
||||||
|
base: "button",
|
||||||
|
padding: "8px 16px",
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "inherit",
|
||||||
|
lineHeight: "inherit",
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
right: {
|
||||||
|
marginLeft: 'auto'
|
||||||
|
},
|
||||||
|
intent: {
|
||||||
|
primary: {
|
||||||
|
background: "var(--pico-primary-background)",
|
||||||
|
border: "1px solid var(--pico-primary-background)",
|
||||||
|
color: "var(--pico-primary-inverse)",
|
||||||
|
|
||||||
|
states: {
|
||||||
|
hover: { background: "var(--pico-primary-hover-background)" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
outline: {
|
||||||
|
border: "1px solid var(--pico-muted-border-color)",
|
||||||
|
color: "var(--pico-color)",
|
||||||
|
|
||||||
|
states: {
|
||||||
|
hover: { borderColor: "var(--pico-primary)" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const FormError = define("FormError", {
|
||||||
|
base: "p",
|
||||||
|
color: "var(--pico-del-color)",
|
||||||
|
display: "none",
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Page ---
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<section>
|
||||||
|
<Styles />
|
||||||
|
<Toolbar>
|
||||||
|
<Btn intent="outline" right id="new-file-btn">
|
||||||
|
+ New File
|
||||||
|
</Btn>
|
||||||
|
</Toolbar>
|
||||||
|
|
||||||
|
<div id="listing"></div>
|
||||||
|
|
||||||
|
<dialog id="new-file-dialog">
|
||||||
|
<article>
|
||||||
|
<header>
|
||||||
|
<button aria-label="Close" rel="prev" id="close-new-dialog"></button>
|
||||||
|
<strong>New File</strong>
|
||||||
|
</header>
|
||||||
|
<form id="new-file-form">
|
||||||
|
<label>
|
||||||
|
Filename
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="new-file-name"
|
||||||
|
placeholder="example.txt"
|
||||||
|
required
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Content
|
||||||
|
<textarea
|
||||||
|
id="new-file-content"
|
||||||
|
rows={6}
|
||||||
|
placeholder="(optional)"
|
||||||
|
></textarea>
|
||||||
|
</label>
|
||||||
|
<FormError id="new-file-error" />
|
||||||
|
<Btn type="submit">Create</Btn>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
|
</section>
|
||||||
|
);
|
||||||
88
src/server/fs.ts
Normal file
88
src/server/fs.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { resolve, normalize, join, dirname } from "node:path";
|
||||||
|
import { readdir, stat, readFile, writeFile, mkdir, unlink } from "node:fs/promises";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
|
||||||
|
function expandHome(p: string): string {
|
||||||
|
return p.startsWith("~") ? join(homedir(), p.slice(1)) : p;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DATA_DIR = resolve(expandHome(process.env.DATA_DIR || "./data"));
|
||||||
|
await mkdir(DATA_DIR, { recursive: true });
|
||||||
|
|
||||||
|
function safePath(path: string): string {
|
||||||
|
const normalized = normalize(path);
|
||||||
|
const full = resolve(DATA_DIR, normalized);
|
||||||
|
if (!full.startsWith(DATA_DIR + "/") && full !== DATA_DIR) {
|
||||||
|
throw new Error(`Path escapes root: ${path}`);
|
||||||
|
}
|
||||||
|
return full;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listFiles(path: string = ".") {
|
||||||
|
const dir = safePath(path);
|
||||||
|
const entries = await readdir(dir, { withFileTypes: true });
|
||||||
|
const results = await Promise.all(
|
||||||
|
entries.map(async (entry) => {
|
||||||
|
const base: { name: string; type: "file" | "directory" } = {
|
||||||
|
name: entry.name,
|
||||||
|
type: entry.isDirectory() ? "directory" : "file",
|
||||||
|
};
|
||||||
|
if (entry.isFile()) {
|
||||||
|
const info = await stat(join(dir, entry.name));
|
||||||
|
return { ...base, size: info.size, modified: info.mtime.toISOString() };
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return { entries: results };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readFileContent(path: string) {
|
||||||
|
const full = safePath(path);
|
||||||
|
const content = await readFile(full, "utf-8");
|
||||||
|
const info = await stat(full);
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
content,
|
||||||
|
size: info.size,
|
||||||
|
modified: info.mtime.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fileExists(path: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const full = safePath(path);
|
||||||
|
const info = await stat(full);
|
||||||
|
return info.isFile();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createFile(path: string, content: string) {
|
||||||
|
const full = safePath(path);
|
||||||
|
const exists = await stat(full).then(() => true, () => false);
|
||||||
|
if (exists) throw new Error(`File already exists: ${path}`);
|
||||||
|
await mkdir(dirname(full), { recursive: true });
|
||||||
|
await writeFile(full, content, "utf-8");
|
||||||
|
const info = await stat(full);
|
||||||
|
return { path, size: info.size, created: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function editFile(path: string, content: string) {
|
||||||
|
const full = safePath(path);
|
||||||
|
const exists = await stat(full).then(() => true, () => false);
|
||||||
|
if (!exists) throw new Error(`File does not exist: ${path}`);
|
||||||
|
await writeFile(full, content, "utf-8");
|
||||||
|
const info = await stat(full);
|
||||||
|
return { path, size: info.size, modified: info.mtime.toISOString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteFile(path: string) {
|
||||||
|
const full = safePath(path);
|
||||||
|
const info = await stat(full).catch(() => null);
|
||||||
|
if (!info) throw new Error(`File does not exist: ${path}`);
|
||||||
|
if (info.isDirectory()) throw new Error(`Cannot delete a directory: ${path}`);
|
||||||
|
await unlink(full);
|
||||||
|
return { path, deleted: true };
|
||||||
|
}
|
||||||
129
src/server/index.ts
Normal file
129
src/server/index.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { Hype } from "@because/hype";
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
||||||
|
import { registerTools } from "./tools.js";
|
||||||
|
import { listFiles, readFileContent, createFile, editFile, deleteFile, fileExists } from "./fs.js";
|
||||||
|
import { cors } from "hono/cors";
|
||||||
|
import { basicAuth } from "hono/basic-auth";
|
||||||
|
import FilePage from "../pages/_file.js";
|
||||||
|
import EditPage from "../pages/_edit.js";
|
||||||
|
import Layout from "../pages/_layout.js";
|
||||||
|
|
||||||
|
const app = new Hype({ pico: true, ok: true });
|
||||||
|
|
||||||
|
// Override Hype's onError to handle Hono's HTTPException (e.g. from basicAuth)
|
||||||
|
app.onError((err, c) => {
|
||||||
|
if ("getResponse" in err) {
|
||||||
|
return (err as any).getResponse();
|
||||||
|
}
|
||||||
|
const isDev = process.env.NODE_ENV !== "production";
|
||||||
|
return c.html(
|
||||||
|
`<!DOCTYPE html><html><body><h1>Error: ${err.message}</h1>${isDev ? `<pre>${err.stack}</pre>` : "<p>An error occurred</p>"}</body></html>`,
|
||||||
|
500
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use("/mcp", cors({
|
||||||
|
origin: "*",
|
||||||
|
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
||||||
|
allowHeaders: ["Content-Type", "mcp-session-id", "Last-Event-ID", "mcp-protocol-version"],
|
||||||
|
exposeHeaders: ["mcp-session-id", "mcp-protocol-version"],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// --- Basic auth for the web UI (skip /mcp) ---
|
||||||
|
|
||||||
|
if (process.env.AUTH_USER && process.env.AUTH_PASS) {
|
||||||
|
app.use("*", async (c, next) => {
|
||||||
|
if (c.req.path === "/mcp" || c.req.path === "/ok") return next();
|
||||||
|
const auth = basicAuth({
|
||||||
|
username: process.env.AUTH_USER!,
|
||||||
|
password: process.env.AUTH_PASS!,
|
||||||
|
});
|
||||||
|
return auth(c, next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- REST API for the web UI ---
|
||||||
|
|
||||||
|
app.get("/api/list", async (c) => {
|
||||||
|
try {
|
||||||
|
const path = c.req.query("path") || ".";
|
||||||
|
return c.json(await listFiles(path));
|
||||||
|
} catch (e: any) {
|
||||||
|
return c.json({ error: e.message }, 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/api/read", async (c) => {
|
||||||
|
const path = c.req.query("path");
|
||||||
|
if (!path) return c.json({ error: "path required" }, 400);
|
||||||
|
try {
|
||||||
|
return c.json(await readFileContent(path));
|
||||||
|
} catch (e: any) {
|
||||||
|
return c.json({ error: e.message }, 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/create", async (c) => {
|
||||||
|
try {
|
||||||
|
const { path, content } = await c.req.json();
|
||||||
|
if (!path) return c.json({ error: "path required" }, 400);
|
||||||
|
return c.json(await createFile(path, content ?? ""));
|
||||||
|
} catch (e: any) {
|
||||||
|
return c.json({ error: e.message }, 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/api/edit", async (c) => {
|
||||||
|
try {
|
||||||
|
const { path, content } = await c.req.json();
|
||||||
|
if (!path) return c.json({ error: "path required" }, 400);
|
||||||
|
if (content === undefined) return c.json({ error: "content required" }, 400);
|
||||||
|
return c.json(await editFile(path, content));
|
||||||
|
} catch (e: any) {
|
||||||
|
return c.json({ error: e.message }, 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/api/files/:path{.*}", async (c) => {
|
||||||
|
try {
|
||||||
|
const path = c.req.param("path");
|
||||||
|
if (!path) return c.json({ error: "path required" }, 400);
|
||||||
|
return c.json(await deleteFile(path));
|
||||||
|
} catch (e: any) {
|
||||||
|
return c.json({ error: e.message }, 400);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- MCP transport (stateless — fresh transport per request) ---
|
||||||
|
|
||||||
|
function createMcpServer(): McpServer {
|
||||||
|
const server = new McpServer({ name: "mcp.txt", version: "1.0.0" });
|
||||||
|
registerTools(server);
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.all("/mcp", async (c) => {
|
||||||
|
const transport = new WebStandardStreamableHTTPServerTransport();
|
||||||
|
const server = createMcpServer();
|
||||||
|
await server.connect(transport);
|
||||||
|
return transport.handleRequest(c.req.raw);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Clean URL routes for file view/edit ---
|
||||||
|
|
||||||
|
function renderPage(c: any, Page: any) {
|
||||||
|
const innerHTML = Page({ c, req: c.req });
|
||||||
|
const withLayout = Layout({ props: app.props, children: innerHTML });
|
||||||
|
return c.html(withLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.get("/edit/:path{.+}", (c: any) => renderPage(c, EditPage));
|
||||||
|
|
||||||
|
app.get("/:path{.+}", async (c: any, next: any) => {
|
||||||
|
const filePath = c.req.param("path");
|
||||||
|
if (!(await fileExists(filePath))) return next();
|
||||||
|
return renderPage(c, FilePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app.defaults;
|
||||||
47
src/server/tools.ts
Normal file
47
src/server/tools.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { listFiles, readFileContent, createFile, editFile } from "./fs.js";
|
||||||
|
|
||||||
|
export function registerTools(server: McpServer) {
|
||||||
|
server.registerTool("list_files", {
|
||||||
|
description: "List files and directories in a given path",
|
||||||
|
inputSchema: {
|
||||||
|
path: z.string().default(".").describe("Directory to list, relative to root"),
|
||||||
|
},
|
||||||
|
}, async ({ path }) => {
|
||||||
|
const result = await listFiles(path);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
||||||
|
});
|
||||||
|
|
||||||
|
server.registerTool("read_file", {
|
||||||
|
description: "Read the contents of a text file",
|
||||||
|
inputSchema: {
|
||||||
|
path: z.string().describe("File path relative to root"),
|
||||||
|
},
|
||||||
|
}, async ({ path }) => {
|
||||||
|
const result = await readFileContent(path);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
||||||
|
});
|
||||||
|
|
||||||
|
server.registerTool("create_file", {
|
||||||
|
description: "Create a new file. Fails if the file already exists.",
|
||||||
|
inputSchema: {
|
||||||
|
path: z.string().describe("File path relative to root"),
|
||||||
|
content: z.string().describe("File contents"),
|
||||||
|
},
|
||||||
|
}, async ({ path, content }) => {
|
||||||
|
const result = await createFile(path, content);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
||||||
|
});
|
||||||
|
|
||||||
|
server.registerTool("edit_file", {
|
||||||
|
description: "Replace the entire contents of an existing file. Fails if the file doesn't exist.",
|
||||||
|
inputSchema: {
|
||||||
|
path: z.string().describe("File path relative to root"),
|
||||||
|
content: z.string().describe("New file contents"),
|
||||||
|
},
|
||||||
|
}, async ({ path, content }) => {
|
||||||
|
const result = await editFile(path, content);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
||||||
|
});
|
||||||
|
}
|
||||||
33
tsconfig.json
Normal file
33
tsconfig.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "hono/jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"$*": ["src/server/*"],
|
||||||
|
"#*": ["src/client/*"],
|
||||||
|
"@*": ["src/shared/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user