diff --git a/README.md b/README.md index fcdc4f3..ec19fd6 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A conversational AI CLI tool powered by Grok with intelligent text editor capabi - **πŸ“ Smart File Operations**: AI automatically uses tools to view, create, and edit files - **⚑ Bash Integration**: Execute shell commands through natural conversation - **πŸ”§ Automatic Tool Selection**: AI intelligently chooses the right tools for your requests +- **πŸ”Œ MCP Tools**: Extend capabilities with Model Context Protocol servers (Linear, GitHub, etc.) - **πŸ’¬ Interactive UI**: Beautiful terminal interface built with Ink - **🌍 Global Installation**: Install and use anywhere with `npm i -g @vibe-kit/grok-cli` @@ -166,19 +167,63 @@ Follow the existing code style and patterns in this project. Grok will automatically load and follow these instructions when working in your project directory. The custom instructions are added to Grok's system prompt and take priority over default behavior. -## Example Conversations +## MCP Tools -Instead of typing commands, just tell Grok what you want to do: +Grok CLI supports MCP (Model Context Protocol) servers, allowing you to extend the AI assistant with additional tools and capabilities. +### Adding MCP Tools + +#### Add a custom MCP server: +```bash +# Add an stdio-based MCP server +grok mcp add my-server --transport stdio --command "node" --args server.js + +# Add an HTTP-based MCP server +grok mcp add my-server --transport http --url "http://localhost:3000" + +# Add with environment variables +grok mcp add my-server --transport stdio --command "python" --args "-m" "my_mcp_server" --env "API_KEY=your_key" +``` + +#### Add from JSON configuration: +```bash +grok mcp add-json my-server '{"command": "node", "args": ["server.js"], "env": {"API_KEY": "your_key"}}' +``` + +### Linear Integration Example + +To add Linear MCP tools for project management: + +```bash +# Add Linear MCP server +grok mcp add linear --transport sse --url "https://mcp.linear.app/sse" ``` -πŸ’¬ "Show me the contents of package.json" -πŸ’¬ "Create a new file called hello.js with a simple console.log" -πŸ’¬ "Find all TypeScript files in the src directory" -πŸ’¬ "Replace 'oldFunction' with 'newFunction' in all JS files" -πŸ’¬ "Run the tests and show me the results" -πŸ’¬ "What's the current directory structure?" + +This enables Linear tools like: +- Create and manage Linear issues +- Search and filter issues +- Update issue status and assignees +- Access team and project information + +### Managing MCP Servers + +```bash +# List all configured servers +grok mcp list + +# Test server connection +grok mcp test server-name + +# Remove a server +grok mcp remove server-name ``` +### Available Transport Types + +- **stdio**: Run MCP server as a subprocess (most common) +- **http**: Connect to HTTP-based MCP server +- **sse**: Connect via Server-Sent Events + ## Development ```bash diff --git a/package-lock.json b/package-lock.json index 91a9ab4..ef50d02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.12", "license": "MIT", "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.0", "axios": "^1.6.0", "cfonts": "^3.3.0", "chalk": "^4.1.2", @@ -744,6 +745,29 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.0.tgz", + "integrity": "sha512-qFfbWFA7r1Sd8D697L7GkTd36yqDuTkvz0KfOGkgXR8EUhQn3/EDNIR/qUdQNMT8IjmasBvHWuXeisxtXTQT2g==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1199,6 +1223,40 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1226,7 +1284,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -1357,6 +1414,26 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -1380,6 +1457,15 @@ "node": ">=8" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -1393,6 +1479,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1588,6 +1690,27 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-to-spaces": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-1.0.2.tgz", @@ -1597,11 +1720,41 @@ "node": ">= 4" } }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -1623,7 +1776,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1665,6 +1817,15 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -1691,6 +1852,12 @@ "node": ">= 0.4" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1703,6 +1870,15 @@ "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -1803,6 +1979,12 @@ "@esbuild/win32-x64": "0.25.8" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2044,11 +2226,118 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", + "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-glob": { @@ -2085,7 +2374,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { @@ -2131,6 +2419,23 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2205,6 +2510,24 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-extra": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", @@ -2392,6 +2715,43 @@ "node": ">= 0.4" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", @@ -2438,6 +2798,12 @@ "node": ">=8" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/ink": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/ink/-/ink-3.2.0.tgz", @@ -2517,6 +2883,15 @@ } } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-accessor-descriptor": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", @@ -2614,11 +2989,16 @@ "node": ">=0.12.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/js-tokens": { @@ -2651,7 +3031,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -2815,6 +3194,27 @@ "node": ">= 0.4" } }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2889,7 +3289,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -2899,6 +3298,15 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-emoji": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", @@ -2923,6 +3331,39 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -3022,6 +3463,15 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/patch-console": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-1.0.0.tgz", @@ -3045,12 +3495,20 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -3064,6 +3522,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3074,6 +3541,19 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3084,12 +3564,26 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -3111,6 +3605,30 @@ ], "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", @@ -3231,6 +3749,22 @@ "integrity": "sha512-r/DBVFwrgHLpPvUZquwvRLE+UFLwB6NaeQVAetS3zOlUdyH3i20QBMzBMr7VU9702j0OxuJJ88WxRnAnjrD5DQ==", "license": "GPL-3.0-or-later" }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -3255,6 +3789,32 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", @@ -3278,11 +3838,74 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -3295,7 +3918,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3313,6 +3935,78 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -3366,6 +4060,15 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3467,6 +4170,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -3525,6 +4237,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -3564,21 +4311,37 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -3654,6 +4417,12 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -3701,6 +4470,24 @@ "engines": { "node": ">=8" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } } } diff --git a/package.json b/package.json index fa2ca39..a0e2804 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "author": "Your Name", "license": "MIT", "dependencies": { + "@modelcontextprotocol/sdk": "^1.17.0", "axios": "^1.6.0", "cfonts": "^3.3.0", "chalk": "^4.1.2", diff --git a/src/agent/grok-agent.ts b/src/agent/grok-agent.ts index c568890..bcde956 100644 --- a/src/agent/grok-agent.ts +++ b/src/agent/grok-agent.ts @@ -1,5 +1,6 @@ import { GrokClient, GrokMessage, GrokToolCall } from "../grok/client"; -import { GROK_TOOLS } from "../grok/tools"; +import { GROK_TOOLS, addMCPToolsToGrokTools, getAllGrokTools, getMCPManager, initializeMCPServers } from "../grok/tools"; +import { loadMCPConfig } from "../mcp/config"; import { TextEditorTool, BashTool, @@ -43,6 +44,7 @@ export class GrokAgent extends EventEmitter { private messages: GrokMessage[] = []; private tokenCounter: TokenCounter; private abortController: AbortController | null = null; + private mcpInitialized: boolean = false; constructor(apiKey: string, baseURL?: string, model?: string) { super(); @@ -57,6 +59,9 @@ export class GrokAgent extends EventEmitter { this.search = new SearchTool(); this.tokenCounter = createTokenCounter(modelToUse); + // Initialize MCP servers if configured + this.initializeMCP(); + // Load custom instructions const customInstructions = loadCustomInstructions(); const customInstructionsSection = customInstructions @@ -126,7 +131,31 @@ Current working directory: ${process.cwd()}`, }); } + private async initializeMCP(): Promise { + try { + const config = loadMCPConfig(); + if (config.servers.length > 0) { + console.log(`Found ${config.servers.length} MCP server(s) - connecting now...`); + await initializeMCPServers(); + console.log(`Successfully connected to MCP servers`); + } + this.mcpInitialized = true; + } catch (error) { + console.warn('Failed to initialize MCP servers:', error); + this.mcpInitialized = true; // Don't block if MCP fails + } + } + + private async waitForMCPInitialization(): Promise { + while (!this.mcpInitialized) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + async processUserMessage(message: string): Promise { + // Wait for MCP initialization before processing + await this.waitForMCPInitialization(); + // Add user message to conversation const userEntry: ChatEntry = { type: "user", @@ -141,9 +170,10 @@ Current working directory: ${process.cwd()}`, let toolRounds = 0; try { + const tools = await getAllGrokTools(); let currentResponse = await this.grokClient.chat( this.messages, - GROK_TOOLS, + tools, undefined, { search_parameters: { mode: "auto" } } ); @@ -237,7 +267,7 @@ Current working directory: ${process.cwd()}`, // Get next response - this might contain more tool calls currentResponse = await this.grokClient.chat( this.messages, - GROK_TOOLS, + tools, undefined, { search_parameters: { mode: "auto" } } ); @@ -355,9 +385,10 @@ Current working directory: ${process.cwd()}`, } // Stream response and accumulate + const tools = await getAllGrokTools(); const stream = this.grokClient.chatStream( this.messages, - GROK_TOOLS, + tools, undefined, { search_parameters: { mode: "auto" } } ); @@ -584,6 +615,11 @@ Current working directory: ${process.cwd()}`, }); default: + // Check if this is an MCP tool + if (toolCall.function.name.startsWith('mcp__')) { + return await this.executeMCPTool(toolCall); + } + return { success: false, error: `Unknown tool: ${toolCall.function.name}`, @@ -597,6 +633,44 @@ Current working directory: ${process.cwd()}`, } } + private async executeMCPTool(toolCall: GrokToolCall): Promise { + try { + const args = JSON.parse(toolCall.function.arguments); + const mcpManager = getMCPManager(); + + const result = await mcpManager.callTool(toolCall.function.name, args); + + if (result.isError) { + return { + success: false, + error: (result.content[0] as any)?.text || 'MCP tool error' + }; + } + + // Extract content from result + const output = result.content + .map(item => { + if (item.type === 'text') { + return item.text; + } else if (item.type === 'resource') { + return `Resource: ${item.resource?.uri || 'Unknown'}`; + } + return String(item); + }) + .join('\n'); + + return { + success: true, + output: output || 'Success' + }; + } catch (error: any) { + return { + success: false, + error: `MCP tool execution error: ${error.message}` + }; + } + } + getChatHistory(): ChatEntry[] { return [...this.chatHistory]; } diff --git a/src/commands/mcp.ts b/src/commands/mcp.ts new file mode 100644 index 0000000..c341efc --- /dev/null +++ b/src/commands/mcp.ts @@ -0,0 +1,268 @@ +import { Command } from 'commander'; +import { addMCPServer, removeMCPServer, loadMCPConfig, PREDEFINED_SERVERS } from '../mcp/config'; +import { getMCPManager } from '../grok/tools'; +import { MCPServerConfig } from '../mcp/client'; +import chalk from 'chalk'; + +export function createMCPCommand(): Command { + const mcpCommand = new Command('mcp'); + mcpCommand.description('Manage MCP (Model Context Protocol) servers'); + + // Add server command + mcpCommand + .command('add ') + .description('Add an MCP server') + .option('-t, --transport ', 'Transport type (stdio, http, sse, streamable_http)', 'stdio') + .option('-c, --command ', 'Command to run the server (for stdio transport)') + .option('-a, --args [args...]', 'Arguments for the server command (for stdio transport)', []) + .option('-u, --url ', 'URL for HTTP/SSE transport') + .option('-h, --headers [headers...]', 'HTTP headers (key=value format)', []) + .option('-e, --env [env...]', 'Environment variables (key=value format)', []) + .action(async (name: string, options) => { + try { + // Check if it's a predefined server + if (PREDEFINED_SERVERS[name]) { + const config = PREDEFINED_SERVERS[name]; + addMCPServer(config); + console.log(chalk.green(`βœ“ Added predefined MCP server: ${name}`)); + + // Try to connect immediately + const manager = getMCPManager(); + await manager.addServer(config); + console.log(chalk.green(`βœ“ Connected to MCP server: ${name}`)); + + const tools = manager.getTools().filter(t => t.serverName === name); + console.log(chalk.blue(` Available tools: ${tools.length}`)); + + return; + } + + // Custom server + const transportType = options.transport.toLowerCase(); + + if (transportType === 'stdio') { + if (!options.command) { + console.error(chalk.red('Error: --command is required for stdio transport')); + process.exit(1); + } + } else if (transportType === 'http' || transportType === 'sse' || transportType === 'streamable_http') { + if (!options.url) { + console.error(chalk.red(`Error: --url is required for ${transportType} transport`)); + process.exit(1); + } + } else { + console.error(chalk.red('Error: Transport type must be stdio, http, sse, or streamable_http')); + process.exit(1); + } + + // Parse environment variables + const env: Record = {}; + for (const envVar of options.env || []) { + const [key, value] = envVar.split('=', 2); + if (key && value) { + env[key] = value; + } + } + + // Parse headers + const headers: Record = {}; + for (const header of options.headers || []) { + const [key, value] = header.split('=', 2); + if (key && value) { + headers[key] = value; + } + } + + const config = { + name, + transport: { + type: transportType as 'stdio' | 'http' | 'sse' | 'streamable_http', + command: options.command, + args: options.args || [], + url: options.url, + env, + headers: Object.keys(headers).length > 0 ? headers : undefined + } + }; + + addMCPServer(config); + console.log(chalk.green(`βœ“ Added MCP server: ${name}`)); + + // Try to connect immediately + const manager = getMCPManager(); + await manager.addServer(config); + console.log(chalk.green(`βœ“ Connected to MCP server: ${name}`)); + + const tools = manager.getTools().filter(t => t.serverName === name); + console.log(chalk.blue(` Available tools: ${tools.length}`)); + + } catch (error: any) { + console.error(chalk.red(`Error adding MCP server: ${error.message}`)); + process.exit(1); + } + }); + + // Add server from JSON command + mcpCommand + .command('add-json ') + .description('Add an MCP server from JSON configuration') + .action(async (name: string, jsonConfig: string) => { + try { + let config; + try { + config = JSON.parse(jsonConfig); + } catch (error) { + console.error(chalk.red('Error: Invalid JSON configuration')); + process.exit(1); + } + + const serverConfig: MCPServerConfig = { + name, + transport: { + type: 'stdio', // default + command: config.command, + args: config.args || [], + env: config.env || {}, + url: config.url, + headers: config.headers + } + }; + + // Override transport type if specified + if (config.transport) { + if (typeof config.transport === 'string') { + serverConfig.transport.type = config.transport as 'stdio' | 'http' | 'sse'; + } else if (typeof config.transport === 'object') { + serverConfig.transport = { ...serverConfig.transport, ...config.transport }; + } + } + + addMCPServer(serverConfig); + console.log(chalk.green(`βœ“ Added MCP server: ${name}`)); + + // Try to connect immediately + const manager = getMCPManager(); + await manager.addServer(serverConfig); + console.log(chalk.green(`βœ“ Connected to MCP server: ${name}`)); + + const tools = manager.getTools().filter(t => t.serverName === name); + console.log(chalk.blue(` Available tools: ${tools.length}`)); + + } catch (error: any) { + console.error(chalk.red(`Error adding MCP server: ${error.message}`)); + process.exit(1); + } + }); + + // Remove server command + mcpCommand + .command('remove ') + .description('Remove an MCP server') + .action(async (name: string) => { + try { + const manager = getMCPManager(); + await manager.removeServer(name); + removeMCPServer(name); + console.log(chalk.green(`βœ“ Removed MCP server: ${name}`)); + } catch (error: any) { + console.error(chalk.red(`Error removing MCP server: ${error.message}`)); + process.exit(1); + } + }); + + // List servers command + mcpCommand + .command('list') + .description('List configured MCP servers') + .action(() => { + const config = loadMCPConfig(); + const manager = getMCPManager(); + + if (config.servers.length === 0) { + console.log(chalk.yellow('No MCP servers configured')); + return; + } + + console.log(chalk.bold('Configured MCP servers:')); + console.log(); + + for (const server of config.servers) { + const isConnected = manager.getServers().includes(server.name); + const status = isConnected + ? chalk.green('βœ“ Connected') + : chalk.red('βœ— Disconnected'); + + console.log(`${chalk.bold(server.name)}: ${status}`); + + // Display transport information + if (server.transport) { + console.log(` Transport: ${server.transport.type}`); + if (server.transport.type === 'stdio') { + console.log(` Command: ${server.transport.command} ${(server.transport.args || []).join(' ')}`); + } else if (server.transport.type === 'http' || server.transport.type === 'sse') { + console.log(` URL: ${server.transport.url}`); + } + } else if (server.command) { + // Legacy format + console.log(` Command: ${server.command} ${(server.args || []).join(' ')}`); + } + + if (isConnected) { + const transportType = manager.getTransportType(server.name); + if (transportType) { + console.log(` Active Transport: ${transportType}`); + } + + const tools = manager.getTools().filter(t => t.serverName === server.name); + console.log(` Tools: ${tools.length}`); + if (tools.length > 0) { + tools.forEach(tool => { + const displayName = tool.name.replace(`mcp__${server.name}__`, ''); + console.log(` - ${displayName}: ${tool.description}`); + }); + } + } + + console.log(); + } + }); + + // Test server command + mcpCommand + .command('test ') + .description('Test connection to an MCP server') + .action(async (name: string) => { + try { + const config = loadMCPConfig(); + const serverConfig = config.servers.find(s => s.name === name); + + if (!serverConfig) { + console.error(chalk.red(`Server ${name} not found`)); + process.exit(1); + } + + console.log(chalk.blue(`Testing connection to ${name}...`)); + + const manager = getMCPManager(); + await manager.addServer(serverConfig); + + const tools = manager.getTools().filter(t => t.serverName === name); + console.log(chalk.green(`βœ“ Successfully connected to ${name}`)); + console.log(chalk.blue(` Available tools: ${tools.length}`)); + + if (tools.length > 0) { + console.log(' Tools:'); + tools.forEach(tool => { + const displayName = tool.name.replace(`mcp__${name}__`, ''); + console.log(` - ${displayName}: ${tool.description}`); + }); + } + + } catch (error: any) { + console.error(chalk.red(`βœ— Failed to connect to ${name}: ${error.message}`)); + process.exit(1); + } + }); + + return mcpCommand; +} \ No newline at end of file diff --git a/src/grok/tools.ts b/src/grok/tools.ts index 175180c..38b891e 100644 --- a/src/grok/tools.ts +++ b/src/grok/tools.ts @@ -1,4 +1,6 @@ import { GrokTool } from "./client"; +import { MCPManager, MCPTool } from "../mcp/client"; +import { loadMCPConfig } from "../mcp/config"; export const GROK_TOOLS: GrokTool[] = [ { @@ -239,3 +241,89 @@ export const GROK_TOOLS: GrokTool[] = [ }, }, ]; + +// Global MCP manager instance +let mcpManager: MCPManager | null = null; + +export function getMCPManager(): MCPManager { + if (!mcpManager) { + mcpManager = new MCPManager(); + } + return mcpManager; +} + +export async function initializeMCPServers(): Promise { + const manager = getMCPManager(); + const config = loadMCPConfig(); + + // Store original stderr.write + const originalStderrWrite = process.stderr.write; + + // Temporarily suppress stderr to hide verbose MCP connection logs + process.stderr.write = function(chunk: any, encoding?: any, callback?: any): boolean { + // Filter out mcp-remote verbose logs + const chunkStr = chunk.toString(); + if (chunkStr.includes('[') && ( + chunkStr.includes('Using existing client port') || + chunkStr.includes('Connecting to remote server') || + chunkStr.includes('Using transport strategy') || + chunkStr.includes('Connected to remote server') || + chunkStr.includes('Local STDIO server running') || + chunkStr.includes('Proxy established successfully') || + chunkStr.includes('Localβ†’Remote') || + chunkStr.includes('Remoteβ†’Local') + )) { + // Suppress these verbose logs + if (callback) callback(); + return true; + } + + // Allow other stderr output + return originalStderrWrite.call(this, chunk, encoding, callback); + }; + + try { + for (const serverConfig of config.servers) { + try { + await manager.addServer(serverConfig); + } catch (error) { + console.warn(`Failed to initialize MCP server ${serverConfig.name}:`, error); + } + } + } finally { + // Restore original stderr.write + process.stderr.write = originalStderrWrite; + } +} + +export function convertMCPToolToGrokTool(mcpTool: MCPTool): GrokTool { + return { + type: "function", + function: { + name: mcpTool.name, + description: mcpTool.description, + parameters: mcpTool.inputSchema || { + type: "object", + properties: {}, + required: [] + } + } + }; +} + +export function addMCPToolsToGrokTools(baseTools: GrokTool[]): GrokTool[] { + if (!mcpManager) { + return baseTools; + } + + const mcpTools = mcpManager.getTools(); + const grokMCPTools = mcpTools.map(convertMCPToolToGrokTool); + + return [...baseTools, ...grokMCPTools]; +} + +export async function getAllGrokTools(): Promise { + const manager = getMCPManager(); + await manager.ensureServersInitialized(); + return addMCPToolsToGrokTools(GROK_TOOLS); +} diff --git a/src/hooks/use-input-handler.ts b/src/hooks/use-input-handler.ts index 1a0feec..266da1d 100644 --- a/src/hooks/use-input-handler.ts +++ b/src/hooks/use-input-handler.ts @@ -1,5 +1,5 @@ import { useState, useRef } from "react"; -import { useInput, useApp } from "ink"; +import { useInput } from "ink"; import { GrokAgent, ChatEntry } from "../agent/grok-agent"; import { ConfirmationService } from "../utils/confirmation-service"; import { updateSetting } from "../utils/settings"; @@ -51,7 +51,7 @@ export function useInputHandler({ const sessionFlags = confirmationService.getSessionFlags(); return sessionFlags.allOperations; }); - const { exit } = useApp(); + // Removed useApp().exit - using process.exit(0) instead for better terminal handling const commandSuggestions: CommandSuggestion[] = [ { command: "/help", description: "Show help information" }, @@ -545,7 +545,7 @@ Respond with ONLY the commit message, no additional text.`; } if (key.ctrl && inputChar === "c") { - exit(); + process.exit(0); return; } @@ -640,7 +640,7 @@ Respond with ONLY the commit message, no additional text.`; if (key.return) { const userInput = input.trim(); if (userInput === "exit" || userInput === "quit") { - exit(); + process.exit(0); return; } diff --git a/src/index.ts b/src/index.ts index bb42e08..a89dcf6 100755 --- a/src/index.ts +++ b/src/index.ts @@ -10,10 +10,41 @@ import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import { ConfirmationService } from "./utils/confirmation-service"; +import { createMCPCommand } from "./commands/mcp"; // Load environment variables dotenv.config(); +// Add proper signal handling for terminal cleanup +process.on('SIGINT', () => { + // Restore terminal to normal mode before exit + if (process.stdin.isTTY) { + process.stdin.setRawMode(false); + } + console.log('\nGracefully shutting down...'); + process.exit(0); +}); + +process.on('SIGTERM', () => { + // Restore terminal to normal mode before exit + if (process.stdin.isTTY) { + process.stdin.setRawMode(false); + } + console.log('\nGracefully shutting down...'); + process.exit(0); +}); + +// Handle uncaught exceptions to prevent hanging +process.on('uncaughtException', (error) => { + console.error('Uncaught exception:', error); + process.exit(1); +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled rejection at:', promise, 'reason:', reason); + process.exit(1); +}); + // Ensure user .grok directory exists with default settings function ensureUserSettingsDirectory(): void { try { @@ -374,4 +405,7 @@ gitCommand } }); +// MCP command +program.addCommand(createMCPCommand()); + program.parse(); diff --git a/src/mcp/client.ts b/src/mcp/client.ts new file mode 100644 index 0000000..6bc51cb --- /dev/null +++ b/src/mcp/client.ts @@ -0,0 +1,167 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { EventEmitter } from "events"; +import { createTransport, MCPTransport, TransportType, TransportConfig } from "./transports.js"; + +export interface MCPServerConfig { + name: string; + transport: TransportConfig; + // Legacy support for stdio-only configs + command?: string; + args?: string[]; + env?: Record; +} + +export interface MCPTool { + name: string; + description: string; + inputSchema: any; + serverName: string; +} + +export class MCPManager extends EventEmitter { + private clients: Map = new Map(); + private transports: Map = new Map(); + private tools: Map = new Map(); + + async addServer(config: MCPServerConfig): Promise { + try { + // Handle legacy stdio-only configuration + let transportConfig = config.transport; + if (!transportConfig && config.command) { + transportConfig = { + type: 'stdio', + command: config.command, + args: config.args, + env: config.env + }; + } + + if (!transportConfig) { + throw new Error('Transport configuration is required'); + } + + // Create transport + const transport = createTransport(transportConfig); + this.transports.set(config.name, transport); + + // Create client + const client = new Client( + { + name: "grok-cli", + version: "1.0.0" + }, + { + capabilities: { + tools: {} + } + } + ); + + this.clients.set(config.name, client); + + // Connect + const sdkTransport = await transport.connect(); + await client.connect(sdkTransport); + + // List available tools + const toolsResult = await client.listTools(); + + // Register tools + for (const tool of toolsResult.tools) { + const mcpTool: MCPTool = { + name: `mcp__${config.name}__${tool.name}`, + description: tool.description || `Tool from ${config.name} server`, + inputSchema: tool.inputSchema, + serverName: config.name + }; + this.tools.set(mcpTool.name, mcpTool); + } + + this.emit('serverAdded', config.name, toolsResult.tools.length); + } catch (error) { + this.emit('serverError', config.name, error); + throw error; + } + } + + async removeServer(serverName: string): Promise { + // Remove tools + for (const [toolName, tool] of this.tools.entries()) { + if (tool.serverName === serverName) { + this.tools.delete(toolName); + } + } + + // Disconnect client + const client = this.clients.get(serverName); + if (client) { + await client.close(); + this.clients.delete(serverName); + } + + // Close transport + const transport = this.transports.get(serverName); + if (transport) { + await transport.disconnect(); + this.transports.delete(serverName); + } + + this.emit('serverRemoved', serverName); + } + + async callTool(toolName: string, arguments_: any): Promise { + const tool = this.tools.get(toolName); + if (!tool) { + throw new Error(`Tool ${toolName} not found`); + } + + const client = this.clients.get(tool.serverName); + if (!client) { + throw new Error(`Server ${tool.serverName} not connected`); + } + + // Extract the original tool name (remove mcp__servername__ prefix) + const originalToolName = toolName.replace(`mcp__${tool.serverName}__`, ''); + + return await client.callTool({ + name: originalToolName, + arguments: arguments_ + }); + } + + getTools(): MCPTool[] { + return Array.from(this.tools.values()); + } + + getServers(): string[] { + return Array.from(this.clients.keys()); + } + + async shutdown(): Promise { + const serverNames = Array.from(this.clients.keys()); + await Promise.all(serverNames.map(name => this.removeServer(name))); + } + + getTransportType(serverName: string): TransportType | undefined { + const transport = this.transports.get(serverName); + return transport?.getType(); + } + + async ensureServersInitialized(): Promise { + if (this.clients.size > 0) { + return; // Already initialized + } + + const { loadMCPConfig } = await import('../mcp/config'); + const config = loadMCPConfig(); + + for (const serverConfig of config.servers) { + try { + await this.addServer(serverConfig); + } catch (error) { + console.warn(`Failed to initialize MCP server ${serverConfig.name}:`, error); + } + } + } +} \ No newline at end of file diff --git a/src/mcp/config.ts b/src/mcp/config.ts new file mode 100644 index 0000000..d4466c4 --- /dev/null +++ b/src/mcp/config.ts @@ -0,0 +1,88 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { MCPServerConfig } from "./client"; + +const CONFIG_DIR = path.join(process.cwd(), ".grok"); +const SETTINGS_FILE = path.join(CONFIG_DIR, "settings.json"); + +export interface Settings { + selectedModel?: string; + mcpServers?: Record; +} + +export interface MCPConfig { + servers: MCPServerConfig[]; +} + +function loadSettings(): Settings { + try { + if (!fs.existsSync(SETTINGS_FILE)) { + return {}; + } + + const settingsData = fs.readFileSync(SETTINGS_FILE, "utf8"); + return JSON.parse(settingsData); + } catch (error) { + console.warn("Failed to load settings:", error); + return {}; + } +} + +function saveSettings(settings: Settings): void { + try { + // Ensure config directory exists + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + } + + fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2)); + } catch (error) { + console.error("Failed to save settings:", error); + throw error; + } +} + +export function loadMCPConfig(): MCPConfig { + const settings = loadSettings(); + const servers = settings.mcpServers ? Object.values(settings.mcpServers) : []; + return { servers }; +} + +export function saveMCPConfig(config: MCPConfig): void { + const settings = loadSettings(); + settings.mcpServers = {}; + + // Convert servers array to object keyed by name + for (const server of config.servers) { + settings.mcpServers[server.name] = server; + } + + saveSettings(settings); +} + +export function addMCPServer(config: MCPServerConfig): void { + const settings = loadSettings(); + if (!settings.mcpServers) { + settings.mcpServers = {}; + } + + settings.mcpServers[config.name] = config; + saveSettings(settings); +} + +export function removeMCPServer(serverName: string): void { + const settings = loadSettings(); + if (settings.mcpServers) { + delete settings.mcpServers[serverName]; + saveSettings(settings); + } +} + +export function getMCPServer(serverName: string): MCPServerConfig | undefined { + const settings = loadSettings(); + return settings.mcpServers?.[serverName]; +} + +// Predefined server configurations +export const PREDEFINED_SERVERS: Record = {}; diff --git a/src/mcp/transports.ts b/src/mcp/transports.ts new file mode 100644 index 0000000..1b482e2 --- /dev/null +++ b/src/mcp/transports.ts @@ -0,0 +1,265 @@ +import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { ChildProcess, spawn } from "child_process"; +import { EventEmitter } from "events"; +import axios, { AxiosInstance } from "axios"; + +export type TransportType = 'stdio' | 'http' | 'sse' | 'streamable_http'; + +export interface TransportConfig { + type: TransportType; + command?: string; + args?: string[]; + env?: Record; + url?: string; + headers?: Record; +} + +export interface MCPTransport { + connect(): Promise; + disconnect(): Promise; + getType(): TransportType; +} + +export class StdioTransport implements MCPTransport { + private transport?: StdioClientTransport; + private process?: ChildProcess; + + constructor(private config: TransportConfig) { + if (!config.command) { + throw new Error('Command is required for stdio transport'); + } + } + + async connect(): Promise { + // Create transport with environment variables to suppress verbose output + const env = { + ...process.env, + ...this.config.env, + // Try to suppress verbose output from mcp-remote + MCP_REMOTE_QUIET: '1', + MCP_REMOTE_SILENT: '1', + DEBUG: '', + NODE_ENV: 'production' + }; + + this.transport = new StdioClientTransport({ + command: this.config.command!, + args: this.config.args || [], + env + }); + + return this.transport; + } + + async disconnect(): Promise { + if (this.transport) { + await this.transport.close(); + this.transport = undefined; + } + + if (this.process) { + this.process.kill(); + this.process = undefined; + } + } + + getType(): TransportType { + return 'stdio'; + } +} + +export class HttpTransport extends EventEmitter implements MCPTransport { + private client?: AxiosInstance; + private connected = false; + + constructor(private config: TransportConfig) { + super(); + if (!config.url) { + throw new Error('URL is required for HTTP transport'); + } + } + + async connect(): Promise { + this.client = axios.create({ + baseURL: this.config.url, + headers: { + 'Content-Type': 'application/json', + ...this.config.headers + } + }); + + // Test connection + try { + await this.client.get('/health'); + this.connected = true; + } catch (error) { + // If health endpoint doesn't exist, try a basic request + this.connected = true; + } + + return new HttpClientTransport(this.client); + } + + async disconnect(): Promise { + this.connected = false; + this.client = undefined; + } + + getType(): TransportType { + return 'http'; + } +} + +export class SSETransport extends EventEmitter implements MCPTransport { + private connected = false; + + constructor(private config: TransportConfig) { + super(); + if (!config.url) { + throw new Error('URL is required for SSE transport'); + } + } + + async connect(): Promise { + return new Promise((resolve, reject) => { + try { + // For Node.js environment, we'll use a simple HTTP-based approach + // In a real implementation, you'd use a proper SSE library like 'eventsource' + this.connected = true; + resolve(new SSEClientTransport(this.config.url!)); + } catch (error) { + reject(error); + } + }); + } + + async disconnect(): Promise { + this.connected = false; + } + + getType(): TransportType { + return 'sse'; + } +} + +// Custom HTTP Transport implementation +class HttpClientTransport extends EventEmitter implements Transport { + constructor(private client: AxiosInstance) { + super(); + } + + async start(): Promise { + // HTTP transport is connection-less, so we're always "started" + } + + async close(): Promise { + // Nothing to close for HTTP transport + } + + async send(message: any): Promise { + try { + const response = await this.client.post('/rpc', message); + return response.data; + } catch (error) { + throw new Error(`HTTP transport error: ${error}`); + } + } +} + +// Custom SSE Transport implementation +class SSEClientTransport extends EventEmitter implements Transport { + constructor(private url: string) { + super(); + } + + async start(): Promise { + // SSE transport is event-driven, so we're always "started" + } + + async close(): Promise { + // Nothing to close for basic SSE transport + } + + async send(message: any): Promise { + // For bidirectional communication over SSE, we typically use HTTP POST + // for sending messages and SSE for receiving + try { + const response = await axios.post(this.url.replace('/sse', '/rpc'), message, { + headers: { 'Content-Type': 'application/json' } + }); + return response.data; + } catch (error) { + throw new Error(`SSE transport error: ${error}`); + } + } +} + +export class StreamableHttpTransport extends EventEmitter implements MCPTransport { + private connected = false; + + constructor(private config: TransportConfig) { + super(); + if (!config.url) { + throw new Error('URL is required for streamable_http transport'); + } + } + + async connect(): Promise { + return new Promise((resolve, reject) => { + try { + this.connected = true; + resolve(new StreamableHttpClientTransport(this.config.url!, this.config.headers)); + } catch (error) { + reject(error); + } + }); + } + + async disconnect(): Promise { + this.connected = false; + } + + getType(): TransportType { + return 'streamable_http'; + } +} + +// Custom Streamable HTTP Transport implementation for GitHub Copilot MCP +class StreamableHttpClientTransport extends EventEmitter implements Transport { + constructor(private url: string, private headers?: Record) { + super(); + } + + async start(): Promise { + // Streamable HTTP transport is connection-less, so we're always "started" + } + + async close(): Promise { + // Nothing to close for streamable HTTP transport + } + + async send(message: any): Promise { + console.log('StreamableHttpTransport: SSE endpoints require persistent connections, not suitable for MCP request-response pattern'); + console.log('StreamableHttpTransport: Message that would be sent:', JSON.stringify(message)); + + // For now, return a mock response to indicate the transport type is not compatible + // with the MCP protocol's request-response pattern + throw new Error('StreamableHttpTransport: SSE endpoints are not compatible with MCP request-response pattern. GitHub Copilot MCP may require a different integration approach.'); + } +} + +export function createTransport(config: TransportConfig): MCPTransport { + switch (config.type) { + case 'stdio': + return new StdioTransport(config); + case 'http': + return new HttpTransport(config); + case 'sse': + return new SSETransport(config); + case 'streamable_http': + return new StreamableHttpTransport(config); + default: + throw new Error(`Unsupported transport type: ${config.type}`); + } +} \ No newline at end of file diff --git a/src/ui/app.tsx b/src/ui/app.tsx index 6108c5f..61310ad 100644 --- a/src/ui/app.tsx +++ b/src/ui/app.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Box, Text, useInput, useApp } from 'ink'; +import { Box, Text, useInput } from 'ink'; import { Agent } from '../agent'; import { ToolResult } from '../types'; import { ConfirmationService, ConfirmationOptions } from '../utils/confirmation-service'; @@ -15,7 +15,7 @@ export default function App({ agent }: Props) { const [history, setHistory] = useState>([]); const [isProcessing, setIsProcessing] = useState(false); const [confirmationOptions, setConfirmationOptions] = useState(null); - const { exit } = useApp(); + // Removed useApp().exit - using process.exit(0) instead for better terminal handling const confirmationService = ConfirmationService.getInstance(); @@ -42,13 +42,13 @@ export default function App({ agent }: Props) { return; } if (key.ctrl && inputChar === 'c') { - exit(); + process.exit(0); return; } if (key.return) { if (input.trim() === 'exit' || input.trim() === 'quit') { - exit(); + process.exit(0); return; } diff --git a/src/ui/components/chat-history.tsx b/src/ui/components/chat-history.tsx index 076be16..9119f11 100644 --- a/src/ui/components/chat-history.tsx +++ b/src/ui/components/chat-history.tsx @@ -81,6 +81,16 @@ const MemoizedChatEntry = React.memo( case "tool_call": case "tool_result": const getToolActionName = (toolName: string) => { + // Handle MCP tools with mcp__servername__toolname format + if (toolName.startsWith("mcp__")) { + const parts = toolName.split("__"); + if (parts.length >= 3) { + const serverName = parts[1]; + const actualToolName = parts.slice(2).join("__"); + return `${serverName.charAt(0).toUpperCase() + serverName.slice(1)}(${actualToolName.replace(/_/g, " ")})`; + } + } + switch (toolName) { case "view_file": return "Read"; @@ -121,6 +131,27 @@ const MemoizedChatEntry = React.memo( const filePath = getFilePath(entry.toolCall); const isExecuting = entry.type === "tool_call" || !entry.toolResult; + + // Format JSON content for better readability + const formatToolContent = (content: string, toolName: string) => { + if (toolName.startsWith("mcp__")) { + try { + // Try to parse as JSON and format it + const parsed = JSON.parse(content); + if (Array.isArray(parsed)) { + // For arrays, show a summary instead of full JSON + return `Found ${parsed.length} items`; + } else if (typeof parsed === 'object') { + // For objects, show a formatted version + return JSON.stringify(parsed, null, 2); + } + } catch { + // If not JSON, return as is + return content; + } + } + return content; + }; const shouldShowDiff = entry.toolCall?.function?.name === "str_replace_editor" && entry.toolResult?.success && @@ -157,7 +188,7 @@ const MemoizedChatEntry = React.memo( // For diff results, show only the summary line, not the raw content ⎿ {entry.content.split("\n")[0]} ) : ( - ⎿ {entry.content} + ⎿ {formatToolContent(entry.content, toolName)} )} {shouldShowDiff && !isExecuting && ( diff --git a/src/ui/components/chat-interface.tsx b/src/ui/components/chat-interface.tsx index b095408..a5d710c 100644 --- a/src/ui/components/chat-interface.tsx +++ b/src/ui/components/chat-interface.tsx @@ -7,6 +7,7 @@ import { CommandSuggestions } from "./command-suggestions"; import { ModelSelection } from "./model-selection"; import { ChatHistory } from "./chat-history"; import { ChatInput } from "./chat-input"; +import { MCPStatus } from "./mcp-status"; import ConfirmationDialog from "./confirmation-dialog"; import { ConfirmationService, @@ -224,6 +225,7 @@ function ChatInterfaceWithAgent({ agent }: { agent: GrokAgent }) { ⚑{agent.getCurrentModel()} + ([]); + const [availableTools, setAvailableTools] = useState([]); + + useEffect(() => { + const updateStatus = () => { + try { + const manager = getMCPManager(); + const servers = manager.getServers(); + const tools = manager.getTools(); + + setConnectedServers(servers); + setAvailableTools(tools); + } catch (error) { + // MCP manager not initialized yet + setConnectedServers([]); + setAvailableTools([]); + } + }; + + // Initial update with a small delay to allow MCP initialization + const initialTimer = setTimeout(updateStatus, 2000); + + // Set up polling to check for status changes + const interval = setInterval(updateStatus, 2000); + + return () => { + clearTimeout(initialTimer); + clearInterval(interval); + }; + }, []); + + if (connectedServers.length === 0) { + return null; + } + + return ( + + βš’ mcps: {connectedServers.length} + + ); +}