diff --git a/.env.sample b/.env.sample index 0bdf8c9..2db2ef3 100644 --- a/.env.sample +++ b/.env.sample @@ -1,4 +1,5 @@ OPENAI_API_KEY=ADD_HERE_YOUR_OPENAI_API_KEY +OPENAI_MODEL=ENTER_HERE_THE_CHOSEN_MODEL FILES_DIR=/Users/youruser/Desktop SANDBOX_MEMORY_LIMIT= SANDBOX_CPU_LIMIT= \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cca4116..3a0b26f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,9 @@ RUN npm pkg delete scripts.prepare && npm ci --production # 5. Pull in the compiled output from builder COPY --from=builder /app/dist ./dist +# 5.5. Copy guideline files needed by the server at runtime +COPY NODE_GUIDELINES.md eslint.config.js ./ + # 6. Expose Docker socket for nested Docker commands # When running: docker run -v /var/run/docker.sock:/var/run/docker.sock ... VOLUME ["/var/run/docker.sock"] diff --git a/NODE_GUIDELINES.md b/NODE_GUIDELINES.md new file mode 100644 index 0000000..8569562 --- /dev/null +++ b/NODE_GUIDELINES.md @@ -0,0 +1,91 @@ +You are an expert Node.js developer. Your purpose is to write modern, efficient, and secure JavaScript code for the Node.js runtime. + +You must **strictly adhere** to the following guidelines in all the code you generate. Failure to follow these rules will result in incorrect and unsafe code. + +--- + +### **1. Core Principles: Modern Syntax and APIs** + +#### **1.1. Embrace ES Modules (ESM)** + +- **Default to ESM:** Write all code using ES Modules (`import`/`export` syntax). This is the modern standard. +- **No CommonJS:** **DO NOT** use CommonJS (`require()`/`module.exports`). +- **Top-Level Await:** Use top-level `await` for asynchronous initialization in your main application file. + +#### **1.2. Use Native APIs First** + +- **HTTP Requests:** Use the global `fetch` API for all HTTP requests. **DO NOT** use `node-fetch`, `axios`, or the deprecated `request` package. +- **Testing:** Use the `node:test` module and `node:assert` for writing tests. **DO NOT** use Jest, Mocha, or Chai unless specifically requested. +- **URL Parsing:** Use the global `URL` constructor (`new URL(...)`). **DO NOT** use the legacy `url.parse()` API. + +#### **1.3. Master Asynchronous Patterns** + +- **`async/await` is Mandatory:** Use `async/await` for all asynchronous operations. It is non-negotiable for clarity and error handling. +- **No Callback Hell:** **NEVER** write nested callbacks (the "pyramid of doom"). If you must interface with a callback-based legacy API, wrap it with `util.promisify`. +- **Avoid Raw Promises Where Possible:** Do not chain `.then()` and `.catch()` when `async/await` provides a cleaner, linear control flow. + +--- + +### **2. Performance and Concurrency** + +#### **2.1. Never Block the Event Loop** + +- **No Synchronous I/O:** The event loop is for non-blocking I/O. **NEVER** use synchronous functions like `fs.readFileSync()`, `crypto.randomBytesSync()`, or `child_process.execSync()` in a server or main thread context. Use their asynchronous promise-based counterparts (e.g., `fs.readFile()` from `fs/promises`). +- **Offload CPU-Intensive Work:** For heavy computations (e.g., complex calculations, image processing, synchronous bcrypt hashing), use `node:worker_threads` to avoid blocking the main thread. + +#### **2.2. Implement Streaming and Backpressure** + +- **Use Streams for Large Data:** For handling large files or network payloads, always use Node.js streams (`fs.createReadStream`, `fs.createWriteStream`). This keeps memory usage low and constant. +- **Respect Backpressure:** Use `stream.pipeline` from the `stream/promises` module to correctly chain streams and handle backpressure automatically. This prevents memory overload when a readable stream is faster than a writable one. + +--- + +### **3. Error Handling and Resilience** + +#### **3.1. Handle Errors Robustly** + +- **Comprehensive `try...catch`:** Wrap all `await` calls in `try...catch` blocks to handle potential runtime errors gracefully. +- **No Unhandled Rejections:** Every promise chain must end with a `catch` or be handled by a `try...catch` block. Unhandled promise rejections will crash the application. +- **Centralized Error Handling:** In server applications (like Express), use centralized error-handling middleware to catch and process all errors consistently. + +#### **3.2. Build Resilient Services** + +- **Set Timeouts:** When making outbound network requests (e.g., with `fetch`), always use an `AbortSignal` to enforce a timeout. Never allow a request to hang indefinitely. +- **Implement Graceful Shutdown:** Your application must handle `SIGINT` and `SIGTERM` signals. On shutdown, you must: + 1. Stop accepting new requests. + 2. Finish processing in-flight requests. + 3. Close database connections and other resources. + 4. Exit the process with `process.exit(0)`. + +--- + +### **4. Security First** + +#### **4.1. Avoid Common Vulnerabilities** + +- **Validate All Inputs:** Never trust user input. Use a schema validation library like `zod` or `joi` to validate request bodies, query parameters, and headers. +- **Prevent Injection:** Use parameterized queries or ORMs to prevent SQL injection. Never construct database queries with string concatenation. +- **Safe Child Processes:** **DO NOT** use `child_process.exec` with unescaped user input, as this can lead to command injection. Use `child_process.execFile` with an array of arguments instead. +- **Secure Dependencies:** Always use a lockfile (`package-lock.json`). Regularly audit dependencies with `npm audit`. + +#### **4.2. Secure Coding Practices** + +- **No Unsafe Execution:** **NEVER** use `eval()` or `new Function('...')` with dynamic strings. It is a massive security risk. +- **Handle Paths Safely:** Use `path.join()` or `path.resolve()` to construct file system paths. Do not use string concatenation, which is vulnerable to path traversal attacks. +- **Manage Secrets:** **NEVER** hardcode secrets (API keys, passwords) in the source code. Load them from environment variables (e.g., using `dotenv` in development). + +--- + +### **5. Code Style and Structure** + +#### **5.1. Modern JavaScript Syntax** + +- **`const` Over `let`:** Use `const` by default. Only use `let` if a variable must be reassigned. **NEVER** use `var`. +- **Strict Equality:** Always use strict equality (`===` and `!==`). **DO NOT** use loose equality (`==` and `!=`). +- **No Prototype Extension:** **NEVER** modify the prototypes of built-in objects like `Object.prototype` or `Array.prototype`. + +#### **5.2. Maintain Clean Code** + +- **Avoid Global State:** Do not store request-specific or user-specific data in global variables. This leads to memory leaks and security issues. Use a request context or dependency injection. +- **Pure Functions:** Prefer pure functions that do not have side effects. Avoid modifying function arguments directly. +- **Prevent Circular Dependencies:** Structure your files and modules to avoid circular `import` statements, which can cause runtime errors. diff --git a/evals/index.ts b/evals/index.ts index 600caf5..9806e93 100644 --- a/evals/index.ts +++ b/evals/index.ts @@ -40,7 +40,7 @@ async function run() { // Initialize OpenAI Audit Client const client = new OpenAIAuditClient({ apiKey: process.env.OPENAI_API_KEY!, - model: 'gpt-4o-mini', + model: process.env.OPENAI_MODEL || 'gpt-4o-mini', }); await client.initializeClient(); console.log('OpenAI Audit Client initialized'); diff --git a/package-lock.json b/package-lock.json index b63d517..4bd98cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,13 @@ "name": "node-code-sandbox-mcp", "version": "1.3.0", "dependencies": { + "@eslint/eslintrc": "^3.1.0", "@modelcontextprotocol/sdk": "^1.17.3", "dotenv": "^17.2.1", + "eslint": "^9.33.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-n": "^17.21.3", + "eslint-plugin-prettier": "^5.5.4", "mime-types": "^3.0.1", "npm-registry-sdk": "^1.2.1", "openai": "^5.13.1", @@ -28,9 +33,6 @@ "@typescript-eslint/eslint-plugin": "^8.40.0", "@typescript-eslint/parser": "^8.40.0", "@vitest/coverage-v8": "^3.2.4", - "eslint": "^9.33.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4", "husky": "^9.1.7", "lint-staged": "^16.1.5", "prettier": "^3.6.2", @@ -1012,7 +1014,6 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -1031,7 +1032,6 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -1041,7 +1041,6 @@ "version": "0.21.0", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", @@ -1056,7 +1055,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1067,7 +1065,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -1080,7 +1077,6 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1090,7 +1086,6 @@ "version": "0.15.2", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -1103,7 +1098,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -1127,7 +1121,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1138,7 +1131,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -1151,7 +1143,6 @@ "version": "9.33.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", - "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1164,7 +1155,6 @@ "version": "2.1.6", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1174,7 +1164,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.15.2", @@ -1188,7 +1177,6 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -1198,7 +1186,6 @@ "version": "0.16.6", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", @@ -1212,7 +1199,6 @@ "version": "0.3.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -1226,7 +1212,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -1240,7 +1225,6 @@ "version": "0.4.2", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -1417,7 +1401,6 @@ "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" @@ -1737,14 +1720,12 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/lint-staged": { @@ -2207,7 +2188,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2220,7 +2200,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -2295,7 +2274,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-ify": { @@ -2348,7 +2326,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/body-parser": { @@ -2453,7 +2430,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2508,7 +2484,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -2525,7 +2500,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2703,7 +2677,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2716,7 +2689,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/colorette": { @@ -2751,7 +2723,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -3978,7 +3949,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, "license": "MIT" }, "node_modules/depd": { @@ -4192,6 +4162,19 @@ "once": "^1.4.0" } }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -4324,7 +4307,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -4337,7 +4319,6 @@ "version": "9.33.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", - "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -4394,11 +4375,25 @@ } } }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, "node_modules/eslint-config-prettier": { "version": "10.1.8", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, "license": "MIT", "bin": { "eslint-config-prettier": "bin/cli.js" @@ -4410,11 +4405,69 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-n": { + "version": "17.21.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.3.tgz", + "integrity": "sha512-MtxYjDZhMQgsWRm/4xYLL0i2EhusWT7itDxlJ80l1NND2AL2Vi5Mvneqv/ikG9+zpran0VsVRXTEHrpLmUZRNw==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.5.0", + "enhanced-resolve": "^5.17.1", + "eslint-plugin-es-x": "^7.8.0", + "get-tsconfig": "^4.8.1", + "globals": "^15.11.0", + "globrex": "^0.1.2", + "ignore": "^5.3.2", + "semver": "^7.6.3", + "ts-declaration-location": "^1.0.6" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": ">=8.23.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", - "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", @@ -4445,7 +4498,6 @@ "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -4462,7 +4514,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -4475,7 +4526,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -4486,7 +4536,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4499,7 +4548,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -4512,7 +4560,6 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.15.0", @@ -4530,7 +4577,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4543,7 +4589,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -4556,7 +4601,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -4569,7 +4613,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -4589,7 +4632,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -4808,7 +4850,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, "license": "Apache-2.0" }, "node_modules/fast-glob": { @@ -4851,7 +4892,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, "license": "MIT" }, "node_modules/fast-uri": { @@ -4929,7 +4969,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" @@ -4972,7 +5011,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -4989,7 +5027,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -5003,7 +5040,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, "license": "ISC" }, "node_modules/foreground-child": { @@ -5321,6 +5357,18 @@ "node": ">=6" } }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/git-raw-commits": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", @@ -5598,7 +5646,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -5627,7 +5674,6 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -5636,6 +5682,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5652,7 +5704,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -5698,7 +5749,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5809,7 +5859,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -5819,7 +5868,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -5847,7 +5895,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -5925,7 +5972,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5945,7 +5991,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -6100,7 +6145,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -6117,7 +6162,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6130,7 +6174,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, "license": "MIT" }, "node_modules/json-parse-better-errors": { @@ -6157,7 +6200,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, "license": "MIT" }, "node_modules/json-stringify-safe": { @@ -6198,7 +6240,6 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -6218,7 +6259,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -6394,7 +6434,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -6445,7 +6484,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, "license": "MIT" }, "node_modules/lodash.mergewith": { @@ -6871,7 +6909,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, "license": "MIT" }, "node_modules/negotiator": { @@ -7025,7 +7062,6 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -7053,7 +7089,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -7069,7 +7104,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -7102,7 +7136,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -7143,7 +7176,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7242,7 +7274,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -7316,7 +7347,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -7326,7 +7356,6 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true, "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -7342,7 +7371,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, "license": "MIT", "dependencies": { "fast-diff": "^1.1.2" @@ -7691,12 +7719,20 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -7945,7 +7981,6 @@ "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -8709,7 +8744,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8742,7 +8776,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -8768,7 +8801,6 @@ "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", - "dev": true, "license": "MIT", "dependencies": { "@pkgr/core": "^0.2.9" @@ -8780,6 +8812,19 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/test-exclude": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", @@ -8940,11 +8985,32 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-declaration-location": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/ts-declaration-location/-/ts-declaration-location-1.0.7.tgz", + "integrity": "sha512-EDyGAwH1gO0Ausm9gV6T2nUvBgXT5kGoCMJPllOaooZ+4VvJiKBdZE7wK18N1deEowhcUptS+5GXZK8U/fvpwA==", + "funding": [ + { + "type": "ko-fi", + "url": "https://ko-fi.com/rebeccastevens" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/ts-declaration-location" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "picomatch": "^4.0.2" + }, + "peerDependencies": { + "typescript": ">=4.0.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -8991,7 +9057,6 @@ "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -9287,7 +9352,6 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9522,7 +9586,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/package.json b/package.json index 46e6027..2ecc7e0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "node-code-sandbox-mcp": "dist/server.js" }, "files": [ - "dist" + "dist", + "NODE_GUIDELINES.md" ], "scripts": { "dev": "node --env-file .env --watch src/server.ts", @@ -30,8 +31,15 @@ "push-release": "git push --follow-tags origin master" }, "dependencies": { + "@eslint/eslintrc": "^3.1.0", "@modelcontextprotocol/sdk": "^1.17.3", + "@typescript-eslint/eslint-plugin": "^8.40.0", + "@typescript-eslint/parser": "^8.40.0", "dotenv": "^17.2.1", + "eslint": "^9.33.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-n": "^17.21.3", + "eslint-plugin-prettier": "^5.5.4", "mime-types": "^3.0.1", "npm-registry-sdk": "^1.2.1", "openai": "^5.13.1", @@ -44,12 +52,7 @@ "@types/mime-types": "^3.0.1", "@types/node": "^24.3.0", "@types/tmp": "^0.2.6", - "@typescript-eslint/eslint-plugin": "^8.40.0", - "@typescript-eslint/parser": "^8.40.0", "@vitest/coverage-v8": "^3.2.4", - "eslint": "^9.33.0", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-prettier": "^5.5.4", "husky": "^9.1.7", "lint-staged": "^16.1.5", "prettier": "^3.6.2", diff --git a/src/linterUtils.ts b/src/linterUtils.ts new file mode 100644 index 0000000..240e21f --- /dev/null +++ b/src/linterUtils.ts @@ -0,0 +1,83 @@ +import { ESLint } from 'eslint'; +import globals from 'globals'; +import nPlugin from 'eslint-plugin-n'; +import prettierPlugin from 'eslint-plugin-prettier'; + +const eslint = new ESLint({ + fix: true, + overrideConfigFile: 'eslint.config.js', + + overrideConfig: { + plugins: { + n: nPlugin, + prettier: prettierPlugin, + }, + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + globals: { + ...globals.node, // Crucial for Node.js environment code + }, + }, + rules: { + // --- Best Practices & Bug Prevention for JS --- + eqeqeq: ['error', 'always'], + 'no-unused-vars': ['warn', { args: 'none', ignoreRestSiblings: true }], + 'no-console': ['warn', { allow: ['warn', 'error'] }], + 'no-return-await': 'error', + 'no-throw-literal': 'error', + + // --- Modern JavaScript & Code Style --- + 'no-var': 'error', + 'prefer-const': 'error', + 'object-shorthand': 'error', + 'prefer-template': 'error', + 'prefer-arrow-callback': 'error', + 'prefer-destructuring': ['warn', { object: true, array: false }], + + // Re-apply prettier rule to ensure it has priority + 'prettier/prettier': 'error', + + // --- Node.js Specific Rules --- + 'n/handle-callback-err': 'error', + 'n/no-deprecated-api': 'error', + 'n/no-new-require': 'error', + 'n/no-unpublished-import': 'off', // Disabled because at linting stage we have not yet run npm i + 'n/no-missing-import': 'off', + }, + }, +}); + +/** + * Lints and auto-fixes the given code string. + * @param code The source code generated by the LLM. + * @returns An object with the fixed code and a formatted string of remaining errors. + */ +export async function lintAndRefactorCode(code: string): Promise<{ + fixedCode: string; + errorReport: string | null; +}> { + const results = await eslint.lintText(code); + const result = results[0]; // We are only linting one string + + // The 'output' property contains the fixed code if fixes were applied. + // If no fixes were needed, it's undefined, so we fall back to the original code. + const fixedCode = result.output ?? code; + + // Filter for errors that could not be auto-fixed + const remainingErrors = result.messages.filter( + (msg) => msg.severity === 2 // 2 for 'error' + ); + + if (remainingErrors.length > 0) { + // Format the remaining errors for feedback + const errorReport = remainingErrors + .map( + (err) => `L${err.line}:${err.column}: ${err.message} (${err.ruleId})` + ) + .join('\n'); + return { fixedCode, errorReport }; + } + + return { fixedCode, errorReport: null }; +} diff --git a/src/runUtils.ts b/src/runUtils.ts index 8ed7e87..4f2bb83 100644 --- a/src/runUtils.ts +++ b/src/runUtils.ts @@ -93,11 +93,11 @@ export function getHostOutputDir(): string { } // This FILES_DIR is an env var coming from the user -export const getFilesDir = (): string => { - return getConfig().filesDir!; +export const getFilesDir = (): string => { + return getConfig().filesDir!; }; -export const getMountFlag = (): string => { +export const getMountFlag = (): string => { const dir = getFilesDir(); return dir ? `-v ${dir}:/workspace/files` : ''; -}; \ No newline at end of file +}; diff --git a/src/server.ts b/src/server.ts index 48e78f5..8058dfc 100644 --- a/src/server.ts +++ b/src/server.ts @@ -18,6 +18,8 @@ import runJsEphemeral, { } from './tools/runJsEphemeral.ts'; import mime from 'mime-types'; import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; import { z } from 'zod'; import { getConfig } from './config.ts'; import { startScavenger, cleanActiveContainers } from './containerUtils.ts'; @@ -28,12 +30,19 @@ import getDependencyTypes, { import searchNpmPackages, { SearchNpmPackagesToolSchema, } from './tools/searchNpmPackages.ts'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); import packageJson from '../package.json' with { type: 'json' }; export const serverRunId = randomUUID(); setServerRunId(serverRunId); +const nodeGuidelines = await fs.readFile( + path.join(__dirname, '..', 'NODE_GUIDELINES.md'), + 'utf-8' +); + // Create the server with logging capability enabled const server = new McpServer( { @@ -150,16 +159,9 @@ server.prompt('run-node-js-script', { prompt: z.string() }, ({ prompt }) => ({ type: 'text', text: `Here is my prompt:\n\n${prompt}\n\n` + - `Follow modern Node.js best practices:\n` + - // I've noticed that gpt4o-mini tends to use CommonJS - `- Use ECMAScript Modules (ESM) syntax (import/export), avoid CommonJS (require/module.exports)\n` + - // gpt4o-mini tends to try to install node-fetch - `- Use native fetch, avoid node-fetch or axios unless absolutely necessary or requested\n` + - `- Prefer top-level await in ES modules when appropriate\n` + - `- Use async/await consistently for asynchronous code, avoid mixing with .then/.catch\n` + - `- Avoid callback-style code in favor of Promises and async/await\n` + - `- Avoid unnecessary dependencies if a native API is available\n` + - `Please write and run a Node.js script.`, + nodeGuidelines + + `\n---\n\n` + + `Please write and run a Node.js script that fulfills my prompt.`, }, }, ], diff --git a/src/tools/runJs.ts b/src/tools/runJs.ts index 950d4e5..33882fe 100644 --- a/src/tools/runJs.ts +++ b/src/tools/runJs.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { execFileSync } from 'node:child_process'; -import { type McpResponse, textContent } from '../types.ts'; +import { type McpResponse, textContent, type McpContent } from '../types.ts'; import { prepareWorkspace } from '../runUtils.ts'; import { DOCKER_NOT_RUNNING_ERROR, @@ -18,6 +18,7 @@ import { getContentFromError, safeExecNodeInContainer, } from '../dockerUtils.ts'; +import { lintAndRefactorCode } from '../linterUtils.ts'; const NodeDependency = z.object({ name: z.string().describe('npm package name, e.g. lodash'), @@ -70,13 +71,19 @@ export default async function runJs({ return { content: [textContent(DOCKER_NOT_RUNNING_ERROR)] }; } + // Lint and refactor the code first. + const { fixedCode, errorReport } = await lintAndRefactorCode(code); + const telemetry: Record = {}; const dependenciesRecord: Record = Object.fromEntries( dependencies.map(({ name, version }) => [name, version]) ); // Create workspace in container - const localWorkspace = await prepareWorkspace({ code, dependenciesRecord }); + const localWorkspace = await prepareWorkspace({ + code: fixedCode, + dependenciesRecord, + }); execFileSync('docker', [ 'cp', `${localWorkspace.name}/.`, @@ -115,7 +122,17 @@ export default async function runJs({ command: `nohup node index.js > output.log 2>&1 &`, }); telemetry.runTimeMs = duration; - if (error) return getContentFromError(error, telemetry); + if (error) { + const errorResponse = getContentFromError(error, telemetry); + if (errorReport) { + errorResponse.content.unshift( + textContent( + `Linting issues found (some may have been auto-fixed):\n${errorReport}` + ) + ); + } + return errorResponse; + } await waitForPortHttp(listenOnPort); rawOutput = `Server started in background; logs at /output.log`; @@ -142,7 +159,17 @@ export default async function runJs({ if (output) rawOutput = output; telemetry.runTimeMs = duration; - if (error) return getContentFromError(error, telemetry); + if (error) { + const errorResponse = getContentFromError(error, telemetry); + if (errorReport) { + errorResponse.content.unshift( + textContent( + `Linting issues found (some may have been auto-fixed):\n${errorReport}` + ) + ); + } + return errorResponse; + } } // Detect the file changed during the execution of the tool in the mounted workspace @@ -156,8 +183,18 @@ export default async function runJs({ const extractedContents = await changesToMcpContent(changes); localWorkspace.removeCallback(); + const responseContent: McpContent[] = []; + if (errorReport) { + responseContent.push( + textContent( + `Linting issues found (some may have been auto-fixed):\n${errorReport}` + ) + ); + } + return { content: [ + ...(responseContent.length ? responseContent : []), textContent(`Node.js process output:\n${rawOutput}`), ...extractedContents, textContent(`Telemetry:\n${JSON.stringify(telemetry, null, 2)}`), diff --git a/src/tools/runJsEphemeral.ts b/src/tools/runJsEphemeral.ts index 80e4aa7..28feb09 100644 --- a/src/tools/runJsEphemeral.ts +++ b/src/tools/runJsEphemeral.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { execFileSync } from 'child_process'; import tmp from 'tmp'; import { randomUUID } from 'crypto'; -import { type McpResponse, textContent } from '../types.ts'; +import { type McpResponse, textContent, type McpContent } from '../types.ts'; import { DEFAULT_NODE_IMAGE, DOCKER_NOT_RUNNING_ERROR, @@ -22,6 +22,7 @@ import { getContentFromError, safeExecNodeInContainer, } from '../dockerUtils.ts'; +import { lintAndRefactorCode } from '../linterUtils.ts'; const NodeDependency = z.object({ name: z.string().describe('npm package name, e.g. lodash'), @@ -70,6 +71,9 @@ export default async function runJsEphemeral({ return { content: [textContent(DOCKER_NOT_RUNNING_ERROR)] }; } + // Lint and refactor the code first. + const { fixedCode, errorReport } = await lintAndRefactorCode(code); + const telemetry: Record = {}; const dependenciesRecord = preprocessDependencies({ dependencies, image }); const containerId = `js-ephemeral-${randomUUID()}`; @@ -98,7 +102,10 @@ export default async function runJsEphemeral({ ]); // Prepare workspace locally - const localWorkspace = await prepareWorkspace({ code, dependenciesRecord }); + const localWorkspace = await prepareWorkspace({ + code: fixedCode, + dependenciesRecord, + }); execFileSync('docker', [ 'cp', `${localWorkspace.name}/.`, @@ -131,7 +138,17 @@ export default async function runJsEphemeral({ containerId, }); telemetry.runTimeMs = duration; - if (error) return getContentFromError(error, telemetry); + if (error) { + const errorResponse = getContentFromError(error, telemetry); + if (errorReport) { + errorResponse.content.unshift( + textContent( + `Linting issues found (some may have been auto-fixed):\n${errorReport}` + ) + ); + } + return errorResponse; + } // Detect the file changed during the execution of the tool in the mounted workspace // and report the changes to the user @@ -143,8 +160,18 @@ export default async function runJsEphemeral({ const extractedContents = await changesToMcpContent(changes); + const responseContent: McpContent[] = []; + if (errorReport) { + responseContent.push( + textContent( + `Linting issues found (some may have been auto-fixed):\n${errorReport}` + ) + ); + } + return { content: [ + ...(responseContent.length ? responseContent : []), textContent(`Node.js process output:\n${output}`), ...extractedContents, textContent(`Telemetry:\n${JSON.stringify(telemetry, null, 2)}`), diff --git a/test/runJs.test.ts b/test/runJs.test.ts index cd91dea..4cb2b22 100644 --- a/test/runJs.test.ts +++ b/test/runJs.test.ts @@ -1,18 +1,10 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as tmp from 'tmp'; import { z } from 'zod'; import runJs, { argSchema } from '../src/tools/runJs.ts'; import initializeSandbox from '../src/tools/initialize.ts'; import stopSandbox from '../src/tools/stop.ts'; import type { McpContentText } from '../src/types.ts'; -import { vi } from 'vitest'; -// import * as childProcess from 'node:child_process'; // REMOVE THIS LINE - -// REMOVE THIS GLOBAL MOCK: -// vi.mock('node:child_process', () => ({ -// execFileSync: vi.fn(() => Buffer.from('')), -// execFile: vi.fn(() => Buffer.from('')), -// })); describe('argSchema', () => { it('should accept code and container_id and set defaults', () => { @@ -224,6 +216,68 @@ describe('runJs basic execution', () => { ); expect(telemetryText).toBeDefined(); }); + + it('should auto-fix linting issues and run the corrected code', async () => { + const result = await runJs({ + container_id: containerId, + // This code has fixable issues: `var` instead of `const`, and extra spacing. + code: `var msg = "hello auto-fixed world" ; console.log(msg)`, + }); + + // 1. Check that no linting report was returned, as it should be auto-fixed. + const lintReport = result.content.find( + (c) => c.type === 'text' && c.text.startsWith('Linting issues found') + ); + expect(lintReport).toBeUndefined(); + + // 2. Check that the execution was successful and the output is correct. + const execOutput = result.content.find( + (c) => c.type === 'text' && c.text.startsWith('Node.js process output:') + ); + expect(execOutput).toBeDefined(); + if (execOutput?.type === 'text') { + expect(execOutput.text).toContain('hello auto-fixed world'); + } + + // 3. Check that there was no execution error. + const execError = result.content.find( + (c) => c.type === 'text' && c.text.startsWith('Error during execution:') + ); + expect(execError).toBeUndefined(); + }); + + it('should report unfixable linting issues and the subsequent execution error', async () => { + const result = await runJs({ + container_id: containerId, + // This code has an unfixable issue: using an undefined variable. + code: `console.log(someUndefinedVariable);`, + }); + + expect(result).toBeDefined(); + + // 1. Check that a linting report was returned. + const lintReport = result.content.find( + (c) => c.type === 'text' && c.text.startsWith('Linting issues found') + ); + expect(lintReport).toBeDefined(); + if (lintReport?.type === 'text') { + expect(lintReport.text).toContain( + "'someUndefinedVariable' is not defined." + ); + expect(lintReport.text).toContain('(no-undef)'); + } + + // 2. Check that the execution also failed and was reported. + const execError = result.content.find( + (c) => c.type === 'text' && c.text.startsWith('Error during execution:') + ); + expect(execError).toBeDefined(); + if (execError?.type === 'text') { + expect(execError.text).toContain( + 'ReferenceError: someUndefinedVariable is not defined' + ); + } + }); }, 10_000); describe('Command injection prevention', () => { diff --git a/test/runJsEphemeral.test.ts b/test/runJsEphemeral.test.ts index 5218864..6045871 100644 --- a/test/runJsEphemeral.test.ts +++ b/test/runJsEphemeral.test.ts @@ -382,8 +382,6 @@ describe('runJsEphemeral', () => { image: PLAYWRIGHT_IMAGE, }); - console.log(result); - // Cleanup delete process.env.RUN_SCRIPT_TIMEOUT; @@ -405,6 +403,68 @@ describe('runJsEphemeral', () => { }, 100_000); }); + describe('runJsEphemeral linting', () => { + it('should auto-fix linting issues and run the corrected code', async () => { + const result = await runJsEphemeral({ + // This code has fixable issues: `var` instead of `const`, and extra spacing. + code: `var msg = "hello auto-fixed world" ; console.log(msg)`, + }); + + // 1. Check that no linting report was returned, as it should be auto-fixed. + const lintReport = result.content.find( + (c) => c.type === 'text' && c.text.startsWith('Linting issues found') + ); + expect(lintReport).toBeUndefined(); + + // 2. Check that the execution was successful and the output is correct. + const execOutput = result.content.find( + (c) => c.type === 'text' && c.text.startsWith('Node.js process output:') + ); + expect(execOutput).toBeDefined(); + if (execOutput?.type === 'text') { + expect(execOutput.text).toContain('hello auto-fixed world'); + } + + // 3. Check that there was no execution error. + const execError = result.content.find( + (c) => c.type === 'text' && c.text.startsWith('Error during execution:') + ); + expect(execError).toBeUndefined(); + }); + + it('should report unfixable linting issues and the subsequent execution error', async () => { + const result = await runJsEphemeral({ + // This code has an unfixable issue: using an undefined variable. + code: `console.log(someUndefinedVariable);`, + }); + + expect(result).toBeDefined(); + + // 1. Check that a linting report was returned. + const lintReport = result.content.find( + (c) => c.type === 'text' && c.text.startsWith('Linting issues found') + ); + expect(lintReport).toBeDefined(); + if (lintReport?.type === 'text') { + expect(lintReport.text).toContain( + "'someUndefinedVariable' is not defined." + ); + expect(lintReport.text).toContain('(no-undef)'); + } + + // 2. Check that the execution also failed and was reported. + const execError = result.content.find( + (c) => c.type === 'text' && c.text.startsWith('Error during execution:') + ); + expect(execError).toBeDefined(); + if (execError?.type === 'text') { + expect(execError.text).toContain( + 'ReferenceError: someUndefinedVariable is not defined' + ); + } + }); + }); + // Skipping this on the CI as it requires a lot of resources // and an image that is not available in the CI environment describeIfLocal( diff --git a/test/runMCPClient.test.ts b/test/runMCPClient.test.ts index a904e8b..60adda1 100644 --- a/test/runMCPClient.test.ts +++ b/test/runMCPClient.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { tmpdir } from 'os'; -import { mkdtempSync, rmSync } from 'fs'; +import { mkdtempSync, rmSync, readFileSync } from 'fs'; import path from 'path'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; @@ -11,8 +11,9 @@ import { execSync } from 'child_process'; import { normalizeMountPath } from './utils.ts'; dotenv.config(); +const __dirname = path.dirname(new URL(import.meta.url).pathname); -describe('runJsEphemeral via MCP client (files) ', () => { +describe('Node.js Code Sandbox MCP Tests', () => { let hostWorkspaceDir: string; let containerWorkspaceDir: string; let client: Client; @@ -22,8 +23,6 @@ describe('runJsEphemeral via MCP client (files) ', () => { containerWorkspaceDir = normalizeMountPath(hostWorkspaceDir); // Build the latest version of the Docker image - // I need to build the image before running the test which requires it - execSync('docker build -t alfonsograziano/node-code-sandbox-mcp .', { stdio: 'inherit', }); @@ -51,6 +50,8 @@ describe('runJsEphemeral via MCP client (files) ', () => { afterAll(() => { rmSync(hostWorkspaceDir, { recursive: true, force: true }); + // Optional: Stop any potentially lingering client connections + client?.close(); }); it('should run a console.log', async () => { @@ -72,7 +73,7 @@ describe('runJsEphemeral via MCP client (files) ', () => { expect(outputText).toContain('Node.js process output'); }); - describe('runJsEphemeral via MCP client (host workspace mounting)', () => { + describe('runJsEphemeral via MCP client (files)', () => { it('should read and write files using the host-mounted /files', async () => { const inputFileName = 'text.txt'; const inputFilePath = path.join(hostWorkspaceDir, inputFileName); @@ -149,4 +150,34 @@ describe('runJsEphemeral via MCP client (files) ', () => { expect((telemetry as { text: string }).text).toContain('"runTimeMs"'); }); }); + + describe('run-node-js-script prompt', () => { + it('should include NODE_GUIDELINES.md in the prompt response', async () => { + const expectedGuidelines = readFileSync( + path.join(__dirname, '../NODE_GUIDELINES.md'), + 'utf-8' + ); + + const userPrompt = "Please create a file named 'test.txt'."; + + // Get the prompt + const result = await client.getPrompt({ + name: 'run-node-js-script', + arguments: { prompt: userPrompt }, + }); + + expect(result).toBeDefined(); + expect(result.messages).toBeInstanceOf(Array); + expect(result.messages.length).toBeGreaterThan(0); + + const messageContent = result.messages[0].content; + expect(messageContent.type).toBe('text'); + + const responseText = messageContent.text; + + // Check that the response text includes both the user's prompt and the full guidelines + expect(responseText).toContain(userPrompt); + expect(responseText).toContain(expectedGuidelines); + }); + }); }, 200_000); diff --git a/test/unit/lintUtils.test.ts b/test/unit/lintUtils.test.ts new file mode 100644 index 0000000..b4d26d9 --- /dev/null +++ b/test/unit/lintUtils.test.ts @@ -0,0 +1,253 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { lintAndRefactorCode } from '../../src/linterUtils.ts'; +import { ESLint } from 'eslint'; + +vi.mock('eslint', () => { + const ESLint = vi.fn(); + ESLint.prototype.lintText = vi.fn(); + return { ESLint }; +}); + +// Get a typed reference to the mocked class and its method +const mockedESLint = vi.mocked(ESLint); +const mockedLintText = vi.mocked(ESLint.prototype.lintText); + +describe('lintAndRefactorCode', () => { + beforeEach(() => { + // Clear mock history before each test + mockedESLint.mockClear(); + mockedLintText.mockClear(); + }); + + it('should return original code and no errors for clean code', async () => { + const code = `const x = 1;`; + mockedLintText.mockResolvedValue([ + { + messages: [], + output: undefined, // No fixes, so output is undefined + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + filePath: '', + suppressedMessages: [], + fatalErrorCount: 0, + }, + ]); + + const { fixedCode, errorReport } = await lintAndRefactorCode(code); + + expect(fixedCode).toBe(code); + expect(errorReport).toBeNull(); + expect(mockedLintText).toHaveBeenCalledWith(code); + }); + + it('should return fixed code and no errors for auto-fixable issues', async () => { + const originalCode = `var x=1`; + const expectedFixedCode = `const x = 1;`; + mockedLintText.mockResolvedValue([ + { + messages: [], // Assuming no remaining errors after fix + output: expectedFixedCode, + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + filePath: '', + suppressedMessages: [], + fatalErrorCount: 0, + }, + ]); + + const { fixedCode, errorReport } = await lintAndRefactorCode(originalCode); + + expect(fixedCode).toBe(expectedFixedCode); + expect(errorReport).toBeNull(); + expect(mockedLintText).toHaveBeenCalledWith(originalCode); + }); + + it('should return an error report for non-fixable errors', async () => { + const codeWithErrors = `const x = y;`; // ReferenceError + const errorMessages = [ + { + severity: 2 as const, + line: 1, + column: 11, + message: "'y' is not defined.", + ruleId: 'no-undef', + }, + ]; + mockedLintText.mockResolvedValue([ + { + messages: errorMessages, + output: undefined, // No fixes applied + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + filePath: '', + suppressedMessages: [], + fatalErrorCount: 0, + }, + ]); + + const { fixedCode, errorReport } = + await lintAndRefactorCode(codeWithErrors); + + expect(fixedCode).toBe(codeWithErrors); + expect(errorReport).not.toBeNull(); + expect(errorReport).toBe("L1:11: 'y' is not defined. (no-undef)"); + expect(mockedLintText).toHaveBeenCalledWith(codeWithErrors); + }); + + it('should return fixed code and an error report for mixed issues', async () => { + const originalCode = `var x=y`; // fixable `var` and `spacing`, unfixable `y` + const partiallyFixedCode = `const x = y;`; + const errorMessages = [ + { + severity: 2 as const, + line: 1, + column: 9, + message: "'y' is not defined.", + ruleId: 'no-undef', + }, + ]; + mockedLintText.mockResolvedValue([ + { + messages: errorMessages, + output: partiallyFixedCode, + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + filePath: '', + suppressedMessages: [], + fatalErrorCount: 0, + }, + ]); + + const { fixedCode, errorReport } = await lintAndRefactorCode(originalCode); + + expect(fixedCode).toBe(partiallyFixedCode); + expect(errorReport).not.toBeNull(); + expect(errorReport).toBe("L1:9: 'y' is not defined. (no-undef)"); + expect(mockedLintText).toHaveBeenCalledWith(originalCode); + }); + + it('should auto-fix `let` to `const` for non-reassigned variables', async () => { + const originalCode = `let x = 5; console.log(x);`; + const expectedFixedCode = `const x = 5; console.log(x);`; + mockedLintText.mockResolvedValue([ + { + messages: [], + output: expectedFixedCode, + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + filePath: '', + suppressedMessages: [], + fatalErrorCount: 0, + }, + ]); + + const { fixedCode, errorReport } = await lintAndRefactorCode(originalCode); + + expect(fixedCode).toBe(expectedFixedCode); + expect(errorReport).toBeNull(); + expect(mockedLintText).toHaveBeenCalledWith(originalCode); + }); + + it('should auto-fix to use object shorthand', async () => { + const originalCode = `const name = 'test'; const obj = { name: name };`; + const expectedFixedCode = `const name = 'test'; const obj = { name };`; + mockedLintText.mockResolvedValue([ + { + messages: [], + output: expectedFixedCode, + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + filePath: '', + suppressedMessages: [], + fatalErrorCount: 0, + }, + ]); + + const { fixedCode, errorReport } = await lintAndRefactorCode(originalCode); + + expect(fixedCode).toBe(expectedFixedCode); + expect(errorReport).toBeNull(); + expect(mockedLintText).toHaveBeenCalledWith(originalCode); + }); + + it('should auto-fix to use template literals', async () => { + const originalCode = `const name = 'world'; const greeting = 'Hello ' + name;`; + const expectedFixedCode = + "const name = 'world'; const greeting = `Hello ${name}`;"; + mockedLintText.mockResolvedValue([ + { + messages: [], + output: expectedFixedCode, + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + filePath: '', + suppressedMessages: [], + fatalErrorCount: 0, + }, + ]); + + const { fixedCode, errorReport } = await lintAndRefactorCode(originalCode); + + expect(fixedCode).toBe(expectedFixedCode); + expect(errorReport).toBeNull(); + expect(mockedLintText).toHaveBeenCalledWith(originalCode); + }); + + it('should report an error for using == instead of ===', async () => { + const codeWithErrors = `if (x == 1) {}`; + const errorMessages = [ + { + severity: 2 as const, + line: 1, + column: 5, + message: "Expected '===' and instead saw '=='.", + ruleId: 'eqeqeq', + }, + ]; + mockedLintText.mockResolvedValue([ + { + messages: errorMessages, + output: codeWithErrors, // No fix applied + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [], + filePath: '', + suppressedMessages: [], + fatalErrorCount: 0, + }, + ]); + + const { fixedCode, errorReport } = + await lintAndRefactorCode(codeWithErrors); + + expect(fixedCode).toBe(codeWithErrors); + expect(errorReport).not.toBeNull(); + expect(errorReport).toBe( + "L1:5: Expected '===' and instead saw '=='. (eqeqeq)" + ); + expect(mockedLintText).toHaveBeenCalledWith(codeWithErrors); + }); +}); diff --git a/website/package-lock.json b/website/package-lock.json index 25b554b..14ec2e6 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -8,13 +8,15 @@ "name": "my-project", "version": "0.0.0", "dependencies": { + "@types/react-router-dom": "^5.3.3", "buffer": "^6.0.3", "clsx": "^2.1.1", "ethers": "^5.6.9", "lucide-react": "^0.509.0", "react": "^18.0.0", "react-dom": "^18.0.0", - "react-icons": "^4.4.0" + "react-icons": "^4.4.0", + "react-router-dom": "^7.8.1" }, "devDependencies": { "@types/react": "^18.0.0", @@ -1231,6 +1233,12 @@ "node": ">= 8.0.0" } }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", @@ -1242,14 +1250,12 @@ "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "node_modules/@types/react": { "version": "18.0.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.14.tgz", "integrity": "sha512-x4gGuASSiWmo0xjDLpm5mPb52syZHJx02VKbqUKdLmKtAwIh63XClGsiTI1K6DO5q7ox4xAsQrU+Gl3+gGXF9Q==", - "dev": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1265,11 +1271,31 @@ "@types/react": "*" } }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "node_modules/@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "node_modules/@vitejs/plugin-react": { "version": "1.3.2", @@ -1608,6 +1634,15 @@ "safe-buffer": "~5.1.1" } }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1623,8 +1658,7 @@ "node_modules/csstype": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", - "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", - "dev": true + "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, "node_modules/debug": { "version": "4.3.4", @@ -2779,6 +2813,44 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.1.tgz", + "integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.1.tgz", + "integrity": "sha512-NkgBCF3sVgCiAWIlSt89GR2PLaksMpoo3HDCorpRfnCEfdtRPLiuTf+CNXvqZMI5SJLZCLpVCvcZrTdtGW64xQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -2894,6 +2966,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3883,6 +3961,11 @@ "picomatch": "^2.2.2" } }, + "@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" + }, "@types/node": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", @@ -3894,14 +3977,12 @@ "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" }, "@types/react": { "version": "18.0.14", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.14.tgz", "integrity": "sha512-x4gGuASSiWmo0xjDLpm5mPb52syZHJx02VKbqUKdLmKtAwIh63XClGsiTI1K6DO5q7ox4xAsQrU+Gl3+gGXF9Q==", - "dev": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3917,11 +3998,29 @@ "@types/react": "*" } }, + "@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "requires": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "requires": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, "@types/scheduler": { "version": "0.16.2", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", - "dev": true + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, "@vitejs/plugin-react": { "version": "1.3.2", @@ -4141,6 +4240,11 @@ "safe-buffer": "~5.1.1" } }, + "cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==" + }, "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4150,8 +4254,7 @@ "csstype": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", - "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", - "dev": true + "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, "debug": { "version": "4.3.4", @@ -4849,6 +4952,23 @@ "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", "dev": true }, + "react-router": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.1.tgz", + "integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==", + "requires": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + } + }, + "react-router-dom": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.1.tgz", + "integrity": "sha512-NkgBCF3sVgCiAWIlSt89GR2PLaksMpoo3HDCorpRfnCEfdtRPLiuTf+CNXvqZMI5SJLZCLpVCvcZrTdtGW64xQ==", + "requires": { + "react-router": "7.8.1" + } + }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -4927,6 +5047,11 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, "source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/website/package.json b/website/package.json index d34bf5e..d6feb87 100644 --- a/website/package.json +++ b/website/package.json @@ -8,13 +8,15 @@ "preview": "vite preview" }, "dependencies": { + "@types/react-router-dom": "^5.3.3", "buffer": "^6.0.3", "clsx": "^2.1.1", "ethers": "^5.6.9", "lucide-react": "^0.509.0", "react": "^18.0.0", "react-dom": "^18.0.0", - "react-icons": "^4.4.0" + "react-icons": "^4.4.0", + "react-router-dom": "^7.8.1" }, "devDependencies": { "@types/react": "^18.0.0", diff --git a/website/public/images/js_ai.jpeg b/website/public/images/js_ai.jpeg new file mode 100644 index 0000000..82a2f89 Binary files /dev/null and b/website/public/images/js_ai.jpeg differ diff --git a/website/public/images/simple_agent.jpeg b/website/public/images/simple_agent.jpeg new file mode 100644 index 0000000..04aa3af Binary files /dev/null and b/website/public/images/simple_agent.jpeg differ diff --git a/website/src/App.tsx b/website/src/App.tsx index ae75fee..ae534f6 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -1,340 +1,16 @@ -import React, { useEffect, useState } from 'react'; -import { - X, - Terminal, - ShieldCheck, - Cpu, - Package, - Code, - Settings, - Server, - Star, -} from 'lucide-react'; - -import { UseCase, useCases } from './useCases'; -import GettingStarted from './Components/GettingStarted'; - -interface Feature { - title: string; - description: string; - icon: React.ElementType; -} - -// Extract unique categories -const allCategories: string[] = [ - ...new Set(useCases.flatMap((u) => u.category)), -].sort(); - -const features: Feature[] = [ - { - title: 'Ephemeral Containers', - description: - 'Run JavaScript in isolated Docker containers that clean up automatically after execution.', - icon: Terminal, - }, - { - title: 'Secure Execution', - description: - 'Sandboxed execution with CPU/memory limits and safe, controlled environments.', - icon: ShieldCheck, - }, - { - title: 'Detached Mode', - description: - 'Keep containers alive after execution to host servers or persistent processes.', - icon: Cpu, - }, - { - title: 'Optimized Docker Images', - description: - 'Use prebuilt Docker images with popular dependencies already installed—perfect for advanced use cases like Playwright, Chart.js, and more.', - icon: Package, - }, - { - title: 'NPM Dependencies', - description: - 'Install dependencies on-the-fly per job using package name and version.', - icon: Settings, - }, - { - title: 'Multi-Tool Support', - description: - 'Use ephemeral runs or long-lived sandbox sessions, depending on your use case.', - icon: Server, - }, -]; +import React from 'react'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import Home from './pages/Home'; +import NodeMCPServer from './pages/NodeMCPServer'; const App: React.FC = () => { - const [selectedCategories, setSelectedCategories] = useState([]); - const [modalOpen, setModalOpen] = useState(false); - const [selectedCase, setSelectedCase] = useState(null); - - const toggleCategory = (cat: string) => { - setSelectedCategories((prev) => - prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat] - ); - }; - - const openModal = (useCase: UseCase) => { - setSelectedCase(useCase); - setModalOpen(true); - }; - - const closeModal = () => { - setModalOpen(false); - setSelectedCase(null); - }; - - useEffect(() => { - if (modalOpen) { - document.body.classList.add('overflow-hidden'); - } else { - document.body.classList.remove('overflow-hidden'); - } - - return () => { - document.body.classList.remove('overflow-hidden'); - }; - }, [modalOpen]); - - const filteredCases = - selectedCategories.length === 0 - ? useCases - : useCases.filter((u) => - u.category.some((c) => selectedCategories.includes(c)) - ); - - const gridBg: React.CSSProperties = { - backgroundImage: - 'linear-gradient(to right, rgba(0,0,0,0.03) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,0,0,0.03) 1px, transparent 1px)', - backgroundSize: '20px 20px', - }; - return ( -
- {/* Hero Section */} -
-

- 🐢🚀 Node.js Sandbox MCP Server -

- {/* Compact MCP Info Banner */} -
-
- 🧠 MCP is a protocol that lets AI models access tools and data - through a standardized interface. -
- - Learn more → - -
- -

- Run JavaScript in secure, disposable Docker containers via the Model - Context Protocol (MCP). Automatic dependency installation included. -

- - -
- - {/* Features */} -
-

Core Features

-
- {features.map(({ title, description, icon: Icon }, i) => ( -
- -

{title}

-

{description}

-
- ))} -
-
- - {/* Getting Started Section */} - - - {/* Contribute Section */} -
-
-

- 🤝 Want to contribute to Node.js + AI? -

-

- Brilliant! If you want to help, I’m opening a few issues on my new - MCP server project. Since the project is still in its early stages, - it’s the perfect time to jump in and become a core - collaborator. 🚀 -

-

- If you're excited about bringing Node.js into the - world of AI applications, leave a comment or DM me, - I'd love to have a chat! -

- - View Open Issues → - -
-
- - {/* Use Cases Grid */} -
-

Use Cases

-

- Discover powerful, real-world examples you can run in seconds. From - file generation to web scraping and AI workflows—this sandbox unlocks - serious JavaScript potential in isolated containers. -

- -
-

Filter by Category

-
- {allCategories.map((cat) => ( - - ))} -
-
- -
- {filteredCases.map((u, index) => ( -
openModal(u)} - className="bg-white bg-opacity-80 p-6 rounded-xl shadow hover:shadow-lg cursor-pointer transition" - > -

- {u.title} -

-

{u.description}

-
- {u.category.map((cat) => ( - - {cat} - - ))} -
-
- ))} -
-
- - {/* Modal */} - {modalOpen && selectedCase && ( -
-
e.stopPropagation()} - > -
-

{selectedCase.title}

- -
-
-
- {selectedCase.category.map((cat) => ( - - {cat} - - ))} -
-
-

- Prompt for AI -

-
-                  {selectedCase.prompt}
-                
-
-
-

- Expected Result -

-
- {selectedCase.result} -
-
-
-
-
- )} - - {/* Footer */} - -
+ + + } /> + } /> + + ); }; diff --git a/website/src/Components/Footer.tsx b/website/src/Components/Footer.tsx new file mode 100644 index 0000000..5a657ec --- /dev/null +++ b/website/src/Components/Footer.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +const Footer: React.FC = () => { + return ( +
+
+
+
+

jsdevai.com

+

+ The ultimate destination for JavaScript developers building + AI-powered applications. +

+
+
+

Tools

+
    +
  • + + Node.js Sandbox MCP + +
  • +
+
+ +
+

Community

+ +
+
+
+

+ © 2025 jsdevai.com • Empowering JavaScript developers in the AI + revolution +

+
+
+
+ ); +}; + +export default Footer; diff --git a/website/src/Components/Header.tsx b/website/src/Components/Header.tsx new file mode 100644 index 0000000..b642c30 --- /dev/null +++ b/website/src/Components/Header.tsx @@ -0,0 +1,149 @@ +import React, { useState } from 'react'; +import { Link, useLocation } from 'react-router-dom'; +import { Menu, X, Terminal, Brain } from 'lucide-react'; + +const Header: React.FC = () => { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const location = useLocation(); + + const toggleMenu = () => { + setIsMenuOpen(!isMenuOpen); + }; + + const closeMenu = () => { + setIsMenuOpen(false); + }; + + const isActive = (path: string) => { + return location.pathname === path; + }; + + return ( +
+
+
+ {/* Logo */} + +
+ +
+ JSDevAI + + + {/* Desktop Navigation */} + + + {/* CTA Button */} +
+ + + Try Sandbox + +
+ + {/* Mobile Menu Button */} + +
+ + {/* Mobile Navigation */} + {isMenuOpen && ( +
+ +
+ )} +
+
+ ); +}; + +export default Header; diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx new file mode 100644 index 0000000..a269132 --- /dev/null +++ b/website/src/pages/Home.tsx @@ -0,0 +1,823 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import { + Brain, + Code, + Zap, + Shield, + Rocket, + Terminal, + ArrowRight, + CheckCircle, + Play, + Github, +} from 'lucide-react'; +import Footer from '../Components/Footer'; +import Header from '../Components/Header'; + +const Home: React.FC = () => { + const gridBg: React.CSSProperties = { + backgroundImage: + 'linear-gradient(to right, rgba(0,0,0,0.03) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,0,0,0.03) 1px, transparent 1px)', + backgroundSize: '20px 20px', + }; + + const features = [ + { + icon: Brain, + title: 'AI-First Development', + description: + 'Build intelligent applications with JavaScript-first AI tools and frameworks', + }, + { + icon: Zap, + title: 'Rapid Prototyping', + description: + 'Quickly iterate and test AI features with our sandboxed development environment', + }, + { + icon: Shield, + title: 'Enterprise Security', + description: + 'Production-ready security practices for AI applications in regulated industries', + }, + { + icon: Rocket, + title: 'Scalable Architecture', + description: + 'Design patterns and best practices for AI systems that grow with your business', + }, + ]; + + const tools = [ + { + title: 'Node.js Sandbox MCP', + description: + 'Run JavaScript in secure Docker containers with automatic dependency management', + category: 'Development Tools', + link: '/mcp', + icon: Terminal, + }, + ]; + + return ( +
+ {/* Header */} +
+ + {/* Hero Section */} +
+
+

+ Build the Future of + AI-Powered Apps + + with JavaScript + +

+ +

+ Your comprehensive toolkit for building intelligent applications. + From AI integration to production deployment, we've got everything + you need to succeed in the AI revolution. +

+ +
+ + + Try Our Sandbox + + + + Explore Tools + +
+ + {/* Hero Image */} +
+
+ JavaScript AI Development - Showcase of AI-powered development workflow with modern tools and frameworks +
+
+
+
+ + {/* Why JavaScript for AI Section - Expanded */} +
+
+ {/* Section Header */} +
+

+ Why JavaScript for AI Development? +

+

+ While Python dominates model training, JavaScript excels at + building the applications that users actually interact with. + Here's why JS is the perfect choice for AI-powered apps. +

+
+ + {/* Main Value Proposition */} +
+
+
+

+ Build Where Users Are +

+

+ JavaScript runs everywhere your users are - browsers, mobile + apps, servers, and edge computing. While Python trains the + models, JavaScript delivers the experience. +

+
+
+
+ +
+ + Frontend AI interfaces that users love + +
+
+
+ +
+ + Real-time AI responses with WebSockets + +
+
+
+ +
+ + Edge AI deployment for low latency + +
+
+
+ + {/* Code Example Image */} +
+ Simple AI Agent Code Example - JavaScript AI chat interface implementation in React +
+
+
+ + {/* AI Use Cases Grid */} +
+

+ What You Can Build with JavaScript AI +

+
+ {/* RAG Applications */} +
+
+ 🔍 +
+

RAG Applications

+

+ Build intelligent search and question-answering systems that + combine your data with AI models. +

+
+ Tools: LangChain.js, Vector DBs, OpenAI API +
+
+ + {/* AI Agents */} +
+
+ 🤖 +
+

AI Agents

+

+ Create autonomous agents that can browse the web, use tools, + and complete complex multi-step tasks. +

+
+ Tools: Playwright, Puppeteer, Function + Calling +
+
+ + {/* Real-time AI Chat */} +
+
+ 💬 +
+

+ Real-time AI Chat +

+

+ Build responsive chat interfaces with streaming responses, + typing indicators, and rich media support. +

+
+ Tools: WebSockets, Server-Sent Events, React +
+
+ + {/* AI-Powered APIs */} +
+
+ ⚡ +
+

AI-Powered APIs

+

+ Create intelligent backends that process natural language, + generate content, and make smart decisions. +

+
+ Tools: Express.js, Fastify, Serverless +
+
+ + {/* Browser AI */} +
+
+ 🌐 +
+

Browser AI

+

+ Run AI models directly in the browser with WebAssembly, + enabling privacy-first AI applications. +

+
+ Tools: ONNX.js, TensorFlow.js, WebAssembly +
+
+ + {/* AI Automation */} +
+
+ 🔄 +
+

AI Automation

+

+ Automate workflows with AI-driven decision making, from + content generation to data processing. +

+
+ Tools: Node.js, Cron Jobs, Webhooks +
+
+
+
+ + {/* JavaScript AI Advantages */} +
+
+

+ 🚀 JavaScript AI Superpowers +

+
+
+

+ Async by Nature +

+

+ JavaScript's async/await makes it perfect for AI API calls, + streaming responses, and handling multiple concurrent AI + operations without blocking the UI. +

+
+
+

+ JSON Native +

+

+ AI APIs speak JSON, and JavaScript speaks JSON natively. No + parsing overhead, no type conversion - just seamless data + flow. +

+
+
+

+ Real-time Ready +

+

+ WebSockets, Server-Sent Events, and WebRTC are built into + the ecosystem. Perfect for streaming AI responses and + collaborative AI features. +

+
+
+
+ +
+

+ 🌍 Ecosystem Advantages +

+
+
+

+ 2M+ NPM Packages +

+

+ The largest package ecosystem in the world includes + countless AI libraries, integrations, and tools ready to use + in your projects. +

+
+
+

+ Universal Deployment +

+

+ Deploy anywhere: Vercel, Netlify, AWS Lambda, Cloudflare + Workers, or traditional servers. One codebase, infinite + possibilities. +

+
+
+

+ Developer Velocity +

+

+ Hot reload, excellent debugging, TypeScript support, and the + best developer tools mean you can iterate on AI features + incredibly fast. +

+
+
+
+
+ + {/* AI Integration Examples */} +
+

+ Popular AI Integrations in JavaScript +

+
+
+
+ 🤖 +
+

OpenAI

+

GPT-4, DALL-E, Whisper

+ + openai-node + +
+
+
+ 🧠 +
+

Anthropic

+

+ Claude, Constitutional AI +

+ + anthropic-sdk + +
+
+
+ 🔗 +
+

LangChain

+

Agents, Chains, Tools

+ + langchainjs + +
+
+
+ 📊 +
+

Vector DBs

+

+ Pinecone, Weaviate, Chroma +

+ + pinecone-js + +
+
+
+ + {/* JavaScript AI Frameworks & Tools */} +
+

+ JavaScript AI Frameworks & Tools +

+
+ {/* Vercel AI SDK */} +
+
+
+ ▲ +
+
+

Vercel AI SDK

+

+ Streaming AI responses +

+
+
+

+ Build AI-powered streaming text and chat UIs with React, + Svelte, Vue, and more. +

+
+ + React/Next.js + + + GitHub → + +
+
+ + {/* TensorFlow.js */} +
+
+
+ TF +
+
+

TensorFlow.js

+

+ Browser ML framework +

+
+
+

+ Run machine learning models directly in the browser with + JavaScript. +

+
+ + ML/AI + + + GitHub → + +
+
+ + {/* Hugging Face */} +
+
+
+ 🤗 +
+
+

Hugging Face

+

Transformers.js

+
+
+

+ Run transformer models in the browser with zero dependencies. +

+
+ + NLP + + + GitHub → + +
+
+ + {/* AutoGen */} +
+
+
+ 🤖 +
+
+

Mastra

+

+ TypeScript Agent Framework +

+
+
+

+ The TypeScript Agent Framework from the Gatsby team. Build AI + agents with modern JavaScript. +

+
+ + Agents + + + Website → + +
+
+ + {/* ChromaDB */} +
+
+
+ 📊 +
+
+

ChromaDB

+

Vector database

+
+
+

+ Embedding database for AI applications with JavaScript client. +

+
+ + Vector DB + + + Docs → + +
+
+ + {/* BMAD-METHOD */} +
+
+
+ 🚀 +
+
+

BMAD-METHOD™

+

+ Universal AI Agent Framework +

+
+
+

+ Universal AI Agent Framework with Agentic Planning and + Context-Engineered Development for any domain. +

+
+ + AI Agents + + + GitHub → + +
+
+
+
+ + {/* Comparison Section */} +
+

+ Python vs JavaScript in AI Development +

+
+
+

+ 🐍 Python's Domain +

+
    +
  • • Model training & fine-tuning
  • +
  • • Data science & research
  • +
  • • Deep learning frameworks
  • +
  • • Scientific computing
  • +
  • • ML experimentation
  • +
+
+
+

+ ⚡ JavaScript's Domain +

+
    +
  • • User-facing AI applications
  • +
  • • Real-time AI interactions
  • +
  • • AI-powered web & mobile apps
  • +
  • • Agent orchestration
  • +
  • • Production AI deployment
  • +
+
+
+
+

+ The Perfect Partnership:{' '} + Train in Python, Deploy in JavaScript +

+
+
+ + {/* Call to Action */} +
+

+ Ready to Build AI Apps with JavaScript? +

+

+ Join thousands of developers who are already building the future + of AI applications with the language they know and love. +

+ + + Start Building Now + +
+
+
+ + {/* Features Section */} +
+
+
+

+ Everything You Need to Build AI Apps +

+

+ From development tools to production best practices, we've curated + the essential resources for modern AI development with JavaScript. +

+
+ +
+ {features.map((feature, index) => ( +
+ +

{feature.title}

+

{feature.description}

+
+ ))} +
+
+
+ + {/* Tools Section */} +
+
+
+

+ Essential AI Development Tools +

+

+ Discover the tools that will accelerate your AI development + journey +

+
+ +
+ {tools.map((tool, index) => ( +
+
+
+ +
+
+
+ + {tool.category} + +
+

{tool.title}

+

{tool.description}

+ {tool.link ? ( + + Learn More + + ) : ( + + Coming Soon + + )} +
+
+
+ ))} +
+
+
+ + {/* Community Section */} +
+
+

+ Join the AI JavaScript Community +

+

+ Connect with developers, share knowledge, and stay updated with the + latest AI development trends and tools. +

+ + +
+
+ + {/* CTA Section */} +
+
+

+ Ready to Build the Future? +

+

+ Start your AI development journey with our comprehensive toolkit and + community support. +

+
+ + + Try Our Sandbox + +
+
+
+ + {/* Footer */} +
+
+ ); +}; + +export default Home; diff --git a/website/src/pages/NodeMCPServer.tsx b/website/src/pages/NodeMCPServer.tsx new file mode 100644 index 0000000..5433cf4 --- /dev/null +++ b/website/src/pages/NodeMCPServer.tsx @@ -0,0 +1,338 @@ +import React, { useEffect, useState } from 'react'; +import { + X, + Terminal, + ShieldCheck, + Cpu, + Package, + Code, + Settings, + Server, + Star, +} from 'lucide-react'; + +import { UseCase, useCases } from '../useCases'; +import GettingStarted from '../Components/GettingStarted'; +import Footer from '../Components/Footer'; +import Header from '../Components/Header'; + +interface Feature { + title: string; + description: string; + icon: React.ElementType; +} + +// Extract unique categories +const allCategories: string[] = [ + ...new Set(useCases.flatMap((u) => u.category)), +].sort(); + +const features: Feature[] = [ + { + title: 'Ephemeral Containers', + description: + 'Run JavaScript in isolated Docker containers that clean up automatically after execution.', + icon: Terminal, + }, + { + title: 'Secure Execution', + description: + 'Sandboxed execution with CPU/memory limits and safe, controlled environments.', + icon: ShieldCheck, + }, + { + title: 'Detached Mode', + description: + 'Keep containers alive after execution to host servers or persistent processes.', + icon: Cpu, + }, + { + title: 'Optimized Docker Images', + description: + 'Use prebuilt Docker images with popular dependencies already installed—perfect for advanced use cases like Playwright, Chart.js, and more.', + icon: Package, + }, + { + title: 'NPM Dependencies', + description: + 'Install dependencies on-the-fly per job using package name and version.', + icon: Settings, + }, + { + title: 'Multi-Tool Support', + description: + 'Use ephemeral runs or long-lived sandbox sessions, depending on your use case.', + icon: Server, + }, +]; + +const App: React.FC = () => { + const [selectedCategories, setSelectedCategories] = useState([]); + const [modalOpen, setModalOpen] = useState(false); + const [selectedCase, setSelectedCase] = useState(null); + + const toggleCategory = (cat: string) => { + setSelectedCategories((prev) => + prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat] + ); + }; + + const openModal = (useCase: UseCase) => { + setSelectedCase(useCase); + setModalOpen(true); + }; + + const closeModal = () => { + setModalOpen(false); + setSelectedCase(null); + }; + + useEffect(() => { + if (modalOpen) { + document.body.classList.add('overflow-hidden'); + } else { + document.body.classList.remove('overflow-hidden'); + } + + return () => { + document.body.classList.remove('overflow-hidden'); + }; + }, [modalOpen]); + + const filteredCases = + selectedCategories.length === 0 + ? useCases + : useCases.filter((u) => + u.category.some((c) => selectedCategories.includes(c)) + ); + + const gridBg: React.CSSProperties = { + backgroundImage: + 'linear-gradient(to right, rgba(0,0,0,0.03) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,0,0,0.03) 1px, transparent 1px)', + backgroundSize: '20px 20px', + }; + + return ( +
+ {/* Header */} +
+ + {/* Hero Section */} +
+

+ 🐢🚀 Node.js Sandbox MCP Server +

+ {/* Compact MCP Info Banner */} +
+
+ 🧠 MCP is a protocol that lets AI models access tools and data + through a standardized interface. +
+ + Learn more → + +
+ +

+ Run JavaScript in secure, disposable Docker containers via the Model + Context Protocol (MCP). Automatic dependency installation included. +

+ + +
+ + {/* Features */} +
+

Core Features

+
+ {features.map(({ title, description, icon: Icon }, i) => ( +
+ +

{title}

+

{description}

+
+ ))} +
+
+ + {/* Getting Started Section */} + + + {/* Contribute Section */} +
+
+

+ 🤝 Want to contribute to Node.js + AI? +

+

+ Brilliant! If you want to help, I’m opening a few issues on my new + MCP server project. Since the project is still in its early stages, + it’s the perfect time to jump in and become a core + collaborator. 🚀 +

+

+ If you're excited about bringing Node.js into the + world of AI applications, leave a comment or DM me, + I'd love to have a chat! +

+ + View Open Issues → + +
+
+ + {/* Use Cases Grid */} +
+

Use Cases

+

+ Discover powerful, real-world examples you can run in seconds. From + file generation to web scraping and AI workflows—this sandbox unlocks + serious JavaScript potential in isolated containers. +

+ +
+

Filter by Category

+
+ {allCategories.map((cat) => ( + + ))} +
+
+ +
+ {filteredCases.map((u, index) => ( +
openModal(u)} + className="bg-white bg-opacity-80 p-6 rounded-xl shadow hover:shadow-lg cursor-pointer transition" + > +

+ {u.title} +

+

{u.description}

+
+ {u.category.map((cat) => ( + + {cat} + + ))} +
+
+ ))} +
+
+ + {/* Modal */} + {modalOpen && selectedCase && ( +
+
e.stopPropagation()} + > +
+

{selectedCase.title}

+ +
+
+
+ {selectedCase.category.map((cat) => ( + + {cat} + + ))} +
+
+

+ Prompt for AI +

+
+                  {selectedCase.prompt}
+                
+
+
+

+ Expected Result +

+
+ {selectedCase.result} +
+
+
+
+
+ )} + + {/* Footer */} +
+
+ ); +}; + +export default App;